Skip to content

fix(scene): carry display zones across the driver-scoped fingerprint change - #414

Draft
hyperb1iss wants to merge 4 commits into
mainfrom
nova/display-zone-fingerprint-remap
Draft

hyperb1iss wants to merge 4 commits into
mainfrom
nova/display-zone-fingerprint-remap

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

Held as a draft until 0.7.0 ships. It is ready for review now.

Since the August change that added the driver segment to every device fingerprint (31e0d5d51), two displays on the owner's rig have been split in two. The Ableton Push 2 and the Corsair iCUE LINK LCD each kept a display zone pointing at the device id their old fingerprint hashed to, and the daemon minted a fresh, empty zone for their new ids. The old LCD zone still carried the configured face (Pulse Temp with custom fonts), so that face silently fell off the pump screen while the scene went on listing four display zones for two displays. After this PR, the next clean discovery sweep moves those zones to the live ids, collapses each duplicate pair onto the zone that carries a face, and tells connected clients, so the LCD gets its face back without anyone touching it.

What this changes

  • Legacy id derivation. The binding remap takes every present device's fingerprint, drops its driver segment to recover the pre-August spelling, and maps that id to the live one only when a saved display zone still points at it. It skips any legacy id that two present devices would produce, or that a present device still uses as its current id.
  • When the sweep runs. The remap used to run only when layout ids changed. These two devices have layout ids that read identically in both formats, because the layout id strips the owner prefix, so their scene state was never carried over. The sweep now also runs when only physical ids change, and the migration journal accepts that case.
  • Collisions. A new core/src/scene/display_collisions.rs collapses display zones that end up targeting the same display. A zone with layers beats an empty one. When both have layers, the zone whose newest layer was assigned most recently wins (layer ids are UUIDv7 and every face assignment mints one). Otherwise the remapped legacy zone wins. Each collapse is logged at info. Fallback references to a removed zone are redirected to the survivor.
  • Client notification. Migration commits now carry their real events: ZoneChanged for each zone updated or removed, ZoneChanged for runtime default faces, and SceneLibraryChanged for each saved scene the rewrite touched, released through the commit ticket in order. Before this, binding and layout-id migrations advanced the scene revision with an empty event batch, so an open UI or TUI kept showing removed zones and failed its next write with 404 or 412.
  • Device settings are left alone. The LCD's settings row is keyed by its USB serial claim, which reads exactly like its old fingerprint, so remapping settings keys would move a correct row away.

Why

Display zones are retained across disconnects on purpose, so a device that comes back finds its face waiting. These two zones could never be matched again, because nothing would ever produce their ids, so retention preserved them forever without ever applying them. Fixing the migration, rather than pruning orphans, recovers the configuration the owner actually set.

Found while rehearsing the demo film: the rehearsal listed the extra zones, and the pump screen showed no face.

