icd_shim: answer the present wait at commit under mailbox/immediate (47 -> 60 fps at cap 60)
icd_shim: answer the present wait at commit under mailbox/immediate, and answer replaced frames
Branch present-wait-answered-at-commit-in-mailbox (47dc1df), base master (52ed43f). Line numbers are at 47dc1df; DXVK references are to gn-2.7.1-gn3, standing in for the container's 2.7.1-gplasync.
Problem
Under gamescope, Dave the Diver at a 60 fps cap on the RP6's 120 Hz panel ran at 47.05 fps, with SF intervals alternating 2 and 3 panel periods. A 30 cap held exactly. X11 ran the same scene at 59.89. Per frame delivered, CPU and GPU cost tie with X11: the gap is frames, not resources. Moving gamescope's vblank onto the panel latch grid (fix 1) cut gamescope's cost but left the frame rate at 46.91.
Mechanism
- The shim's mode differs from the guest's. The container sets
MESA_VK_WSI_PRESENT_MODE=mailbox, soeffective_present_mode(icd_shim/src/proxy.rs:8988) makes the swapchain mailbox (5 slots), and every commit declares mailbox to gamescope. - The guest still believes it has FIFO. The game presents with vsync and the shim offers FIFO, so DXVK calls
vkWaitForPresentKHR(n)for every frame (dxvk_presenter.cpp:1258-1267). DXVK skips that wait only for MAILBOX and IMMEDIATE. - The app thread waits on that answer. After presenting frame m,
D3D11SwapChain::SyncFrameLatencyblocks until frame m-L is signaled (d3d11_swapchain.cpp:626-657), and DXVK signals a frame only after its present wait returns. - The shim answered late.
present::wait(present.rs:204) returned on the first of:- the buffer's
wl_buffer.release, which cannot come before frame n+1 is committed; - a fixed grace of two panel periods after commit (
commit_grace_ns,present.rs:54), 16.7 ms at 120 Hz; - the daemon's latch report, which is read only from
on_buffer_release(proxy.rs:6887→scanout::poll_notes,scanout.rs:672) and never exists when gamescope composites.
- the buffer's
- The loop:
F = (a + p + 2P) / (L + 1), wherea + pis one frame's start-to-commit time. With L = 2 and about 47 ms, F = 21.2 ms = 47 fps, which shows on a 8.33 ms latch grid as 2/3 alternation with 1/4 tails. - Cap 30 holds because the shim's acquire gate (33.3 ms,
gn_acquire_common,proxy.rs:2247) is longer than that loop. gamescope's A52 limiter cannot bind this client: it only gateswl_surface.framecallbacks, which the shim never requests. - Why fix 1 could not help: the grace is a fixed time after commit, whatever gamescope's grid.
Model (present_wait_model.py), cap 60, L = 2:
| Present-wait answer | fps |
|---|---|
| today: min(release, commit + 2P) | 47.0 (hist 1:125, 2:2335, 3:2186, 4:349; device 626 / 3555 / 3537 / 697) |
| daemon latch report, read promptly | 52.0 |
wp_presentation.presented, latch-grid ticks | 56.3 |
| at commit (this PR) | 59.8 (57.7 if L = 3) |
| cap 30, any row | 30.0 |
Change
proxy.rscommit_real(:1697, branch at:1719-1726): if the swapchain's effective mode is mailbox or immediate (answers_present_wait_at_commit), the slot'swait_idis taken under the slot lock and answered at commit (present::committed(id, now, true)→record). FIFO and FIFO_RELAXED are unchanged: the grace clock plus the release answer.proxy.rspresent_mailbox(:2527,:2546): when a newer frame replaces a pending one, the replaced slot'swait_idis answered on the spot (present::discard) and its stalepresent_idis cleared. Before, that id was never committed, released or latched, so a wait on it sat out the 1 s bound inpresent::wait: a latent 1 s hitch the measured runs did not hit.proxy.rsgn_vkQueuePresentKHR(:2649,:2732): a slot presented again while it still carries an armed id answers that id first.present.rs:committed()(:86) chooses between an answer at commit and the grace clock;discard()(:173) answers an id only if it is still armed, andcomplete_on_release(:164) delegates to it;note_outcomeusesis_multiple_of(clippy).
Buffer lifetime is untouched: slots still return only on wl_buffer.release. No format, layout or plane change.
Tests
- New:
present::tests::{a_fifo_commit_is_answered_only_after_the_grace, a_mailbox_commit_is_answered_at_the_commit, a_replaced_frame_is_answered_without_a_commit},proxy::tests::present_wait_is_answered_at_commit_only_for_unpaced_modes. cargo test -p adrenotools_gamescope --lib: 64 passed (Linux aarch64 container).cargo clippy --target aarch64-linux-android -p adrenotools_gamescope --all-targets -- -D warnings: clean.
Device results
Launch 4: fix 1 (gamescope A53/A54 + daemon 0630e1a + app -r 120) plus this shim, logged as gn-vkshim: ICD loaded [shim 202610310936 47dc1df] in the game process.
Setup, every column. Retroid Pocket 6 (Adreno 740 class, 1080x1920 panel at 120 Hz, landscape), Dave the Diver (1868140), DXVK 2.7.1-gplasync, proton-10.0-arm64ec-2, container 1280x720, FPS limiter as listed, the same boat-deck scene. Segments are 180 s (launch 4: 60-120 s). "SF" = the game's SurfaceFlinger layer histogram; "PowerMetrics" = the app's own counter (5 s samples). gamescope-xwm is the steamcompmgr thread.
| Cap 60, cursor hidden | X11 | Shipped gamescope (A52, -r 1200) | Fix 1 | Fix 1 + shim fix |
|---|---|---|---|---|
| fps, SF | 59.89 | 47.05 | 46.91 | 59.13 (47 s segment) |
| fps, PowerMetrics | 59.9 | 47.1 | 46.9 | 59.97 (59.3-60.3) |
| 2x / 3x panel-period share, SF | 95.2 / 2.5 % | 41.5 / 41.3 % | 43.3 / 45.6 % | 94.2 / 2.7 % |
| frame interval p95 / p99, SF, ms | 16.7 / 25.0 | 33.3 / 41.6 | 33.3 / 33.3 | 16.7 / 25.0 |
| game process CPU, % of a core | 176.1 | 127.7 | 128.4 | 135.0 (fix 1 + shim + cursor plane run, 60 s) |
| gamescope CPU, % of a core | — | 8.1 | 5.9 | 6.6 (same run) |
| gamescope wakeups/s | — | 1723 | 623 | not captured (segment cut) |
| gamescope-xwm voluntary wakeups/s | — | 1411 | 424 | ~520 (same run) |
| GPU busy %, PowerMetrics | 79.9 | 60.4 | 59.8 | 77.3 |
| CPU temp °C, PowerMetrics mean | 76.5 | 69.8 | 66.3 | 73.7 |
| Cap 30, cursor hidden | X11 | Shipped gamescope (A52, -r 1200) | Fix 1 | Fix 1 + shim fix |
|---|---|---|---|---|
| fps, SF | not run | 30.00 | 30.00 | not run |
| 2x / 3x / 4x share, SF | not run | 0.1 / 7.0 / 85.4 % | 0.1 / 5.3 / 88.9 % | not run |
| gamescope CPU, % of a core | — | 10.2 | 4.2 | not run |
| gamescope wakeups/s | — | 1510 | 401 | not run |
| gamescope-xwm voluntary wakeups/s | — | 1281 | 269 | not run |
| GPU busy %, PowerMetrics | not run | 38.5 | 38.1 | not run |
| CPU temp °C, PowerMetrics mean | not run | 61.9 | 63.9 | not run |
The fix 1 + shim fix column: SF fps and histogram from a 47 s cap-60 segment (launch 4); PowerMetrics 59.97 fps over 143 samples; game/gamescope CPU from the 60 s hidden-cursor segment of the cursor-plane run (fix 1 + shim + cursor layer, cursor hidden), where the SF fps was 60.00. Temperatures are loose: the X11 run started hot (CPU zone 92.7 °C at segment start).
47.05 → about 60 fps at cap 60, with GPU busy at 77 % (X11: 80 %). GNDRM_LOG_VKSHIM was off, so the present wait … done lines and the swapchain's effective_mode=1 were not captured.
Independence from fix 1
The change is entirely in the shim and does not depend on gamescope's grid, so it should give the same result with the shipped gamescope (A52, -r 1200). That combination was not tested: the only device run with this shim also had fix 1.
Risks
- Latency: in mailbox, DXVK may now run up to L frames ahead of the display. The mailbox shows the newest frame and the acquire gate holds the rate at the cap.
- Real FIFO swapchains (no mailbox override) are unaffected and keep the 2P grace. The follow-up (R2) answers on gamescope's
wp_presentationfeedback, which works for FIFO and in composite mode. - L is unverified for this game. With L = 3 the model gives about 58, not 60; the device reading of 59.97 fits L = 2.
- Cap 30 and the moving-cursor case were not run with this shim. The model and the acquire gate predict cap 30 is unchanged.