LUMIERA.clone/src/steam/engine/node-builder.hpp
Ichthyostega 8a4060861f Invocation: complete simple test case regarding TurnoutSystem
NodeBase_test demonstrates the building blocks of a Render Node,
and verifies low-level mechanics of those building blocks, which
can be quite technical. At the top of this test however are some
very basic interactions, which serve as an introduction.

__Remark__: renamed the low-level technical dispatch-access
for the parameter-accessors in `TurnoutSystem` to be more obvious,
and added comment (I was confused myself how to use them properly)
2025-02-18 23:55:58 +01:00

866 lines
37 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
NODE-BUILDER.hpp - Setup of render nodes connectivity
Copyright (C)
2009, Hermann Vosseler <Ichthyostega@web.de>
2024, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file node-builder.hpp
** Specialised shorthand notation for building the Render Node network.
** During the Builder run, the Render Node network will be constructed by gradually
** refining the connectivity structure derived from interpreting the »high-level Model«
** from the current Session. At some point, it is essentially clear what data streams
** must be produced and what media processing functionality from external libraries
** will be utilised to achieve the goal. This is when the fluent builder notation
** defined in this header comes into play, allowing to package the fine grained and
** in part quite confusing details of parameter wiring and invocation preparation into
** some goal oriented building blocks, that can be combined and directed with greater
** clarity by the control structure to govern the build process.
**
**
** # Levels of connectivity building
**
** The actual node connectivity is established by a process of gradual refinement,
** operating over several levels of abstraction. Each of these levels uses its associated
** builder and descriptor records to collect information, which is then emitted by a
** _terminal invocation_ to produce the result; the higher levels thereby rely on the
** lower levels to fill in and elaborate the details.
** - *Level-1* is the preparation of an actual frame processing operation; the Level-1-builder
** is in fact the implementation class sitting behind a Render Node's _Port._ It is called
** a _Turnout_ and contains a preconfigured »blue print« for the data structure layout
** used for the invocation; its purpose is to generate the actual data structure on the
** stack, holding all the necessary buffers and parameters ready for invoking the external
** library functions. Since the actual data processing is achieved by a _pull processing,_
** originating at the top level exit nodes and propagating down towards the data sources,
** all the data feeds at all levels gradually link together, forming a _TurnoutSystem._
** - *Level-2* generates the actual network of Render Nodes, which in turn will have the
** Turnout instances for Level-1 embedded into their internal ports. Conceptually, a
** _Port_ is where data production can be requested, and the processing will then
** retrieve its prerequisite data from the ports of the _Leads,_ which are the
** prerequisite nodes situated one level below or one step closer to the source.
** - *Level-3* establishes the processing steps and data retrieval links between them;
** at this level, thus the outline of possible processing pathways is established.
** After spelling out the desired connectivity at a high level, the so called »Level-3 build
** walk« is triggered by invoking the [terminal builder operation](\ref ProcBuilder::build()
** on the [processing builder](\ref ProcBuilder) corresponding to the topmost node. This
** build walk will traverse the connectivity graph depth-first, and then start invoking the
** 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
** levels of the build process will generate additional temporary data structures, refined gradually
** until the actual render node network can be emitted. Each builder level can thus be outfitted
** with a custom allocator — typically an instance of lib::AllocationCluster. Notably the higher
** levels can be attached to a separate AllocationCluster instance, which will be discarded once
** the build process is complete, while Level-2 (and below) uses the allocator for the actual
** target data structure, which has to be retained while the render graph is used; more
** specifically until a complete segment of the timeline is superseded and has been re-built.
** @remark syntactically, the custom allocator specification is given after opening a top-level
** builder, by means of the builder function `.withAllocator<ALO> (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.
** \par synthesised additional parameters
** As an extension for (rare and elaborate) special cases, a special evaluation scheme is provided,
** which relies on a »Param Agent Node« as entry point, to invoke additional functors and compute
** additional parameter values, which can then be used in a nested _delegate Node tree._
** This special scheme can be configured from the [nested Port builder](\ref PortBuilderRoot::computeParam()).
** See the detailed description in param-weaving-pattern.hpp
**
** @todo WIP-WIP 1/2025 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.
** @todo as of 1/2025, the Layer-3 builder seems to be beyond the current integration scope. //////////////TICKET #1389 : develop a processing builder
**
** @see steam::engine::NodeFactory
** @see nodewiring.hpp
** @see node-basic-test.cpp
**
*/
#ifndef ENGINE_NODE_BUILDER_H
#define ENGINE_NODE_BUILDER_H
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "steam/engine/weaving-pattern-builder.hpp"
#include "steam/engine/media-weaving-pattern.hpp"
#include "steam/engine/param-weaving-pattern.hpp"
#include "steam/engine/proc-node.hpp"
#include "steam/engine/turnout.hpp"
#include "lib/several-builder.hpp"
#include "lib/format-string.hpp"
#include "lib/index-iter.hpp"
#include <utility>
#include <vector>
namespace steam {
namespace engine {
namespace err = lumiera::error;
using util::_Fmt;
using std::forward;
using std::move;
using std::ref;
namespace { // default policy configuration to use heap allocator
struct UseHeapAlloc
{
template<class I, class E=I>
using Policy = lib::allo::HeapOwn<I,E>;
};
//
}//(End) policy
/**
* A builder to collect working data.
* Implemented through a suitable configuration of lib::SeveralBuilder,
* with a policy configuration parameter to define the allocator to use.
*/
template<class POL, class I, class E=I>
using DataBuilder = lib::SeveralBuilder<I,E, POL::template Policy>;
template<class POL, class DAT>
class NodeBuilder;
template<class POL, class DAT>
class PortBuilderRoot;
/**
* Top-level builder to create a single [Render Node](\ref ProcNode)
* @note
* - Entry-point for the Builder-DSL is \ref prepareNode()
* - A sequence of Ports is defined by `.preparePort()` ... `.completePort()`
* - the Build is completed with the NodeBuilder::build() terminal,
* which generates a \ref Connectivity object, that can be directly
* dropped into the constructor of ProcNode.
*/
template<class POL, class DAT = PatternDataAnchor>
class NodeBuilder
: util::MoveOnly
{
using PortData = DataBuilder<POL, Port>;
using LeadRefs = DataBuilder<POL, ProcNodeRef>;
protected:
StrView symbol_;
LeadRefs leads_;
DAT patternData_;
public:
template<typename...INIT>
NodeBuilder (StrView nodeSymbol, INIT&& ...alloInit)
: symbol_{nodeSymbol}
, leads_{forward<INIT> (alloInit)...}
{ }
template<class BUILD, uint siz, class D0>
NodeBuilder (NodeBuilder<POL,D0>&& pred, SizMark<siz>, BUILD&& entryBuilder)
: symbol_{pred.symbol_}
, leads_{move (pred.leads_)}
, patternData_{move (pred.patternData_), forward<BUILD> (entryBuilder)}
{ }
template<class P, class D0>
friend class NodeBuilder;
NodeBuilder&&
addLead (ProcNode const& lead)
{
leads_.append (ref(lead));
return move(*this);
}
/** recursively enter detailed setup of a single processing port */
PortBuilderRoot<POL,DAT> preparePort();
/**
* cross-builder function to specify usage of a dedicated *node allocator*
* @tparam ALO (optional) spec for the allocator to use
* @tparam INIT (optional) initialisation arguments for the allocator
* @remarks this is a front-end to the extension point for allocator specification
* exposed through lib::SeveralBuilder::withAllocator(). The actual meaning
* of the given parameters and the choice of the actual allocator happens
* through resolution of partial template specialisations of the extension
* point lib::allo::SetupSeveral. Some notable examples
* - withAllocator<ALO>() attaches to a _monostate_ allocator type.
* - `withAllocator<ALO> (ALO<X> allo)` uses a C++ standard allocator
* instance `allo`, dedicated to produce objects of type `X`
* - `withAllocator (AllocationCluster&)` attaches to a specific
* AllocationCluster; this is the most relevant usage pattern.
*/
template<template<typename> class ALO =std::void_t, typename...INIT>
auto
withAllocator (INIT&& ...alloInit)
{
using AllocatorPolicy = lib::allo::SetupSeveral<ALO,INIT...>;
return NodeBuilder<AllocatorPolicy>{symbol_, forward<INIT>(alloInit)...};
}
/************************************************************//**
* Terminal: complete the ProcNode Connectivity defined thus far.
*/
Connectivity
build()
{
PortData ports;
patternData_.collectEntries(ports);
return Connectivity{ports.build()
,leads_.build()
};
}
};
/** Deduction Guide: help the compiler with deducing follow-up NodeBuilder parameters */
template<class POL, class D0, uint siz, class BUILD>
NodeBuilder (NodeBuilder<POL,D0>&&, SizMark<siz>, BUILD&&) -> NodeBuilder<POL, PatternData<D0,BUILD,siz>>;
/**
* Nested DSL builder scope to define a single Port for the enclosing Node.
* @note inherits from NodeBuilder and _slices away_ the subclass when done.
*/
template<class POL, class DAT>
class PortBuilderRoot
: protected NodeBuilder<POL,DAT>
{
public:
NodeBuilder<POL,DAT>
completePort()
{
static_assert(not sizeof(POL),
"can not build a port without specifying a processing function");
}
/** setup standard wiring to adapt the given processing function.
* @return a PortBuilder specialised to wrap the given \a FUN */
template<typename FUN>
auto invoke (StrView portSpec, FUN fun);
/** setup a »ParamAgentNode« to compute additional parameters
* and then delegate into an existing node invocation. */
template<class SPEC>
auto computeParam(SPEC&&);
private:
PortBuilderRoot(NodeBuilder<POL,DAT>&& anchor)
: NodeBuilder<POL,DAT>{move(anchor)}
{ }
friend PortBuilderRoot NodeBuilder<POL,DAT>::preparePort();
};
/**
* @remark while _logically_ this builder-function _descends_ into the
* definition of a port, for the implementation we _wrap_ the existing
* NodeBuilder and layer a PortBuilder subclass „on top“ — thereby shadowing
* the enclosed original builder temporarily; the terminal builder operation
* PortBuilder::completePort() will unwrap and return the original NodeBuilder.
*/
template<class POL, class DAT>
inline PortBuilderRoot<POL, DAT>
NodeBuilder<POL,DAT>::preparePort ()
{
return PortBuilderRoot<POL,DAT>{move(*this)};
}
/**
* Nested DSL-Builder context to define a regular media processing Port
* @remark relies on [delegate sub-builder](\ref WeavingBuilder) for
* technical details of data feed and parameter wiring.
*/
template<class POL, class DAT, class WAB>
class PortBuilder
: public PortBuilderRoot<POL,DAT>
{
using _Par = PortBuilderRoot<POL,DAT>;
WAB weavingBuilder_;
uint defaultPort_;
public:
/** @remark this would allow to pass constructor arguments
* to »buffer inlay objects« automatically placed into media buffers.
* There is a corresponding hook in BufferProvider::getDescriptor(args...)
*/
template<class ILA, typename...ARGS>
PortBuilder&&
createBuffers (ARGS&& ...args)
{
UNIMPLEMENTED ("define builder for all buffers to use");
return move(*this);
}
/** define the output slot number to use as result
* @remark default is to use the first one */
PortBuilder&&
asResultSlot (uint r)
{
weavingBuilder_.selectResultSlot(r);
return move(*this);
}
/** connect the next input slot to existing lead-node given by index
* @note the port to use on this lead is implicitly defaulted to use the same port-number
* as the port which is currently about to be built; this is a common pattern, since
* 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&&
connectLead (uint idx)
{
return connectLeadPort (idx, this->defaultPort_);
}
/** connect the next input slot to either existing or new lead-node" */
PortBuilder&&
connectLead (ProcNode& leadNode)
{
return connectLeadPort (leadNode, this->defaultPort_);
}
/** connect next input to lead-node, using a specific port-number */
PortBuilder&&
connectLeadPort (uint idx, uint port)
{
if (idx >= _Par::leads_.size())
throw err::Logic{_Fmt{"Builder refers to lead-node #%d, yet only %d are currently defined."}
% idx % _Par::leads_.size()
,LERR_(INDEX_BOUNDS)
};
weavingBuilder_.attachToLeadPort (_Par::leads_[idx], port);
return move(*this);
}
/** connect next input to existing or new lead-node, with given port-number */
PortBuilder&&
connectLeadPort (ProcNode& leadNode, uint port)
{
uint knownEntry{0};
for (auto& lead : lib::IndexIter{_Par::leads_}) // leads_ holds ref-wrappers
if (util::isSameObject (leadNode, lead.get()))
break;
else
++knownEntry;
if (knownEntry == _Par::leads_.size())
_Par::addLead (leadNode);
ENSURE (knownEntry < _Par::leads_.size());
return connectLeadPort (knownEntry, port);
}
/** use given port-index as default for all following connections */
PortBuilder&&
useLeadPort (uint defaultPort)
{
this->defaultPort_ = defaultPort;
return move(*this);
}
/**
* Embed the explicitly given parameter-functor into the FeedPrototype,
* so that it will be called on each Node invocation to generate parameters
* to be passed into the actual processing function. The TurnoutSystem acts
* as source for the base coordinates, typically the _absolute nominal Time._
* @return adapted PortBuilder marked with the `FeedPrototype` holding \a PFX
*/
template<class PFX>
auto
attachParamFun (PFX paramFunctor)
{
using AdaptedWeavingBuilder = typename WAB::template Adapted<PFX>;
using AdaptedPortBuilder = PortBuilder<POL,DAT,AdaptedWeavingBuilder>;
//
return AdaptedPortBuilder{move(*this)
,weavingBuilder_.adaptParam (move (paramFunctor))
};
}
/** control parameter(s) by an automation function, based on nominal timeline time */
template<class AUTO>
auto
attachAutomation (AUTO&& aFun)
{
return attachParamFun ([automation = forward<AUTO>(aFun)]
(TurnoutSystem& turnoutSys)
{
return automation (turnoutSys.getNomTime());
});
}
/** embed a fixed value to use for the parameter(s) */
template<typename PAR>
auto
setParam (PAR paramVal)
{
return attachParamFun ([=](TurnoutSystem&) -> PAR
{
return paramVal;
});
}
template<typename PAR, typename...PARS>
auto
setParam (PAR v1, PARS ...vs)
{
return attachParamFun ([=](TurnoutSystem&) -> tuple<PAR,PARS...>
{
return std::make_tuple (v1,vs...);
});
}
/** retrieve the parameter(s) at invocation time through a getter functor,
* which is typically constructed in conjunction with a »Param Agent« node. */
template<typename GET>
auto
retrieveParam (GET&& getter)
{
return attachParamFun ([accessor=forward<GET>(getter)]
(TurnoutSystem& turnoutSys)
{
return turnoutSys.get(accessor);
});
}
template<typename ADA>
auto
adaptParam (ADA&& paramAdaptor)
{
using DecoratedPrototype = typename WAB::template Decorated<ADA>;
using AdaptedPortBuilder = PortBuilder<POL,DAT,DecoratedPrototype>;
//
return AdaptedPortBuilder{move(*this)
,weavingBuilder_.adaptProcFunParam (move (paramAdaptor))
};
}
/** immediately close (≙ fix) some values in a parameter tuple,
* starting from left, while leaving the remaining values open
* to be supplied by automation or another parameter binding. */
template<typename PAR, typename...PARS>
auto
closeParamFront (PAR v1, PARS ...vs)
{
return adaptParam(
WAB::ParamClosure::template closeFront (forward<PAR> (v1)
,forward<PARS>(vs)...));
}
/** immediately close the rightmost parameter positions,
* applying the given values in forward order. */
template<typename PAR, typename...PARS>
auto
closeParamBack (PAR v1, PARS ...vs)
{
return adaptParam(
WAB::ParamClosure::template closeBack (forward<PAR> (v1)
,forward<PARS>(vs)...));
}
/** immediately close a single parameter at designated position
* @tparam idx zero-based index of the element in the param-tuple
*/
template<size_t idx, typename PAR>
auto
closeParam (PAR val)
{
return adaptParam(
WAB::ParamClosure::template close<idx> (forward<PAR> (val)));
}
/*************************************************************//**
* Terminal: complete the Port wiring and return to the node level.
* @remark this prepares a suitable Turnout instance for a port;
* but due to constraints with memory allocation, actual build
* is delayed and packaged as functor into a PatternData instance.
*/
auto
completePort()
{
weavingBuilder_.connectRemainingInputs (_Par::leads_, this->defaultPort_);
return NodeBuilder{static_cast<NodeBuilder<POL,DAT>&&> (*this) // slice away PortBulder subclass data
,weavingBuilder_.sizMark
,weavingBuilder_.build()};
} // chain to builder with extended patternData
private:
template<typename FUN>
PortBuilder(_Par&& base, FUN&& fun, StrView portSpec)
: _Par{move(base)}
, weavingBuilder_{forward<FUN> (fun), _Par::symbol_, portSpec, _Par::leads_.policyConnect()}
, defaultPort_{_Par::patternData_.size()}
{ } // ^^^ by default use next free port
friend class PortBuilderRoot<POL,DAT>;
/** cross-builder to adapt embedded WeavingBuilder type */
template<class WABO>
PortBuilder (PortBuilder<POL,DAT,WABO>&& prevBuilder, WAB&& adaptedWeavingBuilder)
: _Par{move(prevBuilder)}
, weavingBuilder_{move (adaptedWeavingBuilder)}
, defaultPort_{prevBuilder.defaultPort_}
{ }
template<class PX, class DX, class WX>
friend class PortBuilder;
};
/**
* @param qualifier a semantic distinction of the implementation function
* @param fun invocation of the actual _data processing operation._
* @remarks
* - a _»weaving pattern«_ is applied for the actual implementation, which amounts
* to a specific style how to route data input and output and how to actually integrate
* with the underlying media handling library, which exposes the processing functionality.
* - the standard case of this connectivity is to associate input and output connections
* directly with the »parameter slots« of the processing function; a function suitable
* for this pattern takes two arguments (input, output) — each of which is a std::array
* of buffer pointers, corresponding to the »parameter slots«
* - what is bound as \a FUN here thus typically is either an adapter function provided by
* the media-library plug-in, or it is a lambda directly invoking implementation functions
* of the underlying library, using a buffer type (size) suitable for this library and for
* the actual media frame data to be processed.
* - the `fun` is deliberately _taken by-value_ and then moved into a »prototype copy« within
* the generated `Turnout`, from which an actual copy is drawn anew for each node invocation.
* - notably this implies that the implementation code of a lambda will be _inlined_ into the
* actual invocation call, while possibly _creating a copy_ of value-captured closure data;
* this arrangement aims at exposing the actual invocation for the optimiser.
*/
template<class POL, class DAT>
template<typename FUN>
auto
PortBuilderRoot<POL,DAT>::invoke (StrView portSpec, FUN fun)
{
using Prototype = typename FeedManifold<FUN>::Prototype;
using WeavingBuilder_FUN = WeavingBuilder<POL, Prototype>;
return PortBuilder<POL,DAT, WeavingBuilder_FUN>{move(*this), move(fun), portSpec};
}
/**
* Nested sub-Builder analogous to \ref PortBuilder, but for building a _»Param Agent Node«._
* This will compute additional parameters and make them temporarily accessible through the
* TurnoutSystem of the invocation, but only while delegating recursively to another
* computation node, which can then draw upon these additional parameter values.
* @tparam SPEC a ParamBuildSpec, which is a sub-builder to define the parameter-functors
* evaluated on each invocation to retrieve the actual parameter values
*/
template<class POL, class DAT, class SPEC>
class ParamAgentBuilder
: public PortBuilderRoot<POL,DAT>
{
using _Par = PortBuilderRoot<POL,DAT>;
using BlockBuilder = typename SPEC::BlockBuilder;
using PostProcessor = function<void(TurnoutSystem&)>;
BlockBuilder blockBuilder_;
PostProcessor postProcessor_;
Port* delegatePort_;
uint defaultPortNr_;
public:
/** use a lead node designated by ID as delegate to invoke with the extended parameters.
* @note the port to use on this lead is implicitly defaulted to use the same port-number
* as the port which is currently about to be built; this is a common pattern, since
* 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&&
delegateLead (uint idx)
{
return delegateLeadPort (idx, defaultPortNr_);
}
/** use the given node as delegate, but also possibly register it as lead node */
ParamAgentBuilder&&
delegateLead (ProcNode& leadNode)
{
return delegateLeadPort (leadNode, defaultPortNr_);
}
/** use a lead node and specific port as delegate to invoke with extended parameters */
ParamAgentBuilder&&
delegateLeadPort (uint idx, uint port)
{
if (idx >= _Par::leads_.size())
throw err::Logic{_Fmt{"Builder refers to lead-node #%d, yet only %d are currently defined."}
% idx % _Par::leads_.size()
,LERR_(INDEX_BOUNDS)
};
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&&
delegateLeadPort (ProcNode& leadNode, uint port)
{
uint knownEntry{0};
for (auto& lead : lib::IndexIter{_Par::leads_})
if (util::isSameObject (leadNode, lead))
break;
else
++knownEntry;
if (knownEntry == _Par::leads_.size())
_Par::addLead (leadNode);
ENSURE (knownEntry < _Par::leads_.size());
return delegateLeadPort (knownEntry, port);
}
/**
* Install a post-processing function for the parameters.
* This functor will be invoked after the individual parameter values have been created
* by invoking their respective parameter-functor; furthermore, the parameter data block
* in current scope has already been linked with the TurnoustSystem, and thus the new
* parameters are already accessible through this front-end and can be manipulated.
* @remark the purpose is to enable coordinated adjustments on all parameters together,
* immediately before delegating to the nested node evaluation with these parameters.
*/
ParamAgentBuilder&&
installPostProcessor(PostProcessor pp)
{
postProcessor_ = move(pp);
}
/*********************************************************************//**
* Terminal: complete the Param-Agent wiring and return to the node level.
* @remark this prepares a suitable Turnout instance for a port; it will
* actually built later, together with other ports of this Node.
*/
auto
completePort()
{
if (not delegatePort_)
throw err::Logic{"Building a ParamAgentNode requires a delegate node "
"to perform within the scope with extended parameters"
,LERR_(BOTTOM_VALUE)};
string portSpec = "Par+"+delegatePort_->procID.genProcSpec();
ProcAttrib flags;
flags.isProxy = true;
flags.manifold = false;
using WeavingPattern = ParamWeavingPattern<SPEC>;
using TurnoutWeaving = Turnout<WeavingPattern>;
using PortDataBuilder = DataBuilder<POL, Port>;
return NodeBuilder ( static_cast<NodeBuilder<POL,DAT>&&> (*this) // slice away PortBulder subclass data
, SizMark<sizeof(TurnoutWeaving)>{}
,// prepare a builder-λ to construct the actual Turnout-object
[&procID = ProcID::describe(_Par::symbol_,portSpec,flags)
,builder = move(blockBuilder_)
,postProc = move(postProcessor_)
,delegate = delegatePort_
]
(PortDataBuilder& portData) mutable -> void
{
portData.template emplace<TurnoutWeaving> (procID
,move(builder)
,move(postProc)
,*delegate
);
});
} // chain back up to Node-Builder with extended patternData
private:
ParamAgentBuilder(_Par&& base, BlockBuilder&& builder)
: _Par{move(base)}
, blockBuilder_{move(builder)}
, delegatePort_{nullptr}
, defaultPortNr_{_Par::patternData_.size()}
{ } // ^^^ by default use next free port
friend class PortBuilderRoot<POL,DAT>;
};
/** @remarks
* - this is an advanced setup for generating a complex set of _derived parameters,_
* which can then be used by all nodes within a complete subtree of the node-graph.
* - such a setup is not necessary for simple parameters based on nominal timeline time.
* - the purpose is either to avoid redundancy or to draw from additional contextual
* parameter sources (which must be accessible with the help of the processKey or
* some global service or plug-in)
* - another special scenario could be to synthesise further data based on the consolidated
* set of current automation values, possibly together with contextual data; basically
* some kind of _parameter fusion_ that can not reasonably be pre-defined in the
* High-level-Model, but must really be computed late, directly from the render process.
* - this function enters a nested port-builder, which will setup a »Param Weaving Pattern«
* - at Node invocation time, this _Weaving Pattern_ will first evaluate all parameter-funcors,
* then consolidate the generated parameters into a local data block on the stack and link
* this data block into the TurnoutSystem of this invocation; after establishing this
* quite tricky and fragile setup, the invocation will recursively delegate to another
* Node-Port, which thus performs in this extended scope and can refer to all the
* additional parameters.
* - To define the set of parameter-functors, you need to use a helper-builder based on
* \ref steam::engine::ParamBuildSpec, starting with \ref steam::engine::buildParamSpec().
* - this generates a _Param Spec,_ which especially provides _accessor functors_ for each
* of the additional parameters; you need to bind these accessor functors into the
* parameter-functors of nested nodes which want to access the additional parameters.
* - it is thus necessary first to build the _Param Spec,_ then to build the complete
* subtree of processing nodes for the actual processing (aka the _delegate tree_),
* and then finally create a _Param Agent Node_ with this builder, referring to the
* entry point into the processing tree as _delegate lead_ (see \ref ParamAgentBuilder)
* @return a nested \ref ParamAgentBuilder to set up the desired wiring and delegate
* @see NodeFeed_test::feedParamNode()
*/
template<class POL, class DAT>
template<class SPEC>
auto
PortBuilderRoot<POL,DAT>::computeParam(SPEC&& ref)
{
using ParamBuildSpec = std::decay_t<SPEC>;
ParamBuildSpec spec {forward<SPEC>(ref)}; // consumes the spec
return ParamAgentBuilder<POL,DAT,ParamBuildSpec>{move(*this), spec.makeBlockBuilder()};
}
/**
* Entrance point for building actual Render Node Connectivity (Level-2)
* @note when using a custom allocator, the first follow-up builder function
* to apply should be `withAllocator<ALO>(args...)`, prior to adding
* any further specifications and data elements.
*/
inline auto
prepareNode (StrView nodeSymbol)
{
return NodeBuilder<UseHeapAlloc>{nodeSymbol};
}
class ProcBuilder
: util::MoveOnly
{
public:
void //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1389 : return type
requiredSources ()
{
UNIMPLEMENTED ("enumerate all source feeds required");
// return move(*this);
}
void //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1389 : return type
retrieve (void* streamType)
{
(void)streamType;
UNIMPLEMENTED ("recursively define a predecessor feed");
// return move(*this);
}
/****************************************************//**
* Terminal: trigger the Level-3 build walk to produce a ProcNode network.
*/
void //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1389 : return type
build()
{
UNIMPLEMENTED("Level-3 build-walk");
}
};
class LinkBuilder
: util::MoveOnly
{
public:
void //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1389 : return type
from (void* procAsset)
{
(void)procAsset;
UNIMPLEMENTED ("recursively enter definition of processor node to produce this feed link");
// return move(*this);
}
};
/**
* Entrance point for defining data flows and processing steps.
*/
inline auto
retrieve(void* streamType)
{
(void)streamType;
UNIMPLEMENTED("start a connectivity definition at Level-3");
return LinkBuilder{}; //////////////////////////////////////////////////////////////////////////////////TICKET #1389 : this is placeholder code....
}
}} // namespace steam::engine
#endif /*ENGINE_NODE_BUILDER_H*/