icd_shim: serve VK_KHR_xcb_surface so a GL guest takes the acquire-gate fps cap

Merged
JO
11d ago
+214-81

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_surface and strip it from the real instance create, like the other two proxied surface extensions;
  • handle vkCreateXcbSurfaceKHR and 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, 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

filechange
icd_shim/src/proxy.rsproxy 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.rsVkXcbSurfaceCreateInfoKHR
docs/04-gamenative-app.mdsurface interception: three surface extensions, why xcb exists, both X11 routes land in one surface
docs/19-fps-limiter-acquire-rework.mdGL 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 clippy host 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, so tools/exports/ and check-exports.sh are untouched.

Not run on a device yet. Two things to watch there:

  1. A GL guest now creates its swapchain on the shim, so any WSI gap the Vulkan path has becomes reachable from GL — proxy swapchain create and the format/refusal lines are the tell.
  2. 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.