Repository navigation
shim: the present mode follows the container, the cap runs on the vsync grid under mailbox - #46
Conversation
The VsyncNote gives a shim the panel's latch grid (anchor, measured period) and frame generation's commit lead. It does not say where gamescope's vblank grid is: the daemon stamps each page-flip event its latch minus a lead of its own (vblank.rs LeadLoop, ~2-3 ms, measured), and gamescope schedules its next composite from that stamp. A shim that wants to release frames on gamescope's schedule needs that lead too. The note's reserved word now carries it as stamp_lead_ns: the lead of the grid the flip events follow at the time (gndrm_daemon::vblank::display_grid), 0 while they follow none. A move of more than the existing 300 us push step pushes the note, as the phase and the frame-generation lead already do. The wire size is unchanged: a daemon that predates the field sends 0 there, and a shim that predates it never reads it. - modules/gndrm-uapi/src/wire.rs: VsyncNote::stamp_lead_ns in bytes 4..8; round-trip test, and an old note decoding as 0. - modules/gndrm-android/src/vsync.rs: Tracker::track and Tracker::note take the stamp lead and push on its move; sample() and note() read it from the display grid before taking the tracker's lock; test for the push rule.
…e's limiter does not hold gamescope's limiter holds only commits declared FIFO (wlserver_surface_is_fifo compares with VK_PRESENT_MODE_FIFO_KHR alone). A mailbox, immediate or FIFO-relaxed commit is taken at the next composite whatever the cap. Before 7bdcbae the shim's acquire gate stood in for the cap there, on a clock of its own (one release every 1e9 / cap ns), and that clock beat against the panel: a 60 cap on a 59.97 Hz panel ran 57-59 fps on SurfaceFlinger. This adds a gate that releases on the vblank grid gamescope composites from: the daemon's VsyncNote latch grid (measured period, measured phase) moved earlier by the stamp lead the daemon stamps flip events with. gamescope wakes just before each of those instants, so a release there starts the guest's frame right after gamescope took the previous one. Release points (vsync_gate::pacing, Schedule::book): - a cap at the display's rate (within 0.5 %): every vblank, so 60 on a 59.97 Hz panel is 59.97, one frame per latch; - a cap that divides the rate: every refresh/cap-th vblank (30 on 60, 60 on 120); - any other cap below the rate: the vblank nearest each instant of an ideal 1e9/cap timeline (45 on 60 releases 3 of 4 vblanks); - a cap above the rate: the cap's own clock, as before; every vblank already gets a frame. A frame that arrives after its release point starts at once and the next release is the next point of the schedule, so a guest below the cap keeps its own rate instead of falling to a divisor. A phase push from the daemon is followed without letting two releases into one vblank. Before the daemon has sent a note the grid is the mode's nominal period (phase unknown); with no rate known at all, the cap's own clock. On gamescope's override route the gate now runs by the live mode (gamescope_route_pacing): frame generation's source cap on its clock as before; FIFO no gate (gamescope's limiter); anything else under a cap, the vblank grid. The live mode is still FIFO whenever a cap is set, so nothing takes the new path until the mode rule changes. Off the route (the X11 Present route) the gate is unchanged. The 10 s present-mode census gains gate=vsync|nominal|clock|off. - icd_shim/src/vsync_gate.rs (new): the grid from the note, the pacing rule, the schedule; tests for cap == refresh, 30 on 60, 60 on 120, 45 on 60, a late frame, a guest below the cap, a phase push, the nominal fallback, caps above the rate. - icd_shim/src/scanout.rs: every VsyncNote also reaches the gate. - icd_shim/src/proxy.rs: acquire_pacing / gamescope_route_pacing; the acquire gate books on a vsync_gate::Schedule (replaces next_acquire_ns); the census field; test.
…a cap no longer forces FIFO 7bdcbae made the shim declare FIFO to gamescope whenever a cap was set, and ignore MESA_VK_WSI_PRESENT_MODE on the override route, so gamescope's divisor limiter would be the cap. Default containers carry a 60 cap and mailbox in that env, so every such session ran FIFO under gamescope's limiter: a game below the cap quantizes to refresh / divisor (refresh/2, refresh/3 ...), worst on 90 and 120 Hz panels, and the user's present-mode choice is never seen on this route. The mode now follows the container, with the cap enforced by the shim's vblank-grid gate (previous commit) when the mode is not FIFO: env mailbox, no cap mailbox nobody limits env mailbox, cap mailbox the shim's gate on the display's vblank grid env fifo, no cap fifo the display env fifo, cap fifo gamescope's divisor limiter (as today) frame generation on mailbox source-rate gate (unchanged) With no env the guest's own mode is used. fifo_relaxed and immediate are gated like mailbox: gamescope's limiter holds only FIFO declarations. - effective_present_mode honours the env on both routes again, and the env's mode leads the offered list on both (as in Mesa, and as before 7bdcbae). - choose_live_mode: mailbox under frame generation, else the swapchain's mode. - The swapchain keeps the guest's own request (requested_mode) and where its mode came from (mode_source); the present wait is answered at commit when the guest asked for mailbox/immediate or the live mode is one of them, so the rule still follows the mode. The extra mailbox slot follows the mode as before. - The 10 s census gains source=env|guest; requested= is the guest's request. A device run reads, e.g.: gn-vkshim: present mode: declared=mailbox requested=fifo source=env route=gamescope cap=60 gate=vsync lsfg=0 ... - scanout::cap_in_force is no longer used and is removed. - Tests: the rule table above end to end (env, guest, cap, frame generation), and choose_live_mode.
…the vblank grid under mailbox - docs/10-fps-limit.md: who enforces the cap follows the declared mode, and the mode follows the container; the rule table (env mailbox/fifo x cap, frame generation); the gate's release points on the vblank grid, the late-frame rule, the nominal fallback and the census fields; divisor rounding bites the FIFO route only. - docs/15-fps-limiter-pacing-and-vsync.md: the shim's gate as a third place a rate is decided; FIFO paced by gamescope's latch, other modes by the gate on the flip-stamp grid; behaviour changes rewritten for the container's mode; the VsyncNote's stamp_lead_ns. - docs/README.md: index line for 15.
…FO under a cap until it arrives Measured on a Retroid Pocket 6, cap 60. DTD with the new shim and the new daemon, VSync on: 59.983 fps on a 59.983 Hz panel, 0 doubles, 3600/3600 acquires/commits, daemon late=0, the same as the FIFO reference. With the new shim and the old daemon (VsyncNote stamp_lead_ns = 0), VSync off: 58.28 fps, 102 doubles. The shim released 59.3/s on a 59.975 Hz panel and the daemon published 58.3/s with late=0, so frames were replaced in gamescope's mailbox before they reached the daemon: released on the latch instant itself, a frame starts ~2 ms after gamescope's wakeup and straddles the next composite. At 120 Hz the same pairing gave daemon late=818 (13 %), against 109 under FIFO. The gate now runs only on a note that carries the stamp lead: - vsync_gate::current_grid is None until a note with stamp_lead_ns != 0 has arrived. The nominal-period fallback is gone; nothing runs the gate without the daemon's grid. - choose_live_mode: a cap set, a mode other than FIFO and no lead give FIFO, so gamescope's limiter is the cap, exactly as before the container's mode was honoured. The rule is read per present, so a lead that arrives mid-session moves the declaration on the next commit. Frame generation and an env of fifo are unaffected. - The slot count follows the container's mode only when the lead is known at creation. Before that the guest's own request decides it, as it did before; a later lead does not change the count until the swapchain is recreated. - The census reads source=cap-compat while FIFO is declared for want of the lead. gate=nominal is gone. - daemon: the stamp lead is sent as at least 1 ns while the flip events follow a grid, so 0 always means "not known" (a GNDRM_GS_LATCH_LEAD_US=0 override would otherwise read as an old daemon). A payload that ships only this shim keeps today's pacing until the app ships the daemon that sends the lead. - icd_shim/src/vsync_gate.rs: current_grid needs the lead; GateClock::Nominal removed; the module notes give the measurement; test for no note, a note with lead 0 and a note with a lead. - icd_shim/src/proxy.rs: choose_live_mode(mode, generating, capped, lead_known); slot count; census; the rule-table test gains the no-lead rows (env mailbox + cap -> FIFO, env fifo -> fifo either way). - modules/gndrm-android/src/vsync.rs: stamp_lead_ns() sends max(lead, 1). - docs/10, docs/15: the compat rule and the payload-only rollout.
…or does not replace it Retroid Pocket 6, DTD, cap 60, in-game VSync off (requested=immediate), 1ffd476 shim and the new daemon: declared=mailbox gate=vsync, acquires = commits = releases at 59.3/s on a 59.983 Hz panel, the daemon publishing 582-588 per 600 vsyncs with late=0, SurfaceFlinger 58.33 fps with 2.8 % double intervals. With VSync on (requested=fifo) the same shim, daemon and gate gave 59.983 fps and no doubles. On master's FIFO the VSync-off scene gives 59.22 fps, so the guest itself under-delivers by ~0.7 frames a second there; the gate added ~1 more double a second on top. The shim committed every frame it acquired (commits = acquires), and the daemon missed no latch, so the frames lost between the shim's 59.3 and the daemon's 58.3 were replaced inside gamescope's mailbox: two commits inside one composite. That is what Schedule::book did to a late frame. An acquire that started after its release point went at once (correct), but was booked to the point it missed, so its successor was released one period later. The commit waits the frame's render fence (worker_main, then commit_real), so the late frame reaches gamescope after the composite it was released for; the successor, on time, reaches it before the next one; gamescope shows only the successor and the panel repeats the frame before. Each late start cost a replaced frame and a double. A VSync-on guest blocks in the acquire and is never late, which is why the FIFO-requesting cell was clean. Neither the slot count nor the present-wait rule differs between the two cells on this build (the extra slot follows the container's mailbox in both; both are answered at commit), and there is no acquire path around the gate. A late start is now booked by the composite it will make. The present worker measures each gated frame's acquire-to-commit time and keeps a smoothed estimate per swapchain; a frame late by delta makes a later composite than an on-time one when floor((delta + latency + margin) / P) > floor((latency + margin) / P), with margin 2.5 ms for gamescope's red zone and composite. A frame that still makes its composite keeps its point and the schedule goes on as before; one that misses it is booked to the next point, so its successor goes a period later and each composite gets one new frame. A guest pipelined over more than a period is judged against its own on-time latency, so nothing is skipped for it. Late frames are still never held, so a guest below the cap is still not quantized. The ungated census gains the counters a device run separates the causes by: gate_holds (acquires the gate held), late (acquires that started after their point), skipped (points moved past for them), slot_waits (acquires that passed the gate and then waited on a compositor release for an image) and commit_lat_us (the smoothed acquire-to-commit time). - icd_shim/src/vsync_gate.rs: Schedule::book takes the latency; ON_TIME_NS, COMPOSITE_MARGIN_NS, smooth_latency, late_counts; tests: a late frame that misses its composite does not share the next one (it fails with the old booking), a late frame that still makes it keeps its slot, a pipelined guest skips nothing. - icd_shim/src/proxy.rs: GnSlot::acquired_ns, GnSwapchain::commit_latency_ns (set at the gated acquire, measured in commit_real); SLOT_WAIT_N; census. - docs/10, docs/15: the late-frame rule and the census fields.
Retroid Pocket 6, DTD, cap 60, VSync off, shim 9fb3cda: no change (SurfaceFlinger 58.26 fps, 2.9 % doubles, daemon publishes 580-586 per 600 vsyncs with late=0, shim 593 acquires/commits/releases per 10 s). The census showed why: per 10 s, gate_holds 97-154, late 425-478, skipped 6-8, slot_waits 0, commit_lat_us 14090-16949. The guest paces itself at ~59.3 fps (Unity's targetFrameRate with VSync off; its HUD reads "limited by: fps cap"), just below the 59.97 Hz grid, so about three acquires in four arrive after their release point and the gate shapes almost nothing. Booking those late starts by their predicted composite moved a handful of points and left the stream as it was: the frames reach gamescope on the guest's own clock, and its jitter puts two in one composite now and then. Schedule::book goes back to the booking of 2c0c609 (a late start is booked to the point it missed, the next release the next point of the schedule). The counters stay, because they are what separated the causes: late and skipped are counted by the same rule, slot_waits and commit_lat_us as before (the acquire-to-commit time is now census-only). - icd_shim/src/vsync_gate.rs: book() without the latency; the latency tests go; COMPOSITE_MARGIN_NS goes with them. - icd_shim/src/proxy.rs: book_paced without the latency; comments. - docs/10, docs/15: the late-frame sentence as before 9fb3cda.
Retroid Pocket 6, DTD, cap 60, VSync off, shim 9fb3cda and the new daemon: SurfaceFlinger 58.26 fps, 2.9 % doubles; the shim acquired, committed and was released 593 times per 10 s while the daemon published 580-586 per 600 vsyncs with late=0. Every frame the shim committed reached gamescope, and every frame gamescope published made its latch, so the ~1 frame a second in between was replaced inside gamescope's mailbox: two commits between two of its wakeups. The census (gate_holds 97-154, late 425-478, slot_waits 0, commit_lat_us 14-17 ms per 10 s) says the guest paces itself (Unity's frame rate target with VSync off, ~59.3 fps) just below the 59.97 Hz grid: three acquires in four arrive after their release point, so the gate limits the rate but shapes nothing, and the frames reach the commit with the guest's own jitter. FIFO absorbs that jitter with its queue, which is why master's FIFO declaration gives 59.22 fps on this scene. The present worker now keeps one commit per composite window while the acquire gate runs on the vblank grid. Windows run from one gamescope wakeup, taken as 2.5 ms (its 1.65 ms red zone plus its composite) before a stamp instant, to the next. A ready frame whose window already has a commit is held to the stamp instant that closes that window: after the window's composite, so gamescope shows it at the next one. If a newer frame's render finishes while one is held, the newer replaces it, as a mailbox present does (the replaced frame's present wait is answered as discarded). A frame is held at most one period plus the margin. The hold is on the worker thread: the guest's acquire and present never wait on it, and the acquire gate still limits a fast guest's rate. A guest well below the rate rarely has two frames in one window, so it is rarely held. Present-wait answers are unchanged: a frame's wait is answered when it is committed, as before. The census gains win_holds and win_replaced, and the spacing of consecutive commits since the last line in buckets of <15, 15-16.2, 16.2-17.2, 17.2-20 and >20 ms, the guest's own cadence at the commit. In a model of the DTD case (59.3 fps +- 1.5 ms on a 59.97 Hz grid, 15 ms to commit, 1200 frames), commits straight from the fence put two frames in one composite 6 times; with the window none, 24 frames held, none replaced in the shim. - icd_shim/src/vsync_gate.rs: CommitWindow (hold_until, committed), COMPOSITE_MARGIN_NS, the window counters; tests: a self-paced guest at 59.3 fps with jitter gets one commit per composite (and does not without the window), a 32 fps guest is never held, a 300 fps guest is held to one frame per vblank by the gate and the window replaces nothing, a second frame in a window is held to the next stamp instant, re-anchoring. - icd_shim/src/proxy.rs: hold_for_window and slot_fence_signalled on the worker before commit_real; GnSwapchain::window; the commit-spacing histogram; census fields. - docs/10, docs/15: the commit window and the census fields.
Retroid Pocket 6, DTD, cap 60, VSync off, shim 8cf5834: SurfaceFlinger 59.0 fps median, 2.7 % doubles; shim commits 592-594 per 10 s against daemon publishes 583-590 per 600 vsyncs with late=0, win_holds 18-48 and win_replaced 0 per 10 s. gamescope still merged ~0.8 commits a second. The window's boundary was a guess at gamescope's latch point, and that point is not a fixed offset: gamescope takes client commits at its wakeup, the target vblank minus its rolling draw time and red zone (vblankmanager.cpp, 142-199), and the rolling draw time is a peak hold that jumps on any slow draw (164-165) and decays per wakeup (167). Draw time runs from wakeup to the page flip queued (DRMBackend.cpp 4465), on this stack through the daemon's commit, so it is spiky. The client sees only presented (stamped with the wakeup's target vblank, steamcompmgr.cpp 8140, 8168) and discarded, one inequality per frame, and the bounds close only as the guest's commit phase sweeps the period, slower than the decay and never for a guest locked to the grid. A client-side boundary keeps a moving error band of a few ms, and commits inside it still merge. The window and its counters go. The histogram of commit spacing stays: it reads the guest's own cadence at the commit, which is what showed the guest pacing itself. - icd_shim/src/vsync_gate.rs: CommitWindow, COMPOSITE_MARGIN_NS and the window counters removed with their tests. - icd_shim/src/proxy.rs: hold_for_window, slot_fence_signalled and GnSwapchain::window removed; win_holds/win_replaced leave the census; the spacing histogram stays. - docs/10, docs/15: the window paragraphs removed; the spacing field kept.
…-grid gate kept Under mailbox at a cap equal to the display's rate the shim has to keep two commits out of one gamescope composite, and it cannot see when gamescope latches (previous commit). Retroid Pocket 6, DTD, cap 60, in-game VSync off, a guest pacing itself at ~59.3 fps on a 59.98 Hz panel: - mailbox with the gate (1ffd476): 58.33 fps on SurfaceFlinger, 2.8 % double intervals, ~1 commit a second replaced in gamescope's mailbox; - with the late-frame booking (9fb3cda): 58.26 fps, 2.9 %; - with a one-commit-per-window queue at a guessed boundary (8cf5834): 59.0 fps, 2.7 %, ~0.8 a second still replaced; - master's FIFO declaration: 59.22 fps. At this cap one frame per vblank is all the panel shows under either mode; under FIFO gamescope latches one commit per vblank at its own wakeup (steamcompmgr.cpp 8028-8049) and its limiter's divisor is 1. The rule on the override route, frame generation off: - the container's (else the guest's) mode is FIFO: FIFO, no gate, gamescope's limiter, as before; - any other mode, a cap at the display's rate (refresh / cap snaps to 1): FIFO, and the acquire gate stays on the vblank grid, one release per vblank, so the guest never gets ahead of gamescope's FIFO latch and the queue stays about one frame deep (source=cap-rate); - any other mode, a cap below the display's rate: that mode, the gate (unchanged: releases at least two vblanks apart rarely put two frames in one composite, and FIFO with gamescope's limiter quantized a 32 fps game to 30 at 60 on 120 Hz); - no cap: the mode, no gate; - no stamp lead: FIFO, no gate (source=cap-compat), unchanged. A cap above the display's rate keeps the mode with the gate on its own clock. The choice is read per present, so a cap changed mid-session moves the declaration on the next commit. The slot count still follows the container's mode; with the gate holding the guest to one frame per vblank the spare image does not deepen the FIFO queue. A guest that asked for mailbox or immediate still has its present wait answered at commit; a FIFO guest's is answered from the feedback, as under any FIFO declaration. - icd_shim/src/vsync_gate.rs: at_display_rate. - icd_shim/src/proxy.rs: Declared (Mode, Generating, CapCompat, CapRate); choose_live_mode returns the mode and why, from the cap and the grid; live_choice; gamescope_route_pacing keeps the gate for CapRate and turns it off for a FIFO swapchain and for CapCompat; census source=cap-rate; tests: 60 on 59.97 Hz is FIFO with the gate every vblank, 60 on 120 Hz and 30 on 60 are mailbox with the gate every second vblank, env fifo with a cap is FIFO with no gate, no cap is the env's mode with no gate, no lead is cap-compat, frame generation is mailbox on the source-rate clock. - docs/10, docs/15: the rule table, why mailbox is not declared at the display's rate (with the measurements and gamescope's source), the census.
joshuatam
left a comment
There was a problem hiding this comment.
Reviewed at ce1f919. The design is right and the measurements hold it up: I checked the load-bearing premise in gamescope itself — wlserver_surface_is_fifo (src/wlserver.cpp:3412) is FIFO-only and handle_done_commits_xwayland holds a commit only when entry.fifo (src/steamcompmgr.cpp:8254), so "the limiter is the cap for FIFO, the gate is the cap for everything else" is sound. The rule table, choose_live_mode, gamescope_route_pacing, pacing() and the cap-compat fallback all match it. Wire change is compatible both ways (24 bytes, reserved word at offset 4, old daemon writes zeros → reads as "unknown"). cargo test --workspace 304 pass on your tip, clippy clean with -D warnings. (The occasional shim_integration failure is pre-existing — master flakes the same way, 1/10 runs.)
One blocker: Schedule loses the cap after a clock-booked frame.
book_clock computes the clock plan correctly but leaves ideal_ns = 0, and book's on-time branch trusts it as an absolute instant:
Some(due) if at <= due => (due, self.ideal_ns),nearest(ideal_ns + interval) then resolves near CLOCK_MONOTONIC zero, max(slot + period) wins every time, and because the guest keeps arriving on time the restart branch never fires — permanent. Test against this tip, 30 cap on 60 Hz, expecting two vblanks per release:
gaps in vblanks: [0, 1, 1, 1, 1, 1]
assertion failed: a 30 cap must stay two vblanks apart (left: 16675004, right: 33350008)
Reachable mid-session two ways, since both produce Pacing::Clock first: turning frame generation off at a cap below the display rate, and moving a cap from above the panel rate to below it. After either, a 30-on-60 or 60-on-120 cap stops binding. The default 60-at-~60 Hz is immune (interval == period), which is why B1/B7 and C1 wouldn't have shown it. Suggested one-line fix inline; with it the gaps become [1, 2, 2, 2, 2, 2] and all 87 tests pass. Worth a test for the clock→grid transition either way.
Smaller:
docs/15§5 still reports the removed gate (gate_holds,gate_avg_us≈ 22 ms) as what holds a capped session, which §3 now contradicts. This PR owns that document.- Two different
late=counters now exist — your gate-side one and the daemon's missed-latch one. Rename one, or call the distinction out in docs/10's census paragraph. - docs/17 should say the generation path still runs on the cap's own clock, so nobody reads the vblank grid as covering LSFG.
MESA_VK_WSI_PRESENT_MODEis the user-facing control now, and it is already where a user can reach it (the container env, written from the driver's present-mode setting). Worth documenting: it takes Mesa's names (fifo,relaxed,mailbox,immediate—fifo_relaxedis rejected), it is overridden when the cap equals the display's rate (source=cap-rate), and it affects zink guests too since Mesa reads the same variable. Also consider making the "Invalid MESA_VK_WSI_PRESENT_MODE value!" log ungated — today a typo is silent in a shipped session.
On (1) — the rule table. It holds up as written, including FIFO at cap == display rate with the gate kept. B4/B5/B6 justify it: mailbox with any window the shim can guess loses ~1 fps and ~100 replaced frames per minute, because the shim cannot see gamescope's latch point and the draw-time peak hold moves it. Keeping the gate under that declaration is what stops the FIFO queue deepening, so the pair is coherent rather than belt-and-braces.
On (2) — a user choice for that case. No. The container's present mode is already the override everywhere else, and the one place it is overridden is the one you measured losing that fps and those replaced frames — a knob would ship the losing option as a supported configuration, invisible in a support log unless the census is read. MESA_VK_WSI_PRESENT_MODE already exists as an escape hatch for a diagnostic, and source= in the census says afterwards which path ran, which is the property worth having.
| /// ([`crate::scanout::next_acquire_after`]). | ||
| pub(crate) fn book_clock(&mut self, now_ns: u64, period_ns: u64) -> u64 { | ||
| self.next_ns = crate::scanout::next_acquire_after(self.next_ns, now_ns, period_ns); | ||
| self.ideal_ns = 0; |
There was a problem hiding this comment.
The trigger for the state bug described in the review: ideal_ns is read as an absolute CLOCK_MONOTONIC instant by the next book, so leaving it at 0 makes nearest(ideal_ns + interval) resolve near zero and max(slot + period) win every time — the schedule stays at one release per vblank while the guest keeps arriving on time.
Verified against this commit: a 30 cap on 60 Hz gives gaps [0, 1, 1, 1, 1, 1]; with this line they become [1, 2, 2, 2, 2, 2] and all 87 tests pass. Keeping ideal_ns coherent with next_ns also covers the Pacing::Off → grid path, where an ideal from before the pause used to survive and poison the first grid booking.
| self.ideal_ns = 0; | |
| // `book` reads `ideal_ns` as an absolute instant: leaving it at 0 | |
| // resolves `nearest(ideal_ns + interval)` near CLOCK_MONOTONIC zero and | |
| // the schedule collapses to one release per vblank. | |
| self.ideal_ns = self.next_ns; |
…ug.gndrm.mailbox_at_rate) A test flag for the mailbox-at-display-rate study, not a rule change: the default rule (FIFO at a cap at the display's rate, source=cap-rate) is unchanged. With the Android property debug.gndrm.mailbox_at_rate=1 (or, when the property is unset, the env GNDRM_MAILBOX_AT_RATE=1), a cap at the display's rate declares the swapchain's own mode (the container's mailbox) with the vblank-grid acquire gate kept, instead of FIFO. The census reads source=env-at-rate. This pairs with a gamescope that holds a second mailbox commit for the next vblank under its limiter (GameNative/gamescope branch gn-mailbox-hold-under-limiter, A58), so that path can be measured against the FIFO declaration. The effective value is logged once, right after the ICD loaded line: "mailbox_at_rate=<0|1> source=<prop|env|default>". Unit test covers the rule with the switch on and the precedence.
…it can use Schedule::book_clock left ideal_ns at 0, and Schedule::book's on-time branch reads ideal_ns as an absolute instant: nearest(0 + interval) resolved near CLOCK_MONOTONIC zero, max(slot + period) won every time, and because the guest kept arriving on time the restart branch never fired. A 30 cap on 60 Hz after a clock booking released every vblank (gaps [0, 1, 1, 1, 1, 1] instead of two apart) for the rest of the session. Both clock-to-grid transitions reach it: frame generation turned off at a cap below the display's rate (the source-rate clock first), and a cap moved from above the panel's rate to below it. A cap at the display's rate was immune (interval == period), which is why the device runs did not show it. book_clock now leaves ideal_ns at next_ns, the instant the next acquire is due, so the first grid booking continues from it. Reported by @joshuatam in the review of #46, with the fix. - icd_shim/src/vsync_gate.rs: book_clock sets ideal_ns = next_ns; test: a 30 cap on 60 Hz stays two vblanks apart after frames booked on the source-rate clock and on a cap-above-rate clock (it fails with the old line), and with no clock booking.
…'s control, docs/15 and docs/17 as the code is From the review of #46 by @joshuatam: - The census's late= (the acquire gate's late starts) read like the daemon's late= on its vsync check line (publishes that missed their latch). It is now gate_late=, and docs/10 names the difference. - MESA_VK_WSI_PRESENT_MODE is the user's present-mode control on the gamescope route. docs/10 says so: Mesa's names (fifo, relaxed, mailbox, immediate; fifo_relaxed is rejected as Mesa rejects it, and the shim's parser already matches Mesa's), the one override (a non-FIFO value under a cap at the display's rate, source=cap-rate), and that Mesa reads the same variable for a zink guest's own swapchain. The rule table's "fifo_relaxed" becomes the env's "relaxed". "Invalid MESA_VK_WSI_PRESENT_MODE value!" is now logged whether or not GNDRM_LOG_VKSHIM is set, so a typo shows in a shipped session. - docs/15 section 5 still gave the clock gate (gate_holds, gate_avg_us ~22 ms) as what holds a capped session, against section 3. It now leads with the Retroid Pocket 6 runs of this rule and marks the Retroid Pocket Nova numbers as measured with the clock gate, which now runs only for frame generation's source rate, a cap above the display's rate and the X11 Present route. - docs/17 says frame generation's source rate is held on the cap's own clock (gate=clock), not on the vblank grid. - icd_shim/src/proxy.rs: census gate_late=; the invalid-value line ungated. - icd_shim/src/vsync_gate.rs: comment. - docs/10, docs/15, docs/17 as above.
|
@joshuatam thanks for the careful review, and for the fix and the repro. Everything is addressed on the branch:
Tests at a4a7bb0:
Shim artefact: sha256 For context on (2): since your review, gamescope draft PR GameNative/gamescope#31 (A58, a one-deep hold of a second mailbox commit under the limiter) was measured on the same RP6 DTD scene, with mailbox declared at cap == display rate. That is the
|
…efault mode Every container carries MESA_VK_WSI_PRESENT_MODE=mailbox as a stamped default the user did not choose. With no cap in force the shim declared it whatever the guest asked for, so a game's own VSync (a FIFO request) became an uncapped mailbox session: not master's behaviour (master declared the guest's mode there) and not what the user asked for. With no cap, on the override route: - the container's env is fifo: FIFO, as before (an explicit wish wins); - the guest asked for FIFO or FIFO-relaxed: its own mode (source=guest); - the guest asked for mailbox or immediate: the container's mode, mailbox by default (source=env). Such a request means "do not make me wait", and the container's mode is the substitute for it, as on the X11 route; an uncapped immediate guest therefore gets mailbox, not tearing; - no env: the guest's mode, as before. Capped rows are unchanged: below the display's rate the container's mode with the vblank-grid gate; at the display's rate FIFO with the gate (source=cap-rate); env fifo FIFO with gamescope's limiter; frame generation and cap-compat as before. The rule is read per present, so turning the cap on or off mid-session moves the declaration on the next commit. The slot count follows the guest's request in the new case, as on master, when no cap is in force at creation. - icd_shim/src/proxy.rs: choose_live_mode takes the mode's source and the guest's request; Declared::GuestVsync (census source=guest); the slot count; tests: the rule table with the guest's request as a column (no cap + env mailbox + guest fifo or relaxed -> that mode, source=guest; guest mailbox or immediate -> mailbox, source=env; env fifo -> fifo for every guest and cap; capped rows for fifo, mailbox and immediate guests). - docs/10, docs/15: the rule table and the reason.
|
@joshuatam, one more change, to a single cell of the rule table: 1b708d5, no cap in force. Before: the container's env always won. Every container carries Now, with no cap:
Every capped row, frame generation and Shim: sha256 |
Payload 20261103.2 is 20261102.4 with only lib/libadrenotools_gamescope.so replaced (a77a7b77), the shim from GameNative/gamenative-drm#46 at 1b708d5: with no cap, a guest's FIFO request now wins over the container's default mode. gamescope and Xwayland are unchanged; libgamenative_drm.so is unchanged (the daemon sources have not changed since 46ccc70).
…nters behind GNDRM_LOG_VKSHIM; the mailbox-at-rate test switch removed
For shipping, the shim's ungated output stays short:
- The present-mode census prints ungated once when frames first flow, and
again whenever the declared mode, its source, the cap or the gate changes:
present mode: declared=fifo requested=fifo source=cap-rate
route=gamescope cap=60 gate=vsync lsfg=0
source= stays in it, so a support log still says which path ran. The
10 s repeat with the counters (acquires, commits, declared_fifo/other,
releases, feedback/presented/discarded/fallback, gate_holds, gate_late,
skipped, slot_waits, commit_lat_us, spacing) moves behind
GNDRM_LOG_VKSHIM=1, the switch the shim's stats line already uses. The
counters themselves still count from the session's start.
- "Invalid MESA_VK_WSI_PRESENT_MODE value!" stays ungated but prints once:
the env is parsed on every present-mode query and swapchain creation.
- The debug.gndrm.mailbox_at_rate / GNDRM_MAILBOX_AT_RATE test switch from
b084ead goes, with its load-time line, Declared::EnvAtRate and its test:
it served the mailbox-at-display-rate measurement, which is done. The rule
is as before it: FIFO at a cap at the display's rate (source=cap-rate).
- icd_shim/src/proxy.rs: ModeLine and MODE_LINE; mode_census prints the
short line on a change and the full line behind vkshim_enabled();
choose_live_mode without the switch; the one-shot invalid-value line.
- icd_shim/src/lib.rs: the switch's load-time call removed.
- docs/10, docs/15: the census paragraphs.
Payload 20261103.3 is 20261102.4 with only lib/libadrenotools_gamescope.so replaced (cfe96ac2, build string 202611030510c9b7c31), the shim from GameNative/gamenative-drm#46 at c9b7c31: the same present-mode rule as 1b708d5, with the shim's logging gated for shipping. The present-mode line prints once when frames start and again when the declared mode, source, cap or gate changes; its 10 s repeat with the counters needs GNDRM_LOG_VKSHIM=1. The mailbox-at-display-rate test switch is gone. gamescope, Xwayland and libgamenative_drm.so are unchanged.
…play's vsync grid (payload 20261103.1) (#2327) * gamescope: the present mode follows the container, the cap on the display's vsync grid, payload 20261103.1 libgamenative_drm.so is rebuilt (08d6276c) from gamenative-drm shim-present-mode-from-container (#46) at a4a7bb0: the daemon's vsync note carries the flip events' stamp lead, which the shim needs before it gates a cap on the display's real vblank grid. Daemon sources are unchanged since 46ccc70. Payload 20261103.1 is 20261102.4 with only lib/libadrenotools_gamescope.so replaced (fa50add9), the shim from the same head: on the gamescope route it declares the container's present mode; a non-FIFO mode under a cap at the display's rate is declared FIFO with the vblank-grid gate kept, and a cap below the display's rate stays mailbox, enforced by the gate on the real vblank grid instead of gamescope's divisor limiter. gamescope and Xwayland are unchanged. * gamescope: payload 20261103.2, shim from gamenative-drm#46 at 1b708d5 Payload 20261103.2 is 20261102.4 with only lib/libadrenotools_gamescope.so replaced (a77a7b77), the shim from GameNative/gamenative-drm#46 at 1b708d5: with no cap, a guest's FIFO request now wins over the container's default mode. gamescope and Xwayland are unchanged; libgamenative_drm.so is unchanged (the daemon sources have not changed since 46ccc70). * gamescope: payload 20261103.3, shim from gamenative-drm#46 at c9b7c31 Payload 20261103.3 is 20261102.4 with only lib/libadrenotools_gamescope.so replaced (cfe96ac2, build string 202611030510c9b7c31), the shim from GameNative/gamenative-drm#46 at c9b7c31: the same present-mode rule as 1b708d5, with the shim's logging gated for shipping. The present-mode line prints once when frames start and again when the declared mode, source, cap or gate changes; its 10 s repeat with the counters needs GNDRM_LOG_VKSHIM=1. The mailbox-at-display-rate test switch is gone. gamescope, Xwayland and libgamenative_drm.so are unchanged. --------- Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
|
@joshuatam your capture reproduced a real regression. Thanks for taking it. What it was. In the cap-rate cell (a non-FIFO container under a cap at the display's rate), #46 declared FIFO and kept the vblank-grid acquire gate. FIFO's back-pressure and the gate stacked. Once your scene's acquire-to-commit grew past a period after the live 120 to 60 Hz switch (24 ms, against 7-16 ms at 120 Hz), every start was late against a grid the guest could no longer meet, and the session settled at 40 fps. Your counters show it:
Our RP6 runs of that cell stayed near 12 ms, so they never crossed the period. Fix: 2567925.
88 unit tests pass, and clippy is clean. Payload 20261103.4 will carry it. |
A demonstration for discussion. On gamescope's override route the shim now declares the container's present mode, or the guest's mode when there is no env. There are two exceptions:
source=cap-rate gate=off), which is master's behaviour in that cell. gamescope latches one frame per vblank and its limiter (divisor 1) paces the guest.Below the display's rate a non-FIFO mode stays as it is and the shim's gate enforces the cap on the display's real vblank grid; an env of fifo is master's path unchanged.
The daemon change (stamp lead in the
VsyncNote) is required for the gate. A shim-only payload behaves exactly like master: until the lead arrives, a capped non-FIFO swapchain is declared FIFO under gamescope's limiter (source=cap-compat).Problem
7bdcbae made the shim declare FIFO to gamescope whenever a cap is set, and it stopped reading
MESA_VK_WSI_PRESENT_MODEon the override route. The idea was to let gamescope's divisor limiter enforce the cap. The cost:MESA_VK_WSI_PRESENT_MODE=mailbox. So every default session runs FIFO under gamescope's limiter.Before 7bdcbae, mailbox was declared and the shim's acquire gate enforced the cap. That gate ran on its own 1e9/N clock, which drifted against a 59.97 Hz panel and gave 57-59 fps on SurfaceFlinger at cap 60. That drift is why 7bdcbae moved to FIFO.
What changes
The declared mode now follows the container, with one exception: a cap at the display's rate. When the container's mode is not FIFO, the shim enforces the cap with a gate that releases on the display's real vblank grid.
source=guest)source=cap-rate)gate=off), as on mastersource=cap-compat)Why the guest's FIFO wins with no cap. Every container carries
MESA_VK_WSI_PRESENT_MODE=mailboxas a stamped default that users did not choose. With no cap, honouring that default turned a game's own VSync (a FIFO request) into an uncapped mailbox session, which is neither master's behaviour nor what the user asked for.fifois explicit and wins in every case.Why the display-rate cap declares FIFO instead of mailbox. At that cap the panel shows one frame per vblank under either mode. Under mailbox, though, the shim has to keep two commits out of one gamescope composite, and it cannot see when gamescope latches (see "Finding" below). On the RP6 (DTD, cap 60, VSync off, a guest pacing itself at ~59.3 fps on a 59.98 Hz panel):
Under FIFO, gamescope latches one commit per vblank at its own wakeup, and its limiter's divisor at this cap is 1. No gate runs on top of it (2567925): FIFO's back-pressure already holds the guest, and the two stacked produced the regression below. Below the display's rate, mailbox with the gate stays, because the releases are at least two vblanks apart; Witcher 3 was not quantized there, whereas FIFO with gamescope's limiter quantized it to 30 at 60 on 120 Hz.
fifo_relaxedandimmediateare gated like mailbox, because gamescope's limiter holds only FIFO declarations (wlserver_surface_is_fifo).icd_shim/src/vsync_gate.rs): releases land on the daemon'sVsyncNotelatch grid (measured period and phase), moved earlier by the lead the daemon stamps flip events with. That is the grid gamescope composites from.stamp_lead_nshas arrived (no note yet, or an older daemon), a capped swapchain whose mode is not FIFO is declared FIFO, and gamescope's limiter is the cap, exactly as on master. The census showssource=cap-compat. So a payload-only rollout keeps today's pacing until the app ships the new daemon.VsyncNote's reserved word now carriesstamp_lead_ns, the flip-event lead. The wire size is unchanged. The daemon sends at least 1 ns while it stamps on a grid, so 0 always means "unknown". An older daemon sends 0, and the shim then falls back to FIFO under a cap (see above). An older shim ignores the field.present mode: declared= requested= source=env|guest|cap-rate|cap-compat route= cap= gate=vsync|clock|off lsfg=, whererequested=is the guest's own request.GNDRM_LOG_VKSHIM=1it also repeats every 10 s with the counters:acquires= commits= declared_fifo= declared_other= releases= feedback= presented= discarded= fallback= gate_holds= gate_late= skipped= slot_waits= commit_lat_us= spacing=[…].What is unchanged
Commits
daemon: the vsync note carries the flip events' stamp leadshim: an acquire gate on the display's vblank grid, for caps gamescope's limiter does not hold. Nothing takes this path until commit 3.shim: the present mode follows the container on the gamescope route; a cap no longer forces FIFOdocs: ...(docs/10, docs/15, README)shim: no vblank-grid gate without the daemon's stamp lead; declare FIFO under a cap until it arrives. This follows from the RP6 run below.shim: book a late frame to the composite it will make, …(9fb3cda), later backed out byshim: back out the latency booking of late frames; keep its counters(93ab33c).shim: one commit per gamescope composite under the vblank-grid gate(8cf5834), later backed out byshim: back out the commit window; keep the commit-spacing histogram(476aa3e).shim: declare FIFO under a cap at the display's rate, with the vblank-grid gate kept(ce1f919).shim: test switch to keep mailbox at a cap at the display's rate (debug.gndrm.mailbox_at_rate)(b084ead). This is a test flag only; the default rule is unchanged. With the Android propertydebug.gndrm.mailbox_at_rate=1, or the envGNDRM_MAILBOX_AT_RATE=1when the property is unset, a cap at the display's rate declares the swapchain's mode with the vblank-grid gate kept. The census then readssource=env-at-rate, and the switch's value is logged once after the ICD loaded line. Artefact:libadrenotools_gamescope.sosha25633904656a669efc0ae66ddba4c487c03907eefdc649b4392711eed11bdce5f1d, build string202611030213b084ead. 87 shim unit tests pass and clippy is clean.shim: a clock-booked frame leaves the grid schedule an ideal instant it can use(47c11aa). This fixes the review blocker: after a booking on the clock, a cap below the display's rate stopped binding. A test now covers both clock-to-grid transitions.shim, docs: gate_late in the census, the present-mode env as the user's control, docs/15 and docs/17 as the code is(a4a7bb0).shim: with no cap, a guest's FIFO request wins over the container's default mode(1b708d5).shim: the present-mode line ungated at start and on a change, its counters behind GNDRM_LOG_VKSHIM; the mailbox-at-rate test switch removed(c9b7c31). This removes b084ead's switch; the measurement it served (H1, H2) is done.shim: no vblank-grid gate under the cap-rate FIFO declaration; a late start is released at the next grid instant(2567925). This fixes the regression in JT's capture (below).Tests
cargo test -p adrenotools_gamescope --lib(Linux aarch64 container): 88 passed, 0 failed, at 1b708d5. The rule-table test now has the guest's request as a column.cargo test --workspace --no-fail-fast(same container, lld): 305 passed. The one failure wasshim_integrationa_native_driver_never_drives_a_container_icd, the pre-existing flake; that suite passed 3/3 on rerun.cap-rate, gate off;cap-compat;choose_live_modeand the gate per declaration;cargo test -p gndrm-uapi -p gndrm-android -p gndrm-daemon: 182 passed. The note round-trip, an old note decoding as 0, and the stamp-lead push rule are new. The twoabi_ground_truthC-header probes did not run because the test container has nodrm/drm.h; this PR does not touch them.cargo clippy --target aarch64-linux-android -p adrenotools_gamescope -p gndrm-android -p gndrm-daemon --all-targets -- -D warnings: clean.Artefacts
Release aarch64 builds, NDK 27.3:
libadrenotools_gamescope.sofrom 2567925: sha2566355ee9ecdd1d8d8d6e6128d5e06c68536c085bebeaaaa28e70af517b2af9035, build string2026110307122567925. It is the cap-rate regression fix, with the logging as in c9b7c31. The device results above were measured on ce1f919 (sha256b4bff42a…297c) and, for S1, on a4a7bb0. The N1-N4 results were measured on 1b708d5: Witcher 3 at 120 Hz (N3), env fifo (N2) and the no-cap rule (N1) are verified on it.libgamenative_drm.sofrom 46ccc70 (unchanged since): sha25608d6276c268ed9ff169a0cc76f059bf7e30dec2288f13617bfa64c6f4ff407f6App delivery for testing: utkarshdalal/GameNative#2327 (branch
gamescope-present-mode-from-container).gamescope-gamenative-20261103.4.tzst, sha2565126c4ee8f042a69e287a3bde16e3be5a79de0cc052aaeadff5554b6a09fe7b9. It is 20261102.4 with onlylib/libadrenotools_gamescope.soreplaced (the 2567925 build above); gamescope and Xwayland are unchanged. App commit 40dd84163.cfe96ac2…213e), the one in JT's capture, stays on the CDN.a77a7b77…) was used for N4 and stays on the CDN.08d6276c…07f6above andGamescopePayload.VERSION=20261103.2.Gamescope payload 20261103.2 readyandgn-vkshim: ICD loaded [shim 202611030337 1b708d5].The daemon changed, so the vblank-grid gate needs an app build. With only the new shim in a payload, a capped mailbox container is declared FIFO, the same as master.
Regression found and fixed (2567925)
@joshuatam's capture found it: payload 20261103.3, shim c9b7c31, a 1280x960 device, DTD in game, cap 60, env mailbox, in-game VSync on.
declared=mailbox source=env gate=vsyncheld 60 fps, with acquire-to-commit around 7-16 ms.declared=fifo requested=fifo source=cap-rate cap=60 gate=vsync, and within ~10 s the game settled at 40 fps:late=0;gate_holds0,gate_late400/400,skipped+200 per 10 s;Cause. FIFO's back-pressure and the vblank-grid gate stacked in the cap-rate cell. Once a frame took longer than a period from acquire to commit, every start was late against a grid the guest could no longer meet. The RP6 runs of this cell (B7, S1, N4) did not show it because their scene stayed near 12 ms.
Fix (2567925).
source=cap-rate gate=off), which is master's behaviour in that cell.Payload 20261103.4 carries it (sha256
5126c4ee…e7b9, utkarshdalal/GameNative#2327 at 40dd84163).Device verification (Retroid Pocket 6, cap 60)
All runs are on the DTD title scene unless noted. The FIFO reference is master's shipped shim on the same scene.
requested=fifo), new shim + new daemon, 59.983 Hzlate=0. Equal to the FIFO reference.stamp_lead_ns= 0)late=818(13 %). Without the lead the shim now uses master's FIFO path (cap-compat).requested=immediate), 1ffd476 (mailbox + gate)late=0, so ~1 commit/s was merged in gamescope's mailbox.declared=fifo requested=immediate source=cap-rate route=gamescope cap=60 gate=vsync,declared_other=0. Shim commits 592-594 per 10 s against daemon publishes 591-594 per 600 vsyncs, so the B4-B6 gap is gone. SurfaceFlinger over 60 s: 59.199 fps, 46 doubles (1.3 %), none short. FIFO reference: 59.22 fps, 46 doubles/min.slot_waits=0, daemonlate=0, margin p50 +3.4 ms, lead 1.6-2.2 ms. The remaining shortfall is the game's own ~59.3 fps self-pacing.MESA_VK_WSI_PRESENT_MODE=fiforequested=fifo), a4a7bb0 with the default rule, shipped gamescope, cap 60, 59.98 Hzdeclared=fifo requested=fifo source=cap-rate cap=60 gate=vsync. SurfaceFlinger over 60 s: 59.978 fps, 3598 single intervals, 0 doubles. Commits 599-600 per 10 s, equal to the daemon's publishes;slot_waits=0. B1 reference: 59.983 fps, 0 doubles.requested=fifo), 1b708d5 + 46ccc70 daemon, shipped gamescope, 60 Hzdeclared=fifo requested=fifo source=guest route=gamescope cap=0 gate=off. SurfaceFlinger 60 s: 59.973 fps, 3598 single intervals, 0 doubles. Commits +600 per 10 s, equal to the daemon's 600/600 publishes,late=0.presentedtracks commits (9942/9943);slot_waits=0.MESA_VK_WSI_PRESENT_MODE=fifo, cap 60, VSync on, 1b708d5, 60 Hzdeclared=fifo requested=fifo source=env cap=60 gate=off. SurfaceFlinger 59.980 fps, 3598 single intervals, 0 doubles. Commits equal publishes, 600/600.declared=mailbox requested=fifo source=env cap=60 gate=vsync. SurfaceFlinger 32.624 fps (per-second median 33, p5 32). Intervals in 120 Hz periods: 2 = 1.1 %, 3 = 33.5 %, 4 = 61.7 %, 5 = 3.5 %, 6 = 0.2 % (C1: 3 = 25.1 %, 4 = 70.6 %; C2 FIFO: 2 = 8.6 %, 3 = 7.4 %, 4 = 75.3 %, 5 = 7.3 %). Shim commits 319-331 per 10 s, equal to the daemon's 319-332 publishes per ~1200 vsyncs.commit_lat_us~30-31.5 ms;slot_waits=0. See the N3 observation below.Gamescope payload 20261103.2 ready), with the stamp and shim sha (a77a7b77…) as expected;ICD loaded [shim 202611030337 1b708d5]. The file tree before and after differed only in the stamp and the shim. Censusdeclared=fifo requested=fifo source=cap-rate cap=60 gate=vsync. SurfaceFlinger 59.966 fps, 3596 single intervals, 1 double. Commits equal publishes.Observation for review (S1). During S1 the shim alternated between two steady states, and SurfaceFlinger's output was identical in both:
commit_lat_usgate_holds+600,gate_late+11-16gate_holds0,gate_late+600It switched twice over 3 minutes; the census lines at 08:23:37 and 08:25:07 are the transitions. The guest's frame lands either just before its release point or just after it:
Under FIFO with the gate, the panel output does not change. The two states could still differ in input-to-display latency by up to a frame.
@joshuatam, an open question rather than a proposed change: should the gate treat a frame that arrives within a small window after its release point as on time, as the earlier
ON_TIME_NStolerance did for counting? Or is this alternation harmless?Observation for review (N3). On the below-rate mailbox path at 120 Hz, the daemon logged
late=77-175per ~1200 vsyncs:@joshuatam, another open question: should the gate's release point on a 120 Hz grid carry a larger lead?
| H1 | DTD, VSync off (
requested=immediate), b084ead withdebug.gndrm.mailbox_at_rate=1(mailbox + gate at the display's rate) and gamescope A58 (GameNative/gamescope#31,debug.gamescope.gn_mailbox_hold=1), 59.95 Hz | Pass. Censusdeclared=mailbox requested=immediate source=env-at-rate cap=60 gate=vsync,declared_fifo=0. Shim commits 592-594 per 10 s against daemon publishes 592-594, so nothing is merged. SurfaceFlinger over 60 s: 59.269 fps, 41/3556 doubles (1.2 %), against the FIFO reference's 59.22 / 1.3 % and B4-B6's 58.3-59.0 / 2.7-2.9 %.slot_waits=0. gamescopegn_mailbox_hold=1 source=prop, steady stateheld=38-164 replaced=0per 600 vblanks (5 replaced in the whole run, all during loading). || H2 | DTD, in-game VSync on (
requested=fifo), b084ead +debug.gndrm.mailbox_at_rate=1+ gamescope A58 52e1b1f, 59.98 Hz | Pass on rate, with one caveat. Censusdeclared=mailbox requested=fifo source=env-at-rate cap=60 gate=vsync. SurfaceFlinger over 60 s: 59.984 fps, 3599 singles, 0 doubles. Shim +600 acquires/commits/releases per 10 s,gate_holds+600,late0-4,skipped0,slot_waits0. Daemon 600/600,late=0, margin p50 +3.45 ms, lead ~2.2 ms. Caveat: gamescope'sheld=1200per 600 vblanks means every commit was shown one paint late, a standing carry. GameNative/gamescope#31 now bounds it (9fad070,gn_mailbox_hold_max_carry, default 8); a re-run is pending. |Finding: the shim cannot see gamescope's composite point under mailbox (B5, B6)
Run. DTD, cap 60, in-game VSync off (
requested=immediate), 59.98 Hz panel. Results:late=0. So about 0.8 commits per second are merged inside gamescope's mailbox: two commits land between two gamescope wakeups.win_holds18-48 per 10 s,win_replaced=0) removed only a few of those merges.skipped(7-8 per 10 s) is not a lost frame. The schedule only counts a skip when an acquire arrives a whole period after its release point. That count matches the guest's own shortfall, 600 − 593 per 10 s. The gate holds none of those frames.Why measuring from the client cannot fix this. It comes from gamescope's own timing; line numbers are from gamescope
src/:rolling draw time + red zone(vblankmanager.cpp142-199).DRMBackend.cpp4465). On this stack that includes the commit to the daemon, so it is spiky.presentedcarrying the target vblank (steamcompmgr.cpp8140, 8168) anddiscarded. Each presented frame gives one inequality on the latch point.desired_present_timetargets (steamcompmgr.cpp8040) runs into the same band.steamcompmgr.cpp8028-8049).Outcome. ce1f919 implements this: at the display's rate it declares FIFO and keeps the vblank-grid gate; below the display's rate it keeps mailbox with the gate. The commit window (8cf5834) is backed out.
With gamescope A58
GameNative/gamescope#31 (A58, draft) moves the hold that B6 attempted into gamescope, where the latch point is known. While gamescope's FPS limiter is engaged, the first non-FIFO commit for a paint is latched as today, a newer one is held for the next vblank, and a third replaces the held one. The client is never blocked, and FIFO commits are untouched.
source=cap-rate). Thedebug.gndrm.mailbox_at_ratetest switch that H1 and H2 used (b084ead) is removed in c9b7c31.@joshuatam, I'd value your read on two things: