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:
parent
47d7086a74
commit
3718e341be
17 changed files with 1103 additions and 225 deletions
|
|
@ -0,0 +1,169 @@
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue