#pragma mark DisplayLink - RTP PTS timestamp-based frame pacing (experimental) // TODO: handle presentationTimeUs rollover every 13 hours // This frame pacing method attempts to use timestamps from Sunshine to determine when // frames should be displayed. Sunshine v2025.600+ required. - (void)framePacingUsingTimestamps:(CADisplayLink *)link { // All times are in seconds static CFTimeInterval anchorLocal = 0.0f; static CMTime anchorHost; static CFTimeInterval lastTargetLocal = 0.0f; CFTimeInterval start = link.timestamp; CFTimeInterval deadline = link.targetTimestamp; _displayRefreshRate = 1.0f / (deadline - start); [self checkDisplayLayer]; Frame *frame = nil; CFTimeInterval dl0 = CACurrentMediaTime(); static CFTimeInterval avgOverhead = 0.004f; // averaged each callback CFTimeInterval waitFor = deadline - dl0 - avgOverhead; if (waitFor < 0.001f) { waitFor = 0.001f; } // Process the next frame for display frame = [_frameQueue dequeueWithTimeout:waitFor]; if (frame) { CFTimeInterval now = CACurrentMediaTime(); // during stream startup, wait and let the first frames through untouched if (!_seenFrameOne) { Log(LOG_I, @"Frame pacing (pts mode): target %f Hz with %d FPS stream", _displayRefreshRate, self->frameRate); _seenFrameOne = YES; // Just let frame 1 through, we don't want to anchor to it Log(LOG_I, @"[%.3f] rendering frame %d / %.3f, queue size %d", deadline, frame.frameNumber, frame.pts, [_frameQueue count]); [self renderFrame:frame atTime:frame.pts90]; return; } if (!CMTIME_IS_VALID(anchorHost)) { // Use this frame as the anchor point. This is normally frame 2. anchorHost = frame.pts90; anchorLocal = now; Log(LOG_I, @"Setting anchor point: anchorHost=%.3f == anchorLocal=%.3f", CMTimeGetSeconds(anchorHost), anchorLocal); } //CMTime targetTime = CMTimeSubtract(frame.pts90, frameQueue.ptsCorrection); // total of all dropped frames CMTime targetTime = frame.pts90; CFTimeInterval targetLocal = CMTimeGetSeconds(targetTime); #ifdef DISPLAYLINK_VERBOSE // iOS double-buffering renders the frame we queue here during the interval +2 from now CMTime hostDelta = CMTimeSubtract(frame.pts90, anchorHost); CFTimeInterval hostDeltaF = CMTimeGetSeconds(hostDelta); CMTime drift = CMTimeSubtract(hostDelta, CMTimeMakeWithSeconds(now - anchorLocal, NSEC_PER_SEC)); Log(LOG_I, @"[%.3f] rendering frame %d / %.3f @ %.3f, drift %.3f, queue size %d", deadline - anchorLocal, frame.frameNumber, hostDeltaF, targetLocal, CMTimeGetSeconds(drift), [_frameQueue count]); #endif // Schedule the frame at its original timestamp, adjusted by a correction factor (total duration of all dropped frames) [self renderFrame:frame atTime:targetTime]; // Update metrics if (lastTargetLocal != 0) { [self->_callbacks observeFloat:PLOT_FRAMETIME value:(targetLocal - lastTargetLocal) * 1000.0]; } lastTargetLocal = targetLocal; // weighted moving average of how much time displayLink needs after dequeuing a frame. // This is used to avoid overshooting a vsync by waiting too long. const double alpha = 0.1f; avgOverhead = ((CACurrentMediaTime() - dl0) * alpha) + (avgOverhead * (1.0 - alpha)); } // are we returning too late? CFTimeInterval dl2 = CACurrentMediaTime(); if (dl2 > deadline) { Log(LOG_W, @"*** returned past deadline by %.3f ms (%f > %f)", (dl2 - deadline) * 1000.0, dl2, deadline); } }