fix(gamescope): the Upscaling Resolution is the session's scan-out size

Merged
JO
10d ago
+259-41

Description

#2267 added the per-container Upscaling Resolution and passed it to gamescope as -W/-H, but the setting could not take effect. The emulated DRM connector advertised exactly one mode — the app window's size (1280x960 on the test device) — so drm_set_mode looked for an exact-size mode, found none, and overwrote g_nOutputWidth/Height back to the window. Every session composited, allocated and scanned out at the panel's pixel count regardless of the setting: at 720p on a 1080p panel that is 2.25x the area, and gamescope upscaled the guest into it instead of letting the display's compositor upscale a smaller scan-out.

Mechanism. The app now names that size to the daemon as the mode, before nativeStart. GameNativeDrm.nativeSetScanoutGeometry(w, h) sits beside nativeSetPanelRefreshHz; GamescopeState.scanoutSize() reads outputResolution back as [w, h] so the gamescope arguments, the shim's seed (GNDRM_SCANOUT) and the daemon's mode all come from one value and cannot drift; XServerScreen sets it ahead of nativeStart (which fixes the mode from the surface callback), and clears it (0, 0) when the resolution cannot be read so a session never inherits the previous one's size. The presenter keeps the mode (composite, scan-out pool, KMS output) apart from the window (the compositing box) and fits the layer into the window uniformly, so a mode below the window is upscaled once, by SurfaceFlinger, and another aspect is letterboxed rather than stretched. mode == window is the identity and presents exactly as before.

Boundary. A size larger than the window is refused with a log rather than clamped: a bigger mode would composite pixels the display cannot show, only to have them scaled back down.

Default. OUTPUT_RESOLUTION_DEFAULT is now OUTPUT_RESOLUTION_OFF. A container that never opened the tab runs -W/-H == -w/-h == screenSize, i.e. exactly the behaviour every container had before the setting existed; Device's Resolution (the panel) and the explicit sizes remain.

Companion changes. The mode is only half of it, so two other repos move with this PR:

  • GameNative/gamenative-drm#32 — the connector advertises the session's scan-out size, the presenter fits the composite into the window, and the shim learns to answer the pool buffer's row stride.
  • GameNative/gamescope#20 — A51, the scan-out image is created at the pool's row stride. Without it a mode whose width is not a multiple of 320 px (1024x768: gralloc 5120 B vs the driver's tight 4096) shears on every compositing filter.

Verification. assembleLegacyDebug builds and installs with no warnings. Six device sessions on a Retroid Pocket Nova (panel 1280x960@120Hz, Adreno 740 + Turnip 26.3.0-devel, gamescope 3.16.31-41-g5e270d78+, Nidhogg): each case rewrote the container, launched, waited for the first frame, captured logcat and a screenshot, and exited through Back → Quick Menu → Exit.

#ContainerUpscaling ResFilter-W -H / -w -hMode selectedScan-out image pitchFrame
11280x960OffSGSR1280 960 / 1280 9601280x960@120Hz5120 (tight 5120)coherent, 59.4 fps
21280x960OffLinear1280 960 / 1280 9601280x960@120Hz5120 (tight 5120)coherent, 59.8 fps
3800x6001024x768SGSR1024 768 / 800 6001024x768@120Hz5120 (tight 4096)coherent, 59.5 fps
4800x6001024x768Linear1024 768 / 800 6001024x768@120Hz5120 (tight 4096)coherent, 59.5 fps
51280x7201280x720SGSR1280 720 / 1280 7201280x720@120Hz5120 (tight 5120)coherent, 59.9 fps
61280x9601920x1080Linear1920 1080 / 1280 9601280x960@120Hz5120 (tight 5120)refused (> window), mode = window, 60.0 fps

Case 3 is the geometry that used to shear: gamescope logs output image 1024x768 at the pool's row stride 5120 (tight 4096) and dumpsys SurfaceFlinger shows the matching chain — layer geomBufferSize=[0 0 1024 768], buffer w/h:1024x768 … stride:5120 bytes, layer transform 1.2500 to 1280x960, composition=DEVICE. Direct scan-out is untouched: with Linear the plane carries the guest's own buffer (case 4 logs a foreign dma-buf 800x600 pitch=3328 copy), which never touches the composite image. Cases 1 and 2 confirm the new default is a behavioural no-op — nothing is resampled.

Recording

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

Makes the Upscaling Resolution setting take effect: the emulated DRM connector now advertises the chosen size as its mode, so the session composites, allocates, and scans out at that size instead of silently falling back to the app window's dimensions.

The default is now Off, preserving each container's pre-setting behavior exactly (-W/-H equal the screen size); a resolution larger than the window is refused, not clamped. The daemon now receives the panel's active refresh rate, re-reported on display-mode changes rather than the fastest supported rate; this applies to X11 and gamescope sessions alike, so a panel held at 60 Hz counts as 60 for the latch-grid measurement and the FPS-limiter ceiling. Companion changes in gamenative-drm (connector mode, presenter) and gamescope (scan-out stride fix) are required end-to-end. A new test checks the gamescope payload is published on both fetch hosts and installs through the app's download path, so a version bump without a published archive fails in CI instead of on a user's first launch.

Migration

  • Containers that previously relied on the inert setting will now actually present at the configured resolution; verify expected behavior after updating.

Summary by CodeRabbit

  • New Features

    • Gamescope sessions now use the configured scan-out size for their display mode. Without a configured size, they use the container’s screen size rather than a fixed 1920×1080 default.
    • If a scan-out size cannot be determined, the existing display-size fallback remains available.
    • Active display refresh rates are tracked and updated when display modes change. The FPS limiter maximum adjusts accordingly for both X11 and Gamescope sessions.
    • Gamescope sessions report the active refresh rate and scan-out dimensions to the display daemon.
  • Documentation

    • Updated graphics-setting guidance to describe the session scan-out size and its effect on display modes.