From fd4fc1393f257f16b094c3c4cbf237cbb9513ef4 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 10 Nov 2026 01:12:42 +0530 Subject: [PATCH] icd_shim: a self-paced guest at a cap at the display's rate gets the gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On gamescope's override route, a cap at the display's rate and a guest that asked for mailbox or immediate were declared FIFO with no acquire gate (`source=cap-rate`, #46/#47), on the assumption that gamescope's FIFO latch holds the guest to one commit per vblank. It does not hold a guest faster than the panel. Retroid Pocket 6, Portal 2 main menu, in-game VSync off, cap 60 on a 60 Hz panel, per 10 s: before declared=fifo source=cap-rate gate=off acquires/commits/releases 1131-1193, daemon publishes 600, 112-120 fps after declared=mailbox source=cap-rate gate=vsync acquires/commits/releases 600, gate_holds 600, gate_late 0, slot_waits 0, publishes 597-600, 60.0 fps, SurfaceFlinger 59.98 presents/s with no doubled interval in 125 The cell now declares the container's mode, so `gamescope_route_pacing` arms the vblank-grid gate as it does for every other capped cell with a known grid; nothing capped on gamescope's route leans on its FIFO back-pressure any more. The census keeps `source=cap-rate`. The swapchain's image count is untouched: since #59 it follows the swapchain's own mode, not the declaration, so this cell already had mailbox's floor. The cost is the one #46 measured: a guest that paces itself just below the display's rate (DTD, ~59.3 fps) loses ~1 frame a second in gamescope's mailbox (58.3 fps, 2.8 % doubled intervals). A guest faster than the cap is held on the grid by the gate and does not pay it. A container set to fifo keeps the FIFO declaration. docs/10 and docs/15 describe the cell, and docs/15 §1 names the limiter's real setter, `set_app_target_refresh_cycle` through the injector. --- docs/10-fps-limit.md | 46 ++++++++++------- docs/15-fps-limiter-pacing-and-vsync.md | 44 +++++++++------- icd_shim/src/proxy.rs | 69 ++++++++++++------------- 3 files changed, 84 insertions(+), 75 deletions(-) diff --git a/docs/10-fps-limit.md b/docs/10-fps-limit.md index 703f65d..9f0a076 100644 --- a/docs/10-fps-limit.md +++ b/docs/10-fps-limit.md @@ -120,7 +120,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 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 | at the display's rate, guest asked for mailbox or immediate | mailbox (`source=cap-rate`) | the shim's acquire gate, every vblank | | 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 | @@ -148,12 +148,20 @@ divisor cap reads its target exactly. `fifo_relaxed` included, is rejected as Mesa rejects it, the guest's own mode is used, and the shim logs `Invalid MESA_VK_WSI_PRESENT_MODE value!` whether or not `GNDRM_LOG_VKSHIM` is set, so a typo shows in a shipped session. It is - overridden in one case: a non-FIFO value under a cap at the display's rate is - declared FIFO (`source=cap-rate`, below). Mesa reads the same variable, so it - also sets the mode a zink guest's own swapchain asks for below the shim. + overridden in one case: a non-FIFO value under a cap is declared FIFO until the + daemon's stamp lead is known (`source=cap-compat`, below). Mesa reads the same + variable, so it also sets the mode a zink guest's own swapchain asks for below the shim. - **Why mailbox is not declared at the display's rate.** At that cap the panel - shows one frame per vblank under either mode, and under mailbox the shim has to + **Mailbox at the display's rate, and what it costs.** At that cap the panel + shows one frame per vblank under either mode. A self-paced guest was declared + FIFO here with no gate, on the assumption that gamescope's FIFO holds it; it + does not hold a guest faster than the panel. Retroid Pocket 6, Portal 2 main + menu, in-game VSync off, cap 60 on a 60 Hz panel: per 10 s ~1150 acquires, + commits and releases against the daemon's 600 publishes, 112-120 fps. Declared + the container's mailbox with the gate every vblank (`source=cap-rate`), the + same scene runs 600 acquires, commits, releases and `gate_holds` per 10 s, + 597-600 publishes, 60.0 fps, 59.98 presents/s on `SurfaceFlinger` and no + doubled interval in 125. Under mailbox the shim has to keep two commits out of one gamescope composite. It cannot see when gamescope latches: gamescope takes commits at its wakeup, the target vblank minus its rolling draw time and red zone (`vblankmanager.cpp` 142-199), and that draw time @@ -165,11 +173,10 @@ divisor cap reads its target exactly. VSync off, a guest pacing itself at ~59.3 fps on a 59.98 Hz panel): mailbox with the gate gave 58.3 fps on `SurfaceFlinger` with ~1 commit a second replaced in gamescope's mailbox; a one-commit-per-window queue in the shim at a guessed - 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. That holds - for a guest that asked for mailbox or immediate, which paces itself. A guest + 2.5 ms boundary gave 59.0 with ~0.8 a second still replaced; FIFO gave 59.22 + on the same scene. That cost, ~1 doubled frame a second for a guest that paces + itself just below the display's rate, is what mailbox pays here; a guest + faster than the cap is held on the grid by the gate and does not pay it. 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 @@ -185,11 +192,10 @@ divisor cap reads its target exactly. 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 - device after a live 120 to 60 Hz switch: `gate_holds` 0, `gate_late` 400 and + container to fifo restores master's behaviour for that game. FIFO with the + vblank-grid gate stacked on it is worse than either: the two 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 device after a live 120 to 60 Hz switch: `gate_holds` 0, `gate_late` 400 and `skipped` 200 per 10 s, commit latency 24 ms, 400/600 publishes). Below the display's rate the gate's releases are at least two vblanks apart and two frames in one composite are rare, so @@ -227,10 +233,10 @@ divisor cap reads its target exactly. whenever the declared mode, its source, the cap or the gate changes: `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 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`: + own mode, no env or a FIFO request with no cap; `source=cap-rate`: the + container's mode with `gate=vsync` because the cap is the display's rate and the + guest paces itself; `source=cap-rate-vsync`: the same 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 diff --git a/docs/15-fps-limiter-pacing-and-vsync.md b/docs/15-fps-limiter-pacing-and-vsync.md index 062f441..3408f10 100644 --- a/docs/15-fps-limiter-pacing-and-vsync.md +++ b/docs/15-fps-limiter-pacing-and-vsync.md @@ -11,8 +11,8 @@ cursor, scan-out and mode patches. | place | what it holds | set by | |---|---|---| -| the shim's declared present mode | the container's `MESA_VK_WSI_PRESENT_MODE` (else the guest's mode); with no cap, a guest's FIFO request over a non-FIFO env; FIFO under a cap at the display's rate; mailbox under frame generation | the container, the guest and the cap (`docs/10`) | -| gamescope's own limiter | `nRefreshHz / nTargetFPS`, a divisor of the mode's rate; holds FIFO commits only | `debug_set_fps_limit`, sent only when generation is off | +| the shim's declared present mode | the container's `MESA_VK_WSI_PRESENT_MODE` (else the guest's mode); with no cap, a guest's FIFO request over a non-FIFO env; mailbox under frame generation | the container, the guest and the cap (`docs/10`) | +| gamescope's own limiter | `nRefreshHz / nTargetFPS`, a divisor of the mode's rate; holds FIFO commits only | `set_app_target_refresh_cycle`, sent by the daemon's injector only when generation is off | | the shim's acquire gate | the cap, on the display's vblank grid, unless the container's mode is FIFO | `FrameCapNote{fps}` and `VsyncNote` from the daemon | | the presenter's latch grid | the display's latch points, as measured | `VsyncNote` from the daemon (`docs/17`) | @@ -22,12 +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 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 +modes under a cap at the display's rate keeps its mode with the gate every vblank, +whatever the guest asked for: `source=cap-rate` for a guest that asked for mailbox or +immediate, `source=cap-rate-vsync` for one that asked for FIFO (`docs/10`). Under frame +generation the app disarms gamescope's limiter and the shim's time gate holds the source rate instead (`docs/17`). @@ -137,9 +135,7 @@ that rate is the display's. commit per limited vblank, on the display's vblank stream above, and hands the previous buffer back with `wl_buffer.release`, which is what frees the guest's next acquire; a cap that is not the display's rate over a whole number runs at gamescope's - next divisor rate above it. Declared FIFO because the cap is the display's rate - (`source=cap-rate`), no gate runs either: gamescope's FIFO latch alone paces it. - Declared anything else, + next divisor rate above it. Declared anything else, gamescope takes the newest commit at each composite and the shim's acquire gate is the cap (`icd_shim/src/vsync_gate.rs`). Its release points are the instants the flip events are stamped at: the daemon's @@ -177,15 +173,12 @@ that rate is the display's. ## Behaviour changes in gamescope sessions -- **The container's present mode is declared, except under a cap at the display's - rate.** `MESA_VK_WSI_PRESENT_MODE` decides the mode on the gamescope route as on the - X11 Present route; without it the guest's own mode is declared. A container left at +- **The container's present mode is declared.** `MESA_VK_WSI_PRESENT_MODE` decides + the mode on the gamescope route as on the X11 Present route; without it the guest's own mode is declared. A container left at the mailbox default runs, with no cap, FIFO for a guest that asked for FIFO (its own VSync, `source=guest`) and mailbox for any other guest, and mailbox capped by the shim's - vblank-grid gate under a cap below the display's rate. Under a cap at the display's - rate it is declared FIFO with no gate (`source=cap-rate`), because the shim - cannot keep two mailbox commits out of one gamescope composite (`docs/10`). One set - to fifo runs FIFO under gamescope's limiter. + vblank-grid gate under any cap (`source=cap-rate` or `source=cap-rate-vsync` at the + display's rate, `docs/10`). One set to fifo runs FIFO under gamescope's limiter. - **A game below the cap keeps its own rate under mailbox** (caps below the display's rate). Under FIFO and gamescope's limiter each frame is held to whole limited vblanks, so a game below the cap judders @@ -251,8 +244,19 @@ declaration on the same scene (59.22 fps, 1.3 % doubles): 0-2 per 10 s (the guest paces itself at ~59.3); VSync on, 59.983 fps and no doubles. - **The same cap declared mailbox with the gate** lost ~1 commit a second inside - gamescope's mailbox (58.3 fps, 2.8 % doubles), which is why it is declared FIFO - (`docs/10`). + gamescope's mailbox (58.3 fps, 2.8 % doubles). It is declared that way now + anyway, because the FIFO declaration does not hold a guest faster than the panel + (below, and `docs/10`). + +Retroid Pocket 6 (60 Hz), Portal 2 main menu, cap 60, in-game VSync off (the guest +asks for immediate): + +- **Declared FIFO, no gate** (`source=cap-rate gate=off`): per 10 s 1131-1193 + acquires, commits and releases against the daemon's 600 publishes; 112-120 fps. +- **Declared mailbox, the gate every vblank** (`source=cap-rate gate=vsync`): per + 10 s 600 acquires, commits, releases and `gate_holds`, `gate_late` and + `slot_waits` 0, 597-600 publishes; 60.0 fps, 59.98 presents/s on + `SurfaceFlinger`, no doubled interval in 125. Retroid Pocket Nova (120 Hz panel, a 60 Hz mode available), Hades II, Vulkan through zink, measured before the vblank-grid gate, when the acquire gate ran on the cap's diff --git a/icd_shim/src/proxy.rs b/icd_shim/src/proxy.rs index d17abd7..8c3f673 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -6908,13 +6908,13 @@ mod tests { choose_live_mode(mode, ModeSource::Guest, mode, false, 0, None), (mode, Declared::Mode) ); - // 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. + // A cap at the display's rate: the swapchain's mode (with the + // gate), cap-rate for a guest that asked for mailbox or immediate, + // cap-rate-vsync for one that asked for FIFO-relaxed. let at_rate = if mode == r { (r, Declared::CapRateVsync) } else { - (f, Declared::CapRate) + (mode, Declared::CapRate) }; assert_eq!( choose_live_mode(mode, ModeSource::Guest, mode, false, 60, g60), @@ -6968,11 +6968,11 @@ mod tests { gamescope_route_pacing((f, Declared::CapCompat), None, 60, None), Pacing::Off ); - // FIFO at the display's rate: no gate either; gamescope's limiter - // (divisor 1) paces it, as on master. + // Mailbox at the display's rate for a self-paced guest: the gate, + // every vblank. assert_eq!( - gamescope_route_pacing((f, Declared::CapRate), None, 60, grid), - Pacing::Off + gamescope_route_pacing((m, Declared::CapRate), None, 60, grid).clock(), + GateClock::Vsync ); // Mailbox, immediate, FIFO-relaxed under a cap: the gate, on the grid. for mode in [ @@ -7050,8 +7050,8 @@ mod tests { let off = Pacing::Off; // 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. + // gate every vblank, its present wait answered at commit; so does one + // that asked for mailbox or immediate (self-paced), as cap-rate. assert_eq!( row(Some("mailbox"), f, 60, None, g60), (m, "cap-rate-vsync", every(g60, 1)) @@ -7063,7 +7063,7 @@ mod tests { for guest in [m, i] { assert_eq!( row(Some("mailbox"), guest, 60, None, g60), - (f, "cap-rate", off) + (m, "cap-rate", every(g60, 1)) ); } // Capped below the display's rate, env mailbox, whatever the guest @@ -7128,7 +7128,7 @@ mod tests { // 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). + // (mailbox under cap-rate, FIFO under cap-compat). for requested in [f, r, m, i] { for live in [m, i] { assert_eq!( @@ -7142,12 +7142,14 @@ mod tests { ); } for requested in [m, i] { - for why in [CapRate, CapCompat] { - assert_eq!( - present_wait_answer(true, why, requested, f), - WaitAnswer::AtCommit - ); - } + assert_eq!( + present_wait_answer(true, CapRate, requested, m), + WaitAnswer::AtCommit + ); + assert_eq!( + present_wait_answer(true, CapCompat, 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); @@ -10290,8 +10292,9 @@ 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 and the guest asked for - /// mailbox or immediate (`source=cap-rate`). + /// The container's mode with the vblank-grid gate 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 @@ -10321,18 +10324,14 @@ enum Declared { /// 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 -/// its rolling draw time (vblankmanager.cpp). A guest that paces itself just -/// below the display's rate had ~1 frame a second replaced in gamescope's -/// mailbox that way. Under FIFO gamescope latches one commit per vblank at -/// its own wakeup and its limiter (divisor 1) paces the guest. The gate is -/// not kept on top: 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. +/// asked for mailbox or immediate (self-paced): the swapchain's mode with the +/// vblank-grid gate, one release per vblank (`source=cap-rate`). Declared +/// FIFO with no gate, gamescope did not hold a guest faster than the panel: +/// Portal 2 at cap 60 on a 60 Hz panel committed ~115 frames a second and +/// had each one released, against the daemon's 60 publishes. The cost of +/// mailbox here is a guest that paces itself just below the display's rate: +/// gamescope's wakeup moves with its rolling draw time (vblankmanager.cpp), +/// and ~1 frame a second of such a guest is replaced in its mailbox. /// - no cap, the container's env naming a mode other than FIFO and a guest /// that asked for FIFO or FIFO-relaxed: the guest's mode (`source=guest`); /// see [`choose_live_mode`]. @@ -10405,7 +10404,7 @@ fn choose_live_mode( if vsync_request { (mode, Declared::CapRateVsync) } else { - (VK_PRESENT_MODE_FIFO_KHR, Declared::CapRate) + (mode, Declared::CapRate) } } Some(_) => (mode, Declared::Mode), @@ -10436,8 +10435,8 @@ unsafe fn acquire_pacing(sc: *mut GnSwapchain) -> crate::vsync_gate::Pacing { /// - frame generation sets the source rate: the gate holds it on the cap's own /// clock (the app disarms gamescope's limiter then; generation paces the /// output on the latch grid itself); -/// - any FIFO declaration (a FIFO swapchain, FIFO for want of the stamp lead, -/// FIFO because the cap is the display's rate): no gate. gamescope's limiter +/// - any FIFO declaration (a FIFO swapchain, FIFO for want of the stamp +/// lead): no gate. gamescope's limiter /// holds FIFO commits, one per limited vblank, and is the cap; /// - mailbox, immediate, FIFO-relaxed: gamescope's limiter does not hold them /// (`wlserver_surface_is_fifo` is FIFO only), so the gate is the cap, on the