/* NodeLink(Test) - render node connectivity and collaboration Copyright (C) 2024, Hermann Vosseler   **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 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; namespace { ont::Flavr SRC_A = 10; ///< »chain-A« arbitrary source frame marker ont::Flavr SRC_B = 20; ///< similar for »chain-B« Symbol SECONDS_GRID = "grid_sec"; ///< 1-seconds grid for translation Time -> Frame-# const uint NUM_INVOCATIONS = 100; } /***************************************************************//** * @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. * - Nodes can be built and ID metadata can be inspected * - several Nodes can be linked into a render graph * - connectivity can be verified to match definition * - TestFrame data can be computed in a complex processing network * - parameters can be derived from time and fed into the nodes. */ 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 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 (-55); CHECK (-55 == buff.accessAs()); buff = node.pull (port, buff, Time::ZERO, ProcessKey{0}); int result = buff.accessAs(); 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 Invoke some render nodes as linked together. * - use exactly the same topology as in the preceding test * - but this time use TestFrame (random data) and configure * hash-chaining operations provided by »Test Random« * - setup various automation functions, based on the frame-# * - use a pre-computation step to _quantise_ time into frame-# * - install this pre-computation as »Param Agent Node« * - configure individual parameters to consume precomputed frame-# * - use _partial closure_ to supply the source-»flavour« parameter * - also rebuild the expected computations by direct invocation * - sample various test runs with randomly chosen time and port-# * - verify computed data checksums match with expected computation. * @todo 2/25 ✔ define ⟶ ✔ implement */ void trigger_node_port_invocation() { auto testGen = testRand().setupGenerator(); auto testMan = testRand().setupManipulator(); auto testMix = testRand().setupCombinator(); // Prepare for Time-Quantisation --> Frame-# or Offset parameter steam::asset::meta::TimeGrid::build (SECONDS_GRID, 1); auto quantSecs = [&](Time time){ return FrameNr::quant (time, SECONDS_GRID); }; // Prepare a precomputed parameter for the complete tree auto selectFrameNo = [&](TurnoutSystem& tuSys){ return quantSecs(tuSys.getNomTime()); }; 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, 50); }; auto stepMixer = [] (FrameCnt id)-> ont::Factr { 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()}; // Effectively, the following computation is expected to happen... auto verify = [&](Time nomTime, uint port) { ont::FraNo fraNo = quantSecs(nomTime); ont::Flavr fla_A = SRC_A + util::min (port, 1u); ont::Flavr fla_B = SRC_B + util::min (port, 2u); ont::Param param = stepFilter(fraNo); ont::Factr mix = stepMixer (fraNo); TestFrame f1{uint(fraNo),fla_A}; TestFrame f2{uint(fraNo),fla_B}; ont::manipulateFrame (&f1, &f1, param); ont::combineFrames (&f1, &f1, &f2, mix); CHECK (not f1.isPristine()); CHECK ( f2.isPristine()); return f1.getChecksum(); }; BufferProvider& provider = DiagnosticBufferProvider::build(); const BuffDescr buffDescr = provider.getDescriptor(); auto invoke = [&](Time nomTime, uint port) { // Sequence to invoke a Node... BuffHandle buff = provider.lockBuffer(buffDescr); TestFrame& result = buff.accessAs(); CHECK ( result.isPristine()); buff = parNode.pull (port, buff, nomTime, ProcessKey{}); CHECK ( result.isValid()); CHECK (not result.isPristine()); HashVal checksum = result.getChecksum(); buff.release(); return checksum; }; // Computations should be pure (not depending on order) // Thus sample various random times and ports for (uint i=0; i < NUM_INVOCATIONS; ++i) { uint port = rani(3); Time nomTime{rani(60'000),0}; // drive test with a random »nominal Time« <60s with ms granularity // Invoke -- and compare checksum with direct computation CHECK (invoke (nomTime,port) == verify (nomTime,port)); } } }; /** Register this test class... */ LAUNCHER (NodeLink_test, "unit node"); }}} // namespace steam::engine::test