Compare commits

...

63 commits

Author SHA1 Message Date
fc8dd9408a Parameters: establish requirements for a descriptor registry
Since the design established thus far calls for a fixed size container
and a size-constrained stack of virtual interfaces and implementation within,
it is clear that more diverse parts of the type and scale handling must be
forwarded into a second layer, maintained within a registry and keyed by ID.

Taking the prospective use into account, a two-level structure with a
fixed-size top-level index table and dynamically added extents seems to
cover all requirements, and allows for a lightweight, atomic-protected
read-access path.
2026-09-27 01:38:03 +02:00
da9b40df03 Parameters: resolve type precision problems with scale-joinig
The selection of the intermediary working type, used for exponential
and logarithmic functions when combining values from different scales,
was implemented too simplistic.

Following the example of the `NumberJoinType`, it seems preferrable
to spell out the decision logic explicitly. This allows to pick `long doubl`
as working type especially in cases where 64bit integral parameters are
involved, either as source or destination.

With these final tweaks, the `verify_valueJoining()` test is not complete,
presumably with path coverage of the conversion functions.

The Scale descriptor and operational logic is now defined to the point
that it can be integrated into the general Parameter implementation machinery.
2026-09-22 18:39:09 +02:00
fd5548b61b Parameters: verify use of a long-double target scale
Scrutinising the bounds of numeric precision.
As it turns out, most cases work as expected,
including the massive errors incurred by input-values given as float.

However: a logarithmic combination integral -> integral runs in loss of precision
So the selection logic for the intermediary working type is still overly simplistic!
2026-09-22 01:43:45 +02:00
00b0aa0c5b Parameters: expand logarithmic scale patch coverage
The control logic for the various combinations of linear and logarithmic scales is nasty,
and the added path coverage indeed unveiled several bugs.

One of these issues, an unjustified limitation on the input value type of `asLogarithm`,
convinced me to refactor the conversion functions into free functions,
since they effectively ''always span the relation of two scales'' --
writing them as member function introduces an unnecessary asymetry.
2026-09-21 22:10:18 +02:00
206f9635c9 Parameters: cover logarithmic scale conversions
this feature relies on the common logarithmic math,
yet with the additional twist that decibels need an additional factor of 10...
2026-09-21 05:42:33 +02:00
92b7b05ea2 Parameters: solve mixed signed / unsigned computation
There are some corner cases where the C++ number promotion rules can hurt us badly.
Devised a type selection logic to pick a suitable common numeric type explicitly,
using `std::common_type<T1,T2>` in most cases, yet handling explicitly the
case when the computation would wrap into unsigned; in such a mixed sign integral case,
we either use int64_t (when its precision suffices), or long double,
which can hold 64bit of mantissa without precision loss.
2026-09-21 01:05:53 +02:00
c0cff8abe6 Parameters: implementation for metric conversions
LIN -> LIN yet either additive or multiplicative
LOG -> LIN
LIN -> LOG
and different kinds of logarithms

all computations using a common intermediary numeric type,
preferably a suitably sized floating-point
2026-09-20 06:15:38 +02:00
684bb2ca50 Parameters: draft the cases of a generic feed-join
In the general case, a base value and a feed can also be combined
when the metric of both arguments differs; notably a linear multiplier
can be applied to an logarithmic base value by adding its logarithm,
and a logarithmic feed can be multiplied to a linear base value by
taking the exponential function.

Furthermore there are some cases involving nominal scales, which we can mark only, but leave unimplemented
2026-09-19 00:52:51 +02:00
a1b80c22df Parameters: reorganise type conversion dependencies
the generic cross-type assignment and -conversion will also be used by the `par::Scale` to join an automation evaluation result with a base value...
2026-09-17 23:38:02 +02:00
0cbe9d7dd6 Parameters: prepare for a generic Scale-join operation
The `join()`-operation allows to combine a value pulled from automation
to be combined with a local ''base value'' stored in the parameter.

When both the source and the target parameter use the same Scale,
this operation is simple; it becomes more tricky once they don't --
because then linear / logarithmic and type conversions must be handled.

I now decided to provide a generic implementation in the Scale (descriptor),
while the enclosing parameter shall handle the dispatch when connecting
to a opaque source with another Scale. And this generic implementation
relies on the same technique as was used for the parameter type conversions,
thus it should be factored out into a library header.
2026-09-17 23:38:02 +02:00
9221fbeef1 Parameters: document and verify integration into cyclic Scale
- verify using an unsigned integer, for a typical ''angle scale''
- also verify a high-res floating-point scale, and demonstrate rounding issues
2026-09-17 01:17:40 +02:00
549216dba3 Parameters: integrate the cyclic-wrap function
a much more complete and robust implementation,
but requires a precision limit to be pre-computed in the floating-point case
2026-09-16 19:42:55 +02:00
6eb6ec48da Library: better short-hand name for long double
As with all those C++ base types, unfortunately they are giving you
only rough size guarantees. For Lumiera, I defined short hand names
some time ago, notably because the explicit long X and short X qualifiers
do not work with ''constructor syntax'' for direct conversions.

But I choose the name `f128` at that time -- which might raise ill-guided
expectations, since the `long double` is not that long on relevant linux 64bit
platforms; it can just hold 64bit of mantissa at least however.

As replacement, I'll use `lflp` now, because it has also 4 letters,
looks somewhat weird (and thus catches attention) and points into the
right direction. I considered `lfloat` as alternative, but that looks
too much like `float`, visually...
2026-09-21 01:05:53 +02:00
303edcb6a1 Library: verification for all floating-point types
finally I was able to come up with a rather concise test
that works with float, double and long double; especially
the aliasing due to rounding could be demonstrated, and
the precision limit is made plausible.

Interestingly, `std::fabs()` really works precise enough
so that a very large positive domain can be implemented
and tested with exact match
2026-09-16 04:23:55 +02:00
d0e6998b9c Library: investigate what could be tested with floating-point
The floating-point variant turns out to be difficult to test,
since it is never ''actually exact'' -- due to the nature of floating-points.

We can not expect reasonable behaviour at the domain boundaries,
even while in some cases the computation works (and in others it
goes infinite and is sanitised to `minVal`).

The limit for rejection is itself rather a heuristic;
thus it is difficult to show "wrong" behaviour beyond the limit,
while the limit itself is typically already so large that it is
hard to handle in test code -- at least when expecting visible results.
2026-09-15 21:53:56 +02:00
ce980d64bf Library: verify the new cyclicWrap library function
The implementation should cover all integral data types,
both signed and signed, over the entire value domain.

Verify that by testing the bounds and the complex computation path
2026-09-15 01:37:38 +02:00
29a39a0f7f Library: extend text coverage for util::floorwrap
These two helpers ''just happen to work'' in the full domain, and also for unsigned types.
I can not recall that I actually was aware of that, but it's nice.

Thus add some test cases to cover the bounds of the domain
2026-09-14 16:34:15 +02:00
77257c5653 Parameters: numeric-types deep dive
Turns out that implementing a cyclically wrapping domain properly is much more tricky than initially anticipated.

After initial analysis I decided where to draw the line and what ''esoteric features'' aspects to leave out.
Then I used Knowledge-Technology (Claude) to scrutinise and improve my implementation,
and to learn a lot more about integral arithmetics in general.
2026-09-14 01:50:26 +02:00
426d7c610e Parameters: handle simple Scales
- build a default value
 - handle cyclic scales
 - implement clamping behaviour
 - identify a muliplicative factor
 - join a feed either multiplicative or additive
2026-09-12 01:16:16 +02:00
c3c103e1a9 Parameters: mark out the base operations of a Scale
Scales are defined as descriptor records and will be used as const.
Besides some information functions, a Scale
 * allows to conform a value to be compliant with the Scale's constraints
 * can ''join'' and thus apply a ''feed'' on top of a ''base value'';
   this is the foundation for applying an automation function, while
   handling multiplicative and logarithmic cases properly
2026-09-12 00:39:11 +02:00
17c95a9b18 Parameters: in-depth analysis regarding Scale parameters
Notably I considered how and to what extent we will need to represent ''nominal'' or ''ordinal scales''...
In fact we should represent them, yet as cascaded delegate, since that is a relevant topic,
yet not the primary use case -- and a topic with very deep complexity in itself.
2026-09-11 01:44:48 +02:00
59fbb8e6b0 Parameters: consider what to conceptualise as a Scale
It is clear by now that a `Scale` will act as a refinement of a full-domain value type.
However, in order to develop the definition and builder notation for parameters,
a much more detailed understanding of scales is necessary -- including the question
where to draw a line between the complex structured `ParamSet` and a simple
`Parameter` that is conceived to be number-value-like.
2026-08-10 01:28:41 +02:00
5fbe08ebe0 Parameters: sketch a prototype-builder API
...yet the actual problem remains that the value type of the
prototype remains a runtime-dynamic hidden implementation trait.

How can we remould the scale hidden within a parameter, while
in the regular setup, the scale is implemented as an immutable
entry placed into a registry?
2026-09-06 01:39:09 +02:00
a4ae568601 Parameters: mapping out the next steps to draft
...I do ''not intend to go into a parameter deep-dive'' yet,
but I want to set out the basic capabilities to be expected...
2026-09-06 00:50:53 +02:00
f8c1222197 Parameters: work out a solution for an optimised code-path
Overall, this is a questionable endeavour...
 - I want a framework that can be extended flexibly
 - I am concerned about the performance penalty of that flexibility

What makes this situation problematic is the fact that I am deep in
prototyping work here, and have not the slightest idea as to what would be
the actual usage context. So I am lacking a point of reference, yet still
I am engaged into a very tricky (premature) optimisation...

Anyway. I have now found a way to integrate an optimised access code-path
in a safe way, while retaining the general case with flexible type conversion:
 - whenever a BaseDomain<X> is used, a corresponding type-ID is marked into the ID-Record
 - this allows when it is possible to side-step any type conversion,
 - in which case we can also forcibly side-step the virtual dispatch,
   and invoke directly the value-accessors of the default implementation,
   reaching into the embedded value buffer.
2026-09-05 01:29:51 +02:00
41adaa18ef Parameters: streamline automatic type conversion
Can now express the automatic conversion between all allowed `BaseTypes`
by invoking the `TypeHandler` interface directly. The interfaces were streamlined
and storage size is managed automatically at compile time.
2026-09-03 01:42:18 +02:00
d32e4bbc06 Parameters: setup bit-layout for ID-Record 2026-09-03 00:18:46 +02:00
535e372827 Parameters: resolve PolymorpicValue setup
`PolymorphicValue` incurs some quite confusing requirements due to
the clumsy way how the instance handling and copy support must be injected.
As pointed out in #1197, with modern C++, this could be much simplified.

An overhaul of `PolymorphicValue` seems urgent thus -- in the meanwhile
we need to introduce more flexibiltiy with base-class template parameters,
so that we can inject the CopySupport interface at usage site of the
complete implementation class chain. Furthermore, the size of the VTable
is added automatically to the storage requirements in `PolymorphicValue`,
so we must subtract it when specifying the payload size by a compile-time
expression based on `sizeof(ParamData<double>)`

With these tweaks, we can now keep the limit of 4 «slots» (32 bytes)
and still reserve 64bit for ID and type flags.
2026-09-03 00:10:37 +02:00
e0fa980f4c Parameters: introduce ID-Record into the impl-interface
The solution from yesterday works, but is needlessly indirect.
However, if we want to circumvent ''any indirection'' in the
simple standard case, then it turns out to be necessary to
have some kind of ''flag store'' already in the `Disposition`
(the implementation service interface). Because otherwise
it would be impossible to trigger the short-circuit access
without going through some kind of indirection.

