perf(rendering): retain packed classification
Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
This commit is contained in:
parent
f10dec58ba
commit
54d17eb446
7 changed files with 731 additions and 30 deletions
|
|
@ -73,6 +73,26 @@ public sealed class RenderSceneShadowRuntimeTests
|
|||
Assert.Contains("sourceChannel=static", snapshot.FirstMismatch);
|
||||
Assert.Contains("field=transform", snapshot.FirstMismatch);
|
||||
Assert.Single(logs);
|
||||
Assert.Equal(1, snapshot.SceneIndexCounts.Dirty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackedProductOwnership_LeavesDirtyAcknowledgementToProduct()
|
||||
{
|
||||
WorldEntity entity = Entity(23, serverGuid: 0, parentCell: null);
|
||||
CurrentRenderSceneOracle oracle = Capture(entity, []);
|
||||
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||
Register(shadow, entity, RenderProjectionClass.OutdoorStatic);
|
||||
shadow.DrainUpdateBoundary();
|
||||
var controller = new RenderSceneShadowComparisonController(
|
||||
shadow,
|
||||
oracle,
|
||||
acknowledgeDirty: false);
|
||||
|
||||
controller.CompareNow();
|
||||
|
||||
Assert.Equal(1uL, controller.Snapshot.SuccessfulComparisonCount);
|
||||
Assert.Equal(1, controller.Snapshot.SceneIndexCounts.Dirty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
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();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
Hit(cache, generation, id, identity);
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
Hit(cache, generation, id, identity);
|
||||
long allocated =
|
||||
GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue