Commit graph

3248 commits

Author SHA1 Message Date
c60e7d35c6 Parameters: reconsider where to use the visitor-pattern
...turns out as better to code two distinct low-level operations,
albeit implemented by delegation to a single generic assignment implementation.

The visitor patter thus becomes relocated to the high-level interface,
allowing to assign one opaque parameter to another one
2026-07-24 19:44:20 +02:00
a37e99a643 Parameters: attempt to base all access on a single implementation
However then the problem is that we know ''our domain'' but not the ''other domain''...

... and thus this forces one direction to pass through a double dispatch
2026-07-23 01:46:27 +02:00
e48c03e35d Parameters: verify the clamping operation
- systematic testing of boundary values for various numeric types
 - verify even clamping of an uint-128 when targeting float
 - verify signed / unsigned of different rank
 - demonstrate the clamping now integrated into `extractAs()`

As an aside, move the `isNeg()` function (safe test for negative numbers)
into util.hpp and fix related bugs with unsigned there.
2026-07-22 23:10:02 +02:00
e3b6b87e93 Parameters: look for ways to properly clamp signed / unsigned
Signed and unsigned integral types have a partial overlap of definition domains,
yet due to the automatic promotion rules it is impossible to write
a domain clamping function directly, since `std::numeric_limits` returns
the domain boundaries in the given type, and `int(-1) < uint(0)`
evaluates to false, due to int being is promoted to uint.

The attempt to circumvent this problem here relies on a check if the
comparison crosses the zero point, and otherwise relies explicitly
on the promotion to the number with the larger domain.
2026-07-22 01:17:13 +02:00
11bb3bce73 Parameters: draft a family of pre-conditioning operations
- we need a generic operation to clamp to the smaller value domain
 - furthermore we need a virtual function to apply a type specific limiter

__TODO__: this draft does not handle properly signed vs. unsigned types
2026-07-21 01:34:37 +02:00
7d35790e11 Parameters: implement simple value extraction for base types
...so this is not yet a visitor, rather the building blocks required
to be able to extract values and to assign and conform values

And my intention is to build something akin to a set of conversion rules
2026-07-20 01:10:19 +02:00
82b7acac57 Parameters: pick up work on type handling experiments
...after an extended break where I focused on learning IO_URING,
which is the topic of my upcoming talk at Froscon-2026;
(I built a prototype to read and display video with async IO)

Today ''I have re-read my design notes'' and the analysis seems to hold up well;
furthermore I solved the coding problem how to generate type handlers for a list of types.
2026-07-19 22:48:50 +02:00
532408a305 Parameters: design sketch for base-domain and conversion
While it is unfortunate that I seem to get deeper and deeper into that topic...
 - it should be noted that data size matters most (pervasive use expected)
 - the design is leaning towards multiple functional closures
 - both together suggest to use a classical virtual interface, without much ado
 - yet parameters must be copyable values, to allow for evaluation graphs and snapshots
 - so this turns out as yet another use case for ''polymorphic values''
 - furthermore, fluid usage requires flexible conversions between result values
 - given the situation with numeric value types and strings, this implies ''to build a visitor''

🠲 ''putting that work aside for the moment, since I need to focus on my IO_URING research (for FrOSCon 26)''
2026-07-06 01:17:31 +02:00
5df0252cc1 Parameters: preliminary requirement analysis
In the current stage, a design-draft is required that is sufficiently adequate
to be used as code placeholder and to evolve into the full specification eventually.

 - we need a single parameter container type with value semantics
 - parameters must support a selection of basic value types, with conversions
 - the value range can be limited, and a parameter can be associated
   with a ''Scale'' (stepped, continuous, logarithmic)
 - a parameter can be linked to a functional source
 - parameters are (virtually) copyable
2026-07-03 19:55:55 +02:00
36684ff092 Parameters: start a design draft
Parameters and Automation will certainly play a crucial role eventually.
They will form a network of relationships to cover all further concerns that can not directly be conceptualised as arrangement or processing.

A shift in the setup of the Job-Functor arguments necessitates to introduce a `Parameter` type — ''albeit just as placeholder''
2026-07-01 01:50:26 +02:00
8db1813831 Invocation: adapt job parameters to requirements
This is the result of an requirement analysis,
based on the current (better) understanding of ''render job invocation''

 * the `InvocationInstanceID` is @deprecated, because rather a data based hash is required
 * instead, two free parameters are introduced, with a meaning defined by the specific `JobFunctor`
 * notably the functor for rendering, the `RenderInvocation´ can use these for the exit node and an output context

**Note**: the C-structs for the `job` are no longer considered an interface (and will be remoulded into an internal detail of the dispatcher)
2026-06-29 23:19:19 +02:00
64416e3ba8 Invocation: considerations regarding the DataSink to use
While the setup here in the test seems trivial,
this touches on some challenging questions of design:
Presumably the invocation of a Render Job will need some kind of
''Render Context'' — since the relation of the absolute nominal time
to some output frame number is strictly ''a concern of the render process''.

However, it seems prudent to keep that relation separate from the `RenderInvocation`,
so that access to all invocation parameters has to be supplied by the
Render Job Functor.

This change in the test setup now uses a `DiagnosticOutputSlot`
instead of directly allocating some buffer for output; the data
written into the `DataSink` can be investigated after-the-fact.
2026-06-28 01:41:59 +02:00
3bffc00dbc Invocation: verify (and fix) usage of the AllocationCluster
turns out that was not (yet) tested and did not even compile,
due to a missing cross-constructor to get from one data allocation
policy to another one backed by the same cluster.

- added test case to `NodeBuilder_test` to demonstrate basic usage
- use `AllocationCluster` in NodeOpera_test and verify that allocations took place
2026-06-26 19:25:25 +02:00
437ef3a4a5 Invocation: verify correct chained hash computation
Works as expected, since it was directly adapted from NodeLink_test

The preceding changeset however caused a SEGFAULT,
since the initialisation order in the helper class `RenderPipeline`
was not correct, so that the node builder used a not-yet initialised
`AllocationCluster`....
2026-06-26 01:44:53 +02:00
2ddb921e6e Invocation: create a test setup with a complex Render Node tree
In NodeLink_test, a processing pipeline was established that
works on `TestFrame` and is quite complex; here I use a slightly
simplified variant (featuring only a single port) to perform
the same kind of computations.

However, node memory allocations are managed by an `AllocationCluster`
2026-06-25 23:34:52 +02:00
70bef00324 Invocation: provide a fake engine context setup
The purpose of this test is to invoke a Render Node graph
using the full service environment as present in the Render Engine.
However, for sake of clarity, I do not want to pull up the Engine subsystem,
but rather use a local / mocked setup of all the services involved.

The node invocation is performed from within a worker thread,
to demonstrate integration with the thread-local memory pools.

The first simple test case builds and then pulls a source node.
2026-06-25 01:54:21 +02:00
f3dcbfe8b9 pick up work after the LAC-26 Maynooth
...after having settled the problems related to buffer management
and the interplay between buffers and output sinks, the ''next goal''
is to drive ahead the integration between job planning and invocation...
2026-06-24 18:27:12 +02:00
ce40b83bb6 Establish DataSink identity and fix remaining tests
The test suite is now **green** again.

The design rework and clean-up of unnecessary test-driven functionality
in the `DiagnosticBufferProvider` and `DiagnosticOutputSlot` required
to break some very old test-setup, using fake instances of the relevant
context facilities. This setup however was used by the newer family
of test support facilities, notable the `MockJob`, `MockSegmentation`
and `MockDispatcher`. These required a set of `DataSink` handles to be
present (even while they are not used currently).

This could be fixed easily now, relying on the simplified internal structure
of the `OutputSlot`: by re-arranging the types, the implied identity of the
`DataSink` handles could be made explicit, allowing for equality comparison.

__Remark__: even while `DataSink` with only a single abstracted function,
using function notation turned out to be rather confusing at usage site.
Naming this function as `lockFrame(frameNo)` clarifies its purpose nicely.
That call is effectively abstracted by means of the ''buffer type'',
relying on the VTable in the `BufferProvider` implementation.
2026-06-01 23:55:10 +02:00
b804015cd7 Attempt to fix DummyBuilderContext / DummyPlayConnection
These provide a faked context setup for unit testing.
I created them ''out of sheer desperation'' several years ago,
since it is impossible to verify the building blocks if you
don't know what the building itself will look like.

The recent design clean-up of OutputSlot has unfortunately
removed a bunch of »hexagonal hooks« — and now I seem to be unable
to hook into anything with my tests. Oh my!
2026-06-01 19:54:51 +02:00
eeabbb4f60 Fix test breakage
- some tests are notoriously flaky
- increase resource limits for the testsuite to allow stronger stress tests
- provide a "quick" variant of BufferProviderLoad_test with lowered requirements

Furthermore, investigate test scaffolding in `DummyBuilderContext`,
which was broken due to the clean-up in `DiagnosticOutputSlot`.

It seems that only some placeholder instances are required here,
so that we might be successful with just instantiating the `UnimplementedConnection`

__TODO__: need to supply slightly more implementation into the latter,
so that we're able at least to ''create'' such a connection stub.
2026-06-01 01:47:15 +02:00
216ab525fd OutputSlot: integration with buffer management (+documentation)
Within the reworked design of the `OutputSlot` a proxy implementation
of the `BufferProvider` API is used (class `OutputBufferProxy`).

After analysis of the invocation patterns, I concluded that it seems adequate
to attach the special-purpose metadata generated by this integration
to the generic system of thread-local replicated `BufferMetadata` storage.

However, since thread-local instances are effectively static(global),
I was concerned that unit-testing might be impeded by accumulation of
stale metadata in the store linked to the main thread. Thus I created
a special configuration variant to retain the previously used setup
for the sake of unit testing (which applies especially to use of the
`DiagnosticOutputSlot`). Using a `SimpleBufferStateRegistry` for in
that case has the benefit that simply a local instance can be used
in the test and all metadata will be discarded afterwards.

Furthermore the factory syntax for obtaining an `OutputSlot` was improved,
drawing on the recent changes to `lib::ScopedCollection`.

Also I looked through some of the documentation and updated
the UML sequence diagram to reflect `OutputSlot`'s improved internal structure
2026-05-31 01:44:54 +02:00
9461443e84 BufferProvider: successfully complete the load test (see #1396)
With this third test case, a fairly continuous processing
can be demonstrated, while also coordinating allocations
in a decentralised way.

The implementation behaves correct, and seemingly with
satisfactory performance -- while there is certainly
some room for improvement: with the processing pattern
in this test, a nominal computation load of 4 seconds
can be processed with 4 cores in ~ 1.3 seconds
2026-05-30 01:51:44 +02:00
fd0e62076a BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....

This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.


This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
56561c8f78 BufferProvider: implement a simulated worker load pattern
This is the meat of this test case...
- generate a ''random walk'' with buffer allocation requests
- attempt to make the load pattern nasty, since the goal of this test
  is to verify correctness of the complex interplay between local pools
  and central allocator.

''correct pattern was verified single-threaded, with print statements''
2026-05-29 04:35:44 +02:00
f868a7462a BufferProvider: establish a setup for the load test
- provide mock instances of the relevant global services
- use a base config record for the worker thread(s)
- pregenerate ''Buffer Descriptors'' commonly used by the workers
- further access to the buffer manager should then be possible
  by locking one of these descriptors...
2026-05-28 19:15:03 +02:00
9f0d7cc796 BufferProvider: investigate use of the ComputationalLoad with a memory buffer
The intention is to utilise this helper class, that was developed for the
scheduler stress testing; but this time, it should access the actual allocated
render buffers to generate the load.

This implies to use buffers of a significant size; it turned out that
the existing implementation was imprecise, insofar it always used full multiples
of the configured buffer size, which defeats the purpose of performing
a precisely calibrated number of calculation steps. As long as the buffer size
would remain comparatively small, this drift does not matter much.

Thus I changed the implementation now to ensure that exactly the calibrated
number of steps is performed, using a modulus to map it to the buffer memory.

Furthermore, I plan to use a random-walk pattern in the worker thread
to select from a collection of pre-established »buffer types«, similar
to what happens in the actual engine
2026-05-28 05:49:15 +02:00
ae7c9e7889 BufferProvider: in preparation for a load test
To verify integration of this new engine-level facility,
it seems prudent to build a load test (without going into
extended stress- and performance testing).

For that we need
- random buffer demand
- a computational load that actually uses the buffer.

As part of the ''scheduler stress testing'', a self-calibrating
`ComputationalLoad` was developed, which even implements both a
merely computational load, but also a memory accessing computation.

This can be extended to optionally use an external buffer, instead
of allocating a precisely dimensioned memory block directly from heap.
Obviously this extension is ''dangerous'', since there is no way to
verify that the storage passed in can accommodated the storage required
based on the current calibration of the `ComputationalLoad`


As an aside: identified a timing check in `SchedulerService_test`
that showed the tendency to fail when invoked immediately after a build.
2026-05-27 17:08:23 +02:00
7989e825b9 BufferProvider: integrate and verify heuristic trigger of clean-up
The mechanism is tuned now to engage rather quickly
whenever excess capacity builds up in a local allocator pool,
but when all buffers are used most of the time, they will be retained.

Can now demonstrate by integration test that capacity is sent back.
2026-05-26 04:39:41 +02:00
5a7cfb613f BufferProvider: implement heuristic to trigger pool clean-up 2026-05-25 20:24:25 +02:00
08fef0c64c BufferProvider: draft a test setup to play through usage scenarios
The goal is to develop a heuristic when to trigger clean-up of the local pool.

For developing this heuristic, I'll use a unit-test
- represent "allocations" by dummy buffer pointers
- use a queue and a stack to push those "allocations" around
- the "global pool" is just a queue with such dummy pointers
- next step is to define some λ-primitives
- finally, a λ-DSL to allow to perform "N operations of kind X"
2026-05-25 02:02:22 +02:00
37fc78b027 BufferProvider: test setup to verify heuristic clean-up
This test should confirm that such a clean-up happened,
which is not yet implemented (I have a concept draft in mind)
2026-05-24 23:38:37 +02:00
3b9641597f BufferProvider: cover effect of an immediate allocation
This is a fall-back situation,
in case the allocation ''was not announced beforehand''

A direct locked global access is used then, which is verified by this test
2026-05-24 20:02:48 +02:00
0256ad8528 BufferProvider: cover capacity anouncement in unit test
...need an elaborate test setup, with the test worker thread
and the main thread proceeding in lockstep, otherwise the
effect would not be visible. Furthermore, the diagnostic
access for LocalMemPool can be re-used here, thereby
mapping requests onto the pool for the current thread.
2026-05-24 18:44:32 +02:00
e32eec38dd BufferProvider: integrate the new race protection feature
This makes the test case pass, validating the theory.

Each `AllocReceiver` now integrates a `lib::LiveMark` instance,
and the callbacks are dispatched using the corresponding
`LiveMark::SafeHandle`, so that it is no longer possible
to dispatch allocation messages to a defunct receiver.

Furthermore, the `LiveMark` is explicitly disengaged prior
to unwinding a local pool, so that no further pending requests
can be handled ans dispatched concurrently. The local pool
sends back all the remaining allocations via async queue
to the `EngineBufferManager`

Not that such a shutdown can be triggered any time,
since an exception or failure might cause a worker thread
to unwind, thereby destroying the thread-local `LocalMemPool`
without further notice.
2026-05-24 01:38:35 +02:00
cd7581ba76 BufferProvider: investigate tricky race at thread shutdown
...even while this situation is unlikely, it needs to be considered:
A worker thread might terminate unexpectedly, due to an error;
all thread-local objects will be destroyed automatically — while the
`EngineBufferManager` might be processing further allocations requested
by that very thread, and dispatch them towards an already destroyed `LocalMemPool`

This turns out to be a nasty problem, since the whole setup was created
to allow the workers to operate independently from each other; any handshake
or registration/deregistration mechanism would require a lock of sorts,
thereby defeating the whole purpose of the setup.

The best solution I could find is to rely on a instance marker hash,
that can be checked with an atomic read prior to each dispatch of a
memory allocation. The destructor of the local pool would then be able
to ''disengage'', by invalidating that hash.

To evaluate this solution, I developed this mechanism
as a free-standing library component, called `LiveMark`
2026-05-21 01:47:11 +02:00
a4b83135a0 BufferProvider: add the bulk of the LocalBufferStore implementation
...can mostly be implemented by delegating to already developed code.

first test case runs, but behaves unexpectedly
2026-05-19 01:05:26 +02:00
c4db787b25 BufferProvider: planning of heuristics and implementation
Most relevant parts of the `LocalBufferStore` are already available
and just need to be connected. One point poses a challenge however:
when and how to trigger the heuristic clean-up of the local pool,
so that ''on average neither too much memory is wasted, nor
to much unnecessary allocations'' are performed...

Signed-off-by: Ichthyostega <prg@ichthyostega.de>
2026-05-17 01:46:17 +02:00
7448b81d49 Library: improve initial population of ScopedCollection
This is a collection with unique-ownership,
and the ability to create all content objects in ''RAII style''
from the constructor call, using a ''population functor''.

It is often used to maintain a collection of resource handles,
and especially to start a series of threads (for testing).

With the variadic λ-captures from C++20, it is possible to rewrite
the most frequently used population functors to be easier to use.
Notably we can now bind constructor arguments, that will be passed
to each of the created elements — a prominent example being the
thread function when launching several threads. Furthermore,
I added a constructor variant that consumes a Lumiera Iterator.
Adding this kind of generic variants is possible now with C++20,
since overload resolution takes concepts into account.
2026-05-14 22:46:06 +02:00
3a491855e7 Library: introduce a concept for the »Lumiera Forward Iterator«
Well, finally.

Lumiera Iterators were conceived as a concept from the very beginning,
and this idea dates back even before C++11. Furthermore, over time some
''trait templates'' have been developed, to detect if some template argument
might match the known pattern of functions associated with a Lumiera Iterator.

Meanwhile this concept is used pervasively in the whole code base, and also
the traits templates proved to be reliable. It therefore seems that the time is ripe
to gradually introduce a genuine **concept** `lum_iter`

I'll take a defensive approach here, since any mistake would lead to
a massive breakage of compilation. The new concept is defined simply
by delegating to to the tried-and-tested trait template, which is
also renamed `can_IterForEach` ⟼ `can_LumieraIter`

One immediate benefit is that the compiler is now able to select
overloads based on that concept, which allows to drop the tricky
selection of variants based on the SFINAE technique and `enable_if`.

For validation, one very heavily used case of that pattern was switched
in `lib/format-util.hpp', which notably lies in the code-path used by `util::join`
2026-05-14 16:32:54 +02:00
d94bfe64b5 BufferProvider: validate (and fix) concurrent access
The test performs a massively concurrent scenario
that presses with 100 threads and 1 MiB allocation for each.

