acdream/src/AcDream.App/Rendering/WindowsHighResolutionFramePacingWaiter.cs
Erik 3718e341be 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>
2026-07-19 06:29:30 +02:00

169 lines
5.9 KiB
C#

using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace AcDream.App.Rendering;
/// <summary>
/// Low-CPU, sub-millisecond-capable deadline wait for the Windows render loop.
/// One auto-reset timer is reused for the complete window lifetime.
/// </summary>
/// <remarks>
/// <para>
/// A normal <see cref="Thread.Sleep(int)"/> is quantized by the Windows scheduler.
/// At a 165 Hz presentation target, its variable overshoot turned a requested
/// 6.1 ms frame into a 10-16 ms frame. A high-resolution waitable timer keeps
/// the render thread blocked in the kernel without that coarse sleep quantum.
/// </para>
/// <para>
/// <c>CREATE_WAITABLE_TIMER_HIGH_RESOLUTION</c> is available on Windows 10
/// version 1803 and newer, which is the client's supported desktop baseline.
/// Microsoft documents relative due times as negative 100-nanosecond units.
/// </para>
/// </remarks>
internal sealed partial class WindowsHighResolutionFramePacingWaiter :
IFramePacingWaiter,
IDisposable
{
private const uint CreateWaitableTimerHighResolution = 0x00000002;
private const uint TimerModifyState = 0x00000002;
private const uint Synchronize = 0x00100000;
private const uint WaitObject0 = 0x00000000;
private const uint WaitTimeout = 0x00000102;
private const uint WaitFailed = 0xFFFFFFFF;
private const long HundredNanosecondsPerSecond = 10_000_000;
private readonly SafeWaitHandle _timer;
private bool _disposed;
private WindowsHighResolutionFramePacingWaiter(SafeWaitHandle timer)
=> _timer = timer;
public static WindowsHighResolutionFramePacingWaiter Create()
{
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
throw new PlatformNotSupportedException(
"acdream software frame pacing requires Windows 10 version 1803 or newer.");
}
SafeWaitHandle timer = CreateWaitableTimerExW(
0,
0,
CreateWaitableTimerHighResolution,
TimerModifyState | Synchronize);
if (timer.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
timer.Dispose();
throw new Win32Exception(error, "Could not create the high-resolution frame timer.");
}
return new WindowsHighResolutionFramePacingWaiter(timer);
}
public void Wait(long durationTicks, long clockFrequency)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (durationTicks <= 0 || clockFrequency <= 0)
return;
long hundredNanoseconds = ConvertTicksToHundredNanoseconds(
durationTicks,
clockFrequency);
long relativeDueTime = -hundredNanoseconds;
if (!SetWaitableTimerEx(
_timer,
in relativeDueTime,
periodMilliseconds: 0,
completionRoutine: 0,
completionArgument: 0,
wakeContext: 0,
tolerableDelayMilliseconds: 0))
{
throw new Win32Exception(
Marshal.GetLastPInvokeError(),
"Could not arm the high-resolution frame timer.");
}
uint result = WaitForSingleObject(
_timer,
ComputeFailureTimeoutMilliseconds(hundredNanoseconds));
if (result == WaitObject0)
return;
if (result == WaitTimeout)
{
throw new TimeoutException(
"The high-resolution frame timer did not signal before its safety timeout.");
}
int waitError = result == WaitFailed
? Marshal.GetLastPInvokeError()
: unchecked((int)result);
throw new Win32Exception(waitError, "Waiting on the high-resolution frame timer failed.");
}
internal static long ConvertTicksToHundredNanoseconds(
long durationTicks,
long clockFrequency)
{
if (durationTicks <= 0)
throw new ArgumentOutOfRangeException(nameof(durationTicks));
if (clockFrequency <= 0)
throw new ArgumentOutOfRangeException(nameof(clockFrequency));
long wholeSeconds = Math.DivRem(durationTicks, clockFrequency, out long remainder);
if (wholeSeconds >= long.MaxValue / HundredNanosecondsPerSecond)
return long.MaxValue;
long wholeIntervals = wholeSeconds * HundredNanosecondsPerSecond;
long fractionalIntervals = (long)Math.Ceiling(
remainder * (double)HundredNanosecondsPerSecond / clockFrequency);
if (wholeIntervals > long.MaxValue - fractionalIntervals)
return long.MaxValue;
return Math.Max(1, wholeIntervals + fractionalIntervals);
}
private static uint ComputeFailureTimeoutMilliseconds(long hundredNanoseconds)
{
double dueMilliseconds = hundredNanoseconds / 10_000d;
double timeoutMilliseconds = Math.Ceiling(dueMilliseconds) + 1_000d;
return timeoutMilliseconds >= uint.MaxValue - 1d
? uint.MaxValue - 1
: Math.Max(1u, (uint)timeoutMilliseconds);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_timer.Dispose();
}
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial SafeWaitHandle CreateWaitableTimerExW(
nint timerAttributes,
nint timerName,
uint flags,
uint desiredAccess);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetWaitableTimerEx(
SafeWaitHandle timer,
in long dueTime,
int periodMilliseconds,
nint completionRoutine,
nint completionArgument,
nint wakeContext,
uint tolerableDelayMilliseconds);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint WaitForSingleObject(
SafeWaitHandle handle,
uint milliseconds);
}