LUMIERA.clone/tests/core/steam/engine/node-link-test.cpp
Ichthyostega 93812d5a6d Invocation: build a complex Render Node network for integration test
Using basically the same topology as in the preceding test, which focused on connectivity. However, in this case we retrieve actual processing functions from the »Test-Rand« ontology in order to perform hash-chaining computations on full data blocks. And, in addition, a »Param Agent Node« is used.
2025-02-19 19:37:55 +01:00

471 lines
22 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.

/*
NodeLink(Test) - render node connectivity and collaboration
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-link-test.cpp
** The \ref NodeLink_test covers the essence of connected render nodes.
*/
#include "lib/test/run.hpp"
#include "steam/engine/proc-node.hpp"
#include "steam/engine/node-builder.hpp"
#include "steam/engine/test-rand-ontology.hpp"
#include "steam/engine/diagnostic-buffer-provider.hpp"
#include "steam/asset/meta/time-grid.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/util.hpp"
#include "lib/test/diagnostic-output.hpp"/////////////////TODO
#include <array>
using std::array;
using util::isnil;
using util::isSameObject;
namespace steam {
namespace engine{
namespace test {
using lib::time::Time;
using lib::time::QuTime;
using lib::time::FrameNr;
using lib::time::FrameCnt;
/***************************************************************//**
* @test demonstrate and document how [render nodes](\ref proc-node.hpp)
* are connected into a processing network, allowing to _invoke_
* a \ref Port on a node to pull-generate a render result.
* - the foundation layer is formed by the nodes as linked into a network
* - starting from any Port, a TurnoutSystem can be established
* - which in turn allows _turn out_ a render result from this port.
*/
class NodeLink_test : public Test
{
virtual void
run (Arg)
{
seedRand();
build_simple_node();
build_connected_nodes();
trigger_node_port_invocation();
}
/** @test Build Node Port for simple function
* and verify observable properties of a Render Node
* @todo 7/24 ✔ define ⟶ ✔ implement
*/
void
build_simple_node()
{
// use some dummy specs and a dummy operation....
StrView nodeID{ont::DUMMY_NODE_ID};
StrView procID{ont::DUMMY_PROC_ID};
CHECK (nodeID == "Test:dummy"_expect);
CHECK (procID == "op(int)"_expect);
// use the NodeBuilder to construct a simple source-node connectivity
auto con = prepareNode(nodeID)
.preparePort()
.invoke(procID, ont::dummyOp)
.completePort()
.build();
CHECK (isnil (con.leads));
CHECK (1 == con.ports.size());
// can build a ProcNode with this connectivity
ProcNode n1{move(con)};
CHECK (watch(n1).isValid());
CHECK (watch(n1).leads().empty());
CHECK (watch(n1).ports().size() == 1);
// can generate a symbolic spec to describe the Port's processing functionality...
CHECK (watch(n1).getPortSpec(0) == "dummy.op(int)"_expect);
CHECK (watch(n1).getPortSpec(1) == ""_expect);
// such a symbolic spec is actually generated by a deduplicated metadata descriptor
auto& meta1 = ProcID::describe("N1","(arg)");
auto& meta1b = ProcID::describe("N1","(arg)");
auto& meta2 = ProcID::describe("N2","(arg)");
auto& meta3 = ProcID::describe("N1","uga()");
CHECK ( isSameObject (meta1,meta1b));
CHECK (not isSameObject (meta1,meta2));
CHECK (not isSameObject (meta1,meta3));
CHECK (hash_value(meta1) == hash_value(meta1b));
CHECK (hash_value(meta1) != hash_value(meta2));
CHECK (hash_value(meta1) != hash_value(meta3));
CHECK (meta1.genProcSpec() == "N1(arg)"_expect);
CHECK (meta2.genProcSpec() == "N2(arg)"_expect);
CHECK (meta3.genProcSpec() == "N1.uga()"_expect);
// re-generate the descriptor for the source node (n1)
auto& metaN1 = ProcID::describe("Test:dummy","op(int)");
CHECK (metaN1.genProcSpec() == "dummy.op(int)"_expect);
CHECK (metaN1.genProcName() == "dummy.op"_expect);
CHECK (metaN1.genNodeName() == "Test:dummy"_expect);
CHECK (metaN1.genNodeSpec(con.leads) == "Test:dummy-◎"_expect);
}
/** @test Build more elaborate Render Nodes linked into a connectivity network
* - verify nodes with several ports; at exit-level, 3 ports are available
* - using two different source nodes, one with two, one with three ports
* - the 2-port source is linearly chained to a 2-port filter node
* - the exit-level is a mix node, combining data from both chains
* - apply the automatic wiring of ports with the same number, whereby
* the first port connects to the first port on the lead, and so on.
* - yet for the 3rd port at the mix node, on one side the port number
* must be given explicitly, since the »A-side« chain offers only
* two ports.
* @todo 1/25 ✔ define ⟶ ✔ implement
*/
void
build_connected_nodes()
{
// This operation emulates a data source
auto src_op = [](int param, int* res){ *res = param; };
// A Node with two (source) ports
ProcNode n1s{prepareNode("srcA")
.preparePort()
.invoke("a(int)", src_op)
.setParam(5)
.completePort()
.preparePort()
.invoke("b(int)", src_op)
.setParam(23)
.completePort()
.build()};
// A node to add some "processing" to each data chain
auto add1_op = [](int* src, int* res){ *res = 1 + *src; };
ProcNode n1f{prepareNode("filterA")
.preparePort()
.invoke("a+1(int)(int)", add1_op)
.connectLead(n1s)
.completePort()
.preparePort()
.invoke("b+1(int)(int)", add1_op)
.connectLead(n1s)
.completePort()
.build()};
// Need a secondary source, this time with three ports
ProcNode n2s{prepareNode("srcB")
.preparePort()
.invoke("a(int)", src_op)
.setParam(7)
.completePort()
.preparePort()
.invoke("b(int)", src_op)
.setParam(13)
.completePort()
.preparePort()
.invoke("c(int)", src_op)
.setParam(17)
.completePort()
.build()};
// This operation emulates mixing of two source chains
auto mix_op = [](array<int*,2> src, int* res){ *res = (*src[0] + *src[1]) / 2; };
// Wiring for the Mix, building up three ports
// Since the first source-chain has only two ports,
// for the third result port we'll re-use the second source
ProcNode mix{prepareNode("mix")
.preparePort()
.invoke("a-mix(int/2)(int)", mix_op)
.connectLead(n1f)
.connectLead(n2s)
.completePort()
.preparePort()
.invoke("b-mix(int/2)(int)", mix_op)
.connectLead(n1f)
.connectLead(n2s)
.completePort()
.preparePort()
.invoke("c-mix(int/2)(int)", mix_op)
.connectLeadPort(n1f,1)
.connectLead(n2s)
.completePort()
.build()};
// verify Node-level connectivity
CHECK ( is_linked(n1f).to(n1s));
CHECK (not is_linked(n2s).to(n1s));
CHECK (not is_linked(mix).to(n1s));
CHECK ( is_linked(mix).to(n2s));
CHECK ( is_linked(mix).to(n1f));
CHECK (watch(n1s).leads().size() == 0 );
CHECK (watch(n1f).leads().size() == 1 );
CHECK (watch(n2s).leads().size() == 0 );
CHECK (watch(mix).leads().size() == 2 );
// verify Node and connectivity spec
CHECK (watch(n1s).getNodeSpec() == "srcA-◎"_expect );
CHECK (watch(n1f).getNodeSpec() == "filterA◁—srcA-◎"_expect );
CHECK (watch(n2s).getNodeSpec() == "srcB-◎"_expect );
CHECK (watch(mix).getNodeSpec() == "mix┉┉{srcA, srcB}"_expect);
// verify setup of the source nodes
CHECK (watch(n1s).ports().size() == 2 );
CHECK (watch(n1s).watchPort(0).isSrc());
CHECK (watch(n1s).watchPort(1).isSrc());
CHECK (watch(n1s).watchPort(0).getProcSpec() == "srcA.a(int)"_expect );
CHECK (watch(n1s).watchPort(1).getProcSpec() == "srcA.b(int)"_expect );
CHECK (watch(n1s).getPortSpec(0) == "srcA.a(int)"_expect );
CHECK (watch(n1s).getPortSpec(1) == "srcA.b(int)"_expect );
// second source node has 3 ports....
CHECK (watch(n2s).ports().size() == 3 );
CHECK (watch(n2s).watchPort(0).isSrc());
CHECK (watch(n2s).watchPort(1).isSrc());
CHECK (watch(n2s).watchPort(2).isSrc());
CHECK (watch(n2s).watchPort(0).getProcSpec() == "srcB.a(int)"_expect );
CHECK (watch(n2s).watchPort(1).getProcSpec() == "srcB.b(int)"_expect );
CHECK (watch(n2s).watchPort(2).getProcSpec() == "srcB.c(int)"_expect );
CHECK (watch(n2s).getPortSpec(0) == "srcB.a(int)"_expect );
CHECK (watch(n2s).getPortSpec(1) == "srcB.b(int)"_expect );
CHECK (watch(n2s).getPortSpec(2) == "srcB.c(int)"_expect );
// verify 2-chain
CHECK (watch(n1f).leads().size() == 1 );
CHECK (watch(n1f).ports().size() == 2 );
CHECK (watch(n1f).watchPort(0).srcPorts().size() == 1 );
CHECK (watch(n1f).watchLead(0).ports().size() == 2 );
CHECK (watch(n1f).watchLead(0).getNodeName() == "srcA"_expect);
CHECK (watch(n1f).watchPort(0).watchLead(0).getProcSpec() == "srcA.a(int)"_expect );
CHECK (watch(n1f).watchLead(0).watchPort(0).getProcSpec() == "srcA.a(int)"_expect );
CHECK (watch(n1f).watchPort(0).srcPorts()[0] == watch(n1f).watchLead(0).ports()[0]);
CHECK (watch(n1f).watchPort(1).srcPorts()[0] == watch(n1f).watchLead(0).ports()[1]);
// verify mix with 3 ports
CHECK (watch(mix).leads().size() == 2);
CHECK (watch(mix).leads()[0] == n1f );
CHECK (watch(mix).leads()[1] == n2s );
CHECK (watch(mix).ports().size() == 3);
CHECK (watch(mix).watchPort(0).srcPorts().size() == 2 );
CHECK (watch(mix).watchPort(1).srcPorts().size() == 2 );
CHECK (watch(mix).watchPort(2).srcPorts().size() == 2 );
CHECK (watch(mix).watchLead(0).ports().size() == 2 );
CHECK (watch(mix).watchLead(1).ports().size() == 3 );
CHECK (watch(mix).watchPort(0).watchLead(0).getProcName() == "filterA.a+1"_expect );
CHECK (watch(mix).watchLead(0).watchPort(0).getProcName() == "filterA.a+1"_expect );
CHECK (watch(mix).watchPort(1).watchLead(0).getProcName() == "filterA.b+1"_expect );
CHECK (watch(mix).watchLead(0).watchPort(1).getProcName() == "filterA.b+1"_expect );
CHECK (watch(mix).watchPort(2).watchLead(0).getProcName() == "filterA.b+1"_expect ); // special connection to port 1 on lead
CHECK (watch(mix).watchLead(0).watchPort(1).getProcName() == "filterA.b+1"_expect );
CHECK (watch(mix).watchPort(0).srcPorts()[0] == watch(mix).watchLead(0).ports()[0]);
CHECK (watch(mix).watchPort(1).srcPorts()[0] == watch(mix).watchLead(0).ports()[1]);
CHECK (watch(mix).watchPort(2).srcPorts()[0] == watch(mix).watchLead(0).ports()[1]);
CHECK (watch(mix).watchPort(0).watchLead(1).getProcName() == "srcB.a"_expect );
CHECK (watch(mix).watchLead(1).watchPort(0).getProcName() == "srcB.a"_expect );
CHECK (watch(mix).watchPort(1).watchLead(1).getProcName() == "srcB.b"_expect );
CHECK (watch(mix).watchLead(1).watchPort(1).getProcName() == "srcB.b"_expect );
CHECK (watch(mix).watchPort(2).watchLead(1).getProcName() == "srcB.c"_expect );
CHECK (watch(mix).watchLead(1).watchPort(2).getProcName() == "srcB.c"_expect );
CHECK (watch(mix).watchPort(0).srcPorts()[1] == watch(mix).watchLead(1).ports()[0]);
CHECK (watch(mix).watchPort(1).srcPorts()[1] == watch(mix).watchLead(1).ports()[1]);
CHECK (watch(mix).watchPort(2).srcPorts()[1] == watch(mix).watchLead(1).ports()[2]);
//________________________________________________________
// for sake of completeness: all these nodes can be invoked
BufferProvider& provider = DiagnosticBufferProvider::build();
auto invoke = [&](ProcNode& node, uint port)
{ // Sequence to invoke a Node...
BuffHandle buff = provider.lockBufferFor<int> (-55);
CHECK (-55 == buff.accessAs<int>());
buff = node.pull (port, buff, Time::ZERO, ProcessKey{0});
int result = buff.accessAs<int>();
buff.release();
return result;
};
// node|port
CHECK (invoke (n1s, 0 ) == 5);
CHECK (invoke (n1s, 1 ) == 23);
CHECK (invoke (n1f, 0 ) == 5+1);
CHECK (invoke (n1f, 1 ) == 23+1);
CHECK (invoke (n2s, 0 ) == 7);
CHECK (invoke (n2s, 1 ) == 13);
CHECK (invoke (n2s, 2 ) == 17);
CHECK (invoke (mix, 0 ) == (5+1 + 7 )/2);
CHECK (invoke (mix, 1 ) == (23+1 + 13)/2);
CHECK (invoke (mix, 2 ) == (23+1 + 17)/2);
}
/** @test TODO Invoke some render nodes as linked together
* @todo WIP 2/25 🔁 define ⟶ ✔ implement
*/
void
trigger_node_port_invocation()
{
auto testGen = testRand().setupGenerator();
auto testMan = testRand().setupManipulator();
auto testMix = testRand().setupCombinator();
ont::Flavr SRC_A = 10;
ont::Flavr SRC_B = 20;
// Prepare for Time-Quantisation --> Frame-# or Offset parameter
Symbol SECONDS_GRID = "grid_sec";
steam::asset::meta::TimeGrid::build (SECONDS_GRID, 1);
// Prepare a precomputed parameter for the complete tree
auto selectFrameNo = [&](TurnoutSystem& tuSys){ return FrameNr::quant (tuSys.getNomTime(), SECONDS_GRID); };
auto paramSpec = buildParamSpec()
.addSlot (selectFrameNo);
auto accFrameNo = paramSpec.makeAccessor<0>();
// Prepare mapping- and automation-functions
auto stepFilter = [] (FrameCnt id)-> ont::Param { return util::limited (10, -10 + id, 20); };
auto stepMixer = [] (FrameCnt id)-> double { return util::limited (0, + id, 50) / 50.0; };
// note: binds the accessor for the precomputed FrameNo-parameter
auto autoFilter = [=](TurnoutSystem& tuSys){ return stepFilter (tuSys.get (accFrameNo)); };
auto autoMixer = [=](TurnoutSystem& tuSys){ return stepMixer (tuSys.get (accFrameNo)); };
// A Node with two (source) ports
ProcNode n1s{prepareNode("srcA")
.preparePort()
.invoke(testGen.procID(), testGen.makeFun()) // params(frameNo, flavour)
.closeParam<1>(SRC_A + 0) // --> flavour ≔ SRC_A + port#0
.retrieveParam(accFrameNo)
.completePort()
.preparePort()
.invoke(testGen.procID(), testGen.makeFun())
.closeParam<1>(SRC_A + 1) // --> flavour ≔ SRC_A + port#1
.retrieveParam(accFrameNo)
.completePort()
.build()};
// A node to »filter« the data in chain-A
ProcNode n1f{prepareNode("filterA")
.preparePort()
.invoke(testMan.procID(), testMan.makeFun())
.attachParamFun(autoFilter) // filter-param <-- autoFilter(frameNo)
.connectLead(n1s)
.completePort()
.preparePort()
.invoke(testMan.procID(), testMan.makeFun())
.attachParamFun(autoFilter)
.connectLead(n1s)
.completePort()
.build()};
// A secondary source Node, this time with three ports
ProcNode n2s{prepareNode("srcB")
.preparePort()
.invoke(testGen.procID(), testGen.makeFun()) // params(frameNo, flavour)
.closeParam<1>(SRC_B + 0) // --> flavour ≔ SRC_B + port#0
.retrieveParam(accFrameNo)
.completePort()
.preparePort()
.invoke(testGen.procID(), testGen.makeFun())
.closeParam<1>(SRC_B + 1) // --> flavour ≔ SRC_B + port#1
.retrieveParam(accFrameNo)
.completePort()
.preparePort()
.invoke(testGen.procID(), testGen.makeFun())
.closeParam<1>(SRC_B + 2) // --> flavour ≔ SRC_B + port#2
.retrieveParam(accFrameNo)
.completePort()
.build()};
// Wiring for the Mix, building three ports,
// drawing from both source-chains
ProcNode mix{prepareNode("mix")
.preparePort()
.invoke(testMix.procID(), testMix.makeFun())
.attachParamFun(autoMixer) // mixer-param <-- autoMixer(frameNo)
.connectLead(n1f)
.connectLead(n2s)
.completePort()
.preparePort()
.invoke(testMix.procID(), testMix.makeFun())
.attachParamFun(autoMixer)
.connectLead(n1f)
.connectLead(n2s)
.completePort()
.preparePort()
.invoke(testMix.procID(), testMix.makeFun())
.attachParamFun(autoMixer)
.connectLeadPort(n1f,1) // note: using 2nd port from chain-A, which only has two ports
.connectLead(n2s)
.completePort()
.build()};
// Set a »Param-Agent«-Node on top to pre-compute the FrameNo
ProcNode parNode{prepareNode("Param")
.preparePort()
.computeParam(paramSpec)
.delegateLead(mix)
.completePort()
.preparePort()
.computeParam(paramSpec)
.delegateLead(mix)
.completePort()
.preparePort()
.computeParam(paramSpec)
.delegateLead(mix)
.completePort()
.build()};
BufferProvider& provider = DiagnosticBufferProvider::build();
const BuffDescr buffDescr = provider.getDescriptorFor(sizeof(TestFrame));
auto invoke = [&](Time nomTime, uint port)
{ // Sequence to invoke a Node...
BuffHandle buff = provider.lockBuffer(buffDescr);
CHECK (not buff.accessAs<TestFrame>().isValid());
buff = parNode.pull (port, buff, nomTime, ProcessKey{});
HashVal checksum = buff.accessAs<TestFrame>().getChecksum();
buff.release();
return checksum;
};
SHOW_EXPR(invoke(Time::ZERO, 1));
}
};
/** Register this test class... */
LAUNCHER (NodeLink_test, "unit node");
}}} // namespace steam::engine::test