diff --git a/src/steam/engine/buffer-provider.cpp b/src/steam/engine/buffer-provider.cpp index 21fab2a06..0004c8a68 100644 --- a/src/steam/engine/buffer-provider.cpp +++ b/src/steam/engine/buffer-provider.cpp @@ -73,7 +73,7 @@ namespace engine { size_t BufferProvider::getBufferSize (HashVal typeID) const { - metadata::Key& typeKey = bufferStage_->get (typeID); + auto& typeKey = bufferStage_->lookup (typeID); return typeKey.storageSize(); } @@ -124,11 +124,11 @@ namespace engine { BufferProvider::lockBuffer (BuffDescr const& type) { REQUIRE (was_created_by_this_provider (type)); - metadata::Key& typeKey = bufferStage_->get (type); - auto [storage, implMarker] = bufferStore_->provideBuffer (typeKey.storageSize(), type, typeKey.localTag()); - metadata::Entry& entry = bufferStage_->markLocked (typeKey, storage, implMarker); + auto& typeKey = bufferStage_->lookup (type); + auto [storage, localTag] = bufferStore_->provideBuffer (typeKey.storageSize(), type, typeKey.localTag()); + auto& stateKey = bufferStage_->mark_locked (typeKey, storage, localTag); - return BuffHandle (BuffDescr(*this, entry), storage); + return BuffHandle (BuffDescr(*this, HashVal(stateKey)), storage); } @@ -147,13 +147,12 @@ namespace engine { void BufferProvider::emitBuffer (BuffHandle const& handle) { - metadata::Entry& metaEntry = bufferStage_->get (handle); - bufferStore_->mark_emitted (metaEntry.storageSize(), metaEntry.parentKey(), metaEntry.localTag()); - metaEntry.mark(EMITTED); + auto& stateKey = bufferStage_->mark_emitted (handle); + bufferStore_->mark_emitted (stateKey.storageSize(), stateKey.parentKey(), stateKey.localTag()); } - /** BufferProvider API: declare done and detach. + /** BufferProvider API: declare buffer usage as completed and detach from storage. * Client code is required to release _each previously locked buffer_ eventually. * @warning invalidates the BuffHandle, clients mustn't access the buffer anymore. * Right after releasing, an access through the handle will throw; @@ -163,13 +162,11 @@ namespace engine { */ void BufferProvider::releaseBuffer (BuffHandle const& handle) - try { - metadata::Entry& metaEntry = bufferStage_->get (handle); - size_t buffSiz = metaEntry.storageSize(); - metaEntry.mark(FREE); // might invoke embedded dtor function - bufferStore_->detachBuffer (buffSiz, metaEntry.parentKey() - ,std::make_tuple (handle.rawStorage(), metaEntry.localTag())); - bufferStage_->release (metaEntry); + try { // might invoke embedded dtor function + auto& stateKey = bufferStage_->mark_released (handle); + bufferStore_->detachBuffer (stateKey.storageSize(), stateKey.parentKey() + ,std::make_tuple (handle.rawStorage(), stateKey.localTag())); + bufferStage_->discard (handle); } ERROR_LOG_AND_IGNORE (engine, "releasing a buffer from BufferProvider") @@ -184,12 +181,10 @@ namespace engine { void BufferProvider::emergencyCleanup (BuffHandle const& handle, bool invokeDtor) try { - metadata::Entry& metaEntry = bufferStage_->get (handle); - size_t buffSiz = metaEntry.storageSize(); - metaEntry.invalidate (invokeDtor); - bufferStore_->detachBuffer (buffSiz, metaEntry.parentKey() - ,std::make_tuple (handle.rawStorage(), metaEntry.localTag())); - bufferStage_->release (metaEntry); + auto& stateKey = bufferStage_->abandon (handle, invokeDtor); + bufferStore_->detachBuffer (stateKey.storageSize(), stateKey.parentKey() + ,std::make_tuple (handle.rawStorage(), stateKey.localTag())); + bufferStage_->discard (handle); } ERROR_LOG_AND_IGNORE (engine, "cleanup of buffer metadata while handling an error") diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 400a34ba4..794750f2e 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -1228,26 +1228,28 @@ there are only limited sanity checks, and they can be expected to be optimised a Basically the client is responsible for sane buffer access. -
+
Buffers are used to hold the media data for processing and output. Within the Lumiera RenderEngine and [[Player]] subsystem, we use some common concepts to handle the access and allocation of working buffers. Yet this doesn't imply having only one central authority in charge of every buffer -- such an approach wouldn't be possible (due to collaboration with external systems) and wouldn't be desirable either. Rather, there are some common basic usage //patterns// -- and there are some core interfaces used throughout the organisation of the rendering process.
 
-Mostly, the //client code,// i.e. code in need of using buffers, can access some BufferProvider, thereby delegating the actual buffer management. This binds the client to adhere to kind of a //buffer access protocol,// comprised of the ''announcing'', ''locking'', optionally ''attaching'' and finally the ''releasing'' steps. Here, the actual buffer management within the provider is a question of implementation and will be configured during build-up of the scope in question.
+Mostly, the //client code,// i.e. code in need of using buffers, can access some BufferProvider, thereby delegating the actual buffer management. This binds the client to adhere to kind of a //buffer access protocol,// comprised of the ''announcing'', ''locking'', optionally ''attaching'' and finally the ''releasing'' steps. The actual implementation backing these operations is a matter of configuration, which will be setup as part of the ''build process'', when the Render Node network is created.
 
 !usage situations
 ;rendering
-:any calculations and transformations of media data typically require an input- and output buffer. To a large extent, these operations will be performed by specialised libraries, resulting in a call to some plain-C function receiving pointers to the required working buffers. Our invocation code has the liability to prepare and provide those pointers, relying on a BufferProvider in turn.
+:any calculations and transformations of media data typically require an input- and output buffer. To a large extent, these operations will be performed by specialised libraries, often implemented by invoking some plain-C function that receives pointers to working buffers. For each such external processing library, Lumiera need to provide some //glue code//, which is typically loaded as a plug-in. This binding has to ensure that the external library gets the necessary amount of memory for its working buffers -- which can be ensured by registering a suitable ''buffer type'' on the BufferProvider component configured for the Render Engine.
 ;output
-:most any of the existing libraries for handling external output require the client to adhere to some specific protocol. Often, this involves some kind of callback invoked at the external library's discretion, thus forcing our engine to prepare data within an intermediary buffer. Alternatively, the output system might provide some mechanism to gain limited direct access to the output buffers, and such an access can again be exposed to our internal client code through the BufferProvider abstraction.
+:presumably every framework for handling external system output requires the client to adhere to some specific protocol. Often, this involves some kind of callback invoked at the external library's discretion, thus forcing our engine to prepare data within an intermediary buffer. Alternatively, the output system might provide some mechanism to gain limited direct access to the output buffers, and such an access can again be exposed to our internal client code through the BufferProvider abstraction.
+;unit testing
+:the interactions within the Render Engine can be elaborate -- and the unit tests provide the most complete technical documentation, and demonstrate how the components are assumed to collaborate. For this purpose we need simple demo-type implementations, which are easy to reason about, and will often leave out some of the advanced cross-cutting concerns, like concurrency or error handling. Furthermore, some tests need to show that buffers were used the way they are assumed to be used -- and thus require some kind of instrumentation facility to verify actions taken by the BufferProvider
 
 !primary implementations
 ;memory pool
-:in all those situations, where we just need a working buffer for some time, we can rely on our internal custom memory allocator.
-:{{red{only partially implemented as of 12/2024}}} -- as a fallback we just rely on heap allocations through the language runtime
+:in all those situations, where we just need a working buffer for some time, a dedicated custom allocator will be used within the Render Engine. Access to this allocator from the actual worker threads is another, quite advanced topic, since we can not effort to contact a central manager component for each buffer access.
 ;frame cache
 :whenever a calculated result may be of further interest, beyond the immediate need triggering the calculation, it might be eligible for caching.
 :The Lumiera ''frame cache'' is a special BufferProvider, maintaining a larger pool of buffers which can be pinned and kept around for some time,
-:accomodating limited resources and current demand for fresh result buffers.
-
+:accommodating limited resources and current demand for fresh result buffers.
+;test setup
+:a //naive// implementation is used for unit-tests: each buffer request is simply allocated from the heap and this memory is not released or re-used, which allows to verify memory content after invoking the test subject.
 
@@ -1262,13 +1264,13 @@ Beyond that, it can be necessary to associate at least a state flag with //indiv __Note__: while the API to access this service is uniform, conceptually there is a difference between just using the (shared) type information and associating individual metadata, like the buffer state. Type-~IDs, once allocated, will never be discarded (within the lifetime of an Lumiera application instance -- buffer associations aren't persistent). To the contrary, individual metadata //will be discarded,// when releasing the corresponding buffer. According to the ''prototype pattern'', individual metadata is treated as a one-way-off specialisation.
-
+
Analysis of requirement shows that direct access to the buffers holding actual media data is not necessary -- at leas within the entire implementation of the Render Nodes Network. This finding is based on the assumption, that Lumiera does not perform any media processing on its own; all processing is delegated to external libraries. When taking this approach, buffers can be reduced to just some entity to be //managed,// i.e. "allocated", "locked" and "released"; the //actual meaning of these operations can be left to the implementation.// So the code within the render engine just pushes around ''smart-ptr like handles''. These [[buffer handles|BuffHandle]] act as a front-end, being created by and are linked to a {{{BufferProvider}}} implementation. There is no need to manage the lifecycle of buffers automatically, because the use of buffers is embedded into the render calculation cycle, which follows a strict call scheme, which ensures that all buffers are provided before invoking an operation and that buffers are released afterwards. Buffer management can thus be reduced to the following primitive operations...
-* declaring the "type", i.e. the size of a buffer
-* (optionally) a pre-announcement that a given amount of buffers will be required soon
-* the next step is to ensure the necessary buffers are available, by obtaining ("locking") a buffer handle
-* (optionally) the contents of a buffer can be marked as complete ("emitting" a buffer handle)
-* finally the buffer is marked as free ("released") and client code will not touch the buffer handle thereafter.
+* declaring the ''type'', i.e. the size of a buffer, and optionally an object to construct into the buffer storage
+* (optionally) an ''announcement'' that a given amount of buffers will be required soon
+* the next step is to ensure the necessary buffers are available, by obtaining (''locking'') a buffer handle
+* (optionally) the contents of a buffer can be marked as complete (''emitting'' a buffer handle)
+* finally the buffer is marked as free (''released'') and client code will not touch the buffer handle thereafter.
 
 !operations
 While BufferProvider is an interface meant to be backed by various and diverse kinds of buffer and memory management approaches, there is a common set of operations to be supported by any conceivable implementation
@@ -1277,7 +1279,7 @@ While BufferProvider is an interface meant to be backed by various and diverse k
 ;locking
 :this operation actually makes a buffer available for a specific client (thread) and returns a [[buffer handle|BuffHandle]]. The corresponding buffer is marked as used and can't be locked again unless released. If necessary, at that point the BufferProvider might allocate memory to accommodate (especially when the buffers weren't announced beforehand). The locking may fail and raise an exception. Such a failure will be unlikely when buffers have been //announced beforehand.//
 ;attaching
-:optionally the client may attach an object to a locked buffer. This object is placement-constructed into the buffer and will be destroyed automatically when releasing the buffer. Alternatively, the client may provide a pair of constructor- / destructor-functors, to be invoked in a similar way. This allows e.g. to install descriptor structures within the buffer, as required by an external media handling library. Notably, these lifecycle functions are considered part of the »buffer type«
+:optionally the client may request that an object is constructed into the buffer, when the latter is dedicated and locked for this client. Such an object instance will be placement-constructed into the buffer and will be destroyed automatically when releasing the buffer. The client may either specify a C++ language type to construct, possibly with constructor arguments -- or, alternatively, the client may provide a pair of constructor- / destructor-functors, to be invoked in a similar way. This allows e.g. to install descriptor structures within the buffer, as required by an external media handling library. Notably, these lifecycle functions are considered part of the »buffer type«
 ;emitting
 :the client //may optionally mark a state transition// -- whose precise meaning remains implicit and implementation dependent. From the client's perspective, emitting and releasing may seem equivalent, since the buffer content should not be altered after that point. However, conceivably there are usages where it matters for //the consumer// to be sure an expected result was actually achieved, since the producer may well acquire the buffer and then fail to complete the required work, prompting some clean-up safety mechanism to merely release the resources.
 ;releasing
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index a5d114624..d01483536 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -111660,16 +111660,13 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
-  
-    
-  
+  
   
     

zumal das nirgends im Code dokumentiert ist, und diese implizite Annahme nichts zum Begreifen beiträgt (da sich hier zwei Layer vermischen, und man Informationen durch den Implementierungs-Call durchfädeln muß)

- -
+
@@ -111851,7 +111848,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + + @@ -111859,7 +111857,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -111899,32 +111898,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

...hier liegt ein potentiell gefährlicher Hebel — wenngleich auch im Fall von Buffern vermutlich nicht besonders relevant (da Buffer groß sind)

- -
+
- - - +

Für small-objects-Allokatoren kann dieses Problem einen drastischen Einfluß auf die Performance haben. Im konkreten Fall ist die Situation so komplex, daß ich tunlichst jedwede Art von Abschätzung und Optimierung vermeiden sollte.

- -
+
@@ -112296,7 +112289,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

- + @@ -112320,7 +112313,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -112330,7 +112323,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -112608,16 +112601,13 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

weil dort die tatsächlichen Zustandsübergänge stattfinden...

- -
+
@@ -112644,6 +112634,20 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + @@ -112653,16 +112657,13 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Und zwar weil das BufferProvider-Interface auch erweiterte Funktionalität ansteuern soll, wie den Cache oder die OutputSink. Diese brauchen zwingend den emit()-Aufruf. Zwar könnte man jetzt versuchen, diese Delegation aus beiden Zonen zusammenzubauen — aber das widerspricht der Idee dieses Refactorings zur Trennung der Zonen. Also läuft es darauf hinaus, daß solche Spezial-Implementierungen die generische Metadaten/Zustands-Implementierung verwenden, aber einen speziellen BufferStore. Somit müssen alle Zustandsübergänge tatsächlich an beide Implementierungs-Säulen propagiert werden

- -
+
@@ -112682,6 +112683,75 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
+ + + + + + + + + + + + + + + + + +

+ ...sie prüft zwar auf valide Übergänge, aber andere Bedingungen werden nurch durch (in Subroutinen versteckte) Konsistenz-Checks sichergestellt; sieht also nach typischem, kohäsivem Implementierungs-Code aus, der in Ordnung ist, sofern er nicht auf einem API zugänglich gemacht wird +

+ +
+
+ + + + +

+ auch die lock()-Funktion macht nochmal äquivalente checks, aber nur die lock()-Funktion stellt sicher daß es einen Buffer gibt +

+ +
+
+ + + + +

+ konkret: man könnte zwar mark(LOCKED) aufrufen, ohne einen Buffer zu setzen; das würde aber dann prüfen, ob ein ctor aufgerufen werden muß, und an der Stelle eine Exception werfen +

+ +
+
+
+ + + + +

+ ...also ohne zu wissen wo man steht; im Besonderen kann man es aus dem state FREE aufrufen (alle anderen Transitions wären auch über die State-Machine äquivalent möglich). +

+ +
+
+ + + + +

+ schafft damit die Möglichkeit, den dtor-Aufruf extern zu steuern; wird er gefordert, dann werden alle Vorbedingungen geprüft, und wenn es einen registrierten dtor gibt, wird er aufgerufen; letztlich heißt das aber nur, daß man hier explizit wälhen kann, denn dtor nicht  aufzurufen (und tatsächlich ist das sogar der default für diesen Aufruf-Pfad) +

+ +
+
+
+
+
+ + @@ -112740,7 +112810,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -112775,16 +112845,13 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

auf dem BufferStore-API werden lediglich drei Aufrufe übrig bleiben. Von denen zudem zwei speziell sind. Und jede Implementierung wird lediglich dieses Tupel wieder auseinandernehmen...

- -
+
@@ -112802,6 +112869,117 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ denn wir gehen davon aus daß die »emit«-Operation immer nur eine Flag setzt und letztlich nicht scheitern kann. Insofern können wir durchaus die State-Transition zuerst markieren, und dann erst den externen Effekt auf dem Store-API auslösen. Hier hilft uns, daß auf Buffer im Render-Prozeß grundsätzlich nicht concurrent zugegriffen wird +

+ +
+
+ + + + + + + + +

+ ....für die Metadaten im Besonderen, denn diese folgen insofern stets dem BufferStore, als die konkrete Adresse und das LocalTag in den Key eingehen. Daher macht es nie Sinn, einen metadata::Entry zu erhalten. Ganz anders für Buffer, die bleiben im Allokations-Pool, aber das ist rein ein Detail der Allokator-Implementierung. +

+ +
+
+ + + + +

+ Denn wir erlauben nicht, daß ein bereits LOCKED Entry nochmal geLOCKED wird. Derzeit erzwingen wir sogar, daß der Entry noch gar nicht existiert, aber dieses Kriterium wäre eigentlich nicht notwendig: auf den gleichen Entry können wir nur kommen, wenn die Store-Implementierung die gleiche Adresse und das gleiche LocalTag angeliefert hat. Insofern hängt die Konsistenz rein an der internen Konsistenz im Store; wenn bei dem die de-Allokation scheitert, aber er dann damit klar kommt, die gleiche Storage-Adresse wieder zu verwenden, für die gleiche Allokations-Größe, dann soll uns das egal sein +

+ +
+
+ + + +
+ + + + + + + + + +

+ es handelt sich hier um eine Implementierungs-Delegierung, und keinen virtual method call; unter den Annahme, daß auch die Hashtable-Implementierung geinlined wird, dürfte der Optimiser im Stande sein, die common subexpression extraciton zu machen, da ja auch das Löschen einen erfolgreichen Lookup voraussetzt +

+ +
+
+
+ + + + +

+ Klarheit des API geht vor! denn vermutlich spielt der Performance-Unterschied gar keine Rolle — zumindest solange bis wir diese Aspekte alle unter Real-Bedingungen beobachten können... +

+ +
+ +
+
+
+ + + + + + + +