* Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
336 lines
17 KiB
C++
336 lines
17 KiB
C++
/*
|
||
JobPlanningPipeline(Test) - structure and setup of the job-planning pipeline
|
||
|
||
Copyright (C)
|
||
2023, 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 job-planning-pipeline-test.cpp
|
||
** unit test \ref JobPlanningPipeline_test
|
||
*/
|
||
|
||
|
||
#include "lib/test/run.hpp"
|
||
#include "lib/test/test-helper.hpp"
|
||
#include "steam/engine/mock-dispatcher.hpp"
|
||
|
||
#include "lib/iter-explorer.hpp"
|
||
#include "lib/format-string.hpp"
|
||
#include "lib/format-util.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
|
||
using test::Test;
|
||
using lib::eachNum;
|
||
using lib::explore;
|
||
using lib::time::PQuant;
|
||
using lib::time::FrameRate;
|
||
using util::isnil;
|
||
using util::_Fmt;
|
||
|
||
|
||
namespace steam {
|
||
namespace engine{
|
||
namespace test {
|
||
|
||
using lib::time::FixedFrameQuantiser;
|
||
|
||
namespace { // test fixture...
|
||
|
||
/** Diagnostic helper: join all the elements from some given container or iterable */
|
||
template<class II>
|
||
inline string
|
||
materialise (II&& ii)
|
||
{
|
||
return util::join (std::forward<II> (ii), "-");
|
||
}
|
||
|
||
inline PQuant
|
||
frameGrid (FrameRate fps)
|
||
{
|
||
return PQuant (new FixedFrameQuantiser (fps));
|
||
}
|
||
|
||
} // (End) test fixture
|
||
|
||
|
||
|
||
/****************************************************************************//**
|
||
* @test demonstrate interface, structure and setup of the job-planning pipeline.
|
||
* - using a frame step as base tick
|
||
* - invoke the dispatcher to retrieve the top-level JobTicket
|
||
* - expander function to explore prerequisite JobTickets
|
||
* - integration: generate a complete sequence of (dummy)Jobs
|
||
* - scaffolding and mocking used for this test
|
||
* @remark the »pipeline« is implemented as »Lumiera Forward Iterator«
|
||
* and thus forms a chain of on-demand processing. At the output side,
|
||
* fully defined render Jobs can be retrieved, ready for scheduling.
|
||
* @see DispatcherInterface_test
|
||
* @see MockSupport_test
|
||
* @see Dispatcher
|
||
* @see CalcStream
|
||
* @see RenderDriveS
|
||
*/
|
||
class JobPlanningPipeline_test : public Test
|
||
{
|
||
|
||
virtual void
|
||
run (Arg)
|
||
{
|
||
seedRand();
|
||
demonstrateScaffolding();
|
||
buildBaseTickGenerator();
|
||
accessTopLevelJobTicket();
|
||
exploreJobTickets();
|
||
integration();
|
||
}
|
||
|
||
|
||
/** @test document and verify the mock setup used for this test */
|
||
void
|
||
demonstrateScaffolding()
|
||
{
|
||
Time nominalTime = lib::test::randTime();
|
||
int additionalKey = rani(5000);
|
||
|
||
// (1) mocked render Job
|
||
MockJob mockJob{nominalTime, additionalKey};
|
||
mockJob.triggerJob();
|
||
CHECK (MockJob::was_invoked (mockJob));
|
||
CHECK (RealClock::wasRecently (MockJob::invocationTime (mockJob)));
|
||
CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) );
|
||
CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
|
||
|
||
// (2) Build a mocked Segment at [10s ... 20s[
|
||
MockSegmentation mockSegs{MakeRec()
|
||
.attrib ("start", Time{0,10} // start time (inclusive) of the Segment at 10sec
|
||
,"after", Time{0,20} // the Segment ends *before* 20sec
|
||
,"mark", 123) // marker-ID 123 (can be verified from Job invocation)
|
||
.scope(MakeRec() // this JobTicket also defines a prerequisite ticket
|
||
.attrib("mark",555) // using a different marker-ID 555
|
||
.genNode()
|
||
)
|
||
.genNode()};
|
||
fixture::Segment const& seg = mockSegs[Time{0,15}]; // access anywhere 10s <= t < 20s
|
||
JobTicket& ticket = seg.jobTicket(0); // get the master-JobTicket from this segment
|
||
JobTicket& prereq = *(ticket.getPrerequisites()); // pull a prerequisite JobTicket
|
||
|
||
Job jobP = prereq.createJobFor(Time{0,15}); // create an instance of the prerequisites for some time(irrelevant)
|
||
Job jobM = ticket.createJobFor(Time{0,15}); // ...and an instance of the master job for the same time
|
||
CHECK (MockJobTicket::isAssociated (jobP, prereq));
|
||
CHECK (MockJobTicket::isAssociated (jobM, ticket));
|
||
CHECK (not MockJobTicket::isAssociated (jobP, ticket));
|
||
CHECK (not MockJobTicket::isAssociated (jobM, prereq));
|
||
|
||
jobP.triggerJob();
|
||
jobM.triggerJob();
|
||
CHECK (123 == MockJob::invocationAdditionalKey (jobM)); // verify each job was invoked and linked to the correct spec,
|
||
CHECK (555 == MockJob::invocationAdditionalKey (jobP)); // indicating that in practice it will activate the proper render node
|
||
|
||
// (3) demonstrate mocked frame dispatcher...
|
||
MockDispatcher dispatcher; // a complete dispatcher backed by a mock Segment for the whole timeline
|
||
auto [port1,sink1] = dispatcher.getDummyConnection(1); // also some fake ModelPort and DataSink entries are registered
|
||
Job jobD = dispatcher.createJobFor (1, Time{0,30});
|
||
CHECK (dispatcher.verify(jobD, port1, sink1)); // the generated job uses the associated ModelPort and DataSink and JobTicket
|
||
}
|
||
|
||
|
||
|
||
/** @test use the Dispatcher interface (mocked) to generate a frame »beat«
|
||
* - demonstrate explicitly the mapping of a (frame) number sequence
|
||
* onto a sequence of time points with the help of time quantisation
|
||
* - use the Dispatcher API to produce the same frame time sequence
|
||
* @remark this is the foundation to generate top-level frame render jobs
|
||
*/
|
||
void
|
||
buildBaseTickGenerator()
|
||
{
|
||
auto grid = frameGrid(FrameRate::PAL); // one frame ≙ 40ms
|
||
|
||
CHECK (materialise(
|
||
explore (eachNum(5,13))
|
||
.transform([&](FrameCnt frameNr)
|
||
{
|
||
return grid->timeOf (frameNr);
|
||
})
|
||
)
|
||
== "200ms-240ms-280ms-320ms-360ms-400ms-440ms-480ms"_expect);
|
||
|
||
|
||
MockDispatcher dispatcher;
|
||
play::Timings timings (FrameRate::PAL);
|
||
|
||
CHECK (materialise (
|
||
dispatcher.forCalcStream(timings)
|
||
.timeRange(Time{200,0}, Time{500,0}) // Note: end point is exclusive
|
||
)
|
||
== "200ms-240ms-280ms-320ms-360ms-400ms-440ms-480ms"_expect);
|
||
}
|
||
|
||
|
||
/** @test use the base tick to access the corresponding JobTicket
|
||
* through the Dispatcher interface (mocked here).
|
||
*/
|
||
void
|
||
accessTopLevelJobTicket()
|
||
{
|
||
MockDispatcher dispatcher;
|
||
|
||
play::Timings timings (FrameRate::PAL);
|
||
auto [port,sink] = dispatcher.getDummyConnection(0);
|
||
auto pipeline = dispatcher.forCalcStream (timings)
|
||
.timeRange(Time{200,0}, Time{300,0})
|
||
.pullFrom (port);
|
||
|
||
CHECK (not isnil (pipeline));
|
||
CHECK (pipeline->isTopLevel()); // is a top-level ticket
|
||
JobTicket& ticket = pipeline->ticket();
|
||
|
||
Job job = ticket.createJobFor(Time::ZERO); // actual time point is irrelevant here
|
||
CHECK (dispatcher.verify(job, port, sink));
|
||
}
|
||
|
||
|
||
/** @test build and verify the exploration function to discover job prerequisites
|
||
* - use a setup where the master ExitNode requires a prerequisite ExitNode to be pulled
|
||
* - mark the pipeline-IDs, so that both nodes can be distinguished in the resulting Jobs
|
||
* - the `expandPrerequisites()` builder function uses JobTicket::getPrerequisites()
|
||
* - and this »expander« function is unfolded recursively such that first the source
|
||
* appears in the iterator, and as next step the child prerequisites, possibly to
|
||
* be unfolded further recursively
|
||
* - by design of the iterator pipeline, it is always possible to access the `PipeFrameTick`
|
||
* - this corresponds to the top-level JobTicket, which will produce the final frame
|
||
* - putting all these information together, proper working can be visualised.
|
||
*/
|
||
void
|
||
exploreJobTickets()
|
||
{
|
||
MockDispatcher dispatcher{MakeRec() // define a single segment for the complete time axis
|
||
.attrib("mark", 11) // the »master job« for each frame has pipeline-ID ≔ 11
|
||
.scope(MakeRec()
|
||
.attrib("mark",22) // add a »prerequisite job« marked with pipeline-ID ≔ 22
|
||
.genNode())
|
||
.genNode()};
|
||
|
||
play::Timings timings (FrameRate::PAL);
|
||
auto [port,sink] = dispatcher.getDummyConnection(0);
|
||
auto pipeline = dispatcher.forCalcStream (timings)
|
||
.timeRange(Time{200,0}, Time{300,0})
|
||
.pullFrom (port)
|
||
.expandPrerequisites();
|
||
|
||
// the first element is identical to previous test
|
||
CHECK (not isnil (pipeline));
|
||
CHECK (pipeline->isTopLevel());
|
||
Job job = pipeline->ticket().createJobFor (Time::ZERO);
|
||
CHECK (11 == job.parameter.invoKey.part.a);
|
||
|
||
auto visualise = [](auto& pipeline) -> string
|
||
{
|
||
Time frame{pipeline.currPoint}; // can access the embedded PipeFrameTick core to get "currPoint" (nominal time)
|
||
Job job = pipeline->ticket().createJobFor(frame); // looking always at the second element, which is the current JobTicket
|
||
TimeValue nominalTime{job.parameter.nominalTime}; // job parameter holds the microseconds (gavl_time_t)
|
||
int32_t mark = job.parameter.invoKey.part.a; // the MockDispatcher places the given "mark" here
|
||
return _Fmt{"J(%d|%s)"} % mark % nominalTime;
|
||
};
|
||
CHECK (visualise(pipeline) == "J(11|200ms)"_expect); // first job in pipeline is at t=200ms and has mark=11 (it's the master Job for this frame)
|
||
|
||
CHECK (materialise (pipeline.transform (visualise))
|
||
== "J(11|200ms)-J(22|200ms)-J(11|240ms)-J(22|240ms)-J(11|280ms)-J(22|280ms)"_expect);
|
||
}
|
||
|
||
|
||
|
||
/** @test Job-planning pipeline integration test
|
||
* - use the MockDispatcher to define a fake model setup
|
||
* - define three levels of prerequisites
|
||
* - also define a second segment with different structure
|
||
* - build a complete Job-Planning pipeline
|
||
* - define a visualisation to expose generated job parameters
|
||
* - iterate the Job-Planning pipeline and apply the visualisation
|
||
*/
|
||
void
|
||
integration()
|
||
{
|
||
MockDispatcher dispatcher{MakeRec() // start with defining a first segment...
|
||
.attrib("mark", 11) // the »master job« for each frame has pipeline-ID ≔ 11
|
||
.attrib("runtime", Duration{Time{10,0}})
|
||
.scope(MakeRec()
|
||
.attrib("mark",22) // a »prerequisite job« marked with pipeline-ID ≔ 22
|
||
.attrib("runtime", Duration{Time{20,0}})
|
||
.scope(MakeRec()
|
||
.attrib("mark",33) // further »recursive prerequisite«
|
||
.attrib("runtime", Duration{Time{30,0}})
|
||
.genNode())
|
||
.genNode())
|
||
.genNode()
|
||
,MakeRec() // add a second Segment with different calculation structure
|
||
.attrib("start", Time{250,0}) // partitioning the timeline at 250ms
|
||
.attrib("mark", 44)
|
||
.attrib("runtime", Duration{Time{70,0}})
|
||
.scope(MakeRec() // on 2nd level we have two independent prerequisites here
|
||
.attrib("mark", 55) // ...both will line up before the deadline of ticket No.44
|
||
.attrib("runtime", Duration{Time{60,0}})
|
||
.genNode()
|
||
,MakeRec()
|
||
.attrib("mark", 66)
|
||
.attrib("runtime", Duration{Time{50,0}})
|
||
.genNode())
|
||
.genNode()};
|
||
|
||
|
||
play::Timings timings (FrameRate::PAL, Time{0,1}); // Timings anchored at wall-clock-time ≙ 1s
|
||
auto [port,sink] = dispatcher.getDummyConnection(0);
|
||
auto pipeline = dispatcher.forCalcStream (timings)
|
||
.timeRange(Time{200,0}, Time{300,0})
|
||
.pullFrom (port)
|
||
.expandPrerequisites()
|
||
.feedTo (sink);
|
||
|
||
// this is the complete job-planning pipeline now
|
||
// and it is wrapped into a Dispatcher::PlanningPipeline front-end
|
||
CHECK (not isnil (pipeline));
|
||
CHECK (pipeline->isTopLevel());
|
||
// Invoking convenience functions on the PlanningPipeline front-end...
|
||
CHECK (5 == pipeline.currFrameNr());
|
||
CHECK (not pipeline.isBefore (Time{200,0}));
|
||
CHECK ( pipeline.isBefore (Time{220,0}));
|
||
|
||
Job job = pipeline.buildJob(); // invoke the JobPlanning to build a Job for the first frame
|
||
CHECK (Time(200,0) == job.parameter.nominalTime);
|
||
CHECK (11 == job.parameter.invoKey.part.a);
|
||
|
||
auto visualise = [](auto& pipeline) -> string
|
||
{
|
||
Job job = pipeline.buildJob(); // let the JobPlanning construct the »current job«
|
||
TimeValue nominalTime{job.parameter.nominalTime}; // job parameter holds the microseconds (gavl_time_t)
|
||
int32_t mark = job.parameter.invoKey.part.a; // the MockDispatcher places the given "mark" here
|
||
TimeValue deadline{pipeline.determineDeadline()};
|
||
return _Fmt{"J(%d|%s⧐%s)"}
|
||
% mark % nominalTime % deadline;
|
||
};
|
||
CHECK (visualise(pipeline) == "J(11|200ms⧐1s180ms)"_expect); // first job in pipeline: nominal t=200ms,
|
||
// .... 10ms engine latency + 10ms job runtime ⟶ deadline 1s180ms
|
||
CHECK (materialise(
|
||
explore(move(pipeline))
|
||
.transform(visualise)
|
||
)
|
||
== "J(11|200ms⧐1s180ms)-J(22|200ms⧐1s150ms)-J(33|200ms⧐1s110ms)-" // ... -(10+10) | -(10+10)-(10+20) | -(10+10)-(10+20)-(10+30)
|
||
"J(11|240ms⧐1s220ms)-J(22|240ms⧐1s190ms)-J(33|240ms⧐1s150ms)-"
|
||
"J(44|280ms⧐1s200ms)-J(66|280ms⧐1s140ms)-J(55|280ms⧐1s130ms)"_expect); // ... these call into the 2nd Segment
|
||
}
|
||
};
|
||
|
||
|
||
/** Register this test class... */
|
||
LAUNCHER (JobPlanningPipeline_test, "unit engine");
|
||
|
||
|
||
|
||
}}} // namespace steam::engine::test
|