diff --git a/doc/devel/uml/BoUML-diagrams.txt b/doc/devel/uml/BoUML-diagrams.txt index 384c8f0d7..869c4195b 100644 --- a/doc/devel/uml/BoUML-diagrams.txt +++ b/doc/devel/uml/BoUML-diagrams.txt @@ -49,5 +49,4 @@ On occasion, we'll replace them by drawings from our current UML model. 128517: classdiagram 142725 "Time flavours" 133637: classdiagram 143877 "Player Entities" 133637: objectdiagram 144005 "Play Process Structure" -133637: sequencediagram 145157 "output data exchange" 128773: classdiagram 158213 "UIBus" diff --git a/doc/devel/uml/OutputSlot_sequence.svg b/doc/devel/uml/OutputSlot_sequence.svg new file mode 100644 index 000000000..5a70461b5 --- /dev/null +++ b/doc/devel/uml/OutputSlot_sequence.svg @@ -0,0 +1,1672 @@ + + + OutputSlot call sequence + Generated with Umbrello + + + + + + + GPL v2+ or CC-By-SA 4 + + + + + Ichthyostega + + + OutputSlot call sequence + + Lumiera documentation +(UML created with umbrello 5.0) + + + + + + + + + + + + + + + loop [N] + + + + + + + + : OutputManager + + + + + : Client + + + + + : BuffHandle + + + + + : DataSink + + + + + : OutputSlot + + + + + : AllocState + + + + + : BufferProxy + + + + + + : Connection + + + + + + + + mark_emitted() + destroy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + publish() + + + + + + + + + + + lockBuffer + claimBufferFor() + access buffer + frame() + accessAs() + getOpenedSinks() + getOutput + new + allocate() + generate data + emit() + release() + detachBuffer() + destroy + shutDown() + + + + + + + + + + + + + + + + + + + + release() + done + + + + + + + + + Data producer + + + + + + Output Slot + + + + + + Output + implementation + + + diff --git a/doc/devel/uml/fig145157.png b/doc/devel/uml/fig145157.png deleted file mode 100644 index dac8e23cb..000000000 Binary files a/doc/devel/uml/fig145157.png and /dev/null differ diff --git a/src/vault/mem/buffer-proxy-adaptor.hpp b/src/vault/mem/buffer-proxy-adaptor.hpp index d92638a13..a216d9b1e 100644 --- a/src/vault/mem/buffer-proxy-adaptor.hpp +++ b/src/vault/mem/buffer-proxy-adaptor.hpp @@ -19,7 +19,7 @@ ** between the »Buffer Provider Protocol« and the »Output Slot Protocol«. ** Ultimately, however, it was not used; instead, a similar, specially ** tailored construct was directly integrated. - ** @see output-proxy-provider-test.cpp + ** @see buffer-proxy-adaptor-test.cpp ** @see output-buffer-proxy.hpp ** @see output-slot.hpp */ @@ -60,6 +60,7 @@ namespace mem { * - is there only one resource or is this setup statefull? * - callback functors for the lifecycle stages related to the client's access * @todo 3/2026 this is prototyping code and was retained for demonstration purposes. + * @see output-buffer-proxy.hpp (the actual solution derived from this) */ template class BufferProxyAdaptor @@ -87,7 +88,7 @@ namespace mem { } BuffAlloc - provideBuffer (HashVal,size_t siz,LocalTag targetMarker, int64_t customArg) override + provideBuffer (HashVal,size_t siz,LocalTag targetMarker, int64_t) override { BuffAlloc storageSlot{asBuffer(targetMarker), siz, targetMarker}; CONF::on_lock (storageSlot); diff --git a/src/vault/out/diagnostic-output-slot.cpp b/src/vault/out/diagnostic-output-slot.cpp index d44f13beb..32f5d48c9 100644 --- a/src/vault/out/diagnostic-output-slot.cpp +++ b/src/vault/out/diagnostic-output-slot.cpp @@ -333,7 +333,9 @@ namespace out { unique_ptr DiagnosticOutputSlot::setupTrackingConnections (DiagnosticOutputSlot::OutputTracker& outputTracker) { - using AllocState = OutputSlot::AllocState; + constexpr bool SIMPLE_METADATA_TABLE_FOR_TEST{true}; + + using AllocState = OutputSlot::AllocState; using ConStorage = AllocState::ConnectionStorage; return make_unique (outputTracker.numDataFeeds diff --git a/src/vault/out/output-buffer-proxy.hpp b/src/vault/out/output-buffer-proxy.hpp index 42a5caaa1..4cf0ae1bc 100644 --- a/src/vault/out/output-buffer-proxy.hpp +++ b/src/vault/out/output-buffer-proxy.hpp @@ -16,8 +16,15 @@ ** This implementation building-block allows to integrate the output buffers ** managed by a specific output mechanism with the generic implementation of ** OutputSlote, so that only a BuffHandle needs to be exposed to the client. - ** @see output-proxy-provider-test.cpp - ** @see output-buffer-proxy.hpp + ** The client uses this setup in accordance to the »Output Slot Protocol«. + ** @remark this adaptor is generated automatically when some specific + ** output technology is exposed through an OutputSlot; at that + ** point, a custom OutputSlot::Connection implementation + ** is [instantiated](\ref OutputSlot::allocate), causing + ** the setup and wiring of OutputSlot::AllocState. + ** @see buffer-proxy-adaptor-test.cpp (a pathfinder for this design) + ** @see output-alloc-state-test.cpp + ** @see output-slot-connection.hpp ** @see output-slot.hpp */ @@ -27,46 +34,44 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" -#include "lib/symbol.hpp" -#include "lib/meta/util.hpp" #include "vault/mem/buffer-provider.hpp" #include "vault/mem/buffer-provider-setup.hpp" -#include "vault/mem/heap-mem-buffer-store.hpp" #include "vault/mem/simple-buffer-state-registry.hpp" +#include "vault/mem/local-buffer-stage.hpp" -#include -#include - +#include namespace vault{ namespace out { - using std::move; - using lib::Literal; using lib::HashVal; using vault::mem::Buff; using vault::mem::LocalTag; using vault::mem::BuffAlloc; using vault::mem::BuffDescr; - using vault::mem::BuffHandle; - using vault::mem::TypeHandler; ///////////////////////////OOO Rly? + using vault::mem::TypeHandler; using vault::mem::BufferProviderSetup; /** - * Adapter to expose controlled access to some memory resource + * Adapter to expose controlled access to the \ref OutputSlot::Connection * through a [»Buffer Type«](\ref BuffDescr) and a BuffHandle as front-end, * in accordance with the BufferProvider protocol. * - * This template is an implementation building block and needs to be instantiated - * with a policy or configuration to define the flexible parts of the behaviour - * - what a Buffer Type means - * - is there only one resource or is this setup statefull? - * - callback functors for the lifecycle stages related to the client's access - * @todo 3/2026 this is prototyping code and was retained for demonstration purposes. + * This template is an implementation building block for the OutputSlot and + * creates the crucial link between the DataSink handle exposed to the client + * and the actual implementation of the connection technology. It needs to be + * instantiated with the concrete connection implementation type; this happens + * as part of instantiating OutputSlot::AllocState — and this is the + * typical way how a concrete connection is packaged into an OutputSlot. + * + * @tparam CON the actual OutputSlot::Connection implementation + * @tparam isTest use a naive self-contained metadata hashtable; + * by default, a threadsafe system of local metadata tables + * is used in conjunction with the EngineBufferMetadata service */ - template + template class OutputBufferProxy : public BufferProviderSetup { @@ -113,7 +118,10 @@ namespace out { struct Setup { - auto buildStage() { return std::make_unique("OutputBufferProxy"); } + using MetadataRegistry = std::conditional_t; // ◁──────────┨ by default use the production-grade implementation + + auto buildStage() { return std::make_unique("OutputBufferProxy"); } auto buildStore() { return std::make_unique(); } }; diff --git a/src/vault/out/output-slot-connection.hpp b/src/vault/out/output-slot-connection.hpp index 5ed8982ed..ccdf4876f 100644 --- a/src/vault/out/output-slot-connection.hpp +++ b/src/vault/out/output-slot-connection.hpp @@ -13,13 +13,13 @@ /** @file output-slot-connection.hpp ** Interface for concrete output implementations to talk to the OutputSlot frontend. - ** This setup with an OutputSlot concept helps to decouple the render engine implementation + ** This setup with an OutputSlot as façade helps to decouple the render engine implementation ** from the details of handling any kind of external output connection. The act of data output ** is reduced to the operations described by the OutputSlot::Connection interface. ** ** # Interplay of parts involved into an actual implementation ** - ** Each distinct output capability must be registered through an OutputManager (and this is another + ** Each distinct output capability must be registered with an OutputManager (and this is another ** topic beyond the scope of the interaction described here). When a client wants to output data, ** it obtains a suitable OutputSlot through the OutputDirector. This OutputSlot is created and ** initialised properly for this single use by the OutputManager in charge for that kind of @@ -29,26 +29,54 @@ ** ** This allocation state is linked at implementation level to some specific implementation of the ** OutputSlot::Connection interface, represented as template parameter \a CON. An instance will be - ** created for each distinct data feed (which applies only if the media stream type in question - ** comprises several data feeds running in parallel). In many typical cases, only one data feed - ** and thus only a single Connection instance is required. The client can then retrieve a DataSink - ** handle for each data feed. Notably, each DataSink participates in the ref-count and thus keeps - ** the OutputSlot and the Connection(s) alive. Furthermore, [timing constraints](\ref vault::out::Timings) - ** must be defined, to instruct the client when to deliver the data for each prospective frame. Obviously, + ** created for each distinct data feed — which applies only if the media stream type in question + ** comprises several data feeds running in parallel; here a data feed does not necessarily correspond + ** to a _channel_, since it is quite common to _interleave_ several channels in a single feed. In many + ** typical cases, only one data feed and thus only a single Connection instance is required. The client + ** can then retrieve a DataSink handle for each data feed. Notably, each DataSink participates in the + ** ref-count and thus keeps the OutputSlot and the Connection(s) alive. + ** Furthermore, [timing constraints](\ref vault::out::Timings) must be defined, to instruct the client + ** when precisely to deliver the data for each prospective frame (time window of delivery). Obviously, ** details depend on the underlying technology. In some cases, the client might prepare several buffers ** full of data in advance, while for another technology, that, for example, relies on _double buffering_, - ** the client gets only a single precisely limited time window immediately prior to each _buffer flip_. + ** the client has to meet a single precisely limited time window immediately prior to each _buffer flip_. ** ** The client code interacts with the output data buffer through a BuffHandle as front-end. This access ** scheme is similar to what is used by the generic vault::mem::BufferProvider within the Render Engine. ** To enable this setup, the OutputSlot::AllocState embeds a _proxy BufferProvider_ and uses a preconfigured ** [buffer (type) descriptor](\ref vault::mem::BuffDescr) for each connection, which also includes information - ** about the size of the buffer, which is obtained [from the connection](\ref OutputSlot::Connection::getBufferSize) + ** about the size of the buffer, as obtained [from the connection](\ref OutputSlot::Connection::getBufferSize). ** To start work on a new frame, the client invokes DataSink, which actually is a functor. This call delegates ** through the proxy BufferProvider — see \ref OutputBufferProxy::provideBuffer() and thus ends up calling ** into Connection::claimBufferFor(frameNr). ** + ** ## Threadsafe production-grade implementation + ** + ** OutputSlot operates as part of the BufferProvider framework within the Render Engine; it must thus + ** be capable to handle concurrent access. The construction of a OutputSlot instance happens from a single thread, + ** which is typically the session thread (since the output connection is created as part of starting a play process). + ** Immediately at construction, an AllocState must be provided, which in turn creates all the connection objects. + ** The activation of these connections and the access to output buffers (»lock a buffer) will then be initiated + ** from the worker threads. It can be assumed that a single worker processes the job for some output frame, but + ** other workers might start processing other output frames concurrently, assuming the underlying connection + ** technology allows to produce output in advance this way (which is rather uncommon for real-time output, + ** where typically a double-buffering scheme is employed, which implies that at any given time only one + ** single output buffer can be exposed; yet for other kinds of output, like e.g. memory mapped file + ** output, several frames can be delivered in parallel). + ** + ** Notably this implies that the connection implementation must be prepared for receiving data concurrently. + ** Other than that, the OutputBufferProxy will use a LocalSlice, and thus a thread-local + ** metadata table, in concert with the EngineBufferMetadata as central hub. The link to a connection object + ** for a single channel is represented as a [»buffer type«](\ref BuffDescr). When a worker »locks« some + ** frame, the BufferProvider implementation will access the thread-local metadata store to track the + ** state transitions; on first usage it will have to sync-down the type descriptor from the central hub. + ** + ** For the purpose of unit testing, a simple self-contained metadata table can be used instead, which has the + ** advantage that all state is discarded when deleting the OutputSlot object. In fact, most unit tests rely on + ** the \ref DiagnosticOutputSlot, which sets the `isTest` template parameter internally to use this variant. + ** ** @see OutputSlotProtocol_test + ** @see OutputAllocState_test */ @@ -104,6 +132,9 @@ namespace out { * `publish()` is invoked too late, but must keep the buffer in locked / exclusive * state, since the client might still be writing data, even after the deadline. * @note the meaning of FrameID is implementation defined. + * @warning the Connection implementation must be threadsafe; while there will be never + * any contention regarding a single frame number, consecutive frames can and will + * be delivered by different threads, concurrently. */ class OutputSlot::Connection : util::NonCopyable @@ -134,14 +165,15 @@ namespace out { * Theses smart-handles are ref counting and thus the individual * Connection object is kept alive as long as at least one DataSink * handle is retained. + * @tparam isTest use a simple BufferMetadata table in OutputBufferProxy. */ - template + template class OutputSlot::AllocState : public OutputSlot::Allocation { using Connections = lib::ScopedCollection; using OpenedSinks = OutputSlot::OpenedSinks; - using BufferProxy = OutputBufferProxy; + using BufferProxy = OutputBufferProxy; Connections connections_; BufferProxy bufferProxy_; @@ -215,5 +247,33 @@ namespace out { + /* ===== OutputSlot factory functions ===== */ + + /** + * @remark this is the generic builder variant, using a _populator functor_ + * that is passed directly to the constructor of lib::ScopedConnection + */ + template + OutputSlot + OutputSlot::allocate (size_t cnt, FUN&& populator) + { + return OutputSlot{std::make_unique> (cnt, std::forward(populator))}; + } + + /** + * @remark in moste cases this variant is preferable, + * since it creates all connection objects from the same set of constructor arguments + * @note args will be copied into a _populator functor_ that is then applied for each connection to create + */ + template + requires std::is_constructible_v + OutputSlot + OutputSlot::allocate (size_t cnt, ARGS&&... args) + { + using Connections = lib::ScopedCollection; + return OutputSlot{std::make_unique> (cnt, Connections::fill(std::forward(args)...) )}; + } + + }} // namespace vault::out #endif /*VAULT_OUT_OUTPUT_SLOT_CONNECTION_H*/ diff --git a/src/vault/out/output-slot.hpp b/src/vault/out/output-slot.hpp index 5f2ce4fc7..50b53ccf7 100644 --- a/src/vault/out/output-slot.hpp +++ b/src/vault/out/output-slot.hpp @@ -14,7 +14,7 @@ /** @file output-slot.hpp ** An (abstract) capability to send media data to an external output. ** OutputSlot is the central metaphor for the organisation of actual (system level) outputs; - ** using this concept allows to separate and abstract the data calculation and the organisation + ** using this pattern allows to separate and abstract the data calculation and the organisation ** of playback and rendering from the specifics of the actual output sink. Actual output ** possibilities can be added and removed dynamically from various components (vault, stage), ** all using the same resolution and mapping mechanisms @@ -185,7 +185,7 @@ namespace out { * @internal Implementation subclass: * manage active connections through this OutputSlot */ - template + template class AllocState; @@ -209,13 +209,14 @@ namespace out { * @note usually there is no need to subclass OutputSlot or AllocState, * rather this factory should be used to establish an activated * OutputSlot configured to use a specific OutputSlot::Connection. + * @see output-slot-connection.hpp */ + template + requires std::is_constructible_v + static OutputSlot allocate (size_t cnt, ARGS&&... args); + template - static OutputSlot - allocate (size_t cnt, FUN&& populator) - { - return OutputSlot{std::make_unique> (cnt, std::forward(populator))}; - } + static OutputSlot allocate (size_t cnt, FUN&& populator); }; diff --git a/tests/vault/mem/buffer-proxy-adaptor-test.cpp b/tests/vault/mem/buffer-proxy-adaptor-test.cpp index b22622c46..b549c3f76 100644 --- a/tests/vault/mem/buffer-proxy-adaptor-test.cpp +++ b/tests/vault/mem/buffer-proxy-adaptor-test.cpp @@ -41,9 +41,10 @@ namespace test { * rigged such that a pre-existing given buffer (address) can be * passed through and exposed as BuffHandle. * @remark while this looks like a rather convoluted implementation setup, - * this feature is crucial to make the »Output Slot Protocol« + * this feature was crucial to make the »Output Slot Protocol« * mesh up well ([1415]) with the »Buffer Provider Protocol«, * used internally within the Render Engine + * @see output-buffer-proxy.hpp (the implementation actually used) * [1415]: https://issues.lumiera.org/ticket/1415] */ class BufferProxyAdaptor_test : public Test diff --git a/tests/vault/mem/local-mem-pool-test.cpp b/tests/vault/mem/local-mem-pool-test.cpp index f5f1f12a4..c02da814c 100644 --- a/tests/vault/mem/local-mem-pool-test.cpp +++ b/tests/vault/mem/local-mem-pool-test.cpp @@ -22,7 +22,6 @@ #include "lib/iter-explorer.hpp" #include "lib/iter-stack.hpp" #include "lib/format-util.hpp" -#include "test/diagnostic-output.hpp"////////////TODO #include diff --git a/tests/vault/out/output-alloc-state-test.cpp b/tests/vault/out/output-alloc-state-test.cpp index ca6691219..1760a7a47 100644 --- a/tests/vault/out/output-alloc-state-test.cpp +++ b/tests/vault/out/output-alloc-state-test.cpp @@ -100,17 +100,18 @@ namespace vault::out::test { ~MockConnection() { log.call ("MockConnection", "destroy"); } }; - - inline OutputSlot - makeFakeSlot() - { - const uint NUM_CONNECTIONS = 1; - return OutputSlot::allocate (NUM_CONNECTIONS - ,[](auto& storage) - { - storage.template create(); - }); - } + struct FakeSlot + : OutputSlot + { + static constexpr uint NUM_CONNECTIONS = 1; + static constexpr bool SINGLE_THREADED_TEST = true; + using Connections = lib::ScopedCollection; + using MockConnectState = OutputSlot::AllocState; + + FakeSlot() + : OutputSlot{std::make_unique (NUM_CONNECTIONS, Connections::fill())} + { } + }; }//(End) test fixture @@ -139,7 +140,7 @@ namespace vault::out::test { const FrameCnt FRA_NO = 55; { log.event("create FakeSlot"); - auto slot = makeFakeSlot(); + FakeSlot slot; log.event("retrieve DataSink"); auto sinks = slot.getOpenedSinks(); diff --git a/uml/Lumiera.xmi b/uml/Lumiera.xmi index b6f88cd56..78bf22799 100644 --- a/uml/Lumiera.xmi +++ b/uml/Lumiera.xmi @@ -3,7 +3,6 @@ - @@ -13,30 +12,35 @@ - - - - - + + + + + - + - + - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -333,6 +337,7 @@ + @@ -359,7 +364,6 @@ - @@ -374,6 +378,10 @@ + + + + @@ -386,10 +394,10 @@ - - + + - + @@ -504,6 +512,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -517,6 +614,7 @@ + @@ -691,9 +789,10 @@ + - + @@ -782,7 +881,7 @@ - + @@ -948,7 +1047,7 @@ - + @@ -1010,32 +1109,20 @@ - + + - - - - - - - - - - - - - - - - - - + + + + + @@ -1131,6 +1218,7 @@ + @@ -1144,12 +1232,15 @@ - + + + + @@ -1166,11 +1257,13 @@ + + @@ -1198,8 +1291,5 @@ - - - diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 11d92836d..eae00ff56 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -1240,10 +1240,10 @@ At this point, a separation is introduced into the //local coordinator// (which There are only limited sanity checks, and they can be expected to be optimised away for production builds. The render processing is assumed to proceed along preconfigured processing pipelines, which are instantiated from a »blueprint« -- and the [[Builder]] is responsible to arrange this calculation prototype in such a way, that only valid data accesses are possible. The processing functions and the buffer descriptors are preconfigured to match up. -
+
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. 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.
+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'', and finally the ''releasing'' steps. The actual implementation to back these operations is a matter of configuration, and will be setup as part of the ''build process'', when the Render Node network is created.
 
 !usage situations
 ;rendering
@@ -1260,8 +1260,18 @@ Mostly, the //client code,// i.e. code in need of using buffers, can access some
 :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,
 :accommodating limited resources and current demand for fresh result buffers.
+;output proxy
+:this is a special flavour, and is tightly integrated into the OutputSlot implementation: the client obtains a {{{DataSink}}} handle for each feed; when initiateing the output of one frame, a BuffHandle is returned -- which however is backed by a special {{{OutputBufferProxy}}} that translates the client interactions into calls to the {{{OutputSlot::Connection}}} interface. The latter abstracts the access to the underlying output technology.
 ;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.
+
+
+!Buffer handling
+The management of buffer memory is a major cross-cutting concern in the RenderEngine.This topic encompasses several aspects, since it is necessary to supply the right kind (size) of buffer, to mark a place in the [[render node graph|LowLevelModel]] to use such a //buffer type//, yet also to satisfy the demand for buffer memory that accrues concurrently, as the [[workers|SchedulerWorker]] process a constant flow of [[render jobs|RenderJob]]. Furthermore, the //state// of each individual buffer must be //tracked// as part of the BufferMetadata, since usage of a buffer proceeds through several //phases// (→ the operations outlined at the BufferProvider interface).
+
+A far reaching trait of the Lumiera architecture is the decision to //pre-arrange all possible computations// in the given situation into a ''network of nodes''. Within this framework, the BuffHandle plays the role of a junction point to separate and connect the [[processing logic|NodeOperationProtocol]] from the backing services and the management of resources. The render computation does not need to know how buffers are organised. Each worker can use the //buffer type descriptor// {{{BuffDescr}}} to claim a buffer for use. Furthermore, at the start of each job, an expected maximum capacity simultaneously required is //announced//, based on static reasoning over the render node graph. The implementation of buffer handling relies on thread-local services to handle such requests within a local scope as far as possible.
+
+Allocations are the sole responsibility of the central {{{EngineBufferManager}}} -- yet demand for buffer space is accrued in the local memory pools for each worker. To the extent possible, demand and actual allocations are exchanged by asynchronous messaging, to avoid contention on shared locks when accessing the central services. After usage, a buffer is retained in the thread-local pool, and marked with a //score// whenever it can be re-used to satisfy another demand. It is certainly preferable to re-use a buffer available locally, even when this buffer is too large; but it will get a lower score as when the buffer is a perfect match. Buffers that do not score as well are returned on a tight schedule through the message queue. Interestingly, the central {{{EngineBufferManager}}} does not operate on its own -- since the overall design of the Render Engine avoids //dedicated »service threads«//. The rationale is that such dedicated threads would have to be squeezed into the distribution of work onto the cores, running danger to decrease the cache locality and overthrow the pre-established schedule. Rather, the necessary management work is accrued in small chunks and directly integrated into the processing of some job, that just happens to hit an immediate unsatisfied demand for memory.
 
@@ -5318,7 +5328,7 @@ And this raises another intricate question of design: //shall {{{Connection}}} b This would be a very elegant, and modern solution, offering much flexibility for the Implementer of said {{{Connection}}}. The {{{OutputBufferProxy}}} could be templated to that {{{Connection}}} type and thus allow the optimiser to inline and reduce the callback functionality. However -- C++ concepts are a rather new and arcane feature, as they rely on very special syntax and semantics that still requires some time in practical use to catch on and prove its viability. A further objection that can be raised is the fact that compliance to a concept leaves no traces within the implementation -- quite contrary to a classical ~OO-style interface, where the implementation is very obviously marked as such. These considerations bring me to the decision to //reject the use of a concept// here, and to rely on a conventional interface for {{{Connection}}}. Notably it can be expected that new kinds of connection will be added frequently, and so the task for a developer not familiar with all fine points of this design should be reasonably straight forward: „You have to implement the {{{Connection}}} interface“
-
+
OutputSlot is an abstraction, allowing unified treatment of various physical output connections from within the render jobs. 
 
 Each distinct output capability must be registered through an OutputManager. When a client wants to output data, it obtains a suitable OutputSlot through the OutputDirector. This OutputSlot is created and initialised properly for this single use by the {{{OutputManager}}} in charge for that kind of output capability; notably it must be outfitted with an actual implementation of the {{{OutputSlot::Allocation}}} interface, and typically {{{OutputSlot::AllocState<CON>}}} will be used for that purpose.
@@ -5327,7 +5337,7 @@ So actual knowledge regarding the underlying technology is confined to the »dri
 The client code interacts with the output data buffer through a BuffHandle as front-end. This access scheme is similar to what is used by the generic BufferProvider within the Render Engine. To enable this setup, the OutputSlot embeds a //proxy {{{BufferProvider}}}// and uses a preconfigured buffer (type) descriptor for each connection, which also includes information about the size of the buffer. To start work on a new frame, the client invokes the {{{DataSink}}}, which actually is a functor, with a signature {{{BuffHandle(FrameID)}}}. This call delegates through the proxy {{{BufferProvider}}} ends up calling into {{{Connection::claimBufferFor(frameNr)}}}.
 
 !operation steps
-[>img[Sequenz of output data exchange steps|uml/fig145157.png]]As mentioned, the OutputSlot defines a nested ''interface {{{Connection}}}'', with standard operations defined as virtual functions. Each connection technology is expected somehow to support a sequence of actual steps in a ''data exchange protocol''.
+As mentioned, the OutputSlot defines a nested ''interface {{{Connection}}}'', with standard operations defined as virtual functions. Each connection technology is expected somehow to support a sequence of actual steps in a ''data exchange protocol''.
 ;claimBufferFor()
 :claims the specified buffer for exclusive use by the client.
 :Client may now generate output data into that buffer.
@@ -5339,7 +5349,7 @@ The client code interacts with the output data buffer through a BuffHandle as fr
 :effectively redundant after the client has invoked {{{emit()}}} -- but the client may choose not to do so (when it is foreseeable that the deadline can not reasonably be met); in such cases, the {{{release()}}} call is the only state indication the {{{Connection}}} implementation gets.
 :Triggered by the client invoking {{{release()}}}
 
-{{red{WARNING 3/2026 this UML drawing above is outdated}}}
+[img[Sequence of output data exchange steps|uml/OutputSlot_sequence.svg]]
 
 !buffer states
 While the BufferProvider, used as front-end for the client, abstracts away the actual access to the output buffers and just hands out a [[buffer handle|BuffHandle]], the server side (which here is the concrete output slot and the backing output technology) is allowed to associate and maintain a ''state flag'' with each buffer. The general assumption is that each output cycle for a single frame originates from a single RenderJob and thus happens from a single thread; moreover, a memory barrier can be assumed prior to invoking the next job. This implies that state transitions regarding the BufferMetadata can be mapped to states as seen from the perspective of the {{{Connection}}} implementation:
@@ -6285,7 +6295,7 @@ This is the core service provided by the player subsystem. The purpose is to cre
 :any details of this processing remain opaque for the clients; even the player subsystem just accesses the EngineFaçade
 
-
+
//Integration effort to promote the development of rendering, playback and video display in the GUI//
 This IntegrationSlice was started in {{red{2023}}} as [[Ticket #1221|https://issues.lumiera.org/ticket/1221]] to coordinate the completion and integration of various implementation facilities, planned, drafted and built during the last years; this effort marks the return of development focus to the lower layers (after years of focussed UI development) and will implement the asynchronous and time-bound rendering coordinated by the [[Scheduler]] in the [[Vault|Vault-Layer]]
 
@@ -6332,7 +6342,9 @@ __February.25__: built extended verification mechanisms, including some Node met
 
 __January.26__: During the last nine months, unfortunately I was occupied with generic clean-up, upgrade, infrastructure and research work -- and so there was ''no progress'' regarding the PlaybackVerticalSlice. Just before that recess, an important breakthrough was achieved: a complete node graph was built and invoked end to end in a unit test setting. The further integration up to a RenderJob invocation is still blocked by some yet unresolved [[design mismatch (#1395)|https://issues.lumiera.org/ticket/1395]] related to the //»BufferProvider Protocol«// vs the //»OutputSlot Protocol«//; this is where I'll pick up the development work stalled almost a year ago.
 
-__March.26__: Meanwhile I was able to get at the root of the confusion related to BufferProvider and OutputSlot. This insight allows to rectify and realign the architecture, while largely keeping the APIs and both »Protocols« intact. As part of this design revalidation, I did an in-depth re-assessment of buffer metadata handling and got a much better understanding of the requirements for an actual buffer manager implementation -- and it is clear now that at least such a basic implementation is required to achieve a first engine integration. Furthermore, I was indeed able to build a proxy-BufferProvider that can be used as façade within the OutputSlot Protocol, so that both Protocols can be reconciled. The integration of this new proxy implementation necessitates a deeper refactoring within OutputSlot though, leading to a much better and cleaner design overall.
+__March.26__: Meanwhile I was able to get at the root of the confusion related to BufferProvider and OutputSlot. This insight allows to rectify and realign the architecture, while largely keeping the ~APIs and both »Protocols« intact. As part of this design revalidation, I did an in-depth re-assessment of buffer metadata handling and got a much better understanding of the requirements for an actual buffer manager implementation -- and it is clear now that at least such a basic implementation is required to achieve a first engine integration. Furthermore, I was indeed able to build a proxy-BufferProvider that can be used as façade within the OutputSlot Protocol, so that both Protocols can be reconciled. The integration of this new proxy implementation necessitates a deeper refactoring within OutputSlot though, leading to a much better and cleaner design overall.
+
+__May.26__: building upon the insight gained through the preceding design rework, a production-grade buffer memory management scheme could be devised. It features thread-local back-end services for the two »pillars« supporting the implementation of the BufferProvider API. These stateful per-worker services are complemented in both cases by a engine-level central service. Notably, for the {{{BufferStore}}}, which relates to the handling of memory resources, an asynchronous messaging was established to send allocated buffers in response to local demand. The usage of resources in each {{{LocalMemPool}}} is accounted with a score to enable heuristic clean-up of rarely used memory blocks, sending them back to the central {{{EngineBufferManager}}} through a message queue. The reworked OutputSlot has been integrated seamlessly into this framework, and a preliminary load test demonstrates safe operation under pressure.
 
 !Decisions
 ;Scheduler
@@ -7100,7 +7112,7 @@ Prerequisite data for the media calculations can be considered just part of that
 * all it needs to know is the ''effective nominal time'' and an ''invocation instance ID''
 
-
+
At architecure level, the render process is sufficiently characterized by the ''pull principle'', regarding dependencies, processing function and the [[»Render Node operation protocol«|NodeOperationProtocol]]. Yet at the level of actual operations and interactions, several intricate details become relevant, especially //how to manage the buffers// and where to provide additional storage for working data.
 {{red{⚠ In-depth rework underway as of 12/2024...}}}
 ^^┅┅┅┅┅┅the following text is ''superseded''┅┅┅┅┅┅┅┅┅^^
@@ -7118,7 +7130,7 @@ On the other hand, the processing function within the individual node needs to b
 Not everything can be preconfigured though. The pull principle opens the possibility for the node to decide on a per call base what predecessor(s) to pull (if any). This decision may rely on automation parameters, which thus need to be accessible prior to requesting the buffer(s). Additionally, in a later version we plan to have the node network calculate some control values for adjusting the cache and backend timings &mdash; and of course at some point we'll want to utilize the GPU, resulting in the need to feed data from our processing buffers into some texture representation.
 
 !buffer management
-{{red{NOTE 9/11: the following is partially obsolete and needs to be rewritten}}} &rarr; see the BufferTable for details regarding new buffer management...
+{{red{NOTE 9/11: the following is partially obsolete and needs to be rewritten}}} &rarr; see the BufferProvider for details regarding new buffer management...
 
 ...
 
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index 537eb8dc3..136bb0c5c 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -116089,6 +116089,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
+
+
+
+
 
 
 
@@ -116110,6 +116114,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
+
+
+
+
 
 
 
@@ -117383,7 +117391,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
   
 
 
-
+
 
 
 
@@ -120562,7 +120570,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
-
+
 
 
 
@@ -124772,6 +124780,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
+
+
+
 
 
 
@@ -143681,12 +143692,12 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
-
+
 
-
-
-
 
+
+
+
 
 
 
@@ -143759,11 +143770,47 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
 
 
 
+
+
+  
+  
+    

+ Aufbauend auf die Einsichten aus der Design-Arbeit zu »BufferProvider Protocol« und »OutputSlot Protocol« habe ich nun eine Gesamt-Implementierung entworfen für den Betrieb in einem massive-concurrent-Environment. Dabei hatte ich zunächst die Idee weiterverfolgt, die Buffer-Metadaten auszubauen zu einem Informationsträger zur Koordinierung der internen Verwaltung. Nach erheblichen gedanklichen Mühen bin ich aber zu dem Schluß gekommen, daß diese Idee mehr Komplikationen und Komplexitäten schafft, als Klarheit stiftet. Damit bleiben die Buffer-Metadaten vorerst berschränkt auf den Buffer-Typ (+Typ-Handler), sowie das State-Tracking. +

+

+ Außerdem hat sich nun die Frage gestellt bezüglich Austausch über Thread-Grenzen hinweg. Ich habe nun generell dieses Buffer-Management organisiert als eine Menge von thread-lokalen Services, die jeweils mit einem zentralen back-End-Service kommunizieren. Im Falle der BufferMetadaten sind das nun die LocalBufferStage (Implementierung), die das jeweilige LocalSlice<BufferMetadata> trägt, sowie der zentrale Setvice EngineBufferMetadata. Es kommt dabei zu einer up/down-Synchronisation an zwei wohldefinierten Punkten — und in diesem Fall genügt mir (vorerst) ein globales Locking der EngineBufferMetadata, da ich diese SItuation als selten einschätze.  +

+

+ +

+

+ Die Allokationen sind somit komplett getrennt von den Buffer-Metadaten. Sie werden minimal dargestellt als {Buffer*, size_t} und in dieser Form über asynchrone Queues verschickt +

+ +
+ +
+ + + + + + + + + + + + + + + + @@ -143916,7 +143963,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -143925,13 +143972,70 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+ + + + + +

+ ...nach Aufbau der production-Grade-Implementierung des BufferProviders; hatte da eine lange (schmerzhafte) Diskussion mit mir selber geführt, und bin zu dem Schluß gekommen, das Thema Allokation doch komplett in-sich abgeschlossen aufzubauen +

+ +
+
+ + + +
- + + + + + +

+ Für jeden neuen Output-Vorgang wird ein Satz Connection-Objekte erstellt, und für jedes von ihnen ein Buffer-Typ, mit eingebettem Pointer im LocalTag. Diese Buffer-Typen werden massiv verwendet, für jeden Frame, solange dieser Output läuft (Buffer-Lifecycle). Hierfür propagieren sie auch zunächst vom Session-Thread in den globalen Store, und von dort in die jeweiligen thread-local BufferMetadata. +

+

+ +

+

+ Aber nachdem diese Connection geschlossen wird, liegen diese N typen weiterhin in den verteilten BufferMetadata-Instanzen herum +

+ +
+ + + + + +

+ ...aber bei diesen kann man davon ausgehen, daß sich das irgendwann einpendelt... +

+ +
+ +
+ + + + + + + + +
+
+
+ + + +
@@ -143951,7 +144055,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -143960,7 +144064,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

- +
@@ -144000,14 +144104,14 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- - + +
- - + + @@ -144070,8 +144174,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + @@ -144181,13 +144286,14 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + - + @@ -144207,8 +144313,19 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+ + + + +

+ Bisher liegt der TypeHandler als Value-Objekt in den Metadaten, wird also kopiert zur Replikation; das würde mit einer Funktor-Identität als Objekt-Identität kollidieren +

+ +
+
- + + @@ -144394,8 +144511,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- - + + @@ -144414,7 +144531,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144587,7 +144704,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144771,7 +144888,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144861,7 +144978,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -145739,7 +145856,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + @@ -145830,7 +145947,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -145844,7 +145961,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + @@ -145868,11 +145987,369 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + + + + + + +

+ damit meine ich eigentlich seine »Identität« (es ist ja ein ref-counting smart-Handle). Diese verborgene Identität ist tatsächlich der AllocState<CON> +

+ +
+
+ + + + + +

+ ...da es dieser ist, der letztlich einen Play-Prozeß aufbaut +

+ +
+
+ + + + +

+ ...d.h. Kopieren hat (geringfügige) Kosten und kann den andere Threads, die dieses Handle auch verwenden, ausbremsen. Aber an der Oberfläche bleibt das verborgen, und es handelt sich um einen einfachen Pointer) +

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

+ ABER beachte: CON-Implementation muß threadsafe sein +

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

+ zwar würden die Typen zunächst von einem Thread gemeinsam populiert +

+ +
+
+ + + + + +

+ im Besonderen werden sub-Typen hinzugefügt und wieder gelöscht +

+ +
+ +
+
+ + + + +

+ mit dem gleichen Argument, welches ich generell für den BufferManager mache: nach einem initialen Priming müßte jeder Worker alsbald alle Daten bei sich haben, und diese dann sehr viel häufiger einfach lokal lesen und modifizieren +

+ +
+
+
+
+ + + + +

+ Fazit: wir brauchen das gleiche Setup wie für den generischen BufferManager +

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

+ Also die übliche Konfiguration mit direkter Heap-Allokation, so wie das auch für das NaiveBufferSetup verwendet wird: +

+

+ return std::make_unique<vault::mem::SimpleBufferStateRegistry>("OutputBufferProxy"); +

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

+ siehe buffer-local-tag.hpp ⟶ ADL-friend hash_value(LocalTag) +

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

+ ...bleibt der AllocState<CON> am Leben, und damit sind die Adressen / Hashes stabil +

+ +
+
+ + + + + +

+ Denn... +

+
    +
  • + der Hash stimmt nur, wenn die Speicher-Adressse und Buffer-Größe exakt gleich ist +
  • +
  • + der konkrete Connection-Typ allerdings steckt in der Template-Instanz, d.h. im Code-Segment +
  • +
  • + deshalb funktioniert auch ein anderer Connection-Typ damit korrekt, solange die Buffer-Größe paßt +
  • +
+ +
+ +
+
+ + + + +
+ + + +
+ + + + + + + + + + + + + +

+ Das ließe sich per Concept realisieren: wenn der Template-Parameter eine Connection-Subclass ist, greift die Spezialisierung und generiert eine default-Setup-Policy, andernfalls muß der Template-Parameter selber die Policy sein, und diese hat dann eine embedded typedef Connection +

+ +
+
+ + + + +

+ ...diese Variante hätte den Charme daß am bestehenden Code fast nichts geändert werden muß, und man den Test-Schalter normalerweise gar nicht sieht +

+ +
+ +
+ + + + +

+ ...in diesem Fall müßte auch OutputBufferProxy per Konstruktor-Parameter gesteuert werden (also dynamisch); weiterhin sehe ich als problematisch daß der Konstruktor bereits einen relativ anspruchstvollen 2. Parameter nimmt +

+ +
+
+ + + + +

+ ...die default-Implementierung macht nix, aber in speziellen Fällen kann man für einen bestimmten Connection-Typ in das ssetup eingreifen +

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

+ ...weil AllocState eine protected-Definition ist (und auch bleiben soll, um klar zu machen, daß das Interna sind) +

+

+ .....alle normalen Verwendungen sollten ja durch OutputSlot::allocate() gehen; dort kann ich aber sinnvollerweise keinen weiteren Template-Parameter mit Default unterbringen (weil der 2.Parameter, die Populator-Funktion, stets inferiert werden soll) +

+ +
+
+ + + + +

+ aktuell haben wir nur den DiagnosticOutputSlot, sowie den OutputAllocState_test +

+

+ ...und ich erwarte nicht, daß da noch viel dazukommt +

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

+ Die vorgefertigten Populator-Builder in lib::ScopedCollection sind nested static functions, da sie implizit Bezug nehmen auf den Payload-Typ und den ElementHolder — das ist schon normalerweise eine Komplikation, die sich aber i.d.R. umgehen läßt, indem man vorher im Nutz-Kontext eine Typedef einführt für die lib::ScopedCollection<Payload>. Typisches Besipiel: +

+
+
+

+ // Start a collection of workers.... +

+

+ using Threads = ScopedCollection<DummyWorkerThread<>>; +

+

+ Threads threads{MANY_THREADS,  Threads::fill()}; +

+

+ +

+
+
+ +
+
+ + + + +

+ Der wichtigste Fall ist sicherlich ScopedCollection<CON>::fill(ARGS...) +

+

+ Einen solchen Overload könnte man einführen, wenn der andere Overload durch ein Concept geschützt wäre +

+ +
+ + + + + + +

+ ...denn dort kann ich die Populator-Builder direkt verwenden; anstatt auf den »Populator« mit einem std::invocable-Concept abzustellen, könnte ich stattdessen den Overload für die direkte Konstruktion guarden +

+ +
+
+
+ + + +
+ + +
@@ -146364,7 +146841,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + @@ -146409,9 +146886,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -146869,6 +147347,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + @@ -149129,6 +149608,30 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + +

+ Das war ein massives Stück Arbeit — hat auch viel länger gebraucht als unrsprünglich geplant. +

+

+ Dafür habe ich nun bereits einen Prototypen für das Buffer-Management, das meinem Erfahrungs-Stand bzgl. performant/paralleller Systeme entspricht. Dieser Prototyp ist sicherlich noch nicht ausgereift und hat erhebliches Optimierungs-Potential. Zudem habe ich das gesamte Thema »Allokation« komplett übersprungen (und mache einfach Heap-Allokationen im EngineBufferManager) +

+ +
+ +
+
@@ -149210,8 +149713,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -149231,7 +149734,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -149814,6 +150317,17 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

+ mit dem Aufbau der production-grade-Implementierung des BufferProviders mußte ich auch das Problem lösen, wie die einzelnen Threads auf die Metadaten zugreifen: es gibt nun EngineMetadata, sowie in jedem Thread ein LocalSllice<BufferMetadata>..... +

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

- Concepts sind ein relativ komplexes Gebilde, und sie haben eine spezielle Notierung, die in der Form nur in C++ gegeben ist. Nur ein Bruchteil der C++ - Entwickler wir mit Konzepten vertraut werden (auf lange Zeit). Zudem hinterläßt ein Konzept in der Implementierung keine Spuren. Man könnte sich da zwar etwas einfallen lassen, aber das wäre dann noch mehr ein Spezial-Konstrukt. Das ist etwas, was sich erst noch herausbilden muß (auch die Tool-Unterstützung ist ein Aspekt). + Concepts sind ein relativ komplexes Gebilde, und sie haben eine spezielle Notierung, die in der Form nur in C++ gegeben ist. Nur ein Bruchteil der C++ - Entwickler wird mit Konzepten vertraut werden (auf lange Zeit). Zudem hinterläßt ein Konzept in der Implementierung keine Spuren. Man könnte sich da zwar etwas einfallen lassen, aber das wäre dann noch mehr ein Spezial-Konstrukt. Das ist etwas, was sich erst noch herausbilden muß (auch die Tool-Unterstützung ist ein Aspekt).

Demgegenüber ist ein klassisches OO-Interface ein seit 30 Jahren universell bekanntes und weithin unterstütztes Schema, das in den allermeisten Sprachen in irgend einer Form eingesetzt wird (selbst wenn die Sprachen sonst nicht OO sind). Wenn ich einem durchschnittlichen Entwickler heute sage: „Du mußt das Interface Connection implementieren“, dann setzt der sich einfach hin und klopft das runter. @@ -151233,7 +151747,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -151245,6 +151759,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + @@ -179885,7 +180403,7 @@ std::cout << tmpl.render({"what", "World"}) << s - + @@ -181857,6 +182375,9 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo

+ + +