Skip to content

fix(play-setup): persist per-game resolution + add independent Frame Rate row - #261

Open
FabianZettl wants to merge 2 commits into
papi-ux:masterfrom
FabianZettl:fix/persist-play-setup-resolution
Open

fix(play-setup): persist per-game resolution + add independent Frame Rate row#261
FabianZettl wants to merge 2 commits into
papi-ux:masterfrom
FabianZettl:fix/persist-play-setup-resolution

Conversation

@FabianZettl

Copy link
Copy Markdown
Contributor

Context

On the Play Setup screen, Where It Runs and Tuning already persist their per-game choice across sessions (NovaLaunchModeOverrides, AutoQualityProfilePreferences). Resolution did not: picking a resolution worked for the launch it was chosen for, but after a disconnect/reconnect (or any time NovaGameDetailActivity gets recreated) the row silently reset back to the planner's recommendation, as if the choice had never been made.

Root cause

chosenResolution (NovaGameDetailActivity.kt) was a plain mutableStateOf<NovaDisplayResolutionChoice?>(null) Activity-scoped Compose state field with no backing store at all - it only ever got read once by launchOptimization() to build the one-shot launch override blob, then died with the Activity on finish(). There was no persistence path to hook into; the row just looked like a setting without being one.

What this does

Adds NovaResolutionOverrides, mirroring the existing NovaLaunchModeOverrides pattern: only the chosen NovaDisplayResolutionChoice.id is persisted (SharedPreferences, keyed per game), since the NovaDisplayResolutionChoice objects themselves are rebuilt fresh from the planner on every screen open anyway.

  • chooseResolution() now saves the id alongside updating the in-memory state.
  • chosenResolution's initial value is resolved from the saved id against that open's resolutionPlanner(game).visibleChoices via a new loadResolutionOverride() helper.

No changes to the picker UI, the planner, or the launch-time override blob - this only fixes what happens to the choice between launches.

Testing

  • ./gradlew :app:compileRootDebugKotlin - builds clean
  • Built and installed on a real device (AYN Thor, debug build). In Play Setup for Crisis Core –FINAL FANTASY VII– REUNION, changed Resolution to Performance (960x540), then force-stopped the app entirely (not just backgrounded/disconnected) and relaunched. Reopened Play Setup for the same game: Resolution still showed 960x540 with the "Chosen here · applies at launch" caption. This is a stronger test than a session disconnect since it confirms the fix survives full process death, not just Activity recreation within a live process.

Fabian Zettl added 2 commits August 29, 2026 13:55
The Where It Runs and Tuning rows in Play Setup already persist their
per-game choice (NovaLaunchModeOverrides, AutoQualityProfilePreferences),
but Resolution did not - chosenResolution was a plain Activity-scoped
Compose state field with no backing store, so it reset to the planner's
recommendation every time NovaGameDetailActivity was recreated (e.g.
after a session ends and the game detail screen is reopened).

Adds NovaResolutionOverrides, mirroring NovaLaunchModeOverrides: only
the chosen NovaDisplayResolutionChoice.id is persisted (SharedPreferences,
keyed per game), since the choice objects themselves are rebuilt fresh
from the planner on every open. chooseResolution() now saves the id,
and chosenResolution's initial state resolves the saved id back against
that open's planner.visibleChoices.

Testing:
- ./gradlew :app:compileRootDebugKotlin - builds clean
- Built and installed on a real device (AYN Thor, debug build). Set
  Crisis Core's resolution to Performance (960x540) in Play Setup,
  force-stopped Nova entirely (not just backgrounded), relaunched, and
  reopened Play Setup for the same game - resolution was still 960x540
  with the "Chosen here - applies at launch" caption. Confirms the fix
  survives a full process death, not just an Activity recreation.
Resolution and frame rate were coupled: each resolution choice from
the planner carries its own paired rate (e.g. "1440x810x60"), and the
only way to pin an fps independently of that was the global Tuning =
High FPS profile plus the app-wide Settings frame rate - there was no
per-game way to say "this resolution, but 90fps".

The engine to compose an independent fps into the launch blob already
existed (NovaLaunchStreamOverride.compose's fpsOverride parameter,
previously fed only by highFpsPin()); this just gives Play Setup a
second entry point into it.

Adds a Frame Rate row between Resolution and Tuning, offering Auto
plus 30/60/90/120 fps. An explicit pick here composes over whichever
fps the resolution choice (or the host's plan) would have used and
wins over the implicit Tuning = High FPS pin, since a pin made by name
is a more specific answer than one inferred from a quality profile.
Persists per-game via NovaFrameRateOverrides, mirroring the resolution
override fix earlier in this branch - same store, same lifecycle.

Testing:
- ./gradlew :app:compileRootDebugKotlin - builds clean
- Built and installed on a real device (AYN Thor, debug build). For
  Crisis Core -FINAL FANTASY VII- REUNION: set Frame Rate to 90 FPS
  independently of Resolution (left at 1920x1080), force-stopped Nova
  entirely, relaunched, reopened Play Setup - Frame Rate still read
  90 FPS with "Chosen here - applies at launch". Set it back to Auto
  and confirmed the row correctly fell back to deriving the rate from
  the resolution choice again (60 FPS, matching 1920x1080's own paired
  rate) rather than getting stuck on the last pin.
@FabianZettl FabianZettl changed the title fix(play-setup): persist per-game resolution choice in Play Setup fix(play-setup): persist per-game resolution + add independent Frame Rate row Aug 29, 2026
@FabianZettl

Copy link
Copy Markdown
Contributor Author

Added a second, related change on top of the resolution-persistence fix: an independent Frame Rate row in Play Setup, between Resolution and Tuning.

Why

Resolution and frame rate were coupled - each resolution choice from the planner carries its own paired rate (e.g. 1440x810x60), and the only way to pin an fps independently of that was the global Tuning = High FPS profile plus the app-wide Settings frame rate. There was no per-game way to say "this resolution, but 90fps."

The engine to compose an independent fps into the launch blob already existed (NovaLaunchStreamOverride.compose's fpsOverride parameter, previously fed only by highFpsPin()) - this adds a second entry point into it from Play Setup itself.

What this does

  • New Frame Rate row: Auto (falls back to whatever the resolution choice or host plan would have used) plus 30/60/90/120 FPS.
  • An explicit pick wins over the implicit Tuning = High FPS pin, since a pin made by name here is a more specific answer than one inferred from a quality profile.
  • Persists per-game via NovaFrameRateOverrides, mirroring the resolution-override fix earlier in this same branch - same store shape, same lifecycle.

Testing

  • ./gradlew :app:compileRootDebugKotlin - builds clean
  • Built and installed on a real device (AYN Thor, debug build). For Crisis Core –FINAL FANTASY VII– REUNION: set Frame Rate to 90 FPS independently of Resolution (left at 1920x1080), force-stopped Nova entirely, relaunched, reopened Play Setup - Frame Rate still read 90 FPS with "Chosen here · applies at launch". Set it back to Auto and confirmed it correctly fell back to deriving the rate from the resolution choice again (60 FPS, matching 1920x1080's own paired rate) rather than getting stuck on the last pin.

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