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

@ -91,6 +91,62 @@ public sealed class FramePacingControllerTests
Assert.Equal([25L], waiter.Durations);
}
[Theory]
[InlineData(1L, 10_000_000L, 1L)]
[InlineData(1L, 3L, 3_333_334L)]
[InlineData(6_000L, 1_000_000L, 60_000L)]
[InlineData(1_000L, 1_000L, 10_000_000L)]
public void High_resolution_timer_conversion_rounds_up_without_losing_time(
long durationTicks,
long frequency,
long expectedHundredNanoseconds)
{
Assert.Equal(
expectedHundredNanoseconds,
WindowsHighResolutionFramePacingWaiter.ConvertTicksToHundredNanoseconds(
durationTicks,
frequency));
}
[Fact]
public void High_resolution_timer_conversion_saturates_instead_of_overflowing()
{
Assert.Equal(
long.MaxValue,
WindowsHighResolutionFramePacingWaiter.ConvertTicksToHundredNanoseconds(
long.MaxValue,
clockFrequency: 1));
}
[Fact]
public void Windows_high_resolution_timer_arms_and_disposes_its_handle()
{
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
return;
using var timer = WindowsHighResolutionFramePacingWaiter.Create();
timer.Wait(1, System.Diagnostics.Stopwatch.Frequency);
timer.Dispose();
timer.Dispose();
Assert.Throws<ObjectDisposedException>(() => timer.Wait(1, 1));
}
[Fact]
public void Controller_disposes_owned_waiter_exactly_once()
{
var waiter = new DisposableWaiter();
var controller = new FramePacingController(
new FakeClock(frequency: 1_000),
waiter,
ownsWaiter: true);
controller.Dispose();
controller.Dispose();
Assert.Equal(1, waiter.DisposeCount);
}
private sealed class FakeClock(long frequency) : IFramePacingClock
{
public long Frequency { get; } = frequency;
@ -113,4 +169,15 @@ public sealed class FramePacingControllerTests
clock.Timestamp += durationTicks + OvershootTicks;
}
}
private sealed class DisposableWaiter : IFramePacingWaiter, IDisposable
{
public int DisposeCount { get; private set; }
public void Wait(long durationTicks, long clockFrequency)
{
}
public void Dispose() => DisposeCount++;
}
}

View file

