After the leftovers of the first Render-Engine implementation attempt were removed,
only one further usage of `RefArray` remains to be sorted out: the ''Session Element Tracker''.
Luckily, this one did not actually make any use of the abstraction abilities
of the `RefArray` — rather it basically stated that ''the interface is a data structure...''
After considering ''what kind of data'' can be expected to live in this structure,
it became clear that ''this will be a symbolic representation''
And thus the container can be simply switched to a `std::vector`.
This change allows to retain the existing placeholder-implementation unaffected,
while it would be possible to maintain algebraic terms here, in future.
__As an asside__: in order to decide about a suitable replacement in the Session,
I had to consier a first draft regarding the intended usage
and the prospective way of content representation
Investigated this topic again...
* these were initially created before C++11
* at that time, ''non-copyable'' objects were not common place
* but we embraced that concept already, and thus had quite some pain
when attempting to use such objects in STL containers
* with C++11 and ''move semantics'' these problems basically evaporated
* most usages were already upgraded and resolved
* another use case is to handle a state variable, which is based on
an immutable entity (like Time entities); `ItemWrapper` can be used
as a remedy in such a situation
This was a pre-C++11 implementation, and at that time,
I developed the ScopedHolder to allow handling non-copyable objects in STL containers
Meanwhile we have move semantics to achieve the same goal;
and since `ScopedPtrVect` shall be retained, it should be upgraded,
using the copy-and-swap approach
This is a plausible concept, and without obvious replacement
(letting aside `boost::ptr_vector`). It has a small number
of usages, and provides a dedicated API to show the
semantics when used as implementation of an ''Object Manager''
The original implementation used private inheritance from `std::vector`,
which is not really justified here, since we neither use the ''template method''
pattern, nor want to gain access to protected internals.
So this can be replaced with a private member.
...initially, this header was a collection of small helpers made on occasion;
one of them, the `ItemWrapper` used in transforming pipelines came
into widespread use and was much augmented and improved over the years.
many other tiny helpers could be replaced by standard library facilities...
Now looking into largely obsolete library facilities...
Starting from `ScopedHolder`, I found a surprising problem with ''perfect forwarding....''
...which however turned out to be the result of ''sloppy programming'' on my side.
At that time, in 2017, seemingly I was under pressure to define a Session-Command,
which captures a Time-entity as »State Memento«. Which turned out to be impossible,
since the Time entities were conceived as being immutable -- a questionable design
decision (see #1261), which already generated quite some additional complexities.
In the course of this »exercise«, I could again clarify that the implementation
of perfect forwarding works as expected on modern compilers — confusion may arrise
sometimes due to ''copy elision'' (which the compiler is required to perform
since C++17, even when the elided constructor has observable side effects).
And it can be derailed when (as was the case here) a »grab everything« constructor
accidentally ends up generating a copy- or move-constructor for the container class
itself. This is an instance of the problem documented with #963 ...
.... and the best solution is to abide by the rule-of-five (and a half)
and to put that `ReplacableItem` to where it belongs...
During the early stage of the Project, at some point I attempted
to »attack« the topic of Engine and Render Nodes following a ''top down path.''
This effort went into a dead end eventually — due to the total lack
of tangible reference points to relate to. However, the implementation
at that time prompted the development of several supporting facilities,
which remain relevant until today. And it resulted in a ''free wheeling''
compound of implementation structures, which could even be operated
through some highly convoluted unit test.
This piece of implementation code was valuable as starting point for th
»Playback Vertical Slice« in 2024 — resulting in a new design which was
''re-oriented'' towards a new degree of freedom (the »Domain Ontology«)
while handling the configuration and connectivity of Render Nodes in
a rather fixed and finite way. This new approach seems to be much more
successful, as we're now able to build, connect and invoke Render Nodes,
thereby mapping the processing through a functor binding into some
arbitrary, external processing function (which will later be supplied
by a media processing library — and thus be part of some »Domain Ontology«)
Initially we assumed that »handling time« is largely a matter of computation.
''Time is just a value'' and can be treated with integral arithmetic, some
modulus computations and pre-defined constants.
This turned out to be a mistake. Anything related to time is intricate,
and it is essential to distinguish different meanings of "times"
- time values related to an internal computation framework have
implementation-defined meaning and should be ''marked as opaque''
- temporal data can be ''mapped to a grid scale'' — an essential step
for media processing, which however incurs information loss
- externally relevant time specifications are represented symbolically,
by translation into a ''Time Code''
Drawing from these insights, a framework for time handling has been established;
building in part on the low-level function style base implementation.
Exposing this base implementation as a C-library however is considered
dangerous, as it may lure into ''ad hoc'' computations, which are a major
source of inconsistencies and notorious defects in many media applications.
The `FixedFrameQuantiser` relied on three functions from the raw-time handling library.
Since this (and NTSC drop-frame) are the only usages, these functions
can be relocated into the implemntation translation unit `lib/time/quantiser.cpp`
On closer inspection, this reveals some room for improvements:
Instead of relying on raw-computation functions written in C,
we could rather revert the dependency and express these computations
in terms of our Time-entities, which are written in C++, are much more
systematic and provide consistency checks and protection against numeric
overflow, all integrated with linear arithmetic and concise notation.
After performing these rearrangements,
most of the functions can be collapsed into ''almost nothing''.
This was taken as opportunity to re-check and improve the remaining
implementation core of the `FixedFrameQuantiser` -- the handling of
extreme corner cases can be much improved, now representing the
"grid-local time" as `Offset`, which doubles the possible value range.
The reworked unit test shows that, with this change, now the limitation
happens prior to quantisation, meaning that we always get a grid-aligned
result, even in the most extreme corner cases.
...extract these functions and the associated test
from the low-level C time handling library and
document them with a dedicated C++ header and test.
''This is unfinished work'' —
the extracted functions provide only the low level computations;
actually, a specialised time quantisation or time code would be required.
------------
Note though,
after extracting these functions, the rest of the plain-C test
can be removed, since equivalent functionality is covered in
much more detail by the tests of the C++ time handling framework.
Notably this allows to get rid of the direct component accessor functions.
------------
__Remark__: the base implementation of many time conversion functions
and especially NTSC drop-frame was provided by Stefan Kangas
See:
6a44134833
While these function may seem superficially plausible,
I more and more come to the conclusion that offering such
function as ''basic building blocks'' is in itself an
ill-guided approach to handling of time entities.
Time is neither „just a number“ — nor does it „contain“ hours, minutes and seconds.
It is possible to ''represent'' it through a **time-code**, which incurs
a quantisation step and implies a reference grid.
Thus Lumiera ''should not offer'' a »basic time handling library«.
Doing so would be just an invitation to bypass proper time handling
and avoid the use of more demanding but also more adequate mental concepts.
So the next step will be to remove functions not deemed adequate, and
better directly inline the respective modulus based computations.
Other functions can be integrated into the respective implementation
translation units for time quantisation and timecode representation.
Indeed — this change set is kind of sad.
Because I still admire the design of the GAVL library,
and would love to use it for processing of raw video.
However, up to now, we never got to the point of actually
doing so. For the future, I am not sure if there remains
room to rely on lib-GAVL, since FFmpeg roughly covers
a similar ground (and a lot beyond that). And providing
a plug-in for FFmpeg is unavoidable, practically speaking.
So I still retain the nominal dependency on lib-GAVL
in the Build system (since it is still packaged in Debian).
But it is pointless to rely on this library just for an
external type-def `gavl_time_t`. We owe much to this
inspiration, but it can be expected that we'll wrap
these raw time-values into a dedicated marker type
soon, and we certainly won't be exposing any C-style
interface for time calculations in future, since
we do not want anyone to side-step the Lumiera
time handling framework in favour of working
„just with plain numbers“
NOTE: lib-GAVL hompage has moved to Github:
https://github.com/bplaum/gavl
After the Dummy-Player (interface and prototype implementation)
is removed, some further bits of debris can be sorted out.
The first version of the Timeline was mostly rewritten and
the obsolete parts were removed in 2023, yet a small number
of files was kept around as reference for later.
Some of these are no longer considered of any information value,
other ones were moved into the widget package (and further problematic
parts were annotated, but can be still used with GTK-3)
For the [ticket:1221 »Playback Vertical Slice«] one of the next steps
will be to define a way to pass buffers from the core to the UI.
The `DisplayService` and the `DummyPlayerService` where parts of an
early architecture study to see how such a flexible connection between
components in different layers can be accomplished.
The findings from this prototyping work helped to shape the design
of the actual `PlayService`...
As an example, the `PixbufDisplayer` needs packed RGB888 data,
while the `XvDisplayer` expects YUV (MPEG-style) pixels.
The research setup is not well equipped to handle any kind of content
or format negotiation; yet for the experimentation, the connections can
be wired as !SigC-Signals. After the preceding refactorings,
`DummyImageGenerator` can be configured to perform the conversion to YUV
only when necessary, and to use the working buffer flexibly.
When supplied with packed RGB pixel data, the display in the Gtk::Image
is now correct, and also handles layout and scaling appropriately.
- since we now use 32bit int arithmetic (which is faster),
we can also use the exact value of the MPEG / Rec.601 coefficients
- and also the generation of the NTSC colour bar pattern
can be written much simpler and cleare with C++
This is a first step towards the ability to produce several different output formats...
Refactor the code to separate
- the double buffering
- the actual image generation, which works in RGB
- the conversion routine
Furthermore, replace unsigned char by std::byte
and introduce std::array and structured binding
to avoid many usages of pointers; hopefully this
makes the intention of the code clearer.
Verified and cross-checked the actual converion logic;
in fact this is a conversion to "YUV" as used by MPEG,
which in more precise terms is Y'CrCb with Rec.601 colour space
and a scan range limitation (16...235) on the Luma component.
Generally speaking, this experiment shows that we need some additional know-how
regarding the XVideo standard. And we should re-think the means of integration.
From some further debugging end experimenting with this code,
the following conlusions can be drawn:
- the code retrieves the GDK-Window, to which the widget was mapped
- it uses this to access an underlying X-Window
- seemingly the docking panel uses a separate X-Window, which also
hosts the header area of the panel
- this whole underlying X-Window is treated with some compositing method,
presumably (just guessing from the code) we use keying with a
marker-colour. This explains why the whole area of the panel
is no longer updated regularly
- furthermore, we need to take into account that the actual display widget
area uses some part of this window, which can be found out from
the VideoWidget's ''Allocation''
- when correcting the origin of the video display by using this
allocation's origin, at least the display of the video is precisely
at the right location and size
Furthermore, the code takes quite some shortcut and basically
looks for one specific display format, and uses the corresponding
configuration for the "port" it got.
This Format has the Abbreviation "YUY2" (packed)
''it basically works...''
TODO
- the image is updated only when moving the mouse over the widget
- calculation of window decoration is not correct
- a strange transparent zone appears in the UI directly above the widget
The new solution is to (still) use a Gdk::Pixbuf,
because in GTK-3 the more modern alternatives are not yet provided.
But we can pass this Pixbuf directly to Gtk::Image, instead of trying
to do low-level drawing on the X-Window.
Other than that, I more or less transformed the old C-style code
into the corresponding calls in Gdkmm.
__SUCCESS__: we get **some display**
__TODO__ : the displayed image is Garbage, which means that the pixel layout is not correct
In cases where none of the preferred, library based approaches works,
we can attempt to provide a ''poor man's video display'' by placing the
image data into a bitmap, which can be rendered by the UI toolkit.
The existing solution for such bitmap content is the GDK Pixmap.
But note, this approach is ''deprecated with GTK-4'', since,
generally speaking, GTK moves away from the notion of an
''underlying windowing system'' and relies on the concept
of ''graphic surfaces, textures and paintables'' rather...
Here we face the problem that the buttons in the play control panel
need to be connected to the controller, which sits in the viewer panel.
Obviously a direct connection is not correct, since there could be
several panels, and furthermore the controller should be a service and
addressed by commands via UI-Bus.
But this is an experiment, and we'll have to figure out anyway
how the playback-display-connection works, as one of the next tasks
for the »Playback Vertical Slice«
Thus we'll use the PanelManager to fetch the first viewer panel,
and then forward to the controller calls. With this setup,
the controller logic can be verified by printing to STDOUT.
TODO: we are not yet invoking any XVideo code....
While this is not strictly necessary for this experiment,
this is something we should try to establish early:
A »play control« should be handled as an independent UI element,
without tying it logically with some viewer (or timeline); the reason is
that such a play control needs a set of very well designed keyboard bindings,
and thus we will attempt use a focus concept to link to some active viewer
instead of creating one primary viewer, which gets the benefit of the
well accessible keybindings.
Basically we want to create an explicit association between
- a timeline
- some viewer
- a play-control
Introducing a new kind of panel shows again that the `PanelManager`
needs a rework; everything there is way too much ''hard wired''
And the new panel with the play control needs an **Icon** — which is
a challenge in itself; my proposal here is to build on the film metaphor,
and combine the symbol of "Play / Pause" with an stylised film or tape player
(with the secondary idea that this icon also somewhat looks like a owl face)
- place a `DemoController` instance as direct member into the `ViewerPanel`
- create a direct wiring, so that the `DemoController` can push to the `VideoDisplayWidget`
- make the `DemoController` directly instantiate a `TickService` and `DummyImageGenerator`
- reimplement play control functions by direct invocation
- add a new class to the Lumiera CSS stylesheet
- initial assessment shows that the Design of the **Displayer** framework is adequate
- for context: this code originates from the »Kino« video editor 20 years ago
- notably the `XvDisplayer` contains almost no GTK(2)-code
- so it seems feasible to attempt a port to GTK-3
This is a limited research project, and the setup shall be based mostly on existing code.
In the early stage of the Lumiera application, we did some architecture studies
regarding ongoing video generation and display, resulting in a `DemoVideoPlayer`.
This code was broken by the GTK-3 transition, but kept in-tree for later referral.
For this research project, we can mostly short-circuit all of the layer separation
and service communication stuff and build a minimal invocation directly hooked-up
behind the GUI widget. In preparation for such a setup, the existing
demo player code is partially forked by this changeset, pushing aside
the (defunct) !DummyPlayer pseudo-subsystem.
...to be more compliant to the »Lumiera Forward Iterator« concept.
This can be easily achieved by inheriting from util::RegexSearchIter,
similar to the example in CSV.hpp
Regarding #896, I changed the string rendering to use fs::path::generic_string
where appropriate, which means that we're using the normalised path rendering
Since C++17 we can use the std::filesystem instead (and we ''do use it'' indeed)
- relocate the `/lib/file.hpp` header
- adapt the self-discovery of the executable to using std::filesystem
Furthermore, some recherche regarding XVideo and Video Output
- remove obsolete configuration settings
- walk through all settings according to the documentation
https://www.doxygen.nl/manual/config.html
- now try to use the new feature to rely on Clang for C++ parsing
- walk through the doxygen-warnings.txt and fix some obvious misspellings
and structural problems in the documentation comments.
With Debian-Trixie, we are now using Doxygen 1.9.8 —
which produces massively better results in various fine points.
However, there are still problems with automatic cross links,
especially from implementation to the corresponding test classes.
The Boost-Libraries changed their internal implementation
of the formula to chain hash values.
Fortunately, we had already extracted the existing implementation
from Boost 1.67 and incorporated it in-tree, in the Lumiera support libary.
After switching to that `lib:#️⃣:combine()` function, all the graph
computations related to the Scheduler-test-load can be shown to be identical.
So at the moment, the impact is still limited, but this incident highlights
the importance of a controlled, stable (and ideally also portable) hash implementation.
seems that I've played too much with »undefined behaviour« in this test;
basically we can not assume ''any'' specific placement of local variables
in a stack frame....
In this test, what I wanted to demonstrate is that the overflow-block can reside
just »anywhere«, and that HeteraoData is just a light-weight front-End and accessor.
However, I can just demonstrate that without totally ''undefined behaviour;''
placement-new can be used to force the storage at a specific location (in the UninitialiesdStorage);
continue to access and use that data after leaving the nested scope is still
kind-of borderline, yet demonstrates that the data itself is just residing in a storage block...
- with Debian 12/13, the top-level `/bin`, `/sbin` and `/lib`
are collapsed into `/usr`. Seemingly this has prompted changes
to the way the shell prints some error messages. This broke
the expectation of some test of the test-framework itself.
- SCons always had the policy to ''sanitise'' the invocation environment,
to prevent unintended impact of environment settings to the test subject.
Seemingly this now also leads to `$HOME` not being defined.
Our file handling framework however expects to be able to expand "~"
- An old-style cast in the constructor lib::diff::Record(Mutator const&)
is now translated into a static_cast (≙conversion); and since the appropriate
conversion operator is missing on Mutator, the constructor attempts to
create a temporary, by re-invoking the same constructor ⟹ Stackoverflow ↯
- conversion from pointer to bool now counts as ''narrowing conversion''
- constructor names must not include template arguments (enforced with C++20)
- better use std::array for some dummy test code
Several further warnings are due to known obsoleted or questionable constructs
and were left as-is (e.g. for ScopedHolder) or just commented for later referral
* need to upgrade our custom packages to current standards
* switch those packages from CDBS to dh
* re-build on Trixie and upgrade the Lumiera DEB-Depot
After these (in detail quite expensive) preparations,
build with Scons and GCC-14 can be started.
Fix some further (basically trivial) compile problems,
uncovered by the improved type checking of modern compilers.
Note: a tremendous amount of warnings (and depreciations) is
also indicated, which will be addressed later....
Based on the building blocks developed thus far,
it was possible to assemble a typical media processing chain
* two source nodes
* one of these passes data through a filter
* a mixer node on top to combine both chains
* time-based automation for processing parameters
As actual computation, hash-chaining on blocks of
reproducible random data was used, allowing to verify
for every data word that expected computations were
carried out, in the expected order.
Using basically the same topology as in the preceding test, which focused on connectivity. However, in this case we retrieve actual processing functions from the »Test-Rand« ontology in order to perform hash-chaining computations on full data blocks. And, in addition, a »Param Agent Node« is used.
While initially intended as introductory test, it meanwhile
focuses on intricate technical details on the level of
basic building blocks, notably the `FeedManifold`
Now I have added a simple end-to-end demonstration example
how a Render Node is built from scratch, leaving out all
technical details and all convenience front-ends like
the `NodeBuilder` — just one dummy port invoked directly.
NodeBase_test demonstrates the building blocks of a Render Node,
and verifies low-level mechanics of those building blocks, which
can be quite technical. At the top of this test however are some
very basic interactions, which serve as an introduction.
__Remark__: renamed the low-level technical dispatch-access
for the parameter-accessors in `TurnoutSystem` to be more obvious,
and added comment (I was confused myself how to use them properly)
This is a crucial feature, discovered only late, while building
an overall integration test: it is quite common for processing functionality
to require both a technical, and an artistic parametrisation. Obviously,
both are configured from quite different sources, and thus we need a way
to pre-configure ''some parameter values,'' while addressing other ones
later by an automation function. Probably there will be further similar
requirements, regarding the combination of automation and fixed
user-provided settings (but I'll leave that for later to settle).
On a technical level, wiring such independent sources of information
can be quite a challenging organisational problem — which however can be
decomposed using ''partial function closure'' (as building a value tuple
can be packaged into a builder function). Thus in the end I was able to
delegate a highly technical problem to an existing generic library function.
* now able to demonstrate close-front, close-back and close-argument
* can also apply the same cases to `std::array`, with input and
output type seamlessly adapted to `std::array`
__Summary__:
* the first part to prepare a binding involves creating a mapped tuple,
with re-ordered elements and some elements replaced by placehoder-markers.
This part **must not use RValue-References** (doing so would be possible
only under very controlled conditions)
* the second part, which transports these mapped-tuple elements into the binder
''could be converted to perfect-forwarding.'' This would require to replace
the `Apply<N>' by a variadic template, delegating to `std::apply` and `std::bind`
With this changeset, I have modernised a lot of typedefs to make them more legible,
and I have introduced perfect-forwarding in the entrance path, up to the point
where the values are passed to `TupleConstructor`.
With these additions, all conceivable cases are basically addressed.
Take this as opportunity to investigate how the existing implementation
transports values into the Binder, where they will be stored as data fields.
Notably the mechanism of the `TupleConstructor` / `ElmMapper` indeed
''essentially requires'' to pass the initialisers ''by-reference'',
because otherwise there would be limitations on possible mappings.
This implies that not much can be done for ''perfect forwarding'' of initialisers,
but at least the `BindToArgument` can be simplified to take the value directly.
...which should ''basically work,'' since `std::array` is ''»tuple-like«'' —
BUT unfortunately it has a quite distinct template signature which does not fit
into the generic scheme of a product type.
Obviously we'd need a partial specialisation, but even re-implementing this
turns out to be damn hard, because there is no way to generate a builder method
with a suitable explicit type signature directly, because such a builder would
need to accept precisely N arguments of same type. This leads to a different
solution approach: we can introduce an ''adapter type'', which will be layered
on top of `std::array` and just expose the proper type signature so that the
existing Implementation can handle the array, relying on the tuple-protocol.
__Note__: this changeset adds a convenient pretty-printer for `std::array`,
based on the same forward-declaration trick employed recently for `lib::Several`.
You need to include 'lib/format-util.hpp' to actually use it.
What emerges here, seems to be a generic helper to handle
partial closure of ''tuple-like'' data records. In any case,
this is highly technical meta-programming code and mandates
extraction into a separate header — simplifying `NodeBuilder`
...on top of the parameter-decorating functionality developed thus far.
The idea is to allow in the `NodeBuilder` to supply ''some parameters''
directly, while the remaining parameters will be drawn from automation.
Several years ago, I developed some helpers for partial function closure.
Unfortunately these utils are somewhat limited, and rely on some pre-C++11
constructs, yet seem to be usable for the task at hand, since parameters
are always expected as value objects by definition.
This changeset shows a working proof-of concept for left-closing a
parameter tuple with 5 elements; this turns out to surprisingly difficult
due to the full genericity of the acceptable parameter-aggregates...
seemingly the definition can not be much simplified,
since there is no way around handling several definition flavours
of the processing-functor distinctly.
However, the definitions can be rearranged to be clearer,
the resulting type of the `FeedPrototype` can be deduced from the
builder function, and more stringent assertions can be added
...the idea is to limit the scope of possible changes
and rather directly accept a functor to transform the parameters.
We need then to account for the possible flexibility in processing-functor
arguments, while in fact only two cases must be actually handled.
''This proof-of-concept works in test setup''
It seemed that the integration test will end up as a dull repetition
of already coded stuff, just with more ports and thus more boilerplate;
and so I reconsidered what an actually relevant integration test might encompass
- getting parameters from the invocation
- translating and wiring parameters
- which entails to adapt / partially close a processing function!
Thus — surprise — there is a new feature not yet supported by the `NodeBuilder`,
which would be very likely to be used in many real-world use cases: which is
to adapt the parameter tuple expected by the binding from the library.
Obviously we want this, since many »raw« processing functions will expose a mix
of technical and artistic parameters; and we'd like to ''close'' the technical ones.
Such a feature ''should be implementable,'' based on the already developed
technique with the »cross builder«, which implies to switch the template arguments
from within a builder expression. We already do this very thing for adapting
parameter functor, and thus the main difficulty would be to compose an
adaptor functor to the correct argument of the processing functor...
Which is... (well, it is nasty and technical, yet feasible).
Just wanted to use a helper function to build a source-data node.
However, the resulting node had a corrupted Node-ID spec.
Investigation with the debugger showed that the ID was still valid
while in construction and shows up corrupted after returning from the
helper function.
As it turned out, the reason is related to the de-duplication of ProcID data.
While the de-duplicated strings themselves are ''not'' affected, the corruption
happened by an intermediate instance of ProcID, which was inadvertently created
and bound by-value to the builder-λ. The created Port then picks up a reference
to this temporary, leading to the use-after-free of the string_view obejcts.
Obviously, `ProcID` must not be instantiated other than through the static
front-end `ProcID::describe`. Due to the private constructor, I can not make this
object non-copyable (because then the hash-set would not be allowed to emplace it).
But making it at least move-only will provoke a compiler error whenever binding
to a lambda capture by value, which hopefully helps to pinpoint this
insidious problem in the future...
The scheme to provide preconfigured nodes with random `TestFrame` data
seems to be suitable and easy to extend to further cases; should however
always document the setup through a dedicated case in `NodeDevel_test`
Seems to be straight forward now, based on the implementation
of `TestFrame` manipulation provided by the »Test Rand Ontology«
__Remark__: the next goal is to reproduce the complex Node tree
with operations on TestFrame and then to invoke these and verify results.
...while this is not the main objective of this test case,
and another test will focus on invocation with full-fledged
`TestFrame` buffers and hash computation...
...it is still a nice achievement to see that these simple
algebraic operations used for demonstration can actually be
invoked in the whole connected network :-)
Using a Node network with
* two source nodes
* one of them chained up linearly with a filter node
* then on top a mix node to combine both chains
Can now verify the generated port specs and verify proper connections
at node level and at port level
This was a lot of intricate technical work,
and is now verified in-depth, covering all possible cases.
__We can now__
* build Nodes
* verify in detail correct connectivity
* read Node-IDs and processing specifications
* maintain a symbolic spec for the arguments of a Port
(and beyond that, we can also **invoke nodes**, which remains to be formally verified)
An essential goal still to reach is a verification of the `NodeBuilder`'s products
Relying on the low-level diagnostic facilities pioneered last days,
it should now be possible to define simple and readable connectivity-clauses,
allowing to build some connected nodes and then verify the connections explicitly.
Handling of extended attributes in conjunction with the hash
turns out to be a rather complicated topic, with some tricky fine details.
And, most important, at the moment I am lacking the proper perspective
to address it and find adequate solutions. Luckily, the cache-key is
not required at the moment, ''and so this topic will be postponed''
As a minimum to complete the diagnostics functions, it is sufficient to set
the appropriate flags in the `ProcID` directly -- and to add some convenience wrappers.
...especially the extended attributes remain somewhat nebulous,
since non of the prospective usages are close to being implemented right now.
It seems, we'll get two distinct sources at construction time of the Node
* additional qualifiers from the Library plug-in
* internal flags or qualifiers provided by the `NodeBuilder`
Another related concern seems to be generation of cache-keys,
which however will ''consume'' the proc-hash generated by the ProcID,
but not change the ID itself; cache-key generation is a tricky subject
and was somewhat overlooked regarding the connection to the `BufferProvider`.
Opened a new ticket #1292 as reminder for this issue.
...exploiting the ''backdoor access'' bypassing the VTable,
as made possible by a common congruent storage layout.
This is a first proof-of-concept, but also shows that the demo nodes
in NodeMeta_test are wired as expected. What is needed now is to make
this diagnostic access easier to invoke and more bullet-proof, by setting
the proper Attribute bits directly in the `NodeBuilder`
...to create an ''access path for diagnostics'' and further evaluations
while ''bypassing the VTable.''
It is a well-known downside of specifically typed, highly optimisable
template-based code to create a dangerous leverage for generating spurious,
mostly identical virtual function instances added for secondary concerns.
Thus it is a consequence of this design choice, either to forego some diagnostic
and analytical possibilities, or to exploit ''other means'' for retrieving
internal data, which is needed for tangential purposes only. The solution
pursued hereby exploits similar layout of various ''weaving pattern''
template instances to create an ''access backdoor'' for use cases
beyond the primary performance-critical path.
Some additional tests to challenge the parser, which seems to work well.
Without extended analysis into the usage of those node specifications,
it is pointless to expand further on its capabilities. For now, it is
sufficient to have a foundation for hash-computation in place.
__Note__: found a nifty way to give lib::Several an easy toString rendering,
without cranking up the header inclusion load.
This is a nice little goodie: allow to write repeated arguments with the
shorthand notation known from lisp and logic programming. For multi-channel media,
structurally similar wirings for each channel will be quite common....
...at the point where I identified the need to parse nested terms.
The goals are still the same
* write tests to ''verify connectivity'' of nodes generated by the new `NodeBuilder`
* allow for ''extended custom attributes'' in the ProcID
* provide the ability to mark specific parametrisations
* build a Hash-Key to identify a given processing step
__Note Library__: this is the first time `lib::Several` was used to hold a ''const object''.
Some small adjustments in type detection were necessary to make that work.
Access to stored data happens through the `lib::Several` front-end and thus always includes
the const modifier; so casting any const-ness out of the way in the low-level memory management
is not a concern...
This finishes an ''exercise'' in tool design,
which was set off by the requirement to parse the spec-ID of a render node.
While generally within the confines of a helper utility for simple use cases,
the solution became quite succinct and generic, as it allows to handle arbitrary
LL(n) grammars, possibly with recursion.
...which is the reason for this whole excursion into parser business;
we want to accept specification terms with elements from C++ type expressions,
which especially requires to accept complete comma separated lists within
angle brackets or parenthesis, while separating by comma at top level.
The idea is to model ''not as an expression'' but rather as an ''extended quote'',
and to use inverted regular expressions for non-quote-characters as terminal
...evaluating the recursive syntax of a numerical expression!
* so this light-weight parsing support framework indeed allows
to build fully capable LL(x) parsers, when the user knows how
handle syntax clauses and bind the result models
* furthermore, a notation is demonstrated how to arrange the
binding functions so to keep the syntax definition legible
* this involves a shortcut for homogeneous alternatives
The concept was indeed successful, albeit quite difficult to pull off in detail.
It requires a carefully crafted path of Deduction guides and overloads
to effect the switch from std::function to std::function& at the point
where a predeclared syntax clause placeholder is used recursively
In accordance to the plan drafted yesterday, I will try to integrate
this essential capability into the framework established thus far by a trick,
requiring only minimal adjustment, but some work by the user.
Since the parse function is defined as a (unqualified) template argument,
it is possible to emplace either a `std::function`, or a reference thereto.
For this to work, the user is required to pre-define the expected result type,
and, furthermore, must later on assign a fully specified clause, which
also has a model transformation binding attached to yield this predeclared
result type
...several improvements as result from the more elaborate test cases
- spelling out the model types taken as argument can be challenging and tedious,
thus improve the ability to pass a λ-generic.
- furthermore, using structured bindings on a SeqModel can also simplifiy
binding code; this did not work because the compiler picks the wrong strategy
and attempts to bind the structure fields; need to provide explicit speicalisations
to support the »tuple protocol« for SeqModel.
..considered several further helpers, (like auto-joining into a single string),
but in the end did not implement them, due to questionable relevance
The `bindMatch()` as implemented yesterday works only directly on top
of the terminal parsers, which yield a `RegExp`-Matcher. However,
it would be desirable to provide a generic shortcut to always get
some string as result model. A simple fallback is to return
the part of the input-string accepted thus far.
Basically the implementation is already in place;
yet for better error messages we need to find out if the given functor
can handle the model present at this stage. Since generic-λ are not
functions by themselves (but rather templates), we need to ''probe''
with the expected argument and see if instantiation is possible.
⚠ NOTE: still a strange bug related to using the same Syntax several times
Allowing free recursion in grammars is the key enabling feature,
which allows to accept arbitrary complex structures (like numeric expressions).
It is however also the element which makes the task of parsing a challenging endeavour;
after weighting the arguments, I decided ''not to place the focus on advanced usage,''
yet to open a pathway towards representation of such grammars.
Essentially, I consider it acceptable to require some additional work by the user,
if arbitrary recursive grammars are desired; because this design relies on explicitly
given parse functions, we need to introduce some kind of indirection interface,
to allow ''declaring'' a recursive rule first and later to ''supply the definition,''
which obviously then will involve other rules (or itself) recursively.
This leads to a very ''nifty approach'' towards recursion: we require the user
to provide an ''explicit model type'' beforehand, which implies that this is a
simple type, that can be spelled out (no λ) — and so the user is also
''forced to augment the actual rule with a model-binding,'' thereby reducing
the structured return types from the parse into something simple and uniform.
The user ''has to do the hard work,'' but can ''exploit additional knowledge''
related to the specific use case.
All this framework needs to do then is to supply a `std::function`, using the
explicit return type given; everything else will still work as implemented,
since a `std::function` can always stand-in for any arbitrary λ.
This is the very key feature that requires a real parser and can not be handled by regular expressions.
After all the groundwork, it is surprisingly easy provide now;
only coding up all those DSL-variants is tedious. Notably we also
support accepting an ''optional'' bracket, and we support arbitrary
expressions for the ''opening'' and ''closing construct.''
It seemed that using postfix-decorating operators would be a good fit
for the DSL. Exploring this idea further showed however, that such a scheme
is indeed a good fit from the implementation side, but leads to rather confusing
and hard to grasp DSL statements for many non-trivial syntax definition.
The reason is: such a postfix-decorator will by default work on ''everything defined''
up to that point; this is too much in many cases.
The other alternative would be a function-style definition, which has the benefit
to take the sub-clause directly as argument (so the scope is always explicit).
The downside is that argument arrangement is a bit more tricky for the repetition
combinator (there can be mis-matches, since we take the »SPEC« as free-template argument)
And, moreover, with function-style, having more top-level entrance points would
be helpful. Overall, no fundamental roadblock, just more technicalities in the setup
of the DSL functions.
With that re-arrangd structure, an optional combinator could be easily integrated,
and a solution was provided to pick up the parser function from a sub-expression
defined as Syntax object.
Meanwhile, some kind of style scheme has emerged for the DSL:
We're working much with postfix-decorating operators, which
augment or extend the ''whole syntax clauses defined thus far''
In accordance with this scheme, I decided also to treat repeated expression
as a postfix operator (other than initially planned). This means, the actual
body to be repeated is ''the syntax clause defined thus far'', and the
repeat()-operator only details the number of repetitions and an optional delimiter.
After all the preparation, now this panes out quite well:
* use a simple 3-way branch structure
* the model type was already pre-selected by the `_Join` Model selector
* can just pass the result-model elements to a constructor/builder
* incremental extension can be directly mapped to the predecessor model
This is a rather obnoxious limitation of C++ variadics:
the inability to properly match against a mixed sequence with variadics.
The argument pack must always be the last element, which precludes to match
the last or even the penultimate element (which we need here).
After some tinkering, I found a way to recast this as ''rebinding to a remoulded sequence'',
and could package a multitude of related tools into a single helper-template,
which works without any further library dependencies.
🠲 extract into a separate header (`variadic-rebind.hpp`) for ease of use.
So this turned out to be much more challenging than expected,
due to the fact that, with this design, typing information is
only available at compile-time. The key trick was to use a
''double-dispatch'' based on a generic lambda. In the end,
this could be rounded out to be self-contained library helper,
which is even fully copyable and assignable and properly
invokes all payload constructors and destructors.
The flip side is that such a design is obviously very flexible
and direct regarding the parser model-bindings, and it should
be fairly well optimisable, since the structure is entirely
static and without any virtual dispatch.
Proper handling of payload lifecycle was verified using
a tracking test object with checksum.
* the implementation of this ''Sum Type'' got quite technical and complicated;
thus better to be extracted as separate library component
* use this as base for the `AltModel`
* make a usage sketch, invoking only the model interactions required
After exploring the »nested decorator-chain« implementation variant,
I decided to stick to the solution with the λ-vistor, while attempting
to level and smooth-out the design.
* allow to engage ''any'' «slot» at construction
* reverse the order of type parameters to be ascending (as in `std::tuple`)
* make it fully copyable, movable, assignable and provide a `swap()`,
relying on a variant of the "copy-and-swap"-idiom
* add an ''cross-constructor'' for an extended branch set
To represent the result-model for syntax alternatives,
we need a C++ representation for a ''sum type,'' i.e.
a type that can be one from a fixed set of alternatives.
Obviously the implementation will rely on some kind of Union,
or otherwise employ an opaque buffer and perform a forced cast.
Moreover, to be actually usable, a branch-selector-ID must be
captured and stored alongside, so that code processing the results
can detect which branch of the syntax was chosen.
There seem to be several possible avenues to build and structure
an actual class template to provide this implementation model
* a nested decorator-chain
* using a recursive selector-function with a generic-λ
''all these look quite unattractive, unfortunately....''
Seems like a pragmatic choice, which simplifies most syntax definitions significantly.
In exceptional cases, it is still possible to enforce a situation with `\b` or `\B`
* need to pass the parse end-point in the Eval-Result to allow composed models
* this also prepares for support of generic model-binding-λ
With the help of the model-joining case definitions it is then possible to handle sequence extension.
Deliberately I do not engage into fine grained signature checking, since this would lead to very technical code and moreover this is an implementation feature and we control all invocations (with signatures guaranteed to be correct)
Unfortunately, there are some common syntactic structures, which can not easily be dissected by regular expressions alone, since they entail nested subexpressions. While it is possible to get beyond those fundamental limitations with some trickery, doing so remains precisely that, ''trickery.''
After fighting some inner conflicts, since ''I do know how to write a parser'' —
in the end I have brought myself to just do it.
And indeed, as you'd might expect, I have looked into existing library solutions,
and I would not like to have any one of them as part of the project.
* I do not want a ''parser engine'' or ''parser generator''
* I want the directness of recursive-descent, but combined with Regular Expressions as terminal
* I want to see the structure of the used grammar at the definition site of the custom parser function
* I want deep integration of ''model bindings'' into the parse process, i.e. binding-λ
* I do not want to write model-dissecting or pattern-matching code after the parse
* I do not want to expose ''Monads'' as an interface, since they tend to spread unhealthy structure to surrounding code
* I do not want to leak technicalities of the parse mechanics into the using code
* I do not want to impose hard to remember specific conventions onto the user
Thus I've set the following aims:
* The usage should require only a single header include (ideally header-only)
* The entrance point should be a small number of DSL-starter functions
* The parser shall be implemented by recursive-descent, using the parser-combinator technique
* But I want that wrapped into a DSL, to be able to control what is (not) provided or exposed.
* I want a stateful, applicative logic, since parsing, by its very nature, is stateful!
* I want complete compile-time typing, visible to the optimiser, without a virtual »Parser« interface
And last but not least, ''I do not want to create a ticket, since I do not know if those goals can be achieved...''
Building a correct processing-identification is a complex and challenging task; only some aspects can be targeted and implemented right now, as part of the »Playback Vertical Slice«
* components of the ProcID
* parsing the argument-spec
* dispatch of detail information function to retrieve source ports
The choice to rely on strictly typed functor bindings for the Node operation
bears the danger to produce ''template bloat'' — it would be dangerous to add
further functions to the Port-API naïvely; espeically simple information functions
will likely not depend on the full type information.
A remedy to explore would be to exploit properties marked into the Port's `ProcID`
as key for a dispatcher hashtable; assuming that the `NodeBuilder` will be responsible
for registering the corresponding implementation functions, such a solution could even
be somewhat type-safe, as long as the semantics of the ProcID are maintained correctly.
* this changeset builds a complex processing network for the first time
* furthermore, some ideas towards verification are spelled out
''verification not implemented''
...which aims at building up increasingly more complex Node Graphs,
to validate that all clauses are defined and connected properly.
Reconsidering the testing plan: initially especially this test was aimed
primarily at driving me through the construction of the Node builder and
connection scheme. Surprisingly enough, already the first test case basically
forced the complete construction, by setting me on tangential routes,
notably the **parameter handling**.
Now I'm returning to this test plan with an already finished construction,
and thus it can be straightened just to give enough coverage to validate
the correctness of this construction...
The namespace `steam::engine::test::ont` will hold some typical definitions
for the fake „media processing library“ — to be used for validating aspects of mapping and binding.
This picks up the efforts towards a »Test Ontology« from end November:
d80966c1f
The `TestRandOntology` is intended as a playground to gradually find out
how to maintain bindings processing functionality provided by a specific Library
and thus related to a ''Domain Ontology''
Remark: generating symbolic specs might seem like a mere test exercise, yet is in fact
quite crucial, since the node-identity is based on such a spec, which must be ''semantically correct,''
otherwise caching and especially cache invalidation will be broken.
Yesss .... in Lumiera naming and cache invalidation are linked directly ;-)
This is a high-level integration test to sum up this development effort
* an advanced refactoring was carried out to introduce a
flexible and fully-typed binding for the ''processing-functor''
* this entailed a complete rework of the `FeedManifold` to integrate
inline storage for a ''parameter tuple'' and input / output ''buffer tuples''
* optional ''parameter functors'' were included into the design at a deep level,
closely related to the binding of the processing-functor
* the chosen design is thus a compromise between ''everything nodes''
and a ''dedicated parameter-handling'' at invocation level
As a proof-of-concept, an scheme to handle extended parameters was devised,
using a special »Param Agent Node« and extension storage blocks in stack memory.
While not immediately necessary, this design exercise proves the overall design
is flexible enough to accommodate future extended needs.