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.
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
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.
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
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
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.
- build a default value
- handle cyclic scales
- implement clamping behaviour
- identify a muliplicative factor
- join a feed either multiplicative or additive
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
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.
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.
...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?
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.
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.
`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.
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.
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
...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>
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
- `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
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
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.
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).
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....
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.
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...
...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
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
- 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.
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.
- 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
...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
...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.
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)''
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
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''
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
...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''
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)
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.
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
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`....
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`
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.
...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...