lumiera_documentation/tests/core/steam/engine/node-builder-test.cpp
Ichthyostega 1ebefbe36e BufferProvider: identify usages of the default BufferProvider
The prototype implementations of BuferProvider and OutputSlot are confusing;
in-depth analysis shows that this is related to intermingling use
of test instrumentation and features of a prototypical default.

To get anywhere with the existing code, it seems we'd need to split off
the test instrumentation (i.e. `DiagnosticBufferProvider`) altogether,
even if this causes duplication of implementation code. Many years ago,
when first implementing this prototype, I had no clear idea what the usage
in the Render Engine entails, yet I noticed that some basic buffer »tracking«
could be used as implementation primitive to simplify the prototyping work.
This might be one of the reasons, why the "code behind" in the protocol
implementations both in `BufferProvider` and `OutputSlot` became so strongly
tangled, so that I way unable to make both protocols orthogonally composable.
2026-02-09 00:59:35 +01:00

355 lines
14 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.

/*
NodeBuilder(Test) - creation and setup of render nodes
Copyright (C)
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-test.cpp
** Unit test \ref NodeBuilder_test demonstrates how to build render nodes.
*/
#include "lib/test/run.hpp"
#include "steam/engine/engine-ctx.hpp"
#include "steam/engine/node-builder.hpp"
#include "steam/asset/meta/time-grid.hpp"
#include "lib/test/diagnostic-output.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/iter-explorer.hpp"
#include "lib/symbol.hpp"
#include <array>
#include <boost/lexical_cast.hpp>
using lib::Symbol;
using std::string;
using std::array;
using lib::explore;
using lib::time::Time;
using lib::time::QuTime;
using lib::time::FrameNr;
using lib::time::SmpteTC;
namespace steam {
namespace engine{
namespace test {
namespace {
Symbol SECONDS_GRID = "grid_sec";
}
/***************************************************************//**
* @test creating and configuring various kinds of Render Nodes.
*/
class NodeBuilder_test : public Test
{
virtual void
run (Arg)
{
seedRand(); // used for simple time-based „automation“
steam::asset::meta::TimeGrid::build (SECONDS_GRID, 1);
build_simpleNode();
build_Node_fixedParam();
build_Node_dynamicParam();
build_Node_adaptedParam();
build_Node_closedParam();
build_connectedNodes();
build_ParamNode();
}
/** @test build a simple output-only Render Node
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_simpleNode()
{
auto fun = [](uint* buff){ *buff = LIFE_AND_UNIVERSE_4EVER; };
ProcNode node{prepareNode("Test")
.preparePort()
.invoke("fun()", fun)
.completePort()
.build()};
CHECK (watch(node).isSrc());
CHECK (watch(node).ports().size() == 1);
CHECK (LIFE_AND_UNIVERSE_4EVER == invokeRenderNode (node));
}
/**
* @internal Helper for Render Node invocation
* - use default config of EngineCtx (≙test setup) to allocate a result buffer
* - assuming that the Node internally does not allocate further buffers
* - pull from Port #0 of the given node, passing the \a nomTime as argument
* - expect the buffer to hold a single `uint` value after invocation
*/
uint
invokeRenderNode (ProcNode& theNode, Time nomTime =Time::ZERO)
{
BuffHandle buff = EngineCtx::access().mem.lockBufferFor<long> (-55);
ProcessKey key{0};
uint port{0};
CHECK (-55 == buff.accessAs<long>());
// Trigger Node invocation...
buff = theNode.pull (port, buff, nomTime, key);
uint result = buff.accessAs<uint>();
buff.release();
return result;
}
/** @test build a Node with a fixed invocation parameter
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_Node_fixedParam()
{
auto procFun = [](ushort param, uint* buff){ *buff = param; };
ProcNode node{prepareNode("Test")
.preparePort()
.invoke ("fun()", procFun)
.setParam (LIFE_AND_UNIVERSE_4EVER)
.completePort()
.build()};
CHECK (LIFE_AND_UNIVERSE_4EVER == invokeRenderNode (node));
}
/** @test build a Node with dynamically generated parameter
* - use a processing function which takes a parameter
* - use an _automation functor,_ which just quantises
* the time into an implicitly defined grid
* - install both into a render node
* - set a random _nominal time_ for invocation
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_Node_dynamicParam()
{
auto procFun = [](long param, int* buff){ *buff = int(param); };
auto autoFun = [](Time nomTime){ return FrameNr::quant (nomTime, SECONDS_GRID); };
ProcNode node{prepareNode("Test")
.preparePort()
.invoke ("fun()", procFun)
.attachAutomation (autoFun)
.completePort()
.build()};
// invoke with a random »nominal Time« <10s with ms granularity
Time theTime{rani(10'000),0};
int res = invokeRenderNode (node, theTime);
// for verification: quantise the given Time into SMPTE timecode;
QuTime qantTime (theTime, SECONDS_GRID);
CHECK (res == SmpteTC(qantTime).secs);
// Explanation: since the param-functor quantises into a 1-second grid
// and the given time is below 1 minute, the seconds field
// of SMPTE Timecode should match the parameter value
}
/** @test build a node and _adapt the parameters_ for invocation.
* - again use a processing function which takes a parameter
* - but then _decorate_ this functor, so that it takes different arguments
* - attach parameter handling to supply these adapted arguments
* @todo 2/25 ✔ define ⟶ ✔ implement
*/
void
build_Node_adaptedParam()
{
auto procFun = [](ulong param, int* buff){ *buff = int(param); };
auto adaptor = [](string spec){ return boost::lexical_cast<int>(spec); };
ProcNode node{prepareNode("Test")
.preparePort()
.invoke ("fun()", procFun)
.adaptParam (adaptor)
.setParam ("55")
.completePort()
.build()};
CHECK (55 == invokeRenderNode (node));
}
/** @test build a node and partially close (≙ predefine) some parameters,
* while leaving other parameters open to be set on invocation
* through a parameter-functor.
* - define a processing-function which takes an array of parameters,
* which will be handled similar as a tuple with uniform types.
* - demonstrate that several partial-closures can be cascaded;
* first close one parameter given by index, then close staring
* from the front and then aligned to the end
* - now a single param «slot» remains open, which can be wired
* to receive automation data (note: 1-tuple generated automatically)
* @remark it is quite common that processing functionality provided by an
* external library exposes both technical and artistic parameters, which
* leads to the situation that technical parameters can be predetermined
* and configured to a fixed value, while artistic parameters remain open
* for control by the user, either as a fixed setting (e.g. colour balance)
* or even a dynamic control by an automation function).
*/
void
build_Node_closedParam()
{
using Params = array<uint, 5>;
auto procFun = [](Params params, uint* out){ *out = explore(params).resultSum(); };
auto autoFun = [](Time nomTime){ return uint(FrameNr::quant (nomTime, SECONDS_GRID));};
ProcNode node{prepareNode("Test")
.preparePort()
.invoke ("fun()", procFun) // param(·,·,·,·,·)
.closeParam<2> (1) // param(·,·,1,·,·)
.closeParamFront(2) // param(2,·,1,·,·)
.closeParamBack (3,4) // param(2,·,1,3,4)
.attachAutomation (autoFun) // △
.completePort()
.build()};
Time timeOfEvil{5555,0};
CHECK (2+5+1+3+4 == invokeRenderNode (node, timeOfEvil));
}
/** @test build a chain with three connected Nodes
* - have two source nodes, which accept a parameter
* - but configure them differently: one gets a constant,
* while the other draws a random number
* - the third node takes two input buffers and and one output;
* it retrieves the input values, and sums them together
* - use the »simplified 1:1 wiring«, which connects consecutively
* each input slot to the next given node on the same port number;
* here we only use port#0 on all three nodes.
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_connectedNodes()
{
using SrcBuffs = array<uint*, 2>;
auto sourceFun = [](uint param, uint* out) { *out = 1 + param; };
auto joinerFun = [](SrcBuffs src, uint* out){ *out = *src[0] + *src[1]; };
int peek{-1};
auto randParam = [&](TurnoutSystem&){ return peek = rani(100); };
ProcNode n1{prepareNode("Src1")
.preparePort()
.invoke ("fix-val()", sourceFun)
.setParam (LIFE_AND_UNIVERSE_4EVER)
.completePort()
.build()};
ProcNode n2{prepareNode("Src2")
.preparePort()
.invoke ("ran-val()", sourceFun)
.attachParamFun (randParam)
.completePort()
.build()};
ProcNode n3{prepareNode("Join")
.preparePort()
.invoke ("add()", joinerFun)
.connectLead(n1)
.connectLead(n2)
.completePort()
.build()};
CHECK (is_linked(n3).to(n1));
CHECK (is_linked(n3).to(n2));
uint res = invokeRenderNode(n3);
CHECK (res == peek+1 + LIFE_AND_UNIVERSE_4EVER+1 );
CHECK (peek != -1);
}
/** @test demonstrate the setup of a »Param Agent Node«
* - perform effectively the same computation as the preceding test
* - but use two new custom parameters in the Param Agent Node
* - pick them up from the nested source nodes by accessor-functors
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_ParamNode()
{
// Note: using exactly the same functors as in the preceding test
using SrcBuffs = array<uint*, 2>;
auto sourceFun = [](uint param, uint* out) { *out = 1 + param; };
auto joinerFun = [](SrcBuffs src, uint* out){ *out = *src[0] + *src[1]; };
int peek{-1};
auto randParam = [&](TurnoutSystem&){ return peek = rani(100); };
// Step-1 : build a ParamSpec
auto spec = buildParamSpec()
.addValSlot (LIFE_AND_UNIVERSE_4EVER)
.addSlot (randParam)
;
auto get0 = spec.makeAccessor<0>();
auto get1 = spec.makeAccessor<1>();
// Step-2 : build delegate Node tree
ProcNode n1{prepareNode("Src1")
.preparePort()
.invoke ("fix-val()", sourceFun)
.retrieveParam (get0)
.completePort()
.build()};
ProcNode n2{prepareNode("Src2")
.preparePort()
.invoke ("ran-val()", sourceFun)
.retrieveParam (get1)
.completePort()
.build()};
ProcNode n3{prepareNode("Join")
.preparePort()
.invoke ("add()", joinerFun)
.connectLead(n1)
.connectLead(n2)
.completePort()
.build()};
// Step-3 : build Param Agent as entry point
ProcNode n4{prepareNode("Param")
.preparePort()
.computeParam(spec)
.delegateLead(n3)
.completePort()
.build()};
uint res = invokeRenderNode(n4);
CHECK (res == peek+1 + LIFE_AND_UNIVERSE_4EVER+1 );
CHECK (peek != -1);
}
};
/** Register this test class... */
LAUNCHER (NodeBuilder_test, "unit node");
}}} // namespace steam::engine::test