diff --git a/docs/10-fps-limit.md b/docs/10-fps-limit.md index 36fb8b3..5e47e88 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 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) | | fifo | set | fifo | gamescope's divisor limiter | @@ -167,7 +168,23 @@ 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 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; 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 @@ -210,7 +227,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 +240,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..87e06b1 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 @@ -71,9 +73,17 @@ 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: 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/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 2cdde55..93cdbe0 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -1770,8 +1770,16 @@ 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)); + // 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, + ) != WaitAnswer::Later; let id = if at_commit { let _g = (*sc).shared.lock().unwrap(); core::mem::replace(&mut (*s).wait_id, 0) @@ -2012,7 +2020,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 +2435,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); } @@ -2746,9 +2757,67 @@ 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`, declared for the reason `why`. +/// +/// 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 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 + } +} + /// 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 }, + live, + ); + 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) }, @@ -6299,10 +6368,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 +6489,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 +6508,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), @@ -6478,6 +6569,64 @@ 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, + ); + 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, 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 + ); + } + } + // FIFO and FIFO-relaxed out of a FIFO guest: the compositor answers. + 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] fn present_wait_is_answered_at_commit_only_for_unpaced_modes() { assert!(answers_present_wait_at_commit(VK_PRESENT_MODE_MAILBOX_KHR)); @@ -9489,8 +9638,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 +9661,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 +9750,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 +9858,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", @@ -9725,7 +9892,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, @@ -9742,6 +9909,7 @@ unsafe fn mode_census(sc: *mut GnSwapchain) { presented, discarded, fallback, + crate::present::at_queue_count(), GATE_HOLD_N.load(Ordering::Relaxed), late, skipped,