Power tuner counts repeated frames; game pinned to the clusters above the little one

Closed
UT
8d ago
+349-13

Stacked on #2320 (base gamescope-real-vsync-payload-20261102-2): merge #2320 first, then retarget this to master.

Description

What the tuner got wrong. On a Retroid Pocket 6 (Snapdragon 8 Gen 2: c0-2 little, c3-6 big, c7 prime) with SteamWorld Dig 2 under gamescope (cap 60, 60 Hz panel), the ClusterTuner treated 59-60 fps with a p95 of 17 ms as steady and kept trimming. It ended with the perf cluster c3-6 pinned at 499 MHz (min = max) and c7 capped at 1.25-1.7 GHz. Meanwhile PowerManager pinned the game to "all cores but the two slowest" (2-7), so the main thread usually ran on c2 (a little core) or a 0.5 GHz big core. There a frame costs about 11 ms, against 4.4 ms on the prime core, and the game repeats a frame every few seconds. Neither fps nor p95 moves enough to show it.

Change 1: repeated frames count against "steady".

  • The metrics collector counts, from the app's own frame ring (the one the HUD fps uses, fed by onFramePublished under gamescope), how many intervals in the last 10 s were longer than 1.5 × the target period. The count is passed to the tuner as repeatedFrameCount.
  • Any repeat in the window blocks the next trim and holds back a pending warm-start state.
  • A repeat during the watch that follows a trim gives that trim back (and freezes the domain), the same undo an fps drop triggers.
  • Outside the watch, 2 or more repeats in 10 s step the latest trim back (or, for caps that came from a warm start, the domain an fps drop would raise). Step-backs are limited to one per 10 s window, so one burst steps back once.
  • The fps and p95 inputs are unchanged.

Change 2: game core set by cluster.

  • The game is pinned to every cluster above the lowest-frequency one, using the cluster grouping PServerDriver already has: 3-7 on the RP6.
  • If the clusters above the lowest hold fewer than 4 cores (for example a 6+2 part), or there is only one cluster, it falls back to the old "all but the 2 slowest" rule.
  • wineserver, winhandler, services and PulseAudio stay on the 2 slowest cores as before. The pin and winhandler affinity lines are unchanged.

Neither change adds a setting.

Measurement. RP6, SWD2, gamescope cap 60 @ 60 Hz, power control on, payload 20261102.2, same scene (mine entrance), 180 s settle + 90 s capture.

Control (#2320 + compositor affinity)This PR
SurfaceFlinger fps (90 s)59.3359.85
Repeats / 90 s57 (42 missing game frames, 15 latched late)11 (2 game, 9 latched late)
Game allowed cores2-73-7
Main thread on c7 (10 s samples)1 / 3928 / 39
Perf cluster cap during capture614 MHz floor730 MHz (77 s), 614 (10), 499 (5)
Tuner40 trims, 6 undos26 trims, 13 undos (7 repeat-undos), 136 repeat-holds; 6 trims to perf 499 MHz, each stepped back after repeats

Caveat: the run with this PR started from the tuner state file the control run left behind (prime 1.0 / perf 0.61 GHz, against 2.73 / 2.32 GHz for the control), and the prime cap was still being trimmed during the capture.

Reference, power control off: 59.968 fps, 1 repeat in 90 s.

Open knobs.

  • Step-back threshold: 2 repeats per 10 s (chosen because power control off gives about 1 per 90 s, so 2 in one window rarely happens on its own) against 1 per 10 s. A single repeat already blocks trims.
  • The tuner can still trim the perf cluster to its 499 MHz floor before repeats bring it back. A perf-cluster floor during gamescope sessions would stop those dips.

Log lines to look for.

  • PowerManager: Game core set 3, 4, 5, 6, 7 (clusters efficiency=[0, 1, 2] performance=[3, 4, 5, 6] prime=[7], background=0, 1)
  • PowerTuner: hold repeat-hold repeated=N in 10s: …
  • PowerTuner: undo repeat-undo:<prime|perf|gpu> repeated=N in 10s: …
  • PowerTuner: undo undo:<domain> repeated=N in 10s (repeat during a trim watch)
  • repeated=N in 10s on every applied tuner line and on the periodic PowerMetrics line
  • "repeated":N in the tuner jsonl

Tests. 13 new unit tests, all passing:

  • TunerRepeatedFramesTest (6): trim on a clean render, hold on one repeat, undo on a repeat during the watch, one step-back per window, a single repeat holds without stepping back, warm-start fallback
  • GameCoreSetTest (4): 3+4+1 → 3-7, 4+4 → 4-7, 6+2 falls back, single cluster falls back
  • RepeatedFrameCountTest (3)

Recording

None; frame-pacing change, numbers above.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

The power tuner now detects repeated frames that FPS and p95 miss, so it can respond to stutter instead of continuing to trim clocks. Game threads now run on clusters above the lowest-frequency cluster when those clusters provide enough cores.

  • Counts intervals longer than 1.5 target periods over a 10-second window; any repeat blocks trims, and two or more step back the latest trim once per window.
  • Uses the previous “all but the two slowest cores” rule when only one cluster is available or the higher clusters have fewer than four cores.
  • Adds tests for repeat handling, frame counting, and cluster-based core selection.