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
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue