Files
DroidDeck/tools/gamescope/PATCHES.md
T
The412Banner 110cfc28f8 feat: FusionHUD replaces the session HUD, with MangoHud's readings (#1743)
The session HUD is FusionHUD (Bannerlator's HUD) with every reading taken from
the same source, and worked out the same way, as the MangoHud build that runs
Steam's performance overlay in a DroidDeck session.

- gamescope patch 0125 (gamescope-3.16.29-p19) shares the frame times that
  mangoapp reads (one per signalled commit of connector 0) in a ring named by
  DROIDDECK_HUD_FRAMES; the app reads it once per display frame and counts the
  compositor's frames when gamescope sends none.
- MangoHud's rules: FPS over periods of at least 500 ms, frame time = 1000/FPS,
  200-frame graph on a fixed 0-50 ms scale; CPU, GPU, temperatures, RAM, swap,
  VRAM and battery from MangoHud's sources. Zones 22/26/28 are typed as GPU and
  DDR sensors, so Steam's overlay CPU temperature no longer includes them.
- Five sizes in a panel shaped like the DroidDeck mark, with a centred header,
  one left edge, neon outline and FPS; Minimal is the default size. Settings on
  a new "Performance HUD" drawer page, with Bannerlator's options.
- The HUD reads sensors and polls frames only while it is drawn (not while
  hidden, in picture-in-picture or behind another app), keeps its place after
  picture-in-picture, and the diagnostics export has its own reader.

Device-tested on the AYANEO Pocket FIT: same readings as Steam's overlay side
by side in Dead Cells; the HUD updates in a game, hides in picture-in-picture
and comes back in place, no crashes.
2026-10-29 01:53:45 +00:00

212 lines
15 KiB
Markdown

# gamescope patches carried by the app
Built by `.github/workflows/build-gamescope.yml` on top of the exact gamescope the Linux runtime
ships (3.16.29, Arch Linux ARM's package, same build options), and staged from the apk over
`/usr/local/bin/gamescope` at each session start - the hosted runtime image is never touched.
The binary's shared-library needs are checked against `runtime-sonames.txt`, the runtime's own
library list, before anything is published.
- `0002-steamcompmgr-fallback-appid-focus.patch` - Armada (armada-os/armada), verbatim.
- `0009-fix-arm64-steam-night-mode.patch` - Armada, verbatim: the ARM64 client packs the
night-mode property differently; the slider did nothing.
- `0019-steamcompmgr-arm64-virtual-white.patch` - Armada, verbatim: the colour-temperature
slider's (x, y) arrives as one 64-bit element from the ARM64 client; y is recovered from x.
- `0020-color-p3-red-is-wide-gamut.patch` - Armada, verbatim.
- `0100-realtime-queue-and-gamepad-cursor.patch` - this app, two of Armada's ported by hand onto
3.16.29: realtime-priority Vulkan queues on request (`GAMESCOPE_FORCE_VULKAN_REALTIME=1`)
without CAP_SYS_NICE, which proot can never have (a no-op on KGSL Turnip, which has a single
submit-queue priority); and the gamepad-driven cursor sprite following the X pointer that XTest
moves (it sat frozen). The X pointer is asked for only while a cursor image is drawn - every
vblank while shown, every 50 ms while hidden for inactivity - since each ask is a blocking round
trip to Xwayland on the paint thread; with no image, wlserver's position is used as upstream does.
- `0101-nested-wayland-cursor-routing.patch` - this app: `gamescope_cursor_composite=2` lets the
nested Wayland session paint its cursor in the output using the real X pointer position; client
cursor surfaces are cleared to prevent a duplicate at the host pointer, while Gamescope's original
default host-cursor fallback is preserved when its no-keyboard-focus policy selects it. Empty
client cursor images and Gamescope's hidden flag still suppress the painted cursor.
- `0110-wayland-backend-touch.patch` - this app: the nested Wayland backend bound only the host's
pointer and keyboard, so a finger on the phone's screen never reached Steam. It now binds
`wl_touch` too and hands each finger to wlserver's touch path (`wlserver_touchdown` / `motion` /
`up`) - the one a Steam Deck's touchscreen drives - so what a touch does follows the client's
touch mode (Steam's Big Picture sets Passthrough: a real touch, rows scroll under a finger).
Finger ids are offset by one, since the nested pointer already moves wlserver's touch 0.
- `0111-wayland-pointer-warps-in-passthrough.patch` - this app: the nested pointer's motion goes to
wlserver as touch 0, and in Passthrough (Big Picture's touch mode) a motion for a touch that is not
down moves nothing - so in the app's touchpad mode the Steam client saw no hover and a click landed
wherever the pointer had last been. The motion now always warps the real pointer as well
(`bAlwaysWarpCursor`), which the other touch modes did already.
- `0112-restore-iconified-game-on-resume.patch` - this app: the Steam menu is an overlay that
takes input without changing the focus window, and a fullscreen wine game minimizes itself when
it loses input. gamescope only takes a window out of iconic when the focus window changes, and
wine will not activate a window it believes iconic, so after Resume the game stayed minimized: a
black screen with its small caption in the top-left corner (Titanfall 2, GE-Proton 11). The
iconify request is remembered and the window goes back to NormalState before input returns to it,
then focus is handed over again. `GAMESCOPE_RESTORE_FOCUS_WINDOW` on the root window asks for the
same restore from outside (the session script's resume watcher).
- `0114-take-override-redirect-from-mapnotify.patch` - upstream (ValveSoftware/gamescope 3829340),
verbatim; drop it once the runtime's gamescope includes it. Wine creates a game's window
override-redirect and makes it managed before mapping it, which sends no X event. When gamescope
read the window's attributes first, it kept the window as override-redirect and left it out of
`GAMESCOPE_FOCUSABLE_WINDOWS`, so the client kept its loading screen over the running game
(Skyrim SE, A Plague Tale: Innocence, on the games' own Xwayland). MapNotify now refreshes the flag.
- `0115-limiter-paces-per-window.patch` - this app: on a fixed-refresh output the frame limiter
(Steam's Frame Limit) let a capped window's frames out only on every Nth refresh of a global grid,
so a frame finished just after its slot waited a whole extra slot. At 30 on 60 Hz one picture was
held for three or four refreshes - "small stops and ladders" with frame generation - where a game's
own limiter slips such a frame by one refresh. The fixed-refresh branch now paces each window by
time, as the VRR branch already did: its next frame goes out on the first refresh a capped interval
after its last one, with half a refresh of tolerance so an on-time window keeps every Nth refresh.
- `0116-hold-overlay-repaint-for-next-frame.patch` - this app: on a fixed-refresh output every
mangoapp commit asked for a repaint of its own, and mangoapp draws once per game frame just after
that frame was shown, so a 60 fps game on a 120 Hz panel was composited twice per frame - 75 frames
a second reached the screen, one in two 8 ms after the last with no new picture (Valheim, Stardew
Valley, Among Us). The VRR path already leaves such repaints to the next game frame; on a fixed
refresh output they now wait for it too, for at most `external_overlay_hold_cycles` refreshes (4,
so 30 fps at 120 Hz still merges), and are painted on their own after that.
Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this
app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has.
- `0116-release-cursor-shm-buffers.patch` - this app: pointer cursor surfaces have no
X11/xdg window association, so routing their commits through `g_PendingCommits`
retained every old SHM buffer until the surface was destroyed. Ignore cursor-role
commits before taking the buffer lock; X11 cursor images still follow the existing
MouseCursor path. Window commits awaiting association are unchanged.
The live regression probe is `tools/tests/test_gamescope_cursor_shm.py`. Run it
inside an active Gamescope session with access to that process's `/proc` fd
directory. It submits 20 distinct SHM cursor buffers and requires at most the
current buffer to remain open, then none after surface destruction. The
`--unassigned-control` variant omits the cursor role and verifies that all 20
commits awaiting a window association remain queued until surface destruction.
Device validation (2026-10-20): the stock component retained 20/20 cursor buffers;
the patched component retained 1/20, then 0 after destruction. The unassigned
control retained 20/20 and then 0. In Backpack Battles, two batches of ten touch
drags leaked 188 and 197 SHM descriptors before the fix; the same batches added
zero after the fix (112 total descriptors before and after each batch).
## 0117 WSI: cache the present-time Xwayland bypass check
`QueuePresentKHR` in the WSI layer calls `canBypassXWayland` for every swapchain on
every present. Each call makes several synchronous X requests (window geometry,
`_WINE_ALLOW_FLIP`, parent window, a child-window scan, the toplevel window). At
high frame rates that is thousands of round trips a second: on an AYANEO Pocket FIT
running AIO Graphics Test uncapped, Xwayland used 24-32% of a core and the game's
present thread sat about half idle.
The patch can keep the result for `DROIDDECK_WSI_BYPASS_CHECK_MS` milliseconds. Unset
or 0 keeps the per-present check; the app sets 100. With 100, a window resize or a flip/obscure change is seen up to 100 ms
later than before; surface creation and capability queries still check every time.
## 0118 Wayland: preserve signed plane positions
Fill scaling and Steam magnification can place the image at negative x/y coordinates.
The Wayland backend converted these signed positions through an unsigned size helper:
at host scale 120, -120 became 35791275 and the image moved off-screen while overlays
could remain visible. A separate signed conversion uses a 64-bit intermediate and
retains the existing ceiling convention; viewport sizes keep their original helper.
The component build runs `wayland_scaling` for negative crop offsets, positive/zero
positions, fractional host scales and wide intermediates. This patch may be worth
upstreaming: the same unsigned position calls were present in ValveSoftware/gamescope
3.16.29 and master `d175688ad4ec490dc7b0695c5c62f8807684a1f1` when checked on
2026-10-26. Carry it here until an upstream release containing the fix is adopted.
Device validation on AYN Thor (1280x720 session, 1920x1080 Android output):
800x600 and 1600x600 source windows retained their image through Auto, Fit,
Stretch, Fill, Integer and 2x Steam magnification, including returning to Fit.
The old component lost the image in Fill and magnification. The tested executable
matches the pinned p14 archive and the locally assembled APK. Hardware checks used
native Gamescope properties; the physical Back+LB shortcut was not independently
confirmed. The original APK and runtime were restored after validation.
## 0119 WSI: default image count while the frame limiter runs
The app sets `GAMESCOPE_WSI_MIN_IMAGE_COUNT=8` so an uncapped game keeps drawing while
gamescope turns its buffers around. With Steam's Frame Limit on, gamescope forces FIFO,
and the layer always hands the driver MAILBOX, so a game with eight images could queue
up to seven frames inside gamescope before it blocked: each one adds a frame of input
latency at the limited rate.
While the limiter forces FIFO, the layer now reports and enforces its default minimum
of 3 images instead of the raised count; with the limiter off it uses the raised count
as before. DXVK and VKD3D-Proton already recreate their swapchain when the limiter
changes (the layer returns `VK_ERROR_OUT_OF_DATE_KHR`), so their image count follows
the Quick Access menu. Other apps keep their swapchain on a limiter change; they now
get `VK_SUBOPTIMAL_KHR` while their image count no longer matches, unless the layer
marks them as not taking it (`NoSuboptimal`).
## 0120 Wayland: nested refresh follows the host
`-r` fixes the refresh gamescope paces at, and the frame limiter and Steam's Frame Limit
slider use the same value. The app passes the panel refresh at session start, so a
panel switched to another mode during the session (for example in the device's own
quick settings) left gamescope pacing at the old rate until the next session; only the
reported output refresh changed.
With `GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1`, a refresh the host compositor reports
through presentation feedback replaces the `-r` value, so pacing and the limiter switch
with the panel. `-r` still sets the starting rate, which avoids gamescope assuming
60 Hz until the first frame is shown. When the refresh changes, gamescope also sends
its `gamescope_control` display information to Steam again; Steam otherwise reads it
only when it binds.
A host mode switch can report a passing rate for a few frames (72 Hz between 144 and
120 Hz on the AYANEO Pocket FIT), so with following on a new rate is taken once it has
been reported for 300 ms. Without the variable the reported rate is taken at once, as
upstream does.
The rate comes from the feedback of a frame gamescope presents, so with nothing moving
on screen no feedback arrived and the old rate stayed. With following on, gamescope
presents once when the host's `wl_output` mode changes (DroidDeck's compositor sends
the new mode) and keeps presenting while a new rate is pending (since p16).
## 0121 Xwayland: skip the X tree walk for a repeated window override
The WSI layer sends `override_window_content` on every present of an X11 swapchain.
gamescope first walked from that window to its top-level window with `XQueryTree`, one
blocking X round trip per level under the wlserver lock, and only then noticed that the
same swapchain already overrides that window. At thousands of frames per second that is
thousands of round trips to Xwayland every second.
The override now remembers the window it was asked for. When the same swapchain and
surface ask for the same window again within 100 ms, the request returns at once; after
100 ms the walk runs again, so a window that moved to another top level is still found.
## 0122 Wayland: wake steamcompmgr after reading host events
Backport of upstream commit 2962992 by Matthew Schwartz (ValveSoftware/gamescope).
The nested Wayland backend's input thread reads the host display, but steamcompmgr
does not watch that file descriptor, so buffer releases and presentation feedback
queued by that read could wait up to one refresh for steamcompmgr's next poll. The
input thread now wakes steamcompmgr after each read.
## 0123 commit: fence lifetime fixes from upstream
Backport of upstream commits 0efe47c and 666c52b by Matthew Schwartz (ValveSoftware/gamescope).
Pending commits share ownership of their completion list with the Xwayland context, so a late
fence signal can no longer append to a list freed with the context; and a commit being destroyed
no longer hands the image waiter a new reference to itself. Both touch the code 0124 changes.
## 0124 commit: mark a finished frame done at import
Every game frame took two passes of steamcompmgr's loop: the import duplicated the buffer's
fence and handed the commit to the image waiter (epoll add), then the waiter thread saw the fence
signalled, removed and closed it and woke steamcompmgr again to handle the finished commit.
`update_wayland_res` now polls the fence without waiting first. A frame that is already finished
is marked done at once - no duplicate fence, no epoll add or delete, no waiter wake - and the main
loop handles it in the same pass, after the import (FIFO commits still wait for a vblank). The
`commit_ready_check` convar (default on, `gamescopectl commit_ready_check 0`) turns it off; the
`xwm` log counts fences ready at import and waited for, every 10 s.
## 0125 steamcompmgr: share mangoapp's frame times with DroidDeck's HUD
DroidDeck's in-app HUD shows the same frame rate and frame times as Steam's performance overlay.
DroidDeck's mangoapp (Valve's 20260902.1 build) reads the field at byte 16 of gamescope's messages as
its frame time; gamescope 3.16.29 puts `app_frametime_ns` there, the time between signalled commits of
the connector's windows (`mangoapp_nudge_app_frame`). With `DROIDDECK_HUD_FRAMES` set (droiddeck-session
sets `/run/droiddeck-hud/frames`), gamescope maps that file and writes the same commits of connector 0,
whether or not a mangoapp runs, plus the focused pid, output size, refresh, Steam focus and engine name.
The layout is in the comment above `droiddeck_hud_frames_map`; the app reads it in `hudframes.c`.