With these additions, all conceivable cases are basically addressed. Take this as opportunity to investigate how the existing implementation transports values into the Binder, where they will be stored as data fields. Notably the mechanism of the `TupleConstructor` / `ElmMapper` indeed ''essentially requires'' to pass the initialisers ''by-reference'', because otherwise there would be limitations on possible mappings. This implies that not much can be done for ''perfect forwarding'' of initialisers, but at least the `BindToArgument` can be simplified to take the value directly.
347 lines
13 KiB
C++
347 lines
13 KiB
C++
/*
|
||
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/node-builder.hpp"
|
||
#include "steam/engine/diagnostic-buffer-provider.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 a DiagnosticBufferProvider 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)
|
||
{
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
BuffHandle buff = provider.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.
|
||
* @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)
|
||
.closeParamFront (1,2,3,4)
|
||
.attachAutomation (autoFun)
|
||
.completePort()
|
||
.build()};
|
||
|
||
Time timeOfEvil{5555,0};
|
||
CHECK (15 == 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
|