Repository navigation
shim: hold the acquire cadence at the cap period - #34
Merged
joshuatam merged 2 commits intoOct 30, 2026
Merged
Conversation
utkarshdalal
force-pushed
the
acquire-cap-on-the-latch-grid
branch
from
October 30, 2026 14:45
e441f7e to
c72dfd1
Compare
utkarshdalal
force-pushed
the
acquire-cap-on-the-latch-grid
branch
from
October 30, 2026 14:53
c72dfd1 to
b545713
Compare
utkarshdalal
changed the base branch from
master
to
fix/gamescope-resolution-scanout
October 30, 2026 14:53
joshuatam
added a commit
that referenced
this pull request
Oct 30, 2026
…s its row stride (#32) * android: the mode is the session's scan-out size, not the window's The emulated connector advertised exactly one mode: the app window's size. A container's Upscaling Resolution (= gamescope's -W/-H) therefore never matched a mode, `find_mode` fell through to the connector's first mode, and `drm_set_mode` overwrote `g_nOutputWidth/Height` with it. The setting was inert below the panel: the composite, the scan-out images, the pool's buffers and the published layer were all the panel's pixel count (2.25x the area at 720p on a 1080p panel), and gamescope upscaled the guest into that rather than letting the display's compositor upscale a smaller scan-out. The app now names the size before `nativeStart`, and that size is the mode. The presenter takes the window's box separately and fits the committed plane rect into it, so a mode below the window is upscaled once, by SurfaceFlinger, and a mode of another aspect is letterboxed rather than stretched; mode == window is the identity and presents exactly as before. modules/gndrm-android/src/lib.rs `nativeSetScanoutGeometry(w, h)` (JNI) stores the request; `0, 0` clears it, because the value is process-lifetime and a session whose resolution cannot be read must not inherit the previous one. `scanout_geometry` resolves the mode at `nativeStart`: the requested size when it fits the window, else the window's — a larger mode would composite pixels the display cannot show and have them scaled back down, so it is refused with a log rather than clamped. `start` keeps window and mode apart, passes the window size to the server thread, and says both in the start line. `retarget`/`set_window`: a window change adopts the new box (rebuilding the layer when the window itself changed) and never re-modes what gamescope is compositing at. modules/gndrm-android/src/asc.rs The presenter carries `mode_w/mode_h` (pool geometry, `note_framebuffer`, the layer-rect/HIDDEN/shown/republish/teardown lines) and `win_w/win_h` (the compositing box). `publish` maps the CRTC rect through `gndrm_daemon::scanout::layer_rect` before the transaction; the diagnostics print mode and window and the band is measured against the window. modules/gndrm-daemon/src/scanout.rs `layer_rect(crtc, mode, window)`: KMS plane rect in mode pixels → window pixels, clipped to the mode, uniformly scaled and centred. Host-tested in four cases: identity for a mode that fills the window (including a plane letterboxed inside the mode), a 1.5x upscale covering the window, a pillarboxed 4:3 mode, and a plane rect clipped to the mode. modules/gndrm-daemon/tests/golden.rs `connector_advertises_the_session_scanout_size`: GETCONNECTOR and GETCRTC carry the size the device was built with, which is what lets gamescope's -W/-H find a mode instead of falling back to the connector's. tools/exports/libgamenative_drm.map The new JNI entry, so the cdylib's dynamic table stays exactly the allowlist. docs/02-gamenative-drm.md The connector's one mode is the session's scan-out size; the JNI surface lists `nativeSetScanoutGeometry` beside `nativeSetPanelRefreshHz`. docs/03-gamescope-build.md The launch line carries both pairs: -W/-H the scan-out size (the daemon's mode), -w/-h the container screen size (the guest desktop). docs/04-gamenative-app.md Same for the command and env section, and the present path: the CRTC rect is in mode pixels and is fitted into the window, which is where the Upscaling Resolution's upscale happens. * shim: answer the pool buffer's row stride gralloc pads scan-out rows for widths it does not like (1024 px -> 5120 B on Adreno) while a LINEAR Vulkan image is laid out at width * 4. A client that binds the pool buffer at its own pitch writes rows the compositor reads one row apart, which shears every composited frame. Direct scanout never writes that image, so the guest's own buffer stays clean — and a filter, a look or ReShade all force the composite, so the fault reads as "the scaling filter is broken". The shim is the only side that knows the stride: the daemon reports it with every hand-out. It now keeps it, keyed by geometry, and answers it to gamescope's A51 patch, which creates the scan-out image at that stride. An unseen geometry is measured with one throwaway pool allocation — the fd is closed, and the geometry with its padded length stays in the pool's index, which is what the client's real allocation then resolves against. No daemon channel answers 0, and the caller keeps the driver's own layout, so a real KMS session and a shim-less build are untouched. modules/gndrm-shim/gndrm_shim.c The stride table (geometry + length + stride, an entry refreshed on a repeat) and gamenative_drm_scanout_pitch(w, h) with its measure-an-unseen-geometry path: note the stride in scanout_hand_out, and resolve by geometry or by the geometry's tight length for a length-only hand-out. scanout_request's forward declaration moves above the table so the escape has its one definition site. tools/exports/libgndrm_shim.map The new entry point joins the interposed-set allowlist (14 exports). docs/02-gamenative-drm.md A Row stride bullet under §7: whose stride the pool buffer carries, what a client that ignores it looks like, and that the guest's own buffers are the ICD's job (proxy/export) while gamescope's output image is A51's. * docs: the mode's rate is the panel's, and the EDID follows it The refresh split landed in the daemon earlier — the emulated mode advertises the rate the app reports through nativeSetPanelRefreshHz (raised to the frame-generation output rate when that is higher), and SESSION_REFRESH_MHZ is the vblank grid alone — but three docs still described the single-variable model, and the EDID's detailed timing kept a pixel clock whose comment rested on the same obsolete premise: "the advertised refresh is the 1200 Hz grid, which no DTD can express". docs/10-fps-limit.md -r is the session's grid, and the launcher passes a constant 1200 Hz, so the grid, the app-refresh cycle and the limiter's divisor base all read that constant while the mode's rate reaches the clients. The mode paragraph no longer claims 1280x960@1200 for every -r. "Recommended wiring", which still described carrying the fps limit in -r, becomes "How the cap is applied": the JNI fans out to the injector's set_app_target_refresh_cycle and to the frame cap the shim enforces at acquire. The open points now record that divisor rounding is a gamescope-route artefact, and that -r reaches g_nOutputRefresh only transiently before drm_set_mode overwrites it. docs/11-refresh-model.md The gndrm section said one preferred mode of 1280x960@1200 and that -r declares its value everywhere. It now states the split: the mode carries the panel's rate, -r is the grid (paint tick, app-refresh cycle, divisor base), and a cap is a divisor or the shim's acquire floor. Line references re-verified against the tree. docs/15-fps-limiter-pacing-and-vsync.md The §1 mechanism block: the launcher does pass -r, the mode no longer encodes SESSION_REFRESH_MHZ, and the refresh pushed to clients is the panel's. §8.3's "the panel's rate bounds nothing, PanelDesc carries none" and the stale 1200 Hz intent reference are corrected, and the scope note separates what landed from the pacing arithmetic that has not. modules/gndrm-daemon/src/edid.rs The detailed timing carries the mode's own clock, so the descriptor and the connector describe one timing; a clock the 16-bit 10 kHz field cannot hold (a generated rate above 4K) keeps the nominal 60 Hz fallback. The comment records why this is inert either way — gamescope takes the mode from the connector and the EDID only for colorimetry and HDR. Two tests: the descriptor implies the connector's rate at 1280x960@120Hz, and an unrepresentable clock falls back. * shim: hold the acquire cadence at the cap period (#34) * shim: hold the acquire cadence at the cap period * docs: the shim's floor steps from the deadline; log the cap once it is in force --------- Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com> --------- Co-authored-by: Utkarsh Dalal <dalal.utkarsh@gmail.com> Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
The ICD shim's frame cap scheduled the next acquire at
max(prev, now) + period. The condvar wait forprevalways returns a hair after the deadline, sonowwon every frame and that overshoot was added to every period: the cap ran below its target by the wake-up latency per frame, and by more on a loaded or throttled device.next_acquire_afternow stepsprev + periodwhile the guest keeps up, and only re-bases onnowonce the guest is a whole period behind (no debt out of a stall). The two existing cases in the unit test are unchanged by the new rule; a third covers the overshoot.Verification
Odin 3 (8 Elite, panel at 60 Hz), JT's stack (app #2283 + active-refresh report, payload 20261030.2) with only the ICD swapped, Dave the Diver title menu (renders above 60 uncapped), cap 60, two 30 s windows each:
The displayed rate from
dumpsys SurfaceFlinger --latencystays at 59.4–59.8 in both: a handful of presents per 30 s still take two vsyncs downstream, which this change does not address.Scope: 24 lines in
icd_shim/src/scanout.rs. JT's #32 does not touchicd_shim/, so this applies identically on master or onfix/gamescope-resolution-scanout; it was tested on the latter's payload.