Compare commits

...

145 commits

Author SHA1 Message Date
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
ab22f3766c MERGE dev/play into integration
Integrate the work related to buffer management in the render engine.
 * in-depth analysis and rework of the `BufferProvider` framework (see #1395)
 * build a prototype for production-grade buffer allocation handling (closes: #1415)(see #1396)
 * as an aside: Application code and namespace reorganisation
2026-05-31 22:54:12 +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
0204438147 BufferProvider: consider how to setup and wire the LocalMemPool
Basically the setup should be symmetrical to the `BufferStage` setup,
which implies to install the `LocalMemPool` as a thread-local »slice«.

However, in addition we need a mechanism to trigger an automatic clean-up
of the local pool whenever a worker-thread terminates, with the additional
complication that thread-local variables are effectively static.

Since `LocalMemPool` is written such as to abstract from the setup environment,
we need to inject a shutdown function from the setup within `LocalSlice`,
thereby directly accessing the `EngineBufferManager` via dependency-injection.
2026-05-18 19:05:32 +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
7c44cac3df BufferProvider: setup communication structure for passing allocations
- use a base class with the lock-free queue as building block
- both `LocalMemPool` and `EngineBufferManager` are built on top,
  allowing them to exchange `Alloc` entries asynchronously
- ''Allocation requests'' include a receiver callback,
  so that a newly allocated memory block can be dispatched
2026-05-10 01:41:16 +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
24bb03f510 Workflow-publish: include (and rename) images
Images were extracted form the PDF provided by Wouter with the command

pdfimages -png -j wouter260316v4.pdf wouter/v4

As usual, I have added the images separately, into the Website-repository,
in order to save storage space in the main Lumiera repo (since storage in
a Git-repository is permanent and can not be shrinked later without
rewriting the history)

Furthermore, I have changed the naming scheme of the images,
adding a prefix with the version number where some image was added.
2026-05-09 17:26:40 +02:00
5a904eaf2c Workflow-publish: adapt new chapter to Asciidoc 2026-05-01 01:31:27 +02:00
Wouter Verwijlen
5a2dd77bcc Workflow-discussion: Lumiera Workflow Proposals (draft v4)
...added new chapter 1: working with source material

===========================================================
Remark from Ichthyo(committer):

Converted from PDF to raw text with

    pdftotext wouter260316v4.pdf WorkflowProposals.txt

Images extracted with:

    pdfimages -png -j wouter260316v4.pdf wouter/v4

Again adding the images separately to the Website-repository,
in order to save storage space in the main repository....
2026-05-01 00:31:14 +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
e328018b05 BufferProvider: decision to use a free-standing pool with lock-free queue
After several fruitless attempts, I have decided now
to settle for self-contained ''memory pool'' component,
that can be operated **thread-locally** and employs a
**lock-free** message queue for the allotment of memory.

Seemingly the question of how this relates to `BufferMetadata`
must remain unresolved for the time being, until the entire
buffer memory subsystem is available for empirical investigation.
2026-04-24 22:55:25 +02:00
e277913e22 BufferProvider: start planning for the memory handling part
As staring point, it is clear that the `BufferStore` side of the
implementation will rely on a similar structure with a thread-local
base service — which in this case will have to maintain a small pool
of recently used buffer allocations...
2026-04-21 23:30:35 +02:00
3b7877204d BufferProvider: implement and verify the second case for synchronisation
...which happens when a buffer type is referred for the first time
from within a new worker thread, while it is already stored in the
global registry; the full key-chain is down-synced then.
2026-04-20 19:48:20 +02:00
abecd2cbde BufferProvider: implement locking and data synchronisation
...using the first usage situation as guideline,
and verify proper operation through unit-test

- the LocalBufferStage gets a set of thread-local BufferMetadata
- furthermore, it accesses the central hub via dependency-injection
- after each type registration ''from a worker thread'' data is published

As an asside: ensure most metadata registration can ''move'' the TypeHandler
2026-04-20 01:40:15 +02:00
606d98bf9c BufferProvider: establish a new buffer type from within a worker
This test setup defines one of the problematic situations
where a synchronisation is necessary.

__TODO__: actually add locking and that synchronisation to LocalBufferStage
2026-04-19 19:25:30 +02:00
57cd51e3a2 BufferProvider: draft setup for engine-wide coordination hub
What I am building step by step right now is the first draft
of a production-grade render buffer management scheme.
For this I need suitable implementations of `BufferStag` (and `BufferStore`)
These will include a `LocalSlice` to hold thread-local »satelitte services«,
which in turn need a connection to some central coordination hub.

The thread-local instances can access the hub via our `lib::Depend` (I have just
verified that this works as intended, with on-demand creation of the
central instance on first access from some thread. This is important,
since very likely we need to install a dedicated and speficically wired
service instance, which must happen prior to first dependency-request
via `lib::Depend`. And since the worker-threads are created by the Engine,
we get ample opportunity to do so before starting those threads.

Included in this changeset is a first rough draft of the central registry
`EngineBufferMetadata` — I have modelled it analogous to the `SimpleBufferStateRegistry`,
which is the test-level implementation of the `BufferStage` API.
However, we do not want to expose direct buffer allocation operations
on the central hub, and thus I have left out the respecive parts of the API.

Furthermore, I have added an `abandon()` operation, without there beeing
a need for such an operation right now. But since this operation is tricky
and potentially dangerous, I prefer implementing it the correct way,
since I have a clear understanding what this entails at the moment.
2026-04-19 18:02:58 +02:00
1601838037 BufferProvider: reduce synchronisation effort
The recursive synchronisation up the parent-chain can in most cases be skipped,
by relying on the fact that inconsistent data is never stored in the BufferMetadata
table. If thus an entry is locally present and matches the entry from the
other side, then it can be concluded that its parent must also be already in sync.
2026-04-19 15:14:31 +02:00
c05a73d68a BufferProvider: draft mechanism for buffer metadata synchronisation
Since the intention is to run several thread-local instances of `BufferMetadata`
in a concurrent setup, we need a way to publish and exchange chains of
registered type-keys and metadata entries.

The first step is to develop a mechanic for data exchange,
and to define the rules when such synchronisation is (not) allowed.
2026-04-19 01:21:24 +02:00
9bd3e550dc BufferProvider: improve configuration of thread-local dispatch
Since thread_local implies a static setup, we need a way how to
build a new service instance. Usually this would be a default construction,
yet there might be services with special init requirements.

Using an explicit template parameter for this setup configuration
allows to make the primary template parameter just the service itself,
which makes the default usage much clearer.
2026-04-18 16:25:14 +02:00
a06ef95e94 BufferProvider: skeleton for thread-local service dispatch
- Use a ''monostate'' as access point to thread-local service instances
 - rely on a configuration-context to setup the actual service
 - investigate runtime overhead using a multithreaded microbenchmark
2026-04-18 01:10:27 +02:00
8d563d1ea9 BufferProvider: lift the family-ID to the BufferStage level
During the last days, I started with analysis to find a way
to operate the `BufferProvider`, as implemented thus far,
within a massively concurrent environment.

Since I intend to create thread-local caches,
the initialisation of the `BufferMetadata` store must be static.

It turns out that the root-ID, which acts like a mandator ID,
is only used at a single code path, when creating or accessing
an abstract type-Key. It is thus easy to extract and relocate
this distinguishing ID into the actual `BufferStage` implementation.
2026-04-17 19:08:46 +02:00
4dc6885665 BufferProvider: complete the clean-up effort
Looking through all my work notes, it seems that all the
problematic aspects are now solved in a satisfactory way.
 * it is clear how to dependency-inject a `BufferProvider`
 * the internals of the `BufferProvider` front-End have been restructured
 * there are now two ''pillars'' of the implementation: `BufferStage` and `BufferStore`
 * both can be subclassed independently
 * `BufferProviderSetup` exposes a proxying-mechanism (used for `DiagnosticBufferProvider`)
 * the `BufferMetadata` have been reviewed and some minor details were rectified
 * after a re-Design of `OutputSlot`, `BuffHandle` can now be used as unified front-End
 * both `DiagnosticBufferProvider` and `DiagnosticOutputSlot` have been rewritten
 * they are now independent, self-contained specialisations, layered on top

Overall, what might seem like a detour, was an important step towards
well-balanced structures and a better understanding of the Render Engine.
2026-04-14 01:22:55 +02:00
44b3dbe682 BufferProvider: investigate and fix broken NodeDevel_test
The actual mystery, that could ''not'' be clarified,
is why that test did pass previously. Because it was
always broken, insofar it pretended that the output buffer
does hold a valid test frame, while in fact the buffer is
just newly allocated.

The deeper reason behind this mismatch lies in the concept of
''in-place processing'' — which is a very obvious, nullary optimisation,
and thus we want our engine to be ''in-place capable'' in the long run.
Which explains why I did write the dummy-test operations with in-place
processing in mind. Yet the actual version of the render engine leaves
out a lot of future improvements, amongst others the in-place processing.

This implies that — at least at the moment — it is clearly the responsibility
of the processing function to prepare the output buffer, in cases where such
preparation is necessary (and this applies in the case of `TestFrame`,
which needs to be primed with a valid header mark)

This change touches on another unsolved problem in the `TestDomainOntology`:
we need a mechanism to allow the generic builder front-end to set
specific configuration for some kind of processing. Generally speaking,
such things are possible in C++, yet somewhat tricky — and thus I postponed
that aspect and just included a configuration variant, hard-coded for the moment.
2026-04-13 01:08:48 +02:00
ce5f7d5e06 BufferProvider: new reworked implementation validated and complete (closes: #1410)
After an extended round of analysis and cascading reworks,
the long-standing discrepancy between `OutputSlot` and the
''Buffer Provider Protocol'' (#1395) has finally been resolved.
The foundation for this success was laid by means of a restructured
implementation within `BufferProvider` – which has now been
successfully completed and validated.
2026-04-12 23:17:53 +02:00
5686694999 BufferProvider: remove rest of the old implementation
__note__: regarding hash-standard.hpp see Ticket #1422
Mostly this hack worked -- but it is very likely not needed any more.
Today I had to re-wire a partial specialisation for hashing pointers,
that was added somewhere in the past into the STL.
2026-04-12 19:19:32 +02:00
7652ca05cd BufferProvider: add simplistic heap-based BufferStore implementation
Now able to reap the rewards: since all that convoluted and tangled
old ''state tracking'' has been resolved and replaced by dedicated
and self-contained diagnostic subclasses, the old dummy BufferProvider
implementation can be detached, using a similar yet very simple
fake implementation.

For context: the `HeapMemBufferStore` is part of the default configuration
and thus automatically used whenever a unit test needs a `BufferProvider`.
This default implementation is not optimised for speed, and actually
never releases any memory, which allows to verify proper handling
of buffer memory allocation.
2026-04-12 01:31:33 +02:00
2c8ee05a23 OutputSlot: validate and complete redesign (see #1395)
''This is a breakthrough...''
Using the realigned implementation structure in `BufferProvider`,
and especially the ability to establish an overarching context by
passing an connection-ID through the `LocalTag` in the `BufferMetadata`,
it was possible to resolve the longstanding mismatch buried deep within
the `OutputSlot` implementation.

Based on the new internal APIs in `BufferProvider`, it was thus possible
to setup an `OututBufferProxy` that acts as a hub to connect the
`DataSink` handles with the actual `OutputSlot::Connection` implementation.
This change allowed to rectify the convoluted control structure in
`OutputSlot` and finally to manage the Lifecycle properly. Furthermore,
one level of unnecessary implementation-inheritance could be removed,
so that it is no longer necessary to subclass `OutputSlot` in order to
configure the link to a specific output backend.

Using this redesigned internal structure, it was now fairly simple to
re-implement a tracking `DiagnosticOutputSlot` and then to verify proper
integration by passing the `OutputSlotProtocol_test` again.
2026-04-11 01:45:15 +02:00
a0d83b4fe8 Library: investigate design for an opaque inline functor
This is a design study to find out what would be required
to build an opaque function wrapper, similar to `std::function`,
yet without using any heap memory ever.

Such a building block would allow for some interesting options
for a well structured design of elaborate data structures.

This changeset provides an implementation draft.

__Two important findings to note__:
 * it is impossible to get away with just a single function pointer
 * because at the very least we need to invoke the destructor of the
   embedded delegate. Thus something akin to a "manager" is required.
 * this implies that the component would not be so efficient as I hoped,
   since it requires two pointers and requires to emit code for each
   type of delegate.
 * by making the component **NonCopyable**, I avoided the complexities
   of virtual copy support. In case the latter is desired, it would be
   much simpler to switch to a classical OO approach with a VTable
   and use our `PolymorphicValue` wrapper to handle copy and cloning.
2026-04-08 01:53:53 +02:00
0760525ea5 Library: deep dive into type reconciliation and tuple handling
This commit is the aggregated result of extended research
that is only loosely related to current development topics.

One theme was the construction of nested data structures using tuples,
which faces some difficulties due to tuple manipulations being limited
almost entirely to compile-time processing. However, the barrier between
compile-time and runtime can be overcome with the help of a dispatcher
table -- which leads to the secondary problem to handle the diverse
tuple elements with a common, generic functor. Such a functor can be
constructed, if and only if those element types are ''reconcilable''

The STDLIB provides two metafunctions, `std::common_type` and
`std::common_refernce`, which however pose some challenges for the
task at hand, since they include additional processing. In the end,
a custom version of the ''basic type reconciliation'' was implemented
and tested, and then used as foundation to build a dispatcher table
for runtime-indexed access and iteration over tuples.

As an aside, several deeply hidden bugs in our library code were resolved,
and the tuple-helpers, tuple string rendering and associated utilities
were regrouped and modernised.

As result of this research work, the following components are provided:
- meta/common-result
- meta/fun-trampoline
- meta/tuple-index-adaptor
2026-04-07 06:32:23 +02:00
a835a796b2 Library: investigate further ramifications and clean-up Itertools
The detailed analysis of corner cases indicated a lot of hidden dangers
when stretching the `IterExplorer` design beyond its limits.

By deliberate choice, this design was kept open so that advanced cohesive
implementation structures can be built. It is even possible ''and tolerated''
to rely on ''references'' to some degree -- assuming however that the
referred memory location remains stable.

As it turned out, these basic assumptions can be broken by a combination
of read-ahead processing (as is necessary for grouping and filtering),
together with adaptor objects embedded as value, especially when
the resulting pipeline object is then moved or copied during evaluation.

While my general attitude towards such extremely clever implementation
constructs is "don't shoot yourself into your foot unless you need to" --
it seems prudent to clean-up some crappy code from the first attempts
towards an iterator pipeline -- notably some of the »Itertools«.
2026-04-01 01:49:36 +02:00
40d3167d1a Library: integrate automatic adapters to flatten iterable sources
This expands the processing scheme developed thus far, so that
an arbitrary nested iterable can be handled, as long as it can be
adapted by the generic mechanisms provided by IterExplorer.

Notably there is now a type pattern match to distinguish various cases,
even spanning the ''rather esoteric'' possibility that an underlying
iterator produces STL collections by-value.

By examining these special cases, the limitations of the `IteratorExplorer`
design were highlighted, and a number of tricky bugs could be fixed.
2026-03-28 23:27:23 +01:00
d3407094be Library: prepare two wrapper-helpers to cover corner cases
Since the new `flatten()` operation was implemented in the `IterExplorer` framework,
some non-obvious corner cases must be considered; notably the source iterator
might yield something that is not by itself a Lumiera Iterator, yet could be
adapted by applying those generic mechanisms already in place to handle such cases;
however there is the special twist in the case at hand that we do not have to adapt
the ''source iterator'', yet rather the ''product'' of the source iterator —
a requirement that, once understood, can be easily adopted with
yet another iterator-decorator layered below.
2026-03-28 18:09:24 +01:00
00098dd815 Library: develop a flattening primitive for IterExplorer
This is a feature I wasn’t sure for a long time how best to tackle.
It’s worth noting that an initial attempt at `IterExplorer` was based
on the monad pattern, which turned out to be a mistake, since monads
are more of an anti-pattern, than being genuinely useful.

The actual use-case that made me reconsider such a feature it
to build an seamlessly chained sequence from several iterators.

It would be simple to develop a suitable utility as a stand-alone helper,
yet it seemed worth the effort to integrate this into the `IterExplorer`
ecosystem, due to the flexible automatic adaptation to iterable source
pipelines provided therein.

Going this route incurred some cost however, unveiling several defects
related to handling nested iterable types in the generic processing framework.
2026-03-27 23:32:55 +01:00
0326f9e85d Library: further clean-up of type definitions related to iterators
More documentation and occasional rearrangements made as an aside of
debugging issues around handling of Lumiera iterators vs. STL iterators...
2026-03-26 23:39:02 +01:00
f05402c04e Library: a stricter criterion for Lumiera iterators
On occasion of the recent improvements, I noticed that somewhere
in the past I had misunderstood the meaning of the typedefs in iterators:
 * `reference` is the central definition; this is what the iterator yields
 * `value_type` is secondary; this is the type you use to store results

So at times I defined only a `value_type`, wrongly assuming this to be
the primary definition, and all the other ones to be ephemeral.

Consequently the trait to detect a »Lumiera Forward Iterator« should be sharpened...

...which unveils a long-standing bug in our ''Duck Detector''
(it is not possible to define a pointer to a reference, causing
a check for the `reference` typedef erroneously to fail.
2026-03-26 19:08:54 +01:00
fbda9baeb3 Library: streamline value-type bindings
- use uniform naming scheme
- extract type-binding from RangeIter into a trait-template
- use the C++20 iterator concept and trait to detect a STL iterator
2026-03-26 00:46:43 +01:00
68460d4900 Library: a subtle fix to iterator value-type deduction
After some investigation, I seem to understand the problem better now.
This builds upon the Introduction of our `ValueTraits´ two years ago:

e176e54004

The root cause to these ongoing sporadic problems with type deduction
seems to be that we encounter a **clash of concepts**: What Lumiera calls
an »Iterator« is essentially different to the order of terms used by the STL.
In the STL, an iterator can be seen as an ''abstracted pointer'', and is
thus only meaningful in conjunction with a backing container. It is also
this backing STL container that provides the value-type definitions.

In Lumiera however, an ''Iterator is a promise to retrieve a sequence of evaluations''.
For that reason, the Iterator itself provides value-type definitions,
and the notion of "iterable" can largely be conflated with "Lumiera Iterator".

Now, the `lib::RangeIter` can act as a bridge between both usage patterns,
and thus it is possible to create a `RangeIter` both from a Lumiera Iterator
or from a STL container. (As an aside, this is a similar idea as promoted
by the "Range TS" in C++20 -- yet the Lumiera iterator concept is much more
abstract and allows evaluations like tree expansion and search algorithms,
which would be never possible with the STL based notion of iteration)...

Thus the value type binding in `RangeIter` is the only correct place
where to mitigate this ''clash of concepts:''
 * if the `IT` parameter is a Lumiera iterator, we need to retrieve
   the type bindings defined therein
 * otherwise, we can assume that `IT` is an STL iterator and thus
   essentially a pointer, and we must strip that pointer and work from there.
2026-03-25 21:07:26 +01:00
bf77e53b4c BufferProvider: obvious convenience function
...to emplace some object into the buffer space.
I wouldn't expect that to be necessary for the real usage,
but it comes in handy for tests, where we often want to place
some sentinel value into a buffer.
2026-03-24 23:48:13 +01:00
a7c3a8d80d Testsuite: fix breakage
- visible Signature of TestFrame changed due to being relocated into the Vault

- attempt to make the WorkForce_test more robust;
  frequently fails on testsuite run after full build;
  so better ''wait'' for the workers to have started working,
  instead of using a fixed delay (that must be extremely short
  and is hard to predict)
2026-03-24 23:19:05 +01:00
54c2a30f54 OutputSlot: fill in the tracking implementation
Straight-forward:
 - add a map keyed by observed frame numbers
 - individual connection needs to know its own seqNr :-(
 - also need to store and recall the frameNr in the buffer-Index

Fix bug with DataSink constructor
(value constructor shadows copy constructor => Stackoverflow)

''Complete construction and wiring sequence works as intended''
2026-03-24 23:19:05 +01:00
ec9b73d418 OutputSlot: skeleton for a lightweight new tracking implementation
- can now throw away the old convoluted `TrackingInMemoryBlockSequence`
- instead use a simplistic implementation of the Connection interface
- do not perform any kind of validation, allocate new buffer for each frame
- use the `NaiveBufferSetup` (`BufferProvider`) as implementation backend.
2026-03-24 00:48:07 +01:00
bd09d9fbbb OutputSlot: draft new setup for the DiagnosticOutputSlot
This will be the first ''reality test'' for the new `OutputSlot` internal design:
Is it possible to implement a tracking test implementation in a clean
and straight-forward way, avoiding all the tangling and indirection
that plagued the old prototype implementation from 2012?
2026-03-23 23:44:44 +01:00
c87bc7fe27 OutputSlot: complete connection of redesigned implementation (see #1415)
After resolving the inner tensions in the old prototype from 2012,
it was now possible to resolve all issues in a single strike
 * `DataSink` becomes a functor, yet embeds a shared ref-count
 * in the λ-closure a ''specific Buffer Descriptor'' is prepared for each feed
 * building upon the prototyping work, a dedicated `OutputBufferProxy`
   exposes a `BuffHandle` to the client, with `emit()` and `release()` calls
 * the implementation ''delegates directly'' to the `OutputSlot::Connection`
 * an this is where all actual output technology is encapsulated
2026-03-23 00:31:12 +01:00
f29ec44fda OutputSlot: setup a DataSink wired to the proxy-BufferProvider
...this is the gordian λ

Note
 - embedding a copy of the shared-ptr in the closure
 - pre-configure a buffer descriptor (buffer type)
   * with the buffer size as retrieved from the connection
   * and pass the connection itself marked as LocalTag

Thus, on invocation of the DataSink (functor), it suffices
to invoke the lock() function on the BufferDescriptor,
using the new additional argument introduced with the
preceding commit
2026-03-22 17:36:38 +01:00
eeedb7a028 OutputSlot: recast the proxy BufferProvider to use the Connection directly
Reaping the benefits of the preceding refactorings, most of the complex
wiring within `OutputBufferProxy` can be omitted, and the proxy calls
can be delegated directly to the actual `Connection`

There is a catch however: we need to create an additional pathway
to feed the frame-number information into the `BufferStore` backend.
2026-03-22 00:44:02 +01:00
7bae52d1f8 OutputSlot: forge a path towards a clean delegation structure
The solution adopted here is the result of extensive reflection.
Taken literally, or naively, the reworked architecture would imply
to route every call through a delegation-chain that is dispatched
in the state-managing "PImpl" (`AllocState<CON>`).

However, in keeping with the idea of an opaque functor embedded into
`DataSink`, direct and explicitly typed calls can be bound in the
associated closure. Doing so will strengthen the role of the
interface `Connection` — which defines the boundary to the
actual domain of output technology.
2026-03-21 23:15:51 +01:00
473299fb5a OutputSlot: attempt to address deeply rooted structural problems
This changeset attempts to breach into the implementation design of OutputSlot
(while largely retaining the API and the »Output Slot Protocol«).

While the actual problem that brought me to this point can be traced back to some
rather obvious mistake, namely to conflate the `BufferProvider` used as
backing implementation with a `BufferProvider` used as front-end API,
creating the new connections to use an `OutputBufferProxy` unveils some
further long-standing structural mismatch; notably this leads to the
difficulties to get the lifecylce management of `OutputSlot` and `DataSink`
under control.

So I remove one entire level from the implementation,
in a bold move to realign the design with the prospective usage pattern.

Obviously, this breaks a lot of further code, and I need to rewire
the Allocation implementation altogether, and provide a new functor based
implementation for `DataSink`
2026-03-21 00:51:40 +01:00
0438571b73 OutputSlot: setup a dedicated proxy-provider structure
Now the »pathfinder« prototype, the `BufferProxyProvider` turns out as a good starting point;
just can simplify a lot and omit the outer capsule.
2026-03-19 00:48:19 +01:00
2ee6bb3b04 OutputSlot: look for alternative ways to implement the proxy
After a lot of pondering over various solution schemes,
it seems that `BufferProxyAdaptor` is too rigid for the task at hand.
It could be retained as a technology demonstration, and used as blueprint
for a custom-made implementation. In a nutshell, we need a `BufferStore`
that directly delegates to the respective `Connection` — which also implies
to use the `OutputSlot::AllocState` as an intermediate to hendle that delegation.
2026-03-19 00:32:02 +01:00
9b6933b572 OutputSlot: consider how to integrate the proxy provider
...which still looks like a nasty task
 * the good part is, we have a working `BufferProxyAdaptor` now
 * yet this setup does not seem to fit into the `OutputSlot::AllocState`

As an aside, several test definitions need to be relocated to the Vault,
since the whole compound around `BufferProvider` migrated down
2026-03-17 00:48:08 +01:00
ea0ae5f78e BufferProvider: propagate the actual buffer-size info
This is a nasty new requirement, discovered as an aside while looking
into ways to resolve the ''crucial mismatch'' between the `OutputSlot`
and `BufferProvider`, using the proxy-implementation of `BufferProvider`
(thereby expanding on the recent draft with the `BufferProxyAdaptor`)

The point is, if the BufferMetadata will be used beyond the internal coordination
within BufferProvider, then it will be used as part of actual buffer storage.
And this implies that the actual size of the allocation may differ (be greater)
than the nominally required size (since we'll very likely employ some kind
of memory tiling, for reasons of performance).

This leads again to an analysis of `BufferMetadata` usage,
with the conclusion to add the actual storage size directly into the API,
but treat it as an ''extra'' and not integrated into the ''buffer type''

Phew ... quite complex alltogether
2026-03-16 23:45:24 +01:00
94320d2d23 BufferProvider: remove the lock-onlyNew feature
This might have seemed like a good idea, adding an additional ''consistency check''...
However — we ''do have'' a generic check, insofar `BufferMetadata` is a **state engine**.

As long as we're able to prevent, reliably, that the same buffer address
ends up being classified as a different buffer type, this should be enough
to prevent double-use of any buffer (letting aside that `BufferProvider` is
backed by an implementing memory allocator, which in itself should prevent
any double-use.

Removing this feature resolves some ambiguity in the overall design,
and allows to turn `BufferMetadata::lock(type, address)` into a private
implementation function.

In the old code, there were some external access paths directly entering
into that implementation function, which caused a lot of confusion (at least for me...)
2026-03-16 20:37:24 +01:00
7463228d38 BufferProvider: reorganise internal BufferStore API
This refacotring prepares for the planned changes in `OutputSlot`
 * introduce a new compound parameter (tuple) BufferAlloc
 * wire this through all calls involved with storage handling

__Background__:
 * `BufferMetadata` deemed important for the implementation of buffer storage
 * ''new requirement detected'': must be able to handle an actual storage block
   that is ''larger'' than the requested buffer size (⟼ tiling)
 * need compact and uniform info, also for the `BufferProxyAdaptor` callbacks
2026-03-16 00:41:16 +01:00
e0d6362bad BufferProvider: document the adapted structure in UML
There was an old UML diagram created at the time of the first draft.
It is integrated into the »OutputSlot« page in TiddlyWiki. This description
and the diagram are highly relevant for understanding the design in this case.

Unfortunately, Umbrello was "improved" with a lot of features since I used it
for the last time, but the **quality** of the software has been **degraded**,
it is now buggy and the layout is imprecise and gets messed up easily.
2026-03-14 20:44:27 +01:00
9be75c9305 DOC: Migrate to Umbrello version 4:25.04.2-1
__Warning__: the old diagrams look ''slighly off alignment'',
this is very likely due to some change in font handling.

I have set the default font now to 'DejaVu Sans Condensed' 9pt
2026-03-14 19:06:00 +01:00
0178e11e65 BufferProvider: simplify OutputSlot implementation structure
The old implementation relied on two layered APIs:
one for the client and one for the implementer of
the Allocation state. This is logically sound, yet
clumsy in terms of design, since the internal API
turns out to be void of actual meaning.

It seems possible to eliminate this confusing structure
by a clear separation of ''implementation realms''
 * the ''mechanics'' of connection state management
 * the actual implementation of the connection

The first can be provided as generic template subclass,
while the latter should be confined entirely into the
implementation of the `Connection` interface.

Doing so requires to keep each part of the implementation
strictly focused onto its purpose — something I obviously
was not able to see at the time when writing this old
implementation, notably because I was completely guided
and driven by the test/dummy implementation without
considering how an actual implementation of
low-level data output would work.
2026-03-14 03:36:15 +01:00
d91ca67a27 BufferProvider: remove the problematic mismatched API
This API function was the reason why the handling of
`OutputSlot` and `BufferProvider` do not mesh up well,
so that the former could not yet be integrated properly
into the engine.

''Obviously the proper solution still needs to be implemented''
2026-03-13 18:07:28 +01:00
705bd3d2e0 BufferProvider: rename OutputSlot implementation parts
The state of connectedness is represented as class,
which actually is a three step deep hierarchy.
This design seems plausible, since we get
 - an interface visible for clients (`Allocation`)
 - an internal interface
 - the actual implementation, templated to the actual connection

At least we can try to choose names that better indicate
these three related levels.
2026-03-11 17:52:04 +01:00
259d0a5728 BufferProvider: now able to relocate OutputSlot into Vault Layer
The old implementation of `DiagnosticOutputSlot` is now disabled
and will be re-written from scratch, and no longer tangled to `BufferProvider`

 - `OutputManager` remains in Steam-Layer for now
 - the `Timings` and `OutputSlot` are now a self-contained
2026-03-10 23:12:30 +01:00
4fa18cf1d2 BufferProvider: re-assessment of OutputSlot API requirements
One of the goals of this extended clean-up and refactoring is to allow
access to the data buffers of a `DataSink` through a `BuffHandle` as front-end.
Which essentially means that the ''Buffer Provider Protocol'' and the
''Output Slot Protocol'' mesh together well.

After the clean-up of the `BufferProvider` implementation hierarchy,
the viability of this idea was demonstrated with the `BufferProxyAdapter` draft;
so it is now roughly clear what must be changed within the existing implementation
of `OutputSlot`. More specifically, the only actual implementation ever completed
was a dummy implementation for test (`DiagnosticOutputSlot`), which hooked
confusingly into the corresponding dummy implementation of `BufferProvider`,
that was written one year earlier (and was also without actual implementation
at that time).

So the second half of the task ahead seems very similar to the first part:
 - the base implementation of the ''Output Slot Protocol'' seems roughly acceptable
 - but the actual dummy implementation was somewhat confused and should be
   re-written from scratch, without ''any attempt to cut corners by re-using other dummy code''


__Note__ as part of this analysis I discovered some gap in the design
regarding timeout handling ⟶ Ticket #1417
2026-03-10 19:57:41 +01:00
7dee16ba90 BufferProvider: relocate into Vault Layer
building upon all the preparations done during the last days,
the compound of code connected to the `BufferProvide` can now
be migrated down into the vault:
 - `BuffHandle`
 - `BufferProvider`
 - `BufferMetadata`
 - `BufferProviderSetup`
 - the simple standard implementations
 - `DiagnosticBufferProvider`
 - all directly related unit tests
2026-03-08 00:53:41 +01:00
994a4cf557 BufferProvider: provide basic type definitions through buffhandle.hpp
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`
2026-03-07 23:42:35 +01:00
cc9dbd8a40 BufferProvider: relocate TestFrame and dummy-hash computations
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
2026-03-07 23:02:35 +01:00
3caa5cfb03 Tests: reorganise test folders
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
2026-03-07 00:56:19 +01:00
9c74466778 Tests: build an link a test-support library
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.
2026-03-06 23:16:32 +01:00
35f946d8d9 DOC: minor wording improvements 2026-03-04 12:52:04 +01:00
9187812472 MERGE: Application code and namespace reorganisation
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`.
2026-03-04 02:59:57 +01:00
8e250839f4 Application: Adjustments to the Layer structure
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.
2026-03-03 22:16:09 +01:00
ea627c1c7a BufferProvider: document the new design and use it for the prototype
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`
2026-02-27 03:18:43 +01:00
8437710232 BufferProvider: research how to build a policy with callbacks
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.
2026-02-26 02:20:51 +01:00
77217d9785 BufferProvider: bold solution for setup of the policy-baseclass
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.
2026-02-23 16:04:26 +01:00
02ba977c61 BufferProvider: experiment for callback configuration
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
2026-02-23 00:05:44 +01:00
5a203513c9 Library: ensure automatic re-seeding for TestFrame
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!
2026-02-23 00:05:44 +01:00
cfdb659ccb BufferProvider: prototyping for a proxy-BufferProvider
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
2026-02-22 00:30:45 +01:00
722c12e312 BufferProvider: assess status and consider approach to OutputSlot (see #1395)
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
2026-02-20 00:21:29 +01:00
6c880c5d10 BufferProvider: look through further aspects of the API
* 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
2026-02-19 00:46:40 +01:00
8a8f98ceb0 BufferProvider: confirm valid Buffer Protocol usage cycle
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
2026-02-18 01:34:33 +01:00
064651335f BufferProvider: completely restore DiagnosticBufferProvider_test
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.
2026-02-17 23:56:23 +01:00
191667197d BufferProvider: reinstate simple test case
* can now replace old verification code by new diagnostic functions
 * test case runs without failure

''UNRESOLVED:'' failure in verification of memory contents
2026-02-17 20:00:28 +01:00
2ca763bce3 BufferProvider: simplify tracking information -- first successful test
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.
2026-02-17 01:46:44 +01:00
945636c446 BufferProvider: connect the first wire-tap to record locked state
...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...
2026-02-17 00:41:08 +01:00
43c4f7a518 BufferProvider: a proxy implementation mechanism for BufferProvider internals
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
2026-02-16 23:12:22 +01:00
6de1837042 BufferProvider: now able to push down BufferMetadata
This is clearly implementation code and can now be removed from the interfaces;
`SimpleBufferStateRegistry` can thus delegated to a private metadata registry
2026-02-16 22:38:23 +01:00
3d6e2fea75 BufferProvider: reconsider and rearrange the public API
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.
2026-02-16 20:50:04 +01:00
75913725a8 BufferProvider: sharpen criterion for bool(BuffHandle)
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«
2026-02-16 01:44:38 +01:00
c0e2d3b70e BufferProvider: reorder validity handle validity checks
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''
2026-02-16 01:00:12 +01:00
7d237647d4 DOC: normalise towards a single form of gitweb-URL
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.
2026-02-15 23:02:52 +01:00
1230cbae12 BufferProvider: re-think through the process of Key creation
...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.
2026-02-15 22:26:39 +01:00
3738026945 BufferProvider: re-cast implementation in terms of the new API
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«
2026-02-15 02:48:53 +01:00
6f14046c0c BufferProvider: draft mapping of Stage API
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.
2026-02-15 01:52:56 +01:00
5518030a7d BufferProvider: rearrange types to prepare for a BufferStage interface
...still in pursuit of the plan to split the implementation
of the `BufferProvider` in a ''state tracking'' part and a
''storage managment'' part...
2026-02-15 01:52:46 +01:00
9b66818bda BufferProvider: inline the buildHandle function to simplify calls
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.
2026-02-14 03:44:01 +01:00
70e32a3e66 BufferProvider: clean-up types
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).
2026-02-13 21:27:45 +01:00
c3ebade5d2 BufferProvider: investigate and fix broken allocation
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.
2026-02-13 03:17:11 +01:00
75cda5ebc6 BufferProvider: consolidate information passed to BufferStore
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.
2026-02-13 03:12:08 +01:00
925ee0f27e BufferProvider: expand BuffStore API to provide buffer size info
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.
2026-02-12 23:26:16 +01:00
cd415c05aa BufferProvider: implement first strike of diagnostics accessors
...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`)
2026-02-12 20:22:11 +01:00
e10aa751bb BufferProvider: design a new diagnostics API
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
2026-02-12 05:29:03 +01:00
ace20660aa BufferProvider: analyse the existing API
- write some documentation
 - identify some add-on API that must be retracted
 - consider further how to disentangle ''diagnostics'' and ''implementation''
2026-02-12 05:06:13 +01:00
9c5eb11b0c BufferProvider: switch to the new implementation structure
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
2026-02-11 02:13:43 +01:00
a9bdad2c66 BufferProvider: draft a new layout for the implementation
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...
2026-02-10 21:11:02 +01:00
c21ddc1844 BufferProvider: looking for ways to disentangle the implementation (see #1410)
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.
2026-02-10 00:41:41 +01:00
bb6401aaf3 BufferProvider: reshape diagnostics into a decorator
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«.
2026-02-09 03:29:06 +01:00
1ebefbe36e BufferProvider: identify usages of the default BufferProvider
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.
2026-02-09 00:59:35 +01:00
256c446705 BufferProvider: consider to disentangle diagnostics and default implementation
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....
2026-02-09 00:59:30 +01:00
0f12b19148 BufferProvider: devise a better structure for Engine-context access
`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
2026-02-08 01:22:35 +01:00
559bb17c17 BufferProvider: aiming at a simplified engine configuration
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.
2026-02-07 23:45:52 +01:00
d00992ff70 BufferProvider: start with some requirement analysis
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.
2026-01-25 02:19:30 +01:00
3fe6d5f54c BufferProvider: define standard usage as test case
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.
2026-01-24 15:13:16 +01:00
1986850fa3 Infrastructure: Webserver upgrade and maintenance completed
...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
2026-01-23 03:45:52 +01:00
b22ffdacb6 Infrastructure: evaluate Anubis -- document setup 2026-01-13 02:34:27 +01:00
1f0b8085e3 Infrastructure: document simple webserver load monitoring 2026-01-11 02:30:17 +01:00
9df85992ee Infrastructure: upgrade and document the Lumiera Trac 2026-01-08 23:16:29 +01:00
1266427d68 DOC: Intro -- clearer wording
The paragraph that describes the pull processing seems slightly confusing,
notably the follow-up processing is done by child nodes, not siblings.
I attempted to use a more descriptive language
2026-01-06 17:17:45 +01:00
8220981e1b DOC: amend a silly mistake
This was a classical ''collaborative f***up''.
 - in 2018, `cehteh` wrote a infrastructure config summary
 - late in 2025, `ichthyo` merged various documentation content
   and placed that piece of information into the technical docu.

Unfortunately, the Baidu search engine has picked that info up,
and in the last week of December 2025, some stray bots started
to »visit« the staging area.
2025-12-28 23:24:42 +01:00
5289e43066 Some notes regarding the next topic: buffer management 2025-12-17 21:49:53 +01:00
734 changed files with 67030 additions and 11647 deletions

4
README
View file

@ -61,8 +61,8 @@ For building Lumiera, you'll need:
* https://gtkmm.gnome.org/en/[gtkmm]
* https://cgit.freedesktop.org/xorg/lib/libXv[libXv]
* https://wiki.gnome.org/Projects/LibRsvg[lib rSVG]
* https://git.lumiera.org/?p=debian/gdl;a=summary[lib GDL]
* https://git.lumiera.org/?p=debian/gdlmm;a=summary[GDL-mm bindings]
* https://git.lumiera.org/gitweb?p=debian/gdl;a=summary[lib GDL]
* https://git.lumiera.org/gitweb?p=debian/gdlmm;a=summary[GDL-mm bindings]
Further information:

View file

@ -97,6 +97,8 @@ def configure(env):
problems.append('We need boost::lexical_cast')
if not conf.CheckCXXHeader('boost/format.hpp'):
problems.append('We need boost::format (header).')
if not conf.CheckCXXHeader('boost/lockfree/queue.hpp'):
problems.append('We need boost::lockfree (header).')
if not conf.CheckLibWithHeader('boost_program_options','boost/program_options.hpp','C++'):
problems.append('We need boost::program_options (including binary lib for linking).')

View file

@ -1,42 +1,47 @@
The Application : Start-up and Subsystems
=========================================
Application : Start-up and Subsystems
=====================================
:Date: 2009
:Author: Ichthyo
//MENU: label Start-up
.the purpose of »Application-main«
Lumiera is envisioned as a heavyweight yet coherent »**Application**« -- not so much
as platform, framework or operating system. And, in accordance with this focus, we
place no emphasis on possible configuration changes and state transitions within
the running application _as a whole._ And, while in fact it is possible to close
and load a new Session, most of the time we assume the application just to be
``up and running'' -- all required services to be available and interfaces to
be accessible. Without doubt, these are simplifications, but serve us well
to cut down complexity -- yet still there needs to be one dedicated realm
to deal with these specific concerns of configuration, dependency and lifecyle.
Lumiera is envisaged as a heavyweight yet coherent »**Application**« -- which implies
it is not so much seen as ``a platform'', framework or operating system. And, in line
with this understanding, we place no emphasis on possible configuration changes and state
transitions within the running application _as a whole._ The ability to react ``life'' to
some global setting changes can be limited to relevant special cases. And, while it is
indeed possible to close and load a new Session from a running application instance, in
most parts of the code base we assume the application just to be ``up and running'' -- all
required services are assumed to be available and interfaces to be accessible. If some of
these assumptions break, an exception is raised and the application unwinds.
Without doubt, these decisions represent a forcible simplification, and might be limiting
at times, yet serve us well to cut down complexity in large parts of the code base --
however, there needs to be _one dedicated realm_ to deal with all these specific concerns
related to configuration, interdependency and lifecyle.
The Application Realm
---------------------
So we treat all these concerns within a small and self contained structure, clearly
set apart from all the other layers, services and subsystems. This dedicated
_Application Realm_ is organised around the ``Application main object''.footnote:[
This is the singleton `lumiera::AppState`, which is triggered by the `main` function
of the Lumiera Application. The sourcecode is kept in a separate folder 'src/common'
and linked into the shared library 'liblumieracommon.so']
It serves the purpose of pulling up and tearing down the application in a controlled
fashion. And additionally, it provides the Interface and Config core services.
The act of building or tearing down this core realm and main object is what creates
the *Lifecycle* of the application. This is a succession of ``lifecycle phases'' --
and almost all activities happen within the _operational phase,_ when everything
is ``up and running'' or ``just available''.
The Vessel
----------
So we treat all these concerns within a focused and self contained structure, called
the »**Vessel**«, and clearly set apart from all the other layers, services and subsystems.
This dedicated _Application Realm_ is organised around the ``Application main object''.footnote:[
This is the singleton `vessel::Voyage`, which is triggered by the `main` function
of the Lumiera Application. The sourcecode is kept in a separate folder 'src/vessel'
and linked into the shared library 'liblumieravessel.so']
Its purpose is to bootstrap and tear-down the application in a controlled way,
which includes to provide the backbone services, evaluate the start-up parameters
and to connect and start the necessary core components. The beginning and end of
this vessel's voyage therefore marks the *Lifecycle* of the application, as a
succession of ``lifecycle phases'' -- where almost any activity happens within
the _operational phase,_ when everything is considered ``up and running''.
Subsystems
~~~~~~~~~~
However, initially the application needs to be brought up, and at the end, all
parts need to be unwound cleanly. To organise this process, we identify a limited
number of *Subsystems* within the Application, which are more or less independent.
However, before becoming operational, the application must be bootstrapped, and at the end,
all parts need to be unwound cleanly. To organise this process, we identify a limited number
of *Subsystems* within the Application, which are self-sufficient and operate independently,
at least to some degree.
Each link:{ldoc}/design/architecture/Subsystems.html[Subsystem] is self contained
and groups some other parts and services, which typically work together and may
be mutually dependent. These subsystems represent a grouping, applied for the purpose
@ -61,7 +66,7 @@ the startup phase. It is the responsibility of the _Application main object_ to
pull up required functionality, which in turn might result in pulling up
further subsystems as dependencies.
Subsystems are defined by implementing the interface `lumiera::Subsys`, which acts
Subsystems are defined by implementing the interface `vessel::Subsys`, which acts
as façade to conduct the lifecycle, find out about dependencies and shut down
the subsystem in the end. So this interface, together with the _Subsystem Runner,_
define a lifecycle protocol; each subsystem is free to implement this as it
@ -97,20 +102,21 @@ within the engine independently, relying on worker jobs and a thread pool.
Initialisation and Lifecycle
----------------------------
After some discussion,footnote:[See the
link:{rfc}/GlobalInitialization.html[GlobalInitialisation] RfC
from spring 2008. In the beginning, we all agreed to ``keep matters simple''
and build an `init()` function within one central piece of code everyone knows
and hooks into. However, while the outline of the application emerged, there
was a tension between the concern about _over-engineering_ versus the concern
about _tangled and unmanageable complexity._ At some point, an alternative
implementation based on lifecycle callbacks was elaborated, which then turned
into the solution described here. Lumiera then ceased to be the typical UI
application started by GTK, and the existing GTK code was retrofitted to
launch from within a plug-in.]
After some discussion,footnote:[
See the link:{rfc}/GlobalInitialization.html[GlobalInitialisation] RfC
from spring 2008. In the beginning, we all agreed to ``keep matters simple''
and build an `init()` function within one central piece of code everyone knows
and hooks into. However, while the outline of the application emerged, there
was a tension between the concern about _over-engineering_ versus the concern
about _tangled and unmanageable complexity._ At some point, an alternative
implementation based on lifecycle callbacks was elaborated, which then turned
into the solution described here. At that point, Lumiera ceased to be the
typical UI based application started-up by GTK-main, and the existing GTK
code was retrofitted to be launched from within a plug-in.
]
the design leaned toward loosely coupled parts and a formal lifecycle; which
saves us from investigating and coding up the various interdependencies
explicitly. Rather, the parts of the application have to comply to
explicitly. Rather, all parts of the application have to comply to
link:{ldoc}/design/architecture/Subsystems.html#lifecycle[Lifecycle Phases],
and each part has to care for its own state transitions, invoked through
_lifecycle callbacks._ We can distinguish two distinct models how to deal
@ -128,9 +134,9 @@ Application Start
~~~~~~~~~~~~~~~~~
* some fundamental language-level facilities will be prepared during
_static initialisation._ At some point, execution enters `main(argc,arvv)`.
* `AppState::init()` brings up the plugin loader and opens the config-interface.
* `Voyage::init()` brings up the plugin loader and opens the config-interface.
* ...followed by triggering *ON_GLOBAL_INIT*
* the main thread then pulls up the subsystems (`AppState::maybeStart(subsystem)`),
* the main thread then pulls up the subsystems (`Voyage::maybeStart(subsystem)`),
according to the command line options.
* within each subsystem, façade interfaces will be opened through the
interface/plug-in system.
@ -142,7 +148,7 @@ Application Start
the main window.
* there is an *ON_GLOBAL_SHUTDOWN* event, which can be used for normal cleanup;
In case of an emergency exit, the *ON_EMERGENCY_EXIT* event is triggered alternatively.
* the AppState destructor tears down the core systems (config-interface and pluginloader).
* the Voyage destructor tears down the core systems (config-interface and pluginloader).
* we establish a policy to _prohibit any non-local and non-trivial activities_ during the
tear-down phase after leaving the `main()` function.

View file

@ -1,15 +1,71 @@
Lumiera Design: Application and Session Configuration
=====================================================
Application and Session Configuration
=====================================
*TODO* : 'write design document'
.After initial discussion, this topic was postponed.
[CAUTION]
In its current form, as of 2026, the Lumiera application is headed towards a first
integration of the parts developed thus far. A simple configuration file, 'setup.ini'
is used to supply some minimal parameters for bootstrap of the application; all further
settings are defined as local constants in the source code.
The Lumiera application uses two quite different sources for configuration
**Configuration and Parametrisation** of software describe a state of setting, that lies
between the fixed structure embodied into the code and the transient work state of the
application. It encompasses all settings and options that determine how a software system
behaves in a specific environment, without changing its underlying source code.
- individual __settings values__ can be loaded from an application- and user configuration in the conventional way
- various facilities, especially in the Steam-Layer, base the actual behavour on __queries__, which are
to be resolve employing a rules based system '(planned)'. Configuration rules will be provided by the
application (defaults), a session template and rules stored in the actual session.
The purpose of configuration
----------------------------
For a video editing application, the scope of ``configuration'' can be far reaching.
Not only are there numerous parameters in the engine, that must be fine-tuned for optimal
performance, but also several patterns of behaviour are available that can not be decided by
first principle alone and must thus be left open for the user to configure to their preference.
Some of these are linked to the actual user, some to the setup in the system, and some to the
current project.
Furthermore, many of these ``settings'' turn out, upon closer inspection, to be a form of
rule-based knowledge. In a project with the scale of a movie, it is essential for a fluid
and intuitive working style to handle some default setup and configuration of connections
and conversions automatically, while the user can have very valid reasons to prohibit
some other specific class of automatic conversions to be applied without explicit confirmation.
-> see also the link:rfc/ConfigLoaderDraft2008.html[Config Loader brainstorming from 2008] (implementation details)
After some initial, prototyping for a ``config loader'', it was deemed too risky to engage
into any kind of preliminary design without knowing the possible ramifications. +
-> link:/x/rfc/ConfigLoaderDraft2008.html[Config Loader brainstorming from 2008]
The following preliminary measures have now been established for the ongoing development:
- whenever a _tweaking parameter_ is discovered, it will be extracted and marked
with a **language constant** in the setup or integration header of the software component.
- whenever a _decision point_ is encountered, that can not be related to technical dependencies,
it is marked as a **query call** -- with a corresponding, hard-wired _faked configuration rule_.
The integration of some kind of configuration rule language is considered part of the primary agenda.
Requirements
------------
To track any insights gained regarding the requirements for configuration,
https://issues.lumiera.org/ticket/1224[Ticket #1224 (»Layered Configuration«)]
was added to the list of medium-term
https://issues.lumiera.org/report/17[»Focus Topics«]
The following requirements have been identified to date (2026)
- It is pointless to build one huge flat global "configuration file";
rather, groups of configuration settings are scoped and closely related to a _local context_
- the degree of _consistency checking_ must be tied to such a local context, where the
situation is reasonably clear: the consumer of specific configuration usually has
clear-cut requirements to demand some configuration to be mandatory, while other
configuration is optional. Misspelled or misplaced configuration that does not
fit in such a category must be flagged as error.
- such a locally defined set of expected and optional configuration might become
subject to versioning (forward- / backward compatibility)
- Configuration must be assembled from several configuration sources,
with clearly defined precedence and shadowing. Notably we have default configuration,
system-wide configuration, user specific configuration and project related configuration.
- a _preferences UI_ must be able to alter configuration settings, thereby intelligently
routing the changes into the appropriate configuration source or create a suitable overlay
- when configuration causes a decision from a rule to flip, this fact must be recorded
permanently in the session; the reason for this mechanism is that configuration might
change later, and the application must be able then to point out all places
affected by the change.
- Any detected configuration change must be captured and logged as Event and thus
be un-doable cleanly, with all possible ripple effects.

View file

@ -1,10 +1,48 @@
Design Documents: Application Framework
=======================================
The Application Realm
=====================
// Menu : prepend child ApplicationStart
* link:Config.html[Configuration]
* link:plugin_loader.html[Plugin Loader]
* link:ApplicationStart.html[Application Subsystems and Lifecycle]
Shaping the outside world through software requires a variety of effective mechanisms
that capture, represent, and transform elements of reality. A well-designed part is
focused on a single aspect, is coherent and cohesive in itself, with links to other
parts based on concepts and abstraction rather then on shared technology. Yet this
structural perspective must be complemented by a meaningful overall arrangement that
supports the desired workflow and can be adapted to circumstances.
In software systems, such coherency does not emerge spontaneously. It must be supported
by definition work to establish suitable terms and interfaces, components must be
interconnected and configured, and the overall state needs coordination.
An application is therefore more than just a collection of software components.
In the Lumiera Architecture, this application realm is embodied into a structure
called the »**Vessel**«:
- it is built as a library with only basic linkage to the support library
- it can be invoked from a `main()` function, with an argument parser
- at source code level, it provides all the types and definitions
required to express the _binding_ between components
- _conceptually,_ all layers are encased in the _application vessel_
- and while the Vessel can not just ``control'' the configuration of parts
on upper layers, it has to bootstrap their ability to adapt to circumstances.
--
&nbsp;
--
Dependencies::
While the code base is organised into _three layers,_ the actual implementation
structures are arranged into **Subsystems** that _depend_ on each other and
can be _started and shut down_
+
-> link:ApplicationStart.html[Subsystems and Lifecycle]
Configuration::
As a means to adapt to the environment and enable flexibility,
this topic is surprisingly intricate and development has barely scratched
the surface: while the effective configuration is assembled from several
layered sources, the meaning (and documentation) of individual settings
is scoped, and users can expect the ability to override some settings
from within the application.
+
-> link:Config.html[Configuration]

View file

@ -1,5 +0,0 @@
Lumiera Design: Plugin system
=============================
Eventually, this will have design documentation for the Plugin system.

View file

@ -237,7 +237,7 @@ Lifecycle Events
The Application as a whole conducts a well defined lifecycle; whenever transitioning to the next phase,
a _Lifecycle Event_ is issued. Components may register a notification hook with the central _Lifecycle Manager_
(see 'include/lifecycle.h') to be invoked whenever a specific event is emitted. The process of registration
can be simplified by planting a static variable of type `lumiera::LifecycleHook`.
can be simplified by planting a static variable of type `vessel::LifecycleHook`.
WARNING: A callback enrolled this way needs to be callable at the respective point in the lifecycle,
otherwise the application will crash.
@ -246,7 +246,7 @@ WARNING: A callback enrolled this way needs to be callable at the respective poi
Invoked as early as possible, somewhere in the static initialisation phase prior to entering `main()`.
In order to install a callback hook for this event, the client must plant a static variable somewhere.
`AppState`::
`Voyage`::
This is the Lumiera »Application Object«. It is a singleton, and should be used by `main()` solely.
While not a lifecycle event in itself, it serves to bring up some very fundamental application services:
+
@ -255,7 +255,7 @@ WARNING: A callback enrolled this way needs to be callable at the respective poi
- the application configuration
--
+
After starting those services within the `AppState::init()` function,
After starting those services within the `Voyage::init()` function,
the event `ON_GLOBAL_INIT` is emitted.
`ON_GLOBAL_INIT`::

View file

@ -0,0 +1,19 @@
Extending the Application
=========================
During the inception phase, the link:/x/rfc/AllPluginInterfacesAreC.html[idea was commended]
to bootstrap the application as a multi-lingual compound of plug-ins, for maximum flexibility
and ease of contribution. Although this approach garnered much support initially, the ensuing
discussions questioned its viability, with well-known plugin-based systems and frameworks
cited as cautionary example.
A prototype implementation
(-> link:PluginBrainstorm.html[draft for the plugin interface])
was nevertheless developed, but it proved problematic in practical use.
The goal of a flexibly expandable application is still maintained,
but the path to achieving it should be based on actual requirements.
Development of an https://issues.lumiera.org/ticket/1212[Extension Concept #1212]
was thus placed on the list of medium-term
https://issues.lumiera.org/report/17[»Focus Topics«]

View file

@ -7,22 +7,23 @@ Lumiera Design Documents
// Menu : prepend child design/gui
// Menu : prepend child architecture
// Menu : append child plugins
// Menu : append child extension
// Menu : append child workflow
// Menu : append child governance
Lumiera is to be a professional tool for video editing on
Lumiera aspires to be a professional tool for video editing on
GNU/Linux systems. The vision of the development team defines a modern design
for the core of a Non-Linear Editing software. A key aspect of Lumiera
is the strong separation between the user interface and the processing core.
We are developing Lumiera with a GTK GUI but that will in no way be exclusive;
other GUIs can be written, as well as scripts to drive the core.
We are developing Lumiera with a general purpose GTK UI -- which is loaded as
an optional component, so that the core can also be invoked from scripts to
use the editing and processing capabilities of the engine.
This becomes possible by an ongoing effort to decrease coupling. Each major
component in the application strives to be open to extensions, but closed
against external modification. The presentation, the model and the ``performing''
of the model are separate and self-contained parts of the application. Lumiera
as a processing core will be able to perform any conceivable
task on video and audio, maybe even tasks entirely unrelated to video editing.
of the model are separate and self-contained parts of the application.
Lumiera as a processing core will be able to perform any conceivable task
on video and audio, maybe even tasks entirely unrelated to video editing.
.Workflow
*****************
@ -52,7 +53,8 @@ specialised GUIs. The interface is the component that is closest to the user. I
provides purely visual interaction with the user. Within this work environment, the user
manipulates, organizes, loads, configures all sorts of data, especially MObjects (media
objects) and Assets. These elements are contained within a structure called the Session. +
-> link:gui/index.html[GUI/Stage-Layer design documents]
-> link:gui/index.html[GUI/Stage-Layer design documents] +
-> link:{ldoc}/technical/stage/index.html[GUI technical documentation]
Steam: the Transformation Layer
@ -87,24 +89,23 @@ the screen or to external output. +
Extra Components
----------------
The Application
~~~~~~~~~~~~~~~
The application controls the initialization and shutdown procedures as well as
loading external elements such as plug-ins, which are widely used throughout
Lumiera. It acts as the framework which supports core component operations. This
framework is complemented by a library of commonly used components, algorithms and data structures. +
-> link:application/index.html[Application framework]
The Vessel
~~~~~~~~~~
A structure that is anchored below all layers, yet contains everything, conceptually.
Definitions for building common interfaces are located here, as is the mechanism for initialisation
and shutdown of the application, the loading of bootstrap configuration and optional components.
It encloses and coordinates all parts to work together as a coherent whole.
This -> link:application/index.html[Application framework] is complemented by
a link:{ldoc}/technical/library/index.html[Support-Lbrary] with commonly used components,
algorithms and data structures.
link:plugins/index.html[Plugins]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Plug-ins play an important role within Lumiera since the application relies
heavily on current FLOSS implementations and external libraries. Moreover, the
application will be configurable and can be extended in various
ways; whenever some extension isn't permanent or will be used only on demand, it
is packaged as a separate module into a plug-in. For example, the GUI of Lumiera
is a plugin. +
-> design documents regarding the link:plugins/index.html[Plugins]
Extensions
~~~~~~~~~~
Lumiera uses proven and field-tested implementations for the actual media processing,
which are available as FLOSS or open-source libraries. To avoid extensive external dependencies
on the main application however, such libraries need to be loaded through _adaptor plug-ins._
Furthermore, the application will be configurable and it is conceivable that external
extensions will be created. Designing and building a future-proof framework for configuration
and extension is an important medium-term goal for further development.

View file

@ -1,14 +1,30 @@
Design Documents: Vault
=======================
Low-level Services
==================
What follows is a summary of Lumiera's *Data Handling Backend*
Towards the _upper side_ of the Lumiera core, in »Steam Layer«,
several components and subsystems work together to provide functionality
that is close to the requirements of the _problem domain_ -- editing and
rendering of media. The implementation of these features however requires
a foundation of generic, technical services: Memory needs to be allocated,
working buffers must be provided and managed, work tasks coordinated.
It is hard to associate these responsibilities directly to
some specific domain, other than just _data processing._
This is the foundation layer responsible for any high performance or high volume
data access. Within Lumiera, there are two main kinds of data handling:
A common trait of all those special aspects of processing is that they
require special care and attention to achieve the necessary level of
performance, while they are _technical_ in nature and do not directly
reflect and model structures of the real world.
Data Handling
-------------
The task of editing video and processing media requires to read and write
a significant amount of data, while using the capabilities of the hardware
to the best degree possible. Lumiera thus needs a foundation layer for
high volume data access; two kinds of data handling can be distinguished:
* The Session and the object models manipulated through the GUI are kept in memory.
They are backed by a _storage backend,_ which provides database-like storage and
especially logging, replaying and ``Undo'' of all ongoing modifications..
They will be backed by a _storage backend,_ which provides database-like storage
and especially logging, and controlled execution of _mutating actions._
* Media data is handled _frame wise_ -- prefetching data asynchronously.
The goal is to optimize CPU utilization; large scale data IO will be performed

View file

@ -1,4 +0,0 @@
Design Documents: Plug-In
=========================
* link:PluginBrainstorm.html[early draft for the plugin interface]

View file

@ -1,16 +1,15 @@
Lumiera Workflow Proposals
==========================
:Author: Wouter Verwijlen
:Date: 19 August 2025
:Date: 16 March 2026
:TOC:
I would like to share a first version of a collection of workflow ideas for Lumiera. These ideas come
from having cut hundreds (or possibly thousands) of videos over the past 20 years as a professional
editor, working in Avid Media Composer, Adobe Premiere Pro (from before it was called "Pro"),
Lightworks and DaVinci Resolve.
I would like to share a collection of workflow ideas for Lumiera. These ideas come from
having cut hundreds (or possibly thousands) of videos over the past 20 years as a professional
editor, working in Avid Media Composer, Adobe Premiere, Lightworks and DaVinci Resolve.
This document focuses on analysing the behaviour of Non-linear Editors (NLE's), in other words
software in which a user can edit films and videos, in order to discuss possible solutions for the
This document focuses on analysing the behaviour of Non-linear Editors (NLE's): software
in which a user can edit films and videos, in order to discuss possible solutions for the
design of Lumiera.
The main ideas behind these proposals are:
@ -178,14 +177,354 @@ specific project in order to select the parts needed for the final edit. I will
which different NLE's handle this and will propose possible directions for Lumiera to support
certain workflows.
[red]#To be written:#
Project creation
~~~~~~~~~~~~~~~~
When starting a new project, a user often needs to set the project resolution and framerate. Adobe
Premiere (optionally) shows an import panel immediately after this. The first time you drop a clip
on a timeline, Resolve and Premiere will ask if you want to change your project settings to match
the clip settings, when these are different.
.Premiere's import panel
image::{imgg}/wouter/v4-01-importPanel.jpg[width="100%", alt="Premiere's import panel"]
Projects themselves exist within an application's database (Resolve), or as a file/folder on local or
shared storage (Avid, Premiere, Lightworks, FCP).
One of the first things that needs to be setup is the organisational structure of the project, in other
words: the structure that's going to house all of the project's contents. Most applications use bins for
this, a bin being simply a container for items like video clips. In a sense they are similar to folders
on a computer's storage, except bins usually only exist within the project in your NLE. Avid's bins
also exist as folders on local or shared storage, but this is an exception.
For top level organisation, bins can either be placed in other bins (Premiere, Resolve) or in
groups/folders (Avid, Lightworks).
As we'll see throughout this document, Final Cut Pro is usually the only application that takes a
different approach. Projects are called ``libraries'' and FCP sorts footage based on events (top level
containers of timelines, footage and keyword collections), keywords (tags) and collections, instead
of bins (timelines are confusingly called ``projects''). Footage, when imported, is placed in an event
and from there all emphasis is on the metadata, in how you rate and tag (parts of) the footage. This
also serves as a replacement of the subclips feature found in other NLE's.
.Final Cut Pro's Keywords dialog. +
image::{imgg}/wouter/v4-02-keywords.jpg[width="100%", alt="Final Cut Pro's Keywords dialog"]
^↑``Travel part 1'' and ``Travel part 2'' on the left are events.^
In any case, when working in a professional environment, you will probably want to start with a
template that defines your top level structure, instead of having to recreate your entire bin setup
for every project. Surprisingly, at the time of writing only Adobe Premiere has introduced a project
template feature (in version 2024). In other apps you will need to manually create your own template project
and duplicate that, or even literally copy and paste bins from a template project in a file browser (Avid).
Importing footage
~~~~~~~~~~~~~~~~~
Every application works with clips: virtual representations of (parts of) the video files that exist on
your preferred type of storage. Getting footage into your project (``ingest'') will create these clips in
your project. Most apps will offer several options to do this:
. Creating links to the original video files, wherever these may reside (on your computer,
external storage, network storage or in the cloud).
. Copying your files to a dedicated folder managed by the application (this could again be on
local or network storage). Importing to a dedicated application folder is very useful for
feature film editing and TV production, but is less common elsewhere in the media industry.
Its strong point is that when your media is managed by the application, you won't have to
re-link files when moving to a different editing station.
. Optionally, files can be transcoded during import. New media files will be created in either
the app's dedicated folder or to a user specified location. The transcoded files will be much
more optimised for editing, resulting in better playback performance.
The latter is part of what is known as the *offline-online* workflow, in which you work with
optimised files, and once an edit is finished (``picture lock''), the clips on the timeline
will be exchanged with clips that link to the original camera files. Often, after colour grading
in an external application, all colour graded shots are rendered with their colour corrections
and placed back onto the timeline in the NLE.
A specific type of offline-online workflow is the *proxy workflow*, that allows switching
on-the-fly between the proxy files and the original camera files and will ensure that exports
of your video will use these original camera files when rendering. When using a proxy workflow,
you can either create optimised files inside the application itself (or in Premiere's case, via
Adobe Media Encoder) or attach existing proxy files that were created by a third party application
or by the camera during recording.
After importing media and possibly creating/attaching proxies, clip attributes might need altering.
You might need to change incorrectly determined frame rates or, more commonly, change the audio
channel mapping of your clips (e.g. setup clips with two audio channels as dual mono instead of
stereo).
Clip types
^^^^^^^^^^
We usually find the following types of clips in NLE's:
Source clips:: they represent media files (containing video and/or audio streams) to its full
extent, and for other types of media (most notably still images) -- these usually get a default
duration upon import.
Subclips:: they represent a portion of a video or audio file.
Timeline clips:: duplicates of source clips that only exist within a timeline. Properties of
these clips can often be altered without altering the same properties of the source clip.
Compound clips/nested sequences:: these are in fact (sub)timelines that can contain any
number of tracks and clips.
Multicam clips:: (group clips in Avid), these are compound clips, in which multiple clips are
synchronised and combined into one. This compound contains clips with different camera angles
(for video) or audio recordings for the same event.
Filler (Avid), FX clips (Lightworks) or Adjustment Clips (Premiere, Resolve):: these special clips
can hold effects that are applied to all clips on lower tracks of a timeline, directly underneath.
Bin types
^^^^^^^^^
Often there are two types of bins in which these clips sit
. ``Normal'' bins.
. Search bins (Premiere) / filters (Lightworks) / smart bins (Resolve) / smart collections
(FCP): for these you setup certain rules based on clip names or metadata. All clips to which
these rules apply are automatically added to these bins. This means that the same clip can
simultaneously exist in a regular bin and a search bin, in contrast to normal bins, where each
clip will only exist in a single bin and you'd have to duplicate this clip for it to be also
placed in another bin (Lightworks being the exception here).
.Filter options in Lightworks
image::{imgg}/wouter/v4-03-filterOptions.jpg[alt="Filter options in Lightworks"]
Resolve adds a third kind of bins, known as ``Power bins'': these are bins that are accessible by all projects.
Bin views
^^^^^^^^^
Bins offer several views to see its contents. These include:
* List View / Text View: all items in the bin are listed vertically. Multiple columns will show
different kinds of metadata -- the user can choose which fields are displayed and can
rearrange their order.
* Icon View (Premiere) / Frame View (Avid) / Thumbnail View (Resolve) / Tile View
(Lightworks): this will show a small frame from each clip in a raster. In general you're able
to change which frame is displayed if you wish to see another.
* Freeform View (Premiere): same as Icon View, but you can freely position the clips
wherever you want. Note that Avid and Lightworks allow you to do this within Icon/Frame
View itself.
* Script View (Avid): similar to Text View, but with a frame being displayed for each clip and
a large field for entering comments. In Script View, the vertical order of clips can be
rearranged, in contrast to Text View, which only supports automatic sorting.
* Metadata View (Resolve): similar to Avid's Script View, except you can't reorder clips.
* Filmstrip View (Final Cut Pro): similar to Icon View, but [red]#TODO: missing text??#
Avid is famous for allowing you to save your Text or Script View configuration as a preset that can
be quickly recalled. Premiere allows this through its Metadata Display window, Final Cut Pro by
Control+left clicking any column header and Lightworks via the Columns > Presets menu, but these
methods are less straightforward than Avid's. Another neat feature that Avid has is a bin map: an
overview of bin contents that can be toggled when in Frame View.
.Creating a custom view preset in Avid Media Composer, which can be selected from the drop-down menu in the top bar.
image::{imgg}/wouter/v4-04-customView.jpg[width="120%", alt="Creating a custom view preset"]
Note that all apps offer a search/filter box (or button) on top of each bin view. Final Cut Pro and
Resolve allow skimming clips from the bin view, simply by hovering the mouse on top of the
thumbnail and moving it from left to right.
Organising footage
~~~~~~~~~~~~~~~~~~
Once your footage is represented by clips in bins, organisation can begin. Wildly different methods
exist for this. In the simplest version you will just drop your footage in one or several bins and be
done with it. In professional environments there are often strict bin structures and naming
conventions, and additional methods for organising are being used:
* First and foremost: with multi-camera shoots and/or when audio has been recorded
separately, now is the time to synchronise these. Create multicam clips and/or synchronise
sound, either in the NLE itself, or in an external application. I personally create source reels
first (see further down this list), export these to another application for syncing, import the
synced reel back, and in case of a multicam shoot, split this reel into multiple reels and
convert each to a multicam clip (Premiere and Resolve both support this).
* Metadata. This is a very powerful way to search for or filter specific types of content and it
could be a chapter on its own. Metadata is often created by the camera during recording,
added during offloading by a DIT (digital imaging technician) or applied after ingest by
assistant editors. Nowadays metadata is also added by AI/machine learning algorithms to
help identify the contents of a clip, create transcriptions from audio, etc. Avid provides the
option to (automatically or manually) connect clips to lines in a script (ScriptSync).
* The aforementioned subclips are often used to extract sections out of a longer clip.
* Sequence/timeline markers to mark points in time, or sections (so-called ranged or spanned
markers; terminology is different per app).
+
.Plenty of marker options in Adobe Premiere
image::{imgg}/wouter/v4-05-markerOptions.png[width="70%", alt="Marker options in Premiere"]
+
* Timeline clip notes (Avid): these can be added to a timeline clip, and will stay connected to
this clip. You can't trim them off and they will automatically move whenever a clip moves.
They can be burned into a render upon export. Premiere and Resolve offer clip markers
instead -- these are similar to sequence markers, but are attached to clips.
* Tagging or rating (parts of) clips (FCP and to a lesser extent Resolve). FCP's rating only
goes as far as letting you mark clips or sections as ``favourite'' or reject clips entirely,
but its keyword system (as described later in this chapter) is much more elaborate and useful.
Resolve has an option to add keywords, but its workflow is quite clunky when trying to
apply this to a section within a clip.
* Clip colours. Often there exist multiple levels at which a clip can get a colour. Colours can be
applied based on source clip (usually different colours for different media types), timeline
clip, timeline track, offline footage, different framerates, and more. Avid has the most
options in this field and lets you choose exactly which types of colours to display on the
timeline. Manually set clip colours are a very useful organisational tool, e.g. to differentiate
between several cameras, interviewed persons in a documentary or different locations.
+
.Avid has many options for choosing how clips are coloured in the timeline
image::{imgg}/wouter/v4-06-clipColor.png[width="50%", alt="Coosing clip colours in Avid"]
+
* Flags (Resolve). Similar to markers, but these apply to entire (source) clips, while markers
apply to a single point in time or a time range within a clip. Multiple flags can be added to
each source clip.
* Reels: source footage is divided over one or multiple timelines to be able to quickly navigate
many clips without having to load individual clips in the source viewer. These are called
``source reels''. A special kind of source reel is a ``sync map'', which contains all footage
of a day of shooting from multiple cameras synced and laid out chronologically.
FCP doesn't quite support source reels and instead offers continuous playback from the
browser window. Resolve has a ``source tape'' option that works in a similar way.
.Simple example of a source reel (top), converted to a sync map (bottom)
image::{imgg}/wouter/v4-07-syncMap.png[width="100%", alt="Example of a sync map", link="{imgg}/wouter/v4-07-syncMap.png"]
Viewing footage
^^^^^^^^^^^^^^^
Once basic organisation is in place, the editor can start watching and adding further notes/comments
to narrow down large amounts of footage and seek out options of useful content for the edit. Most
apps offer two types of monitors for video playback: source and record/program monitors. FCP has
only a single monitor that switches between these two functions. Some others (Avid, Resolve) give
users the choice between single or multi viewer modes. Lightworks and Avid support an unlimited
amount of floating viewers.
.You can open (and position) as many viewers as you like in Lightworks
image::{imgg}/wouter/v4-08-multiViewer.jpg[width="120%", alt="Multiple viewers in Lightworks", link="{imgg}/wouter/v4-08-multiViewer.jpg"]
Viewers are usually coupled with timecode display and have a ton of settings.
I won't go into details about these.
In most applications you can also watch your source footage from the bin when in Frame/Icon
View. You'll be able to use JKL shortcuts for playback, mark in and out points and add it to the
timeline, similar as to when the clip is loaded in the source viewer. Final Cut Pro lets you click and
drag in-out regions in the thumbnail.
Selects
^^^^^^^
Time to create a selection of material to consider for the rough cut. One way to do this, as
previously described, is by creating subclips or by tagging. Drop the subclips in a bin in
Icon / Frame View and you have a good visual overview of selected shots. This is great for selecting
B-roll material, or for projects that require a very visual approach, f.e. trailers, music videos
or aftermovies (not hoping to ever cut one of those again though, but that's a different story 😉).
Another way to select footage is to cut selections from source clips or source reels to one or more
selects reels: these are timelines that hold your selections and are only used for that purpose. Selects
reels can then be loaded again as if they were a source clip, in order for an another selection to be
made. This is called *reel-to-reel editing*. Many apps allow seeing the source timeline, either in their
own panel (Premiere) or window (Lightworks), or by switching the timeline panel between a source
and record mode (Avid, Resolve). A method known as _pancake timeline editing_ works when an
application supports viewing multiple timelines simultaneously. By stacking timelines above each
other you can easily drag selections from one timeline to another. This is possible in Premiere,
Lightworks and Resolve.
.Pancake timeline editing in Lightworks
image::{imgg}/wouter/v4-09-pancake.png[width="110%", alt="Example of a sync map", link="{imgg}/wouter/v4-09-pancake.png"]
Reels can be further managed by using markers to describe certain points in time or sections, and
these markers can be viewed as a list in their own panel. When selecting a marker in this panel, the
playhead of the corresponding monitor will jump to the (start) frame of this marker. Markers can
have multiple input fields and a colour. They are one of the most important tools to be able to make
notes and navigate large amounts of footage over extended periods of time. As an aside: Lightworks
has an option to turn ranged markers into subclips, quite useful, as it allows you to use markers
to make your selection.
Some NLE's have a ``ripple markers'' option for timeline markers. This means markers will move
when clips in the timeline are shortened or extended.
FCP has ``todo markers'' that can be checked. And when tagging clips with keywords in FCP,
keyword collections are automatically created in which all sections of clips with the same keyword
can be found. It's also possible to manually create keyword collections and drag and drop clips or
tagged sections (``content ranges'') there.
Back to the reels: one advantage of reel-to-reel editing is that it's very useful to see the audio
waveforms of your source clips on an actual timeline: this way you can freely zoom and scroll.
Final Cut Pro will only display a clip's waveform at the bottom of the clip preview in the browser,
but when an interview is an hour or longer, this waveform will be squeezed in a way that's of little
use.
Another advantage of reel-to-reel editing is a sense of space: you get an idea of where certain
footage can be found (more to the left, in the middle, or at the end of a reel). Combined with
markers, clip colours and gaps between sections, this makes it easier to find things back later. A third
advantage is that it's easy to filter in rounds: you can narrow down your footage in multiple reel-to-reel
selection passes.
I personally often use a variation of reel-to-reel editing, in which I lift sections in my source reel.
I then duplicate this reel, remove the first video and audio track(s) and keep the lifted parts to create
my selects reel. The advantage here is that I can rate sections as _not useful_ (V1), _possibly useful_
(V2), _quite useful_ (V3) or _highly useful_ (V4) by lifting the video clip one, two or three tracks up --
see it as a 4-star rating system. I have seen other editors use this method for selecting footage,
although in all fairness I'm not sure how widespread this technique is.
.Lifting selections; also note the use of clip colours, gaps and markers for organising the selects reel.
image::{imgg}/wouter/v4-10-selectsReel.png[width="100%", alt="Example of a sync map", link="{imgg}/wouter/v4-10-selectsReel.png"]
In FCP you can create your own rating system by using keywords (as shown in the screenshot at the
start of this chapter) and then having everything filtered through keyword collections, as described
in this https://blog.frame.io/2016/10/18/make-fcp-x-log-better-1/[blog post at Frame.io].
Quite handy as well!
Proposals
~~~~~~~~~
* Which system(s) of organising a project and selecting footage should Lumiera support? I
would say: most. They don't interfere with each other, so let the user choose. Definitely
support source timelines and let people rate, tag and mark all they want. Subclips might not
be necessary when a tagging/keyword system is present, as Final Cut has proven.
* Metadata-based organising, searching and filtering of content are _must-haves_.
* I would include a native rating system (1 to 5 stars should suffice) that works both on clip
sections and entire clips, rather than FCP's favourite/reject system, as it's more granular.
* As for bins-in-bins vs bins-in-folders: the first one is more flexible (bins can contain both
media files and other bins, while folders can only contain bins) and more closely resembles
how folders work on a PC, so my preference would be to go that route, although there's also
something to be said for the strict division of function that folders+bins have.
* I would like to propose an option to automatically create source reels based on rules you can
set, for example: one source reel for every day of shooting. These automatic source reels
could have a filter function to select which clips are being displayed on the timeline. This
way it becomes easy to say something like ``show me all clips from the second day of the
shoot from camera A''.
* A versioning system for timelines is something that only Resolve has recently picked up
with its Create (and Restore) Timeline Backup feature, and honestly I've been amazed by
how this hasn't been a core feature in NLE's for decades now, as it makes so much sense to
have. Let users make snapshots and perhaps make one automatically every day, without the
need to manually duplicate and rename timelines and putting them in some sort of archive
bin.
* Importing: auto-import based on watch folders could be very useful during certain stages of
an edit. Setup which folders to watch and whenever a new file is dropped in that folder it
will be imported into a bin of choice. Especially handy when working with sound effects.
However, we might not want to be scanning folders all the time.
* About sound effects: there's a free application called Metadigger that let's you quickly filter
your sound collection by metadata and then copy sound files to your project folder with a
keyboard shortcut. Something like this, built-in, would be quite handy for those of us with
local sound effect libraries.
* A small one: I'd like each project to remember the last location you imported from. I often
switch between projects and each time I want to import something in current NLE's, the file
dialog points to the last used directory, which is often the wrong one.
Not a big deal, but not ideal either.
- Asset organisation: bins vs metadata-based structures (+ search bins/filters)
- Viewing footage: single clips vs source reels; continuous playback from browser
- Source monitor + program monitor vs a single monitor
- Creating selects: selects reels, subclips and keyword collections
- On markers and notes
- From selects to editing in the timeline: Timelines in the source viewer: reel to reel editing
Chapter 2: the timeline
-----------------------
@ -283,7 +622,7 @@ Editors often proudly share screenshots of their timelines on social media, and
impressive, but these are in fact pretty fragile structures.
.One of my own timelines, which is relatively simple by comparison to those of many Hollywood movies
image::{imgg}/wouter/01-timeline.jpg[width="110%", alt="Screenshot of a timeline", link="{imgg}/wouter/01-timeline.jpg"]
image::{imgg}/wouter/v3-01-timeline.jpg[width="110%", alt="Screenshot of a timeline", link="{imgg}/wouter/v3-01-timeline.jpg"]
So naturally, a question would be: what will happen if we would let go of the track paradigm? This
is what Final Cut Pro has done, starting from the rewrite of Final Cut Pro X. At the time, a
@ -300,10 +639,10 @@ primary storyline, and all connected clips automatically move with it. With a mo
ignore clip connections, so that you can also easily move a primary clip elsewhere without its
connected siblings coming along.
image::{imgg}/wouter/02-trim-magnetic-1.jpg[width="100%", alt="(before trim)"]
image::{imgg}/wouter/v3-02-trim-magnetic-1.jpg[width="100%", alt="(before trim)"]
.Extending one clip will automatically move all connected clips in order to keep sync
image::{imgg}/wouter/02-trim-magnetic-2.jpg[width="100%", alt="(after trim)"]
image::{imgg}/wouter/v3-02-trim-magnetic-2.jpg[width="100%", alt="(after trim)"]
_Sounds good._ Why not just copy this? One reason is that FCP assumes that all clips that are not on
the primary storyline should be connected to this primary storyline. This might work well for fiction
@ -318,7 +657,7 @@ How else could we group clips together that ought to be connected? +
We could create groups that are directly editable, like so:
.Compound clips
image::{imgg}/wouter/03-grouped-1.png[width="100%", alt="Timeline with compound clips"]
image::{imgg}/wouter/v3-03-grouped-1.png[width="100%", alt="Timeline with compound clips"]
However, there are a few problems with this: we expect rendering to happen from top to bottom, in
which case the b-roll would cover the subtitles. On top of that, it's hard to see at which points video
@ -327,7 +666,7 @@ clips overlap. Last but not least: it looks rather unorganised.
Can we restructure this? Perhaps like this:
.Overlapping compound clips
image::{imgg}/wouter/03-grouped-2.png[width="100%", alt="Timeline with stacked overlapping compound clips"]
image::{imgg}/wouter/v3-03-grouped-2.png[width="100%", alt="Timeline with stacked overlapping compound clips"]
But how exactly would a user interact with a timeline like this? I will leave the answer to that
question for an actual design document, as for now I just wanted to show the concept of grouping so
@ -354,7 +693,7 @@ FCP's precision editor. It doesn't change the working of keyboard shortcuts or t
alters the timeline via animations in a very fluid way.
.Final Cut Pro's precision editor
image::{imgg}/wouter/04-fcp-precision-editor.jpg[precision editor]
image::{imgg}/wouter/v3-04-fcp-precision-editor.jpg[precision editor]
Another example is the Trim View in DaVinci Resolve: whenever you perform a trimming
operation, the program monitor displays a trim view (showing the frames left and right of the cut
@ -386,7 +725,7 @@ the use of a _contextual bar_ that appears over the bottom part of the timeline
mode is active.
.Contextual bar with options for the mode/tool
image::{imgg}/wouter/05-contextual-bar.png[width="100%", alt="contextual bar"]
image::{imgg}/wouter/v3-05-contextual-bar.png[width="100%", alt="contextual bar"]
Currently I'm thinking of using this bar on two or three occasions:
@ -467,10 +806,10 @@ One last thing I would like to address is the destruction that an insert operati
this can be prevented by proper track management, but you can wonder why you would ever want
an insert action to cause the following:
image::{imgg}/wouter/06-sync-insert-1.png[height=110, alt="sync insert"]
image::{imgg}/wouter/v3-06-sync-insert-1.png[height=110, alt="sync insert"]
.Inserting a clip with sync locks enabled for V2 might split a clip in Avid Media Composer.
image::{imgg}/wouter/06-sync-insert-2.png[height=110, alt="sync insert"]
image::{imgg}/wouter/v3-06-sync-insert-2.png[height=110, alt="sync insert"]
In this case, if you'd like the clips on V2 to stay in sync, the overlapping clip will be split in two.
If you don't want this to happen you would have to move the overlapping clip to another track
@ -622,7 +961,7 @@ will move both the viewport and playhead. Clicking the left mouse button will ac
position and close the widget.
+
.Skim widget
image::{imgg}/wouter/07-1-skim.png[width="100%", alt="A skim widget rendered as overlay"]
image::{imgg}/wouter/v3-07-1-skim.png[width="100%", alt="A skim widget rendered as overlay"]
Autoscroll widget:: when the user moves the mouse cursor slightly left or right, it will enter zones
@ -631,14 +970,14 @@ When the user moves the mouse farther away from the initial starting point, the
increase. This will also work vertically. The playhead will move along as well.
+
.Autoscroll widget
image::{imgg}/wouter/07-2-autoscroll.png[width="100%", alt="A widget for auto-scrolling"]
image::{imgg}/wouter/v3-07-2-autoscroll.png[width="100%", alt="A widget for auto-scrolling"]
Zoom widget:: moving the mouse left or right from the centre will zoom horizontally, up and
down will zoom vertically.
+
.Zoom widget
image::{imgg}/wouter/07-3-zoom.png[width="100%", alt="An overlay widget to control zooming"]
image::{imgg}/wouter/v3-07-3-zoom.png[width="100%", alt="An overlay widget to control zooming"]
Mouse-only navigation:
@ -648,7 +987,7 @@ mouse button. When released, it will commit to the new location/view. If a user
immediately releases the button, a regular context menu will appear.
+
.Combined skim, zoom and scroll widget
image::{imgg}/wouter/07-4-combined.png[width="100%", alt="Overlay widget to combin skim, zoom and autoscrol function"]
image::{imgg}/wouter/v3-07-4-combined.png[width="100%", alt="Overlay widget to combin skim, zoom and autoscrol function"]
Why popup widgets?
@ -708,7 +1047,7 @@ what any other NLE does at the moment, but enabling multi-clip selection by keyb
good reason to go in this direction.
.Selection mockup: selected clips are highlighted in yellow, the cursor is a white dot.
image::{imgg}/wouter/08-selection.png[width="100%", alt="selecting clips"]
image::{imgg}/wouter/v3-08-selection.png[width="100%", alt="selecting clips"]
For selecting, we can introduce an ``add to selection'' key which adds the highlighted clip to the
current selection (apart from the convention of using Ctrl+mouse click). We could also think of
@ -723,10 +1062,10 @@ Most NLE's have functions for selecting many clips at once, called ``select forw
backward''. In Adobe Premiere these are tools: click and you select all clips to either the left or right
of the mouse cursor, and using Shift will toggle between selecting all clips on a single track or on
all tracks (it doesn't respect track activation status though, which it should). Other NLE's (Resolve,
Avid, Lightworks) only have keyboard shortcuts for these functions, which do only affect activated
tracks. This is a bit faster: in Premiere you need to switch to the Select Forward/Backward Tool,
make your selection, then switch back to your previous tool. With a keyboard shortcut you only
have to press a single key (if your track selection is correct).
Avid, Lightworks) have keyboard shortcuts for these functions (in Avid you can place a button in the
GUI as well), which do affect activated tracks only. This is a bit faster: in Premiere you need to
switch to the Select Forward/Backward Tool, make your selection, then switch back to your previous
tool. With a keyboard shortcut you only have to press a single key (if your track selection is correct).
Arranging clips
~~~~~~~~~~~~~~~
@ -789,7 +1128,7 @@ entering trim mode.
* Lightworks has a trim mode that defaults to ripple trimming, unless you specifically use the
“drag to black” feature by Control-clicking trim edges when entering trim mode. When
using the keyboard, one needs to mark a section and use the Remove command (similar to
Avid''s ``lift'') to create a gap.
Avid's ``lift'') to create a gap.
Next to ripple/non-ripple trimming there are three other edit operations closely related to trimming
that need to be mentioned:
@ -816,10 +1155,10 @@ tracks automatically stay in sync. This means (manually) enabling or disabling t
track. Once set, trimming a single clip can be enough to have all tracks move along, although you
might still need to manually select trim sides for overlapping clips to get the desired result.
image::{imgg}/wouter/09-sync-trim-1.png[height=140, alt="trimming with sync-lock(before)"]
image::{imgg}/wouter/v3-09-sync-trim-1.png[height=140, alt="trimming with sync-lock(before)"]
.Avid's sync locks will split longer clips by default, in order to keep sync. Such destructive behaviour should be avoided in Lumiera.
image::{imgg}/wouter/09-sync-trim-2.png[height=140, alt="trimming with sync-lock(after)"]
image::{imgg}/wouter/v3-09-sync-trim-2.png[height=140, alt="trimming with sync-lock(after)"]
Without sync locks you will have to select trim sides on every track that needs to stay in sync,
although, as we'll see, even that can be accomplished by a single keystroke. FCP is the exception
@ -850,7 +1189,7 @@ These commands can be combined: when pressing multiple keys, new trim sides are
selection. This allows for quickly selecting many trim sides on many tracks.
.One keystroke led to the selection of these four trim sides in Lightworks
image::{imgg}/wouter/10-trim-side.png["select trim side"]
image::{imgg}/wouter/v3-10-trim-side.png["select trim side"]
Another feature that was already mentioned in a previous chapter, is FCP's precision editor, which
can be triggered by double clicking a cut or by keyboard shortcut. This shows how much footage
@ -937,7 +1276,7 @@ exited automatically whenever the user clicks in an empty spot of the timeline o
mode key (T). This key can also be used to return to the last-used trim configuration.
.Mockup of the trim mode. The striped area marks the pre- and post-roll that will be previewed.
image::{imgg}/wouter/11-trim-mode.png[width="100%", alt="using trim mode"]
image::{imgg}/wouter/v3-11-trim-mode.png[width="100%", alt="using trim mode"]
Splitting and merging clips
@ -973,7 +1312,7 @@ I would like to propose the ability to divide a timeline into multiple sections.
will have a header in the ruler that can be edited, to give each section a name (similar to how
duration markers in Premiere are displayed).
image::{imgg}/wouter/12-sections.png[width="100%", alt="Timeline with sections"]
image::{imgg}/wouter/v3-12-sections.png[width="100%", alt="Timeline with sections"]
The benefits of sections:
@ -1030,7 +1369,7 @@ transition from a clip's start or end to the position of the playhead.
* Avid has a configuration panel for adding transitions:
image::{imgg}/wouter/13-transition.png[align="center", alt="transition properties"]
image::{imgg}/wouter/v3-13-transition.png[align="center", alt="transition properties"]
* Some apps allow adding transitions to every cut in the in-out range (Avid, Lightworks). Avid
gives you the option to skip cuts that already have a transition. Very useful for adding short
@ -1100,6 +1439,12 @@ Chapter 3: finishing
--------------------
[red]#To be written.#
- Nodes vs Layers
- Audio mixing
- Color grading
- Visual effects compositing
- Titling
Chapter 4: a broader GUI concept
--------------------------------
[red]#To be written.#

View file

@ -51,7 +51,7 @@ Current Release
[red]#Lumiera is not usable yet#.
- A »Preview Release« was
https://git.lumiera.org/?p=LUMIERA;a=tag;h=refs/tags/v0.pre.04[published at Dec 2, 2025]
https://git.lumiera.org/gitweb?p=LUMIERA;a=tag;h=refs/tags/v0.pre.04[published at Dec 2, 2025]
- link:/debian[DEB packages]
were built for »Trixie« and »Noble«

View file

@ -214,7 +214,7 @@ Interface Definition Language
-----------------------------
In his work on the plugin loader, _Cehteh_ created a first draft how to export an interface, and calls for review.
An example can be found in
https://git.lumiera.org/?p=LUMIERA;a=blob;f=tests/backend/test-interfaces.c;h=09fa25afa3cb57b79a2717d0bf3a8253afeba000;hb=ebcc5c7fd2d8700f3375e3309b724ac2517a0f40[tests/backend/test-interfaces.c]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=tests/backend/test-interfaces.c;h=09fa25afa3cb57b79a2717d0bf3a8253afeba000;hb=ebcc5c7fd2d8700f3375e3309b724ac2517a0f40[tests/backend/test-interfaces.c]
An interface is a list of »slots« mapping functions. The interface descriptor is itself realised as interface,
and thus can be versioned, extended and specialised.

View file

@ -95,7 +95,7 @@ For now the focus is on getting a single frame into the gui and there will be ti
The devs agreed to meet up on Saturday to get the latest version up and running.
NOTE: Demonstration in https://git.lumiera.org/?p=LUMIERA;a=blob;f=src/common/guifacade.cpp;h=60a16d0f67bd95b43e44662daa9c0525235e14f5;hb=5f10e65852de2c51364c2e2a5a1cf29cae81c6b2#l64[guifacade.cpp]
NOTE: Demonstration in https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=src/common/guifacade.cpp;h=60a16d0f67bd95b43e44662daa9c0525235e14f5;hb=5f10e65852de2c51364c2e2a5a1cf29cae81c6b2#l64[guifacade.cpp]
(The thread shall be forked from within the GUI code not from the facade)
This was discussed and ichthyo has already pushed a new cleaned up state and resolved most of the dependency problems with the liblumiera. It raised the question: do we prefer a UI facade to a GUI facade or do we want an extra script facade someday?

View file

@ -191,7 +191,7 @@ don't think we get much program options, maybe something to set a GUI skin.
Moreover, I've written last year a thin wrapper around the commandline and
integrated it with the boost options parser such that user code can receive the
remaining options as a vector of `std::string`s. Please have a look at
https://git.lumiera.org/?p=LUMIERA;a=blob;f=tests/common/mainsuite.cpp;h=455bfd98effd0b7dbe6597f712a1bdfa35232308;hb=80e1e382f42512ebf2e10a802f77e50327b8fb73[the test class runner main]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=tests/common/mainsuite.cpp;h=455bfd98effd0b7dbe6597f712a1bdfa35232308;hb=80e1e382f42512ebf2e10a802f77e50327b8fb73[the test class runner main]
for an usage example. I really want our Lumiera main to be clean and expressive
in the way showed there. Probably the most important part of the startup is
pulling up the session core; because of that I think most of the startup

View file

@ -15,8 +15,8 @@ This proposal intends to capture envisioned Architecture of the Application.
image:{ldoc}/devel/draw/rendered/Lumi.Architecture-1.png[Overview of Lumiera Architecture]
^The SVG source of this drawing is
https://git.lumiera.org/?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-1.svg;hb=HEAD[maintained in GIT(2008)]·
https://git.lumiera.org/?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-2.svg;hb=HEAD[(current: 2022)]^
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-1.svg;hb=HEAD[maintained in GIT(2008)]·
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-2.svg;hb=HEAD[(current: 2022)]^
Description
@ -75,7 +75,7 @@ Conclusion
----------
Accepted. The drawing was moved into the GIT tree. +
It was upgraded and reworked
https://git.lumiera.org/?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-2.svg;hb=0b9f2e2c3139cad542013caac593cb5972a2e3f6[in 2022]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob_plain;f=doc/devel/draw/Lumi.Architecture-2.svg;hb=0b9f2e2c3139cad542013caac593cb5972a2e3f6[in 2022]
Back to link:/x/DesignProcess.html[Lumiera Design Process overview]

View file

@ -106,7 +106,7 @@ details of carrying out the operations are much more involved.
Please see the
link/wiki/renderengine.html#Rendering[Development internal doc (TiddlyWiki)]
and the
https://git.lumiera.org/?p=LUMIERA;a=blob;f=src/proc/engine/procnode.hpp;h=9cf3a2ea8c33091d0ee992ec0fc8f37bb5874d34;hb=f9452f654cf39a4abbbeecfd81bef51aa347b321[Draft: ProcNode (src)]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=src/proc/engine/procnode.hpp;h=9cf3a2ea8c33091d0ee992ec0fc8f37bb5874d34;hb=f9452f654cf39a4abbbeecfd81bef51aa347b321[Draft: ProcNode (src)]
for details
(and/or contact Ichthyo for in-depth discussion of those technical details)

View file

@ -149,7 +149,7 @@ We soon settled upon the following toolchain, which is used up to today:
history is consolidated eventually into the
link:{rfc}/MasterRepositorySetup.html#historical_remark[»official Lumiera repository«],
which is made accessible at
https://git.lumiera.org/?p=LUMIERA;a=summary[`git://git.lumiera.org/LUMIERA`]
https://git.lumiera.org/gitweb?p=LUMIERA;a=summary[`git://git.lumiera.org/LUMIERA`]
- We never used link:{rfc}/GitSubmoduleTransistion.html#_comments[Git submodules]...
- https://www.scons.org/[SCons] became the primary buildsystem soon,
with Autotools discontinued in 2011
@ -158,14 +158,14 @@ We soon settled upon the following toolchain, which is used up to today:
- Website and documentation pages are generated by
https://asciidoc-py.github.io/[Asciidoc], with source files for
documentation maintained
https://git.lumiera.org/?p=LUMIERA;a=blob;f=doc/index.txt;h=9bc0f11e6fb67588388e7616320b695841571278;hb=20f3252892d5b53a05ecd8aeb571122806baba17[in-tree],
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=doc/index.txt;h=9bc0f11e6fb67588388e7616320b695841571278;hb=20f3252892d5b53a05ecd8aeb571122806baba17[in-tree],
published
https://git.lumiera.org/?p=website;a=blob;f=build_website.sh;h=60b90e18d5732d66566baf2c20a217212a5dcece;hb=aefc91e13a95a3c742dc8143c374ec982e0aa56b[automatically on push].
https://git.lumiera.org/gitweb?p=website;a=blob;f=build_website.sh;h=60b90e18d5732d66566baf2c20a217212a5dcece;hb=aefc91e13a95a3c742dc8143c374ec982e0aa56b[automatically on push].
- A https://classic.tiddlywiki.com/[TiddlyWiki] is used for development notes
- for Unit-Tests, we use our own minimalist
https://git.lumiera.org/?p=LUMIERA;a=blob;f=src/lib/test/run.hpp;h=035633af9bde7a8053bcd304baffc21693dcf031;hb=20f3252892d5b53a05ecd8aeb571122806baba17[Test-runner].
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=src/lib/test/run.hpp;h=035633af9bde7a8053bcd304baffc21693dcf031;hb=20f3252892d5b53a05ecd8aeb571122806baba17[Test-runner].
and a
https://git.lumiera.org/?p=LUMIERA;a=blob;f=tests/test.sh;h=8c13810eb475bbacd97c528607e6750f47bb145e;hb=20f3252892d5b53a05ecd8aeb571122806baba17[shell script]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=tests/test.sh;h=8c13810eb475bbacd97c528607e6750f47bb145e;hb=20f3252892d5b53a05ecd8aeb571122806baba17[shell script]
to support definition of test collections.
- A Debian package is maintained with
https://wiki.debian.org/PackagingWithGit[git-buildpackage] and published

View file

@ -68,9 +68,9 @@ rather -- a task no one was willing to take on.
Yet the Lumiera project succeeded in building a web presence based on
https://asciidoc-py.github.io/[Asciidoc] sources
https://git.lumiera.org/?p=website;a=summary[checked into Git],
https://git.lumiera.org/gitweb?p=website;a=summary[checked into Git],
with a lightweight
https://git.lumiera.org/?p=website;a=blob;f=build_website.sh;h=60b90e18d5732d66566baf2c20a217212a5dcece;hb=aefc91e13a95a3c742dc8143c374ec982e0aa56b[commit-hook script]
https://git.lumiera.org/gitweb?p=website;a=blob;f=build_website.sh;h=60b90e18d5732d66566baf2c20a217212a5dcece;hb=aefc91e13a95a3c742dc8143c374ec982e0aa56b[commit-hook script]
to automatically publish any changes on git-push -- a scheme
which comes close to the gist of this RfC.

View file

@ -16,7 +16,7 @@ This describes how the shared MASTER is set up and syncronized.
Description
~~~~~~~~~~~
We have now a shared master repository in
https://git.lumiera.org/?p=LUMIERA;a=summary[/git/LUMIERA].
https://git.lumiera.org/gitweb?p=LUMIERA;a=summary[/git/LUMIERA].
- The public/anonymous git url is [yellow-background]#`git://git.lumiera.org/LUMIERA`#
- for people with ssh access it is pushable at [purple]#`UID@git.lumiera.org:/git/LUMIERA`#.
@ -71,10 +71,10 @@ That being said -- the shared master at [yellow-background]#`git://git.lumiera.o
still contains the _official_ state of the Lumiera codebase, while
actual development is published first in a repository per developer
(so effectively in
https://git.lumiera.org/?p=lumiera/ichthyo;a=summary[the Ichthyo repository]
https://git.lumiera.org/gitweb?p=lumiera/ichthyo;a=summary[the Ichthyo repository]
for the time being);
development is consolidated intermittently into the
https://git.lumiera.org/?p=LUMIERA;a=summary[LUMIERA Git].
https://git.lumiera.org/gitweb?p=LUMIERA;a=summary[LUMIERA Git].
''''

View file

@ -18,7 +18,7 @@ Propose a documentation/planning about the setup.
Description
~~~~~~~~~~~
Take a look at my draft at:
https://git.lumiera.org/?p=LUMIERA;a=blob;f=doc/devel/nobug_flags.txt;h=325141acb52a7be7f75425359b6057c74ed30253;hb=a03e3c5e73a4c0be2264ac4e4e0f02eb8749837b[nobug_flags.txt]
https://git.lumiera.org/gitweb?p=LUMIERA;a=blob;f=doc/devel/nobug_flags.txt;h=325141acb52a7be7f75425359b6057c74ed30253;hb=a03e3c5e73a4c0be2264ac4e4e0f02eb8749837b[nobug_flags.txt]
I've added the things I planning for the backend, others might add their own
plans there too. So far this is an early draft, comments welcome.

View file

@ -210,7 +210,7 @@ during this effort leads to the following refinements:
- Workers manage their capacity through a statistical distribution of _sleep times._
In this form, the proposal can be considered as implemented. +
^(see -> https://git.lumiera.org/?p=LUMIERA;a=commit;h=d71eb37[d71eb37]
^(see -> https://git.lumiera.org/gitweb?p=LUMIERA;a=commit;h=d71eb37[d71eb37]
and https://issues.lumiera.org/ticket/1344[#1344])^
Ichthyostega:: '2024-04-20 01:55 CEST'

View file

@ -49,6 +49,4 @@ On occasion, we'll replace them by drawings from our current UML model.
128517: classdiagram 142725 "Time flavours"
133637: classdiagram 143877 "Player Entities"
133637: objectdiagram 144005 "Play Process Structure"
133637: sequencediagram 145157 "output data exchange"
133637: classdiagram 151685 "Player Output"
128773: classdiagram 158213 "UIBus"

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 256 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,7 +1,7 @@
Documentation
=============
This documentation section contains documentation for both users and developers.
This documentation section contains documentation for users and developers alike.
== User ==
@ -60,8 +60,8 @@ deeper documentation dungeons.
At the second layer, the reader must make the first major decision on which
router to pursue: documentation with a heavy code based leaning; or more descriptive
documentation on the design of Lumiera which avoids a code narrative. This
section has two main sections:
documentation on the design of Lumiera, which omits most technical details of the implementation.
Consequently, the documentation is divided into two main parts:
* link:design/index.html[Lumiera Developer Design Documents]
* link:technical/index.html[Lumiera Developer Technical Documents]
@ -75,14 +75,14 @@ section has two main sections:
[width="90%",frame="topbot",options="header",cols="^,2<"]
|================================================================================================
|Sub-System|Remarks
|Section|Remarks
|link:design/architecture/[Architecture]|Structure of Lumiera
|link:design/gui/[GUI]|The Lumiera ⟷ user interaction layer
|link:design/model/[Model]|Low-level and high-level models
|link:design/engine/[Engine]|Renderer, output generation, ...
|link:design/lowlevel/[Low-level]|Low-level and system services
|link:design/application/[Application]|Application framework
|link:design/plugins/[Plugins]|Dynamically loaded Extensions
|link:design/extension/[Extension]|Dynamically loaded Extensions
|link:design/workflow/[Workflow]|Workflow and Interaction Design
|link:design/governance/[Governance]|Meta project concerns
|================================================================================================
@ -93,12 +93,12 @@ section has two main sections:
[width="90%",frame="topbot",options="header",cols="^,2<"]
|================================================================================================
|Sub-System|Remarks
|Section|Remarks
|link:technical/build/[Build System]|Build, Packaging and CI
|link:technical/stage/[Stage]|GUI (technical aspects)
|link:technical/steam/[Steam]|Session and processing coordination
|link:technical/vault/[Vault]|Low-level operations
|link:technical/library/[Support lib]|Interface and support libraries
|link:technical/library/[Library]|Utilities and custom frameworks
|link:technical/code/[Code base]|Code management, organisation, ...
|link:technical/infra/[Infrastructure]|Website and developer tooling ...
|link:technical/howto/[Developer HowTos]|Instructions and guides for developers

View file

@ -40,7 +40,7 @@ Lumiera expects a ``standard'' desktop installation running a XServer.footnote:[
Wayland is ``just around the corner'' since 10 years now. Well, recently it
seems it is _really_ just around the corner. We are aware of the necessary
changes, which are not very complicated to implement (since we are using
GTK-3). However, as long as Wayland simply crashes or hands in our
GTK-3). However, as long as Wayland simply crashes or hangs in our
everyday work setup with XFCE, we will not consider this topic
a priority. In the meantime, please report any problems
you encounter with Lumiera running on XWayland.
@ -97,7 +97,8 @@ Languages and Tools
* BOOST (listed below are the Debian package names)
- libboost-dev (at least *1.83*)
- libboost-program-options-dev
- notably including `boost::lockfree`
- libboost-program-options-dev (link dependency)
* Script languages
- Python (*3.10*) required for building (->SCons)
@ -123,7 +124,7 @@ Build Tools
* pkg-config
* Doxygen (optional)
* Asciidoc (optional)
* Git -> https://git.lumiera.org/?p=LUMIERA;a=summary[git://git.lumiera.org/LUMIERA]
* Git -> https://git.lumiera.org/gitweb?p=LUMIERA;a=summary[git://git.lumiera.org/LUMIERA]
footnote:[
_Ichthyo_ maintains a repo at https://github.com/Ichthyostega/Lumiera/[Github ^~Ichthyostega/Lumiera^]
]
@ -160,9 +161,9 @@ Libraries
which is possible, but would require some considerable effort. As of 2025, GDL is still usable,
but no longer available through the standard distribution channels. +
We provide a Git repository at our Website for
https://git.lumiera.org/?p=debian/gdl;a=summary[libGDL (3.40) +git://git.lumiera.org/debian/gdl+]
https://git.lumiera.org/gitweb?p=debian/gdl;a=summary[libGDL (3.40) +git://git.lumiera.org/debian/gdl+]
and the
https://git.lumiera.org/?p=debian/gdlmm;a=summary[GDL C++ bindings (3.7.3) +git://git.lumiera.org/debian/gdlmm+]
https://git.lumiera.org/gitweb?p=debian/gdlmm;a=summary[GDL C++ bindings (3.7.3) +git://git.lumiera.org/debian/gdlmm+]
and we maintain custom Debian packages for both in our
link:/debian/[Debian DEB depot].
]

View file

@ -79,8 +79,8 @@ How the Lumiera Debian package works
Since we maintain both the Debian package and our build system, we can always try to do
_the heavy lifting_ in our link:{ldoc}/technical/build/SCons.html[SCons build system],
as this helps to keep the Debian packaging as simple as possible. Please have a look at the
-> https://git.lumiera.org/?p=debian/lumiera;a=blob;hb=refs/heads/deb;f=debian/control[control file]
· https://git.lumiera.org/?p=debian/lumiera;a=blob;hb=refs/heads/deb;f=debian/rules[debian/rules]
-> https://git.lumiera.org/gitweb?p=debian/lumiera;a=blob;hb=refs/heads/deb;f=debian/control[control file]
· https://git.lumiera.org/gitweb?p=debian/lumiera;a=blob;hb=refs/heads/deb;f=debian/rules[debian/rules]
- we rely on the _Debhelper Sequencer_ "`dh_*`" in our 'debian/rules' +
-> https://manpages.debian.org/trixie/debhelper/dh.1.en.html[Manpage]
@ -106,7 +106,7 @@ into `master` and marked with a tag there.
For the actual packaging, we use these release tags as _upstream tag_ and perform a merge
from that tag into the **debian branch**.footnote:[
This _debian branch_ is called `deb` by convention. As mentioned above, it is published through
a separate https://git.lumiera.org/?p=debian/lumiera;a=summary[Git repository `git://git.lumiera.org/debian/lumiera`],
a separate https://git.lumiera.org/gitweb?p=debian/lumiera;a=summary[Git repository `git://git.lumiera.org/debian/lumiera`],
which is kept separate, since packaging is considered a ``downstream process''.
]
This branch carries only the additions and tweaks done for debian packaging. Yet still, this `deb`
@ -115,7 +115,7 @@ repository as the `master` branch. When the packaging process is complete, the p
DEB version is marked with a tag `debian/<full_debian_version_spec>`
Note however that there is another branch in the aforementioned `debian/lumiera` repository,
which is called https://git.lumiera.org/?p=debian/lumiera;a=shortlog;h=refs/heads/depot[depot]
which is called https://git.lumiera.org/gitweb?p=debian/lumiera;a=shortlog;h=refs/heads/depot[depot]
and is related to our link:/debian[DEB package depot] on Lumiera.org. This branch carries a completely
different directory structure, since it is used with the
https://wiki.debian.org/DebianRepository/SetupWithReprepro[»reprepro« tool from Debian]

View file

@ -352,6 +352,6 @@ shared object (dynamic library), which is then again a SCons target
and can be passed to other executable builders as library dependency
for compilation and linking. Look into 'src/SConscript' or 'test/SConscript'
to see examples of that technique -- which we also use to define that
global compound target variables like `core`, `app_lib`, `core_lib`,
global compound target variables like `core`, `vessel_lib`, `core_lib`,
`vault_lib`, and `support_lib`. These in turn are essential for
building the layered dependency hierarchy in our code.

View file

@ -322,8 +322,8 @@ actively step by step
modules of the application will be placed by default
. after the core application has been loaded and all direct dependencies are resolved,
but still before entering `main()`, the class `lumiera::AppState` will be initialised,
which in turn holds a member of type `lumiera::BasicSetup`. The latter will figure out
but still before entering `main()`, the class `vessel::Voyage` will be initialised,
which in turn holds a member of type `vessel::BasicSetup`. The latter will figure out
the location of the executable footnote:[this is a Linux-only trick, using `/proc/self/exe`]
and require a 'setup.ini' file in the same directory. This setup file is mandatory.

View file

@ -88,6 +88,33 @@ test with random numbers::
several threads; each thread should use its own generator then (yet this can be handled sloppy if
the _quality_ of the random number actually does not matter!)
special test setup::
It can be very helpful to use _policy based design_ for some complex evaluation or algorithm,
even in cases where it is clear that the actual usage will only ever involve one specific data type
and configuration. Not only does such a structured approach help with clarifying the design itself,
since the core of the evaluation logic can be abstracted from data representation -- yet also the
testing and verification of correctness is simplified, as the test can be written to use simple,
human-readable data (strings, symbols), and, furthermore, many kinds of instrumentation can be
included into the test data to gain detailed insight what is happening in the test subject.
verify behaviour sequence::
Use the `lib::EventLog`:
+
- within the test, notable points are marked into the log, possibly with arguments
- after the test, the Event Log can be verified with a search-sequence
- this way it is possible to ensure that some events did happen, and happened
in a very specific order, or that some event did _not_ happen after another event
- the `test::Tracker` defined in 'src/test/tracking-dummy.hpp' will mark all constructor,
destructor and copy events into an embedded Event Log instance. By inheriting or mix-in
from that type, the allocation and instance behaviour of some service can be scrutinised.
verify incidences and concurrency::
Aside from using some generic tracing utility, sometimes it is much easier to setup
a _specifically tailored instrumentation_. This can be achieved with 'incidence-count.hpp'
+
- for instrumentation, the `markEnter()` and `markLeave()` functions need to be invoked
- after the test run, various statistics can be computed and evaluated, including
cumulated time and concurrency for the "cases" marked with enter / leave.
Common Tasks
@ -109,6 +136,12 @@ structured data::
- also useful as output format, both for debugging and because it can be matched against
an expected structure, which is generated with the DSL notation.
- [yellow-background]#(planned)# will be able to map this from/to JSON easily.
scoped collection::
unique-ownership collection that holds a sequence of objects in a preallocated fixed
storage block. Notably even non-copyable objects can be managed (which is not possible
with a `std::vector`), and type-erased instances of subclasses can be mixed. Furthermore
there is a constructor variant that allows to populate a `ScopedCollection` in RAII-style.
This can e.g. be used to fire-off a set of Threads or open a set of wrapped connections.
Iterating
~~~~~~~~~
@ -134,6 +167,26 @@ IterSource::
even the production-state can be hooked up into the IterSource instance (with a smart-ptr).
This allows e.g. to send a Diff to the UI through the UI-Bus, while the actual generation
and application both happen demand-driven or _lazy..._
State Core::
When some algorithm involves a statefull evaluation, it might be helpful to package that into
a _State Core_ container. This is a class that is expected to expose three operations, which
allow to build an iteration on top. The header 'iter-adapter.hpp' provides several adapters
and wrappers, notable `lib::IterStateWrapper` wraps any _state core_ and provides a
Lumiera Iterator interface; likewise, `IterExplorer` heavily relies on that concept and
is able to accept and iterate any state core in a pipeline.
special iterators::
Some additional adaptors are provided as building block so that specific evaluations
can be made to conform with the »Lumiera Iterator« concept
+
- several given iterators can be combined into a iteration over tuples with 'lib/iter-zip.hpp'
- there is also a variation 'lib::izip' that adds a running index into the _zipping operation_
- IterExplorer provides a `flatten()` operation, that can be used to join an iterator-of-iterators
into a single seamless iteration lazily.
- when a STL container also supports _backwards iteration_, then a lumiera iterator can be
created that can _switch direction_ dynamically, with the help of 'iter-cursor.hpp'
- a STL »deque« container can be used both as a stack and a queue; the header 'iter-stack.hpp'
provides an iteration adapter for both usage schemes, so that either the stack or the queue
can be _"discharged"_ as a Lumiera Iterator (possibly with an IterExplorer pipeline)
Formatting
@ -155,6 +208,25 @@ Formatting
because there are way more capable solutions _out there_ ☺)
Setup and wiring
~~~~~~~~~~~~~~~~
Generally speaking, we prefer _constructor wiring_ whenever possible, to avoid creating
unnecessary statefulness (as would be the case when connecting dependencies through setters).
This design also favours decomposition into self-contained components with a single purpose,
and simplifies error handling with the help of the RAII pattern.
However, sometimes a piece of code depends on some remote and even global service; in these
cases, the `lib::Depend` framework can be used to access that service via type name. The
actual service can either be created on-demand as a singleton, or it can be installed
explicitly as a service instance. Furthermore, it is possible to "push aside" the
current service instance temporarily and replace it by a _test mock_.
One point to note is that `lib::Depend` is a _monostate_, and not a _singleton_.
The actual service should be written as a free-standing class, that can be instantiated
directly for the purpose of testing.
Language constructs
-------------------
Templates
@ -235,4 +307,14 @@ unpack iterator into tuple::
- see tuple-record-init-test.cpp
- used in 'command-simple-closure.hpp' to receive parameter values sent via UI-Bus and package them
into a tuple for invocation of a Steam-Layer command.
+
index and iterate over tuple elements::
This is another example of bridging the compile-time / runtime barrier, using a trampoline.
Obviously such a solution requires that it is somehow possible to define a generic functor / λ that
can accept any of the tuple elements. And, in order to build a runtime index access, we additionally
need to convert each of the tuple elements into a _common reconciled type_.
+
- If such a type exists, can be figured out with the help of the `meta::CommonReturn` _metafunction_
- `lib::meta::TupleIdxAdaptor` provides the necessary machinery to define a subscript `operator[]`
- see `TupleIdxAdaptor_test` for some usage examples, including a runtime iteration...

View file

@ -9,34 +9,34 @@ Technical Documentation
// Menu : prepend child overview
This documentation section contains technical documentation about Lumiera. To
get an *overview* of all the internals and components, you might want to read
This documentation section contains technical documentation about Lumiera. +
To get an *overview* of all the internals and components, you might want to read
link:overview.html[Lumiera the Inner Core]
== Three Layers
The technical documentation is split in three parts, one for each of the three
main layers of Lumiera. You may want to read the
link:{ldoc}/design/index.html[Design Documents] first to get an overview of all the
components.
The technical documentation is divided into three main parts and some supplements,
corresponding to the code organisation in three layers. The information here can be
very detailed and technical at times, and the topics are organised by individual components.
* link:stage/index.html[*Stage Layer*] : Documents related to the GTK based Lumiera GUI
* link:steam/index.html[*Steam Layer*] : Documents regarding Session, models and the engine structure
* link:steam/index.html[*Steam Layer*] : Documents regarding Session, the models and the engine
* link:vault/index.html[*Vault Layer*] : Documents about the data and processing backend
To better understand the underlying concepts, it might be helpful to also consult the
link:{ldoc}/design/index.html[Design Documents].
== Tools
.Development
* link:/doxy/[*Doxygen* generated documentation] : API documentation of the Lumiera code.
* link:/doxy/[*Doxygen*] : API documentation of the Lumiera code.
* organisation of the link:code/index.html[Code Base] in general
* link:/x/CodingGuidelines.html[Coding Style] and further guidelines
.Building
* link:build/index.html[Buildsystem] : Installation & compilation tools, dependencies and packaging.
* link:build/index.html[Buildsystem] : Installation and compilation tools, dependencies and packaging.
.HOWTO
* link:/x/DevHowto.html[Developer *HOWTOs*] : Collection of instructions, recipes, hints and similar
materials intended for developers
* link:/x/DevHowto.html[Developer *HOWTOs*] : Collection of instructions, recipes, hints, misdeeds
and confessions and similar helpful resources for developers

View file

@ -8,30 +8,32 @@ NOTE: This page holds content copied from the old "main" TiddlyWiki +
It needs to be reworked, formatted and generally brought into line
********************************************
.Lumiera relies on *test-driven development*
.Lumiera employs test-driven development
This page discusses the overall organisation
of test code, and the tools used for
running test cases
********************************************
Tests are the only form of documentation, known to provides some resilience against
becoming outdated. Tests help to focus on the usage, instead of engaging in spurious
Tests are the only known form of documentation that offers some resilience against
getting outdated. Tests help to focus on the usage, instead of engaging in spurious
implementation details. Developers are highly encouraged to write the tests _before_
the actual implementation, or at least alongside and interleaved with expanding the
feature set of the actual code.
There may be exceptions to this rule. Not every single bit needs to be covered by tests.
Some features are highly cross-cutting and exceptionally difficult to cover with tests.
And sometimes, just an abstract specification is a better choice.
Test code must not duplicate the logic of implementation code, it should represent an
abstracted, outside, logical view of the matter at hand. And sometimes it is the better
choice to simply write an abstract specification in understandable language.
As a rule of thumb, consider to write test code which is easy to read and understand,
_like a narration_ to show off the relevant properties of the test subject.
Test Structure
--------------
- a *test case* is an executable, expected to run without failure, and optionally producing
some verifiable output
- simple test cases may be written as stand-alone application, while the more tightly
integrated test cases can be written as classes within the Lumiera application framework.
- a *test case* is an executable, expected to run without failure; test code not only invokes
a _subject under test_ -- the important part is to _verify_ its behaviour.
- test cases should use the +CHECK+ macro of NoBug to verify test results, since the normal
assertions may be de-configured for optimised release builds.
- our test runner script 'test.sh' provides mechanisms to check for expected output
@ -41,15 +43,13 @@ several functions within the same test fixture. This allows to create a ``narrat
in the code: first do this, than do that, and now that, and now this should happen...
Generally speaking, it is up to the individual test to take care or isolate himself
from any _dependencies_. Test code and application code uses the same mechanisms
for accessing other components within the application. Up to now (2014), there
was no need for any kind of _dependency injection_, nor did we face any
difficulties with tainted state.
for accessing other components within the application.
Test classes are organised into a tree closely mirroring the main application source
code tree. Large sections of this test tree are linked together into *test libraries*.
Some of these are linked against a specific (sub)scope of the application, like e.g.
only against the support library, the application framework or the vault. Since we
use _strict dependencies_, this linking step will spot code not being placed at the
use _strict dependencies_, this linking step will also spot code not placed at the
correct scope within the whole system. As a final step, the build system creates a
*test runner* application (`target/test-suite`), which links dynamically against _all_
the test libraries and thus against all application dependencies.
@ -81,9 +81,9 @@ to help with automating the build and test execution, test code should adhere to
- test classes should be placed into a sub namespace +...::test+
- specific definitions for a single test case may rely on global variables,
but these should live within an anonymous namespace
- all test executables should be named according to the pattern +test-*+
- free standing test executables should be named according to the pattern `test-*`
- all test source code is within the 'tests' subtree
- immediately within the 'tests/' directory are the test collection definitions +*.tests+,
- immediately within the 'tests/' directory are the test collection definitions `*.tests`,
ordered by number prefixes
- below are the directories with test cases, to be grouped into the aforementioned test-executables.
- we distinguish two ways to write and link tests: _standalone_ and _suite_
@ -107,7 +107,7 @@ See the following
.hello-world-test example
[source,C]
----------------------------------------------------------------
#include "lib/test/run.hpp"
#include "test/run.hpp"
#include <iostream>
using std::cout;

View file

@ -18,22 +18,25 @@ The main Website.
- Data is in '/var/www'. The Website is a git repository.
- Configuration is at '/etc/apache2/sites-available/lumiera.conf'
- cehteh cares for the administration.
- cehteh and ichthyo are responsible for the administration.
- To proprose some changes clone the git repository and send a
notfication/merge-request to the mailinglist.
Some directories symlink to other directories (see below).
https://staging.lumiera.org
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The »staging site«
~~~~~~~~~~~~~~~~~~
Testing repository for the Website. Web development is done and
reviewed here before it gets pushed to the main Website.
Testing repository for the Website. It is configured in Apache
to be served from a separate virtual host, with an obvious DNS
name (which _must not_ be mentioned in published content!).
Web development is done and reviewed here before it gets
pushed to the main Website.
- Data is in '/var/www-staging'. The Website is a git repository.
- Configuration is at '/etc/apache2/sites-available/staging.conf'
- cehteh cares for the administration.
- cehteh and ichthyo are responsible for the administration.
- To proprose some changes clone the git repository and send a
notfication/merge-request to the mailinglist.
@ -108,5 +111,113 @@ Upload directory where people can rsync data.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Clone of the lumiera source repository to serve the documentation within.
/var/local/trac/
~~~~~~~~~~~~~~~~
This is the configuration root of our https://trac.edgewall.org/[Trac] installation
for the Lumiera issue tracker. This is actually a Git repository, since our setup
became gradually more elaborate over time. We even have some local tweaks to
the CSS styles and the Subtickets-Plugin (Python code).
- a copy of the Apache configuration is checked in as well
- note some scripts in the 'scripts' subdirectory
- an export of current Trac resources was dumped into 'static'
and is served directly by Apache from there
- the actual »Trac-environment« with config and SQLite-DB is in 'trac-env'
- all source trees of all essential plug-ins are checked into 'PluginBuild',
so that any plugin could be regenerated by `python3 setup.py bdist_egg`
- a dump of the TracDB is also checked into that Git
Setup and Configuration
-----------------------
- Apache is configured with the `worker'-MPM
- Trac runs under WSGI configured with a pool of _multithread daemon processes_
Load and Calibration
~~~~~~~~~~~~~~~~~~~~
As of 1/2026, we employ a simplistic load monitoring based on evaluating the
running processes and some server statistics. This is implemented as a bash script,
checked in as '/var/local/trac/scripts/collect-server-stats' and configured to run
every two minutes (via SystemD timer). The resulting data trail can be found in
'/var/log/apache2/loadwatch.csv', with the following columns:
- Timestamp: ISO UTC min
- Sum webserver...
* Processes
* Threads
* overall resident memory (KiB)
* factor of overall memory resident
- Apache
* workload factor
* Processes
* Threads
* resident memory
- WSGI-Trac
* workload factor
* Processes
* Threads
* resident memory
- Server-statistics
* ∅ requests per second
* ∅ request duration in ms
* currently active slots
* currently idle workers
* scoreboard of slot allocation
The parameters of the Apache MPM and the WSGI daemon are then tuned such
as to make good use of the available memory, while avoiding excess idle workers.
Anubis
~~~~~~
https://anubis.techaro.lol/[Anubis] is a service that was discussed controversially
in the community this year. It was created by a
https://github.com/Xe[Canadian Open-Source programmer] as an immediate stopgap
to help protect the _»small internet«_ from the endless storm of requests that
flood in from AI companies. It is configured as a gateway proxy, which classifies
incoming requests based on request properties and sends suspicious clients first
to a JavaScript proof-of-work challenge. After passing Anubis, the client gets
a Cookie to prevent further challenges for some limited time.
Anubis was reported to be very effective for keeping aggressive scraper bots away,
yet the bots have adapted already to some degree. So this remains a game of
cat and mouse. Our first evaluations however indicate very promising numbers,
by keeping out 92% of PageImpressions and UniqueUsers, thereby reducing
the traffic deliverey by the Lumiera issue tracker by more than 80%.
Introducing such an entrance gateway may be seen as an extreme measure;
the rationale is that we have reached such a level of threat that generic rate limits
would have to be so severe that they practically render those resources useless.
Without these protective measures, we would no longer be able to provide Lumiera.org
as a free resource to everyone.
[underline]#Legal stance#: I consider this usage covered by the provisions of GDPR.
Setting this cookie is justified by our legitimate interest of keeping the service operational;
we evaluate IP data patterns for the sole purpose of detecting abuse of our website,
and we state that fact clearly on our GDPR page.
Setup
^^^^^
Anubis runs as single-instance SystemD service unit `anubis@lumiera`, with transient
storage backend only. It is integrated into the Website as a _gateway_ (reverse proxy).
Anubis is configured by a set of rules in https://cel.dev/[Common Excpression Language].
The active rule set is configured in '/etc/anubis' and also checked into the
xref:_var_local_trac[Trac-Git]. Rules are evaluated according to the _commited-choice_
pattern, i.e. the first rule where the guard condition matches will terminate the
evaluation and yield a decision. Anubis was installed from a custom DEB package
and ships with
https://github.com/TecharoHQ/anubis/tree/main/data[a set of rule building blocks],
that can be imported from the `(data)` prefix -- which translates into the path
'/usr/share/doc/anubis/data'
TIP: https://anubis.techaro.lol/[Homepage]
· https://anubis.techaro.lol/docs/admin/native-install[Installation]
· https://anubis.techaro.lol/docs/admin/environments/apache[Apache-setup]
· https://github.com/TecharoHQ/anubis[Github]
As of 1/2026, Anubis protects our
https://issues.lumiera.org/timeline[Issue tracker],
the https://git.lumiera.org/[Git repository browser] and the
https://lumiera.org/doxy/[API-doc] -- which together constitute
the majority of pages accessible on the website.

View file

@ -109,7 +109,7 @@ Configuration
~~~~~~~~~~~~~
All the current configuration and the state of the Debian depot index files is tracked
in the aforementioned ``debian/lumiera'' Git repository, on a separate branch called
https://git.lumiera.org/?p=debian/lumiera;a=shortlog;h=refs/heads/depot[`depot']
https://git.lumiera.org/gitweb?p=debian/lumiera;a=shortlog;h=refs/heads/depot[`depot']
TIP: this is a slightly unconventional setup, insofar the `depot' branch carries
a directory tree that is completely separate from the main Lumiera source tree, which

View file

@ -1,41 +1,70 @@
Library documentation
=====================
//Menu: label Support Lib
Alongside with the core Lumiera Application, we're also creating several *Libraries*. +
This section holds pages with in-depth technical documentation regarding those. Please don't
expect a coherent or complete coverage for the time being -- library functionality is typically
``discovered'' while in the middle of the implementation, then factored out, reused and remoulded
over time.
Alongside with the core Lumiera Application, and as part of ongoing development work,
a lot of *Library code* is written. Many of this happens on occasion. Library solutions
are frequently ``discovered'', while in the middle of the implementation, driven by the
desire to re-use some code structure that was built just recently and in a similar way.
Support Library
---------------
Over time, a huge and growing number of generic building blocks has been collected.
Hoever, not all parts of this collection are equally well maintained and presentable.
It happened time and again that some pattern extracted into the library turned out
to be rather some kind or ephemeral habit -- maybe even a bad habit in hindsight --
and turned into a dead-end eventually. Furthermore, a lot of our library code has
been superseded by very similar features of newer language standards.footnote:[
It seems like developers working on similar problems tend to have similar ideas
to improve productivity. Unfortunately, the solutions resulting from this convergence
of ideas have a tendency not to ``converge'' so well in practical terms.
For example, Lumiera had a tuple type a long time before a mature tuple implementation
was available in the Boost libraries, yet in the end, we could retrofit most of our
tuple-handling library. However, other cases did not play out so well: we evolved
the idea of ``iterators'' to become a lazy processing pipeline, but unfortunately
our design leans more into the direction of functional programming and is much
more expressive and powerful as abbreviation than the »Range-TS« from C++20.
]
The entrance barrier for something to be added to our _Support Library_ is rather low:
- extracting it as a library function _must make the client code clearer_
- it must be possible to describe its purpose and its usage in _generic terms_ --
without referring to data types and definitions from our domain models.
- it must be _essentially stateless_ and self-contained
- it must be easy to document with an unit-test.
Over time, parts of the Support Library have evolved into some kind of a framework:
Notably the link:{ldoc}/design/architecture/time/index.html[time and timecode] handling,
our link:Dependencies.html[dependency and service handling],
the link:DiffFramework.html[»Diff framework«], together with the recursive property trees
used for link:{ldoc}/design/architecture/ETD.html[»External Tree Description«], the
link:iterator.html[iterator pipeline builders], the metaprogramming helpers for transforming
function signatures and generic sequences, and our collection of string conversion, parsing
and rendering functions. These all turned into distinguishing traits of the Lumiera code base.
The link:{ldoc}/technical/overview.html#_library[»Library« section of the »Overview« document]
contains a long list of short descriptions for relevant features of the Support Library.
.how to crack nuts...
[NOTE]
In the how-to section, there is a page with a loose collection of
link:{ldoc}/technical/howto/crackNuts.html[successful solution patterns],
many of which rely on unique features of our Support Lib.
Interface Library
-----------------
The Lumiera Application is designed deliberately in a way to be controllable by scripts. All of
the Session, the models and the vault are fully operational without the GUI. Any significant
functionality can be addressed through the primary Façade Interfaces.
functionality shall be usable through the primary Façade Interfaces alone. Yet in order to create
such an open and extensible application as envisioned, we also need to build up an _interface layer._
Eventually, this should evolve into a binding library for using Lumiera -- however, none of this
interface building work has been accomplished yet, as of 2026. We do not even have a complete
picture how _event sourcing_ meshes together with our UI commands and the configuration rules.
But, in order to make such an open and extensible application a reality, we need to build up
an 'interface layer' -- provided as library for client code and script adapters to link against.
This library allows to access the primary external interfaces and exposes everything needed to
work with the specifics of the application.
[,yellow]#TODO as of 1/2012:# _this is just a nebulous plan for now._ Nothing of the work required
really to address facilities from the outside has been done. All we've managed to achieve is actually
to use clean interfaces and decouple the parts of the application appropriately.
- we still need to identify those core concepts which need to be exposed
- we still need to identify those select core concepts which need to be exposed
- we need to refactor the corresponding data types to resolve cross dependencies to the implementation
- then we could think about moving those data types into an _official Lumiera interface_
Support Library
---------------
There is a huge collection of generic building blocks, re-used more or less frequently.
The're written in a generic way, but in accordance to the rest of the source base, relying only
on the general error handling and logging framework used within Lumiera. (Some facilities may depend
on other parts of the Application; this can be qualified as a bug -- please complain when you've found
something of that sort).
- only then we could think about moving those data types into an _official Lumiera interface_

View file

@ -1,7 +1,7 @@
Lumiera: The Inner Core
=======================
:Author: Lumiera_Core_Developers
:Date: 2012 / 2025
:Date: 2012 / 2026
:toc:
[abstract]
@ -30,7 +30,7 @@ This three Layers are:
The Steam Layer::
Keeps the Session, generates the rendering graphs for sequences,
arranges what to do when and how.
prearranges elements and decides what to do when and how.
The Vault Layer::
Manages worker pools, schedules jobs, does the memory management for the
@ -39,13 +39,11 @@ This three Layers are:
The extra components are:
Lumiera::
The main program itself, which starts all Subsystems and controls the
overall lifecycle.
Common::
Vital components and common services which must be available for pulling up
any part of the system.
The Vessel::
This is the »backbone« of the application, it is rooted below the three
layers and encompasses everything. It defines the periphery, starts all
Subsystems, controls the overall lifecycle and provides vital services
which must be available for engaging any part of the system.
Library::
A lot of (largely) stateless helper functionality used throughout the code.
@ -55,9 +53,9 @@ Coding Style
~~~~~~~~~~~~
The Lumiera team agreed on using GNU coding style with slight adaptations.
Generally speaking, we strive to keep the code base consistent and stick to
widely accepted guidelines and best practices. See our separate
link:{ldoc}/technical/code/codingGuidelines.html[Coding Guidelines] page.
In general, we strive to maintain source code consistency and adhere to widely
accepted guidelines and practices of good craftsmanship. See our separate
link:{ldoc}/technical/code/CodingGuidelines.html[Coding Guidelines] page.
@ -148,7 +146,7 @@ contained within the Session, which belongs to the _steam layer_ below.
User interfaces are implemented as plug-ins and are pulled up on demand,
they won't contain any relevant persistent state beyond presentation.
_As of 2023, the one and only interface under active development is
_As of 2026, the one and only interface under active development is
the Lumiera GTK GUI,_ based on GTK-3 / gtkmm. The sources are in tree
(directory 'src/stage') and it is integrated into the standard build and
installation process. By default, running the 'lumiera' executable will
@ -250,7 +248,7 @@ for each output channel is required.
The player service
^^^^^^^^^^^^^^^^^^
Client code accesses the player (subsystem) through the play-facade (`lumiera::Play`).
Client code accesses the player (subsystem) through the play-facade (`Play`).
The exposed service allows to _set up an output connection for playback or rendering,_
resulting in a play-controller object.
@ -285,11 +283,6 @@ performance critical.
- the OutputSlot exposed by the output manager is responsible for accepting timed frame delivery
Vault Layer
-----------
Engine Interface
~~~~~~~~~~~~~~~~
While on itself just a thin interface and adaptation layer forwarding calls to
@ -318,16 +311,21 @@ the job ticket acts as blueprint for the actual jobs to be enqueued
with the _Scheduler._
Vault Layer
-----------
Scheduler
~~~~~~~~~
The Scheduler serves as the central hub in the implementation of the RenderEngine
and coordinates the processing resources of the application. Its purpose is to
invoke and control the dependency and time based execution of the _Render Jobs._
Regarding architecture, the Scheduler is located in the Vault-Layer and running the Scheduler
is equivalent to activating the »Vault Subsystem«. An EngineFaçade acts as entrance point,
providing high-level render services to other parts of the application: render jobs can be
activated under various timing and dependency constraints, which can be grouped
As far as architecture is concerned, the Scheduler is located in the Vault-Layer and
is a _passive service_ -- like most components in the vault it needs to be put into
use by some coordinating and integrating facility. An EngineFaçade, itself located
above, in the »Steam« layer, acts as entrance point, to provide the high-level
render services to other parts of the application: render jobs can be activated
under various timing and dependency constraints, which can be grouped
into three basic modes of control:
Deadline::
@ -362,6 +360,29 @@ The scheduler is able to observe and maintain dependencies between render jobs,
to break down an extended calculation into a chain of small steps.
The Work Force
~~~~~~~~~~~~~~
Since the typical work job dimensions can be expected to lie in the range of _some milliseconds,_
the Lumiera engine can adopt a statistical model of capacity distribution. This is notably
different to an _operating system scheduler_ where the response times are measured in
microseconds and the coordination of work approaches hardware limits. These observations
lead to the (maybe slightly surprising approach) to rely on an _actively pulling_ work force
combined with a _passive scheduling service._
The _Work Force_ is a **pool of worker threads** with a size typically matching the nominal
number of independent CPU cores in the system. Spinning up the Work Force is equivalent to
activating the »Vault Subsystem«. Deliberately, there is no dedicated ``engine'' or
``manager'' thread. All workers participate in the coordination work on equal footing.
An idle worker enters the (passive) scheduling code, and thereby performs management
work up to the point where the next render job is determined. The worker then leaves
the management realm and continues with performing this job. However, the scheduling
code preloads each worker with some random sleep delay in order to distribute the
``incidence'' of idle workers and to avoid contention. This way, the available
computing capacity is distributed stochastically into that time window where
demand is foreseeable, based on the next job's starting time, the currently
dispatched jobs and the observed load factor.
Resource Management
~~~~~~~~~~~~~~~~~~~
@ -378,12 +399,12 @@ the system and current circumstances.
Common Services
---------------
Vessel: Common Services
-----------------------
Subsystem runner
~~~~~~~~~~~~~~~~
Conceptually, the application is structured into layers. However, as actual runtime
Conceptually, the application is structured into layers. However, as the actual runtime
entities we rely on a small number of link:{ldoc}/design/architecture/Subsystems.html[Subsystems],
which can be started and stopped and have well defined dependencies:
@ -418,17 +439,14 @@ link:{ldoc}/design/architecture/Subsystems.html#_lifecycle[documentation]
[small]#(likewise incomplete, but we're getting there, eventually [yellow-background]##2018##)#
Plugin loader
~~~~~~~~~~~~~
_tbw_
Advice framework
~~~~~~~~~~~~~~~~
This is a ``whiteboard'' system, allowing implementation-level services to _publish_
some piece of information as _advice_, while other parts of the system may pick up
this advice just by a name token, without requiring a direct knowledge of the
original _advisor._ The _Advice System_ is a singleton service maintaining a
lookup and registration data structure. Individual _piece of advice_ elements
original _advisor._ The _Advice System_ is a singleton service that maintains
a lookup and registration data structure. Individual _piece of advice_ elements
are stored _as value copy_. Publishing new advice requires locking, but accessing
advice is lock-free (actually there needs to be a memory barrier ``somewhere'',
otherwise the advice requesting client might not see new advice)
@ -458,13 +476,27 @@ Library
-------
The Lumiera support library contains lots of helper functionality
factored out from the internals and re-used. It is extended as we go.
factored out from the internals and re-used. Some parts evolved into
basic frameworks, that are used pervasively throughout the code base.
- link:{ldoc}/design/architecture/time/index.html[time and timecode]
(-> xref:_time[below])
- link:Dependencies.html[dependency and service handling]
(-> xref:_singletons_and_dependencies[below]),
- the link:DiffFramework.html[»Diff framework«]
- property trees for link:{ldoc}/design/architecture/ETD.html[»External Tree Description«]
(-> xref:_external_tree_description[below])
- the link:iterator.html[iterator pipeline builders]
(-> xref:_iterators[below])
- various metaprogramming helpers (function signatures and generic sequences)
- string conversion, parsing and rendering functions
practical shortcuts
~~~~~~~~~~~~~~~~~~~
The header 'lib/util.hpp' defines some shortcuts heavily used throughout
the code base. The idea is to highlight a common semantic meaning, while
hide differentiation on the technical details.
The header 'lib/util.hpp' defines some shortcuts that are used almost
everywhere in the code base. The general idea is to highlight a common semantic meaning,
while hide the differentiation related to the technical details.
isnil:: indicate a _missing value_, irrespective if this is a NULL pointer,
an empty string or an empty container. Several of our custom wrappers also
@ -473,8 +505,29 @@ isnil:: indicate a _missing value_, irrespective if this is a NULL pointer,
contains:: indicate that a value is _somehow contained_ within a collection,
irrespective if this is a set, a map, a vector or string (substring test)
sanitise:: make any string usable as identifier.
limited:: to bracket some value.
unConst:: a functional notation to cast away `const`, which is used typically
at the barrier between a type safe front-end and a low-level back-end,
and to avoid needless duplication of accessor code implementation
for sake of providing `const` information functions.
isSameObject:: identity test based on bare memory location,
which is used a lot in assertions and unit tests
string shorthands::
trim, startsWith, endsWith, boolVal, sanitise (turn arbitrary string into an identifier)
Literals and Symbols
~~~~~~~~~~~~~~~~~~~~
Besides the well-known C++ strings (and `string_view`), Lumiera introduces
two marker types for string tokens
Literal:: a C-string literal directly given in the Source code
Symbol:: an _interned_ token stored in a Symbol table, so that its
address hash can be used as unique identity
Locking
@ -892,6 +945,17 @@ and assignment incur the cost of an additional virtual call,
assuming the target objects cooperate by mixing in a
copy management interface.
.uninitialised storage
A cornerstone of the C++ language is the deterministic and reliable invocation
of constructors and destructors. However, when building low-level storage schemes,
a frequent requirement is to cover the precise storage of an object collection,
_without_ actually invoking any constructor or destructor. The `UninitialisedStorage`
template is a building block to support this task. It is parametrised with a type
and an element count, and provides a force-casted array-like access to the storage cells.
This can be used either to placement-construct some elements on-demand, or to ``project''
an object-like view over existing data in the storage.
Unique Identifiers
~~~~~~~~~~~~~~~~~~

View file

@ -1,5 +1,5 @@
Technical Documentation: Stage Layer
====================================
Eventually, this will have technical documentation for the GUI.
This section offers a loose collection with detailed information regarding the GUI implementation.

View file

@ -1,27 +1,36 @@
Technical Documentation: Steam-Layer
====================================
Eventually, this section will hold technical documentation for the Steam-Layer.
As implementation progresses, this section will gradually compile
the technical documentation for the Steam layer.
Development Scrapbook TiddlyWiki
--------------------------------
Hermann Voßeler, core developer of the Steam-Layer uses an separate embedded wiki
for his day-to-day design sketches, notes and also for the more persistent planning.
This *TiddlyWiki* is a nifty JavaScript wiki embedded in a single HTML page; as such
it lives right within the source tree, where it can be worked on alongside with the
code. Besides the actual planning and descriptions, this wiki also links to the
UML diagrams created with *BOUML* during the ongoing planning and design.
It is sometimes difficult to write complete documentation while a system is still under construction.
For taking work notes and formulating new ideas while working, _Ichthyostega_ frequently uses the
https://classic.tiddlywiki.com/[TiddlyWiki] -- which is a nifty JavaScript wiki embedded within a
single HTML page; as such it lives right within the source tree, where it can be worked on alongside
with the code. Notes and sketches for the planning often evolve into detailed descriptions, complete
with test data and UML diagrams to document the structure. Material drafted in the TiddlyWiki
will be moved gradually into this documentation section, as time permits.
This setup started as a temporary solution -- but as uWiki is not expected to come
to life soon, the current website can't serve as replacement for a real wiki, an
we'll stick to that situation for the time being. As the content in the TiddlyWiki
becomes more mature and stable, it will be moved over into this section here.
This arrangement started as a temporary solution at a time where we did not have a suitable website
for permanent documentation -- and it was kept ever since, as the TiddlyWiki is very convenient
to work with, once you are accustomed to its quirks.footnote:[
It is quite fluid and inspiring to work with indeed. The content is an unordered collection
of small notepad pages, called ``Tiddlers'', which can be opened and closed conveniently.
Any "CamelCaseWord" becomes a new Tiddler, recent changes appear in a timeline, and Tiddlers
can be organised with tags. The only point to be careful is that you need to _save_
your work, which can be achieved with the `save' action in the menu to overwrite
the old version in the Git tree. Sometimes this might produce Git conflicts,
which are easy to resolve, since the content is plain text.
]
Additions to the TiddlyWiki are usually made during coding, and are deliberately committed always
together with the corresponding code. As such, these additions will propagate to Lumiera/Master eventually;
yet to make the newest development notes easier to access, we have put up a `rsync`ed version of the
TiddlyWiki online for direct access over the internet (which then is of course read-only).
While additions to the TiddlyWiki generally propagate to Lumiera/Master through
the normal merges, we've put up a rsynced version of the TiddlyWiki online for
direct access (of course read-only).
-> access the Proy-Layer link:/wiki/renderengine.html[TiddlyWiki online here]
-> access the link:/wiki/renderengine.html[TiddlyWiki online here]

View file

@ -113,8 +113,11 @@ each have a distinct _type_ which explicitly identifies the possible connections
modules. Within these constraints, any combination is possible without
hidden restrictions.
Lumiera is not a set of Lego bricks, nor is it a business application
driven by finite usage stories.
However, the application strives for a certain degree of internal coherence. +
Lumiera is not a set of Lego bricks allowing you to build ``just anything''
(in a clumsy way) -- nor is it the next universal programming platform or
application framework driven by pre-canned success stories, where you
``just push the button'' and magic does the rest.
Medium Level Abstraction and Project Specific Conventions
@ -131,9 +134,8 @@ details such as header format conversion or access to individual channels.
To complement this approach, Lumiera does _not_ rely on hard-wired, global
conventions -- rather we allow the building up of project specific conventions and
rules xref:rules[<-] to fit a given requirement and preferred working style.
Lumiera will be supplied with a conventional template and a default
configuration to ease the learning curve for new users.
However, in its default configuration, Lumiera will be supplied with a conventional
UI template and key bindings to ease the learning curve for new users.
[[graphs]]
@ -144,12 +146,12 @@ Processing video (and audio) data can be conceived as a graph (more precisely
a _directed acyclic graph_). In this model of video processing, data flows
along the edges of the graph and is processed at the nodes.
The following figure depicts manipulating video data as a graph. The nodes at
the top of the graph are where data input occurs. From there, the data moves on
to the next nodes: the direct siblings of the root. Here, the video data is
pre-processed. All other operations on the data can be represented by
nodes, and data flows from one operation to the next along the nodes of the
graph.
The following figure depicts manipulating video data as a graph.
Input data is retrieved from the source nodes, which are located at the
top of the figure. From there, the data moves on to the next stage, where
video and audio data and meta information is pre-processed. All the further
operations on data can be represented by nodes, and data flows from one operation
to the next along the nodes of the graph.
image:{imgd}/lumiera_big_graph.png[Example for a graph]
@ -218,16 +220,15 @@ Lumiera consists of three major parts:
In this section, we discuss the user's perspective when working with the GUI.
Although Lumiera will initially ship with a standard, default GUI, we do not presume that this GUI will be suitable for all uses.
We expect there to be multiple, different GUIs, each designed for different
kinds of tasks. We expect that some of these will be written by users, and the
steam-layer is designed to facilitate this easily.
Although Lumiera ships with a conventional desktop UI by default, we do not presume
that this GUI will be suitable for all uses. We expect there to be several specialised
UIs eventually, designed to handle very specific kinds of tasks.
Indeed, Lumiera can even work
satisfactorily without a GUI, for example, for special purposes
(e.g. as headless render-node xref:rendernode[<-] or frameserver
xref:frameserver[<-]). Scripts will eventually be written
to facilitate automated processing.
xref:frameserver[<-]). A script interface will be added at some
point to facilitate automated processing.
Now it is time to take a look at the preliminary Lumiera GUI:
@ -505,21 +506,19 @@ A Plug-in is a kind of generalisation of a library.
All applications use, to varying degrees of intensity, libraries. A programmer
will not reinvent the wheel each time he sits down to programme an
application. A programmer will typically borrow and use features and
functionality from other programmers---or even borrow from himself, stuff
functionality from other programmers -- or even borrow from himself, stuff
written long ago in the past. Such features are collected together in
libraries.
A library is used in an application by _linking_ the library into the
application. (There are other things to be done, but we'll call these 'details',
which wont concern us here.) There are different ways to _link_ a library
into an application: linking statically and linking dynamically.
A library is used in an application by _linking_ the library into the application.
However, there are different ways to _link_ a library: linking statically and
linking dynamically.
_Static Linking_ is done while the application is being built, or
compiled. It is performed by the linker, which executes as the last step
of the compilation process. The linker can perform some sanity checks
(mostly checks regarding formally correct function invocation) and warn the user
hat some particular feature is being used incorrectly. The user can then correct
the offending code, and recompile. +
of the compilation process. The linker can perform some sanity checks and
some optimisation. In the end, the functionality of the library is ``baked into''
the resulting executable. +
There are a number of disadvantages associated with static linking. Features and
libraries are being constantly improved. If the application wants to use new
features, it will have to be recompiled with the new library which provides the
@ -532,15 +531,14 @@ features can be used by the application. The features provided by a dynamic
library are loaded when the application starts to run.
However both methods exhibit a number of shortcomings. Wouldn't it be better if
all features could be loaded only when needed? If features could be loaded only
when needed, then they could also be unloaded when not required, thus saving
memory and possibly increasing performance. This scheme of making features
available to an application is known as run-time linking, aka plug-ins.
all features could be loaded only when needed? This scheme of making features
available to an application on-demand is known as run-time linking. A component
intended to be added this way, at run time, by the user, is known as a Plug-in.
Plug-ins offer other benefits: the application can continue to use both the old
features and the new features together, side-by-side, by using the version
number associated with the plug-in. This saves the application from the havoc
of library incompatibility that is
associated with other linking methods.
number associated with the plug-in. Furthermore, Plug-ins can load other Plug-ins
and thus extend the application in creative ways, beyond anything the original
developers of the application had planned.
Most modern applications use plug-ins, some are heavily dependent on plug-ins
and only provide limited functionality without any plug-ins.

View file

@ -54,7 +54,7 @@ in addition to the following tools and libraries:
* https://www.boost.org/[Boost libraries]
* https://github.com/bplaum/gavl[GAVL library]
* https://nobug.pipapo.org/[NoBug] ~([purple]#⚠ see below#)~
* Lumiera https://git.lumiera.org/?p=LUMIERA;a=summary[source code]
* Lumiera https://git.lumiera.org/gitweb?p=LUMIERA;a=summary[source code]
The GUI depends on the following:

View file

@ -115,9 +115,9 @@ sudo make install
but was orphaned in 2021. Since it is currently a prerequisite for building Lumiera,
we maintain a clone of the original repository and a Debian package on Lumiera.org.
This is relevant both for
https://git.lumiera.org/?p=debian/gdl;a=summary[libGDL (3.40) +git://git.lumiera.org/debian/gdl+]
https://git.lumiera.org/gitweb?p=debian/gdl;a=summary[libGDL (3.40) +git://git.lumiera.org/debian/gdl+]
and the
https://git.lumiera.org/?p=debian/gdlmm;a=summary[GDL C++ bindings (3.7.3) +git://git.lumiera.org/debian/gdlmm+].
https://git.lumiera.org/gitweb?p=debian/gdlmm;a=summary[GDL C++ bindings (3.7.3) +git://git.lumiera.org/debian/gdlmm+].
]
* first you have to build libGTL itself...

View file

@ -1,6 +1,7 @@
/* try.cpp - to try out and experiment with new features....
* scons will create the binary bin/try
*/
// 03/26 - runtime index access and iteration over tuple-likes
// 06/25 - provide a concept to accept _tuple-like_ objects
// 06/25 - investigate function type detection of std::bind Binders
// 12/24 - investigate problem when perfect-forwarding into a binder
@ -10,22 +11,28 @@
/** @file try.cpp
* Develop a concept to detect _tuple-like_ classes, based on the requirements
* of the »tuple protocol«. Using some ideas from [Stackoverflow] as starting point.
* However, we model both a _friend function_ `get` and a similar member function
* as alternatives for element access, and we break down the checks into sub-concepts.
* [Stackoverflow]: https://stackoverflow.com/q/68443804/444796
* Work out a construction scheme to build an efficient element-accessor to data
* in a tuple-like with run-time indexing. As an extension, run-time pulled iteration
* can be established, using the IterExplorer framework.
* @see lib::meta::FunTrampoline
* @see lib::meta::TupleIdxAdaptor
*/
#include "lib/format-cout.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/test/diagnostic-output.hpp"
#include "lib/format-util.hpp"
#include "test/test-helper.hpp"
#include "test/diagnostic-output.hpp"
#include "lib/meta/common-result.hpp"
#include "lib/meta/tuple-concept.hpp"
#include "lib/meta/tuple-helper.hpp"
#include "lib/meta/function.hpp"
#include "lib/iter-adapter.hpp"
#include "lib/hetero-data.hpp"
#include "lib/meta/trait.hpp"
#include "lib/util.hpp"
#include <concepts>
#include <array>
using std::string;
@ -41,14 +48,263 @@ template<lib::meta::tuple_like X>
void
show()
{
cout << "Tup!! "<< lib::test::showType<X>() <<endl;
cout << "Tulik!! "<< ::test::showType<X>() <<endl;
lib::meta::forEachIDX<X> ([](auto i)
{
using Elm = std::tuple_element_t<i, X>;
cout <<" "<<uint(i)<<": "<< lib::test::showType<Elm>() <<endl;
cout <<" "<<uint(i)<<": "<< ::test::showType<Elm>() <<endl;
});
}
namespace lib {
namespace meta{
#if false ///////////////////////////////////////////////////////////////////////////////////////////////////TODO : new generic library functions
/**
* Concept to require a generic type sequence `Types<TYPES...>`
*/
template<class TSEQ>
concept typeseq = requires (TSEQ seq)
{
// TSEQ can invoke a function with a Types<TYPES> subclass
[]<typename...TYPES>(Types<TYPES...>&){/*placeholder*/}(seq);
};
#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions
template<template<class> class BUILD, typeseq TSEQ>
class FunTrampoline
{
using AllElm = ElmTypes<TSEQ>;
using Indices = AllElm::Idx;
using Builder = BUILD<TSEQ>;
using Functor = Builder::Functor;
using Table = std::array<Functor, AllElm::SIZ>;
template<size_t...idx>
static constexpr Table
buildFunctors (std::index_sequence<idx...>)
{
return {Builder().template build<idx>()...};
}
static constexpr Table functors = buildFunctors (Indices{});
public:
static constexpr size_t size() { return functors.size(); }
static constexpr size_t memSiz() { return sizeof(functors); }
static Functor const&
dispatch (size_t idx)
{
ENSURE (idx < functors.size());
return functors[idx];
}
using ResType = _Fun<Functor>::Ret;
};
#if false ///////////////////////////////////////////////////////////////////////////////////////////////////TODO : new generic library functions
template<typename T>
using isConst = std::is_const<remove_reference_t<T>>;
template<typename T>
using isLRef = std::is_lvalue_reference<T>;
template<class SEQ>
using Any_Const = ElmTypes<SEQ>::template OrAll<isConst>;
template<class SEQ>
using All_LRef = ElmTypes<SEQ>::template AndAll<isLRef>;
template<class SEQ>
static constexpr bool Any_Const_v = Any_Const<SEQ>::value;
template<class SEQ>
static constexpr bool All_LRef_v = All_LRef<SEQ>::value;
template<typename X, typename U, typename...TYPES>
struct ProbeCommonType
{
template<typename T1, typename T2>
using Reconciled = decltype(true ? std::declval<T1>() : std::declval<T2>());
template<typename RES>
struct FoundResult
: std::true_type
{
using Type = RES;
};
struct NoResultFound
: std::false_type
{ };
template<class T1
,class T2
,typename = Reconciled<T1,T2>>
static FoundResult<Reconciled<T1,T2>>
check(int);
template<class,class>
static NoResultFound
check(...);
using FirstResult = decltype(check<X,U>(0));
template<bool success, typename...TS>
struct Join
{
using Result = FirstResult;
};
template<typename T1, typename...TS>
struct Join<true, T1,TS...>
{
//wenn RES erfolgreich
using CT = FirstResult::Type;
using Result = ProbeCommonType<CT,T1,TS...>::Result;
};
using Result = Join<FirstResult::value, TYPES...>::Result;
};
template<typename...TYPES>
struct CommonResult
: ProbeCommonType<TYPES...>::Result
{ };
template<typename T1>
struct CommonResult<T1>
: CommonResult<T1,T1>
{ };
template<>
struct CommonResult<>
: std::false_type
{ };
template<typename...TYPES>
struct CommonResult<Types<TYPES...>>
: CommonResult<TYPES...>
{ };
template<typename...TYPES>
using CommonResult_t = CommonResult<TYPES...>::Type;
template<typename...TYPES>
static constexpr bool has_CommonResult_v = CommonResult<TYPES...>::value;
template<typename...TYPES>
concept reconcilable = requires
{
typename CommonResult<TYPES...>::Type;
};
template<typename...TYPES>
concept reconcilable_ref = reconcilable<TYPES...>
and isLRef_v<CommonResult_t<TYPES...>>;
template<typename...TYPES>
struct CommonResultTraits
{
static_assert (reconcilable<TYPES...>, "unable to reconcile these types");
using CommonVal = std::common_type_t<TYPES...>;
using ResType = CommonResult_t<TYPES...>;
//NOTE: std::common_type decays (strips CV and ref)
static constexpr bool makeConst = isConst_v<ResType>;
static constexpr bool exposeRef = isLRef_v<ResType>;
};
#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions
template<class TUP>
class TupleIndexAdaptor
: public TUP
{
template<typeseq SEQ>
struct SubscriptBuilder
{
using Tuple = RefTraits<TUP>::value_type;
using AddRefs = ElmTypes<SEQ>::template Apply<std::add_lvalue_reference_t>;
using ResType = CommonResult_t<AddRefs>;
using Functor = ResType(*)(Tuple&);
template<size_t idx>
constexpr auto
build()
{
return [](Tuple& tuple) -> ResType
{
return getElm<idx> (tuple);
};
}
};
using TypeSeq = ElmTypes<TUP>::Seq;
using Accessor = FunTrampoline<SubscriptBuilder, TypeSeq>;
public:
using ResType = Accessor::ResType;
using reference = ResType;
using value_Type = RefTraits<ResType>::value_type;
template<typename T>
TupleIndexAdaptor(T&& dataSrc)
: TUP{forward<T> (dataSrc)}
{ }
using TUP::TUP;
static constexpr size_t size() { return Accessor::size(); }
ResType
operator[] (size_t idx)
{
return Accessor::dispatch(idx)(*this);
}
};
template<class TUP>
TupleIndexAdaptor(TUP) -> TupleIndexAdaptor<std::decay_t<TUP>>;
}}
using namespace lib::meta;
template<typeseq SEQ>
struct Stringify
{
using Tuple = ElmTypes<SEQ>::Tup;
using Functor = string(*)(Tuple const&);
template<size_t idx>
constexpr auto
build()
{
using ElmT = std::tuple_element_t<idx, Tuple>;
return [](Tuple const& tuple)
{
return util::toString<ElmT> (getElm<idx> (tuple));
};
}
};
struct Uber { };
struct Untr : Uber { };
struct Unta : Uber { };
int
main (int, char**)
@ -57,20 +313,10 @@ main (int, char**)
using Arr = std::array<int,3>;
using Hetero = lib::HeteroData<int,string>::Chain<short>::ChainExtent<bool,lib::meta::Nil>::ChainType;
SHOW_EXPR((lib::meta::tuple_sized<Tup> ))
SHOW_EXPR((lib::meta::tuple_sized<Arr> ))
SHOW_EXPR((lib::meta::tuple_sized<Hetero> ))
SHOW_EXPR((lib::meta::tuple_sized<int> ))
SHOW_EXPR((lib::meta::tuple_element_accessible<Tup,0>))
// SHOW_EXPR((lib::meta::tuple_element_accessible<Tup,2>))
SHOW_EXPR((lib::meta::tuple_element_accessible<Hetero,0>))
SHOW_EXPR((lib::meta::tuple_accessible<Tup>))
SHOW_EXPR((lib::meta::tuple_accessible<Hetero>))
SHOW_EXPR((lib::meta::tuple_like<Tup> ))
SHOW_EXPR((lib::meta::tuple_like<Arr> ))
SHOW_EXPR((lib::meta::tuple_like<Hetero> ))
SHOW_EXPR((lib::meta::tuple_like<string> ))
SHOW_EXPR((lib::meta::tuple_like<int> ))
show<Tup>();
@ -78,25 +324,112 @@ main (int, char**)
show<Hetero>();
show<int>();
SHOW_EXPR((std::tuple_size_v<const Tup>))
using Elm1 = std::tuple_element_t<1, const Tup>;
SHOW_TYPE(Elm1)
using TupConstSeq = lib::meta::ElmTypes<const Tup>::Seq;
SHOW_TYPE(TupConstSeq)
using TupStringy = FunTrampoline<Stringify, ElmTypes<Tup>::Seq>;
SHOW_EXPR(TupStringy::size())
using T1 = decltype(lib::meta::getElm<0> (std::declval<Tup>()));
SHOW_TYPE(T1)
using T2 = decltype(lib::meta::getElm<0> (std::declval<Tup&>()));
SHOW_TYPE(T2)
using T3 = decltype(lib::meta::getElm<0> (std::declval<Tup const&>()));
SHOW_TYPE(T3)
using H1 = decltype(lib::meta::getElm<4> (std::declval<Hetero>()));
SHOW_TYPE(H1)
using H2 = decltype(lib::meta::getElm<4> (std::declval<Hetero&>()));
SHOW_TYPE(H2)
using H3 = decltype(lib::meta::getElm<4> (std::declval<Hetero const&>()));
SHOW_TYPE(H3)
Tup tup23{2,3};
Tup tup42{4,2};
SHOW_TYPE(decltype(std::get<1>(tup42)))
SHOW_EXPR(std::get<1>(tup42))
SHOW_EXPR(TupStringy::dispatch(0)(tup23))
SHOW_EXPR(TupStringy::dispatch(0)(tup42))
SHOW_EXPR(TupStringy::dispatch(1)(tup23))
SHOW_EXPR(TupStringy::dispatch(1)(tup42))
using T1 = Types<int const&,long&, char&>;
using T2 = Types<int*,void*>;
using T3 = Types<int,string>;
SHOW_EXPR((has_CommonResult_v<T1>))
SHOW_EXPR((has_CommonResult_v<T2>))
SHOW_EXPR((has_CommonResult_v<T3>))
SHOW_EXPR((has_CommonResult_v<>))
SHOW_EXPR((has_CommonResult_v<float*>))
SHOW_EXPR((has_CommonResult_v<float*,void* const>))
SHOW_TYPE(CommonResult_t<T1>)
SHOW_TYPE(CommonResult_t<T2>)
SHOW_TYPE(CommonResult_t<float*>)
using CoTx = CommonResult_t<float*,float const*>;
SHOW_TYPE(CoTx)
TupleIndexAdaptor<Tup> idxAdapt{tup42};
SHOW_EXPR(sizeof(tup42))
SHOW_EXPR(sizeof(idxAdapt))
SHOW_TYPE(TupleIndexAdaptor<Tup>::ResType)
SHOW_EXPR(idxAdapt.size())
SHOW_EXPR(idxAdapt[0])
SHOW_EXPR(idxAdapt[1])
SHOW_EXPR(getElm<0>(static_cast<Tup&> (idxAdapt)))
SHOW_EXPR(getElm<1>(static_cast<Tup&> (idxAdapt)))
SHOW_TYPE(decltype(idxAdapt[1]))
using Tup2 = std::tuple<int,int&, int>;
int ii{23};
Tup2 tup21{-1,ii,55};
TupleIndexAdaptor idxApt2{tup21};
SHOW_TYPE(decltype(tup21))
using IdxApt2 = decltype(idxApt2);
SHOW_TYPE(IdxApt2)
SHOW_TYPE(IdxApt2::ResType)
SHOW_EXPR(idxApt2[0])
SHOW_EXPR(idxApt2[1])
SHOW_EXPR(idxApt2[2])
SHOW_EXPR(++idxApt2[0])
SHOW_EXPR(idxApt2[1]++)
SHOW_EXPR(++++idxApt2[2])
SHOW_EXPR(tup21)
SHOW_EXPR(Tup2(idxApt2))
SHOW_EXPR(ii)
using TupIt = lib::IterableDecorator<lib::IdxStoreCore<IdxApt2>>;
TupIt tupIt{move(idxApt2)};
SHOW_EXPR( bool(tupIt) )
SHOW_EXPR( *tupIt )
SHOW_TYPE(decltype(*tupIt))
++*tupIt;
--ii;
SHOW_EXPR( *tupIt )
SHOW_EXPR(util::join(tupIt, "▶▶"))
SHOW_EXPR(( reconcilable<long,string> ))
SHOW_EXPR(( reconcilable<long&,string&> ))
SHOW_EXPR(( reconcilable<long,short> ))
SHOW_EXPR(( reconcilable<long&,short&> ))
SHOW_EXPR(( reconcilable_ref<long,string> ))
SHOW_EXPR(( reconcilable_ref<long&,string&> ))
SHOW_EXPR(( reconcilable_ref<long,short> ))
SHOW_EXPR(( reconcilable_ref<long&,const long&> ))
using TX = CommonResult_t<long&,const long&>;
SHOW_TYPE(TX)
SHOW_EXPR(( reconcilable<float*,void*> ))
SHOW_EXPR(( reconcilable<void,void*> ))
SHOW_EXPR(( reconcilable<float*&,void*&> ))
using TX2 = CommonResult_t<float*&,float*&>;
SHOW_TYPE(TX2)
SHOW_EXPR(( reconcilable<Uber,Unta> ))
SHOW_EXPR(( reconcilable<Untr,Unta> ))
SHOW_EXPR(( reconcilable<Untr&,Unta&> ))
SHOW_EXPR(( reconcilable<Uber&,Unta&> ))
SHOW_EXPR(( reconcilable_ref<Uber&,Unta&> ))
SHOW_EXPR(( reconcilable_ref<Uber&,Unta> ))
SHOW_EXPR(( reconcilable<Uber&,Unta> ))
SHOW_EXPR(( reconcilable<Uber,Untr&> ))
using TX3 = CommonResult_t<Uber&,Unta>;
SHOW_TYPE(TX3)
using TX4 = CommonResult_t<Unta&, const Uber&>;
SHOW_TYPE(TX4)
SHOW_EXPR(( reconcilable<Uber&,Unta&,Untr&> ))
using TX5 = CommonResult_t<const Uber&,Unta&,Untr&>;
SHOW_TYPE(TX5)
using TX6 = CommonResult_t<const int&,uint16_t,int64_t>;
SHOW_TYPE(TX6)
SHOW_EXPR(( CommonResult<int,string>::value ))
SHOW_EXPR(( CommonResult<int,string,long>::value ))
SHOW_EXPR(( CommonResult<const int,int&>::value ))
SHOW_EXPR(( reconcilable<const int&,uint16_t,int64_t> ))
SHOW_EXPR(( reconcilable<const Uber&,Unta&,Untr&> ))
SHOW_EXPR(( reconcilable<Uber&,const Unta&,Untr&> ))
cout << "\n.gulp." <<endl;
return 0;

View file

@ -12,16 +12,16 @@ Import('env icons config')
# define the source file/dirs comprising each artifact to be built.
lLib = env.SharedLibrary('lumierasupport', srcSubtree('lib'), install=True)
lApp = env.SharedLibrary('lumieracommon', srcSubtree('common'), addLibs=lLib, install=True)
lVault = env.SharedLibrary('lumieravault', srcSubtree('vault'), addLibs=lLib+lApp, install=True)
lSteam = env.SharedLibrary('lumierasteam', srcSubtree('steam'), addLibs=lLib+lApp+lVault,install=True)
lApp = env.SharedLibrary('lumieravessel', srcSubtree('vessel'),addLibs=lLib, install=True)
lVault = env.SharedLibrary('lumieravault', srcSubtree('vault'), addLibs=lLib+lApp, install=True)
lSteam = env.SharedLibrary('lumierasteam', srcSubtree('steam'), addLibs=lLib+lApp+lVault, install=True)
# in reverse dependency order
core = lSteam+lVault+lApp+lLib # used to build the core application
app_lib = lApp+lLib # use to link against the platform
core_lib = core # use to link against the core application
vault_lib = lVault+app_lib # use to link against the low-level backend
support_lib = lLib # use to link against the support lib only
vessel_lib = lApp+support_lib # use to link against the application-framework
vault_lib = lVault+vessel_lib # use to link against the low-level backend
core_lib = lSteam+vault_lib # use to link against the core application
lumiera = ( env.Program('lumiera', ['lumiera/main.cpp'] + core, install=True)
+ config
@ -32,6 +32,10 @@ lumiera = ( env.Program('lumiera', ['lumiera/main.cpp'] + core, install=True)
env.SymLink('$DESTDIR/bin/lumiera',env.path.installExe+'lumiera','../lib/lumiera/lumiera')
# Unit/Integration test support
test_support = env.SharedLibrary('testsupport', srcSubtree('test'), addLibs=vessel_lib, install=False)
# building Lumiera Plugins
plugins = [] # currently none
@ -49,4 +53,4 @@ gui = ( guimodule
)
Export('lumiera core core_lib app_lib vault_lib support_lib plugins envGtk guimodule gui')
Export('lumiera core core_lib vessel_lib vault_lib support_lib test_support plugins envGtk guimodule gui')

View file

@ -1 +0,0 @@
Common application services

View file

@ -122,7 +122,7 @@ extern "C" {
#endif /* =========================== CL Interface ===================== */
#include "common/interface.h"
#include "vessel/interface.h"
#include "lib/hash-value.h"

View file

@ -1,5 +1,5 @@
/*
LIFECYCLE.h - interface for registering and triggering lifecycle callbacks
LIFECYCLE.hpp - interface for registering and triggering lifecycle callbacks
Copyright (C)
2008, Hermann Vosseler <Ichthyostega@web.de>
@ -11,32 +11,30 @@
*/
/** @file lifecycle.h
/** @file lifecycle.hpp
** Installing and invoking of application lifecycle event callbacks.
** This service is a facade for and implemented by the lumiera::LifecycleRegistry.
** This service is a facade for and implemented by the vessel::LifecycleRegistry.
** By placing a static LifecycleHook variable or by calling LifecycleHook::add,
** a callback can be registered to be executed on a specific application lifecycle
** event. Examples being #ON_BASIC_INIT, #ON_GLOBAL_INIT. Other subsystems may
** register additional events for more specific purpose.
**
** @note the implementation resides in lib/lifecycle.cpp and gets linked into liblumiera.so
** @see lumiera::AppState
** @see vessel::Voyage
** @see main.cpp
*/
#ifndef LUMIERA_LIFECYCLE_H
#define LUMIERA_LIFECYCLE_H
#ifndef VESSEL_LIFECYCLE_H
#define VESSEL_LIFECYCLE_H
#ifdef __cplusplus
#include "lib/symbol.hpp"
#include "lib/nocopy.hpp"
namespace lumiera {
namespace vessel {
using lib::Symbol;
@ -81,21 +79,5 @@ namespace lumiera {
} // namespace lumiera
#else /* =========== C interface ====================== */
//defined in liblumiera.so
extern const char * lumiera_ON_BASIC_INIT;
extern const char * lumiera_ON_GLOBAL_INIT;
extern const char * lumiera_ON_GLOBAL_SHUTDOWN;
void lumiera_LifecycleHook_add (const char* eventLabel, void callbackFun(void));
void lumiera_Lifecycle_trigger (const char* eventLabel);
#endif
#endif
} // namespace vessel
#endif /*VESSEL_LIFECYCLE_H*/

View file

@ -116,7 +116,7 @@ extern "C" {
#endif /* =========================== CL Interface ===================== */
#include "common/interface.h"
#include "vessel/interface.h"
LUMIERA_INTERFACE_DECLARE (lumieraorg_SessionCommand, 0,
LUMIERA_INTERFACE_SLOT (const char*, cycle, (const char*, const char*)),

View file

@ -51,6 +51,7 @@
#include <cstddef>
#include <utility>
#include <memory>
#include <list>

View file

@ -31,7 +31,7 @@
** @warning since these qualifiers manipulate by side-effect, it is up to the user to ensure
** consistent settings and behaviour (typically within a terminal `build()` function).
** @note the `qualify()` function accepts the qualifier functors by-reference; thus it is up
** to the user to either pass them by-value, by reverence or even by perfect forwarding.
** to the user to either pass them by-value, by reference or even by perfect forwarding.
**
** @see BuilderQualifierSupport_test
** @see ElementBoxWidget as usage example

View file

@ -376,7 +376,7 @@ namespace lib {
using ResultVal = meta::_Fun<FUN>::Ret;
using Subclass = meta::Strip<ResultVal>::TypePlain;
static_assert (std::is_pointer<ResultVal>::value,
static_assert (std::is_pointer_v<ResultVal>,
"Function must yield a pointer to a heap allocated instance");
};

View file

@ -44,7 +44,7 @@
using util::join;
using boost::lexical_cast;
using lib::time::TimeValue;
using lib::transformIterator;
using lib::transformIter;
using util::almostEqual;
using lib::hash::LuidH;
@ -297,9 +297,9 @@ namespace diff{
return (Rec::TYPE_NIL==rec.getType()? "" : rec.getType())
+ "{"
+ join (transformIterator (rec.attribs(), renderAttrib))
+ join (transformIter (rec.attribs(), renderAttrib))
+ (isnil(rec.scope())? "" : "|")
+ join (transformIterator (rec.scope() , renderChild))
+ join (transformIter (rec.scope() , renderChild))
+ "}"
;
}

View file

@ -375,8 +375,6 @@ namespace diff{
iterator end() const;
using ChildDataIter = TransformIter<Rec::scopeIter, DataCap const&>;
/** visit the _data_ of nested child elements
* @return an iterator over the DataCap elements of all children,
* in case this GenNode actually holds a Record.
@ -384,26 +382,26 @@ namespace diff{
* @note this iterator visits _only_ the children, which are
* by definition unnamed. It does _not_ visit attributes.
*/
friend ChildDataIter
friend auto
childData (GenNode const& n)
{
return ChildDataIter{ n.data.childIter()
, [](GenNode const& child) ->DataCap const&
return transformIter(n.data.childIter()
,[](GenNode const& child) ->DataCap const&
{
return child.data;
}
};
);
}
friend ChildDataIter
friend auto
childData (Rec::scopeIter&& scopeIter)
{
return ChildDataIter{ std::forward<Rec::scopeIter>(scopeIter)
, [](GenNode const& child) ->DataCap const&
return transformIter(std::forward<Rec::scopeIter>(scopeIter)
,[](GenNode const& child) ->DataCap const&
{
return child.data;
}
};
);
}
@ -776,7 +774,7 @@ namespace diff{
inline std::optional<X>
DataCap::retrieveAttribute (string key) const
{
static_assert (not std::is_reference_v<X>
static_assert (not meta::isRef_v<X>
,"optional access only possible by value");
Rec* nested = unConst(this)->maybeAccessNestedRec();

View file

@ -100,7 +100,7 @@ namespace diff{
using std::__and_;
using std::__not_;
using lib::meta::enable_if;
using lib::meta::can_IterForEach;
using lib::meta::can_LumieraIter;
using lib::meta::can_STL_ForEach;
using DiffStep = TreeDiffLanguage::DiffStep;
@ -168,7 +168,7 @@ namespace diff{
* @note source iterator is copied into a heap allocated IterSource
*/
template<class IT>
MutationMessage(IT&& ii, enable_if< can_IterForEach<IT>, void*> =nullptr)
MutationMessage(IT&& ii, enable_if< can_LumieraIter<IT>, void*> =nullptr)
: _FrontEnd{iter_source::wrapIter (std::forward<IT>(ii))}
{ }
@ -179,7 +179,7 @@ namespace diff{
*/
template<class CON>
MutationMessage(CON& container, enable_if< __and_< can_STL_ForEach<CON>
,__not_<can_IterForEach<CON>>>, void*> =nullptr)
,__not_<can_LumieraIter<CON>>>, void*> =nullptr)
: _FrontEnd{iter_source::eachEntry(container)}
{ }

View file

@ -308,18 +308,16 @@ namespace diff{
using iterator = IterAdapter<ElmIter, const Record*>;
using scopeIter = iter_stl::_SeqT<const Storage>::Range;
using keyIter = TransformIter<scopeIter, string>;
using valIter = TransformIter<scopeIter, Access>;
/** default iteration exposes all data within this "object", starting with the attributes */
iterator begin () const { return iterator(this, attribs_.empty()? children_.begin() : attribs_.begin()); }
iterator end () const { return iterator(); }
scopeIter attribs() const { return iter_stl::eachElm(attribs_); }
scopeIter scope() const { return iter_stl::eachElm(children_); }
scopeIter attribs() const { return iter_stl::eachElm (attribs_); }
scopeIter scope() const { return iter_stl::eachElm (children_); }
keyIter keys() const { return transformIterator(attribs(), extractKey); }
valIter vals() const { return transformIterator(attribs(), extractVal); }
auto keys() const { return transformIter (attribs(), extractKey); }
auto vals() const { return transformIter (attribs(), extractVal); }
protected: /* ==== API for the IterAdapter ==== */
@ -792,11 +790,11 @@ namespace diff{
Record<VAL>::operator std::string() const
{
using util::join;
using lib::transformIterator;
using lib::transformIter;
return "Rec("
+ (TYPE_NIL==type_? "" : type_)
+ (isnil(this->attribs())? "" : "| "+join (transformIterator (this->attribs(), renderAttribute))+" ")
+ (isnil(this->attribs())? "" : "| "+join (transformIter (this->attribs(), renderAttribute))+" ")
+ (isnil(this->scope())? "" : "|{"+join (this->scope())+"}")
+ ")"
;

View file

@ -43,7 +43,7 @@
#include "lib/idi/genfunc.hpp"
#include "lib/format-string.hpp"
#include "lib/format-util.hpp"
#include "lib/test/event-log.hpp"
#include "test/event-log.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"
@ -58,8 +58,8 @@ namespace diff{
namespace error = lumiera::error;
using lib::Literal;
using lib::test::EventLog;
using lib::test::EventMatch;
using test::EventLog;
using test::EventMatch;
using iter_stl::eachElm;
using util::unConst;
using util::isnil;
@ -110,12 +110,12 @@ namespace diff{
renderRecord (Rec const& record)
{
using util::join;
using lib::transformIterator;
using lib::transformIter;
return "Rec("
+ (Rec::TYPE_NIL==record.getType()? "" : record.getType())
+ (isnil(record.attribs())? "" : "| "+join (transformIterator (record.attribs(), renderAttribute))+" ")
+ (isnil(record.scope())? "" : "|{"+join (transformIterator (record.scope(), renderChild))+"}")
+ (isnil(record.attribs())? "" : "| "+join (transformIter (record.attribs(), renderAttribute))+" ")
+ (isnil(record.scope())? "" : "|{"+join (transformIter (record.scope(), renderChild))+"}")
+ ")"
;
}
@ -289,14 +289,14 @@ namespace diff{
string
showContent () const
{
return join (transformIterator (begin(), renderNode));
return join (transformIter (begin(), renderNode));
}
/** render elements waiting in source buffer to be accepted */
string
showSrcBuffer () const
{
return join (transformIterator (eachElm(prev_content_), renderNode));
return join (transformIter (eachElm(prev_content_), renderNode));
}
EventMatch

View file

@ -76,6 +76,7 @@
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/meta/trait.hpp"
#include "lib/meta/function.hpp"
#include "lib/diff/gen-node.hpp"
#include "lib/diff/tree-mutator.hpp"
#include "lib/iter-adapter-stl.hpp"

View file

@ -24,7 +24,7 @@
#include "lib/error.hpp"
#include "include/lifecycle.h"
#include "include/lifecycle.hpp"
#include "lib/meta/util.hpp"
#include "lib/util.hpp"
@ -154,6 +154,9 @@ namespace lumiera {
namespace error {
namespace {
using vessel::LifecycleHook;
using vessel::ON_BASIC_INIT;
void install_unexpectedException_handler ()
{
std::set_terminate (lumiera_unexpectedException);

View file

@ -197,14 +197,15 @@ apologies for that."
"|(\\w+::)+\\(anonymous namespace\\)::"
"|lib::meta::"
"|lib::time::"
"|lib::test::"
"|lib::diff::"
"|lib::"
"|util::"
"|\\btest::"
"|steam::(asset::|mobject::(session::)?|play::)?"
"|stage::model"
"|stage::ctrl"
"|lumiera::"
"|vessel::"
, regex::ECMAScript | regex::optimize};
static regex lolong {"long long"

View file

@ -100,6 +100,119 @@ namespace util {
namespace { // helper for printing type diagnostics
template<typename X>
struct TypeDiagnostics
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = "";
};
template<typename X>
struct TypeDiagnostics<const X>
{
using Type = X;
static constexpr auto prefix = "const ";
static constexpr auto postfix = "";
};
template<typename X>
struct TypeDiagnostics<X&>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = "&";
};
template<typename X>
struct TypeDiagnostics<X&&>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = " &&";
};
template<typename X>
struct TypeDiagnostics<X const&>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = " const&";
};
template<typename X>
struct TypeDiagnostics<X const&&>
{
using Type = X;
static constexpr auto prefix = "const ";
static constexpr auto postfix = " &&";
};
template<typename X>
struct TypeDiagnostics<X *>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = " *";
};
template<typename X>
struct TypeDiagnostics<const X *>
{
using Type = X;
static constexpr auto prefix = "const ";
static constexpr auto postfix = " *";
};
template<typename X>
struct TypeDiagnostics<const X * const>
{
using Type = X;
static constexpr auto prefix = "const ";
static constexpr auto postfix = " * const";
};
template<typename X>
struct TypeDiagnostics<X * const>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = " * const";
};
template<typename X>
struct TypeDiagnostics<X * const *>
{
using Type = X;
static constexpr auto prefix = "";
static constexpr auto postfix = " * const *";
};
}//(End)decoration helper
/** diagnostic type output, including const and similar adornments
* @warning operates after-the-fact and relies on mangled type names
* plus several heuristics. Output might thus not be entirely correct,
* especially when several levels of const, pointer and references are involved.
* If in doubt, place the test::TypeDebugger<T> to reveal the type as the compiler sees it.
* @remarks the function lib::meta::typeStr was deliberately written such as to remove type adornments
* and does not work on all kinds of reference. This helper complements this and
* attempts to preserve const-ness, pointer and reference decoration.
*/
template<typename X>
inline std::string
showType()
{
using Case = TypeDiagnostics<X>;
using Type = Case::Type;
return Case::prefix
+ lib::meta::humanReadableTypeID (typeid(Type).name())
+ Case::postfix;
}
template<typename...TS>
inline std::string
showTypes()
{
return "<| " + ((showType<TS>()+", ") + ... + "|>");
}
namespace {
/** toggle to prefer specialisation with direct lexical conversion */
template<typename X>

View file

@ -46,7 +46,7 @@
namespace util {
using lib::meta::can_IterForEach;
using lib::meta::lum_iter;
using std::string;
using std::forward;
using std::move;
@ -135,13 +135,13 @@ namespace util {
{
using Val = lib::meta::ValueTypeBinding<IT>::value_type;
return lib::transformIterator(forward<IT>(src), util::toString<Val>);
return lib::transformIter (forward<IT>(src), util::toString<Val>);
}
namespace { // helper to build range iterator on demand
template<class CON, typename TOGGLE = void>
template<class CON>
struct _RangeIter
{
using StlIter = CON::const_iterator;
@ -153,8 +153,8 @@ namespace util {
{ }
};
template<class IT>
struct _RangeIter<IT, lib::meta::enable_if< can_IterForEach<IT>> >
template<lum_iter IT>
struct _RangeIter<IT>
{
IT iter;

View file

@ -44,9 +44,11 @@
** Please ensure it is always included _before_ the latter. Failing to do so will result
** in mysterious failures.
**
** @todo 4/2014 doesn't work as expected. My suspicion is that in the actual use case (PlacementIndex),
** the type providing the hasher is mixed in through inheritance, and the template specialisation
** for this base type is not considered on lookup. ///////TICKET #722
** @todo 4/2026 this shady trickery works without problems since many years now,
** while it is not clear if we actually need to do these horrible things any more.
** Even while we may retain that automatic bridge to boost-functional hash, doing so
** should be possible with the current STDLIB without having to patch and rewire
** the library implementation. //////////////////////////////////////////////////////////////////////TICKET #1422 : re-assess the situation !
**
** @see HashIndexed
** @see LUID
@ -106,10 +108,6 @@ namespace std {
{
static_assert (sizeof(TY) < 0, "No hash implementation found. "
"Either specialise std::hash or provide a boost-style hash_value via ADL.");
// the default provides *no* hash implementation
// and adds a marker type for metaprogramming
typedef int NotHashable;
};
/**
@ -129,8 +127,8 @@ namespace std {
/**
* Primary class template for std::hash.
* We provide no default implementation, but a marker type
* to allow detection of custom implementation through metaprogramming
* We provide no default implementation, but bridge automatically
* when a boost-style hash function can be picked up via ADL.
*
* @note this definition has been \em hijacked by Lumiera
* to add an automatic bridge to use boost::hash functions
@ -140,12 +138,14 @@ namespace std {
: public _HashImplementationSelector<TY>
{ };
}
}// namespace std
/* ==== Evil Evil ==== */
/* ==== Evil Evil ==== */ ////////////////////////////////////////////////////////////////TICKET #1422 : very likely we do not need this manipulation any more !!!
#define hash hash_HIDDEN
#define _Hash_impl _Hash_impl_HIDDEN
@ -204,6 +204,12 @@ namespace std {
STD_HASH_IMPL (long double)
#undef STD_HASH_IMPL
}
/** rewire the partial specialisation for pointer types */
template<typename TY>
struct hash<TY*> : public hash_HIDDEN<TY*> { };
}// namespace std
#endif /*LIB_HASH_STANDARD_H*/

View file

@ -50,8 +50,8 @@ namespace lib {
using ResVal = decltype(data_->operator[](0));
using value_type = meta::RefTraits<ResVal>::Value;
using reference = meta::RefTraits<ResVal>::Reference;
using value_type = meta::RefTraits<ResVal>::value_type;
using reference = meta::RefTraits<ResVal>::reference;
bool
checkPoint() const

View file

@ -31,7 +31,7 @@
** this to the pattern of _type erasure_, where we discard specific
** knowledge after construction; for _such_ cases, opaque-holder.hpp
** might be a better fit.
** @see lib::TransformIter
** @see lib::iter::Transform
** @see lib::explore
**
*/

View file

@ -61,7 +61,7 @@ namespace lib {
public:
/** this iterator adapter is meant to wrap an iterator yielding pointer values */
using pointer = meta::ValueTypeBinding<IT>::value_type;
static_assert(std::is_pointer_v<pointer>);
static_assert(is_pointer_v<pointer>);
using value_type = std::remove_pointer_t<pointer>;
using reference = value_type&;
@ -74,8 +74,8 @@ namespace lib {
using ValueTypeBase = std::remove_const_t<value_type>; // value_type without const
using WrappedIterType = IterType<IT>::template SimilarIter< ValueTypeBase* * >::Type;
using WrappedConstIterType = IterType<IT>::template SimilarIter<const ValueTypeBase* * >::Type;
using WrappedIterType = IterRebind<IT>::template SimilarIter< ValueTypeBase* * >::Type;
using WrappedConstIterType = IterRebind<IT>::template SimilarIter<const ValueTypeBase* * >::Type;
using IterType = PtrDerefIter<WrappedIterType>;
using ConstIterType = PtrDerefIter<WrappedConstIterType>;

View file

@ -90,15 +90,18 @@ namespace iter_stl {
/**
* helper baseclass to simplify
* defining customised wrapped STL iterators
* @internal helper baseclass to simplify defining customised wrapped STL iterators.
* @remark this is meant to be compliant to a STL input-iterator;
* it's _not_ a »Lumiera Forward Iterator« (lacks the bool check for iteration end)
* @warning minimal definition, since it is only intended to be wrapped into lib::RangeIter
*/
template<typename DEF>
struct WrappedStlIter : DEF
{
using Iter = DEF::Iter;
using reference = DEF::reference;
using pointer = DEF::pointer;
using Iter = DEF::Iter;
using value_type = DEF::value_type;
using reference = DEF::reference;
using pointer = DEF::pointer;
WrappedStlIter() : i_() { }
@ -112,6 +115,10 @@ namespace iter_stl {
friend bool operator== (WrappedStlIter const& i1, WrappedStlIter const& i2) { return i1.i_ == i2.i_; }
friend bool operator!= (WrappedStlIter const& i1, WrappedStlIter const& i2) { return i1.i_ != i2.i_; }
/** @note the following dummy definitions are required to fulfil the concept std::input_iterator */
WrappedStlIter& operator++(int) { NOTREACHED("never used as STL iterator"); return *this; }
using difference_type = ptrdiff_t;
private:
mutable Iter i_;
};
@ -424,11 +431,14 @@ namespace iter_stl {
/** create empty snapshot */
IterSnapshot() { }
// standard copy operations acceptable
/** take snapshot by discharging the given Lumiera Forward iterator
* @warning depending on the implementation backing the source iterator,
* this might or might not yield side-effects.
*/
template<class IT>
template<class IT, typename =meta::disable_if_self<IterSnapshot, IT>>
IterSnapshot (IT&& src)
{
for ( ; src; ++src)
@ -439,7 +449,7 @@ namespace iter_stl {
* @warning depending on the implementation backing the source iterator,
* this might or might not yield side-effects.
*/
template<class IT>
template<class IT, typename =meta::disable_if_self<IterSnapshot, IT>>
IterSnapshot (IT const& src)
{
for (IT copy{src}; copy; ++copy)
@ -462,11 +472,6 @@ namespace iter_stl {
buffer_.emplace_back (*pos);
}
IterSnapshot(IterSnapshot &&) = default;
IterSnapshot(IterSnapshot const&) = default;
IterSnapshot& operator= (IterSnapshot const&) = default;
IterSnapshot& operator= (IterSnapshot &&) = default;
explicit
operator bool() const
{

View file

@ -30,7 +30,8 @@
** - very similar is IterableDecorator, but this time directly as
** decorator to inherit from the »state core«, and without checks.
** - the RangeIter allows just to expose a range of elements defined
** by a STL-like pair of "start" and "end" iterators
** by a STL-like pair of "start" and "end" iterators; thus it
** serves a bridge between STL iterators and Lumiera iterators.
**
** Some more specific use cases are provided in the extension header
** iter-adapter-ptr-deref.hpp
@ -62,14 +63,14 @@
** Similar to the STL, instead of using a common "Iterator" base class,
** we rather define a common set of functions and behaviour which can be
** expected from any such iterator. These rules are similar to STL's
** "forward iterator", with the addition of an bool check to detect
** iteration end. The latter is inspired by the \c hasNext() function
** "input iterator", with the addition of an bool check to detect
** iteration end. The latter is inspired by the `hasNext()` function
** found in many current languages supporting iterators. However, by
** inspiration from functional programming, we deliberately do not
** support the various extended iterator concepts from STL and boost
** drawing from insights promoted in functional programming, deliberately
** we do not support the various extended iterator concepts from the STL
** (random access iterators, output iterators, arithmetics, difference
** between iterators and the like). According to this concept,
** _an iterator is a promise for pulling values once,_
** between iterators and the like). According to this concept proposed
** here, _an iterator is a promise for pulling values once,_
** and nothing beyond that.
**
** Notably,
@ -87,10 +88,48 @@
** Conceptually, a Lumiera Iterator represents a lazy stream of calculations
** rather than a target value considered to be »within« a container. And while
** the result is in may cases deliberately _exposed as a reference,_ in order
** to keep the door open for special-case manipulations, for the typical usage
** it is _discouraged_ to assume anything about the source, beyond the limited
** access to some transient state as exposed during active iteration. Together,
** these rules enable a _loose coupling_ to the source of data.
** to allow for tightly coupled implementation in special cases -- for the
** typical usage it is _discouraged_ to assume anything about the source,
** beyond the limited access to some transient state as exposed during
** active iteration. Together, these rules enable a _loose coupling_
** to the source of data.
**
** ## Usage
**
** All the building blocks defined her provide the bridge definitions to allow
** using a »Lumiera Forward Iterator« in common language constructs: It can be
** used directly in for-each loops (notably this implies that a Lumiera iterator
** is used in the same position where usually the container would be referred).
** Yet, beyond that, Lumiera iterators can also be used in normal `for` loops
** and even in while loops, due to the bool-check to detect iteration end.
** A default constructed Lumiera iterator is always in _invalid state_
** and can be used as _sentinel value_ like the `end()` iterator.
** Yet, while Lumiera iterators can be copied and assigned, the effect of
** these operations is implementation defined; in some cases, taking a copy
** may allow to create a snapshot, while in other cases (especially when the
** Lumiera iterator uses an reference to tap into some remote service) all
** copies will refer to the same shared state. The rationale for this design
** was to simplify both building and using Lumiera Iterators; they are meant
** as an implementation tool, and not for creating external interfaces.
**
** However, there is a special provision for the latter use case, in the form
** of the lib::IterSource interface and framework; an `IterSource` embeds a
** classic OO interface with virtual methods, and thus allows to abstract the
** backing implementation entirely away.
**
** ## Concepts and Traits
**
** The »Lumiera Forward Iterators« do not map directly to any specification from
** the C++ standard. The concept std::input_iterator comes close, but a Lumiera
** iterator can not fulfil this concept, since it lacks the postfix increment.
** In the opposite direction, there is also no subsumption possible, since a
** STL iterator does not offer a bool-check to detect iteration end.
**
** Notably the header `lib/meta/trait.hpp` defines some detector traits, allowing
** to recognise if something is an iterable STL container, or a Lumiera iterator.
** Furthermore, lib::iter::Trait defines a transparent mechanism to extract the
** basic type definitions (`reference` ≙ what the iterator yields and
** `value_type` ≙ what to use to store results from an iterator)
**
** @see iter-adapter-test.cpp
** @see itertools.hpp
@ -105,6 +144,7 @@
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/meta/value-type-binding.hpp"
#include <iterator>
@ -120,6 +160,9 @@ namespace util { // see lib/util.hpp
namespace lib {
using std::is_pointer_v;
using std::remove_pointer_t;
using std::remove_reference_t;
namespace { // internal helpers
inline void
@ -149,7 +192,14 @@ namespace lib {
namespace iter {
/** type binding helper: an iterato's actual result type */
using meta::is_StateCore;
using meta::can_LumieraIter;
using meta::can_STL_ForEach;
using meta::ValueTypeBinding;
using meta::RefTraits;
/** type binding helper: an iterator's actual result type */
template<class IT>
using Yield = decltype(std::declval<IT>().operator*());
@ -157,6 +207,35 @@ namespace lib {
template<class COR>
using CoreYield = decltype(std::declval<COR>().yield());
/** some legacy STL type iterators do not strictly fulfil all requirements,
* yet were marked by a specialisation of `std::iterator_traits` */
template<class IT>
concept legacy_traits_marked = requires
{
typename std::iterator_traits<IT>::iterator_category;
};
/** Iterator-Traits: definitions to level the difference between
* Lumiera-Iterators and the STL usage of ranges / iterators.
*/
template<class IT>
struct Trait
{
static constexpr bool is_LumieraIter = meta::is_LumieraIter_v<IT>;
static constexpr bool is_STLIter = std::input_iterator<IT>;
static constexpr bool is_Legacy = legacy_traits_marked<IT>;
static_assert(is_LumieraIter or is_STLIter or is_Legacy
,"source type must be an iterator (either Lumiera or STL");
using _ValTrait = std::conditional_t<is_LumieraIter, ValueTypeBinding<IT>
, RefTraits<std::iter_reference_t<IT>>>;
using value_type = _ValTrait::value_type;
using reference = _ValTrait::reference;
using pointer = _ValTrait::pointer;
};
}
@ -302,7 +381,7 @@ namespace lib {
protected:
using ConRef = meta::RefTraits<CON>::Reference;
using ConRef = meta::RefTraits<CON>::reference;
/** allow derived classes to access backing container */
ConRef source() { return source_; }
@ -370,15 +449,15 @@ namespace lib {
* @see iter-explorer-test.hpp
* @see iter-adaptor-test.cpp
*/
template<class ST, typename T =iter::CoreYield<ST>>
template<class ST, typename RES =iter::CoreYield<ST>>
class IterStateWrapper
{
ST core_;
public:
using value_type = meta::RefTraits<T>::Value;
using reference = meta::RefTraits<T>::Reference;
using pointer = meta::RefTraits<T>::Pointer;
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
IterStateWrapper (ST&& initialState)
: core_(std::forward<ST>(initialState))
@ -399,9 +478,9 @@ namespace lib {
}
/* === lumiera forward iterator concept === */
/* === Lumiera Forward Iterator concept === */
T
RES
operator*() const
{
__throw_if_empty();
@ -411,13 +490,13 @@ namespace lib {
pointer
operator->() const
{
if constexpr (meta::isLRef_v<T>)
if constexpr (meta::isLRef_v<RES>)
{
__throw_if_empty();
return & core_.yield(); // core interface: yield
}
else
static_assert (!sizeof(T),
static_assert (!sizeof(RES),
"can not provide operator-> "
"since iterator pipeline generates a value");
}
@ -500,7 +579,7 @@ namespace lib {
class IterStateCore
: public IT
{
static_assert (lib::meta::can_IterForEach<IT>::value
static_assert (iter::can_LumieraIter<IT>::value
,"Lumiera Iterator required as source");
protected:
IT&
@ -545,7 +624,7 @@ namespace lib {
class CheckedCore
: public COR
{
static_assert (lib::meta::is_StateCore<COR>::value
static_assert (iter::is_StateCore<COR>::value
,"Adapted type must expose a »state core« API");
protected:
COR&
@ -608,6 +687,7 @@ namespace lib {
* @warning be sure to understand the implications of this setup
* - when initialised by reference, the container's contents will be copied
* - when move-initialised, the container will be destroyed with this iterator
* - when moved / swapped during iteration, the iterator is invalidated (-> data corruption)
* - the container API remains visible (baseclass), which could confuse trait detection
*/
template<class CON>
@ -649,6 +729,67 @@ namespace lib {
template<class CON>
concept subscriptable = requires (CON& con, size_t idx)
{
typename CON::reference;
{ con[idx] } -> std::convertible_to<typename CON::reference>;
{ con.size() } -> std::convertible_to<size_t>;
};
template<class CON>
using is_subscriptable = std::bool_constant<subscriptable<remove_reference_t<CON>>>;
/**
* Adapter to package some container store inline and access it bey sequenced subscript.
* This setup works similar to the ContainerCore, yet has the benefit of being movable.
* @warning using any kind of inline storage in the context of IterExplorer is dangerous.
* Anything beyond the actual iteration state and especially an extended data storage
* might lead to surprising and harmful behaviour, due to the contradiction to the
* common mental image associated with an iterator. These dangers are amplified
* when such embedded data is transformed, filtered or modified by reference.
*/
template<subscriptable CON>
class IdxStoreCore
: public CON
{
size_t i_;
public:
IdxStoreCore (CON&& container)
: CON(std::forward<CON> (container))
, i_{0}
{ }
IdxStoreCore()
: CON{}
, i_{CON::size()}
{ } // explicitly ensure that iterator is empty
// copy and assignment acceptable (warning!)
/* === »state core« protocol API === */
bool
checkPoint() const
{
return i_ < CON::size();
}
decltype(auto)
yield() const
{
return util::unConst(*this).operator[] (i_);
} // rationale: const iterator ≠ const_iterator
void
iterNext()
{
++i_;
}
};
/**
* Decorator-Adapter to make a »*State Core*« iterable as Lumiera Forward Iterator.
@ -694,9 +835,9 @@ namespace lib {
public:
using YieldRes = iter::CoreYield<COR>;
using value_type = meta::RefTraits<YieldRes>::Value;
using reference = meta::RefTraits<YieldRes>::Reference;
using pointer = meta::RefTraits<YieldRes>::Pointer;
using value_type = meta::RefTraits<YieldRes>::value_type;
using reference = meta::RefTraits<YieldRes>::reference;
using pointer = meta::RefTraits<YieldRes>::pointer;
/** by default, pass anything down for initialisation of the core.
@ -782,17 +923,23 @@ namespace lib {
/**
* Accessing a STL element range through a Lumiera forward iterator,
* Access a STL element range through a »Lumiera Forward Iterator«.
* An instance of this iterator adapter is completely self-contained
* and allows to iterate once over the range of elements, until
* and allows to iterate _once_ over the range of elements, until
* `pos==end`. Thus, a custom container may expose a range of
* elements of an embedded STL container, without controlling
* the details of the iteration (as is possible using the
* the details of the iteration (as would be possible using the
* more generic IterAdapter).
* @note
* - when IT is just a pointer, we use the pointee as value type
* - but when IT is a class, we expect the usual STL style nested typedefs
* `value_type`, `reference` and `pointer`
* - when IT is a STL iterator, we rely on the iterator concepts and
* especially on `std::iter_reference_t` to find the `reference_type`
* - but when IT is a »Lumiera Forward Iterator«, we need to evaluate
* nested typedefs `value_type`, `reference` and `pointer`
* @remark the reason for this tricky distinction is a _clash of concepts:_
* In Lumiera, »Iterator« has a special meaning and can used on itself,
* while in the STL, an »Iterator« is a pointer in disguise and is
* only meaningful when the backing container is known. This adaptor,
* RangeIter, acts as a bridge and levels those usage styles.
*/
template<class IT>
class RangeIter
@ -800,14 +947,10 @@ namespace lib {
IT p_;
IT e_;
using _ValTrait = meta::ValueTypeBinding<meta::remove_pointer_t<IT>>;
public:
using pointer = _ValTrait::pointer;
using reference = _ValTrait::reference;
/// @note special twist, since a STL const_iterator would yield a non-const `value_type`
using value_type = std::remove_reference<reference>::type;
using pointer = iter::Trait<IT>::pointer;
using reference = iter::Trait<IT>::reference;
using value_type = iter::Trait<IT>::value_type;
RangeIter (IT const& start, IT const& end)
@ -1027,13 +1170,13 @@ namespace lib {
/**
* Helper for type rewritings:
* get the element type for an iterator like entity
* build a similar iterator with another value type
*/
template<class TY>
struct IterType;
struct IterRebind;
template<template<class,class> class Iter, class TY, class CON>
struct IterType<Iter<TY,CON>>
struct IterRebind<Iter<TY,CON>>
{
using Container = CON;
using ElemType = TY ;
@ -1046,13 +1189,13 @@ namespace lib {
};
template<class IT>
struct IterType<RangeIter<IT>>
: IterType<IT>
struct IterRebind<RangeIter<IT>>
: IterRebind<IT>
{
template<class T2>
struct SimilarIter ///< rebind to rewritten Iterator wrapped into RangeIter
{
using WrappedIter = IterType<IT>::template SimilarIter<T2>::Type;
using WrappedIter = IterRebind<IT>::template SimilarIter<T2>::Type;
using Type = RangeIter<WrappedIter>;
};
};

View file

@ -56,9 +56,9 @@ namespace lib {
public:
using pointer = meta::ValueTypeBinding<IT>::pointer;
using reference = meta::ValueTypeBinding<IT>::reference;
using value_type = std::remove_reference<reference>::type; ///< @note will be const for const iterators (while const_iterator::value_type isn't)
using pointer = iter::Trait<IT>::pointer;
using reference = iter::Trait<IT>::reference;
using value_type = iter::Trait<IT>::value_type;
CursorGear()

View file

@ -13,13 +13,13 @@
*/
/** @file iter-explorer.hpp
** Building tree expanding and backtracking evaluations within hierarchical scopes.
** Build tree expanding and backtracking evaluations within hierarchical scopes.
** Based on the *Lumiera Forward Iterator* concept and using the basic IterAdapter templates,
** these components allow to implement typical evaluation strategies, like conditional expanding
** or depth-first exploration of a hierarchical structure. Since the access to this structure is
** abstracted through the underlying iterator, what we effectively get is a functional datastructure.
** The implementation is based on the idea of a "state core", which is wrapped right into the iterator
** itself (value semantics) -- similar to the IterStateWrapper, which is one of the basic helper templates
** The implementation is based on the idea of a **State Core**, which is wrapped right into the iterator
** itself (value semantics) — similar to the IterStateWrapper, which is one of the basic helper templates
** provided by iter-adapter.hpp.
**
** @remark historically, this template, as well as the initial IterExplorer (draft from 2012) can be
@ -36,7 +36,7 @@
** calculations with exception handling but also simple data structures like lists or trees). The key point with any
** monad is the ability to _bind a function_ into the monad; this function will work on the _contained base values_
** and produce a modified new monad instance. In the simple case of a list, "binding" a function basically means
** to _map the function onto_ the elements in the list. (strictly speaking, it means the `flatMap` operation)
** to _map the function onto_ the elements in the list. (more precisely, this indicates a `flatMap` operation)
**
** # A Pipeline builder
** Based on such concepts, structures and evaluation patterns, the IterExplorer serves the purpose to provide
@ -49,8 +49,9 @@
** on each element, rather it is triggered by issuing a dedicated `expandChildren()` call on the processing
** pipeline. Thus, binding the expansion functor has augmented the data source with the ability to explore
** some part in more detail _when required_.
** - the *transform operation* installs a function to be mapped onto each element retrieved from the underlying source
** - in a similar vein, the *filter operation* binds a predicate to decide about using or discarding data
** - the **transform** operation installs a function to be mapped onto each element retrieved from the underlying source
** - in a similar vein, the **filter** operation binds a predicate to decide about using or discarding iterator results
** - the **flatten** operation allows to combine a sequence of nested _iterables_ into a single seamless iterator
** - in concert, expand- and transform operation allow to build hierarchy evaluation algorithms without exposing
** any knowledge regarding the concrete hierarchy used and explored as data source.
** - further special convenience adaptors and _terminal functions_ are provided.
@ -101,6 +102,7 @@
#include "lib/error.hpp"
#include "lib/uninitialised-storage.hpp"
#include "lib/meta/duck-detector.hpp"
#include "lib/meta/common-result.hpp"
#include "lib/meta/function.hpp"
#include "lib/meta/trait.hpp"
#include "lib/item-wrapper.hpp"
@ -217,6 +219,14 @@ namespace lib {
}
};
// (forward declarations to allow defining all traits together)
template<class SRC, typename RES>
class RedressAdapter;
template<class SRC, typename WRA>
class WrapperAdapter;
}//(End) namespace iter_explorer : basic iterator wrappers
@ -237,9 +247,10 @@ namespace lib {
using std::common_type_t;
using std::conditional_t;
using std::is_convertible;
using std::is_lvalue_reference;
using std::remove_reference_t;
using meta::is_StateCore;
using meta::can_IterForEach;
using meta::can_LumieraIter;
using meta::can_STL_ForEach;
using meta::ValueTypeBinding;
using meta::has_TypeResult;
@ -248,17 +259,24 @@ namespace lib {
template<class SRC>
struct shall_wrap_STL_Iter
: __and_<can_STL_ForEach<SRC>
,__not_<can_IterForEach<SRC>>
,__not_<can_LumieraIter<SRC>>
>
{ };
template<class SRC>
struct shall_use_Lumiera_Iter
: __and_<can_IterForEach<SRC>
: __and_<can_LumieraIter<SRC>
,__not_<is_StateCore<SRC>>
>
{ };
template<class SRC>
struct shall_use_StateCore
: __and_<__not_<can_LumieraIter<SRC>>
,is_StateCore<SRC>
>
{ };
/**
@ -273,8 +291,8 @@ namespace lib {
template<class SRC>
struct _DecoratorTraits<SRC, enable_if<is_StateCore<SRC>>>
{
using SrcRaw = lib::meta::Strip<SRC>::Type;
using SrcVal = meta::RefTraits<iter::CoreYield<SrcRaw>>::Value;
using SrcRaw = lib::meta::Strip<SRC>::TypeReferred;
using SrcVal = meta::RefTraits<iter::CoreYield<SrcRaw>>::value_type;
using SrcIter = lib::IterableDecorator<lib::CheckedCore<SrcRaw>>;
};
@ -288,7 +306,7 @@ namespace lib {
template<class SRC>
struct _DecoratorTraits<SRC, enable_if<shall_wrap_STL_Iter<SRC>>>
{
static_assert (not std::is_rvalue_reference<SRC>::value,
static_assert (not std::is_rvalue_reference_v<SRC>,
"container needs to exist elsewhere during the lifetime of the iteration");
using SrcIter = iter_explorer::StlRange<SRC>;
using SrcVal = SrcIter::value_type;
@ -312,6 +330,78 @@ namespace lib {
{ };
/**
* @internal Type-selector to adapt a recursive iterable for flattening...
* @tparam SRC the underlying source iterator (which yields a nested iterable as result)
* @tparam RES result type of the source iterator (this is the nested iterable itself)
* @remark to explain the general scheme employed here...
* - we want to add a processing layer with a iter_explorer::Flattener
* - this requires a source-iterator that yields a nested iterable of sorts
* - the source-iterator as such has already been adapted by the [generic mechanism](\ref _DecoratorTraits)
* - however there remains the twist that the _nested result_ (which is some kind of iterable)
* also needs to be adapted, so that it can be handled as Lumiera Forward Iterator...
* - this can be achieved by interspersing an adapter layer _below_ the Flattener,
* so that the result of the underlying iterator will be adapted / casted / wrapped
* - furthermore, we have to distinguish the cases where the underlying iterator produces a value result
* - overall this trait template is instantiated with the type \a SRC of the underlying iterator
* - with the help of the second parameter's default, the \a RES result type of this iterator is determined
* - the specialisation matcher then classifies this result type and determines an adaptation strategy
* - finally, the Flattener is parametrised with this `SrcAdapter` type, but we pass `move(*this) to
* its constructor; since `*this` inherits from \a SRC, the Adapter will pick up and wrap \a SRC
* There are two basic adaptation strategies. First, if the source iterator yields an iterator by reference,
* and we can adapt this iterator on-the-fly (without requiring additional storage), we can just cast the result.
* In all other cases, a setup similar to a Transformer is required: for each source-result (which is in itself
* iterable), a wrapper is constructed into an _inline storage space_ (implemented as \ref lib::ItemWrapper),
* and this wrapper is exposed by-ref as "result value", so that the Flattener can iterate it until exhausted.
*/
template<class SRC, typename RES=iter::Yield<SRC>, typename SEL=void>
struct _FlatteningTraits
{
static_assert (!sizeof(SRC), "Can not build Flattener: Unable to figure out how to iterate the nested iterable.");
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_< is_lvalue_reference<RES>, can_LumieraIter<RES> >>>
{
using SrcAdapter = meta::RefTraits<SRC>::value_type;
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_<__not_<is_lvalue_reference<RES>>, can_LumieraIter<RES> >>>
{
using NestedIter = meta::RefTraits<RES>::value_type;
using SrcAdapter = iter_explorer::WrapperAdapter<SRC,NestedIter>;
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_< is_lvalue_reference<RES>, shall_use_StateCore<RES> >>>
{
using CoreRaw = lib::meta::Strip<RES>::TypeReferred;
using NestedIter = IterableDecorator<CoreRaw>;
using SrcAdapter = iter_explorer::RedressAdapter<SRC,NestedIter>;
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_<__not_<is_lvalue_reference<RES>>, shall_use_StateCore<RES> >>>
{
using CoreRaw = lib::meta::Strip<RES>::TypeReferred;
using NestedIter = IterableDecorator<CoreRaw>;
using SrcAdapter = iter_explorer::WrapperAdapter<SRC,NestedIter>;
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_< is_lvalue_reference<RES>, shall_wrap_STL_Iter<RES> >>>
{
using NestedIter = iter_explorer::StlRange<RES>;
using SrcAdapter = iter_explorer::WrapperAdapter<SRC,NestedIter>;
};
template<class SRC, typename RES>
struct _FlatteningTraits<SRC,RES, enable_if<__and_<__not_<is_lvalue_reference<RES>>, is_subscriptable<RES> >>>
{
using StoreRaw = lib::meta::Strip<RES>::TypeReferred;
using NestedIter = IterableDecorator<IdxStoreCore<StoreRaw>>;
using SrcAdapter = iter_explorer::WrapperAdapter<SRC,NestedIter>;
};
/**
* helper to derive a suitable common type when expanding children
@ -327,19 +417,15 @@ namespace lib {
using ResIter = _DecoratorTraits<RES>::SrcIter;
using SrcYield = iter::Yield<SRC>;
using ResYield = iter::Yield<ResIter>;
using _CommonT = meta::CommonResultYield<SrcYield,ResYield>;
using _CommonT = meta::CommonResult<SrcYield,ResYield>;
static constexpr bool can_reconcile = _CommonT::value;
static constexpr bool isRefResult = _CommonT::isRef;
static_assert (can_reconcile,
"source iterator and result from the expansion must yield compatible values");
static_assert (is_const_v<SrcYield> == is_const_v<ResYield>,
"source and expanded types differ in const-ness");
using YieldRes = _CommonT::ResType;
using value_type = _CommonT::value_type;
using reference = _CommonT::reference;
using pointer = _CommonT::pointer;
using YieldRes = _CommonT::Type;
};
}//(End) IterExplorer traits
@ -478,13 +564,15 @@ namespace lib {
struct _ReduceTraits
{
using Result = iter_explorer::_FunTraits<FUN,SRC>::Res;
using ResVal = lib::meta::RefTraits<Result>::Value;
using ResVal = lib::meta::RefTraits<Result>::value_type;
};
/* ========= IterExplorer pipeline building blocks ========= */
/**
* @internal Base of pipe processing decorator chain.
* IterExplorer allows to create a stack out of various decorating processors
@ -516,45 +604,81 @@ namespace lib {
};
/**
* @internal Iterator adapter to _virtually project_ an arbitrary type \a RES
* onto the result of the underlying Lumiera Iterator, by means of a force-cast.
*/
template<class SRC, typename RES>
class RedressAdapter
: public SRC
{
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source.");
using SrcRes = iter::Yield<SRC>;
static_assert(meta::isLRef_v<SrcRes>, "Source iterator must yield its result by-ref.");
public:
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
reference
operator*() const
{
return reinterpret_cast<reference> (SRC::operator*());
}
pointer
operator->() const
{
return & operator*();
}
using SRC::SRC;
};
/**
* @internal Decorator for IterExplorer adding the ability to "expand children".
* The expandChildren() operation is the key element of a depth-first evaluation: it consumes
* one element and performs a preconfigured _expansion functor_ on that element to yield
* its "children". These are given in the form of another iterator, which needs to be
* compatible to the source iterator ("compatibility" boils down to both iterators
* yielding a compatible value type). Now, this _sequence of children_ effectively
* replaces the expanded source element in the overall resulting sequence; which
* means, the nested sequence was _flattened_ into the results. Since this expand()
* operation can again be invoked on the results, the implementation of such an evaluation
* requires a stack datastructure, so the nested iterator from each expand() invocation
* can be pushed to become the new active source for iteration. Thus the primary purpose
* of this Expander (decorator) is to integrate those "nested child iterators" seamlessly
* into the overall iteration process; once a child iterator is exhausted, it will be
* popped and iteration continues with the previous child iterator or finally with
* the source iterator wrapped by this decorator. The source pipeline is only pulled
* once the expanded children are exhausted.
* The expandChildren() operation is the key element of a _depth-first evaluation:_
* it consumes one element and performs a preconfigured _expansion functor_ on that element
* to yield its "children". These are given in the form of another iterator, which needs to be
* compatible to the source iterator (within this context, "compatibility" boils down to both
* iterators yielding a compatible value type). Now, this _sequence of children_ effectively
* replaces the expanded source element in the overall resulting sequence; which means that
* the nested sequence was _flattened_ into the results. Since this expand() operation can
* again be invoked on the results, the implementation of such an evaluation requires a
* **stack datastructure**, so the nested iterator from each expand() invocation can be
* _pushed on top_ in order to become the new active source for iteration. Thus the prime
* purpose of this Expander (decorator) is to integrate those "nested child iterators"
* seamlessly into the overall iteration process; once a child iterator is exhausted,
* it will be _popped_ and iteration continues with the previous child iterator, or,
* finally, with the source iterator wrapped by this decorator. Overall, the given
* source pipeline is only pulled, whenever the expanded children are exhausted.
* @remark since we allow a lot of leeway regarding the actual form and definition of the
* _expansion functor_, there is a lot of minute technical details, mostly confined
* within the _FunTraits traits. For the same reason, we need to prepare two different
* bindings of the passed raw functor, one to work on the source sequence, and the other
* one to work on the result sequence of a recursive child expansions; these two sequences
* need not be implemented in the same way, which simplifies the definition of algorithms.
* @tparam SRC the wrapped source iterator, typically a IterExplorer or nested decorator.
* @tparam FUN the concrete type of the functor passed. Will be dissected to find the signature
* _expansion functor_, various technical details must be observed, and these minutiae
* are largely maintained within the _FunTraits traits. For the same reason, we need to
* prepare two different bindings of the passed raw functor, where one of them is used
* to work on the source sequence, while the other one handles the result sequence of
* _recursive child expansions;_ these two sequences need not be implemented precisely
* in the same way — what matters here is compatible behaviour — and this flexible
* setup helps to simplify the definition of search- and evaluation algorithms.
* @tparam SRC the wrapped source iterator, typically an iterator adaptor or nested decorator.
* @tparam FUN actual type of the functor passed. Will be type-matched to detect its signature.
* @note the _return type_ of #yield depends _both_ on the return type produced from the original
* sequence and the return type of the sequence established through the expand functor.
* An attempt is made to _reconcile these_ and this attempt may fail (at compile time).
* The reason is, any further processing downstream can not tell if data was produced
* by the original sequence of the expansion sequence. Notably, if one of these two
* delivers results by-value, then the Expander will _always_ deliver all results
* by-value, because it would not be possible to expose a reference to some value
* The rationale is that any further processing downstream can not tell if a data result
* was produced by the original sequence of the expansion sequence. Notably, if one of
* these two delivers results by-value, then the Expander will _always_ choose to deliver
* all its results by-value, since it is impossible to expose a reference to some value
* that was just delivered temporarily from a source iterator.
*/
template<class SRC, class RES>
class Expander
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
using _Trait = _ExpanderTraits<SRC,RES>;
using ResIter = _Trait::ResIter;
@ -622,9 +746,9 @@ namespace lib {
/** @note result type bindings based on a common type of source and expanded result */
using YieldRes = _Trait::YieldRes;
using value_type = _Trait::value_type;
using reference = _Trait::reference;
using pointer = _Trait::pointer;
using value_type = meta::RefTraits<YieldRes>::value_type;
using reference = meta::RefTraits<YieldRes>::reference;
using pointer = meta::RefTraits<YieldRes>::pointer;
bool
@ -699,7 +823,7 @@ namespace lib {
/**
* @internal extension to the Expander decorator to perform expansion automatically on each iteration step.
* @todo as of 12/2017, this is more like a proof-of concept and can be seen as indication, that there might
* @todo as of 12/2017, this is more like a proof-of concept and can be seen as indication that there might
* be several flavours of child expansion. Unfortunately, most of these conceivable extensions would
* require a flexibilisation of Expander's internals and thus increase the complexity of the code.
* Thus, if we ever encounter the need of anything beyond the basic expansion pattern, we should
@ -726,7 +850,7 @@ namespace lib {
/**
* @internal extension to the Expander decorator to perform expansion delayed on next iteration.
* @internal extension to the Expander decorator to perform a delayed expansion on next iteration.
*/
template<class SRC>
class ScheduledExpander
@ -761,14 +885,94 @@ namespace lib {
};
/**
* @internal Decorator for IterExplorer to expand one level of nested iterables.
* This mechanism requires that the _result value_ yielded by the underlying iterator
* is itself iterable. Applying this decorator thus has the effect to »flatten« exactly
* one level of nesting.
* @note The result produced by underlying \a SRC _must be a Lumiera iterator_.
* Other kinds of nested iterables can be handled only when adapted accordingly.
*/
template<class SRC>
class Flattener
: public SRC
{
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
using NestedIT = iter::Yield<SRC>;
static_assert(can_LumieraIter<NestedIT>::value, "flatten() operation expects nested iterator");
public:
using value_type = meta::ValueTypeBinding<NestedIT>::value_type;
using reference = meta::ValueTypeBinding<NestedIT>::reference;
using pointer = meta::ValueTypeBinding<NestedIT>::pointer;
Flattener() =default;
// inherited default copy operations
Flattener (SRC&& dataSrc)
: SRC{move (dataSrc)}
{
pullNested();
}
/** refresh state when other layers manipulate the source sequence.
* @note possibly pulls pipeline to re-establish the invariant */
void
expandChildren()
{
SRC::expandChildren();
pullNested();
}
public: /* === Iteration control API for IterableDecorator === */
bool
checkPoint() const
{
ENSURE (not srcIter() or * srcIter());
return bool(srcIter());
}
reference
yield() const
{
return **srcIter();
}
void
iterNext()
{
++ *srcIter();
pullNested();
}
protected:
SRC&
srcIter() const
{
return unConst(*this);
}
/** @note Invariant: positioned at next nested source element, if any */
void
pullNested()
{
while (srcIter() and not * srcIter())
++srcIter();
}
};
/**
* @internal Decorator for IterExplorer to map a transformation function on all results.
* @internal Decorator for IterExplorer to map a transformation function onto all results.
* The transformation function is invoked on demand, and only once per item to be treated,
* storing the treated result into an universal value holder buffer. The given functor
* is adapted in a similar way as the "expand functor", so to detect and convert the
* expected input on invocation.
* @note the result-type of the #yield() function _must be_ `reference`, even when
* @note the result-type of the #yield() function _must be_ a `reference`, even when
* the TransformFunctor produces a value; otherwise we can not provide a safe
* `operator->` on any iterator downstream. This is also the reason why the
* ItemWrapper is necessary, precisely _because we want to support_ functions
@ -782,7 +986,7 @@ namespace lib {
class Transformer
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
using TransformFunctor = function<RES(SRC&)>;
using TransformedItem = wrapper::ItemWrapper<RES>;
@ -790,10 +994,10 @@ namespace lib {
TransformFunctor trafo_;
TransformedItem treated_;
public:
using value_type = meta::ValueTypeBinding<RES>::value_type;
using reference = meta::ValueTypeBinding<RES>::reference;
using pointer = meta::ValueTypeBinding<RES>::pointer;
public: // Note: result might be a nested iterator -- not using ValueTypeBinding here!
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
template<typename FUN>
@ -826,6 +1030,7 @@ namespace lib {
t1.treated_.reset();
t2.treated_.reset();
swap (t1.trafo_, t2.trafo_);
swap (t1.srcIter(), t2.srcIter());
}
@ -880,6 +1085,125 @@ namespace lib {
/**
* Iterator adapter to package each iterator result into a **wrapper object** of type \a WRA.
* Built as a simplified variant of the Transformer, it can not invoke an arbitrary function,
* yet will destroy the previous wrapper and construct a new wrapper into the embedded storage
* buffer on each iteration.
* @note similar as the Transformer, WrapperAdapter will _re-engage_ after copy, move and swap.
* This implies to discard a wrapped object residing in the ItermWrapper's storage; the next
* result-pull will then cause it to be regenerated from source, since the absolute storage
* locations both of the ItemWrapper storage, and the underlying source iterator has changed.
* Notably this protects against insidious data corruption in cases where some part of the
* implementation (typically indirectly) happens to take a reference into the source.
* @warning this mechanism might have surprising ramifications, especially when the underlying
* iterator has generated a collection by-value; this data will be discarded whenever the
* iterator pipeline is moved, which might happen automatically when adding further layers.
* Notably there is an interference with possible iterator decorators _added downstream_,
* that need to pull ahead for grouping and filtering.
* @remark Users of WrapperAdapter, and of IterExplorer are responsible in general to ensure
* that any embodied structure preferably relies on value copy of iterator results rather,
* and otherwise only employs references into stable memory locations.
*/
template<class SRC, typename WRA>
class WrapperAdapter
: public SRC
{
using WrappedItem = wrapper::ItemWrapper<WRA>;
WrappedItem wrapped_{/*created empty*/};
public: // Note: result might be a nested iterator -- not using ValueTypeBinding here!
using value_type = meta::RefTraits<WRA>::value_type;
using reference = meta::RefTraits<WRA>::reference;
using pointer = meta::RefTraits<WRA>::pointer;
WrapperAdapter (SRC&& dataSrc)
: SRC{move (dataSrc)}
{ }
WrapperAdapter() =default;
WrapperAdapter (WrapperAdapter const& o)
: SRC{o}
, wrapped_{/* deliberately empty: force re-engage */}
{ }
WrapperAdapter (WrapperAdapter && o)
: SRC{move (o)}
, wrapped_{/* deliberately empty: force re-engage */}
{ }
WrapperAdapter&
operator= (WrapperAdapter changed)
{
swap (*this,changed);
return *this;
}
friend void
swap (WrapperAdapter& t1, WrapperAdapter& t2)
{
using std::swap;
t1.wrapped_.reset();
t2.wrapped_.reset();
swap (t1.srcIter(), t2.srcIter());
}
/* === decorated Lumiera Forward Iterator === */
reference
operator*() const
{
return unConst(this)->reWrapIt();
}
pointer
operator->() const
{
return & operator*();
}
WrapperAdapter&
operator++()
{
wrapped_.reset(); // discard current wrapper
++ srcIter();
return *this;
}
/** refresh state when other layers manipulate the source sequence */
void
expandChildren()
{
wrapped_.reset();
SRC::expandChildren();
}
ENABLE_USE_IN_STD_RANGE_FOR_LOOPS (WrapperAdapter);
friend bool
operator== (WrapperAdapter const& w1, WrapperAdapter const& w2)
{
return w1.srcIter() == w2.srcIter();
}
private:
SRC&
srcIter() const
{
return unConst(*this);
}
reference
reWrapIt ()
{
if (not wrapped_) // build a wrapper instance once per src item
wrapped_ = * srcIter();
return *wrapped_;
}
};
/**
* @internal Decorator for IterExplorer to group consecutive elements into fixed sized chunks.
* One group of elements is always prepared eagerly, and then the next one on iteration.
@ -892,7 +1216,7 @@ namespace lib {
class Grouping
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
protected:
using Group = std::array<RES, grp>;
@ -1002,14 +1326,15 @@ namespace lib {
/**
* @internal Decorator for IterExplorer to group consecutive elements controlled by some
* grouping value \a GRP and compute an aggregate value \a AGG for each such group as
* **grouping value** \a GRP and compute an aggregate value \a AGG for each such group as
* iterator yield. The first group is consumed eagerly, each further group on iteration;
* thus when the aggregate for the last group appears as result, the source iterator has
* already been exhausted. The aggregate is default-initialised at start of each group
* and then the computation functor \a FAGG is invoked for each consecutive element marked
* with the same _grouping value_ — and this grouping value itself is obtained by invoking
* the functor \a FGRP on each source value. All computation are performed on-the-fly. No
* capturing or reordering of the source elements takes place, rather groups are formed
* already been exhausted. The aggregate is default-initialised at start of each group,
* followed by invoking the [computation functor](\ref #Aggregator) for each consecutive
* element that has been marked with the same _grouping value_ — and this grouping value
* itself is obtained by invoking the [grouping functor](\ref #Grouping) on each value
* produced by the underlying source iterator. All computation is performed on-the-fly.
* No capturing or reordering of the source elements takes place, rather groups are formed
* based on the changes of the grouping value over the source iterator's result sequence.
* @tparam AGG data type to collect the aggregate; must be default constructible and assignable
* @tparam GRP value type to indicate a group
@ -1018,14 +1343,14 @@ namespace lib {
* This limitation was deemed acceptable (adapting a function with several arguments would
* require quite some nasty technicalities). The first argument of this `aggFun` refers
* to the accumulator by value, and thereby also implicitly defines the aggregate result type.
* @warning the Aggregator \a AGG *must not capture references* to upstream internal state, because
* @warning the #Aggregator *must not capture references* to upstream internal state, because
* the overall pipeline will be moved into final location _after the initial ctor call._
*/
template<class SRC, typename AGG, class GRP>
class GroupAggregator
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
protected:
using SrcValue = meta::ValueTypeBinding<SRC>::value_type;
@ -1038,9 +1363,9 @@ namespace lib {
Aggregator aggregate_;
public:
using value_type = meta::RefTraits<AGG>::Value;
using reference = meta::RefTraits<AGG>::Reference;
using pointer = meta::RefTraits<AGG>::Pointer;
using value_type = meta::RefTraits<AGG>::value_type;
using reference = meta::RefTraits<AGG>::reference;
using pointer = meta::RefTraits<AGG>::pointer;
GroupAggregator() =default;
// inherited default copy operations
@ -1108,13 +1433,13 @@ namespace lib {
* Similar to the Transformer, the given functor is adapted as appropriate. However,
* we require the functor's result type to be convertible to bool, to serve as approval test.
* The filter predicate and thus the source iterator is evaluated _eagerly_, to establish the
* *invariant* of this class: _if a "current element" exists, it has already been approved._
* **invariant** of this class: _if a "current element" exists, it has already been approved._
*/
template<class SRC>
class Filter
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
protected:
using FilterPredicate = function<bool(SRC&)>;
@ -1201,7 +1526,7 @@ namespace lib {
* capturing it _by value._ After building, this remoulded version can be assigned to the
* original filter functor, under the assumption that both are roughly compatible. Moreover,
* since we wrap the actual lambda into an adapter, allowing for generic lambdas to be used
* as filter predicates, this setup allows for a lot of leeway regarding the concrete predicates.
* as filter predicates, this setup provides a lot of wiggle room for defining predicates.
* @note whenever the filter is remoulded, the invariant is immediately
* [re-established](\ref Filter::pullFilter() ), possibly forwarding the sequence
* to the next element approved by the new version of the filter.
@ -1215,7 +1540,7 @@ namespace lib {
: public Filter<SRC>
{
using _Filter = Filter<SRC>;
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
public:
MutableFilter() =default;
@ -1365,6 +1690,7 @@ namespace lib {
/**
* @internal Decorator for IterExplorer to cut iteration once a predicate ceases to be true.
* Similar to Filter, the given functor is adapted as appropriate, yet is required to yield
@ -1379,7 +1705,7 @@ namespace lib {
class StopTrigger
: public IterStateCore<SRC>
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
using Core = IterStateCore<SRC>;
using Cond = function<bool(SRC&)>;
@ -1579,7 +1905,7 @@ namespace lib {
class IterExplorer
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source");
static_assert(can_LumieraIter<SRC>::value, "Lumiera Iterator required as source");
public:
@ -1698,6 +2024,25 @@ namespace lib {
}
/** adapt this IterExploer to flatten _one level of nested Lumiera Iterators_.
* Obviously this processing can only be applied when the underlying pipeline yields a result
* that can be handled as being iterable; the resulting pipeline will seamlessly produce all
* results from all non-empty nested iterators, in sequence.
* @remark when chained behind a suitable [transformer(fun)](\ref transform()), the processing
* pattern is similar to the `flatMap(fun)` or `M >= fun` operation known from Monad theory.
* @return processing pipeline with attached [flattening](\ref iter_explorer::Flattener) decorator
*/
auto
flatten()
{
using Adapted = _FlatteningTraits<SRC>::SrcAdapter;
using ResCore = iter_explorer::Flattener<Adapted>;
using ResIter = _DecoratorTraits<ResCore>::SrcIter;
return IterExplorer<ResIter> (ResCore{Adapted{move(*this)}});
}
/** adapt this IterExplorer to pipe each result value through a transformation function.
* Several "layers" of mapping can be piled on top of each other, possibly mixed with the
* other types of adaptation, like the child-expanding operation, or a filter. Obviously,
@ -1760,7 +2105,7 @@ namespace lib {
static_assert (meta::is_BinaryFun<FAGG>());
using ArgType1 = _Fun<FAGG>::Args::List::Head;
using Aggregate = meta::RefTraits<ArgType1>::Value;
using Aggregate = meta::RefTraits<ArgType1>::value_type;
using ResCore = iter_explorer::GroupAggregator<SRC, Aggregate, GroupVal>;
using ResIter = _DecoratorTraits<ResCore>::SrcIter;
@ -1905,7 +2250,7 @@ namespace lib {
asPtr()
{
using Val = meta::ValueTypeBinding<SRC>::value_type;
static_assert (not std::is_pointer_v<Val>);
static_assert (not is_pointer_v<Val>);
return IterExplorer::transform ([](Val& ref){ return &ref; });
}

View file

@ -44,6 +44,7 @@
#include "lib/meta/util.hpp"
#include "lib/meta/function.hpp"
#include "lib/iter-adapter.hpp"
#include "lib/itertools.hpp"
#include "lib/nocopy.hpp"
@ -310,7 +311,7 @@ namespace lib {
{
using Src = std::remove_reference<IT>::type;
using ResVal = lib::meta::_Fun<FUN>::Ret;
using TransIter = TransformIter<Src, ResVal>;
using TransIter = decltype(lib::transformIter (std::declval<IT>(), std::declval<FUN>()));
using Iter = IterSource<ResVal>::iterator;
};
@ -326,26 +327,23 @@ namespace lib {
// we can strip the const added by the STL map types
using KeyType = std::remove_const<ConstKeyType>::type;
using KeyIter = TransformIter<Src, KeyType>;
using ValIter = TransformIter<Src, ValType>;
static KeyType takeFirst (PairType const& pair) { return pair.first; }
static ValType takeSecond(PairType const& pair) { return pair.second;}
};
template<class IT>
_PairIterT<IT>::KeyIter
auto
takePairFirst (IT&& source)
{
return transformIterator(forward<IT>(source), _PairIterT<IT>::takeFirst );
return transformIter (forward<IT>(source), _PairIterT<IT>::takeFirst );
}
template<class IT>
_PairIterT<IT>::ValIter
auto
takePairSecond (IT&& source)
{
return transformIterator(forward<IT>(source), _PairIterT<IT>::takeSecond );
return transformIter (forward<IT>(source), _PairIterT<IT>::takeSecond );
}
} //(END) type helpers...
@ -375,10 +373,10 @@ namespace lib {
auto
singleVal (VAL&& something)
{
using Src = decltype(singleValIterator (forward<VAL>(something)));
using Src = decltype(singletonIter (forward<VAL>(something)));
using Val = _IterT<Src>::Val;
return IterSource<Val>::build (new WrappedLumieraIter<Src>{singleValIterator (forward<VAL>(something))});
return IterSource<Val>::build (new WrappedLumieraIter<Src>{singletonIter (forward<VAL>(something))});
}
@ -400,7 +398,7 @@ namespace lib {
return IterSource<ValType>::build (
new WrappedLumieraIter<TransIT> (
transformIterator (forward<IT>(source), processingFunc)));
transformIter (forward<IT>(source), processingFunc)));
}
@ -408,7 +406,7 @@ namespace lib {
* all the keys of the given Map or Hashtable
*/
template<class MAP>
_MapT<MAP>::KeyIter
auto
eachMapKey (MAP& map)
{
using Range = RangeIter<typename MAP::iterator>;
@ -438,13 +436,13 @@ namespace lib {
* the distinct keys
*/
template<class MAP>
_MapT<MAP>::KeyIter
auto
eachDistinctKey (MAP& map)
{
using Range = RangeIter<typename MAP::iterator>;
Range contents (map.begin(), map.end());
return wrapIter (filterRepetitions (takePairFirst(contents)));
return wrapIter (filterRepeat (takePairFirst(contents)));
}

View file

@ -31,7 +31,7 @@
#include "lib/iter-adapter.hpp"
#include "lib/iter-explorer.hpp"
#include "lib/meta/tuple-helper.hpp"
#include "lib/meta/tuple-concept.hpp"
#include <utility>

File diff suppressed because it is too large Load diff

View file

@ -18,14 +18,14 @@
#include "lib/error.hpp"
#include "include/lifecycle.h"
#include "include/lifecycle.hpp"
#include "lib/lifecycleregistry.hpp"
#include "lib/util.hpp"
namespace lumiera {
namespace vessel {
// ==== implementation Lifecycle Registry =======
@ -64,7 +64,6 @@ namespace lumiera {
if (isNew and eventLabel == ON_BASIC_INIT)
callbackFun(); // when this code executes,
// then per definition we are already post "basic init"
// (which happens in the AppState ctor); thus fire it immediately
}
@ -83,31 +82,4 @@ namespace lumiera {
CStr ON_EMERGENCY ("ON_EMERGENCY");
} // namespace lumiera
extern "C" { /* ==== implementation C interface for lifecycle hooks ======= */
const char * lumiera_ON_BASIC_INIT = lumiera::ON_BASIC_INIT;
const char * lumiera_ON_GLOBAL_INIT = lumiera::ON_GLOBAL_INIT;
const char * lumiera_ON_GLOBAL_SHUTDOWN = lumiera::ON_GLOBAL_SHUTDOWN;
const char * lumiera_ON_EMERGENCY = lumiera::ON_EMERGENCY;
void
lumiera_LifecycleHook_add (const char* eventLabel, void callbackFun(void))
{
lumiera::LifecycleHook (eventLabel, callbackFun);
}
void
lumiera_Lifecycle_trigger (const char* eventLabel)
{
lumiera::LifecycleRegistry::instance().execute (eventLabel);
}
}
} // namespace vessel

View file

@ -14,7 +14,7 @@
/** @file lifecycleregistry.hpp
** Helper for registering lifecycle event callbacks, which are
** provided as a global service by lumiera::AppState. This service
** provided as an application service within the »vessel«. This service
** allows to enrol functions under a given label and then to call
** all those registered functions.
** @note this is in fact an event mechanism, and if we start using
@ -22,12 +22,12 @@
** boost::signals. (which has the downside of being an binary
** dependency).
**
** @see appstate.hpp
** @see voyage.hpp
*/
#ifndef LUMIERA_LIFECYCLEREGISTRY_H
#define LUMIERA_LIFECYCLEREGISTRY_H
#ifndef VESSEL_LIFECYCLE_REGISTRY_H
#define VESSEL_LIFECYCLE_REGISTRY_H
#include "lib/util.hpp"
@ -39,7 +39,7 @@
#include <map>
namespace lumiera {
namespace vessel {
using util::contains;
using std::function;
@ -50,7 +50,7 @@ namespace lumiera {
/**
* Registry of callback functions accessible by a label (ID)
* provided at registration. Registered functions will be added
* to a list, which can be triggered via label. Used by AppState
* to a list, which can be triggered via label. Used by vessel::Voyage
* to implement the lumiera lifecycle (init, shutdown) hooks.
*/
class LifecycleRegistry
@ -98,5 +98,5 @@ namespace lumiera {
} // namespace lumiera
#endif
} // namespace vessel
#endif /*VESSEL_LIFECYCLE_REGISTRY_H*/

View file

@ -32,7 +32,7 @@
** under the assumption that the allocator is a _monostate_, and
** exploiting _empty-base-optimisation,_ footprint is minimal.
** @note this linked list container is _intrusive_ and thus needs the help
** of the element type, which must *provide a pointer member* `next`.
** of the element type, which must **provide a pointer member** `next`.
** Consequently, each such node element can not be member in
** several collections at the same time (unless they share all elements)
** @note as usual, any iterator relies on the continued existence and

179
src/lib/live-mark.hpp Normal file
View file

@ -0,0 +1,179 @@
/*
LIVE-MARK.hpp - safety-tag to prevent invoking a deceased partner
Copyright (C)
2026, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file live-mark.hpp
** Memory marker tag to prevent segfault in communication with a defunct receiver.
** The tag comprises an unique hash value, that can be embedded into another object
** and tied to the lifecycle. This setup allows to build a safe-handle, that offers
** a liveness check of the communication partner and prevents dereferencing and
** invoking into the stale memory of an expired receiver. Notably this allows
** to safeguard asynchronous communication between thread-local entities,
** even when some of the participating threads terminates unexpectedly.
**
** @todo WIP 5/2026 developing a thread-local allocation pool
**
** @see engine-buffer-manager.hpp usage example
** @see LiveMark_test
**
*/
#ifndef LIB_LIVE_MARK_H_
#define LIB_LIVE_MARK_H_
#include "lib/error.hpp"
#include "lib/hash-combine.hpp"
#include "lib/util.hpp"
#include <atomic>
#include <concepts>
namespace lib {
namespace err = lumiera::error;
using lib::HashVal;
class LiveMark;
template<class SUBJ>
concept alive_marked = requires (SUBJ const& subject)
{
{ subject.getLiveMark() } -> std::convertible_to<LiveMark const&>;
};
/**
* Liveness marker tag to be incorporated into another object,
* so that a communication link can be severed automatically when
* the receiver object goes out of scope.
* @see AllocReceiver
* @see LiveMark_test
*/
class LiveMark
{
std::atomic<HashVal> id_;
public:
LiveMark() = default;
~LiveMark() { disengage(); } ///< @note the actual protection happens here
/**
* Create a LiveMark that is connected
* to the Subject to be protected.
*/
LiveMark (alive_marked auto& subject)
: id_{generateID (subject)}
{ }
// using default copy/assignment
/** deactivate and mark as dead (irreversible) */
void disengage() { id_.store (0, std::memory_order_relaxed); }
/**
* A smart-handle for safe protected access to some object
* that might go out of scope eventually. This works under the
* assumption that the target object embeds a LiveMark, which
* is expressed through the concept alive_marked. The marker
* HashVal is unique for each instance and distinct memory
* location, and thus even access to a new object instance
* with the same type, placed into the same memory location
* will be rejected, after the original target was destroyed.
*/
template<alive_marked SUBJ>
class SafeHandle
{
SUBJ * target_{nullptr};
HashVal check_{0};
public:
SafeHandle() = default;
// default copy operations
/** liveness check */
operator bool() const
{
return bool(target_)
and check_ == retrieveID (*target_);
}
/* ==== subject access ==== */
SUBJ&
operator*() const
{
if (not bool(*this))
throw err::State{"Accessing defunct target"
, LERR_(BOTTOM_VALUE)};
return * target_;
}
SUBJ*
operator->() const
{
return & SafeHandle::operator*();
}
private:
SafeHandle (SUBJ& target)
: target_{& target}
, check_{retrieveID(target)}
{ }
friend class LiveMark;
/** The heart of the implementation: peek into the
* target location to see if a valid LiveMark is embedded.
*/
static HashVal
retrieveID (SUBJ& target)
{
return target.getLiveMark().id_.load (std::memory_order_relaxed);
}
};
/**
* Create a safe smart-handle to refer to the given subject,
* similar to a raw pointer. Yet the handle will bool-check
* to `false` when the target object was destroyed
*/
template<alive_marked SUBJ>
SafeHandle<SUBJ>
makeHandle (SUBJ& subject)
{
return SafeHandle{subject};
}
private:
/** Generate unique HashVal.
* @remark seeded by a nonce (counter) and the memory address,
* which makes it impossible to get the same hash at the same
* target address more than once (unless there is a collision
* in the 64bit hash space, which is extremely rare)
*/
template<alive_marked X>
static HashVal
generateID (X const& targetAddress)
{
static std::atomic_size_t nonce;
HashVal mark = nonce.fetch_add (1, std::memory_order_relaxed);
hash::combine (mark, util::addrID (targetAddress));
return mark;
}
};
} // namespace lib
#endif /*LIB_LIVE_MARK_H_*/

86
src/lib/local-slice.hpp Normal file
View file

@ -0,0 +1,86 @@
/*
LOCAL-SLICE.hpp - access front-end to thread-local instances
Copyright (C)
2026, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file local-slice.hpp
** Front-end to attach to a set of thread-local instances.
** This setup helps to adapt a given service implementation to be deployed
** as per-thread instances, operating in a massively concurrent situation.
** The services installed this way must be able to work as self-contained
** instances transparently, possibly cooperating with a central hub behind
** the scenes, which however must be coordinated by the services themselves.
**
** Since thread-local storage is effectively static, the configuration of
** instances happens at some point _prior_ to the first use from within a
** thread, and otherwise not further specified. The LocalSlice access front-end
** is a _monostate_ and all instances thus expose the same set of per-thread
** service instances, which are created on-demand (by the C++ runtime system).
**
** @warning as of 2022 the implementation of `thread_local` in the GNU runtime
** was observed to incur seemingly unnecessary overhead, which however
** is relevant only in a high-performance environment. However, some
** [casual benchmarks](\ref LocalSlice_test::investigatePerformance)
** indicated that the problem does not seem to be _that serious_...
** @see LocalSlice_test
** @see LocalBufferStage (usage example)
** @see buffer-provider-load-test.cpp
**
*/
#ifndef LIB_LOCAL_SLICE_H_
#define LIB_LOCAL_SLICE_H_
#include "lib/integral.hpp"
namespace lib {
/** Extension point for LocalSlice: Service initialisation */
template<class SRV>
struct LocalSlice_DefaultSetup
{
using Service = SRV;
static Service init() { return Service(); }
};
/**
* Common access point to thread-local service instances.
* @tparam CONF setup to initialise the service.
*/
template<class SRV, class CONF =LocalSlice_DefaultSetup<SRV>>
struct LocalSlice
{
LocalSlice() = default;
// using default copy/assignment
using Service = CONF::Service;
/** service access */
Service&
operator*() const
{
thread_local Service srvInstance{CONF::init()};
return srvInstance;
}
Service*
operator->() const
{
return & LocalSlice::operator*();
}
};
} // namespace lib
#endif /*LIB_LOCAL_SLICE_H_*/

View file

@ -0,0 +1,236 @@
/*
COMMON-RESULT.hpp - reconciled return types
Copyright (C)
2024,2026 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file common-result.hpp
** Determine a common result type for combined functors.
** This header defines a metafunction to determine if, and how the return types
** of several functors or accessors can be combined. This task becomes relevant
** when building exploration strategies defined over a functional datastructure.
** A notable example happens when performing a _tree expansion_ from within a
** [iterator pipeline](\ref iter-explorer.hpp) or when mapping an aggregating
** functor onto a [tuple of values](\ref tuple-concept.hpp).
**
** The basic technique to find such a _reconciled type_ is to rely on the
** _ternary operator_ (`test? yes_expr : no_expr;`), which is specified to
** great detail in the C++ standard. However, the STDLIB provides only two
** tools tailored for specific cases:
** - std::common_type is what you can use to _store_ such a reconciled result,
** and includes _decaying_ the type.
** - std::common_reference is oriented towards passing of references,
** and includes elaborate mapping logic for mixture of RValues and LValues.
**
** So there is a need to expose the foundational logic for type reconciliation,
** without any special adaptation rules. Furthermore, direct support for passing
** the selection of types in the form of a [Type Sequence](\ref typelist.hpp)
** is included.
**
** @see CommonResult_test
** @see lib::iter_explorer::Expander
** @see tuple-idx-adaptor.hpp
*/
#ifndef LIB_META_COMMON_RESULT_H
#define LIB_META_COMMON_RESULT_H
#include "lib/meta/trait.hpp"
#include "lib/meta/typelist.hpp"
namespace lib {
namespace meta {
namespace detail {// Implementation: reconcile N return types...
/** @internal helper to find a reconciled result type.
* This is the technical part, and relies on a SFINAE technique:
* The test succeeds, if a ternary operator with both type results
* can be instantiated in the signature of a `check` template.
*
* Furthermore, the reconciliation for a pair of types can be
* generalised to N types by gradually reconciling the following
* types with the already established result.
*
* @remark 2026 the GNU Libstdc++ relies on precisely the same
* technique, which implies that this approach works reliably.
*/
template<typename X, typename U, typename...TYPES>
class _ProbeCommonType
{
/******************************************************************************//**
* Core of the type reconciliation logic: determine the _common type_.
* The implementation relies on the **ternary operator** — the C++ language
* specifies a detailed and elaborate algorithm for this operator to reconcile
* the results of the "true" and "false" parts of `test? true_part : false_part`;
* such an expression compiles if and only if these criteria are met and a common
* type can be determined. The results of this procedure are quite solid, and
* satisfactory in most usage situations. A mixture of references, values const
* and volatile is handled properly. Only some rare corner cases are not covered,
* like upcasting two subclasses to a common interface (actually in some cases,
* involving multiple inheritance, a single authoritative solution for this
* special problem does not exist, and thus these cases were left out
* from the language spec).
*/
template<typename T1, typename T2>
using Reconciled = decltype(true ? std::declval<T1>() : std::declval<T2>());
/** Marker type: successful test */
template<typename RES>
struct FoundResult
: std::true_type
{
using Type = RES;
};
struct NoResultFound
: std::false_type
{ };
/** SFINAE setup to probe for a reconciled type */
template<class T1
,class T2
,typename = Reconciled<T1,T2>>
static FoundResult<Reconciled<T1,T2>>
check(int);
template<class,class>
static NoResultFound
check(...);
/** perform the reconciliation test */
using FirstResult = decltype(check<X,U>(42));
/** Helper: Join results of pairwise reconciliation
* to extend the result to a sequence of types */
template<bool success, typename...TS>
struct Join
{
using Result = FirstResult;
};
template<typename TX, typename...TS>
struct Join<true, TX,TS...> ///<@note matches when FirstResult was successful
{
using CT = FirstResult::Type;
using Result = _ProbeCommonType<CT,TX,TS...>::Result;
};
public:
/** Retrieve the overall result of this metafunction */
using Result = Join<FirstResult::value, TYPES...>::Result;
};
}//(End)implementation: type reconciliation
/**
* Metafunction to find a _common result type_.
* @tparam TYPES a variadic sequence of types to be reconciled.
* Finding such a common result type can be relevant when results
* from several functors shall be combined, e.g. when mapping and
* aggregating results of a function application to a tuple.
* It is important to note that such a function may return arbitrary
* types, either by value or by reference, and possibly marked as const.
*
* \par Interface
* The `struct` CommonResult itself is a std::bool_constant that
* indicates if the given types can be reconciled. When successful,
* a nested typedef `Type` is present and aliased to a type that
* can represent all given result types as a function return.
*
* @remarks
* - the C++ standard offers similar functionality as
* + std::common_type<TYPES...>
* + std::common_reference<TYPES...>
* - however, the standard solution, while based on the same logic
* as this implementation, adds additional transformations, like
* decaying the type (for std::common_type) or handling various
* corner cases with RValue references. The specification of
* expected behaviour is quite elaborate for those cases
* - the implementation presented here is focused on _returning_
* results from a function and thus relies _solely_ on the
* logic of the ternary operator (`? : `)
* - when several LValue-reference types are given, the result
* is only a reference when all results can be presented safely
* through such a reference; in most cases the result falls back
* to returning by-value.
* @note Concepts are provided to require that a selection of types
* is reconcilable, or even reconcilable into a reference type.
* @see CommonResult_test
*/
template<typename...TYPES>
struct CommonResult
: detail::_ProbeCommonType<TYPES...>::Result
{ };
template<typename T1>
struct CommonResult<T1>
: CommonResult<T1,T1>
{ };
template<>
struct CommonResult<>
: std::false_type
{ };
/** Specialisation to work transparently also on a type sequence,
* passed-in as a single template parameter. */
template<typename...TYPES>
struct CommonResult<Types<TYPES...>>
: CommonResult<TYPES...>
{ };
template<typename...TYPES>
using CommonResult_t = CommonResult<TYPES...>::Type;
template<typename...TYPES>
static constexpr bool has_CommonResult_v = CommonResult<TYPES...>::value;
template<typename...TYPES>
concept reconcilable = requires
{
typename CommonResult<TYPES...>::Type;
};
template<typename...TYPES>
concept reconcilable_ref = reconcilable<TYPES...>
and isLRef_v<CommonResult_t<TYPES...>>;
template<typename...TYPES>
struct CommonResultTraits
{
static_assert (reconcilable<TYPES...>, "unable to reconcile these types");
using CommonVal = std::common_type_t<TYPES...>;
using ResType = CommonResult_t<TYPES...>;
//NOTE: std::common_type decays (strips CV and ref)
static constexpr bool makeConst = isConst_v<ResType>;
static constexpr bool exposeRef = isLRef_v<ResType>;
};
}} // namespace lib::meta
#endif /*LIB_META_COMMON_RESULT_H*/

View file

@ -108,13 +108,15 @@
* and check the static bool value field.
* @warning none of these checks can detect private members
*/
#define META_DETECT_NESTED(_TYPE_) \
template<typename TY> \
class HasNested_##_TYPE_ \
{ \
\
template<class X> \
static Yes_t check(X::_TYPE_ *); \
#define META_DETECT_NESTED(_TYPE_) \
template<typename TY> \
class HasNested_##_TYPE_ \
{ \
\
template<class X \
,typename = std::void_t<typename X::_TYPE_> \
> \
static Yes_t check(int); \
template<class> \
static No_t check(...); \
\

237
src/lib/meta/fun-inline.hpp Normal file
View file

@ -0,0 +1,237 @@
/*
FUN-INLINE.hpp - abstract functor with local delegate
Copyright (C)
2026 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file fun-inline.hpp
** A generic abstracted functor with the implementation stored locally.
** This template is an implementation building block and allows to create
** an abstraction barrier, similar to what std::function does. In contrast
** to the latter however, FunInline places the actual implementation delegate
** into an embedded opaque buffer, never using any heap memory.
**
** @todo WIP-WIP 4/2026 this is more like a conceptual draft,
** not sure where this design exercise will lead to.
** @warning after investigating the implementation technique to some degree,
** it became clear that **copy support** is the greatest challenge.
** All »the rest« might be surprisingly simple to implement, yet
** the demand for a movable / copyable value-object effectively
** translates into something like a VTable — and the result
** would be similar to lib::PolymorphicValue — just with
** some special binding to the function operator.
** For this reason, FunInline is now declared to be util::NonCopyable
** @todo 4/2026 I've finally got this draft hammered out.
** It is far from what I wanted to achieve...
** Notably a second function pointer is used
** That is ugly, but it does the job.
**
** @see FunInline_test
**
*/
#ifndef LIB_META_FUN_INLINE_H_
#define LIB_META_FUN_INLINE_H_
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/meta/function.hpp"
#include "lib/meta/typeseq-util.hpp"
#include "lib/meta/variadic-helper.hpp"
#include <utility>
namespace lib {
namespace meta{
namespace err = lumiera::error;
using std::forward;
namespace test { class FunInline_test; }
/**
* An opaque front-end for a functor stored inline.
* The type of the implementation delegate is erased
* from the type, retaining only the generic signature.
* @note this wrapper object is defined as NonCopyable,
* because anything else would require a VTable,
* and raise serious concerns regarding identity.
* @warning deliberately **not threadsafe**
* @see FunInline_test
*/
template<class SIG
,size_t siz
,typename ALI = int64_t
>
class FunInline
{
static_assert (!sizeof(SIG), "function signature required");
};
template<typename RET, typename...ARGS
,size_t siz
,typename ALI
>
class FunInline<RET(ARGS...), siz, ALI>
: util::NonCopyable
{
using _F = _Fun<RET(ARGS...)>;
static_assert (_F{}, "function signature required");
using Sig = _F::Sig;
using Ret = _F::Ret;
using Args = _F::Args;
using RArgs = ElmTypes<Args>::template Apply<std::add_rvalue_reference_t>;
using IArgs = Prepend<void*, RArgs>::Seq;
using ISig = BuildFunType<Ret, IArgs>::Sig;
using Dtor = void(void*);
/** virtualised delegation through function pointer(s) */
ISig* invoker_{nullptr};
Dtor* destroy_{nullptr};
/** opaque storage buffer to emplace the functor delegate */
alignas(ALI) mutable
std::byte buf_[siz];
/**
* The Invoker template is instantiated with the type of the concrete
* delegate functor (which resides in the opaque storage buffer).
* The purpose of this template is to cause generation of a static
* delegation function that can be invoked through the #invoker_ pointer.
* In the typical usage situation, the delegate type \a FUNC is implemented
* mostly with inline functions, and thus the bulk of the actual implementation
* code will be emitted into this static delegation function. The template
* \ref InvokerBuilder provides the blueprint for this code generation.
*/
template<class FUN>
struct Invoker
{
static Ret
dispatch (void* instance, ARGS&&...args)
{
return functor(instance) (forward<ARGS> (args)...);
}
static void
destroy (void* instance)
{
if (instance)
functor(instance).~FUN();
}
static FUN&
functor (void* instance)
{
REQUIRE (instance);
return * static_cast<FUN*> (instance);
}
using Sig = _F::Sig;
using ISig = decltype(dispatch);
};
/** allow the Unit test to poke into internals */
friend class test::FunInline_test;
public:
~FunInline(){ destroy(); }
FunInline() = default;
template<class FUNC>
FunInline (FUNC&& functor)
{
using Functor = std::decay_t<FUNC>;
emplace<Functor> (forward<FUNC> (functor));
activate<Functor>();
}
// NonCopyable
operator bool() const
{
return bool(destroy_) and bool(invoker_);
}
/* == Functor interface == */
Ret
operator() (ARGS&&... args)
{
if (not invoker_)
throw err::State{"Attempt to invoke inactive function."};
return (*invoker_) (&buf_, forward<ARGS> (args)...);
}
private: /* == Instance management == */
void
destroy()
{
if (destroy_)
(*destroy_) (&buf_);
destroy_ = nullptr;
invoker_ = nullptr;
}
template<class FUNC>
void
activate()
{
ASSERT_VALID_SIGNATURE (FUNC, Sig);
REQUIRE (!destroy_ and !invoker_, "Lifecycle logic broken");
destroy_ = Invoker<FUNC>::destroy;
invoker_ = Invoker<FUNC>::dispatch;
}
/** Abbreviation for placement new */
template<class TY, typename...INIT>
TY&
emplace (INIT&& ...args)
{
static_assert (siz >= sizeof(TY), "Embedded storage buffer too small");
static_assert (alignof(ALI) >= alignof(TY), "Alignment requirements not fulfilled");
destroy();
try {
return *new(&buf_) TY {std::forward<INIT> (args)...};
}
catch (...)
{
ENSURE (not destroy_);
throw;
}
}
/// @todo unused ... not required?
template<typename XXX>
XXX&
accessAs() const
{
return * std::launder (reinterpret_cast<XXX*> (&buf_));
}
};
}}// namespace lib::meta
#endif /*LIB_META_FUN_INLINE_H_*/

View file

@ -0,0 +1,125 @@
/*
FUN-TRAMPOLINE.hpp - generate static functor table
Copyright (C)
2026 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file fun-trampoline.hpp
** Build a trampoline table with functors.
** This Metaprogramming helper uses a _builder template_ to fill an embedded
** static dispatcher table with generated functors. These can then be accessed
** by index, typically to be invoked.
**
** The setup works entirely on a static level, so that any mention of some
** FunTrampoline template instantiation causes the compiler to populate the
** corresponding functor table. This arrangement can be used, for example,
** to auto-generate an index accessor for a specific tuple, so that a
** runtime subscript or iteration can be derived. Obviously doing so
** requires the types in the tuple to be [reconcilable](\ref common-type.hpp),
** so that a generic functor can be defined to handle all fields in the tuple
** based on a common generic scheme. Without that, tuples can in general
** only be iterated and transformed by compile-time metaprogramming.
**
** @see FunTrampoline_test
** @see CommonResult_test
** @see tuple-idx-adaptor.hpp
*/
#ifndef LIB_META_FUN_TRAMPOLINE_H
#define LIB_META_FUN_TRAMPOLINE_H
#include "include/logging.h"
#include "lib/meta/function.hpp"
#include "lib/meta/variadic-helper.hpp"
#include <utility>
#include <array>
namespace lib {
namespace meta {
/**
* FunTrampoline relies on a helper template
* to be used as _trampoline builder_ to generate
* a sequence of functors that can be placed into a
* static dispatcher table. Thus, a trampoline_builder
* - must be default constructible
* - must define a Functor type...
* - that can be used as array element
* - and must provide a templated build<idx>() function
*/
template<class B>
concept trampoline_builder = requires(B builder)
{
typename B::Functor;
std::array<typename B::Functor, 1>{};
requires std::default_initializable<B>;
{ builder.template build<0>() } -> std::convertible_to<typename B::Functor>;
};
/**
* A static functor table, populated automatically.
* @tparam BUILD helper Template to generate functors
* that are placed into a static table
* @tparam TSEQ sequence of types to instantiate
* the functors with, iteratively.
* @remarks
* - the given type sequence is handled with the
* ElmTypes helper template (see \ref variadic-helper.hpp)
* - the BUILD template is instantiated with that \a TSEQ
* - statically-for-each-index, the build<idx>() function
* on the builder is invoked, which is expected to produce
* an instance of BUILD::Functor
* @warning as soon as some instance of this FunTrampoline
* is used / referred, the compiler will emit the trampoline
* table into static storage; the resulting increase of the
* executable size is something to be aware of, when using
* this construct for transient processing, driven by
* actual argument types.
*/
template<template<class> class BUILD, typeseq TSEQ>
requires trampoline_builder<BUILD<TSEQ>>
class FunTrampoline
{
using AllElm = ElmTypes<TSEQ>;
using Indices = AllElm::Idx;
using Builder = BUILD<TSEQ>;
using Functor = Builder::Functor;
using Table = std::array<Functor, AllElm::SIZ>;
template<size_t...idx>
static constexpr Table
buildFunctors (std::index_sequence<idx...>)
{
return {Builder().template build<idx>()...};
}
static constexpr Table functors = buildFunctors (Indices{});
public:
static constexpr size_t size() { return functors.size(); }
static constexpr size_t memSiz() { return sizeof(functors);}
static Functor const&
dispatch (size_t idx)
{
ENSURE (idx < functors.size());
return functors[idx];
}
using ResType = _Fun<Functor>::Ret;
};
}}// namespace lib::meta
#endif /*LIB_META_FUN_TRAMPOLINE_H*/

View file

@ -41,7 +41,7 @@ This code is heavily inspired by
** instantiation is mixed or inherited into the resulting product.
**
** @see generator-test.cpp
** @see lumiera::query::ConfigRules usage example
** @see vessel::query::ConfigRules usage example
** @see typelist.hpp
**
*/

View file

@ -1,56 +0,0 @@
/*
MAYBE-COMPARE.hpp - guarded invocation of comparisons
Copyright (C)
2009, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file maybe-compare.hpp
** helpers for fail-safe invocation of comparison operations from generic code.
*/
#ifndef LIB_META_MAYBE_COMPARE_H
#define LIB_META_MAYBE_COMPARE_H
namespace lib {
namespace meta{
/**
* Trait template for invoking equality comparison.
* This allows to treat some types specifically....
*/
template<typename X>
struct Comparator
{
static bool
equals (X const& x1, X const& x2)
{
return x1 == x2;
}
};
/* == add specialisations here == */
template<typename X>
inline bool
equals_safeInvoke (X const& x1, X const& x2)
{
return Comparator<X>::equals(x1,x2);
}
}} // namespace lib::meta
#endif

View file

@ -0,0 +1,147 @@
/*
PROP-BUILDER.hpp - generate data record with arbitrary named fields
Copyright (C)
2026, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file prop-builder.hpp
** A builder for compile-time generation of data records with arbitrary named fields.
** Generally speaking, a "record" in this context means just a struct or class with some
** data fields, and can be easily defined to suit any needs. However, such a class definition
** always incurs some _syntactical overhead_ — which is fine for spelling out some type in
** general, but can be problematic when the data record is only used once, locally and
** is without further significance beyond that single usage. Such a situation often
** arises in the context of _policy based design_ and flexible configuration of
** some generic facility, especially when part of this configuration involves
** the definition of _callback functions._
**
** A common requirement that lends itself well to policy based design arises when such callbacks
** establish a deeply interwoven link into local implementation details; this kind of interconnected
** implementation can be expressed succinctly through Lambda functions with contextual capture, and
** allows to setup a tight interconnection between implementation back-ends, without exposing any
** of these implementation details at the interface / dependency-injection level. However, the task
** to package several such specific callbacks into a policy instance and pass this configured policy
** for creation of service instances, can be surprisingly complex, verbose, confusing and leads
** to code that is hard to maintain. The reasons for this challenge are manifold:
** - contextual capture works only well from directly within an implementation function
** - such a setup can not be expressed well through types and interfaces, since the
** generated closure is _essentially a value_ and its type can not be spelled out.
** - a policy with several callbacks thus turns into a template with several type parameters,
** and must be initialised through a likewise template based constructor; the resulting code
** tends to become very technical, verbose, surprisingly tricky and hard to decipher for
** any reader not familiar with all the intricate details of the implementation.
**
** The root of these difficulties of proper formulation seems to be related to the nature
** of cohesive, closely interwoven implementation code. As long as this kind of structure
** can be kept close together, it works surprisingly well and expresses intent clearly.
** Yet as soon as it must be taken apart, expressed in names, types and interfaces,
** a high conceptual burden is added, which leads to the danger of generalisation
** and re-use beyond the very specific case, to which the original formulation
** of the problem was well adapted and adequate.
**
** The solution promoted here thus aims to remove the need to define any type and interface
** explicitly; a builder-expression is created rather, which allows to bind all callbacks
** and further parameters directly inline, within a single constructor invocation.
** Such a setup becomes possibly through a combination of some C++20 features, linked
** together by lambda expressions generated from a _macro_.
** - the PropBuilder acts as base class for the generated records, and is used to
** initiate the build-up of the definition expression.
** - several »layers« of derived classes are added step by step, where each such
** layer adds one further _property_ attribute, that is initialised by a given value.
** - the _junctor_ to combine those layer is provided by the template function
** `PropBuilder::define(builder-λ, value)`. It uses the new _explicit object_
** syntax introduced with C++20, to pick up the "self type" as template argument.
** - by invoking this builder function, the current "this"-object is _moved_ into
** the builder, where it is used to create and initialise a derived class.
** - for each »layer« a specific lambda is generated by a macro — and it is this
** technique that allows to accept a lexical token form the program text and
** use this token as field name in the next derived class to generate.
** @see PropBuilder_test for detailed explanation
** @see OutputProxyProvider_test for a usage example
*/
#ifndef LIB_META_PROP_BUILDER_H
#define LIB_META_PROP_BUILDER_H
#include "lib/meta/trait.hpp"
#include <utility>
namespace lib {
namespace meta{
using std::move;
using std::forward;
/**
* Anchor record and builder mechanics for generating data records.
* - start a builder expression by constructing an anonymous `PropBulder()`
* - invoke PropBuilder::define() for each further property to add
* - pass a builder-λ as first argument, and the data value as second
* - generate a suitable builder-λ with the macro `PROP_FIELD(fieldname)`
* - overall, the builder expression generates a RValue, that will usually
* be passed directly to the constructor of another class.
* @see PropBuilder_test::simpleUsage
*/
struct PropBuilder
{
/**
* Core builder operation: invoke a builder-λ to extend »this«
* @param layerBuilder a specially crafted invokable, that takes _this object_
* and the given property value and creates an instance of a derived class.
* @note the macro \ref PROP_FIELD below will generate a suitable argument.
* @param val an arbitrary (movable) data element to be stored as property.
* @remark C++20 introduced the syntax for _explicit object member functions_,
* which allow to bind the "self" object instance to an explicitly defined
* first function parameter. Since this builder operation will be invoked
* on the generated compound object, the template parameter \a BAS will
* denote this compound, and the next builder-λ will derive from it.
*/
template<class BAS, class LAY, typename VAL>
auto
define (this BAS&& chain, LAY const& layerBuilder, VAL&& val)
{
return layerBuilder (move(chain), forward<VAL>(val));
}
};
#define PROP_FIELD(FIELD_NAME) \
[]<class BAS, typename VAL> \
(BAS&& bas, VAL&& val) \
{ \
using BaseLayer = std::decay_t<BAS>;\
using Property = std::decay_t<VAL>; \
\
struct DataLayer_ ## FIELD_NAME \
: BaseLayer \
{ \
Property FIELD_NAME; \
\
DataLayer_ ## FIELD_NAME (BaseLayer b \
,Property v) \
: BaseLayer{move(b)} \
,FIELD_NAME{move(v)} \
{ } \
}; \
return DataLayer_ ## FIELD_NAME {forward<BAS>(bas)\
,forward<VAL>(val) \
}; \
}
}} // namespace lib::meta
#endif /*LIB_META_PROP_BUILDER_H*/

View file

@ -57,9 +57,6 @@
//Forward declarations for the Unwrap helper....
namespace boost{
template<class X> class reference_wrapper;
}
namespace std {
template<class X> class reference_wrapper;
template<class X> class shared_ptr;
@ -90,9 +87,7 @@ namespace meta {
using std::remove_pointer_t;
using std::remove_reference_t;
using std::conditional_t;
using std::is_reference_v;
using std::is_lvalue_reference_v;
using std::is_rvalue_reference_v;
using std::is_pointer_v;
using std::is_pointer;
using std::is_base_of;
using std::is_convertible;
@ -108,13 +103,22 @@ namespace meta {
using std::__or_;
template<typename T>
static constexpr bool isConst_v = std::is_const_v<remove_reference_t<T>>;
using isConst = std::is_const<remove_reference_t<T>>;
template<typename T>
static constexpr bool isLRef_v = std::is_lvalue_reference_v<T>;
using isLRef = std::is_lvalue_reference<T>;
template<typename T>
static constexpr bool isRRef_v = std::is_rvalue_reference_v<T>;
using isRRef = std::is_rvalue_reference<T>;
template<typename T>
static constexpr bool isRef_v = std::is_reference_v<T>;
using isRef = std::is_reference<T>;
template<typename T>
static constexpr bool isConst_v = isConst<T>::value;
template<typename T>
static constexpr bool isLRef_v = isLRef<T>::value;
template<typename T>
static constexpr bool isRRef_v = isRRef<T>::value;
template<typename T>
static constexpr bool isRef_v = isRef<T>::value;
/**
@ -159,19 +163,6 @@ namespace meta {
}
};
template<typename X>
struct Unwrap<boost::reference_wrapper<X>>
: std::true_type
{
using Type = X;
static X&
extract (boost::reference_wrapper<X> wrapped)
{
return wrapped;
}
};
template<typename X>
struct Unwrap<std::reference_wrapper<X>>
: std::true_type
@ -249,8 +240,8 @@ namespace meta {
template<typename X>
struct Strip
{
using TypeUnconst = conditional_t<is_reference_v<X>
, conditional_t<is_rvalue_reference_v<X>
using TypeUnconst = conditional_t<isRef_v<X>
, conditional_t<isRRef_v<X>
, remove_cv_t<remove_reference_t<X>> &&
, remove_cv_t<remove_reference_t<X>> & >
, remove_cv_t<X>>;
@ -274,33 +265,33 @@ namespace meta {
template<typename TY>
struct RefTraits
{
typedef TY Value;
typedef TY* Pointer;
typedef TY& Reference;
typedef TY value_type;
typedef TY& reference;
typedef TY* pointer;
};
template<typename TY>
struct RefTraits<TY *>
{
typedef TY* Value;
typedef TY** Pointer;
typedef TY*& Reference;
typedef TY* value_type;
typedef TY*& reference;
typedef TY** pointer;
};
template<typename TY>
struct RefTraits<TY &>
{
typedef TY Value;
typedef TY* Pointer;
typedef TY& Reference;
typedef TY value_type;
typedef TY& reference;
typedef TY* pointer;
};
template<typename TY>
struct RefTraits<TY &&>
{
typedef TY Value;
typedef TY* Pointer;
typedef TY& Reference;
typedef TY value_type;
typedef TY& reference;
typedef TY* pointer;
};
@ -328,7 +319,11 @@ namespace meta {
, is_same<I,S>
>
{ };
template<typename S, typename I>
inline constexpr bool is_Subclass_v = is_Subclass<S,I>();
/** compare for unadorned base type, disregarding const and references */
template<typename S, typename I>
struct is_basically
@ -339,8 +334,8 @@ namespace meta {
/** verify the first (special) type can stand-in for the second */
template<typename S, typename G>
struct can_StandIn
: std::is_convertible<typename RefTraits<S>::Reference
,typename RefTraits<G>::Reference
: std::is_convertible<typename RefTraits<S>::reference
,typename RefTraits<G>::reference
>
{ };
@ -505,24 +500,36 @@ namespace meta {
* This is just a heuristic, based on some common properties
* of such iterators; it is enough to distinguish it from an
* STL container, but can certainly be refined.
* @note deliberately a Lumiera iterator does _not fulfil_
* the concept std::input_iterator. And vice versa:
* a STL iterator lacks the `bool` check for iteration end.
* @see IterableClassification_test
*/
template<typename T>
class can_IterForEach
class can_LumieraIter
{
using Type = Strip<T>::Type;
using Type = Strip<T>::TypeReferred;
META_DETECT_NESTED(value_type);
META_DETECT_NESTED(reference);
META_DETECT_OPERATOR_DEREF();
META_DETECT_OPERATOR_INC();
public:
enum{ value = std::is_constructible<bool, Type>::value
and HasNested_value_type<Type>::value
and HasNested_reference<Type>::value
and HasOperator_deref<Type>::value
and HasOperator_inc<Type>::value
};
};
template<class IT>
constexpr bool is_LumieraIter_v = meta::can_LumieraIter<IT>::value;
template<class IT>
concept lum_iter = is_LumieraIter_v<IT>;
/** Trait template to detect a type exposing a »state core« API.
@ -530,10 +537,10 @@ namespace meta {
* with the help of lib::IterStateWrapper or lib::IterableDecorator.
* This check is heuristic, based on the presence of function names.
*/
template<typename T>
template<typename COR>
class is_StateCore
{
using Type = Strip<T>::Type;
using Type = Strip<COR>::TypeReferred;
META_DETECT_FUNCTION_ARGLESS(checkPoint);
META_DETECT_FUNCTION_ARGLESS(iterNext);
@ -546,15 +553,24 @@ namespace meta {
};
};
template<class COR>
constexpr bool is_StateCore_v = is_StateCore<COR>::value;
template<class COR>
concept state_core = is_StateCore_v<COR>;
/** Trait template to detect a type usable with the STL for-each loop.
* Basically we're looking for the functions to get the begin/end iterator
* @remark many Lumiera Iterators define those additional functions any
* can thus be used in a for-each loop, since a default-constructed
* Lumiera Iterator can act as an "end" sentinel.
*/
template<typename T>
class can_STL_ForEach
{
using Type = Strip<T>::Type;
using Type = Strip<T>::TypeReferred;
struct is_iterable
{
@ -618,7 +634,7 @@ namespace meta {
template<typename T>
class can_STL_backIteration
{
using Type = Strip<T>::Type;
using Type = Strip<T>::TypeReferred;
struct is_backIterable
{

View file

@ -0,0 +1,305 @@
/*
TUPLE-CONCEPT.hpp - metaprogramming support for tuple-like data types
Copyright (C)
2025, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file tuple-concept.hpp
** Metaprogramming with tuples-of-types and the `std::tuple` record.
** This header complements typelist.hpp and provides a bridge from type sequences
** to the tuple type provided by the standard library, including traits and
** helpers to build tuple types from metaprogramming and to pretty-print tuples.
**
** Notably, a `concept tuple_like` is provided here, which is satisfied for any type
** in compliance with the »[tuple protocol]«. This solution reaches beyond the scope
** of the C++ standard — and in fact opposes the decision of the C++ Committee
** _not to provide such a concept_, as this can not be defined in full genericity
** for any type that can be handled by the _structural bindings of the C++ language.
**
** Since the Lumiera support library is part of a specific application framework, and
** does not intend to be a general purpose library, we prefer a partially complete
** definition that allows us to subsume and handle custom types under a »Tuple« concept.
** @note Unfortunately the C++ committee did not only decline to provide a tuple_like concept,
** rather they choose to lock-down essential parts of the accompanying library to their
** internal and hard-wired selection of what is considered "tuple-like". This forces us
** to redefine and replace also the crucial function `std::apply`, so that a generic
** function can be applied to any _tuple-like_ entity.
** @see TupleHelper_test
** @see tuple-helper.hpp extended tuple support functions
** [tuple protocol]: https://en.cppreference.com/w/cpp/language/structured_binding.html#Case_2:_binding_a_type_implementing_the_tuple_operations
*/
#ifndef LIB_META_TUPLE_CONCEPT_H
#define LIB_META_TUPLE_CONCEPT_H
#include "lib/meta/util.hpp"
#include "lib/meta/variadic-helper.hpp"
#include <type_traits>
#include <functional>
#include <tuple>
namespace lib {
namespace meta {
using std::remove_cv_t;
using std::is_reference_v;
using std::remove_reference_t;
/** @internal building-block: a type supporting the `tuple_size` metafunction */
template<class TUP>
concept tuple_sized = requires
{
{ std::tuple_size<TUP>::value } -> std::convertible_to<size_t>;
};
/** @internal building-block: a type where elements can be accessed through a `get` friend function */
template<class TUP, std::size_t idx>
concept tuple_adl_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ get<idx>(tup) } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
/** @internal building-block: a type where elements can be accessed through a `get` member function */
template<class TUP, std::size_t idx>
concept tuple_mem_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ tup.template get<idx>() } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
template<class TUP, std::size_t idx>
concept tuple_element_accessible = tuple_mem_accessible<TUP,idx> or tuple_adl_accessible<TUP,idx>;
template<class TUP>
concept tuple_accessible =
tuple_sized<TUP> and
WithIdxSeq<std::tuple_size_v<TUP>>::andAll([](auto idx)
{
return tuple_element_accessible<TUP,idx>;
});
/**
* Concept to mark any type compliant to the »tuple protocol«
* @note a type satisfying this concept can participate in _structured bindings_,
* yet not every type that can be used in such bindings will fulfil this concept,
* since structured bindings can also be applied to _some POD like_ data records.
*/
template<class TUP>
concept tuple_like = not is_reference_v<TUP>
and tuple_sized<remove_cv_t<TUP>>
and tuple_accessible<remove_cv_t<TUP>>;
/**
* Concept to require an instance of std::tuple<TYPES...>`
* @note other than `concept` \ref tuple_like, this aims
* explicitly at the implementation form STDLIB.
*/
template<class TUP>
concept tup = requires (TUP tup)
{
// check that a function can be invoked with a std::tuple<TYPES>
[]<typename...TYPES>(std::tuple<TYPES...>){/*placeholder*/}(tup);
};
template<class TUP>
struct is_Tuple : std::bool_constant<tup<TUP>> { };
template<class TUP>
static constexpr bool is_Tuple_v = is_Tuple<TUP>::value;
/**
* Helper for abstracted / unified access to member elements of any _tuple-like_
* @remark preferably uses a `get<i>` member function, falling back to a
* free function `get`, which is found by ADL.
*/
template<std::size_t idx, class TUP>
requires tuple_like<std::remove_reference_t<TUP>>
decltype(auto)
getElm (TUP&& tup)
{
using Tup = std::remove_reference_t<TUP>;
static_assert (0 < std::tuple_size_v<Tup>);
if constexpr (tuple_mem_accessible<Tup,0>)
{
if constexpr (std::is_reference_v<TUP>)
return tup.template get<idx>();
else
{ // return value copy when tuple given as RValue
using Elm = std::tuple_element_t<idx, TUP>;
Elm elm(tup.template get<idx>());
return elm;
}
}
else
{ // ▽▽▽ ADL
using std::get;
return get<idx> (std::forward<TUP> (tup));
}
}
/**
* Specialisation of variadic access for any tuple-like
* @see variadic-helper.hpp
*/
template<tuple_like TUP>
struct ElmTypes<TUP>
{
template<typename>
struct Extract;
template<size_t...idx>
struct Extract<std::index_sequence<idx...>>
{
using ElmTypes = Types<std::tuple_element_t<idx,TUP> ...>;
};
static constexpr size_t SIZ = std::tuple_size_v<TUP>;
using Idx = std::make_index_sequence<SIZ>;
using Seq = Extract<Idx>::ElmTypes;
using Tup = RebindVariadic<std::tuple, Seq>::Type;
template<template<class> class META>
using Apply = ElmTypes<Seq>::template Apply<META>;
template<template<typename...> class O>
using Rebind = RebindVariadic<O, Seq>::Type;
template<template<class> class PRED>
using AndAll = ElmTypes<Apply<PRED>>::template Rebind<std::__and_>;
template<template<class> class PRED>
using OrAny = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
};
namespace { // apply to tuple-like : helpers...
/** @internal invocation helper similar to C++17 — but preferring a custom `get` impl */
template<typename FUN, typename TUP, size_t...Idx>
constexpr decltype(auto)
__unpack_and_apply (FUN&& f, TUP&& tup, std::index_sequence<Idx...>)
{
return std::invoke (std::forward<FUN> (f)
,getElm<Idx> (std::forward<TUP>(tup))...
);
}
/** @internal invoke a metafunction with \a FUN and all element types from the _tuple-like_ \a TUP */
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup
{
using Tupl = std::decay_t<TUP>;
using Elms = ElmTypes<Tupl>::Seq;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup<META, FUN, TUP&>
{
using Tupl = std::decay_t<TUP>;
using Elms = typename ElmTypes<Tupl>::template Apply<std::add_lvalue_reference_t>;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<class FUN, class TUP>
inline constexpr bool can_nothrow_invoke_tup = _InvokeMetafunTup<std::is_nothrow_invocable,FUN,TUP>::Type::value;
}
/**
* Replacement for `std::apply` — yet applicable to _tuple-like custom types_.
* For unclear reasons, the standard chooses to reject such custom types, and
* only allows a fixed set of explicitly defined facilities from the Stdlib
* (tuple, pair, array, and some ranges stuff).
* @todo 6/2025 as a first step, this replicates the implementation from C++17;
* the second step would be to constrain this to a concept `tuple_like`
*/
template<class FUN, class TUP> requires tuple_like<remove_reference_t<TUP>>
constexpr decltype(auto)
apply (FUN&& f, TUP&& tup) noexcept (can_nothrow_invoke_tup<FUN,TUP> )
{
using Indices = std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<TUP>>>;
return __unpack_and_apply (std::forward<FUN> (f)
,std::forward<TUP> (tup)
,Indices{}
);
}
/**
* Tuple iteration: perform some arbitrary operation on each element of a tuple.
* @note the given functor must be generic, since each position of the tuple
* may hold a data element of different type.
* @remark credits to David Vandevoorde (member of C++ committee) for using
* std::apply to unpack the tuple's contents into an argument pack and
* then employ a fold expression with the comma operator.
*/
template<class TUP, class FUN> requires tuple_like<remove_reference_t<TUP>>
constexpr void
forEach (TUP&& tuple, FUN fun)
{
lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{
(fun (std::forward<ELMS>(elms)), ...);
}
,std::forward<TUP> (tuple));
}
/**
* Apply some arbitrary function onto all elements of a tuple.
* @return a new tuple constructed from the results of this function
* @note the functor must be generic and able to work with all element types
* @warning pay attention to references; the function can take arguments by-ref
* and manipulate them (side-effect), and it may return references,
* which will be placed as such into the result tuple; furthermore,
* the argument tuple can also be taken as reference...
* @remark The tuple constructor invocation is preceded by a regular argument evaluation,
* which has unspecified evaluation order (even in C++17); _no assumptions_ can be
* made regarding the order the functor will see the source tuple elements.
* Notably this differs from #forEach, where a fold-expression with comma-operator
* is used, which is guaranteed to evaluate from left to right.
*/
template<class TUP, class FUN> requires tuple_like<remove_reference_t<TUP>>
constexpr auto
mapEach (TUP&& tuple, FUN fun)
{
return lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{ //..construct the type explicitly (make_tuple would decay fun result types)
using Tuple = std::tuple<decltype(fun (std::forward<ELMS>(elms))) ...>;
return Tuple (fun (std::forward<ELMS>(elms)) ...);
}
,std::forward<TUP> (tuple));
}
}} // namespace lib::meta
#endif /*LIB_META_TUPLE_CONCEPT_H*/

View file

@ -2,7 +2,7 @@
TUPLE-HELPER.hpp - metaprogramming utilities for type and data tuples
Copyright (C)
2016, Hermann Vosseler <Ichthyostega@web.de>
2016,2025 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
@ -18,17 +18,38 @@
** to the tuple type provided by the standard library, including traits and
** helpers to build tuple types from metaprogramming and to pretty-print tuples.
**
** Notably, a `concept tuple_like` is provided, which is satisfied for any type in compliance
** with the »tuple protocol«. Together with a [generic accessor][\ref lib::meta::getElm],
** this allows to handle all _tuple-like_ types uniformly.
** @note Due to an unfortunate limitation of the standard, we're forced to provide our own alternative
** implementation to replace `std::apply`, so that a function can be applied to any _tuple-like_
** Notably the handling of tuples is based on a **Concept** `tuple_like`,
** that is satisfied for any type in compliance with the »[tuple protocol]«.
** Together with a [generic accessor][\ref lib::meta::getElm], this allows
** to handle all _tuple-like_ types uniformly.
** This solution reaches beyond the scope of the C++ standard, which declines
** to define such a concept and rather provides an "exposition-only" concept
** that is limited to a hard-wired selection of library types, yet does not
** cover custom user provided types able to participate in _structured bindings_.
** @note Due to that unfortunate limitation of the standard, we're forced to
** provide our own alternative implementation of `std::apply`.
**
** Furthermore, a generic iteration construct is provided, to instantiate
** a generic Lambda for each element of a given tuple, which allows to write
** generic code »for each tuple element«.
**
** @see control::CommandDef usage example
** @remark 2026-03 this header underwent some significant rework in recent years.
** The Lumiera project had developed a `Tuple` type way before something similar
** was considered for inclusion into the C++ language standard; flexible and
** specifically typed storage records were used, in conjunction with LISP-style
** metaprogramming (»Loki Typelists«), from the very start of the project.
** Storing an arbitrary function, combined with a tuple of arguments, was a
** major undertaking at those days, and required a huge body of support
** functionality — which needed to be remoulded gradually, over the
** following years, to become compatible with newer C++ features.
** - basic handling, like access and compile-time iteration became trivial
** - however, the project still relies on specific metaprogramming tools,
** to simplify handling and transforming of variadic metafunctions.
**
** [tuple protocol]: https://en.cppreference.com/w/cpp/language/structured_binding.html#Case_2:_binding_a_type_implementing_the_tuple_operations
** @see variadic-helper.hpp
** @see tuple-closure.hpp : usage example
** @see feed-manifold.hpp : use of unified variadics and tuples
** @see TupleHelper_test
** @see typelist.hpp
** @see function.hpp
@ -40,268 +61,31 @@
#ifndef LIB_META_TUPLE_HELPER_H
#define LIB_META_TUPLE_HELPER_H
#include "lib/meta/typelist.hpp"
#include "lib/integral.hpp"
#include "lib/meta/tuple-concept.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/meta/typelist-util.hpp"
#include "lib/meta/typeseq-util.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/meta/util.hpp"
#include <tuple>
#include <utility>
#include <sstream>
#include <functional>
namespace util { // forward declaration
namespace util { // forward -> see lib/format-obj.hpp ...
template<typename TY>
std::string
toString (TY const& val) noexcept;
}
std::string toString (TY const&) noexcept;
template<typename X, typename COND>
struct StringConv;
}
namespace lib {
namespace meta {
/** trait to detect tuple types */
template<typename T>
struct is_Tuple
: std::false_type
{ };
template<typename...TYPES>
struct is_Tuple<std::tuple<TYPES...>>
: std::true_type
{ };
template<typename...TYPES>
struct is_Tuple<const std::tuple<TYPES...>>
: std::true_type
{ };
using std::remove_cv_t;
using std::is_reference_v;
using std::remove_reference_t;
/** @internal building-block: a type supporting the `tuple_size` metafunction */
template<class TUP>
concept tuple_sized = requires
{
{ std::tuple_size<TUP>::value } -> std::convertible_to<size_t>;
};
/** @internal building-block: a type where elements can be accessed through a `get` friend function */
template<class TUP, std::size_t idx>
concept tuple_adl_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ get<idx>(tup) } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
/** @internal building-block: a type where elements can be accessed through a `get` member function */
template<class TUP, std::size_t idx>
concept tuple_mem_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ tup.template get<idx>() } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
template<class TUP, std::size_t idx>
concept tuple_element_accessible = tuple_mem_accessible<TUP,idx> or tuple_adl_accessible<TUP,idx>;
template<class TUP>
concept tuple_accessible =
tuple_sized<TUP> and
WithIdxSeq<std::tuple_size_v<TUP>>::andAll([](auto idx)
{
return tuple_element_accessible<TUP,idx>;
});
/**
* Concept to mark any type compliant to the »tuple protocol«
*/
template<class TUP>
concept tuple_like = not is_reference_v<TUP>
and tuple_sized<remove_cv_t<TUP>>
and tuple_accessible<remove_cv_t<TUP>>;
/**
* Helper for abstracted / unified access to member elements of any _tuple-like_
* @remark preferably uses a `get<i>` member function, falling back to a
* free function `get`, which is found by ADL.
*/
template<std::size_t idx, class TUP>
requires(tuple_like<std::remove_reference_t<TUP>>)
decltype(auto)
getElm (TUP&& tup)
{
using Tup = std::remove_reference_t<TUP>;
static_assert (0 < std::tuple_size_v<Tup>);
if constexpr (tuple_mem_accessible<Tup,0>)
{
if constexpr (std::is_reference_v<TUP>)
return tup.template get<idx>();
else
{ // return value copy when tuple given as RValue
using Elm = std::tuple_element_t<idx, TUP>;
Elm elm(tup.template get<idx>());
return elm;
}
}
else
{ // ▽▽▽ ADL
using std::get;
return get<idx> (std::forward<TUP> (tup));
}
}
namespace { // apply to tuple-like : helpers...
/** @internal invocation helper similar to C++17 — but preferring a custom `get` impl */
template<typename FUN, typename TUP, size_t...Idx>
constexpr decltype(auto)
__unpack_and_apply (FUN&& f, TUP&& tup, std::index_sequence<Idx...>)
{
return std::invoke (std::forward<FUN> (f)
,getElm<Idx> (std::forward<TUP>(tup))...
);
}
/** @internal invoke a metafunction with \a FUN and all element types from the _tuple-like_ \a TUP */
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup
{
using Tupl = std::decay_t<TUP>;
using Elms = ElmTypes<Tupl>::Seq;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup<META, FUN, TUP&>
{
using Tupl = std::decay_t<TUP>;
using Elms = typename ElmTypes<Tupl>::template Apply<std::add_lvalue_reference_t>;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<class FUN, class TUP>
inline constexpr bool can_nothrow_invoke_tup = _InvokeMetafunTup<std::is_nothrow_invocable,FUN,TUP>::Type::value;
}
/**
* Replacement for `std::apply` — yet applicable to _tuple-like custom types_.
* For unclear reasons, the standard chooses to reject such custom types, and
* only allows a fixed set of explicitly defined facilities from the Stdlib
* (tuple, pair, array, and some ranges stuff).
* @todo 6/2025 as a first step, this replicates the implementation from C++17;
* the second step would be to constrain this to a concept `tuple_like`
*/
template<class FUN, class TUP> requires(tuple_like<remove_reference_t<TUP>>)
constexpr decltype(auto)
apply (FUN&& f, TUP&& tup) noexcept (can_nothrow_invoke_tup<FUN,TUP> )
{
using Indices = std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<TUP>>>;
return __unpack_and_apply (std::forward<FUN> (f)
,std::forward<TUP> (tup)
,Indices{}
);
}
/**
* Tuple iteration: perform some arbitrary operation on each element of a tuple.
* @note the given functor must be generic, since each position of the tuple
* may hold a data element of different type.
* @remark credits to David Vandevoorde (member of C++ committee) for using
* std::apply to unpack the tuple's contents into an argument pack and
* then employ a fold expression with the comma operator.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr void
forEach (TUP&& tuple, FUN fun)
{
lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{
(fun (std::forward<ELMS>(elms)), ...);
}
,std::forward<TUP> (tuple));
}
/**
* Apply some arbitrary function onto all elements of a tuple.
* @return a new tuple constructed from the results of this function
* @note the functor must be generic and able to work with all element types
* @warning pay attention to references; the function can take arguments by-ref
* and manipulate them (side-effect), and it may return references,
* which will be placed as such into the result tuple; furthermore,
* the argument tuple can also be taken as reference...
* @remark The tuple constructor invocation is preceded by a regular argument evaluation,
* which has unspecified evaluation order (even in C++17); _no assumptions_ can be
* made regarding the order the functor will see the source tuple elements.
* Notably this differs from #forEach, where a fold-expression with comma-operator
* is used, which is guaranteed to evaluate from left to right.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr auto
mapEach (TUP&& tuple, FUN fun)
{
return lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{ //..construct the type explicitly (make_tuple would decay fun result types)
using Tuple = std::tuple<decltype(fun (std::forward<ELMS>(elms))) ...>;
return Tuple (fun (std::forward<ELMS>(elms)) ...);
}
,std::forward<TUP> (tuple));
}
/**
* Specialisation of variadic access for any tuple-like
* @see variadic-helper.hpp
*/
template<tuple_like TUP>
struct ElmTypes<TUP>
{
template<typename>
struct Extract;
template<size_t...idx>
struct Extract<std::index_sequence<idx...>>
{
using ElmTypes = Types<std::tuple_element_t<idx,TUP> ...>;
};
static constexpr size_t SIZ = std::tuple_size_v<TUP>;
using Idx = std::make_index_sequence<SIZ>;
using Seq = Extract<Idx>::ElmTypes;
using Tup = RebindVariadic<std::tuple, Seq>::Type;
template<template<class> class META>
using Apply = ElmTypes<Seq>::template Apply<META>;
template<template<typename...> class O>
using Rebind = RebindVariadic<O, Seq>::Type;
template<template<class> class PRED>
using AndAll = ElmTypes<Apply<PRED>>::template Rebind<std::__and_>;
template<template<class> class PRED>
using OrAll = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
};
namespace { // rebinding helper to create std::tuple from a type sequence
template<typename SEQ>
@ -389,8 +173,8 @@ namespace meta {
* selecting a (partial) specialisation based on the source type given.
* @see ExtractArg
*/
template< typename TYPES
, template<class,class, size_t> class _ElmMapper_
template<typename TYPES
,template<class, class, unsigned> class _ElmMapper_
>
struct TupleConstructor
: Tuple<TYPES>
@ -398,16 +182,18 @@ namespace meta {
/** meta-sequence to drive instantiation of the ElmMapper */
using SequenceIterator = BuildIdxIter<TYPES>::Ascending;
template<size_t idx, class SRC>
template<unsigned idx, class SRC>
using ElmMapperFor = _ElmMapper_<std::decay_t<SRC>, Tuple<TYPES>, idx>;
template<unsigned idx, class SRC>
static auto
mapElm (SRC&& init) ///< initialise an instance of the element-mapper
{
return _ElmMapper_<std::decay_t<SRC>
,Tuple<TYPES>
, idx
>{std::forward<SRC> (init)};
return ElmMapperFor<idx,SRC>{std::forward<SRC> (init)};
}
protected:
template<class SRC, size_t...idx>
TupleConstructor (SRC&& initVals, IndexSeq<idx...>)
@ -454,135 +240,66 @@ namespace meta {
{
return TupleConstructor<TYPES, ExtractArg>{std::forward<SRC> (values)};
}
/* ======= Tuple toString rendering ======= */
/**
* Decorating a tuple type with auxiliary data access operations.
* This helper template builds up a subclass of the given TUP (base) type
* (which is assumed to be a Tuple or at least need to be copy constructible
* from `Tuple<TYPES>` ). The purpose is to use the Tuple as storage record, but
* to add a layer of access functions, which in turn might rely on the exact
* type of the individual elements within the Tuple. To achieve this, for each
* type within the Tuple, the TUP type is decorated with an instance of the
* template passed in as template template parameter _X_. Each of these
* decorating instances is provided with an index number, allowing to
* access "his" specific element within the underlying tuple.
*
* The decorating template _X_ need to take its own base class as template
* parameter. Typically, operations on _X_ will be defined in a recursive fashion,
* calling down into this templated base class. To support this, an instantiation
* of _X_ with the empty type sequence is generated for detecting recursion end
* (built as innermost decorator, i.e. the immediate subclass of TUP)
* convenience function to dump a given tuple's contents, converted to string.
* @note this is the basic functionality to place rendered data into a `ostringstream.`
*/
template
< template<class,class,class, uint> class _X_ ///< user provided template<Type, Base, TupleType, arg-idx>
, typename TYPES ///< Sequence of types to use within the Accessor
, class TUP =Tuple<TYPES> ///< the tuple type to build on
, uint i = 0 ///< tuple element index counter
>
class BuildTupleAccessor
template<class TUP>
inline void
joinTuple (TUP const& tup, std::string delim, std::ostringstream& buffer)
{
// prepare recursion...
using Head = Split<TYPES>::Head;
using Tail = Split<TYPES>::Tail;
using NextBuilder = BuildTupleAccessor<_X_, Tail,TUP, i+1>;
using NextAccessor = NextBuilder::Product;
public:
/** type of the product created by this template.
* Will be a subclass of TUP */
using Product = _X_< Head // the type to use for this accessor
, NextAccessor // the base type to inherit from
, TUP // the tuple type we build upon
, i // current element index
>;
};
forEachIDX<TUP> ([&](auto i)
{
using Elm = std::tuple_element_t<i, TUP>;
if (i)
buffer << delim;
buffer << util::toString<Elm> (getElm<i> (tup));
});
}
template
< template<class,class,class, uint> class _X_
, class TUP
, uint i
>
class BuildTupleAccessor< _X_, Types<>, TUP, i>
template<class TUP>
inline void
joinTuple (TUP const& tup, std::ostringstream& buffer)
{
public:
using Product = _X_<Nil, TUP, TUP, i>; // Note: i == tuple size
};
joinTuple (tup, ",");
}
/**
* Helper to dump tuple contents.
* Defined to act as "Accessor" for BuildTupleAccessor, this helper template
* allows to create a recursive operation to invoke string conversion on
* all elements within any given tuple.
/** convenience function to join the tuple's contents into a single string.
* @param delim optional delimiter to intersperse, defaults to `","`
*/
template
< typename TY
, class BASE
, class TUP
, uint idx
>
struct TupleElementDisplayer
: BASE
{
using BASE::BASE;
std::string
dump (std::string const& prefix ="(") const
{
return BASE::dump (prefix + util::toString(std::get<idx>(*this))+",");
}
};
template<class TUP, uint n>
struct TupleElementDisplayer<Nil, TUP, TUP, n>
: TUP
{
TupleElementDisplayer (TUP const& tup)
: TUP(tup)
{ }
std::string
dump (std::string const& prefix ="(") const
{
if (1 < prefix.length())
// remove the trailing comma
return prefix.substr (0, prefix.length()-1) +")";
else
return prefix+")";
}
};
/**
* convenience function to dump a given tuple's contents.
* Using the BuildTupleAccessor, we layer a stack of Instantiations of
* the TupleElementDisplayer temporarily on top of the given tuple,
* just to invoke a recursive call chain through these layers
* and get a string representation of each element in the
* tuple.
*/
template<typename...TYPES>
template<class TUP>
inline std::string
dump (std::tuple<TYPES...> const& tuple)
{
using BuildAccessor = BuildTupleAccessor<TupleElementDisplayer, Types<TYPES...>>;
using Displayer = BuildAccessor::Product ;
return static_cast<Displayer const&> (tuple)
.dump();
}
joinTuple (TUP const& tup, std::string delim =",")
{
std::ostringstream buffer;
joinTuple (tup, delim, buffer);
return buffer.str();
}
template<class TUP>
inline void
joinTupleParen (TUP const& tup, std::ostringstream& buffer)
{
buffer << '(';
joinTuple (tup, ",", buffer);
buffer << ')';
}
template<class TUP>
inline std::string
joinTupleParen (TUP const& tup)
{
std::ostringstream buffer;
joinTupleParen (tup, buffer);
return buffer.str();
}
}} // namespace lib::meta
@ -595,12 +312,15 @@ namespace util {
static std::string
invoke (std::tuple<TYPES...> const& tuple) noexcept
try {
return "«"+typeStr(tuple)
+ "»──" + lib::meta::dump (tuple);
std::ostringstream buffer;
buffer << "«"
<< typeStr(tuple)
<< "»──";
lib::meta::joinTupleParen (tuple, buffer);
return buffer.str();
}
catch(...) { return FAILURE_INDICATOR; }
};
} // namespace util
#endif /*LIB_META_TUPLE_HELPER_H*/

View file

@ -0,0 +1,131 @@
/*
TUPLE-IDX-ADAPTOR.hpp - runtime-indexed access to tuples
Copyright (C)
2026 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file tuple-idx-adaptor.hpp
** Adaptor to provide access to tuple elements by runtime subscript.
** Tuples are data records with distinct fields of different type; thus
** most of indexing, iteration and transformation of tuples need to be
** performed statically, at compile-time. However, under the proviso that
** the types in a given tuple are somehow _reconcilable_, a dispatcher
** table with access functors can be built, so that effectively all fields
** can be handled by instances of a generic functor. This setup allows to
** define a subscript `operator[]`, so that the suitable functor is picked
** with runtime-index and applied to the tuple to yield the indicated field,
** albeit converted into a _common type_.
**
** While this is certainly a very specific setup, and incurs some cost,
** since the functor dispatcher tables must be prepared statically, such
** a runtime-controlled index access allows for dynamic and data-driven
** processing of tuple_like structures, which are fully type-safe,
** compact in storage and easy to nest. Furthermore, run-time
** processing of arguments passed to variadic functions
** is enabled, providing a new kind of flexible API.
**
** @see TupleIdxAdaptor_test
** @see FunTrampoline_test
*/
#ifndef LIB_META_TUPLE_IDX_ADAPTOR_H
#define LIB_META_TUPLE_IDX_ADAPTOR_H
#include "include/logging.h"
#include "lib/iter-adapter.hpp"
#include "lib/meta/tuple-helper.hpp"
#include "lib/meta/common-result.hpp"
#include "lib/meta/fun-trampoline.hpp"
#include <string>
namespace lib {
namespace meta {
template<class TUP>
class TupleIndexAdaptor
: public TUP
{
/** Helper to generate a dispatch table for
* this specific instance of tuple element types */
template<typeseq SEQ>
struct SubscriptBuilder
{
static_assert (reconcilable<SEQ>
,"A common run-time index accessor can only be built "
"if all types in the tuple can be reconciled into a "
"common type used for the access");
using Tuple = RefTraits<TUP>::value_type;
using AddRefs = ElmTypes<SEQ>::template Apply<std::add_lvalue_reference_t>;
using ResType = CommonResult_t<AddRefs>;
using Functor = ResType(*)(Tuple&);
template<size_t idx>
constexpr auto
build()
{
return [](Tuple& tuple) -> ResType
{
return getElm<idx> (tuple);
};
}
};
using TypeSeq = ElmTypes<TUP>::Seq;
using Accessor = FunTrampoline<SubscriptBuilder, TypeSeq>;
public:
using Tuple = TUP;
using ResType = Accessor::ResType;
using reference = ResType;
using value_Type = RefTraits<ResType>::value_type;
static constexpr bool isRefAccess = isLRef_v<ResType>;
/** construct by passing a source tuple */
template<typename T>
TupleIndexAdaptor(T&& dataSrc)
: TUP{forward<T> (dataSrc)}
{ }
/// lift all constructors of the adapted tuple
using TUP::TUP;
Tuple & tuple() { return *this; }
Tuple const& tuple() const { return *this; }
operator std::string() const
{
return "iTup" + joinTupleParen (this->tuple());
}
/* === subscriptable === */
static constexpr size_t size() { return Accessor::size(); }
ResType
operator[] (size_t idx)
{
return Accessor::dispatch(idx)(*this);
}
};
/// deduction guide
template<class TUP>
TupleIndexAdaptor(TUP) -> TupleIndexAdaptor<std::decay_t<TUP>>;
}}// namespace lib::meta
#endif /*LIB_META_TUPLE_IDX_ADAPTOR_H*/

View file

@ -115,5 +115,21 @@ namespace meta {
using Seq = Types<>;
};
/**
* Concept to require a generic type sequence `Types<TYPES...>`
*/
template<class TSEQ>
concept typeseq = requires (TSEQ seq)
{
// TSEQ can invoke a function with a Types<TYPES> subclass
[]<typename...TYPES>(Types<TYPES...>&){/*placeholder*/}(seq);
};
template<class TSEQ>
static constexpr bool is_Typeseq_v = typeseq<TSEQ>;
}} // namespace lib::meta
#endif

View file

@ -44,6 +44,44 @@
namespace lib {
namespace meta {
template<class T, class TYPES>
struct Prepend;
/* ========= Specialisations ========= */
/**
* Additional specialisation of the basic type sequence type,
* allowing to re-create a (flat) type sequence from a typelist.
* @remark recursively prepend-to-tail of the type sequence.
*/
template<class H, class T>
struct Types< Node<H,T> >
{
using List = Node<H,T>;
using Seq = Prepend< H
, typename Types<T>::Seq
>::Seq;
};
template<>
struct Types<Nil>
{
using List = Nil;
using Seq = Types<>;
};
template<>
struct Types<NilNode>
: Types<Nil>
{ };
/* ========= Information functions ========= */
/**
* Find the index of the first incidence of a type in a type-sequence.
@ -82,6 +120,13 @@ namespace meta {
{ };
/* ========= Type Sequence Manipulation ========= */
/**
* Helper: prepend a type to an existing type sequence,
* thus shifting all elements within the sequence
@ -99,33 +144,6 @@ namespace meta {
/**
* Additional specialisation of the basic type sequence type,
* allowing to re-create a (flat) type sequence from a typelist.
* @remark can now be built with the help of \ref Prepend.
*/
template<class H, class T>
struct Types< Node<H,T> >
{
using List = Node<H,T>;
using Seq = Prepend< H
, typename Types<T>::Seq
>::Seq;
};
template<>
struct Types<Nil>
{
using List = Nil;
using Seq = Types<>;
};
template<>
struct Types<NilNode>
: Types<Nil>
{ };
/** Helper: separate parts of a type sequence
*/

View file

@ -505,19 +505,19 @@ namespace util {
/* === Literals for common size designations === */
inline uint
constexpr inline uint
operator""_KiB (ullong const siz)
{
return uint(siz) * 1024u;
}
inline uint
constexpr inline uint
operator""_MiB (ullong const siz)
{
return uint(siz) * 1024u*1024u;
}
inline ullong
constexpr inline ullong
operator""_GiB (ullong const siz)
{
return siz * 1024uLL*1024uLL*1024uLL;

View file

@ -27,9 +27,9 @@
** Within the STL, there is a convention to provide nested typedefs to indicate
** type variations in relation to the basic payload type of the container. We
** follow this convention and support especially the
** - `value_type` (what is conceived to be "in" the container or iterator)
** - a simple (LValue) reference to the payload
** - a pointer at the payload.
** - `value_type` : how to store what is conceived to be "in" the container or iterator)
** - a simple (LValue) reference to the payload: how you access some content
** - a to form pointer at the payload, which is relevant for `operator->`
**
** A custom container should likewise provide such type definitions, and the
** type rebinding helper template defined in this header makes it easy to
@ -37,6 +37,12 @@
** creates an *Extension Point*: when some payload type requires special
** treatment, an explicit specialisation to this rebinding trait may be
** injected alongside with the definition of the payload type.
** @warning A special twist may happen when processing a function in an iterator (pipeline).
** In this special case, `ValueTypeBinding` should _not_ be used, since the function
** might yield again a nested iterator; yet we want to treat that iterator as a value
** in that special case, and not look into the iterator or retrieve results from it.
** Notably this situation applies to any kind of transforming iterator.
** lib::meta::RefTraits should be used in such cases.
**
** The CommonResultYield type rebinding helper allows to reconcile several
** essentially compatible result types; it is used in iterator pipelines,
@ -53,7 +59,6 @@
#define LIB_META_VALUE_TYPE_BINDING_H
#include "lib/error.hpp"
#include "lib/meta/trait.hpp"
@ -107,64 +112,22 @@ namespace meta {
template<typename TY, typename SEL =void>
struct ValueTypeBinding
{
using value_type = RefTraits<TY>::Value;
using reference = RefTraits<TY>::Reference;
using pointer = RefTraits<TY>::Pointer;
using value_type = RefTraits<TY>::value_type;
using reference = RefTraits<TY>::reference;
using pointer = RefTraits<TY>::pointer;
};
/** specialisation for classes providing STL style type binding definitions */
template<typename TY>
struct ValueTypeBinding<TY, enable_if<use_ValueTypebindings<TY>> >
{
using _SrcType = RefTraits<TY>::Value;
using _SrcType = RefTraits<TY>::value_type;
using value_type = _SrcType::value_type;
using reference = _SrcType::reference;
using pointer = _SrcType::pointer;
};
/**
* Decision helper to select between returning results by value or reference.
* - when both types can not be reconciled, _no type result_ is provided;
* this case can be detected by a compile-time bool-check
* - when one of both types is `const`, the `ResType` will be const
* - when both types are LValue-references, then the result will be a reference,
* otherwise the result will be a value type
* @see IterExplorer::expand()
*/
template<typename T1, typename T2, bool = has_TypeResult<std::common_type<T1,T2>>()>
struct CommonResultYield
: std::false_type
{ };
template<typename T1, typename T2>
struct CommonResultYield<T1, T2, true >
: std::true_type
{
using _Common = std::common_type_t<T1,T2>;
// NOTE: unfortunately std::common_type decays (strips cv and reference)
static constexpr bool isConst = isConst_v<T1> or isConst_v<T2>;
static constexpr bool isRef = isLRef_v<T1> and isLRef_v<T2>;
using _ConstT = std::conditional_t<isConst
, const _Common
, _Common
>;
using _ValRef = std::conditional_t<isRef
, std::add_lvalue_reference_t<_ConstT>
, std::remove_reference_t<_ConstT>
>;
using ResType = _ValRef;
using value_type = RefTraits<ResType>::Value;
using reference = RefTraits<ResType>::Reference;
using pointer = RefTraits<ResType>::Pointer;
};
}} // namespace lib::meta
#endif /*LIB_META_VALUE_TYPE_BINDING_H*/

View file

@ -16,9 +16,14 @@
** Metaprogramming with type sequences based on variadic template parameters.
** The type rebinding- and helper templates in this header allow to perform
** simple sequence manipulations on sequences of template parameters extracted
** from variadic parameter packs. The goal is to (pre)process flexible argument
** lists _at compile time,_ driven by template instantiation, allowing to specialise
** and react specifically on some concrete pattern of argument types.
** from variadic parameter packs. Among other things, this allows the sequence
** of function arguments to be flexibly reshaped and remoulded at compile-time.
** Driven by template instantiation, this enables adaptation in response to
** specific argument types for each actual instance. Such metaprogramming
** capabilities can be amplified significantly through the use of generic-λ.
** @note with C++23, this framework could be expanded to cover also any
** »tuple-like« product type, drawing from our non-standard extension
** with the `concept tuple_like`, as defined in \ref tuple-concept.hpp.
**
** @remark in Lumiera, over time three different approaches were developed for
** handling sequences of types in metaprogramming; some of these techniques
@ -26,8 +31,10 @@
** - templates with variadic arguments (e.g. std::tuple) can be manipulated directly
** - a type-sequence `Types<T...>` can be primed / rebound from other variadic templates
** - Loki-style type-lists are created from type-sequences and enable elaborate manipulations
**
** @see feed-manifold.hpp advanced usage example in the Render Engine
** @see TupleHelper_test
** @see VariadicHelper_test
** @see tuple-concept.hpp
** @see typelist.hpp
** @see function.hpp
** @see generator.hpp
@ -42,7 +49,7 @@
#include "lib/meta/typelist-util.hpp"
#include "lib/meta/typeseq-util.hpp"
#include "lib/meta/variadic-rebind.hpp"
#include "lib/meta/util.hpp"
#include "lib/meta/trait.hpp"
namespace lib {
namespace meta {
@ -160,7 +167,7 @@ namespace meta {
* - the nested template #Apply wraps each type into another template
* - #Rebind likewise instantiates another template with the element types
* - #AndAll applies a predicate and combines the result with _logical and_
* - #OrAll similarly evaluates _logical or_ on the application results
* - #OrAny similarly evaluates _logical or_ on the application results
*/
template<class X, typename =void>
struct ElmTypes
@ -177,7 +184,7 @@ namespace meta {
template<template<class> class PRED>
using AndAll = std::__and_<PRED<X>>;
template<template<class> class PRED>
using OrAll = std::__or_<PRED<X>>;
using OrAny = std::__or_<PRED<X>>;
};
/** Partial specialisation to handle type sequences */
@ -199,10 +206,23 @@ namespace meta {
using AndAll = ElmTypes<Apply<PRED>>::template Rebind<std::__and_>;
template<template<class> class PRED>
using OrAll = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
using OrAny = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
};
// Note: a further specialisation for any »tuple-like« is defined in tuple-helper.hpp
// Note: a further specialisation for any »tuple-like« is defined in tuple-concept.hpp
template<class SEQ>
using Any_Const = ElmTypes<SEQ>::template OrAny<isConst>;
template<class SEQ>
using All_LRef = ElmTypes<SEQ>::template AndAll<isLRef>;
template<class SEQ>
static constexpr bool Any_Const_v = Any_Const<SEQ>::value;
template<class SEQ>
static constexpr bool All_LRef_v = All_LRef<SEQ>::value;

View file

@ -26,7 +26,7 @@
namespace lumiera {
namespace lib {
void
initialise_NoBug ()

View file

@ -27,17 +27,17 @@
#ifndef NOBUG_INIT_H
#define NOBUG_INIT_H
#include "include/lifecycle.h"
#include "include/lifecycle.hpp"
#include <nobug.h>
namespace lumiera {
namespace lib {
void initialise_NoBug ();
namespace {
LifecycleHook trigger_init_ (ON_BASIC_INIT, &initialise_NoBug);
vessel::LifecycleHook trigger_init_ (vessel::ON_BASIC_INIT, &initialise_NoBug);
} }

Some files were not shown because too many files have changed in this diff Show more