Skip to content

fix(render): keep each display's backdrop on the zone that places it - #440

Merged
hyperb1iss merged 2 commits into
mainfrom
nova/display-backdrop-owner
Oct 21, 2026
Merged

hyperb1iss merged 2 commits into
mainfrom
nova/display-backdrop-owner

Conversation

@hyperb1iss

@hyperb1iss hyperb1iss commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

An LCD with no face layers shows its slice of the scene canvas as its backdrop, and in a multi-zone scene a later zone could paint over that slice. On the owner's rig, putting the Ableton Push 2 in its own zone with a Touch Ripple layer made pad ripples show up partially on the Lian Li fan LCDs and the Corsair pump LCD. With this PR, a display's box in the scene canvas always shows the zone that places that display, whatever LED outputs from other zones overlap it.

💡 What was happening

The scene canvas is a projection: each scene zone contributes one layer per output in its layout, copying the zone's texture into that output's placed box over an opaque black base, in zone order (append_projection_composition_layers_for_zone in render_zones/projection.rs). The primary zone places the case's LED devices and the display outputs. Custom LED zones come later. Wherever a later zone's LED output overlapped a display's box in the layout, the later zone won, and the display showed the other zone's content. Spec 86 §5.1 called overlapping outputs "a harmless detail of a preview image". That stops being true once display backdrops read the scene canvas: those pixels land on hardware.

The overlap is ordinary. A desk device like the Push 2 has no physical relationship to the case, so its layout boxes sit wherever there is room, and on the owner's layout that is on top of the LCDs. It stayed invisible while every device lived in one zone, because every overlap painted the same content.

🛠️ What changes

The scene canvas now composes in two strata over the black base: every LED output first, in zone order, then every display output, also in zone order. Display outputs are identified with the predicate the LED sampler and device routing already use (is_led_sampled_zone, false for the Display segment), cached per output as a ProjectionStratum when projection metadata is built, so the cache already rebuilds when a layout changes.

Every path that builds the scene canvas follows the same order:

Path Change
GPU projected scene (render_pass.rs, projection.rs) LED layers insert at a display_layer_start boundary kept on ProjectedSceneFrames; display layers append after it. The capacity check runs before any layer lands, so steady frames still allocate nothing and stay GPU resident.
CPU scene composition (compose_authoritative_scene_canvas) Two passes in the same order, including the fallback with no cached projection metadata.
Full-scene identity fast path The copy that stands in for a zone's single output now runs only in that output's stratum.
Single full-scene zone shortcut Unchanged, with a comment: with one contributing zone, every box shows that zone's own pixels.

LED-versus-LED overlaps keep zone order, which only ever reaches preview images. Spec 86 §5.1 now states the two-stratum rule and why.

🧪 Validation

Six new tests in render_zones/tests/projection.rs use an 8x8 scene: a red zone places a display in the top-left quadrant and an LED output in the bottom-right, and a later blue zone places one LED output across the centre that overlaps both. They check that the display box stays red where blue overlaps it, blue shows outside the display box, the later zone wins LED-versus-LED overlap, and uncovered pixels stay black. They cover the CPU path without metadata, a full render_scene on the CPU compositor, the identity fast path, layer ordering, a full render_scene on the GPU that asserts a GPU-resident frame, and two overlapping displays keeping zone order. With the classifier forced back to the old single stratum, five of them fail (display box at (2, 2) ... left: [0, 0, 255] right: [255, 0, 0]); the display-versus-display test passes either way, as intended.

Command Result
cargo fmt --all -- --check clean
clippy -p hypercolor-daemon --all-targets -D warnings, --no-default-features --lib, --no-default-features --features wgpu,persistence-test-hooks --all-targets clean
cargo test -p hypercolor-daemon --lib -- --test-threads=1 render_zones with HYPERCOLOR_REQUIRE_GPU_TESTS=1 120 passed
Full daemon lib suite with persistence-test-hooks, serial 1271 passed
--test render_thread_tests 65 passed
display_output_tests, canvas_capture_tests, display_preview_tests 44, 12, 6 passed

An independent verifier passed it. Its own probes showed the cached CPU path, the no-metadata fallback, and CPU-composed appended layers are byte-identical on a rotated three-zone scene, a four-frame GPU render_scene matches the CPU reference pixel for pixel with flat allocation counters, and a refused append leaves the layers and the boundary untouched. The extra Vec::insert shifting costs about 1 µs per frame in a worst case of 8 displays ahead of 80 LED outputs.

On hardware: the owner's rig runs this change on top of #438, with the Push 2 and ROLI touch zones over a primary zone that places three fan LCDs, the pump LCD, and the iCUE LINK LCD.