Need to define a generic ID-Record with some flexible extension space
that can be used by subclasses; and notably we need to generate some
base-type-IDs on a static level -- which should be possible, since all
allowed BaseTypes are defined as Type-Sequence.
2026-09-03 00:02:16 +02:00
f6cc5fe1d9 Parameters: simple testcase PASS (see #1431)
Add value access implementation of the Provision interface
by direct cast and access to the embedded value in `ParamData<VAL>`;
obviously this is an indirect dispatch, based on the actual type `VAL`

The test demonstrates the basic machinery built thus far
 - have an embedded container with inline storage
 - dispatch using type information encoded into the VTable
2026-09-02 01:48:51 +02:00
7af51ae92e Parameters: first rough-edged implementation of value access
...as expected, the first attempt to walk the full access path
unveils a host of problematic aspects in the design...
2026-09-02 01:27:27 +02:00
5c7a65bc18 Parameters: allow to pass an initial parameter value
...and in addition check that the defined value type
is consistent with the list of pre-configured `BaseTypes`

Signed-off-by: Ichthyostega <prg@ichthyostega.de>
2026-09-02 01:19:24 +02:00
9f19f5d895 Parameters: build container with in-place init
turns out to be quite tricky, due to the overly convoluted implementation
of PolymorphicValue (which looks quite dated, generally speaking,
and could use a complete overhaul).

- pass the basic value type as template parameter through the Builder
- allow for the ability to define an initial value for the parameter
- invoke the protected constructor of PolymorphicValue, directly passing
  a ''type tag'' to specify the implementation data package to be placed
  into the embedded data buffer
2026-09-02 01:19:24 +02:00
fca0c08b22 Parameters: now able to define the complete implementation layout
- `Disposition` is the service-API used by the front-End container
 - `ParamData<TY>` is a complete implementation with a inline-value buffer
 - The front-End is based on the `PolymorphicValue` container from our support-Lib
2026-09-01 23:15:09 +02:00
9665c5d320 Parameters: establish layered implementation order
Finally able to settle upon a layering for the implementation facilities

 Provision:: represents the core idea of a parameter
 Domain:: assumes the ability to handle typed values
 Disposition:: complete implementation capabilities

 BaseDomain:: establish type conversion functionality
 TypedEntity:: provide a type and ID descriptor and scale configuration
 ParamData:: full implementation based on an embedded value buffer
2026-09-01 22:55:56 +02:00
3bd64935f3 Parameters: reorganise TypeHandler interface
The original design (based on Alexandrescu) is more elegant,
but has the downside to bloat the VTable size due to the mix-in
arrangement used to generate the interface.

The ongoing design has revealed a tight constraint on element size
for the parameters; thus the change to a linearised inheritance chain.

The overall purpose remains the same: the ability to generate a
double-dispatch type conversion, so that the actual value type of the
parameter can be treated as an implicit implementation detail.
2026-09-01 20:46:40 +02:00
1f29d202b4 Parameters: look for a suitable arrangement of interfaces
While the role of all the relevant interfaces has been determined,
it still remains difficult to establish a sensible relationship.
Which one is the most fundamental interface?

After a lot of mulling, it seems that the `Provision` interface
should be used as foundation, since it describes the very core
of the parameter concept developed here -- even while the actual
form of the operations must be ''somewhat cunningly reversed'',
so that the knowledge regarding the underlying type can be
kept an opaque implementation detail.

The `Domain` interface would then be layered on top, and describes
the capability to transfer values between buffers marked with different
domains (which effectively requires a double-dispatch in the general case).
2026-09-01 01:53:43 +02:00
d582b95e7c Parameters: detailed the prototype design sketch (see #1431)
Last week, I resumed the work that had been suspended
due to my focus on this year’s Froscon talk (on `io_uring`).
I have reconsidered my previous design ideas and sought to
arrive at a coherent design. Nevertheless, these are preliminary,
exploratory design studies that should be regarded as a basis
for future work rather....
2026-08-31 00:24:37 +02:00
f0cdb2f5d2 clean-up: Bugfixes in passing
Discovered through design-related code analysis
Knowledge-Technology(Claude) spotted discrepancies that led to these fixes.

Remark: the irregular ordering of check predicates stems from the way I developed the logic and bears no meaning.
2026-09-26 18:22:42 +02:00
0661b27c74 clean-up: resolve some Clang conformance divergences (see #928)
Knowledge-Technology(Claude Opus 5) used
 - to analyse and reassess the situation
 - to discuss and weigh the findings and develop solutions
 - to perform the selected mitigation and verify success

----

= Report by Claude =

A sweep with Clang-22 over all 597 translation units (driven from the newly
generated compilation database) showed 271 of them failing, yet with the
2502 diagnostics tracing back to very few root sites. This resolves those
where GCC is simply the more permissive of the two; the remaining clusters
are a matter of design and are left alone for now.

 * `using Alias::OriginalName;` does *not* declare inherited constructors.
   Per [namespace.udecl] a using-declarator names constructors only when its
   terminal name equals the last component of the nested-name-specifier -- so
   through an alias, the terminal name is read as the injected-class-name,
   i.e. a type, and Clang then demands `typename`. Clang is right here.
   Affects `hetero-data.hpp` (19 TUs) and `iter-source.hpp` (62 TUs); in the
   latter the alias is renamed `_I` -> `Adapter` while we are at it.
   Note `iter-source` as a whole awaits a design review, see #1125

 * P1787R6 requires a template-argument-list after the `template` keyword.
   `holder.template emplace (...)` in `opaque-holder.hpp` and two sites in
   `node-builder.hpp` supply none, so the keyword has to go (83 TUs).

 * A default template argument may be given only once per scope. The
   out-of-class definition of `OutputSlot::AllocState` repeated the default
   from its declaration in 'output-slot.hpp' -- note the friend-declaration
   further down in the same header already got this right.

 * `constexpr auto ROUGH_PRECISION = pow (10, -3)` is not a constant
   expression by the standard; only GCC accepts it, through a builtin.
   Hence `util::ipow()` in 'util-quant.hpp', an exponentiation with integral
   exponent by square-and-multiply, as a counterpart to `ilog2()`. Exact for
   representable base and result, and the static_assert documents that
   `ipow(10.0,-3)` is bit-identical to `1e-3`. 193 TUs depended on this
   single line.

Furthermore, two findings made while surveying:

 * `lib/stat/statistic.hpp` held a second, dormant instance of the same
   `pow` problem, in a `round<places>()` helper that nothing in Lumiera ever
   instantiates -- which is why Clang never saw it. Since the helper is also
   the naive and imprecise formulation, it is removed here; a note records
   that it stays in use upstream in yoshimi-test, where this header comes
   from, and that decimal rounding belongs to #1360 if ever wanted here.

 * The alias `DataBuilder` was defined twice, identically, in
   'weaving-pattern-builder.hpp' and in 'node-builder.hpp' which includes
   the former. Both compilers tolerate the duplicate definition and only
   Clang objects to the repeated default argument, which is why this went
   unnoticed. The duplicate is dropped and its documentation moved to the
   surviving definition -- together with a @todo, since `DataBuilder`,
   `SizMark` and `UseHeapAlloc` are vocabulary of the node-builder DSL and
   reside where they do by accident of include order.
2026-10-01 02:57:44 +02:00
109d682408 Buildsystem: generate a compilation database for clangd (closes: #1436)
Knowledge-Technology(Claude Opus 5) used
 - to assess the situation and present implementation avenues
 - for discussion while shaping the solution
 - for generating code and validating results

Clang based tooling is used frequently for code analysis and navigation.
This requires however that the exact flags and invocation is captured for
each possible translation unit in the code base. That knowledge exists solely
in the build system, and the established way to hand it over is the
LLVM "JSON Compilation Database".

SCons has provided a `CompilationDatabase` builder since 4.0, and it fits
our setup well: it installs an emitter on the object builders and captures
each command line while the dependency graph is constructed. Hence nothing
is compiled -- generating takes about two seconds -- and the result always
describes the complete build graph as declared, rather than the target
actually requested on this invocation.

This feature is provided as ''opt-in'', either through the sticky option
`COMPILEDB=yes` or by direct invocation with `scons compiledb`

The `compile_commands.json` is generated into the build output `target/' dir
For configuration, a `.clangd` in project root instructs the language server
where to find this file.

__Note__ the ordering constraint recorded in `CompileDB.py`:
the emitter must be installed only after `Platform.configure()` has run,
or it also captures the `conftest_*` compilations of the configure checks.
That is why this is a plain helper invoked from the `SConstruct` -- similar to
Setup, Options, Platform -- and not a tool loaded from the `LumieraEnvironment`.
2026-10-01 01:08:23 +02:00
595bb538be Buildsystem: remove the CCache and DistCC integration (closes: #1438)
Both were added in 12/2008, at a time when the choice of SCons still had
to be defended, and neither was ever used in practice.

The MD5 based change detection of SCons, in concert with the declarative
build dependency tree alleviates the need for "cleaning paranoia" and makes
full rebuilds a rare event.

Beyond being unused, both tool integrations require to rewrite the `$CXX`
construction variable into "wrapper g++", which silently changes what any
tool reading the build commands concludes about the compiler in use. That
matters for the compilation database, which needs to be added next.
2026-10-01 01:05:52 +02:00
7c35af3717 Workflow: integrate some work notes in my mindmap
I took some work notes during that ongoing discussion in a separate ''scratchpad document.''
As part of the effort to create an overview digest, I also revisited these work notes
and extracted a complete listing of all mails, each marked with some key topics.

Furthermore I took some notes while interacting with claude and revising the generated text.
2026-07-29 22:15:31 +02:00
4cdf01bc72 Parameters: better API for opaque transfer
The type-independent data transfer is based on a double-dispatch,
which is implemented as visitor pattern; so the operation must start
from the source parameter's VTable and be ''deflected back'' to the receiver

This completes the base operations required to establish a »Domain« (data type)
Adding more test coverage to demonstrate usage patterns...
2026-07-25 21:52:51 +02:00
c60e7d35c6 Parameters: reconsider where to use the visitor-pattern
...turns out as better to code two distinct low-level operations,
albeit implemented by delegation to a single generic assignment implementation.

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

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

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

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

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

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

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

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

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

A shift in the setup of the Job-Functor arguments necessitates to introduce a `Parameter` type — ''albeit just as placeholder''
2026-07-01 01:50:26 +02:00
793c9950d4 Invocation: evaluate ways to represent control parameters
As a follow-up to the decisions regarding Render Job arguments,
it should be noted that the `ProcessKey` in the `TurnoutSystem` becomes redundant;
we'd need some access to ''generic Parameters'' rather
2026-06-30 16:30:41 +02:00
ce2c3d2c06 Invocation: draft a generalised output context
...the plan is to wrap the `DataSink` and also to expose possible further control parameters here...

Rather difficult to get started, since mostly related to ''planned features''
2026-06-30 01:35:53 +02:00
8db1813831 Invocation: adapt job parameters to requirements
This is the result of an requirement analysis,
based on the current (better) understanding of ''render job invocation''

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

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

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

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

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

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

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

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

The first simple test case builds and then pulls a source node.
2026-06-25 01:54:21 +02:00
f3dcbfe8b9 pick up work after the LAC-26 Maynooth
...after having settled the problems related to buffer management
and the interplay between buffers and output sinks, the ''next goal''
is to drive ahead the integration between job planning and invocation...
2026-06-24 18:27:12 +02:00
86 changed files with 13374 additions and 884 deletions

10
.clangd Normal file
View file

@ -0,0 +1,10 @@
# Configuration for the `clangd` language server -- https://clangd.llvm.org/config
#
# The compilation database is generated by the SCons build system into the target tree:
# scons COMPILEDB=yes (sticky: recorded in ./optcache)
# scons compiledb (one-off, compiles nothing)
# clangd does not search 'target/' on its own, hence this pointer.
CompileFlags:
CompilationDatabase: target

View file

@ -29,6 +29,7 @@ sys.path.append('./admin/scons')
import Setup
import Options
import Platform
import CompileDB
from Buildhelper import *
from LumieraEnvironment import *
@ -39,6 +40,7 @@ from LumieraEnvironment import *
env = Setup.defineBuildEnvironment() # dirs & compiler flags
env = Platform.configure(env) # library dependencies
compileDB = CompileDB.setup(env) # code intelligence (opt-in: COMPILEDB=yes)
@ -61,7 +63,7 @@ env.Clean ('build', [ 'scache.conf', '.sconf_temp', '.sconsign.dblite', 'config.
#Import('lumiera plugins tools gui testsuite doxydoc')
Import('lumiera plugins tools gui testsuite')
build = env.Alias('build', lumiera + plugins + tools + gui)
build = env.Alias('build', lumiera + plugins + tools + gui + compileDB)
# this defines the SCons **default target**
env.Default('build')

View file

@ -140,39 +140,7 @@ def createPlugins(env, directory, **kw):
def checkCommandOption(env, optID, val=None, cmdName=None):
""" evaluate and verify an option, which may point at a command.
besides specifying a path, the option may read True, yes or 1,
denoting that the system default for this command should be used.
@return: True, if the key has been expanded and validated,
False, if this failed and the key was removed
"""
if not val:
if not env.get(optID): return False
else:
val = env.get(optID)
if val=='True' or val=='true' or val=='yes' or val=='1' or val == 1 :
if not cmdName:
print("WARNING: no default for %s, please specify a full path." % optID)
del env[optID]
return False
else:
val = env.WhereIs(cmdName)
if not val:
print("WARNING: %s not found, please specify a full path" % cmdName)
del env[optID]
return False
if not os.path.isfile(val):
val = env.WhereIs(val)
if val and os.path.isfile(val):
env[optID] = val
return True
else:
del env[optID]
return False
# removed 10/2026 : checkCommandOption() "evaluate and verify an option, which may point at a command"

59
admin/scons/CompileDB.py Normal file
View file

@ -0,0 +1,59 @@
# coding: utf-8
##
## CompileDB.py - SCons build: generate a compilation database for clangd
##
# 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.
#####################################################################
from SCons.Script import COMMAND_LINE_TARGETS
def setup(env):
""" Emit a *compilation database*, describing every compiler invocation of the
complete build. This JSON file is the interface used by `clangd` and the other
tools built on the Clang front-end, which need to reproduce the exact flags
of a translation unit in order to parse it the way the build does.
The actual work is performed by the `CompilationDatabase` builder, which SCons
provides since 4.0: it installs an additional *emitter* on the object builders,
capturing each command line while the dependency graph is constructed. Thus no
compilation is involved -- `scons compiledb` takes about two seconds -- and the
database always covers the *complete declared build*, not just the requested
target, since the SConscript definitions are processed into a dependency tree
on every invocation anyway.
@return a dependency node, to be added to the `build` alias, or an empty list
when the feature is switched off (in which case nothing at all is
installed into the build environment).
@note **must be invoked after** `Platform.configure(env)`. The emitter would
otherwise also capture the `conftest_*` compilations performed by the
configure checks, which then show up as bogus entries below '.sconf_temp'.
This ordering constraint is why this module is a plain helper invoked
from the 'SConstruct' -- like `Setup`, `Options` and `Platform` -- and
not a SCons tool loaded from the `LumieraEnvironment` constructor.
@remark the database is written into the (relocatable) target tree, while the
'.clangd' configuration in the project root directs the language server
there; consumers do not search 'target/' on their own.
"""
if not isActivated(env): return []
env.Tool('compilation_db') # the builder provided by SCons
env['COMPILATIONDB_USE_ABSPATH'] = True # make recorded content unambiguous
compileDB = env.CompilationDatabase('$TARGDIR/compile_commands.json')
env.Alias('compiledb', compileDB)
return compileDB
def isActivated(env):
""" generation is opt-in through the (sticky) build option,
but can also be requested for a single invocation by naming the target.
"""
return env['COMPILEDB'] or 'compiledb' in COMMAND_LINE_TARGETS

View file

@ -44,8 +44,6 @@ class LumieraEnvironment(Environment):
self.path = Record (extract_localPathDefs(buildSetup)) # ◁───── e.g. buildExe -> env.path.buildExe
self.libInfo = {}
self.Tool("BuilderDoxygen")
self.Tool("ToolDistCC")
self.Tool("ToolCCache")
register_LumieraIconBuilder(self)
register_LumieraResourceBuilders(self)
register_LumieraCustomBuilders(self)

View file

@ -29,13 +29,12 @@ def defineCmdlineVariables(buildVars):
('ARCHFLAGS', 'Set architecture-specific compilation flags (passed literally to gcc)','')
,('CC', 'Set the C compiler to use.', 'gcc')
,('CXX', 'Set the C++ compiler to use.', 'g++')
,PathVariable('CCACHE', 'Integrate with CCache', '', PathVariable.PathAccept)
,PathVariable('DISTCC', 'Invoke C/C++ compiler commands through DistCC', '', PathVariable.PathAccept)
,EnumVariable('BUILDLEVEL', 'NoBug build level for debugging', 'ALPHA', allowed_values=('ALPHA', 'BETA', 'RELEASE'))
,BoolVariable('DEBUG', 'Build with debugging information and no optimisations', False)
,BoolVariable('OPTIMIZE', 'Build with strong optimisation (-O3)', False)
,BoolVariable('VALGRIND', 'Run Testsuite under valgrind control', True)
,BoolVariable('VERBOSE', 'Print full build commands', False)
,BoolVariable('COMPILEDB','Generate target/compile_commands.json for clangd', False)
,('TESTSUITES', 'Run only test suites matching the given pattern', '')
,('TESTMODE', 'test suite error mode for test.sh', '')
# ,BoolVariable('OPENGL', 'Include support for OpenGL preview rendering', False)
@ -60,6 +59,7 @@ Special Targets:
testcode: additionally compile the Testsuite
check : build and run the Testsuite
doc : generate documentation (Doxygen)
compiledb: just (re)generate target/compile_commands.json for clangd
all : build and testcode and doc
install : install created artifacts at PREFIX

View file

@ -1,67 +0,0 @@
# coding: utf-8
##
## ToolCCache.py - SCons tool for integrating with CCache compiler cache
##
# Copyright (C) Lumiera.org and FreeOrion.org
# 2008, 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.
#####################################################################
# This SCons builder was extracted from http://www.freeorion.org/
# FreeOrion is an open-source platform-independent galactic conquest game
#
# history: 12/2008 adapted for Lumiera build system
import os
from Buildhelper import *
def generate(env):
""" Modify the environment such as to redirect any
C/C++ compiler invocations through CCache, while using
CCache config variables found in the os.environment.
"""
if not exists(env): return
assert env['CCACHE']
if not env['CCACHE'] in env['CC']:
env['CC'] = env.subst('$CCACHE $CC')
if not env['CCACHE'] in env['CXX']:
env['CXX'] = env.subst('$CCACHE $CXX')
print(env.subst("* Build using $CCACHE"))
for i in ['HOME'
,'CCACHE_DIR'
,'CCACHE_TEMPDIR'
,'CCACHE_LOGFILE'
,'CCACHE_PATH'
,'CCACHE_CC'
,'CCACHE_CPP2'
,'CCACHE_PREFIX'
,'CCACHE_DISABLE'
,'CCACHE_READONLY'
,'CCACHE_NOSTATS'
,'CCACHE_NLEVELS'
,'CCACHE_HARDLINK'
,'CCACHE_RECACHE'
,'CCACHE_UMASK'
,'CCACHE_HASHDIR'
,'CCACHE_UNIFY'
,'CCACHE_EXTENSION'
]:
if i in os.environ and i not in env:
env['ENV'][i] = os.environ[i]
def exists(env):
""" Ensure CCache is available.
"""
return checkCommandOption(env, 'CCACHE', cmdName='ccache')

View file

@ -1,56 +0,0 @@
# coding: utf-8
##
## ToolDistCC.py - SCons tool for distributed compilation using DistCC
##
# Copyright (C) Lumiera.org and FreeOrion.org
# 2008, 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.
#####################################################################
# This SCons builder was extracted from http://www.freeorion.org/
# FreeOrion is an open-source platform-independent galactic conquest game
#
# history: 12/2008 adapted for Lumiera build system
import os
from Buildhelper import *
def generate(env):
""" Modify the environment such as to redirect any
C/C++ compiler invocations through DistCC. Additionally
pull in the environment config variables used by DistCC
"""
if not exists(env): return
assert env['DISTCC']
if not env['DISTCC'] in env['CC']:
env['CC'] = env.subst('$DISTCC $CC')
if not env['DISTCC'] in env['CXX']:
env['CXX'] = env.subst('$DISTCC $CXX')
print(env.subst("* Build using $DISTCC"))
for i in ['HOME'
,'DISTCC_HOSTS'
,'DISTCC_VERBOSE'
,'DISTCC_FALLBACK'
,'DISTCC_LOG'
,'DISTCC_MMAP'
,'DISTCC_SAVE_TEMPS'
,'DISTCC_TCP_CORK'
,'DISTCC_SSH'
]:
if i in os.environ and i not in env:
env['ENV'][i] = os.environ[i]
def exists(env):
""" Ensure DistCC exists.
"""
return checkCommandOption(env, 'DISTCC', cmdName='distcc')

View file

@ -238,6 +238,7 @@ Targets
- *check* builds test code and runs our test-suites
- *research* builds just the research tree
- *doc* builds documentation (currently just Doxygen)
- *compiledb* (re)generates the xref:CompilationDB[compilation database] -- compiles nothing
- *all* builds the Application, test-suites and documentation
- *install* builds and installs the Lumiera Application
@ -265,8 +266,34 @@ of the application to be re-built. And of course it means, that you only ever co
necessary.
With SCons, there is no need for the usual ``cleaning paranoia''. Similarly, there is no need
for CCache (but using DistCC rocks !). Unfortunately, calculating those MD5 sums requires some
time on each build, even if the net result is that nothing will happen at all.
for CCache, since the content signatures already prevent any redundant compilation within a
working tree. Unfortunately, calculating those MD5 sums requires some time on each build,
even if the net result is that nothing will happen at all.
.CCache and DistCC (removed 10/2026)
[NOTE]
--
Both were supported by the build system from 12/2008 until 10/2026, as options `CCACHE`
and `DISTCC`, each redirecting the compiler invocation through a wrapper command.
Neither was ever used in practice, and both are considered to be of marginal relevance.
- *CCache* caches compilation results across working trees and over time. SCons provides a
superior solution with content MD5, taking the defined dependency graph into account.
CCache would only provide a _secondary, opaque notion_ of ``this is the same compilation'' --
and when such a judgement is wrong, the resulting confusion is expensive to diagnose.
Furthermore, conditional compilation is also based on the MD5 change detection, and
turns out to be highly effective -- to the degree that full rebuilds are rare.
- *DistCC* distributes compilation over a network of build hosts. It was added when one
contributor had a private build network available; the project has had no such setup for
many years. Note also that DistCC needs to ship _preprocessed_ translation units over
the wire -- for a template-heavy C++ codebase the payload grows with exactly those
units that are slowest, so the benefit is smallest where it would be most welcome.
Beyond being unused, both carried a concrete liability: each rewrote the `$CXX` construction
variable into `wrapper g++` -- a mutation that silently changes what any tool reading the
build commands will conclude about the compiler in use, the
xref:CompilationDB[compilation database] among them.
--
Configuration options
^^^^^^^^^^^^^^^^^^^^^
@ -355,3 +382,62 @@ to see examples of that technique -- which we also use to define that
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.
[[CompilationDB]]
Compilation database
^^^^^^^^^^^^^^^^^^^^
The tooling built on the Clang front-end -- the https://clangd.llvm.org/[clangd] language
server, `clang-tidy`, `clang-format`'s include ordering, and various code browsers -- must
be able to parse a translation unit *exactly the way the build parses it*: same include
paths, same preprocessor definitions, same language standard. Since the build system is the
only place where that knowledge exists, the tools expect it to be handed over, in the form
of a JSON file listing one entry per compiler invocation. The format is specified by LLVM as
the https://clang.llvm.org/docs/JSONCompilationDatabase.html[JSON Compilation Database];
clangd's own https://clangd.llvm.org/design/compile-commands[notes on compile commands]
explain how a language server consumes it, and which heuristic it applies for header files,
which never appear in the database themselves.
Generation is *opt-in*, through the sticky option `COMPILEDB=yes`, or for a single
invocation by naming the `compiledb` target. Both write 'target/compile_commands.json'.
When the option is set, the database is a dependency of the `build` alias and is thus
refreshed by every ordinary build.
.How it works
SCons has provided a `CompilationDatabase` builder since version 4.0. Rather than
scraping build output, it installs an additional *emitter* on the object builders: whenever
an object node is created while the SConscript definitions are read, the emitter captures
the unexpanded command line together with the environment in force for that node. This
setup has several consequences:
Nothing is compiled::
Generating the database takes about two seconds, because it only requires
the dependency graph to be constructed, while no build commands will be evaluated.
All targets are captured::
The generated database always describes the complete build, not the requested target.
A plain `scons compiledb` covers 'src/', 'tests/', 'research/' and 'src/tool/' alike,
including the per-subtree flag differences -- the GTK include paths appear on the
'src/stage/' entries and nowhere else, `-Itests` is given only below 'tests/'.
Note that the captured command lines are *not* affected by the `VERBOSE` option: the
emitter reads the underlying `$CXXCOM` construction variable directly, not the abbreviated
progress messages configured in 'Setup.py'.
.How the feature is attached
The integration lives in 'admin/scons/CompileDB.py' and is invoked from the 'SConstruct'
through `CompileDB.setup(env)`, which returns the generated node so that the `build` alias
stays defined in one single, prominent place. Unlike the other custom tools, this one is
deliberately *not* loaded from the `LumieraEnvironment`: the emitter must be installed only
*after* `Platform.configure(env)` has run, since it would otherwise also capture the `conftest_*`
compilations of the configure checks and litter the database with entries below '.sconf_temp'.
.Consuming the database
clangd searches for 'compile_commands.json' beside the source and in a 'build/' subdirectory,
but not in our 'target/'. The '.clangd' configuration file in the project root supplies that
pointer, and is the place for any further language-server settings; its syntax is documented
under https://clangd.llvm.org/config[clangd configuration]. Note that the compilation
database describes what *GCC* is invoked with, which might not in every case be what Clang accepts;
Yet the database is still correct and useful in those spots; the parse simply degrades locally.
TIP: Lumiera embraces the policy to be compilable both with GCC and Clang
(but Clang support has fallen back at times, temporarily)

View file

@ -1,6 +1,7 @@
/* try.cpp - to try out and experiment with new features....
* scons will create the binary bin/try
*/
// 09/26 - ways to build a cyclically wrapping number domain
// 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
@ -11,425 +12,146 @@
/** @file try.cpp
* 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
* Investigate techniques for creating a cyclically wrapping numeric domain.
* Actually this could be achieved with my `floordiv()` utility, yet the ramifications
* were not clear -- so I settled to build a stand-alone solution, based on the naive modulus
* technique, attempting to fix the negative offsets. It became clear that this approach would
* have to consider the domain boundaries and identify cases that could not be handlet. At that
* point, I used Claude to scrutinise and improve my draft, leading to a generic tool that is
* now relocated into the \ref util-quant.hpp header
* @see lib::par::Scale
*/
#include "lib/format-cout.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-quant.hpp"
#include "lib/util.hpp"
#include <array>
using std::string;
template<typename X>
#include <cmath>
template<typename NUM>
constexpr NUM _MAX = std::numeric_limits<NUM>::max();
template<typename NUM>
constexpr NUM _MIN = std::numeric_limits<NUM>::lowest();
template<typename N>
void
show()
{
SHOW_TYPE(X)
}
showCase (N val, N minVal, N maxVal)
{
cout << "val:"<<val<<" (min:"<<minVal<<" , max:"<<maxVal<<") ->"<< util::cyclicWrap (val,minVal,maxVal)<<endl;
}
template<lib::meta::tuple_like X>
template<typename N>
void
show()
{
cout << "Tulik!! "<< ::test::showType<X>() <<endl;
lib::meta::forEachIDX<X> ([](auto i)
{
using Elm = std::tuple_element_t<i, X>;
cout <<" "<<uint(i)<<": "<< ::test::showType<Elm>() <<endl;
});
}
showFloorDiv (N num, N den)
{
cout << "FloorDiv:"<<num<<" / "<<den<<" ->"<< util::floordiv(num,den)<<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 { };
template<typename N>
void
showFloorWrap (N num, N den)
{
auto fwrap = util::floorwrap (num,den);
cout << "FloorWrap:"<<num<<" / "<<den<<" -> q="<<fwrap.quot<<" r="<<fwrap.rem<<endl;
}
int
main (int, char**)
{
using Tup = std::tuple<long,short>;
using Arr = std::array<int,3>;
using Hetero = lib::HeteroData<int,string>::Chain<short>::ChainExtent<bool,lib::meta::Nil>::ChainType;
const int PER = 3;
int i = -5;
int d = i / PER;
cout << "d="<<d<<endl;
cout << "fd="<<util::floordiv(i,PER)<<endl;
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> ))
uint u = _MAX<uint>;
show<Tup>();
show<Arr>();
show<Hetero>();
show<int>();
cout << u << " fd="<<util::floordiv(u,uint(PER))<<" ...re="<<PER*util::floordiv(u,uint(PER))<<endl;
using TupStringy = FunTrampoline<Stringify, ElmTypes<Tup>::Seq>;
SHOW_EXPR(TupStringy::size())
showFloorDiv (7,5);
showFloorDiv (-7,5);
showFloorDiv (7,-5);
showFloorDiv (-7,-5);
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))
showFloorWrap (7,5);
showFloorWrap (-7,5);
showFloorWrap (7,-5);
showFloorWrap (-7,-5);
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)
showFloorWrap (_MAX<int>,1);
showFloorWrap (_MAX<int>,2);
showFloorWrap (_MAX<int>,5);
showFloorWrap (_MIN<int>,1);
showFloorWrap (_MIN<int>,2);
showFloorWrap (_MIN<int>,5);
showFloorWrap (_MAX<int>,-1);
showFloorWrap (_MAX<int>,-2);
showFloorWrap (_MAX<int>,-5);
showFloorDiv (_MAX<int>,1);
showFloorDiv (_MAX<int>,2);
showFloorDiv (_MAX<int>,5);
showFloorDiv (_MIN<int>,1);
showFloorDiv (_MIN<int>,2);
showFloorDiv (_MIN<int>,5);
showFloorDiv (_MAX<int>,-1);
showFloorDiv (_MAX<int>,-2);
showFloorDiv (_MAX<int>,-5);
showFloorWrap (_MAX<uint>,1u);
showFloorWrap (_MAX<uint>,2u);
showFloorWrap (_MAX<uint>,5u);
showFloorDiv (_MAX<uint>,1u);
showFloorDiv (_MAX<uint>,2u);
showFloorDiv (_MAX<uint>,5u);
showCase<int>(-7,-5,5);
showCase<int>(-6,-5,5);
showCase<int>(-5,-5,5);
showCase<int>(-4,-5,5);
showCase<int>(+5,-5,5);
showCase<int>(+6,-5,5);
showCase<int>(+7,-5,5);
showCase<int>(+8,-5,5);
showCase<int>(+9,-5,5);
showCase<int>(+10,-5,5);
showCase<int>(+11,-5,5);
showCase<int>(+12,-5,5);
showCase<int>(+14,-5,5);
showCase<int>(+15,-5,5);
showCase<int>( -7,-10,-7);
showCase<int>( -8,-10,-7);
showCase<int>( -9,-10,-7);
showCase<int>(-10,-10,-7);
showCase<int>(-11,-10,-7);
showCase<int>(-12,-10,-7);
showCase<int>(-13,-10,-7);
showCase<int>(-14,-10,-7);
showCase<int>(-15,-10,-7);
showCase<uint>(5,1000,1003);
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&> ))
showCase<float>(0.2 ,-2,-1.8);
showCase<float>(0 ,-2,-1.8);
showCase<float>(-0.8,-2,-1.8);
showCase<float>(-1.79,-2,-1.8);
showCase<float>(-1.8,-2,-1.8);
showCase<float>(-1.81,-2,-1.8);
showCase<float>(-1.99,-2,-1.8);
showCase<float>(-2 ,-2,-1.8);
showCase<float>(-2.01,-2,-1.8);
cout << "\n.gulp." <<endl;
return 0;

View file

@ -27,9 +27,9 @@
** mostly at start and then hold and use the memory, the allocation is never
** actually _closed_ — implying that further allocations can be added during
** the whole life time, which may possibly even trigger a further base allocation
** if storage space in the last Extent is exhausted. In theory, it would be possible
** if storage space if the last Extent is exhausted. In theory, it would be possible
** to use a custom allocation (in the AllocationCluster::StorageManager::Extents,
** which is a lib::LinkedElements and could be parametrised with a allocator template).
** which is a lib::LinkedElements and could be parametrised with an allocator template).
** Allocations are never discarded, and thus any alloted memory will be kept until
** the whole AllocationCluster is destroyed as a compound.
** \par using as STL allocator
@ -361,7 +361,13 @@ namespace lib {
Policy (AllocationCluster& clu)
: AllocationPolicy<I,E,Adapter> (clu.getAllocator<std::byte>())
: Base{clu.getAllocator<std::byte>()}
{ }
/** @note allow cross-construction from any AllocationCluster based allocator */
template<class U, class V>
Policy (AllocationPolicy<U,V,Adapter>& relatedPolicy)
: Base(relatedPolicy)
{ }
bool

View file

@ -363,11 +363,11 @@ namespace util {
string showDecimal (double val) noexcept { return showFloatingPoint (val, PRECISION_DECIMAL<double>); }
string showDecimal (float val) noexcept { return showFloatingPoint (val, PRECISION_DECIMAL<float>); }
string showDecimal (f128 val) noexcept { return showFloatingPoint (val, PRECISION_DECIMAL<f128>); }
string showDecimal (lflp val) noexcept { return showFloatingPoint (val, PRECISION_DECIMAL<lflp>); }
string showComplete (double val) noexcept { return showFloatingPoint (val, PRECISION_COMPLETE<double>); }
string showComplete (float val) noexcept { return showFloatingPoint (val, PRECISION_COMPLETE<float>); }
string showComplete (f128 val) noexcept { return showFloatingPoint (val, PRECISION_COMPLETE<f128>); }
string showComplete (lflp val) noexcept { return showFloatingPoint (val, PRECISION_COMPLETE<lflp>); }
string

View file

@ -93,10 +93,10 @@ namespace util {
std::string showDecimal (double) noexcept;
std::string showDecimal (float) noexcept;
std::string showDecimal (f128) noexcept;
std::string showDecimal (lflp) noexcept;
std::string showComplete (double)noexcept;
std::string showComplete (float) noexcept;
std::string showComplete (f128) noexcept;
std::string showComplete (lflp) noexcept;

View file

@ -14,7 +14,7 @@
/** @file format-util.hpp
** Collection of small helpers and convenience shortcuts for diagnostics & formatting.
** - util::str() performs a failsafe to-String conversion, thereby preferring a
** - util::toString() performs a failsafe to-String conversion, thereby preferring a
** built-in conversion operator, falling back to just a mangled type string.
** - util::join() generates an enumerating string from elements
** of an arbitrary sequence or iterable. Elements will be passed

View file

@ -30,8 +30,11 @@
#ifndef LIB_HASH_VALUE_H
#define LIB_HASH_VALUE_H
#include <stdlib.h>
#ifdef __cplusplus
#include <cstdlib>
#else
#include <stdlib.h>
#endif
/**
* storage for a Lumiera unique ID,
@ -44,8 +47,6 @@ typedef lumiera_uid* LumieraUid;
#ifdef __cplusplus /* =========== C++ definitions ====================== */
#include <climits>
namespace lib {
/** a STL compatible hash value */

View file

@ -108,7 +108,7 @@ namespace lib {
{
using Tuple = std::tuple<DATA...>;
using Tuple::tuple;
using Tuple::Tuple;
template<typename SPEC>
void linkInto (HeteroData<SPEC>&);

View file

@ -21,7 +21,6 @@
#ifndef LIB_INTEGRAL_H
#define LIB_INTEGRAL_H
#include <stdlib.h>
#include <cstddef>
#include <cstdint>
@ -33,10 +32,10 @@ using llong = long long int;
using ullong = unsigned long long int;
using ushort = unsigned short int;
using f128 = long double;
static_assert(10 <= sizeof(f128));
using lflp = long double;
static_assert(10 <= sizeof(lflp));
const uint LIFE_AND_UNIVERSE_4EVER = 42;
#endif /*LIB_INTEGRAL_H*/

View file

@ -138,10 +138,10 @@ namespace lib {
struct iterator
: IterAdapter<Pos, DataHandle>
{ ////////////////////////////////////////////////////TICKET #1125 : should be build on top of IterStateWrapper rather than IterAdapter!
using _I = IterAdapter<Pos, DataHandle>;
using _I::IterAdapter;
using Adapter = IterAdapter<Pos, DataHandle>;
using Adapter::Adapter; // expose inherited ctors
operator string() const {return _I::source()? string(*_I::source()) : "⟂"; }
operator string() const {return Adapter::source()? string(*Adapter::source()) : "⟂"; }
};
/** build an iterator frontend for the given source,

View file

@ -26,6 +26,7 @@
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>

View file

@ -46,7 +46,6 @@
#include "lib/hash-value.h" /* Types lumiera_uid and LumieraUid */
#include <stdlib.h>

View file

@ -114,7 +114,7 @@ namespace meta{
* 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.
* \ref FunInline::activate<FUNC>() provides the blueprint for this code generation.
*/
template<class FUN>
struct Invoker

View file

@ -115,6 +115,26 @@ namespace meta {
{ };
/**
* Metafunction: build an ID-Type based on the position in the list
* @remark when the type is not in the list, TypeIdxID ≔ 0
*/
template<typename TY, typename TYPES, uint idx=1>
struct TypeIdxID
: std::integral_constant<uint, 0>
{ };
template<typename TY, typename TYPES, uint idx>
struct TypeIdxID<TY, Node<TY,TYPES>, idx> ///< @return 1-based position in case of match
: std::integral_constant<uint, idx>
{ };
template<typename X, typename TY, typename TYPES, uint idx>
struct TypeIdxID<X, Node<TY,TYPES>, idx> ///< else recurse to check next element
: TypeIdxID<X, TYPES, idx+1>
{ };
/**
* Build a list of const types from a given typelist.

View file

@ -456,12 +456,12 @@ namespace util {
/** show maximum reproducible decimal representation */
std::string showDecimal (double) noexcept;
std::string showDecimal (float) noexcept;
std::string showDecimal (f128) noexcept;
std::string showDecimal (lflp) noexcept;
/** show enough decimal digits to represent every distinct value */
std::string showComplete (double) noexcept;
std::string showComplete (float) noexcept;
std::string showComplete (f128) noexcept;
std::string showComplete (lflp) noexcept;
std::string showSize (size_t) noexcept;

View file

@ -810,7 +810,7 @@ namespace lib {
using Holder = InPlaceBuffer<BA, sizeof(SUB), DEFAULT>;
Holder& holder = *static_cast<Holder*> (buffer_);
return holder.template emplace (std::forward<SUB> (implementation));
return holder.emplace (std::forward<SUB> (implementation));
}
/** Abbreviation for placement new of a subclass SUB into the opaque buffer*/

174
src/lib/par/disposition.hpp Normal file
View file

@ -0,0 +1,174 @@
/*
DISPOSITION.hpp - actual constitution of a parameter descriptor
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 disposition.hpp
** Concrete configuration of a parameter descriptor with type and value provision.
** This is the full definition, with all details spelled out, and what is actually
** stored within the _outer shell_ of a Parameter. The publicly visible interface
** of the latter is implemented by delegating to the constituents of the disposition.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_DISPOSITION_H
#define LIB_PAR_DISPOSITION_H
#include "lib/par/spec.hpp"
#include "lib/par/provision.hpp"
#include "lib/par/param-type.hpp"
namespace lib {
namespace par {
using ValStorage = ValBuffStorage<BASETYPE_MAX_SIZ>;
/**
* Implementation-Service: complete Parameter functionality.
* Includes the ability to access / set a »value«, attach automation,
* identify and convert underlying value data types and handle
* trait- and identity tags and the specifics of the value scale.
*/
class Disposition
: public Domain
{
IDRecord idr_;
public:
template<typename X>
X extract() const;
template<typename X>
void setVal (X);
protected:
template<typename X>
bool
isBaseDomainMatch() const
{
return idr_.baseTypeID == BaseTypeID<X>();
}
template<typename X>
void
markBaseDomain()
{
idr_.baseTypeID = BaseTypeID<X>();
}
};
/**
* A complete configuration and setup of a parameter, including a type specification
* with domain and scale, and a value provision. Can be generated through a builder
* notation and stored within the Parameter object (as a container).
*/
template<typename VAL, class IFA =Disposition> /////////////////////////////////////////////////////TICKET #1197 : the second template argument is quite confusing and became necessary due to the limitations of PolymorphicValue
class ParamData
: public TypedEntity<VAL, IFA>
{
VAL val_;
Provision* src_;
public:
ParamData (VAL initVal)
: val_{std::move (initVal)}
, src_{nullptr}
{
Disposition::markBaseDomain<VAL>();
}
/* === Provision Interface === */
void retrieveInto (ValBuff&) const override final; ///< @note is sometimes invoked non-virtually for sake of optimisation
void setValFrom (ValBuff const&) override final;
};
template<typename X>
using FullDomain = BaseDomain<X, Disposition>;
template<typename X>
using SimpleData = ParamData<X, Disposition>;
template<typename VAL, class IFA>
void
ParamData<VAL, IFA>::retrieveInto (ValBuff& target) const
{
asValue<VAL>(target) = val_;
}
template<typename VAL, class IFA>
void
ParamData<VAL, IFA>::setValFrom (ValBuff const& newVal)
{
val_ = asValue<VAL>(newVal);
}
/**
* @warning optimisation to a direct access will be applied
* when the baseTypeID in the IDRecord matches the type \a X.
* It is assumed in this case that the actual implementation of
* the Disposition interface is a ParamData<X>, allowing to side-step
* any virtual dispatch; since the implementation of #retrieveInto and
* #setValFrom is defined in the same source file, the optimiser will
* inline the call, resulting in a direct value access.
*/
template<typename X>
X
Disposition::extract() const
{
X result; // RVO
if (isBaseDomainMatch<X>())
{ // short-circuit by direct force-cast, bypassing virtual dispatch
auto& paramData = * static_cast<SimpleData<X> const *> (this);
paramData.SimpleData<X>::retrieveInto (asValBuff (result));
}
else
{ // run full type-conversion dispatch
ValStorage valueBuffer;
this->retrieveInto (valueBuffer);
this->extractAs (result, valueBuffer);
}
return result;
}
template<typename X>
void
Disposition::setVal (X changedVal)
{
if (isBaseDomainMatch<X>())
{ // short-circuit by direct force-cast, bypassing virtual dispatch
auto& paramData = * static_cast<SimpleData<X>*> (this);
paramData.SimpleData<X>::setValFrom (asValBuff (changedVal));
}
else
{ // run full type-conversion dispatch
ValStorage valueBuffer;
this->conform (valueBuffer, changedVal);
this->setValFrom (valueBuffer);
}
}
}} // namespace lib::par
#endif /*LIB_PAR_DISPOSITION_H*/

99
src/lib/par/domain.hpp Normal file
View file

@ -0,0 +1,99 @@
/*
DOMAIN.hpp - represent the value domain of parameters
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 domain.hpp
** Specification of a base type and a value domain for parameters.
** A Domain descriptor implicitly also encodes the concrete value type
** and exposes a set of virtual functions to handle all supported value conversions.
** Implementing this interface creates a conversion matrix and allows to implement
** generic conversions by double-dispatch. Since virtual interfaces must be complete
** at compile time however, such an approach requires to define a finite set of
** supported parameter value types, which seems adequate, since the universe
** of possible values is limited anyway: elementary values in parameters
** must be primitive base types that can be created and destroyed
** without side effect. Relying on the further features of
** the parameter type system, compound types (vectors)
** and limited sub-scales can be defined.
**
** @todo WIP-WIP-WIP this is a draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_DOMAIN_H
#define LIB_PAR_DOMAIN_H
#include "lib/par/spec.hpp"
#include "lib/par/provision.hpp"
#include "lib/meta/generator.hpp"
#include "lib/meta/trait.hpp"
namespace lib {
namespace par {
/* ===== Type Handler Interface for generic parameter value access ===== */
class HandlerBase
: public Provision
{
public:
void extractAs();
void conform();
};
template<typename X, class PAR>
class TypeHandler
: public PAR
{
public:
virtual void extractAs (X& targetVal, ValBuff const& valBuff) const =0;
virtual void conform (ValBuff& targetBuff, X const& srcVal) =0;
using PAR::extractAs;
using PAR::conform;
};
/** build a generic visitor interface for all types in list */
using TypeHandlerInterface = meta::InstantiateChained<BaseTypes::List, TypeHandler, HandlerBase>;
/**
* Interface: mark the allowed value domain of a parameter value.
* The identity of the actual implementation can be used as type descriptor,
* since all concrete full base domains are registered at compile time; the
* virtual Domain interface exposes the possible conversions.
* @todo 7/2026 not clear if we can support all conversions unconditionally,
* or if some notion of an _impossible conversion_ is required. It is
* clear however, that the access to an actual parameter **must fail**
* at compile time, if a sensible conversion can not be provided.
*/
class Domain
: public TypeHandlerInterface
{
public:
virtual ~Domain(); ///< this is an interface
virtual void applyLimit (ValBuff&) =0;
virtual void transferTo (ValBuff const& source, Domain& targetDomain, ValBuff& target) const =0;
protected:
};
}} // namespace lib::par
#endif /*LIB_PAR_DOMAIN_H*/

53
src/lib/par/nom-set.hpp Normal file
View file

@ -0,0 +1,53 @@
/*
NOM-SET.hpp - nominal scale domain
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 nom-set.hpp
** Collection of identifiers to constitute the domain for a nominal or ordinal scale.
** The scale descriptor is used as delegate from the ParamType descriptor and maintained
** within a global registry for sake of deduplication. It is expected that a lot of parameters
** will share some common scales, like e.g. percentage values or decibels. The setup of a scale
** may limit the value range, provide hints for the UI representation -- yet may possibly also
** encompass nominal scales that comprise a selection of ID values.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_NOM_SET_H
#define LIB_PAR_NOM_SET_H
#include "lib/par/spec.hpp"
namespace lib {
namespace par {
/**
* Interface: describe the properties of a value scale to use for parameters,
* including range limits and metric (linear, logarithmic). The core operation
* is to conform a value to this specific scale.
* @todo not clear if we want a virtual interface here....?
*/
class NomSet
{
public:
};
}} // namespace lib::par
#endif /*LIB_PAR_NOM_SET_H*/

View file

@ -0,0 +1,37 @@
/*
ParamType - parameter type record including domain and scale
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 param-type.cpp
** Implementation of parameter type registration.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#include "lib/error.hpp"
#include "lib/par/parameter.hpp"
#include "lib/par/provision.hpp"
#include "lib/par/param-type.hpp"
namespace lib {
namespace par {
/* emit VTables in this translation unit... */
Provision::~Provision() { }
Domain::~Domain() { }
}} // namespace lib::par

144
src/lib/par/param-type.hpp Normal file
View file

@ -0,0 +1,144 @@
/*
PARAM-TYPE.hpp - parameter type record including domain and scale
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 param-type.hpp
** Specification of the value domain and scale of a parameter.
** The implementation of parameters is built up in layers, starting from the generic
** interfaces \ref Provision, which describes the fundamental capabilities of a
** parameter as conceptualised here) and \ref Domain to support type conversions.
**
** All extensions beyond this point can be considered implementation-level services.
** The BaseDomain layer provides the actual type conversion services, based on internal
** knowledge of the concrete data type handled by this parameter.
**
** A \ref TypedEntity descriptor adds storage to capture type properties and allow
** for an ID registration. In addition, the concrete value type is tagged, marking
** the allowed value range, clipping behaviour and specifics of the scale (discrete,
** stepping, continuous, logarithmic...).
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_PARAM_TYPE_H
#define LIB_PAR_PARAM_TYPE_H
#include "lib/par/domain.hpp"
#include "lib/par/type-handler.hpp"
#include "lib/meta/generator.hpp"
namespace lib {
namespace par {
// GCC > 13 warns at class definition when a new overload shadows an inherited virtual function.
// While theoretically correct, in practice any call will be dispatched through the base interface,
// which in turn is also generated by metaprogramming from a type list.
// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109740
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"")
template<typename VAL, class IFA>
struct DomainSetup
{
template<typename X, class PAR>
class TypeHandlerImpl
: public PAR
{
void extractAs (X& targetVal, ValBuff const& valBuff) const override;
void conform (ValBuff& targetBuff, X const& srcVal) override;
};
using TypeHandlerChain = meta::InstantiateChained<BaseTypes::List, TypeHandlerImpl, IFA>;
};
template<typename VAL, class IFA>
class BaseDomain
: public DomainSetup<VAL, IFA>::TypeHandlerChain
{
public:
void applyLimit (ValBuff&) override;
void transferTo (ValBuff const& source, Domain& targetDomain, ValBuff& target) const override;
};
/* ===== type conversion virtual dispatch ===== */
template<typename VAL, class IFA>
template<typename X, class PAR>
inline void
DomainSetup<VAL,IFA>::TypeHandlerImpl<X,PAR>::extractAs (X& targetVal, ValBuff const& valBuff) const
{
VAL const& srcVal = asValue<VAL> (valBuff);
assignConverted (targetVal, srcVal);
}
template<typename VAL, class IFA>
template<typename X, class PAR>
inline void
DomainSetup<VAL,IFA>::TypeHandlerImpl<X,PAR>::conform (ValBuff& targetBuff, X const& srcVal)
{
VAL& targetVal = asValue<VAL> (targetBuff);
assignConverted (targetVal, srcVal);
}
template<typename VAL, class IFA>
inline void
BaseDomain<VAL,IFA>::applyLimit (ValBuff& valBuff)
{
VAL& val{asValue<VAL> (valBuff)};
val = preClamp<VAL> (val);
}
template<typename VAL, class IFA>
inline void
BaseDomain<VAL,IFA>::transferTo (ValBuff const& source, Domain& targetDomain, ValBuff& target) const
{
targetDomain.conform (target, asValue<VAL> (source));
targetDomain.applyLimit (target);
}
/**
* Service-Interface: represent a specific configuration of parameter value type,
* range and scale. Every ParamType descriptor is also a _prototype_ and has
* an unique identity, ensured by registration, so that it acts as a type identifier.
* @todo RLY? registration??
* Furthermore, the interface exposes allowed conversion and conforming operations.
*/
template<typename VAL, class IFA>
class TypedEntity
: public BaseDomain<VAL,IFA>
{
public:
};
}} // namespace lib::par
#endif /*LIB_PAR_PARAM_TYPE_H*/

33
src/lib/par/parameter.cpp Normal file
View file

@ -0,0 +1,33 @@
/*
Parameter - generic parameter values
Copyright (C)
2008,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 parameter.cpp
** Implementation functions of the Parameter abstraction.
**
** @todo this is a draft and placeholder, as of 2026
*/
#include "lib/error.hpp"
#include "lib/par/parameter.hpp"
namespace lib {
namespace par {
/** */
}} // namespace lib::par

174
src/lib/par/parameter.hpp Normal file
View file

@ -0,0 +1,174 @@
/*
PARAMETER.hpp - generic parameter values
Copyright (C)
2008,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 parameter.hpp
** A generic typed value that can be discovered and automated.
** The actual data type of the parameter is an opaque implementation detail,
** yet value ranges and conversions are handled automatically. It is thus
** both a descriptor and an opaque container. Parameter elements can be copied,
** yet the presence of some specific parameter at a given point in the model
** constitutes an identity -- and the parameter descriptor can be tagged
** with that identity.
**
** @todo this is a draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_PARAMETER_H
#define LIB_PAR_PARAMETER_H
#include "lib/par/disposition.hpp"
#include "lib/polymorphic-value.hpp"
#include "lib/util.hpp"
#include <utility>
namespace lib {
namespace par {
namespace {
static constexpr size_t PAYLOAD_SIZ = sizeof(ParamData<double>) - sizeof(void*); ////////////////////////TICKET #1197 : this is a mess! PolymorphicValue adds the ADMIN_OVERHEAD (but our payload has already a VTable)
using ParameterImplAPI = Disposition;
using CopySupportMarker = polyvalue::CopySupport<ParameterImplAPI>;
using ParamContainer = PolymorphicValue<ParameterImplAPI, PAYLOAD_SIZ, CopySupportMarker>;
}
using util::unConst;
/**
* Descriptor and container to hold a generic parameter.
* The value can be fixed or supplied by an automation function.
*/
class Parameter
: protected ParamContainer
{
public:
template<typename VAL>
VAL getVal() const;
template<typename VAL>
void setVal (VAL&&);
/* === Builder API === */
template<typename VAL>
class Builder;
template<typename VAL>
static Builder<VAL>
forType() { return {}; }
template<typename VAL>
static Builder<std::decay_t<VAL>>
forValue (VAL&& initVal)
{
using PayloadType = std::decay_t<VAL>;
return forType<PayloadType>()
.withValue (std::forward<VAL> (initVal));
}
class ProtoBuilder;
static ProtoBuilder from (Parameter const& prototype);
protected:
template<class IMP, typename...ARGS>
Parameter (IMP* typeTag, ARGS&&... args)
: PolymorphicValue (typeTag, std::forward<ARGS>(args)...)
{ }
};
template<typename VAL> requires par_basetype<VAL>
class Parameter::Builder
{
VAL initVal_{};
public:
Parameter
build()
{
using ImplPackage = ParamData<VAL, CopySupportMarker>; //////////////////////////////////////////TICKET #1197 : remove the necessity to inject the CopySupportMarker
ImplPackage* typeTag{nullptr};
Parameter resParam{typeTag, std::move (initVal_)};
return resParam;
}
template<typename X>
Builder&&
withValue (X&& initVal)
{
initVal_ = std::forward<X> (initVal);
return std::move(*this);
}
};
class Parameter::ProtoBuilder
{
Parameter ref_;
public:
ProtoBuilder (Parameter const& prototype)
: ref_{prototype}
{ }
Parameter
build()
{
return ref_;
}
template<typename X>
ProtoBuilder&&
maxVal (X&& upperBound)
{
/////////////////////////////////////////////OOO how to remould a scale?
return std::move(*this);
}
};
inline Parameter::ProtoBuilder
Parameter::from (Parameter const& prototype)
{
return ProtoBuilder{prototype};
}
template<typename VAL>
inline VAL
Parameter::getVal() const
{
static_assert (isBaseType<VAL>, "only preconfigured parameter BaseTypes are supported");
return unConst(this)->getPayload().extract<VAL>();
}
template<typename VAL>
inline void
Parameter::setVal (VAL&& changedVal)
{
static_assert (isBaseType<VAL>, "only preconfigured parameter BaseTypes are supported");
getPayload().setVal (changedVal);
}
}} // namespace lib::par
#endif /*LIB_PAR_PARAMETER_H*/

113
src/lib/par/provision.hpp Normal file
View file

@ -0,0 +1,113 @@
/*
PROVISION.hpp - determine how a parameter's value is given
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 provision.hpp
** Provide access to a parameter's value, either directly or by automation.
** Each parameter has a _base value_ and a time-based current value. The latter
** is retrieved by evaluating an automation function.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_PROVISION_H
#define LIB_PAR_PROVISION_H
#include "lib/time/timevalue.hpp"
namespace lib {
namespace par {
using time::Time;
/** Marker for an opaque value buffer with
* undisclosed size and content type */
struct ValBuff { /*placeholder*/ };
template<typename X>
constexpr inline ValBuff&
asValBuff (X& something)
{
void* rawMem{& something};
return * static_cast<ValBuff*> (rawMem);
}
template<typename X>
constexpr inline ValBuff const&
asValBuff (X const& something)
{
void const * rawMem{& something};
return * static_cast<ValBuff const *> (rawMem);
}
template<typename X>
constexpr inline X&
asValue (ValBuff& storage)
{
void* rawMem{& storage};
return * static_cast<X*> (rawMem);
}
template<typename X>
constexpr inline X const&
asValue (ValBuff const& storage)
{
void const * rawMem{& storage};
return * static_cast<X const *> (rawMem);
}
template<size_t bytes>
struct ValBuffStorage
: ValBuff
{
std::byte data[bytes];
};
/**
* Interface: access to a parameter's value, to adjust or evaluate automation.
* Conceptually, the value is decomposed into a _base value_ and possibly a
* time-controlled adjustment (automation). The base value can be retrieved
* and assigned, while the current time-based value is the result of evaluation.
* Any parameter value is implicitly associated with a Type that defines the
* underlying [value domain](\ref Domain) and possibly a \ref Scale that
* defines range limits and metric constraints.
* @remark the value access and manipulation works _backwards_ from an implicit
* value that is conceptually located _within_ this provision towards
* an ValBuff for external access. The reason for this indirect
* formulation is that the »value« is assumed to comply to some data type,
* which however remains an opaque internal detail and is not disclosed.
*/
class Provision
{
public:
virtual ~Provision(); ///< this is an interface
virtual void retrieveInto (ValBuff&) const { /*NOP*/ }
virtual void pullAt (Time, ValBuff&) const { /*NOP*/ }
virtual void setValFrom (ValBuff const&) { /*NOP*/ }
virtual void connect (Provision&) { /*NOP*/ }
virtual void disconnnect() { /*NOP*/ }
};
}} // namespace lib::par
#endif /*LIB_PAR_PROVISION_H*/

343
src/lib/par/scale.hpp Normal file
View file

@ -0,0 +1,343 @@
/*
SCALE.hpp - value scale to use for parameters
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 scale.hpp
** Specification of the allowed value scale of a parameter, including range and metric.
** The scale descriptor is used as delegate from the ParamType descriptor and maintained
** within a global registry for sake of deduplication. It is expected that a lot of parameters
** will share some common scales, like e.g. percentage values or decibels. The setup of a scale
** may limit the value range, provide hints for the UI representation -- yet may possibly also
** encompass nominal scales that comprise a selection of ID values.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_SCALE_H
#define LIB_PAR_SCALE_H
#include "lib/error.hpp"
#include "lib/par/domain.hpp"
#include "lib/par/type-handler.hpp"
#include "lib/util-quant.hpp"
#include "lib/format-string.hpp"
//#include "lib/util.hpp"
#include <optional>
namespace lib {
namespace par {
namespace err = lumiera::error;
using std::abs;
using util::_Fmt;
using std::optional;
enum Metric {LIN ///< linear ordinary numeric scale
,BIN ///< binary logarithm
,NAT ///< natural logarithm
,DEC ///< decibels (1/10 of 10-based log)
,NOM ///< value encodes a nominal or ordinal scale
};
/**
* Interface: describe the properties of a value scale to use for parameters,
* including range limits and metric (linear, logarithmic). The core operation
* is to conform a value to this specific scale.
* @todo not clear if we want a virtual interface here....?
*/
template<typename VAL>
class Scale
{
public:
Metric metric{LIN};
/////////////////////////////OOO Quantiser?
optional<VAL> minVal{};
optional<VAL> maxVal{};
optional<VAL> minUse{};
optional<VAL> maxUse{};
optional<VAL> neutral{};
optional<VAL> defVal{};
/////////////////////////////OOO »Sentinels« and nominal scales?
VAL cyclicLim =0;
/* ===== information functions ===== */
bool isValid() const;
bool isFactor() const;
bool isCyclic() const;
bool isLimited() const;
bool isLogarithmic() const;
/* ===== conforming operations ===== */
VAL getDefault() const;
VAL conform (VAL const&) const;
template<typename SRC>
VAL join (VAL const&, SRC const&, Scale<SRC> const&) const;
VAL join (VAL const&, VAL const&) const;
};
/* ===== information functions ===== */
/** self consistency check */
template<typename VAL>
inline bool
Scale<VAL>::isValid() const
{
return ((not minVal or not maxVal)
or *minVal <= *maxVal
)
and (not minUse
or (not minVal or *minVal <= *minUse)
)
and (not maxUse
or (not maxVal or *maxUse <= *maxVal)
)
and ((not minUse or not maxUse)
or *minUse <= *maxUse
)
and (not neutral
or ( (not maxVal or *neutral <= *maxVal)
and (not minVal or *minVal <= *neutral)
)
)
and (not defVal
or ( (not maxVal or *defVal <= *maxVal)
and (not minVal or *minVal <= *defVal)
)
)
and (not isCyclic()
or (minVal and maxVal
and *minVal < *maxVal
and VAL(0) < cyclicLim
and abs (*maxVal) <= cyclicLim)
)
;
}
template<typename VAL>
inline bool
Scale<VAL>::isFactor() const
{
return metric == LIN
and neutral
and *neutral == VAL(1);
}
template<typename VAL>
inline bool
Scale<VAL>::isCyclic() const
{
return VAL(0) != cyclicLim;
}
template<typename VAL>
inline bool
Scale<VAL>::isLimited() const
{
return (maxVal or minVal)
and not isCyclic();
}
template<typename VAL>
inline bool
Scale<VAL>::isLogarithmic() const
{
return metric == DEC
or metric == NAT
or metric == BIN;
}
/* ===== conforming operations ===== */
/** Build initial value that fits into the scale. */
template<typename VAL>
inline VAL
Scale<VAL>::getDefault() const
{
return defVal? *defVal
: neutral? *neutral
: conform (VAL{});
}
/**
* Accommodate a raw value from the underlying domain,
* so that it conforms with this Scale definition.
*/
template<typename VAL>
inline VAL
Scale<VAL>::conform (VAL const& rawVal) const
{
if (isCyclic())
{
REQUIRE (minVal and maxVal);
REQUIRE (*minVal < *maxVal);
if constexpr (std::is_floating_point_v<VAL>)
if (not (std::abs (rawVal) < cyclicLim))
throw err::Invalid {_Fmt{"Parameter value %4.2g beyond supported numeric precision "
"for cyclic wrapping into [%1.2g...%1.2g[ "}
% rawVal % *minVal % *maxVal
};
return util::cyclicWrap (rawVal, *minVal, *maxVal);
}
if (minVal and rawVal < *minVal)
return *minVal;
if (maxVal and rawVal > *maxVal)
return *maxVal;
// no further constraint to enforce...
return rawVal;
}
namespace { // logarithmic scale conversions...
/** transform a logarithmic value into an exponential factor */
template<typename TAR, typename VAL>
inline auto
expFactor (Metric metric, VAL const& value)
{
using WorkType = CommonComputeType<VAL,TAR>;
switch (metric)
{
case NAT: return std::exp (WorkType(value));
case BIN: return std::exp2 (WorkType(value));
case DEC: return std::pow (WorkType(10), WorkType(value/10.0));
default:
throw err::Logic{_Fmt{"Attempt to compute exponential "
"from a non-logarithmic value with metric %d"}
% metric};
break;
}
}
/** transform this value into a suitable logarithm for the target scale */
template<typename TAR, typename VAL>
inline auto
asLogarithm (Metric targetMetric, VAL const& value)
{
if (isNeg (value))
throw err::Invalid{_Fmt{"Attempt to take logarithm from %f < 0"}
% value};
using WorkType = CommonComputeType<VAL,TAR>;
switch (targetMetric)
{
case NAT: return std::log (WorkType(value));
case BIN: return std::log2 (WorkType(value));
case DEC: return std::log10 (WorkType(value)) * 10;
default:
throw err::Logic{_Fmt{"Attempt to compute logarithm "
"for a non-logarithmic metric %d"}
% targetMetric};
break;
}
}
/** provide the adaptation factor from this logarithmic scale
* to the given other logarithmic scale and target type \a VAL
* @remarks
* - use the logarithm conversion formula logb(x) ≡ logb(k)·logk(x)
* - if target is in decibel, have to apply factor 10; asLogarithm() does that already
* - if we are in decibel, log10 = dB /10; fold that factor in the logarithm of the base,
* by using log(b)·x ≡ log(b^x) and rewrite that as b^x = exp(ln(b)·x)
*/
template<typename TAR, typename VAL>
inline auto
baseScale (Metric targetMetric, Metric srcMetric)
{
using WorkType = CommonComputeType<VAL,TAR>;
if (srcMetric == targetMetric)
return WorkType(1);
auto srcBase = [&]{switch (srcMetric)
{
case BIN: return WorkType(2);
case NAT: return std::exp (WorkType(1));
case DEC: return std::exp (std::log (WorkType(10)) / 10);
default:
throw err::Logic{"Attempt to treat non-logarithmic scales as logarithms"};
break;
}};
return asLogarithm<WorkType> (targetMetric, srcBase());
}
}//(End) log helpers
/**
* Join and combine an additional feed value with an anchor value.
* @note actual joining operation is picked based on both scales and types involved.
* @remark notably multiplicative contributions and logarithmic scales to be considered.
*/
template<typename VAL>
template<typename SRC>
inline VAL
Scale<VAL>::join (VAL const& value, SRC const& srcFeed, Scale<SRC> const& feedScale) const
{
VAL res{};
using JoinT = NumberJoinType<VAL,SRC>;
if (NOM == metric)
UNIMPLEMENTED ("nominal and ordinal scales");
if (LIN == metric)
{
if (feedScale.isLogarithmic())
assignConverted (res, value * expFactor<VAL> (feedScale.metric, srcFeed));
else
if (feedScale.isFactor())
assignConverted (res, JoinT(value) * JoinT(srcFeed));
else
assignConverted (res, JoinT(value) + JoinT(srcFeed));
}
else
if (this->isLogarithmic())
{
if (feedScale.isLogarithmic())
assignConverted (res, value + baseScale<VAL,SRC> (metric,feedScale.metric) * srcFeed);
else
if (feedScale.isFactor())
assignConverted (res, value + asLogarithm<VAL> (metric, srcFeed));
else
throw err::Invalid {"adding a non-logarithmic data feed on top of a logarithmic base value is pointless."};
}
else
NOTREACHED ("Unexpected metric conversion case");
return conform (res);
} // last step: re-establish target scale rules
/**
* Simplified join() variant, assuming the feed is based on the same scale.
*/
template<typename VAL>
inline VAL
Scale<VAL>::join (VAL const& value, VAL const& feed) const
{
return conform (isFactor()? value * feed
: value + feed
);
}
}} // namespace lib::par
#endif /*LIB_PAR_SCALE_H*/

95
src/lib/par/spec.hpp Normal file
View file

@ -0,0 +1,95 @@
/*
SPEC.hpp - parameter trait specification
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 spec.hpp
** Common definition and specification record for parameters.
** While most functionality of parameters is indirected in some way, allowing
** for future extension, some basic traits are marked as flag, so that frequently
** used code-paths might take a shortcut. Notably the disposition interface, which
** describes the full implementation service of a parameter container, explicitly
** reserves one «slot» of storage for flags, type markers and an ID record.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_SPEC_H
#define LIB_PAR_SPEC_H
#include "lib/symbol.hpp"
#include "lib/meta/typelist.hpp"
#include "lib/meta/typelist-util.hpp"
namespace lib {
namespace par {
/**
* Fixed collection of elementary types supported in parameters
*/
using BaseTypes = meta::Types<int
,int64_t
,uint
,uint64_t
,double
,float
,bool
>;
static constexpr uint BASETYPE_CNT = meta::count<BaseTypes::List>();
static constexpr uint BASETYPE_MAX_SIZ = meta::maxSize<BaseTypes::List>();
template<typename T>
static constexpr bool isBaseType = meta::isInList<T, BaseTypes::List>::value;
/** @note Only some fixed base data types are supported as Parameter value type. */
template<typename T>
concept par_basetype = isBaseType<T>;
template<typename T>
using BaseTypeID = meta::TypeIdxID<T, BaseTypes::List>;
/* ===== setup of ID space ===== */
namespace {
auto _bit_fit = [](uint bits){ return (1u << bits) - 1; };
}
static constexpr uint _BASE_TYPE_BITS = 4;
static constexpr uint _SCALES_BITS = 10;
static constexpr uint _PARAM_ID_BITS = 18;
static constexpr uint MAX_BASETYPES = _bit_fit(_BASE_TYPE_BITS); ///< max number of parameter base types that can be configured
static constexpr uint MAX_SCALES = _bit_fit(_SCALES_BITS); ///< max number of distinct scales that can be defined at runtime
static constexpr uint MAX_PARAM_ID = _bit_fit(_PARAM_ID_BITS); ///< max number of distinct param identities that can be created
static_assert (BASETYPE_CNT <= MAX_BASETYPES);
struct IDRecord
{
uint baseTypeID :_BASE_TYPE_BITS = 0;
uint scaleID :_SCALES_BITS = 0;
uint paramIDID :_PARAM_ID_BITS = 0;
};
}} // namespace lib::par
#endif /*LIB_PAR_SPEC_H*/

View file

@ -0,0 +1,226 @@
/*
TYPE-HANDLER.hpp - generic type conversion handling support
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 type-handler.hpp
** Generic building blocks to support parameter value conversions and adaptation.
** The implementation of parameters is built such as to conceal the actual type of
** the parameter values -- which requires a virtual dispatch at some point. Yet the
** underlying type conversion functionality can be defined in a generic way, based
** on the actual types involved. Simple value-like parameters are based on a fixed
** collection of number types, both integral and floating-point. Thus the actual
** conversion is handled by the promotion- and coercion rules of C++ in most cases.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_TYPE_HANDLER_H
#define LIB_PAR_TYPE_HANDLER_H
#include "lib/par/spec.hpp"
#include "lib/meta/trait.hpp"
#include "lib/util.hpp"
#include <concepts>
#include <limits>
namespace lib {
namespace par {
using util::isNeg;
using std::same_as;
using std::integral;
using std::signed_integral;
using std::floating_point;
using std::numeric_limits;
using std::common_type_t;
using std::is_signed_v;
using std::is_unsigned_v;
using std::is_integral_v;
using std::is_floating_point_v;
/* ===== type conversion implementation details ===== */
/** Constraint: type \a SUB can not represent the full domain of type \a BAS
* @todo implicitly also implements a limitation to »numeric« types,
* due to the use of std::numeric_limits
*/
template<typename SUB, typename BAS>
concept sub_domain = signed_integral<SUB> != signed_integral<BAS>
or numeric_limits<SUB>::max() < numeric_limits<BAS>::max()
or numeric_limits<SUB>::lowest() > numeric_limits<BAS>::lowest()
;
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wsign-compare\"")
// the following functions are specifically crafted to work around
// the ambiguities caused by automatic signed-to-unsigned promotion;
// furthermore, the automatic promotion allows to perform the comparison.
template<typename X, typename Y>
inline constexpr bool
isLower_safe (X const& x, Y const& y)
{
return isNeg(x) == isNeg(y)? x < y
: isNeg(x);
}
template<typename X, typename Y>
inline constexpr bool
isLarger_safe (X const& x, Y const& y)
{
return isNeg(x) == isNeg(y)? x > y
: isNeg(y);
}
_Pragma("GCC diagnostic pop")
/** generic helper to conform into a common value domain
* @tparam SUB the implied target value domain
* @tparam BAS source data type
* @return a value in source data type,
* yet conformed into the target data range
*/
template<typename SUB, typename BAS>
inline constexpr BAS
preClamp (BAS const& value)
{
return value;
}
/** special case: clamp the source value into the target value domain.
* @remark tricky due to automatic conversions between signed/unsigned */
template<typename SUB, typename BAS> requires (sub_domain<SUB,BAS>
and not same_as<SUB,bool>)
inline constexpr BAS
preClamp (BAS const& rawVal)
{
static_assert (std::is_constructible_v<BAS,SUB const&>
,"Value domains must overlap at least partially "
"and values must be cross-constructible within overlap");
auto upperBound = numeric_limits<SUB>::max();
auto lowerBound = numeric_limits<SUB>::lowest();
return isLarger_safe(rawVal, upperBound)? BAS(upperBound)
: isLower_safe (rawVal, lowerBound)? BAS(lowerBound)
: rawVal;
}
/** special case: trigger a bool at the 0.5 level */
template<same_as<bool> B, floating_point F>
inline constexpr F
preClamp (F const& rawVal)
{
return 0.5 < rawVal? F{1} : F{0};
}
template<typename V, typename X>
inline void
assignConverted (V& targetVal, X const& srcVal)
{
if constexpr (std::is_assignable_v<V&, X const&>)
{
targetVal = preClamp<V> (srcVal);
}
else
if constexpr (std::is_constructible_v<V, X const&>)
{
targetVal.~V();
new(&targetVal) V (preClamp<V> (srcVal));
}
else
static_assert (!sizeof(X), "this type conversion is not supported");
}
template<typename T>
concept number = integral<T> or floating_point<T>;
namespace {
static_assert (64 <= std::numeric_limits<lflp>::digits
,"long double type can not represent uint64_t exactly");
/** @internal decision logic for combining numeric parameters */
template<typename V, typename W>
auto
selectNumberJoinType()
{
if constexpr (is_floating_point_v<V> or is_floating_point_v<W>)
return std::type_identity<common_type_t<V,W>>{};
else
if constexpr (is_signed_v<V> == is_signed_v<W>)
return std::type_identity<common_type_t<V,W>>{};
else
{ // mixed signed/unsigned integral: need a type wide enough to accommodate.
using U = std::conditional_t<is_signed_v<V>, W,V>;
if constexpr (8 > sizeof(U))
return std::type_identity<int64_t>{};
else
return std::type_identity<lflp>{};
// no standard integral type can hold uint64_t's full range signed.
// Expand to long double, which can hold 64bit without precision loss
}
}
/** @internal logic to pick a suitable floating-point for intermediary computations */
template<typename V, typename W>
auto
selectCommonComputeType()
{
if constexpr (is_integral_v<V> and is_integral_v<W>)
{ // ensure integral precision is retained to the degree possible...
if constexpr (8 > sizeof(V) and 8 > sizeof(W))
return std::type_identity<double>{};
else
return std::type_identity<lflp>{};
}
else
return std::type_identity<common_type_t<V,W>>{};
}
}
/**
* @return A common type to perform simple numeric operations safely.
* - when one of the type parameters is floating-point, computations switch over there
* - on integral types with same signedness, std::common_type handles a sane common ground
* - when mixed computations can be managed within signed 64bit, switch up to that
* - in the remaining problematic case(s), long double is used, as it can accommodate
* the full 64bit value range without loss on platforms relevant for Lumiera.
*/
template<number V, number W>
using NumberJoinType = typename decltype(selectNumberJoinType<V,W>())::type;
/**
* best-precision floating point type to carry out
* a numeric operation to transform and adapt value scales.
* @note the result type is always some floating point,
* since the goal is to use the exponential / logarithm family.
*/
template<number V, number W>
using CommonComputeType = typename decltype(selectCommonComputeType<V,W>())::type;
}} // namespace lib::par
#endif /*LIB_PAR_TYPE_HANDLER_H*/

View file

@ -0,0 +1,101 @@
/*
TYPE-REGISTRY.hpp - generic registry for type or policy descriptors
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 type-registry.hpp
** Generic registration table and access mechanism to type descriptors keyed by ID.
** The implementation of parameters is based on layered virtual interfaces and implementations,
** packaged into a fixed storage buffer that is transported with value semantics. This overall shape
** implies a strict storage size regime and thus forces to _fold away_ the more flexible parts of
** the behaviour variations into a second layer, retaining only an ID in the core implementation.
**
** The type registry for parameters provides the generic mechanism underlying this setup scheme:
** - it is parametrised by an implementation type (of the descriptor) with a fixed size
** - the storage is organised by a fixed-sized index table and heap allocated storage extents.
** - entries are never discarded, and thus an atomic lock suffices _for read access_
** @remark while this scheme is defined in a generic way, it aims at one specific
** usage pattern and should not be stretched beyond that.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
#ifndef LIB_PAR_TYPE_REGISTRY_H
#define LIB_PAR_TYPE_REGISTRY_H
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
//#include "lib/meta/trait.hpp"
//#include "lib/util.hpp"
//#include <concepts>
//#include <limits>
namespace lib {
namespace par {
/**
* Management of descriptor entries keyed by numeric ID.
* Based on a fixed index table and extents added dynamically.
* Slots can be claimed, creating an entry with stable address.
* Creation and access are thread safe, Entries are never discarded.
*/
template<class ELM>
class TypeRegistry
: util::NonCopyable
{
public:
class ID;
template<typename...INIT>
ID makeSlot (INIT&&...);
ELM& operator[] (ID) const;
};
/** type tagged numeric registry entry ID */
template<class ELM>
class TypeRegistry<ELM>::ID
{
uint idx_;
public:
ID (uint ix =0) : idx_{ix} { }
operator uint() const { return idx_; }
// default copy operations
};
template<class ELM>
template<typename...INIT>
inline TypeRegistry<ELM>::ID
TypeRegistry<ELM>::makeSlot (INIT&&... initArg)
{
UNIMPLEMENTED ("allocate new slot, possibly grow extents, under mutex lock");
}
template<class ELM>
inline ELM&
TypeRegistry<ELM>::operator[] (ID slot) const
{
UNIMPLEMENTED ("access slot by ID, protected by atomic acquire");
}
}} // namespace lib::par
#endif /*LIB_PAR_TYPE_REGISTRY_H*/

View file

@ -516,7 +516,7 @@ namespace lib {
static PolymorphicValue
build (ARGS&&... args)
{
Adapter<IMP>* type_to_build_in_buffer(0);
IMP* type_to_build_in_buffer{};
return PolymorphicValue (type_to_build_in_buffer, std::forward<ARGS>(args)...);
}

View file

@ -122,12 +122,12 @@ namespace util {
u = 0!=u? u:1;
auto [d,r] = util::iDiv (num, den);
// round to smallest integer fraction, to shake off "number dust"
f128 const ROUND_ULP = 1 + 1/(f128(std::numeric_limits<int64_t>::max()) * 2);
lflp const ROUND_ULP = 1 + 1/(lflp(std::numeric_limits<int64_t>::max()) * 2);
// construct approximation quantised to 1/u
f128 frac = f128(r) / den;
lflp frac = lflp(r) / den;
int64_t res = d*u + int64_t(frac*u * ROUND_ULP);
ENSURE (abs (f128(res)/u - rational_cast<f128>(Rat{num,den})) <= 1.0/abs(u)
ENSURE (abs (lflp(res)/u - rational_cast<lflp>(Rat{num,den})) <= 1.0/abs(u)
,"Requantisation error exceeded num=%li / den=%li -> res=%li / quant=%li"
, num, den, res, u);
return res;

View file

@ -17,7 +17,19 @@
** - average over the N last elements in a data sequence
** - simple linear regression with weights (single predictor variable)
** - also over a time series with zero-based indices
**
**
** @remark this header was imported from the [yoshimi-test] acceptance suite, where it
** is maintained as `src/util/statistic.hpp` within namespace `util` -- so the
** two copies are kept deliberately similar, to allow carrying fixes either way.
** Note in particular that a helper `round<places>(double)` for decimal rounding
** was **removed here** (10/2026): it was never instantiated within Lumiera, while
** its `constexpr double shift{pow(10.0,places)}` is not a constant expression by
** the standard (while GCC accepts it, relying on a _builtin_). It remains in use
** upstream, in `suite/step/TimingObservation.cpp`. Should decimal rounding be
** wanted here, settle it together with the other half-solutions collected in #1360
** -- and note util::ipow() now provides the constexpr exponentiation.
**
** [yoshimi-test]: https://github.com/Ichthyostega/yoshimi-test/
*/
@ -70,17 +82,6 @@ namespace stat{
return std::apply (makeArray, forward<TUP> (tuple));
}
template<size_t places>
inline double
round (double val)
{
constexpr double shift{pow(10.0, places)};
return std::round(val*shift) / shift;
}
/**
* Read-only view into a segment within a sequence of data
* @tparam D value type of the data series

View file

@ -542,8 +542,8 @@ namespace lib {
addCode(COND);
break;
case TagSyntax::END_IF:
addLead();
__checkBalanced(IF);
addLead();
if (hasElse())
linkJumpToNext();
else
@ -557,8 +557,8 @@ namespace lib {
addCode(ITER);
break;
case TagSyntax::END_FOR:
addLead();
__checkBalanced(FOR);
addLead();
if (hasElse())
linkJumpToNext();
else
@ -570,9 +570,9 @@ namespace lib {
closeScope();
break;
case TagSyntax::ELSE:
addLead();
__checkInScope();
__checkNoDup();
addLead();
if (IF == scope_.top().clause)
{
markJumpInScope();

View file

@ -22,12 +22,15 @@
#include "lib/integral.hpp"
#include <type_traits>
#include <concepts>
#include <cstdlib>
#include <climits>
#include <cfloat>
#include <limits>
#include <cmath>
#include <nobug.h>
namespace util {
@ -136,7 +139,7 @@ namespace util {
}
/** quantise towards ceiling.
/** quantise towards ceiling.
* This function models to fit something into a tiled structure.
*/
template<typename U> requires std::unsigned_integral<U>
@ -154,6 +157,104 @@ namespace util {
/**
* Transform into a cyclically wrapping domain.
* @tparam N any integral type, signed or unsigned
* @return mapping into the range [minVal...maxVal(
* @remark This function exploits the equivalence between signed and corresponding unsigned type
* to perform the modulus computation with positive numbers, because the modulus operator of C++
* works flipped for negative numbers, and oriented towards zero, while we want here always
* the same cyclic wrap-around, irrespective of the position relative to zero.
* - mathematically we want: `minVal + (rawVal-minVal) modulus PERIOD`
* - the unsigned representation of a negative number wraps cleanly, and thus maxVal - minVal,
* when converted into unsigned, is off by multiples of the whole number domain length, which
* is again absorbed by wrapping. Thus surprisingly `PERIOD = U(U(maxVal) - U(minVal))` is exact
* and can always be represented in the unsigned type.
* - for rawVal < minVal a flipped definition is used that can also be represented as unsigned:
* ** define dist ≔ minVal - rawval, then dist ≥ 1 for rawVal < minVal
* ** mathematically we need offset ≡ (-dist) modulus PERIOD
* ** we could compute PERIOD - (dist % PERIOD) -- but that would flip for dist ≡ PERIOD
* ** thus both arguments are shifted by -1, which keeps them in-range and computable as unsigned
* @note the repeated casts `U(signed)` prevent C++ integer promotion to kick in (esp. by short and char)
* @remark Claude was used (9/26) to improve and then formally verify this solution.
*/
template<std::integral N>
inline constexpr N
cyclicWrap (N rawVal, N minVal, N maxVal)
{
REQUIRE (minVal < maxVal);
using U = std::make_unsigned_t<N>;
const U PERIOD = U(U(maxVal) - U(minVal)); // always representable
U offset;
if (rawVal >= minVal)
offset = U(U(rawVal) - U(minVal)) % PERIOD;
else
{
const U dist = U(U(minVal) - U(rawVal)); // dist ≥ 1
offset = PERIOD-1 - (dist-1) % PERIOD; // ≡ (-dist) modulus PERIOD
}
return N(U(U(minVal) + offset));
}
/**
* Transform into a cyclically wrapping domain: floating point implementation.
* @return mapping into the range [minVal...maxVal(
* @warning this implementation can not cover extreme numeric values beyond #limit_cyclicWrap()
* @remark within the usable numeric range, the flipped modulus for values below minVal can simply
* be fixed by shifting once by PERIOD. But there is a caveat: extremely small negative offsets
* could be absorbed, so that the result lands on +PERIOD
*/
template<std::floating_point F>
inline constexpr F
cyclicWrap (F rawVal, F minVal, F maxVal)
{
REQUIRE (minVal < maxVal);
const F PERIOD = maxVal - minVal;
F offset = std::fmod (rawVal - minVal, PERIOD);
if (offset < 0)
offset += PERIOD;
if (not (offset < PERIOD)) // can happen due to ε or non-finite values
offset = 0;
F res = minVal + offset; // could be rounded to maxVal (or become infinite)
return res < maxVal? res : minVal;
}
/**
* Validation helper for the floating-point implementation of #cyclicWrap().
* Values beyond the computed limit can not be handled by the implementation,
* due to the limited internal resolution of floating-point computations.
* @param period length of the cyclic wrap period (≔ maxVal - minVal)
* @return a limit for each parameter values, taken absolutely
* @remark at the core of the #cyclicWrap() computation sits a numeric difference,
* taken modulo the period length. Each floating point number has a minimum resolution ("ULP").
* When the ULP of the difference computation surpasses the periodic cycle length, the cyclicWrap
* becomes meaningless (albeit computed correct without glitch). And since that offset is then
* added on top of meanValue, the same argument holds, so `std::abs (minVal) <= limit` is required,
* and by transitive argument also for `maxVal` and `rawVal`. Furthermore, the difference and the
* period length must also be finite numbers by themselves.
* @note for performance reasons, that limit should be computed once, checked against `minVal` and `maxVal`,
* and then before each computation of #cyclicWrap(), `std::abs (rawVal) <= limit` should be checked.
* It is recommended to fail with an hard error, since resolving theses settings typically requires
* an intervention by the user (parameters need to be adjusted).
* @remark Claude was used (9/26) to analyse the computation and derive a boundary criterion.
*/
template<std::floating_point F>
inline constexpr F
limit_cyclicWrap (F period)
{
REQUIRE (std::isnormal (period));
REQUIRE (F(0) < period);
F limit = period / std::numeric_limits<F>::epsilon();
if (not std::isfinite (limit))
limit = std::numeric_limits<F>::max();
return limit / 2; // exact
}
/**
* epsilon comparison of doubles.
* @remarks Floating point calculations are only accurate up to a certain degree,
@ -170,12 +271,13 @@ namespace util {
* @see https://en.wikipedia.org/wiki/Unit_in_the_last_place
* @todo 3/2024 seems we have solved this problem several times meanwhile /////////////////////////////////TICKET #1360 sort out floating-point rounding and precision
*/
template<std::floating_point F>
inline constexpr bool
almostEqual (double d1, double d2, unsigned int ulp =2) noexcept
almostEqual (F d1, F d2, uint ulp =2) noexcept
{
using std::fabs;
return fabs (d1-d2) < DBL_EPSILON * fabs (d1+d2) * ulp
or fabs (d1-d2) < DBL_MIN; // special treatment for subnormal results
return fabs (d1-d2) < std::numeric_limits<F>::epsilon() * fabs (d1+d2) * ulp
or fabs (d1-d2) < std::numeric_limits<F>::min(); // special treatment for subnormal results
}
@ -224,6 +326,47 @@ namespace util {
return logB;
}
static_assert (-1 == ilog2(-5));
static_assert ( 0 == ilog2( 1));
static_assert ( 5 == ilog2(42));
/**
* Exponentiation with integral exponent (counterpart to #ilog2).
* @param base the value to be raised
* @param exponent integral exponent; a negative exponent yields the reciprocal
* @remark `std::pow` can **not** be used in a constant expression: the standard does
* not mark it `constexpr` and only GCC accepts it there, through a _builtin_.
* Besides, for a small integral exponent, repeated multiplication is exact
* whenever base and result are representable -- unlike the generic
* implementation by `exp(ln(base)·exponent)`.
* @note the implementation uses [square-and-multiply], so it performs `ilog2(exponent)` squarings;
* the reciprocal is formed only once, on the final result, and is thus correctly rounded
* @see ilog2()
* [square-and-multiply]: https://en.wikipedia.org/wiki/Exponentiation_by_squaring
*/
template<std::floating_point F>
inline constexpr F
ipow (F base, int exponent) noexcept
{
bool reciprocal = exponent < 0;
uint64_t exp = reciprocal? -int64_t(exponent) // (widen first, to cover INT_MIN)
: int64_t(exponent);
F res{1};
for ( ; exp; exp >>= 1, base *= base)
if (exp & 1)
res *= base;
return reciprocal? F(1)/res
: res;
}
static_assert (1.0 == ipow (10.0, 0));
static_assert (1e3 == ipow (10.0, 3));
static_assert (1e-3 == ipow (10.0,-3)); // ◁───── exactly the nearest double to 0.001
static_assert (0.25 == ipow ( 2.0,-2));
static_assert (3125 == ipow ( 5.0, 5));
} // namespace util
#endif /*UTIL_QUANT_H*/

View file

@ -60,7 +60,9 @@ namespace util {
inline constexpr int
sgn (NUM n)
{
return (n==0)? 0 :((n<0)? -1:+1 );
return n== NUM{0}? 0
: n < NUM{0}? -1
: +1;
}
template <class N1, class N2>
@ -77,12 +79,20 @@ namespace util {
return n1 < n2? N1(n2) : n1;
}
/** @remark safe to use also on unsigned */
template<typename NUM>
inline constexpr bool
isNeg (NUM const& num)
{
return num < NUM{0};
};
/** cut a numeric value to be >=0 */
template <typename NUM>
inline constexpr NUM
noneg (NUM val)
{
return (0<val? val : 0);
return max (val, NUM(0));
}
/** force a numeric to be within bounds, inclusively */

View file

@ -668,8 +668,8 @@ namespace model {
n2 = d2==u? n2 : reQuant (n2,d2, u);
FSecs res{s1*n1 + s2*n2, u};
auto f128 = [](Rat n){ return rational_cast<long double>(n); }; // can't use the guess from above,
ENSURE (abs (f128(res) - (f128(t1)+f128(t2))) < 1.0/u); // double precision is not sufficient
auto lflp = [](Rat n){ return rational_cast<long double>(n); }; // can't use the guess from above,
ENSURE (abs (lflp(res) - (lflp(t1)+lflp(t2))) < 1.0/u); // double precision is not sufficient
return detox (res);
}
}

View file

@ -175,13 +175,13 @@ namespace control {
Symbol instanceID{prototypeID, invocationID};
Command& instance = table_[instanceID];
if (instance)
throw new error::Logic (_Fmt{"Attempt to create a new Command instance '%s', "
"while an instance for this invocationID %s "
"is currently open for parametrisation and "
"not yet dispatched for execution."}
% instanceID % invocationID
, LERR_(DUPLICATE_COMMAND)
);
throw error::Logic (_Fmt{"Attempt to create a new Command instance '%s', "
"while an instance for this invocationID %s "
"is currently open for parametrisation and "
"not yet dispatched for execution."}
% instanceID % invocationID
, LERR_(DUPLICATE_COMMAND)
);
// create new clone from the prototype
table_[instanceID] = move (Command::get(prototypeID).newInstance());
ENSURE (instance, "cloning of command prototype failed");

View file

@ -63,9 +63,7 @@ namespace engine {
static lib::Depend<EngineCtx> access;
private:
EngineCtx();
friend class lib::DependencyFactory<EngineCtx>;
};

View file

@ -117,14 +117,6 @@ namespace engine {
}
namespace { ///////////////////////////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
inline bool
operator== (InvocationInstanceID const& l, InvocationInstanceID const& r)
{
return lumiera_invokey_eq (unConst(&l), unConst(&r));
}
}
/**
* Helper for tests: verify the given invocation parameters match this JobTicket.
*/

View file

@ -138,8 +138,8 @@ using lib::LUID;
{
if (empty()) return false;
InvocationInstanceID empty; /////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
return not lumiera_invokey_eq (&util::unConst(provision_).invocationSeed, &empty)
InvocationInstanceID empty; //////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
return provision_.invocationSeed != empty
and provision_.exitNode.isValid()
and util::and_all (provision_.prerequisites
,[](auto& pq){ return pq.prereqTicket.isValid(); });

View file

@ -152,7 +152,7 @@ namespace engine {
/**
* Standard implementation for a _Weaving Pattern_ to connect
* the input and output data feeds (buffers) into a processing function.
* @tparam INVO a configuration / policy base class to _adapt for invocation_
* @tparam INVO a configuration class / prototype to _adapt for invocation_
* @note assumptions made regarding the overall structure
* - `INVO::Feed` defines an _invocation adapter_ for the processing function
* - `INVO::buildFeed()` is a functor to (repeatedly) build `Feed` instances

View file

@ -58,7 +58,7 @@
** specialised parametrisation for some media handling library, and a lot of cross-linking pointers,
** it is important to care for efficient usage of memory with good locality. Furthermore, the higher
** levels of the build process will generate additional temporary data structures, refined gradually
** until the actual render node network can be emitted. Each builder level can thus be outfitted
** until the actual render node network can be emitted. Each builder level can thus be outfitted
** with a custom allocator — typically an instance of lib::AllocationCluster. Notably the higher
** levels can be attached to a separate AllocationCluster instance, which will be discarded once
** the build process is complete, while Level-2 (and below) uses the allocator for the actual
@ -155,16 +155,6 @@ namespace engine {
/**
* A builder to collect working data.
* Implemented through a suitable configuration of lib::SeveralBuilder,
* with a policy configuration parameter to define the allocator to use.
*/
template<class POL, class I, class E=I>
using DataBuilder = lib::SeveralBuilder<I,E, POL::template Policy>;
template<class POL, class DAT>
class NodeBuilder;
@ -257,7 +247,7 @@ namespace engine {
Connectivity
build()
{
PortData ports;
PortData ports{leads_.policyConnect()}; // use same allocator...
patternData_.collectEntries(ports);
return Connectivity{ports.build()
,leads_.build()
@ -506,8 +496,8 @@ namespace engine {
closeParamFront (PAR v1, PARS ...vs)
{
return adaptParam(
WAB::ParamClosure::template closeFront (forward<PAR> (v1)
,forward<PARS>(vs)...));
WAB::ParamClosure::closeFront (forward<PAR> (v1)
,forward<PARS>(vs)...));
}
/** immediately close the rightmost parameter positions,
@ -517,8 +507,8 @@ namespace engine {
closeParamBack (PAR v1, PARS ...vs)
{
return adaptParam(
WAB::ParamClosure::template closeBack (forward<PAR> (v1)
,forward<PARS>(vs)...));
WAB::ParamClosure::closeBack (forward<PAR> (v1)
,forward<PARS>(vs)...));
}
/** immediately close a single parameter at designated position

View file

@ -68,7 +68,7 @@ namespace engine{
BufferProvider&
EngineFacilities::setupCacheProvider() ////////////////////////////////////////////////////////////////TICKET #1223 : in what ways does client code need to interact with the Frame Cache / Management?
{
return getNaiveBufferManager();
return setupBufferProvider();
}

View file

@ -0,0 +1,64 @@
/*
RENDER-OUTPUT-CTX.hpp - output control parameters for rendering
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 render-output-ctx.hpp
** A configuration context with output control parameters for render processing.
** This context parameter store is associated with the [calculation streams](\ref CalcStream)
** and accessed from the [render node invocation](\ref RenderInvocation) to retrieve additional
** and context dependent settings for aspects beyond the automated parameters and the actual
** render function. Notably the output DataSink is abstracted and the corresponding target
** data buffer is exposed through this context.
** @todo »Playback Vertical Slice« -- effort towards first integration of render process ///////////////////TICKET #1221
** @todo 6/2026 still not entirely decided regarding the organisation of Render Job parameters;
** introducing such a context seems a good way to retain some limited flexibility while
** keeping matters focused.
*/
#ifndef STEAM_ENGINE_RENDER_OUTPUT_CTX_H
#define STEAM_ENGINE_RENDER_OUTPUT_CTX_H
#include "steam/common.hpp"
#include "vault/mem/buffhandle.hpp"
#include "vault/out/output-slot.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/nocopy.hpp"
namespace steam {
namespace engine{
using lib::time::Time;
using vault::mem::BuffHandle;
/**
* Render output control parameters and configuration details.
*/
class RenderOutputCtx
: util::MoveOnly
{
using Output = vault::out::OutputSlot::DataSink;
Output sink_;
public:
BuffHandle getOutputBuffer (Time absoluteNominalTime);
};
}} // namespace steam::engine
#endif /*STEAM_ENGINE_RENDER_OUTPUT_CTX_H*/

View file

@ -163,6 +163,17 @@ namespace engine {
/**
* A builder to collect working data.
* Implemented through a suitable configuration of lib::SeveralBuilder,
* with a policy configuration parameter to define the allocator to use.
* @todo 10/2026 extract the builder-DSL definitions? `DataBuilder` and `SizMark`
* are vocabulary of the node-builder language and are used from
* 'node-builder.hpp' as well -- they reside here merely because this is
* the upstream header in the include order, not because the concepts
* belong to the weaving pattern. The same holds for `UseHeapAlloc`,
* which sits in an anonymous namespace over there.
*/
template<class POL, class I, class E=I>
using DataBuilder = lib::SeveralBuilder<I,E, POL::template Policy>;

View file

@ -50,6 +50,12 @@ namespace fixture {
* must remain fixed in memory as long as any derived render jobs are alive.
*
* @todo WIP-WIP as of 4/2023 -- starting with a fake implementation /////////////////////TICKET #1306 : create an actual link to the low-level Model
* @deprecated 2025 now it looks like this entity was just a step stone towards the actual design;
* during the first prototypical implementation it became clear that the concept of an
* »ExitNode« needs to be repurposed (and shifted by one level in the architecture).
* However -- it is not clear yet in which way to address/designate the actual node
* that shall be pulled from a given render job; there might be some mapping based
* on a _feed ID_ (or a process-ID).
*/
class NodeGraphAttachment
{

View file

@ -16,6 +16,8 @@
** Core abstraction: automation parameter interpolator.
** Each interpolator implementation has the ability to resolve intermediary
** values and to provide a parameter value for _every arbitrary point in time_.
** @deprecated 2026 the basic idea is valid — yet the final design will certainly be different;
** the only usage of this code is Fixture::getAutomation() ⟼ `Auto<double>*`
*/

View file

@ -16,6 +16,8 @@
** Implementation functions of the Parameter abstraction.
**
** @todo as of 2016, we still need to work out our parameter/automation concept.
** @deprecated 2026 the basic idea is valid — yet the final design will certainly be different;
** the only usage of this code is Fixture::getAutomation() ⟼ `Auto<double>*`
*/

View file

@ -16,6 +16,8 @@
** Core abstraction: parameter to be controlled and possibly automated.
**
** @todo as of 2016, we still need to work out our parameter/automation concept.
** @deprecated 2026 the basic idea is valid — yet the final design will certainly be different;
** the only usage of this code is Fixture::getAutomation() ⟼ `Auto<double>*`
*/

View file

@ -16,6 +16,8 @@
** Implementation functions of the Parameter abstraction.
**
** @todo as of 2016, we still need to work out our parameter/automation concept.
** @deprecated 2026 the basic idea is valid — yet the final design will certainly be different;
** the only usage of this code is Fixture::getAutomation() ⟼ `Auto<double>*`
*/

View file

@ -16,6 +16,8 @@
** Core abstraction: an entity to expose a single parameter
**
** @todo as of 2016, we still need to work out our parameter/automation concept.
** @deprecated 2026 the basic idea is valid — yet the final design will certainly be different;
** the only usage of this code is Fixture::getAutomation() ⟼ `Auto<double>*`
*/

View file

@ -33,12 +33,14 @@
#include "lib/symbol.hpp"
#include "lib/meta/trait.hpp"
#include "lib/util-quant.hpp"
#include "lib/time/timevalue.hpp"
#include "test/transiently.hpp"
#include "lib/format-obj.hpp"
#include "lib/random.hpp"
#include <boost/lexical_cast.hpp>
#include <concepts>
#include <typeinfo>
#include <cstdlib>
#include <utility>
@ -61,14 +63,15 @@ namespace test{
using util::showType;
using util::showTypes;
using std::floating_point;
constexpr auto ROUGH_PRECISION = pow (10, -3);
constexpr auto ROUGH_PRECISION = util::ipow (10.0, -3);
constexpr auto EPSILON_ULP = 5;
template<typename F, typename N>
constexpr inline enable_if< std::is_floating_point<F>,
bool >
template<floating_point F, typename N>
constexpr inline
bool
roughEQ (F val, N target, F limit =ROUGH_PRECISION)
{
REQUIRE (0 < limit);
@ -76,9 +79,9 @@ namespace test{
} //////////////////////////////////////////////////////////////////////////TICKET #1360 looks like this problem was solved several times
template<typename F>
constexpr inline enable_if< std::is_floating_point<F>,
F >
template<floating_point F>
constexpr inline
F
ulp (F val)
{
val = fabs (val);
@ -90,9 +93,9 @@ namespace test{
return scaledUlp;
}
template<typename F, typename N>
constexpr inline enable_if< std::is_floating_point<F>,
bool >
template<floating_point F, typename N>
constexpr inline
bool
epsEQ (F val, N target, uint ulps =EPSILON_ULP)
{
return abs (val - target) < ulps * ulp<F> (target);

View file

@ -32,6 +32,7 @@
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

View file

@ -32,6 +32,19 @@
#include <typeinfo>
/** @deprecated 6/2026 very likely the InvocationInstanceID will be obsoleted,
* and instead just the two parameter values need to be compared.
* As an interim solution, comparing the two 64bit data words
*/
bool
operator== (InvocationInstanceID const& l, InvocationInstanceID const& r)
{
return l.code.w1 == r.code.w1
and l.code.w2 == r.code.w2;
}
namespace vault{
namespace gear {
@ -115,44 +128,4 @@ namespace gear {
return hash;
}
}} // namespace vault::gear
namespace {
using vault::gear::Job;
inline Job&
forwardInvocation (lumiera_jobDefinition& jobDef)
{
Job& job = static_cast<Job&> (jobDef);
return job;
}
}
extern "C" { /* ==== implementation C interface for job invocation ======= */
void
lumiera_job_invoke (LumieraJobDefinition jobDef)
{
REQUIRE (jobDef);
forwardInvocation(*jobDef).triggerJob();
}
size_t
lumiera_job_get_hash (LumieraJobDefinition jobDef)
{
REQUIRE (jobDef);
return hash_value (forwardInvocation (*jobDef));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
#include "lib/luid.h"
int
lumiera_invokey_eq (void* l, void* r)
{
return lumiera_uid_eq ((LumieraUid)l, (LumieraUid)r);
}
}/* extern "C" */

View file

@ -32,6 +32,13 @@
** the job over to the scheduler.
**
** @warning as of 4/2023 the Job datastructure will be remoulded ///////////////////////////////////////////TICKET #1280
** @deprecated in 2026 it does not seem like a good idea anymore to use "Render Jobs"
** to act like an _interface_ between Steam and Vault layer. Meanwhile, while
** the concept of **Jobs** (coroutines) is still upheld, the C datastructure job,
** as defined in this header, seems to mix up the traits of an interface and
** aspects of implementation. It is largely superseded by the _Activity Language_
** of the scheduler, and will likely be remoulded into a technical artifact of
** preparing job definitions in the Dispatcher.
**
** @see SchedulerFrontend
** @see JobTicket
@ -100,6 +107,11 @@ typedef struct lumiera_jobClosure* LumieraJobClosure;
* hash key for each invocation, which can be used for caching; obviously
* this hash need to be built from the JobTicket, based on ProcNode structure
* and the nominal Time. ///////////////////////////////////TICKET #1293
* @deprecated 6/2026 now able to conduct a full analysis of required information,
* and it seems the job definition does not need any kind of identity marker;
* caching will rely on chained specification hash keys rather.
* The existing tests will have to switch over using the two free parameter
* values, now present in each LumieraJobParameter...
*/
union InvocationInstanceID
{
@ -128,8 +140,9 @@ struct lumiera_jobParameter_struct
raw_time_64 nominalTime; /////////////////////////////////////////////////////////////////////////TICKET #1295 job invocation parameter: framework to interpret this time
InvocationInstanceID invoKey;
//////////////////////////////////////////////////////////////TODO: place an additional parameter value here, or make the instanceID globally unique?
////////////////////////////////////////////////////////////////////////////////////TICKET #1293 job invocation identity
////////////////////////////////////////////////////////////////////////////////////TICKET #1295 : rework Job parameters to accommodate input / output info required for rendering
////////////////////////////////////////////////////////////////////////////////////TICKET #1293 job invocation identity (possibly not needed anymore, can be derived from the data...)
size_t srcParam;
size_t sinkParam;
};
typedef struct lumiera_jobParameter_struct lumiera_jobParameter;
typedef lumiera_jobParameter* LumieraJobParameter;
@ -194,6 +207,9 @@ typedef lumiera_jobDescriptor* LumieraJobDescriptor;
#include <string>
/** @warning performs a comparison on the entire packed content */
bool operator== (InvocationInstanceID const&, InvocationInstanceID const&);
namespace vault{
namespace gear {
@ -284,6 +300,8 @@ namespace gear {
this->jobClosure = &specificJobDefinition;
this->parameter.nominalTime = _raw(nominalFrameTime);
this->parameter.invoKey = invoKey;
this->parameter.srcParam = 0;
this->parameter.sinkParam = 0;
}
// using standard copy operations
@ -326,28 +344,6 @@ namespace gear {
}} // namespace vault::gear
extern "C" {
#endif /* =========================== C Interface ===================== */
/** trigger execution of a specific job,
* assuming availability of all prerequisites */
void lumiera_job_invoke (LumieraJobDefinition);
/** calculate a hash value based on the Job's \em identity. */
size_t lumiera_job_get_hash (LumieraJobDefinition);
int lumiera_invokey_eq (void* l, void* r);
#ifdef __cplusplus
}
#endif
#endif /*VAULT_GEAR_JOB_H*/

View file

@ -167,7 +167,7 @@ namespace out {
* handle is retained.
* @tparam isTest use a simple BufferMetadata table in OutputBufferProxy.
*/
template<class CON, bool isTest=false>
template<class CON, bool isTest> // ◁───── default defined at declaration (output-slot.hpp)
class OutputSlot::AllocState
: public OutputSlot::Allocation
{

View file

@ -52,7 +52,7 @@
namespace lib {
namespace time{
class Quantiser;
typedef std::shared_ptr<const Quantiser> PQuant;
using PQuant = std::shared_ptr<const Quantiser>;
}}
namespace vault {

View file

@ -53,6 +53,11 @@ return: 0
END
TEST "Cyclic wrapping number domain" CyclicWrap_test <<END
return: 0
END
TEST "Deleter function collection" DelStash_test <<END
return: 0
END

32
tests/16parameter.tests Normal file
View file

@ -0,0 +1,32 @@
TESTING "Library Test Suite: parameters" ./test-suite --group=param
TEST "Parameter Values" Parameter_test <<END
return: 0
END
TEST "Parameter Domain Type" ParameterDomain_test << END
return: 0
END
PLANNED "Parameter Provision" ParameterProvision_test << END
return: 0
END
TEST "Parameter value scales" ParameterScale_test << END
return: 0
END
PLANNED "Enumeration domains" NomSet_test << END
return: 0
END
PLANNED "Parameter type registry" TypeRegistry_test << END
return: 0
END

View file

@ -26,7 +26,7 @@ TEST "Proc Node metadata key" NodeMeta_test <<END
END
PLANNED "Proc Node operation modes" NodeOpera_test <<END
TEST "Proc Node operation modes" NodeOpera_test <<END
END

View file

@ -0,0 +1,196 @@
/*
CyclicWrap(Test) - verify cyclically wrapping number domain
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 util-floordiv-test.cpp
** unit test \ref CyclicWrap_test
*/
#include "test/run.hpp"
#include "lib/util-quant.hpp"
#include "lib/util.hpp"
using util::isLimited;
using std::floating_point;
namespace util {
namespace test {
namespace{ // Test helpers...
template<typename NUM>
constexpr NUM _MAX = std::numeric_limits<NUM>::max();
template<typename NUM>
constexpr NUM _MIN = std::numeric_limits<NUM>::lowest();
template<floating_point FLO>
constexpr FLO _EPS = std::numeric_limits<FLO>::epsilon();
} // (End) Test helpers
/******************************************************************//**
* @test Demonstrate and verify a cyclic wrapping numeric domain.
* - show the wrapping behaviour with simple numbers
* - verify the behaviour pattern for all integer types (signed / unsigned)
* - verify the computation works at the very bounds of the domain
* - verify the most difficult path with a negative minVal,
* and the smallest number in the domain
* - show a similar simple wrapping behaviour with all float types
* - demonstrate the meaning of the precision limit
* - demonstrate aliasing and sanitised rounding errors
* @see util::cyclicWrap
* @see parameter-scale-test.cpp
*/
class CyclicWrap_test : public Test
{
virtual void
run (Arg)
{
simpleDemonstration();
verifyIntegerTypes<int>();
verifyIntegerTypes<uint>();
verifyIntegerTypes<int8_t>();
verifyIntegerTypes<uint8_t>();
verifyIntegerTypes<int16_t> ();
verifyIntegerTypes<uint16_t>();
verifyIntegerTypes<int32_t> ();
verifyIntegerTypes<uint32_t>();
verifyIntegerTypes<int64_t> ();
verifyIntegerTypes<uint64_t>();
verifyFloatTypes<float>();
verifyFloatTypes<double>();
verifyFloatTypes<lflp>();
}
void
simpleDemonstration ()
{
CHECK (10 == cyclicWrap (10,10,15));
CHECK (11 == cyclicWrap (11,10,15));
CHECK (12 == cyclicWrap (12,10,15));
CHECK (13 == cyclicWrap (13,10,15));
CHECK (14 == cyclicWrap (14,10,15));
CHECK (10 == cyclicWrap (15,10,15));
CHECK (11 == cyclicWrap (16,10,15));
CHECK (12 == cyclicWrap (17,10,15));
CHECK (13 == cyclicWrap (18,10,15));
CHECK (14 == cyclicWrap (19,10,15));
CHECK (10 == cyclicWrap (20,10,15));
CHECK (10 == cyclicWrap ( 0,10,15));
CHECK (14 == cyclicWrap (-1,10,15));
}
template<typename I>
void
verifyIntegerTypes ()
{
const I LOWER(-100); // note: expected to wrap for unsigned types
const I UPPER(LOWER+5);
auto cyc = [&](I val){ return cyclicWrap (val, LOWER,UPPER); };
CHECK (LOWER == cyc(LOWER+0) );
CHECK (LOWER+1 == cyc(LOWER+1) );
CHECK (LOWER+2 == cyc(LOWER+2) );
CHECK (LOWER+3 == cyc(LOWER+3) );
CHECK (LOWER+4 == cyc(LOWER+4) );
CHECK (LOWER == cyc(LOWER+5) );
CHECK (LOWER == cyc(LOWER-5) );
CHECK (LOWER+1 == cyc(LOWER-4) );
CHECK (LOWER+2 == cyc(LOWER-3) );
CHECK (LOWER+3 == cyc(LOWER-2) );
CHECK (LOWER+4 == cyc(LOWER-1) );
CHECK (isLimited (LOWER, cyc(_MAX<I>), UPPER));
CHECK (isLimited (LOWER, cyc(_MIN<I>), UPPER));
CHECK (isLimited (LOWER, cyc(_MAX<I>-1), UPPER));
CHECK (isLimited (LOWER, cyc(_MIN<I>+1), UPPER));
//------ Demonstrate behaviour at lower domain bound
//
I dist = LOWER - _MIN<I>; // guaranteed to be positive and representable in-domain
I step = (dist / 5) * 5; // the next applicable grid point
I excess = dist - step;
I offset = 5 - excess; // flip orientation (since the input val lies below LOWER)
CHECK (LOWER + offset == cyc(_MIN<I>) );
}
template<typename F>
void
verifyFloatTypes()
{
auto cyc = [&](F val){ return cyclicWrap (val, F(-10),F(-5)); };
CHECK (almostEqual (F(-10), cyc(0)) );
CHECK (almostEqual (F( -9), cyc(1)) );
CHECK (almostEqual (F( -8), cyc(2)) );
CHECK (almostEqual (F( -7), cyc(3)) );
CHECK (almostEqual (F( -6), cyc(4)) );
CHECK (almostEqual (F(-10), cyc(5)) );
CHECK (almostEqual (F( -9), cyc(6)) );
CHECK (almostEqual (F( -8), cyc(7)) );
CHECK (almostEqual (F( -7), cyc(8)) );
CHECK (almostEqual (F( -6), cyc(9)) );
CHECK (almostEqual (F(-10), cyc(105)));
// For each period-length, a precision limit can be computed;
// whenever bounds or inputs approach that limit, the rounding errors
// approach the order of magnitude of the period-length, and thus
// computing of wrapped values becomes pointless...
F period5 = F(5);
F limit5 = limit_cyclicWrap<F> (period5);
// one ULP-Step at the limit equals already half the period-length:
CHECK (almostEqual (period5/2, limit5 * _EPS<F>));
// so small steps will alias...
CHECK (limit5 + 1 == limit5);
// HOWEWER, very large periods can be handled (within numeric precision)
CHECK (1 == cyclicWrap<F>( 1, 1,_MAX<F>) );
CHECK (2 == cyclicWrap<F>( 2, 1,_MAX<F>) ); // note: these values are *exact* -- due to std::fmod()
CHECK (3 == cyclicWrap<F>( 3, 1,_MAX<F>) );
CHECK (4 == cyclicWrap<F>( 4, 1,_MAX<F>) );
CHECK (5 == cyclicWrap<F>( 5, 1,_MAX<F>) );
CHECK (1 == cyclicWrap<F>(-1, 1,_MAX<F>) ); // yet this one gets aliased with the minVal
F epsStep = _MAX<F>*_EPS<F>;
// yet the following input is clearly below minVal and will wrap upwards...
F wrapped = cyclicWrap (-epsStep, F(1),_MAX<F>);
CHECK ( 1 < wrapped);
CHECK (wrapped < _MAX<F>);
}
};
LAUNCHER (CyclicWrap_test, "unit common");
}} // namespace util::test

View file

@ -0,0 +1,87 @@
/*
NomSet(Test) - verify nominal and ordinal scale enumerations
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 nom-set-test.cpp
** unit test \ref NomSet_test
*/
#include "test/run.hpp"
//#include "test/test-helper.hpp"
#include "lib/par/nom-set.hpp"
#include "test/diagnostic-output.hpp"/////////////TODO
//#include <utility>
//#include <string>
//#include <vector>
//using std::string;
//using std::vector;
//using std::swap;
namespace lib {
namespace par {
namespace test{
// using lumiera::error::LUMIERA_ERROR_LOGIC;
namespace {//Test fixture....
}//(End)Test fixture
/********************************************************************************//**
* @test document the definition of domain enumerations for nominal / ordinal scales.
*/
class NomSet_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_typeMarker();
}
void
simpleUsage()
{
UNIMPLEMENTED ("nebbich");
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_typeMarker()
{
}
};
/** Register this test class... */
LAUNCHER (NomSet_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -0,0 +1,293 @@
/*
ParameterDomain(Test) - document the handling of parameter type domain
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 parameter-domain-test.cpp
** unit test \ref ParameterDomain_test
*/
#include "test/run.hpp"
//#include "test/test-helper.hpp"
#include "lib/par/domain.hpp"
#include "lib/par/param-type.hpp"
#include "test/diagnostic-output.hpp"////////////TODO
//#include <utility>
//#include <string>
//#include <vector>
#include <concepts>
//using std::string;
//using std::vector;
//using std::swap;
namespace lib {
namespace par {
namespace test{
// using lumiera::error::LUMIERA_ERROR_LOGIC;
using std::floating_point;
namespace {//Test helpers....
template<typename NUM>
constexpr NUM _MAX = std::numeric_limits<NUM>::max();
template<typename NUM>
constexpr NUM _MIN = std::numeric_limits<NUM>::lowest();
template<floating_point FLO>
constexpr FLO _EPSILON = std::numeric_limits<FLO>::epsilon();
}//(End)Test helpers
/**************************************************************************//**
* @test cover properties of generic parameter containers.
*/
class ParameterDomain_test : public Test
{
virtual void
run (Arg)
{
seedRand();
basics();
verify_valueLimits();
verify_valueAccess();
verify_valueSetting();
verify_opaqueAssign();
}
void
basics()
{
}
/** @test base operation to bracket a given value
* so that it fits within another target type's value domain.
* @note properly handles the signed vs. unsigned mismatch, so that
* conversion from signed to unsigned can not wrap.
*/
void
verify_valueLimits()
{
// Generic check if the first (target) type can hold
// only a limited part of the second (source) type's domain
CHECK ((sub_domain<uint,int> == true));
CHECK ((sub_domain<int,uint> == true));
CHECK ((sub_domain<short,long> == true));
CHECK ((sub_domain<long,short> == false));
CHECK ((sub_domain<float,double> == true));
CHECK ((sub_domain<double,float> == false));
// Limit a value to within a target type's domain
// (without actually converting into the target type)
CHECK (preClamp<int> (23) == 23);
CHECK (preClamp<int> (-55) == -55);
CHECK (preClamp<uint> (23) == 23);
CHECK (preClamp<uint>(-55) == 0);
CHECK (preClamp<short> (_MAX<int64_t>) == 32767);
CHECK (preClamp<short> (_MIN<int64_t>) == -32768);
CHECK (preClamp<ushort> (_MAX<float>) == 65535);
CHECK (preClamp<ushort> (_MIN<float>) == 0 );
// the return value has the same type as the argument...
CHECK (preClamp<float> (_MAX<uint64_t>) == _MAX<uint64_t>);
CHECK (preClamp<float> (_MIN<double>) == double(_MIN<float>) );
// Warning: the following is not standard C++, yet supported on GCC / CLang / libstdc++
static_assert (_MAX<float> < _MAX<unsigned __int128>);
CHECK (float(_MAX<unsigned __int128>) == "inf"_expect );
CHECK ((unsigned __int128)_MAX<float> == "340282346638528859811704183484516925440"_expect );
CHECK (preClamp<float> (_MAX<unsigned __int128>) == (unsigned __int128)_MAX<float>);
// Note: a trigger at 0.5+ε is built into the clamp towards bool
CHECK (preClamp<bool> (0.5) == 0 );
CHECK (preClamp<bool> (0.5+_EPSILON<double>) == 1 );
CHECK (preClamp<bool> (_MIN<double>) == 0 );
// Systematic coverage of signed ⟷ unsigned
CHECK (preClamp<int8_t> (_MAX<int16_t>) == 127);
CHECK (preClamp<int8_t> ( int16_t{0}) == 0);
CHECK (preClamp<int8_t> (_MIN<int16_t>) == -128);
CHECK (preClamp<int8_t> (_MAX<uint16_t>) == 127);
CHECK (preClamp<int8_t> (_MIN<uint16_t>) == 0);
CHECK (preClamp<uint8_t> (_MAX<int16_t>) == 255);
CHECK (preClamp<uint8_t> ( int16_t{0}) == 0);
CHECK (preClamp<uint8_t> (_MIN<int16_t>) == 0);
CHECK (preClamp<int16_t> (_MAX<int8_t>) == 127);
CHECK (preClamp<int16_t> ( int8_t{0}) == 0);
CHECK (preClamp<int16_t> (_MIN<int8_t>) == -128);
CHECK (preClamp<int16_t> (_MAX<uint8_t>) == 255);
CHECK (preClamp<int16_t> (_MIN<uint8_t>) == 0);
CHECK (preClamp<uint16_t> (_MAX<int8_t>) == 127);
CHECK (preClamp<uint16_t> ( int8_t{0}) == 0);
CHECK (preClamp<uint16_t> (_MIN<int8_t>) == 0);
CHECK (preClamp<int8_t> (_MAX<int8_t>) == 127);
CHECK (preClamp<int8_t> ( int8_t{0}) == 0);
CHECK (preClamp<int8_t> (_MIN<int8_t>) == -128);
CHECK (preClamp<int8_t> (_MAX<uint8_t>) == 127);
CHECK (preClamp<int8_t> (_MIN<uint8_t>) == 0);
CHECK (preClamp<uint8_t> (_MAX<int8_t>) == 127);
CHECK (preClamp<uint8_t> ( int8_t{0}) == 0);
CHECK (preClamp<uint8_t> (_MIN<int8_t>) == 0);
}
template<typename X>
using FullDomain = BaseDomain<X, Domain>;
/** @test base function to access the contents of an opaque buffer
* with known type, and convert into another target type.
* - the base interface #Domain represents an opaque type
* - it exposes #TypeHandler interfaces for a fixed selection
* of common elementary #BaseTypes
* - here we use a `BaseDomain<int>` as actual implementation,
* yet knowledge regarding the type `int` is erased and any
* access is based solely on a `Domain&`
* - an opaque source ValBuf is passed explicitly
* @remark this covers the value access capability of a
* type handler interface, working on external storage.
*/
void
verify_valueAccess()
{
FullDomain<int> domInt;
CHECK (sizeof(FullDomain<int>) <= sizeof(void*)); // only the VTable
// use the generic interface for access...
Domain& dom{domInt};
int val = 1 + rani (1000);
ValBuff& src = asValBuff (val);
uint target1{0};
CHECK (not target1);
dom.extractAs (target1, src);
CHECK (target1 == uint(val));
float target2{0};
dom.extractAs (target2, src);
CHECK (target2 == float(val));
// value clamped to target domain...
val = _MIN<int>;
uint64_t target3{55};
dom.extractAs (target3, src);
CHECK (target3 == _MIN<uint64_t>);
}
/** @test base function to assign a directly given data value
* into an opaque buffer with known (yet different) type.
* @remark since the type handler interface covers both reading
* and writing, here the roles are reversed here, so that the
* target of the assignment is the opaque buffer, whose actual
* type remains hidden behind the #Domain interface.
*/
void
verify_valueSetting()
{
FullDomain<int> domInt;
Domain& dom{domInt};
int val_in_buff = 0;
ValBuff& target = asValBuff (val_in_buff);
uint src1 = 1 + rani (1000);
dom.conform (target, src1);
CHECK (val_in_buff == int(src1));
double src2 = ranRange (1,100);
CHECK (1 <= src2 and src2 < 100);
dom.conform (target, src2);
CHECK (val_in_buff == int(floor (src2)));
// value clamped to target domain...
auto mini = _MIN<int64_t>;
dom.conform (target, mini);
CHECK (val_in_buff == _MIN<int>);
}
/** @test value transfer between opaque buffers with double-dispatch.
* - use several typed values, yet access these only marked as `ValBuff`
* - the call must be passed through _two virtual functions_, in order
* to re-gain the fully typed context; this is the »visitor pattern«
* @remark the API might seem bewildering; this is due to passing both source
* and target as reference to _some value buffer_, since this API describes
* only a data type disposition, while the actual data storage is defined
* as derived parameter data type.
*/
void
verify_opaqueAssign()
{
double double_val = ranRange (2,100);
int64_t i64_val{0};
bool boo{false};
FullDomain<double> ddom;
FullDomain<int64_t> idom;
FullDomain<bool> bdom;
Domain& d1{ddom};
Domain& d3{bdom};
CHECK (i64_val == 0);
d1.transferTo (asValBuff(double_val), idom, asValBuff(i64_val));
CHECK (i64_val == int64_t(floor (double_val)));
CHECK (not boo);
d1.transferTo (asValBuff(double_val), bdom, asValBuff(boo));
CHECK (boo);
CHECK (double_val != 1.0);
d3.transferTo (asValBuff(boo), ddom, asValBuff(double_val));
CHECK (double_val == 1.0);
}
};
/** Register this test class... */
LAUNCHER (ParameterDomain_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -0,0 +1,91 @@
/*
ParameterProvision(Test) - verify parameter value provision and automation
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 parameter-provision-test.cpp
** unit test \ref ParameterProvision_test
*/
#include "test/run.hpp"
//#include "test/test-helper.hpp"
#include "lib/par/parameter.hpp"
//#include "lib/format-cout.hpp"
//#include <utility>
//#include <string>
//#include <vector>
//using std::string;
//using std::vector;
//using std::swap;
namespace lib {
namespace par {
namespace test{
// using lumiera::error::LUMIERA_ERROR_LOGIC;
namespace {//Test fixture....
}//(End)Test fixture
/**************************************************************************//**
* @test document behaviour of parameter value provision and automation.
*/
class ParameterProvision_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_Value();
verify_Function();
}
void
simpleUsage()
{
}
void
verify_Value()
{
}
void
verify_Function()
{
}
};
/** Register this test class... */
LAUNCHER (ParameterProvision_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -0,0 +1,365 @@
/*
ParameterScale(Test) - verify parameter value scales
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 parameter-test.cpp
** unit test \ref ParameterScale_test
*/
#include "test/run.hpp"
#include "test/test-helper.hpp"
#include "lib/par/scale.hpp"
#include "test/diagnostic-output.hpp"/////////////TODO
using test::roughEQ;
namespace lib {
namespace par {
namespace test{
namespace {//Test helpers....
template<typename NUM>
constexpr NUM _MAX = std::numeric_limits<NUM>::max();
template<typename NUM>
constexpr NUM _MIN = std::numeric_limits<NUM>::lowest();
template<floating_point FLO>
constexpr FLO _EPSILON = std::numeric_limits<FLO>::epsilon();
}//(End)Test helpers
/**************************************************************************//**
* @test demonstrate features and verify behaviour of value Scale definitions.
* - a par::Scale is a descriptor record with some feature configuration settings
* - some combination of setting marks specific scale features to be present
* - values can be _conformed_ to comply with the scale constraints
* - a Scale can be configured to maintain cyclic wrapping values
* - values defined on different scales can be _joined_ (numerically combined)
* - scales can be defined to be logarithmic, and work together with linear scales.
*/
class ParameterScale_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_Predicate();
verify_Conforming();
verify_cyclicScale();
verify_valueJoining();
}
void
simpleUsage()
{
Scale<int> scale = {.minVal = -5, .maxVal = 23};
CHECK (scale.isValid());
CHECK (-5 == scale.conform (-55));
CHECK (23 == scale.conform (+55));
CHECK ( 5 == scale.join (2,3) );
CHECK (-5 == scale.join (23,-55));
}
/** @test verify the feature detection and descriptor predicates */
void
verify_Predicate()
{
Scale<double> scale;
CHECK (scale.isValid());
CHECK (not scale.isCyclic());
CHECK (not scale.isLimited());
CHECK (not scale.isLogarithmic());
scale.minVal = -5;
CHECK (scale.isValid());
CHECK (not scale.isCyclic());
CHECK ( scale.isLimited());
scale.cyclicLim = 10;
CHECK (not scale.isValid());
CHECK ( scale.isCyclic());
CHECK (not scale.isLimited());
scale.maxVal = +5;
CHECK ( scale.isValid());
CHECK ( scale.isCyclic());
CHECK (not scale.isLimited());
CHECK (not scale.isFactor());
scale.neutral = 2;
CHECK (not scale.isFactor());
scale.neutral = 1;
CHECK ( scale.isFactor());
CHECK (scale.metric == LIN);
CHECK (not scale.isLogarithmic());
scale.metric = NOM;
CHECK (not scale.isLogarithmic());
scale.metric = DEC;
CHECK ( scale.isLogarithmic());
scale.metric = BIN;
CHECK ( scale.isLogarithmic());
scale.metric = NAT;
CHECK ( scale.isLogarithmic());
}
/** @test accommodate values to comply with the scale
* - optionally a maximum and minimum can be enforced
* - optionally the value can be cyclically wrapped
* @todo WIP 9/26 ✔ define ⟶ ✔ implement
*/
void
verify_Conforming()
{
Scale<float> scale;
CHECK (1e5f == scale.conform (1e5f));
scale.maxVal = -5.0f;
CHECK (-5.0f == scale.conform (1e5f));
CHECK (scale.isValid());
scale.minVal = 5.0f;
CHECK (not scale.isValid());
scale.maxVal = 8.0f;
CHECK (scale.isValid());
CHECK (5.0f == scale.conform (-5.0f));
CHECK (not scale.isCyclic());
scale.cyclicLim = 100;
CHECK (scale.isCyclic());
CHECK (7.0f == scale.conform (-5.0f));
CHECK (5.0f == scale.conform (-4.0f));
CHECK (6.0f == scale.conform (-3.0f));
CHECK (7.0f == scale.conform (-2.0f));
VERIFY_FAIL ("beyond supported numeric precision"
, scale.conform (-1e5f));
}
/** @test Scales can be configured to wrap cyclically, e.g. for angles.
* - demonstrate an unsigned angle scale with short precision
* - demonstrate a high-resolution scale symmetric to zero
* @todo WIP 9/26 ✔define ⟶ ✔ implement
*/
void
verify_cyclicScale()
{
Scale<ushort> angleScale = {.minVal=0, .maxVal=360, .cyclicLim=0xFFFF};
CHECK (angleScale.isValid());
CHECK (angleScale.isCyclic());
CHECK ( 0 == angleScale.conform(0));
CHECK (180 == angleScale.conform(180));
CHECK (240 == angleScale.conform(240));
CHECK ( 0 == angleScale.conform(360));
CHECK (359 == angleScale.conform(719));
CHECK ( 0 == angleScale.conform(65520));
CHECK ( 15 == angleScale.conform(65535));
Scale<lflp> tinyScale = {.minVal=-lflp(1)/100, .maxVal=lflp(1)/100};
tinyScale.cyclicLim = util::limit_cyclicWrap(*tinyScale.maxVal - *tinyScale.minVal);
CHECK (1e16 < tinyScale.cyclicLim);
CHECK (tinyScale.isValid());
CHECK (tinyScale.isCyclic());
CHECK ( tinyScale.conform(0 ) == 0 );
CHECK (roughEQ (tinyScale.conform(0.001) , 0.001));
CHECK (roughEQ (tinyScale.conform(0.005) , 0.005));
CHECK (roughEQ (tinyScale.conform(0.00999), 0.00999));
CHECK (roughEQ (tinyScale.conform(0.01 ),-0.0099999)); // surprise: 1/100 not exactly representable in floating-point
// since the domain is symmetric and 2/100 long, the ten-folds will fall into the middle,
// but they typically do not land precisely on zero due to inherent "number dust"
CHECK (1e15 > fabs(tinyScale.conform(1)));
CHECK (0 != tinyScale.conform(1) );
auto beyond_precision = -10 * tinyScale.cyclicLim;
VERIFY_FAIL ("beyond supported numeric precision"
, tinyScale.conform (beyond_precision));
}
/** @test Base value and data feeds from differing Scales can be joined together
* - when the target scale defines limits (or is cyclic),
* this conforming is applied after all conversions
* - a feed can be marked as "factor", in which case
* the feed is multiplied to the target rather than added
* - even scales with integral type can carry log meaning
* - suitable intermediary types used for good precision,
* and especially to avoid wrapping signed into unsigned
* - the supported logarithmic flavours are accommodated,
* possibly converting the base of the logarithm accordingly
* - the factor 10 inherent to decibels is applied / removed
* before combining decibel values with other logarithms
* - applying a linear factor to a logarithmic scale is
* transformed into an addition in the logarithmic domain
* - adding a linear offset to a logarithmic scale is rejected
* - extended precision scales handled using `lflp` (≙`long double`)
* - this allows to retain full precision of 64bit integrals
* @todo WIP 9/26 ✔ define ⟶ ✔ implement
*/
void
verify_valueJoining()
{
Scale<ushort> scale = {.minVal = 2, .maxVal = 10};
Scale<int64_t> lscale;
CHECK ( 5 == scale.join (2, int64_t(3), lscale)); // 2+3 ≡ 5 (all computations done as double)
CHECK (10 == scale.join (5, int64_t(8), lscale)); // 5+8 ≡ 13 ⟼ capped to 10
lscale.neutral = 1;
CHECK (lscale.isFactor());
CHECK ( 6 == scale.join (2, int64_t(3), lscale)); // 2*3 ≡ 6
CHECK ( 2 == scale.join (2, int64_t(-3),lscale)); // 2*-3 ≡ -6 ⟼ first conditioned to ushort domain, then capped to 2
lscale.metric = DEC; // in dB
CHECK ( 7 == scale.join (4, int64_t(3), lscale)); // 4 + 3dB ⟼ 4 * 1.995 ≡ 7.98 ⟼ truncated to 7
Scale<short> sscale;
Scale<uint64_t> uscale = {.neutral = 1};
// Note: number promotion rules of C++ would backfire nastily here...
CHECK (short(-3) * uint64_t(2) > numeric_limits<int64_t>::max());
CHECK (short(-6) == sscale.join (-3, uint64_t(2), uscale));
//---floating-point logarithmic scale conversions---
//
Scale<float> fscale = {.metric = NAT};
Scale<float> feed_dB = {.metric = DEC};
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log(100.0f) )); // dB ⟼ natural logarithm; note 20dB ≙ factor 10 * lg(100); input 20 divided by 10
fscale.metric = BIN;
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log2(100.0f))); // dB ⟼ binary logarithm; (factor 1/10 on input is folded into the log factor)
fscale.metric = DEC;
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , 20.0f )); // dB ⟼ dB just passed-through and then added to value ≔ 0.0
fscale.metric = DEC;
lscale.metric = BIN;
CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log10(1.0f/8) *10)); // binary log (given as int value) ⟼ dB
lscale.metric = NAT;
CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log10(exp(-3))*10)); // natural log (input as int) ⟼ dB
lscale.metric = DEC;
CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), -3 )); // dB ⟼ dB just passed-through
fscale.metric = BIN;
lscale.metric = BIN;
CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), -3 )); // lb ⟼ lb just passed-through
lscale.metric = NAT;
CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log2 (exp(-3)) )); // natural log ⟼ binary log
lscale.metric = DEC;
CHECK (roughEQ (fscale.join (0, int64_t(-30),lscale), log2 (1.0/1000) )); // conversion factor: dB/10 ⟼ binary log
fscale.metric = LIN;
lscale.metric = BIN;
CHECK (roughEQ (fscale.join (1, int64_t(-3), lscale), 1.0f/8 )); // binary log applied as factor to a linear scale
lscale.metric = NAT;
CHECK (roughEQ (fscale.join (1, int64_t(-1), lscale), 1.0f/exp(1))); // natural log applied as factor 1/e
lscale.metric = DEC;
CHECK (roughEQ (fscale.join (1, int64_t(-30),lscale), 1.0f/1000 )); // decibels applied as factor : -30 dB ≙ 10^-3
fscale.metric = BIN;
lscale.metric = LIN;
lscale.neutral = 1;
CHECK (lscale.isFactor());
CHECK (fscale.isLogarithmic());
CHECK ( fscale.join (1, int64_t(8), lscale) == 1 + 3); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there (exact)
fscale.metric = NAT;
CHECK (roughEQ (fscale.join (1, int64_t(8), lscale), 1 + logf(8))); // feed factor 8 ⟼ now applied to a natural log scale (and added to one, in float)
fscale.metric = DEC;
CHECK (roughEQ (fscale.join (1, int64_t(100),lscale), 1 + 20)); // feed factor 8 ⟼ now applied as offset in decibels
lscale.neutral = 0;
CHECK (not lscale.isFactor());
VERIFY_FAIL ("adding a non-logarithmic data feed on top of a logarithmic base value"
, fscale.join (1, int64_t(8), lscale));
//---verify a long-double target scale
//
Scale<lflp> megaScale = {.minVal = -10, .maxVal = +10}; // setup a cyclic scale using the long double domain
megaScale.cyclicLim = util::limit_cyclicWrap<lflp>(20); // (cyclic wrap is handled by conform(); just another little twist)
CHECK (megaScale.isCyclic());
megaScale.metric = DEC;
fscale.metric = LIN;
fscale.neutral = 1;
CHECK (fscale.isFactor());
CHECK ( 5 == megaScale.join ( 5, 100.0f, fscale)); // input ⟼ decibels, then added to the base value, and finally wrapped ∈ [-10 ... 10[
CHECK (-5 == megaScale.join (-5, 100.0f, fscale));
CHECK ( 5 == megaScale.join (-5, 1000.0f, fscale)); // ...so here we get -5dB + 30dB
// now push that to the limits....
CHECK (logb (1e18) < logb (_MAX<int64_t>)); // use the largest power of 10 representable in int64_t
CHECK (logb (_MAX<int64_t>) < logb (megaScale.cyclicLim)); // ...which the (cyclic) target scale can still handle precisely!
lflp res1 = megaScale.join (-5, 1e18f, fscale); // log10 ⟼ 18, so we add -5dB + 180dB;
CHECK (roughEQ (res1, -5)); // the cyclicWrap then computes 175 - (-10) ≡ 185 % 20 (≙5) + (minVal ≙ -10)
// but the computation is not exact...
CHECK (0 != std::fmod (res1, 1));
// yet the cause lies in the input already:
lflp srcErr = lflp(1e18f) - pow (lflp(10), 18);
CHECK (1e10 < fabs (srcErr)); // ... so the float input incurs a gigantic error margin
// however: use an exact input, and the result will be exact
lscale.neutral = 1;
CHECK (lscale.isFactor());
CHECK (-5 == megaScale.join (-5, int64_t(1e18), lscale));
lflp res2 = megaScale.join (-5, int64_t(1e18)+1, lscale);
CHECK (-5 != res2); // even a tiny offset makes it through the computation
CHECK (-16 > log10 (res2 -(-5))); // error is below the 16. decimal place (plausible, since log10 is computed)
// compare to target scale with lesser precision...
Scale<double> doubleTargetScale = {.metric = DEC};
Scale<uint64_t> longTargetScale = {.metric = DEC};
CHECK (180 == doubleTargetScale.join (0, int64_t(1e18), lscale)); // uses double for internal log10 computation
CHECK (180 == doubleTargetScale.join (0, int64_t(1e18)-10, lscale)); // offset chosen large enough to prevail in int64_t but not in double
CHECK (180 == longTargetScale.join (0, int64_t(1e18), lscale)); // 64bit integral scales use long-double for log10 computation internally
CHECK (179 == longTargetScale.join (0, int64_t(1e18)-10, lscale)); // ...and thus the offset survives the log10 and is truncated down on conversion
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_NominalScale()
{
}
};
/** Register this test class... */
LAUNCHER (ParameterScale_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -0,0 +1,137 @@
/*
Parameter(Test) - verify generic parameter values
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 parameter-test.cpp
** unit test \ref Parameter_test
*/
#include "test/run.hpp"
//#include "test/test-helper.hpp"
#include "lib/par/parameter.hpp"
#include "test/diagnostic-output.hpp"/////////////TODO
//#include <utility>
//#include <string>
//#include <vector>
//using std::string;
//using std::vector;
//using std::swap;
namespace lib {
namespace par {
namespace test{
// using lumiera::error::LUMIERA_ERROR_LOGIC;
namespace {//Test fixture....
}//(End)Test fixture
/**************************************************************************//**
* @test cover properties of generic parameter containers.
*/
class Parameter_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_Builder();
verify_Identity();
verify_ParamType();
verify_Automation();
}
void
simpleUsage()
{
CHECK (sizeof(Parameter) <= 4 * sizeof(void*));
CHECK (BASETYPE_MAX_SIZ <= sizeof(ValStorage));
Parameter par = Parameter::forType<int>().build();
CHECK (0 == par.getVal<int>());
par.setVal (12.3);
CHECK (12 == par.getVal<int>());
CHECK (12u == par.getVal<uint64_t>());
CHECK (12.0 == par.getVal<double>());
CHECK (true == par.getVal<bool>());
}
/** @test explore ways to define a parameter...
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_Builder()
{
Parameter p1 = Parameter::forValue(123.45f).build();
CHECK (123.45f == p1.getVal<float>());
p1.setVal (12u);
CHECK (12.0 == p1.getVal<double>());
Parameter p2 = Parameter::from(p1)
.maxVal (sqrt(2))
.build();
SHOW_EXPR(p2.getVal<float>());
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_Identity()
{
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_ParamType()
{
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_Automation()
{
}
};
/** Register this test class... */
LAUNCHER (Parameter_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -0,0 +1,117 @@
/*
TypeRegistry(Test) - verify nominal and ordinal scale enumerations
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 type-registry.cpp
** unit test \ref TypeRegistry_test
*/
#include "test/run.hpp"
//#include "test/test-helper.hpp"
#include "lib/par/type-registry.hpp"
#include "test/tracking-dummy.hpp"
#include "lib/util.hpp"
#include "test/diagnostic-output.hpp"/////////////TODO
//#include <utility>
//#include <string>
//#include <vector>
//using std::string;
//using std::vector;
//using std::swap;
using util::isSameObject;
namespace lib {
namespace par {
namespace test{
// using lumiera::error::LUMIERA_ERROR_LOGIC;
namespace {//Test fixture....
}//(End)Test fixture
using ::test::Dummy;
using Registry = TypeRegistry<Dummy>;
using RegID = Registry::ID;
/**************************************************************************//**
* @test verify a index/extent based registration table for type descriptors
* - new slots can be claimed and then re-accessed by ID
* - storage is stable even while new extents are added, and not leaked
* - read access can happen concurrently with allocating new slots.
*/
class TypeRegistry_test : public Test
{
virtual void
run (Arg)
{
seedRand();
simpleUsage();
verify_storageSanity();
verify_concurrentAccess();
}
void
simpleUsage()
{
const int MARK = 1 + rani (1000);
Registry registry;
RegID id = registry.makeSlot (MARK);
Dummy& entry = registry[id];
CHECK (MARK == entry.getID());
CHECK (isSameObject (entry, registry[id]));
VERIFY_FAIL ("not yet allocated", registry[12345]);
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_storageSanity()
{
UNIMPLEMENTED ("verify coherent storage handling and proper clean-up");
}
/** @test
* @todo WIP 9/26 🔁 define ⟶ implement
*/
void
verify_concurrentAccess()
{
UNIMPLEMENTED ("press slot allocation and data access concurrently");
}
};
/** Register this test class... */
LAUNCHER (TypeRegistry_test, "unit lib");
}}} // namespace lib::par::test

View file

@ -171,7 +171,7 @@ namespace test{
}
/** @test various ways to build an populate the container
/** @test various ways to build and populate the container
* - with a defined interface type \a I, instances of arbitrary subclasses
* can be added, assuming there is sufficient pre-allocated buffer space;
* all these subclass instances are accessed through the common interface.

View file

@ -224,7 +224,7 @@ namespace test{
CHECK (format4Csv(double(1) / 3) == "0.333333333333333"_expect );
CHECK (format4Csv(float(1) / 3) == "0.333333"_expect );
CHECK (format4Csv(f128(1) / 3) == "0.333333333333333333"_expect);
CHECK (format4Csv(lflp(1) / 3) == "0.333333333333333333"_expect);
CHECK (format4Csv(bool(1)) == "true"_expect );
CHECK (format4Csv(bool(0)) == "false"_expect);
CHECK (format4Csv("Starship-3") == "\"Starship-3\""_expect ); // 3rd test today ;-)

View file

@ -30,7 +30,7 @@
using ::Test;
using util::isnil;
using util::_Fmt;
using std::integral;
namespace util {
namespace test {
@ -42,6 +42,13 @@ namespace test {
const uint NUM_ELMS_PERFORMANCE_TEST = 50000000;
const uint NUMBER_LIMIT = 1 << 30;
template<typename NUM>
constexpr NUM _MAX = std::numeric_limits<NUM>::max();
template<typename NUM>
constexpr NUM _MIN = std::numeric_limits<NUM>::lowest();
typedef std::vector<int> VecI;
VecI
@ -108,7 +115,7 @@ namespace test {
{
seedRand();
verifyBehaviour ();
verifyBehaviour();
verifyIntegerTypes<int>();
verifyIntegerTypes<long>();
@ -116,6 +123,8 @@ namespace test {
verifyIntegerTypes<int64_t>();
verifyIntegerTypes<llong>();
verifyBounds();
if (not isnil (arg))
runPerformanceTest();
}
@ -154,7 +163,7 @@ namespace test {
template<typename I>
void
verifyIntegerTypes ()
verifyIntegerTypes()
{
I n,d,expectedRes;
@ -168,11 +177,51 @@ namespace test {
}
void
verifyBounds()
{
auto verify = []<integral I>(I num, I den, I expect_quot, I expect_rem)
{
auto res = floorwrap (num,den);
CHECK (res.quot == expect_quot, "quotient mismatch");
CHECK (res.rem == expect_rem , "remainder mismatch");
CHECK (res.quot == floordiv (num,den), "floordiv diverges");
};
//----- num | den | quotient | remainder
verify (_MAX<int>, 1, _MAX<int> , 0);
verify (_MAX<int>, 2, _MAX<int>/2 , 1);
verify (_MAX<int>, 5, _MAX<int>/5 , 2);
verify (_MIN<int>, 1, _MIN<int> , 0);
verify (_MIN<int>, 2, _MIN<int>/2 , 0);
verify (_MIN<int>, 5, _MIN<int>/5 -1, 2); // _MIN<int> ≡ -2147483648 /5 ≡ -429496729 -1 step ≡ -429496730 · 5 ≡ -2147483650 ⟹ Δ = +2
verify (_MAX<int>,-1, -_MAX<int> , 0);
verify (_MAX<int>,-2, -_MAX<int>/2 -1,-1);
verify (_MAX<int>,-5, -_MAX<int>/5 -1,-3); // Δ ≡ -3 here since -_MAX<int> ≡ _MIN<int> + 1
verify (_MAX<uint>, 1u, _MAX<uint> ,0u);
verify (_MAX<uint>, 2u, _MAX<uint>/2 ,1u);
verify (_MAX<uint>, 5u, _MAX<uint>/5 ,0u);
verify (_MIN<int64_t>, int64_t(1), _MIN<int64_t> , int64_t(0));
verify (_MIN<int64_t>, int64_t(2), _MIN<int64_t>/2 , int64_t(0));
verify (_MIN<int64_t>, int64_t(5), _MIN<int64_t>/5 -1, int64_t(2)); // 2^63 / 5 ≡ 1844674407370955161.6 ; 1844674407370955162 · 5 ⟹ Δ = +2
verify (_MIN<int8_t>, int8_t(1), int8_t(-128) , int8_t(0));
verify (_MIN<int8_t>, int8_t(2), int8_t(-64 ) , int8_t(0));
verify (_MIN<int8_t>, int8_t(5), int8_t(-26 ) , int8_t(2)); // 2^7 / 5 ≡ 25.6 ; 26 · 5 ≡ 130 ⟹ Δ = +2
verify (_MAX<int8_t>, int8_t(5), int8_t( 25 ) , int8_t(2)); // (2^7-1) / 5 ≡ 25.4 ; 25 · 5 ≡ 125 ⟹ Δ = +2
}
/** @test timing measurements to compare implementation details.
* This test uses a sequence of random integers, where the values
* used as denominator are ensured not to be zero.
*
*
* \par measurement results
* My experiments (AMD Athlon-64 4200 X2) gave me
* the following timing measurements in nanoseconds:

View file

@ -36,7 +36,7 @@
#include "lib/format-string.hpp"
#include "lib/format-cout.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/luid.h"
#include "lib/hash-indexed.hpp"
#include "lib/util.hpp"
#include <boost/lexical_cast.hpp>

View file

@ -250,7 +250,7 @@ namespace test {
{
InvocationInstanceID empty; ///////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
JobClosure& jobFunctor = static_cast<JobClosure&> (*job.jobClosure); //////////////////////////TICKET #1287 : fix actual interface down to JobFunctor (after removing C structs)
return lumiera_invokey_eq (&util::unConst(job).parameter.invoKey, &empty)
return job.parameter.invoKey == empty
and util::isSameObject (jobFunctor, nopFunctor());
}

View file

@ -127,7 +127,7 @@ namespace test {
CHECK (INSTANCEOF (vault::gear::NopJobFunctor, static_cast<JobClosure*> (nopJob.jobClosure))); //////////TICKET #1287 : fix actual interface down to JobFunctor (after removing C structs)
CHECK (nopJob.parameter.nominalTime == frameTime);
InvocationInstanceID empty; ///////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
CHECK (lumiera_invokey_eq (&nopJob.parameter.invoKey, &empty));
CHECK (nopJob.parameter.invoKey == empty);
CHECK (MockJob::isNopJob(nopJob)); // this diagnostic helper checks the same conditions as done here explicitly
MockJobTicket mockTicket;
@ -211,7 +211,7 @@ namespace test {
// create another job from the (empty) seg1
job = seg1.jobTicket(0).createJobFor (someTime);
InvocationInstanceID empty; /////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
CHECK (lumiera_invokey_eq (&job.parameter.invoKey, &empty)); // indicates that it's just a placeholder to mark a "NOP"-Job
CHECK (job.parameter.invoKey == empty); // indicates that it's just a placeholder to mark a "NOP"-Job
CHECK (seg1.jobTicket(0).empty());
CHECK (seg1.empty());
CHECK (not seg2.empty());

View file

@ -20,6 +20,7 @@
#include "steam/engine/engine-ctx.hpp"
#include "steam/engine/node-builder.hpp"
#include "steam/asset/meta/time-grid.hpp"
#include "lib/allocation-cluster.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/iter-explorer.hpp"
@ -66,11 +67,12 @@ namespace test {
build_Node_closedParam();
build_connectedNodes();
build_ParamNode();
useAllocationCluster();
}
/** @test build a simple output-only Render Node
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_simpleNode()
@ -97,11 +99,10 @@ namespace test {
* - expect the buffer to hold a single `uint` value after invocation
*/
uint
invokeRenderNode (ProcNode& theNode, Time nomTime =Time::ZERO)
invokeRenderNode (ProcNode& theNode, Time nomTime =Time::ZERO, uint port =0)
{
BuffHandle buff = EngineCtx::access().mem.lockBufferFor<long> (-55);
ProcessKey key{0};
uint port{0};
CHECK (-55 == buff.accessAs<long>());
@ -115,7 +116,6 @@ namespace test {
/** @test build a Node with a fixed invocation parameter
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_Node_fixedParam()
@ -139,7 +139,6 @@ namespace test {
* the time into an implicitly defined grid
* - install both into a render node
* - set a random _nominal time_ for invocation
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_Node_dynamicParam()
@ -171,7 +170,6 @@ namespace test {
* - again use a processing function which takes a parameter
* - but then _decorate_ this functor, so that it takes different arguments
* - attach parameter handling to supply these adapted arguments
* @todo 2/25 ✔ define ⟶ ✔ implement
*/
void
build_Node_adaptedParam()
@ -239,7 +237,6 @@ namespace test {
* - use the »simplified 1:1 wiring«, which connects consecutively
* each input slot to the next given node on the same port number;
* here we only use port#0 on all three nodes.
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_connectedNodes()
@ -287,7 +284,6 @@ namespace test {
* - perform effectively the same computation as the preceding test
* - but use two new custom parameters in the Param Agent Node
* - pick them up from the nested source nodes by accessor-functors
* @todo 12/24 ✔ define ⟶ ✔ implement
*/
void
build_ParamNode()
@ -343,6 +339,49 @@ namespace test {
CHECK (res == peek+1 + LIFE_AND_UNIVERSE_4EVER+1 );
CHECK (peek != -1);
}
/** @test demonstrate use of a custom allocator
* - use local AllocationCluster instance for this test
* - place and manage the resulting ProcNode in this cluster
* - furthermore, allocate administrative internals also there
* - build a Node with two ports, each with a parameter and output buffer
* - demonstrate the created Node works as expected
* - verify that allocations happened
*/
void
useAllocationCluster()
{
auto procFun = [](ushort param, uint* buff){ *buff = param; };
lib::AllocationCluster alloc;
CHECK (0 == alloc.numBytes());
auto& node = alloc.create<ProcNode>( // ◁───────────────────────────────────┨ place resulting ProcNode into the cluster
prepareNode("Test")
.withAllocator(alloc) // ◁───────────────────────────────────┨ policy to use this cluster for internal allocations
.preparePort()
.invoke ("funA()", procFun)
.setParam (LIFE_AND_UNIVERSE_4EVER)
.completePort()
.preparePort()
.invoke ("funB()", procFun)
.setParam (LIFE_AND_UNIVERSE_4EVER + 13)
.completePort()
.build()
);
CHECK (LIFE_AND_UNIVERSE_4EVER == invokeRenderNode (node, Time::ZERO, 0));
CHECK (LIFE_AND_UNIVERSE_4EVER+13 == invokeRenderNode (node, Time::ZERO, 1));
// minimum bounds of memory allocations...
const auto MIN_EXPECT = sizeof(ProcNode)
+ 2 * (sizeof(Port) + sizeof(BuffDescr) // defined two ports, each with an output type...
+ 2*(sizeof(void*) + sizeof(std::function<void()>))); // and parameter + buffer + proc-fun and param-fun
CHECK (MIN_EXPECT < alloc.numBytes());
}
};

View file

@ -39,7 +39,7 @@ using lib::Symbol;
using util::isnil;
using util::isSameObject;
using test::TestFrame;
using test::testRand;
using test::testRand;
namespace ont = test::ont;
namespace steam {
@ -362,7 +362,7 @@ namespace test {
// Prepare a precomputed parameter for the complete tree
auto selectFrameNo = [&](TurnoutSystem& tuSys){ return quantSecs(tuSys.getNomTime()); };
auto selectFrameNo = [&](TurnoutSystem& turSys){ return quantSecs(turSys.getNomTime()); };
auto paramSpec = buildParamSpec()
.addSlot (selectFrameNo);
@ -373,8 +373,8 @@ namespace test {
auto stepMixer = [] (FrameCnt id)-> ont::Factr { return util::limited (0, + id, 50) / 50.0; };
// note: binds the accessor for the precomputed FrameNo-parameter
auto autoFilter = [=](TurnoutSystem& tuSys){ return stepFilter (tuSys.get (accFrameNo)); };
auto autoMixer = [=](TurnoutSystem& tuSys){ return stepMixer (tuSys.get (accFrameNo)); };
auto autoFilter = [=](TurnoutSystem& turSys){ return stepFilter (turSys.get (accFrameNo)); };
auto autoMixer = [=](TurnoutSystem& turSys){ return stepMixer (turSys.get (accFrameNo)); };
// A Node with two (source) ports

View file

@ -17,28 +17,375 @@
#include "test/run.hpp"
//#include "lib/util.hpp"
#include "lib/allocation-cluster.hpp"
#include "test/test-rand-ontology.hpp"
#include "vault/out/diagnostic-output-slot.hpp"
#include "vault/mem/buffer-provider-setup.hpp"
#include "vault/mem/engine-buffer-metadata.hpp"
#include "vault/mem/engine-buffer-manager.hpp"
#include "vault/mem/local-buffer-stage.hpp"
#include "vault/mem/local-buffer-store.hpp"
#include "steam/engine/engine-facilities.hpp"
#include "steam/engine/node-builder.hpp"
#include "steam/asset/meta/time-grid.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/depend-inject.hpp"
#include "lib/thread.hpp"
//#include "lib/symbol.hpp"
#include "lib/util.hpp"
#include "test/diagnostic-output.hpp"///////////////TODO
//using std::string;
using lib::Thread;
using std::this_thread::yield;
using std::this_thread::sleep_for;
using std::chrono_literals::operator ""us;
using lib::time::Time;
using lib::time::QuTime;
using lib::time::FrameNr;
using lib::time::FrameCnt;
using lib::time::FrameRate;
using lib::time::PGrid;
using steam::asset::meta::TimeGrid;
using vault::out::DiagnosticOutputSlot;
//using lib::Symbol;
//using util::isnil;
//using util::isSameObject;
using util::isLimited;
using test::TestFrame;
using test::testRand;
namespace ont = test::ont;
namespace steam {
namespace engine{
namespace test {
namespace { // ========== Test Setup ==========
using vault::mem::BufferProviderSetup;
using vault::mem::LocalBufferStage;
using vault::mem::LocalBufferStore;
using vault::mem::EngineBufferMetadata;
using vault::mem::EngineBufferManager;
class TestEngineBufferSetup
: public BufferProviderSetup
, public EngineFacilities
{
public:
TestEngineBufferSetup()
: BufferProviderSetup{*this}
{ }
auto buildStage() { return std::make_unique<LocalBufferStage>("NodeOpera_test"); }
auto buildStore() { return std::make_unique<LocalBufferStore>(); }
BufferProvider& setupBufferProvider() override { return *this; }
};
struct TestEngineCtx
{
// install mock instances for the relevant services
lib::DependInject<EngineBufferMetadata>::Local<> metaHub;
lib::DependInject<EngineBufferManager>::Local<> globalPool;
// install a rigged access front-end for the buffer management
lib::DependInject<EngineFacilities>::Local<TestEngineBufferSetup> bufferSetup;
lib::DependInject<EngineCtx>::Local<EngineCtx> frontEndCtx;
};
}
/***************************************************************//**
* @test check render node operation modes and collaboration.
* @todo 6/2026 this test shell demonstrate all relevant operation modes
* of render nodes, at the level of an component-integration-test.
* It is not clear yet what further operation modes this entails,
* beyond (obviously) invoking a render job... ////////////////////////////////////////////////TICKET #1367 : build a node invocation
* @remark the purpose of this test is to accomplish component integration
* regarding relevant usages of the render node network, seen from the
* perspective of an (exit) render node that is used / invoked / pulled.
*/
class NodeOpera_test : public Test
{
virtual void run(Arg)
virtual void
run (Arg)
{
UNIMPLEMENTED ("operate some render nodes as linked together");
seedRand();
invokeSimpleNode();
invokeNodeTree();
}
/** @test directly invoke a simple source node.
* - use the node builder to create a simple node
* immediately into the current stack frame
* - the processing function feeds a fixed random number
* into the given output buffer
* - then launch a worker thread to pull this node
* - create an output buffer directly from the worker thread
* - invoke the single port of the node, which is visible
* since it was created before starting the local worker thread
* - after the invocation, the fixed random number should have been
* placed into the output buffer
* - after the worker thread has finished, a single buffer allocation
* should have happened; this allocation was sent back automatically
* and can be claimed and released by the global buffer manager.
*/
void
invokeSimpleNode()
{
int randNum{rani()};
auto fun = [&](int* buff){ *buff = randNum; };
TestEngineCtx mockEngine;
ProcNode node{prepareNode("Test")
.preparePort()
.invoke("fun()", fun)
.completePort()
.build()};
Thread testWorker{[&] /* === directly perform a node invocation === */
{
BuffHandle buff = EngineCtx::access().mem.lockBufferFor<int> (-55);
Time nomTime{Time::ZERO};
ProcessKey key{0};
uint port{0};
CHECK (-55 == buff.accessAs<int>());
// Trigger Node invocation...
buff = node.pull (port, buff, nomTime, key);
CHECK (randNum == buff.accessAs<int>());
buff.release();
}};
while (testWorker)
yield(); // wait for worker to finish
// the central services were indeed requested / created
CHECK (mockEngine.bufferSetup);
CHECK (mockEngine.globalPool);
CHECK (mockEngine.metaHub);
// at least two buffer types registered (one generic for int)
CHECK (2 <= mockEngine.metaHub->cntEntries());
// one buffer was requested...
CHECK (1 == watch(*mockEngine.globalPool).numAllocs());
sleep_for (500us); // additional delay for the local pool's destructor
// (which is invoked after the thread has terminated)
// process and close remaining allocations
// sent back by the local pool's destructor...
mockEngine.globalPool->processPendingRequests();
// now there should be no memory leaks.......
CHECK (0 == watch(*mockEngine.globalPool).bytesLeased());
}
/** @test perform a RenderInvocation to pull from a complex node tree into an output sink.
* - use a test helper class to build a node topology similar to NodeLink_test
* - the ProcNodes and all backing structures are managed by an AllocationCluster
* - use a DiagnosticOutputSlot (by default configured for buffers sized for TestFrame)
*/
void
invokeNodeTree()
{
TestEngineCtx mockEngine;
RenderPipeline pipeline;
DiagnosticOutputSlot oSlot;
auto sink0 = * oSlot.getOpenedSinks();
const uint OUTPUT_FRAME{123};
Thread testWorker{[&] /* === perform a render invocation === */
{
Time nomTime{rani(60'000),0}; // drive test with a random »nominal Time« <60s with ms granularity
ProcessKey key{0};
uint port{0};
BuffHandle buff = sink0.lockFrame (OUTPUT_FRAME); // claim output buffer
CHECK (not buff.accessAs<TestFrame>().isValid()); // underlying buffer is zero initialised, thus no valid TestFrame yet
// Trigger Node invocation...
buff = pipeline.exitNode.pull (port, buff, nomTime, key);
buff.release();
// DiagnosticOutputSlot allows to investigate the data sent into the DataSink...
TestFrame const& result = watch(oSlot).feed(0).frame(OUTPUT_FRAME).accessAs<TestFrame>();
CHECK (result.isValid()); // data matches checksum
CHECK (not result.isPristine()); // is not a source frame (but processed)
CHECK (result.getChecksum() == pipeline.expectedChecksum (nomTime));
}};
while (testWorker)
yield(); // wait for worker to finish
sleep_for (500us);
mockEngine.globalPool->processPendingRequests();
// now there should be no memory leaks.......
CHECK (0 == watch(*mockEngine.globalPool).bytesLeased());
}
/**
* Helper context to build and manage a processing chain of Render Nodes.
* In the real Render Engine, these processing chains are generated by the Builder,
* and organised into »Segments« of the timeline, where each segment features a
* distinct node connectivity (i.e. typically for one clip).
* For this test, a single top-level _exit node_ is exposed.
* @note this helper context is constructed in _RAII style_,
* and the embedded AllocationCluster unwinds automatically.
* @see \ref NodeLink_test for a detailed breakdown of a similar topology
*/
class RenderPipeline
: util::NonCopyable
{
static constexpr ont::Flavr SRC_A = 10; ///< »chain-A« arbitrary source frame marker
static constexpr ont::Flavr SRC_B = 20; ///< similar for »chain-B«
PGrid secondsGrid_{TimeGrid::build (FrameRate{1})};
FrameCnt quantSecs (Time time) { return secondsGrid_->gridPoint (time); };
/** custom allocator for this node tree */
lib::AllocationCluster alloc_;
public:
ProcNode& exitNode{buildTree()};
HashVal
expectedChecksum (Time nomTime)
{
ont::FraNo fraNo = quantSecs(nomTime);
ont::Param param = stepFilter(fraNo);
ont::Factr mix = stepMixer (fraNo);
TestFrame f1{uint(fraNo),SRC_A};
TestFrame f2{uint(fraNo),SRC_B};
ont::manipulateFrame (&f1, &f1, param);
ont::combineFrames (&f1, &f1, &f2, mix);
return f1.getChecksum();
}
private:
ont::Param stepFilter(FrameCnt id) { return util::limited (10, -10 + id, 50); };
ont::Factr stepMixer (FrameCnt id) { return util::limited (0, + id, 50) / 50.0; };
/** @internal wire the processing node topology.
* This fake "render pipeline" is based on hash chaining computations,
* operating on TestFrame (with reproducible random data content).
* Two processing chains are built, each producing test frames;
* the first chain even adds a "filter". Some "parameters" are added
* to change the random data in a deterministic way. Finally, both chains
* are "mixed" together into a single result TestFrame. Furthermore, the
* parameter automation is based on a precomputed _frame number_ parameter,
* that is derived from the _absolute nominal time_ by quantisation into a
* time grid with a stepping of 1 second (-> `FrameRate{1}`). This precomputed
* frame number parameter is stored in a parameter data block on the call stack,
* prior to recursively evaluating the node chain attached below.
* @note the generated Render Nodes are placed into the AllocationCluster,
* together with all the further storage used by those nodes internally.
* The real Render Engine uses an AllocationCluster per Segment of the timeline.
*/
ProcNode&
buildTree()
{
// Prepare a precomputed parameter for the complete tree
auto selectFrameNo = [&](TurnoutSystem& turSys){ return quantSecs (turSys.getNomTime()); };
auto paramSpec = buildParamSpec()
.addSlot (selectFrameNo);
auto accFrameNo = paramSpec.makeAccessor<0>();
// Automation functions using the accessor for the precomputed FrameNo-parameter
auto autoFilter = [&](TurnoutSystem& turSys){ return stepFilter (turSys.get (accFrameNo)); };
auto autoMixer = [&](TurnoutSystem& turSys){ return stepMixer (turSys.get (accFrameNo)); };
// use processing functions from the »TestRand Ontology«
auto testGen = testRand().setupGenerator();
auto testMan = testRand().setupManipulator();
auto testMix = testRand().setupCombinator();
// A source node to produce random test frames
auto& n1s = alloc_.create<ProcNode>(
prepareNode("srcA")
.withAllocator(alloc_)
.preparePort()
.invoke(testGen.procID(), testGen.makeFun()) // params(frameNo, flavour)
.closeParam<1>(SRC_A) // --> fix the flavour ≔ SRC_A
.retrieveParam(accFrameNo)
.completePort()
.build());
// A node to »filter« the data in chain-A
auto& n1f = alloc_.create<ProcNode>(
prepareNode("filterA")
.withAllocator(alloc_)
.preparePort()
.invoke(testMan.procID(), testMan.makeFun())
.attachParamFun(autoFilter) // filter-param <-- autoFilter(frameNo)
.connectLead(n1s)
.completePort()
.build());
// A secondary source Node for the »chain-B«
auto& n2s = alloc_.create<ProcNode>(
prepareNode("srcB")
.withAllocator(alloc_)
.preparePort()
.invoke(testGen.procID(), testGen.makeFun()) // params(frameNo, flavour)
.closeParam<1>(SRC_B) // --> fix the flavour ≔ SRC_B
.retrieveParam(accFrameNo)
.completePort()
.build());
// A mixing Node to combine both chains
auto& mix = alloc_.create<ProcNode>(
prepareNode("mix")
.withAllocator(alloc_)
.preparePort()
.invoke(testMix.procID(), testMix.makeFun())
.attachParamFun(autoMixer) // mixer-param <-- autoMixer(frameNo)
.connectLead(n1f)
.connectLead(n2s)
.completePort()
.build());
// Place a »Param-Agent«-Node on top to pre-compute the FrameNo
auto& parNode = alloc_.create<ProcNode>(
prepareNode("Param")
.withAllocator(alloc_)
.preparePort()
.computeParam(paramSpec)
.delegateLead(mix)
.completePort()
.build());
// the AllocationCluster managed some storage...
CHECK (isLimited (1_KiB, alloc_.numBytes(), 5_KiB));
// here the »Param-Agent« also acts as the Exit-Node
return parNode;
}
};
};

View file

@ -303,7 +303,7 @@ namespace test {
* - the prime goal is to demonstrate correctness, balancing memory demand
* mostly through lock-free queues, without loosing any allocation.
*/
class BufferProviderLoad_test : public Test
class BufferProviderLoad_test : public Test
{
virtual void

View file

@ -92,9 +92,9 @@ namespace test {
CHECK (a2.siz > 555);
// can use that memory without blowing up....
auto doooh = new(a1.mem) f128{std::numeric_limits<f128>::max()};
auto doooh = new(a1.mem) lflp{std::numeric_limits<lflp>::max()};
*doooh /= 2;
CHECK (2 == std::numeric_limits<f128>::max() / *doooh);
CHECK (2 == std::numeric_limits<lflp>::max() / *doooh);
allo.deallocate (a1);
allo.deallocate (a2);

File diff suppressed because it is too large Load diff