namespace XFESystem.Desktop; /// /// Tick-precision dirty-frame pacing. This measures renderer submissions, never /// monitor scanout or VSync. An idle desktop presents no frames. After an overrun, /// one current frame can run immediately; obsolete deadlines are skipped rather /// than queued, and the original fractional-tick phase is retained. /// public sealed class FramePacer { private readonly long _frequency; private readonly UInt128 _periodNumerator; private long _origin; private long _lastBegin; private long _lastEnd; public FramePacer(long timestampFrequency, int targetMilliHz) { if (timestampFrequency <= 0) throw new ArgumentOutOfRangeException(nameof(timestampFrequency)); if (targetMilliHz <= 0) throw new ArgumentOutOfRangeException(nameof(targetMilliHz)); _periodNumerator = (UInt128)(ulong)timestampFrequency * 1000; if (_periodNumerator < (uint)targetMilliHz) throw new ArgumentOutOfRangeException(nameof(targetMilliHz), "Target exceeds timestamp tick precision"); _frequency = timestampFrequency; TargetMilliHz = targetMilliHz; } public int TargetMilliHz { get; } public long NextDeadlineTicks { get; private set; } public long FramesRendered { get; private set; } public long Overruns { get; private set; } public long LastRenderDurationTicks { get; private set; } /// Observed Rendered call cadence, including idle time; not display refresh. public double ActualRendererFps => FramesRendered < 2 || _lastBegin <= _origin ? 0 : (FramesRendered - 1) * (double)_frequency / (_lastBegin - _origin); public bool ShouldRender(long nowTicks, bool dirty) { if (nowTicks < 0) throw new ArgumentOutOfRangeException(nameof(nowTicks)); return dirty && (FramesRendered == 0 || nowTicks >= NextDeadlineTicks); } public void Rendered(long beginTicks, long endTicks) { if (beginTicks < 0) throw new ArgumentOutOfRangeException(nameof(beginTicks)); if (endTicks < beginTicks) throw new ArgumentOutOfRangeException(nameof(endTicks)); if (FramesRendered != 0 && beginTicks < _lastEnd) throw new ArgumentOutOfRangeException(nameof(beginTicks), "Render timestamps must be monotonic and nonoverlapping"); if (FramesRendered == 0) _origin = beginTicks; _lastBegin = beginTicks; _lastEnd = endTicks; LastRenderDurationTicks = endTicks - beginTicks; if ((UInt128)(ulong)LastRenderDurationTicks * (uint)TargetMilliHz > _periodNumerator && Overruns < long.MaxValue) Overruns++; if (FramesRendered < long.MaxValue) FramesRendered++; UInt128 phaseAtBegin = (UInt128)(ulong)(beginTicks - _origin) * (uint)TargetMilliHz / _periodNumerator; UInt128 phaseAtEnd = (UInt128)(ulong)(endTicks - _origin) * (uint)TargetMilliHz / _periodNumerator; UInt128 nextPhase = phaseAtBegin + 1; if (phaseAtEnd > nextPhase) nextPhase = phaseAtEnd; UInt128 numerator = nextPhase * _periodNumerator; UInt128 elapsedDeadline = (numerator + (uint)TargetMilliHz - 1) / (uint)TargetMilliHz; UInt128 absoluteDeadline = (ulong)_origin + elapsedDeadline; NextDeadlineTicks = absoluteDeadline > long.MaxValue ? long.MaxValue : (long)absoluteDeadline; } }