/* NodeBase(Test) - unit test to cover the render node base elements Copyright (C) 2009, 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-base-test.cpp ** Unit test \ref NodeBase_test covers elementary components of render nodes. */ #include "lib/test/run.hpp" #include "lib/iter-zip.hpp" #include "lib/meta/function.hpp" #include "lib/several-builder.hpp" #include "steam/engine/proc-node.hpp" #include "steam/engine/turnout.hpp" #include "steam/engine/turnout-system.hpp" #include "steam/engine/feed-manifold.hpp" #include "steam/engine/node-builder.hpp" #include "steam/engine/diagnostic-buffer-provider.hpp" #include "steam/engine/buffhandle-attach.hpp" #include "lib/test/test-helper.hpp" #include "lib/util.hpp" using std::tuple; using std::array; using util::isSameAdr; using lib::test::showType; using lib::makeSeveral; using lib::izip; namespace steam { namespace engine{ namespace test { /******************************************************//** * @test basic render node structure and building blocks. * This test documents and verifies some fundamental * Render Node structures, looking at intricate technical * details, which are usually hidden below the NodeBuidler. * - #verify_NodeStructure is a demonstration example * to show fundamentals of node construction and * invocation, using a dummy implementation. * - the following cases cover extremely technical details * of the FeedManifold, which serves as junction point * between Render Node and external library functions. * - in a similar style, \ref NodeFeed_test covers the * various parameter- and data connections of Nodes * in a »clean-room« setting * - much more high-level is NodeLink_test, covering * the construction of a Render Node network * - NodeBuilder_test focuses on aspects of node * generation, as packaged into the NodeBuilder. */ class NodeBase_test : public Test { virtual void run (Arg) { seedRand(); verify_TurnoutSystem(); verify_NodeStructure(); verify_FeedManifold(); verify_FeedPrototype(); } /** @test the TurnoutSystem as transient connection hub for node invocation * - for most invocations, just the nominal timeline time and an * arbitrary process indentification-key is placed into fixed * «slots« within the TurnoutSystem, from where these parameters * can be retrieved by actual processing functions; * - for some special cases however, additional storage blocks * can be chained up, to allow accessing arbitrary parameters * through the TurnoutSystem as front-end. */ void verify_TurnoutSystem() { Time nomTime{rani(10'000),0}; // drive test with a random »nominal Time« <10s with ms granularity TurnoutSystem turoSys{nomTime}; // a time spec is mandatory, all further parameters are optional CHECK (turoSys.getNomTime() == nomTime); // can access those basic params from within the render invocation. CHECK (turoSys.getProcKey() == ProcessKey{}); /* == That's all required for basic usage. == */ // Demonstrate extension-block to TurnoutSystem // Used to setup elaborate parameter-nodes... double someVal = defaultGen.uni(); // some param value, computed by »elaborate logic« auto spec = buildParamSpec() .addValSlot (someVal); // declare a parameter slot for an extension data block auto acc0 = spec.makeAccessor<0>(); // capture an accessor-functor for later use {// Build and connect extension storage block auto dataBlock = // ...typically placed locally into a nested stack frame spec.makeBlockBuilder() .buildParamDataBlock(turoSys); turoSys.attachChainBlock (dataBlock); // link extension data block into the TurnoutSystem CHECK (turoSys.get(acc0) == someVal); // now able to retrieve data from extension block turoSys.detachChainBlock (dataBlock); } // base block continues to be usable... CHECK (turoSys.getNomTime() == nomTime); } /** @test very basic structure of a Render Node. * - All render processing happens in \ref Port implementations * - here we use a dummy port, which just picks up a parameter * from the TurnoutSystem and writes it into the output buffer; * no further recursive call happens — so this is a source node. * - To _incorporate_ this Port implementation into a Render Node, * the _connectivity_ of the node network must be defined: * + each node has a list of »Leads« (predecessor nodes) * + and an array of port implementation (here just one port) * - note that data placement relies on lib::Several, which can * be configured to use a custom allocator to manage storage * - furthermore, a node gets some ID descriptors, which are used * to generate processing metadata (notably a hash key for caching) * - for the actual invocation, foremost we need a _buffer provider_ * - and we need to supply the most basic parameters, like the * nominal timeline time and a proccess-Key. These will be * embedded into the TurnoutSystem, to be accessible throughout * the complete recursive node-pull invocation. * - This test verifies that the actual invocation indeed happened * and placed a random parameter-value into the output buffer. * @remark In reality, processing operations are delegated to a * media-processing library, which requires elaborate buffer handling * and typically entails recursive calls to predecessor nodes. This * intricate logic is handled by the typical Port implementation * known as \ref MediaWeavingPattern; notably the processing will * rely on a transient data structure called \ref FeedManifold, which * is verified in much more detail [below](\ref #verify_FeedManifold) */ void verify_NodeStructure() { class DummyProcessing : public Port { public: DummyProcessing (ProcID& id) : Port{id} { } /** Entrance point to the next recursive step of media processing. */ BuffHandle weave (TurnoutSystem& turnoutSystem, OptionalBuff outBuffer) override {// do something deeply relevant, like feeding a dummy parameter... outBuffer->accessAs() = turnoutSystem.getProcKey(); return *outBuffer; } }; // Prepare Connectivity for the Node auto leadNodes = makeSeveral(); // empty, no predecessor nodes auto nodePorts = makeSeveral() // build the port implementation object(s) .emplace (ProcID::describe ("TestDummy","live(long)")); // Build a Render Node ProcNode theNode{Connectivity{nodePorts.build() ,leadNodes.build() }}; // Inspect Node metadata... CHECK (watch(theNode).isSrc()); CHECK (watch(theNode).leads().size() == 0); CHECK (watch(theNode).ports().size() == 1); CHECK (watch(theNode).getNodeSpec () == "TestDummy-◎"_expect ); CHECK (watch(theNode).getPortSpec(0) == "TestDummy.live(long)"_expect ); // prepare for invoking the node.... BufferProvider& provider = DiagnosticBufferProvider::build(); BuffHandle buff = provider.lockBufferFor (-55); CHECK (-55 == buff.accessAs()); // allocated some data buffer for the result, with a marker-value Time nomTime{Time::ZERO}; ProcessKey key = 1 + rani(100); // here we »hide« some data value in the ProcessKey uint port{0}; // we will pull port-#0 of the node // Trigger Node invocation... buff = theNode.pull (port, buff, nomTime, key); CHECK (key == buff.accessAs()); // DummyProcessing port placed ProcessKey into the output-buffer buff.release(); } /** @test the FeedManifold as adapter between Engine and processing library... * - bind local λ with various admissible signatures * - construct specifically tailored FeedManifold types * - use the DiagnosticBufferProvider for test buffers * - create FeedManifold instance, passing the λ and additional parameters * - connect BuffHandle for these buffers into the FeedManifold instance * - trigger invocation of the function * - look into the buffers and verify effect * @remark within each Render Node, a FeedManifold is used as junction * to tap into processing functionality provided by external libraries. * Those will be adapted by a Plug-in, to be loaded by the Lumiera core * application. The _signature of a functor_ linked to the FeedManifold * is used as kind of a _low-level-specification_ how to invoke external * processing functions. Obviously this must be complemented by a more * high-level descriptor, which is interpreted by the Builder to connect * a suitable structure of Render Nodes. * @see feed-manifold.h * @see NodeLinkage_test */ void verify_FeedManifold() { // Prepare setup to build a suitable FeedManifold... long r1 = rani(100); using Buffer = long; //______________________________________________________________ // Example-1: a FeedManifold to adapt a simple generator function auto fun_singleOut = [&](Buffer* buff) { *buff = r1; }; using M1 = FeedManifold; CHECK (not M1::hasInput()); CHECK (not M1::hasParam()); CHECK (0 == M1::FAN_P); CHECK (0 == M1::FAN_I); CHECK (1 == M1::FAN_O); // instantiate... M1 m1{fun_singleOut}; CHECK (1 == m1.outBuff.array().size()); CHECK (nullptr == m1.outArgs ); // CHECK (m1.inArgs ); // does not compile because storage field is not provided // CHECK (m1.param ); BufferProvider& provider = DiagnosticBufferProvider::build(); BuffHandle buff = provider.lockBufferFor (-55); CHECK (buff.isValid()); CHECK (buff.accessAs() == -55); m1.outBuff.createAt (0, buff); // plant a copy of the BuffHandle into the output slot CHECK (m1.outBuff[0].isValid()); CHECK (m1.outBuff[0].accessAs() == -55); m1.connect(); // instruct the manifold to connect buffers to arguments CHECK (isSameAdr (m1.outArgs, *buff)); CHECK (*m1.outArgs == -55); m1.invoke(); // invoke the adapted processing function (fun_singleOut) CHECK (buff.accessAs() == r1); // result: the random number r1 was written into the buffer. //_____________________________________________________________ // Example-2: adapt a function to process input -> output buffer auto fun_singleInOut = [](Buffer* in, Buffer* out) { *out = *in + 1; }; using M2 = FeedManifold; CHECK ( M2::hasInput()); CHECK (not M2::hasParam()); CHECK (1 == M2::FAN_I); CHECK (1 == M2::FAN_O); // instantiate... M2 m2{fun_singleInOut}; CHECK (1 == m2.inBuff.array().size()); CHECK (1 == m2.outBuff.array().size()); CHECK (nullptr == m2.inArgs ); CHECK (nullptr == m2.outArgs ); // use the result of the preceding Example-1 as input // and get a new buffer to capture the output BuffHandle buffOut = provider.lockBufferFor (-99); CHECK (buff.accessAs() == r1); CHECK (buffOut.accessAs() == -55); ///////////////////////////////////////OOO should be -99 --> aliasing of buffer meta records due to bug with hash generation // configure the Manifold-2 with this input and output buffer m2.inBuff.createAt (0, buff); m2.outBuff.createAt(0, buffOut); CHECK (m2.inBuff[0].isValid()); CHECK (m2.inBuff[0].accessAs() == r1 ); CHECK (m2.outBuff[0].isValid()); CHECK (m2.outBuff[0].accessAs() == -55); ////////////////////////////////OOO should be -99 // connect arguments to buffers m2.connect(); CHECK (isSameAdr (m2.inArgs, *buff)); CHECK (isSameAdr (m2.outArgs, *buffOut)); CHECK (*m2.outArgs == -55); ////////////////////////////////OOO should be -99 m2.invoke(); CHECK (buffOut.accessAs() == r1+1); //______________________________________ // Example-3: accept complex buffer setup using Sequence = array; using Channels = array; using Compound = tuple; auto fun_complexInOut = [](Channels in, Compound out) { auto [seq,extra] = out; for (auto [i,b] : izip(in)) { (*seq)[i] = *b + 1; *extra += *b; } }; using M3 = FeedManifold; CHECK ( M3::hasInput()); CHECK (not M3::hasParam()); CHECK (3 == M3::FAN_I); CHECK (2 == M3::FAN_O); CHECK (showType() == "array"_expect); CHECK (showType() == "tuple*, long*>"_expect); // instantiate... M3 m3{fun_complexInOut}; CHECK (3 == m3.inBuff.array().size()); CHECK (2 == m3.outBuff.array().size()); // use existing buffers and one additional buffer for input BuffHandle buffI0 = buff; BuffHandle buffI1 = buffOut; BuffHandle buffI2 = provider.lockBufferFor (-22); CHECK (buffI0.accessAs() == r1 ); // (result from Example-1) CHECK (buffI1.accessAs() == r1+1); // (result from Example-2) CHECK (buffI2.accessAs() == -55 ); ///////////////////////////////////////OOO should be -22 // prepare a compound buffer and an extra buffer for output... BuffHandle buffO0 = provider.lockBufferFor (Sequence{-111,-222,-333}); BuffHandle buffO1 = provider.lockBufferFor (-33); CHECK ((buffO0.accessAs() == Sequence{-111,-222,-333})); CHECK (buffO1.accessAs() == -55 ); ///////////////////////////////////////OOO should be -33 // configure the Manifold-3 with these input and output buffers m3.inBuff.createAt (0, buffI0); m3.inBuff.createAt (1, buffI1); m3.inBuff.createAt (2, buffI2); m3.outBuff.createAt(0, buffO0); m3.outBuff.createAt(1, buffO1); m3.connect(); // Verify data exposed prior to invocation.... auto& [ia0,ia1,ia2] = m3.inArgs; auto& [oa0,oa1] = m3.outArgs; auto& [o00,o01,o02] = *oa0; CHECK (*ia0 == r1 ); CHECK (*ia1 == r1+1); CHECK (*ia2 == -55 ); /////////////////////////////////////////////////////OOO should be -22 CHECK ( o00 == -111); CHECK ( o01 == -222); CHECK ( o02 == -333); CHECK (*oa1 == -55 ); /////////////////////////////////////////////////////OOO should be -33 m3.invoke(); CHECK (*ia0 == r1 ); // Input buffers unchanged CHECK (*ia1 == r1+1); CHECK (*ia2 == -55 ); /////////////////////////////////////////////////////OOO should be -22 CHECK ( o00 == *ia0+1); // Output buffers as processed by the function CHECK ( o01 == *ia1+1); CHECK ( o02 == *ia2+1); CHECK (*oa1 == -55 + *ia0+*ia1+*ia2); ///////////////////////////////////////////OOO should be -33 //_________________________________ // Example-4: pass a parameter tuple using Params = tuple; // Note: demonstrates mix of complex params, an array for input, but just a simple output buffer auto fun_ParamInOut = [](Params param, Channels in, Buffer* out) { auto [s,l] = param; *out = 0; for (Buffer* b : in) *out += (s+l) * (*b); }; using M4 = FeedManifold; CHECK (M4::hasInput()); CHECK (M4::hasParam()); CHECK (2 == M4::FAN_P); CHECK (3 == M4::FAN_I); CHECK (1 == M4::FAN_O); CHECK (showType() == "array"_expect); CHECK (showType() == "long *"_expect); CHECK (showType() == "tuple"_expect); // Note: instantiate passing param values as extra arguments short r2 = 1+rani(10); long r3 = rani(1000); M4 m4{Params{r2,r3}, fun_ParamInOut}; // parameters directly given by-value auto& [p0,p1] = m4.param; CHECK (p0 == r2); // parameter values exposed through manifold CHECK (p1 == r3); // wire-in existing buffers for this example m4.inBuff.createAt (0, buffI0); m4.inBuff.createAt (1, buffI1); m4.inBuff.createAt (2, buffI2); m4.outBuff.createAt(0, buffO1); CHECK (*ia0 == r1 ); // existing values in the buffers.... CHECK (*ia1 == r1+1); CHECK (*ia2 == -55 ); /////////////////////////////////////////////////////OOO should be -22 CHECK (*oa1 == -55 + *ia0+*ia1+*ia2); ///////////////////////////////////////////OOO should be -33 m4.connect(); m4.invoke(); // processing combines input buffers with parameters CHECK (*oa1 == (r2+r3) * (r1 + r1+1 -55)); /////////////////////////////////////OOO should be -22 //______________________________________ // Example-5: simple parameter and output auto fun_singleParamOut = [](short param, Buffer* buff) { *buff = param-1; }; using M5 = FeedManifold; CHECK (not M5::hasInput()); CHECK ( M5::hasParam()); CHECK (1 == M5::FAN_P); CHECK (0 == M5::FAN_I); CHECK (1 == M5::FAN_O); CHECK (showType() == "tuple<>"_expect); CHECK (showType() == "long *"_expect); CHECK (showType() == "short"_expect); // instantiate, directly passing param value M5 m5{r2, fun_singleParamOut}; // wire with one output buffer m5.outBuff.createAt(0, buffO1); m5.connect(); CHECK (m5.param == r2); // the parameter value passed to the ctor // CHECK (m5.inArgs ); // does not compile because storage field is not provided CHECK (*m5.outArgs == *oa1); // still previous value sitting in the buffer... m5.invoke(); CHECK (*oa1 == r2 - 1); // processing has placed result based on param into output buffer // done with these buffers buffI0.release(); buffI1.release(); buffI2.release(); buffO0.release(); buffO1.release(); } /** @test Setup of a FeeManifold to attach parameter-functors */ void verify_FeedPrototype() { // Prepare setup to build a suitable FeedManifold... using Buffer = long; BufferProvider& provider = DiagnosticBufferProvider::build(); BuffHandle buff = provider.lockBufferFor (-55); //_______________________________________ // Case-1: Prototype without param-functor auto fun_singleParamOut = [](short param, Buffer* buff) { *buff = param-1; }; using M1 = FeedManifold; using P1 = M1::Prototype; CHECK ( P1::hasParam()); // checks that the processing-function accepts a parameter CHECK (not P1::hasParamFun()); // while this prototype has no active param-functor CHECK (not P1::canActivate()); P1 p1{move (fun_singleParamOut)}; // create the instance of the prototype, moving the functor in CHECK (sizeof(p1) <= sizeof(void*)); TurnoutSystem turSys{Time::NEVER}; // Each Node invocation uses a TurnoutSystem instance.... M1 m1 = p1.buildFeed(turSys); //... and also will create a new FeedManifold from the prototype CHECK (m1.param == short{}); // In this case here, the param value is default constructed. m1.outBuff.createAt(0, buff); // Perform the usual steps for an invocation.... CHECK (buff.accessAs() == -55); m1.connect(); CHECK (*m1.outArgs == -55); m1.invoke(); CHECK (*m1.outArgs == 0 - 1); // fun_singleParamOut() -> param - 1 and param ≡ 0 CHECK (buff.accessAs() == 0 - 1); long& calcResult = buff.accessAs(); // for convenience use a reference into the result buffer //_____________________________________________ // Case-2: Reconfigure to attach a param-functor long rr{11}; // ▽▽▽▽ Note: side-effect auto fun_paramSimple = [&](TurnoutSystem&){ return rr += 1+rani(100); }; using P1x = P1::Adapted; CHECK ( P1x::hasParam()); CHECK ( P1x::hasParamFun()); CHECK (not P1x::canActivate()); P1x p1x = p1.moveAdaptedParam (move(fun_paramSimple)); M1 m1x = p1x.buildFeed(turSys); // ◁————————— param-functor invoked here CHECK (rr == m1x.param); // ...as indicated by the side-effect short r1 = m1x.param; // the rest works as always with FeedManifold (which as such is agnostic of the param-functor!) m1x.outBuff.createAt(0, buff); m1x.connect(); m1x.invoke(); // Invoke the processing functor CHECK (calcResult == r1 - 1); // ...which computes fun_singleParamOut() -> param-1 // but let's play with the various instances... m1.invoke(); // the previous FeedManifold is sill valid and connected CHECK (calcResult == 0 - 1); // and uses its baked in parameter value (0) m1x.invoke(); CHECK (calcResult == r1 - 1); // as does m1x, without invoking the param-functor // create yet another instance from the prototype... M1 m1y = p1x.buildFeed(turSys); // ◁————————— param-functor invoked here CHECK (rr == m1y.param); CHECK (r1 < m1y.param); // ...note again the side-effect m1y.outBuff.createAt(0, buff); m1y.connect(); m1y.invoke(); // ...and so this third FeedManifold instance... CHECK (calcResult == rr - 1); // uses yet another baked-in param value; m1x.invoke(); // recall that each Node invocation creates a new CHECK (calcResult == r1 - 1); // FeedManifold on the stack, since invocations are m1.invoke(); // performed concurrently, each with its own set of CHECK (calcResult == 0 - 1); // buffers and parameters. //_______________________________ // Case-3: Integrate std::function using ParamSig = short(TurnoutSystem&); using ParamFunction = std::function; // a Prototype to hold such a function... using P1F = P1::Adapted; CHECK ( P1F::hasParam()); CHECK ( P1F::hasParamFun()); CHECK ( P1F::canActivate()); P1F p1f = p1x.clone() // if (and only if) the embedded functors allow clone-copy .moveAdaptedParam(); // then we can fork-off and then adapt a cloned prototype // Need to distinguish between static capability and runtime state... CHECK (not p1 .canActivate()); // Case-1 had no param functor installed... CHECK (not p1 .isActivated()); // and thus also can not invoke such a functor at runtime CHECK (not p1x.canActivate()); // Case-2 has a fixed param-λ, which can not be activated/deactivated CHECK ( p1x.isActivated()); // yet at runtime this functor is always active and callable CHECK ( p1f.canActivate()); // Case-3 was defined to hold a std::function, which thus can be toggled CHECK (not p1f.isActivated()); // yet in current runtime configuration, the function is empty // create a FeedManifold instance from this prototype M1 m1f1 = p1f.buildFeed(turSys); // no param-functor invoked, CHECK (m1f1.param == short{}); // so this FeedManifold will use the default-constructed parameter // but since std::function is assignable, we can activate it... CHECK (not p1f.isActivated()); p1f.assignParamFun ([](TurnoutSystem&){ return 47; }); CHECK ( p1f.isActivated()); M1 m1f2 = p1f.buildFeed(turSys); // ◁————————— param-functor invoked here CHECK (m1f2.param == 47); // ...surprise: we got number 47... p1f.assignParamFun(); CHECK (not p1f.isActivated()); // can /deactivate/ it again... M1 m1f3 = p1f.buildFeed(turSys); // so no param-functor invoked here CHECK (m1f3.param == short{}); // done with buffer buff.release(); //_____________________________________ // Addendum: type conversion intricacies auto lambdaSimple = [ ](TurnoutSystem&){ return short(47); }; auto lambdaCapture = [&](TurnoutSystem&){ return short(47); }; using LambdaSimple = decltype(lambdaSimple); using LambdaCapture = decltype(lambdaCapture); CHECK ( (std::is_constructible::value)); CHECK ( (std::is_constructible::value)); CHECK (not (std::is_constructible::value)); // Surprise! a non-capture-λ turns out to be bool convertible, // which however is also true for std::function, // yet for quite different reasons: While the latter has a // built-in conversion operator to detect /inactive/ state, // the simple λ decays to a function pointer, which makes it // usable as implementation for plain-C callback functions. using FunPtr = short(*)(TurnoutSystem&); CHECK (not (std::is_convertible::value)); CHECK ( (std::is_convertible::value)); CHECK (not (std::is_convertible::value)); // ..which allows to distinguish these cases.. // CHECK (true == _ParamFun::isConfigurable::value); CHECK (false == _ParamFun::isConfigurable::value); CHECK (false == _ParamFun::isConfigurable::value); } }; /** Register this test class... */ LAUNCHER (NodeBase_test, "unit node"); }}} // namespace steam::engine::test