Skip to content

feat(config): render at 60 fps by default and migrate the old 30 - #441

Merged
hyperb1iss merged 4 commits into
mainfrom
nova/default-fps-60
Oct 21, 2026
Merged

hyperb1iss merged 4 commits into
mainfrom
nova/default-fps-60

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

A fresh 0.7.1 install on an M3 Pro ran its render loop at 30 fps while each frame took about 0.5 ms. The shipped daemon.target_fps default was 30, and the adaptive FPS controller never upshifts past its configured target, so every machine was capped at 30 no matter how much headroom it had. With this PR the default is 60, and the adaptive tiers keep protecting slower machines: the loop still downshifts fast on consecutive budget misses.

💡 What changes

The default render target is 60. defaults::target_fps() now reads a new DEFAULT_TARGET_FPS = 60 in crates/hypercolor-types/src/config/daemon.rs.

Schema 7 moves the retired 30 off disk. Every config save serializes the whole HypercolorConfig, so any install that ever saved one setting carries target_fps = 30 and would never see the new default. This is the same situation schema 6 solved for the beat threshold (695bb1b71), and it uses the same pattern: loading a schema 6 file whose daemon.target_fps is exactly the integer 30 drops the value, so the new default applies and the next save writes it. Any other value is a deliberate choice and stays. A schema 7 file keeps 30 if someone picks it later. Schemas 4 and 5 chain through every step. Git history confirms 30 was the default for the whole life of schemas 4 through 6 (it changed from 60 to 30 on 2026-03-03, and schema 4 arrived on 2026-04-25), so a 30 in those files is the serialized default rather than a choice.

The durable store inventory and packaging/managed/durable-stores.json now read 4 through 7 and write 7. The refusal messages build their range from the constants. The Target FPS description in Settings, the guide, the configuration reference, and the performance troubleshooting page now say 60.

⚠️ Decisions this carries

  • Downgrades become one-way again. Once a build with this change saves a config at schema 7, 0.7.1 and older refuse it as written by a newer Hypercolor, exactly as schema 6 did for 0.7.0.
  • Watched work doubles on stock installs. Nothing else's knob changed, but several things follow the render tier: the dashboard preview stream requests the current tier, Servo HTML effects render at the loop's rate, and devices that accept more than 30 fps get more frames. On a stock install, a visible preview and a Servo effect now cost roughly twice what they did at 30. Two companion changes address that cost: output-demand gating parks the loop when nothing consumes frames, and the UI per-frame work is being cut separately.

Left at 30 on purpose: Servo's DEFAULT_EFFECT_FPS_CAP (a placeholder before an effect's first frame, which also sets the first-paint stall grace), capture, WebSocket, face, and display caps, the OpenRGB bridge's default_target_fps, and each device's max_fps. Those have their own defaults and contracts.

🧪 Validation

Migration tests in crates/hypercolor-core/tests/config_tests.rs: schema 4, 5, and 6 files with 30 load as 60; a real v6 save serialized with to_string_pretty round-trips to 60; on a v6 file 10, 20, 45, 60, and 120 stay as written; a v7 file keeps 30; a migrated file reaches disk at schema 7 holding 60; a v4 file chains through the start-key rename, the beat threshold, and the render target; a v6 file keeps a beat threshold of 0.6. With the migration broken on purpose, the four tests that depend on it fail.

Command Result
cargo fmt --all -- --check clean
clippy -D warnings --all-targets on types, core, daemon (daemon with persistence-test-hooks) clean
cargo test -p hypercolor-types 670 passed
core config_tests, config_sources_tests, config_persistence_tests, engine_tests 127 passed
daemon startup_tests, daemon_integration_tests, durable_store_tests, mcp_tests, api_tests, --test-threads=1 381 passed
hypercolor-ui cargo check --lib --locked ok
zola build ok

An independent verifier reviewed the default and migration commits and passed them; its one nit (carry the render target through the schema 3 hand-migration test) is the third commit.

