Skip to content

shim: at a cap at the display's rate, a guest's FIFO request keeps the container's mode with the vblank-grid gate - #48

Merged
utkarshdalal merged 3 commits into
masterfrom
shim-cap-rate-by-guest-request
Nov 3, 2026
Merged

utkarshdalal merged 3 commits into
masterfrom
shim-cap-rate-by-guest-request

Conversation

@utkarshdalal

@utkarshdalal utkarshdalal commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

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_queue equal 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:

  1. JT's device on payload 20261103.6.
  2. One VSync-on game on the Odin 3 at a 60 cap.
  3. A per-frame release-to-latch measurement from a debug build: master vs this PR, same scene.

@joshuatam, a follow-up to #46 and #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 #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 string 202611031208762e559. In payload 20261103.6.
  • Shim from 8a5b695 (in payload 20261103.5): sha256 a0bc3ab2ed1135885a8dff94d691fa389f63286bc54ec44befea970e2fbbd26f, build string 2026110310298a5b695.

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

Utkarsh Dalal added 2 commits November 3, 2026 15:19
…e 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.
…st 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.
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 #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.
utkarshdalal pushed a commit to utkarshdalal/GameNative that referenced this pull request Nov 3, 2026
…c-on cell only

Payload 20261103.6 is 20261102.4 with only lib/libadrenotools_gamescope.so
replaced (d04525f2, build string 202611031208762e559), the shim from
GameNative/gamenative-drm#48 at 762e559. It narrows 20261103.5: the present
wait is answered when the game queues the present only for a game with
in-game VSync on at a cap equal to the panel's rate in a container whose
present mode is not fifo; every other case answers as on master.
gamescope, Xwayland and libgamenative_drm.so are unchanged.
@utkarshdalal
utkarshdalal merged commit d45afb7 into master Nov 3, 2026
utkarshdalal added a commit to utkarshdalal/GameNative that referenced this pull request Nov 3, 2026
… the vsync grid (#2332)

* gamescope: payload 20261103.5, a VSync-on game at the panel's rate on the vsync grid

Payload 20261103.5 is 20261102.4 with only lib/libadrenotools_gamescope.so
replaced (a0bc3ab2, build string 2026110310298a5b695), the shim from
GameNative/gamenative-drm#48 at 8a5b695. At a cap equal to the panel's rate
a game with in-game VSync on is presented as mailbox with the shim's
vsync-grid gate, and its present wait is answered when it queues the
present, so its CPU and GPU work overlap instead of alternating. Games with
VSync off and every other case are unchanged. gamescope, Xwayland and
libgamenative_drm.so are unchanged.

* gamescope: payload 20261103.6, the present wait at queue for the VSync-on cell only

Payload 20261103.6 is 20261102.4 with only lib/libadrenotools_gamescope.so
replaced (d04525f2, build string 202611031208762e559), the shim from
GameNative/gamenative-drm#48 at 762e559. It narrows 20261103.5: the present
wait is answered when the game queues the present only for a game with
in-game VSync on at a cap equal to the panel's rate in a container whose
present mode is not fifo; every other case answers as on master.
gamescope, Xwayland and libgamenative_drm.so are unchanged.

---------

Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant