diff --git a/src/steam/engine/engine-service.cpp b/src/steam/engine/engine-service.cpp index 7692c0e95..7a9dee174 100644 --- a/src/steam/engine/engine-service.cpp +++ b/src/steam/engine/engine-service.cpp @@ -69,14 +69,14 @@ namespace engine{ CalcStreams EngineService::calculate(ModelPort mPort, Timings nominalTimings, - OutputConnection& output, + OutputSlot& outputSlot, Quality serviceQuality) { //////////////////////////////////////////////////TICKET #1301 : prepare proper Dispatcher here, including translation ModelPort -> portIDX RenderEnvironment& renderConfig = configureCalculation (mPort,nominalTimings,serviceQuality); function triggerRenderStart = bind (activateCalculation, _1, ref(renderConfig)); CalcStreams runningCalculations; - append_all (transform (output.getOpenedSinks() + append_all (transform (outputSlot.getOpenedSinks() ,triggerRenderStart ) ,runningCalculations); diff --git a/src/steam/engine/engine-service.hpp b/src/steam/engine/engine-service.hpp index 4764ffd27..dd4c77de3 100644 --- a/src/steam/engine/engine-service.hpp +++ b/src/steam/engine/engine-service.hpp @@ -73,7 +73,7 @@ namespace engine{ using mobject::ModelPort; using vault::out::Timings; - using OutputConnection = vault::out::OutputSlot::Allocation; + using vault::out::OutputSlot; @@ -141,7 +141,7 @@ namespace engine{ CalcStreams calculate(ModelPort mPort, Timings nominalTimings, - OutputConnection& output, + OutputSlot& outputSlot, Quality serviceQuality =QoS_DEFAULT); CalcStreams diff --git a/src/steam/play/render-configurator.cpp b/src/steam/play/render-configurator.cpp index e1fe77104..bb600e6f8 100644 --- a/src/steam/play/render-configurator.cpp +++ b/src/steam/play/render-configurator.cpp @@ -68,7 +68,7 @@ namespace play { { REQUIRE (outputResolver_); OutputSlot& slot = outputResolver_->getOutputFor (port); - if (!slot.isFree()) + if (not slot.isActive()) throw error::State{"unable to acquire a suitable output slot" /////////////////////TICKET #197 #816 --- could use util::_Fmt here , LERR_(CANT_PLAY)}; return slot; @@ -78,16 +78,16 @@ namespace play { engine::CalcStreams buildCalculationStreams (ModelPort port, OutputSlot& output) { - OutputSlot::Allocation& activeOutputConnection = output.allocate(); - Timings nominalTimings = activeOutputConnection.timingConstraints() - .constrainedBy(playbackTimings_); + Timings nominalTimings = output.timingConstraints() + .constrainedBy(playbackTimings_); - return activateEngine (port, nominalTimings, activeOutputConnection, renderQuality_); + //////////////////////////////////////////TICKET #878 consider to store a copy of the OutputSlot (≙ref-couting handle) somewhere in a »PlayProcess« to keep it alive + return activateEngine (port, nominalTimings, output, renderQuality_); } engine::CalcStreams - activateEngine (ModelPort port, Timings timings, OutputSlot::Allocation& activeOutputConnection, RenderQuality quality) + activateEngine (ModelPort port, Timings timings, OutputSlot& activeOutputConnection, RenderQuality quality) { return EngineService::instance().calculate (port, timings, activeOutputConnection, quality); } diff --git a/src/vault/out/diagnostic-output-slot.hpp b/src/vault/out/diagnostic-output-slot.hpp index f4558986a..fc96931fa 100644 --- a/src/vault/out/diagnostic-output-slot.hpp +++ b/src/vault/out/diagnostic-output-slot.hpp @@ -397,15 +397,19 @@ namespace out { TrackingInMemoryBlockSequence const& accessSequence (uint channel) { +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to disentangle BufferProvider implementation REQUIRE (!isFree(), "diagnostic OutputSlot not (yet) connected"); REQUIRE (channel <= getOutputChannelCount()); // return static_cast (state_->access (channel)); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code UNIMPLEMENTED ("the old tracking API is abandoned, the new one not yet implemented"); } public: - DiagnosticOutputSlot() { } + DiagnosticOutputSlot() + : OutputSlot(unique_ptr{nullptr}) //////////////////////OOO Kabooooooom!! --- should actually configure a suitable Allcation subclass here + { } diff --git a/src/vault/out/output-slot-connection.hpp b/src/vault/out/output-slot-connection.hpp index 66e75eee7..a5aafd60f 100644 --- a/src/vault/out/output-slot-connection.hpp +++ b/src/vault/out/output-slot-connection.hpp @@ -71,9 +71,9 @@ namespace out { /** @internal represents the \em active * point in each of the per-channel connections * used when this OutputSlot is operational. - * + * * # OutputSlot Core API - * + * * Actually, this extension point towards the implementation * of the actual output handling carries the core API of OutputSlot. * Thus, the task of actually implementing an OutputSlot boils down @@ -127,40 +127,29 @@ namespace out { { using Connections = lib::ScopedCollection; using OpenedSinks = OutputSlot::OpenedSinks; + using BufferProxy = OutputBufferProxy; Connections connections_; - - OutputBufferProxy proxyBufferProvider_; - + BufferProxy bufferProxy_; /* == Allocation Interface == */ - OpenedSinks - getOpenedSinks() + void + release() override { - //////////////////////////TICKET #878 not re-entrant, lifecycle isn't clear - REQUIRE (this->isActive()); - return lib::iter_source::transform (eachElm(connections_), connectOutputSink); + TODO ("propagate closing of output"); } Timings - timingConstraints() - { - UNIMPLEMENTED ("find out about timing constraints"); //////////////////////////TICKET #831 + getTimings() override + { //////////////////TODO somehow configure the timings into this implementation object + UNIMPLEMENTED ("a lot regarding the Timings is still not clear..."); //////////////////////////TICKET #831 } - bool - isActive() const - { - return 0 < connections_.size(); - } - CON& - access (uint chanNr) const - { - return connections_[chanNr]; - } + /* == Callback API for BufferProxy == */ + protected: /* == API for OutputSlot-Impl == */ @@ -176,7 +165,7 @@ namespace out { template AllocState(uint numChannels, CTOR&& populator) : connections_(numChannels, std::forward(populator)) - , proxyBufferProvider_{*this} + , bufferProxy_{*this} { } @@ -187,9 +176,9 @@ namespace out { { TRACE (test, "activating Con=%p", &connection ); - DataSink newSink; - newSink.activate(&connection, shutdownConnection); - return newSink; +// DataSink newSink; +// newSink.activate(&connection, shutdownConnection); +// return newSink; } static void diff --git a/src/vault/out/output-slot.cpp b/src/vault/out/output-slot.cpp index 34e1da1d6..2e60c619b 100644 --- a/src/vault/out/output-slot.cpp +++ b/src/vault/out/output-slot.cpp @@ -34,68 +34,61 @@ namespace out { - OutputSlot::~OutputSlot() { } // emit VTables here.... - + // emit VTables here.... OutputSlot::Allocation::~Allocation() { } - OutputSlot::Connection::~Connection() { } - - - /** whether this output slot is occupied - * @return true if currently unconnected and - * able to connect and handle output data + /** + * @internal setup of the OutputSlot and the PImpl (Allocation) for active connected state. + * Some wiring must be carried out here, so that the ref-count based lifecycle management + * works as desired. All customisation for some specific kind of output mechanism and + * the corresponding OutputSlot::Connection implementation is embedded into the actual + * type of the PImpl, i.e. the Allocation subclass passed in as argument. The OutputSlot, + * together with the DataSink handles, will take shared ownership of the connection setup. */ - bool - OutputSlot::isFree() const + shared_ptr + OutputSlot::connect (unique_ptr allocation) { - return not this->state_; + REQUIRE (allocation); + return {allocation.release() + ,[](Allocation* allo) {// disconnection call-back + REQUIRE(allo); + allo->release(); + delete allo; + }}; } + - /** claim this OutputSlot for active use as output sink(s). - * At any point, a given slot can only be used for a single - * ongoing output process (which may serve several channels though). - * The assumption is for the OutputSlot to be picked through a query - * to some OutputManater, so the parameters (resolution, sample rate...) - * should be suited for the intended use. Thus no additional configuration - * is necessary. - * @return Allocation representing the "connected state" from the client's POV. - * The client may retrieve the effectively required Timings from there, - * as well as the actual output sinks, ready for use. - * @remarks calls back into #buildState, where the concrete OutputSlot - * is expected to provide a private Connection implementation, - * subclassing OutputSlot::Allocation + /** + * Entrance point: retrieve a sequence of actual DataSink handles, one for each data feed. + * Typically, these _sink handles_ will be propagated to some calculation stream and further + * embedded into render job definitions, so that the render invocation can use the sink handle + * to get a buffer for each frame. + * @return an iterator that produces DataSink objects; these are ref-counting handles + * and also functor objects with the signature `BuffHandle(FrameID)`, where the + * FrameID is defined in relation to the _frame grid_ defined by the Timings. */ - OutputSlot::Allocation& - OutputSlot::allocate() + OutputSlot::OpenedSinks + OutputSlot::getOpenedSinks() { - if (not isFree()) - throw err::Logic ("Attempt to open/allocate an OutputSlot already in use."); - state_ = this->buildState(); - return *state_; } - void - OutputSlot::disconnect() + Timings + OutputSlot::timingConstraints() { - if (not isFree()) - state_.reset(0); + REQUIRE(alloc_); + return alloc_->getTimings(); } /* === DataSink frontend === */ - BuffHandle - DataSink::lockBufferFor(FrameID frameNr) - { - return impl().claimBufferFor(frameNr); - } diff --git a/src/vault/out/output-slot.hpp b/src/vault/out/output-slot.hpp index 8423a57f9..38256edc7 100644 --- a/src/vault/out/output-slot.hpp +++ b/src/vault/out/output-slot.hpp @@ -21,33 +21,44 @@ ** ** Each OutputSlot is an unique and distinguishable entity. It corresponds explicitly to an ** external output, or a group of such outputs (e.g. left and right sound card output channels), - ** or an output file or similar capability accepting media content. Initially, an output slot - ** needs to be provided, configured and registered, using an implementation suitable for the - ** kind of media data to be sent (sound, video) and also suitable for the special circumstances - ** of the output capability (render a file, display video in a GUI widget, send video to some - ** full screen display, establish a Jack port, just use some kind of "sound out"). An output - ** slot is always limited to a single kind of media, and to a single connection unit, but - ** this connection may still be comprised of multiple channels - ** (e.g. stereoscopic video, multichannel sound). + ** or an output file or similar capability accepting media content. The setup for an OutputSlot + ** is provided, configured and registered with an OutputManager, using an implementation suitable + ** for the kind of media data to be sent (sound, video) and also suitable for the desired kind if + ** output capability (render to file, display video in a GUI widget, send video to a full screen + ** display, establish a Jack port or use any kind of "sound out"). Thus it is always limited to + ** a single kind of media, and to a single connection unit, but this connection may still comprise + ** several data feeds (e.g. stereoscopic video, multichannel sound). The number of such feeds + ** and the expected data format depends on the _media stream type_ used for configuring or + ** retrieving this OutputSlot. For example, a stereo sound output may expect only a single + ** data feed, where data blocks for both channels are sent in interleaved format. ** - ** In order to be usable as output sink, an output slot needs to be \em allocated: At any time, - ** there may be only a single client using a given output slot this way. To stress this point: - ** output slots don't provide any kind of inherent mixing capability; any adaptation, mixing, - ** overlaying and sharing needs to be done within the nodes network producing the output data - ** to be fed into the slot. (in special cases, some external output capabilities -- e.g. the - ** Jack audio connection system -- may still provide additional mixing capabilities, - ** but that's beyond the scope of the Lumiera application) + ** Once the client retrieves some OutputSlot, it was _allocated_ for exclusive use for that client; + ** At any time, there may be only a single client using a given output capability this way. It should + ** be noted thus that output slots don't provide any kind of inherent mixing capability; any adaptation, + ** mixing, overlaying and sharing needs to be done within the Render Node Network that produces the + ** data to be output. (As an aside, _some_ external output frameworks — e.g. the Jack audio connection + ** system — may still provide additional mixing capabilities, yet these are beyond the scope of the + ** Lumiera application) + ** + ** The OutputSlot object is a (copyable) front-end handle, that should be stored by the client and + ** discarded when further output is no longer required. From the OutputSlot, a set of actual DataSink + ** handles can be retrieved (one for each possible data feed). Starting from these handles, a two-phase + ** output protocol can be executed for each frame of data to send to the output. This includes obtaining + ** a BuffHandle, similar to what the BufferProvider exposes. The suitability of the buffer and the data + ** format is not validated in any way at that level, since the setup works under the assumption that + ** the stream type and further metadata has been validated or configured suitably at a higher level. + ** However, the OutputSlot exposes a definition of _timing constraints_ to describe when data must + ** be provided, and what deadline to observe for each frame. The client is required to comply to these + ** vault::out::Timings both when acquiring the BuffHandle for some frame and when _emitting_ data -- + ** any use outside these limits will cause that frame to be treated as a glitch. Furthermore, + ** the client is required to invoke BuffHandle::release() as soon as no further access to + ** the buffer is required (and after invoking `emit()`). Failure to do so will cause + ** a blocked buffer and typically leads to abort of the output and play process. + ** However, individual frames that miss the deadline are handled gracefully; + ** the actual implementation is expected to provide some kind of de-click + ** or de-flicker facility, which kicks in automatically whenever + ** a timing failure is detected. ** - ** Once allocated, the output slot returns a set of concrete sink handles (one for each - ** physical channel expecting data). The calculating process feeds its results into those handles. - ** Size and other characteristics of the data frames are assumed to be suitable, which typically - ** won't be verified at that level anymore. Besides that, the allocation of an output slot reveals - ** detailed timing expectations. The client is required to comply to these timings when _emitting_ - ** data -- they are even required to provide a current time specification, alongside with the data. - ** Based on this information, the output slot has the ability to handle timing failures gracefully; - ** the concrete output slot implementation is expected to provide some kind of de-click or - ** de-flicker facility, which kicks in automatically when a timing failure is detected. - ** ** @see OutputSlotProtocol_test ** @see diagnostic-output-slot.hpp */ @@ -66,6 +77,7 @@ #include "lib/util.hpp" #include +#include namespace vault { @@ -73,101 +85,114 @@ namespace out { using vault::mem::BuffHandle; using lib::time::FrameCnt; + using std::shared_ptr; using std::unique_ptr; - - class DataSink; - using FrameID = FrameCnt; + /** + * Handle to represent an opened connection ready to receive media data for output. + * Each DataSink (handle) corresponds to an OutputSlot::Connection entry. Data is + * published frame wise in a two-phase protocol: in the first stage, the client + * gets exclusive access to an output buffer. Once the data is ready for output, + * the client signals `emit()` and the buffer enters the second stage, where the + * intern output mechanism of that specific connection gets exclusive access. + * + * The DataSink handle is a _functor_ and can be invoked with a frame number, + * to start such a two-phase transaction for _this specific frame._ The result + * is a BuffHandle, to be used in the usual way as with a BufferProvider: + * - the client can access the buffer memory, either raw or with a forced cast + * - the client invokes BuffHandle::emit() to indicate that data is ready for output + * - once the client is done with that handle, it **must** invoke BuffHandle::release() + * Each output mechanism defines specific constraints regarding the time window when + * to get such a BuffHandle and when `emit()` must have been called. These constraints + * are indicated by the vault::out::Timings, that can be retrieved from the OutputSlot. + * + * @note DataSink embeds a ref-counting handle to detect automatically when some + * connection can be released; the _allocation of an OutputSlot_ is released + * once all connections were discarded. + */ + class DataSink + : public std::function + { }; + + /****************************************************************************//** * Interface: Generic output sink. * An OutputSlot represents the possibility to send data through multiple * 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 _claimed_ for output by invoking #allocate, which returns a - * representation of the connection state. This operation is exclusive. - * The actual [output sinks](\ref DataSink) can be retrieved - * through the Allocation object returned from there. + * preconfigured instance from some OutputManager. At that point, the given + * OutputSlot is already activated and reserved for exclusive use for the client. + * The further lifecycle is managed by ref-count, which implies that OutputSlot + * is a (copyable) front-end handle. For the actual output, the client has to + * [retrieve the DataSink handles](\ref getOpenedSinks()), one for each independent + * data feed associated with this OutputSlot. How many feeds this are, depends on + * the _media stream type_ for which this OutputSlot was configured. The retrieved + * DataSink handles also participate in the ref-count based connection state; once + * the OutputSlot and all DataSink handles are discarded, the output connection + * and all associated resources are released. + * @see DataSink for the next steps to perform on the handles for output + * in compliance with the »Output Slot protocol«. */ class OutputSlot - : util::NonCopyable { public: - virtual ~OutputSlot(); - using OpenedSinks = lib::IterSource::iterator; - class Allocation + OpenedSinks getOpenedSinks(); + Timings timingConstraints(); + + /** is this OutputSlot allocated/activated? */ + bool + isActive() const { - public: - virtual OpenedSinks getOpenedSinks() =0; - virtual Timings timingConstraints() =0; - - virtual bool isActive() const =0; - - ~Allocation(); ///< this is an interface - }; - - - /** can this OutputSlot be allocated? */ - bool isFree() const; - - /** claim this slot for exclusive use */ - Allocation& allocate(); - - /** disconnect from this OutputSlot - * @warning may block until DataSinks are gone */ - void disconnect(); + return bool(alloc_); + } /** established output feed */ class Connection; protected: + /** @internal interface for the allocated state */ + class Allocation + { + public: + virtual ~Allocation(); ///< this is an interface + + virtual void release() =0; + virtual Timings getTimings() =0; + }; + /** active connections through this OutputSlot */ template class AllocState; - unique_ptr state_; + shared_ptr alloc_; - /** build the _connected state,_ - * based on the existing configuration - * within this concrete OutputSlot */ - virtual unique_ptr buildState() =0; - }; - - - - /** - * denotes an opened connection ready to receive media data for output. - * Each DataSink (handle) corresponds to an OutputSlot::Connection entry. - * Data is handed over frame wise in a two-phase protocol: first, the client - * gets exclusive access to an output buffer, and then, when done, the buffer - * is handed over by an #emit call. - */ - class DataSink - : public lib::Handle - { + shared_ptr + connect (unique_ptr); public: - BuffHandle lockBufferFor(FrameID); + /** by default marked as inactive/defunct */ + OutputSlot() = default; + /** + * Build an OutputSlot that is in _connected state_. + * @param allocation fully configured for a specific kind of Connection. + */ + OutputSlot (unique_ptr allocation) + : alloc_{connect (move(allocation))} + { } - friend bool operator== (DataSink const& sink1, DataSink const& sink2) - { - return not (sink1 and sink2) - or (sink1 and sink2 - and util::isSameObject (sink1.impl(), sink2.impl())); - } + // standard copy operations acceptable }; - }} // namespace vault::out #endif /*VAULT_OUT_OUTPUT_SLOT_H*/ diff --git a/tests/steam/engine/engine-interface-test.cpp b/tests/steam/engine/engine-interface-test.cpp index 69384cba1..99f7c44aa 100644 --- a/tests/steam/engine/engine-interface-test.cpp +++ b/tests/steam/engine/engine-interface-test.cpp @@ -47,8 +47,7 @@ namespace test { using vault::out::Timings; using lib::time::FrameRate; - typedef asset::ID PID; - typedef OutputSlot::Allocation Allocation; + using PID = asset::ID; namespace { // test fixture... @@ -89,11 +88,10 @@ namespace test { ModelPort port(pipe); DiagnosticOutputSlot oSlot; - Allocation& output = oSlot.allocate(); Timings timings (FrameRate::PAL); /////////TODO // Invoke test subject... - CalcStreams calc = engine.calculate(port, timings, output); + CalcStreams calc = engine.calculate(port, timings, oSlot); ////TODO some direct checks on the calculation stream?? diff --git a/tests/steam/engine/mock-support-test.cpp b/tests/steam/engine/mock-support-test.cpp index 0c0a711b2..1357d9a17 100644 --- a/tests/steam/engine/mock-support-test.cpp +++ b/tests/steam/engine/mock-support-test.cpp @@ -389,13 +389,21 @@ namespace test { auto [port0,sink0] = dispatcher.getDummyConnection(0); auto [port1,sink1] = dispatcher.getDummyConnection(1); CHECK (port0 != port1); + UNIMPLEMENTED ("Identity of a DataSink"); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot CHECK (sink0 != sink1); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code CHECK (port0.isValid()); CHECK (port1.isValid()); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot + /////////////////////////OOO seemingly this is no longer possible (and unnecessary), since DataSink participates in the ref-count now CHECK (sink0.isValid()); CHECK (sink1.isValid()); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code CHECK (not ModelPort().isValid()); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot CHECK (not DataSink().isValid()); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code CHECK (0 == dispatcher.resolveModelPort(port0)); CHECK (1 == dispatcher.resolveModelPort(port1)); diff --git a/tests/steam/play/dummy-builder-context.hpp b/tests/steam/play/dummy-builder-context.hpp index 4eaeddba2..35f80d2e3 100644 --- a/tests/steam/play/dummy-builder-context.hpp +++ b/tests/steam/play/dummy-builder-context.hpp @@ -27,7 +27,16 @@ ** - a set of ModelPort elements ** - a set of DataSink handles, as if an actual playback connection has been established ** @todo 5/2023 add more elements as needed for test driven integration of Player and engine - ** @todo this was invented in 2012 -- but development of the player subsystem stalled thereafter. + ** @todo this was invented in 2012 -- as an act of desperation, since everything to build upon + ** seemed to evade my grip. My hope at that time was so somehow _bootstrap_ an explorative + ** design in order to get away from that top-down reasoning, that seemed to lead nowhere. + ** However, work on the player had to be postponed to care for the GUI first; meanwhile + ** I follow a similar idea, relying on »vertical integration slices« + ** @deprecated 3/2026 not sure if we need this henceforth... + ** It might be useful though in case we consider to build some kind of + ** _component integration test_ -- yet for this to work, we'd need + ** - a fake calculation + ** - a fake OutputSlot ** ** @see mock-dispatcher.hpp ** @see JobPlanningPipeline_test @@ -66,7 +75,7 @@ namespace test { /** * @todo 5/2023 quick-n-dirty placeholder to be able to fabricate fake DataSink handles (`Handle`) - */ + */ /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : after the refactoring of OutputSlot 3/2026 it is not that easy any more. We need a Fake OutputSlot class UnimplementedConnection : public vault::out::OutputSlot::Connection { @@ -166,8 +175,11 @@ namespace test { modelPorts_.push_back (ModelPort(pipeB)); // prepare corresponding placeholder DataSink (a fake active output connection) + UNIMPLEMENTED ("need mock/fake OutputSlot"); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to disentangle BufferProvider implementation dataSinks_.emplace_back().activate (std::make_shared()); dataSinks_.emplace_back().activate (std::make_shared()); +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code } diff --git a/tests/steam/play/dummy-play-connection.hpp b/tests/steam/play/dummy-play-connection.hpp index 65c0e0429..efaee3bc8 100644 --- a/tests/steam/play/dummy-play-connection.hpp +++ b/tests/steam/play/dummy-play-connection.hpp @@ -59,6 +59,11 @@ ** @todo this was invented in 2012 -- but development of the player subsystem stalled thereafter. ** As of 2016, I still consider this design valid and intend to pick up development when ** able to address this topic again. At the moment, the UI-Session connection is more urgent. + ** @deprecated 3/2026 the idea as such is valid, but I doubt it can be implemented this way + ** without creating a massive amount of fake implementation for the sole purpose of + ** demonstrating the component integration. On the other hand, it might be worthwhile + ** to build a complete framework for dummy data so that a running render engine + ** can be demonstrated at integration test level. ** ** @see stage::PlaybackController usage exampleS */ @@ -128,7 +133,7 @@ namespace test { /** search through all port <-> sink connections * supported by this DummyPlayConnection - * @return `true` if found both a math on port an sink. + * @return `true` if found both a match on port an sink. */ bool isSupported (ModelPort port, DataSink sink) @@ -138,8 +143,11 @@ namespace test { if (port == *it) { auto [refPort, refSink] = getModelPort(i); + UNIMPLEMENTED ("need a fake OutputSlot and need to place some Stream-ID into the data sink"); +#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of the OutputSlot default implementation if (refSink == sink) return true; +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code } return false; } diff --git a/tests/vault/out/output-slot-protocol-test.cpp b/tests/vault/out/output-slot-protocol-test.cpp index 24d8e93df..8012ae4c7 100644 --- a/tests/vault/out/output-slot-protocol-test.cpp +++ b/tests/vault/out/output-slot-protocol-test.cpp @@ -58,28 +58,27 @@ namespace test { // and then registered with / retrieved from an OutputManager DiagnosticOutputSlot oSlot; - // Client claims the OutputSlot - // and opens it for exclusive use. - OutputSlot::Allocation& alloc = oSlot.allocate(); + // Note: an OutputSlot is automatically activated / allocated + // for exclusive use by this client. // Now the client is able to prepare // "calculation streams" for the individual // Channels to be output through this slot. - OutputSlot::OpenedSinks sinks = alloc.getOpenedSinks(); + OutputSlot::OpenedSinks sinks = oSlot.getOpenedSinks(); DataSink sink0 = *sinks; DataSink sink1 = *++sinks; // within the frame-calculation "loop" // we perform a data exchange cycle FrameCnt frameNr = 123; - BuffHandle buff00 = sink0.lockBufferFor (frameNr); - BuffHandle buff10 = sink1.lockBufferFor (frameNr); + BuffHandle buff00 = sink0(frameNr); + BuffHandle buff10 = sink1(frameNr); // rendering process calculates content.... buff00.accessAs() = testData(0,0); // while further frames might be processed in parallel - BuffHandle buff11 = sink1.lockBufferFor (frameNr+1); + BuffHandle buff11 = sink1(frameNr+1); buff11.accessAs() = testData(1,1); buff10.accessAs() = testData(1,0); diff --git a/wiki/renderengine.html b/wiki/renderengine.html index bad0152c7..ade7ee57b 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -5214,7 +5214,7 @@ Thus there are two serious problem situations &rarr; OutputSlotImpl -
+
The OutputSlot interface describes a point where generated media data can actually be sent to the external world. It is expected to be implemented by adapters and bridges to existing drivers or external interface libraries, like a viewer widget in the GUI, ALSA or Jack sound output, rendering to file, using an external media format library. The design of this core facility was rather difficult and stretched out over quite some time span -- this page documents the considerations and the decisions taken.
 
 !intention
@@ -5291,6 +5291,12 @@ So the problem is that BufferProvider is conceived as a central service facility
 * an actual output buffer, provided by the {{{Connection}}} back-end, is registered with the metadata and represented as BuffHandle
 * calls on that BuffHandle will invoke the {{{BufferStore}}} API and must be delegated to the corresponding {{{Connection}}}.
 Taken together, this implies that the {{{OutputSlot::AllocState<CON>}}} turns into a delegation hub. It has to maintain the active connections and can thus be used to back a custom made {{{BufferStore}}} implementation, using the information embedded into the {{{LocalTag}}} to select the proper {{{Connection}}} for delegation -- which provides the actual implementation of all buffer management and state handling for the specific output mechanism.
+
+-----
+!!! Establish a proper Lifecycle
+While the original design for OutputSlot seems //elegant, on a theoretical level,// the implementation was constantly plagued with complexities related to the lifecycle. Some relevant aspects could not be settled at all -- which was not noticed until the recent attempts to integrate the OutputSlot into the (likewise reworked) Render Engine implementation. A turning point in all this evolution of design is to remove the //state abstraction// -- in favour of representing the progression of state directly as a sequence of context data frames. The deeper change, which motivated this shift, was to abandon the idea of a single, global, overarching and final scheme of »Rendering«. It is acknowledged rather that the actual rendering for some unspecified »rendering« will be done by an external library -- and each such library establishes its own terminology, processing scheme and conventions, which finally are rooted in some specific ''Domain Onotology''. The Lumiera Render Engine is now understood as a connected graph of //adaptors//, organised around some minimal common denominator, like the fact that data frames are calculated, and that each such frame can be addressed by a media type and a point in time.
+
+In line with this shift in focus, the OutputSlot is seen as yet another element of representational state: it is a token to embody an allocated output connection. This implies to unify the notion of the {{{OutputSlot}}} with the {{{Allocation}}}, and turn both into a handle passed to the client process. All aspects of lifecycle can be managed automatically with such ref-counting handles. This has some cost in terms of performance (thread contention when accessing the atomic ref count), but that is deemed acceptable, since output is handled on a rather slow time scale, like producing a data frame every 10 milliseconds. The specific output mechanism remains »allocated« thus as long as some element of the client processing still holds onto any such handle, either the {{{OutputSlot}}} or some {{{DataSink}}}. The latter is reduced into a //named function//, which however also embeds a shared handle to the common allocation state. With this rearrangement in the design, there is now a cohesive domain of connection management, while the actual, technical connection is separated out and packaged behind the {{{Connection}}} interface.
 
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 2be086577..278722590 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -83808,7 +83808,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83817,6 +83817,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200

+
@@ -84485,7 +84486,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -85389,7 +85390,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -85398,6 +85399,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200

+
@@ -111079,7 +111081,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -111128,6 +111130,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +
    +
  • + Werden DataSink über Threadgrenzen hinweg propagiert? +
  • +
  • + muß irgendwo eine automatische Instanzverwaltung angehängt werden? (⟶ ref-counting) +
  • +
+ +
+ + + + + +
@@ -118951,7 +118973,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -118973,6 +118996,85 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + +

+ oh (Her)Mann! +

+

+ Damals, 2023 dachte ich, ich kann das in einem Ansturm alles »wuppen«. Inzwischen hab ich ¾ meiner sehr guten Einsichten schon wieder vergessen, und sitze nur auf den alten Vorstellungen, die sich irgendwie ins Hirn eingegraben haben +

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

+ ..wie üblich, ich setze auf symbolische Repräsentation von Sprach-artigen Konzepten +

+ +
+
+ + + + + +

+ die Job-Daten werden in den BlockFlow kopiert +

+ +
+
+ + + + +

+ und zwar letztlich tief verborgen im JobTicket +

+ +
+ + + + +

+ das notiere ich jetzt, 3/2026, während ich grade versuche, meine eigene (halbfertige) Konzeption von vor 2 Jahren in Erinnerung zu rufen +

+ +
+
+
+
+ + + + + + + + + + + + + + @@ -119123,8 +119225,15 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + + + @@ -119274,7 +119383,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -131037,7 +131146,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -137658,6 +137767,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+ + +
@@ -138024,7 +138136,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -138056,7 +138169,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -138206,6 +138319,351 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + +

+ Das bisherige Design war da relativ elegant, insofern man die Verschaltung von außen praktisch nicht sehen konnte. Denn das Connection-API war zwar da, aber üblicherweise für den Client verborgen (dazu müßte man output-slot-connection.hpp includieren). Diese Eleganz war aber dadurch erkauft, daß die Connection-Impl sehr eng mit der Allocation-Impl verwoben sein mußte, es gab da ein komplexes Ping-Pong, und das trägt viel zur verwirrenden Struktur bei, denn das bleibt auf Ebene der Schnittstellen-Definition eine implizite Annahme, und wird erst sichtbar, wenn man sich eine konkrete Implementierung anschaut. +

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

+ denn nun hängt alles an dem kontextuellen Binding, das man in einem Lambda einrichtet und dann in die Funktion speichert; ein Lambda mit Ref-Closure ist beliebig kopierbar, aber nicht zuweisbar; eine std::function, die mit einem solchen Lambda initialisiert wurde, wäre sogar zuweisbar +

+ +
+
+ + + + +

+ Durch die Lösung mit dem Funktor gäbe es dann wirklich nur noch eine einzige Operation; bisher hatten wir eine versteckte weitere Operation, insofern das lib::Handle mit einem Deleter konfiguriert wurde +

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

+ Es ist elegant, insofern alle Zustände und Momente des Protokolls direkt repräsentiert sind. Das ist aber eine rein schematische Betrachtungsweise. Von einer tatsächich geeigneten Architektur erwarte ich, daß sie den notwendigen Raum gibt, in einer zwanglosen Weise. Das ist hier nicht der Fall. Daher muß ich mich von meinen eigenen Errungenschaften von »damals« trennen (denn damals hatte ich diese Perspektive noch nicht) +

+ +
+ +
+ + + + +

+ Zusammengenommen fällt auf +

+
    +
  • + die Anker-Entität ist weitgehend leer und spielt weder in der Implementierung, noch im Interface eine Rolle +
  • +
  • + der zentrale pImpl wird jetzt zu einem weak-ptr (also einem reinen »Anhängsel«) +
  • +
  • + ich kämpfe immerfort mit Problemen, daß Callbacks nicht „an der richtigen Stelle ankommen“ +
  • +
  • + die Zugangspunkte und der Kontext müssen auf verschlungenden Pfaden dorthin gebracht werden, wo sie gebraucht werden. +
  • +
+

+ Kurz gefaßt, was geschieht, geschieht nicht am rechten Ort +

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

+ weil wir ihn dann später in die DataSinks verdrahten wollen +

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

+ der Deleter hat bisher nur Connection::shutDown() aufgerufen; alles darüberhinaus war nur eine Absichtserklärung — im Besonderen das Problem mit dem Blocken (im Fall eines Emergency-Shutdown) hab ich seinerzeit (2012) noch nicht einmal erkannt ...  inzwischen hab ich es wenigstens mal notiert als Anforderung »für später« +

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

+ wenn ein Client noch eine Connection hat, dann ist nicht klar, was passieren soll, wenn auf oberer Ebene die Allocation geschlossen wird — und zwar weil nicht klar ist wie die Allokation und die einzelnen Feeds zusammenhängen +

+ +
+
+ + + + +

+ der typische low-level-Ansatz wäre: dann mach's halt nich falsch +

+ +
+ + + +

+ und das heißt, man löst Probleme nicht architektonisch, durch eine Anordnung die dem Wesen der Sache entspricht, sondern man setzt einfach einen Machtspruch, und wältzt damit alles Unpassende, das man selber nicht bewältigt hat, auf den Client ab +

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

+ Diese geanzen Schwierigkeiten, die Rolle der DataSinks festzulegen, deuten doch vor allem darauf hin, daß nicht nur der Bau, sondern auch die Architektur selber noch unklar ist, und erst schrittweise entfaltet werden soll +

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

+ ...damit sich eine Allocation, die nicht mehr benötigt wird, von dort abmelden kann. Das bedeutet, der OutputSlot muß non-copyable sein. (Was er derzeit ist, aber man verliert so einen Freiheitsgrad) +

+ +
+
+ + + + +

+ außerdem ist Thread-Safety hier ein echtes Problem +

+ +
+ + + +

+ OutputSlot wird definitiv stets und massiv über Thread-Grenzen hinweg verwendet +

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

+ die Parameter / Kenndaten werden in den Acticity-Term kopiert +

+ +
+ + + +

+ ...sie liegen also während der Lebensdauer des Jobs im BlockFlow (Memory-Manager) +

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

+ der Bezug auf die DataSink hat value semantics (als Job-Parameter) +

+ +
+
+ + + + + +

+ Und zwar wird diese Read-Barrier vom GroomingToken impliziert, denn der Thread, der letztlich den Job ausführt, hat vorher stets und garantiert etwas Admin-Arbeit geleistet und dafür das (atomic) GroomingToken erlangt ⟹ data visibility guaranteed +

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

+ ...und zwar aus mehrerlei Gründen +

+
    +
  • + wir müßten hierzu für jede Connection einen eigenen ref-cnt pflegen +
  • +
  • + dieser ref-count müßte als Atomic implementiert sein und von einem separaten destructor-Hook in einem speziellen Handle-Typ aufgerufen werden, was die Implementierung deutlich komplexer macht +
  • +
+ +
+
+ + + + + +

+ ⟹ kein weiteres Locking in AllocState notwendig. Der ref-count in shared-ptr verwendet bereits einen Atomic (effektiv würde das allein bereits genügen, aber ich bin mir dessen nicht ganz sicher). Zudem gibt es das GroomingToken. Bliebe nur noch der Fall, daß die Connection durch Beenden des Play-Prozesses geschlossen wird; in diesem Fall sollte man wohl noch etwas tun. +

+ +
+
+ +
@@ -138242,6 +138700,22 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
+ + + + + + + + + + + + + + + + @@ -140119,7 +140593,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + @@ -159692,8 +160166,37 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + + + + + + + + + + + + + + + + + + +

+ das bedeutet, sie unterliegen einem Deadline-basierten Memory-Management und werden selbständig / automatisch weggeworfen, sobald ein Job laut Deatline nicht mehr laufen kann. Konsequenz: einen Activity-Term mit abgelaufener Deadline darf man nicht mehr anfassen!!  (der Scheduler berücksichtigt das ganz trickreich in seiner Implementierung) +

+ +
+ +
+
+ + + + @@ -160687,7 +161190,7 @@ std::cout << tmpl.render({"what", "World"}) << s Dieser kann durch alltägliche Umstände verletzt werden

- Dann ist ein spezielle Notzustand zu aktivieren + Dann ist ein spezieller Notzustand zu aktivieren

@@ -160904,7 +161407,7 @@ std::cout << tmpl.render({"what", "World"}) << s
- + @@ -161041,6 +161544,34 @@ std::cout << tmpl.render({"what", "World"}) << s + + + + + + + + + + + + + + + + + + + + + + + + + + + +