Skip to content

android: A58 — under the FPS limiter, hold a second mailbox commit for the next vblank - #31

Closed
utkarshdalal wants to merge 2 commits into
gamenativefrom
gn-mailbox-hold-under-limiter
Closed

utkarshdalal wants to merge 2 commits into
gamenativefrom
gn-mailbox-hold-under-limiter

Conversation

@utkarshdalal

@utkarshdalal utkarshdalal commented Nov 3, 2026 •

Copy link
Copy Markdown

Branch gn-mailbox-hold-under-limiter (9fad070), base gamenative. Adds one patch, android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch. The number is 0054 because 0052 belongs to #25 (A56) and #23 (A54), and 0053/A57 to the closed #30. A58 applies over the 0001-0051 series alone. CI build: run 41567798845 (9fad070), green. Binary sha256 4feeadc116518bd7ff2c4f158022f054106f8607d0f451bf6d4ff36560f7bd94. H1 and H2 below ran on the first revision, 52e1b1f (47d8fe6c…, run 41564086468), which had no carry bound. The baseline is the shipped 65d024fc…, which is the CI build of the same series without A58.

Pairs with GameNative/gamenative-drm#46.

Problem

Under the FPS limiter, gamescope's mailbox merges a second commit into one composite window. A guest pacing itself just under the refresh rate hits this about once a second. The case here is DTD with in-game VSync off: Unity targets ~59.3 fps, the cap is 60 and the panel runs at 59.95-59.98 Hz.

  • Non-FIFO done commits are latched on every steamcompmgr pass, not only at vblank (handle_done_commits_xwayland / handle_done_commits_xdg).
  • When two commits complete before the same paint, the second replaces the first in HeldCommits[BASE]. The first is released and its presentation feedback is discarded.
  • The panel then shows the previous frame twice. Without this patch, the shim-only mailbox attempts in shim: the present mode follows the container, the cap runs on the vsync grid under mailbox gamenative-drm#46 measured 58.3-59.0 fps with 2.7-2.9 % doubled intervals.

The client cannot avoid the merge, because it cannot predict gamescope's latch point:

  • The latch point is gamescope's vblank wakeup: the target vblank minus the rolling draw time minus the red zone (src/vblankmanager.cpp 142-199).
  • The rolling draw time holds peaks. A draw more than half a red zone above it replaces it at once (164-165), and it then decays per wakeup (167).
  • "Draw time" runs from wakeup to page flip queued (src/Backends/DRMBackend.cpp ~4465). On gamenative-drm that includes the commit to the daemon, so it is spiky.
  • So the latch point moves by milliseconds and jumps on any spike. All the client gets back is presented / discarded, one inequality per frame.
  • shim: the present mode follows the container, the cap runs on the vsync grid under mailbox gamenative-drm#46 tried booking late frames (B5) and a commit-window queue at a guessed boundary (B6). Both still had ~0.8-1 commit/s merged.

Only gamescope knows its latch point, so the hold has to live in gamescope.

Change

All of this is gamenative-only, behind the convar gn_mailbox_hold. It applies only while the FPS limiter is engaged for the window: a target FPS is set, steamcompmgr_window_should_limit_fps is true and VRR is off. For non-FIFO done commits:

  • Latch: the first commit to complete for the coming paint is latched as today.
  • Hold: a newer commit is kept in the done list instead of replacing it. It is latched by the first done-commit pass of the next vblank cycle; the vblank pass always runs one, so a held commit never waits for the client.
  • Replace: a third commit replaces the held one. The replaced commit is dropped from the window's commit_queue; ~commit_t then releases its buffer and sends discarded, exactly as a mailbox replace does today.

