From 2c8ee05a23fa643b988b59cb315acdce2e4e87c2 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 10 Apr 2026 23:15:32 +0200 Subject: [PATCH] OutputSlot: validate and complete redesign (see #1395) ''This is a breakthrough...'' Using the realigned implementation structure in `BufferProvider`, and especially the ability to establish an overarching context by passing an connection-ID through the `LocalTag` in the `BufferMetadata`, it was possible to resolve the longstanding mismatch buried deep within the `OutputSlot` implementation. Based on the new internal APIs in `BufferProvider`, it was thus possible to setup an `OututBufferProxy` that acts as a hub to connect the `DataSink` handles with the actual `OutputSlot::Connection` implementation. This change allowed to rectify the convoluted control structure in `OutputSlot` and finally to manage the Lifecycle properly. Furthermore, one level of unnecessary implementation-inheritance could be removed, so that it is no longer necessary to subclass `OutputSlot` in order to configure the link to a specific output backend. Using this redesigned internal structure, it was now fairly simple to re-implement a tracking `DiagnosticOutputSlot` and then to verify proper integration by passing the `OutputSlotProtocol_test` again. --- doc/devel/uml/OutputSlot_components.svg | 9318 ++++++++++------- src/lib/meta/fun-inline.hpp | 2 +- src/vault/out/diagnostic-output-slot.cpp | 132 +- src/vault/out/diagnostic-output-slot.hpp | 92 +- src/vault/out/output-slot.hpp | 5 +- tests/vault/out/output-slot-protocol-test.cpp | 69 +- uml/Lumiera.xmi | 258 +- wiki/renderengine.html | 17 +- wiki/thinkPad.ichthyo.mm | 3030 ++++-- 9 files changed, 7644 insertions(+), 5279 deletions(-) diff --git a/doc/devel/uml/OutputSlot_components.svg b/doc/devel/uml/OutputSlot_components.svg index 6f663e34f..98dca4bee 100644 --- a/doc/devel/uml/OutputSlot_components.svg +++ b/doc/devel/uml/OutputSlot_components.svg @@ -26,17 +26,17 @@ @@ -73,12 +73,12 @@ fill="none" stroke="#8284e2" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,110.631,285.05)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,110.585,255.043)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g4"> @@ -88,19 +88,18 @@ y="0" width="470" height="255" - id="rect3" - style="stroke-width:1.26587" /> + id="rect3" /> @@ -108,12 +107,12 @@ fill="none" stroke="black" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,110.631,0.996678)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,110.585,0.99626)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g6" /> @@ -121,12 +120,12 @@ fill="none" stroke="#8284e2" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,110.631,0.996678)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,110.585,0.99626)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g7"> @@ -134,21 +133,20 @@ vector-effect="non-scaling-stroke" x="0" y="0" - width="275" - height="270" - id="rect6" - style="stroke-width:1.26587" /> + width="270" + height="245" + id="rect6" /> @@ -156,12 +154,12 @@ fill="none" stroke="black" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g9" /> @@ -169,12 +167,12 @@ fill="none" stroke="black" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g10" /> @@ -182,46 +180,45 @@ fill="none" stroke="black" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.00966102,-0.996631,0.996631,0.00966102,230.233,137.291)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0,-0.99626,0.99626,0,150.435,469.238)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g11" /> + d="m -4.095393,-10 -5,10 5,10 L 0.90460701,0 -4.095393,-10" + id="path11" /> @@ -229,1060 +226,1033 @@ fill="none" stroke="black" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g14" /> + d="M 225,519.09539 H 175" + id="path14" /> - «for each channel» + id="g19" /> + + - - + id="g23"> + d="m 134.696,410 -39.8241,39.087 v 38.082" + id="path22" /> + + id="g27" /> + + d="m -24.325639,-1.6535638 5.413204,10.3294109 6.566822,-9.63728743" + id="path27" /> - - «smart handle» + + d="M-6,0 L0,10 L6,0" + id="path35" /> + d="M 230,256.4 V 151.6" + id="path38" /> - - + id="g43"> + create + + id="g47" /> + + d="m -8.2318857,-5.2890225 6,10 6,-10" + id="path47" /> - - + id="g51"> + d="M 185,464.9996 245.0001,490" + id="path50" /> + + id="g55" /> + + d="M0,-10 L-5,0 L0,10 L5,0 L0,-10" + id="path55" /> - - + id="g59"> + d="M 316,195.744 H 134.696 V 365" + id="path58" /> + - + «for each channel» + d="M-6,0 L0,10 L6,0 L-6,0" + id="path67" /> - - 1 + id="g71"> + + + id="g75" /> + + d="M-6,0 L0,10 L6,0" + id="path75" /> - - «active» + @@ -1290,45 +1260,44 @@ fill="none" stroke="#00004f" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - transform="matrix(0.996678,0,0,0.996678,185.382,469.435)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.00241921,0.996257,-0.996257,0.00241921,270.01,55.4888)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g84"> + d="M8,0 C8,4.41828 4.41828,8 0,8 C-4.41828,8 -8,4.41828 -8,0 C-8,-4.41828 -4.41828,-8 0,-8 C4.41828,-8 8,-4.41828 8,0 M0,-8 L0,8 M-8,0 L8,0" + id="path83" /> @@ -1336,236 +1305,245 @@ fill="none" stroke="#00004f" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g87"> + d="M363.024,99.6776 L395,99.6 L395,165" + id="path86" /> - - + id="g91"> + 1 + - + «active» + d="M -6.3996202,-0.00198237 -0.39962025,9.9980176 5.6003798,-0.00198237" + id="path99" /> @@ -1573,237 +1551,234 @@ fill="none" stroke="#00004f" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g103"> + d="m 301.4,414.8 h -46.8 v 71" + id="path102" /> + d="M-6,0 L0,10 L6,0" + id="path107" /> + d="m 269.8,151.4 v 23.4 h 65.4" + id="path110" /> + d="M0,-10 L-5,0 L0,10 L5,0 L0,-10" + id="path115" /> @@ -1811,4834 +1786,6153 @@ fill="none" stroke="#00004f" stroke-opacity="1" - stroke-width="1.26587" + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - transform="matrix(0.996678,0,0,0.996678,-83.7209,-48.8372)" - font-family="'DejaVu Sans'" - font-size="11.5px" + transform="matrix(0.99626,0,0,0.99626,-83.6858,-43.8354)" + font-family="DejaVu Sans" + font-size="11.5" font-weight="400" font-style="normal" id="g119"> + d="M502.399,384.573 L514.887,384.431 L515,451.4" + id="path118" /> + id="g122" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + «smart_handle» + + + + + id="rect146" /> - - - BufferProxyAdaptor - - - - - - - - - - - - - - - buildDescriptor() - - - - - - - - lockBuffer() - - - - - - - - - - + id="g149" /> + + BufferProxyAdaptor - - BufferProvider - + id="g152" + transform="matrix(1,0,0,1,0,0)" /> - - + id="g154"> + points="0,14 130,14 " + id="polyline153" /> + id="g155"> + points="0,21 130,21 " + id="polyline154" /> + + + id="g159"> + getDescriptor() + - getDescriptor() - + id="g161" /> + id="g165" + transform="matrix(1,0,0,1,0,0)" /> - lockBuffer() + id="g167" /> + + - + id="g171"> + BufferProvider + + id="g173" + transform="matrix(1,0,0,1,0,0)" /> + id="g175"> + + - + points="0,21 140,21 " + id="polyline175" /> + id="g179" /> + SpecificOutputSlot - - - - - - - - - - - - - - - buildState() + id="text179">getDescriptor() + + + + + + lockBuffer() + + + + + id="g192" + transform="matrix(1,0,0,1,0,0)" /> - - + id="g195"> + width="145" + height="95" + id="rect194" /> + id="g196" /> + + OutputManager + + - SpecificConnection - + id="g200" + transform="matrix(1,0,0,1,0,0)" /> - - + id="g202"> + points="0,14 145,14 " + id="polyline201" /> + id="g203"> + points="0,21 145,21 " + id="polyline202" /> + + + id="g207"> + openOutput() + + id="g212" /> + + + + + width="120" + height="85" + id="rect215" /> + id="g217" /> + id="g218" /> + id="g219"> BuffHandle + id="text218">SpecificConnection + id="g220" /> + id="g221" + transform="matrix(1,0,0,1,0,0)" /> - - - - - - - - + id="g223"> + + + + + + + + emit() + id="text227">claimBufferFor() - - - - + id="g230" /> + + + + release() + id="text233">publish() - - - - - - + id="g239" /> - - + id="g240"> Connection + id="text239">release() + + - - + id="g246" + transform="matrix(1,0,0,1,0,0)" /> - - + id="g247" /> + id="g249"> + + + id="g251" /> + claimBufferFor() + id="text251">BuffHandle - + id="g254" + transform="matrix(1,0,0,1,0,0)" /> - - transfer() + id="g256"> + + + + + id="g261"> + emit() + - shutDown() - + id="g263" /> + id="g267"> + release() + + id="g272" /> + + + + + width="120" + height="95" + id="rect275" /> - - - OutputSlot - - - - + Connection + id="g281" + transform="matrix(1,0,0,1,0,0)" /> - state_ + + id="g284"> + + - + getBufferSize() + id="g293" /> + isFree() + id="text293">claimBufferFor() - + id="g299" /> + allocate() + id="text299">publish() - + id="g306"> + release() + - - + id="g308" /> + id="g311" /> + DataSink + id="text311">shutDown() - - - + id="g315" /> - - + id="g316" /> + id="g318" + transform="matrix(1,0,0,1,0,0)" /> - lockBufferFor() + id="g320" /> + + - + id="g324"> + OutputSlot + - - + id="g326" + transform="matrix(1,0,0,1,0,0)" /> + - + + points="0,14 120,14 " + id="polyline327" /> + Allocation + id="text331">alloc_ - - + id="g336" /> + points="0,28 120,28 " + id="polyline336" /> + id="g341" /> + getOpenedSinks() + id="text341">isActive() - + id="g347" /> + timingConstraints() + id="text347">getTimings() - + id="g353" /> + isActive() + id="text353">getOpenedSinks() - + id="g360" + transform="matrix(1,0,0,1,0,0)" /> - - + id="g362" /> + width="100" + height="45" + id="rect362" /> CON + id="text365">DataSink + id="g368" + transform="matrix(1,0,0,1,0,0)" /> + id="g370"> + + - AllocState - + id="g372" /> + id="g374"> + descriptor + - - + id="g376" /> - - connections_ + id="g379"> + + - - + id="g384"> + invoke() + - AllocState() «constructor» - + id="g390" + transform="matrix(1,0,0,1,0,0)" /> + id="g393"> + + connectOutputSink() + id="text395">Allocation + id="g398" + transform="matrix(1,0,0,1,0,0)" /> - - - shutdownConnection() + id="g400"> + + + + + + id="g405"> + connect() + - + id="g411"> Actual Output Driver + id="text410">release() + + id="g416" /> + + getTimings() + + + + + + + + + + + + + + + + BuffDescr + + + + + + + + + + + + + + + lock() + + + + + + + + + + + + + + + + + + + CON + + + + + + + + AllocState + + + + + + + + + + + + connections_ + + + + + + + + + + + + + + AllocState() «constructor» + + + + + + + + connect() + + + + + + + + release() + + + + + + + + getTimings() + + + + + + + + + + + Output Management + id="text502" + style="font-size:13.5533px">Output Management + id="g504" /> + id="g505" /> + id="g506" /> + + Actual Output Driver + + + + + d="M0,0 L0,60 L130,60 L130,5 L125,0 L0,0" + id="path509" /> + id="polyline509" /> + id="polyline510" /> + id="g511" /> + id="g512" /> + id="g513"> Knowledge specific + id="text512"> Knowledge specific + id="g514" /> + id="g515" + transform="matrix(1,0,0,1,0,0)" /> + id="g516" /> + id="g517" /> + id="g518"> for this kind of + id="text517"> for this kind of + id="g519" /> + id="g520" + transform="matrix(1,0,0,1,0,0)" /> + id="g521" /> + id="g522" /> + id="g523"> output driver + id="text522"> output driver + id="g524" /> + id="g525" + transform="matrix(1,0,0,1,0,0)" /> + id="g526" /> + id="g527" /> + id="g528" /> + id="g529"> + d="M0,0 L0,45 L125,45 L125,5 L120,0 L0,0" + id="path528" /> + id="polyline528" /> + id="polyline529" /> + id="g530" /> + id="g531" /> + id="g532"> Client + id="text531"> Client + id="g533" /> + id="g534" + transform="matrix(1,0,0,1,0,0)" /> + id="g535" /> + id="g536" /> + id="g537"> front-end + id="text536"> front-end + id="g538" /> + id="g539" + transform="matrix(1,0,0,1,0,0)" /> + id="g540" /> + id="g541" /> + id="g542" + transform="matrix(1,0,0,1,0,0)" /> + id="metadata543"> OutputSlot components + rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" /> Ichthyostega Lumiera documentation + + + GPL v2+ or CC-By-SA 4 + + + rdf:about="http://creativecommons.org/licenses/by-sa/4.0/"> - +#include #include #include #include @@ -36,8 +65,7 @@ using util::contains; using util::isSameAdr; -// using std::vector; -// using Config = DiagnosticOutputSlot::Config; +using std::vector; using std::byte; using std::move; using std::make_unique; @@ -62,6 +90,11 @@ namespace out { } }//(End)Impl details + + /** + * @internal the diagnostic tracking PImpl + * @remark this defines the _actual identity_ of an DiagnostricOutputSlot + * and is also the place where all observed data is collected */ class DiagnosticOutputSlot::OutputTracker : public Config , util::NonCopyable @@ -95,13 +128,6 @@ namespace out { return bufferType_.lockBuffer(); } - diagn::FeedLog const& - getFeed (uint feedNr) - { - REQUIRE (feedNr < feed_.size()); - return feed_[feedNr]; - } - void recordClaim (uint feedNr, FrameID frame, Buff* buff) { @@ -126,6 +152,25 @@ namespace out { REQUIRE (isSameAdr(frameInfo.storage, buff)); frameInfo.released = currentTime(); } + + + /* ===== diagnostic functions ===== */ + + /** skim through all recorded FrameInfo, over all feeds */ + auto + allBlocks() const + { + return lib::explore(feed_) + .transform([](diagn::FeedLog const& feedLog){ return feedLog.allRecordedBlocks(); }) + .flatten(); + } + + diagn::FeedLog const& + getFeed (uint feedNr) const + { + REQUIRE (feedNr < feed_.size()); + return feed_[feedNr]; + } }; /** @@ -141,8 +186,44 @@ namespace out { return lib::NullValue::get(); } + uint + OutputDiagnostic::cntLocked() + { + return dos_.tracker_->allBlocks() + .filter([](diagn::FrameInfo const& frame){ return frame.wasLocked(); }) + .count(); + } + + uint + OutputDiagnostic::cntEmitted() + { + return dos_.tracker_->allBlocks() + .filter([](diagn::FrameInfo const& frame){ return frame.wasEmitted(); }) + .count(); + } + + uint + OutputDiagnostic::cntReleased() + { + return dos_.tracker_->allBlocks() + .filter([](diagn::FrameInfo const& frame){ return frame.wasReleased(); }) + .count(); + } + + + + /** + * An instrumented dummy data output connection. + * By default, it supports an unlimited number of frames + * and exposes a new buffer for each different frame requested. + * @remark the implementation uses an index table to detect if + * buffer memory address passed back from the client + * was actually a buffer handed out by this connection, + * and to retrieve the associated BuffHandle that was used + * to get this buffer from a backing memory manager. + */ class DiagnosticOutputSlot::DummyConnection : public OutputSlot::Connection { @@ -200,7 +281,6 @@ namespace out { } - public: DummyConnection (OutputTracker& tracker, uint thisFeed) : tracker_{tracker} @@ -210,6 +290,7 @@ namespace out { + DiagnosticOutputSlot::~DiagnosticOutputSlot() { } // emit destructors for hidden implementation here.... DiagnosticOutputSlot::DiagnosticOutputSlot (Config config) @@ -230,13 +311,19 @@ namespace out { * so that this contradiction can be circumvented by constructing it first, as a constructor * argument \a trackingSetup prepared through a chained constructor call. It is thus visible * in the complete scope of the constructor, so that we can pass it to the base class - * constructor, and later then move it into the final location in the derived class. + * constructor, and later then move it into the final location in the derived class. + * + * @note \ref setupTrackingConnections performs that kind of wiring, + * that usually would be done within an OutputManager: it prepares a set of + * OutputSlot::Connection objects, which however are implemented as DummyConnection here, + * and back-wired internally to the OututTracker, so that it is possible to verify + * the invocations that actually happened while using the DiagnosticOutputSlot. */ DiagnosticOutputSlot::DiagnosticOutputSlot (shared_ptr trackingSetup) : OutputSlot{setupTrackingConnections (*trackingSetup)} , tracker_{move(trackingSetup)} { } - + shared_ptr DiagnosticOutputSlot::setupOutputTracker (Config&& config) { @@ -251,9 +338,20 @@ namespace out { return make_unique (outputTracker.numDataFeeds ,[&,i=0](ConStorage& storage) mutable { storage.create (outputTracker, i++); } - ); + ); // a »population functor« that creates a sequence of DummyConnections } + /** + * @param timePoint reference to a "current" time point, + * that can be manipulated from the test setup. + */ + void + DiagnosticOutputSlot::fixCurrentTime (TimeVar const& timePoint) + { + REQUIRE (tracker_); + tracker_->currentTime = [&timePoint] ->Time { return timePoint; }; + } + }} // namespace vault::out diff --git a/src/vault/out/diagnostic-output-slot.hpp b/src/vault/out/diagnostic-output-slot.hpp index 114fa6d19..6ede77ec2 100644 --- a/src/vault/out/diagnostic-output-slot.hpp +++ b/src/vault/out/diagnostic-output-slot.hpp @@ -13,8 +13,33 @@ /** @file diagnostic-output-slot.hpp ** An facility for writing unit-tests against the OutputSlot interface. - ** + ** The setup of DiagnosticOutputSlot is layered, insofar it is foremost + ** a regular OutputSlot, yet has some kind of special _Output Manager_ + ** layered on top. For context, in regular use, it is the task of some + ** specialised OutputManager to provide a suitably configured OutputSlot + ** so that the client can use a specific output facility, like e.g. a + ** video viewer in the UI, or the sound card output. Yet since this is + ** a test setup, DiagnosticOutputSlot is backed by a _fake implementation_ + ** of the OutputSlot::Connection interface. + ** + ** This layered construction allows for a simple and straight-forward + ** usage, where the test can just create a DiagnosticOutputSlot instance, + ** possibly passing a configuration record. This is in contrast to the + ** real usage, where, as pointed out above, the OutputSlot must be + ** maintained and connected to a real output driver by the OutputManager. + ** By default, a configurable number of DataSink handles can be retrieved + ** from this setup, and each sink allows to allocate an unlimited number + ** of different frame buffers, in any order. Furthermore, the OutputSlot + ** can be copied (and thereby sliced) and passed to the code to be tested. + ** @todo 4/2026 the plan is to add the ability to define constraints and + ** limitations through the configuration passed at construction. + ** + ** After invoking the code under test, it is necessary to hold onto + ** one copy of DiagnostricOutputSlot, which maintains the internal + ** connection to the diagnostic information collected. Using the + ** `watch(slot)` syntax, this information can be investigated. ** @see output-slot-protocol-test.cpp + ** @see output-slot.hpp */ @@ -24,23 +49,14 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" -#include "include/logging.h" -#include "vault/out/output-slot.hpp" -#include "vault/out/output-slot-connection.hpp" -#include "vault/mem/buffhandle.hpp" -#include "vault/mem/naive-buffer-setup.hpp" -#include "lib/time/timevalue.hpp" -#include "lib/time/grid.hpp" -#include "lib/p.hpp" -#include "lib/null-value.hpp" -#include "lib/iter-explorer.hpp" -#include "lib/iter-source.hpp" /////////////OOO wech -#include "lib/symbol.hpp" -#include "lib/util.hpp" -#include "vessel/advice.hpp" #include "test/test-frame.hpp" +#include "lib/time/timevalue.hpp" +#include "lib/iter-adapter-stl.hpp" +#include "lib/iter-explorer.hpp" +#include "lib/null-value.hpp" + +#include "vault/out/output-slot.hpp" -#include #include #include #include @@ -50,24 +66,14 @@ namespace vault { namespace out { namespace err = lumiera::error; - using lib::Symbol; using lib::HashVal; - using util::unConst; - using util::contains; using test::TestFrame; - using lib::time::FrameRate; +//using lib::time::FrameRate; ///////TODO planned for time restrictions using lib::time::TimeVar; using lib::time::Time; - using vault::mem::NaiveBufferSetup; - using vault::mem::BuffHandle; - using vault::mem::BuffDescr; - using std::as_const; using std::shared_ptr; - using std::make_unique; - using PGrid = lib::P; - namespace { // diagnostics & internals.... @@ -79,6 +85,7 @@ namespace out { namespace diagn { using Buff = vault::mem::Buff; + /** diagnostic record from handling one data frame */ struct FrameInfo { TimeVar locked{Time::NEVER}; @@ -86,8 +93,8 @@ namespace out { TimeVar released{Time::NEVER}; bool wasLocked() const { return locked != Time::NEVER; } - bool wasEmitted() const { return locked != Time::NEVER; } - bool wasReleased() const { return locked != Time::NEVER; } + bool wasEmitted() const { return emitted != Time::NEVER; } + bool wasReleased() const { return released != Time::NEVER; } size_t buffSize{0}; Buff* storage{nullptr}; @@ -100,6 +107,7 @@ namespace out { bool operator== (BU const&) const; }; + /** diagnostic record of handled frames _in a single feed_ */ struct FeedLog { std::map recorded; @@ -115,12 +123,18 @@ namespace out { } auto - blockIter(uint startFrame =0) const + allBlocks(uint startFrame =0) const { return lib::explore (lib::NumIter{startFrame, std::numeric_limits::max()}) .transform([this](uint idx){ return frame(idx); }) ; } + + auto + allRecordedBlocks() const + { + return lib::iter_stl::eachVal (recorded); + } }; template @@ -189,6 +203,14 @@ namespace out { static constexpr Config defaults() { return {}; } }; + /** set the _running current time_ to be fixed to the given value. + * @remark by default, without calling this function, each tracked invocation + * is marked by the current system time; by calling this function + * however, instead of a running time, fixed step stones will be + * recorded, so that the test can basically define the time axis. + */ + void fixCurrentTime (TimeVar const&); + /** * Official (default) constructor to create a DiagnosticOutputSlot. * When invoked without argument, the default Config applies, otherwise @@ -228,17 +250,17 @@ namespace out { : dos_{theSlot} { } - uint cntLocked() { return 0; } - uint cntEmitted() { return 0; } - uint cntReleased() { return 0; } + uint cntLocked(); + uint cntEmitted(); + uint cntReleased(); diagn::FeedLog const& feed (uint feedNr); auto - getFeed (uint feedNr) + getFeed (uint feedNr, uint startFrame =0) { - return feed(feedNr).blockIter(); + return feed(feedNr).allBlocks(startFrame); } }; diff --git a/src/vault/out/output-slot.hpp b/src/vault/out/output-slot.hpp index fc3659841..80ab2e615 100644 --- a/src/vault/out/output-slot.hpp +++ b/src/vault/out/output-slot.hpp @@ -22,8 +22,8 @@ ** 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. 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 + ** is provided, configured and registered with an OutputManager, using a suitable implementation + ** for the kind of media data to be sent (sound, video), and also suitable for the desired kind of ** 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 @@ -160,6 +160,7 @@ namespace out { protected: + /** @internal interface for the allocated state */ class Allocation; using PAlloc = std::shared_ptr; diff --git a/tests/vault/out/output-slot-protocol-test.cpp b/tests/vault/out/output-slot-protocol-test.cpp index 2afddafc3..9b2380251 100644 --- a/tests/vault/out/output-slot-protocol-test.cpp +++ b/tests/vault/out/output-slot-protocol-test.cpp @@ -20,12 +20,15 @@ #include "vault/out/diagnostic-output-slot.hpp" #include "vault/mem/buffhandle.hpp" +#include "vault/real-clock.hpp" #include "test/test-frame.hpp" using test::TestFrame; using test::testData; +using vault::RealClock; using vault::mem::BuffHandle; +using lib::time::FSecs; namespace vault{ namespace out { @@ -68,25 +71,34 @@ namespace test { DataSink sink0 = *sinks; DataSink sink1 = *++sinks; + // some test markers.... + const FrameCnt START = 123; + TimeVar testTime = Time::ZERO; + oSlot.fixCurrentTime(testTime); + auto markTime = [&]{ return Time( testTime += FSecs(1) ); }; + // within the frame-calculation "loop" // we perform a data exchange cycle - FrameCnt frameNr = 123; - BuffHandle buff00 = sink0(frameNr); - BuffHandle buff10 = sink1(frameNr); + auto MARK_0 = markTime(); + BuffHandle buff00 = sink0(START); + BuffHandle buff10 = sink1(START); // rendering process calculates content.... buff00.emplace (testData(0,0)); // while further frames might be processed in parallel - BuffHandle buff11 = sink1(frameNr+1); + auto MARK_1 = markTime(); + BuffHandle buff11 = sink1(START+1); buff11.emplace (testData(1,1)); buff10.emplace (testData(1,0)); // Now it's time to emit the output + auto MARK_2 = markTime(); buff11.emit(); buff00.emit(); // Buffers must be marked as released when done + auto MARK_3 = markTime(); buff00.release(); buff10.release(); buff11.release(); @@ -97,32 +109,49 @@ namespace test { CHECK (watch(oSlot).cntEmitted() == 2); CHECK (watch(oSlot).cntReleased() == 3); - CHECK ( watch(oSlot).feed(0).frame(123).wasLocked()); - CHECK (not watch(oSlot).feed(0).frame(124).wasLocked()); - CHECK ( watch(oSlot).feed(1).frame(123).wasLocked()); - CHECK ( watch(oSlot).feed(1).frame(124).wasLocked()); + CHECK ( watch(oSlot).feed(0).frame(START ).wasLocked()); + CHECK (not watch(oSlot).feed(0).frame(START+1).wasLocked()); + CHECK ( watch(oSlot).feed(1).frame(START ).wasLocked()); + CHECK ( watch(oSlot).feed(1).frame(START+1).wasLocked()); - CHECK ( watch(oSlot).feed(0).frame(123).wasEmitted()); - CHECK (not watch(oSlot).feed(0).frame(124).wasEmitted()); - CHECK (not watch(oSlot).feed(1).frame(123).wasEmitted()); - CHECK ( watch(oSlot).feed(1).frame(124).wasEmitted()); + CHECK ( watch(oSlot).feed(0).frame(START ).wasEmitted()); + CHECK (not watch(oSlot).feed(0).frame(START+1).wasEmitted()); + CHECK (not watch(oSlot).feed(1).frame(START ).wasEmitted()); + CHECK ( watch(oSlot).feed(1).frame(START+1).wasEmitted()); - CHECK ( watch(oSlot).feed(0).frame(123).wasReleased()); - CHECK (not watch(oSlot).feed(0).frame(124).wasReleased()); - CHECK ( watch(oSlot).feed(1).frame(123).wasReleased()); - CHECK ( watch(oSlot).feed(1).frame(124).wasReleased()); + CHECK ( watch(oSlot).feed(0).frame(START ).wasReleased()); + CHECK (not watch(oSlot).feed(0).frame(START+1).wasReleased()); + CHECK ( watch(oSlot).feed(1).frame(START ).wasReleased()); + CHECK ( watch(oSlot).feed(1).frame(START+1).wasReleased()); - auto stream0 = watch(oSlot).getFeed(0); - auto stream1 = watch(oSlot).getFeed(1); + auto stream0 = watch(oSlot).getFeed(0, START); + auto stream1 = watch(oSlot).getFeed(1, START); CHECK ( stream0); CHECK (*stream0 == testData(0,0)); ++stream0; - CHECK (!stream0); + CHECK (not stream0->wasLocked()); CHECK ( stream1); CHECK (*stream1 == testData(1,0)); ++stream1; CHECK (*stream1 == testData(1,1)); ++stream1; - CHECK (!stream1); + CHECK (not stream1->wasLocked()); + + // Verify that recorded times match the »step stones« defined above... + auto recordedTime_0 = watch(oSlot).feed(0).frame(START).locked; // Frame 0.0 + auto recordedTime_1 = watch(oSlot).feed(1).frame(START+1).locked; // Frame 1.1 + auto recordedTime_2 = watch(oSlot).feed(0).frame(START ).emitted; // Frame 0.0 + auto recordedTime_3 = watch(oSlot).feed(0).frame(START ).released; // Frame 0.0 + // ..while Frame#1 on Sink#0 was actually never requested... + auto recordedTime_N = watch(oSlot).feed(0).frame(START+1).locked; // Frame 0.1 + + CHECK (recordedTime_0 < recordedTime_1); + CHECK (recordedTime_1 < recordedTime_2); + CHECK (recordedTime_2 < recordedTime_3); + CHECK (recordedTime_0 == MARK_0); + CHECK (recordedTime_1 == MARK_1); + CHECK (recordedTime_2 == MARK_2); + CHECK (recordedTime_3 == MARK_3); + CHECK (Time::NEVER == recordedTime_N); } }; diff --git a/uml/Lumiera.xmi b/uml/Lumiera.xmi index 0d2da4fe3..b6f88cd56 100644 --- a/uml/Lumiera.xmi +++ b/uml/Lumiera.xmi @@ -10,6 +10,7 @@ + @@ -326,151 +327,179 @@ - - + + - + + - - - + + + + + - - - + + + - + + - + - - + + + - - - + + + - + - + - - + + + - - - - - - - - - - + + + + + + + + + + - - + + + + + + + - - - + + + - - + - - + + - + + + + + + + + + - - + + + + + + + + + + - - + + - - - - - - - - - + + + - - + + - - - + + + - - - + + + - + - - - - - - - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + @@ -490,8 +519,10 @@ - - + + + + @@ -636,9 +667,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -727,7 +782,7 @@ - + @@ -893,7 +948,7 @@ - + @@ -955,7 +1010,7 @@ - + @@ -982,7 +1037,7 @@ - + @@ -1065,13 +1120,15 @@ + + + - @@ -1082,34 +1139,43 @@ - + - - + + + - + + + - + + - - - - - - - + + + + + + + + + + + + diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 485b243cf..d6d8f83f7 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -5157,7 +5157,7 @@ Thus the mapping is a copyable value object, using an associative array. It may First and foremost, mapping can be seen as a //functional abstraction.// As it's used at implementation level, encapsulation of detail types in't the primary concern, so it's a candidate for generic programming: For each of those use cases outlined above, a distinct mapping type is created by instantiating the {{{OutputMapping<DEF>}}} template with a specifically tailored definition context ({{{DEF}}}), which takes on the role of a strategy. Individual instances of this concrete mapping type may be default created and copied freely. This instantiation process includes picking up the concrete result type and building a functor object for resolving on the fly. Thus, in the way typical for generic programming, the more involved special details are moved out of sight, while being still in scope for the purpose of inlining. But there //is// a concern better to be encapsulated and concealed at the usage site, namely accessing the rules system. Thus mapping leads itself to the frequently used implementation pattern where there is a generic frontend as header, calling into opaque functions embedded within a separate compilation unit. -
+
Within the Lumiera player and output subsystem, actually sending data to an external output requires to allocate an ''output slot''
 This 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 of playback and rendering from the specifics of the actual output sink. Actual output possibilities (video in GUI window, video fullscreen, using a beamer, sound, Jack, rendering to file) can be added and removed dynamically from various components (Vault, Stage), all using the same resolution and mapping mechanisms (&rarr; OutputManagement)
 
@@ -5170,7 +5170,7 @@ Once the client retrieves the OutputSlot for some desired output capability, the
 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 ''data sink 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, which includes to obtain 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 StreamType and further metadata has been validated or configured suitably at a higher level. However, the OutputSlot exposes a definition of [[timing constraints|Timings]] 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.
 
 !!!usage and implementation
-Clients retrieve a reference to an output slot by asking a suitable OutputManager for an output possibility that supports some specific media and format. Usually, they just "claim" this slot by invoking {{{allocate()}}}, which causes actual output connections to be built and data handover mechanisms to be established, behind the scenes. For each such connection -- corresponding to a single distinct data feed within the media format handled by this ~OutputSlot -- the client gets a smart-handle {{{DataSink}}}. The feed may, or may not correspond to a nominal channel in the media format, since some output interfaces require to deliver the data in separate buffers per channel, while others require data to be interleaved. Generally speaking, the concrete ~OutputSlot implementation performs operations rather specific to the kind of output and external interface in question -- thereby calling directly into a specific connection implementation. The »''Output Slot Protocol''« however is designed in a way to abstract from all those technical details.
+Clients retrieve an OutputSlot (≙handle) by asking a suitable OutputManager for an output possibility that supports the desired media and output mode. When this setup succeeds, the underlying output channel has been //allocated// exclusively to this client, so that actual output connections are built and data handover mechanisms were established behind the scenes. All these allocated resources will remain active as long as any of the connected handles is alive. Depending on the actual output technology and the kind of data stream to send to output, a single OutputSlot can manage several active data streams processed in parallel, like e.g. a 3D video output that involves sending two video feeds to separate beamers. For each such connection -- corresponding to a single distinct data feed within the media format handled by this ~OutputSlot -- the client gets a smart-handle {{{DataSink}}}. The feed may, or may not correspond to a nominal channel in the media format, since some output interfaces require to deliver the data in separate buffers per channel, while others require data to be interleaved. Generally speaking, the concrete ~OutputSlot implementation performs operations rather specific to the kind of output and external interface in question -- thereby calling directly into a specific connection implementation. The »''Output Slot Protocol''« however is designed in a way to abstract from all those technical details. Obviously, the Render Node Network must have been prepared in a way that matches the number of connections and the kind of data expected at the output interface. By design choice, there is no further automatic adaptation or translation layer -- whenever something must be converted, preprocessed or adapted, the corresponding computations are managed within the Render Engine itself. 
 
 !!!timing expectations
 Besides the sink handles, allocation of an output slot defines some timing constraints, to which the client must comply. These [[Timings]] are detailed and explicit, including a grid of deadlines for each frame to deliver, plus a fixed //latency.// Within this context, &raquo;latency&laquo; implies a requirement to be ahead of the nominal time by a certain amount, to compensate for the processing time that is necessary to propagate the media to the physical output pin. The output slot implementation itself is bound by external constraints to deliver data at a fixed framerate and aligned to an externally defined timing grid, and furthermore the data needs to be handed over ahead of these time points by an time amount given by the latency. Depending on the data exchange model, additional timing constraints may be imposed to further limit the time window when the buffer must be provided, transferred and released.
@@ -5178,13 +5178,14 @@ Besides the sink handles, allocation of an output slot defines some timing const
 Within this setup, the assumption is for the client to have elaborate timing capabilities at his disposal. More specifically, the „client“ is assumed to be a //job// running within the engine scheduler and thus can be configured to run //after// some other job has finished, and to run within certain time limits. Thus the client is able to run within a //window of current nominal time// -- which is suitably close to the actual system time. On the other hand, the [[Timings]] linked to each {{{DataSink}}} are likewise aligned with the current system time. This enables the output slot implementation to work out from the current system time is a call is timely or overdue -- and to react accordingly.
 
 !!!output modes
-some concrete output connections and drivers embody a specific operation mode (e.g. sample rate or number of channels). The decision and setup of these operational configuration is initiated together with the [[resolution|OutputMapping]] of an OutputDesignation within the OutputManager, finally leading to an output slot (reference), which can be assumed to be suitably configured, before the client allocates this slot for active use. Moreover, an individual output sink (corresponding to some data feed) may just remain unused -- generally speaking, any output channel will just feature silence or remain black, unless or until until there is an {{{emit()}}} call and a successful data handover took place.
+some concrete output connections and drivers embody a specific operation mode (e.g. sample rate or number of channels). The decision and setup of these operational configuration is initiated together with the [[resolution|OutputMapping]] of an OutputDesignation within the OutputManager, finally leading to an output slot (copyable handle), which can be assumed to be suitably configured for this output action, and has already been //allocated// to this specific client for exclusive use. Moreover, an individual output sink (corresponding to some data feed) may just remain unused -- generally speaking, any output channel will just feature silence or remain black, unless or until until there is an {{{emit()}}} call and a successful data handover took place.
 
 As an aside,  it is acknowledged that there are some output systems with additional flexibility, e.g. ~Jack-Audio, that allow to generate an arbitrary number of output pins -- Lumiera will support this flexibility by allowing to set up additional output slots and attach this information to the current session &rarr; SessionConfigurationAttachment
 
 !!!Lifecycle and storage
-The concrete OutputSlot implementation is owned and managed by the facility that actually provides the output capability in question. Once such an output facility becomes usable within Lumiera, it will register this implementation through a suitable OutputManager. For example, the GUI provides several video viewer widgets, while some sound output backend provides sound ports. The associated OutputSlot implementation object is required to stay alive as long as it's registered with an OutputManager. It needs to be de-registered explicitly prior to destruction -- and this deregistration may block until all clients using this slot did terminate. Beyond that, an output slot implementation is responsible to handle all kinds of failures gracefully -- preferably by just emitting a signal (callback functor).
-{{red{TODO 7/11: Deregistration is an unsolved problem....}}}
+Any OutputSlot instance handed out to the client is actually backed by a concrete implementation of the interface {{{OutputSlot::Connection}}}. This is where the handling of actual data output is implemented, using functionality of the facility that actually provides the output capability in question. Once such an output facility becomes usable within Lumiera, it will register this implementation through a suitable OutputManager. For example, the GUI provides several video viewer widgets, while some sound output backend provides sound ports. It is assumed that this backing low-level implementation remains available as long as the Lumiera application holds onto opened connection.
+
+The OutputSlot object handed out to the Render Engine code embeds ref-counting handles, so that the {{{Connection}}} objects remain alive as long as some render or playback process is active and holding a copy of either the OutputSlot or a {{{DataSink}}} handle. Beyond that, a {{{Connection}}} implementation is responsible to handle all kinds of failures gracefully.
 
 !!!unified data exchange cycle
 The planned delivery time of a frame is used as an ID throughout that cycle
@@ -5214,7 +5215,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
@@ -5245,12 +5246,12 @@ Besides the sink handles, allocation of an output slot defines some timing const
 !!relation to buffer management
 {{red{WIP 11/11}}} meanwhile I have worked out the BufferProvider interface in detail, including an explicit buffer handover protocol that is is backed by a tiny state engine to track the BufferMetadata. This mechanism provides an additional transition to {{{BLOCKED}}} state when order or timing constraints are being violated. Yet how to detect and resolve such a floundered state from the engine point of view still remains to be addressed.
 
+{{red{//At this point,//}}} the initial design draft from 2012 became bogged down in a thicket of confused implementation. Based on, and driven by a placeholder implementation at that time, a {{{BufferProvider}}}, that was first utilised to simplify building the test setup, was then conflated with the //interface// exposed towards prospective clients. Notably the prototype implementation relied on passing a BuffHandle from a backing BufferProvider directly to the client. Yet only the BuffHandle itself can be considered part of the protocol, and acts as client interface, necessitating a mechanism that propagates the //lifecycle events// (locking, emitting, releasing) into a state coordinator within the opened OutputSlot.
+
 {{red{WIP 3/2026}}} after a long break, we are now facing the problem how to make BufferProvider and OutputSlot work together. Both define a protocol and data exchange models -- and both exhibit a weakness regarding the releasing of buffers. At which time is it safe to release a buffer, when the handover didn't happen? This is similar to the problem of closing a network connection, i.e. the problem is generally unsolvable, but can be handled pragmatically within certain limits.
 * For handles in the Render Engine we can generally assume that the regular call path is always completed, which ensures that every handle gets released. Exceptions may happen, but are //not handled// -- rather the Engine will be shut down in such a case, which could be arranged to discard all buffer memory unconditionally
 * Regarding the »Output Slot Protocol« it is not clear yet which part will be responsible for checking the [[timing constraints|Timings]]; the actual {{{Connection}}} implementation looks like a plausible candidate. Furthermore, it is not clear yet {{red{3/26}}} where precisely an underlying buffer provider will be integrated, and if doing so is mandatory
 
-At this point, the initial design draft from 2012 became bogged down in a thicket of confused implementation. Based on, and driven by a placeholder implementation at that time, a {{{BufferProvider}}} utilised to simplify building the test setup was conflated with the //interface// exposed towards prospective clients. Notably the prototype implementation relied on passing a BuffHandle from a backing BufferProvider directly to the client. Yet only the BuffHandle itself can be considered part of the protocol, and acts as client interface, necessitating a mechanism that propagates the //lifecycle events// (locking, emitting, releasing) into a state coordinator within the opened OutputSlot.
-
 -----
 !Implementation / design problems
 How to handle the selection of those two data exchange models!
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index d7ed6c7bb..5f8163c9a 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -51617,6 +51617,8 @@
 
 
 
+
+
 
 
 
@@ -51625,1006 +51627,22 @@
 
 
 
-
-
+
+
+
+
 
 
 
-
-
-
-
-
-
-
-  
-  
-    

- das heißt, der erste erschöpfte Iterator terminiert die gesamte Sequenz -

- -
+ + + + - - - - -

- damit man auch die Fälle erschlagen kann, in denen irgendwo eine Index-Variable gebraucht wird; oft ist das nämlich der einzige Grund, dann doch eine klassische For-Iteration zu machen -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- stelle fest: ich darf kein Quell-Tupel konstruieren -

- -
-
- - - - - - - - - - - - - -

- Es ist eigentlich nur eine Verpackung für std::apply, welches man mit dem gleichen Aufruf-Aufwand stets auch direkt aufrufen könnte. Hinzu kommt, daß nun die Argument-Ordnung auf dem API entweder links-rum oder rechts-rum nicht paßt und verwirrend ist -

- -
-
- - - - -

- der spezielle Trick mit dem inneren Lambda ist nur für ein Tupel notwendig, um die Tupel-Elemente flexibel aber korrekt wieder in die Hand zu bekommen (alternativ mußte man vor C++17 mit einem Index-Seq-Iterator arbeiten, was sehr undurchsichtig ist) -

- -
- -
- - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- ...und damit sind nämlich beide Ansätze letzlich wieder versöhnt. Das war die wichtige Einsicht beim zweiten Design-Anlauf. Beim ersten Anlauf wollte ich »brav« sein und Werte liefern, und das ganze Design wurde dadurch extrem undurchschaubar und wackelig, denn natürlich wurde beim ersten Kontakt mit der Realität klar, daß man dann smart-Pointer durch die Gegend schiebt. -

- -
-
- - - - -

- ....aber auch der Punkt, an dem es immer wieder gefährlich wird. -

-

- Warum ist das so wichtig? -

-

- Antwort: weil nur dadurch ein Iterator durch eine Abstraktions-Barriere hindurch arbeiten kann. Der Aufbau eines komplexen Such-Algorithmus erfolgt meist in mehreren Layern, und man möchte eben nicht, daß irgendwo von unten Werte geliefert werden müssen, die dann oben »passen«. Sondern man möchte mehrere (oft virtuelle) Funktionsaufrufe aufeinander mappen. Und nicht zuletzt dadurch kann man geziehlt einzelne Werte in einem anderen Implementierungs-Realm zur Manipulation exponieren. Denn tatsächlich ist ein solcher IterExplorer eine View in eine State-Core -

- -
-
-
- - - - -

- Im Normalfall liefert schon der Quell-Iterator eine Referenz (oder const-Referenz). Und die wird dann durch das Value-Tuple durchgemappt und zeigt eigentlich in die State-Core oder einen unterliegenden Container. Wenn aber an irgend einer Stelle tatsächlich ein Wert geliefert werden muß, so wird das auf diesem Level stets abgefedert, und die stabile Storage für den Wert liegt dann im Iterator selber (im ItemWrapper) -

- -
-
-
-
- - - - - - - - - - - - -

- (ob dieserer test portabel ist....) -

- -
- - -
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- andere Tricks mit der _rawCore()-Zugriffsfunktion scheinen nicht zu gehen -

- -
-
- - - - -

- ...da alle Processing-Layer auf State-Cores beruhen, präsentieren wir dem darüberzulegenden IterExplorer die neue Core, und der findet dann die unter diesem Layer liegende CheckedCore des Vorgängers und strippt den neuen Layer weg. YESS es funktioniert!!!!!! -

- -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -

- vorher: -

-

- IterExplorer< -

-

-   BaseAdapter< -

-

-     IterableDecorator<uint, -

-

-       CheckedCore< -

-

-         IterExplorer< -

-

-           IterableDecorator<uint, -

-

-             CheckedCore< -

-

-               Transformer< -

-

-                 BaseAdapter<NumIter<int> >, -

-

-                 uint -

-

- nachher: -

-

- IterExplorer< -

-

-   IterableDecorator<uint, -

-

-     CheckedCore< -

-

-       Transformer< -

-

-         BaseAdapter<NumIter<int> >, -

-

-         uint -

- -
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -

- NumIter hatte ich bei Gelegenheit aus Test-Code extrahiert, ohne weiter viel Gedanken darauf zu ver(sch)wenden.... -

- -
-
- - - - -

- Bestenfalls eliminiert der Optimiser ohnehin alle Indirektionen, aber auch alle Kopien. Wenn das aber nicht geht, weil es von irgendwo noch einen Zugriff geben könnte, dann sitzt man auf einem Speicherzugriff, wo andernfalls direkt ein Register verwendet werden könnte. Und wenn dann auch noch die Referenz non-const ist, versaut man sich leicht internen State und wundert sich dann... Nee, nee! Wenn man wirklich Seiteneffekte über Referenzen möchte, dann soll man sich das bitte explizit auscoden, aber nicht eine Library-Funktion kreativ nutzen -

- -
-
- - - - - - - -

- ich kann mir keinen Anwendungsfall vorstellen, wo so etwas nicht zumindest überraschend wäre.... Ja man könnte INT = custom magic type setzen, aber wie war das nochmal mit Magie und so? -

- -
-
- - - - - - - - - - - -

- Das heißt: in dem Moment wo wir die transformer-Funktion tatsächlich installieren, packen wir sie in ein std::function-Objekt mit einer neu synthetisierten Signatur. Diese ist (der Intention nach) paßgenau, d.h. sie nimmt den yield des vorgelagerten Iterators als Input -

- -
-
- - - - - - -

- typedef typename IT::reference InType; -

-

- function<VAL(InType)> trafo_; -

- -
-
- - - - -

- damals wollte ich erst mal schnell durch, wie üblich; -

-

- viele Jahre später habe ich dann den NumIter definiert, und das ist vielleicht das erste Beispiel, bei dem dieses neue Konzept wirklich ausgereitzt wurde. Ich habe so dunkel in Erinnerung, daß es dann nicht funktioniert hat, und ich deshalb „einfach“ das const in die eingebetteten Ergebnis-Typdefinitionen gepackt habe. Was ja nicht falsch war, aber der Weg des geringsten Wiederstandes -

- -
-
- - - - - -

- Und das ist mir zunehmend wichtiger geworden, vor allem nach meinen Erfahrungen mit dem monadischen IterExplorer v1. Grade weil man manchmal wirklich nur Values bieten kann, muß diese Hintertür offen bleiben. Andererseits wäre es ein gefährlicher Fehlschluß, daß eine Type-def »reference« hier „in Wirkklichkeit“ meint, das was der Iterator liefert. Der Schluß liegt nahe, und deshalb bin ich ihm hier auch verfallen. Aber wenn man mit derartigem Konzept-Mapping erst mal anfängt, dann verliert man schnell die Kontrolle -

- -
-
-
- - - - - - -

- in iter-adapter.hpp -

- -
- -
- - - - - - - -

- also exakt die zentrale Stelle, an der entschieden wird, ob eine nachgeschaltete Funktion auf den Resultat-Wert losgeht, oder direkt auf den Iterator-Typ zugreift -

- -
-
- - - - - -
-
-
-
- - - - - - - -

- jetzt wirds aber brandgefährlich -

- -
-
- - - - - - - - - - - - - - - - -

- ...das würde bedeuten: man reicht eine weitere Typ-Def komplett durch die ganze Kette durch, und jeweils nur am Ende der Kette würde diese für den operator* gelten. Da wir aber Ketten im Builder auch noch nachträglich weiter verlängern, indem eine neue Core darüber gesetzt wird, ist das jedoch nicht ohne Weiteres realisierbar -

- -
- - -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- ...auch wenn es normalerweise kein Performance-Problem darstellt, weil der Optimiser redundante Aufrufe problemlos eliminiert, macht es die Typen für den Außenstehend noch viel undurchtringbarer, und belastet auch die Debug-Builds durch den Umfang unnötiger Typ-Information -

- -
- -
- - - - -
- - - - - - - - - - - - - - - - - -

- das zwingt mich, einen inneren Quell-Layer in den äußeren Wrapper zu schieben; selbst wenn ich das Argument by-value nehme, ist es damit noch nicht in dem Objekt-Feld, wo es hingehört. Das bedeutet dann, entweder eine Kopie (und man ist vom Optimiser abhängig), oder eben ein move(SRC). -

- -
-
-
-
- - - - - - - - - - - - - -

- tatsächlich ist der Filter besonders gefährdet -

- -
- - - -

- weil der Filter selber ja nur den Feed pullt, aber selber keine Werte speichert; damit erzeugt er möglicherweise upstream obsolete Referenzen. -

- -
-
-
- - - - -

- ich brauche einen engaged-State -

- -
- - - - - - - - - - - - - - - -

- ...er läßt nämlich den ItemWrapper zunächst leer, und verwendet das für eine Lazy-invocation der Transformation -

- -
-
- - - - - - - - - - - - -

- ungutes Gefühl... -

-

- das Design hat hier eine Schwachstelle -

- -
- - -
-
- - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- ...dann würden wir eine ganze Pipeline »flippen« können, wenn der erste Result-Typ auf Value schwenkt -

- -
-
- - - - -

- habe die mehrfachen Adapter nun allesamt weg; daher wäre es nun doch denkbar, da die Call-Pfade nun von einem Punkt aus aufspalten (nämlich Core::yield ->  entweder operator* oder operator-> ) -

- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -

- Booo!  is_const_v<T&> ist immer false -

- -
-
- - - - - -

- ruck-zuck ein paar Stunden weg ... hätte man doch gleich einen Test geschrieben -

- -
- -
-
- - - -
- - - - - - -
- - - - - - - -
    -
  • - dediziertes Trait schaffen -
  • -
  • - Propagations-Logik explizit auscoden -
  • -
  • - den operator-> auf Values weglassen bzw mit compile-Fehler unterdrücken -
  • -
- -
-
- - - - -

- ...da bisher alles auf Rückgabe by-ref ausgelegt ist, und dieser Fall deshalb durch die Änderung ohne Modifikation durchgereicht wird -

- -
-
- - - - -

- es wird vermutlich auf wenige Fälle beschränkt bleiben, in denen es aber durchaus hilfreich ist, da sowohl Generatoren als auch Transformatoren einfacher zu schreiben sind -

- -
-
-
-
- - - -
- - - -
- - - - - - - - - - - - -

- der auto-Expander kann und darf ja nicht wissen, wo genau unterhalb die Expansion stattfindet; wenn eben gar keine stattfindet, dann bleiben alle durchgereichten expandChildren()-Aufrufe ohne Wirkung, aber ein iterNext() wird dann auch nicht mehr gesendet -

- -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- ich kann mir keinen Fall vorstellen, in dem der Default gelten könnte; vermutlich habe ich den Default damals angeschrieben, als ich mir das Konzept neu ausgedacht habe, und mir noch nicht klar war, wohin das führt (und wie bedeutend das mal wird....) -

- -
-
-
- - - - - - - - - - - -

- ...und zwar in LinkedElements. -

-

- Das ist nicht irgend ein Randfall, sondern eine der zentralen Nutzungsmöglichkeiten: um nämlich einen iterator und einen const-iterator aus der gleichen Core zu erzeugen -

- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- + + @@ -57610,7 +56628,1175 @@ - + + + + + + + + + + + + + + + + + + +

+ matchAtStart(str, regex) : partieller Match am Anfang verankert +

+ +
+ + + +

+ wichtig für Text-Parsing, wo man bestimmte Tokens akzeptieren  möchte, aber durchaus ein Rest »dahinter« übrig bleibten darf. +

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

+ das heißt, diese Funktion darf es nur einmal geben, und das muß dann »die« einschlägige Implementierung sein, ohne Wenn und Aber +

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

+ also... +

+
    +
  • + nimmt einen Lumiera Forward-Iterator +
  • +
  • + verpackt ihn als »state-core« +
  • +
+ +
+
+ + + + + + + + + + +

+ einfach und minimalistisch implementiert — und zudem sind undendliche Folgen nur etwas für Mathematiker und Freunde der funktionalen Programmierung; in der Praxis braucht man zwingend stets eine Abbruch-Bedingung, allein schon weil sonst die Engine „Amok laufen“ könnte (man bedenke nur, daß die interne Zeit-Repräsentation bereits lange vor der Frame-Nummer wrappt) +

+ +
+ + +
+
+ + + + +

+ optional die Möglichkeit für einen getriggerten Stop vorsehen +

+ +
+ + + + + + + + + + + + + +
    +
  • + Hilfsfunktionen fehlen (z.B. Adaptieren des Funktors an StateCore) +
  • +
  • + TreeExplorer sollte itertools nicht includieren (gegenwärtig tut er's aber nur wegen IterSource, und das könnte man refactorn) +
  • +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +

+ das heißt, der erste erschöpfte Iterator terminiert die gesamte Sequenz +

+ +
+
+ + + + +

+ damit man auch die Fälle erschlagen kann, in denen irgendwo eine Index-Variable gebraucht wird; oft ist das nämlich der einzige Grund, dann doch eine klassische For-Iteration zu machen +

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

+ stelle fest: ich darf kein Quell-Tupel konstruieren +

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

+ Es ist eigentlich nur eine Verpackung für std::apply, welches man mit dem gleichen Aufruf-Aufwand stets auch direkt aufrufen könnte. Hinzu kommt, daß nun die Argument-Ordnung auf dem API entweder links-rum oder rechts-rum nicht paßt und verwirrend ist +

+ +
+
+ + + + +

+ der spezielle Trick mit dem inneren Lambda ist nur für ein Tupel notwendig, um die Tupel-Elemente flexibel aber korrekt wieder in die Hand zu bekommen (alternativ mußte man vor C++17 mit einem Index-Seq-Iterator arbeiten, was sehr undurchsichtig ist) +

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

+ ...und damit sind nämlich beide Ansätze letzlich wieder versöhnt. Das war die wichtige Einsicht beim zweiten Design-Anlauf. Beim ersten Anlauf wollte ich »brav« sein und Werte liefern, und das ganze Design wurde dadurch extrem undurchschaubar und wackelig, denn natürlich wurde beim ersten Kontakt mit der Realität klar, daß man dann smart-Pointer durch die Gegend schiebt. +

+ +
+
+ + + + +

+ ....aber auch der Punkt, an dem es immer wieder gefährlich wird. +

+

+ Warum ist das so wichtig? +

+

+ Antwort: weil nur dadurch ein Iterator durch eine Abstraktions-Barriere hindurch arbeiten kann. Der Aufbau eines komplexen Such-Algorithmus erfolgt meist in mehreren Layern, und man möchte eben nicht, daß irgendwo von unten Werte geliefert werden müssen, die dann oben »passen«. Sondern man möchte mehrere (oft virtuelle) Funktionsaufrufe aufeinander mappen. Und nicht zuletzt dadurch kann man geziehlt einzelne Werte in einem anderen Implementierungs-Realm zur Manipulation exponieren. Denn tatsächlich ist ein solcher IterExplorer eine View in eine State-Core +

+ +
+
+
+ + + + +

+ Im Normalfall liefert schon der Quell-Iterator eine Referenz (oder const-Referenz). Und die wird dann durch das Value-Tuple durchgemappt und zeigt eigentlich in die State-Core oder einen unterliegenden Container. Wenn aber an irgend einer Stelle tatsächlich ein Wert geliefert werden muß, so wird das auf diesem Level stets abgefedert, und die stabile Storage für den Wert liegt dann im Iterator selber (im ItemWrapper) +

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

+ (ob dieserer test portabel ist....) +

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

+ andere Tricks mit der _rawCore()-Zugriffsfunktion scheinen nicht zu gehen +

+ +
+
+ + + + +

+ ...da alle Processing-Layer auf State-Cores beruhen, präsentieren wir dem darüberzulegenden IterExplorer die neue Core, und der findet dann die unter diesem Layer liegende CheckedCore des Vorgängers und strippt den neuen Layer weg. YESS es funktioniert!!!!!! +

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

+ vorher: +

+

+ IterExplorer< +

+

+   BaseAdapter< +

+

+     IterableDecorator<uint, +

+

+       CheckedCore< +

+

+         IterExplorer< +

+

+           IterableDecorator<uint, +

+

+             CheckedCore< +

+

+               Transformer< +

+

+                 BaseAdapter<NumIter<int> >, +

+

+                 uint +

+

+ nachher: +

+

+ IterExplorer< +

+

+   IterableDecorator<uint, +

+

+     CheckedCore< +

+

+       Transformer< +

+

+         BaseAdapter<NumIter<int> >, +

+

+         uint +

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

+ NumIter hatte ich bei Gelegenheit aus Test-Code extrahiert, ohne weiter viel Gedanken darauf zu ver(sch)wenden.... +

+ +
+
+ + + + +

+ Bestenfalls eliminiert der Optimiser ohnehin alle Indirektionen, aber auch alle Kopien. Wenn das aber nicht geht, weil es von irgendwo noch einen Zugriff geben könnte, dann sitzt man auf einem Speicherzugriff, wo andernfalls direkt ein Register verwendet werden könnte. Und wenn dann auch noch die Referenz non-const ist, versaut man sich leicht internen State und wundert sich dann... Nee, nee! Wenn man wirklich Seiteneffekte über Referenzen möchte, dann soll man sich das bitte explizit auscoden, aber nicht eine Library-Funktion kreativ nutzen +

+ +
+
+ + + + + + + +

+ ich kann mir keinen Anwendungsfall vorstellen, wo so etwas nicht zumindest überraschend wäre.... Ja man könnte INT = custom magic type setzen, aber wie war das nochmal mit Magie und so? +

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

+ Das heißt: in dem Moment wo wir die transformer-Funktion tatsächlich installieren, packen wir sie in ein std::function-Objekt mit einer neu synthetisierten Signatur. Diese ist (der Intention nach) paßgenau, d.h. sie nimmt den yield des vorgelagerten Iterators als Input +

+ +
+
+ + + + + + +

+ typedef typename IT::reference InType; +

+

+ function<VAL(InType)> trafo_; +

+ +
+
+ + + + +

+ damals wollte ich erst mal schnell durch, wie üblich; +

+

+ viele Jahre später habe ich dann den NumIter definiert, und das ist vielleicht das erste Beispiel, bei dem dieses neue Konzept wirklich ausgereitzt wurde. Ich habe so dunkel in Erinnerung, daß es dann nicht funktioniert hat, und ich deshalb „einfach“ das const in die eingebetteten Ergebnis-Typdefinitionen gepackt habe. Was ja nicht falsch war, aber der Weg des geringsten Wiederstandes +

+ +
+
+ + + + + +

+ Und das ist mir zunehmend wichtiger geworden, vor allem nach meinen Erfahrungen mit dem monadischen IterExplorer v1. Grade weil man manchmal wirklich nur Values bieten kann, muß diese Hintertür offen bleiben. Andererseits wäre es ein gefährlicher Fehlschluß, daß eine Type-def »reference« hier „in Wirkklichkeit“ meint, das was der Iterator liefert. Der Schluß liegt nahe, und deshalb bin ich ihm hier auch verfallen. Aber wenn man mit derartigem Konzept-Mapping erst mal anfängt, dann verliert man schnell die Kontrolle +

+ +
+
+
+ + + + + + +

+ in iter-adapter.hpp +

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

+ also exakt die zentrale Stelle, an der entschieden wird, ob eine nachgeschaltete Funktion auf den Resultat-Wert losgeht, oder direkt auf den Iterator-Typ zugreift +

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

+ jetzt wirds aber brandgefährlich +

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

+ ...das würde bedeuten: man reicht eine weitere Typ-Def komplett durch die ganze Kette durch, und jeweils nur am Ende der Kette würde diese für den operator* gelten. Da wir aber Ketten im Builder auch noch nachträglich weiter verlängern, indem eine neue Core darüber gesetzt wird, ist das jedoch nicht ohne Weiteres realisierbar +

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

+ ...auch wenn es normalerweise kein Performance-Problem darstellt, weil der Optimiser redundante Aufrufe problemlos eliminiert, macht es die Typen für den Außenstehend noch viel undurchtringbarer, und belastet auch die Debug-Builds durch den Umfang unnötiger Typ-Information +

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

+ das zwingt mich, einen inneren Quell-Layer in den äußeren Wrapper zu schieben; selbst wenn ich das Argument by-value nehme, ist es damit noch nicht in dem Objekt-Feld, wo es hingehört. Das bedeutet dann, entweder eine Kopie (und man ist vom Optimiser abhängig), oder eben ein move(SRC). +

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

+ tatsächlich ist der Filter besonders gefährdet +

+ +
+ + + +

+ weil der Filter selber ja nur den Feed pullt, aber selber keine Werte speichert; damit erzeugt er möglicherweise upstream obsolete Referenzen. +

+ +
+
+
+ + + + +

+ ich brauche einen engaged-State +

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

+ ...er läßt nämlich den ItemWrapper zunächst leer, und verwendet das für eine Lazy-invocation der Transformation +

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

+ ungutes Gefühl... +

+

+ das Design hat hier eine Schwachstelle +

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

+ ...dann würden wir eine ganze Pipeline »flippen« können, wenn der erste Result-Typ auf Value schwenkt +

+ +
+
+ + + + +

+ habe die mehrfachen Adapter nun allesamt weg; daher wäre es nun doch denkbar, da die Call-Pfade nun von einem Punkt aus aufspalten (nämlich Core::yield ->  entweder operator* oder operator-> ) +

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

+ Booo!  is_const_v<T&> ist immer false +

+ +
+
+ + + + + +

+ ruck-zuck ein paar Stunden weg ... hätte man doch gleich einen Test geschrieben +

+ +
+ +
+
+ + + +
+ + + + + + +
+ + + + + + + +
    +
  • + dediziertes Trait schaffen +
  • +
  • + Propagations-Logik explizit auscoden +
  • +
  • + den operator-> auf Values weglassen bzw mit compile-Fehler unterdrücken +
  • +
+ +
+
+ + + + +

+ ...da bisher alles auf Rückgabe by-ref ausgelegt ist, und dieser Fall deshalb durch die Änderung ohne Modifikation durchgereicht wird +

+ +
+
+ + + + +

+ es wird vermutlich auf wenige Fälle beschränkt bleiben, in denen es aber durchaus hilfreich ist, da sowohl Generatoren als auch Transformatoren einfacher zu schreiben sind +

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

+ der auto-Expander kann und darf ja nicht wissen, wo genau unterhalb die Expansion stattfindet; wenn eben gar keine stattfindet, dann bleiben alle durchgereichten expandChildren()-Aufrufe ohne Wirkung, aber ein iterNext() wird dann auch nicht mehr gesendet +

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

+ ich kann mir keinen Fall vorstellen, in dem der Default gelten könnte; vermutlich habe ich den Default damals angeschrieben, als ich mir das Konzept neu ausgedacht habe, und mir noch nicht klar war, wohin das führt (und wie bedeutend das mal wird....) +

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

+ ...und zwar in LinkedElements. +

+

+ Das ist nicht irgend ein Randfall, sondern eine der zentralen Nutzungsmöglichkeiten: um nämlich einen iterator und einen const-iterator aus der gleichen Core zu erzeugen +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ @@ -60977,7 +61163,6 @@ -
@@ -63354,7 +63539,7 @@
- + @@ -63365,7 +63550,7 @@

- + @@ -63376,6 +63561,29 @@ + + + + + + + + +

+ ....wobei allerdings auch das letztlich fraglich ist — denn was genau wäre der Unterschied zu dem bereits implementierten Mechanismus, der die Pipeline hinter ein IterSource-Interface packt? +

+

+ ⟹ es wäre ausschließlich der Umstand, daß die Pipeline-Implementierung in lokaler Storage liegt, und keinen Heap-Speicher benötigt. Abgesehen davon sind beide Lösungen äquivalent (unter der Annahme, daß IterSource wie geplant überarbeitet wird, so daß dieses Interface kongruent wird zur »StateCore«). Denn, wie ich durch meine Implementierungs-Studie gesehen habe, es geht hier nicht nur um einen verpackten Function-Pointer, sondern es geht stets um ein virtuelles interface,  denn auch das Instance-Management (copy, move, assign, swap, destroy) muß virtualisiert werden. +

+ +
+ + +
+ + + +
@@ -63501,7 +63709,7 @@ - + @@ -63511,6 +63719,7 @@ + @@ -63716,6 +63925,16 @@ + + + + +

+ ...denn ich wollte eine Iterator-Funktion definieren, und als Abstraktionsbarriere verwenden. Iteratoren sind aber essentiell kopierbar +

+ +
+
@@ -63723,8 +63942,16 @@ - + + + + + + + + + @@ -63734,12 +63961,165 @@ + + + + + - - - - + + + + + + + + + + + + +

+ Wenn man für jede auftretende Kombination von Funktions-Signatur und konkret gebundenem Funktor eine Basisklasse generiert, dann erzeugt man eine Menge von Funktionen, die jeweils eine VTable liefern; und da andererseist so ein Function-Wrapper gerne aus generischem Code verwendet wird, kann daraus ein gefährlicher Hebel entstehen +

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

+ Kopien und Datenbewegungen machen aktuelle CPUs extrem schnell, und Speicher ist billig +

+ +
+
+ + + + +

+ das heißt, jede der beiden Operationen wir mit dem absoluten Minimum an Indirektion aufgerufen (nämlich ein Funktionspointer) +

+ +
+
+ + + + +

+ und zwar einmal als Funktion; das ist u.U dann etwas mehr Code, als wenn man das Roh-Objekt bewegen würde (weil der Optimiser da nichts machen kann), aber auch weniger, als u.U. das direkte Inlinen von mehreren Instanz-Operationen. Jedenfalls kann auch eine VTable-basierte Implementierung nicht mit weniger Code auskommen, sofern virtual copy support gewünscht ist +

+ +
+
+ + + + +

+ Es gibt jeweils eine Instanz der Invoke und der Manager-Funktion für jeden Typ (also jeweils zwei Funktionen) — aber keine VTable  und keine Typeinfo +

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

+ Rein auf Basis der abzählbaren Einträge im Executable sehe ich nur einen geringen Unterschied: nämlich daß die generischen VTable-Operationen wegfallen. Abgesehen davon erfordern beide Ansätze jeweils zwei Funktionen / virtuelle Funktionen. +

+

+ +

+

+ Der Trade-off besteht also zwischen ein paar tausend (unterer Bereich) zusätzliche Einträge vs. jeweils ein zusätzliches Datenwort in jeder Instanz zur Laufzeit. +

+ +
+
+ + + + +

+ Und das heißt, ich kann keine Entscheidungen über knapp bemessene Performance-Unterschiede treffen auf einer rein-generischen Basis. Im Einzelfall (wie z.B. für lib::Several) besteht ja immer noch die Möglichkeit einer handgeschriebenen Implementierung, die u.U auch zusätzliches Wissen bezüglich der konkreten Situation nutzbar machen könnte (beispielsweise geht es im ArrayBucket für lib::Several nur um eine abstrahierte Deleter-Funktion, d.h. es genügt tatsächlich ein einziger Funktionspointer). +

+

+ +

+

+ Insofern gibt es auf generischer Ebene nur die Feststellung, daß man mit einer ansonsten »praktisch äquivalenten« implementierung (halt ein Pointer mehr) für jede Instanz zwei Interface-Einträge und den zugehörigen Assembly einspart.... Im Vergleich dazu ist bezüglich der Laufzeit der Unterschied viel relevanter, wenn für transiente Operationen (wie einen Iterator) eine Heap-Allokation vermieden werden kann. +

+ +
+
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
@@ -64379,6 +64759,62 @@
+ + + + + +

+ »Polymorphic Value« +

+ +
+ + + + + + +

+ Es gab also keinen unmittelbaren Bedarf, aber ich habe immer wieder bemerkt, daß so etwas nützlich wäre und konzisere APIs ermöglichen würde... +

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

+ Eine Komponente, die geeignet einen Instance-Manager unterstützt, würde automatisch wie ein einfacher Value handhabbar, mit Move-, Copy-, Asignment und Swap-Support. Zwar wird all dies normalerweise automatisch von der C++ - Sprache unterstützt, erfordert aber komplette Offenlegung des Typs — ein Instance-Manager wird also relevant, wenn man den konkreten Typ verbergen möchte, aber trotzdem Wert-Semantik bieten möchte +

+ +
+ + + + + + + + + + +
+
+
@@ -64700,7 +65136,7 @@ - + @@ -64909,7 +65345,7 @@ - + @@ -65219,7 +65655,7 @@

- + @@ -73813,7 +74249,7 @@

  • - die Factory kann sich selbst erzeugen, indem sie auf einen Monostate zurückgreift. Das ist in der Praxis der wichtigste Fall, der im Besonderen für die normale Heap-Allokation gilt, aber auch Thread-Local und damit Kontext-Bezogen auslegbar ist. + die Factory kann sich selbst erzeugen, indem sie auf einen Monostate zurückgreift. Das ist in der Praxis der wichtigste Fall, der im Besonderen für die normale Heap-Allokation gilt, aber auch Thread-Local und damit Kontext-bezogen auslegbar ist.
  • es könnte nur Copy-Construction oder Delegation erlaubt sein, und die Factory ist in diesem Fall nur ein Front-End-Handle @@ -73916,7 +74352,7 @@

    - ...auch hier soll die Verdrahtung verborgen bleiben; jedoch ist man nicht auf einen Objekttyp festgelegt, und außerdem soll es die Möglichkeit geben, esplizit eine destroy()-Funktion aufzurufen, oder alternativ schon aus der Erzeugung ein managing-handle zu bekommen + ...auch hier soll die Verdrahtung verborgen bleiben; jedoch ist man nicht auf einen Objekttyp festgelegt, und außerdem soll es die Möglichkeit geben, explizit eine destroy()-Funktion aufzurufen, oder alternativ schon aus der Erzeugung ein managing-handle zu bekommen

    @@ -82238,8 +82674,8 @@ - + @@ -82247,8 +82683,8 @@ - + @@ -86895,6 +87331,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + @@ -104917,7 +105354,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - + @@ -105021,6 +105458,18 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + + + + +

    + Stand 2026: nach dem Aufräumen und Restrukturieren der Implementierung von BufferProvider habe ich die Metadaten behutsam verallgemeintert, und nun konsitent auf der internen Ebene zwischen BufferStage und BufferStore zugänglich gemacht. Speziell das LocalTag wird damit zu einem Kommunikations-System, über das sich eine konkrete Einbindung in ein Storage- oder Output-System über das ganze Protokoll hinweg koordinieren kann. Im OutputSlot stellt das LocalTag in jedem DataSink-Handle intern die Verknüpfung zur darunterliegenden Connection-Implementierung her, und ermöglicht so einen BufferStore, der direkt an die Output-Bufferverwaltung in der Connection delegiert. +

    + +
    + + +
    @@ -105406,6 +105855,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + @@ -105423,6 +105873,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + + +
    @@ -113642,8 +114095,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
    - +
    @@ -118894,7 +119347,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -118922,8 +119375,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -118935,8 +119388,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -118944,7 +119397,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -118987,7 +119441,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -119022,22 +119477,41 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - +

    - Auf dem Weg einer systematischen Analyse wäre ich (mein typisches Mindset zugrunde gelegt) niemals darauf gekommen, diese Kongruenz zwischen den Interfaces zu sehen. Erfahrungsgemäß finde ich solche Dinge immer erst durch eine extrem mühsame Empirie, viel Aufräumen, und unvorstellbar langwierige Reflexionen. Das alles ist mir hier erstpart geblieben + Auf dem Weg einer systematischen Analyse wäre ich (mein typisches Mindset zugrunde gelegt) niemals darauf gekommen, diese Kongruenz zwischen den Interfaces zu sehen. Erfahrungsgemäß finde ich solche Dinge immer erst durch eine extrem mühsame Empirie, viel Aufräumen, und unvorstellbar langwierige Reflexionen.... +

    +

    + +

    +

    + Das alles ist mir hier erstpart geblieben — und ich nehme das als Wink, daß ich dieser Fährte weiter folgen sollte (und meine Zweifel beiseite legen)

    + + + + +

    + Das sah erst wie ein Hack aus, und wie der Versuch, zwei unzusamenhängende Systeme zusammenzuwürgen. Ich bin dann aber langsam, schrittweise den Widersprüchen nachgegangen, und habe die Struktur rebalanciert. Ich weiß dann gar nicht, wie das gegangen ist, aber plötzlich hatte sich das Design im OutputSlot »gelegt« — und die ganze Struktur (BufferProvider, BufferMetadata, OutputBufferProxy, AllocState) erscheint mir jetzt kohärent und erweiterungsfähig. +

    + +
    +
    - - - + + + + + + @@ -119118,15 +119592,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + +

    + Das war zunächst der Blocker in der OutputSlot-Implementierung; und indem ich die Widersprüche hier aufgelöst habe, hat sich diese Implementierung »gelegt« +

    + +
    - + + - + - + @@ -119135,12 +119620,22 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

    + + + + + +
    +
    + + + +
    -
    - + @@ -119154,7 +119649,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -119173,7 +119668,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -119182,6 +119677,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + @@ -119484,7 +119983,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -119826,7 +120325,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -119880,6 +120379,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + @@ -119902,7 +120404,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121021,8 +121523,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + @@ -121044,13 +121548,14 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - + + + - + + - + @@ -121069,7 +121574,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121120,7 +121625,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121143,8 +121648,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -121446,7 +121951,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -121482,7 +121988,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -121511,9 +122018,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + @@ -121561,8 +122068,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -121654,7 +122162,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121666,7 +122174,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121692,7 +122200,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + @@ -121700,10 +122210,11 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + - + @@ -121712,11 +122223,14 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

    + +
    - - + + + @@ -121754,7 +122268,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -121805,7 +122320,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -121825,8 +122340,14 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + - + + + @@ -121842,8 +122363,41 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -122638,19 +123192,48 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + - - + + + + - - - - + + + + + + + + + + +

    + Der erste Entwurf von 2012 hatte da so einige Funktionalität, von der ich nicht weiß, ob sie was taugt; allein schon die Definition eines »Time Grid« hängt komplett in der Luft. Seinerzeit hatte ich ein TimeGrid-Asset reingezogen, was die Layer-Ordnung überwirft, denn das Thema »Output« gehört ganz klar zu den low-Level-Services, darf sich also nur auf LIbrary-Funktionen abstützen. Tatsächlich werden wir ein Grid in den »Timings« (Constraints) haben, aber das klingt ehr nach einem runtime-Setup für einen konkreten laufenden Ausgabe-Prozeß, und nicht nach einem Modell-Element. +

    +

    + +

    +

    + Ich habe diesen Teil des Entwurfs nun komplett gelöscht, denn ich halte nichts davon, mir hier etwas auszudenken, bevor ich nicht wenigstens eine konkrete laufende Implementierung zustandegebracht habe (seufz) +

    + +
    + +
    - + + + + @@ -122678,8 +123261,22 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + + + + +

    + der OutputBufferProxy ist jetzt nahtlos in die überarbeitete Standard-Implementierung des OutputSlot integriert ... das war sehr mühsam, am Ende ist aber das Design wunderbar aufgegangen +

    + +
    + + + +
    @@ -122690,21 +123287,56 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
    - - - + + + + - - - + + + + + + + + + + + + - + + + + + + + + + +

    + Der Schlüssel war, die Dependency-Ebenen auseinander zu sortieren, und genau zu prüfen, wer wen wie genau getypt sehen muß. Überraschenderweise hat sich dabei der Proxy-Buffer-Provider von einer Not-Kompromiß in ein entscheidendes Design-Element verwandelt: er stellt nämlich jetzt die Verbindung zwischen front-End (DataSink) und Backend (Connection) her. Der OutputSlot::AllocState ist nur noch dafür zuständig, die Connections zu halten/managen, aber partizipiert nicht mehr im eigentlichen Protokoll. Und der Lebenszyklus wird durch das ref-counting-Handle zusammengehalten, das ganz oben, im OutputSlot sitzt, aber zugleich auch in die λ-capture der DataSinks eingebettet ist. +

    +

    + +

    +

    + Im Grunde weiß ich nicht, wie ich da drauf gekommen bin — auf einmal hatte sich alles »gelegt« +

    + +
    + +
    +
    +
    + + @@ -122731,6 +123363,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) +
    @@ -142367,6 +143000,17 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

    + Das hat sich als die entscheidende Gelenkstelle herausgestellt, an der ich beim ersten Entwurf 2012 gescheitert bin; nun habe ich unter der Vorgabe, daß beide Protokolle kongruent gemacht werden müssen, die Implementierungen neu verbunden, und dafür ein Proxy-Setup für den BufferProvider geschaffen. Das war nur möglich auf Basis der vorausgegangenen Überarbeitung der internen Struktur im BufferProvider, und einem behutsamen Glattziehen und Verallgemeinern der Metadataten. Damit wurde der OutputBufferProxy, und das LocalTag in den Metadaten, zu dem Bindeglied, das nun die ganze Struktur zusammenhält +

    + +
    + +
    @@ -142626,14 +143270,16 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - - - + + + + + + + - + @@ -142797,8 +143443,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -142838,7 +143485,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -142883,7 +143530,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -142965,7 +143613,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -143078,7 +143726,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -143223,8 +143871,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -143423,7 +144071,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -144014,9 +144663,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + @@ -144039,10 +144688,16 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + + + + + @@ -144064,15 +144719,68 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -144094,7 +144802,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144135,7 +144843,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144150,6 +144858,37 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

    + Um es explizit auszuformulieren... +

    +
      +
    • + das Test-Setup erzeugt einen DiagnosticOutputSlot +
    • +
    • + dieser ist aber kopierbar, d.h. das zu testende Subjekt kann eine Kopie bekommen +
    • +
    • + u.U wird das zu testende Output-Slot-Protocol auf dieser Kopie ausgeführt +
    • +
    • + nach dem Testaufruf geht man über das Verifikations-API auf irgend eine Kopie los — +
    • +
    • + aber diese Kopie muß natürlich tatsächlich ein DiagnosticOutputSlot sein, (damit sie den shared-PImpl enthält, denn sonst würde man ja an die Tracking-Daten gar nicht mehr rankommen) +
    • +
    • + außerdem muß während dem Test irgendwo diese DiagnosticOutputSlot-Variante bestehen bleiben, um die Tracking-Infrastruktur am Leben zu halten; weil dem so ist, habe ich auch darauf verzichtet, shared-ptr für die Rück-Kanäle in der Tracking-Instrumentierung zu verwenden. Denn letztlich ist das hier Testcode, also wenn's knallt, dann knallts +
    • +
    + +
    + +
    @@ -144176,8 +144915,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
    - - + + + @@ -144190,9 +144930,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + + @@ -144213,7 +144954,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144230,8 +144971,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + @@ -144256,7 +144998,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144285,7 +145027,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -144349,9 +145092,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - + + + @@ -144371,17 +145114,60 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + +

    + wir erzeugen leere diagn::Frame information-Records bei Bedarf, und diese beruhen auf lib::NullValue, werden also (statsich) shared. +

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

    + man muß lediglich einen passenden Populator-Funktor in den Konstruktor geben, der mit der konkreten Connection-Implementierung verdrahtet +

    + +
    +
    + + + + +
    -
    -
    - + + + + - + + @@ -159571,8 +160357,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -172638,8 +173424,8 @@ std::cout << tmpl.render({"what", "World"}) << s - + @@ -208129,8 +208915,8 @@ env.Command('out.bin', 'in.bin', my_action)
- +