Repository navigation
icd_shim: serve VK_KHR_xcb_surface so a GL guest takes the acquire-gate fps cap - #35
Merged
Merged
Conversation
…te fps cap A GL guest on this stack is GL-on-Vulkan (Mesa zink), and zink's kopper WSI is xcb-only on X11: it creates its swapchain from VK_KHR_xcb_surface. The shim advertised only VK_KHR_wayland_surface and VK_KHR_xlib_surface, so that create never happened — gn_vkCreateSwapchainKHR found no shim surface and returned VK_ERROR_INITIALIZATION_FAILED. With no swapchain of ours there is no acquire call of ours either, so the frame cap (applied in gn_acquire_common, before a slot is handed out) never reached a GL guest and only Vulkan was paced. Serve the extension: advertise it, strip it from the real instance create like the other two proxied surface extensions, and handle vkCreateXcbSurfaceKHR / vkGetPhysicalDeviceXcbPresentationSupportKHR. Both X11 create infos differ only in connection type and neither present route reads it (the override route binds the XID to a gamescope surface, the DRI3 route opens its own connection from DISPLAY), so the surface construction is one helper, x11_surface(), shared by xlib and xcb. GL now creates the same GnSurface, on the same swapchain ring, paced by the same gate as a Vulkan guest. This is the DRM-side answer to gamescope PR #22 (release limited frames on the vblank time grid): a gamescope-side limiter cannot align with the acquire gate because its paint gate only reaches a client that asked for wl_surface.frame or FIFO, and this shim asks for neither. Capping at acquire is where a frame that may not start yet waits — nothing rendered is thrown away and nothing on its way to the panel is delayed. icd_shim/src/vk.rs: VkXcbSurfaceCreateInfoKHR. docs/04-gamenative-app.md: the shim now advertises three surface extensions, and why xcb exists (zink WSI), including that both X11 routes land in one surface. docs/19-fps-limiter-acquire-rework.md: the GL row of the placement matrix and the §7 risk list now say a zink guest is capped at acquire, and that a GL client presenting without Vulkan (glx/DRI3 into Xwayland) still never reaches the ICD.
The cap moved from the release floor to the acquire, and its census did not.
`REL_HOLD_N`/`REL_HOLD_SUM_US`/`REL_HOLD_MAX_US`/`REL_HOLD_GE_8333`/`REL_CAPPED_N`
were never incremented after the move, so the `cap=[…]` block printed
`held=0 held_us=0 …` for a cap that was working and for one that was inert
alike, and the documented reading rule ("held>0 means the cap is working") could
not be satisfied by any session. A target that never arrived, a target that never
bound, and a gate that bound and still delivered short were indistinguishable.
Count the gate where it acts, in gn_acquire_common: one park per acquire (the
first instant the deadline held it, not one per wake-up, since a wake-up that
finds the deadline still ahead parks again), the total parked time, and its worst
case. Derive `acq_fps` from the acquire interval window already collected at the
same site — the rate at the instant the cap acts, which is the only number a
target can be checked against; the app's frame ring is a display count and reads
low on its own when the compositor coalesces.
cap=[fps= notes= gate_holds= gate_us= gate_avg_us= gate_max_us= acq_fps=]
docs/10-fps-limit.md, docs/19-fps-limiter-acquire-rework.md: the census fields
and how they read — notes=0 a target that never arrived, notes>0 with
gate_holds=0 a target that arrived and never bound (the guest or the compositor
is the slower element), gate_holds>0 with acq_fps under target a schedule fault.
Also correct the §3.1 pseudocode in docs/19: it described `max(next + period,
now)`, which is not the shipped rule. The schedule steps `next += period` while
the acquire is less than a whole period late, and re-anchors on `now + period`
only past that, so a late frame cannot put its lateness into the cadence.
A zink guest takes the gate only through kopper's xcb surface. The container's libGL.so.1 is Mesa's libgl-xlib target (software xlib winsys, XShmPutImage): it renders on a headless Vulkan instance, never creates an X11 surface, and LIBGL_KOPPER_DISABLE is not a string in it. Measured 2026-10-31 on a Retroid Pocket 6 with this shim loaded in SteamWorld Dig 2: no swapchain, no cap line, 55.9 fps at a 60 cap. Until the kopper/EGL backend ships, GL guests are capped by gamescope's own limiter, which is why it stays armed.
5 of 8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Caps a GL session at the step where a frame is acquired, the same gate a Vulkan session already goes through — no gamescope-side limiter involved.
A guest GL app here is GL-on-Vulkan (Mesa zink), and zink's kopper WSI is xcb-only on X11: it creates its swapchain from
VK_KHR_xcb_surface. The shim advertised onlyVK_KHR_wayland_surfaceandVK_KHR_xlib_surface, so that create never happened —gn_vkCreateSwapchainKHRfound no shim surface and returnedVK_ERROR_INITIALIZATION_FAILED. No swapchain of ours means no acquire call of ours, so the frame cap applied ingn_acquire_common(checked before a slot is handed out) never reached a GL guest, and only Vulkan was paced.Serve the extension:
VK_KHR_xcb_surfaceand strip it from the real instance create, like the other two proxied surface extensions;vkCreateXcbSurfaceKHRandvkGetPhysicalDeviceXcbPresentationSupportKHR;DISPLAY), so the surface construction is one helper,x11_surface(), shared by xlib and xcb.GL now creates the same
GnSurface, over the same swapchain ring, paced by the same acquire gate as a Vulkan guest.Why acquire, and not gamescope PR #22
gamescope #22 moves the compositor's non-VRR limit onto a vblank time grid so a late compositor wakeup stops holding the next release a whole vblank longer. The grid is the right fix for the arithmetic error it describes, but it is still a display-side pacer, and it cannot align with the cap this project actually enforces:
wl_surface.frame(steamcompmgr_latch_frame_done → unlockedForFrameCallback) or through the FIFO commit deferral, which needs gamescope to read the surface as FIFO. This shim asks for no frame callbacks and declares mailbox, so a target set in gamescope never reaches the guest — a null we have already measured, not a knob left unturned.So the better option for #22's problem is: keep the cap at acquire and close the GL hole underneath it — which is what this PR does. With this, #22 is not needed for a capped session on this stack; its grid arithmetic remains independently correct for a client that does use frame callbacks.
Files
icd_shim/src/proxy.rsVK_KHR_xcb_surfaceadvertised (extra2→3) and stripped from the real create;gn_vkCreateXcbSurfaceKHR,gn_vkGetPhysicalDeviceXcbPresentationSupportKHR; sharedx11_surface()icd_shim/src/vk.rsVkXcbSurfaceCreateInfoKHRdocs/04-gamenative-app.mddocs/19-fps-limiter-acquire-rework.mdVerification
cargo check -p adrenotools_gamescope— clean.cargo test -p adrenotools_gamescope --lib— 60 passed.cargo clippyhost and--target aarch64-linux-android— one pre-existing warning (present.rs:131,manual_is_multiple_of) in a file this PR does not touch; no new warnings.#[no_mangle]added, sotools/exports/andcheck-exports.share untouched.Not run on a device yet. Two things to watch there:
proxy swapchain createand the format/refusal lines are the tell.-Dglx=false, so that path is closed by absence rather than by this shim.