Repository navigation
Shim waits for the panel to release a buffer before the guest renders into it - #60
utkarshdalal wants to merge 2 commits into
Conversation
…renders into it
DXVK games under gamescope tear on 60 Hz panels above about 40 fps, vsync on
or off. On direct scan-out the daemon publishes the guest's pool buffer and
sends gamescope's page-flip event with the publish, so gamescope hands back
the buffer that flip replaced while SurfaceFlinger may still be scanning it
out. The shim frees the slot on that release, the guest acquires it at once
and renders into the buffer on the panel. The daemon's own release-fence wait
only guards its next publish of that buffer, not the guest's writes.
The flip-event timing stays as it is (holding the event for the release
halves the rate above half the panel's), so the release goes to the buffer's
producer instead:
- wire: a ReleaseFence note ("GNRF") on the scan-out channel, sent only to a
connection that declared CAP_RELEASE_FENCE. A pending note says a buffer was
replaced on the layer and its fence follows; the final note carries the
release fence as a sync file (SCM_RIGHTS), or none when SurfaceFlinger
reported none.
- daemon: each show transaction records the inode of the buffer it replaces.
The pending note goes out when the transaction is built, before the apply
and so before the flip event; the completion callback forwards the
previous-release fence it already queries for that inode. The daemon's own
wait is unchanged.
- shim: subscribes with CAP_RELEASE_FENCE, keeps notes only for buffers it
was handed, and at acquire waits for the slot's release (the note still on
its way, then the fence) for at most 33 ms or the acquire's timeout. Each
fence is waited once and closed; a fence that arrives after its wait ran
out is dropped; a timeout is counted and logged once. The wait is on the
CPU: the fence only exists once the replacing transaction completes, which
can be after the acquire, and a binary semaphore cannot be waited before
its signal is submitted. An older daemon sends no notes and acquire is as
before.
clippy's doc_markdown lint (-D warnings) flags the bare name.
Frame pacing: read this before the device runThe fix is right for tearing, but it changes what bounds an acquire, so the pacing question is worth settling explicitly rather than discovering on a panel. Three risks, in the order I'd check them. 1. It collides with the acquire gate at a cap equal to the panel rateThe wait lands at the end of (*(*sc).slots.as_mut_ptr().add(idx as usize)).acquired_ns = now_ns;
if gated {
(*sc).release.book_paced(now_ns, pacing); // ← the gate says "start now"
}
break;
...
crate::scanout::wait_release(ino, budget); // ← and then the acquire blocks
The fragile combination is cap == panel rate with If it does bite, the order is the fix rather than the wait: take the release wait before the gate books, so the gate's release instant stays the last thing that happens before the guest renders. 2. A missing note costs 33 ms, not one frame
3. With two images there is no alternative; with three or more most of the wait is avoidableTwo images means one free slot, so "wait" or "tear" are the only options — that is why this is the right fix and not a wrong one. With three or more, Note on the counters
Suggestion: re-derive the acquire gate against this changeSeparately from the fix itself, this PR breaks an assumption the gate was built on, and I think the gate is worth a look as a whole rather than patched around. The assumption that no longer holds. The gate's model is: a slot that is free is a slot that is renderable, so releasing the guest at grid instant k gives it from k to k+1 to render and commit. That was true while the release came from the compositor's What that implies for the gate, concretely:
What I'd suggest checking, in one device run per arm (cap 60 / cap 30 / uncapped, same scene,
If that comes back clean, the gate needs nothing. If it doesn't, the ordering fix is small but the reasoning in |
Problem
A user reports tearing in DXVK games under gamescope on a Mangmi Air Y Pro (SD865, 1280x960 at 60 Hz): Skyrim LE, Arkham City, L4D and NFS Most Wanted 2012. It only shows above about 40 fps, with vsync on or off, and screen recordings don't capture it. That points at scan-out, not at the frames themselves.
Mechanism
On direct scan-out the guest's pool buffer goes straight to the panel, and the guest gets that buffer back before the panel has finished reading it:
modules/gndrm-daemon/src/lib.rs:2305,commit_worker_loop).page_flip_handlerthen releases the buffer that flip replaced (wl_buffer.release).icd_shim/src/proxy.rs:8065,on_buffer_release), and the game's nextvkAcquireNextImageKHRhands it out right away.modules/gndrm-android/src/asc.rs:1467), but that wait only guards the daemon's own next publish of the slot, not the game's writes into it.The shim already waits for the game's render fence before it commits, so a frame that is still rendering is not the cause.
Why not a daemon-only fix
The fix
The flip event keeps its timing. The release goes to the process that renders into the buffer:
modules/gndrm-uapi/src/wire.rs): a newReleaseFencenote ("GNRF") on the existing scan-out channel. It is only sent to connections that declaredCAP_RELEASE_FENCE(bit 3). There are two kinds:inowas replaced on the layer, and its fence will follow;SCM_RIGHTS, or no fd when SurfaceFlinger reported none (the buffer is already free).modules/gndrm-android/src/asc.rs):build_show(:3607) sends the pending note before the apply, so it is on the shim's socket before the flip event leaves.on_complete(:414) pops that entry and forwards theASurfaceTransactionStats_getPreviousReleaseFenceFdit already queries. The fd is borrowed (pushes::push_with_fd,modules/gndrm-daemon/src/pushes.rs:133), so the daemon's own wait still gets its copy.icd_shim/src/scanout.rs):CAP_RELEASE_FENCEand keeps notes only for buffers it was handed (record_release,:850).vkAcquireNextImageKHRhands out a slot (icd_shim/src/proxy.rs:2847),wait_release(:976) waits for that slot's release: first for a note still on its way, then for the fence. The wait is bounded by 33 ms or the acquire's own timeout, and atimeout == 0acquire never blocks.poll_notesnow drains every queued frame and receives descriptors.Why the wait is on the CPU and not imported into the acquire semaphore/fence. The release fence only exists once the transaction that replaced the buffer completes, and that is normally after gamescope released the buffer, so often after the acquire. Binary semaphores can't be waited on before their signal is submitted, and the shim signals the guest's semaphore/fence with an empty submit inside the acquire. So a fence that hasn't arrived yet has to be waited for on the CPU anyway. When it does arrive (SurfaceFlinger's completion fires after the replacement is presented), it has usually already signalled, so the poll returns immediately. An import would also need
VK_KHR_external_semaphore_fdenabled on the game's device, which the shim does not control.Compatibility. An old daemon sends no notes, so the shim behaves as on master. An old shim never subscribes, so the daemon sends it nothing.
What is unchanged
Test plan
Host tests ran in
rust:1-bookwormon aarch64. The crates use Linux libc, so they don't build on macOS.cargo test -p gndrm-uapi --lib: 20 passed. Covers the GNRF round-trip and rejection of other frames. (abi_ground_truthneeds the Linux C headers; it fails the same way on master on a macOS host.)cargo test -p gndrm-daemon: 100 + 8 + 36 passed.push_with_fdsends the fd only toCAP_RELEASE_FENCEsinks, and each sink gets its own duplicate.cargo test -p adrenotools_gamescope --lib: 130 passed, including these new cases:cargo clippy --target aarch64-linux-android -p gndrm-android -p adrenotools_gamescope -p gndrm-daemon -p gndrm-uapi --all-targets -- -D warnings: clean (77c581d fixes adoc_markdownlint).tools/build-android.shwith the darwin NDK linker.check-exportsPASS (daemon 20 exports, shim 3) andcheck-elf-depsOK for both libraries. Thegn-utilsprobes don't link against the local prefix'slibvulkan; they don't ship and this PR doesn't touch them. 9e79a84 and 77c581d build byte-identical libraries:libgamenative_drm.so: sha25628d5d474273052cf24adf64b353139da2ee9c4c4ebf8b522c87427c7e85493c3, md5ece0bab966db488fec6876515194f3d6libadrenotools_gamescope.so: sha2562dcb1948521f82ed59ab254e6866d3c1fa124dca07354b8ec17701f347cc4ce6gamescope-gamenative-20261107.1.tzst(sha256a14c58fe…19cf). It must not merge before this PR.