diff --git a/src/vault/mem/buffer-metadata.hpp b/src/vault/mem/buffer-metadata.hpp index d5b4cd526..c12efba4d 100644 --- a/src/vault/mem/buffer-metadata.hpp +++ b/src/vault/mem/buffer-metadata.hpp @@ -286,13 +286,18 @@ namespace mem { protected: Entry (Key const& parent - ,Buff* bufferPtr =nullptr + ,Buff* bufferPtr =nullptr + ,size_t actualSize =0 ,LocalTag const& specialTag =LocalTag::UNKNOWN ) : Key{Key::forEntry (parent, bufferPtr, specialTag)} , state_{bufferPtr? LOCKED:NIL} , buffer_{bufferPtr} - { } + { + if (bufferPtr and actualSize) + Key::storageSize_ = actualSize; + ENSURE (Key::storageSize() >= parent.storageSize()); + } /// BufferMetadata is allowed to create friend class vault::mem::BufferMetadata; @@ -372,10 +377,11 @@ namespace mem { } Entry& - lock (Buff* newBuffer) + lock (Buff* newBuffer, size_t actualSize =0) { __must_be_FREE(); buffer_ = newBuffer; + storageSize_ = std::max (actualSize, storageSize_); return mark (LOCKED); } @@ -692,19 +698,44 @@ namespace mem { * metadata Entry to account for this fact. This might include * invoking a constructor function, in case the type (Key) * defines a (nontrivial) TypeHandler. + * @param parentKey designates the _buffer type_ to base on + * @param buffer an actual memory address, which defines a distinct Entry + * @param size the size of the actual allocation. When size ≡ 0, + * the nominal size is assumed, as defined through parentKey. + * Otherwise, the actual size must be _larger_. + * @param specifics an optional qualification marker, which is managed + * and evaluated by the BufferStore implementation and treated as + * opaque data, as far as BufferMetadata is concerned. However, + * the LocalTag becomes part of the identity of the metadata record. + * Thus, either the parentKey must not include a LocalTag, or the + * LocalTag given here must be identical to what was associate + * with the _buffer type_ represented by parentKey. * @throw error::Fatal when locking a NULL buffer * @throw exceptions which might be raised by a TypeHandler's * constructor function. In this case, the Entry remains * created, but is marked as FREE */ Entry& - markLocked (Key const& parentKey, Buff* buffer, LocalTag const& specifics =LocalTag::UNKNOWN) + markLocked (Key const& parentKey + ,Buff* buffer + ,size_t actualSize =0 + ,LocalTag const& specifics =LocalTag::UNKNOWN) { - if (!buffer) + if (not buffer) throw err::Fatal{"Attempt to lock for a NULL buffer. Allocation floundered?" , LERR_(BOTTOM_VALUE)}; + if (not actualSize) + actualSize = parentKey.storageSize(); + REQUIRE (parentKey.storageSize() <= actualSize); - return BufferMetadata::lock (parentKey, buffer, specifics); + return BufferMetadata::lock (parentKey, buffer, actualSize, specifics); + } + + /** variation to use when a LocalTag is given, yet size is implicitly taken from parent */ + Entry& + markLocked (Key const& parentKey, Buff* buffer, LocalTag const& specifics) + { + return markLocked (parentKey, buffer, size_t(0), specifics); } /** purge the bare metadata Entry from the metadata tables. @@ -760,13 +791,14 @@ namespace mem { Entry& lock (Key const& parentKey ,Buff* concreteBuffer + ,size_t actualSize ,LocalTag const& specifics =LocalTag::UNKNOWN) { - if (!concreteBuffer) + if (not concreteBuffer) throw err::Invalid{"Attempt to lock a slot for a NULL buffer" , LERR_(BOTTOM_VALUE)}; - Entry newEntry{parentKey, concreteBuffer, specifics}; + Entry newEntry{parentKey, concreteBuffer, actualSize, specifics}; Entry* existing = table_.fetch (newEntry); if (existing and existing->isLocked()) @@ -774,9 +806,9 @@ namespace mem { , LERR_(LIFECYCLE)}; if (not existing) - return store_as_locked (newEntry); // actual creation + return store_as_locked (newEntry); // store new Entry marked as locked else - return existing->lock (concreteBuffer); + return existing->lock (concreteBuffer, actualSize); } diff --git a/src/vault/mem/buffer-provider.cpp b/src/vault/mem/buffer-provider.cpp index bdd5eaa17..e68778be0 100644 --- a/src/vault/mem/buffer-provider.cpp +++ b/src/vault/mem/buffer-provider.cpp @@ -149,12 +149,12 @@ namespace mem { BufferProvider::lockBuffer (BuffDescr const& type) { REQUIRE (was_created_by_this_provider (type)); - auto& typeKey = bufferStage_->lookup (type); - auto [storage, actualSize, localTag] = bufferStore_->provideBuffer (type,typeKey.storageSize(),typeKey.localTag()); - auto& stateKey = bufferStage_->mark_locked (typeKey, storage, localTag); + auto& typeKey = bufferStage_->lookup (type); + BuffAlloc alloc= bufferStore_->provideBuffer (type,typeKey.storageSize(),typeKey.localTag()); + auto& stateKey = bufferStage_->mark_locked (typeKey, alloc); - return BuffHandle (buildDescriptor(stateKey), storage); - } // NOTE: not the underlying parent descriptor! + return BuffHandle (buildDescriptor(stateKey), getAddr(alloc)); + } // NOTE: not the underlying parent descriptor! /** BufferProvider API: state transition to _emitted state_. diff --git a/src/vault/mem/buffer-provider.hpp b/src/vault/mem/buffer-provider.hpp index fd644db57..1a6a03db7 100644 --- a/src/vault/mem/buffer-provider.hpp +++ b/src/vault/mem/buffer-provider.hpp @@ -100,6 +100,15 @@ ** highly complex setup for the production-grade implementation variant, attempts ** towards such a refactoring were postponed, after re-evaluation 2/2026. ** + ** @warning buffer management via BufferProvider and BuffHandle does not automatically + ** _maintain proper alignment_. Rather, it relies on the storage allocator to + ** provide a buffer suitably aligned for the target type to hold. In most cases, + ** this target location will actually be storage located on the heap, yet some + ** processing might impose additional constraints regarding alignment to memory + ** blocks, causing degraded performance when violated; so this topic is a + ** possible subtle pitfall. + ** @todo 2026 we might consider adding alignment information to the associated TypeHandler. + ** ** @see buffer-provider-protocol-test.cpp ** @see output-slot.hpp ** @see engine-ctx.hpp @@ -142,6 +151,8 @@ namespace mem { * @remark LocalTag can be used by the storage-impl. */ using BuffAlloc = std::tuple; + inline Buff* getAddr (BuffAlloc& alloc) { return std::get<0> (alloc); } + inline size_t getSize (BuffAlloc& alloc) { return std::get<1> (alloc); } LUMIERA_ERROR_DECLARE (BUFFER_MANAGEMENT); ///< Problem providing working buffers @@ -207,14 +218,14 @@ namespace mem { virtual ~BufferStage() { } ///< this is an interface virtual ID defineBufferType (size_t, TypeHandler =TypeHandler::RAW, LocalTag =LocalTag::UNKNOWN) =0; - virtual ID lookup (HashVal) =0; - virtual ID mark_locked (ID typeKey, Buff*, LocalTag) =0; - virtual ID mark_emitted (HashVal stateKey) =0; - virtual ID mark_released (HashVal stateKey) =0; - virtual ID abandon (HashVal, bool destroy=false) =0; - virtual void discard (HashVal stateKey) =0; - virtual bool isAllotted (HashVal stateKey) const =0; - virtual bool isAccessible (HashVal stateKey) const =0; + virtual ID lookup (HashVal) =0; + virtual ID mark_locked (ID typeKey, BuffAlloc) =0; + virtual ID mark_emitted (HashVal stateKey) =0; + virtual ID mark_released (HashVal stateKey) =0; + virtual ID abandon (HashVal, bool destroy=false) =0; + virtual void discard (HashVal stateKey) =0; + virtual bool isAllotted (HashVal stateKey) const =0; + virtual bool isAccessible (HashVal stateKey) const =0; }; class BufferStore @@ -225,10 +236,10 @@ namespace mem { public: virtual ~BufferStore() { } ///< this is an interface - virtual uint prepareBuffers (HashVal typeID, uint cnt, size_t) =0; - virtual BuffAlloc provideBuffer (HashVal typeID, size_t, LocalTag) =0; - virtual void mark_emitted (HashVal typeID, BuffAlloc) =0; - virtual void detachBuffer (HashVal typeID, BuffAlloc) =0; + virtual uint prepareBuffers (HashVal typeID, uint cnt, size_t) =0; + virtual BuffAlloc provideBuffer (HashVal typeID, size_t, LocalTag) =0; + virtual void mark_emitted (HashVal typeID, BuffAlloc) =0; + virtual void detachBuffer (HashVal typeID, BuffAlloc) =0; }; unique_ptr bufferStage_; diff --git a/src/vault/mem/buffhandle.hpp b/src/vault/mem/buffhandle.hpp index 2db1f5db8..fe987f84b 100644 --- a/src/vault/mem/buffhandle.hpp +++ b/src/vault/mem/buffhandle.hpp @@ -15,28 +15,39 @@ ** A front-end to support the buffer management within the render nodes. ** When pulling data from predecessor nodes and calculating new data, each render node ** needs several input and output buffers. These may be allocated and provided by various - ** different "buffer providers" (for example the frame cache). Typically, the real buffers - ** will be passed as parameters to the actual job instance when scheduled, drawing on the - ** results of prerequisite jobs. Yet the actual job implementation remains agnostic with - ** respect to the way actual buffers are provided; the invocation just pushes BuffHandle - ** objects around. The actual render function gets an array of C-pointers to the actual - ** buffers, and for accessing those buffers, the node needs to keep a table of buffer - ** pointers, and for releasing the buffers later on, we utilise the buffer handles. + ** different [»buffer providers«](\ref buffer-provider.hpp) (for example the frame cache). + ** Buffers are classified by a _buffer type,_ which essentially is the size of the storage, + ** but could optionally also include a constructor and destructor function. Which type of + ** buffer is used at some node invocation and which provider is backing and managing the + ** allocation is considered static configuration of the Render Node Network and established + ** by the **Builder**. Typically the actual render job is unaware of these details and only + ** provides a parameter, that _just happens to select_ the proper source buffer for the + ** frame to be computed. ** - ** These buffer handles are based on a [Buffer Descriptor record](\ref BuffDescr), - ** which is opaque as far as the client is concerned. BuffDescr acts as a representation - ** of the type or kind of buffer. The only way to obtain such a BuffDescr is from a concrete - ** BufferProvider implementation. A back-link to this owning and managing provider is embedded - ** into the BuffDescr, which thus may be used as a _configuration tag,_ allowing to retrieve a - ** concrete buffer handle when needed, corresponding to an actual buffer provided and managed - ** behind the scenes. There is no automatic resource management; clients are responsible to - ** invoke BuffHandle#release when done. + ** However, the Node Network as such is strictly typed (at compile time). Information regarding + ** the _buffer type_ are encoded into a [Buffer Descriptor](\ref BuffDescr). These descriptors + ** are embedded into the _feed prototype_ of each node and used, by the actual _Node invocation_, + ** to retrieve a [Buffer Handle](\ref BuffHandle). On the other hand, the _feed prototype_ was + ** also attached to a typed context at build time, so that the invocation of the actual + ** _processing function_ can access the buffers and any further parameters strictly typed. ** - ** @warning buffer management via BuffHandle and BuffDescr does _not automatically maintain - ** proper alignment._ Rather, it relies on the storage allocator to provide a buffer - ** suitably aligned for the target type to hold. In most cases, this target location - ** will actually be storage maintained on heap through some STL collection; - ** this topic is a possible subtle pitfall non the less. + ** Usage of these descriptors and handles is organised in accordance to the »Buffer Provider Protocol«, + ** which also explains the structure of the BufferProvider API. + ** - the first step is to obtain a BuffDescr from the BufferProvider, which implies + ** to define at least the required storage size (optionally also a constructor/destructor). + ** - the usage of some buffers with a given type can be _announced_ optionally, which allows + ** to pre-arrange some storage space and associate it with the calling thread. + ** - an actual allocation is initiated by _locking_ the descriptor, which yields a BuffHandle. + ** - from this handle, the buffer memory can be accessed. + ** - the client is then responsible to [»emit«](\ref BuffHandle::emit) on the handle, + ** once the computation is complete and data can be published. This step can be omitted, + ** and it depends on the actual situation if invoking this operation matters. For example, + ** it is very relevant for a result sent to some output sink, or for a buffer attached + ** to the frame cache. Without _emitting_, the data will not be propagated in those cases. + ** - however, it is always mandatory to [»release«](\ref BuffHandle::release) the handle. + ** + ** @note both BuffDescr and BuffHandle store a back-link to the managing BufferProvider. + ** @warning BuffHandle is _not a smart-ptr_ — the client is responsible to `release()` ** ** @see BufferProvider ** @see BufferProviderProtocol_test usage demonstration @@ -53,8 +64,8 @@ #include "lib/hash-value.h" -namespace vault{ -namespace mem { +namespace vault { +namespace mem { namespace err = lumiera::error; using lib::HashVal; @@ -75,10 +86,10 @@ namespace mem { * An opaque descriptor to identify the type and further properties of a data buffer. * For each kind of buffer, there is somewhere a BufferProvider responsible for the * actual storage management. This provider may "lock" a buffer for actual use, - * returning a BuffHandle. - * @note this descriptor and especially the #subClassification_ is really owned - * by the BufferProvider, which may use (and even change) the opaque contents - * to organise the internal buffer management. + * returning a BuffHandle. BuffDescr is a convenient front-end to represent the arrangement + * for some type / kind of buffer, and to [allocate](\ref BuffDescr::lockBuffer) it actually. + * @note this descriptor and especially meaning of the #subClassification_ is + * implementation-defined and tied to a specific BufferProvider. */ class BuffDescr { @@ -111,8 +122,16 @@ namespace mem { /** - * Handle for a buffer for processing data, abstracting away the actual implementation. - * The real buffer pointer can be retrieved by dereferencing this smart-handle class. + * Handle to designate a buffer with data for processing, thereby abstracting from the + * memory management implementation. The buffer content can be accessed through an unchecked + * cast operation, assuming that the client has otherwise complete control over the data type + * to be accessed, since the only way to retrieve such a handle is from a BuffDescr. + * Besides some information function, BuffHandle exposes the important API functions + * - `emit()` : indicate that processing is complete and the data can be used / published + * - `release()` : indicate that the client will not touch this handle any more, so that + * storage can be repurposed. + * @note This is a low-level feature and not a *smart-handle* — it is the clien's sole + * responsibility to invoke the `release()` function reliably. */ class BuffHandle { @@ -123,9 +142,9 @@ namespace mem { public: /** @internal a buffer handle may be obtained by "locking" * a buffer from the corresponding BufferProvider */ - BuffHandle(BuffDescr const& typeInfo, void* storage = 0) - : descriptor_(typeInfo) - , pBuffer_(static_cast(storage)) + BuffHandle(BuffDescr const& typeInfo, Buff* storage =nullptr) + : descriptor_{typeInfo} + , pBuffer_{storage} { } // using standard copy operations diff --git a/src/vault/mem/diagnostic-buffer-provider.cpp b/src/vault/mem/diagnostic-buffer-provider.cpp index 34cd9f335..55be5ab1c 100644 --- a/src/vault/mem/diagnostic-buffer-provider.cpp +++ b/src/vault/mem/diagnostic-buffer-provider.cpp @@ -170,10 +170,10 @@ namespace mem { } ID - mark_locked (ID typeKey, Buff* storage, LocalTag implMark) override + mark_locked (ID typeKey, BuffAlloc storage) override { - ID stateKey = stage_->mark_locked (typeKey, storage, implMark); - tracker_.record_locked (stateKey, storage); + ID stateKey = stage_->mark_locked (typeKey, storage); + tracker_.record_locked (stateKey, getAddr(storage)); return stateKey; } diff --git a/src/vault/mem/simple-buffer-state-registry.hpp b/src/vault/mem/simple-buffer-state-registry.hpp index 6fcc6aa25..07b1ed975 100644 --- a/src/vault/mem/simple-buffer-state-registry.hpp +++ b/src/vault/mem/simple-buffer-state-registry.hpp @@ -72,8 +72,9 @@ namespace mem { } // deliberately: create storage, and return reference to it ID - mark_locked (ID typeKey, Buff* storage, LocalTag implMark) override + mark_locked (ID typeKey, BuffAlloc alloc) override { + auto& [storage,size,implMark] = alloc; return metadata_.markLocked (typeKey, storage, implMark); } diff --git a/tests/vault/mem/buffer-metadata-test.cpp b/tests/vault/mem/buffer-metadata-test.cpp index 0e9a27e7b..5ed75079c 100644 --- a/tests/vault/mem/buffer-metadata-test.cpp +++ b/tests/vault/mem/buffer-metadata-test.cpp @@ -155,7 +155,7 @@ namespace test { CHECK (LOCKED == m2.state()); CHECK (SOME_POINTER == m2.access()); // buffer pointer associated - // entries are unique and identifiable + // entries are unique and identifiable by a memory address HashVal keyX = meta_->key(key1, SOME_POINTER); CHECK (meta_->isLocked(keyX)); CHECK (keyX != key1); @@ -385,6 +385,7 @@ namespace test { VERIFY_ERROR (LIFECYCLE, entry.access() ); meta_->markLocked (key, SOME_POINTER); + //Note: we locked the key and this changed the existing entry CHECK (entry.isLocked()); entry.mark (EMITTED); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 1698434a1..2e67d8384 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -114290,6 +114290,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + @@ -114593,8 +114596,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -115001,7 +115004,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -115015,7 +115018,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -115024,9 +115027,12 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+
- + + +
@@ -115042,16 +115048,19 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- - + + + - + + - + + @@ -115088,12 +115097,15 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + - - - + + + + + @@ -115152,7 +115164,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -115160,9 +115173,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Und damit spielt sich das System dann irgendwann auf einen festen Pool von Buffer-»Slots« ein, die permanent im Speicher bleiben. Allerdings bedeutet das dann für die Metadaten ein Cartesisches Produkt  zwischen der Zahl der Memory-Slots und der Zahl der verschiedenen Buffer-Typen.... @@ -115173,9 +115184,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Ob das also gefährlich wird, oder eine etwas ausufernde Metadaten-Storage durchaus überkompensiert wird von dem Wegfall der permanenten Allokation/Deallokation (und Hashtable-Pflege), kann nicht auf theoretischer Ebene abgehandelt werden. Mehr noch: es ist sogar zweifelhaft, ob sich diese Frage überhaupt zuverlässig beantworten läßt, auch empirisch! Dazu müßte man einen Weg finden, um die Stabilität des Verhaltens zu beurteilen, über verschieden Nutzszenarien hinweg @@ -115186,9 +115195,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Allerdings, wenn man sich darauf einläßt, Speicher nicht wegzuräumen, dann muß man explizit den Fall betrachten, daß der Speicher volläuft. In dem Fall bräuchte es einen »Sweep«, der über die Metadaten-Tabelle iteriert und alle Einträge pauschal löscht, die als FREE markiert sind. The usual trade-offs apply. @@ -115201,9 +115208,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Einsicht: nicht re-Locking sondern das onlyNew ist hier redundant @@ -115220,7 +115225,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -115228,12 +115233,24 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + + + + + + + + + + + + + @@ -117793,6 +117810,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + @@ -137496,9 +137517,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

fragwürdig ist im Besonderen der hierarchische Ansatz, der die konkreten Storage-Metadaten als Spezialisierung eines Typ-Eintrags behandelt. @@ -137509,9 +137528,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Im Besonderen stehen dort all die relevanten Infos, die jeder Memory-Allocator benötigt: die konkrete Adresse und die Size (möglicherweise sogar ein MRU-counter, oder ähnliche technische Informationen) @@ -137999,6 +138016,12 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + +