Files
DroidDeck/tools/gamescope/PATCHES.md
T

26 KiB

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.

The non-Adreno test branch's own patches are numbered 0920-0935 (0120-0135 until main took 0121-0125), so they apply after DroidDeck's. Applied in either order the tree is the same.

The system PowerVR driver uses the existing SDL/Wayland swapchain backend. Its additional patches are:

  • 0920-nested-swapchain-capability-fallback.patch: negotiate the surface's usages, mutable-format support, image count, alpha and extent; use offscreen composition and GPU copies when required.
  • 0921-powervr-single-pixel-rcas.patch: use the existing single-pixel RCAS shader on PowerVR, where the quad variant fails compilation.
  • 0922-powervr-linear-dmabuf-import.patch: retain the explicit linear layout when PowerVR rejects the mutable-format query, and intersect imported-image and FD memory-type masks.
  • 0923-powervr-rgba-copy-surface.patch: require an advertised RGBA SDR copy surface matching the shader's rgba8 storage view. The tested workaround leaves other vendors, HDR and direct composition unchanged; it does not restrict game-facing BGRA formats.
  • 0924-sdl-preserve-explicit-refresh.patch and 0925-sdl-wait-before-present.patch: preserve configured refresh across SDL events and finish composition before presenting without a render-finished semaphore. The build runs the focused capability, format and refresh tests.

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.

  • 0926-explicit-sync-switch.patch: GAMESCOPE_DISABLE_EXPLICIT_SYNC=1 makes the nested (Wayland and SDL) backends report no explicit sync, so gamescope does not offer wp_linux_drm_syncobj_manager_v1 and clients use implicit sync. The app sets it on a GPU that is not an Adreno when the kernel is older than 6.6 (no DRM_IOCTL_SYNCOBJ_EVENTFD: an Exynos 2400e on 6.1 logged hundreds of "timeline: DRM_IOCTL_SYNCOBJ_EVENTFD failed" and a game hung at a black screen) or when the render node is Mali's stand-in (/dev/mali0 has no DRM syncobjs).
  • 0927-shm-buffers-upload-every-commit.patch (David Cimo, for Mali through Venus; his 0114): with no DRM node, Xwayland has no glamor and commits a few wl_shm buffers over and over with new pixels. gamescope memoizes one texture per wlr_buffer, and a shm buffer's texture is a copy taken at import, so the screen cycled through the first few frames forever while Steam ran on behind them. Only dma-buf textures are memoized now; a shm buffer is uploaded on every commit. Adreno never takes this path, since its Xwayland hands over dma-bufs. Also what XWAYLAND_NO_GLAMOR sessions need.
  • 0929-log-failing-compute-pipeline.patch: when vkCreateComputePipelines fails, one extra line per shader type names the pipeline (shader type, layer count, ycbcr/colorspace masks, fp16) and the SPIR-V capabilities its shader declares. A Mali-G615 through Venus refused all 119 composite pipelines with VK_ERROR_INITIALIZATION_FAILED and nothing said which feature it objected to.
  • 0930-composite-without-immutable-ycbcr-samplers.patch: one composite pipeline is compiled at start-up; when the driver refuses it, the descriptor layout is rebuilt without binding 4's immutable YCbCr samplers (that binding then takes the slot's ordinary sampler) and the probe tried again, with both outcomes in the log. GAMESCOPE_NO_YCBCR_SAMPLERS=1 forces it. A Mali-G615 through Venus refused every composite shader (blit, blur, RCAS: SPIR-V capabilities Shader, Sampled1D, ImageQuery, no Float16) while EASU, NIS and the NV12 converter, which share the layout but never touch binding 4, compiled.
  • 0931-memory-type-fallback-keep-probe.patch: findMemoryType asked for DEVICE_LOCAL|HOST_VISIBLE memory falls back to host-visible memory without DEVICE_LOCAL, and a failure logs the requested properties, the type bits and every memory type. The Mali bridge models a unified-memory GPU like a discrete one (host-visible types without DEVICE_LOCAL), so the upload buffer found nothing and the backend failed. Also: 0930's probe pipeline is kept instead of destroyed, and skipped when GAMESCOPE_NO_YCBCR_SAMPLERS=1 (destroying a pipeline the Venus host had refused killed the render server).
  • 0932-name-hung-up-waitable.patch: "IWaitable hung up. Aborting." now names the waitable's class, its fd and what the fd is (/proc/self/fd), and the Wayland backend's input thread says why the compositor connection ended (protocol error or errno) first. An S24 FE aborted there after 60/120 Hz flips and after a resume with nothing else in the log.
  • 0933-flippable-image-unexportable-modifiers.patch: a flippable (dma-buf exported) image whose modifiers the driver supports but does not report EXPORTABLE uses them anyway, and an image with no usable modifier fails with a log line instead of asserting in CVulkanTexture::BInit. The Mali bridge forwards the phone driver's answer, which leaves EXPORTABLE out, though it exports dma-bufs.
  • 0934-flippable-image-no-backend-modifiers.patch: when the nested backend offers no modifier at all for a flippable image's format, CVulkanTexture::BInit tries linear and logs it instead of asserting !modifiers.empty(). NieR Replicant ended the session there on a Pixel 9a under Venus.
  • 0935-flippable-image-linear-when-driver-rejects-all.patch: when the driver answers every offered modifier with VK_ERROR_FORMAT_NOT_SUPPORTED and linear was among them, the flippable image tries linear anyway; if it cannot be made, BInit fails as before. An r32p1 Mali-G610 (Vulkan 1.1) behind the Mali bridge rejected both of the backend's modifiers for AR24, so the session ended at start (0933's "no usable modifier"); the bridge makes such images without the handle type and exports them through a dma-heap.

