feat(daemon): make display mounting a device setting with a UI picker - #252
Merged
Conversation
|
Warning Review limit reached
On-demand reviews are free for the next 14 days. After that, they cost $0.25 per reviewed file. Or wait 49 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (6)
📒 Files selected for processing (44)
Comment |
Wave 3 put a display's mounting rotation on the face target and the
default-face preference, carried it across zone bindings, and exposed it
only through the face composition route. That turned faces but nothing
else: a screen with a media layer and no face had no face assignment for
the route to patch and no target rotation to carry, so media on a panel
installed upside down rendered upside down. The setting was also per
scene zone, so a scene switch could quietly lose it.
Mounting is a fact about the hardware. It now lives in the device's user
settings as display_rotation, persisted beside brightness, reported on
the device summary (display-capable devices only) and the display
summary, and written through PUT /api/v1/devices/{id}, which rejects it
for hardware without a panel. The display output resolver folds the
device rotation into the viewport, which the CPU encoder path and the
GPU finalize shader already apply to the scene underlay and the face
alike, so faces, effects, and media all turn together and the setting
holds across scenes and faces. The face target, the default-face
preference, the bus target, and the face routes lose their rotation
fields.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wLVBLPsmC77AYYibty6Su
There was no way to set a display's orientation from the UI. A shared mounting picker now sits beside brightness in the device detail sidebar and in the Studio screen header next to the preview, for display-capable devices only. It writes the device's display_rotation, tracks the pick optimistically, and refreshes the device and display lists so the Studio preview follows. The wire-token table lives in a leptos-free module with a round-trip test against the daemon's serde spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wLVBLPsmC77AYYibty6Su
Picks up display_rotation on the device summary and update request, rotation on the display summary, and the removal of rotation from the face assignment and composition requests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wLVBLPsmC77AYYibty6Su
hyperb1iss
force-pushed
the
nova/display-mounting
branch
from
September 7, 2026 00:33
4d1115c to
ac00431
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A display mounted upside down needs everything drawn on it turned, and the user needs a place to say so. Neither was true: the wave 3 rotation only turned faces and had no UI.
💡 What changes
Adding a media layer to an inverted Lian Li LCD rendered it upside down while the face on the same panel read upright. The rotation from wave 3 lived on the face target and the default-face preference, was carried across zone bindings at output time, and could only be set through the face composition route. A screen with media and no face has no face assignment for that route to patch and no target rotation to carry, so nothing turned. The setting was also per scene zone, so a scene switch could lose it, and no UI exposed it at all.
Mounting is a fact about the hardware, so it now lives in the device's user settings as
display_rotation, persisted beside brightness. It is written throughPUT /api/v1/devices/{id}(rejected with 422 for hardware without a panel), reported on the device summary for display-capable devices and on the display summary, and folded into the display viewport by the output resolver. The CPU encode path and the GPU finalize shader already apply the viewport turn to the scene underlay and the face alike, so faces, effects, and media all turn together, and the setting holds across scenes and faces. The face target, the default-face preference, the bus target, the face routes, and the MCP face tools lose their rotation fields.The UI gains a mounting picker (Upright, 90° clockwise, Upside down, 90° counter-clockwise) beside brightness in the device detail sidebar and in the Studio screen header next to the preview. It writes the device setting, tracks the pick optimistically, and refreshes the device and display lists so the Studio preview follows. The python client is regenerated for the new fields.
🎯 Invariant
One rotation per physical display, owned by the device, applied at the viewport so every frame path turns the same way. Nothing rendered to a display bypasses the viewport.
🧪 Validation
cargo test -p hypercolor-daemon --features persistence-test-hooks(full suite)display_rotation_tests(set through the device route, reported on both summaries, survives a brightness-only update, refused for an LED strip) andautomatic_display_output_turns_with_the_device_mount_rotation, which pushes a red/blue split canvas through the JPEG path with adeg180device and asserts the halves swapcargo test -p hypercolor-core -p hypercolor-typescargo clippy --workspace --all-targets --features hypercolor-daemon/persistence-test-hooks -- -D warningscargo test,cargo clippy --lib --tests -- -D warningscargo fmt --all -- --checkand the UI crate fmt checkcd python && uv run ruff check . && uv run pytest -qPersisted files that still carry the old
rotationkey on a face target or default-face preference load fine, since those structs do not deny unknown fields; the value is simply dropped. The three Lian Li defaults written during wave 3 testing carrieddeg180, so after this lands the mount for those fans needs setting once from the new picker.🔍 Review focus
Start with the viewport fold in
crates/hypercolor-daemon/src/display_output/mod.rs(the resolver readstracked.user_settings.display_rotation, and the viewport signature already includes rotation, so a settings change rebuilds the worker). Thenupdate_deviceincrates/hypercolor-daemon/src/api/devices/mod.rsfor the display-only guard.📌 Follow-ups
The Lian Li receiver has a hardware rotate command (spec 80 §4.4 hook). Software rotation at the viewport covers every panel uniformly, so the hardware path stays unused for now.
🤖 Generated with Claude Code
https://claude.ai/code/session_018wLVBLPsmC77AYYibty6Su