fix #225: stabilize render pacing and frame CPU

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>
This commit is contained in:
Erik 2026-07-19 06:29:30 +02:00
parent 47d7086a74
commit 3718e341be
17 changed files with 1103 additions and 225 deletions

View file

@ -12,27 +12,41 @@ namespace AcDream.App.Rendering;
/// 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
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, ThreadFramePacingWaiter.Instance)
: 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.");
}
@ -102,6 +116,16 @@ internal sealed class FramePacingController
=> 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
@ -128,28 +152,3 @@ internal sealed class StopwatchFramePacingClock : IFramePacingClock
public long GetTimestamp() => Stopwatch.GetTimestamp();
}
internal sealed class ThreadFramePacingWaiter : IFramePacingWaiter
{
public static ThreadFramePacingWaiter Instance { get; } = new();
private ThreadFramePacingWaiter()
{
}
public void Wait(long durationTicks, long clockFrequency)
{
if (durationTicks <= 0 || clockFrequency <= 0)
return;
double milliseconds = durationTicks * 1000d / clockFrequency;
// A kernel sleep is intentionally preferred over a final spin. It can
// overshoot a presentation deadline slightly, but it keeps normal CPU
// use low—the central reason this fallback exists.
int sleepMilliseconds = Math.Max(
1,
(int)Math.Ceiling(Math.Min(milliseconds, int.MaxValue)));
Thread.Sleep(sleepMilliseconds);
}
}