From 16a6a0d630c6f29b7a9c853d4ff698701f28444e Mon Sep 17 00:00:00 2001
From: Ichthyostega
Date: Sat, 4 Jan 2025 19:28:58 +0100
Subject: [PATCH] Invocation: integration test to use the Param Agent Node
Builder
incidentally, this is also the first test case ever to involve linked nodes,
so it revealed several bugs in the related code, which was not yet tested.
This is a ''move-builder'' and thus represents a tricky and sometimes dangerous setup,
while allowing to switch the type context in the middle of the build process.
It is essential to return a RValue-Reference from all builder calls which
stay on the same builder context.
After fixing those minor (and potentially dangerous) aspects regarding move-references,
the code built yesterday worked as expected!
---
src/steam/engine/node-builder.hpp | 68 +++++++---
src/steam/engine/param-weaving-pattern.hpp | 37 +++---
tests/core/steam/engine/node-feed-test.cpp | 138 ++++++++++++++-------
wiki/thinkPad.ichthyo.mm | 93 +++++++++++---
4 files changed, 240 insertions(+), 96 deletions(-)
diff --git a/src/steam/engine/node-builder.hpp b/src/steam/engine/node-builder.hpp
index b7655ff8c..8de8ea001 100644
--- a/src/steam/engine/node-builder.hpp
+++ b/src/steam/engine/node-builder.hpp
@@ -54,7 +54,6 @@
** Level-2 builder operations bottom-up to generate and wire up the corresponding Render Nodes.
**
** ## Using custom allocators
- **
** Since the low-level-Model is a massive data structure comprising thousands of nodes, each with
** specialised parametrisation for some media handling library, and a lot of cross-linking pointers,
** it is important to care for efficient usage of memory with good locality. Furthermore, the higher
@@ -68,6 +67,35 @@
** @remark syntactically, the custom allocator specification is given after opening a top-level
** builder, by means of the builder function `.withAllocator (args...)`
**
+ **
+ ** # Building Render Nodes
+ **
+ ** At Level-2, actual render nodes are generated. The NodeBuilder creates a suitably configured
+ ** \ref Connectivity object, which can be dropped directly into a ProcNode. Managing the storage
+ ** of those Render Nodes themselves is beyond the scope of the builder; so the user of the builder
+ ** is responsible for the lifecycle of generated ProcNode objects.
+ **
+ ** ## Flavours of the processing function
+ ** The binding to the actual data processing operations (usually supplied by an external library)
+ ** is established by a **processing-functor** passed to configure the [Port builder](\PortBuilderRoot::invoke()).
+ ** The supported signatures of this functor are quite flexible to allow for various flavours of invocation.
+ ** Data types of parameters and buffers are picked up automatically (at compile time), based on the
+ ** signature of the actual function supplied. The accepted variations are described in detail
+ ** [here](\ref feed-manifold.hpp). Basically, a function can take parameters, input- and output-buffers,
+ ** yet only the output-buffers are mandatory. Several elements of one kind can be passed as tuple.
+ **
+ ** ## Handling of Invocation Parameters
+ ** Typically, a processing operation can be configured in various ways, by passing additional
+ ** setup- and invocation parameters. This entails both technical aspects (like picking some specific
+ ** data format), organisational concerns (like addressing a specific frame-number) and elements of
+ ** artistic control, like choosing the settings of a media processing effect. Parameters will thus
+ ** be collected from various sources, which leads to an additional binding step, where all these
+ ** sources are retrieved and the actual parameter value or value tuple is produced. This specific
+ ** _parameter binding_ is represented as a **parameter-functor**. Whenever the processing-function
+ ** accepts a parameter argument, optionally a such parameter-functor can be installed; this functor
+ ** is supplied with the \ref TurnoutSystem of the actual invocation, which acts as front-end to
+ ** access contextual parameters.
+ **
** @todo WIP-WIP-WIP 10/2024 Node-Invocation is reworked from ground up -- some parts can not be
** spelled out completely yet, since we have to build this tightly interlocked system of
** code moving bottom up, and then filling in further details later working top-down.
@@ -105,6 +133,7 @@ namespace engine {
using util::_Fmt;
using std::forward;
using std::move;
+ using std::ref;
namespace { // default policy configuration to use heap allocator
@@ -168,10 +197,10 @@ namespace engine {
friend class NodeBuilder;
- NodeBuilder
+ NodeBuilder&&
addLead (ProcNode const& lead)
{
- leads_.append (lead);
+ leads_.append (ref(lead));
return move(*this);
}
@@ -284,7 +313,7 @@ namespace engine {
public:
template
- PortBuilder
+ PortBuilder&&
createBuffers (ARGS&& ...args)
{
UNIMPLEMENTED ("define builder for all buffers to use");
@@ -293,7 +322,7 @@ namespace engine {
/** define the output slot to use as result
* @remark default is to use the first one */
- PortBuilder
+ PortBuilder&&
asResultSlot (uint r)
{
weavingBuilder_.selectResultSlot(r);
@@ -306,21 +335,21 @@ namespace engine {
* when a top-level node exposes N different flavours, its predecessors will very
* likely also be configured to produce the pre-product for these flavours.
*/
- PortBuilder
+ PortBuilder&&
connectLead (uint idx)
{
return connectLeadPort (idx, this->defaultPort_);
}
/** connect the next input slot to either existing or new lead-node" */
- PortBuilder
+ PortBuilder&&
conectLead (ProcNode& leadNode)
{
return connectLeadPort (leadNode, this->defaultPort_);
}
/** connect next input to lead-node, using a specific port-number */
- PortBuilder
+ PortBuilder&&
connectLeadPort (uint idx, uint port)
{
if (idx >= _Par::leads_.size())
@@ -333,7 +362,7 @@ namespace engine {
}
/** connect next input to existing or new lead-node, with given port-number */
- PortBuilder
+ PortBuilder&&
connectLeadPort (ProcNode& leadNode, uint port)
{
uint knownEntry{0};
@@ -350,7 +379,7 @@ namespace engine {
}
/** use given port-index as default for all following connections */
- PortBuilder
+ PortBuilder&&
useLeadPort (uint defaultPort)
{
this->defaultPort_ = defaultPort;
@@ -502,21 +531,21 @@ namespace engine {
* when a top-level node exposes N different flavours, its predecessors will very
* likely also be configured to produce the pre-product for these flavours.
*/
- ParamAgentBuilder
+ ParamAgentBuilder&&
delegateLead (uint idx)
{
return delegateLeadPort (idx, defaultPortNr_);
}
/** use the given node as delegate, but also possibly register it as lead node */
- ParamAgentBuilder
+ ParamAgentBuilder&&
delegateLead (ProcNode& leadNode)
{
return delegateLeadPort (leadNode, defaultPortNr_);
}
/** use a lead node and specific port as delegate to invoke with extended parameters */
- ParamAgentBuilder
+ ParamAgentBuilder&&
delegateLeadPort (uint idx, uint port)
{
if (idx >= _Par::leads_.size())
@@ -524,13 +553,14 @@ namespace engine {
% idx % _Par::leads_.size()
,LERR_(INDEX_BOUNDS)
};
- delegatePort_ = & _Par::leads_[idx].getPort (port);
+ ProcNode& leadNode = _Par::leads_[idx];
+ delegatePort_ = & leadNode.getPort (port);
return move(*this);
}
/** use the specific port on the given node as delegate,
* while possibly also registering it as lead node. */
- ParamAgentBuilder
+ ParamAgentBuilder&&
delegateLeadPort (ProcNode& leadNode, uint port)
{
uint knownEntry{0};
@@ -556,7 +586,7 @@ namespace engine {
* @remark the purpose is to enable coordinated adjustments on all parameters together,
* immediately before delegating to the nested node evaluation with these parameters.
*/
- ParamAgentBuilder
+ ParamAgentBuilder&&
installPostProcessor(PostProcessor pp)
{
postProcessor_ = move(pp);
@@ -600,7 +630,6 @@ namespace engine {
} // chain back up to Node-Builder with extended patternData
private:
- template
ParamAgentBuilder(_Par&& base, BlockBuilder&& builder)
: _Par{move(base)}
, blockBuilder_{move(builder)}
@@ -645,10 +674,11 @@ namespace engine {
template
template
auto
- PortBuilderRoot::computeParam(SPEC&& spec)
+ PortBuilderRoot::computeParam(SPEC&& ref)
{
using ParamBuildSpec = std::decay_t;
- return ParamAgentBuilder{spec.makeBlockBuilder()};
+ ParamBuildSpec spec {forward(ref)}; // consumes the spec
+ return ParamAgentBuilder{move(*this), spec.makeBlockBuilder()};
}
diff --git a/src/steam/engine/param-weaving-pattern.hpp b/src/steam/engine/param-weaving-pattern.hpp
index adb3a9d4d..69537610d 100644
--- a/src/steam/engine/param-weaving-pattern.hpp
+++ b/src/steam/engine/param-weaving-pattern.hpp
@@ -15,16 +15,16 @@
/** @file param-weaving-pattern.hpp
** Construction kit to establish a set of parameters pre-computed prior to invocation
** of nested nodes. This arrangement is also known as »Parameter Agent Node« (while actually
- ** it is a Weaving Patter residing within some Node's Port). The use-case is to provide a set
+ ** it is a Weaving Pattern residing within some Node's Port). The use-case is to provide a set
** of additional parameter values, beyond what can be derived directly by a parameter-functor
** based on the _absolute-nominal-Time_ of the invocation. The necessity for such a setup may
** arise when additional context or external state must be combined with the nominal time into
** a tuple of data values, which shall then be consumed by several follow-up evaluations further
** down into a recursive invocation tree _for one single render job._ The solution provided by
- ** the Parameter Agent Node relies on placing those additional data values into a tuple stored
- ** directly in the render invocation stack frame, prior to descending into further recursive
- ** Node evaluations. Notably, parameter-functors within the scope of this evaluation tree can
- ** then access these additional parameters through the TurnoutSystem of the overall invocation.
+ ** the Parameter Agent Node relies on placing those additional data values into a tuple, which
+ ** is then stored directly in the render invocation stack frame, prior to descending into further
+ ** recursive Node evaluations. Notably, parameter-functors within the scope of this evaluation tree
+ ** can then access these additional parameters through the TurnoutSystem of the overall invocation.
**
** @see node-builder.hpp
** @see weaving-pattern-builder.hpp
@@ -42,7 +42,6 @@
#include "steam/common.hpp"
#include "steam/engine/turnout.hpp"
#include "steam/engine/turnout-system.hpp"
-#include "steam/engine/feed-manifold.hpp" ////////////TODO wegdamit
#include "lib/uninitialised-storage.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/meta/tuple-helper.hpp"
@@ -65,16 +64,17 @@ namespace engine {
using std::function;
using std::make_tuple;
using std::tuple;
- using lib::Several;////TODO RLY?
+ using lib::meta::Tuple;
+ using lib::meta::ElmTypes;
- template
+ template
struct ParamBuildSpec
{
using Functors = tuple;
- using ResTypes = typename lib::meta::ElmTypes::template Apply;
- using ParamTup = lib::meta::Tuple;
+ using ResTypes = typename ElmTypes::template Apply;
+ using ParamTup = Tuple;
Functors functors_;
@@ -82,12 +82,17 @@ namespace engine {
: functors_{move (funz)}
{ }
+ /** can be copied if all functors are copyable... */
+ ParamBuildSpec clone() { return *this; }
+
+
template
auto
addSlot (FUN&& paramFun)
{
- return ParamBuildSpec{std::tuple_cat (move(functors_)
- ,make_tuple (forward(paramFun)))};
+ using FunN = std::decay_t;
+ return ParamBuildSpec{std::tuple_cat (move(functors_)
+ ,make_tuple (forward(paramFun)))};
}
template
@@ -112,14 +117,13 @@ namespace engine {
* @remark HeteroData defines a nested struct `Chain`, and with the help of `RebindVariadic`,
* the type sequence from the ParamTup can be used to instantiate this Chain context.
*/
- using ChainCons = typename lib::meta::RebindVariadic::Type;
+ using ChainCons = typename lib::meta::RebindVariadic::Type;
/** a (static) getter functor able to work on the full extended HeteroData-Chain
* @remark the front-end of this chain resides in TurnoutSystem */
template
struct Accessor
- : util::MoveOnly
{
static auto&
getParamVal (TurnoutSystem& turnoutSys)
@@ -233,7 +237,7 @@ namespace engine {
/** Preparation: create a Feed data frame to use as local scope */
Feed
- mount (TurnoutSystem& turnoutSys)
+ mount (TurnoutSystem&)
{
return Feed{};
}
@@ -265,6 +269,7 @@ namespace engine {
weft (Feed& feed, TurnoutSystem& turnoutSys)
{
feed.outBuff = delegatePort_.weave (turnoutSys, feed.outBuff);
+ ENSURE (feed.outBuff);
}
/** clean-up: detach the parameter-data-block.
@@ -274,7 +279,7 @@ namespace engine {
fix (Feed& feed, TurnoutSystem& turnoutSys)
{
turnoutSys.detachChainBlock(feed.block());
- return feed.outBuff;
+ return *feed.outBuff;
}
};
diff --git a/tests/core/steam/engine/node-feed-test.cpp b/tests/core/steam/engine/node-feed-test.cpp
index 72a6fd15f..623a1dd8f 100644
--- a/tests/core/steam/engine/node-feed-test.cpp
+++ b/tests/core/steam/engine/node-feed-test.cpp
@@ -39,6 +39,8 @@ using lib::time::Time;
using lib::time::FSecs;
using lib::time::FrameNr;
using lib::test::showType;
+using std::make_tuple;
+using std::get;
namespace steam {
@@ -109,6 +111,7 @@ namespace test {
}
+
/** @test create extended parameter data for use in recursive Node invocation.
* - demonstrate the mechanism of param-functor invocation,
* and how a Param-Spec is built to create and hold those functors
@@ -122,71 +125,120 @@ namespace test {
void
feedParamNode()
{
- steam::asset::meta::TimeGrid::build("grid_sec", 1);
+ // Assuming that somewhere in the system a 1-seconds time grid was predefined...
+ steam::asset::meta::TimeGrid::build ("grid_sec", 1);
- // Parameter-functor based on time-quantisation into a 1-seconds-grid
- auto fun1 = [](TurnoutSystem& turSys)
- {
- return FrameNr::quant (turSys.getNomTime(), "grid_sec");
- };
+ //_______________________________________________
+ // Demo-1: demonstrate the access mechanism directly;
+ // create and link an extended parameter block.
- // The Param-Spec is used to coordinate type-safe access
+ // This test will create an extension data block with two parameters,
+ // one of these is generated from time-quantisation into a 1-seconds-grid
+ auto createParmFun = [](TurnoutSystem& turnoutSys) -> long
+ {
+ return FrameNr::quant (turnoutSys.getNomTime(), "grid_sec");
+ };
+
+ // The »Param-Spec« is used to coordinate type-safe access
// and also is used as a blueprint for building a Param(Agent)Node
+ // Note the builder syntax to add several parameter »slots«...
auto spec = buildParamSpec()
.addValSlot (LIFE_AND_UNIVERSE_4EVER)
- .addSlot (move (fun1))
+ .addSlot (createParmFun)
;
- // The implied type of the parameter-tuple to generate
+ // Implied type of the parameter-tuple to generate
using ParamTup = decltype(spec)::ParamTup;
CHECK (showType() == "tuple"_expect);
- // can now store accessor-functors for later use....
- auto acc0 = spec.makeAccessor<0>();
+ auto acc0 = spec.makeAccessor<0>(); // can now store accessor-functors for later use....
auto acc1 = spec.makeAccessor<1>();
- // drive test with a random »nominal Time« <10s with ms granularity
- Time nomTime{rani(10'000),0};
- TurnoutSystem turnoutSys{nomTime};
- // can now immediately invoke the embedded parameter-functors
- auto v0 = spec.invokeParamFun<0> (turnoutSys);
+ // Prepare for invocation....
+ Time nomTime{rani(10'000),0}; // drive test with a random »nominal Time« <10s with ms granularity
+ TurnoutSystem turnoutSys{nomTime}; // build minimal TurnoutSystem for invocation, just with this time parameter
+ auto v0 = spec.invokeParamFun<0> (turnoutSys); // can now immediately invoke the embedded parameter-functors
auto v1 = spec.invokeParamFun<1> (turnoutSys);
- CHECK (v0 == LIFE_AND_UNIVERSE_4EVER); // ◁————————— the first paramFun yields the configured fixed value
- CHECK (v1 == FrameNr::quant (nomTime, "grid_sec")); // ◁————————— the second paramFun accesses the time in TurnoutSystem
-
+ CHECK (v0 == LIFE_AND_UNIVERSE_4EVER); // ◁————————— the first paramFun yields the configured fixed value
+ CHECK (v1 == FrameNr::quant (nomTime, "grid_sec")); // ◁————————— the second paramFun accesses the time via TurnoutSystem
+
+
// after all setup of further accessor functors is done
- // finally transform the ParamSpec into a storage-block-builder:
- auto blockBuilder = spec.makeBlockBuilder();
+ // finally transform the ParamSpec into a storage-block-builder
+ auto blockBuilder = spec.clone().makeBlockBuilder(); // (use clone() since we're re-using the same spec in Demo-2 below)
{ // Now build an actual storage block in local scope,
// thereby invoking the embedded parameter-functors...
auto paramBlock = blockBuilder.buildParamDataBlock (turnoutSys);
- // Values are now materialised into paramBlock
- CHECK (v0 == paramBlock.get<0>());
+ CHECK (v0 == paramBlock.get<0>()); // Values are now materialised into paramBlock
CHECK (v1 == paramBlock.get<1>());
- // link this extension block into the parameter-chain in TurnoutSystem
- turnoutSys.attachChainBlock(paramBlock);
+ turnoutSys.attachChainBlock(paramBlock); // link this extension block into the parameter-chain in TurnoutSystem;
+ CHECK (v0 == acc0.getParamVal (turnoutSys)); // Can now access the parameter values through the TurnoutSystem as front-End
+ CHECK (v1 == acc1.getParamVal (turnoutSys)); // ...using the pre-configured accessor-functors stored above
- // can now access the parameter values through the TurnoutSystem as front-End
- // using the pre-configured accessor-functors stored above
- CHECK (v0 == acc0.getParamVal (turnoutSys));
- CHECK (v1 == acc1.getParamVal (turnoutSys));
-
- // should detach extension block before leaving scope
- turnoutSys.detachChainBlock(paramBlock);
- }
-
- using Spec = decltype(spec);
- using WaPa = ParamWeavingPattern;
- using Feed = WaPa::Feed;
+ turnoutSys.detachChainBlock(paramBlock); // should detach extension block before leaving scope
+ }// extension block is gone...
- Feed feed;
- feed.emplaceParamDataBlock (blockBuilder, turnoutSys);
-SHOW_EXPR(feed.buffer[0].get<0>())
-SHOW_EXPR(feed.buffer[0].get<1>())
- TODO ("implement a simple Builder for ParamAgent-Node");
- TODO ("then use both together to demonstrate a param data feed here");
+ { // Demonstrate the same access mechanism
+ // but integrated into a Weaving-Pattern
+ using Spec = decltype(spec);
+ using WeavingPattern = ParamWeavingPattern;
+ using Feed = WeavingPattern::Feed;
+
+ Feed feed;
+ feed.emplaceParamDataBlock (blockBuilder, turnoutSys);
+ // note that the param-data-block is embedded into the feed,
+ // so that it can be easily placed into the current stack frame
+ CHECK (v0 == feed.block().get<0>());
+ CHECK (v1 == feed.block().get<1>());
+ }
+
+
+
+ //_________________________________________________
+ // Demo-2: perform exactly the same access scheme,
+ // but now embedded into a Render Node graph.
+ using Param = tuple;
+
+ // The processing function uses two parameter values
+ auto processFun = [](Param par, long* buff)
+ {
+ *buff = get<0>(par) + get<1>(par);
+ };
+ // These parameter values are picked up from the extended TurnoutSystem,
+ // relying on the accessor objects, which were created from the ParamSpec
+ auto accessParam = [acc0,acc1]
+ (TurnoutSystem& turnoutSys) -> Param
+ {
+ return make_tuple (acc0.getParamVal (turnoutSys)
+ ,acc1.getParamVal (turnoutSys));
+ };
+
+ ProcNode delegate{prepareNode("Delegate")
+ .preparePort()
+ .invoke("proc()", processFun)
+ .attachParamFun (accessParam)
+ .completePort()
+ .build()};
+
+ ProcNode paramAgent{prepareNode("Param")
+ .preparePort()
+ .computeParam (move(spec))
+ .delegateLead (delegate) // ◁————————— linked to the Delegate-Node
+ .completePort()
+ .build()};
+
+ // Prepare result buffer for invocation
+ BufferProvider& provider = DiagnosticBufferProvider::build();
+ BuffHandle buff = provider.lockBufferFor (-55);
+ CHECK (-55 == buff.accessAs());
+
+ // Invoke Port#0 on the top-level Node (≙ the ParamAgent)
+ buff = paramAgent.getPort(0).weave(turnoutSys, buff); // ◁————————— generate Param-Values, link into TurnoutSystem, invoke Delegate
+ CHECK (v0+v1 == buff.accessAs());
+
+ buff.release();
}
};
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index 1ba65f3ae..f1d5123d7 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -98065,16 +98065,44 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
-
-
+
+
-
-
-
+
+
+
+
-
+
+
+
+
+
+ dann exakt das gleiche Setup
+
+
+ per ParamAgentBuilder konstruieren
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -98083,7 +98111,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
-
+
@@ -98111,8 +98139,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
...die dann später den eigentlichen Turnout produziert und in den DataBuilder für die Node-Ports „abwirft“
-
-
+
@@ -98157,9 +98184,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
-
-
-
+
+
+
@@ -98167,17 +98194,47 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
rein nach Definition sollte es bereits funktionieren
-
-
+
-
+
+
+
+
+
+
+ naja ... einige technische Kleinigkeiten haben noch geklemmt
+
+
+
+
+
-
+
+
+
+
+
+ ...bisweilen hat man eben doch einen Builder (z.B. wie hier, für Operationen in dem Builder selbst). Dann kann es ggfs gefährlich sein, eine Operation auf dem Builder aufzurufen, wenn danach ein neues Objekt konstruiert wird. Wir müssen ein neues Objekt konstruieren bei jedem Cross-Builder-Schritt, d.h. diese Gefahr läßt sich nicht bannen. Die normalen Operationen jedoch können eine RValue-Referenz zurückgeben (analog zu lib::SeveralBuilder). Denn eine RValue-Referenz ist auch eine generelle Referen, und erlaubt den Aufruf von Member-Funktionen, so daß dann deren Rückgabewert eigentlich bestimmt, was gebaut wird. Das ist kniffelig und kapp am Abgrund, aber nur durch einen solchen RValue-Builder sind Cross-Builder mit Weiterentwicklung der Typ-Parameter im Build-Prozeß möglich
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+