feat(play-setup): make High FPS tuning binding via one composed launch blob - #246
Conversation
…h blob The Tuning row's High FPS option used to be only a request string on the /optimize query, which the host was free to decline, and often did: a game holding a Recovery profile launched at 30 FPS while the row still read High FPS. It now pins the stream to the Settings frame rate. The pin rides in a composed launch blob that sets safe_target_fps_relaxed, the release field the resolver already honors, so overriding a recovery hold is an explicit, visible act: the Tuning caption states the pin and exactly what the host would have run, the row carries the override accent, and the launch button says the pinned rate instead of promising a recovery launch it will not perform. The new NovaLaunchStreamOverride.compose replaces the display planner's buildLaunchOptimizationOverride, which rebuilt the blob from scratch and silently dropped the stability block, so picking a resolution discarded the recovery clamp as a side effect. Composition deep-copies the host blob instead: a resolution pick pins width and height and leaves the fps clamp standing, and only the fps pin releases it. The resolution row now shows width x height alone, since the trailing rate was the host's plan rather than that row's decision. Paths that bypass Play Setup compose the same way: Game.kt's self-queried optimization fallback applies the pin for Continue Playing launches, and ShortcutTrampoline now honors the per-game tuning preference instead of a hardcoded "auto" before composing. The launch-wait guard in attemptLaunch is read from the raw blob now, because a pick or pin makes the composed blob non-null while the desktop-Steam answer is still on the wire. Two new StreamSyncManagerTest cases pin the resolver ordering the design rests on: a paired override is still clamped by a confirmed recovery, and the relaxed flag on the same blob is what releases it.
|
Device QA completed on the RP6 (com.papi.nova.benchmark updated in place to 1.3.7-benchmark built from Settings (#245): frame rate 120 survived a Balanced to Quality to Balanced preset round trip; presets changed only resolution, bitrate, and codec. Panel offers 30/60/90/120 as expected on the 120Hz display. Binding pin (this PR), end to end with Control Ultimate Edition in a live recovery-labeled state:
Ask vs grant (#247): on Auto, the detail status and Play Setup plan read "Recovery profile / 30 FPS · you asked 120 · Limited by: Network"; with the pin, "120 FPS pinned (host offered Recovery profile / 30 FPS)". Resolution pick on Auto: picked the Sharp card (2880x1620); row showed "2880x1620 · Chosen here · applies at launch" with width x height only. Launch resolved Both sessions ended via End Session with clean teardown (no labwc orphans, no zombies). Device left at the library with Tuning = High FPS restored on Control. |
Summary
/optimizequery, which the host was free to decline — a game holding a Recovery profile launched at 30 FPS while the row still read "High FPS". It now pins the stream to the Settings frame rate through a composed launch blob that setssafe_target_fps_relaxed, the release fieldresolveAutoSafeTargetFpsalready honors. Overriding a recovery hold is an explicit, visible act: the Tuning caption states the pin and what the host would have run ("Pins 120 FPS · overrides the recovery hold (host would run 30)"), the row carries the override accent, and the launch button reads "Launch 120 FPS · your pick" instead of promising a recovery launch it will not perform.auto/quality/stabilitykeep the host in control. (Design note: an FPS row was considered and rejected — Play Setup's act column is deliberately fixed at four rows per theNovaPlaySetupRowbudget; papi picked the binding-tuning shape.)NovaLaunchStreamOverride.compose— one composed launch blob. Replaces the display planner'sbuildLaunchOptimizationOverride, which rebuilt the blob from scratch and silently dropped thestabilityblock, so picking a resolution discarded the recovery clamp as a side effect. Composition deep-copies the host blob: a resolution pick pins width×height and leaves the fps clamp standing (intended behavior change — the accidental recovery bypass is gone); only the fps pin releases the clamp, explicitly.Game.kt's self-queried optimization fallback applies the pin for Continue-Playing launches;ShortcutTrampolinehonors the per-game tuning preference instead of a hardcoded"auto"before composing. TheattemptLaunchwait-guard is read from the raw blob now, since a pick or pin makes the composed blob non-null while the desktop-Steam answer is still on the wire.target_modestays on the option cards.StreamSyncManagerTestcases pin the resolver ordering the design rests on: a paired override is still clamped by a confirmed recovery, and the relaxed flag on the same blob is what releases it.Exact candidate
Commit:b1d70f1d3514ac3d9bedd6e030caf701083489f5Tree:7b7954c9f2bb6cda4ee687b568b1c74170a59c9cParent:ba72d2cffab381bbc67404306e4cac051424e3d4Base:ba72d2cffab381bbc67404306e4cac051424e3d4(origin/master, carries fix(settings): stop presets rewriting fps and share panel culling #245)Verification
:app:testNonRoot_gameDebugUnitTest— 1382 tests, 0 failures, 0 errors, 0 skipped (XML-counted; includes the newNovaLaunchStreamOverrideTestand the three new resolver pins):app:lintNonRoot_gameDebug -PlintFailOnError=true— 0 new errors (baseline noise only):app:assembleNonRoot_gameDebugAndroidTest— compiles (public symbol removed fromNovaDisplayResolutionPlanner; CI never builds this variant)NovaLaunchSourceGuardTest: composer symbol + raw-blob wait-guard)