🤖 Generated with Claude Code

hyperb1iss and others added 4 commits October 19, 2026 21:52
The render loop treats daemon.target_fps as a ceiling: the FPS controller
never upshifts above it and downshifts on its own after two consecutive
budget misses. A shipped default of 30 pinned every fresh install to the
Medium tier however much headroom the machine had; a fresh 0.7.1 install
on a Mac held 30 fps with frames finishing in about half a millisecond.
The default is now 60, the top tier, and slower machines stay protected
by the same downshift that already guards a hand-set 60.

The default lives in one constant, DEFAULT_TARGET_FPS, read by the serde
default. The settings page, the configuration guide, the guide overview,
and the low FPS troubleshooting page now name 60 as the default and Full
as the stock steady state. Tests that asserted 30, or wrote a value equal
to the new default, now pin 60 or pick 45 so each still proves a change.

Config files are not touched here: a file saved by an earlier build
carries target_fps = 30 explicitly and keeps it until a schema migration
moves it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every config write serializes the whole HypercolorConfig, so any install
that has ever saved a single setting carries `target_fps = 30` on disk
and would never see the new default of 60.

Schema 7 exists to move that value: loading a v6 file drops a
`daemon.target_fps` of exactly 30, so the current default applies and
the next save writes it. Any other value is a deliberate choice and
stays, and a v7 file keeps 30 if someone picks it later. Schemas 4 and 5
shipped the same 30 default and now chain through every migration, and
the v5 to v6 step stamps schema 6 itself so the chain stays explicit.

Tests round-trip a real v6 save, follow a migrated file through its next
save to a v7 file holding 60, keep 10, 20, 45, 60, and 120 on a v6 file
and 30 on a v7 file, and walk a v4 file through all three migrations at
once. The configuration guide says which files move.

Bumping the schema is a one-way door for downgrades: once a v7 build
saves the file, an older build refuses it as written by a newer
hypercolor. The durable store inventory and its packaged declaration
move to read 4 through 7 and write 7, and the refusal messages, which
read the range from the schema constants, now name 4 through 7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hand-migration test proves a schema 3 file edited onto schema 4 runs
every later in-memory migration. It only carried the retired beat
threshold, so nothing pinned that such a file also reaches the v6 to v7
step. The fixture now carries target_fps = 30 as well and expects the
current default after the load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The render_loop check formats the tier through FpsTier's Display impl,
which prints the rate and the name, so a stock config reports
`tier=60fps (full)` rather than a bare tier name. The example now
matches, and the prose says what the field carries.

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

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 17 billable files and costs up to $4.25.

Or wait 14 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: Advanced
  • Run ID: b1ed9ef7-37a9-4e6e-b4ab-1bec34111d37
📥 Commits

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

📒 Files selected for processing: 17
  • crates/hypercolor-core/src/config/mod.rs
  • crates/hypercolor-core/tests/config_persistence_tests.rs
  • crates/hypercolor-core/tests/config_sources_tests.rs
  • crates/hypercolor-core/tests/config_tests.rs
  • crates/hypercolor-daemon/src/durable_stores.rs
  • crates/hypercolor-daemon/tests/daemon_integration_tests.rs
  • crates/hypercolor-daemon/tests/durable_store_tests.rs
  • crates/hypercolor-daemon/tests/startup_tests.rs
  • crates/hypercolor-types/src/config/daemon.rs
  • crates/hypercolor-types/src/config/mod.rs
  • crates/hypercolor-types/src/config/root.rs
  • crates/hypercolor-types/tests/config_tests.rs
  • crates/hypercolor-ui/src/components/settings_sections.rs
  • docs/content/guide/_index.md
  • docs/content/guide/configuration.md
  • docs/content/troubleshooting/performance.md
  • packaging/managed/durable-stores.json
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@hyperb1iss
hyperb1iss merged commit b21a440 into main Oct 21, 2026
84 of 85 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant