Sync intervals, tested on a screen nobody can see
Present(2), (3) and (4) now show each frame for 2, 3 or 4 refreshes, and the pacing holds even when macOS isn't drawing the window at all.
What changed
A D3D12 game calls Present(SyncInterval = N) to show a frame for N vertical blanks (the moment the display starts a new refresh). Interval 1 is ordinary vsync, 2 halves the frame rate, and 0 means don't wait. The layer only switched CAMetalLayer.displaySyncEnabled on or off, so intervals 2, 3 and 4 all behaved like 1.
Now each drawable (the texture Metal hands out for one frame on screen) is presented with presentAfterMinimumDuration: set to N - 0.5 refresh periods. The half period keeps timing jitter from pushing a frame one refresh too late. The layer also paces the frame itself: the GPU's "finished" signal no longer goes straight to the game. A listener passes it on no earlier than N refresh periods after the previous frame's.
The interesting part
The first version of the new test failed in a strange way: every interval, including 0, took 8.3 ms per frame on a 60 Hz display. The sessions run unattended, with the screen locked. I wrote a small native Cocoa program with no Wine involved and got the same result. The window counts as occluded (hidden behind something else, here the lock screen), so no frame is ever shown: presentedTime is 0 for every drawable. Drawables still come back every 8-10 ms, whatever the sync settings say. Only a visible window is paced by the display.
On Windows, the desktop compositor keeps pacing a covered window at the refresh rate. So the CPU pacing is not just there to help the test. A game minimised behind other windows should not start rendering as fast as it can. It also paces swap chains that don't use DXGI's frame-latency object: Present blocks once three frames are queued, and at interval 2 it now takes 33.35 ms per call.
One cost, found in review: that wait happens while the queue is locked. A game running more than three frames ahead therefore holds its own later submissions for up to three sync intervals. That is DXGI's behaviour at the latency limit, just enforced one step deeper.
Numbers
- Before, screen locked, 60 Hz: 8.30 / 8.44 / 8.31 / 8.35 ms per frame for intervals 1-4.
- After: 16.66 / 33.33 / 50.06 / 66.72 ms, interval 0 at 8.33 ms, and 33.35 ms for a swap chain without the latency object at interval 2 (full run).
- Full run: 775 pass, 27 fail, 108 skip of 910; 0 newly failing, 1 newly passing (api/present_interval).
- Unverified: timing with the screen unlocked and the window visible, where the display and the CPU pacing must agree on the refresh rate.
- before
- after
- expected
Present sync interval · ms per frame
- 08.35 before, 8.33 after, 0 expected
- 18.3 before, 16.66 after, 16.67 expected
- 28.44 before, 33.33 after, 33.33 expected
- 38.31 before, 50.06 after, 50 expected
- 48.35 before, 66.72 after, 66.67 expected
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton455 pass, 18 fail, 104 skip of 577
- vkd3d177 pass, 9 fail, 4 skip of 190
- apps77 pass, 0 fail, 0 skip of 77
- api (+1)62 pass, 0 fail, 0 skip of 62
- samples4 pass, 0 fail, 0 skip of 4