Gradually, the header buffhandle.hpp became the minimal include
to use basic `BufferProvider` functionality, since `BuffHandle`
provides proxy functionality to lock and access a data buffer.
The whole compound connected to the `BufferProvider` has evolved
into a cohesive yet central connection hub related to buffer memory;
it should thus be extracted, moved down to »Vault« layer and used
4as a foundation for various high-level tasks. In preparation of
this step, relevant marker typedefs are re-arranged;
notably ''Stream Type'' now includes `buffhandle.hpp`
These are generic vehicles to verify computation structures.
Since they are in no way related to our models, they belong into »Vault« layer
Furthermore this reorganisation of test support components is prerequisite
to also move further generic facilities down into the Vault
This resolves a long-standing issue:
we could not use proper include paths for test-helper code that was defined alongside the tests.
Since some test-helper code must now be relocated into the »Vault«-Layer,
I took the opportunity to finally resolve this annoyance. The solution is two-fold
* introduce a dedicated test-support library
* remove the quirks that were necessary to support some plain-C tests
Together, this allows to raise the include-directory by one level,
and then to adjust all folder names so that they match the namespace
This is something I wanted to clean-up since a very long time indeed,
but always postponed it, since it requires a lot of nasty clean-up and disentangling....
Several parts had been stashed away into `src/lib/test`, which are not really related:
* the testrunner and testsuite builder
* generic helper utilities
* specialised test-support facilities, e.g. the EventLog
This changeset introduces a new top-level test folder and a namespace `::test`
Code from this folder is linked into a shared library `libtestsupport.so`,
which resides in the linkage hierarchy between »Vessel« and »Vault«,
yet is no longer part of the Lumiera application and only linked into the test-suite.
The next step with the `BufferProvider` and `OutputSlot` refactoring
will require to create new definition headers and to move those self-contained
services down into the »Vault« layer. In preparation, I decided deliberately to
abandon that old idea of a ''minimal bootstrap'' of arbitrary components in
favour of building the application as a coherent whole, with well-defined
boundaries and a backbone structure, marked as such explicitly by name
and arrangement of the code.
The changes were documented and applied to the integration branch first,
and now it's time to join that effort with the ongoing work on `BufferProvider`.
During the last months, a lot of gradual changes to policies and guidelines
had been finalised and documented. Moreover, recent clarifications regarding
memory management in the render engine will lead to a clearer definition
of the »Vault« Layer's role, with some ''engine code'' expected to move down.
On this occasion, I reworked relevant sections in the documentation to
confirm the role that now falls to central parts of the overall architecture.
Notably I decided to rename those parts related to the application backbone
as »Vessel« — to underpin the more active and defining role they assumed.
Explored further aspects of this new design;
as an aside, I tried to build a Concept to describe the policy,
which howerer turned out to be much more challenging than expected.
Lastly, I could now complete the prototype of `BufferProxyAdaptor`
While it is trivially simple to define some struct with callback functions,
such a definition turns out to be obnoxiously complex and hard to read in pracice,
thereby nullifying any benefit from the shorthand λ-based notation.
This kind of problem with the readability at client-side is quite common
when using callbacks for a policy-based design. To overcome this difficutly,
it would be necessary to define the policy and the callbacks in one
succinct builder expression. Unfortunately such a builder syntax
typically requires to write a lot of boilerplate template code.
After several failed experiments, I have found a bold new way to
exploit features of C++20 in conjunction with a macro, allowing
to generate a suitable data record directly form an C++ expression.
The idea is to use a "transient" Setup helper class, which lives
only during the invocation of the BufferProviderSetup-constructor.
This transient frame allows to accept a policy class from the client,
and move it through this transient Setup helper and finally into
the BufferStore backend implementation, where it is actually needed.
Now I have a working setup for a `BufferProxyAdaptor`.
Unfortunately this is still confusingly complex.
This is due to my intention to build a generic template,
that can be configured with a policy to invoke arbitrary callbacks,
whenever one of the passed-through buffers transitions through its lifecycle.
* we need to configure the `BufferProviderSetup` to use our specific implementation classes
* and the second one of these, our `BufferStore` implementation needs to invoke the callbacks
This closes a ''conceptual gap.''
There was a `lib::TestFrame::reseed()` function, which should be invoked
whenever a clean / deterministinc seed is required for computations involving
`TestFrame` data.
On occasion I noticed that most actual usages of `TestFrame`, which became quite common
lately, were missing this `reseed()` call, while the enclosing test
had indeed a proper Test::seedRand() call.
So my conclusion is: if even ''I myself'' miss that fine point frequently,
it is justified to automated this setup, even while `TestFrame` is right now
the only client of such a setup-facility.
There is a twist though.
`TestFrame` is part of the ''Engine'' test coverage, i.e. it is linked
into `libtest-core.so`. While the support for random numbers and reseeding
is obviously a very basic library feature, and linked into `liblumiera.so`
**Oh well**.
So now we have ''yet another'' static registration service.
I kept everything as simple as possible, which means, we continue
to use the ''common, global random generator'' from the library,
and there is no protection against some other code using those
library functions in a non-deterministic way.
`lib/random-reseed.cpp` is just a glorified hard-wired setup, beware!
This is an implementation experiment to validate the improvements
and internal clean-up. Previously, I was unable to implement such a
forwarding proxy buffer provider, as would be required for the
intended harmonisation of the `OutputSlot` interface with `BufferProvider`
With the reworked verison of the BufferProvider standard implementation
it seems straight-forward to...
* define a »Buffer Type« that marks any of the prepared buffers to pass-through
* use the resulting Buffer Descriptor together with a rigged storage backend,
that just returns the ''local tag'' as buffer adress
The first round of the [ticket:1410 BufferProvider clean-up] is roughly complete.
* a much cleaner diagnostic interface has been defined and implemented
* this allowed to modernise the `DiagnosticBufferProvider_test`
and the `BufferProviderProtocol_test`
* the implementation scheme has been split in two realms
* the old heap-memory based implementation has been re-arranged,
so that the aforementioned tests are **green** again.
Now is the point to draft a solution to get the interfaces of
`BufferProvider` and `OutputSlot` aligned, so that the latter
can expose its lifecycle through the former.
Today, I have analysed various aspects and developed a pathway
to get into that task, and next I will reshape the previously
failed `OutputProxyProvider_test` into a prototype to
validate this idea
* write down some first considerations regarding requirements
of the actual production-strength implementation of buffer management
* consider relevance of the part of the API that was retained
* write a new series of test cases to cover handling of embedded objects,
using the test support for tracking object creation
Some weeks ago, at the time where I started to dig into this topic,
I extended the BufferProviderProtocol_test to perform a buffer usage pattern
that mimics the usage in the real Render Engine, as implemented last year.
For this, I used the `TestFrame` and the chained-hash dummy computations.
Now, after reordering and clean-up of the `BufferProvider` implementation interfaces,
it can be showed with this computation scheme that
- all buffers are indeed allocated and handed out as expected
- the metadata handling is correct and points to the processed buffers
- the complete memory used in this test is uncorruped and holds correct numbers
It covers now roughly the same procedures as before,
yet the scheme how to do verification is quite different.
Furthermore, we allocate large random-sized buffers now
and implant `TestFrame` instances, and so also the verification
covers a significant storage area.
* can now replace old verification code by new diagnostic functions
* test case runs without failure
''UNRESOLVED:'' failure in verification of memory contents
The drafted setup for capturing tracking information was indeed way to elaborate.
Notably I wanted to capture the actual `BuffHandle` -- and that caused a host
of visibility and access problems, since that handle was meant to be used
as a front-end to the BufferTracker, and only that.
Now we just capture the hash-IDs and the storage size,
and we do not even keep relations or record a state-ENUM;
we just smash a copy of that data into a registry vector
for each state separately, which allows to do a lookup,
count the number of handled allocations and access the memory.
...this turns out to be surprisingly difficult,
due to various subclass / protected visibility problems.
Not sure if the component design within `DiagnosticBufferProvider`
is sane yet...
Use a generic helper function in the `BufferProviderSetup` base class
to allow layering of a decorator implementation on top of an already
instantiated implementation class.
After the preceding internal reorganisation, it suffices
for the `DiagnosticBufferProvider` to instrument the `BufferStage` interface,
in order to capture and record all relevant buffer state transitions
This is clearly implementation code and can now be removed from the interfaces;
`SimpleBufferStateRegistry` can thus delegated to a private metadata registry
In the end I refrained from applying most of the changes I had intended.
I am aware that `BufferProvider` is still quite a massive structure,
yet with the implementation refactorings done thus far, it seems
to be reasonably clear and self-contained now.
It would be possible to totally overthrow the grown structure and
expose only the two handles (BuffDescr and BuffHandle), together with
a factory function, but I cannot see how this would help to make the
code clearer and more approachable. Maybe it would make the usage
more obvious from the client's perspective — yet, to be honest,
there are not ''that much clients'' to justify such a move.
Furthermore, the implementation is far from finished: for both of
the back-end lines, a fully production-strength implementation
must be created, that is able to operate in a highly concurrent
environment; very likely some lock-free messaging and hand-over
between threads will become necessary, adding to the complexity.
Thus it seems better to note the ''design critique'', but to
postpone any further refactorings to a point where we can watch
a fully developed Render Engine under production load.
doing so requires to trace a new call path from front to back,
into the `BufferMetadata` implementation: there is now an explicit check
that the buffer is in `LOCKED` state, but not yet `EMITTED` and als that
it was not yet `released()`
I think the additional effort is justified
* in the regular / currently intended usage in the Render Engine,
this predicate is not required, since compliance to the ''Buffer Provider Protocol''
as such is sufficient to ensure that
* but since all of this is getting more generic and polished now,
I could imagine future usages beyond the current confines; and then,
this semantics is what you'd expect from such a `bool()` check:
»I am allowed to use it«
These were defined in a confusing and implementation-oriented way.
Since we have now mostly abstracted away the metadata handling from the
`BufferProvider` front-end, a more succinct definition was chosen
* a ''Descriptor'' is valid when the lookup succeeds
* a ''Handle'' is likewise valid when the underlying
state-Key can be resolved, and the memory address is given
Some aspects regarding the state remain unresolved though.
Ideally we'd want a `BuffHandle` to be valid preciselc when
it ''is in valid state to be accessed''
In the past we allowed a lot of leeway when referring to our
web-interface to the Lumiera Git repositories (git.lumiera.org/gitweb)
Unfortunately this caused the AI scraper bots »out there« to capture
millions of URLs for all the different flavours of spelling them out.
And now they haunt our website and try to scrap that content again and again;
even while we effectively prevent that kind of abuse, rejecting those
ongoing attempts at content-scraping causes a dangerously high amount of traffic.
With this changeset, all cross references in the documentation section
are rewritten to the only remaining valid form of that Gitweb URL.
...while the implementation provided herein is trivial,
it represents the entrance point into the process covered by the
''Buffer Provider Protocol''; thus I went again through all the
stages of the implementation, and confirmed my analyiss from 2024:
9a23aa773b
Today I'd even go so far as to call this a **crietrion of consistency**:
The path leading to the LOCK-transition must be reproducible.
The existing implementation, now re-cast into a new API,
ensures this by requiring an explicitly given Buffer Descriptor
at that point, and ''does not offer any one-step shortcut.''
__The point to note is__: the system used for implementing those type-IDs,
which is a tree of chained hashes, is deliberately open and extensible.
''In theory'' it would be possible thus to arrive at different hash-IDs
for the same criteria, if these are added gradually, in several steps,
as a refinement. This leeway is acceptable, as long as it can not happen
that two competing allocation processes want to lock the same
memory address, but tracked through different parent type-IDs.
The `BufferProvider` is both an interface and a facade, with
standard implementations to handle the state transitions and
further steps of the ''Buffer Provider Protocol''.
These implementations have gotten somewhat out of hand, since
they directly mixed memory allocation with very elaborate usage
of the `BufferMetadata`. The goal for this series of refactorings
is to introduce two ''implementation interfaces'' -- thereby
raising the abstraction level in this »code behind«
This whole exercise is an attempt to make the whole implementation
clearer and more approachable — especially regarding the future extension
to a concurrency-hardened variant. The implementation is still backed
by a `BufferMetadata` table, and the rearranged code can be inlined.
In the old version of `BufferProvider`, there was a single
backend implementation, which was assumed to call-back into
`buildHandle(typeID, storage, localTag)` after allocating memory.
This implies, the state transition to `LOCKED` happened there.
My next goal is to clarify and abstract also the state transition API,
and this refactoring helps a lot to see the underlying call structure.
Now able to remove some obsoleted part of the old implementation.
The existing implementation of the ''Buffer Provider Protocol'' is
quite old and dated meanwhile; it was more like an explorative prototype
and mixed up implementation and test code.
At that time, notably the assumption was that the core render engine
will be implemented in plain-C and will push around `void*` all over the place.
The `BufferProvider` was meant as an adaptor, while questions of typing
were added as an afterthought. Especially some functionality to use
the `BuffHandle` in the way of a smart-ptr are completely obsoleted
by the reworked design of the Render Engine (2024).
Since the BufferStore implementation component is now supposed
to handle only the memory allocation, a complete end-to end data flow
must be ensured, so that allocation / deallocation functions get all
the relevant information. Notably, the `LocalTag` is now used as an
implementation-defined conduit for further (opaque) marker data.
Since all data passed over this API are simple values (even `LocalTag`
is effectively a 64bit integer), we can now avoid relying on output
parameters or similar side-effect tricks.
This change largely resolves the problems with the storage API,
so that the implementation can now be split in two disjoint segments.
One operation, `mark_emitted()`, is still misplaced on that API, yet
must be retained until the refactoring on the metadata side is complete
(this latter refactoring can not be started sensibly, before the old
»tracking« implementation is cleaned up to care for allocation solely).
With this change, the existing implementation, which combines the
tracking functionality and the allocation, has been squeezed down
into the heap-memory implementation class, yet the tracking functionality
is already detached. The remaining code down there is a huge pile of mess,
that can hopefully be dismantled soon. But at least all of this code
passes the compiler again, and so it should be possible now to focus
on getting the `DiagnosticBufferProvider` up and running, thereby
providing a new clean re-implementation of the diagnostic functions.
This is an important step to prepare a split of the existing implementation APIs
into a type-management part and a memory-allocation part; generally speaking,
a memory allocator needs to know the allocation size, preferably also for
releasing memory. Up to now, this information could be retrieved locally,
with a metadata lookup through the hash-ID; now we ensure that the
size is always passed alongside through the implementation API.
Incidentally, it looks like we might be able to form a common allocation-descriptor,
to represent this internal API in a more concise way. Note further that
all these changes are considered not relevant for the performance,
since work buffers are large and media computation dominates.
...just enough to be able to write the assertions in the first test case
(..and the test case fails still, due to the pending work on sorting out
the implementation APIs between `BufferStage` und `BufferStore`)
To push ahead with the refactoring,
I will build the test/diagnostics API completely anew,
and attach it solely to an instance of `DiagnosticBufferProvider`.
Unfortunately this ''leads us into chainsaw surgery...''
* it uncovers a host of conceptual contradictions
* actually, the `HeapMemProvider` was more an implementation
of diagnostic tracking, than it was an actual `BufferProvider`
implementation (hence the old name: `TrackingHeapBlockProivder`)
* and this lured me into using these tracking functions directly,
while the `DiagnosticBufferProvider` remained in unfinished state.
So basically I have now **disabled** all verification from the Unit-tests.
I'll have to rebuild them from scratch, thereby reproducing the logic/intention
- write some documentation
- identify some add-on API that must be retracted
- consider further how to disentangle ''diagnostics'' and ''implementation''
More or less forcibly squeeze all the existing implementation
into the new basic structure with two separate »pillars«
**Breakages**
- existing code expected the `BufferStore` to transition to ''locked'' state
- existing code expected `provideLockedBuffer(typeID)` to tail-call the
`buildHandle()` function of the enclosing `BufferProvider` base implementation
- various diagnostic usages bypassed the official APIs and called directly
into the implementation. This is no longer possible; rather, `DiagnosticBufferProvider`
needs to provide additional diagnostic API to suit those purposes
As a consequence of preceding experiments..
* I'll stick to the structure of the BufferProvider front-end interface
* also the »code behind« will remain there; a purely abstract API has no merit here
* however, the ''setup'' of the actual implementation will move into a derived class
* this derived scope class, `BufferProviderSetup` will house two implementation classes
* these two, now named `Stage` and `Store` provide the base implementation
for the two "pillars" of the implementation
Admitted, this structure is now much more elaborate, but it's the only path forward
I'm currently able to come up in order to disentangle the existing implementation mess...
The outward interface has stood the test of time quite well.
But the back-side towards the implementation is a tangled mess,
presumably due to being developed from prototyping code: the
actual implementation is completely overgrown by diagnostics
and test instrumentation, since running in unit test setup
was the only actual usage of this code ever since.
My first attempt with decomposing into a decorator-chain failed.
Thus the new line of attack is to introduce two implementation
pillars right from top-level
- `BufferStage` : the state and metadata handling
- `BufferStore` : the storage backend
These are defined as nested interfaces, and the further refactoring
starts with subclassing everything, nested as it is.
The intention is to build the tracking functionality from scratch,
while wrapping the old implementation as delegate. Hopefully this allows
to reduce the complexity of the existing implementation step by step,
until I'm ''able to grasp what this implementation does''.
This old implementation works, but is highly problematic code.
The Git history shows that I had already problems to understand this code
while building the DiagnosticOutputSlot, one year later.
To my defense, in both cases (2011 and 2012), I had no clear idea yet
how the processing within the engine would work, and thus I considered
that Buffer Type registry a mere implementation detail, that can be
lumped into the »code behind«.
The prototype implementations of BuferProvider and OutputSlot are confusing;
in-depth analysis shows that this is related to intermingling use
of test instrumentation and features of a prototypical default.
To get anywhere with the existing code, it seems we'd need to split off
the test instrumentation (i.e. `DiagnosticBufferProvider`) altogether,
even if this causes duplication of implementation code. Many years ago,
when first implementing this prototype, I had no clear idea what the usage
in the Render Engine entails, yet I noticed that some basic buffer »tracking«
could be used as implementation primitive to simplify the prototyping work.
This might be one of the reasons, why the "code behind" in the protocol
implementations both in `BufferProvider` and `OutputSlot` became so strongly
tangled, so that I way unable to make both protocols orthogonally composable.
The implementation code for `DiagnosticOutputProvider` and `TrackingHeapBlockProvider`
look very confusing and tangled. It seems, instrumentation for tests
got mixed up with the effort to provide a simplistic prototype implementation.
What we need foremost here is the latter; most tests that import the
DiagnosticOutputProvider do not even use any instrumentation....
`EngineCtx` is considered merely an access front-end now;
After some analysis of existing code, it seems the RenderEnvironment
is the most appropriate framework to configure the global engine services
and thus to ''boot the Render Engine''.
The access to the `EngineCtx` is tricky, because of the requirements
* it will be dependency-injected into the internals of the Render Nodes
* excessive indirection should be avioded; preferrably the actual
code in the render pipeline sees a pointer (coded as language reference)
which directly refers to the buffer and cache manager.
* but on the other hand, we want configurability; simple tests shall
access "the buffer manager" without much ado, while the actual
render engine obviously has a life cycle and needs an administrative
access to the various global implementation facilities
* this both together implies some kind of ''virtual constructor''
* and since such a pattern can not be implemented in C++ with simple
inheritance (because the base constructor does not "see" the actual
implementation type at the time when it's invoked), the solution
is to rely on a second indirection through our dependency-injection
framework: thus the setup of the actual implementation is what
can be configured flexibly, and overridden by mock implementation
for some test cases
While this topic is only tangentially related to the problems
with the »Buffer Provider Protocol«, the configuration of service instances
is relevant, insofar it provides a configuration point for unit testing.
The structure of the `EngineCtx` was created as a placeholder, to get those
issues out of the way while drafting the render node code; in its current
form it is clearly both to complicated and inflexible.
My intention is to merge the `EngineCtx::Facilities`, and to prefer
configuration of more elaborate setup by subclasses.
It seems evident that the the problems with the existing BufferProvider implementation
are rooted in a lack of understanding of the actual usage situation at that time(2011).
This caused a »hexagonal design«, i.e. something entirely driven by uint testing
for the sake of unit testing.
So to identify the unsuitable decisions, some degree of deductive work is necessary,
based on our much better understanding of the Render Engine.
A crucial challenge is that buffer management must perform well
within a massively concurrent environment, and do so without excessive latency.
This picks up the work for [ticket:1395 resolve integration of BufferProvider with DataSink (#1395)]:
Almost a year ago, before the long hiatus due to important general clean-up
and maintenance work, and the research regarding Video output —
a discrepancy was discovered between the »Buffer Provider Protocol«
and the »Output Slot Protocol«. This mismatch prevents us from building
a simple preliminary `BufferProvider` to be used for the engine integration
as part of the [ticket:1228 »Playback Vertical Slice«].
After re-establisihing the work context, I have read through the new
Render Node code built last year, and distilled the sequence of
buffer management operations that actually happen during a recursive
`pull()` call. With this kowledge, I am now able to fill in the
''standard case for rendering'' — which I was unable to formulate
during the first attempt of building the Render Engine in 2011.
__Next Steps__: Redesign of the standard implementation base of the
`BufferProvider` to prepare for a massively concurrent usage.
...some notes to my personal mindmap.
- Webserver OS upgrade
- Clean-up of services and cronjobs
- Apache configuration updated and fine-tuned
- deployed Anubis to protect against overload by AI-scraper bots