⚠️ Not covered

  • A display output counts only when its segment is named Display. Every HAL display driver and the simulator use that name, and the UI copies the segment name into layouts, so this needs a hand-edited or legacy layout to miss.
  • A device whose display output is not placed in the layout falls back to a full-canvas viewport, so its LCD shows the whole scene regardless of ordering. That is existing behaviour and a placement choice.
  • If the zone that places a display has no enabled layers, or a GPU-resident owner zone has not produced its first frame yet (an HTML effect still loading), its display box gets no display layer for that frame, and overlapping LED outputs can still show there.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved scene composition so LED effects render before display backdrops.
    • Display backdrops now remain associated with their owning zones, including when zones overlap.
    • Preserved consistent ordering across CPU and GPU rendering paths.
  • Documentation

    • Updated rendering documentation to describe LED and display layer ordering.

hyperb1iss and others added 2 commits October 19, 2026 21:26
A display whose zone has no face layers shows its slice of the scene
canvas as its backdrop, and the scene canvas composed every output in
zone order. A later zone's LED outputs that overlapped a display's box
painted that zone's effect onto the physical display, so a custom
zone's ripple layer showed up on LCDs the primary zone places. It hid
while every device sat in one zone, because every overlap painted the
same content.

Projection now composes in two strata over the base: every LED output
in zone order, then every display output in zone order. Display outputs
are the ones is_led_sampled_zone rejects, the test the LED sampler and
device routing already use. The GPU projected path inserts LED layers
at a boundary index inside the admitted layer scratch, so frames stay
allocation free and GPU resident. The CPU reconstruction runs the same
two passes, and its full-scene identity copy runs in the stratum of the
one output it stands in for. The single full-scene zone path needs no
ordering, since one zone places every output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Spec 86 §5.1 carried over Spec 64's view that zone-order overwrite in
the scene canvas is a harmless detail of a preview image. That holds
for LED outputs only. A display's box in the scene canvas is that
display's backdrop, so it reaches hardware, and a later zone's LED
output overlapping it painted the wrong zone onto the display.

The section now states the two-stratum order (LED outputs first, then
display outputs, each in zone order), the predicate that identifies a
display output, why the box has to show the zone that places the
display, and how the CPU reconstruction and the single full-scene zone
path line up with it.

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

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a5768127-3625-4e95-9f45-435d6eebd831
📥 Commits

Reviewing files that changed from the base of the PR and between ce477c8 and d010677.

📒 Files selected for processing: 5
  • crates/hypercolor-daemon/src/render_thread/render_zones/projection.rs
  • crates/hypercolor-daemon/src/render_thread/render_zones/render_pass.rs
  • crates/hypercolor-daemon/src/render_thread/render_zones/scene_assembly.rs
  • crates/hypercolor-daemon/src/render_thread/render_zones/tests/projection.rs
  • docs/specs/86-gpu-resident-multi-zone.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

Changes

The projection pipeline now separates LED outputs from display backdrops. It composes LED layers first and display layers afterward. CPU and GPU paths share a tracked display-layer boundary. Tests and specifications cover ordering, ownership, overlap, and GPU-resident rendering.

Projection ordering

Layer / File(s) Summary
Projection strata and composition
crates/hypercolor-daemon/src/render_thread/render_zones/projection.rs
Cached projections classify outputs as LED or display strata. Scene-canvas composition and composition-layer insertion preserve LED-then-display ordering.
Render-pass boundary wiring
crates/hypercolor-daemon/src/render_thread/render_zones/render_pass.rs
ProjectedSceneFrames tracks display_layer_start. CPU and GPU projection paths pass and update the boundary.
Projection coverage and specification
crates/hypercolor-daemon/src/render_thread/render_zones/tests/projection.rs, crates/hypercolor-daemon/src/render_thread/render_zones/scene_assembly.rs, docs/specs/86-gpu-resident-multi-zone.md
Tests cover backdrop ownership, overlapping outputs, identity projections, GPU residency, and pixel results. Documentation records the two-stratum ordering and single-zone path.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant SceneProjection
  participant ProjectionStratum
  participant RenderPass
  participant SceneLayers
  SceneProjection->>ProjectionStratum: classify LED and display outputs
  SceneProjection->>RenderPass: pass display_layer_start
  RenderPass->>SceneLayers: insert LED layers before display layers
  RenderPass->>SceneLayers: append display backdrop layers
Loading
Merge Risk: ⚪ Minimal · up to d0106

No actionable merge blocker is established. The change is mergeable after normal checks.

Security architecture risk: ⚪ Minimal · up to d0106

The change prevents overlapping LED content from replacing a display’s backdrop. The reviewed paths preserve existing permissions and per-zone LED sources, and failed rendering does not publish partially composed output. No material security risk was identified in this change.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected change affects scene-image precedence, including display backdrops. It does not redirect LED sampling to that shared image or expand the inspected helpers’ authority beyond the renderer.

Trust Boundaries and Controls

  • observed — Stratum selection follows the existing output-name convention, not an authentication or authorization decision. The new ordering prevents LED overlap from painting over display backdrops but does not establish a security boundary between independently configured outputs.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: preserving each display backdrop on the zone that places it.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 4 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@hyperb1iss
hyperb1iss merged commit 0a6d7cd into main Oct 21, 2026
43 checks passed
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