This highlighted my still limited understanding of the situation
and leads to a much better solution, where, on-demand, some worker
performs all the accrued allocation and management work.

So effectively we do not even need to use the synchronous API
2026-05-13 23:18:06 +02:00
0567633bd9 BufferProvider: add implementation for the asynchronous hand-over
...and define a test case, to perform the typical usage sequence
within a lot of threads in parallel.
2026-05-13 01:47:51 +02:00
76542e873a BufferProvider: decide how to introduce locking
I prefer to favour a small number of locks at API level.
See https://issues.lumiera.org/ticket/1429#comment:1
2026-05-13 01:20:52 +02:00
5a47b099be BufferProvider: find a solution for processing allocations
Implementing the `EngineBufferManager` the desired way,
with the ability to post allocation requests asynchronously,
raises the difficult question of who will ultimately perform
the allocation / deallocation and further management work.

The point is that this management effort takes up capacity
originally assigned for the render workers themselves, either
by implicitly pushing aside those threads, or as a dedicated
processing assigned to some worker.

In the current stage of development it seems impossible to decide
upon the proper mechanism to schedule this excess work explicitly.

The solution proposed hereby however circumvents this dilemma,
insofar it allows to implement the synchronous, fallback processing
in terms of the asynchronous processing, just performed exclusively
within the current thread. Unless some processing will be scheduled
explicitly (which can be added as performance optimisation later on),
the handling of a direct allocation request will ''fold down'' into
the current worker eventually, thereby using it's processing capability
without the need of any explicit scheduling.
2026-05-12 23:46:55 +02:00
9835e837c5 BufferProvider: in search for a viable implementation structure
While all of this aggregate can be considered ''deep implementation'',
the structure of dependencies bears danger to get confusing. Even more so,
since an elaborate pooling memory allocator will be added to and interconnected
with this structure.

