Place the frame thread on the prime cluster and keep spinners off it

Closed
UT
2d ago
+875-9

Description

On big.LITTLE devices the thread that sets the frame often does not land on the fastest core. On the Retroid Pocket 6 (Cult of the Lamb), once the spinner was throttled in wine (proton-wine PR 119), the prime core (core 7) sat at 8 % while the 72 %-busy main thread stayed on the gold cores, and fps stayed at 26.

This adds FrameThreadPlacement. Every 2 s it reads the game's threads, wineserver and the Steam processes (steam.exe, steamservice.exe, steamhost*.exe) from /proc, sorts the threads into roles and sets their affinity with sched_setaffinity (same uid, no privilege). Nothing is configured per game.

  • Frame thread: the busiest game thread (>= 15 % of a core) that wakes 20 to 5000 times a second and is not a spinner. It goes on the highest-capacity cluster (RP6: core 7, Odin 3: cores 6-7). The role is sticky: another thread takes it only after beating it by more than 20 % CPU for 2 windows in a row. If the frame thread exits or starts spinning, it is replaced straight away.
  • Spinner: a game thread at >= 90 % of a core with < 50 voluntary switches a second. It goes on every core outside the prime cluster, and never on the low-capacity cores alone.
  • IPC threads: wineserver's main thread, Steam's IPC:CSteamEngine thread (its comm reads IPC:CSteamEngin), and any wineserver or Steam thread above 10 % CPU. They go on every core outside the low-capacity clusters.
  • When the game is held to a core set (the power-control pin, or the container's explicit CPU list), each role's cores are cut down to the cores that set shares. A role with no shared cores is skipped and logged.
  • Masks are applied again every window, because a winhandler pin or a cpuset move on activity resume resets them. Only role changes and releases are logged.
  • A thread inherits the affinity of the thread that created it, so while a game thread is placed, game threads that appeared since the last window get the game pin (all cores if there is no pin) instead of staying on the frame or spinner cores.
  • When a thread loses its role, a game thread gets the game pin back (all cores if there is no pin). A wineserver or Steam thread gets back the Cpus_allowed_list it had before it was first placed. Everything placed is released at session end, and also when power control is switched off mid-session.

Gate: this needs a PServer driver with cluster capacities (placementClusters()) and the profile's power control on. Without them nothing is placed. Kill switches, with no UI: PrefManager frameThreadPlacement (default true) and the container env var GN_FRAME_PLACEMENT=0.

game_exited gets frame_thread_tid, frame_thread_comm, spinner_tids and frame_placement_applied from the same extension point as the process telemetry. This is not gated on analytics.

Unit tests cover role classification, frame stickiness, the cluster masks for the RP6, the Odin 3 and edge layouts, game-pin intersection, the window apply/release flow and the session summary.

Device verification is pending: RP6 with Cult of the Lamb (frame thread on core 7), and Odin 3 with DOOM 2016.

Recording

Not applicable (no UI change). Device verification pending.

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

Places the frame thread on the highest-capacity cluster on big.LITTLE devices so it no longer lands on a slower core and caps FPS. FrameThreadPlacement re-reads the game's threads, wineserver, and Steam from /proc every 2 s, classifies them by role, and pins each role to a core set with sched_setaffinity (same uid, no privileges, nothing configured per game).

Behavior

  • The frame thread — the busiest game thread (≥15% CPU) that wakes 20–5000×/s and is not a spinner — is pinned to the highest-capacity cluster. It is sticky: replaced only after being beaten by >20% CPU for two windows, or immediately if it exits or starts spinning.
  • Spinners (game threads at ≥90% CPU with <50 voluntary switches/s) go on every core outside the prime cluster; IPC threads (wineserver's main thread, Steam's engine IPC thread, busy wineserver/Steam threads) go on every core outside the low-capacity clusters.
  • Masks are re-applied every window because a winhandler pin or a cpuset move on activity resume resets them. New threads created by a placed game thread inherit the game pin, since a thread inherits its creator's mask.
  • A thread that loses its role gets the game pin back (game threads) or the mask it had before placement (wineserver/Steam threads); everything placed is released at session end and when power control is switched off.
  • Placement is gated on a PServer driver providing cluster capacities and the profile's power control being on. Kill switches, no UI: frameThreadPlacement pref (default true) and container env var GN_FRAME_PLACEMENT=0.
  • Session telemetry now reports frame_thread_tid, frame_thread_comm, spinner_tids, and frame_placement_applied.
  • Unit tests cover classification, frame stickiness, cluster masks for several layouts, game-pin intersection, new-thread inheritance, and the apply/release flow.
  • Device verification is pending on RP6 with Cult of the Lamb and Odin 3 with DOOM 2016.

Summary by CodeRabbit

  • New Features
    • Added automatic CPU core placement for game threads on supported devices when power control is enabled. Placement can be disabled through the frame-thread placement setting.
    • Added frame-thread placement details to session exit reports.