Repository navigation
Releases: hyperb1iss/hypercolor
Release list
Hypercolor 0.6.2
Release Notes v0.6.2
Released: 2026-10-07
This release fixes the Windows startup failure that made 0.6.1 unusable on every Windows install. FXC, the DX12 shader compiler wgpu uses on Windows, expanded the default workgroup zero-init of the 256-entry WideRgb scan array into per-element stores and spent roughly 17 seconds on each of the three scan kernels grouping them (commit 302de9f3); the resulting stall held 0.6.1 startup for 54 s on the reference desktop, well past the app supervisor's 20 second health budget. Alongside that, macOS is back with its first build since 0.3.2 (Apple silicon only), the workspace becomes publishable on crates.io, and the CI compiler cache moves to Cloudflare R2.
🌟 Highlights
⚡️ Windows daemon starts in seconds instead of a minute
gpu_area_sat.rs now compiles the area SAT and hierarchy kernels with zero_initialize_workgroup_memory: false through a new scan_compilation_options(). Every lane writes its own scan_values slot before the first barrier and only reads slots already written, so the fill was dead work on every backend. Measured on an RTX 4070 SUPER: scan_horizontal_tiles drops from 18017 ms to 71 ms, and daemon startup to healthy drops from 58-87 s to 3.7 s. Both WGSL files now document the write-before-read invariant the change depends on.
✅ Windows GPU tests actually run in CI
No CI lane had a GPU adapter and the Windows job built the daemon without wgpu, so no SparkleFlinger shader was ever compiled on Windows before a release went out. A new Rust Windows GPU job builds the daemon with only the wgpu feature, forces the WARP software adapter, pins FXC so the lane compiles shaders the way installs do, and requires GPU tests instead of letting them skip. The pipeline warmup budget test (gpu/tests/warmup.rs, 30 s budget) runs alone in its own step so other shader compiles cannot skew the measurement, and create-release now waits for the job.
🔥 macOS ships for Apple silicon only
The macos-26-intel native app build took about three hours per release and a runner died mid-build during 0.6.1. build-native-app and sign-macos now carry a single macos-arm64 entry, attach-macos requires exactly the arm64 tarball, its checksum, the arm64 DMG, and the DMG's notarization receipt, and both the formula and the cask declare depends_on arch: :arm64.
The arm64 build shipped notarized: Apple accepted both the app and the DMG, and Homebrew installs 0.6.2 through the formula and the cask on macOS 15.2 or newer. See Upgrade notes.
📦 The workspace publishes to crates.io
The CLI package in crates/hypercolor-cli is renamed from hypercolor-cli to hypercolor, so cargo install hypercolor installs the hypercolor binary. Every internal dependency gained an explicit version requirement (33 entries in [workspace.dependencies] plus 12 direct path dependencies), and scripts/set-version.ts stamps and verifies those requirements alongside the workspace version so a stale requirement cannot publish a crate asking for the previous release of its siblings.
🐛 Dashboard gauges and uptime stop freezing
Three separate stalls in the desktop dashboard are fixed: gauge exponential moving averages now step on every metrics sample via ema_step in fps_display, the uptime pill is anchored to its arrival time and advanced from the wall clock, and both track a new metrics_tick counter that bumps before the payload equality gate, so a paused render loop sending identical payloads no longer stalls the display.
⚡️ Windows and GPU
- Skip workgroup zero-init in the area SAT and hierarchy scan kernels; the WGSL in
area_sat.wgslandarea_hierarchy.wgsldocuments the invariant that makes it safe - New SparkleFlinger GPU CI job on a DX12 WARP runner, running one process per test under nextest without fail-fast
gpu/tests/warmup.rsasserts pipeline warmup fits the daemon startup budget, with a failure message pointing atRUST_LOG=wgpu_core=trace,wgpu_hal=traceto find a stalling shader- Test display readbacks get more room on slow adapters, and pipeline warmup is gated on the shipped DX12 compiler
macos-gpu-interopgates the capture cache owner constructor; the macOS screen test unwraps the wgpu 30 mapped range
🐛 Desktop app and UI
- Preview frame cadence:
should_publish_preview_framerounded a target rate up to a whole millisecond interval, so 30 fps became 34 ms and the gate refused every other frame of a ~33 ms render loop, delivering 15-19 fps to clients asking for 30. The gate now publishes once per1/fpsslot of the uptime clock using a fixed-point slot index that stays exact past u32 milliseconds of uptime - Gauge averages no longer freeze when an fps source locks to its target (Engine stuck at 53.3 against a steady 60)
StatusPilltakes its value as a signal;Stringand&strcallers convert unchanged- The dev macOS app bundle builds only the
.app
📦 Packaging and distribution
- Homebrew carry-forward:
update-homebrewwaits forattach-macosto finish rather than succeed, still fenced bycreate-release. Linux checksums are always required; macOS takes one of two complete forms, the release's own tarball and DMG, or no macOS assets at all, in which case the renderer carries the macOS stanza forward from the tap. A release carrying only one of the two macOS assets stops the job --withdraw-macos: the renderer gains a withdrawn macOS state that replaces theon_macosdownload block with a fatalNotarizedMacosBuildRequirement, so a macOS install or upgrade fails with a reason instead of resolving the 0.3.2 build still in the tap.readPublishedMacosrecognises the withdrawn stanza, so carry mode keeps it withdrawn and the next release with a notarized build renders macOS and the cask in full again- Installers refuse Intel Macs before downloading:
get-hypercolor.shandinstall-release.shstop with a clear message instead of 404ing on a missing asset. All three of the installers andverify-release-artifact.shasksysctl hw.optional.arm64first so a shell under Rosetta is treated as Apple silicon, with the lookup keepingPATHfirst and falling back to/usr/sbin - Bundled data moved inside the crates that read it:
data/attachments/builtin/becomescrates/hypercolor-core/attachments/anddata/openrgb/detectors.tomlbecomescrates/hypercolor-openrgb-host/data/detectors.toml. A crates.io tarball carries only the crate, so the old layout madeopenrgb-hostfail to compile from its package and silently gavecorean empty attachment catalog. Core's build script now fails when the folder is missing or holds no templates - Three crates stay off crates.io by design:
hypercolor-daemon,hypercolor-app, andhypercolor-windows-helper, each of which bundles files from outside its crate directory or only makes sense as a signed build
🔧 CI and build caching
- sccache on Cloudflare R2: the Actions cache hit its configured storage budget and turned read-only, so tag and release builds started cold (the Windows native app build for 0.6.1 hit 2% of 2,733 compile requests). R2 holds the compiler cache outside that budget with no egress charges. The action enables R2 only after a signed HEAD request proves the endpoint answers and accepts the key, otherwise it logs a warning and falls back to the local disk cache. Credentials go to curl on stdin and are masked
- Write access follows the rule the Actions cache already enforced:
READ_WRITEfor pushes, dispatches, and schedules on the default branch,READ_ONLYfor every pull request, tag, and opted-out lane. Fork pull requests receive no secrets - Source mtime refresh after cache restore: artifacts without source metadata are invalidated, malformed snapshots are rejected, refreshed times stay reusable and strictly newer on hosted runners
- The Windows target archive is keyed by dependencies rather than commit
- Rust tests now run when the release installers change; the macOS workspace lane checks every target
- Docs added for the CI cache layers, the shared R2 cache, and write errors in read-only runs
✅ Tests
- Layout cancellation tests wait on a new
AfterWorkflowtest hook instead of pollinglayouts.jsonunder a fixed five second deadline, which was flaking on loaded Windows runners and re-opening the files the workflow was atomically replacing. Outside thepersistence-test-hooksfeature the hooks are a ready future, so production builds pay nothing. The four cancellation tests passed 200 consecutive runs with every core pinned - The Servo CSS probe matrix now compiles on macOS, still opt-in behind
HYPERCOLOR_RUN_SERVO_CSS_PROBES, so thewebgl2-clearprobe guarding the Servo 0.6 webgl feature can run on a Mac. Verified on an M3 Pro: all eight probes pass at both display sizes
Breaking changes
Intel Macs are no longer supported
The release no longer carries a macos-amd64 tarball or an -x86_64.dmg. The curl installers stop with "Intel Macs are not supported; Hypercolor for macOS requires Apple silicon" before any download, and Homebrew refuses Intel Macs with its own architecture message from depends_on arch: :arm64. Uninstall still works on any Mac, since install-release.sh refuses only on the install path. Intel hosts still map to macos-amd64 in verify-release-artifact.sh, which keeps local source builds verifiable there.
The CLI crate publishes as hypercolor
The package in crates/hypercolor-cli is now named hypercolor. The library target is pinned to hypercolor_cli, so every use hypercolor_cli:: path stays valid, including extension crates built on run_with_extensions. A downstream workspace that names the dependency hypercolor-cli needs package = "hypercolor" on that entry and nothing else. Any -p hypercolor-cli or --exclude hypercolor-cli in your own scripts needs the new package name.
Bundled data paths moved...
Hypercolor 0.6.1
Release Notes v0.6.1
Released: 2026-10-01
A follow-up release focused on Servo render latency, GPU import safety on Linux, and two deployment gaps: a headless Docker runtime for server installs and the missing Visual C++ runtime on Windows. The Servo worker no longer polls on fixed sleeps, Linux GPU imports refuse to run across mismatched devices, and Windows installs work on machines without the VC++ redistributable.
🌟 Highlights
⚡ Servo worker driven by wakeups instead of 1ms sleeps
The Servo worker previously polled with std::thread::sleep(Duration::from_millis(1)) while waiting on script evaluation, page loads, and memory reports. Servo now receives an EventLoopWaker (worker/wake.rs), and the worker blocks on a latched condvar (ServoWakeSignal::wait_until) that Servo signals whenever it queues embedder work. The memory-report reply, which bypasses the embedder channels, uses WakingSender so it wakes the worker both when it delivers and when it is dropped undelivered.
Debug-build bench at 640x480, 60Hz, 600 frames: Bubble Garden (rAF WebGL) eval 2.13ms -> 0.13ms, worker frame 3.7ms -> 1.2ms; Digital Rain (host-driven canvas2D) eval 2.17ms -> 0.55ms, worker frame 3.6ms -> 1.4ms. The same measurement showed duplicate frames moving from 0-3 to 0-9 across repeat runs, so rAF-driven pages can still occasionally repeat a frame.
🎞️ Render-tick frame clock replaces Servo's 120Hz timer thread
New worker/frame_clock.rs adds ServoFrameClock, a RefreshDriver whose frames start on the worker's own render tick, and FrameClockRenderingContext, a wrapper that forwards every other RenderingContext method to the platform context so GPU import, readback, and WebGL surface plumbing are unchanged. Frame readiness is no longer held back waiting on a free-running timer.
Because Servo's RenderingContext trait names surfman, glow, and euclid types in its signatures, hypercolor-core gained three optional dependencies behind the servo feature: euclid, glow, and surfman. The wakeup synchronization itself uses only stdlib Arc/Mutex/Condvar.
🐛 Linux GPU imports refuse mismatched devices
crates/hypercolor-linux-gpu-interop/src/linux/device_identity.rs compares the device UUID and driver UUID reported by GL (GL_DEVICE_UUID_EXT, GL_DRIVER_UUID_EXT) against Vulkan's VkPhysicalDeviceIDProperties before allocating shared memory. Previously an import could succeed syntactically while GL ran on llvmpipe or an iGPU and Vulkan ran elsewhere, producing garbage frames with no signal. The mismatch now fails fast and falls back to CPU readback.
🐳 Headless Docker runtime for network lighting
New packaging/docker/Dockerfile, compose.yaml, and compose.gpu.yaml ship a headless daemon on ubuntu:24.04 that renders HTML effects through Servo on Mesa's surfaceless EGL and drives WLED controllers over DDP. Published to ghcr.io/hyperb1iss/hypercolor for amd64 and arm64.
🔧 Windows binaries ship the VC++ runtime they link
hypercolor-daemon.exe, hypercolor.exe, hypercolor-windows-helper.exe, hypercolor-smbus-service.exe, and the mozangle libEGL.dll/libGLESv2.dll all link the MSVC runtime dynamically, but no runtime DLLs were bundled. On a Windows machine without the Visual C++ Redistributable the daemon could not load, never bound its port, and was killed and respawned by the supervisor every 20 seconds, while the UI reported "verified Hypercolor connection is unavailable". Build runners always carry the redistributable, which is why CI never caught it, and every earlier Windows release has the same gap.
⚡ Servo Rendering
ServoWakeSignallatches wakeups, so a signal raised while the worker was spinning Servo's event loop returns immediately and no message slips between a spin and a wait.await_animation_frame()waits up toANIMATION_FRAME_GRACE(2ms) for an animating page that still owes a frame, woken by Servo rather than by a timer. Sessions that missANIMATION_FRAME_MISS_LIMIT(3) consecutive frames stop waiting and re-arm when a frame arrives on its own.- New
frame_wait_usfield onServoRenderStageTimings, alongsideevaluate_scripts_us,event_loop_us,paint_us,readback_us, andtotal_us. The animation-frame wait is timed separately from the event-loop stage. - Each session now owns an
Rc<ServoFrameClock>, wired at session creation throughFrameClockRenderingContext::new()and ticked viaframe_clock.start_frame().
🐛 Linux GPU Interop
- Added
GpuFrameImportFallbackReason::DeviceUuidMismatch(code 27, labeldevice_uuid_mismatch) inhypercolor-gpu-frame, andLinuxGpuInteropError::DeviceUuidMismatch { gl, vulkan }carrying both formatted UUID sets for diagnosis. - The mismatch is treated as non-transient: auto mode settles on CPU readback instead of retrying an import that cannot succeed.
- The GL external-memory loader now requires
glGetUnsignedBytevEXTandglGetUnsignedBytei_vEXT, exposed asGlExternalMemoryFunctions::get_unsigned_bytev_extandget_unsigned_bytei_v_ext. Contexts and devices that report no UUIDs, or all-zero UUIDs, skip the check rather than failing. DeviceUuidMismatchincrementsSERVO_RENDER_GPU_IMPORT_ADAPTER_MISMATCH_TOTAL, the same counter as the WindowsAdapterLuidMismatchcase.- New
crates/hypercolor-gpu-frame/tests/fallback_reason_tests.rsasserts every fallback code from 1 to 27 round-trips to a unique label and pinsDeviceUuidMismatchto code 27. tests/raw_gl_fixture_tests.rsnow serializes through aFIXTURE_LOCK: Mutex<()>acquired byacquire_fixture(); creating GL contexts from parallel test threads deadlocked in the driver.
🐳 Server Deployment
- The container runs unprivileged as UID/GID
10001, serves the web UI and API on port 9420/tcp, streams DDP on UDP 4048, and requiresHYPERCOLOR_API_KEY. State lives in thehypercolor-datavolume at/var/lib/hypercolor, with XDG paths pointed into it. compose.gpu.yamlis an overlay that maps/dev/driand needsHOST_RENDER_GIDandHOST_VIDEO_GIDset to the host device groups. Activate withCOMPOSE_FILE=compose.yaml:compose.gpu.yaml.- Images are published from tested artifacts rather than rebuilt: the release job loads the exact image that passed the smoke test, tags
${version}-amd64/${version}-arm64plus a multiarch index, and applieslatestonly to stable releases. - New
just docker-build <dist_dir> [image]andjust docker-test [image]recipes (Linux only, honoringCONTAINER_ENGINE). - New CI jobs
docker-e2e("Docker / Headless HTML and WLED") on PRs and main, andpublish-dockeron tags, serialized behind thehypercolor-docker-publicationconcurrency group withcancel-in-progress: falseso no release publication is dropped. scripts/tests/docker-smoke.mjsproves unprivileged startup, API-key enforcement, UI serving, Servo HTML rendering, animated DDP output to a WLED emulator, config and scene persistence across restart, and clean exit on SIGTERM.scripts/tests/docker-workflow.test.mjschecks the release workflow contracts.- New guide at
docs/content/guide/docker.md, withchoose-your-install.mdandinstallation.mdupdated to point server users at it.
🔧 Windows Packaging
Copy-VcRuntime()inscripts/stage-app-bundle-assets.ps1locates the redistributable throughVCToolsRedistDirorvswhere.exeand stagesmsvcp140.dll,vcruntime140.dll, andvcruntime140_1.dllinto both the bundle root andtools/, for x86_64 and aarch64 targets.- New
scripts/check-windows-dll-closure.ps1extracts the built NSIS installer and reads PE import tables withllvm-objdump, deliberately never consulting System32 orPATH, which would hide the bug on build runners. It requires all five binaries to be present, fails whenllvm-objdumperrors or a binary reports zero imports, and resolves every MSVC runtime import against files shipped beside the binary. - CI clears stale
bundle/nsisdirectories before each Tauri build so a cached installer cannot pass the check. HYPERCOLOR_STOP_BROKERininstaller-hooks.nshnow callsStop-Service -Name HypercolorSmBus -Forceand then waits onWaitForStatus('Stopped', [TimeSpan]::FromSeconds(20)).sc.exe stopreturns when the stop is requested, so upgrades and uninstalls were replacing files the broker still held open. The macro runs from bothNSIS_HOOK_PREINSTALLandNSIS_HOOK_PREUNINSTALL, and a missing service is not an error.
🔨 Maintenance
nix/release.jsonpinned to 0.6.0 with refreshed x86_64 and aarch64 hashes.Cargo.lockupdated for the new optionalhypercolor-coredependencies.
Upgrade Notes
- Linux GPU import users: if telemetry starts reporting
device_uuid_mismatch(fallback code 27), the process is running GL and Vulkan on different devices. CheckLIBGL_ALWAYS_SOFTWAREand hybrid-GPU environment variables; frames were likely already wrong before this release, they just failed silently. - Out-of-tree GL loaders must now resolve
glGetUnsignedBytevEXTandglGetUnsignedBytei_vEXTor importer setup will fail with an explicit missing-symbol error. - Windows: reinstall to pick up the bundled VC++ runtime. The upgrade after 0.6.1 is the first one where the SMBus broker is actually running on machines that lacked the redistributable, and that upgrade runs the 0.6.1 uninstaller, which includes the stop-and-wait hook.
- Docker: start from
packaging/docker/compose.yaml, generateHYPERCOLOR_API_KEY(openssl rand -hex 32) into.env, and pinHYPERCOLOR_VERSION=0.6.1if you do not wantlatest. Back up thehypercolor-datavolume before upgrading. - Servo telemetry:
frame_wait_usis a new render-stage field; add it to dashboards that break down frame cost. Expect lowerevent_loop_usas well, since the 1ms polling sleeps that inflated it are gone.
Hypercolor 0.6.0
[0.6.0] - 2026-09-30
Linux gains a managed release install with a durable update journal, rollback on probation failure, and a sandboxed release unit. The daemon keeps previous generations of every store it owns and records state-changing requests in an audit log. Lian Li L-Wireless RGB now paces on the fans' own echoes, and the macOS signing lane moved into its own job with a fast selftest.
This entry documents the range v0.5.1..HEAD.
Added
Managed Linux release installs
- Add the
hypercolor-installcrate and drive Linux installs through one replayable orchestration. Releases live under${XDG_DATA_HOME:-~/.local/share}/hypercolor/releases, update state under${XDG_STATE_HOME:-~/.local/state}/hypercolor/update, and~/.local/lib/hypercolor/install-journal.jsonpoints every installer at the recorded locations. The locator switch is the single commit point: before it, the old install is still in charge and nothing visible has changed. - Hold a started release through a probation window before committing. The installer proves the service runs that release's own daemon and that the local API answers with the release's version, then watches systemd's change notifications for a fixed 90 seconds. A crash, restart, stop, or watchdog kill in that window rolls back at once. (The window is an internal constant in
hypercolor-install, not a CLI option; the code also carries a 10-minute internal ceiling.) - Report the release that runs again after a rollback: its unit, version, systemd invocation, process ID, and the SHA-256 of the executable it runs, so the result can be matched against
systemctl --user status hypercolor. - Render the daemon unit with
Type=notifyand a sandbox (ProtectSystem=strict,ProtectHome=read-only,PrivateTmp,NoNewPrivileges, withReadWritePathslimited to config, data, and daemon state). The unit needs systemd 239 or newer, and the user manager builds the sandbox inside an unprivileged user namespace, so kernel and security policy must allow those. - Write a contract line as the unit's first line naming
hypercolor-public-1, so an installer never misjudges a unit written by a build it does not know. Each unit names one release directly. - Remove releases nothing references after each install, and uninstall from the recorded roots through
hypercolor __uninstall-release: stop and disable the service, remove only the entries the installer generated, and delete the releases directory, update state, and~/.local/lib/hypercolor, while keeping data, runtime state, and configuration. - Add
docs/content/guide/linux-release-installs.mdcovering upgrade targets, ancestry rules, public-directory refusal, the sandbox's requirements, and uninstall limits. - Add the ordinary-user systemd guest proof harness under
scripts/qualification/linux-user-guest/, pinning every kill point for probation, rollback reports, launcher swap, sandbox, and release-unit recovery.
Daemon state history and audit trail
- Keep previous generations of every daemon-owned store through
hypercolor-persistence's rolling replacement history: 10 generations by default, with a 30-second minimum interval between captures. Unchanged rewrites no longer hide stable history, and a pending shutdown capture survives them. - Exclude stores where old copies are useless or harmful:
instance-id,asset-library,user-effects,legacy-profiles,driver-inventory,credentials,attachment-templates, anddevice-binding-journal. Alias scan stamps are compared by equivalence solast_seen_epoch_schurn never creates a generation. - Record state-changing requests in a persistent audit log and serve them from
GET /api/v1/system/audit, newest first, withlimitdefaulting to 100 and clamped to 1000. Entries carry timestamp, transport (http,websocket,mcp), method, path, optional MCP tool, status, socket peer, forwarded-for and user-agent (each capped at 256 characters), the durable stores whose bytes changed, and latency. The peer is taken from the socket, never from a header. - Write status 499 for a request dropped before its response, held until the last attributed task finishes, and audit layout publications.
- Inventory the durable stores and ship the result as a release file (
share/hypercolor/durable-stores.json, built frompackaging/managed/durable-stores.json). It names each store's format, owner, root, location, schema range, written version, and migration mode, for tools rather than the installer.
Nix packaging
- Add
flake.nixwith a binary package and a NixOS module. Options:services.hypercolor.enable,package,autoStart,logLevel,extraArgs,smbus.enable, andinput.allDevices. CI builds the flake on changes, validates the release pin, and opens a pin pull request after each release.
Web UI
- Speak Remote bridge contract 2 for the canvas preview:
previewState(),previewStream(), andsetPreview(). The preview plays the bridge's video track in a muted inline<video>and is sized from the bridge's transport path. - Resolve daemon media through the installed transport with a shared cache: 256 entries, a 48 MiB total budget, 4 concurrent fetches, and no caching above 8 MiB per entry.
- Count preview frames received, displayed, dropped, and unobserved, and keep the JPEG preview moving when the worker cannot.
- Support host transport and a strict browser CSP, backed by an owned incremental browser response reader that holds at most one browser chunk and one caller-sized copy.
Hardware
- Pace L-Wireless RGB on the fans' echoed frame tags over a sliding per-cluster window, treating the echo as a cumulative acknowledgement. On the reference V1 rig, one frame per echo ran at 3 fps; with a window of 29 to 30, the fans confirmed 25.6 to 26.5 fps of 26.6 to 28.3 offered (spec 80 §6.11, measured 2026-09-28). Windows halve when the shared TX backlogs, staleness is judged against the measured status cadence, and a lagging cluster falls out of step instead of stalling its peers.
- Recover a fan-side stall in stages: two TX resets, then rests of 10 s, 30 s, 1 min, 2 min, and 5 min with upkeep holding fan speed while the protocol asks the backend for a fresh connect, then a power-cycle message.
Protocol::session_restartlets a driver ask for its device to be connected afresh. - Pace Push 2 LED output on device acknowledgements within one 512-byte endpoint packet, write raw MIDI messages whole or not at all, keep every zone fresh inside the ack window, and report a stalled MIDI output from the kernel backlog, probing at a spacing that grows from 250 ms. Log firmware, power source, and uptime at connect.
Release engineering
- Add
macos-signing-selftest.yml, a signing rehearsal bounded at 30 minutes (timeout-minutes: 30), andmacos-notary-status.yml, which lists recent Apple notarization submissions and prints Apple's log for one on request using the repository's App Store Connect key, with no Mac required. - Sign macOS artifacts in their own job from
build-native-app's unsigned payload, so a slow notarization reruns without a four-hour rebuild, and publish the release without waiting on Apple. Verify macOS release signatures on the user's Mac during install. - Add release-workflow and version-resolution script tests, a release artifact parity test proving producer, verifier, and Rust validator agree, and
scripts/check-ui-html-sinks.sh.
Other
- Admit selected scene activation fields only with exact evidence, and guard exact runtime zone color mutations.
- Render the GitHub social preview and README banner from the brand field.
Changed
- Move the release installer out of
hypercolor-cli/src/install/into thehypercolor-installcrate, and route Linux commands through the recorded managed authority elected under the state lock. - Upgrade the HTML effect renderer to Servo 0.6 (SpiderMonkey 140 to 153). Servo 0.6 makes WebGL an opt-in feature, so
hypercolor-corerequestswebglexplicitly; every shader effect goes through WebGL2, and awebgl2-clearCSS probe fixture fails the matrix if no WebGL2 context is available. jemalloc is restored as the daemon's global allocator afterservo-allocator0.6 made it opt-in. - Replace the vendored
midirwith upstream 0.11, which drops thersaadvisory ignore (RUSTSEC-2023-0071) fromdeny.toml. - Vendor
tachys0.2.18 with a narrow patch: a module-privatehc-statictrusted-types policy forToTemplatemarkup and borrowed-static HTML/SVG inert elements, so Chromium'srequire-trusted-types-for 'script'still rejects runtime raw markup. An embedding host may install a frozen provider before the app starts. - Refresh dependencies:
rustls0.23.45 forRUSTSEC-2026-0285,utoipa6 withutoipa-axum0.3 and swagger-ui 10,dirs7,mdns-sd0.21,tokio-tungstenite0.30,nix0.31,mach20.7,tao0.37 for the hidden Windows Servo window,wasm-bindgen0.2.129, Playwright 1.63 withws8.22, Bun 1.4.2, plus semver-compatible workspace, web UI, SDK, and Python client updates.ccis held at 1.4 so the Windows Servo build compiles. - Give the Windows CI lane 180 minutes for a fully cold rebuild after the Servo 0.6 refresh, overlap release validation, and reuse macOS binaries across jobs. Each notarization wait is bounded at 60 minutes (
NOTARY_WAIT_TIMEOUT: 60m). - Create the service's writable directories before its sandbox with an
ExecStartPrethat runs outside it, and recreate a deleted configuration root. - Isolate test state: sandbox the daemon e2e harness from the invoking user, relocate every directory in daemon lifecycle unit tests, and isolate the Playwright stack's state and connection environment.
- Retire Linux-first framing across specs and design notes, and complete the cross-platform pass in the ecosystem narrative.
Fixed
- Restore observed device nam...
Hypercolor 0.5.1
Hypercolor 0.5.1
Studio controls now retain their scroll position, focus, and open sections
while values change. Device assignments save immediately and share undo/redo
history with layout edits. This release also restores wireless fan RGB after
fan-speed upkeep and removes unnecessary CSS transitions from Studio.
Studio editing
- Layer-stack and Effects controls stay mounted during scene updates. Rejected
edits restore the server value in place, and effect schemas are cached until
reconnect. - Device assignments and zone moves save automatically. Undo and redo follow
the order of assignment and layout edits, including removal and restoration
of offline outputs. - The toolbar distinguishes saved state, pending assignments, and unsaved
layout changes. Placement drafts still require Save. - Scene and zone changes cancel superseded editing operations without leaving
the toolbar stuck in Saving. Replay preserves unrelated newer changes. - Narrower transitions on the resize handle and search field avoid unnecessary
animation of inherited scrollbar colors. Broader UI CPU work remains separate.
Wireless fan lighting
The Lian Li wireless driver caches the latest RGB frame and restores it after
all fan-speed commands, before clock or pairing traffic. Recovery preserves
black frames from pause and stop, cluster orientation, and existing packet
pacing. The cache resets when a new device session starts.
Packet tests verify the recovery order and payloads. The local candidate also
passed live transport checks, but physical confirmation that rainbow flicker
has stopped is still pending. This change concerns fan RGB lighting, not the
LCD image transport. The existing 30 FPS wireless ceiling is unchanged.
API and Python client
The new endpoint, POST /api/v1/scene/members/edit, applies membership changes
atomically and returns the committed scene document plus canonical before/after
receipts. The generated Python client includes synchronous and asynchronous
bindings.
Clients must provide the active scene_id and a numeric If-Match revision
(quoted or bare). A missing precondition or * returns 422. Stale revisions
return 412; a changed active scene or drifted membership returns 409. After a
conflict, read the current scene and rebuild the intended edit before retrying.
Use the returned receipts for undo so hidden output metadata is preserved.
Existing assignment endpoints remain available.
Verification and upgrade
Workspace verification, UI tests, browser regressions, and Linux, Windows, and
both macOS architecture checks passed before release preparation. New coverage
includes atomic membership edits, history replay, control-widget retention,
and wireless RGB recovery.
No configuration migration is required. Devices without a stable USB serial
can still acquire a different identity when moved between ports or hubs; this
release does not guess which identical device should inherit an assignment.
Hypercolor 0.5.0
Release Notes v0.5.0
Released: 2026-09-08
This release lands two large hardware efforts: full Lian Li TL LCD and L-Wireless support (wired panels, RF controller, wireless LCD receivers) and a productized OpenRGB fallback with installation guidance, detector partitioning, and managed server startup. Alongside them, a new rig-setup agent skill turns a physical PC build into a working spatial layout, and the Studio canvas gets a hard look at frame-budget behavior on dense scenes.
🌟 Highlights
🔌 Lian Li TL LCD and L-Wireless hardware support
Spec 80 shipped in three waves. Hypercolor now drives the wired Uni Fan TL LCD panel (04fc:7393), the L-Wireless RF controller (0416:8040 TX paired with its 0416:8041 RX sibling through a new CompanionTransport), and the wireless LCD receivers for TL (1cbe:0006) and SL V3 (1cbe:0005). Wireless RGB payloads travel through a pure-Rust tinyuz codec (crates/hypercolor-hal/src/drivers/lianli/wireless/tinyuz.rs) built against the 4 KiB firmware dictionary, with ten golden .yuz fixtures pinning the bitstream. Fan streaming is armed once and paced at 30 fps.
🌈 OpenRGB fallback you no longer have to hand-run
New crate hypercolor-openrgb-host detects the installed OpenRGB binary (native, Flatpak, or AppImage), probes the SDK server, checks Linux permissions, writes a managed detector partition, and builds a headless launch spec. The desktop app supervises a loopback-only server; the CLI can own one persistently. Native drivers always win per physical device: a conflict guard disables the bridge route and records why.
🖥️ Native-first device coverage
Hypercolor now tracks what it cannot drive. GET /devices/unclaimed lists USB hardware no enabled native driver claims, GET /devices/coverage joins native, bridge, and unclaimed views per physical device, and GET /system/openrgb reports installation, endpoint probes, permission checks, and coverage in one payload. The UI surfaces this through unclaimed_hardware.rs and bridge_status.rs, including prefilled device-support issue links.
🤖 The rig-setup agent skill
A user-facing skill that onboards a physical build end to end: inventory, coverage, interview, generate, dial in. skills/rig-setup/scripts/gen_layout.py turns a case spec (mm geometry, mount positions) plus a rig spec into attachment profiles, a spatial layout, and a named scene, supporting offline generation from a saved template catalog. coverage.py decides native vs bridge vs unsupported (falling back to lsusb, system_profiler, or pnputil), and request_support.py checks for existing requests and prints a prefilled issue URL; passing --file submits the request through an authenticated gh session. Skills now ship in the release tarball under share/hypercolor/skills.
⚡ Studio canvas under a frame budget
layout_canvas.rs drops overlapping per-output backdrop filters, memoizes render metadata via Memo::new, samples pointer geometry once per animation frame (flushing final movement on release), and keeps <For> outputs keyed across edits. Measured on an 80-output scene during development, median frame intervals fell from 66.7 ms to 16.7 ms and median hover work from 5 ms to 0.7 ms. e2e/tests/studio-performance.spec.mjs pins the resulting behavior: drag and undo state, output stacking during hover, absence of per-output backdrop filters, inspector freshness, and no redundant scene fetch.
🌐 Browser-neutral UI transports
hypercolor-ui no longer assumes it is running in a browser tab. New HttpTransport and WebSocketTransport seams let a remote shell route daemon calls and event streams without inheriting browser URL construction or bearer credentials, with cancellable incremental bodies, bounded multipart, and BrowserBlobSource for File and Blob uploads. Browser-normalized remote paths (absolute, protocol-relative, backslash, control characters) are rejected before they reach a transport.
🔌 Hardware
- Shared display encoding layer (
crates/hypercolor-hal/src/display/): Corsair LCD framing and Push 2 display encoding both moved onto one engine with a single hook, an error channel, and skip-and-warn behavior pinned bydisplay_layer_tests.rs. - Per-command response plans: the USB actor carries a response plan with a tolerance, and a retried command now drains leftover response reports (20 ms budget) instead of desyncing every later exchange.
- Placeholder serial quirk: descriptors can declare that a device reports a placeholder serial, and the claim layer refuses it rather than minting a bogus identity.
- Ambiguous HID panels: when two indistinguishable panels are present, the driver refuses to guess and counts devices by path.
- Display mounting is a device setting: faces, media, and effects all turn with the screen's mounting rotation.
DisplaySummarygains arotationfield (serde-defaulted), and Studio plus device detail expose a mounting picker. - Frames fit the wire cap: the daemon scales display frames to each device's transport limit and tells identical panels apart by USB port.
- ROLI Blocks: LUMI key lighting added to the blocksd bridge, plus a
blocks_rainbowexample.
🌈 OpenRGB Bridge
- CLI:
hypercolor openrgb status | hints | partition | start | stop | resize <device> <zone> <size>. The managed server binds loopback only, runs headless, and logs to<data>/logs/openrgb.log. - Detector partition (
data/openrgb/detectors.toml): per-family prefix matching forrazer,lianli,corsair,dygma,nollie, andasus/ENE. Families with an active native driver are disabled in OpenRGB's config; user toggles outside the managed keys are preserved, and re-enabling requires an explicit list. - Permission checks (Linux):
udev_rules,i2c_dev_module,i2c_nodes_writable,hidraw_nodes_writable, each carrying a concrete remedy command. - Driver hardening: writers paced to
target_fpsfrom a detector-class table, one shared SDK link per endpoint with a reconnecting task, zone resizing requires explicit opt-in and a resizable zone, brightness verified on output activation, frame-shape mismatches guarded, and serial plus location published in discovery metadata. - Live reconciliation: flipping
drivers.<id>.enabledregisters or unregisters the output backend without a restart, andhypercolor diagnose --check openrgbchecks bridge connectivity and reports output-disabled routes. - Zone sizes:
PATCH /config/keys/drivers.openrgb.zone_sizesmerges an object patch against current settings, keyed by driver-minted controller fingerprint then zone name, and applies on connect.
🔭 Events and API
ConfigChangedis now published fromConfigManageritself (crates/hypercolor-core/src/config/change_stream.rs), so every writer (REST, CLI, MCP, migrations, external edits) emits exactly one event per persisted transition. Multi-leaf changes collapse to the deepest shared prefix with a null payload; per-key redaction keeps credentials out of subtree events.LayoutChangedis published from the layout domain, including auto-layout repair and device-binding migration, carryingcurrentandpreviousids.- New WebSocket event
unclaimed_devices_changedinprotocol/websocket-v1.json. - New REST endpoints:
GET /devices/unclaimed,GET /devices/coverage,POST /devices/forget,GET /system/openrgb, andPATCH /config/keys/{key}. - The Python client is regenerated for display rotation, display mounting, OpenRGB status, coverage, unclaimed devices, and saved-controller deletion.
🐛 Fixes
- Saved controllers no longer leave layout ghosts. Deleting a saved-only controller now forgets its outputs across live layouts, saved layouts, every scene, and the default runtime snapshot, with physical ownership persisted so offline cleanup survives a restart. Studio requires registry evidence before offering the delete.
- Hue: saturated entertainment colors use full brightness again, and channel topology plus gamuts refresh together on rediscovery, retaining the last valid topology when a refresh fails.
- Primary-zone members survive restarts. Auto-layout leaves a device alone until it reports segments, and startup restore adopts outputs the persisted primary zone holds but the active layout lacks.
- Default-face deletes reach orphaned preferences for displays that were removed or re-fingerprinted.
- TL fan geometry moved from a ring topology to 26 explicit positions in physical LED order (two 13-LED side arcs), so spatial sampling lands where the LEDs actually are.
- Studio inline fields no longer commit twice on Enter-then-blur, and hover no longer raises large background outputs above smaller targets.
- SMBus discovery serializes with live output through one process-wide arbiter per bus path, so ASUS probe sequences stop interleaving with device writes.
- Installer: absent systemd unit properties, retained-directory scaffolding checks, managed icon symlinks, lazy launcher discovery, and identity proof through
/api/v1/systeminstead of the retired/api/v1/server. - Packaging: declared asset roots are always created, application and asset mounts can differ, and archive contracts are enforced in CI.
- Discovery: unclaimed USB inventory stays current on hotplug, identical rediscovery stays revision-neutral, and a device keeps its resolved shape across shapeless rescans.
👷 Build and CI
- The
rust-build-cacheaction split into tested Node modules (configure.mjs,sccache-lifecycle.mjs,native-cache.mjs,source-mtimes.mjs,restore-state.mjs), each with a.test.mjscompanion run by a workflow-lint job. source-mtimes.mjsrestores mtimes for content-identical tracked files after a cache restore, cutting false Cargo rebuilds; cached source times are invalidated when symlinks change.- Compiler cache lifecycle is owne...
Hypercolor 0.4.0
Release Notes v0.4.0
Released: 2026-09-01
Hypercolor 0.4.0 lands native macOS screen capture and host input, then rebuilds the daemon's public surface around two locked specs: 76 (internal API unification) and 78 (canonical API resource model). Nearly every REST route, WebSocket topic, and error body changed shape, and the in-repo clients (UI, TUI, CLI, app) plus the generated Python client moved with them in lockstep. The platform layer was also re-cut: capability seams live in neutral code, and OS calls live in audited platform crates with portable stubs.
This is a breaking release. Read the Breaking Changes and Upgrade Notes sections before updating any external client.
🌟 Highlights
🍎 Native macOS capture and host input
Five new platform crates land the macOS pipeline: hypercolor-macos-capture, hypercolor-macos-input, hypercolor-macos-owner, hypercolor-macos-media, and hypercolor-macos-session. Capture acquires frames through ScreenCaptureKit, hands opaque IOSurface surfaces to the compositor, and reduces them on Metal through hypercolor-macos-gpu-interop's native_reduction.metal. Host input runs on a CGEvent tap with an exact two-axis scroll contract, and hypercolor-macos-owner coordinates which process owns the TCC grants so privacy prompts land on a single, attested owner.
🔀 One canonical API surface
Specs 76 and 78 converge the daemon: every route error renders through DomainError into one frozen envelope, the live scene lives at /api/v1/scene as a real resource tree, power and brightness merge into a single /api/v1/output resource, and profiles fold into scenes. The 49 open-coded rollback/admit/save/publish rituals in the scene path collapse into one begin_scene_mutation / commit_scene pair with an ordered commit sequencer, so two commits can no longer publish in reverse order after out-of-order persistence.
🎨 A shared color kernel
New hypercolor-color crate owns every conversion, blend, and LED channel encoding, with a matching TypeScript kernel in sdk/packages/core/src/color/ and a GLSL prelude injected at build time (sdk/packages/core/src/tooling/glsl-prelude.ts). Rust and Bun both fence against the same table in sdk/shared/color-vectors.json (261 shared vectors), so a drift between the daemon and an effect fails CI instead of shipping as a hue shift.
🔌 Neutral platform capability boundaries
Lifecycle, publication, launcher, input, filesystem, and render policy moved behind shared seams, and the per-OS sibling modules that used to mirror each other collapsed into one engine per capability. Files carrying cfg(target_os) under crates/hypercolor-core/src and crates/hypercolor-daemon/src fall from 32 to 12, and the remaining ones are composition roots. New crates in this wave: hypercolor-gpu-frame, hypercolor-worker-retention, hypercolor-persistence, hypercolor-platform-fs (expanded), hypercolor-pipewire-interop, hypercolor-linux-input, hypercolor-linux-session, hypercolor-windows-session, hypercolor-windows-telemetry, and hypercolor-driver-support.
🛡️ Signed and gated macOS releases
Release builds now require signed artifacts. packaging/macos/signing-manifest.tsv drives a manifest-based signing actor, entitlements are split for the daemon and its sidecar, SDK and deployment targets are gated by scripts/verify-macos-deployment-target.sh, and the release lane will not publish unless the native acceptance suite passes. A signed TCC acceptance canary (crates/hypercolor-daemon/src/macos_tcc_canary/) proves the privacy grants a real install depends on.
🍎 macOS Platform
- Capture: ScreenCaptureKit acquisition with bounded callbacks, a surface pool, exact plane import, HDR capability bound to what the stream actually delivers, and recovery fencing so an interrupted stream cannot publish a stale frame.
- Color: a calibrated SDR capture path with an oracle test, LED tone-mapping configuration, and smoothing preserved across curve transitions.
- Ownership:
hypercolor-macos-ownerpersists daemon owner handovers, attests private daemon sessions, and derives launcher authority from process evidence rather than a filename. App sidecars stop through retained handles and stale reclaim is bound to the audit token. - Input: event-tap sessions publish per-kind platform state, and protected actions bind to the exact executor that can satisfy them instead of prompting opportunistically.
- Diagnostics:
screen_parity_diagnostics.rsreports live pipeline parity, source timing distributions ship in the status payload, andcrates/hypercolor-macos-capture/examples/dump_macos_frame.rsdumps a frame for inspection. - Compatibility: capture survives the macOS 26 screen delivery metadata changes, and the deployment floor is macOS 15.2.
🔀 API and Protocol Convergence
- Canonical error envelope.
ApiErroras aResponsefactory is gone;Result<T, Response>is forbidden and scan-gated bycrates/hypercolor-daemon/tests/api_error_surface_tests.rs. Errors render asApiErrorBody { error: { code, message, … }, meta }. - Live scene tree.
/api/v1/sceneexposes the running scene:/scene/zones,/scene/zones/{zone}/layout,/scene/zones/{zone}/members,/scene/zones/{zone}/layers,/scene/zones/{zone}/layers/order, and/scene/zones/{zone}/layers/{layer}/controls. Mutations resolve atomically against a base revision. - Zones, not groups. Render groups are
zonesin every path, payload field, and event. Effects targetzone_id. - One output resource. Power and brightness merge into
GET/PATCH /api/v1/output. - Config as a resource.
GET /config,GET/PUT/DELETE /config/keys/{key},POST /config/reset, andGET /config/schema, all driven by a key registry (crates/hypercolor-types/src/config_registry.rs). Rejections no longer echo secret values back to the caller. - Shared request and response types. Device, display, layout, asset, scene, effect, library, and attachment payloads now live once in
hypercolor-types::apiand are consumed by the daemon, UI, TUI, and CLI instead of being mirrored per client. - WebSocket registry. One declarative topic registry generates topic ids, typed configs, validation, and relay dispatch. Subscriptions are keyed:
display_previewbydevice_id,interactive_previewby a client-chosenpreview_id. Golden binary fixtures undercrates/hypercolor-daemon/tests/fixtures/ws/fence the wire bytes. - MCP. The agent surface converges on the same domain services; 17 dead stateless tool stubs and
execute_toolare deleted, phantom parameters are gone, and tool schemas are closed. - OpenAPI. Route registration records
(method, path, operation)into a catalog and a test asserts the catalog matches the documented operations, replacing the hand-maintained table.
🎨 Color, Effects, and SDK
hypercolor-colorprovidesRgb,Rgba,LinearRgba,Hsv,Hsl,Oklab,Oklch, hex parsing with no silent fallbacks,PixelBlendMode, andDevicePixelLayoutencoding. Linearization is visible in every function name.- The compositor's sRGB tables,
color_wave's LUTs, the web UI's color sites, and driver channel encoding all source from the kernel. - SDK effects parse color through the shared kernel; the GLSL prelude is vendored deterministically so formatter runs do not churn the diff.
- Exact two-axis scroll reaches effects intact on macOS, Windows, Linux, and browser inputs, with legacy wheel units preserved for existing effects.
- Servo upgrades to 0.5.0, and WebGL and Canvas2D effects now render host-side on macOS.
🧱 Platform, Drivers, and HAL
hypercolor-driver-apiis traits and types only; credentials, mDNS, pairing plumbing, and control-surface builders move to the newhypercolor-driver-support.- Native transport policy moves into the drivers themselves, and Push 2's MIDI lifecycle is isolated from USB workers so one device's reconnect cannot stall another.
hypercolor-platform-fsgains a transactional Unix tree replacement with staging, rollback, and authority checks, plususer_dirs.- Imported cross-host device layouts rebind on discovery instead of stranding.
hypercolor-trayis deleted; tray supervision is owned by the app.
🧰 Install, Service, and CLI
- A transactional installer lands under
crates/hypercolor-cli/src/install/with per-platform executors, a payload manifest, launcher stores, proof and validation stages, and rollback.crates/hypercolor-cli/tests/install_transaction_tests.rsand the platform suites cover it. hypercolor accessexposes the protected-source actions explicitly:authorize-input-monitoring,authorize-screen-recording, andchoose-screen-source(which routes to/capture/source). Nothing prompts implicitly at startup.hypercolor statuscan watch the daemon through ownership events.- CLI and TUI resolve config paths without a literal
~fallback. - A storage-tier path-migration harness (
crates/hypercolor-daemon/src/path_migration.rs) moves persisted files forward once, with backup, atomic write ordering, and restart idempotence.
🐍 Clients
- The Python client is regenerated against the canonical envelope and resource model, and is marked 0.4.0a1. Control types the public models are typed with are now exported.
- The UI, TUI, CLI, and desktop app all read the canonical error envelope and the keyed WebSocket wire.
👷 Build and CI
- Bun upgrades to 1.4.0.
- macOS CI runs capture, status, and bootstrap fixtures, and enforces Servo Clippy before merge.
- Release publishes Intel macOS artifacts alongside Apple Silicon.
- Generated-client checks are gated on their real inputs, and the shared color vector table triggers the Rust vector test on edit.
💥 Breaking Changes
- Error shape changed everywhere. Every route error is now
ApiErrorBody { error, meta }. Bespoke per-r...
Hypercolor 0.3.2
Release Notes v0.3.2
Released: 2026-08-15
This release makes global output power a first-class daemon concept, unifies bundled and saved presets into a single effect-scoped stack, and adds a trusted in-process API bridge for local callers. Persistence gained explicit durability reporting, and the build system picked up a cache-aware wrapper plus a pressure-triggered target GC.
🌟 Highlights
Global output power replaces ad-hoc pause
New GET /api/v1/output/power and PUT /api/v1/output/power endpoints expose a real state machine backed by OutputPowerMode (running, paused) and OutputPowerStatus (running, paused, stopped). Pausing holds outputs at their off frame while preserving live effect state, so resume picks up where you left off instead of restarting the effect. A destructive stop is now a distinct, observable state.
One preset stack per effect
GET /api/v1/effects/{id}/presets returns bundled and saved presets in a single list via EffectPresetSummary, each tagged with EffectPresetOrigin::Bundled or Saved plus an editable flag. POST /api/v1/effects/{id}/presets/{preset_id}/apply applies one directly, with an optional render_group in ApplyEffectPresetRequest. Bundled presets now carry stable identifiers, and duplicate bundled identities are rejected at load.
Trusted local execution bridge
The new crates/hypercolor-daemon/src/api/local.rs adds TrustedLocalApi, letting in-process callers execute daemon requests without a network hop or an API key. Requests are marked TrustedLocalControl and run at AccessTier::Control. Paths are validated against /api/v1/*; absolute URLs, authority headers, and traversal are rejected. TrustedLocalWebSocket provides a bounded, cancellable channel transport for /api/v1/ws.
Durable native materialization
persistence.rs now returns AtomicWriteCommitResult with four distinct outcomes: FailedBeforeReplacement, ReplacementVisibleButNotDurable, DurableWritten, and Superseded. A parent-directory fsync failure after a successful rename is no longer silently collapsed into success, so callers can tell visible from durable.
Build cache and target GC
scripts/cargo-cache-build.sh routes Trunk and Cargo through a shim that injects build.target-dir instead of exporting CARGO_TARGET_DIR, keeping sccache hits stable across isolated targets. scripts/cargo-target-gc.sh plus the hypercolor-cargo-target-gc systemd user service and timer reclaim disk under pressure, with dry-run by default and dirty worktrees always preserved.
✨ API and Contracts
ActiveEffectResponsegainedactive_preset_modified: bool, so clients can show when live controls have diverged from the selected preset. Manual control edits keepactive_preset_idand setactive_preset_modifiedtotrue; onlyPOST /api/v1/effects/current/reset(viareset_group_controls) clears the id.PauseEffectResponseandResumeEffectResponseare now explicit types. Pause reportsoff_output_behaviorandoff_output_color.- New
hypercolor-typesmoduleapi/output.rsholdsSetOutputPowerRequestandOutputPowerResponse, both registered in the OpenAPI document. - The WebSocket
hellostate payload gained an optionalactive_preset_id, letting clients render preset identity on connect. - Session power transitions are guarded by a
transition_generationcounter. In-flight fades and reconnect scans abandon themselves when superseded, making replayed or late updates idempotent. - Media uploads above Axum's default multipart body limit now return a proper
413throughmultipart_error_response()instead of a generic multipart failure. The route limit is sized from the library's 2 GiB hard cap plus bounded framing overhead.
🤖 Clients and SDKs
- Python: added
get_output_power(),set_output_power(paused),get_effect_presets(effect_id), andapply_effect_preset(effect_id, preset_id, render_group=None)on both async and sync clients. A newOutputPowerStatemodel exposesstateand apausedconvenience property. The_generatedmodels and theoutputAPI package were regenerated to match. - TypeScript SDK:
normalizePresetKey()intooling/html.tscollapses control characters (0x1c–0x1f,0x85) and runs of whitespace into single spaces without regex escape hazards, andpreset-idis parsed and normalized from HTML effects.tooling/validate.tsrejects presets whose ids collide after normalization. Dependency bumps:typescript^6.0.3→^7.0.2,@types/node^26.1.0→^26.1.2,@clack/prompts^1.6.0→^1.7.0,@biomejs/biome2.5.2→2.5.6. - E2E:
@playwright/test1.61.1→1.62.0,ws8.21.0→8.21.1, with a new spec asserting the global pause control.
🐛 Fixes
- Servo capture clocks:
frame_queue.rssetsemit_frame_timingand dispatchesLightScriptFrameUpdate::TimingScript, keeping RAF-driven capture clocks advancing on quiet pages. - Screen capture startup: the CPU reducer waits for worker threads to initialize before probing fanout allocation.
- Native sync durability: favorite and config updates acknowledge only after persistence commits, the exact installed snapshot is returned, and deleted tombstones survive favorite revisions.
- Device vendor marks:
vendors.rstracks image load failures and falls back to a styled monogram instead of rendering a broken image. - WLED: the backend reports an
OutputCadencederived from target FPS withwith_max_frame_silence(KEEPALIVE_INTERVAL), and Windows tests retry delete-pending file locks. - UI presets: client-side matching heuristics in
preset_matching.rswere deleted. Preset identity is now server-authoritative, withresolve_legacy_preset_id()migrating olderbundled:*ids. - Cargo profiles: dependency debug symbols were restored, and the dev/preview profiles use
line-tables-onlyfor workspace crates. A newdebuggingprofile opts intodebug = "full".
📝 Documentation Site
- Search loads the Elasticlunr library and index separately, recovers from transient index failures, derives section labels from the URL path, and restores focus to the trigger on close.
- Mobile gained a working navigation surface via the
nav-hamburgertoggle, and the nav bar scrolls horizontally with a fade mask instead of clipping. - Code blocks use class-based theming with
github-lightandone-dark-pro, switching without a reload. - Heading anchors use a margin marker template (
anchor-link.html) instead of an emoji, and the site now carries the app's brand assets, favicons, and motion tokens. - The Agents/MCP mermaid diagram renders, and docs images carry intrinsic width and height to prevent lazy-load reflow.
🔧 Build and CI
scripts/cargo-cache-lock.rssupervises build locks with signal forwarding and TTY handoff, keeping job control intact.- sccache normalizes checkout paths through
SCCACHE_BASEDIRSso C/C++ artifacts are shared across worktrees. justgaineddebug-build,build-wrapper-test,cargo-gc-test, and thegc*family;scripts/tests/covers both the wrapper and the GC.- Docs-only merges no longer rebuild the
compat,sdk, andui-testmatrices, and the Tauri bundle step shares one absolute target dir with the main build. - Unix release packaging pipelines are cached, and Rust 1.95 daemon lints are satisfied.
⚠️ Breaking Changes
- Pause is no longer a stop.
POST /api/v1/effects/pausepreserves live state and holds outputs at the off frame. Clients that relied on pause tearing down the active effect must call the stop path explicitly, or readOutputPowerStatus::Stoppedto distinguish the two. - Pause/resume response shapes changed. Responses are now
PauseEffectResponseandResumeEffectResponse. Pause addsoff_output_behaviorandoff_output_color. The/api/v1/effects/pauseand/api/v1/effects/resumepaths remain as compatibility endpoints;/api/v1/output/poweris the canonical surface. - Python
pause_rendering()andresume_rendering()now returnOutputPowerStateinstead ofMutationResult. Update any code reading mutation fields off those calls. - Tray menu ids split. The single
PAUSE_RESUMEtoggle becamePAUSE_OUTPUTandRESUME_OUTPUT, and the command changed fromTogglePausetoSetPaused(bool). - UI preset matching removed.
crates/hypercolor-ui/src/components/preset_matching.rsand its tests are gone. Consumers should read preset identity andactive_preset_modifiedfrom the daemon. PresetTemplategains a requiredidfield. Rust consumers constructingPresetTemplateliterals must supply an id (usehypercolor_types::library::PresetId::stable(name)), and the generated Pythonpreset_templatemodel changed shape.- Duplicate bundled preset identities are rejected. Effect bundles shipping two presets that normalize to the same id fail to load instead of silently shadowing one another. The loader in
crates/hypercolor-core/src/effect/loader.rsraisesduplicate bundled preset id: ..., andinstall_effect()wraps it inApiError::internal(...), so an upload toPOST /api/v1/effects/installis rejected with a500.
Upgrade Notes
- Point pause/resume integrations at
PUT /api/v1/output/powerwith{"state": "paused"}or{"state": "running"}and read status fromGET /api/v1/output/power. - Regenerate any OpenAPI-derived clients: the schema adds the
outputtag, effect preset models, andactive_preset_modified. - Migrate stored preset references from legacy
bundled:*strings to the stable identifiers returned byGET /api/v1/effects/{id}/presets. - If you ship HTML effects, give each
presetapreset-idand check that ids remain unique after whitespace normalization;validateflags collisions before upload, which is cheaper than debugging a failed install. - Callers that inspected atomic write results should handle `Replaceme...
Hypercolor 0.3.1
Release Notes v0.3.1
Released: 2026-08-11
Hypercolor becomes genuinely cross-platform in this release. Windows gets first-class host input capture and Desktop Duplication screen capture, effects can now react to your keyboard and mouse, and the daemon's state layer was rebuilt around transactional, fallible resource admission so a failed allocation retains the last good frame instead of blanking your lights. The web UI collapses to a single Studio workspace and finally works on a phone.
🌟 Highlights
⌨️ Interactive Effects: Keyboard and Mouse as Input Sources
Effects can now read live keyboard and mouse state. hypercolor-core/src/input/ gained an evdev host source on Linux (evdev.rs), a Raw Input source on Windows (windows.rs plus the new hypercolor-windows-input crate), and browser-preview injection (browser.rs) so the Studio canvas drives the same pipeline. The SDK exposes it through sdk/packages/core/src/input/ with getInputData() returning held keys, newly pressed keys, ordered KeyInputEvent/MouseInputEvent batches with capture timestamps, and an InputAvailability block. Keystrike (sdk/src/effects/keystrike/main.ts) ships as the showcase: chromatic ripples on keypress, shockwaves on click.
Capture is off by default and gated twice: consent ([input] enabled) and demand (only while an interactive effect is running). Key events were moved off the default-subscribed WebSocket events channel onto a control-tier route policy, so a passive dashboard client never sees keystrokes.
🖥️ Windows Desktop Duplication Capture
The new hypercolor-windows-capture crate captures the compositor's presented output through DXGI Desktop Duplication. It costs nothing on a static screen and needs no permission grant. Reduction runs on the GPU first via duplication/reduction.hlsl and duplication/gpu_reduction.rs with a three-slot staging ring and event-query polling; explicit failure classification falls back to the arbitrary-resolution CPU reducer in input/screen/reducer.rs. GET /api/v1/capture/monitors enumerates real monitors (index, stable source id, OS device name, dimensions, primary flag) so the settings dropdown lists your actual displays instead of guessing.
🔗 Portable Device Identity and Layout Re-binding
hypercolor-types/src/portable.rs introduces typed PortableIdentityClaim variants (USB serial, MAC address, Hue bridge id, Nanoleaf serial) that survive reboots and re-cabling. Only devices that can prove an identity get a portable key; SMBus deliberately has no constructor, because a bus location is not an identity. Pins persist in device-aliases.json, and the new binding endpoints heal orphaned layouts without editing them.
⚡ Hierarchical GPU Area Sampling
render_thread/sparkleflinger/gpu_area_sat.rs with area_sat.wgsl and area_hierarchy.wgsl replaces the fixed-ceiling readback path with a hierarchical summed-area scan. Readback now scales with LED count rather than canvas resolution, which is what removed the arbitrary canvas dimension limits across layout, preview, simulator, and config.
📱 One Studio, and It Works on a Phone
Studio is the only workspace now. The studio_ui_beta flag, the /displays, /layout, and /assets routes, and their page modules are gone. components/mobile_nav.rs adds a bottom tab bar below the 768px breakpoint with safe-area padding for notched phones, and settings was reorganized from nine flat tabs into four product-shaped ones with Advanced disclosures.
🎹 Input Pipeline
- Linux (evdev): reads
/dev/input/event*through udevuaccess, no group membership required.udev/70-hypercolor-input.rulesis ordered beforesystemdseat-late rules and now ships in release payloads. - Windows (Raw Input): absolute pointer positioning, derived key repeat, and honest session-boundary detection. Raw Input cannot cross a session, so a daemon running as a service reports degradation instead of silently capturing nothing.
- Routing:
input/routing.rsanddaemon/src/interaction_routing.rsadd an explicitInteractionRoutePolicy(host, browser, merge) with authoritative browser route leases bound to source incarnation, so a reconnecting preview cannot steal another session's events. - Health:
input/status.rspublishes per-source health (live, degraded, unavailable) with denied-resource counts. The daemon surfaces it on system status and the UI renders it throughcomponents/input_access_banner.rs, which offers a one-click consent toggle or the exact udev command when devices are denied. - Worker lifecycles are transactional across audio, evdev, media, screen, and Windows sources: partial startup rolls back every source, and timed-out workers are retained rather than leaked.
📺 Screen Capture
- Arbitrary source and output resolutions throughout. Cadence and canvas ceilings were removed from
config,layout,preview, andsimulator; validation now checks real invariants instead of magic maximums. - Capacity is admitted transactionally through
input/screen/admission.rs, with a lock-freeScreenCapacityStatusHandlepublishing capture demand, analysis plans, and compute capacity. Steady-state and transition admission are separated so a settings change cannot double-charge the budget. - Wayland/PipeWire negotiates compositor-real formats to first light, stamps restore tokens with their session, and publishes exact CPU branches through the shared publication hub (
input/screen/wayland.rs). - Letterbox detection no longer eats whole frames, downscales hold source aspect ratio, and canonical crop origins transform correctly.
- Screen-mirroring effects are excluded from capture-of-self, and Screen Cast metadata is platform-neutral.
🔌 Devices, Persistence, and API
GET /api/v1/devices/bindingsreturns two halves of a re-bind decision:unresolvedlayout bindings no attached device resolves, andcandidatesattached devices no layout references.POST /api/v1/devices/rebindre-pins a chosen device's portable key onto an orphaned binding's recorded identity. Returns 409 if the target device is currently live, and 422 if the device carries no portable identity claim (cross-driver inheritance is refused with 422 as well, before anything mutates).- Device settings schema v2 normalizes the key space to portable-key-first with local-fingerprint fallback. Migration is automatic and non-destructive: a
device-settings.pre-v2.bakbackup is written once, existing rows stay at their old keys until each device re-binds, and files from a newer schema load read-only rather than being overwritten. - New durable stores:
driver-inventory.json(daemon/src/driver_inventory.rs) keeps driver-owned discovery hints such as learned network targets, persisted independently and cleared when a device is deleted. Deleting a device now also prunes its default faces and preferences. - Atomic state replacement: the new
hypercolor-platform-fscrate replaces state files throughMoveFileExWwithMOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGHon Windows and validates destination filesystem identity to close a TOCTOU race.daemon/src/persistence.rsorders writers by generation with a retry supervisor and reports shutdown convergence, so a crash mid-write no longer truncates layouts or scenes.
🔀 WebSocket and Preview Transport
- New binary frame tags carry u32 dimensions:
0x0Bwide preview,0x0Cwide zone preview,0x0Dwide interactive preview,0x0Ewide screen zones,0x11extended screen zones. - Chunked transport arrives as
0x0Fpreview chunk and0x10preview cancel, letting large previews stream and be cancelled cleanly. - Clients negotiate
preview_transport_v2by fieldwise minimum against the peer's advertised budgets, falling back topreview_transport_v1. Reassembly, sender, and cursor state are each byte-bounded, and preview caches are bounded by resident bytes rather than frame count. - Interactive previews are connection-scoped and addressed by
preview_idthroughapi/ws/interactive_preview_relay.rs, with isolated render lanes indaemon/src/interactive_preview/. - Slow-consumer drops are now summarized instead of logged one line at a time.
🎨 Effects and SDK
- Cover artwork for 40 bundled effects, embedded inline at build time by
sdk/packages/core/src/tooling/build.tsand served by the daemon. The UI resolves cards by effect id instead of a guessed slug and falls back through daemon cover, cached client thumbnail, then category gradient. Artwork was raised to 960px for high-DPI cards. - Saved presets are promoted into the catalog, so a preset you save shows up where you look for effects.
- The SDK build prunes retired artifacts after full builds, escapes Windows scaffold manifests correctly, and
face-devopens the devices page. - The bundled catalog is kept in one copy, and the effect loader probes cargo dev trees so
cargo runfinds it.
🛠️ Hardware and Driver Fixes
- Push 2: MIDI pacing now applies to every output path and palette sysex is bounded to 16 writes per frame with nearest-neighbor fallback. Rawmidi goes nonblocking with a poll-based 1s deadline, so a wedged device surfaces as a timeout instead of hanging the lane.
- SMBus: ENE indirect reads send their delays in-batch instead of fragmenting, moving the wait off the tokio tick. DRAM modules connect reliably and frame rates roughly double on dual-DIMM setups.
- Windows PawnIO: SMBus polls stall rather than sleeping on the kernel timer, removing the ~60ms quantization that made polling unusable.
- Hue: the connect deadline now budgets the entire bridge handshake (id, lights, entertainment, streaming, DTLS) and runs in the background, so a slow or absent bridge no longer stalls discovery.
- Nanoleaf: external control streams to an injectable UDP port.
- Windows sensors: CPU temperature re-probes on a 20s backoff ...
Hypercolor 0.2.1
Release Notes: Hypercolor 0.2.1
Released: 2026-07-15
Hypercolor is a Linux-first, open-source RGB lighting engine written in Rust. This is the project's initial public release: a complete, from-scratch build of every layer, from USB wire protocols to a GPU-accelerated render compositor to a reactive web UI.
🌟 Highlights
✨ SparkleFlinger Render Pipeline
A custom render-thread compositor that blends effect surfaces at 60 fps on a 640x480 RGBA canvas. Effects produce frames as independent surfaces; SparkleFlinger latches, crossfades, and composites them into one canonical frame per tick. GPU composition via wgpu is the default path on supported hardware, with a CPU fallback. Zone sampling maps the final canvas to LED positions using bilinear or Gaussian area sampling, with per-zone brightness control. Frame pacing uses adaptive admission control to hold steady cadence under load.
✨ 12 Native Hardware Driver Families
Native USB/HID and network drivers ship for 12 device families with 179 supported devices across 32 tracked vendors:
- Razer (55 supported devices via zerocopy
RazerReport) - Corsair (Lighting Node, iCUE LINK, LCD displays, Bragi peripherals)
- ASUS Aura (USB HID and SMBus/I2C for DRAM DIMMs)
- Lian Li (Uni Hub ENE, TL, and legacy protocols)
- Nollie (Gen1, Gen2, NOS2, Legacy, Stream65 protocols)
- PrismRGB (HID interrupt transport with dynamic topology)
- Ableton Push 2 (MIDI control + bulk LCD display)
- QMK (OpenRGB HID protocol for custom keyboards)
- WLED (DDP/E1.31 network streaming)
- Philips Hue (Entertainment API with DTLS streaming)
- Nanoleaf (mDNS discovery, UDP panel streaming)
- Govee (LAN multicast + cloud API with rate limiting)
Dygma (Focus protocol) and ROLI Blocks (blocksd bridge) have driver code present but no currently supported devices. An OpenRGB SDK bridge provides fallback coverage for hardware not yet natively supported.
Transport layers include hidraw, hidapi, SMBus/I2C, USB bulk, USB control, serial, MIDI, and network (UDP/TCP/DTLS). Device discovery runs in parallel with USB hotplug, mDNS, and multicast scanning.
✨ 44 Built-In Effects with TypeScript SDK
The @hypercolor/sdk npm package (sdk/packages/core/) provides a declarative effect() and canvas() API for authoring LED effects in TypeScript. 44 curated effects ship in sdk/src/effects/, spanning Canvas 2D, WebGL shaders, and audio-reactive compositions. Each effect declares typed controls (sliders, color pickers, dropdowns, sensor bindings) and curated presets. A create-hypercolor scaffolding CLI generates new effect workspaces.
Effects run inside an embedded Servo web engine with LightScript, a JS bridge that injects audio spectrum data, sensor readings, and keyboard/mouse input into effect pages each frame. GPU import (Linux via Vulkan/EGL, macOS via IOSurface, Windows via ANGLE/D3D11) pipes rendered frames directly into the wgpu compositor without CPU readback on supported platforms.
✨ Multi-Zone Scene Engine
Scenes assign independent effect stacks to named zones. Each zone gets its own render group, layer stack (effects, media, screen regions, web viewports), and spatial layout. Zones are created, renamed, and managed through the REST API and Studio UI. Layer composition supports normal, screen, additive, and difference blend modes. Scene state persists across restarts, and snapshot mode guards runtime scenes from accidental mutations.
✨ Display Faces for LCD Devices
Devices with built-in displays (Corsair LCD coolers, Ableton Push 2) can render HTML-based "faces" showing clocks, sensor gauges, now-playing info, spectrum visualizers, and system telemetry. The Face SDK (sdk/packages/core/src/faces/) provides typed accessors for display descriptors, sensor data, media playback state, and audio spectrum. Seven flagship faces ship: Neon Clock, Pulse Temp, Sensor Grid, SilkCircuit HUD, Now Playing, Spectrum, and System Pulse. Face composition supports blend modes so faces overlay the active effect on the device's screen.
✨ Cross-Platform Desktop App
A Tauri 2 desktop shell (hypercolor-app) wraps the Leptos WASM web UI and manages a supervised daemon process. The app provides a system tray with brightness presets, scene status, and diagnostics export. First-run setup handles autostart configuration and hardware support detection. Native installers ship for Linux (.deb, AUR PKGBUILD, systemd service), macOS (.dmg with hardened runtime), and Windows (NSIS per-machine installer with PawnIO SMBus broker for motherboard RGB and ANGLE EGL for Servo GPU import).
🤖 Render Pipeline and Performance
- SparkleFlinger renders through CPU or GPU backends, selected at startup based on adapter probes. GPU path uses
wgpucompute shaders for blend, sample, and preview-scale passes. - Deferred GPU zone sampling overlaps readback with frame output push, measured with dual-slot readback buffers and sample wait metrics.
- Frame pacing uses a ring-buffer admission history with percentile-based tier promotion/demotion.
- Display output workers run per-device with paced queues, write retry on failure, and fuzzy frame dedup for WLED.
- Preview surfaces publish on demand: WebSocket relays encode RGBA or JPEG frames scaled to subscriber resolution. Loopback clients get raw RGBA; remote clients get JPEG at negotiated sizes.
- Servo renderer sessions run on a dedicated worker thread. Host-driven frame clocking replaced JS animation loops, giving the daemon control of effect cadence. Soft stall detection and circuit breakers retire unhealthy Servo sessions.
🔧 API and Integration
- REST API with typed envelope responses, OpenAPI spec generation (
/api/openapi.json), tiered auth (loopback read-only, API key for writes, network access controls), rate limiting, and CORS binding. - WebSocket protocol with typed channels for canvas frames, spectrum data, device metrics, scene events, and control surface mutations. Binary frame encoding uses channel-tagged headers.
- MCP server for AI agent control with prompts, resources, and tool endpoints covering effects, devices, scenes, displays, and system diagnostics.
- Python client (
hypercoloron PyPI) with OpenAPI-generated operations, typed models, WebSocket event streaming, and Home Assistant helpers. - CLI (
hypercolor) with SilkCircuit-themed help, connection profiles, shell completions, and full API coverage including scene, zone, driver, and control surface commands. Embeds the TUI ashypercolor tui.
🎨 UI and Developer Experience
- Web UI built with Leptos 0.8 CSR + Tailwind CSS v4, served by the daemon. Pages: Dashboard (performance telemetry waterfall, gauges, favorites), Effects (browse, apply, control with presets), Studio (multi-zone composition with zone tree, layer panel, and per-zone canvas preview), Devices (brand cards with vendor logos for 25 manufacturers, per-device metrics, driver control surfaces), Displays (face picker, blend composition, JPEG preview), Layout (spatial editor with undo/redo, compound selection, auto-layout), Settings, and Media (asset library with drag-and-drop upload).
- TUI (
hypercolor-tui) built with Ratatui, featuring 60fps terminal canvas preview via Kitty/Sixel/quarterblock protocols, spectrum-reactive border pulse, ambient canvas bleed, effect transition crossfades, idle breathing animation, resizable split panels, mouse interaction, and multi-zone/multi-scene support. - Luminary design system with semantic CSS custom property tokens, light/dark themes, ambient hue extraction from the live canvas, animated glow elevation, and SilkCircuit brand gradient accents.
- justfile with recipes for daemon, UI, TUI, SDK, effect builds, GPU mode, and cross-platform setup scripts.
🐛 Hardware and Device Management
- Device lifecycle state machine with connect/disconnect/reconnect, capped retry backoff, and keepalive for Razer and Corsair iCUE LINK devices.
- Non-blocking HAL dispatch with parallel discovery scans across USB, SMBus, and network backends.
- Per-device brightness control (global and per-zone), frame output acknowledgements with EWMA delivery rate tracking, and LED priority over display writes for USB devices.
- 54 auto-layout attachment profiles for Corsair, Lian Li, generic strips/fans/matrices, Nollie, and Formulamod components.
- Session and power awareness: systemd-logind integration, screensaver detection, configurable off-output behavior, and Windows sleep/resume rediscovery.
- Extensible driver module registry (
hypercolor-driver-api) with typed control surfaces, pairing flows, config enablement, and hot-reload on control value changes. zerocopytyped protocol structs for Razer, Corsair Lighting Node, ASUS Aura, PrismRGB, Corsair LCD, Razer Seiren V3, and ROLI Blocks.
⚡ Input Sources
- Audio: PulseAudio native capture on Linux, FFT spectrum analysis, beat detection, transient gating, and live input switching. Audio data feeds into effects through the LightScript bridge.
- Screen capture: Wayland portal-based capture with sector grid, color tuning, and ambilight zone routing. A dedicated ambilight edge-projection effect ships in the SDK.
- Sensors: CPU/GPU temperature, fan speeds, and NVML metrics. Windows reads via PawnIO MSR/SMN kernel modules. Sensor bindings map sensor values to effect controls.
- MPRIS: Now-playing metadata with bounded album art for display faces.
- Keyboard/mouse: evdev input capture for interactive HTML effects.
📝 Documentation and Packaging
- Full Zola documentation site with Luminary theme, covering installation, quick start, hardware compatibility, Studio multi-zone walkthrough, effect authoring (TypeScript, GLSL, Canvas 2D, raw HTML, native Rust, display faces), API reference (REST, WebSocket, MCP, CLI, Python), architecture internals, and contributing guides.
- Automated release pipeline: `scripts/dist...