Skip to content

dataflow: tell a port's own unit apart from the one its address asks for - #2255

Open
jcelerier wants to merge 1 commit into
masterfrom
dataflow-port-units
Open

dataflow: tell a port's own unit apart from the one its address asks for#2255
jcelerier wants to merge 1 commit into
masterfrom
dataflow-port-units

Conversation

@jcelerier

Copy link
Copy Markdown
Member

The score half of a three-repo change. Needs ossia/libossia#934 and
celtera/avendish#200 first
— the submodule pointers are deliberately not bumped
here, so this branch does not build until those two land.

Where this started

A report of "an occasional bug when setting an OSC address that has a unit
(orientation.quaternion) as the input of a micromap". Reproduced with the JS
scripting API and oscsend: two processes reading one OSC address, re-point
either one, and the other silently stops receiving for the rest of the session.
The root cause is a reference count in libossia; this repo contributed to it.

What changes here

register_inlet_impl registered its port with the execution state twice — once
inside set_destination_impl, once explicitly — while teardown unregisters once.
That happened to compensate for the off-by-one in message_queue::reg(), so the
common case balanced and the bug only showed when an address was bound while
playing. With libossia fixed, the extra registration is simply wrong and is
removed.

A port had two opinions about its unit and one field to hold them. What the
process declares and what the address asks for with @[unit] were both written
into the execution port's type, so setting an address destroyed the declaration,
clearing one wiped it entirely, and a new address without a qualifier kept the old
unit. set_destination_impl now writes address_unit — unconditionally, the way
it already did for the index — so dropping a qualifier gives the declaration back.

Process::Port gains a declared unit, held as a State::Unit so Port.hpp
does not pull in the dataspace headers. It is virtual because a port loaded from a
saved document is built by the inherited deserializing constructors, which run no
subclass code. HSVSlider overrides it and its setupExecution is deleted;
register_inlet / register_outlet push the declaration to the execution port
once. A unit the node declared for itself wins over the widget's — an avnd colour
port knows whether it is rgb or rgba — while a plain value type does not.

ControlSurface reads the declaration from the model port instead of from
whatever setupExecution left behind. That gives its outlet — the port that
actually carries an address there — the unit it never had, and removes the
in_exec scheduling hack whose comment said it existed only because
set_destination overwrote the type.

Testing

  • test_unit_dataflow_value gains A port's declared unit: a plain control
    declares nothing; a colour control declares rgba, including through a
    Process::Port&, which is how the execution setup asks for it; setUnit for
    per-instance units; and an HSVSlider serialized and reloaded through
    Process::load_inlet still declares rgba.
  • Avnd_port_unit_Test (new) pins the avendish-side deduction: arity for
    xy / xyz / xyzw and rgb / rgba, and halp_meta(unit, …) resolving.
  • End to end through --script: two micromaps on osc:/q@[orientation.quaternion],
    re-point one, the other keeps receiving (it did not, before); a colour control
    bound to a color.hsv address converts, and still converts after its address
    is cleared and re-bound
    , which is the declaration surviving; an outlet in
    metres into a millimetre address sends 2000 where it sent 2; 1900 address
    changes under OSC traffic with no crash.
  • Green: test_unit_dataflow_value (70), Avnd_port_unit_Test (16),
    Avnd_point_tracker_Test (535), Avnd_ossia_value_Test,
    test_unit_port_address_list, test_unit_port_drop, and
    test_unit_automation_values / test_unit_mapping_values case by case (the
    whole-binary SIGSEGV there is the known pre-existing multiple-run_in_app
    problem on Windows, unrelated).

Known and not addressed

  • State::Unit's constructor heap-allocates, so this costs one small allocation
    per port. Dropping m_unit and leaving unit() purely virtual would remove it —
    every current user is a fixed-unit port — at the cost of the per-instance case.
  • The new virtual changes Process::Port's vtable: out-of-tree addons built
    against an older SDK need a rebuild.
  • The Gradient process still smuggles its declared unit through a qualifier on an
    otherwise empty address, which is serialized. Migrating it to setUnit() is a
    model change and is left out.
  • The score-side wiring above is covered by the end-to-end scripts rather than by
    the test suite; a real test needs an Execution::Context, which nothing in
    tests/ builds today. Execution::DocumentPlugin::setupContext and
    SetupContext::inlets are public, so it is reachable — it just needs playback
    and one case per binary here.

🤖 Generated with Claude Code

https://claude.ai/code/session_0197hSihNtB1bPw9WDUV3SR2

A port has two possible opinions about what its values are: the process
declares one (a colour control is rgba, a gain decibels) and the port's
address can override it with @[unit]. Both were written into the same field
on the execution port, so setting an address destroyed the process's own
declaration, clearing one left nothing at all behind, and a new address
without a qualifier kept the previous unit. libossia now keeps them apart;
this side stops conflating them.

Process::Port gains a declared unit, held as a State::Unit so that Port.hpp
does not have to include the dataspace headers. It is virtual because a port
that comes back from a saved document is built by the inherited
deserializing constructors, which run no subclass code: HSVSlider overrides
it and its setupExecution goes away, and register_inlet / register_outlet
push the declaration to the execution port once. A unit the node declared
for itself is more precise than the widget's - an avnd colour port knows
whether it is rgb or rgba - so it wins; a plain value type does not.

set_destination_impl writes address_unit, unconditionally the way it already
did for the index, so dropping a qualifier gives the declaration back.
ControlSurface reads the declaration from the model port rather than from
whatever setupExecution left behind, which also gives its outlet - the port
that actually carries an address there - the unit it never had, and removes
the scheduling hack that existed only because set_destination overwrote the
type.

register_inlet_impl also registered its port with the execution state a
second time, on top of the one inside set_destination_impl, while teardown
unregisters once. That compensated for an off-by-one in libossia's
message_queue; with that fixed, the extra registration is just wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197hSihNtB1bPw9WDUV3SR2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant