mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
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.
212 lines
15 KiB
Markdown
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`.
|