The shared DroidDeck patch also includes 0116-release-cursor-shm-buffers.patch: 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. It ignores 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.

0126 steamcompmgr: a topmost focus window keeps other processes' windows beneath it

EA games on Steam start through the EA app. Its Steam launcher (EASteamLauncher.exe) shows the game's splash as a layered tool window, and the EA app (EADesktop.exe, another process) opens its sign-in window WS_EX_TOPMOST. gamescope took the splash as the sign-in's override window, so it was painted over most of the sign-in and became gamescope's keyboard focus window (Burnout Paradise Remastered, gamescopectl focus_info). On Windows a topmost window stays above every window that is neither topmost nor owned by it, so a window from another process that is neither is no longer an override candidate for a topmost focus window. Both windows' pids must be known; gamescope keeps -1 when XRes cannot name the client, which says nothing about the process. Same-process popups, topmost popups (WebView2 and CEF menus) and windows transient for the focus window are unchanged. gamescope read _WINE_HWND_EXSTYLE only from a PropertyNotify, and it listens for those from map on, so the style Wine sets before mapping a window was never known; map_win now reads it with the other properties, which the layered-launcher and Xalia rules rely on too. tools/tests/test_gamescope_override_stacking.py compiles the rule from this patch.

0127 steamcompmgr: a three way relative mouse switch

relative_mouse is read where the decision is made: 0 leaves it to the rule below, 1 forces relative motion for the focused game except Steam's own windows and composites the cursor, 2 never goes relative. For games that confine a visible cursor and warp it, which the automatic rule cannot see.

0128 steamcompmgr: a hidden cursor under any pointer constraint is relative

The rule only counted a lock with a cursor position hint. A game that hides the cursor and confines the pointer through a grab (Wine's ClipCursor) has a constraint with no hint, so the nested pointer stayed absolute, the constraint held the X pointer still and no raw motion was produced: the camera froze. 0133 narrows this, and the upstream hint rule, to games that hold the X pointer.

0129 steamcompmgr, wayland: release a pointer lock at the game's pointer

A game can move the X pointer while it holds the lock (Wine's ClipCursor recentres it), and the nested host never hears about it. On unlock the host's next absolute motion dropped the pointer back at its old position, often near a corner. The unlock now sends the X pointer as a cursor position hint and commits it first. A cursor forced On by relative_mouse also stays visible while idle, since the host cursor is hidden during the lock.

0130 steamcompmgr: fit the X screen to a larger fullscreen window

A game can open a fullscreen window larger than its X screen (Valheim at 1366x768 on 1280x720). The X server keeps the pointer inside the screen, so the outer part of the window could not be reached and motion past the edge had to be undone. The server's mode now follows such a window, and returns once the focused window fits, focus moves to Steam or no window has focus.

0131 steamcompmgr, wlserver: follow a game's pointer warps

Under a nested host the seat's absolute position came from the host, so a game that warped a visible X pointer to the screen centre read the distance back to the host's position on the next motion, and the view spun. When the X pointer stays far from the seat position while input moves, gamescope takes the host's lock and applies each frame's motion to the X pointer itself until the game stops warping. Absolute motion also sends relative motion, as a real compositor does, so XInput2 raw motion reaches games that read only that. A touch moves the pointer without relative motion, so tapping the screen does not turn the camera.

The X pointer is read in root coordinates and the seat position is local to the focused window, so the X pointer is moved into the focused window's coordinates before the two are compared. Without that, any window not at the root origin (a dialog, or a windowed program) looked like a game warping the pointer, following started, and the pointer was pinned to the window's corner. A touch always lands where the finger is and ends following, so taps work in every window.

0132 sdl, steamcompmgr: follow a game's window mode

With --follow-game-window-mode, the outer window of a nested SDL gamescope follows the focused game window. A window asking for _NET_WM_STATE_FULLSCREEN, or a Steam game covering the X screen without minimise or maximise functions or decorations in _MOTIF_WM_HINTS, makes the outer window fullscreen on the desktop. Anything else makes it a normal window of the game window's size, or a maximised one when that size does not fit the desktop with its title bar and panel. A change is applied only when the wanted mode changes, so a manual resize or Super+F stays.

0133 steamcompmgr, wlserver: a pointer constraint is relative only while the game holds the X pointer

A game that hides the system cursor, draws its own and grabs the pointer to its window (Stardew Valley, Balatro) gets a lock from Xwayland, which emulates the game's warps with a cursor position hint. Upstream kept any hinted lock relative and 0128 added every other constraint, so gamescope held the host's pointer lock and DroidDeck turned every direct touch swipe into camera motion: dragging in those games only moved their cursor. Without relative mode such a lock still follows gamescope's relative motion and touches still place the pointer. gamescope now compares the motion it sends (seat movement, or relative motion while the seat is locked) with how the X pointer moves. A hinted constraint starts relative, as upstream, also when its hint arrives with the game's first warp, and is released after 30 moving frames on which the X pointer follows. Any constraint goes relative once the X pointer moves less than a quarter of that motion on three frames within half a second, as it does when the game recentres it or clips it to a small rectangle (PEAK). tools/tests/test_gamescope_pointer_hold.py compiles the rule from this patch.