icd_shim: serve VK_KHR_xcb_surface so a GL guest takes the acquire-gate fps cap
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 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. No swapchain of ours means no acquire call of ours, so the frame cap applied in gn_acquire_common (checked before a slot is handed out) never reached a GL guest, and only Vulkan was paced.
Serve the extension:
- advertise
VK_KHR_xcb_surfaceand strip it from the real instance create, like the other two proxied surface extensions; - handle
vkCreateXcbSurfaceKHRandvkGetPhysicalDeviceXcbPresentationSupportKHR; - 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, 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:
- gamescope's paint gate reaches a client only through
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. - The cap is enforced at acquire: a frame that may not start yet waits before it is given an image. Nothing rendered is thrown away and nothing already on its way to the panel is delayed. A display-side limiter costs up to a period of display delay by construction.
- One limiter, one number: the app's target reaches the shim, and the shim paces the guest. Two limiters (gamescope's + ours) with different rounding is the mismatch this PR removes for GL.
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
| file | change |
|---|---|
icd_shim/src/proxy.rs | proxy module note; VK_KHR_xcb_surface advertised (extra 2→3) and stripped from the real create; gn_vkCreateXcbSurfaceKHR, gn_vkGetPhysicalDeviceXcbPresentationSupportKHR; shared x11_surface() |
icd_shim/src/vk.rs | VkXcbSurfaceCreateInfoKHR |
docs/04-gamenative-app.md | surface interception: three surface extensions, why xcb exists, both X11 routes land in one surface |
docs/19-fps-limiter-acquire-rework.md | GL row of the placement matrix; §7 risk list: zink guest is capped at acquire, a GL client presenting without Vulkan (glx/DRI3 into Xwayland) still never reaches the ICD |
Verification
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
#[no_mangle]added, sotools/exports/andcheck-exports.share untouched.
Not run on a device yet. Two things to watch there:
- A GL guest now creates its swapchain on the shim, so any WSI gap the Vulkan path has becomes reachable from GL —
proxy swapchain createand the format/refusal lines are the tell. - A GL client that presents without Vulkan at all (glx straight into Xwayland) still never touches the ICD and has no cap from us. Xwayland is built
-Dglx=false, so that path is closed by absence rather than by this shim.