Skip to content

feat(daemon): make display mounting a device setting with a UI picker - #252

Merged
hyperb1iss merged 3 commits into
mainfrom
nova/display-mounting
Sep 7, 2026
Merged

feat(daemon): make display mounting a device setting with a UI picker#252
hyperb1iss merged 3 commits into
mainfrom
nova/display-mounting

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

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 through PUT /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

Check Result
cargo test -p hypercolor-daemon --features persistence-test-hooks (full suite) 61 test binaries pass, including the new display_rotation_tests (set through the device route, reported on both summaries, survives a brightness-only update, refused for an LED strip) and automatic_display_output_turns_with_the_device_mount_rotation, which pushes a red/blue split canvas through the JPEG path with a deg180 device and asserts the halves swap
cargo test -p hypercolor-core -p hypercolor-types pass (scene manager, bus, device settings serde round trip with the new field)
cargo clippy --workspace --all-targets --features hypercolor-daemon/persistence-test-hooks -- -D warnings clean
UI crate: cargo test, cargo clippy --lib --tests -- -D warnings pass, including the new wire-token round trip against the daemon's serde spelling
cargo fmt --all -- --check and the UI crate fmt check clean
cd python && uv run ruff check . && uv run pytest -q clean, 149 passed

Persisted files that still carry the old rotation key 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 carried deg180, 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 reads tracked.user_settings.display_rotation, and the viewport signature already includes rotation, so a settings change rebuilds the worker). Then update_device in crates/hypercolor-daemon/src/api/devices/mod.rs for 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

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 15dbc083-9ef2-4005-8134-e19030769d8d

📥 Commits

Reviewing files that changed from the base of the PR and between 6cacd79 and ac00431.

⛔ Files ignored due to path filters (6)
  • python/src/hypercolor/_generated/models/device_summary.py is excluded by !**/_generated/**
  • python/src/hypercolor/_generated/models/display_summary.py is excluded by !**/_generated/**
  • python/src/hypercolor/_generated/models/display_summary_list_item.py is excluded by !**/_generated/**
  • python/src/hypercolor/_generated/models/set_display_face_request.py is excluded by !**/_generated/**
  • python/src/hypercolor/_generated/models/update_device_request.py is excluded by !**/_generated/**
  • python/src/hypercolor/_generated/models/update_display_face_composition_request.py is excluded by !**/_generated/**
📒 Files selected for processing (44)
  • crates/hypercolor-core/src/bus/mod.rs
  • crates/hypercolor-core/src/device/registry.rs
  • crates/hypercolor-core/src/scene/mod.rs
  • crates/hypercolor-core/tests/bus_tests.rs
  • crates/hypercolor-core/tests/device_tests.rs
  • crates/hypercolor-core/tests/scene_manager_default_tests.rs
  • crates/hypercolor-daemon/src/api/controls.rs
  • crates/hypercolor-daemon/src/api/devices/mod.rs
  • crates/hypercolor-daemon/src/api/displays.rs
  • crates/hypercolor-daemon/src/device_settings.rs
  • crates/hypercolor-daemon/src/discovery/device_helpers.rs
  • crates/hypercolor-daemon/src/display_output/mod.rs
  • crates/hypercolor-daemon/src/display_preferences.rs
  • crates/hypercolor-daemon/src/domain/device_binding/persistence_tests.rs
  • crates/hypercolor-daemon/src/domain/display.rs
  • crates/hypercolor-daemon/src/domain/effect/identity/tests.rs
  • crates/hypercolor-daemon/src/domain/scene.rs
  • crates/hypercolor-daemon/src/mcp/tools/displays.rs
  • crates/hypercolor-daemon/src/render_thread/display_lane.rs
  • crates/hypercolor-daemon/src/render_thread/render_zones/tests.rs
  • crates/hypercolor-daemon/tests/api_tests.rs
  • crates/hypercolor-daemon/tests/display_output_tests.rs
  • crates/hypercolor-daemon/tests/display_preferences_tests.rs
  • crates/hypercolor-daemon/tests/display_rotation_tests.rs
  • crates/hypercolor-daemon/tests/domain_display_service_tests.rs
  • crates/hypercolor-daemon/tests/persistence_tests.rs
  • crates/hypercolor-daemon/tests/startup_tests.rs
  • crates/hypercolor-types/src/api/devices.rs
  • crates/hypercolor-types/src/api/displays.rs
  • crates/hypercolor-types/src/device.rs
  • crates/hypercolor-types/src/scene.rs
  • crates/hypercolor-types/tests/device_tests.rs
  • crates/hypercolor-ui/src/api/devices.rs
  • crates/hypercolor-ui/src/api/displays.rs
  • crates/hypercolor-ui/src/components/device_detail.rs
  • crates/hypercolor-ui/src/components/mod.rs
  • crates/hypercolor-ui/src/components/mounting_select.rs
  • crates/hypercolor-ui/src/display_rotation.rs
  • crates/hypercolor-ui/src/lib.rs
  • crates/hypercolor-ui/src/pages/studio/stage.rs
  • crates/hypercolor-ui/tests/display_api_tests.rs
  • crates/hypercolor-ui/tests/display_rotation_tests.rs
  • crates/hypercolor-ui/tests/layout_utils_tests.rs
  • docs/content/api/rest.md

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

hyperb1iss and others added 3 commits September 6, 2026 17:33
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
hyperb1iss force-pushed the nova/display-mounting branch from 4d1115c to ac00431 Compare September 7, 2026 00:33
@hyperb1iss
hyperb1iss merged commit a9ed4b8 into main Sep 7, 2026
34 checks passed
@hyperb1iss
hyperb1iss deleted the nova/display-mounting branch September 7, 2026 04:17
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