From 0178e11e6540ab50888a451219701c3adb0f6c82 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 13 Mar 2026 22:35:46 +0100 Subject: [PATCH] BufferProvider: simplify OutputSlot implementation structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old implementation relied on two layered APIs: one for the client and one for the implementer of the Allocation state. This is logically sound, yet clumsy in terms of design, since the internal API turns out to be void of actual meaning. It seems possible to eliminate this confusing structure by a clear separation of ''implementation realms'' * the ''mechanics'' of connection state management * the actual implementation of the connection The first can be provided as generic template subclass, while the latter should be confined entirely into the implementation of the `Connection` interface. Doing so requires to keep each part of the implementation strictly focused onto its purpose — something I obviously was not able to see at the time when writing this old implementation, notably because I was completely guided and driven by the test/dummy implementation without considering how an actual implementation of low-level data output would work. --- src/lib/scoped-collection.hpp | 34 +++--- src/vault/out/diagnostic-output-slot.hpp | 32 +++-- src/vault/out/output-slot-connection.hpp | 109 ++++++----------- src/vault/out/output-slot.hpp | 54 ++++----- tests/lib/scoped-collection-test.cpp | 6 +- tests/stage/test/test-nexus.hpp | 3 + wiki/thinkPad.ichthyo.mm | 148 ++++++++++++++++++++++- 7 files changed, 242 insertions(+), 144 deletions(-) diff --git a/src/lib/scoped-collection.hpp b/src/lib/scoped-collection.hpp index bc23b3033..a42948bb7 100644 --- a/src/lib/scoped-collection.hpp +++ b/src/lib/scoped-collection.hpp @@ -12,44 +12,46 @@ */ /** @file scoped-collection.hpp - ** Managing a collection of non-copyable polymorphic objects in compact storage. - ** This helper supports the frequently encountered situation where a service - ** implementation internally manages a collection of implementation related - ** sub-components with reference semantics. Typically, those objects are - ** being used polymorphically, and often they are also added step by step. - ** The storage holding all those child objects is allocated in one chunk - ** and never adjusted. + ** Manage a collection of non-copyable polymorphic objects in compact storage. + ** This helper supports common situation where a service implementation has to + ** manages a fixed collection of implementation related sub-components, treated + ** with reference semantics. Typically, those objects are used polymorphically, + ** and often even with _type erasure_ and added step by step through a builder. + ** The storage to hold all those child objects is allocated in one chunk + ** and never adjusted. Individual children are never re-ordered or removed, + ** only the collection as a whole is discarded eventually. ** ** # usage patterns ** ** The common ground for all usage of this container is to hold some elements ** with exclusive ownership; when the enclosing container goes out of scope, ** all the dtors of the embedded objects will be invoked. Frequently this - ** side effect is the reason for using the container: we want to own some - ** resource handles to be available exactly as long as the managing object - ** needs and accesses them. + ** side effect is one of the reasons for using the container: we want to own + ** some resource handles and be sure they are available exactly as long as + ** the managing scope is alive and able to access them. ** ** There are two different usage patterns for populating a ScopedCollection ** - the "stack style" usage creates an empty container (using the one arg - ** ctor just to specify the maximum size). The storage to hold up to this - ** number of objects is (heap) allocated right away, but no objects are + ** ctor to specify the maximum size only). The storage capacity to hold + ** a number of objects, up to the predefined maximum, is (heap) allocated + ** immediately at construction of the container, yet no child objects are ** created. Later on, individual objects are "pushed" into the collection ** by invoking #emplaceElement() to create a new element of the default ** type `I`) or #emplace(args) to create some subtype. This way, - ** the container is being filled successively. + ** the container is gradually filled up. ** - the "RAII style" usage strives to create all of the content objects ** right away, immediately after the memory allocation. This usage pattern ** avoids any kind of "lifecycle state". Either the container comes up sane ** and fully populated, or the ctor call fails and any already created ** objects are discarded. ** @note intentionally there is no operation to discard individual objects, - ** all you can do is to #clear() the whole container. + ** the only option is to #clear() the whole container. ** @note the container can hold instances of a subclass of the type defined ** by the template parameter `I`. But you need to ensure in this case ** that the defined buffer size for each element (2nt template parameter) ** is sufficient to hold any of these subclass instances. This condition ** is protected by a static assertion (compilation failure). - ** @warning when using subclasses, a virtual dtor is mandatory + ** @warning when using subclasses, defining a virtual dtor is mandatory! ** @warning deliberately **not threadsafe** ** ** @see ScopedCollection_test @@ -160,7 +162,7 @@ namespace lib { * Ctor fails in case of any error during element creation. * @param builder functor to be invoked for each "slot". * It gets an ElementHolder& as parameter, and should - * use this to create an object of some I-subclass + * use this to create an object of suitable I-subclass */ template ScopedCollection (size_t maxElements, CTOR builder) diff --git a/src/vault/out/diagnostic-output-slot.hpp b/src/vault/out/diagnostic-output-slot.hpp index 262c656ca..f4558986a 100644 --- a/src/vault/out/diagnostic-output-slot.hpp +++ b/src/vault/out/diagnostic-output-slot.hpp @@ -342,7 +342,7 @@ namespace out { * remains in memory until shutdown of the current executable */ class DiagnosticOutputSlot - : public OutputSlotImplBase + : public OutputSlot { static const uint MAX_CHANNELS = 5; ///////////////////////////////OOO Build heap-based implementation with arbitrary number of feeds!! @@ -363,30 +363,27 @@ namespace out { * thus allowing to verify the handling of individual buffers */ class SimulatedOutputSequences - : public AllocStateHub + : public AllocState { - typedef AllocStateHub _Base; - - void - buildConnection(ConnectionStorage storage) - { -#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to disentangle BufferProvider implementation - storage.create(); -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code - } + using _Base = AllocState; public: SimulatedOutputSequences (uint numChannels) - : _Base(numChannels) - { - init(); - } + : _Base{ numChannels + ,[](ConnectionStorage& storage) + { + UNIMPLEMENTED ("new Tracking implementation for DiagnosticOutputSlot"); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to disentangle BufferProvider implementation + storage.create(); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code + }} + { } }; /** hook into the OutputSlot frontend */ - unique_ptr + unique_ptr buildState() { return make_unique ( @@ -402,7 +399,8 @@ namespace out { { REQUIRE (!isFree(), "diagnostic OutputSlot not (yet) connected"); REQUIRE (channel <= getOutputChannelCount()); - return static_cast (state_->access (channel)); +// return static_cast (state_->access (channel)); + UNIMPLEMENTED ("the old tracking API is abandoned, the new one not yet implemented"); } diff --git a/src/vault/out/output-slot-connection.hpp b/src/vault/out/output-slot-connection.hpp index d00209429..990e43dee 100644 --- a/src/vault/out/output-slot-connection.hpp +++ b/src/vault/out/output-slot-connection.hpp @@ -33,19 +33,20 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" #include "vault/out/output-slot.hpp" -#include "vault/mem/buffhandle.hpp" #include "lib/scoped-collection.hpp" #include "lib/iter-adapter-stl.hpp" #include "lib/iter-source.hpp" #include "lib/handle.hpp" -//#include "lib/time/timevalue.hpp" +#include "lib/time/timevalue.hpp" //#include "steam/engine/buffer-provider.hpp" //#include "steam/play/timings.hpp" //#include "lib/sync.hpp" +#include "vault/mem/buffer-provider.hpp" //#include #include -#include +#include +//#include //#include @@ -53,8 +54,8 @@ namespace vault { namespace out { using vault::mem::BuffHandle; -//using vault::mem::BufferProvider; -//using lib::time::Time; + using vault::mem::BufferProvider; + using lib::time::TimeValue; /////////////////OOO Rly? //using std::string; using lib::iter_stl::eachElm; @@ -102,61 +103,29 @@ namespace out { - /** - * Extension point for Implementation. - * The AllocState is where the concrete output - * handling implementation is expected to reside. - * OutputSlot is a frontend and accesses - * AllocState in the way of a PImpl. - */ - class OutputSlot::AllocState - : public OutputSlot::Allocation - , util::NonCopyable - { - public: - virtual ~AllocState() { } - - virtual Connection& access (uint) const =0; - }; - - - - /* ===== Building blocks for OutputSlot implementation ===== */ - /** Base for OutputSlot standard implementation */ - class OutputSlotImplBase - : public OutputSlot - { - protected: - template - class AllocStateHub; - }; - - /** - * Maintaining a list of active connections. - * Base class for the typical implementation approach. - * Using this class is \em not mandatory. But obviously, - * we'd get to manage a selection of Connection objects - * representing the "active points" in several media channels - * connected through this OutputSlot. These Connection subclasses - * are what is referenced by the DataSink smart-ptrs handed out - * to the client code. As AllocState implements the Allocation - * API, it has the liability to create these DataSink smart-ptrs, - * which means to wire them appropriately and also provide an - * deleter function (here #shutdownConnection) to be invoked - * when the last copy of the smart-handle goes out of scope. - * - * The typical standard/base implementation provided here - * manages a collection of active Connection subclass objects. + * Implementation of active Allocation state: maintaining + * a list of active connections. An instance is created whenever + * a client _locks_ (and thereby »allocates«) the OutputSlot. The + * actual implementation of OutputSlot::buildState() will employ a + * custom Connection, where all the actual state handling and data + * transport is implemented. Such a Connection subclass is also what + * is referenced by the DataSink smart-hendle, that is handed out + * to the client code and allows to mark the state transitions. + * 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. The deleter function #shutdownConnection is + * is invoked for each abandoned connection; once all connections + * are defunct, the AllocState itself can be deconfigured. */ template - class OutputSlotImplBase::AllocStateHub - : public OutputSlot::AllocState + class OutputSlot::AllocState + : public OutputSlot::Allocation { - typedef lib::ScopedCollection Connections; - typedef OutputSlot::OpenedSinks OpenedSinks; + using Connections = lib::ScopedCollection; + using OpenedSinks = OutputSlot::OpenedSinks; Connections connections_; @@ -191,32 +160,22 @@ namespace out { protected: /* == API for OutputSlot-Impl == */ - void - init() ///< derived classes need to invoke this to build the actual connections - { - //////////////////////////TICKET #878 really build all at once? or on demand? - connections_.populate_by (&AllocStateHub::buildConnection, this); - } - using ConnectionStorage = Connections::ElementHolder&; + using ConnectionStorage = Connections::ElementHolder; - /** factory function to build the actual - * connection handling objects per channel */ - virtual void buildConnection(ConnectionStorage) =0; - - - AllocStateHub(uint numChannels) - : connections_(numChannels) - { } - - public: - virtual - ~AllocStateHub() + /** + * The actual implementation of OutputSlot::buildState() needs to build + * an AllocState instance, and has to provide a _population functor_ + * to setup the actual \a CON (Connection implementation) instances. + * @see ScopedCollection_test::building_RAII_Style() + */ + template + AllocState(uint numChannels, CTOR&& populator) + : connections_(numChannels, std::forward(populator)) { } - - private: // Implementation details + private: /* === Implementation details === */ static DataSink connectOutputSink (CON& connection) diff --git a/src/vault/out/output-slot.hpp b/src/vault/out/output-slot.hpp index 38cba0e1d..16040df4b 100644 --- a/src/vault/out/output-slot.hpp +++ b/src/vault/out/output-slot.hpp @@ -59,11 +59,11 @@ #include "lib/error.hpp" #include "lib/handle.hpp" -#include "lib/time/timevalue.hpp" -#include "vault/mem/buffer-provider.hpp" +#include "vault/mem/buffhandle.hpp" #include "vault/out/timings.hpp" #include "lib/iter-source.hpp" #include "lib/nocopy.hpp" +#include "lib/util.hpp" #include @@ -72,11 +72,7 @@ namespace vault { namespace out { using vault::mem::BuffHandle; - using vault::mem::BufferProvider; using lib::time::FrameCnt; - using lib::time::TimeValue; - using lib::time::Time; - using std::unique_ptr; @@ -93,28 +89,15 @@ namespace out { * channels to some kind of external sink (video in GUI window, video full screen, * sound, Jack, rendering to file). Clients are expected to retrieve a suitably * preconfigured implementation from some OutputManager instance. An OutputSlot - * needs to be \em claimed for output by invoking #allocate, which returns a + * needs to be _claimed_ for output by invoking #allocate, which returns a * representation of the connection state. This operation is exclusive. - * The actual \link DataSink output sinks \endlink can be retrieved + * The actual [output sinks](\ref DataSink) can be retrieved * through the Allocation object returned from there. */ class OutputSlot : util::NonCopyable { - protected: - - /** active connections through this OutputSlot */ - class AllocState; - - unique_ptr state_; - - /** build the \em connected state, - * based on the existing configuration - * within this concrete OutputSlot */ - virtual unique_ptr buildState() =0; - - public: virtual ~OutputSlot(); @@ -128,13 +111,9 @@ namespace out { virtual Timings getTimingConstraints() =0; - protected: - ~Allocation(); ///< never to be managed by clients directly + ~Allocation(); ///< this is an interface }; - /** established output channel */ - class Connection; - /** can this OutputSlot be allocated? */ bool isFree() const; @@ -145,6 +124,22 @@ namespace out { /** disconnect from this OutputSlot * @warning may block until DataSinks are gone */ void disconnect(); + + /** established output feed */ + class Connection; + + + protected: + /** active connections through this OutputSlot */ + template + class AllocState; + + unique_ptr state_; + + /** build the _connected state,_ + * based on the existing configuration + * within this concrete OutputSlot */ + virtual unique_ptr buildState() =0; }; @@ -168,16 +163,11 @@ namespace out { { return not (sink1 and sink2) or (sink1 and sink2 - and & sink1.impl() == & sink2.impl()); - } - friend bool operator!= (DataSink const& sink1, DataSink const& sink2) - { - return not (sink1 == sink2); + and util::isSameObject (sink1.impl(), sink2.impl())); } }; - }} // namespace vault::out #endif /*VAULT_OUT_OUTPUT_SLOT_H*/ diff --git a/tests/lib/scoped-collection-test.cpp b/tests/lib/scoped-collection-test.cpp index f2f872fd5..d30e5fb9f 100644 --- a/tests/lib/scoped-collection-test.cpp +++ b/tests/lib/scoped-collection-test.cpp @@ -188,7 +188,7 @@ namespace test{ // Verify correct behaviour of iteration end - CHECK (! (coll.end())); + CHECK (not bool(coll.end())); CHECK (isnil (coll.end())); VERIFY_ERROR (ITER_EXHAUST, *coll.end() ); @@ -303,7 +303,7 @@ namespace test{ CollD coll (6, Populator(rr, trigger)); - CHECK (!isnil (coll)); + CHECK (not isnil (coll)); CHECK (6 == coll.size()); CHECK (0 != Dummy::checksum()); @@ -314,7 +314,7 @@ namespace test{ CHECK (coll[4].calc(0) == 4 + rr); CHECK (coll[5].calc(0) == 5 + rr + trigger); // what does this check prove? - // - the container was indeed populated with DubDummy objects + // - the container was indeed populated with SubDummy objects // since the overridden version of Dummy::acc() did run and // reveal the trigger value // - the population was indeed done with the anonymous Populator diff --git a/tests/stage/test/test-nexus.hpp b/tests/stage/test/test-nexus.hpp index d49bfca2e..83f22073d 100644 --- a/tests/stage/test/test-nexus.hpp +++ b/tests/stage/test/test-nexus.hpp @@ -38,6 +38,7 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" +#include "lib/symbol.hpp" #include "stage/ctrl/bus-term.hpp" #include "stage/model/tangible.hpp" #include "stage/ctrl/state-manager.hpp" @@ -49,6 +50,8 @@ #include +using lib::Symbol; + namespace stage { namespace test{ diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 2869b52c9..3379f9120 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -137463,6 +137463,131 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + + + + +

+ Interessant ist daran, daß ich das damals überahupt nicht gesehen habe; mein Level war also, daß ich zwar ziemlich direkt ein solches Protokoll in OO-Interfaces übersetzen konnte, aber das Denken in Zonen / Domänen und Perspektiven der Implementierung war mir noch fremd, ich konnte dies grade so ansatzweise erahnen, aber hab mich in der konkreten Implementierung nur von Haken zu Öse durchgehangelt... +

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

+ ...damit meine ich +

+
    +
  • + entweder die ganze Allokation (und damit die Verwendung des OutputSlot) ist praktisch einheitlich; es werden zwar N Buffer zugewiesen, aber der Transfer-Mechanismus läuft für alle gemeinsam +
  • +
  • + oder die Allokation ist lediglich eine logische Klammer, und die eigentliche Implementierung ist N-fach aufgedoppelt, für jeden einzelnen Feed +
  • +
+ +
+ +
+
+
+ + + + + +

+ virtual Connection& access  (uint) const  +

+

+ +

+

+ ...natürlich wird die verwendet, aber irgendwo aus der konkreten Implementierung (DiagnosticOutputSlot) heraus. Das ist ein telltale sign — natürlich sollte eine solche Interface-Methode verwendet werden, aber die Verwendung einer Methode sollte direkt vom Interface aus offensichtlich sein, ohne in die Implementierung schauen zu müssen. Das ist hier nicht der Fall; warum sollte ein konkreter OutputSlot von oben herab auf einzelnen Connections zugreifen müssen? Konkret habe ich das verwendet, um das ganze Diagnose-API darauf aufzubauen. Jaja, sowas nennt man »hexagonales Design« — Diagnose-State und Verifikationen sollte man niemals verwenden, um daran die Architektur auszurichten. +

+ +
+
+ + + + +

+ und dafür wird aber die VTable »durchgefädelt« ... erst eine davon abgeleitete konkrete Implementierung bietet dann die Factory-Methode, die den Typ tatsächlich festlegt +

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

+ ....oder war das mit der ScopedCollection ein späterer retro-fit? +

+

+ Jedenfalls halte ich das aus heutiger Sicht immer noch für eine gute Lösung; man könnte höchtstens die ScopedCollection noch etwas ausbauen (Policy für die Storage? UninitialisedStorage für die Impl verwenden?) +

+ +
+
+ + + + +

+ eins ist klar: Lambdas sind schön +

+ +
+ +
+
+
+ + + + + + + + +
+
@@ -168295,7 +168420,7 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo - + @@ -168345,6 +168470,21 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo + + + + + + + +

+ möglicherweise weil ich eine Version aus Debian/stable verwende? +

+ +
+
+ +
@@ -205197,6 +205337,12 @@ env.Command('out.bin', 'in.bin', my_action)
+ + + + + +