acdream/tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs
Erik ff5d86175f feat(rendering): index incremental scene queries
Maintain packed render worksets as lifecycle deltas arrive and synchronize active animated statics without scanning the resident world. This keeps Slice F non-drawing while preparing exact spatial and feature queries for shadow comparison.

Release: 8,206 passed, 5 skipped.
2026-07-24 22:12:26 +02:00

582 lines
21 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
namespace AcDream.App.Tests.Rendering;
public sealed class ArchRenderSceneTests
{
[Fact]
public void Apply_RegisterChannelUpdatesAndUnregister_PreservesTypedRecord()
{
RenderSceneGeneration generation = Generation(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord original = Record(
id: 10,
incarnation: 3,
RenderProjectionClass.LiveDynamicRoot,
x: 1,
bucket: 0xAABB);
RenderDeltaApplyResult register = scene.Apply(
[RenderProjectionDelta.Register(generation, 1, original)]);
Assert.Equal(1, register.Applied);
Assert.Equal(1, register.Registered);
Assert.Equal(
new RenderProjectionCounts(1, 0, 0, 1, 0, 0),
scene.Counts);
RenderProjectionRecord transformed = original with
{
PreviousTransform = new PreviousRenderTransform(
original.Transform.LocalToWorld),
Transform = new RenderTransform(
Matrix4x4.CreateTranslation(4, 5, 6)),
Bounds = new RenderWorldBounds(
new Vector3(3, 4, 5),
new Vector3(5, 6, 7)),
SortKey = new RenderSortKey(99),
};
RenderProjectionRecord appearance = transformed with
{
MeshSet = new RenderMeshSet(Asset(80), 4, 7),
Material = new RenderMaterialVariant(12, 13, 0.5f),
DegradeState = new RenderDegradeState(2, 8),
};
RenderProjectionRecord flags = appearance with
{
Flags = RenderProjectionFlags.Draw
| RenderProjectionFlags.Translucent,
};
RenderProjectionRecord rebucketed = flags with
{
Residency = new RenderSpatialResidency(
Bucket(0xCCDD),
0xCCDD0000,
0xCCDD0102),
};
RenderDeltaApplyResult updates = scene.Apply(
[
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateTransform,
generation,
2,
transformed),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateAppearance,
generation,
3,
appearance),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateFlags,
generation,
4,
flags),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.Rebucket,
generation,
5,
rebucketed),
]);
Assert.Equal(4, updates.Applied);
Assert.Equal(4, updates.Updated);
Assert.True(scene.OpenQuery().TryGet(original.Id, out var stored));
Assert.Equal(rebucketed.Transform, stored.Transform);
Assert.Equal(rebucketed.Bounds, stored.Bounds);
Assert.Equal(rebucketed.MeshSet, stored.MeshSet);
Assert.Equal(rebucketed.Material, stored.Material);
Assert.Equal(rebucketed.Flags, stored.Flags);
Assert.Equal(rebucketed.Residency, stored.Residency);
Assert.Equal(
RenderDirtyMask.All,
stored.DirtyMask);
RenderDeltaApplyResult unregister = scene.Apply(
[
RenderProjectionDelta.Unregister(
generation,
6,
original.Id,
original.OwnerIncarnation),
]);
Assert.Equal(1, unregister.Applied);
Assert.Equal(1, unregister.Unregistered);
Assert.Equal(default, scene.Counts);
Assert.False(scene.OpenQuery().TryGet(original.Id, out _));
}
[Fact]
public void Register_NewerIncarnationReplacesAndOldCallbacksCannotRemoveIt()
{
RenderSceneGeneration generation = Generation(4);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord first = Record(
44,
10,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord replacement = Record(
44,
11,
RenderProjectionClass.ActiveAnimatedStatic,
x: 9);
scene.Apply([RenderProjectionDelta.Register(generation, 1, first)]);
RenderDeltaApplyResult replaced = scene.Apply(
[RenderProjectionDelta.Register(generation, 2, replacement)]);
RenderDeltaApplyResult staleRegister = scene.Apply(
[RenderProjectionDelta.Register(generation, 3, first)]);
RenderDeltaApplyResult staleDelete = scene.Apply(
[
RenderProjectionDelta.Unregister(
generation,
4,
first.Id,
first.OwnerIncarnation),
]);
Assert.Equal(1, replaced.Replaced);
Assert.Equal(1, replaced.Registered);
Assert.Equal(1, staleRegister.RejectedStaleIncarnation);
Assert.Equal(1, staleDelete.RejectedStaleIncarnation);
Assert.Equal(
new RenderProjectionCounts(1, 0, 0, 0, 1, 0),
scene.Counts);
Assert.True(scene.OpenQuery().TryGet(first.Id, out var current));
Assert.Equal(replacement.OwnerIncarnation, current.OwnerIncarnation);
Assert.Equal(replacement.Transform, current.Transform);
}
[Fact]
public void Apply_RejectsWrongGenerationOutOfOrderAndMissingDeltas()
{
RenderSceneGeneration generation = Generation(7);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord record = Record(
70,
1,
RenderProjectionClass.IndoorCellStatic);
RenderDeltaApplyResult result = scene.Apply(
[
RenderProjectionDelta.Register(Generation(6), 1, record),
RenderProjectionDelta.Register(generation, 2, record),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateFlags,
generation,
2,
record),
RenderProjectionDelta.Unregister(
generation,
3,
Projection(999),
Incarnation(1)),
]);
Assert.Equal(1, result.Applied);
Assert.Equal(1, result.RejectedGeneration);
Assert.Equal(1, result.RejectedOutOfOrderSequence);
Assert.Equal(1, result.RejectedMissing);
Assert.Equal(1, scene.Counts.Total);
}
[Fact]
public void Clear_AdvancesGenerationReclaimsRecordsAndInvalidatesBorrowedQuery()
{
RenderSceneGeneration firstGeneration = Generation(1);
using var scene = new ArchRenderScene(firstGeneration);
RenderProjectionRecord record = Record(
1,
1,
RenderProjectionClass.EquippedChild);
scene.Apply(
[RenderProjectionDelta.Register(firstGeneration, 1, record)]);
RenderSceneQuery borrowed = scene.OpenQuery();
scene.Clear(Generation(2));
Assert.Equal(Generation(2), scene.Generation);
Assert.Equal(default, scene.Counts);
Assert.Equal(0, scene.Memory.EntityCount);
Assert.Throws<InvalidOperationException>(() => _ = borrowed.Counts);
Assert.Throws<ArgumentOutOfRangeException>(
() => scene.Clear(Generation(2)));
RenderProjectionRecord next = record with
{
OwnerIncarnation = Incarnation(2),
};
RenderDeltaApplyResult applied = scene.Apply(
[RenderProjectionDelta.Register(Generation(2), 1, next)]);
Assert.Equal(1, applied.Registered);
}
[Fact]
public void FiveProjectionClasses_UseNarrowArchetypesAndReportRetainedMemory()
{
RenderSceneGeneration generation = Generation(9);
using var scene = new ArchRenderScene(generation);
RenderProjectionClass[] classes =
[
RenderProjectionClass.OutdoorStatic,
RenderProjectionClass.IndoorCellStatic,
RenderProjectionClass.LiveDynamicRoot,
RenderProjectionClass.ActiveAnimatedStatic,
RenderProjectionClass.EquippedChild,
];
var deltas = new RenderProjectionDelta[classes.Length];
for (int i = 0; i < classes.Length; i++)
{
RenderProjectionRecord record = Record(
(ulong)i + 1,
1,
classes[i]);
deltas[i] = RenderProjectionDelta.Register(
generation,
(ulong)i + 1,
record);
}
scene.Apply(deltas);
RenderSceneMemoryAccounting memory = scene.Memory;
Assert.Equal(5, scene.Counts.Total);
Assert.Equal(5, memory.EntityCount);
Assert.Equal(5, memory.ArchetypeCount);
Assert.Equal(5, memory.AllocatedChunkCount);
Assert.True(memory.ArchEntityCapacity >= memory.EntityCount);
Assert.True(memory.EstimatedChunkPayloadBytes > 0);
Assert.True(memory.ProjectionLookupCapacity >= memory.EntityCount);
Assert.True(memory.EstimatedProjectionLookupBytes > 0);
Assert.Equal(
memory.EstimatedChunkPayloadBytes
+ memory.EstimatedProjectionLookupBytes
+ memory.EstimatedIndexBytes,
memory.TotalEstimatedBytes);
}
[Fact]
public void DynamicSynchronization_UpdatesOnlyMatchingLiveIncarnation()
{
RenderSceneGeneration generation = Generation(10);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord record = Record(
100,
5,
RenderProjectionClass.LiveDynamicRoot);
scene.Apply([RenderProjectionDelta.Register(generation, 1, record)]);
DynamicProjectionUpdate stale = new(
record.Id,
Incarnation(4),
new RenderTransform(Matrix4x4.CreateTranslation(100, 0, 0)),
record.Bounds);
DynamicProjectionUpdate current = new(
record.Id,
record.OwnerIncarnation,
new RenderTransform(Matrix4x4.CreateTranslation(8, 9, 10)),
new RenderWorldBounds(
new Vector3(7, 8, 9),
new Vector3(9, 10, 11)));
scene.SynchronizeDynamicSources(
new DynamicProjectionSyncInput(generation, [stale, current]));
Assert.True(scene.OpenQuery().TryGet(record.Id, out var stored));
Assert.Equal(record.Transform.LocalToWorld, stored.PreviousTransform.LocalToWorld);
Assert.Equal(current.Transform, stored.Transform);
Assert.Equal(current.Bounds, stored.Bounds);
}
[Fact]
public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse()
{
RenderSceneGeneration generation = Generation(12);
using var first = new ArchRenderScene(generation);
using var second = new ArchRenderScene(generation);
RenderProjectionRecord a = Record(
1,
1,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord b = Record(
2,
1,
RenderProjectionClass.LiveDynamicRoot);
first.Apply(
[
RenderProjectionDelta.Register(generation, 1, a),
RenderProjectionDelta.Register(generation, 2, b),
]);
second.Apply(
[
RenderProjectionDelta.Register(generation, 1, b),
RenderProjectionDelta.Register(generation, 2, a),
]);
var buffer = new RenderSceneDigestBuffer();
RenderSceneDigest firstDigest = first.BuildDigest(buffer);
int retainedCapacity = buffer.Capacity;
RenderSceneDigest repeatedDigest = first.BuildDigest(buffer);
RenderSceneDigest secondDigest =
second.BuildDigest(new RenderSceneDigestBuffer());
Assert.Equal(firstDigest, repeatedDigest);
Assert.Equal(firstDigest, secondDigest);
Assert.Equal(retainedCapacity, buffer.Capacity);
}
[Fact]
public void Mutation_FromAnotherThread_IsRejected()
{
RenderSceneGeneration generation = Generation(15);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord record = Record(
15,
1,
RenderProjectionClass.LiveDynamicRoot);
Exception? captured = null;
var mutationThread = new Thread(() =>
{
try
{
scene.Apply(
[RenderProjectionDelta.Register(generation, 1, record)]);
}
catch (Exception error)
{
captured = error;
}
});
mutationThread.Start();
mutationThread.Join();
InvalidOperationException error =
Assert.IsType<InvalidOperationException>(captured);
Assert.Contains("owning update thread", error.Message);
Assert.Equal(0, scene.Counts.Total);
}
[Fact]
public void IncrementalIndices_PreserveOutdoorCellDynamicAndFeatureMembership()
{
const uint indoorCell = 0x12340100u;
RenderSceneGeneration generation = Generation(16);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord outdoorStatic = Record(
1,
1,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord indoorStatic = Record(
2,
1,
RenderProjectionClass.IndoorCellStatic) with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x1234FFFFu,
indoorCell),
Source = new RenderSourceMetadata() with
{
ParentCellId = indoorCell,
},
};
RenderProjectionRecord animatedStatic = Record(
3,
1,
RenderProjectionClass.ActiveAnimatedStatic);
RenderProjectionRecord outdoorDynamic = Record(
4,
1,
RenderProjectionClass.LiveDynamicRoot);
RenderProjectionRecord cellDynamic = Record(
5,
1,
RenderProjectionClass.LiveDynamicRoot) with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x1234FFFFu,
indoorCell),
Source = new RenderSourceMetadata() with
{
ParentCellId = indoorCell,
},
};
RenderProjectionRecord equipped = Record(
6,
1,
RenderProjectionClass.EquippedChild) with
{
Flags = RenderProjectionFlags.Draw
| RenderProjectionFlags.Selectable
| RenderProjectionFlags.Translucent
| RenderProjectionFlags.LightCandidate
| RenderProjectionFlags.PortalStraddling,
};
RenderProjectionRecord[] records =
[
outdoorStatic,
indoorStatic,
animatedStatic,
outdoorDynamic,
cellDynamic,
equipped,
];
var deltas = new RenderProjectionDelta[records.Length];
for (int i = 0; i < records.Length; i++)
{
deltas[i] = RenderProjectionDelta.Register(
generation,
(ulong)i + 1,
records[i]);
}
scene.Apply(deltas);
RenderSceneQuery query = scene.OpenQuery();
Assert.Equal(
new RenderSceneIndexCounts(
OutdoorStatic: 2,
IndoorCellStatic: 1,
Dynamic: 3,
OutdoorDynamic: 2,
PortalStraddlingDynamic: 1,
Translucent: 1,
Selectable: 6,
LightCandidate: 1,
Dirty: 6),
query.IndexCounts);
Assert.Equal(1, query.GetCellStaticCount(indoorCell));
Assert.Equal(1, query.GetCellDynamicCount(indoorCell));
var cellRecords = new RenderProjectionRecord[1];
Assert.Equal(1, query.CopyCellStaticsTo(indoorCell, cellRecords));
Assert.Equal(indoorStatic.Id, cellRecords[0].Id);
Assert.Equal(1, query.CopyCellDynamicsTo(indoorCell, cellRecords));
Assert.Equal(cellDynamic.Id, cellRecords[0].Id);
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
}
[Fact]
public void IncrementalIndices_UpdateRebucketReplaceRemoveAndDirtyAcknowledge()
{
const uint indoorCell = 0x22220100u;
RenderSceneGeneration generation = Generation(17);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord original = Record(
17,
1,
RenderProjectionClass.LiveDynamicRoot);
scene.Apply(
[RenderProjectionDelta.Register(generation, 1, original)]);
scene.ClearDirty();
Assert.Equal(0, scene.OpenQuery().IndexCounts.Dirty);
RenderProjectionRecord flagged = original with
{
Flags = original.Flags
| RenderProjectionFlags.Translucent
| RenderProjectionFlags.LightCandidate
| RenderProjectionFlags.PortalStraddling,
Source = original.Source with { ParentCellId = indoorCell },
};
RenderProjectionRecord rebucketed = flagged with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x2222FFFFu,
indoorCell),
};
scene.Apply(
[
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateFlags,
generation,
2,
flagged),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.Rebucket,
generation,
3,
rebucketed),
]);
RenderSceneQuery updated = scene.OpenQuery();
Assert.Equal(0, updated.IndexCounts.OutdoorDynamic);
Assert.Equal(1, updated.IndexCounts.PortalStraddlingDynamic);
Assert.Equal(1, updated.IndexCounts.Translucent);
Assert.Equal(1, updated.IndexCounts.LightCandidate);
Assert.Equal(1, updated.IndexCounts.Dirty);
Assert.Equal(1, updated.GetCellDynamicCount(indoorCell));
RenderProjectionRecord replacement = rebucketed with
{
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = Incarnation(2),
};
scene.Apply(
[RenderProjectionDelta.Register(generation, 4, replacement)]);
RenderSceneQuery replaced = scene.OpenQuery();
Assert.Equal(0, replaced.IndexCounts.Dynamic);
Assert.Equal(1, replaced.IndexCounts.IndoorCellStatic);
Assert.Equal(1, replaced.GetCellStaticCount(indoorCell));
scene.Apply(
[
RenderProjectionDelta.Unregister(
generation,
5,
replacement.Id,
replacement.OwnerIncarnation),
]);
Assert.Equal(default, scene.OpenQuery().IndexCounts);
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
}
private static RenderProjectionRecord Record(
ulong id,
ulong incarnation,
RenderProjectionClass projectionClass,
float x = 0,
ulong bucket = 1)
{
Matrix4x4 transform = Matrix4x4.CreateTranslation(x, 2, 3);
return new RenderProjectionRecord(
Projection(id),
projectionClass,
Incarnation(incarnation),
new RenderTransform(transform),
new PreviousRenderTransform(transform),
new RenderMeshSet(Asset(id + 100), 2, 1),
new RenderMaterialVariant(id + 200, id + 300, 1),
new RenderSpatialResidency(
Bucket(bucket),
(uint)(bucket << 16),
(uint)((bucket << 16) | 0x101)),
new RenderWorldBounds(
new Vector3(x - 1, 1, 2),
new Vector3(x + 1, 3, 4)),
RenderProjectionFlags.Draw | RenderProjectionFlags.Selectable,
new RenderDegradeState(0, 1),
new RenderSortKey(id + 400),
RenderDirtyMask.All);
}
private static RenderProjectionId Projection(ulong value) =>
RenderProjectionId.FromRaw(value);
private static RenderOwnerIncarnation Incarnation(ulong value) =>
RenderOwnerIncarnation.FromRaw(value);
private static RenderSceneGeneration Generation(ulong value) =>
RenderSceneGeneration.FromRaw(value);
private static RenderSpatialBucket Bucket(ulong value) =>
RenderSpatialBucket.FromRaw(value);
private static RenderAssetHandle Asset(ulong value) =>
RenderAssetHandle.FromRaw(value);
}