The first commit converted the four members the issue named and left the other sites alone, reasoning that none had been observed failing. A 20-run complete-solution baseline disproved that within minutes: run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal… run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint… Both new names are the same shape as the four — one warm call, then a thousand-iteration loop inside the measured window — and neither had been recorded anywhere. "Not observed failing" only ever meant "not yet observed", and leaving known-shape sites in place would have guaranteed the acceptance gate failed. Run 19 is the sharper lesson: the issue named `SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first commit converted a *different* test in that same file, so the actually-named member was still on the old shape. Matching by file was not matching by test. Every strict-zero site in the assembly is now on the probe — ten tests. Two came out stricter rather than merely steadier: `StaticRenderProjectionJournalTests` was measuring a synchronise whose journal does **not** coalesce. Repeating it grew the journal by 1,000 entries per call — 192,000 by the end of a probe run — so the steady state the test claimed to measure did not exist and the single-call window had been hiding it. Its step is now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside the measured window for the first time and asserts the journal ends empty. `RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It now counts its own dispatches and pins the callback count against that, so the assertion still proves the fast path ran the callback every time without being coupled to a loop bound that no longer exists. Left alone deliberately: the four sites asserting a tolerance rather than zero — `CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb this noise and none has flaked; changing a bound in either direction is a separate decision from fixing a measurement. Worth noting that `PortalProjectionTests`' ceiling exists explicitly to tolerate "a tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch", which is exactly what the probe removes, so it could probably be tightened to zero now — recorded in the issue rather than done here. Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
215 lines
7.7 KiB
C#
215 lines
7.7 KiB
C#
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Wb;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
public sealed class PackedProjectionClassificationCacheTests
|
|
{
|
|
[Fact]
|
|
public void UnchangedProjection_ReusesClassificationAcrossFrames()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderSceneGeneration generation =
|
|
RenderSceneGeneration.FromRaw(1);
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity identity = Identity(7);
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in identity);
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
|
|
cache.BeginFrame(generation);
|
|
Assert.True(cache.TryGetReusable(
|
|
id,
|
|
in identity,
|
|
RenderDirtyMask.Transform,
|
|
out PackedProjectionClassificationEntry? reused));
|
|
cache.EndFrame();
|
|
|
|
Assert.Same(entry, reused);
|
|
Assert.Equal(0, cache.Snapshot.StaticRebuildCount);
|
|
Assert.Equal(1, cache.Snapshot.CrossFrameReuseCount);
|
|
Assert.Equal(1, cache.Snapshot.ProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void AppearanceDirty_RequiresRebuildButTransformDirtyDoesNot()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderSceneGeneration generation =
|
|
RenderSceneGeneration.FromRaw(1);
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity identity = Identity(7);
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in identity);
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
|
|
cache.BeginFrame(generation);
|
|
Assert.False(cache.TryGetReusable(
|
|
id,
|
|
in identity,
|
|
RenderDirtyMask.Appearance,
|
|
out _));
|
|
PackedProjectionClassificationEntry rebuilt =
|
|
cache.BeginRebuild(id, in identity);
|
|
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
|
|
|
Assert.Same(entry, rebuilt);
|
|
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void IncompleteBuild_CanReplayDuplicateRouteOnlyInSameFrame()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderSceneGeneration generation =
|
|
RenderSceneGeneration.FromRaw(1);
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity identity = Identity(7);
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in identity);
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: false);
|
|
|
|
Assert.True(cache.TryGetReusable(
|
|
id,
|
|
in identity,
|
|
RenderDirtyMask.All,
|
|
out _));
|
|
Assert.Equal(1, cache.Snapshot.SameFrameReuseCount);
|
|
cache.EndFrame();
|
|
|
|
cache.BeginFrame(generation);
|
|
Assert.False(cache.TryGetReusable(
|
|
id,
|
|
in identity,
|
|
RenderDirtyMask.None,
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void IncarnationOrAppearanceChange_CannotReuseRetainedPayload()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderSceneGeneration generation =
|
|
RenderSceneGeneration.FromRaw(1);
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity original = Identity(7);
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in original);
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedClassificationIdentity replacement = Identity(17);
|
|
Assert.False(cache.TryGetReusable(
|
|
id,
|
|
in replacement,
|
|
RenderDirtyMask.None,
|
|
out _));
|
|
PackedProjectionClassificationEntry rebuilt =
|
|
cache.BeginRebuild(id, in replacement);
|
|
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
|
|
Assert.Same(entry, rebuilt);
|
|
Assert.Equal(replacement, rebuilt.Identity);
|
|
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
|
Assert.Equal(0, cache.Snapshot.CrossFrameReuseCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerationReplacementAndUnseenProjectionReleaseRetainedEntries()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity identity = Identity(7);
|
|
|
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in identity);
|
|
entry.Batches.Capacity = 32;
|
|
entry.SelectionParts.Capacity = 16;
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
Assert.True(cache.Snapshot.RetainedPayloadBytes > 0);
|
|
|
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
|
cache.EndFrame();
|
|
|
|
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
|
Assert.Equal(1, cache.Snapshot.RetiredProjectionCount);
|
|
Assert.Equal(0, entry.Batches.Capacity);
|
|
Assert.Equal(0, entry.SelectionParts.Capacity);
|
|
Assert.Equal(0, cache.Snapshot.RetainedPayloadBytes);
|
|
|
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(2));
|
|
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmUnchangedProjectionCache_AllocatesZeroBytes()
|
|
{
|
|
var cache = new PackedProjectionClassificationCache();
|
|
RenderSceneGeneration generation =
|
|
RenderSceneGeneration.FromRaw(1);
|
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
PackedClassificationIdentity identity = Identity(7);
|
|
|
|
cache.BeginFrame(generation);
|
|
PackedProjectionClassificationEntry entry =
|
|
cache.BeginRebuild(id, in identity);
|
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
cache.EndFrame();
|
|
|
|
// #250: the measured window was a 1,000-iteration loop written inline.
|
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
|
"PackedProjectionClassificationCache warm cache hit",
|
|
() => Hit(cache, generation, id, identity));
|
|
}
|
|
|
|
private static void Hit(
|
|
PackedProjectionClassificationCache cache,
|
|
RenderSceneGeneration generation,
|
|
RenderProjectionId id,
|
|
PackedClassificationIdentity identity)
|
|
{
|
|
cache.BeginFrame(generation);
|
|
if (!cache.TryGetReusable(
|
|
id,
|
|
in identity,
|
|
RenderDirtyMask.None,
|
|
out _))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The warmed projection classification was not reusable.");
|
|
}
|
|
cache.EndFrame();
|
|
}
|
|
|
|
private static PackedClassificationIdentity Identity(ulong seed) =>
|
|
new(
|
|
RenderOwnerIncarnation.FromRaw(seed),
|
|
new RenderMeshSet(
|
|
RenderAssetHandle.FromRaw(seed + 1),
|
|
MeshCount: 2,
|
|
Revision: seed + 2),
|
|
new RenderMaterialVariant(
|
|
PaletteKey: seed + 3,
|
|
TextureReplacementKey: seed + 4,
|
|
Opacity: 1f),
|
|
new RenderDegradeState(
|
|
Level: 0,
|
|
Revision: (uint)(seed + 5)),
|
|
new RenderSceneHash128(seed + 6, seed + 7),
|
|
new RenderSceneHash128(seed + 8, seed + 9));
|
|
}
|