shim: at a cap at the display's rate, a guest's FIFO request keeps the container's mode with the vblank-grid gate
Status and merge criteria
Do not merge yet.
Verified on the RP6 (narrowed build 762e559). Setup: Dave the Diver at 1080p, 60 Hz, cap 60, in-game VSync on, in the heavy spot.
- App fps: 60.0 / 58.8 / 55.8 / 60.0, with the GPU at 75-86 %.
- SurfaceFlinger: 54.8-57.7 fps.
- Acquire-to-commit: 15 ms.
at_queueequal to commits, no slot waits. - Master's cell in the same spot: 42 fps, 23 ms.
Latency, stated honestly. Source: the RP6 capture analysis, latency-compare.md.
- No per-frame release-to-latch stamps exist, so these are estimates.
- GPU-bound regime: this PR's frames are an estimated ~4-5 ms later per frame than master's two-vblank frames, while delivering ~25 % more frames (49.5 vs 39.7 fps, 60 vs 174 repeats per 10 s in that capture).
- When the frame fits in a period: the gate pins the phase and the two should be equal. That is unmeasured.
- Commit 1 alone vs both commits: no measurable difference in the heavy scene (commit_lat 21.0 vs 20.0 ms, 47.9 vs 49.5 fps). The render fence was already signalled when each frame was queued, so commit 2's contribution there is smaller than first reported.
Merge criteria:
- JT's device on payload 20261103.6.
- One VSync-on game on the Odin 3 at a 60 cap.
- A per-frame release-to-latch measurement from a debug build: master vs this PR, same scene.
@joshuatam, a follow-up to GameNative/gamenative-drm#46 and GameNative/gamenative-drm#47 for the 40 fps you still see on payload 20261103.4. I'd like your read on it.
Scope. Both changes here affect one situation only: the limiter at the panel's rate, in-game VSync on, and a container on a present mode other than fifo. Every other case presents and answers exactly as on master. Setting a game's container present mode to fifo restores master's behaviour for that game.
Problem
Your capture on 20261103.4 still settles at 40 fps. The setup is shim 2567925, a 1280x960 device, DTD in game, cap 60, env mailbox, in-game VSync on, at 60 Hz. The census reads declared=fifo source=cap-rate gate=off, which is master's behaviour in this cell. The shim makes 400 commits per 10 s against the daemon's 400/600 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 one. Declared FIFO, gamescope's presented answers that wait 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 was answered at commit and it held 60 fps. On the Retroid Pocket 6, the cap-rate cell declared mailbox with the gate gave 59.983 fps and 0 doubles (B1 in GameNative/gamenative-drm#46).
What changes
Only the cap-at-display-rate cell, when the container's mode is not FIFO, splits by what the guest asked for:
| container mode | cap | guest asked for | declared | what limits the rate |
|---|---|---|---|---|
| mailbox (default) | at the display's rate | FIFO or FIFO-relaxed (in-game VSync on) | mailbox (source=cap-rate-vsync) | the shim's vblank-grid gate, every vblank; present wait answered when the guest queues the present; mailbox image count |
| mailbox (default) | at the display's rate | mailbox or immediate (self-paced) | fifo (source=cap-rate) | gamescope's limiter (divisor 1), no gate (unchanged) |
| mailbox (default) | below the display's rate | any | mailbox | the gate (unchanged) |
| mailbox (default) | none | FIFO / other | fifo (source=guest) / mailbox | unchanged |
| fifo | any | any | fifo | gamescope's limiter (unchanged) |
| any, frame generation on | any | any | mailbox | source-rate gate (unchanged) |
A self-paced guest (VSync off) stays on FIFO because, declared mailbox at this cap, it had about 1 frame a second replaced in gamescope's mailbox (B4-B6).
The census's slot_waits and commit_lat_us now count whether or not the gate runs, so a GNDRM_LOG_VKSHIM=1 capture of a FIFO cell shows its image waits and its acquire-to-commit time.
Present wait answered at queue in the cap-rate-vsync cell (8a5b695, narrowed in 762e559)
What was measured. On the RP6 with this PR's first shim (d61b614) the new cell ran at 42 fps. Setup: DTD at 1080p, cap 60, in-game VSync on, 60 Hz.
- Acquire-to-commit was 23 ms, and the game's main thread used 43 % of a core.
- The GPU sat 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, and 18 ms × 680/550 ≈ 22 ms.
- So the frame is GPU-time-bound, but the governor sees bubbles and does not ramp up.
Why. DXVK with VSync on calls vkWaitForPresentKHR because its own swapchain is FIFO; it cannot see that the shim declared mailbox. It waits for the present of frame N − L before it starts frame N. The shim answered that wait in commit_real, after the worker had waited for the frame's render fence. With L = 1, the CPU of frame N+1 therefore waited for the GPU to finish frame N.
Why X11 holds 60. On X11 the same game overlaps CPU and GPU. The app's X server sends PresentCompleteNotify while handling the PresentPixmap request, before the GPU is done.
Change. In the cap-rate-vsync cell only (gamescope's route, cap at the panel's rate, non-fifo container mode, guest asked for FIFO or FIFO-relaxed), the present wait is answered in present_dispatch, when the guest queues the present. The acquire gate still holds the rate.
- 8a5b695 applied this to every mailbox or immediate presentation. 762e559 narrows it to this one cell.
- Everything else keeps master's answer timing:
- mailbox and immediate at the commit, after the render fence;
- FIFO and FIFO-relaxed from gamescope's presentation feedback.
- That covers caps below the panel's rate, no cap, frame generation, and a self-paced guest under cap-rate or cap-compat. Off gamescope's route nothing changes.
- A frame whose declaration changed between queue and commit is answered at the commit.
- Each id is answered exactly once. It is taken off the slot under the lock, so the commit, a mailbox replacement, the release and the daemon's latch report find nothing left to answer.
- The periodic census gains
at_queue=.
Tests
cargo test -p adrenotools_gamescope --lib(Linux aarch64 container): 89 passed at 762e559. The answer-point test checks that only cap-rate-vsync is answered at queue: the container's mode, frame generation and a self-paced guest under cap-rate or cap-compat are answered at the commit, FIFO by the compositor, and off the route as on master. New and updated cases:- 60 on 59.97 Hz, env mailbox, guest fifo or relaxed → mailbox, gate every vblank;
- same with guest mailbox or immediate → FIFO, gate off;
- env fifo → FIFO, gate off;
- 60 on 120 Hz and no cap → unchanged.
cargo clippy --target aarch64-linux-android -p adrenotools_gamescope -p gndrm-android -p gndrm-daemon --all-targets -- -D warnings: clean.
Shim only; the daemon is unchanged.
Artefacts
- Shim from 762e559: sha256
d04525f26ad2693715b0f8e281d87be4be779631cb6232f664b493b03d433163, build string202611031208762e559. In payload 20261103.6. - Shim from 8a5b695 (in payload 20261103.5): sha256
a0bc3ab2ed1135885a8dff94d691fa389f63286bc54ec44befea970e2fbbd26f, build string2026110310298a5b695.
Shim only; the daemon is unchanged.
Verification
Measured on the RP6: Dave the Diver at 1920x1080, 60 Hz, cap 60, in-game VSync on, same spot. Census declared=mailbox requested=fifo source=cap-rate-vsync cap=60 gate=vsync.
| d61b614 (gate, wait answered after the fence) | 8a5b695 (wait answered at queue) | |
|---|---|---|
| fps, moderate load | 42 | 60.0 |
| fps, heaviest spot | — | 53-56 (GPU governor holding 550 MHz) |
commit_lat | 23 ms | 14.3 ms |
at_queue | — | equal to commits |
Still pending: JT's device.
Delivery
- Payload
gamescope-gamenative-20261103.5.tzst, sha256aa4425180fd893be843811506c4d46d5facd566ceed6bbc5ebf5569297a156cc. It is 20261102.4 with only the shim replaced by the 8a5b695 build above (a0bc3ab2…bd26f). - App: utkarshdalal/GameNative#2332 (branch
gamescope-payload-20261103-5, commit 96d03dfc7). It depends on this PR and must not merge before it. - Payload 20261103.6 (
gamescope-gamenative-20261103.6.tzst, sha256d8d40c58db928e25f59371eb3fe337638d140f13febef1d3facd4747c387c183) carries 762e559. utkarshdalal/GameNative#2332 now ships it (commit 9a7991215). Payload 20261103.5 (8a5b695, the wider answer-at-queue) stays on the CDN.