While basically the `FeedPrototype` could be created directly, passing both the processing- and the parameter-functor, in practice a two-step configuration can be expected, since the processing-functor is built by the Library-Plug-in, while the parameter-functor is then later added as decoration by the builder. Thus we need the ability to ''collect configuration'' within the Level-2 builder, which can be achieved by a ''cross-builder'' mechanic, where we create an adapted builder from the augmented configuration. A similar approach is also used to add the configuration of the custom allocator. Added an extensive demo in the test, playing with several instances to highlight the point where the parameter-functor is actually invoked.
407 lines
19 KiB
C++
407 lines
19 KiB
C++
/*
|
||
NodeBase(Test) - unit test to cover the render node base elements
|
||
|
||
Copyright (C)
|
||
2009, 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-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 "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/diagnostic-buffer-provider.hpp"
|
||
#include "steam/engine/buffhandle-attach.hpp"
|
||
#include "lib/test/test-helper.hpp"
|
||
//#include "lib/format-cout.hpp"
|
||
#include "lib/test/diagnostic-output.hpp"/////////////////////TODO
|
||
#include "lib/format-util.hpp"///////////////////////////////TODO
|
||
#include "lib/util.hpp"
|
||
|
||
|
||
//using std::string;
|
||
using std::tuple;
|
||
using std::array;
|
||
using util::isSameAdr;
|
||
using lib::test::showType;
|
||
using lib::izip;
|
||
|
||
|
||
namespace steam {
|
||
namespace engine{
|
||
namespace test {
|
||
|
||
|
||
namespace { // Test fixture
|
||
/**
|
||
*/
|
||
}
|
||
|
||
|
||
/***************************************************************//**
|
||
* @test basic render node properties and behaviour.
|
||
*/
|
||
class NodeBase_test : public Test
|
||
{
|
||
virtual void
|
||
run (Arg)
|
||
{
|
||
seedRand();
|
||
verify_TurnoutSystem();
|
||
verify_FeedManifold();
|
||
verify_FeedPrototype();
|
||
UNIMPLEMENTED ("build a simple render node and then activate it");
|
||
}
|
||
|
||
/** @test the TurnoutSystem as transient coordinator for node invocation
|
||
*/
|
||
void
|
||
verify_TurnoutSystem()
|
||
{
|
||
Time nomTime{rani(10'000),0}; // drive test with a random »nominal Time« <10s with ms granularity
|
||
TurnoutSystem invoker{nomTime}; // a time spec is mandatory, all further parameters are optional
|
||
}
|
||
|
||
|
||
/** @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<decltype(fun_singleOut)>;
|
||
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<Buffer> (-55);
|
||
CHECK (buff.isValid());
|
||
CHECK (buff.accessAs<long>() == -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<long>() == -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<long>() == 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<decltype(fun_singleInOut)>;
|
||
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<Buffer> (-99);
|
||
CHECK (buff.accessAs<long>() == r1);
|
||
CHECK (buffOut.accessAs<long>() == -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<long>() == r1 );
|
||
CHECK (m2.outBuff[0].isValid());
|
||
CHECK (m2.outBuff[0].accessAs<long>() == -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<long>() == r1+1);
|
||
|
||
|
||
//______________________________________
|
||
// Example-3: accept complex buffer setup
|
||
using Sequence = array<Buffer,3>;
|
||
using Channels = array<Buffer*,3>;
|
||
using Compound = tuple<Sequence*, Buffer*>;
|
||
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<decltype(fun_complexInOut)>;
|
||
CHECK ( M3::hasInput());
|
||
CHECK (not M3::hasParam());
|
||
CHECK (3 == M3::FAN_I);
|
||
CHECK (2 == M3::FAN_O);
|
||
CHECK (showType<M3::ArgI>() == "array<long*, 3ul>"_expect);
|
||
CHECK (showType<M3::ArgO>() == "tuple<array<long, 3ul>*, 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<Buffer> (-22);
|
||
CHECK (buffI0.accessAs<long>() == r1 ); // (result from Example-1)
|
||
CHECK (buffI1.accessAs<long>() == r1+1); // (result from Example-2)
|
||
CHECK (buffI2.accessAs<long>() == -55 ); ///////////////////////////////////////OOO should be -22
|
||
// prepare a compound buffer and an extra buffer for output...
|
||
BuffHandle buffO0 = provider.lockBufferFor<Sequence> (Sequence{-111,-222,-333});
|
||
BuffHandle buffO1 = provider.lockBufferFor<Buffer> (-33);
|
||
CHECK ((buffO0.accessAs<Sequence>() == Sequence{-111,-222,-333}));
|
||
CHECK (buffO1.accessAs<long>() == -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<short,long>;
|
||
// 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<decltype(fun_ParamInOut)>;
|
||
CHECK (M4::hasInput());
|
||
CHECK (M4::hasParam());
|
||
CHECK (2 == M4::FAN_P);
|
||
CHECK (3 == M4::FAN_I);
|
||
CHECK (1 == M4::FAN_O);
|
||
CHECK (showType<M4::ArgI>() == "array<long*, 3ul>"_expect);
|
||
CHECK (showType<M4::ArgO>() == "long *"_expect);
|
||
CHECK (showType<M4::Param>() == "tuple<short, long>"_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<decltype(fun_singleParamOut)>;
|
||
CHECK (not M5::hasInput());
|
||
CHECK ( M5::hasParam());
|
||
CHECK (1 == M5::FAN_P);
|
||
CHECK (0 == M5::FAN_I);
|
||
CHECK (1 == M5::FAN_O);
|
||
CHECK (showType<M5::ArgI>() == "tuple<>"_expect);
|
||
CHECK (showType<M5::ArgO>() == "long *"_expect);
|
||
CHECK (showType<M5::Param>() == "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<Buffer> (-55);
|
||
|
||
auto fun_singleParamOut = [](short param, Buffer* buff) { *buff = param-1; };
|
||
using M1 = FeedManifold<decltype(fun_singleParamOut)>;
|
||
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.createFeed(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<long>() == -55);
|
||
m1.connect();
|
||
CHECK (*m1.outArgs == -55);
|
||
|
||
m1.invoke();
|
||
CHECK (*m1.outArgs == 0 - 1); // fun_singleParamOut() -> param - 1 and param ≡ 0
|
||
CHECK (buff.accessAs<long>() == 0 - 1);
|
||
long& calcResult = buff.accessAs<long>(); // for convenience use a reference into the result buffer
|
||
|
||
|
||
|
||
//_____________________________________
|
||
// 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<decltype(fun_paramSimple)>;
|
||
CHECK ( P1x::hasParam());
|
||
CHECK ( P1x::hasParamFun());
|
||
CHECK (not P1x::canActivate());
|
||
|
||
P1x p1x = p1.moveAdapted (move(fun_paramSimple));
|
||
M1 m1x = p1x.createFeed(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.createFeed(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.
|
||
}
|
||
};
|
||
|
||
|
||
/** Register this test class... */
|
||
LAUNCHER (NodeBase_test, "unit node");
|
||
|
||
|
||
|
||
}}} // namespace steam::engine::test
|