From d109f5e1fb48a74a291b9a4b79a19c779148c2f0 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 22 Jun 2023 20:23:55 +0200 Subject: [PATCH] bye bye Monad (closes #1276) after completing the recent clean-up and refactoring work, the monad based framework for recursive tree expansion can be abandoned and retracted. This approach from functional programming leads to code, which is ''cool to write'' yet ''hard to understand.'' A second design attempt was based on the pipeline and decorator pattern and integrates the monadic expansion as a special case, used here to discover the prerequisites for a render job. This turned out to be more effective and prolific and became standard for several exploring and backtracking algorithms in Lumiera. --- src/lib/hierarchy-orientation-indicator.hpp | 134 --- src/lib/iter-explorer.hpp | 1027 ----------------- src/steam/engine/calc-plan-continuation.cpp | 124 -- src/steam/engine/calc-plan-continuation.hpp | 132 --- src/steam/engine/dispatch-table.cpp | 18 +- src/steam/engine/dispatch-table.hpp | 8 +- src/steam/engine/dispatcher.cpp | 24 +- src/steam/engine/dispatcher.hpp | 67 +- src/steam/engine/frame-coord.hpp | 133 --- src/steam/engine/job-planning.hpp | 323 +----- src/steam/engine/job-ticket.hpp | 193 +--- src/steam/engine/render-drive.cpp | 9 +- src/steam/engine/render-drive.hpp | 3 +- src/steam/engine/time-anchor.hpp | 196 ---- tests/15library.tests | 5 - tests/47engine.tests | 2 +- tests/core/steam/engine/mock-dispatcher.cpp | 2 +- tests/core/steam/engine/mock-dispatcher.hpp | 16 +- .../steam/engine/scheduler-interface-test.cpp | 6 +- tests/core/steam/engine/timings-test.cpp | 10 - .../steam/fixture/fixture-segment-test.cpp | 1 - .../hierarchy-orientation-indicator-test.cpp | 416 ------- tests/library/iter-explorer-test.cpp | 582 ---------- tests/library/iter-tree-explorer-test.cpp | 41 +- wiki/renderengine.html | 10 +- wiki/thinkPad.ichthyo.mm | 149 ++- 26 files changed, 185 insertions(+), 3446 deletions(-) delete mode 100644 src/lib/hierarchy-orientation-indicator.hpp delete mode 100644 src/lib/iter-explorer.hpp delete mode 100644 src/steam/engine/calc-plan-continuation.cpp delete mode 100644 src/steam/engine/calc-plan-continuation.hpp delete mode 100644 src/steam/engine/frame-coord.hpp delete mode 100644 src/steam/engine/time-anchor.hpp delete mode 100644 tests/library/hierarchy-orientation-indicator-test.cpp delete mode 100644 tests/library/iter-explorer-test.cpp diff --git a/src/lib/hierarchy-orientation-indicator.hpp b/src/lib/hierarchy-orientation-indicator.hpp deleted file mode 100644 index 82e22d1c3..000000000 --- a/src/lib/hierarchy-orientation-indicator.hpp +++ /dev/null @@ -1,134 +0,0 @@ -/* - HIERARCHY-ORIENTATION-INDICATOR.hpp - helper to mark level on tree navigation - - Copyright (C) Lumiera.org - 2013, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file hierarchy-orientation-indicator.hpp - ** Helper to support navigating a tree structure. - ** The OrientationIndicator records reference levels (depth into the tree) - ** and can then be used to determine the relative orientation between the - ** previously marked reference level and the current reference level. - ** This simple state capturing mechanism can be used to track the path - ** of a tree visitation, or to sync an external stack with a currently - ** investigated tree level. - ** - ** The relative orientation value can be retrieved through an int conversion; - ** to ease recursive programming, this statefull value can be incremented and - ** decremented without influencing the captured reference level. - ** - ** @see job-ticket.hpp usage example - ** @see HierarchyOrientationIndicator_test - ** - */ - - -#ifndef HIERARCHY_ORIENTATION_INDICATOR_H -#define HIERARCHY_ORIENTATION_INDICATOR_H - - -#include -#include -#include - -namespace lib { - - using std::string; - - - class OrientationIndicator - { - size_t refLevel_; - ptrdiff_t offset_; - - public: - OrientationIndicator() - : refLevel_(0) - , offset_(0) - { } - - // using default copy/assignment - - operator ptrdiff_t() const - { - return offset_; - } - - - - void - markRefLevel (size_t newRefLevel) - { - REQUIRE (newRefLevel < size_t(std::numeric_limits::max()) ); - - offset_ -= newRefLevel - refLevel_; - refLevel_ = newRefLevel; - } - - /** define the current offset as new reference point */ - OrientationIndicator& - markRefLevel () - { - REQUIRE (0 < ptrdiff_t(refLevel_) + offset_); - - markRefLevel (refLevel_ + offset_); - ENSURE (offset_ == 0); - return *this; - } - - OrientationIndicator& - resetToRef () - { - offset_ = 0; - return *this; - } - - OrientationIndicator& - operator+= (int adj) - { - offset_ += adj; - return *this; - } - - OrientationIndicator& - operator-= (int adj) - { - offset_ -= adj; - return *this; - } - - OrientationIndicator& - operator++ () - { - this->operator +=(1); - return *this; - } - - OrientationIndicator& - operator-- () - { - this->operator -=(1); - return *this; - } - }; - - - -} // namespace lib -#endif /*HIERARCHY_ORIENTATION_INDICATOR_HPP_*/ diff --git a/src/lib/iter-explorer.hpp b/src/lib/iter-explorer.hpp deleted file mode 100644 index d9fe4a4ae..000000000 --- a/src/lib/iter-explorer.hpp +++ /dev/null @@ -1,1027 +0,0 @@ -/* - ITER-EXPLORER.hpp - building blocks for iterator evaluation strategies - - Copyright (C) Lumiera.org - 2012, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file iter-explorer.hpp - ** Helper template(s) for establishing various evaluation strategies for hierarchical data structures. - ** Based on the Lumiera Forward Iterator concept and using the basic IterAdaptor templates, - ** these components allow to implement typical evaluation strategies, like e.g. depth-first or - ** breadth-first exploration of a hierarchical structure. Since the access to this structure is - ** abstracted through the underlying iterator, what we effectively get is a functional datastructure. - ** The implementation is based on the IterStateWrapper, which is one of the basic helper templates - ** provided by iter-adapter.hpp. - ** - ** \par Iterators as Monad - ** The fundamental idea behind the implementation technique used here is the \em Monad pattern - ** known from functional programming. A Monad is a (abstract) container created by using some specific functions. - ** This is an rather abstract concept with a wide variety of applications (things like IO state, parsers, combinators, - ** calculations with exception handling but also simple data structures like lists or trees). The key point with any - ** monad is the ability to \em bind a function into the monad; this function will work on the \em internals of the - ** monad and produce a modified new monad instance. In the simple case of a list, "binding" a function - ** basically means to map the function onto the elements in the list. - ** - ** \par Rationale - ** The primary benefit of using the monad pattern is to separate the transforming operation completely from - ** the mechanics of applying that operation and combining the results. More specifically, we rely on an iterator - ** to represent an abstracted source of data and we expose the combined and transformed results again as such - ** an abstracted data sequence. The transformation to apply can be selected at runtime (as a functor), and - ** also the logic how to combine elements can be implemented elsewhere. The monad pattern defines a sane - ** way of representing this partial evaluation state without requiring a container for intermediary - ** results. This is especially helpful when - ** - a flexible and unspecific source data structure needs to be processed - ** - and this evaluation needs to be done asynchronously and in parallel (no locking, immutable data) - ** - and a partial evaluation needs to be stored as continuation (not relying on the stack for partial results) - ** - ** \par preconfigured solutions - ** This header provides some preconfigured applications of this pattern - ** - the DefaultCombinator processes the source elements on demand, feeding them through - ** the given functor and using the resulting iterator to deliver the result elements - ** - Chained iterator uses similar building blocks just to get the "flattening" of - ** a sequence of iterators into a single result iterator - ** - the RecursiveExhaustingEvaluation is another kind of combination strategy, - ** which recursively evaluates the given function and combines the results - ** such as to produce classical depth-first and breadth-first search orders. - ** - the more low-level RecursiveSelfIntegration combinator strategy actually - ** delegates to the result set iterator implementation to perform the collecting - ** and re-integrating of intermediary results. This approach is what we actually - ** use in the steam::engine::Dispatcher - ** - ** Alternatively, just the basic IterExplorer template can be used together with a custom - ** "combinator strategy" and typically even a specific iterator or sequence to implement very specific - ** and optimised data structure evaluation patterns. This strategy needs to define some way to hold onto - ** the original source elements, feed them through the functor on demand and recombine the result sets - ** into a new sequence to be delivered on demand. - ** Actually this is what we utilise for the continuous render job generation within the scheduler. - ** All the other preconfigured variants defined here where created as proof-of-concept, to document - ** and verify this implementation technique as such. - ** - ** @todo as of 2017, this framework is deemed incomplete and requires more design work. ////////////////////TICKET #1116 - ** @deprecated Monads considered harmful -- as of 4/2023 this framework is about to be abandoned - ** - ** @warning preferably use value semantics for the elements to be processed. Recall, C++ is not - ** really a functional programming language, and there is no garbage collector. It might be - ** tempting just to pass pointers through a whole evaluation chain. Indeed, you can do so, - ** but make sure you understand the precise timing of the evaluation, expansion and - ** re-integration steps with regards to memory management; an "explorer function" - ** may pass a reference or pointer to some transient source, which is gone after - ** incrementing the source iterator. - ** @see IterExplorer_test - ** @see iter-adapter.hpp - ** @see itertools.hpp - ** @see IterSource (completely opaque iterator) - ** - */ - - -#ifndef LIB_ITER_EXPLORER_H -#define LIB_ITER_EXPLORER_H - - -#include "lib/error.hpp" -#include "lib/meta/function.hpp" -#include "lib/iter-adapter.hpp" -#include "lib/iter-stack.hpp" -#include "lib/meta/trait.hpp" -#include "lib/null-value.hpp" -#include "lib/util.hpp" - -#include -#include - - -namespace lib { - - namespace iter_explorer { - - template - class DefaultCombinator; - } - - - - /** - * Adapter for using an Iterator in the way of a Monad - * This allows to "bind" or "flatMap" a functor, thereby creating - * a derived version of the iterator, yielding the (flattened) combination - * of all the results generated by this bound functor. The rationale is - * to apply some exploration or expansion pattern on the elements of the - * source sequence (source iterator) -- while completely separating out - * the \em mechanics how to treat and combine individual elements. - * - * \par implementation approach - * IterExplorer is a thin wrapper, based on IterStateWrapper; thus the assumption - * is for the actual elements to be generated by a "state core", which is embedded - * right into each instance. To provide the monad bind operator \c >>= , we need - * the help of a strategy template, the so called \em Combinator. This strategy - * contains the details how to combine the results of various iterators and - * to join them together into a single new IterExplorer instance. Thus, obviously - * this Combinator strategy template relies on additional knowledge about the - * source iterator's implementation. Such is unavoidable, since C++ isn't a - * functional programming language. - * - * When invoking the bind (flat map) operator, a suitably represented \em functor - * is embedded into an instance of the Combinator template. Moreover, a copy of - * the current IterExplorer is embedded alongside. The resulting object becomes - * the state core of the new IterExplorer object returned as result. - * So the result \em is an iterator, but -- when "pulled" -- it will in turn - * pull from the source iterator and feed the provided elements through the - * \em exploration functor, which this way has been \em bound into the resulting - * monad. The purpose of the Combinator strategy is to join together various - * resulting iterators, thereby "flattening" one level of "monad packaging" - * (hence the name "flat map operator"). - * - * @remarks figuring out what happens here just from the code might lead to confusion. - * You really need to grasp the monad concept first, which is a design pattern - * commonly used in functional programming. For the practical purpose in question - * here, you might consider it a "programmable semicolon": it links together a - * chain of operations detailed elsewhere, and provided in a dynamic fashion - * (as functor at runtime). More specifically, these operations will expand - * and explore a dynamic source data set on the fly. Notably this allows - * us to handle such an "exploration" in a flexible way, without the need - * to allocate heap memory to store intermediary results, but also without - * using the stack and recursive programming. - * @warning be sure to consider the effects of copying iterators on any hidden state - * referenced by the source iterator(s). Basically any iterator must behave - * sane when copied while in iteration: IterExplorer first evaluates the - * head element of the source (the explorer function should build an - * independent, new result sequence based on this first element). - * Afterwards, the source is \em advanced and then \em copied - * into the result iterator. - * @warning IterExplorer and all the provided combination strategies are deliberately - * designed to work on sequences of values. These values will indeed - * be \em copied for invocation of the exploration function. The rationale - * for this choice is data locality and the danger of dangling references - * to an implicitly created temporary (since in it is so common practice - * in C++ to use \c const& ). Thus, if you need to work on references - * to a common data structure, you need to use either pointers or - * some reference wrapper explicitly as value type right from start. - * @param SRC the source sequence or iterator to wrap - * @param _COM_ "Combinator" strategy template. When binding (\c >>= ) a function, - * an instance of that strategy becomes the new SRC for the created new - * IterExplorer. This instantiation of the strategy gets as type parameters: - * - this IterExplorer's instance type - * - the type of the function bound with \c >>= - */ - template class _COM_ = iter_explorer::DefaultCombinator - > - class IterExplorer - : public IterStateWrapper - { - - - public: - typedef typename SRC::value_type value_type; - typedef typename SRC::reference reference; - typedef typename SRC::pointer pointer; - - /** Metafunction: the resulting type when binding ("flat mapping") - * a functor of type FUN. Basically the result of binding a function - * is again an IterExplorer (with an "expanded" state core type) */ - template - struct FlatMapped - { - typedef IterExplorer<_COM_, _COM_> Type; - }; - - - - /** by default create an empty iterator */ - IterExplorer() { } - - - /** wrap an iterator-like state representation - * to build it into a monad. The resulting entity - * is both an iterator yielding the elements generated - * by the core, and it provides the (monad) bind operator. - */ - explicit - IterExplorer (SRC const& iterStateCore) - : IterStateWrapper (iterStateCore) - { } - - - /** monad bind ("flat map") operator. - * Using a specific function to explore and work - * on the "contents" of this IterExplorer, with the goal - * to build a new IterExplorer combining the results of this - * function application. The enclosing IterExplorer instance - * provides a Strategy template _COM_, which defines how those - * results are actually to be combined. An instantiation of - * this "Combinator" strategy becomes the state core - * of the result iterator. - */ - template - typename FlatMapped::Type - operator >>= (FUN explorer) - { - typedef _COM_ Combinator; // instantiation of the combinator strategy - typedef typename FlatMapped::Type ResultsIter; // result IterExplorer using that instance as state core - - return ResultsIter ( - Combinator (explorer // build a new iteration state core - ,accessRemainingElements())); // based on a copy of this iterator / sequence - } - - - - private: - IterExplorer const& - accessRemainingElements() - { - return *this; - } - }; - - - - - - - - - - - namespace iter_explorer { ///< predefined "exploration strategies", policies and configurations - - using util::unConst; - using lib::meta::enable_if; - using lib::meta::disable_if; - using std::function; - using meta::_Fun; - - - /** - * Building block: just evaluate source elements. - * This strategy will be tied into a "Combinator" - * to hold the actual functor bound into the enclosing - * IterExplorer monad to work on the contained elements. - */ - template - struct ExploreByFunction - : function - { - template - ExploreByFunction(FUN explorationFunctionDefinition) - : function(explorationFunctionDefinition) - { } - - ExploreByFunction() { } ///< by default initialised to bottom function - }; - - /** - * Support for a special use case: an Iterator of Iterators, joining results. - * In this case, already the source produces a sequence of Iterators, which - * just need to be passed through to the output buffer unaltered. Using this - * within the DefaultCombinator strategy creates a combined, flattened iterator - * of all the source iterator's contents. - */ - template - struct UnalteredPassThrough; - - template - struct UnalteredPassThrough - { - IT operator() (IT elm) const { return elm; } - bool operator! () const { return false; } ///< identity function is always valid - }; - - - - /** - * Building block: evaluate and combine a sequence of iterators. - * This implementation helper provides two kinds of "buffers" (actually implemented - * as iterators): A result buffer (iterator) which holds a sequence of already prepared - * result elements, which can be retrieved through iteration right away. And a supply buffer - * (iterator) holding raw source elements. When the result buffer is exhausted, the next source - * element will be pulled from there and fed through the "evaluation strategy", which typically - * is a function processing the source element and producing a new result buffer (iterator). - */ - template class _EXP_ = ExploreByFunction ///< Strategy: how to store and evaluate the function to apply on each element - > - class CombinedIteratorEvaluation - { - typedef typename _Fun::Ret ResultIter; - typedef typename SRC::value_type SrcElement; - typedef _EXP_ Explorer; - - - SRC srcSeq_; - ResultIter results_; - Explorer explorer_; - - public: - typedef typename ResultIter::value_type value_type; - typedef typename ResultIter::reference reference; - typedef typename ResultIter::pointer pointer; - - - CombinedIteratorEvaluation() { } - - CombinedIteratorEvaluation(FUN explorerFunction) - : srcSeq_() - , results_() - , explorer_(explorerFunction) - { } - - // using standard copy operations - - - void - setSourceSequence (SRC const& followUpSourceElements) - { - REQUIRE (explorer_); - srcSeq_ = followUpSourceElements; - } - - private: - bool - findNextResultElement() - { - while (!results_ && srcSeq_) - { - results_ = explorer_(*srcSeq_); - ++srcSeq_; - } - return bool(results_); - - } - - public: /* === Iteration control API for IterStateWrapper== */ - bool - checkPoint() const - { - return unConst(this)->findNextResultElement(); - } - - reference - yield() const - { - return *results_; - } - - void - iterNext() - { - ++results_; - } - }; - - /** - * a generic "Combinator strategy" for IterExplorer. - * This default / fallback solution doesn't assume anything beyond the - * source and the intermediary result(s) to be Lumiera Forward Iterators. - * @note the implementation stores the functor into a std::function object, - * which might cause heap allocations, depending on the function given. - * Besides, the implementation holds one instance of the (intermediary) - * result iterator (yielded by invoking the function) and a copy of the - * original IterExplorer source sequence, to get the further elements - * when the initial results are exhausted. - */ - template - class DefaultCombinator - : public CombinedIteratorEvaluation - { - typedef typename _Fun::Ret ResultIter; - - public: - DefaultCombinator() { } - - DefaultCombinator(FUN explorerFunction, SRC const& sourceElements) - : CombinedIteratorEvaluation(explorerFunction) - { - this->setSourceSequence (sourceElements); - } - }; - - - /** Metafunction to detect an iterator yielding an iterator sequence */ - template - struct _is_iterator_of_iterators - { - typedef typename IT::value_type IteratorElementType; - - enum{ value = meta::can_IterForEach::value }; - }; - - - template - class ChainedIteratorImpl - : public CombinedIteratorEvaluation - { }; - - - /** - * Special iterator configuration for combining / flattening the - * results of a sequence of iterators. This sequence of source iterators - * is assumed to be available as "Iterator yielding Iterators". - * The resulting class is a Lumiera Forward Iterator, delivering all the - * elements of all source iterators in sequence. - * @remarks this is quite similar to the IterExplorer monad, but without - * binding an exploration function to produce the result sequences. - * Rather, the result sequences are directly pulled from the source - * sequence, which thus needs to be an "Iterator of Iterators". - * Beyond that, the implementation relies on the same building - * blocks as used for the full-blown IterExplorer. - * @param ITI iterator of iterators - * @param SEQ type of the individual sequence (iterator). - * The value_type of this sequence will be the overall - * resulting value type of the flattened sequence - */ - template - class ChainedIters; - - template - class ChainedIters> - > - : public IterStateWrapper - > - { - public: - ChainedIters(ITI const& iteratorOfIterators) - { // note: default ctor on parent -> empty sequences - this->stateCore().setSourceSequence (iteratorOfIterators); - } - }; - - /** - * Convenience specialisation: manage the sequence of iterators automatically. - * @note in this case the \em first template parameter denotes the \em element sequence type; - * we use a IterStack to hold the sequence-of-iterators in heap storage. - * @warning this specialisation will not be picked, if the \em value-type - * of the given iterator is itself an iterator - */ - template - class ChainedIters> - > - : public IterStateWrapper, SEQ> - > - { - public: - typedef IterStack IteratorIterator; - - ChainedIters(IteratorIterator const& iteratorOfIterators) - { - this->stateCore().setSourceSequence (iteratorOfIterators); - } - - /** empty result sequence by default */ - ChainedIters() { } - }; - - - - - - - /** - * A "Combinator strategy" allowing to expand and evaluate a - * (functional) data structure successively and recursively. - * Contrary to the DefaultCombinator, here the explorer is evaluated - * repeatedly, feeding back the results until exhaustion. The concrete - * exploration function needs to embody some kind of termination condition, - * e.g. by returning an empty sequence at some point, otherwise infinite - * recursion might happen. Another consequence of this repeated re-evaluation - * is the requirement of the source sequence's element type to be compatible - * to the result sequence's element type -- we can't \em transform the contents - * of the source sequence into another data type, just explore and expand those - * contents into sub-sequences based on the same data type. (While this contradicts - * the full requirements for building a Monad, we can always work around that kind - * of restriction by producing the element type of the target sequence by implicit - * type conversion) - * - * \par strategy requirements - * To build a concrete combinator a special strategy template is required to define - * the actual implementation logic how to proceed with the evaluation (i.e. how to - * find the feed of the "next elements" and how to re-integrate the results of an - * evaluation step into the already expanded sequence of intermediary results. - * Moreover, this implementation strategy pattern is used as a data buffer - * to hold those intermediary results. Together, this allows to create - * various expansion patterns, e.g. depth-first or breadth-first. - * - \c Strategy::getFeed() accesses the point from where - * to pull the next element to be expanded. This function must not - * yield an empty sequence, \em unless the overall exploration is exhausted - * - \c Strategy::feedBack() re-integrates the results of an expansion step - * - * @warning beware, when tempted to pass elements by reference (or pointer) - * through the explorer function, make sure you really understand the - * working mode of the #iterate function with regards to memory management. - * When ResultIter attempts just to store a pointer, after incrementing - * \c ++feed(), depending on the internals of the actual src iterator, - * this pointer might end up dangling. Recommendation is to let the - * Explorer either take arguments or return results by value (copy). - */ - template class _BUF_ - > - class RecursiveExhaustingEvaluation - { - typedef typename _Fun::Ret ResultIter; - typedef typename _Fun::Sig Sig; - typedef function Explorer; - typedef _BUF_ Buffer; - - Buffer resultBuf_; - Explorer explore_; - - - public: - typedef typename ResultIter::value_type value_type; - typedef typename ResultIter::reference reference; - typedef typename ResultIter::pointer pointer; - - RecursiveExhaustingEvaluation (Explorer fun, SRC const& src) - : resultBuf_() - , explore_(fun) - { - resultBuf_.feedBack( - initEvaluation (src)); - } - - RecursiveExhaustingEvaluation() { }; - - // standard copy operations - - - private: - /** Extension point: build the initial evaluation state - * based on the source sequence (typically an IterExplorer). - * This is a tricky problem, since the source sequence is not - * necessarily assignment compatible to the ResultIter type - * and there is no general method to build a ResultIter. - * The solution is to rely on a "builder trait", which - * needs to be defined alongside with the concrete - * ResultIter type. The actual builder trait will - * be picked up through a free function (ADL). */ - ResultIter - initEvaluation (SRC const& initialElements) - { - ResultIter startSet; - return build(startSet).usingSequence(initialElements); - } // extension point: free function build (...) - - - /** @note \c _BUF_::getFeed is required to yield a non-empty sequence, - * until everything is exhausted. Basically the buffer- and re-integrated - * result sequences can be expected to be pulled until finding the next - * non-empty supply. This is considered hidden internal state and thus - * concealed within this \em const and \em idempotent function. - */ - ResultIter & - feed() const - { - return unConst(this)->resultBuf_.getFeed(); - } - - - public: /* === Iteration control API for IterStateWrapper== */ - - bool - checkPoint() const - { - return bool(feed()); - } - - reference - yield() const - { - return *feed(); - } - - void - iterNext() - { - REQUIRE (feed()); - ResultIter nextStep = explore_(*feed()); - ++ feed(); - resultBuf_.feedBack (nextStep); - } - }; - - - /** - * Strategy building block for recursive exhausting evaluation. - * Allows to create depth-fist or breadth-first evaluation patterns, just - * by using a suitably intermediary storage container to hold the partially - * evaluated iterators created at each evaluation step. Using a stack and - * pushing results will create a depth-first pattern, while using a queue - * will evaluate in layers (breadth-first). In both cases, the next - * evaluation step will happen at the iterator returned by #getFeed. - * @warning uses an empty-iterator marker object to signal exhaustion - * - this marker \c IT() may be re-initialised concurrently - * - accessing this marker during app shutdown might access - * an already defunct object - */ - template< class IT - , template class _QUEUE_ ///< the actual container to use for storage of intermediary results - > - class EvaluationBufferStrategy - { - _QUEUE_ intermediaryResults_; - - - /** @return default constructed (=empty) iterator - * @remarks casting away const is safe here, since all - * you can do with an NIL iterator is to test for emptiness. - */ - IT & - emptySequence() - { - return unConst(NullValue::get()); - } // unsafe during shutdown - - - public: - IT & - getFeed () - { - // fast forward to find the next non-empty result sequence - while (intermediaryResults_ && ! *intermediaryResults_) - ++intermediaryResults_; - - if (intermediaryResults_) - return *intermediaryResults_; - else - return emptySequence(); - } - - void - feedBack (IT const& newEvaluationResults) - { - intermediaryResults_.insert (newEvaluationResults); - } - }; - - - - /** - * concrete strategy for recursive \em depth-first evaluation. - * Using heap allocated storage in a STL Deque (used stack-like) - */ - template - struct DepthFirstEvaluationBuffer - : EvaluationBufferStrategy - { }; - - /** - * concrete strategy for recursive \em breadth-first evaluation. - * Using heap allocated storage in a STL Deque (used queue-like) - */ - template - struct BreadthFirstEvaluationBuffer - : EvaluationBufferStrategy - { }; - - - - /** - * preconfigured IterExplorer "state core" resulting in - * depth-first exhaustive evaluation - */ - template - struct DepthFirstEvaluationCombinator - : RecursiveExhaustingEvaluation - { - DepthFirstEvaluationCombinator() { } - - DepthFirstEvaluationCombinator(FUN explorerFunction, SRC const& sourceElements) - : RecursiveExhaustingEvaluation (explorerFunction,sourceElements) - { } - }; - - /** - * preconfigured IterExplorer "state core" resulting in - * breadth-first exhaustive evaluation - */ - template - struct BreadthFirstEvaluationCombinator - : RecursiveExhaustingEvaluation - { - BreadthFirstEvaluationCombinator() { } - - BreadthFirstEvaluationCombinator(FUN explorerFunction, SRC const& sourceElements) - : RecursiveExhaustingEvaluation (explorerFunction,sourceElements) - { } - }; - - - - - /** - * IterExplorer "state core" for progressively expanding - * an initial result set. This initial set can be conceived to hold the seed - * or starting points of evaluation. Elements are consumed by an iterator, at - * the front. Each element is fed to the "explorer function". This exploration - * returns an expanded result sequence, which is immediately integrated into the - * overall result sequence, followed by further exploration of the then-to-be first - * element of the result sequence. All this exploration is driven on-demand, by - * consuming the result sequence. Exploration will proceed until exhaustion, - * in which case the exploration function will yield an empty result set. - * - * This strategy is intended for use with the IterExplorer -- most prominently - * in use for discovering render prerequisites and creating new render jobs for - * the engine. The RecursiveSelfIntegration strategy is a partially reduced - * and hard-coded variation on the #RecursiveExhaustingEvaluation in depth-first - * configuration. - * This setup works in conjunction with a special result sequence type, - * with the ability to re-integrate results yielded by partial evaluation. - * But the working pattern is more similar to the #CombinedIteratorEvaluation, - * where the focal point of further expansion is always at the front end of - * further items yet to be consumed and expanded; thus the evaluation is - * bound to proceed depth-first (i.e. it is \em not possible to "integrate" - * any intermediary result with the \em whole result set, only with the part - * reachable immediately at the evaluation front) - * - * \par usage considerations - * There needs to be a specific sequence or iterator type, which is used to - * hold the result set(s). This custom type together with the Explorer function - * are performing the actual expansion and re-integration steps. The latter is - * accessed through the free function \c build(sequence) -- which is expected - * to return a "builder trait" for manipulating the element set yielded by - * the custom iterator type returned by the Explorer function. - * - * @param SRC the initial result set sequence; this iterator needs to yield - * values of the special ResultIter type, which are then expanded - * until exhaustion by repeated calls to the Explorer function - * @param FUN the Explorer function of type ResultIter -> ResultIter - * @note the ResultIter type is defined implicitly through the result type - * of the Explorer function. Similarly the result value type - * is defined through the typedef \c ResultIter::value_type - * @warning beware of dangling references; make sure you never pass a - * reference or pointer through the Explorer function unaltered. - * Because some source iterator might expose a reference to a - * transient object just for the purpose of expanding it. - * The very reason of building iterator pipelines is to - * avoid heap allocation and to produce intermediaries - * on demand. So make sure in such a case that there is - * at least one real copy operation in the pipeline. - */ - template - class RecursiveSelfIntegration - { - typedef typename _Fun::Ret ResultIter; - typedef typename _Fun::Sig Sig; - typedef typename SRC::value_type Val; - typedef function Explorer; - - SRC srcSeq_; - ResultIter outSeq_; - Explorer explore_; - - public: - typedef typename ResultIter::value_type value_type; - typedef typename ResultIter::reference reference; - typedef typename ResultIter::pointer pointer; - - RecursiveSelfIntegration (Explorer fun, SRC const& src) - : srcSeq_(src) - , outSeq_() - , explore_(fun) - { } - - RecursiveSelfIntegration() { }; - - // standard copy operations - - - private: - /** ensure the next elements to be processed - * will appear at outSeq_ head. When outSeq_ - * is still empty after this function, - * we're done. - * @note \em not calling this function after - * construction, because the typical user - * of this template will do that implicitly - * through invocation of #checkPoint */ - bool - findNextResultElement() - { - while (!outSeq_ && srcSeq_) - { - Val& nextElement(*srcSeq_); - build(outSeq_).wrapping(nextElement); // extension point: free function build(...).wrapping(...) - ++srcSeq_; - } - return bool(outSeq_); - } - - void - iterate () - { - REQUIRE (outSeq_); - ResultIter nextSteps = explore_(*outSeq_); - ++ outSeq_; - build(outSeq_).usingSequence(nextSteps); // extension point: free function build(...).usingSequence(...) - } - - - public: /* === Iteration control API for IterStateWrapper== */ - - bool - checkPoint() const - { - return unConst(this)->findNextResultElement(); - } - - reference - yield() const - { - return *outSeq_; - } - - void - iterNext() - { - this->iterate(); - } - }; - - - - - /** - * Helper template to bootstrap a chain of IterExplorers. - * This is a "state core", which basically just wraps a given - * source iterator and provides the necessary free functions - * (iteration control API) to use this as iteration state - * within IterExplorer. - * @note to ease building such an initial version of the Iterator Monad, - * use the free function \link #exploreIter \endlink - */ - template - class WrappedSequence - : public IT - { - public: - WrappedSequence() - : IT() - { } - - WrappedSequence(IT const& srcIter) - : IT(srcIter) - { } - - - /* === Iteration control API for IterStateWrapper == */ - - bool - checkPoint() const - { - return bool(*this); - } - - typename IT::reference - yield() const - { - return *(*this); - } - - void - iterNext() - { - ++(*this); - } - }; - - - template - struct DepthFirst - : IterExplorer, DepthFirstEvaluationCombinator> - { - DepthFirst() { }; - DepthFirst(SRC const& srcSeq) - : IterExplorer, DepthFirstEvaluationCombinator> (srcSeq) - { } - }; - - template - struct BreadthFirst - : IterExplorer, BreadthFirstEvaluationCombinator> - { - BreadthFirst() { }; - BreadthFirst(SRC const& srcSeq) - : IterExplorer, BreadthFirstEvaluationCombinator> (srcSeq) - { } - }; - - - }//(End) namespace iter_explorer : predefined policies and configurations - - - - - /* ==== convenient builder free functions ==== */ - - template - inline IterExplorer> - exploreIter (IT const& srcSeq) - { - return IterExplorer> (srcSeq); - } - - - template - inline iter_explorer::DepthFirst - depthFirst (IT const& srcSeq) - { - return iter_explorer::DepthFirst (srcSeq); - } - - - template - inline iter_explorer::BreadthFirst - breadthFirst (IT const& srcSeq) - { - return iter_explorer::BreadthFirst (srcSeq); - } - - - - template - inline iter_explorer::ChainedIters - iterChain(IT const& seq) - { - typename iter_explorer::ChainedIters::IteratorIterator sequenceOfIterators; - - sequenceOfIterators.push (seq); - return iter_explorer::ChainedIters(sequenceOfIterators); - } - - template - inline iter_explorer::ChainedIters - iterChain(IT const& seq1, IT const& seq2) - { - typename iter_explorer::ChainedIters::IteratorIterator sequenceOfIterators; - - sequenceOfIterators.push (seq2); - sequenceOfIterators.push (seq1); - return iter_explorer::ChainedIters(sequenceOfIterators); - } - - template - inline iter_explorer::ChainedIters - iterChain(IT const& seq1, IT const& seq2, IT const& seq3) - { - typename iter_explorer::ChainedIters::IteratorIterator sequenceOfIterators; - - sequenceOfIterators.push (seq3); - sequenceOfIterators.push (seq2); - sequenceOfIterators.push (seq1); - return iter_explorer::ChainedIters(sequenceOfIterators); - } - - template - inline iter_explorer::ChainedIters - iterChain(IT const& seq1, IT const& seq2, IT const& seq3, IT const& seq4) - { - typename iter_explorer::ChainedIters::IteratorIterator sequenceOfIterators; - - sequenceOfIterators.push (seq4); - sequenceOfIterators.push (seq3); - sequenceOfIterators.push (seq2); - sequenceOfIterators.push (seq1); - return iter_explorer::ChainedIters(sequenceOfIterators); - } - - template - inline iter_explorer::ChainedIters - iterChain(IT const& seq1, IT const& seq2, IT const& seq3, IT const& seq4, IT const& seq5) - { - typename iter_explorer::ChainedIters::IteratorIterator sequenceOfIterators; - - sequenceOfIterators.push (seq5); - sequenceOfIterators.push (seq4); - sequenceOfIterators.push (seq3); - sequenceOfIterators.push (seq2); - sequenceOfIterators.push (seq1); - return iter_explorer::ChainedIters(sequenceOfIterators); - } - - - - -} // namespace lib -#endif diff --git a/src/steam/engine/calc-plan-continuation.cpp b/src/steam/engine/calc-plan-continuation.cpp deleted file mode 100644 index c737058eb..000000000 --- a/src/steam/engine/calc-plan-continuation.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - CalcPlanContinuation - closure for planning a chunk of jobs - - Copyright (C) Lumiera.org - 2013, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -* *****************************************************/ - - -/** @file calc-plan-continuation.cpp - ** Implementation elements of render process planning. - ** @deprecated 4/2023 »Playback Vertical Slice« -- reworked into the RenderDrive /////////////////////////TICKET #1221 - */ - - -#include "steam/engine/calc-plan-continuation.hpp" -#include "steam/engine/frame-coord.hpp" -#include "steam/engine/job-ticket.hpp" -#include "lib/time/timevalue.hpp" -//#include "lib/frameid.hpp" -//#include "steam/state.hpp" - -#include - - - -namespace steam { -namespace engine { - - - /** entry point (interface JobClosure): invoke the concrete job operation. - * In this case, the job operation is responsible for planning a chunk of actual render jobs. - */ - void - CalcPlanContinuation::invokeJobOperation (JobParameter parameter) - { - ASSERT (parameter.nominalTime == timings_.getFrameStartAt (parameter.invoKey.frameNumber)); - - this->performJobPlanningChunk (parameter.invoKey.frameNumber); - } - - - void - CalcPlanContinuation::signalFailure (JobParameter parameter, JobFailureReason reason) - { - UNIMPLEMENTED ("what needs to be done when a planning continuation cant be invoked?"); - } - - - bool - CalcPlanContinuation::verify (Time nominalTime, InvocationInstanceID invoKey) const - { - return timings_.isValid() - && Time::MIN < nominalTime && nominalTime < Time::MAX - && nominalTime == timings_.getFrameStartAt (invoKey.frameNumber); - } - - - InvocationInstanceID - CalcPlanContinuation::buildInstanceID (HashVal seed) const - { - UNIMPLEMENTED ("systematically generate an invoKey, distinct for the nominal time"); - } - - size_t - CalcPlanContinuation::hashOfInstance (InvocationInstanceID invoKey) const - { - return boost::hash_value (invoKey.frameNumber); - } - - - - - - Job - CalcPlanContinuation::prepareRenderPlanningFrom (FrameCnt startFrame) - { - InvocationInstanceID invoKey; - invoKey.frameNumber = startFrame; - Time nominalPlanningStartTime = timings_.getFrameStartAt (startFrame); - - return Job(*this, invoKey, nominalPlanningStartTime); - } - - - void - CalcPlanContinuation::performJobPlanningChunk(FrameCnt nextStartFrame) - { -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////UNIMPLEMENTED :: TICKET #1301 - JobPlanningSequence jobs = dispatcher_.onCalcStream(modelPort_, channel_) - .establishNextJobs(refPoint); - - Job nextChunkOfPlanning = buildFollowUpJobFrom (refPoint); -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////UNIMPLEMENTED :: TICKET #1301 TimeAnchor refPoint(timings_, nextStartFrame); - - UNIMPLEMENTED ("the actual meat: access the scheduler and fed those jobs"); - } - - - Job - CalcPlanContinuation::buildFollowUpJobFrom (TimeAnchor const& refPoint) - { - return this->prepareRenderPlanningFrom( - refPoint.getNextAnchorPoint()); - } - - - - -}} // namespace engine diff --git a/src/steam/engine/calc-plan-continuation.hpp b/src/steam/engine/calc-plan-continuation.hpp deleted file mode 100644 index cff25c38e..000000000 --- a/src/steam/engine/calc-plan-continuation.hpp +++ /dev/null @@ -1,132 +0,0 @@ -/* - CALC-PLAN-CONTINUATION.hpp - closure for planning a chunk of jobs - - Copyright (C) Lumiera.org - 2013, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - - -/** @file calc-plan-continuation.hpp - ** A specialised render job to care for the planning of the calculation process itself. - ** Rendering is seen as an open-ended, ongoing process, and thus the management and planning - ** of the render process itself is performed chunk wise and embedded into the other rendering - ** calculations. The _"rendering-as-it-is-planned-right-now"_ can be represented as a closure - ** to the jobs, which perform and update this plan on the go. And in fact, the head of the - ** calculation process, the CalcStream, holds onto such a closure to access current planning. - ** - ** @deprecated 4/2023 »Playback Vertical Slice« -- reworked into the RenderDrive /////////////////////////TICKET #1221 - */ - - -#ifndef STEAM_ENGINE_CALC_PLAN_CONTINUATION_H -#define STEAM_ENGINE_CALC_PLAN_CONTINUATION_H - -#include "steam/common.hpp" -#include "steam/mobject/model-port.hpp" -#include "steam/engine/time-anchor.hpp" -#include "steam/engine/dispatcher.hpp" -#include "steam/play/timings.hpp" -#include "vault/engine/job.h" - - -namespace steam { -namespace engine { - -// using std::function; - using vault::engine::JobParameter; - using vault::engine::JobClosure; - using mobject::ModelPort; -// using lib::time::TimeSpan; -// using lib::time::FSecs; -// using lib::time::Time; - using lib::time::FrameCnt; - using lib::HashVal; - - - /** - * Special job to perform the job planning. - * This closure extends the existing planning of frame jobs to add a chunk - * of additional future jobs. Included with this chunk will be a recursive - * self re-invocation to trigger planning of the next chunk. Overall, this - * planning process is determined and controlled by the CalcStream owning - * this closure. - * - * @deprecated 4/2023 »Playback Vertical Slice« -- reworked into the RenderDrive //////////////////////////TICKET #1221 - */ - class CalcPlanContinuation - : public JobClosure - { - - play::Timings const& timings_; - Dispatcher& dispatcher_; - const ModelPort modelPort_; - const uint channel_; - - - /* === JobClosure Interface === */ - - JobKind - getJobKind() const override - { - return META_JOB; - } - - bool verify (Time, InvocationInstanceID) const override; - size_t hashOfInstance (InvocationInstanceID) const override; - InvocationInstanceID buildInstanceID (HashVal) const override; - void invokeJobOperation (JobParameter) override; - void signalFailure (JobParameter, JobFailureReason) override; - - - - - public: - /** - * @todo - */ - CalcPlanContinuation(play::Timings const& timings - ,Dispatcher& dispatcher - ,ModelPort modelPort - ,uint channel) - : timings_(timings) - , dispatcher_(dispatcher) - , modelPort_(modelPort) - , channel_(channel) - { } - - /** create the "start trigger job" - * Scheduling this job will effectively get a calculation stream - * into active processing, since it causes the first chunk of job planning - * plus the automated scheduling of follow-up planning jobs. The relation - * to real (wall clock) time will be established when the returned job - * is actually invoked - * @param startFrame where to begin rendering, relative to the nominal - * time grid implicitly given by the ModelPort to be pulled - */ - Job prepareRenderPlanningFrom (FrameCnt startFrame); - - - private: - void performJobPlanningChunk(FrameCnt nextStartFrame); - Job buildFollowUpJobFrom (TimeAnchor const& refPoint); - }; - - - -}} // namespace steam::engine -#endif diff --git a/src/steam/engine/dispatch-table.cpp b/src/steam/engine/dispatch-table.cpp index 874683309..814c515b9 100644 --- a/src/steam/engine/dispatch-table.cpp +++ b/src/steam/engine/dispatch-table.cpp @@ -24,33 +24,17 @@ /** @file dispatch-table.cpp ** Implementation details of render job generation. ** @todo draft from 2011, stalled, relevance not yet clear + ** @todo 6/2023 still unimplemented, as is the Fixture, which needs to back the Dispatcher */ #include "steam/engine/dispatch-table.hpp" -//#include "lib/frameid.hpp" -//#include "steam/state.hpp" namespace steam { namespace engine { -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - /** */ - FrameCoord - DispatchTable::locateRelative (FrameCoord const&, FrameCnt frameOffset) - { - UNIMPLEMENTED ("real implementation of the core dispatch operation"); - } - - bool - DispatchTable::isEndOfChunk (FrameCnt, ModelPort port) - { - UNIMPLEMENTED ("determine when to finish a planning chunk"); - } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - JobTicket& DispatchTable::getJobTicketFor (size_t, TimeValue nominalTime) { diff --git a/src/steam/engine/dispatch-table.hpp b/src/steam/engine/dispatch-table.hpp index 43f03a5f1..0de6b616a 100644 --- a/src/steam/engine/dispatch-table.hpp +++ b/src/steam/engine/dispatch-table.hpp @@ -23,7 +23,7 @@ /** @file dispatch-table.hpp ** Implementation details of render job generation. - ** @todo draft from 2011, stalled, relevance not yet clear + ** @todo 6/2023 »PlaybackVerticalSlice« : completing and integrating the core engine step by step */ @@ -45,7 +45,7 @@ namespace engine { // class ExitNode; /** - * @todo 11/11 extremely fuzzy at the moment + * @todo 6/2023 gradually building up the core engine components... */ class DispatchTable : public Dispatcher @@ -53,10 +53,6 @@ namespace engine { /* ==== Dispatcher interface ==== */ -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - FrameCoord locateRelative (FrameCoord const&, FrameCnt frameOffset) override; - bool isEndOfChunk (FrameCnt, ModelPort port) override; -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline size_t resolveModelPort (ModelPort) override; JobTicket& getJobTicketFor (size_t, TimeValue nominalTime) override; diff --git a/src/steam/engine/dispatcher.cpp b/src/steam/engine/dispatcher.cpp index 0920fd5ea..2ada8f6ca 100644 --- a/src/steam/engine/dispatcher.cpp +++ b/src/steam/engine/dispatcher.cpp @@ -23,13 +23,11 @@ /** @file dispatcher.cpp ** Implementation parts of job generation within an ongoing render process - ** @todo valid draft, unfortunately stalled in 2013 + ** @todo 2023 do we actually need a separate translation unit for this? */ #include "steam/engine/dispatcher.hpp" -//#include "lib/frameid.hpp" -//#include "steam/state.hpp" @@ -39,26 +37,6 @@ namespace engine { Dispatcher::~Dispatcher() { } // emit VTables and Typeinfo here.... -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete - /** @todo WIP */ - FrameCoord - Dispatcher::JobBuilder::relativeFrameLocation (TimeAnchor& refPoint, FrameCnt frameOffset) - { - FrameCoord frame; - frame.absoluteNominalTime = refPoint; - frame.absoluteFrameNumber = refPoint.getStartFrame(); - frame.realTimeDeadline = refPoint.establishDeadlineFor (frameOffset); -// frame.modelPort = this->modelPort_; ////////////////////////////TICKET #1301 : translation to model-port-ID now when Dispatcher-Pipeline is built -// frame.channelNr = this->channel_; - - ENSURE (frame.isDefined()); - return dispatcher_->locateRelative (frame, frameOffset); - } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - - diff --git a/src/steam/engine/dispatcher.hpp b/src/steam/engine/dispatcher.hpp index 58ccf3da4..10d333ab1 100644 --- a/src/steam/engine/dispatcher.hpp +++ b/src/steam/engine/dispatcher.hpp @@ -28,7 +28,6 @@ ** the Dispatcher is responsible for transforming the generic setup of such a calculation stream ** into a sequence of concrete jobs, anchored at some distinct point in time. ** - ** @todo valid draft, unfortunately stalled in 2013 ** @todo as of 4/2023 a complete rework of the Dispatcher is underway //////////////////////////////////////TICKET #1275 */ @@ -38,12 +37,11 @@ #include "steam/common.hpp" #include "steam/mobject/model-port.hpp" -#include "steam/engine/frame-coord.hpp" #include "steam/engine/job-ticket.hpp" #include "steam/engine/job-planning.hpp" #include "steam/play/timings.hpp" #include "steam/play/output-slot.hpp" -#include "lib/iter-tree-explorer.hpp" +#include "lib/iter-explorer.hpp" #include "lib/time/timevalue.hpp" #include "lib/nocopy.hpp" @@ -60,8 +58,9 @@ namespace engine { using mobject::ModelPort; using play::Timings; using play::DataSink; - using lib::time::FrameCnt; + using lib::time::TimeValue; using lib::time::TimeSpan; + using lib::time::FrameCnt; using lib::time::FSecs; using lib::time::Time; @@ -91,33 +90,13 @@ namespace engine { class Dispatcher : util::NonCopyable { -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsolete - struct JobBuilder - { - Dispatcher* dispatcher_; - ModelPort modelPort_; - uint channel_; - - FrameCoord relativeFrameLocation (TimeAnchor& refPoint, FrameCnt frameCountOffset =0); - - JobPlanningSequence - establishNextJobs (TimeAnchor& refPoint) - { - return JobPlanningSequence( - relativeFrameLocation(refPoint), - *dispatcher_); - } - }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsolete -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... + struct PipeFrameTick; template struct PipelineBuilder; - public: virtual ~Dispatcher(); ///< this is an interface @@ -157,36 +136,6 @@ namespace engine { */ virtual size_t resolveModelPort (ModelPort) =0; - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - /** core dispatcher operation: based on the coordinates of a reference point, - * establish binding frame number, nominal time and real (wall clock) deadline. - * @return new FrameCoord record (copy), with the nominal time, frame number - * and deadline adjusted in accordance to the given frame offset. - */ - virtual FrameCoord locateRelative (FrameCoord const&, FrameCnt frameOffset) =0; - - virtual bool isEndOfChunk (FrameCnt, ModelPort port) =0; - - ////////TODO: API-1 = just get next frame, without limitations .... CHECK - ////////TODO: API-2 = query limitation of planning chunk .... CHECK - ////////TODO: API-3 = establish next chunk .... still WIP - - ////////TODO: Question: why not embedding the time anchor directly within the location generator?? - //////// Answer: no this would lead to a huge blob called "the dispatcher" - - ////////TODO: immediate point to consider: the time anchor is responsible for the real timing calculations. But how to introduce the play strategy *here* ? - - ////////////TODO: the solution is simple: get rid of the additional job placed magically into the chunk - //////////// instead, provide a dedicated API function to create exactly that job - //////////// and *enclosed* into a specialised JobClosure subclass, embody the code for the follow-up - //////////// As a corollary: the scheduling deadline should be defined right *within* the job! - - ////////////TODO: remaining issues - //////////// - the TimeAnchor needs to be created directly from the JobParameter. No mutable state! - //////////// - but this leads to a lot of duplicated Timings records, unless we rewrite the TimeAnchor to be noncopyable and use a Timings const& -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - /** * Core Dispatcher operation: locate the appropriate Segment and * retrieve/derive a »blueprint« for render job generation. @@ -272,7 +221,7 @@ namespace engine { timeRange (Time start, Time after) { SRC::activate (start,after); - return buildPipeline (lib::treeExplore (move(*this))); + return buildPipeline (lib::explore (move(*this))); } // expected next to invoke pullFrom(port,sink) @@ -340,9 +289,9 @@ namespace engine { protected: /** @internal type rebinding helper to move the given tree-Explorer pipeline * and layer a new PipelineBuilder subclass on top. - * @note TreeExplorer itself is defined in a way to always strip away any existing - * top-level TreeExplorer, then add a new processing layer and finally - * place a new TreeExplorer layer on top. Taken together, this setup will + * @note IterExplorer itself is defined in a way to always strip away any existing + * top-level IterExplorer, then add a new processing layer and finally + * place a new IterExplorer layer on top. Taken together, this setup will * *slice away* the actual PipelineBuilder layer, move the resulting pipeline * into the next building step and finally produce a cleanly linked processing * pipeline without any interspersed builders. Yet still, partially constructed diff --git a/src/steam/engine/frame-coord.hpp b/src/steam/engine/frame-coord.hpp deleted file mode 100644 index 26611fe45..000000000 --- a/src/steam/engine/frame-coord.hpp +++ /dev/null @@ -1,133 +0,0 @@ -/* - FRAME-COORD.hpp - unique distinct coordinates of a frame to be calculated - - Copyright (C) Lumiera.org - 2012, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - - -/** @file frame-coord.hpp - ** Tuple data type to address distinct frames within the render engine calculations. - */ - - -#ifndef STEAM_ENGINE_FRAME_COORD_H -#define STEAM_ENGINE_FRAME_COORD_H - -#include "steam/common.hpp" -#include "steam/mobject/model-port.hpp" -#include "lib/time/timevalue.hpp" - -#include - - -namespace steam { -namespace engine { - - using mobject::ModelPort; - using lib::time::FrameCnt; - using lib::time::TimeValue; - using lib::time::TimeVar; - using lib::time::Time; - - - /** - * effective coordinates of a frame to be calculated. - * Frame coordinates are produced as result of the Dispatcher call, - * thus forming the foundation of an actual ProcNode invocation - * A frame render job can be characterised by - * - the nominal (timeline) time of the frame - * - the corresponding frame-number - * - a real wall-clock time deadline for delivery - * - the actual node to pull data from, defined indirectly through - * ModelPort and channel number (as used within the Segmentation) - * - * @remarks consider frame coordinates being "boiled down" to the actual values. - * There is no reference to any kind of time grid (or similar session internals). - * - * @todo 1/12 WIP-WIP-WIP defining the invocation sequence and render jobs - * @todo 4/23 WIP-WIP-WIP recast the dispatch- and job invocation sequence - */ - struct FrameCoord - { - - TimeVar absoluteNominalTime; - FrameCnt absoluteFrameNumber; - - size_t modelPortIDX; - - - /** build an \em undefined frame location */ - FrameCoord() - : absoluteNominalTime{Time::NEVER} - , absoluteFrameNumber{std::numeric_limits::max()} - , modelPortIDX{0} - { } - - explicit - FrameCoord (TimeValue nominalTime, size_t portIDX =0) - : absoluteNominalTime{nominalTime} - , absoluteFrameNumber{std::numeric_limits::max()} - , modelPortIDX{portIDX} - { } - - FrameCoord (TimeValue nominalTime, FrameCnt frameNr, size_t portIDX =0) - : absoluteNominalTime{nominalTime} - , absoluteFrameNumber{frameNr} - , modelPortIDX{portIDX} - { } - - // using default copy operations - - }; - - - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - /** - * Facility for producing a sequence of FrameCoord. - * This interface describes the essence of generating - * a series of frame locations, which is necessary for - * planning render jobs. To implement it, actually some - * kind of [frame grid](\ref lib::time::Quantiser) is - * necessary -- in practice we use a Dispatcher, which is - * backed by the Segmentation (i.e. the render nodes network). - * @deprecated 6/2023 obsolete and replaced by the pipeline builder API in Dispatcher - */ - class FrameSequencer - : util::NonCopyable - { - - public: - virtual ~FrameSequencer(); ///< this is an interface - - FrameCoord - getNextFrame (FrameCoord refPoint) - { - return locateRelative (refPoint, +1 ); - } - - protected: - virtual FrameCoord locateRelative (FrameCoord const&, FrameCnt frameOffset) =0; - }; -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - - - -}} // namespace steam::engine -#endif diff --git a/src/steam/engine/job-planning.hpp b/src/steam/engine/job-planning.hpp index 9819857ad..66fb823ca 100644 --- a/src/steam/engine/job-planning.hpp +++ b/src/steam/engine/job-planning.hpp @@ -70,7 +70,6 @@ #include "steam/common.hpp" #include "vault/engine/job.h" #include "steam/engine/job-ticket.hpp" -#include "steam/engine/frame-coord.hpp" #include "steam/play/output-slot.hpp" #include "steam/play/timings.hpp" #include "lib/time/timevalue.hpp" @@ -87,6 +86,7 @@ namespace engine { using play::DataSink; using play::Timings; using lib::time::Time; + using lib::time::TimeVar; using lib::time::Duration; @@ -212,7 +212,6 @@ namespace engine { { } - Time doCalcDeadline(Timings const& timings) { @@ -226,325 +225,9 @@ namespace engine { - jobTicket_.getExpectedRuntime() - timings.engineLatency; } - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete - /** build a new JobPlanning object, - * set to explore the prerequisites - * at the given planning situation - */ - JobPlanning - discoverPrerequisites() const - { - if (isnil (plannedOperations_)) - return JobPlanning(); - else - return JobPlanning (plannedOperations_->discoverPrerequisites (0) //////////////////////////////TICKET #1301 : was: point_to_calculate_.channelNr) - ,this->point_to_calculate_); - } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete - - /** integrate another chain of prerequisites into the current evaluation line. - * Further evaluation will start to visit prerequisites from the new starting point, - * and return to the current evaluation chain later on exhaustion of the side chain. - * Especially in case the current evaluation is empty or already exhausted, the - * new starting point effectively replaces the current evaluation point */ - friend void - integrate (JobPlanning const& newStartingPoint, JobPlanning& existingPlan) - { - if (isnil (existingPlan.plannedOperations_)) - { // current evaluation is exhausted: switch to new starting point - existingPlan.point_to_calculate_ = newStartingPoint.point_to_calculate_; - } - existingPlan.plannedOperations_.push (newStartingPoint.plannedOperations_); - existingPlan.plannedOperations_.markTreeLocation(); - } - - - /* === Iteration control API for IterStateWrapper== */ - - bool - checkPoint () const - { - return not isnil (plannedOperations_); - } - - JobPlanning& - yield () const - { - REQUIRE (checkPoint()); - return unConst(*this); - } - - void - iterNext () - { - plannedOperations_.pullNext(); - plannedOperations_.markTreeLocation(); - } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... }; - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - /** - * iterator, exposing a sequence of JobPlanning elements - */ - class PlanningState - : public lib::IterStateWrapper - { - typedef lib::IterStateWrapper _Iter; - - - public: - /** inactive evaluation */ - PlanningState() - : _Iter() - { } - - explicit - PlanningState (JobPlanning const& startingPoint) - : _Iter(startingPoint) // note: invoking copy ctor on state core - { } - - // using the standard copy operations - - - - - /* === API for JobPlanningSequence to expand the tree of prerequisites === */ - - /** attach and integrate the given planning details into this planning state. - * Actually the evaluation proceeds depth-first with the other state, - * returning to the current position later for further evaluation */ - PlanningState & - wrapping (JobPlanning const& startingPoint) - { - integrate (startingPoint, this->stateCore()); - return *this; - } - - PlanningState & - usingSequence (PlanningState const& prerequisites) - { - if (isnil (prerequisites)) - return *this; - else - return this->wrapping(*prerequisites); - // explanation: PlanningState represents a sequence of successive planning points. - // actually this is implemented by switching an embedded JobPlanning element - // through a sequence of states. Thus the initial state of an investigation - // (which is a JobPlanning) can stand-in for the sequence of prerequisites - } - - - - /** Extension point to be picked up by ADL. - * Provides access for the JobPlanningSequence - * for combining and expanding partial results. - */ - friend PlanningState& - build (PlanningState& attachmentPoint) - { - return attachmentPoint; - } - }; - - - - - /** this is the core operation to drive planning ahead: - * discover the prerequisites of some operation -- here - * "prerequisites" are those operations to be performed - * within separate Jobs beforehand. - * @note this function is intended to be flat-mapped (">>=") - * onto a tree-like monad representing the evaluation process. - */ - inline PlanningState - expandPrerequisites (JobPlanning const& calculationStep) - { - PlanningState newSubEvaluation( - calculationStep.discoverPrerequisites()); - return newSubEvaluation; - } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - - - - - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - /** - * Abstraction: a Facility to establish frame coordinates - * and identify and access the execution plan for this frame. - * @see Dispatcher the service interface actually used - */ - class FrameLocator - : public FrameSequencer - { - public: - - JobTicket& - getJobTicketFor (FrameCoord const& location) - { - return accessJobTicket (location.modelPortIDX, location.absoluteNominalTime); - } - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete - bool canContinue (FrameCoord const& location) - { -// return not isEndOfChunk (location.absoluteFrameNumber, -// location.modelPort); - } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : likely to become obsolete - - protected: - virtual JobTicket& accessJobTicket (size_t, TimeValue nominalTime) =0; - virtual bool isEndOfChunk (FrameCnt, ModelPort port) =0; - }; -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1301 : obsoleted by rework of Dispatcher-Pipeline - - - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - /** - * Generate a sequence of starting points for Job planning, - * based on the underlying frame grid. This sequence will be - * used to seed a JobPlanningSequence for generating a chunk - * of frame render jobs within a given CalcStream in the player. - * Evaluation of that seed will then expand each starting point, - * until all prerequisites for those frames are discovered, - * resulting in a sequence of Jobs ready to be handed over - * to the scheduler for time-bound activation. - */ - class PlanningStepGenerator - { - FrameLocator* locationGenerator_; - FrameCoord currentLocation_; - - //////////////////////////////////////////TODO duplicated storage of a FrameCoord record - //////////////////////////////////////////TODO nextEvaluation_ is only needed to initialise the "current" sequence - //////////////////////////////////////////TODO within the RecursiveSelfIntegration strategy. Maybe this storage could be collapsed? - JobPlanning nextEvaluation_; - - void - use_current_location_as_starting_point_for_planning() - { - JobTicket& processingPlan = locationGenerator_->getJobTicketFor (currentLocation_); - - nextEvaluation_ = JobPlanning(processingPlan.startExploration() - ,currentLocation_); - } - - - - public: - typedef JobPlanning value_type; - typedef JobPlanning& reference; - typedef JobPlanning * pointer; - - - PlanningStepGenerator(FrameCoord startPoint, FrameLocator& locator) - : locationGenerator_(&locator) - , currentLocation_(startPoint) - { - REQUIRE (startPoint.isDefined()); - use_current_location_as_starting_point_for_planning(); - } - - // default copyable - - - /* === Iteration control API for IterStateWrapper== */ - - bool - checkPoint () const - { - return currentLocation_.isDefined(); - } // might indicate end of this planning chunk (or of playback altogether) - - - JobPlanning& - yield () const - { - ENSURE (checkPoint()); - return unConst(this)->nextEvaluation_; - } - - - void - iterNext () - { - if (locationGenerator_->canContinue (currentLocation_)) - { - currentLocation_ = locationGenerator_->getNextFrame (currentLocation_); - this->use_current_location_as_starting_point_for_planning(); - ENSURE (this->checkPoint()); - } - else - { // indicate end-of playback or a jump to another playback position - currentLocation_ = FrameCoord(); - } - } - }; - - - - /* type definitions for building the JobPlaningSequence */ - - typedef PlanningState (*SIG_expandPrerequisites) (JobPlanning const&); - - typedef lib::IterExplorer JobPlanningChunkStartPoint; - typedef JobPlanningChunkStartPoint::FlatMapped::Type ExpandedPlanningSequence; - - - - /** - * This iterator represents a pipeline to pull planned jobs from. - * To dispatch individual frame jobs for rendering, this pipeline is generated - * and wired internally such as to interpret the render node definitions. - * - * \par Explanation of the structure - * - * The JobPlanningSequence is constructed from several nested layers of functionality - * - for the client, it is an iterator, exposing a sequence of JobPlanning elements - * - a JobPlanning element allows to add a frame render job to the scheduler - * - actually such an element can even be \em converted directly into a Job (descriptor) - * - the sequence of such JobPlanning elements (that is, the iterator) is called a PlanningState, - * since evaluating this iterator effectively drives the process of job planning ahead - * - this planning process is \em implemented as a recursive evaluation and exploration of - * a tree of prerequisites; these prerequisites are defined in the JobTicket datastructure - * - there is an underlying grid of evaluation starting points, each corresponding to a - * single frame. Typically, each frame generates at least two jobs, one for fetching - * data, and one for the actual calculations. Depending on the actual render network, - * a lot of additional jobs might be necessary - * - this basic frame grid is generated by the PlanningStepGenerator, which is - * effectively backed by the Dispatcher and thus the render node model. - * - * @remarks JobPlanningSequence is a monad, and the operation to explore the prerequisites - * is applied by the \c >>= (monad flat map operation). This approach allows us - * to separate the technicalities of exhausting tree exploration from the actual - * "business code" to deal with frame job dependencies - */ - class JobPlanningSequence - : public ExpandedPlanningSequence - { - - public: - JobPlanningSequence(engine::FrameCoord startPoint, FrameLocator& locator) - : ExpandedPlanningSequence( - JobPlanningChunkStartPoint( - PlanningStepGenerator(startPoint,locator)) - - >>= expandPrerequisites) // "flat map" (monad operation) - { } - }; -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - - - -}} // namespace steam::engine -#endif +}}// namespace steam::engine +#endif /*STEAM_ENGINE_JOB_PLANNING_H*/ diff --git a/src/steam/engine/job-ticket.hpp b/src/steam/engine/job-ticket.hpp index 82f56eafb..cd423b5e2 100644 --- a/src/steam/engine/job-ticket.hpp +++ b/src/steam/engine/job-ticket.hpp @@ -37,7 +37,6 @@ #include "steam/common.hpp" #include "vault/engine/job.h" -#include "steam/engine/frame-coord.hpp" #include "steam/engine/exit-node.hpp" #include "lib/time/timevalue.hpp" #include "lib/linked-elements.hpp" @@ -56,8 +55,9 @@ namespace engine { using vault::engine::Job; using vault::engine::JobFunctor; using vault::engine::JobClosure; /////////////////////////////////////////////////////////////////////TICKET #1287 : fix actual interface down to JobFunctor (after removing C structs) -using lib::time::Duration; using lib::LinkedElements; +using lib::time::Duration; +using lib::time::Time; using util::isnil; using lib::HashVal; using lib::LUID; @@ -82,11 +82,12 @@ using lib::LUID; * on invocation another, specific function (= the job). * * @note JobTicket is effectively immutable after construction - * @todo 4/23 WIP-WIP-WIP defining the invocation sequence and render jobs + * @todo 6/23 WIP rework and integration for »PlaybackVerticalSlice« */ class JobTicket : util::NonCopyable { + /** @internal management of prerequisites */ struct Prerequisite { Prerequisite* next{nullptr}; // for intrusive list @@ -100,7 +101,7 @@ using lib::LUID; using Prerequisites = LinkedElements; - /** what handling this task entails */ + /** @internal what handling this task entails */ struct Provision { JobFunctor& jobFunctor; @@ -124,6 +125,8 @@ using lib::LUID; template static Provision buildProvisionSpec (ExitNode const&, ALO&); + + public: template JobTicket (ExitNode const& exitNode, ALO& allocator) @@ -133,31 +136,6 @@ using lib::LUID; static JobTicket NOP; -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete - class ExplorationState; - friend class ExplorationState; - ExplorationState startExploration() const; ////////////////////////////TICKET #1276 : likely to become obsolete - ExplorationState discoverPrerequisites (uint channelNr =0) const; ////////////////////////////TICKET #1276 : likely to become obsolete -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - - Job createJobFor (Time nominalTime); - - Duration getExpectedRuntime(); - - auto - getPrerequisites () - { - return lib::transformIterator (this->empty()? Prerequisites::iterator() - : provision_.prerequisites.begin() - ,[](Prerequisite& prq) -> JobTicket& - { - return prq.prereqTicket; - }); - } - - bool empty() const { @@ -176,6 +154,34 @@ using lib::LUID; ,[](auto& pq){ return pq.prereqTicket.isValid(); }); } + + /** + * Core operation: iterate over the prerequisites, + * required to carry out a render operation based on this blueprint + * @return iterator exposing the prerequisites as JobTicket& + */ + auto + getPrerequisites () + { + return lib::transformIterator (this->empty()? Prerequisites::iterator() + : provision_.prerequisites.begin() + ,[](Prerequisite& prq) -> JobTicket& + { + return prq.prereqTicket; + }); + } + + /** + * Core operation: build a concrete render job based on this blueprint + */ + Job createJobFor (Time nominalTime); + + /** + * Core operation: guess expected runtime for rendering + */ + Duration getExpectedRuntime(); + + protected: static InvocationInstanceID timeHash (Time, InvocationInstanceID const&); bool verifyInstance (JobFunctor&, InvocationInstanceID const&, Time) const; @@ -183,104 +189,6 @@ using lib::LUID; }; -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete - class JobTicket::ExplorationState - { - typedef Prerequisites::iterator SubTicketSeq; - typedef std::stack SubTicketStack; //////////////////////////TODO use a custom container to avoid heap allocations - - SubTicketStack toExplore_; - OrientationIndicator orientation_; - - public: - ExplorationState() { } - - ExplorationState (Prerequisites& prerequisites) - { - if (not isnil (prerequisites)) - toExplore_.push (prerequisites.begin()); - } - - // using default copy operations - - - bool - empty() const - { - return toExplore_.empty(); - } - - - void - markTreeLocation() - { - orientation_.markRefLevel (toExplore_.size()); - orientation_.resetToRef(); - ENSURE (0 == orientation_); - } - - - void - pullNext() - { - if (empty()) - throw lumiera::error::Logic ("Exploration of Job prerequisites floundered. " - "Attempt to iterate beyond the end of prerequisite list" - ,lumiera::error::LUMIERA_ERROR_ITER_EXHAUST); - ASSERT (toExplore_.top().isValid()); - - ++(toExplore_.top()); - while ( !toExplore_.empty() - && toExplore_.top().empty()) - toExplore_.pop(); - - ENSURE (empty() || toExplore_.top().isValid()); - } - - - void - push (ExplorationState subExploration) // note: passing deliberately by value - { - if (subExploration.empty()) return; - - pushAllPrerequisites (subExploration.toExplore_); - } - - - JobTicket const * - operator->() const - { - REQUIRE (!empty() && toExplore_.top().isValid()); - REQUIRE (toExplore_.top()->prereqTicket.isValid()); - - return & toExplore_.top()->prereqTicket; - } - - - private: - void - pushAllPrerequisites (SubTicketStack& furtherPrerequisites) - { - REQUIRE (!isnil (furtherPrerequisites)); - - if (1 == furtherPrerequisites.size()) - { - this->toExplore_.push (furtherPrerequisites.top()); - } - else - { // pathological case: several levels of prerequisites - // --> push recursively to retain level ordering - SubTicketSeq deepestLevel (furtherPrerequisites.top()); - furtherPrerequisites.pop(); - pushAllPrerequisites (furtherPrerequisites); - this->toExplore_.push (deepestLevel); - } - } - }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - @@ -314,33 +222,6 @@ using lib::LUID; } - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete - /// @deprecated : could be expendable ... likely incurred solely by the use of Monads as design pattern - inline JobTicket::ExplorationState - JobTicket::startExploration() const - { - UNIMPLEMENTED ("somehow build a self-referential pseudo-prerequisite, and seed an ExplorationState with that"); - /////////////////////TODO problem is: we need a JobTicket::Prerequisite instance, where the descriptor points to "self" (this JobTicket) - /////////////////////TODO : but this instance needs to reside somewhere at a safe location, since we want to embed a LinkedElements-iterator - /////////////////////TODO : into the ExplorationState. And obviously we do not want that instance in each JobTicket, only in the top level ones - - /////////////////////TODO : on second thought -- better have a top-level entry point to the evaluation of a frame - /////////////////////TODO basically this inherits from Prerequisite and lives somehow in the dispatcher-table or segment - } - - - inline JobTicket::ExplorationState - JobTicket::discoverPrerequisites (uint channelNr) const - { - return empty()? ExplorationState() - : ExplorationState (util::unConst(provision_).prerequisites); - } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 : likely to become obsolete -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1276 :: to be refactored... - - -}} // namespace steam::engine -#endif +}}// namespace steam::engine +#endif /*STEAM_ENGINE_JOB_TICKET_H*/ diff --git a/src/steam/engine/render-drive.cpp b/src/steam/engine/render-drive.cpp index b314194c3..3ac4fc9cb 100644 --- a/src/steam/engine/render-drive.cpp +++ b/src/steam/engine/render-drive.cpp @@ -28,11 +28,8 @@ #include "steam/engine/render-drive.hpp" -#include "steam/engine/frame-coord.hpp" #include "steam/engine/job-ticket.hpp" #include "lib/time/timevalue.hpp" -//#include "lib/frameid.hpp" -//#include "steam/state.hpp" #include @@ -102,16 +99,14 @@ namespace engine { void RenderDrive::performJobPlanningChunk(FrameCnt nextStartFrame) { - TimeAnchor refPoint(getTimings(), nextStartFrame); UNIMPLEMENTED ("the actual meat: advance the render process"); } Job - RenderDrive::buildFollowUpJobFrom (TimeAnchor const& refPoint) + RenderDrive::buildFollowUpJobFrom (Time refPoint) { - return this->prepareRenderPlanningFrom( - refPoint.getNextAnchorPoint()); + UNIMPLEMENTED ("create a follow-up job to pick up job-planning at or after the refPoint"); } diff --git a/src/steam/engine/render-drive.hpp b/src/steam/engine/render-drive.hpp index 7c54198b2..8101780a6 100644 --- a/src/steam/engine/render-drive.hpp +++ b/src/steam/engine/render-drive.hpp @@ -41,7 +41,6 @@ #include "steam/common.hpp" #include "steam/mobject/model-port.hpp" -#include "steam/engine/time-anchor.hpp" #include "steam/engine/dispatcher.hpp" #include "steam/play/timings.hpp" #include "vault/engine/job.h" @@ -147,7 +146,7 @@ namespace engine { private: void performJobPlanningChunk(FrameCnt nextStartFrame); - Job buildFollowUpJobFrom (TimeAnchor const& refPoint); + Job buildFollowUpJobFrom (Time refPoint); }; diff --git a/src/steam/engine/time-anchor.hpp b/src/steam/engine/time-anchor.hpp deleted file mode 100644 index d89e0e566..000000000 --- a/src/steam/engine/time-anchor.hpp +++ /dev/null @@ -1,196 +0,0 @@ -/* - TIME-ANCHOR.hpp - current render evaluation time point closure - - Copyright (C) Lumiera.org - 2012, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - - -/** @file time-anchor.hpp - ** Representation of a _continuation point_ for planning the render process. - ** In the Lumiera engine, render and playback processes are modelled as infinite streams, - ** which are evaluated chunk wise. The TimeAnchor is used to mark a point, where the - ** _planning_ of further render jobs will be picked up and continued later on - ** - ** @todo this is part of an implementation draft from 2013, - ** to create a complete outline of player and render job generation. - ** @todo as of 2016 this effort is stalled, but remains valid - ** - ** @deprecated 5/23 during rework of Playback / Rendering the concept TimeAnchor lost its substance - */ - - -#ifndef STEAM_ENGINE_TIME_ANCHOR_H -#define STEAM_ENGINE_TIME_ANCHOR_H - -#include "steam/common.hpp" -#include "vault/real-clock.hpp" -#include "lib/time/timevalue.hpp" -#include "steam/play/timings.hpp" -#include "steam/engine/frame-coord.hpp" - - - -namespace steam { -namespace engine { - - using vault::RealClock; - using lib::time::Offset; - using lib::time::Duration; - using lib::time::FrameCnt; - using lib::time::TimeVar; - using lib::time::Time; - - - /** - * The process of playback or rendering is a continued series of exploration and evaluation. - * The outline of what needs to be calculated is determined continuously, proceeding in - * chunks of evaluation. Each of these continued partial evaluations establishes a distinct - * anchor or breaking point in time: everything before this point can be considered settled - * and planned thus far. Effectively, this time point acts as a evaluation closure, - * to be picked up for the next partial evaluation. Each time anchor defines a span of the - * timeline, which will be covered with the next round of job planning; the successive next - * TimeAnchor will be located at the first frame \em after this time span, resulting in - * seamless coverage of the whole timeline. Whenever a TimeAnchor is created, a relation - * between nominal time, current engine latency and wall clock time is established, This way, - * the TimeAnchor closure is the definitive binding between the abstract logical time of the - * session timeline, and the real wall-clock time forming the deadline for rendering. - * - * # internals - * The time anchor associates a nominal time, defined on the implicit time grid - * of some given Timings, with an actual wall clock time. Due to the usage situation, - * the TimeAnchor takes on the secondary meaning of a breaking point; everything \em before - * this anchor point has been handled during the preceding invocations of an ongoing chunk wise - * partial evaluation of the timeline to be "performed" within this play process. - * - the #timings_ serve as an abstracted grid (actually, the implementation - * does refer to a grid defined somewhere within the session) - * - the actual #anchorPoint_ is defined as frame number relative to this grid - * - this anchor point is scheduled to happen at a #relatedRealTime_, based on - * system's real time clock scale (typically milliseconds since 1970). - * This schedule contains a compensation for engine and output latency. - * - * @remarks please note that time anchors are set per CalcStream. - * Since different streams might use different frame grids, the rhythm - * of these planning operations is likely to be specific for a given stream. - * The relation to real time is established anew at each time anchor, so any - * adjustments to the engine latency will be reflected in the planned job's - * deadlines. Actually, the embedded Timings record is responsible for this - * timing calculation and for fetching the current EngineConfig. - * - * @deprecated 5/23 during rework of Playback / Rendering it turns out that TimeAnchor - * is completely redundant and void of substantial meaning - * - * @see Dispatcher - * @see DispatcherInterface_test - * @see Timings - */ - class TimeAnchor - { - play::Timings timings_; - FrameCnt anchorPoint_; - Time relatedRealTime_; - - static Time - expectedTimeofArival (play::Timings const& timings, FrameCnt startFrame, Offset startDelay) - { - TimeVar deadline = timings.isTimebound()? timings.getTimeDue(startFrame) - : RealClock::now(); - return deadline + startDelay; - } - - - public: - TimeAnchor (play::Timings timings, FrameCnt startFrame, Offset startDelay =Offset::ZERO) - : timings_(timings) - , anchorPoint_(startFrame) - , relatedRealTime_(expectedTimeofArival(timings,startFrame,startDelay)) - { } - - // using default copy operations - - - - /** set a follow-up TimeAnchor point. - * After planning a chunk of jobs, the dispatcher uses - * this function to set up a new breaking point (TimeAnchor) - * and places a continuation job to resume the planning activity. - * @note precisely satisfies the planning chunk duration - * @return a frame number suitable to build the next TimeAnchor - * based on the current play::Timings. This new start point - * will be anchored at the grid point following the end of - * the previous planning chunk, resulting in a seamless - * coverage of the timeline - */ - FrameCnt - getNextAnchorPoint() const - { - return timings_.establishNextPlanningChunkStart (this->anchorPoint_); - } - - - /** @internal for debugging and diagnostics: - * explicitly cast this TimeAnchor onto the underlying - * nominal time scale (as defined by the Timings of this - * playback or render process). */ - operator lib::time::TimeValue() const - { - return timings_.getFrameStartAt (anchorPoint_); - } - - - /** @return the frame at which any job planning - * for this planning chunk will start */ - FrameCnt getStartFrame() const - { - return anchorPoint_; - } - - - /** define the deadline for a grid point relative to this reference point. - * Since a TimeAnchor represents the definitive link between nominal time - * and ongoing wall clock time, and since all of the current output stream - * related timing information is available -- including the engine and the - * output latency -- this is the place to do the final decision. - * @param frameOffset frame count offset relative to this TimeAnchor point - * @return the latest real absolute wall clock time at which this frame - * has to be delivered to the OutputSlot. This deadline is exclusive, - * i.e. time < deadline is required. - */ - Time - establishDeadlineFor (FrameCnt frameOffset) - { - return this->relatedRealTime_ - + timings_.getRealOffset(frameOffset); - } - - - /** convenience shortcut, employing the deadline calculation in relation - * to current wall clock time */ - Offset - remainingRealTimeFor (FrameCoord plannedFrame) - { - FrameCnt frameOffset = plannedFrame.absoluteFrameNumber - anchorPoint_; - return Offset(RealClock::now() - ,establishDeadlineFor(frameOffset)); - } - }; - - - -}} // namespace steam::engine -#endif diff --git a/tests/15library.tests b/tests/15library.tests index 22408d9d7..99a6d9541 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -419,11 +419,6 @@ return: 0 END -TEST "Hierarchy rebuilding" HierarchyOrientationIndicator_test < - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -* *****************************************************/ - -/** @file hierarchy-orientation-indicator-test.cpp - ** unit test \ref HierarchyOrientationIndicator_test - */ - - -#include "lib/test/run.hpp" - -#include "lib/hierarchy-orientation-indicator.hpp" -#include "lib/iter-adapter-stl.hpp" -#include "lib/iter-explorer.hpp" -#include "lib/itertools.hpp" -#include "lib/util.hpp" - -#include -#include -#include -#include -#include - - -namespace lib { -namespace test { - - namespace { // test fixture: a random Tree to navigate... - - using std::rand; - using std::function; - using lib::transformIterator; - using lib::iter_stl::eachAddress; - using util::contains; - using util::max; - - /* -- size of the test tree ---- */ - const uint MAX_CHILDREN_CNT(5); // children per Node (5 means 0 to 4 children) - const double CHILD_PROBABILITY(0.45); // probability for a Node to have any children - const uint TEST_SEQUENCE_LENGTH(50); // test uses a sequence of Node trees - // 5 - 45% - 50 produce roughly 1000 Nodes and tree depths of about 12 - uint nextChildID(1); - - - /** - * pick a random child count below #MAX_CHILDREN_CNT - * with a probability to get any count above zero - * as defined by CHILD_PROBABILITY - */ - inline uint - pick_random_count() - { - uint bottom((1.0/CHILD_PROBABILITY - 1) * MAX_CHILDREN_CNT); - uint limit = bottom + MAX_CHILDREN_CNT; - ASSERT (0 < bottom); - ASSERT (bottom < limit); - - int cnt = (rand() % limit) - bottom; - return max(0, cnt); - } - - /** (sub)tree of test data */ - struct Node - : boost::equality_comparable - { - typedef std::vector Children; - - int id_; - Children children_; - - - Node(int i) ///< build node explicitly without children - : id_(i) - { } - - - Node() ///< build a random test subtree - : id_(nextChildID++) - { - uint c = pick_random_count(); - for (uint j=0; j NodeSeq; - - - /** - * Function to generate a depth-first tree visitation - */ - NodeSeq - exploreChildren (Node* node) - { - NodeSeq children_to_visit; - build(children_to_visit).usingSequence (eachAddress (node->children_)); - return children_to_visit; - } - - - - struct VisitationData - { - int id; - int orientation; - - VisitationData(int refID, - int direction =0) - : id(refID) - , orientation(direction) - { } - }; - - /** - * This functor visits the nodes to produce the actual test data. - * The intention is to describe a visitation path through a tree structure - * by a sequence of "up", "down", and "level" orientations. The test we're - * preparing here will attempt to re-create a given tree based on these - * directional information. The actual visitation path is created by - * a depth-first exploration of the source tree. - */ - class NodeVisitor - { - typedef std::deque NodePath; - typedef NodePath::reverse_iterator PathIter; - - NodePath path_; - - public: - // using default ctor and copy operations - - static function - create () { return NodeVisitor(); } - - - VisitationData - operator() (Node* node) - { - int direction = establishRelation (node); - return VisitationData(node->id_, direction); - } - - private: - /** Helper for this test only: find out about the hierarchical relation. - * In the real usage situation, the key point is that we \em record - * this relation on-the-fly, when visiting the tree, instead of - * determining it after the fact. */ - int - establishRelation (Node* nextNode) - { - REQUIRE (nextNode); - uint level = path_.size(); - uint refLevel = level; - for (PathIter p = path_.rbegin(); - 0 < level ; --level, ++p ) - { - Node* parent = *p; - if (parent->hasChild (*nextNode)) - { - // visitation continues with children below this level - path_.resize(level); - path_.push_back(nextNode); - return (level - refLevel) + 1; - } - } - ASSERT (0 == level); - - // nextNode not found as child (i.e. fork) within current tree path - // --> start new tree path at root - path_.clear(); - path_.push_back(nextNode); - return (0 - refLevel) + 1; - } // by convention, root is an implicitly pre-existing context at level 0 - }; - - - /** - * the core of this test: rebuilding a tree - * based on visitation data, including the \em orientation - * of the visitation path (up, down, siblings). After construction, - * the embedded #children_ will reflect the original sequence as - * described by the given treeTraversal. - * @remarks this is a blueprint for the scheduler interface, - * which accepts a sequence of jobs with dependencies. - */ - struct TreeRebuilder - : Node - { - template - TreeRebuilder (IT treeTraversal) - : Node(0) - { - populate (transformIterator (treeTraversal, - NodeVisitor::create())); - } - - private: - template - void - populate (IT treeVisitation) - { - struct Builder - { - Builder (Node& startPoint) - : parent_(NULL) - , current_(&startPoint) - { } - - void - populateBy (IT& treeVisitation) - { - while (treeVisitation) - { - int direction = treeVisitation->orientation; - if (direction < 0) - { - treeVisitation->orientation += 1; - return; - } - else - if (direction > 0) - { - treeVisitation->orientation -= 1; - Node* refPoint = startChildTransaction(); - populateBy (treeVisitation); - commitChildTransaction(refPoint); - } - else - { - addNode (treeVisitation->id); - ++treeVisitation; - }}} - - private: - Node* parent_; - Node* current_; - - void - addNode (int id) - { - current_ = & parent_->makeChild(id); - } - - Node* - startChildTransaction() - { - Node* oldRefPoint = parent_; - ASSERT (current_); - parent_ = current_; // set new ref point - return oldRefPoint; - } - - void - commitChildTransaction(Node* refPoint) - { - parent_ = refPoint; - current_ = parent_; - } - }; - - - Builder builder(*this); // pre-existing implicit root context - builder.populateBy (treeVisitation); - } - }; - - - } //(End) test fixture - - - - - - /***********************************************************************//** - * @test describing and rebuilding a tree structure - * while visiting the tree in depth first order. - * To keep track of the level changes during that navigation, - * we use an indicator to represent the relative level difference - * compared to the previously visited node in the tree. - * - * @see HierarchyOrientationIndicator - * @see DispatcherInterface_test - */ - class HierarchyOrientationIndicator_test : public Test - { - - virtual void - run (Arg) - { - demonstrate_tree_rebuilding(); - verify_OrientationIndicator(); - } - - /** @test demonstrate how a Node tree structure can be rebuilt - * just based on the visitation sequence of an original tree. - * This visitation captures the local data of the Node (here the ID) - * and the orientation of the visitation path (down, next sibling, up) - * - * This is a demonstration and blueprint for constructing the scheduler interface. - * The Scheduler accepts a series of new jobs, but jobs may depend on each other, - * and the jobs are created while exploring the dependencies in the render engine's - * node graph (low-level-model). - */ - void - demonstrate_tree_rebuilding () - { - Node::Children testWood; - for (uint i=0; i < TEST_SEQUENCE_LENGTH; ++i) - testWood.push_back(Node()); - - TreeRebuilder reconstructed (depthFirst (eachAddress(testWood)) >>= exploreChildren); - - CHECK (reconstructed.children_ == testWood); - } - - - void - verify_OrientationIndicator () - { - OrientationIndicator orient; - - CHECK (0 == orient); - ++++orient; - CHECK (+2 == orient); - orient.markRefLevel(); - CHECK ( 0 == orient); - --orient; - CHECK (-1 == orient); - - orient.markRefLevel (4); - CHECK (-3 == orient); - - orient.markRefLevel (4); - CHECK (-3 == orient); - - orient -= orient; - CHECK ( 0 == orient); - - ++orient; - CHECK (+1 == orient); - orient.resetToRef(); // now at level == 4 - orient.markRefLevel (7); - CHECK (-3 == orient); - - orient += 200; - orient -= 190; - CHECK (+7 == orient); - - OrientationIndicator o2(orient); - o2.markRefLevel(16); - CHECK (-2 == o2); - CHECK (+7 == orient); - } - }; - - - /** Register this test class... */ - LAUNCHER(HierarchyOrientationIndicator_test, "unit common"); - -}} // namespace lib::test diff --git a/tests/library/iter-explorer-test.cpp b/tests/library/iter-explorer-test.cpp deleted file mode 100644 index b5e9be9e3..000000000 --- a/tests/library/iter-explorer-test.cpp +++ /dev/null @@ -1,582 +0,0 @@ -/* - IterExplorer(Test) - verify evaluation patterns built using iterators - - Copyright (C) Lumiera.org - 2012, Hermann Vosseler - - This program 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. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -* *****************************************************/ - -/** @file iter-explorer-test.cpp - ** The \ref IterExplorer_test covers and demonstrates several usage scenarios and - ** extensions built on top of the \ref lib::IterExplorer template. These introduce some - ** elements from Functional Programming, especially the _Monad Pattern_ to encapsulate - ** and isolate intricacies of evolving embedded state. At usage site, only a _state - ** transition function_ need to be provided, thereby focusing at the problem domain - ** and thus reducing complexity. - ** - ** The setup for this test relies on a demonstration example of encapsulated state: - ** a counter with start and end value, embedded into an iterator. Basically, this - ** running counter, when iterated, generates a sequence of numbers start ... end. - ** So -- conceptually -- this counting iterator can be thought to represent this - ** sequence of numbers. Note that this is a kind of abstract or conceptual - ** representation, not a factual representation of the sequence in memory. - ** The whole point is _not to represent_ this sequence in runtime state at once, - ** rather to pull and expand it on demand. Thus, all the examples demonstrate in - ** this case "build" on this sequence, they expand it into various tree-like - ** structures, without actually performing these structural operations in memory. - ** - ** All these tests work by first defining these _functional structures_, which just - ** yields an iterator entity. We get the whole structure it conceptually defines - ** only if we "pull" this iterator until exhaustion -- which is precisely what - ** the test does to verify proper operation. Real world code of course would - ** just not proceed in this way, like pulling everything from such an iterator. - ** Often, the very reason we're using such a setup is the ability to represent - ** infinite structures. Like e.g. the evaluation graph of video passed through - ** a complex processing pipeline. - ** - ** @todo as of 2017, this framework is deemed incomplete and requires more design work. ////////////////////TICKET #1116 - ** @deprecated Monads considered harmful -- as of 4/2023 this framework is about to be abandoned - */ - - - -#include "lib/test/run.hpp" -#include "lib/test/test-helper.hpp" -#include "lib/iter-adapter-stl.hpp" -#include "lib/format-cout.hpp" -#include "lib/format-util.hpp" -#include "lib/util.hpp" - -#include "lib/iter-explorer.hpp" -#include "lib/meta/trait.hpp" - -#include -#include -#include - - -namespace lib { -namespace test{ - - using ::Test; - using util::isnil; - using util::isSameObject; - using lib::iter_stl::eachElm; - using lib::iter_explorer::ChainedIters; - using lumiera::error::LERR_(ITER_EXHAUST); - using std::string; - - - namespace { // test substrate: simple number sequence iterator - - /** - * This iteration _"state core" type_ describes - * a sequence of numbers yet to be delivered. - */ - class State - { - uint p,e; - - public: - State(uint start, uint end) - : p(start) - , e(end) - { } - - bool - checkPoint () const - { - return p < e; - } - - uint& - yield () const - { - return util::unConst (checkPoint()? p : e); - } - - void - iterNext () - { - if (not checkPoint()) return; - ++p; - } - }; - - - - /** - * A straight ascending number sequence as basic test iterator. - * The tests will dress up this source sequence in various ways. - */ - class NumberSequence - : public IterStateWrapper - { - - public: - explicit - NumberSequence(uint end = 0) - : IterStateWrapper (State(0,end)) - { } - NumberSequence(uint start, uint end) - : IterStateWrapper (State(start,end)) - { } - - /** allow using NumberSequence in LinkedElements - * (intrusive single linked list) */ - NumberSequence* next =nullptr; - }; - - inline NumberSequence - seq (uint end) - { - return NumberSequence(end); - } - - inline NumberSequence - seq (uint start, uint end) - { - return NumberSequence(start, end); - } - - NumberSequence NIL_Sequence; - - - /** - * an arbitrary series of numbers - * @note deliberately this is another type - * and not equivalent to a NumberSequence, - * while both do share the same value type - */ - typedef IterQueue NumberSeries; - - - /** - * _"exploration function"_ to generate a functional datastructure. - * Divide the given number by 5, 3 and 2, if possible. Repeatedly - * applying this function yields a tree of decimation sequences, - * each leading down to 1 - */ - inline NumberSeries - exploreChildren (uint node) - { - NumberSeries results; - if (0 == node % 5 && node/5 > 0) results.feed (node/5); - if (0 == node % 3 && node/3 > 0) results.feed (node/3); - if (0 == node % 2 && node/2 > 0) results.feed (node/2); - return results; - } - - - /** Diagnostic helper: "squeeze out" the given iterator - * and join all the elements yielded into a string - */ - template - inline string - materialise (II&& ii) - { - return util::join (std::forward (ii), "-"); - } - - template - inline void - pullOut (II & ii) - { - while (ii) - { - cout << *ii; - if (++ii) cout << "-"; - } - cout << endl; - } - - } // (END) test helpers - - - - - - - - /*******************************************************************//** - * @test use a simple source iterator yielding numbers - * to build various functional evaluation structures, - * based on the \ref IterExplorer template. - * - the [state adapter](\ref verifyStateAdapter() ) - * iterator construction pattern - * - helper to [chain iterators](\ref verifyChainedIterators() ) - * - building [tree exploring structures](\ref verifyDepthFirstExploration()) - * - the [monadic nature](\ref verifyMonadOperator()) of IterExplorer - * - a [recursively self-integrating](\ref verifyRecrusiveSelfIntegration()) - * evaluation pattern - * - * # Explanation - * - * Both this test and the IterExplorer template might be bewildering - * and cryptic, unless you know the *Monad design pattern*. Monads are - * heavily used in functional programming, actually they originate - * from Category Theory. Basically, Monad is a pattern where we - * combine several computation steps in a specific way; but instead - * of intermingling the individual computation steps and their - * combination, the goal is to isolate and separate the _mechanics - * of combination_, so we can focus on the actual _computation steps:_ - * The mechanics of combination are embedded into the Monad type, - * which acts as a kind of container, holding some entities - * to be processed. The actual processing steps are then - * fed to the monad as "function object" parameters. - * - * Using the monad pattern is well suited when both the mechanics of - * combination and the individual computation steps tend to be complex. - * In such a situation, it is beneficial to develop and test both - * in isolation. The IterExplorer template applies this pattern - * to the task of processing a source sequence. Typically we use - * this in situations where we can't afford building elaborate - * data structures in (global) memory, but rather strive at - * doing everything on-the-fly. A typical example is the - * processing of a variably sized data set without - * using heap memory for intermediary results. - * - * @see IterExplorer - * @see IterAdapter - */ - class IterExplorer_test : public Test - { - - virtual void - run (Arg) - { - verifyStateAdapter(); - - verifyMonadOperator (); - verifyChainedIterators(); - verifyRawChainedIterators(); - - verifyDepthFirstExploration(); - verifyBreadthFirstExploration(); - verifyRecursiveSelfIntegration(); - } - - - - /** @test demonstrate the underlying solution approach of IterExplorer. - * All of the following IterExplorer flavours are built on top of a - * special iterator adapter, centred at the notion of an iterable state - * element type. The actual iterator just embodies one element of this - * state representation, and typically this element alone holds all the - * relevant state and information. Essentially this means the iterator is - * _self contained_. Contrast this to the more conventional approach of - * iterator implementation, where the iterator entity actually maintains - * a hidden back-link to some kind of container, which in turn is the one - * in charge of the elements yielded by the iterator. - */ - void - verifyStateAdapter () - { - NumberSequence ii = seq(9); - CHECK (!isnil (ii)); - CHECK (0 == *ii); - ++ii; - CHECK (1 == *ii); - pullOut(ii); - CHECK ( isnil (ii)); - CHECK (!ii); - - VERIFY_ERROR (ITER_EXHAUST, *ii ); - VERIFY_ERROR (ITER_EXHAUST, ++ii ); - - ii = seq(5); - CHECK (materialise(ii) == "0-1-2-3-4"); - ii = seq(5,8); - CHECK (materialise(ii) == "5-6-7"); - - ii = NIL_Sequence; - CHECK ( isnil (ii)); - CHECK (!ii); - } - - - - /** @test verify a helper to chain a series of iterators into a "flat" result sequence. - * This convenience helper is built using IterExplorer building blocks. The resulting - * iterator _combines_ and _flattens_ a sequence of source iterators, resulting in a - * simple sequence accessible as iterator again. Here we verify the convenience - * implementation; this uses a STL container (actually std:deque) behind the scenes - * to keep track of all added source iterators. - */ - void - verifyChainedIterators () - { - typedef ChainedIters Chain; - - Chain ci = iterChain (seq(5),seq(7),seq(9)); - - CHECK (!isnil (ci)); - pullOut (ci); - CHECK ( isnil (ci)); - VERIFY_ERROR (ITER_EXHAUST, *ci ); - VERIFY_ERROR (ITER_EXHAUST, ++ci ); - - CHECK (isnil(Chain())); - CHECK (!iterChain (NIL_Sequence)); - - // Iterator chaining "flattens" one level of packaging - NumberSequence s9 = seq(9); - ci = iterChain (s9); - - for ( ; s9 && ci; ++s9, ++ci ) - CHECK (*s9 == *ci); - - CHECK (isnil(s9)); - CHECK (isnil(ci)); - - // Note: Iterator chain is created based on (shallow) copy - // of the source sequences. In case these have an independent - // per-instance state (like e.g. NumberSequence used for this test), - // then the created chain is independent from the source iterators. - s9 = seq(9); - ci = iterChain (s9); - CHECK (0 == *s9); - CHECK (0 == *ci); - - pullOut (ci); - CHECK (isnil(ci)); - CHECK (0 == *s9); - pullOut (s9); - CHECK (isnil(s9)); - } - - - /** @test variation of the iterator chaining facility. - * This is the "raw" version without any convenience shortcuts. - * The source iterators are provided as iterator yielding other iterators. - */ - void - verifyRawChainedIterators () - { - typedef std::vector IterContainer; - typedef RangeIter IterIter; - - typedef ChainedIters Chain; - - NumberSequence s5 (1,5); - NumberSequence s7 (5,8); - NumberSequence s9 (8,10); - - CHECK (1 == *s5); - CHECK (5 == *s7); - CHECK (8 == *s9); - - IterContainer srcIters; - srcIters.push_back (s5); - srcIters.push_back (s7); - srcIters.push_back (s9); - - IterIter iti = eachElm(srcIters); - CHECK (!isnil (iti)); - - // note: iterator has been copied - CHECK ( isSameObject (srcIters[0], *iti)); - CHECK (!isSameObject (s5, *iti)); - - Chain chain(iti); - CHECK (!isnil (iti)); - CHECK (1 == *chain); - - ++chain; - CHECK (2 == *chain); - - CHECK (1 == *s5); // unaffected of course... - CHECK (5 == *s7); - CHECK (8 == *s9); - - ++++chain; - CHECK (4 == *chain); - ++chain; - CHECK (5 == *chain); // switch over to contents of 2nd iterator - ++++++++chain; - CHECK (9 == *chain); - - ++chain; - CHECK (isnil(chain)); - VERIFY_ERROR (ITER_EXHAUST, *chain ); - VERIFY_ERROR (ITER_EXHAUST, ++chain ); - } - - - - /** @test a depth-first visiting and exploration scheme of a tree like system, - * built on top of the IterExplorer monad. - * - * # Test data structure - * We build a functional datastructure here, on the fly, while exploring it. - * The `exploreChildren(m)` function generates this tree like datastructure: - * For a given number, it tries to divide by 5, 3 and 2 respectively, possibly - * generating multiple decimation sequences. - * - * If we start such a tree structure e.g. with a root node 30, this scheme yields: - * \code - * ( 30 ) - * ( 6 10 15 ) - * ( 2 3 2 5 3 5 ) - * ( 1 1 1 1 1 1 ) - * \endcode - * This tree has no meaning in itself, beyond being an easy testbed for tree exploration schemes. - * - * # How the exploration works - * We use a pre defined Template \ref DepthFirstExplorer, which is built on top of IterExplorer. - * It contains the depth-first exploration strategy in a hardwired fashion. Actually this effect is - * achieved by defining a specific way how to _combine the results of an exploration_ -- the latter - * being the function which generates the data structure. To yield a depth-first exploration, all we - * have to do is to delve down immediately into the children, right after visiting the node itself. - * - * Now, when creating such a DepthFirstExplorer by wrapping a given source iterator, the result is again - * an iterator, but a specific iterator which at the same time is a Monad: It supports the `>>=` operation - * (also known as _bind_ operator or _flatMap_ operator). This operator takes as second argument a function, - * which in our case is the function to generate or explore the data structure. - * - * The result of applying this monadic `>>=` operation is a _transformed_ version of the source iterator, - * i.e. it is again an iterator, which yields the results of the exploration function, combined together - * in the order as defined by the built-in exploration strategy (here: depth first) - * - * @note technical detail: the result type of the exploration function (here `exploreChildren()`) determines - * the iterator type used within IterExplorer and to drive the evaluation. The source sequence used to - * seed the evaluation process can actually be any iterator yielding assignment compatible values: The - * second example uses a NumberSequence with unsigned int values 0..6, while the actual expansion and - * evaluation is based on NumberSeries using signed int values. - */ - void - verifyDepthFirstExploration () - { - NumberSeries root = elements(30); - string explorationResult = materialise (depthFirst(root) >>= exploreChildren); - CHECK (explorationResult == "30-6-2-1-3-1-10-2-1-5-1-15-3-1-5-1"); - - NumberSequence to7 = seq(7); - explorationResult = materialise (depthFirst(to7) >>= exploreChildren); - CHECK (explorationResult == "0-1-2-1-3-1-4-2-1-5-1-6-2-1-3-1"); - } - - - - - /** @test a breadth-first visiting and exploration scheme of a tree like system, - * built on top of the IterExplorer monad. - * Here, an internal queue is used to explore the hierarchy in layers. - * The (functional) datastructure is the same, just we're visiting it - * in a different way here, namely in rows or layers. - */ - void - verifyBreadthFirstExploration () - { - NumberSeries root = elements(30); - string explorationResult = materialise (breadthFirst(root) >>= exploreChildren); - CHECK (explorationResult == "30-6-10-15-2-3-2-5-3-5-1-1-1-1-1-1"); - } - - - - /** @test verify a variation of recursive exploration, this time to rely - * directly on the result set iterator type to provide the re-integration - * of intermediary results. Since our `exploreChildren()` function returns - * a NumberSeries, which basically is a IterQueue, the re-integration of expanded - * elements will happen at the end, resulting in breadth-first visitation order -- - * but contrary to the dedicated `breadthFirst(..)` explorer, this expansion is done - * separately for each element in the initial seed sequence. Note for example how the - * expansion series for number 30, which is also generated in verifyBreadthFirstExploration(), - * appears here at the end of the explorationResult sequence - * @remarks this "combinator strategy" is really intended for use with custom sequences, - * where the "Explorer" function works together with a specific implementation - * and exploits knowledge about specifically tailored additional properties of - * the input sequence elements, in order to yield the desired overall effect. - * Actually this is what we use in the steam::engine::Dispatcher to generate - * a series of frame render jobs, including all prerequisite jobs - */ - void - verifyRecursiveSelfIntegration () - { - typedef IterExplorer - ,iter_explorer::RecursiveSelfIntegration> SelfIntegratingExploration; - - NumberSeries root = elements(10,20,30); - SelfIntegratingExploration exploration(root); - string explorationResult = materialise (exploration >>= exploreChildren); - CHECK (explorationResult == "10-2-5-1-1-20-4-10-2-2-5-1-1-1-30-6-10-15-2-3-2-5-3-5-1-1-1-1-1-1"); - } - - - - /** @test cover the basic monad bind operator, - * which is used to build all the specialised Iterator flavours. - * The default implementation ("Combinator strategy") just joins and flattens the result sequences - * created by the functor bound into the monad. For this test, we use a functor `explode(top)`, - * which returns the sequence 0...top. - */ - void - verifyMonadOperator () - { - auto explode = [](uint top) { return seq(0,top); }; - - // IterExplorer as such is an iterator wrapping the source sequence - string result = materialise (exploreIter(seq(5))); - CHECK (result == "0-1-2-3-4"); - - // now, if the source sequence yields exactly one element 5... - result = materialise (exploreIter(seq(5,6))); - CHECK (result == "5"); - - // then binding the explode()-Function yields just the result of invoking explode(5) - result = materialise (exploreIter(seq(5,6)) >>= explode); - CHECK (result == "0-1-2-3-4"); - - // binding anything into an empty sequence still results in an empty sequence - result = materialise (exploreIter(seq(0)) >>= explode); - CHECK (result == ""); - - // also, in case the bound function yields an empty sequence, the result remains empty - result = materialise (exploreIter(seq(1)) >>= explode); - CHECK (result == ""); - - // combining an empty sequence and the one element sequence (seq(0,1)) results in just one element - result = materialise (exploreIter(seq(2)) >>= explode); - CHECK (result == "0"); - - // multiple result sequences will be joined (flattened) into one sequence - result = materialise (exploreIter(seq(5)) >>= explode); - CHECK (result == "0-0-1-0-1-2-0-1-2-3"); - - // since the result is a monad, we can again bind yet another function - result = materialise((exploreIter(seq(5)) >>= explode) >>= explode); - CHECK (result == "0-0-0-1-0-0-1-0-1-2"); - - // Explanation: - // 0 -> empty sequence, gets dropped - // 1 -> 1-element sequence {0} - // 2 -> {0,1} - // 3 -> {0,1,2} - - // Note: when cascading multiple >>= the parentheses are necessary, since in C++ unfortunately - // the ">>=" associates to the right, while the proper monad bind operator should associate to the left - } - }; - - - - LAUNCHER (IterExplorer_test, "unit common"); - - -}} // namespace lib::test - diff --git a/tests/library/iter-tree-explorer-test.cpp b/tests/library/iter-tree-explorer-test.cpp index 28043bd81..94f1969d2 100644 --- a/tests/library/iter-tree-explorer-test.cpp +++ b/tests/library/iter-tree-explorer-test.cpp @@ -20,39 +20,32 @@ * *****************************************************/ -/** @file iter-tree-explorer-test.cpp - ** The \ref IterTreeExplorer_test covers and demonstrates a generic mechanism - ** to expand and evaluate tree like structures. In its current shape (as of 2017), - ** it can be seen as an preliminary step towards retrofitting IterExplorer into - ** a framework of building blocks for tree expanding and backtracking evaluations. +/** @file iter-explorer-test.cpp + ** The \ref IterExplorer_test covers and demonstrates a generic mechanism + ** to expand and evaluate tree like structures. It was created in response to + ** a recurring need for configurable tree expanding and backtracking evaluations. ** Due to the nature of Lumiera's design, we repeatedly encounter this kind of ** algorithms, when it comes to matching configuration and parametrisation against ** a likewise hierarchical and rules based model. To keep the code base maintainable, ** we deem it crucial to reduce the inherent complexity in such algorithms by clearly ** separate the _mechanics of evaluation_ from the actual logic of the target domain. ** - ** Similar to IterExplorer_test, this test relies on a demonstration setup featuring - ** a custom encapsulated state type: we rely on a counter with start and end value, - ** embedded into an iterator. Basically, this running counter, when iterated, generates - ** a descending sequence of numbers start ... end. - ** So -- conceptually -- this counting iterator can be thought to represent this - ** sequence of numbers. Note that this is a kind of abstract or conceptual - ** representation, not a factual representation of the sequence in memory. - ** The whole point is _not to represent_ this sequence in runtime state at once, - ** rather to pull and expand it on demand. + ** This test relies on a demonstration setup featuring a custom encapsulated state type: + ** we rely on a counter with start and end value, embedded into an iterator as »state core«. + ** This running counter, when iterated, generates a descending sequence of numbers start ... end. + ** So -- conceptually -- this counting iterator can be conceived as _representing_ this sequence + ** of numbers, while not actually representing all these numbers as data in memory. And this is + ** the whole point of the exercise: _not to represent_ this sequence in runtime state at once, + ** rather to __pull and expand it on demand._ ** ** All these tests work by first defining these _functional structures_, which just ** yields an iterator entity. We get the whole structure it conceptually defines - ** only if we "pull" this iterator until exhaustion -- which is precisely what - ** the test does to verify proper operation. Real world code of course would - ** just not proceed in this way, like pulling everything from such an iterator. - ** Often, the very reason we're using such a setup is the ability to represent - ** infinite structures. Like e.g. the evaluation graph of video passed through - ** a complex processing pipeline. - ** - ** @warning as of 4/2023 the alternative Monad-style iterator framework "iter-explorer" will be retracted - ** and replaced by this design here, which will then be renamed into IterExplorer //////////////////////////////TICKET #1276 - ** + ** only if we »pull« and »materialise« this iterator until exhaustion — which essentially + ** is what the test does to verify proper operation. In contrast, _Real World Code_ of course + ** would not proceed in this way, like pulling everything from such an iterator. Since often + ** the very reason we're using such a setup is the ability to represent infinite structures. + ** Like e.g. the evaluation graph of video passed through a complex processing pipeline. + ** */ diff --git a/wiki/renderengine.html b/wiki/renderengine.html index ec0faa4af..6ba963737 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -521,7 +521,7 @@ noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print previ
-
+
//Monads are of questionable usefulness//
 
 Monads are a concept and theoretical framework from category theory.
@@ -536,9 +536,7 @@ What remains is a set of clever technicalities. Such can be applied and executed
 When we care for complexity, we do so while we care for matters tangible to humans. Related to software, this is the need to maintain it, to adjust it, to adapt and remould it, to keep it relevant. At the bottom of all of these lies the need to understand software. And this Understanding mandates to use terms and notions, even patterns, which evoke meaning -- even to the uninitiated. How can „Monads“ even be helpful with that?
 
 !To make sensible usage of Monads
-Foremost, they should be kept as what they are: technicalities. For the understanding, they must be subordinate to a real concept or pattern. One with the power to reorient our view of matters at hand.
-Thus we ask: what can be said about Monads?
-
+Foremost, they should be kept as what they are: technicalities. For the understanding, they must be subordinate to a real concept or pattern. One with the power to reorient our view of matters at hand. Thus we ask: //what can be said about Monads?//
 !!!Formalism
 Regarding the formalism, it should be mentioned
 * that a Monad is a //type constructor// -- it takes a type parameter and generates a new instant of monadic kind.
@@ -4271,9 +4269,9 @@ Most calculations are not monolithic and can not be performed in-memory in a sin
 Pre-planning this chain of schedules is only necessary for {{{TIMEBOUND}}} playback -- in the other cases, for //best effort// or //background// computations, just the sequence of dependencies must be observed, starting the next step only after all prerequisites are ready. And while the [[Scheduler]] actually has the capability to react on such logical dependencies, for precise timing the schedule deadlines will be prepared in the JobPlanningPipeline. Working backwards from the »top-level« frame job, prerequisites will be discovered incrementally by a recursive depth-first »tree exploration«. Defined by a //exploration function// within an iterator-pipeline construct, further layers of child dependencies are discovered and pushed onto a stack. At some point, a »leaf prerequisite« is discovered -- and at that point the complete chain of dependencies resides within this stack, each represented as a {{{JobPlanning}}} data record. The computation is arranges in a way to link these dependent planning contexts together, allowing to determine the dependent scheduling deadlines by recurring to the parent {{{JobPlanning}}} record, all the way up to the root record, which aligns to an time gird point externally set by the playback timings.
 
-
+
//Depth-first evaluation pipeline used in the FrameDispatcher to generate the next chunk of [[render jobs|RenderJob]]//
-This is an implementation structure backed and established by the [[Dispatcher|FrameDispatcher]] and operated by the RenderDrive core of each CalcStream, where it is assembled by a //builder notation// -- backed by the {{{TreeExplorer}}} iterator pipeline framework; besides the typical filter and transform operations, the latter offers an »expansion« mechanism to integrate a //monadic exhaustive depth-first search,// allowing to pick up all prerequisites of a given calculation, proceeding step by step within a local planning context.
+This is an implementation structure backed and established by the [[Dispatcher|FrameDispatcher]] and operated by the RenderDrive core of each CalcStream, where it is assembled by a //builder notation// -- backed by the {{{IterExplorer}}} iterator pipeline framework; besides the typical filter and transform operations, the latter offers an »expansion« mechanism to integrate a //monadic exhaustive depth-first search,// allowing to pick up all prerequisites of a given calculation, proceeding step by step within a local planning context.
 
 !Stages of the Job-planning pipeline
 ;frame-tick
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index eae7b5425..d6adec09e 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -54535,7 +54535,7 @@
 
 
 
-
+
 
   
     
@@ -54557,8 +54557,8 @@
     
   
 
-
-
+
+
 
   
     
@@ -54685,21 +54685,24 @@
 
 
 
-
-
+
+
+
+
 
 
 
 
 
 
-
+
+
 
 
 
 
 
-
+
 
 
 
@@ -54772,24 +54775,26 @@
 
 
 
-
+
 
+
 
-
+
 
-
-
-
-
+
+
+
+
 
-
+
 
 
 
 
 
 
-
+
+
 
 
 
@@ -54810,7 +54815,7 @@
 
 
 
-
+
 
 
 
@@ -69110,8 +69115,8 @@
 
 
 
-
-
+
+
 
 
 
@@ -69124,12 +69129,12 @@
 
 
 
-
+
 
-
-
-
-
+
+
+
+
 
 
   
@@ -69454,7 +69459,8 @@
 
 
 
-
+
+
 
 
 
@@ -69490,7 +69496,7 @@
 
 
 
-
+
 
 
 
@@ -69529,6 +69535,36 @@
 
 
 
+
+
+  
+    
+    
+  
+  
+    

+ Einige Aspekte wachsen noch graduell, möglicherweise jedoch bis zum Ende + vom Playback +

+
    +
  • + besagter Stack für die child-Prerequisites braucht Speicher gemäß + maximaler Rekursionstiefe (normalerweise wenig) +
  • +
  • + in jedem neu berührtten Segment muß einmal das JobTicket aufgebaut + werden (wird dann aber von weiteren Play-Vorgängen wiederverwendet) +
  • +
+

+ �� abgesehen davon ist der Speicher stabil +

+ +
+
+ + +
@@ -69723,7 +69759,7 @@ - + @@ -69740,11 +69776,27 @@
- - + + + + + + + + +

+ ...auch das erscheint mir heute als eine Folge der problematischen Monaden-Struktur. In dem neueren Pipeline-Design liefert die Expand-Funktion eben ganz bewußt zunächst die Parent-Node, und dann erst die Kinder, bzw. man kann die Navigation sogar steuern (da expandChildren() explizit aufgerufen wird). Damit ist ein solcher Struktur-Marker gar nicht mehr notwendig. Diese These wird dadruch bestätitgt, daß ich seither diverse rekursive Auswertungen implementiert habe, und nie mehr auf den HierarchyOrientationIndicator zurückgekommen bin +

+ +
+
- - + + + + + +
@@ -69951,12 +70003,12 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + @@ -69975,8 +70027,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -69991,17 +70043,17 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - - + + - + @@ -70048,7 +70100,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -70116,16 +70168,17 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + +
- + + @@ -84405,11 +84458,11 @@ class Something - + - - + + @@ -84493,11 +84546,11 @@ class Something - - + + - +