From d61b614ae3db8231d9cb1f096dd9370fb7a8e888 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 3 Nov 2026 15:19:22 +0530 Subject: [PATCH 1/3] shim: at a cap at the display's rate, a guest's FIFO request keeps the container's mode with the vblank-grid gate @joshuatam's capture on payload 20261103.4 (shim 2567925; a 1280x960 device, DTD in game, cap 60, env mailbox, in-game VSync on, 60 Hz) still settled at 40 fps with the gate off in the cap-rate cell: declared FIFO, 400 commits per 10 s against 400/600 daemon publishes. The shipped stack (2bfe8b3) does the same after a live 120 to 60 Hz switch. A guest with VSync on waits for each frame's presentation before it starts the next. Declared FIFO, that wait is answered by gamescope's presented at its latch, so the guest's whole frame has to fit between two latches; with about 16 ms of work at 60 Hz and three images, frames alternate between one and two vblanks. At 120 Hz the same session was declared mailbox with the gate, its wait answered at commit, and held 60 fps; on the Retroid Pocket 6 the same cell declared mailbox with the gate gave 59.983 fps and no doubles (B1). A guest with VSync off paces itself and, declared mailbox at this cap, had ~1 frame a second replaced in gamescope's mailbox (B4-B6), so FIFO stays for it. At a cap at the display's rate, with the container's mode not FIFO: - the guest asked for FIFO or FIFO-relaxed: the container's mode with the vblank-grid gate, one release per vblank (source=cap-rate-vsync); the present wait is answered at commit, and the swapchain keeps that mode's image count, the spare image included; - the guest asked for mailbox or immediate: FIFO, no gate (source=cap-rate), unchanged. An env of fifo still wins everywhere; caps below the display's rate, no cap, frame generation and cap-compat are unchanged. The census's slot_waits and commit_lat_us now count whether or not the gate runs, so a FIFO cell's image waits and acquire-to-commit time show in a GNDRM_LOG_VKSHIM=1 capture. - icd_shim/src/proxy.rs: Declared::CapRateVsync in choose_live_mode and the census (source=cap-rate-vsync); the slot count keeps the container's mode for it; acquired_ns and slot waits recorded on every acquire; tests: 60 on 59.97 Hz + env mailbox + guest fifo or relaxed -> mailbox, gate every vblank; guest mailbox or immediate -> FIFO, gate off; env fifo -> FIFO, gate off; 60 on 120 and no cap unchanged. - docs/10, docs/15: the rule table and the reason. --- docs/10-fps-limit.md | 24 ++++++-- docs/15-fps-limiter-pacing-and-vsync.md | 6 +- icd_shim/src/proxy.rs | 73 ++++++++++++++++++++----- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/docs/10-fps-limit.md b/docs/10-fps-limit.md index 36fb8b3..b96dc9a 100644 --- a/docs/10-fps-limit.md +++ b/docs/10-fps-limit.md @@ -119,7 +119,8 @@ divisor cap reads its target exactly. |---|---|---|---| | mailbox (default) | none, guest asked for FIFO (in-game VSync on) | fifo (`source=guest`) | the display (FIFO) | | mailbox (default) | none, guest asked for mailbox or immediate | mailbox | nobody | - | mailbox | at the display's rate | fifo (`source=cap-rate`) | gamescope's limiter (divisor 1), one FIFO commit per vblank; no gate, as on master | + | mailbox | at the display's rate, guest asked for FIFO (in-game VSync on) | mailbox (`source=cap-rate-vsync`) | the shim's acquire gate, every vblank; the present wait is answered at commit | + | mailbox | at the display's rate, guest asked for mailbox or immediate | fifo (`source=cap-rate`) | gamescope's limiter (divisor 1), one FIFO commit per vblank; no gate, as on master | | mailbox | below the display's rate | mailbox | the shim's acquire gate, on the display's vblank grid | | fifo | none | fifo | the display (FIFO) | | fifo | set | fifo | gamescope's divisor limiter | @@ -167,7 +168,17 @@ divisor cap reads its target exactly. 2.5 ms boundary gave 59.0 with ~0.8 a second still replaced; FIFO on master gives 59.22 on the same scene. Under FIFO gamescope latches one commit per vblank at its own wakeup (`steamcompmgr.cpp` 8028-8049) and its limiter's - divisor at this cap is 1, which is master's behaviour in this cell. The + divisor at this cap is 1, which is master's behaviour in this cell. That holds + for a guest that asked for mailbox or immediate, which paces itself. A guest + that asked for FIFO (in-game VSync on) waits on each frame's presentation + before starting the next; declared FIFO, that wait is gamescope's `presented` + at its latch, so the whole frame has to fit between two latches, and with + ~16 ms of work and three images it fell to 40 fps at 60 Hz (a 1280x960 + device, DTD in game: 400 commits per 10 s, 400/600 publishes). That guest is + declared the container's mode with the gate every vblank instead + (`source=cap-rate-vsync`): its wait is answered at commit, the gate paces it, + and the swapchain keeps the mailbox image count (Retroid Pocket 6, DTD, + VSync on, cap 60: 59.983 fps, no doubles). The vblank-grid gate is not kept on top of it: FIFO's back-pressure already holds the guest, and the two stacked turned a guest whose acquire-to-commit grew past a period into a 40 fps session on a 60 Hz panel (DTD on a 1280x960 @@ -210,7 +221,9 @@ divisor cap reads its target exactly. `present mode: declared=… requested=… source=env|guest|cap-rate|cap-compat route=gamescope cap=… gate=vsync|clock|off lsfg=…` (`source=guest`: the guest's own mode, no env or a FIFO request with no cap; `source=cap-rate`: declared FIFO - because the cap is the display's rate, with `gate=off`; `source=cap-compat`: + because the cap is the display's rate and the guest paces itself, with + `gate=off`; `source=cap-rate-vsync`: the container's mode with `gate=vsync` + because the cap is the display's rate and the guest asked for FIFO; `source=cap-compat`: declared FIFO for want of the stamp lead, with `gate=off`; `gate=clock` is frame generation's source rate or a cap above the display's rate). With `GNDRM_LOG_VKSHIM=1` the same line repeats every 10 s with the counters a device @@ -221,8 +234,9 @@ divisor cap reads its target exactly. gate held; `gate_late`, acquires that started after their release point (not the daemon's `late=` on its `vsync: check` line, which counts publishes that missed their latch); `skipped`, release points moved past for them; - `slot_waits`, acquires that passed the gate and then waited on a compositor - release for a free image; `commit_lat_us`, the smoothed acquire-to-commit time; + `slot_waits`, acquires that passed the gate (if any) and then waited on a + compositor release for a free image; `commit_lat_us`, the smoothed + acquire-to-commit time (both counted whether or not the gate runs); `spacing=[<15:… 15-16.2:… 16.2-17.2:… 17.2-20:… >20:…]`, the spacing of consecutive commits in ms since the last line, the game's own cadence as it reaches gamescope. Off the override route (the opt-in X11 diff --git a/docs/15-fps-limiter-pacing-and-vsync.md b/docs/15-fps-limiter-pacing-and-vsync.md index 479b86d..7b8c67e 100644 --- a/docs/15-fps-limiter-pacing-and-vsync.md +++ b/docs/15-fps-limiter-pacing-and-vsync.md @@ -22,8 +22,10 @@ fifo, or no env and a FIFO guest), gamescope's limiter passes every and holds the guest's commits to it. Declared mailbox, immediate or FIFO-relaxed (mailbox is the container default), the limiter does not hold them and the shim's acquire gate releases the guest's frames on the display's vblank grid. A container in one of those -modes under a cap at the display's rate is declared FIFO with no gate -(`source=cap-rate`), as on master: gamescope latches one commit per vblank at its own +modes under a cap at the display's rate is declared FIFO with no gate when the guest +asked for mailbox or immediate (`source=cap-rate`), as on master, and keeps its mode with +the gate every vblank when the guest asked for FIFO (`source=cap-rate-vsync`, `docs/10`). +For the former: gamescope latches one commit per vblank at its own wakeup, which the shim cannot see under mailbox, and its limiter (divisor 1) paces the guest (`docs/10`). Under frame generation the app disarms gamescope's limiter and the shim's time gate holds the source rate instead diff --git a/icd_shim/src/proxy.rs b/icd_shim/src/proxy.rs index 2cdde55..a841bdd 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -2012,7 +2012,9 @@ unsafe extern "C" fn gn_vkCreateSwapchainKHR( // than its own mode (no stamp lead yet, a cap at the display's rate, a // guest's FIFO request with no cap), the guest's own request decides the // count too, as on master: a spare image would only deepen gamescope's FIFO - // queue. + // queue. A cap at the display's rate with a guest's FIFO request declares + // the container's mode with the gate (`Declared::CapRateVsync`), and keeps + // that mode's count, the spare image included. let declared_at_creation = choose_live_mode( mode, mode_source, @@ -2425,18 +2427,19 @@ unsafe fn gn_acquire_common( } unsafe { acquire_take_free(sc, &mut idx) }; if idx >= 0 { + let now_ns = monotonic_ns(); + // For the census's acquire-to-commit time, gated or not. + (*(*sc).slots.as_mut_ptr().add(idx as usize)).acquired_ns = now_ns; if gated { // One timeline for the whole swapchain: the next frame may // not start before the release this one was booked to. - let now_ns = monotonic_ns(); - (*(*sc).slots.as_mut_ptr().add(idx as usize)).acquired_ns = now_ns; (*sc).release.book_paced(now_ns, pacing); } break; } - if gated && !slot_waited { - // Past the gate with no image free: this acquire waits on the - // compositor's release, not on the gate. + if !slot_waited { + // Past the gate (if any) with no image free: this acquire waits + // on the compositor's release. slot_waited = true; SLOT_WAIT_N.fetch_add(1, Ordering::Relaxed); } @@ -6299,10 +6302,17 @@ mod tests { choose_live_mode(mode, ModeSource::Guest, mode, false, 0, None), (mode, Declared::Mode) ); - // A cap at the display's rate: FIFO. + // A cap at the display's rate: FIFO for a guest that asked for + // mailbox or immediate, the swapchain's mode (with the gate) for + // one that asked for FIFO-relaxed. + let at_rate = if mode == r { + (r, Declared::CapRateVsync) + } else { + (f, Declared::CapRate) + }; assert_eq!( choose_live_mode(mode, ModeSource::Guest, mode, false, 60, g60), - (f, Declared::CapRate) + at_rate ); // A cap below it: the swapchain's mode. assert_eq!( @@ -6413,6 +6423,7 @@ mod tests { let label = match (choice.1, source) { (Declared::CapCompat, _) => "cap-compat", (Declared::CapRate, _) => "cap-rate", + (Declared::CapRateVsync, _) => "cap-rate-vsync", (Declared::GuestVsync, _) => "guest", (_, ModeSource::Env) => "env", (_, ModeSource::Guest) => "guest", @@ -6431,13 +6442,27 @@ mod tests { interval_ns: n * g.unwrap().period_ns, }; let off = Pacing::Off; - // Capped, env mailbox, whatever the guest asked for: - for guest in [f, m, i] { - // 60 on 59.97 Hz: FIFO, no gate (gamescope's limiter). + // Cap at the display's rate (60 on 59.97 Hz), env mailbox: a guest + // that asked for FIFO (VSync on) or FIFO-relaxed gets mailbox with the + // gate every vblank, its present wait answered at commit; one that asked + // for mailbox or immediate (self-paced) gets FIFO with no gate. + assert_eq!( + row(Some("mailbox"), f, 60, None, g60), + (m, "cap-rate-vsync", every(g60, 1)) + ); + assert_eq!( + row(Some("mailbox"), r, 60, None, g60), + (m, "cap-rate-vsync", every(g60, 1)) + ); + for guest in [m, i] { assert_eq!( row(Some("mailbox"), guest, 60, None, g60), (f, "cap-rate", off) ); + } + // Capped below the display's rate, env mailbox, whatever the guest + // asked for: + for guest in [f, m, i] { // 60 on 120 Hz, 30 on 60: mailbox, the gate every second vblank. assert_eq!( row(Some("mailbox"), guest, 60, None, g120), @@ -9489,8 +9514,13 @@ enum Declared { Generating, /// FIFO for want of the daemon's stamp lead (`source=cap-compat`). CapCompat, - /// FIFO because the cap is the display's rate (`source=cap-rate`). + /// FIFO because the cap is the display's rate and the guest asked for + /// mailbox or immediate (`source=cap-rate`). CapRate, + /// The container's mode with the vblank-grid gate because the cap is the + /// display's rate and the guest asked for FIFO or FIFO-relaxed + /// (`source=cap-rate-vsync`). + CapRateVsync, /// No cap and a guest that asked for FIFO or FIFO-relaxed: its own mode /// over the container's non-FIFO default (`source=guest`). GuestVsync, @@ -9507,8 +9537,16 @@ enum Declared { /// vblank-grid gate is not run without the lead: released on the latch /// points themselves, frames straddle gamescope's composite and are /// replaced in its mailbox. The census reads `source=cap-compat`. -/// - a cap at the display's rate and a mode other than FIFO: FIFO, with no -/// acquire gate (`source=cap-rate`), as on master. At that cap one +/// - a cap at the display's rate, a mode other than FIFO and a guest that +/// asked for FIFO or FIFO-relaxed (in-game VSync on): the swapchain's mode +/// with the vblank-grid gate, one release per vblank (`source=cap-rate-vsync`). +/// The guest's present wait is then answered at commit and the gate paces +/// it. Declared FIFO it was serialised behind gamescope's `presented` for +/// each frame, and with ~16 ms of work per frame and three images it fell +/// to 40 fps at 60 Hz. +/// - a cap at the display's rate, a mode other than FIFO and a guest that +/// asked for mailbox or immediate (self-paced): FIFO, with no acquire gate +/// (`source=cap-rate`), as on master. At that cap one /// frame per vblank is all the panel shows under either mode, and under /// mailbox the shim would have to keep two commits out of one gamescope /// composite without knowing when gamescope latches: its wakeup moves with @@ -9588,7 +9626,11 @@ fn choose_live_mode( match grid { None => (VK_PRESENT_MODE_FIFO_KHR, Declared::CapCompat), Some(g) if crate::vsync_gate::at_display_rate(cap_fps, g) => { - (VK_PRESENT_MODE_FIFO_KHR, Declared::CapRate) + if vsync_request { + (mode, Declared::CapRateVsync) + } else { + (VK_PRESENT_MODE_FIFO_KHR, Declared::CapRate) + } } Some(_) => (mode, Declared::Mode), } @@ -9692,6 +9734,7 @@ unsafe fn mode_census(sc: *mut GnSwapchain) { source: match (why, unsafe { (*sc).mode_source }) { (Declared::CapCompat, _) => "cap-compat", (Declared::CapRate, _) => "cap-rate", + (Declared::CapRateVsync, _) => "cap-rate-vsync", (Declared::GuestVsync, _) => "guest", (_, ModeSource::Env) => "env", (_, ModeSource::Guest) => "guest", From 8a5b695f9f63cea9ad05ff6b348af063f997adf3 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 3 Nov 2026 15:59:03 +0530 Subject: [PATCH 2/3] shim: answer a mailbox or immediate frame's present wait when the guest queues it, not after its render fence Retroid Pocket 6, PR 48 shim (d61b614), DTD at 1080p, cap 60, in-game VSync on, 60 Hz, declared mailbox with the vblank-grid gate: 42 fps with 23 ms from acquire to commit; the game's main thread at 43 % of a core, no core saturated; the GPU at 65-69 % busy and 550-615 MHz with no clock cap. In a heavier spot earlier the frame took 18 ms at 680 MHz and 88 % busy; 18 ms x 680/550 is the 22-23 ms seen now. The frame is GPU-time-bound, but the GPU's governor sees bubbles and does not ramp. The bubbles come from the present wait. DXVK with VSync on waits for the present of frame N - L before starting frame N (L its frame latency) and calls vkWaitForPresentKHR because its own swapchain is FIFO; the shim's mailbox declaration is invisible to it. The shim answered that wait in commit_real, after the worker had waited the frame's render fence, so with L = 1 the CPU of frame N+1 could not start until the GPU had finished frame N: CPU and GPU alternated instead of overlapping. On the X11 route the same game overlaps and holds 60: the wrapper's Mesa X11 WSI completes the id on PresentCompleteNotify, which the app's X server sends while handling the PresentPixmap request (PresentExtension.java), before the GPU is done. On gamescope's route a wait on a frame that goes out as mailbox or immediate, or whose guest asked for either, is now answered in present_dispatch, when the guest queues the present. The acquire gate still holds the rate. FIFO and FIFO-relaxed declarations keep the answer from gamescope's presentation feedback; off the route the answer stays at the commit. Frame generation (mailbox) is answered at queue the same way. Each id is answered once: present_dispatch takes it off the slot under the swapchain lock, so the commit, a mailbox replacement and the release see no wait to answer, and the daemon's later latch report finds the id no longer armed (present::record_for). A slot whose mode changed between queue and commit is still answered at the commit. The periodic census gains at_queue=, the waits answered this way; presented, discarded and fallback keep counting the compositor's answers. - icd_shim/src/proxy.rs: WaitAnswer and present_wait_answer; the answer in present_dispatch; commit_real answers what is left; census at_queue=; test: the answer point for each declaration, on and off the route. - icd_shim/src/present.rs: answered_at_queue and its counter. - docs/10, docs/15: where the wait is answered, with the measurement. --- docs/10-fps-limit.md | 5 +- docs/15-fps-limiter-pacing-and-vsync.md | 13 ++- icd_shim/src/present.rs | 16 ++++ icd_shim/src/proxy.rs | 103 +++++++++++++++++++++++- 4 files changed, 129 insertions(+), 8 deletions(-) diff --git a/docs/10-fps-limit.md b/docs/10-fps-limit.md index b96dc9a..6b67025 100644 --- a/docs/10-fps-limit.md +++ b/docs/10-fps-limit.md @@ -119,7 +119,7 @@ divisor cap reads its target exactly. |---|---|---|---| | mailbox (default) | none, guest asked for FIFO (in-game VSync on) | fifo (`source=guest`) | the display (FIFO) | | mailbox (default) | none, guest asked for mailbox or immediate | mailbox | nobody | - | mailbox | at the display's rate, guest asked for FIFO (in-game VSync on) | mailbox (`source=cap-rate-vsync`) | the shim's acquire gate, every vblank; the present wait is answered at commit | + | mailbox | at the display's rate, guest asked for FIFO (in-game VSync on) | mailbox (`source=cap-rate-vsync`) | the shim's acquire gate, every vblank; the present wait is answered when the guest queues the present | | mailbox | at the display's rate, guest asked for mailbox or immediate | fifo (`source=cap-rate`) | gamescope's limiter (divisor 1), one FIFO commit per vblank; no gate, as on master | | mailbox | below the display's rate | mailbox | the shim's acquire gate, on the display's vblank grid | | fifo | none | fifo | the display (FIFO) | @@ -176,7 +176,8 @@ divisor cap reads its target exactly. ~16 ms of work and three images it fell to 40 fps at 60 Hz (a 1280x960 device, DTD in game: 400 commits per 10 s, 400/600 publishes). That guest is declared the container's mode with the gate every vblank instead - (`source=cap-rate-vsync`): its wait is answered at commit, the gate paces it, + (`source=cap-rate-vsync`): its wait is answered when it queues the present + (`docs/15`), the gate paces it, and the swapchain keeps the mailbox image count (Retroid Pocket 6, DTD, VSync on, cap 60: 59.983 fps, no doubles). The vblank-grid gate is not kept on top of it: FIFO's back-pressure already holds diff --git a/docs/15-fps-limiter-pacing-and-vsync.md b/docs/15-fps-limiter-pacing-and-vsync.md index 7b8c67e..63d0006 100644 --- a/docs/15-fps-limiter-pacing-and-vsync.md +++ b/docs/15-fps-limiter-pacing-and-vsync.md @@ -73,9 +73,16 @@ that rate is the display's. periods after the commit when no release report arrives first. - **Mailbox and immediate.** A guest that asked for either does not wait, and neither would one whose frames go out as mailbox or immediate (frame generation, or the - container's env while the guest believes it has FIFO), so those waits are answered at - the commit rather than holding the guest's frame-latency loop for two periods - (`answers_present_wait_at_commit`). + container's env while the guest believes it has FIFO), so those waits are not held for + the compositor (`present_wait_answer`). On gamescope's route they are answered when the + guest queues the present, before the frame's render fence, as the X11 route's X server + answers PresentCompleteNotify at the PresentPixmap request: DXVK's frame-latency wait + then lets the CPU of the next frame run while the GPU renders this one, and the acquire + gate holds the rate (`at_queue=` in the periodic census). Answered after the fence, a + latency-1 guest alternated CPU and GPU: Retroid Pocket 6, DTD at 1080p, cap 60, VSync + on, 60 Hz, 42 fps with 23 ms from acquire to commit, the GPU at 65-69 % busy and + 550 MHz because its governor saw the bubbles. Off that route they are answered at the + commit. - **A replaced frame.** Under mailbox a frame can be replaced before it is shown. It is never committed, released or latched under that id, so its wait is answered when it is discarded instead of timing out. diff --git a/icd_shim/src/present.rs b/icd_shim/src/present.rs index 95c6ed7..c974053 100644 --- a/icd_shim/src/present.rs +++ b/icd_shim/src/present.rs @@ -199,6 +199,22 @@ pub(crate) fn committed(id: u64, sc: usize, commit_ns: u64, answer_now: bool) { } } +/// Waits answered when the guest queued the present (mailbox or immediate on +/// gamescope's route), for the census. +static AT_QUEUE_N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +/// A wait on `id` answered when the guest queued its present, before the +/// frame's render fence (see `proxy::present_wait_answer`). +pub(crate) fn answered_at_queue(id: u64, sc: usize, ns: u64) { + AT_QUEUE_N.fetch_add(1, Ordering::Relaxed); + record_for(id, sc, ns); +} + +/// How many waits were answered at queue. +pub(crate) fn at_queue_count() -> usize { + AT_QUEUE_N.load(Ordering::Relaxed) +} + /// Whether the guest enabled each feature. The shim serves both, so the answer /// is whichever the guest asked for at device creation. static ID_ENABLED: AtomicBool = AtomicBool::new(false); diff --git a/icd_shim/src/proxy.rs b/icd_shim/src/proxy.rs index a841bdd..f82a361 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -1770,8 +1770,15 @@ unsafe fn commit_real(sc: *mut GnSwapchain, idx: i32, fence_ms: u128) { // neither would one whose frames go out as mailbox or immediate // (the container's mode, or frame generation): both are answered // here. A FIFO guest declared FIFO keeps the release-or-grace answer. - let at_commit = answers_present_wait_at_commit((*sc).requested_mode) - || answers_present_wait_at_commit(live_present_mode(sc)); + // On gamescope's route a mailbox or immediate frame's wait was + // answered when the guest queued it (`present_dispatch`), so a wait + // still on the slot here is one whose mode changed since; it is + // answered now. + let at_commit = present_wait_answer( + !(*(*sc).surf).swapchain_obj.is_null(), + (*sc).requested_mode, + live_present_mode(sc), + ) != WaitAnswer::Later; let id = if at_commit { let _g = (*sc).shared.lock().unwrap(); core::mem::replace(&mut (*s).wait_id, 0) @@ -2749,9 +2756,65 @@ unsafe fn present_sync_commit(sc: *mut GnSwapchain, slot: *mut GnSlot, idx: i32) } } +/// When a present wait on a frame is answered. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WaitAnswer { + /// When the guest queues the present (`present_dispatch`). + AtQueue, + /// When the frame is committed, its render fence waited (`commit_real`). + AtCommit, + /// By the compositor: presentation feedback, the release, or the grace. + Later, +} + +/// [`WaitAnswer`] for a frame of a swapchain the guest made for `requested` +/// that goes out as `live`. +/// +/// A guest that asked for mailbox or immediate does not wait, and neither would +/// one whose frames go out as mailbox or immediate (the container's mode, or +/// frame generation). On gamescope's route that wait is answered when the guest +/// queues the present, before the frame's render fence, as the X11 route does +/// (its X server sends PresentCompleteNotify while handling the PresentPixmap +/// request): DXVK's frame-latency wait then lets the CPU of the next frame run +/// while the GPU renders this one, and the acquire gate, not the wait, holds the +/// rate. Off that route the answer stays at the commit. FIFO and FIFO-relaxed +/// are answered by the compositor. +fn present_wait_answer( + gamescope_route: bool, + requested: VkPresentModeKHR, + live: VkPresentModeKHR, +) -> WaitAnswer { + if answers_present_wait_at_commit(requested) || answers_present_wait_at_commit(live) { + if gamescope_route { + WaitAnswer::AtQueue + } else { + WaitAnswer::AtCommit + } + } else { + WaitAnswer::Later + } +} + /// Route a present to the discipline its effective present mode names. unsafe fn present_dispatch(sc: *mut GnSwapchain, slot: *mut GnSlot, idx: i32) { mode_census(sc); + let answer = present_wait_answer( + unsafe { !(*(*sc).surf).swapchain_obj.is_null() }, + unsafe { (*sc).requested_mode }, + unsafe { live_present_mode(sc) }, + ); + if answer == WaitAnswer::AtQueue { + // Taken off the slot, so neither the commit, a replacement nor the + // release answers it again; the daemon's later latch report finds it + // no longer armed. + let id = { + let _g = unsafe { (*sc).shared.lock().unwrap() }; + unsafe { core::mem::replace(&mut (*slot).wait_id, 0) } + }; + if id != 0 { + crate::present::answered_at_queue(id, sc as usize, monotonic_ns()); + } + } match unsafe { live_present_mode(sc) } { VK_PRESENT_MODE_IMMEDIATE_KHR => unsafe { present_immediate(sc, slot, idx) }, VK_PRESENT_MODE_MAILBOX_KHR => unsafe { present_mailbox(sc, slot, idx) }, @@ -6503,6 +6566,39 @@ mod tests { ci.presentMode = 1000; assert_eq!(guest_present_mode(&ci), f); } + #[test] + fn the_present_wait_answer_point_follows_the_declaration() { + let (m, f, i, r) = ( + VK_PRESENT_MODE_MAILBOX_KHR, + VK_PRESENT_MODE_FIFO_KHR, + VK_PRESENT_MODE_IMMEDIATE_KHR, + VK_PRESENT_MODE_FIFO_RELAXED_KHR, + ); + // gamescope's route: mailbox or immediate going out (the container's + // mode, cap-rate-vsync, frame generation) is answered when queued, + // whatever the guest asked for. + for live in [m, i] { + for requested in [f, r, m, i] { + assert_eq!( + present_wait_answer(true, requested, live), + WaitAnswer::AtQueue + ); + } + } + // A guest that asked for mailbox or immediate is answered when queued + // even while its frames go out FIFO (cap-rate, cap-compat). + assert_eq!(present_wait_answer(true, i, f), WaitAnswer::AtQueue); + assert_eq!(present_wait_answer(true, m, f), WaitAnswer::AtQueue); + // FIFO and FIFO-relaxed out of a FIFO guest: the compositor answers. + assert_eq!(present_wait_answer(true, f, f), WaitAnswer::Later); + assert_eq!(present_wait_answer(true, r, r), WaitAnswer::Later); + assert_eq!(present_wait_answer(true, f, r), WaitAnswer::Later); + // Off gamescope's route the unpaced modes stay answered at the commit. + assert_eq!(present_wait_answer(false, f, m), WaitAnswer::AtCommit); + assert_eq!(present_wait_answer(false, i, i), WaitAnswer::AtCommit); + assert_eq!(present_wait_answer(false, f, f), WaitAnswer::Later); + } + #[test] fn present_wait_is_answered_at_commit_only_for_unpaced_modes() { assert!(answers_present_wait_at_commit(VK_PRESENT_MODE_MAILBOX_KHR)); @@ -9768,7 +9864,7 @@ unsafe fn mode_census(sc: *mut GnSwapchain) { // Since the last line, so each line reads one window's cadence. let gaps = COMMIT_GAPS.each_ref().map(|b| b.swap(0, Ordering::Relaxed)); crate::logf!(gndrm_obf::obf!( - "present mode: declared={} requested={} source={} route={} cap={} gate={} lsfg={} acquires={} commits={} declared_fifo={} declared_other={} releases={} feedback={} presented={} discarded={} fallback={} gate_holds={} gate_late={} skipped={} slot_waits={} commit_lat_us={} spacing=[<15:{} 15-16.2:{} 16.2-17.2:{} 17.2-20:{} >20:{}]", + "present mode: declared={} requested={} source={} route={} cap={} gate={} lsfg={} acquires={} commits={} declared_fifo={} declared_other={} releases={} feedback={} presented={} discarded={} fallback={} at_queue={} gate_holds={} gate_late={} skipped={} slot_waits={} commit_lat_us={} spacing=[<15:{} 15-16.2:{} 16.2-17.2:{} 17.2-20:{} >20:{}]", mode_name(line.declared), mode_name(line.requested), line.source, @@ -9785,6 +9881,7 @@ unsafe fn mode_census(sc: *mut GnSwapchain) { presented, discarded, fallback, + crate::present::at_queue_count(), GATE_HOLD_N.load(Ordering::Relaxed), late, skipped, From 762e559276f6d6eaacfe585e5904a89f8ad75288 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 3 Nov 2026 17:37:25 +0530 Subject: [PATCH 3/3] shim: answer the present wait at queue for the cap-rate-vsync cell only 8a5b695 answered every mailbox or immediate presentation's wait when the guest queued it. The measurement it rests on is one cell: Retroid Pocket 6, DTD at 1080p, cap 60, in-game VSync on, 60 Hz, declared mailbox with the vblank-grid gate (source=cap-rate-vsync): 42 fps with the wait answered after the render fence, 60.0 with it answered at queue (acquire to commit 23 -> 14.3 ms). Nothing measured the other mailbox and immediate cases. The answer at queue now applies only there: gamescope's route, a cap at the display's rate, a container mode other than FIFO, a guest that asked for FIFO or FIFO-relaxed. Every other presentation is answered exactly as on master: a frame going out as mailbox or immediate, or from a guest that asked for either (caps below the display's rate, no cap, frame generation, cap-rate and cap-compat with a self-paced guest), at the commit after the fence; FIFO and FIFO-relaxed from gamescope's presentation feedback. Each id is still answered once, and a frame whose declaration changed between queue and commit is answered at the commit. With this, both commits of GameNative/gamenative-drm#48 change one situation only: the limiter at the panel's rate, in-game VSync on, and a container on a present mode other than fifo. A container set to fifo never reaches it. - icd_shim/src/proxy.rs: present_wait_answer takes the declaration's reason; present_dispatch and commit_real pass it; test: at queue only for cap-rate-vsync, at commit for the container's mode, frame generation and a self-paced guest under cap-rate or cap-compat, the compositor for FIFO, master's answers off the route. - docs/10, docs/15: the answer at queue described as the one case. --- docs/10-fps-limit.md | 7 +- docs/15-fps-limiter-pacing-and-vsync.md | 19 +++-- icd_shim/src/proxy.rs | 108 +++++++++++++++--------- 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/docs/10-fps-limit.md b/docs/10-fps-limit.md index 6b67025..5e47e88 100644 --- a/docs/10-fps-limit.md +++ b/docs/10-fps-limit.md @@ -179,7 +179,12 @@ divisor cap reads its target exactly. (`source=cap-rate-vsync`): its wait is answered when it queues the present (`docs/15`), the gate paces it, and the swapchain keeps the mailbox image count (Retroid Pocket 6, DTD, - VSync on, cap 60: 59.983 fps, no doubles). The + VSync on, cap 60: 59.983 fps, no doubles; with the wait answered at queue, + 42 fps before and 60.0 after in a GPU-heavy 1080p scene). This is the only + case answered at queue; every other mailbox or immediate presentation is + answered at the commit, as on master. A container set to + `MESA_VK_WSI_PRESENT_MODE=fifo` never reaches it, so setting a game's + container to fifo restores master's behaviour for that game. The vblank-grid gate is not kept on top of it: FIFO's back-pressure already holds the guest, and the two stacked turned a guest whose acquire-to-commit grew past a period into a 40 fps session on a 60 Hz panel (DTD on a 1280x960 diff --git a/docs/15-fps-limiter-pacing-and-vsync.md b/docs/15-fps-limiter-pacing-and-vsync.md index 63d0006..87e06b1 100644 --- a/docs/15-fps-limiter-pacing-and-vsync.md +++ b/docs/15-fps-limiter-pacing-and-vsync.md @@ -74,15 +74,16 @@ that rate is the display's. - **Mailbox and immediate.** A guest that asked for either does not wait, and neither would one whose frames go out as mailbox or immediate (frame generation, or the container's env while the guest believes it has FIFO), so those waits are not held for - the compositor (`present_wait_answer`). On gamescope's route they are answered when the - guest queues the present, before the frame's render fence, as the X11 route's X server - answers PresentCompleteNotify at the PresentPixmap request: DXVK's frame-latency wait - then lets the CPU of the next frame run while the GPU renders this one, and the acquire - gate holds the rate (`at_queue=` in the periodic census). Answered after the fence, a - latency-1 guest alternated CPU and GPU: Retroid Pocket 6, DTD at 1080p, cap 60, VSync - on, 60 Hz, 42 fps with 23 ms from acquire to commit, the GPU at 65-69 % busy and - 550 MHz because its governor saw the bubbles. Off that route they are answered at the - commit. + the compositor: they are answered at the commit, after the frame's render fence + (`present_wait_answer`). One case is answered earlier, when the guest queues the + present: `source=cap-rate-vsync` (a cap at the display's rate, a container mode other + than FIFO, a guest that asked for FIFO, declared mailbox with the vblank-grid gate). + That guest waits on every frame, and answered after the fence a latency-1 guest + alternated CPU and GPU: Retroid Pocket 6, DTD at 1080p, cap 60, VSync on, 60 Hz, 42 fps + with 23 ms from acquire to commit, the GPU at 65-69 % busy and 550 MHz because its + governor saw the bubbles. Answered at queue, as the X11 route's X server answers + PresentCompleteNotify at the PresentPixmap request, CPU and GPU overlap and the gate + holds the rate (60.0 fps there, 14.3 ms; `at_queue=` in the periodic census). - **A replaced frame.** Under mailbox a frame can be replaced before it is shown. It is never committed, released or latched under that id, so its wait is answered when it is discarded instead of timing out. diff --git a/icd_shim/src/proxy.rs b/icd_shim/src/proxy.rs index f82a361..93cdbe0 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -1770,14 +1770,15 @@ unsafe fn commit_real(sc: *mut GnSwapchain, idx: i32, fence_ms: u128) { // neither would one whose frames go out as mailbox or immediate // (the container's mode, or frame generation): both are answered // here. A FIFO guest declared FIFO keeps the release-or-grace answer. - // On gamescope's route a mailbox or immediate frame's wait was - // answered when the guest queued it (`present_dispatch`), so a wait - // still on the slot here is one whose mode changed since; it is - // answered now. + // A cap-rate-vsync frame's wait was answered when the guest queued + // it (`present_dispatch`), so one still on the slot here is a frame + // whose declaration changed since; it is answered now. + let (live, why) = live_choice(sc); let at_commit = present_wait_answer( !(*(*sc).surf).swapchain_obj.is_null(), + why, (*sc).requested_mode, - live_present_mode(sc), + live, ) != WaitAnswer::Later; let id = if at_commit { let _g = (*sc).shared.lock().unwrap(); @@ -2768,28 +2769,28 @@ enum WaitAnswer { } /// [`WaitAnswer`] for a frame of a swapchain the guest made for `requested` -/// that goes out as `live`. +/// that goes out as `live`, declared for the reason `why`. /// -/// A guest that asked for mailbox or immediate does not wait, and neither would -/// one whose frames go out as mailbox or immediate (the container's mode, or -/// frame generation). On gamescope's route that wait is answered when the guest -/// queues the present, before the frame's render fence, as the X11 route does -/// (its X server sends PresentCompleteNotify while handling the PresentPixmap -/// request): DXVK's frame-latency wait then lets the CPU of the next frame run -/// while the GPU renders this one, and the acquire gate, not the wait, holds the -/// rate. Off that route the answer stays at the commit. FIFO and FIFO-relaxed -/// are answered by the compositor. +/// One case is answered when the guest queues the present, before the frame's +/// render fence: gamescope's route, a cap at the display's rate and a guest +/// that asked for FIFO declared the container's mailbox with the vblank-grid +/// gate (`Declared::CapRateVsync`). That guest waits on every frame (DXVK with +/// VSync on), and answered after the fence its CPU and GPU alternated; at queue, +/// as the X11 route answers (its X server sends PresentCompleteNotify while +/// handling the PresentPixmap request), they overlap and the gate holds the +/// rate. Everything else keeps the answer it had: a frame going out as mailbox +/// or immediate, or from a guest that asked for either, at the commit; FIFO and +/// FIFO-relaxed by the compositor. fn present_wait_answer( gamescope_route: bool, + why: Declared, requested: VkPresentModeKHR, live: VkPresentModeKHR, ) -> WaitAnswer { - if answers_present_wait_at_commit(requested) || answers_present_wait_at_commit(live) { - if gamescope_route { - WaitAnswer::AtQueue - } else { - WaitAnswer::AtCommit - } + if gamescope_route && why == Declared::CapRateVsync { + WaitAnswer::AtQueue + } else if answers_present_wait_at_commit(requested) || answers_present_wait_at_commit(live) { + WaitAnswer::AtCommit } else { WaitAnswer::Later } @@ -2798,10 +2799,12 @@ fn present_wait_answer( /// Route a present to the discipline its effective present mode names. unsafe fn present_dispatch(sc: *mut GnSwapchain, slot: *mut GnSlot, idx: i32) { mode_census(sc); + let (live, why) = unsafe { live_choice(sc) }; let answer = present_wait_answer( unsafe { !(*(*sc).surf).swapchain_obj.is_null() }, + why, unsafe { (*sc).requested_mode }, - unsafe { live_present_mode(sc) }, + live, ); if answer == WaitAnswer::AtQueue { // Taken off the slot, so neither the commit, a replacement nor the @@ -6574,29 +6577,54 @@ mod tests { VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_RELAXED_KHR, ); - // gamescope's route: mailbox or immediate going out (the container's - // mode, cap-rate-vsync, frame generation) is answered when queued, - // whatever the guest asked for. - for live in [m, i] { - for requested in [f, r, m, i] { + use Declared::{CapCompat, CapRate, CapRateVsync, Generating, GuestVsync, Mode}; + // The one case answered at queue: cap-rate-vsync on gamescope's route. + for requested in [f, r] { + assert_eq!( + present_wait_answer(true, CapRateVsync, requested, m), + WaitAnswer::AtQueue + ); + } + // Every other mailbox or immediate presentation keeps master's answer + // at the commit: below-rate caps and no cap (the container's mode), + // frame generation, and a guest that asked for mailbox or immediate + // (declared FIFO under cap-rate or cap-compat). + for requested in [f, r, m, i] { + for live in [m, i] { assert_eq!( - present_wait_answer(true, requested, live), - WaitAnswer::AtQueue + present_wait_answer(true, Mode, requested, live), + WaitAnswer::AtCommit + ); + } + assert_eq!( + present_wait_answer(true, Generating, requested, m), + WaitAnswer::AtCommit + ); + } + for requested in [m, i] { + for why in [CapRate, CapCompat] { + assert_eq!( + present_wait_answer(true, why, requested, f), + WaitAnswer::AtCommit ); } } - // A guest that asked for mailbox or immediate is answered when queued - // even while its frames go out FIFO (cap-rate, cap-compat). - assert_eq!(present_wait_answer(true, i, f), WaitAnswer::AtQueue); - assert_eq!(present_wait_answer(true, m, f), WaitAnswer::AtQueue); // FIFO and FIFO-relaxed out of a FIFO guest: the compositor answers. - assert_eq!(present_wait_answer(true, f, f), WaitAnswer::Later); - assert_eq!(present_wait_answer(true, r, r), WaitAnswer::Later); - assert_eq!(present_wait_answer(true, f, r), WaitAnswer::Later); - // Off gamescope's route the unpaced modes stay answered at the commit. - assert_eq!(present_wait_answer(false, f, m), WaitAnswer::AtCommit); - assert_eq!(present_wait_answer(false, i, i), WaitAnswer::AtCommit); - assert_eq!(present_wait_answer(false, f, f), WaitAnswer::Later); + assert_eq!(present_wait_answer(true, Mode, f, f), WaitAnswer::Later); + assert_eq!( + present_wait_answer(true, GuestVsync, f, f), + WaitAnswer::Later + ); + assert_eq!(present_wait_answer(true, CapRate, f, f), WaitAnswer::Later); + assert_eq!( + present_wait_answer(true, CapCompat, r, f), + WaitAnswer::Later + ); + assert_eq!(present_wait_answer(true, Mode, r, r), WaitAnswer::Later); + // Off gamescope's route: as on master. + assert_eq!(present_wait_answer(false, Mode, f, m), WaitAnswer::AtCommit); + assert_eq!(present_wait_answer(false, Mode, i, i), WaitAnswer::AtCommit); + assert_eq!(present_wait_answer(false, Mode, f, f), WaitAnswer::Later); } #[test]