Repository navigation
Conversation
a897dd5 to
0a4610f
Compare
…n the game's queue only draws it The present command buffer ran the MetalFX spatial scaler (1920x1080 → the fullscreen canvas) on the game's queue at each display-link tick, inside or right after the frame's segments: about 2.6 ms of GPU time a frame at the low clock macOS keeps for Breath of the Wild, which took frames from about 15 to 17–18 ms (48–56 fps). With present snapshots, snap_take now scales the presented rectangle into an upscaled texture of the slot, in the snapshot's command buffer right after the frame's last segment, and the present draws it 1:1 (0.3–0.5 ms). A canvas size or format change remakes the scaler and the slot's texture (the old one stays resident until the slot's next use), and any mismatch takes the old path for that frame. DM_PRESENT_FX_AT_SNAP=0 restores it, ab compares both; DM_PRESENT_UPSCALE takes ab too. @present_gpu_ms and @snap_gpu_ms give both command buffers' GPU time per frame. M5 Pro run, the two placements switched every 30 s: the present 2.58–2.66 → 0.34–0.52 ms; the user reports a cleaner picture. t_present_snap fxsnap (both encode modes, Metal validation): exact scaled pictures, a refused resized or reformatted canvas, a canvas change remaking the scaler, residency of the replaced textures.
…it in variable-refresh fullscreen Breath of the Wild in fullscreen on a ProMotion Mac (M5 Pro): at one frame of a session (87, 251, 342 and 355 s in four of them) the system starts showing every frame exactly 4.17 ms (1/240 s) after the display link's target and keeps that offset. Nothing on the host side changes at that frame. From then on the link calls back a tick late about every 11-12 frames: 56 fps on the 60 Hz link, 58.6 on the 120 Hz link at frame latency 2 and 1 alike. A display link made again keeps the offset. Without the link (mode 0, the present thread presenting each frame for its period) the game held 60.03 fps for 15 minutes at game time 1.000. - link_late_note checks each frame's presentation time against its link target. When 120 presents in a row were three quarters or more 2 ms or more late, a 60 fps game leaves the link for mode 0, where it fits (link_leave_ok): fullscreen on a variable-refresh screen, at any rate. Fixed-rate screens and windows keep the link; leaving those conditions takes pacing back to it. DM_PRESENT_RESYNC=0 turns the switch off. - Each move of the frames' offset against the link's targets is logged with its first present, the callback's time after the flip two frames back, the screen's rate and refresh kind, fullscreen, latency and pacing mode (the first 8, then a count a minute). - DM_NVN_TSV gains where the link callback that took each frame spent its time: @link_slack_ms (update.targetTimestamp minus the present call), @link_wait_ms (snapshot and external-producer waits), @link_alloc_ms (present_now's allocator wait), @link_run_ms (entry to exit), @link_idle_ms (since the previous callback exited), @link_held_ms (the longest callback that took no frame since the previous present), @link_lock_ms (the wait for g_pq_mu). On 200 late frames on the 120 Hz link our callbacks ran 0.16 ms (max 0.5) with no wait, each present came 8.1 ms before its deadline, and the link thread sat idle 20.7 ms before each late callback where it waits 8.33 ms otherwise: the system called back late, not the host. - The link callback's hold counts the link's own ticks (link_hold_s): a frame stays on screen for its whole period when that covers two or more ticks. The fixed "period > 25 ms" let every tick of the 120 Hz link (DM_PRESENT_LINK=2) take a 60 fps frame, and with present snapshots the game ran at 87 fps and 1.16 of real time. The 60 and 30 Hz links hold exactly as before. t_link_ticks 48 checks (the hold on 120, 60 and 30 Hz and virtual ticks; the shift judge; link_leave_ok; the offset tracker); t_present_rescue --metal; check_t_present_snap. A replay of the judge and the tracker over four sessions' frame rows dated the shift to the frame found by hand and fired where 56 fps began. Not checked: the switch itself in a run; external screens.
Donkey Kong Bananza in fullscreen on a variable-refresh screen: the screen's 1/240 s shift came while the game had set interval 2, on the 30 Hz link. The switch that leaves the link took only 60 fps games, so frames came 33 and 58 ms apart, 26-27 fps for 33 s, until the game went back to interval 1 and pacing left the link at 60. - link_late_note asks to leave the link at any frame rate: mode 0 presents each frame for its period, 30 and 60 alike. - link_sync no longer takes pacing back to the link when the rate changes after the move; only a screen or window that no longer fits does. - t_present_rescue runs the handoff at 60 and at 30 fps and checks that a change of rate keeps mode 0 (6 checks fail without the change).
0a4610f to
ab0e4ef
Compare
|
Independent data point for the display-link shift, on another game and chip: Ocarina of Time (0400486024DEC000 v131072), MacBook Pro M4 Pro (Mac16,8), built-in ProMotion screen, macOS 27.0.1, fullscreen ( In a 38-minute session, long stretches had every 11th frame at 25 ms (three 120 Hz ticks), frame p95 about 24.7 ms and 27–30 frames dropped per 300 on screen: about 56 fps. During those stretches the CPU was near idle (106–148 % of one core,
The two short sessions include area loads. I'll add a longer |
Split out of #930 (4 of 4). This one has two commits. The display-link commit is built on the MetalFX one (both change the present path in
nvn_mtl.m), so they go together.Summary
1. MetalFX scales the picture at the snapshot. The MetalFX scaler ran on the game's queue at each display-link tick, between or right after the frame's segments, and cost about 2.6 ms of every frame. With present snapshots it now runs in the snapshot's command buffer, right after the frame's last segment, and the present only draws the result 1:1.
On by default on the Mac and off on iOS, where it hasn't been measured (
DM_PRESENT_FX_AT_SNAP=1turns it on).DM_PRESENT_FX_AT_SNAP=0restores the old placement, andabcompares the two.@present_gpu_msand@snap_gpu_msgive each command buffer's GPU time.2. Pacing leaves the display link when the screen shifts under it. At some point in a fullscreen session on a ProMotion Mac, macOS starts showing every frame 1/240 s after the display link's target. From then on the link calls back a tick late about every 12 frames (56 fps). Presenting without the link (mode 0) holds 60.
Also in this commit:
@link_*timing rows for the link callback, and a hold fix. A 60 fps frame on the 120 Hz link (DM_PRESENT_LINK=2) is now held for two ticks; before, the game ran at 87 fps and 1.16× speed there.DM_PRESENT_RESYNC=0keeps the link.Related: #603 (Mario Kart 8 Deluxe:
DM_PRESENT_LINK=0removes the same late presents on an M3 Max), #347 / #348, #822.Evidence
Breath of the Wild 1.9.0, MacBook Pro M5 Pro, macOS 27.2, built-in ProMotion screen, fullscreen.
MetalFX. Before: the present cost 2.58–2.66 ms of GPU time on the game's queue (48–56 fps in busy scenes).
After: 0.34–0.52 ms, measured in the same run with the placement switched every 30 s.
Display link. The shift started at 87, 251, 342 and 355 s in four sessions.
In run 11 the host switched about 2 s after the shift, with no freeze. Timing rows on 200 late frames: our callback ran 0.16 ms (max 0.5) with no waits, and the link thread sat idle 20.7 ms before each late callback instead of 8.33 ms. So the lateness comes from macOS, not from our code.
Tests:
t_link_ticks48,t_present_rescue --metal86,check_t_present_snap23 checks.Builds and tests on its own on current
main: host and CPU gfx tests, sanitizers, tool tests, clang-tidy and ruff/pyright pass locally.Merge Danger
Door: two-way.
DM_PRESENT_FX_AT_SNAP=0andDM_PRESENT_RESYNC=0turn each part off.Blast Radius: Mac presentation.
DM_PRESENT_LINK=2keep the current behaviour.DM_PRESENT_LINK=2.docs/reference/frame-rate.md).regress.py impact: Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom are reached, and Hades through the present rule. None of them is on this machine, soregress.py check --gameswas not run.Update: rebased on main, and the shift at 30 fps
This branch is rebased on today's main. The conflicts with the probe-ramp change (now on main) are resolved, so
queuedandfxsnapboth run int_present_snap.A new commit makes pacing leave the shifted display link at 30 fps too. Before, the leave condition required
mtl_fps_target() == 60. Donkey Kong Bananza shifted during an interval-2 scene on the 30 Hz link and showed 33 and 58 ms frames, 26-27 fps for 33 s. Mode 0 now presents each frame for its period at 60 and at 30 fps alike, and a change of rate no longer brings pacing back to the link.t_present_rescue: the hand-off now runs at 60 and at 30 fps. Without the fix 6 checks fail; with it, 100 pass.t_present_snap: 166 checks pass in both encode modes, with Metal validation.