frameclock: resolve each frame from its own present, and plan for when frames are really shown - #50
Merged
Merged
Conversation
…n frames are really shown A host whose frames reach the screen some refreshes after they are submitted — a GPU renderer, where the compositor shows a drawable after its work finishes — could not tell frameclock what its frames did. `FrameSubmission::deferred` resolves a frame with the next tick's `prev_actual_present`, which is the refresh that just passed, not that frame's own present, and only one frame can be waiting at a time. Three changes make the loop true for such hosts: - `FrameSubmission::reported` submits a frame the host will report later. `FrameDriver::report_frame(frame_index, FrameReport)` resolves it from its own measured present, in any order, up to `REPORTED_FRAME_CAPACITY` frames deep; a submission past that resolves the oldest commit-only, as a superseding deferred submission does. `FrameReport::work` says what the submission cost beyond the host's own span — the duration of the GPU work it queued, not when that finished, since a frame queued behind others waits and waiting is not what the frame costs. Build cost is the longer of the two, so the scheduler sees a frame that cannot hold the display's refresh. - `SchedulerConfig::present_latency_alpha` learns how late frames are really shown and moves `FramePlan::target_present`, and so what the frame is sampled for, by whole refresh intervals. Without it the only answer to a late frame is deeper pipeline lookahead, which plans fewer frames rather than better ones: a 120 fps scene fell to about 12 fps once its real presents were reported. `FramePlan::present_latency` carries what a plan applied so feedback corrects the estimate instead of compounding it. - A frame is late when it is shown more than half a refresh past its target. The timestamp for the refresh a frame was planned for lands a little after that target, so a strict comparison called nearly every frame late. `frameclock_apple` exposes `media_time_to_host_time`, which turns the Core Animation media times that Metal reports (a drawable's `presentedTime`, a command buffer's GPU times) into `HostTime`, and says plainly that a display link's timestamp is the last refresh rather than the app's own present. Measured in Lightweald at 3456x2104 on a 120 Hz display: frames are now sampled for 1.8-2.5 ms from when they are shown, against 16.7-33 ms early before, with no frame shown late over 300-frame spans and no change in frame rate or latency.
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.
A host whose frames reach the screen some refreshes after they are submitted — a GPU renderer, where the compositor shows a drawable once its work finishes — cannot tell frameclock what its frames did.
FrameSubmission::deferredresolves a frame with the next tick'sprev_actual_present, which is the refresh that just passed rather than that frame's own present, and only one frame can be waiting at a time.Found while pacing Lightweald's Metal samples with
FrameDriverover aCADisplayLink: those frames reach the screen two to four refreshes after the tick that started them, and fed the tick's timestamp the driver's missed-deadline count swung between 0 and 246 of 300 frames on one scene.Changes
Late, per-frame reports.
FrameSubmission::reportedsubmits a frame the host will report later;FrameDriver::report_frame(frame_index, FrameReport)resolves it from its own measured present, in any order, up toREPORTED_FRAME_CAPACITYframes deep. A submission past that resolves the oldest commit-only, as a superseding deferred submission does. This should suit Wayland'swp_presentationfeedback and swapchain statistics as well as Metal's presented handlers.What a frame costs.
FrameReport::workis what the submission cost beyond the host's own span: the duration of the GPU work it queued, not when that work finished. A frame queued behind others waits, and counting the wait made the scheduler choose a slower cadence, which lengthened the wait again — 120 fps decayed to 20. Build cost is now the longer of the host's span and that work, so the scheduler sees a frame that cannot hold the display's refresh.Presentation-latency compensation.
SchedulerConfig::present_latency_alphalearns how late frames are really shown and movesFramePlan::target_present, and so the time the frame is sampled for, by whole refresh intervals. Without it the only answer to a late frame is deeper pipeline lookahead, which plans fewer frames rather than better ones: with real presents reported, a 120 fps scene fell to about 12 fps.FramePlan::present_latencycarries what a plan applied, so feedback corrects the estimate instead of compounding it.A miss is a refresh, not a tick. The timestamp for the refresh a frame was planned for lands a little after the target, so a strict comparison called nearly every frame late. A frame is late when it is shown more than half a refresh past its target.
frameclock_appleexposesmedia_time_to_host_time, which turns the Core Animation media times Metal reports (a drawable'spresentedTime, a command buffer's GPU times) intoHostTime, and says plainly that a display link's timestamp is the last refresh rather than the app's own present.Measured
Lightweald at 3456x2104 on a 120 Hz display, two frames in flight:
Frame rate and submit-to-screen latency are unchanged: 107-118 fps on one scene, 53-73 fps on a heavier one.
Still open
A scene that cannot hold 120 Hz settles around 54 fps, between the 60 and 40 that divide the refresh: frameclock picks a steady cadence, but the display keeps ticking at 120 Hz, so frames land on alternating refreshes. Asking Core Animation for the chosen cadence ratchets the rate down instead — frameclock derives display timing from the ticks it receives, so slower ticks look like a slower display, and scenes settle at 30 fps. Giving the scheduler the display's real range rather than what the last tick implied looks like the fix; happy to take that on in a follow-up.
Notes
Breaking:
PresentFeedbackandPresentFeedbackEventgain fields (work,present_latency),FramePlangainspresent_latency,SchedulerStategainspresent_latency_ticks, and the trace format recordsworkin place of nothing. Callers in-tree are updated. The Windows examples were updated to buildPresentFeedbackEventwith::new; they are not buildable on macOS, so CI is their first check.