acdream/tests/AcDream.App.Tests/Rendering/StaticRenderProjectionJournalTests.cs
Erik 5d19c56d15 feat(rendering): journal static scene projections
Append exact static and EnvCell-shell projection deltas only at committed spatial activation and detach receipts. Same-landblock rehydrate now reconciles retained, new, and omitted presentation identities without constructing or drawing the shadow scene in production.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 21:57:33 +02:00

280 lines
10 KiB
C#

using System.Collections.Immutable;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Rendering;
public sealed class StaticRenderProjectionJournalTests
{
private const uint LandblockId = 0x1234FFFFu;
[Fact]
public void Reconcile_RegistersOnlyStaticEntitiesAndEnvCellShells()
{
RenderSceneGeneration generation = Generation(1);
var journal = new RenderProjectionJournal(generation);
var statics = new StaticRenderProjectionJournal(journal);
WorldEntity staticEntity = Entity(7, position: new Vector3(1, 2, 3));
WorldEntity liveEntity = Entity(8, serverGuid: 0x80000008u);
LandblockBuild build = Build(
LandblockId,
[staticEntity, liveEntity],
includeShell: true);
var publication = Publication(build);
statics.Reconcile(build, publication);
Assert.Equal(2, statics.ProjectionCount);
Assert.Equal(2, journal.Count);
Assert.All(
journal.Pending.ToArray(),
delta => Assert.Equal(
RenderProjectionDeltaKind.Register,
delta.Kind));
using var scene = new ArchRenderScene(generation);
RenderDeltaApplyResult applied = journal.DrainTo(scene);
Assert.Equal(2, applied.Registered);
Assert.Equal(0, journal.Count);
Assert.Equal(2, scene.Counts.Total);
Assert.Equal(1, scene.Counts.OutdoorStatic);
Assert.Equal(1, scene.Counts.IndoorCellStatic);
RenderProjectionId staticId =
StaticRenderProjectionJournal.StaticEntityId(
LandblockId,
staticEntity.Id);
Assert.True(scene.OpenQuery().TryGet(staticId, out var projected));
CurrentRenderProjectionFingerprint expected =
CurrentRenderSceneOracle.CreateProjectionFingerprint(
LandblockId,
staticEntity);
Assert.Equal(expected.Transform, projected.Source.TransformFingerprint);
Assert.Equal(expected.Geometry, projected.Source.GeometryFingerprint);
Assert.Equal(expected.Appearance, projected.Source.AppearanceFingerprint);
Assert.Equal(LandblockId, projected.Residency.OwnerLandblockId);
}
[Fact]
public void Reconcile_SameLandblockRetainsUpdatesOmitsAndAddsDeterministically()
{
RenderSceneGeneration generation = Generation(2);
var journal = new RenderProjectionJournal(generation);
var statics = new StaticRenderProjectionJournal(journal);
WorldEntity retained = Entity(1);
WorldEntity omitted = Entity(2);
LandblockBuild first = Build(
LandblockId,
[retained, omitted],
includeShell: true);
statics.Reconcile(first, Publication(first));
using var scene = new ArchRenderScene(generation);
journal.DrainTo(scene);
RenderProjectionId retainedId =
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 1);
Assert.True(scene.OpenQuery().TryGet(retainedId, out var before));
WorldEntity moved = Entity(1, position: new Vector3(10, 0, 0));
WorldEntity added = Entity(3);
LandblockBuild replacement = Build(
LandblockId,
[moved, added],
includeShell: false);
statics.Reconcile(replacement, Publication(replacement));
Assert.Equal(4, journal.Count);
Assert.Equal(
[
RenderProjectionDeltaKind.UpdateTransform,
RenderProjectionDeltaKind.Register,
RenderProjectionDeltaKind.Unregister,
RenderProjectionDeltaKind.Unregister,
],
journal.Pending.ToArray().Select(delta => delta.Kind).ToArray());
journal.DrainTo(scene);
Assert.Equal(2, scene.Counts.Total);
Assert.True(scene.OpenQuery().TryGet(retainedId, out var after));
Assert.Equal(before.OwnerIncarnation, after.OwnerIncarnation);
Assert.Equal(moved.Position, after.Transform.Position);
Assert.False(scene.OpenQuery().TryGet(
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 2),
out _));
Assert.False(scene.OpenQuery().TryGet(
StaticRenderProjectionJournal.EnvCellShellId(
LandblockId,
(LandblockId & 0xFFFF0000u) | 0x0100u),
out _));
}
[Fact]
public void ProjectionIdentity_DistinguishesDuplicateLocalIdsAcrossLandblocks()
{
const uint otherLandblock = 0x5678FFFFu;
var journal = new RenderProjectionJournal(Generation(3));
var statics = new StaticRenderProjectionJournal(journal);
LandblockBuild first = Build(
LandblockId,
[Entity(42)],
includeShell: false);
LandblockBuild second = Build(
otherLandblock,
[Entity(42)],
includeShell: false);
statics.Reconcile(first, Publication(first));
statics.Reconcile(second, Publication(second));
Assert.Equal(2, statics.ProjectionCount);
Assert.NotEqual(
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 42),
StaticRenderProjectionJournal.StaticEntityId(otherLandblock, 42));
}
[Fact]
public void StaleFarPublication_DoesNotMutateProjectionJournal()
{
var journal = new RenderProjectionJournal(Generation(4));
var statics = new StaticRenderProjectionJournal(journal);
LandblockBuild build = Build(
LandblockId,
[Entity(1)],
includeShell: true);
var stale = Publication(build) with { RequiresActivation = false };
statics.Reconcile(build, stale);
Assert.Equal(0, statics.ProjectionCount);
Assert.Equal(0, journal.Count);
}
[Fact]
public void Retire_IsExactAndIdempotentForFullAndNearLayerReceipts()
{
var journal = new RenderProjectionJournal(Generation(5));
var statics = new StaticRenderProjectionJournal(journal);
LandblockBuild build = Build(
LandblockId,
[Entity(1), Entity(2)],
includeShell: true);
statics.Reconcile(build, Publication(build));
using var scene = new ArchRenderScene(Generation(5));
journal.DrainTo(scene);
var retirement = new GpuLandblockRetirement(
LandblockId,
LandblockRetirementKind.NearLayer,
build.Landblock.Entities);
statics.Retire(retirement);
statics.Retire(retirement);
Assert.Equal(3, journal.Count);
Assert.Equal(0, statics.ProjectionCount);
Assert.All(
journal.Pending.ToArray(),
delta => Assert.Equal(
RenderProjectionDeltaKind.Unregister,
delta.Kind));
RenderDeltaApplyResult result = journal.DrainTo(scene);
Assert.Equal(3, result.Unregistered);
Assert.Equal(0, scene.Counts.Total);
}
[Fact]
public void Clear_AdvancesExactGenerationAndDropsRetainedState()
{
var journal = new RenderProjectionJournal(Generation(6));
var statics = new StaticRenderProjectionJournal(journal);
LandblockBuild build = Build(
LandblockId,
[Entity(1)],
includeShell: true);
statics.Reconcile(build, Publication(build));
statics.Clear(Generation(7));
Assert.Equal(Generation(7), journal.Generation);
Assert.Equal(0, journal.Count);
Assert.Equal(0, statics.ProjectionCount);
Assert.Equal(0, statics.LandblockCount);
Assert.Throws<ArgumentOutOfRangeException>(
() => statics.Clear(Generation(7)));
}
private static GpuLandblockSpatialPublication Publication(
LandblockBuild build,
bool requiresActivation = true) =>
new(
build.LandblockId,
build.Landblock,
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
?? [],
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
requiresActivation);
private static LandblockBuild Build(
uint landblockId,
IReadOnlyList<WorldEntity> entities,
bool includeShell)
{
EnvCellLandblockBuild? envCells = null;
if (includeShell)
{
uint cellId = (landblockId & 0xFFFF0000u) | 0x0100u;
envCells = new EnvCellLandblockBuild(
landblockId,
Array.Empty<LoadedCell>(),
[
new EnvCellShellPlacement(
cellId,
0x2_0000_0001UL,
0x0D000001u,
1,
ImmutableArray<ushort>.Empty,
new Vector3(4, 5, 6),
Quaternion.Identity,
Matrix4x4.CreateTranslation(4, 5, 6),
new WbBoundingBox(Vector3.Zero, Vector3.One),
new WbBoundingBox(
new Vector3(4, 5, 6),
new Vector3(5, 6, 7))),
]);
}
return new LandblockBuild(
new LoadedLandblock(
landblockId,
new LandBlock(),
entities,
PhysicsDatBundle.Empty),
envCells);
}
private static WorldEntity Entity(
uint id,
uint serverGuid = 0,
Vector3 position = default) =>
new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0x01000000u + id,
Position = position,
Rotation = Quaternion.Identity,
MeshRefs =
[
new MeshRef(
0x01010000u + id,
Matrix4x4.CreateTranslation(0, 0, id)),
],
};
private static RenderSceneGeneration Generation(ulong value) =>
RenderSceneGeneration.FromRaw(value);
}