Replace scheduler-quantized software sleeps with a reusable Windows high-resolution deadline timer, expose pacing in the frame profiler, and make shutdown wake every persistent mesh worker without losing the shared signal. Preserve retail alpha order while using a stable radix, skip duplicate deferred-alpha SSBO packing, pack light sets, cache static selection descriptors, and retire historical material groups at the whole-frame boundary. The fixed dense-Caul sample improved from roughly 9-12 ms CPU to 5.3-6.2 ms without reducing visual quality. Release build succeeds with zero warnings and all 6,300 tests pass with five intentional skips. Three independent retail, architecture, and adversarial reviews are clean; the post-review connected route remains pending because local ACE is offline. Co-authored-by: OpenAI Codex <codex@openai.com>
154 lines
4.4 KiB
C#
154 lines
4.4 KiB
C#
using System.Diagnostics;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Deadline-based software frame pacer used only when the user disables
|
|
/// VSync. VSync-on presentation waits in the driver's buffer swap instead.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Silk.NET's <c>FramesPerSecond</c> gate still runs its outer loop as a busy
|
|
/// poll. This owner performs a real thread wait, bounding both render work and
|
|
/// CPU use. A missed deadline is rebased from the current time so one slow
|
|
/// frame cannot trigger a burst of catch-up frames.
|
|
/// </remarks>
|
|
internal sealed class FramePacingController : IDisposable
|
|
{
|
|
private readonly IFramePacingClock _clock;
|
|
private readonly IFramePacingWaiter _waiter;
|
|
private readonly bool _ownsWaiter;
|
|
|
|
private FramePacingPolicy _policy;
|
|
private long _periodTicks;
|
|
private long _nextDeadline;
|
|
private bool _hasDeadline;
|
|
private bool _disposed;
|
|
|
|
public FramePacingController()
|
|
: this(
|
|
StopwatchFramePacingClock.Instance,
|
|
WindowsHighResolutionFramePacingWaiter.Create(),
|
|
ownsWaiter: true)
|
|
{
|
|
}
|
|
|
|
internal FramePacingController(
|
|
IFramePacingClock clock,
|
|
IFramePacingWaiter waiter)
|
|
: this(clock, waiter, ownsWaiter: false)
|
|
{
|
|
}
|
|
|
|
internal FramePacingController(
|
|
IFramePacingClock clock,
|
|
IFramePacingWaiter waiter,
|
|
bool ownsWaiter)
|
|
{
|
|
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
|
_waiter = waiter ?? throw new ArgumentNullException(nameof(waiter));
|
|
_ownsWaiter = ownsWaiter;
|
|
if (_clock.Frequency <= 0)
|
|
throw new ArgumentOutOfRangeException(nameof(clock), "Clock frequency must be positive.");
|
|
}
|
|
|
|
internal FramePacingPolicy Policy => _policy;
|
|
|
|
public void Apply(FramePacingPolicy policy)
|
|
{
|
|
if (_policy == policy)
|
|
return;
|
|
|
|
_policy = policy;
|
|
if (policy.UseVSync
|
|
|| policy.SoftwareLimitHz is not { } limitHz
|
|
|| !double.IsFinite(limitHz)
|
|
|| limitHz <= 0d)
|
|
{
|
|
_periodTicks = 0;
|
|
_nextDeadline = 0;
|
|
_hasDeadline = false;
|
|
return;
|
|
}
|
|
|
|
_periodTicks = Math.Max(
|
|
1L,
|
|
checked((long)Math.Round(_clock.Frequency / limitHz)));
|
|
_nextDeadline = AddSaturating(_clock.GetTimestamp(), _periodTicks);
|
|
_hasDeadline = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wait until the current frame's presentation deadline. GameWindow wires
|
|
/// this after its render callback and before Silk.NET's automatic swap.
|
|
/// </summary>
|
|
public void CompleteFrame()
|
|
{
|
|
if (!_hasDeadline || _periodTicks <= 0)
|
|
return;
|
|
|
|
long deadline = _nextDeadline;
|
|
long now = _clock.GetTimestamp();
|
|
|
|
if (now < deadline)
|
|
{
|
|
do
|
|
{
|
|
long remaining = deadline - now;
|
|
_waiter.Wait(remaining, _clock.Frequency);
|
|
now = _clock.GetTimestamp();
|
|
}
|
|
while (now < deadline);
|
|
|
|
long followingDeadline = AddSaturating(deadline, _periodTicks);
|
|
_nextDeadline = followingDeadline > now
|
|
? followingDeadline
|
|
: AddSaturating(now, _periodTicks);
|
|
return;
|
|
}
|
|
|
|
// The frame reached or missed its deadline without waiting. Rebase
|
|
// from now instead of advancing through old deadlines: there is never
|
|
// an immediate catch-up frame after a stall or long portal load.
|
|
_nextDeadline = AddSaturating(now, _periodTicks);
|
|
}
|
|
|
|
private static long AddSaturating(long value, long increment)
|
|
=> value > long.MaxValue - increment
|
|
? long.MaxValue
|
|
: value + increment;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_disposed = true;
|
|
|
|
if (_ownsWaiter && _waiter is IDisposable disposable)
|
|
disposable.Dispose();
|
|
}
|
|
}
|
|
|
|
internal interface IFramePacingClock
|
|
{
|
|
long Frequency { get; }
|
|
|
|
long GetTimestamp();
|
|
}
|
|
|
|
internal interface IFramePacingWaiter
|
|
{
|
|
void Wait(long durationTicks, long clockFrequency);
|
|
}
|
|
|
|
internal sealed class StopwatchFramePacingClock : IFramePacingClock
|
|
{
|
|
public static StopwatchFramePacingClock Instance { get; } = new();
|
|
|
|
private StopwatchFramePacingClock()
|
|
{
|
|
}
|
|
|
|
public long Frequency => Stopwatch.Frequency;
|
|
|
|
public long GetTimestamp() => Stopwatch.GetTimestamp();
|
|
}
|