Now considering the following route:
- `engine-buffer-allocator.hpp` has elements of a common service / interface
- but `engine-buffer-allocator.cpp `is also the most deeply nested implementation backend
- `local-buffer-store.hpp` is a junction point
- `local-mem-pool.hpp` is considered an insolated implementation detail
- ultimately, this will be included and wired in `render-enviroment.cpp`
2026-05-09 17:27:31 +02:00
bf446626a8 BufferProvider: draft for the basic global allocator interface
This implementation and the accompanying test might seem pointless,
yet the point is that the interface is aligned to the emerging
concepts for memory allocators in Lumiera, while also directly
delegating to the std::allocator for the time being.

Before we're able to observe the actual allocator trend and behaviour,
it is pretty much moot to engage into any kind of optimisation;
the generic heap allocator performs quite well on recent C++ runtimes,
and, furthermore, the most important performance tweak is to use
a thread-local pool of recent allocations, which should allow
our jobs to run without impeding each other.
2026-04-28 00:59:50 +02:00
76dbb980b5 BufferProvider: more testing and refinements of the heuristic
Seems to work now roughly as expected, so that the next layers
of functionality can be built on top. It seems impossible
to determine the viability of this whole approach without
taking the full buffer memory management into use with
a real-world load scenario.
2026-04-27 21:26:50 +02:00
077ef891ca BufferProvider: testing of heuristics... 2026-04-27 01:52:21 +02:00
5fafb16807 BufferProvider: decide to use only a simple heuristics for reservation
At the begin of each Render Node `pull()`, the render job is expected
to ''announce'' a maximum expected allocation requirement to the `BufferProvider`,
which information is passed-on to the `LocalMemPool` to determine
to which degree it can be satisfied with allocations from that local pool.

Without actual empiric observation, it is impossible to determine
the relevance of this step, how it should be resolved heuristically
against the known contents of the pool, and how much effort to spend
on that step.

Thus I'll settle for the ''simplest thing that can possibly work'' —
which is to check only for close matches and rather rely on additional
allocations, forwarded asynchronously, assuming that the clean-up
heuristics will deal with excess allocations quickly.
2026-04-27 00:25:10 +02:00
b59cb12974 BufferProvider: devise a heuristic for buffer selection
- take into account how much memory will be wasted when
  using some allocation to satisfy a (smaller) allocation request
- furthermore prefer allocations used frequently in the past
- when an allocation is used, tag it with a score that also
  reflects this degree of waste
- select buffers with low score first for clean-up
2026-04-26 01:36:35 +02:00
dd37600c4d BufferProvider: setup implementation structure for the pool
- use boost::lockfree to implement the in-queue with new allocations
- use a std::list with an internal administrative record
- automatically ingest the in-queue prior to accessing the pool
2026-04-25 01:41:49 +02:00