icd_shim: answer the present wait at commit under mailbox/immediate (47 -> 60 fps at cap 60)

Merged
UT
10d ago
+125-8

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, so effective_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::SyncFrameLatency blocks 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 loop: F = (a + p + 2P) / (L + 1), where a + p is 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 gates wl_surface.frame callbacks, 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 answerfps
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 promptly52.0
wp_presentation.presented, latch-grid ticks56.3
at commit (this PR)59.8 (57.7 if L = 3)
cap 30, any row30.0

Change

  • proxy.rs commit_real (:1697, branch at :1719-1726): if the swapchain's effective mode is mailbox or immediate (answers_present_wait_at_commit), the slot's wait_id is 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.rs present_mailbox (:2527, :2546): when a newer frame replaces a pending one, the replaced slot's wait_id is answered on the spot (present::discard) and its stale present_id is cleared. Before, that id was never committed, released or latched, so a wait on it sat out the 1 s bound in present::wait: a latent 1 s hitch the measured runs did not hit.
  • proxy.rs gn_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, and complete_on_release (:164) delegates to it; note_outcome uses is_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 hiddenX11Shipped gamescope (A52, -r 1200)Fix 1Fix 1 + shim fix
fps, SF59.8947.0546.9159.13 (47 s segment)
fps, PowerMetrics59.947.146.959.97 (59.3-60.3)
2x / 3x panel-period share, SF95.2 / 2.5 %41.5 / 41.3 %43.3 / 45.6 %94.2 / 2.7 %
frame interval p95 / p99, SF, ms16.7 / 25.033.3 / 41.633.3 / 33.316.7 / 25.0
game process CPU, % of a core176.1127.7128.4135.0 (fix 1 + shim + cursor plane run, 60 s)
gamescope CPU, % of a core—8.15.96.6 (same run)
gamescope wakeups/s—1723623not captured (segment cut)
gamescope-xwm voluntary wakeups/s—1411424~520 (same run)
GPU busy %, PowerMetrics79.960.459.877.3
CPU temp °C, PowerMetrics mean76.569.866.373.7
Cap 30, cursor hiddenX11Shipped gamescope (A52, -r 1200)Fix 1Fix 1 + shim fix
fps, SFnot run30.0030.00not run
2x / 3x / 4x share, SFnot run0.1 / 7.0 / 85.4 %0.1 / 5.3 / 88.9 %not run
gamescope CPU, % of a core—10.24.2not run
gamescope wakeups/s—1510401not run
gamescope-xwm voluntary wakeups/s—1281269not run
GPU busy %, PowerMetricsnot run38.538.1not run
CPU temp °C, PowerMetrics meannot run61.963.9not 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_presentation feedback, 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.