display: the mode is the display's rate, the cap is at acquire, and generation follows the multiplier

Merged
JO
10d ago
+1061-1164

The emulated display's mode is the rate the compositor runs at, so it is the display's own and nothing else: the fps cap and the frame-generation multiplier never enter it. The cap is enforced at acquire, before a frame is drawn, and a batch generates multiplier - 1 frames plus the source frame, so a 40 cap produces and counts 80 frames a second at x2, 120 at x3 and 160 at x4, on any display.

Measured on a Retroid Pocket Nova, Hades II, cap 40, changing only the multiplier in the QuickMenu, one session per display:

displayx2x3x4mode
120 Hz80.0120.0160.0120 Hz throughout, one selecting mode, no backend dirtles
60 Hz79.8120.0160.060 Hz throughout, 40.3 acquires/s

What changed

  • The mode is served live. The connector's mode is built from process state (set_advertised_refresh_mhz) on every read instead of being snapshotted at session start, so a display change reaches a running session; the app applies one by dirtying gamescope's backend (backend_set_dirty, allowlisted in the injector).
  • The ring counts what was produced. FrameAcquired { count, generated } carries the shim's acquire and generation totals, and the daemon's deltas drive onFramePublished, so a session running above the display's rate reads as that rate rather than as what the display latched.
  • Pushes go to every subscriber. Cap and latch-grid notes are delivered to every connection that subscribed, not just the one holding the allocator (pushes.rs, new), so a superseded connection keeps receiving them.
  • A retired connection that asks to allocate yields the allocator back, so the shim reconnects and reclaims it instead of waiting out its exchange on a dead channel — this is the Vsync toggle freeze.
  • Generation follows the multiplier. panel_budget and source_pace are gone: nothing about the display reshapes the source rate or caps the output, and a batch is admitted once per acquired source frame rather than once per present, which is what had x4 producing 164 where 160 was asked for.
  • The latch grid is used only when a batch fits the source period; a batch that does not goes out spread evenly on the shim's clock, because on the grid the source frame lands a period late and the queue carries that lateness forever.
  • The daemon's vblank grid is the mode's rate. The flip timeline used to claim a fabricated 833 us tick from a constant of its own while the mode carried the display's rate; SESSION_REFRESH_MHZ is gone and an event's timestamp is the vblank the frame lands on, one display period after the previous one.

Docs 10, 11, 17, 19 and the index are updated to the current model, and docs/15 is rewritten: it was a 752-line audit of the fabricated grid, and is now the current state of where a rate is decided, the mode, where vsync is real, and what the daemon owns.

Verification

cargo test --workspace: 256 passed, 0 failed. Clippy clean on the host and aarch64-linux-android. Device runs are the table above, plus 6 consecutive swapchain recreations through a Vsync toggle with 0 slot init failed. The fabricated session grid is gone from the daemon, the docs and the gamescope patch train.

Paired changes

  • GameNative app — https://github.com/utkarshdalal/GameNative/pull/2313: reports the display's own rate to this daemon, applies a display
    change through backend_set_dirty, and ships this build as payload 20261101.1. It is the
    caller of everything above: without it the connector's rate is never reported and the mode is
    the launch-time default.