Properties:

  • The queue is one deep and the client is never blocked.
  • A window that delivers at or below the refresh rate never loses a frame.
  • A held commit's earliest present time is stamped by the pass that latches it, so presentation feedback reports the vblank it was actually shown on.
  • Frame callbacks are unchanged: one per vblank, after a latch.
  • FIFO commits are untouched, as are unlimited windows and VRR. With a shim that declares FIFO (today's payload), A58 changes nothing.

The carry is bounded (9fad070)

A held commit is shown one paint late. A client that keeps delivering exactly one commit per vblank never drains that delay:

  • Every new commit finds its paint already taken by the carried one, so it is carried in turn.
  • The result is a standing one-frame delay that the frame rate does not show. H2 hit this: every commit was carried, from a hitch during loading onwards.

So after gn_mailbox_hold_max_carry paints in a row (default 8, about 133 ms) have shown a carried commit, the next fresh commit is latched at once and replaces the carried one:

  • This is a mailbox replace: one frame is dropped and the window is back on its own paint.
  • A client below the refresh rate (H1) drains the carry by itself at its next gap, as before.
  • 0 makes the carry unbounded, which is the 52e1b1f behaviour.
  • 1 latches every fresh commit at once whenever its paint holds a carried one. That keeps no carry across a second paint, which makes the hold close to plain mailbox for a client near the refresh rate.

Implementation:

  • A per-window marker, gnMailboxLatchedForPaint (steamcompmgr_shared.hpp), records which paint the window's last non-FIFO commit was latched for. It holds vblank_idx + 1; vblank_idx advances after the vblank pass's done-commit handling, so every pass with the same index feeds the same paint.
  • gn_mailbox_hold_entry() is called from both done-commit handlers, after the FIFO check.

Switches

  • gn_mailbox_hold: -1 = auto, the default (on when GAMENATIVE_DRM_SOCKET is set), 0 = off, 1 = on.
  • The env gamescope_gn_mailbox_hold=0/1 overrides it through the existing convar env mapping.
  • On Android, the property debug.gamescope.gn_mailbox_hold=0/1 wins when set. It is settable with an unrooted adb shell setprop.
  • gn_mailbox_hold_max_carry (default 8, 0 = unbounded) follows the same pattern: env gamescope_gn_mailbox_hold_max_carry, property debug.gamescope.gn_mailbox_hold_max_carry.
  • The value is resolved once, at the first done-commit pass.

Logs (xwm scope)

  • Once at start: gn_mailbox_hold=<0|1> source=<default|env|prop> max_carry=N.
  • About every 600 vblanks, while the hold does anything: gn_mailbox_hold: deferred=N carried_paints=C max_run=R collapsed=X replaced=M over K vblanks.
    • deferred: commits held for a later paint, each counted once.
    • carried_paints: paints that showed a commit one paint late. This is the latency the hold adds.
    • max_run: the longest run of carried paints in a row.
    • collapsed: carries ended by the bound.
    • replaced: held commits dropped for a newer one.

The 52e1b1f counter

52e1b1f logged held=N, counting a held commit once for every compositor pass in which it stayed held. A held commit stays held for at least two passes: the pass it completes in, and the vblank pass of the paint it was held from. It is latched by the first pass after vblank_idx advances.

So H2's steady held=1200 over 600 vblanks means every one of the ~600 commits was deferred once. That is a real one-paint delay on every frame, not a counting artefact. Likewise, H1's held=38-164 means about 19-82 deferrals per 600 vblanks.

There is no off-by-one on the marker. It is set to vblank_idx + 1 at latch, and every pass after the vblank pass computes vblank_idx + 2, so the first commit of a new paint is never mistaken for an already-latched one. It was the carried commit that took the paint.

Device verification (Retroid Pocket 6)

H1 setup:

H1 result:

  • Census: declared=mailbox requested=immediate source=env-at-rate cap=60 gate=vsync, declared_fifo=0.
  • gamescope: gn_mailbox_hold=1 source=prop.
  • Holds in steady state: held=38-164 replaced=0 per 600 vblanks, on the 52e1b1f counter. All 5 replacements in the run happened during loading.

H2 setup: the same stack (52e1b1f), with in-game VSync on (requested=fifo) and a 59.98 Hz panel.

H2 result:

  • Census: declared=mailbox requested=fifo source=env-at-rate cap=60 gate=vsync.
  • Shim: +600 acquires/commits/releases per 10 s, gate_holds +600, late 0-4, skipped 0, slot_waits 0, commit_lat_us ~6.0 ms.
  • Daemon: 600/600, late=0, margin p50 +3.45 ms, lead ~2.2 ms.
  • gamescope: gn_mailbox_hold=1 source=prop, replaced=0 in steady state.
  • held=1200 per 600 vblanks from the fourth window on, meaning every commit was carried one paint (see above). 9fad070 bounds this.
  • Shim commits 592-594 per 10 s against daemon publishes 592-594, so no commit is merged.
  • slot_waits=0.
run stack SurfaceFlinger, 60 s doubled intervals
H1 mailbox + vblank-grid gate, A58 hold 59.269 fps 41/3556 (1.2 %)
master FIFO declared, gamescope's limiter 59.22 fps 1.3 %
GameNative/gamenative-drm#46 B4-B6 shim-only mailbox (gate / late booking / commit window), no A58 58.3-59.0 fps 2.7-2.9 %
H2 DTD, in-game VSync on, mailbox + gate, A58 52e1b1f 59.984 fps, 3599 singles 0 (but every commit shown one paint late; see above)
H2' same, A58 9fad070 (carry bound 8) pending pending (expect carried_paints near 0 after one collapsed)
H1' as H1, A58 9fad070 pending pending (checks that the bound keeps H1 lossless)

The remaining shortfall from 59.95 is the game's own ~59.3 fps self-pacing, which is the same as under master's FIFO.

Buffer pressure

With the hold, a window can have one buffer on screen, one latched and one held. A client with only three swapchain images could then wait for a release now and then. In H1 the shim's slot_waits stayed at 0. Clients that render directly to Xwayland with a three-image swapchain have not been measured.

Pairing with gamenative-drm GameNative/gamenative-drm#46

GameNative/gamenative-drm#46 makes the shim declare the container's present mode on the override route. Its one exception is a cap at the display's rate, where it declares FIFO because the shim alone cannot keep two commits out of one composite. With A58 in the payload, that exception can be dropped: gamescope and the shim ship in the same archive. Until then, GameNative/gamenative-drm#46 keeps the exception by default, and debug.gndrm.mailbox_at_rate turns on mailbox at the display's rate for testing.

@joshuatam, I'd value your read on the hold point. Is latching the held commit at the next cycle's first done-commit pass right, and should the hold also cover windows that are not the focus base plane?

…r the next vblank

A mailbox client pacing itself just under the refresh rate gets two commits
into one composite window about once a second, and the newer one replaces the
older before the paint, so the panel repeats a frame. Under the limiter, with
gn_mailbox_hold on (default on gamenative-drm), the second commit is held and
latched at the next vblank; a third replaces the held one. Test build for the
mailbox-at-display-rate study; see the patch header.
… gn_mailbox_hold_max_carry paints

With in-game VSync on the client delivers one commit per vblank, so once a
commit was held every later one found its paint taken by the carried commit
and was carried in turn: a standing one-frame delay that frame rate cannot
show. After gn_mailbox_hold_max_carry paints in a row (default 8, 0 =
unbounded, property debug.gamescope.gn_mailbox_hold_max_carry) a fresh commit
is latched at once and replaces the carried one.

The report counted a held commit once per compositor pass it stayed held
(twice per vblank); it now counts each deferral once and reports
carried_paints, max_run and collapsed alongside replaced.
@utkarshdalal

Copy link
Copy Markdown
Author

Closing. The measurements this PR produced argue against shipping it.

  • With in-game VSync on at a cap equal to the display's rate, the hold matched FIFO on frame rate (59.984 fps, 0 doubled intervals) but gamescope's own counter showed every frame being shown one paint after it arrived: once a hold forms, a guest delivering one commit per vblank never drains it. The carry bound added in 9fad070 only trades that standing delay for a dropped frame every N paints.
  • FIFO declared with the vblank-grid acquire gate (shim: the present mode follows the container, the cap runs on the vsync grid under mailbox gamenative-drm#46, source=cap-rate) gives the same cadence with a queue of at most one frame and nothing carried. At the display's rate there is no latency left for mailbox to recover, because the panel only flips at vblank.
  • Below the display's rate the gate releases at least two vblanks apart, so two commits in one composite is rare and the hold has nothing to do.

So the one case this patch addresses is already served by FIFO plus the gate, and gamenative-drm#46 keeps that rule. The branch gn-mailbox-hold-under-limiter stays for reference (A58 at 9fad070, CI run 41567798845); the run logs are listed in the description above.

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