@ -20,6 +20,7 @@ public sealed class FramePacingPolicyTests
[InlineData(60)]
[InlineData(120)]
[InlineData(144)]
[InlineData(240)]
public void Normal_VSync_off_uses_active_monitor_refresh_limit(int refreshHz)
{
var policy = FramePacingPolicy.Resolve(

View file

@ -94,6 +94,91 @@ public sealed class RetailAlphaQueueTests
Assert.Equal(new[] { 1 }, particles.BatchSizes);
}
[Fact]
public void Flush_LargeInputMatchesStableDescendingRetailOrder()
{
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
var random = new Random(0xAC2013);
float[] distances = new float[4_096];
queue.BeginFrame();
for (int i = 0; i < distances.Length; i++)
{
distances[i] = i % 127 switch
{
0 => float.NaN,
1 => float.PositiveInfinity,
2 => -0f,
3 => -1f,
_ => random.Next(0, 64),
};
queue.Submit(source, i, distances[i]);
}
queue.EndFrame();
int[] expected = Enumerable.Range(0, distances.Length)
.OrderByDescending(i => NormalizeForReference(distances[i]))
.ToArray();
Assert.Equal(expected, source.PreparedTokens);
}
[Fact]
public void Flush_ExtremeFloatKeysMatchNormalizedRetailOrder()
{
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
float[] distances =
[
1f,
float.BitIncrement(1f),
float.BitDecrement(1f),
float.Epsilon,
float.MaxValue,
0f,
-0f,
float.NaN,
float.PositiveInfinity,
float.NegativeInfinity,
-float.Epsilon,
1f,
];
queue.BeginFrame();
for (int i = 0; i < distances.Length; i++)
queue.Submit(source, i, distances[i]);
queue.EndFrame();
int[] expected = Enumerable.Range(0, distances.Length)
.OrderByDescending(i => NormalizeForReference(distances[i]))
.ToArray();
Assert.Equal(expected, source.PreparedTokens);
}
[Fact]
public void Flush_EqualDistanceOrderStartsFreshInEachFrameScope()
{
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.Submit(source, 1, 12f);
queue.Submit(source, 2, 12f);
queue.EndFrame();
queue.BeginFrame();
queue.Submit(source, 9, 12f);
queue.Submit(source, 8, 12f);
queue.EndFrame();
Assert.Equal(
["alpha:1", "alpha:2", "alpha:9", "alpha:8"],
log);
}
[Fact]
public void ComputeViewerDistance_UsesTransformedGfxSortCenter()
{
@ -113,11 +198,15 @@ public sealed class RetailAlphaQueueTests
Assert.Equal(2f, cypt, precision: 5);
}
private static float NormalizeForReference(float value)
=> float.IsFinite(value) && value > 0f ? value : 0f;
private sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
{
public List<int> BatchSizes { get; } = new();
public int ResetCount { get; private set; }
public int PrepareCount { get; private set; }
public IReadOnlyList<int> PreparedTokens => _prepared;
private int[] _prepared = [];
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)

View file

@ -1,11 +1,215 @@
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Meshing;
using Xunit;
namespace AcDream.App.Tests.Rendering.Wb;
public class InstanceGroupClearTests
{
[Fact]
public void ClassificationCache_PreservesImmutableSelectionPartDescriptors()
{
var cache = new EntityClassificationCache();
Matrix4x4 restPose =
Matrix4x4.CreateRotationZ(0.75f) *
Matrix4x4.CreateTranslation(1f, 2f, 3f);
CachedSelectionPart[] parts =
[
new CachedSelectionPart(0x0001_0002, 0x01001234u, restPose),
];
GroupKey key = MakeKey(0xAA);
cache.Populate(
entityId: 100,
landblockHint: 0xA9B40000u,
batches: [new CachedBatch(key, 0xAA, Matrix4x4.Identity)],
selectionParts: parts);
Assert.True(cache.TryGet(100, 0xA9B40000u, out EntityCacheEntry? entry));
CachedSelectionPart part = Assert.Single(entry!.SelectionParts);
Assert.Equal(parts[0], part);
Matrix4x4 entityWorld = Matrix4x4.CreateTranslation(20f, 30f, 40f);
Assert.Equal(restPose * entityWorld, part.RestPose * entityWorld);
}
[Fact]
public void InstanceLightSet_PacksAndCopiesAllRetailSlotsInOrder()
{
int[] source = [3, 7, 11, 15, 19, 23, 27, 31];
var packed = WbDrawDispatcher.InstanceLightSet.From(source);
int[] destination = Enumerable.Repeat(-99, 12).ToArray();
packed.CopyTo(destination, 2);
Assert.Equal(source, destination[2..10]);
Assert.Equal(-99, destination[1]);
Assert.Equal(-99, destination[10]);
}
[Fact]
public void PartitionInstanceGroups_DeferredAlphaIsNotStagedInImmediateBuffers()
{
var opaqueGroup = MakeGroup(TranslucencyKind.Opaque, 2, new Vector3(3f, 4f, 0f));
var alphaGroup = MakeGroup(TranslucencyKind.AlphaBlend, 3, new Vector3(0f, 0f, 10f));
var additiveGroup = MakeGroup(TranslucencyKind.Additive, 1, new Vector3(1f, 0f, 0f));
var emptyGroup = new WbDrawDispatcher.InstanceGroup
{
Translucency = TranslucencyKind.Opaque,
};
var opaque = new List<WbDrawDispatcher.InstanceGroup>();
var transparent = new List<WbDrawDispatcher.InstanceGroup>();
var deferred = WbDrawDispatcher.PartitionInstanceGroups(
[opaqueGroup, alphaGroup, additiveGroup, emptyGroup],
deferTransparent: true,
cameraWorldPosition: Vector3.Zero,
opaque,
transparent);
Assert.Equal(6, deferred.VisibleInstances);
Assert.Equal(2, deferred.ImmediateInstances);
Assert.Equal([opaqueGroup], opaque);
Assert.Equal([alphaGroup, additiveGroup], transparent);
Assert.Equal(25f, opaqueGroup.SortDistance);
Assert.Equal(100f, alphaGroup.SortDistance);
var immediate = WbDrawDispatcher.PartitionInstanceGroups(
[opaqueGroup, alphaGroup, additiveGroup],
deferTransparent: false,
cameraWorldPosition: Vector3.Zero,
opaque,
transparent);
Assert.Equal(6, immediate.VisibleInstances);
Assert.Equal(6, immediate.ImmediateInstances);
}
[Fact]
public void CachedGroupHandle_RequiresLiveMatchingRegistration()
{
var group = new WbDrawDispatcher.InstanceGroup { Registration = 17 };
var key = new GroupKey(
FirstIndex: 0,
BaseVertex: 0,
IndexCount: 6,
BindlessTextureHandle: 0xAA,
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque);
var cached = new CachedBatch(
key,
0xAA,
Matrix4x4.Identity,
Group: group,
GroupRegistration: 17);
Assert.True(WbDrawDispatcher.TryResolveCachedGroup(cached, out var resolved));
Assert.Same(group, resolved);
group.Registration = 0;
Assert.False(WbDrawDispatcher.TryResolveCachedGroup(cached, out _));
Assert.False(WbDrawDispatcher.TryResolveCachedGroup(
cached with { Group = null },
out _));
}
[Fact]
public void FramePrune_RetiresOnlyGroupsAbsentForWholePreviousFrame()
{
GroupKey liveKey = MakeKey(0xAA);
GroupKey retiredKey = MakeKey(0xBB);
var live = new WbDrawDispatcher.InstanceGroup
{
Registration = 11,
LastUsedFrame = 9,
};
live.Matrices.Add(Matrix4x4.Identity);
var retired = new WbDrawDispatcher.InstanceGroup
{
Registration = 12,
LastUsedFrame = 8,
};
retired.Matrices.Capacity = 64;
var groups = new Dictionary<GroupKey, WbDrawDispatcher.InstanceGroup>
{
[liveKey] = live,
[retiredKey] = retired,
};
var retiredKeys = new List<GroupKey>();
var staleCache = new CachedBatch(
retiredKey,
retiredKey.BindlessTextureHandle,
Matrix4x4.Identity,
Group: retired,
GroupRegistration: retired.Registration);
int retiredCount = WbDrawDispatcher.PruneInstanceGroupsUnusedBeforeFrame(
groups,
retiredKeys,
oldestLiveFrame: 9);
Assert.Equal(1, retiredCount);
Assert.Single(groups);
Assert.Same(live, groups[liveKey]);
Assert.Single(live.Matrices);
Assert.Equal(11, live.Registration);
Assert.Equal(0, retired.Registration);
Assert.Equal(0, retired.Matrices.Capacity);
Assert.False(WbDrawDispatcher.TryResolveCachedGroup(staleCache, out _));
Assert.Empty(retiredKeys);
}
[Fact]
public void FramePrune_KeepsGroupsUsedByDifferentDrawScopesInSameFrame()
{
GroupKey landscapeKey = MakeKey(0xAA);
GroupKey paperdollKey = MakeKey(0xBB);
var landscape = new WbDrawDispatcher.InstanceGroup
{
Registration = 21,
LastUsedFrame = 14,
};
var paperdoll = new WbDrawDispatcher.InstanceGroup
{
Registration = 22,
LastUsedFrame = 14,
};
var groups = new Dictionary<GroupKey, WbDrawDispatcher.InstanceGroup>
{
[landscapeKey] = landscape,
[paperdollKey] = paperdoll,
};
int retiredCount = WbDrawDispatcher.PruneInstanceGroupsUnusedBeforeFrame(
groups,
[],
oldestLiveFrame: 14);
Assert.Equal(0, retiredCount);
Assert.Equal(2, groups.Count);
Assert.Equal(21, landscape.Registration);
Assert.Equal(22, paperdoll.Registration);
}
private static GroupKey MakeKey(ulong textureHandle) => new(
FirstIndex: 0,
BaseVertex: 0,
IndexCount: 6,
BindlessTextureHandle: textureHandle,
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque);
private static WbDrawDispatcher.InstanceGroup MakeGroup(
TranslucencyKind translucency,
int instanceCount,
Vector3 firstPosition)
{
var group = new WbDrawDispatcher.InstanceGroup { Translucency = translucency };
for (int i = 0; i < instanceCount; i++)
group.Matrices.Add(Matrix4x4.CreateTranslation(firstPosition + new Vector3(i, 0f, 0f)));
return group;
}
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
// seven per-instance parallel lists — Matrices, LocalSortCenters, Slots,
// LightSets, IndoorFlags, Opacities, SelectionLighting — appended in lockstep
@ -22,7 +226,7 @@ public class InstanceGroupClearTests
grp.Matrices.Add(Matrix4x4.Identity);
grp.LocalSortCenters.Add(Vector3.Zero);
grp.Slots.Add(1u);
grp.LightSets.Add(-1);
grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
grp.IndoorFlags.Add(0u);
grp.Opacities.Add(1.0f);
grp.SelectionLighting.Add(new Vector2(0f, 1f));

View file

@ -0,0 +1,34 @@
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class ObjectMeshWorkerLifecycleTests
{
[Fact]
public void Shutdown_never_resets_the_shared_worker_wake_signal()
{
Assert.Equal(
ObjectMeshManager.PreparationWorkerWakeAction.Exit,
ObjectMeshManager.DecidePreparationWorkerWake(
isDisposed: true,
hasPendingRequests: false,
stagingAtHighWater: true,
arenaBackpressured: true));
Assert.Equal(
ObjectMeshManager.PreparationWorkerWakeAction.ResetAndWait,
ObjectMeshManager.DecidePreparationWorkerWake(
isDisposed: false,
hasPendingRequests: false,
stagingAtHighWater: false,
arenaBackpressured: false));
Assert.Equal(
ObjectMeshManager.PreparationWorkerWakeAction.Process,
ObjectMeshManager.DecidePreparationWorkerWake(
isDisposed: false,
hasPendingRequests: true,
stagingAtHighWater: false,
arenaBackpressured: false));
}
}