Gradually, the header buffhandle.hpp became the minimal include to use basic `BufferProvider` functionality, since `BuffHandle` provides proxy functionality to lock and access a data buffer. The whole compound connected to the `BufferProvider` has evolved into a cohesive yet central connection hub related to buffer memory; it should thus be extracted, moved down to »Vault« layer and used 4as a foundation for various high-level tasks. In preparation of this step, relevant marker typedefs are re-arranged; notably ''Stream Type'' now includes `buffhandle.hpp`
365 lines
14 KiB
C++
365 lines
14 KiB
C++
/*
|
||
PROC-NODE.hpp - Key abstraction of the Render Engine: a Processing Node
|
||
|
||
Copyright (C)
|
||
2008, 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 proc-node.hpp
|
||
** Interface to the processing nodes and the Render Nodes network.
|
||
** The Lumiera Render Engine is based on a graph of interconnected Render Nodes.
|
||
** This »Low-level-Model« is pre-arranged _by a Builder_ as result of compiling
|
||
** and interpreting the arrangement created by the user in the Session, known as
|
||
** »High-level-Model«. All ways to possibly _perform_ (play, render) the current
|
||
** arrangement are thus encoded into the configuration and connectivity of
|
||
** ProcNode elements.
|
||
**
|
||
** # Usage
|
||
**
|
||
** Regarding access, there are three different interfaces to consider
|
||
** - the ProcNode#pull is the invocation interface. It is function-call style
|
||
** - the builder interface, comprising the NodeBuilder (TODO 2024 what else actually?).
|
||
** - the control of playback and rendering processes is accomplished by the Player.
|
||
** For actual processing, Lumiera relies on functionality provided by dedicated
|
||
** domain libraries (e.g. FFmpeg for video processing). A binding implemented
|
||
** as a Lumiera Plug-in will expose such a Library's resources as _Assets_
|
||
** and will set up _function bindings_ to be embedded into Render Nodes.
|
||
**
|
||
** By using the NodeBuilder interface, concrete \ref ProcNode and \ref Port instances
|
||
** are created, interconnected and attached below the Fixture, which is the »backbone«
|
||
** of the low-level-Model. The coordination and the act of invoking this NodeBuilder
|
||
** is conducted by structures in the **Builder** subsystem of Lumiera, which works
|
||
** similar to a compiler for programming languages — with the difference that within
|
||
** this application an edit and media arrangement is compiled into „executable“ form,
|
||
** ready for rendering and performance. In this context _performance_ implies to »play«
|
||
** (render) part of a »Timeline«, which is accomplished through a PlayProcess, which
|
||
** in turn breaks down the work into individual »Render Jobs« organised through the
|
||
** [Scheduler](\ref scheduler.hpp). Through such a sequence of translations, the
|
||
** processing of a frame ends up as a [job to invoke](\ref render-invocation.hpp)
|
||
** some entrance point into the Render Node network.
|
||
**
|
||
** # Arrangement of Render Nodes
|
||
**
|
||
** The arrangement of ProcNode elements in the render graph exhibits a [DAG] topology.
|
||
** Each node _knows only its direct predecessors,_ designated as »Lead Nodes« or »Leads«.
|
||
** Conceptually, each Node represents a specific processing capability, delegating internally
|
||
** to some actual Library implementation of the desired processing algorithm. Yet in reality,
|
||
** several _flavours_ of this processing capability are typically required. For example, maybe
|
||
** sound processing will be expected in stereo format (channel interleaved blocks of audio samples),
|
||
** but in addition also the two individual mono channels will be required independently. Or a video
|
||
** processing pipeline might be required in full resolution, but also sampled down for display in
|
||
** a GUI viewer window or for thumbnail images. The existence of several _flavours of computation_
|
||
** might seem obvious or irrelevant — yet touches on a fundamental decision: in Lumiera,
|
||
** **no media processing happens beyond the Render Nodes.** Even for the down-sampled
|
||
** tiny preview images, a render pipeline is specifically preconfigured, and then
|
||
** exposed through a \ref Port on the Render Node.
|
||
**
|
||
** The actual rendering thus proceeds through the successive activation of Ports. Internally, each
|
||
** Port is connected to _predecessor ports,_ which can be _»pulled«_ to generate the _input data_ for
|
||
** the current processing step. Conducting the invocation of a single processing step in a Port thus
|
||
** requires the interplay of several, intricately interwoven activities — forming a »Weaving Pattern«:
|
||
** Establishing a frame, pulling from predecessors, spawning out further memory buffers to hold computed
|
||
** result data and finally triggering the actual »weft«. The _Port on a Node_ is thus _an interface_,
|
||
** actually implemented by a \ref Turnout, which comprises a _Weaving Pattern Template._ The most
|
||
** common scheme for media processing is embodied by the \ref MediaWeavingPattern template, yet
|
||
** other Weaving Patterns may be configured to adapt to different processing needs (e.g. hardware
|
||
** accelerated computation).
|
||
**
|
||
** Templates in C++ _must be instantiated,_ with arguments specific to the usage. Which would be
|
||
** the signature of the processing function, the types and number of input- and output buffers
|
||
** and an additional tuple of specific parameters to pass, like e.g. the frame number or the
|
||
** (possibly automated) parameter settings for an effect. The invocation of a Port thus calls
|
||
** through a (classical, function-virtual) interface into specific code instantiated within the
|
||
** Library-adapter Plug-in. At compile time, consistency of involved buffer types and function
|
||
** signatures and memory allocation schemes can be ensured, so that no further checks and dynamic
|
||
** adaptation and transformation is necessary at runtime. The engine implementation works on
|
||
** data tuples, typed buffer pointers and helpers checked for memory safety — and not on
|
||
** plain arrays and void pointers.
|
||
** @remark A future extension to this scheme is conceivable, where common processing pipelines
|
||
** are pre-compiled in entirety, possibly combined with hardware acceleration.
|
||
**
|
||
** @todo WIP 2/2025 Node-Invocation is reworked from ground up for the »Playback Vertical Slice«
|
||
**
|
||
** @see turnout.hpp
|
||
** @see turnout-system.hpp
|
||
** @see node-builder.hpp
|
||
** @see node-link.test.cpp
|
||
** [DAG]: https://en.wikipedia.org/wiki/Directed_acyclic_graph
|
||
*/
|
||
|
||
#ifndef STEAM_ENGINE_PROC_NODE_H
|
||
#define STEAM_ENGINE_PROC_NODE_H
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/nocopy.hpp"
|
||
#include "lib/hash-value.h"
|
||
#include "include/limits.hpp"
|
||
#include "steam/engine/buffhandle.hpp"
|
||
#include "steam/engine/turnout-system.hpp"
|
||
#include "lib/format-string.hpp"
|
||
#include "lib/several.hpp"
|
||
|
||
#include <string>
|
||
#include <optional>
|
||
|
||
|
||
|
||
namespace steam {
|
||
namespace engine {
|
||
namespace err = lumiera::error;
|
||
|
||
using std::move;
|
||
using std::string;
|
||
using std::nullopt;
|
||
using std::optional;
|
||
using lib::HashVal;
|
||
using util::_Fmt;
|
||
|
||
class ProcID;
|
||
class ProcNode;
|
||
class ProcNodeDiagnostic;
|
||
|
||
using ProcNodeRef = std::reference_wrapper<ProcNode>;
|
||
using OptionalBuff = std::optional<BuffHandle>;
|
||
|
||
|
||
/** arbitrary safety limit on fain-in / fan-out
|
||
* @note expect lower limits in practice caused by AllocationCluster */
|
||
const size_t MAX_NODE_ARG = LUMIERA_MAX_ORDINAL_NUMBER / 2;
|
||
|
||
|
||
|
||
class Port
|
||
: util::NonCopyable
|
||
{
|
||
public:
|
||
virtual ~Port(); ///< this is an interface
|
||
Port (ProcID& id) : procID{id} { }
|
||
|
||
virtual BuffHandle weave (TurnoutSystem&, OptionalBuff =std::nullopt) =0;
|
||
|
||
ProcID& procID;
|
||
|
||
/// Port has reference semantics: all instances are distinct
|
||
friend bool operator== (Port const& pl, Port const& pr){ return & pl == & pr;}
|
||
friend bool operator!= (Port const& pl, Port const& pr){ return not (pl == pr); }
|
||
};
|
||
|
||
using PortRef = std::reference_wrapper<Port>;
|
||
|
||
/**
|
||
* Interface: Description of the input and output ports,
|
||
* processing function and predecessor nodes for a given ProcNode.
|
||
*
|
||
* @todo the design of this part is messy in several respects.
|
||
* Basically, its left-over from a first prototypical implementation from 2008
|
||
* As of 1/2012, we're re-shaping that engine interface and invocation with a top-down approach,
|
||
* starting from the player. Anyhow, you can expect the basic setup to remain as-is: there will
|
||
* be a ProcNode and a Connectivity descriptor, telling how it's connected to its predecessors,
|
||
* and defining how the Node is supposed to operate
|
||
*
|
||
* @todo WIP-WIP-WIP 2024 Node-Invocation is reworked from ground up for the »Playback Vertical Slice«
|
||
*/
|
||
class Connectivity
|
||
{
|
||
public: /* === public information record describing the node graph === */
|
||
|
||
using Ports = lib::Several<Port>;
|
||
using Leads = lib::Several<ProcNodeRef>;
|
||
|
||
Ports ports;
|
||
Leads leads;
|
||
|
||
Connectivity (Ports&& pr, Leads&& lr)
|
||
: ports(move(pr))
|
||
, leads(move(lr))
|
||
{ }
|
||
|
||
};
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Key abstraction of the Render Engine: A Data processing Node.
|
||
*
|
||
* @todo WIP 2025 Node-Invocation is reworked from ground up for the »Playback Vertical Slice«
|
||
*/
|
||
class ProcNode
|
||
: util::NonCopyable
|
||
{
|
||
|
||
Connectivity wiring_;
|
||
|
||
public:
|
||
ProcNode (Connectivity&& con)
|
||
: wiring_(move(con))
|
||
{ }
|
||
|
||
|
||
Port&
|
||
getPort (uint portIdx)
|
||
{
|
||
if (portIdx >= wiring_.ports.size())
|
||
throw err::Logic{_Fmt{"Accessing node-port #%d, while only %d ports are defined."}
|
||
% portIdx % wiring_.ports.size()
|
||
,LERR_(INDEX_BOUNDS)
|
||
};
|
||
return wiring_.ports[portIdx];
|
||
}
|
||
|
||
/// ProcNode has reference semantics: all instances are distinct
|
||
friend bool operator== (ProcNode const& nl, ProcNode const& nr){ return & nl == & nr;}
|
||
friend bool operator!= (ProcNode const& nl, ProcNode const& nr){ return not (nl == nr); }
|
||
|
||
|
||
/*************************************************************//**
|
||
* Engine Core operation: render and pull output from this node.
|
||
* Activate the indicated Port, which may recursively pull from
|
||
* »Lead Nodes« (predecessors) and invoke the operation embodied
|
||
* by this node. The stack frame of this call acts as anchor
|
||
* to hold the TurnoutSystem for parameter access and coordination.
|
||
* @param portIdx the port (≙flavour) to activate
|
||
* @param output a BuffHandle configured suitably to hold results
|
||
* @param nomTime _absolute nominal Time_ to key this invocation
|
||
* @param procKey additional process key for context parameters
|
||
* @return handle to the buffer containing the calculated result.
|
||
*/
|
||
BuffHandle
|
||
pull (uint portIdx, BuffHandle output, Time nomTime, ProcessKey procKey)
|
||
{
|
||
TurnoutSystem turnoutSystem{nomTime, procKey};
|
||
return getPort(portIdx).weave (turnoutSystem, output);
|
||
}
|
||
|
||
/// „backdoor“ to watch internals from tests
|
||
friend class ProcNodeDiagnostic;
|
||
};
|
||
|
||
|
||
/* ========== Diagnostic and Testing ========== */
|
||
|
||
class PortDiagnostic;
|
||
|
||
class ProcNodeDiagnostic
|
||
: util::MoveOnly
|
||
{
|
||
ProcNode& n_;
|
||
|
||
public:
|
||
ProcNodeDiagnostic (ProcNode& theNode)
|
||
: n_{theNode}
|
||
{ }
|
||
|
||
auto& leads() { return n_.wiring_.leads; }
|
||
auto& ports() { return n_.wiring_.ports; }
|
||
|
||
bool isSrc() { return n_.wiring_.leads.empty(); }
|
||
|
||
bool
|
||
isValid()
|
||
{
|
||
return 0 < ports().size();
|
||
///////////////////////////////////////////////////TODO 10/2024 more to verify here
|
||
}
|
||
|
||
string getNodeName(); ///< show the node's descriptive name
|
||
string getNodeSpec(); ///< generate a descriptive Spec of this ProcNode for diagnostics
|
||
HashVal getNodeHash(); ///< calculate an unique hash-key to designate this node
|
||
|
||
string getPortSpec (uint portIdx); ///< generate a descriptive diagnostic Spec for the designated Turnout
|
||
HashVal getPortHash (uint portIdx); ///< calculate an unique, stable and reproducible hash-key to identify the Turnout
|
||
|
||
ProcNodeDiagnostic watchLead(uint leadIdx);
|
||
PortDiagnostic watchPort(uint portIdx);
|
||
};
|
||
|
||
inline ProcNodeDiagnostic
|
||
watch (ProcNode& theNode)
|
||
{
|
||
return ProcNodeDiagnostic{theNode};
|
||
}
|
||
|
||
|
||
|
||
class PortDiagnostic
|
||
: util::MoveOnly
|
||
{
|
||
Port& p_;
|
||
|
||
public:
|
||
PortDiagnostic (Port& thePort)
|
||
: p_{thePort}
|
||
{ }
|
||
|
||
lib::Several<PortRef> const& srcPorts();
|
||
|
||
bool isSrc() { return srcPorts().empty(); }
|
||
|
||
string getProcName(); ///< generate a combined name for the node and the qualification of the port
|
||
string getProcSpec(); ///< generate a descriptive diagnostic Spec for the Turnout sitting behind this Port
|
||
HashVal getProcHash(); ///< calculate an unique, stable and reproducible hash-key to identify the associated operation
|
||
|
||
PortDiagnostic watchLead(uint leadIdx);
|
||
|
||
bool verify_connected (uint input, Port&);
|
||
bool verify_connected (Port&);
|
||
};
|
||
|
||
inline PortDiagnostic
|
||
watch (Port& thePort)
|
||
{
|
||
return PortDiagnostic{thePort};
|
||
}
|
||
|
||
|
||
|
||
|
||
/** Helper for connectivity-checks in tests */
|
||
class _ConCheck
|
||
: util::MoveOnly
|
||
{
|
||
ProcNodeDiagnostic anchor;
|
||
ProcNode* srcNode{nullptr};
|
||
Port* srcPort{nullptr};
|
||
optional<uint> leadNo{nullopt};
|
||
optional<uint> portNo{nullopt};
|
||
optional<uint> srcNo {nullopt};
|
||
optional<uint> srcPNo{nullopt};
|
||
|
||
public:
|
||
_ConCheck (ProcNode& n)
|
||
: anchor{watch(n)}
|
||
{ }
|
||
|
||
/** implement decision logic based on context given. */
|
||
operator bool();
|
||
|
||
_ConCheck to (ProcNode& n) { srcNode = &n; return move(*this); }
|
||
_ConCheck to (Port& p) { srcPort = &p; return move(*this); }
|
||
_ConCheck asLead (uint idx){ leadNo = idx; return move(*this); }
|
||
_ConCheck toLead (uint idx){ leadNo = idx; return move(*this); }
|
||
_ConCheck asSrc (uint idx){ srcNo = idx; return move(*this); }
|
||
_ConCheck port (uint idx){ portNo = idx; return move(*this); }
|
||
_ConCheck atPort (uint idx){ srcPNo = idx; return move(*this); }
|
||
};
|
||
|
||
/**
|
||
* start a DSL expression to verify node connectivity.
|
||
* @see NodeMeta_test::verify_ID_connectivity()
|
||
*/
|
||
inline _ConCheck
|
||
is_linked (ProcNode& n)
|
||
{
|
||
return _ConCheck{n};
|
||
}
|
||
|
||
|
||
}} // namespace steam::engine
|
||
#endif /*STEAM_ENGINE_PROC_NODE_H*/
|