/* 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 "lib/test/diagnostic-output.hpp"/////////////////TODO #include "lib/util.hpp" #include using std::array; using util::isnil; using util::isSameObject; namespace steam { namespace engine{ namespace test { /***************************************************************//** * @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 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 TODO Invoke some render nodes as linked together * @todo WIP 12/24 🔁 define ⟶ implement */ void trigger_node_port_invocation() { UNIMPLEMENTED ("operate some render nodes as linked together"); } }; /** Register this test class... */ LAUNCHER (NodeLink_test, "unit node"); }}} // namespace steam::engine::test