namespace XFESystem.Desktop;
/// <summary>
/// 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.
/// </summary>
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; }
/// <summary>Observed Rendered call cadence, including idle time; not display refresh.</summary>
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;
}
}
namespace XFESystem.Desktop;
/// <summary>
/// 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.
/// </summary>
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; }
/// <summary>Observed Rendered call cadence, including idle time; not display refresh.</summary>
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;
}
}