icd_shim: the swapchain image count is the guest's, and one ring behind it

Merged
JO
4d ago
+259-136

Two commits on top of master, both from the same question: who owns the swapchain's image count.

1. The image count is the guest's (fd48d9d)

The shim asked for minImageCount + 1 — plus one more for mailbox, plus one more for frame generation — while advertising a minImageCount of 2. That is the Winlator app-side swapchain's idiom (caps.minImageCount + 1), carried into a component that writes the capabilities itself, and it produced Mesa's totals only by accident (2 + 1 = 3, 2 + 1 + 1 = 4). What it cost was the ask: a guest that sizes its per-image state from vkGetSwapchainImagesKHR got a number it never asked for, which is exactly what the ten exes in image_count.rs fail on.

Mesa's convention (wsi_common_wayland.c), now followed:

  • the caps advertise SC_DEFAULT_IMAGES = 3 (WSI_WL_DEFAULT_NUM_IMAGES)
  • creation honours the ask, with a mailbox floor of SC_MAILBOX_IMAGES = 4 (WSI_WL_BUMPED_NUM_IMAGES) — a floor, not an addend
  • SC_FRAMEGEN_IMAGES = 3 is the one shim-specific term, a floor on the source images, and a no-op for every conforming guest

The count no longer depends on the declared mode, so slot_mode, extra_slot and the create-time choose_live_mode call go. Shipped counts are unchanged for most sessions — DXVK asks minImageCount + 1 of the caps, so 4 either way. The changes are a native guest that honoured the old caps (3 → its own 3) and a vkd3d guest (4 → 3). A game on Mesa's strict list still gets exactly its ask with no bump of any kind.

image_count.rs loses its StrictGame struct with it: with every surface advertising 3, Mesa's vk_x11_override_min_image_count entry (Hades, 3) has nothing to override.

2. One ring behind the guest's images (5b788a0)

A strict game's hidden copies and frame generation's destinations are two mechanisms that never run together (spare_in_use is false whenever generation is live), but they were allocated as two consecutive sets: hidden slots first, then 3 × (multiplier − 1) + 2 destinations after them. A listed game with generation on paid for both — DOOM at x2 held 2 + 4 + 5 = 11 slots at 720p where 7 would do.

They now share one ring sized dest_ring(hidden, destinations) = the larger, never the sum. Sharing is safe because both allocators free a slot through the same listener (on_buffer_release clearing in_flight under shared); the hidden allocator, whose range used to be disjoint, now also skips slots a queued batch is about to be written into (gen_due, this swapchain's only). That guard is what makes a mid-session generation toggle safe.

Memory

sessionslots beforeslots after
DOOM, generation off2 + 4 = 62 + 4 = 6 (unchanged)
DOOM, generation x22 + 4 + 5 = 112 + 5 = 7
DOOM, generation x42 + 4 + 11 = 172 + 11 = 13
non-strict, generation offask + 1ask
non-strict, generation onask + 2max(ask, 3)

At 720p one slot is 3.69 MB, so the listed-game-with-generation case saves 14.7 MB per swapchain.

Verified

clippy -D warnings clean (shim, uapi, android, daemon); 124 + 99 + 36 + 13 + 19 + 8 + 2 + 2 tests pass; build-android.sh rc 0; check-exports.sh PASS.

Measured on device (Retroid Pocket Nova, 1280×960)

Menu scenes only, one session per title, each ended through Back → QuickMenu → Exit. Same device, same three titles, the branch payload against master.

Each arm is identified by its rule, not just its md5: ask 3 → 3 images is this branch, ask 3 → 4 and 5 is master's ask + 1 + extra.

titlearmask → slotsswapchainsslot memorymenu
Hades IIbranch3 → 3, 3 → 3229.5 MB120 fps
Hades IImaster3 → 4, 3 → 5244.2 MB120 fps
Davebranch4 → 4119.7 MB60 fps
Davemaster3 → 4119.7 MB60 fps
Nidhoggbranchno Vulkan swapchain—060 fps
Nidhoggmasterno Vulkan swapchain—058 fps

One slot at this extent is 1280×960×4 = 4.92 MB.

Hades II: −3 slots, −14.7 MB. That is the change working as intended, and it also exposes a defect in the old rule: master gives one process's two swapchains different counts (4 and 5) for the same ask, because slot_mode follows the declared mode and the declared mode races the cap note — one swapchain is created before it (Declared::CapCompat, the guest's FIFO, no spare) and one after (mailbox, spare). The branch gives 3 and 3 whichever instant creation lands.

Dave: unchanged, as designed. It asks caps + 1 (DXVK), so master computes (2+1) + 1 = 4 and this branch 3 + 1 = 4 — the same number from opposite directions. DXVK titles are not what this saves.

Nidhogg: not applicable. OpenGL through Xwayland; it never creates a Vulkan swapchain, so there is no slot for the rule to size.

PSS was recorded and is not used as evidence: it moved −29 MB / +13 MB / −74 MB between arms, far more than the ~15 MB of slots, and in the wrong direction for the title where nothing changed. EGL mtrack does not track the pool either (30.3 MB identical across Hades II's two arms). The created count from proxy swapchain … images is the only deterministic instrument here.

Not verified — device runs pending

  1. A generation-on session at the new ring size (5 destinations at x2, unchanged in number, but now shared with the hidden range).
  2. A mid-session generation toggle — the gen_due guard is the only thing between a copy and a slot being drawn into.