LUMIERA.clone/tests/core/steam/engine/job-planning-pipeline-test.cpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* 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.
2024-11-17 23:42:55 +01:00

336 lines
17 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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