Verification

  • Added or updated tests
  • Added or updated docs (README, AGENTS.md, relevant spec, or guide)
  • just verify passes locally (Rust fmt + lint + test)
  • just deny passes (required for dependency or license changes)
  • just ui-test and just ui-build pass (required for crates/hypercolor-ui/)
  • just sdk-lint, just sdk-check, and just sdk-build pass (required for sdk/)
  • just python-verify passes (required for python/)
  • just compat-check passes (required for data/drivers/vendors/*.toml)
  • just docs-build passes (required for docs or README changes)
  • cd docs && zola check passes (required for docs link/content changes)
  • Packaging scripts were syntax-checked (required for scripts/ or packaging/)
  • just e2e-build passes with the normal Servo stack (required for daemon/UI/effect integration changes)
  • just e2e-build-cpu passes when validating the CPU smoke fallback
  • just e2e passes against the Servo stack (required for end-to-end behavior changes; starts daemon/browser)
  • Tested on real hardware, simulator, or e2e harness (describe below)

The regression test reproduces the live shape through a full discovery sweep: saved display zones pointing at the legacy hashes of a Push 2 and a serial-claimed LCD (one carrying a face), fresh empty zones for the current ids, and devices discovered under driver-scoped fingerprints. It asserts the remap, the collapse that keeps the face, the exact ordered event batch with a single revision bump, and a second sweep that changes and emits nothing. On the base this test fails, because base reconciliation plans no remap for these layouts and returns early, leaving all four zones.

Check Result
discovery_tests, single-threaded 18 passed (rerun independently after the last commit)
daemon --lib, single-threaded 1187 passed
internal_api_surface_tests 29 passed
core scene tests, including scene_device_binding_remap_tests pass (two html_loader_tests need generated effect files and fail without them, unrelated)
clippy -D warnings on core and daemon (--all-targets), cargo check --workspace, fmt pass

The full just verify gate deadlocks on this machine's GPU tests in parallel, so the daemon suite ran single-threaded instead. The computed ids match the four observed on the rig. The live rig itself has not run this build: the migration needs a clean full sweep with no scan errors, so it applies on the next daemon start that sees every device.

Notes for reviewers

  • Start at the legacy-id derivation in domain/device_binding.rs. An independent review checked it against every fingerprint shape in DeviceIdentifier::fingerprint (USB serial, USB and HID path fallback, SMBus, network MAC, generic bridges) and found no wrong-device mapping. It does not cover drivers whose fingerprint changed format in other ways in the same commit: Hue, Nanoleaf, and the ROLI blocksd to roli rename. None of those exposes a display today.
  • Recency means "face assigned most recently", not "configured most recently": a control edit keeps its layer id, so it does not count.
  • Downgrade: an older binary refuses a migration journal that holds only physical-id changes, so it matters only if a migration is interrupted and the daemon is then downgraded.
  • The same August change stranded other per-device state this PR deliberately does not move, because moving it would visibly change the rig: WLED-Gledopto's attachment profile and a settings row (brightness 0.58) under its old id, and stale Nollie and PrismRGB attachment rows. The effect-id migration has the same empty-event-batch silence fixed here and is tracked separately.

🤖 Generated with Claude Code

hyperb1iss and others added 4 commits October 12, 2026 16:12
When a display's physical identity is remapped, the zone authored
against its old identity lands on a display that may already own an
empty surface minted for the new identity. Scene validation rejects two
display zones for one device, so the remap now keeps exactly one.

The zone carrying layers survives. When both carry layers, the one
whose newest layer was assigned most recently wins, read from the
UUIDv7 layer ids that every face assignment mints; when that is not
knowable the remapped zone wins. Each collapse is logged at info with
the reason. Runtime default faces drop a retargeted duplicate in favor
of the default already registered, matching how the display preference
store resolves the same collision.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…prints

Adding the driver segment to physical fingerprints rehashed every
physical DeviceId once. Layout reconciliation never noticed displays
like the Push 2 and the iCUE LINK LCD, because their layout ids read the
same in both encodings, and the sweep only ran when a layout id moved.
Scenes kept display zones pointing at dead ids while fresh empty
surfaces were minted for the live ones, so configured faces fell off
the screen.

The pre-scoping id is a pure function of the current fingerprint:
{namespace}:{driver}:{key} was {namespace}:{key}. The sweep now derives
it for every present device and maps it to the live id whenever a
persisted display zone still targets it. An id two present devices
derive, or one a present device still owns, is left alone. The sweep
runs when only physical identities move, through the existing journal
and transaction, so every participant and the scene store are rewritten
by their usual writers, and the scene remap collapses the resulting
duplicate display zones. Once nothing targets an old id the pass plans
nothing, so later sweeps are no-ops.

Device settings are deliberately left out: a claimed USB device's
settings key is its portable claim, which reads exactly like its old
fingerprint, so mapping those keys would move canonical rows away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drives two complete discovery sweeps over a named scene shaped like the
rig that lost its LCD face: display zones authored against Push 2 and
iCUE LINK LCD ids minted before driver-scoped fingerprints, the LCD's
carrying its face, plus empty surfaces for the ids discovery reports
now. The LCD is discovered with a portable serial claim, as on real
hardware, which keeps it out of the shape-based reconciler.

The first sweep must leave one display zone per display, the face
intact on the live LCD, the scene valid and the journal cleared. The
second sweep must not advance the scene revision or change the scene.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The device binding migration published its scene commit with an empty
event batch, so the revision advanced while nothing told observers. The
web UI and the TUI refetch scenes only on scene event hints or a
reconnect, so when displays were already connected and an earlier sweep
had deferred the migration, open clients kept the collapsed zones and
the old display targets and failed their next write against them. The
layout identity remap shared the same silent publication.

Preparing the migration now diffs the live scene state against the
candidate it installs and carries the result through admission and
persistence to the commit ticket, which releases it in admission order
like any other mutation: ZoneChanged for each zone the rewrite updated,
created, or removed, ZoneChanged against the active scene for runtime
default faces as their overlay path announces them, and
SceneLibraryChanged for every saved scene the rewrite touched.

ZoneChanged and SceneLibraryChanged carry no revision field, so the
sweep test ties the batch to its commit by asserting the migration is
exactly one scene commit and that a settled sweep emits nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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