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.
336 lines
12 KiB
C#
336 lines
12 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)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ActiveAnimatedSynchronization_ReclassifiesAndUpdatesOnlyActiveSource()
|
|
{
|
|
RenderSceneGeneration generation = Generation(8);
|
|
var journal = new RenderProjectionJournal(generation);
|
|
var statics = new StaticRenderProjectionJournal(journal);
|
|
WorldEntity animated = Entity(1);
|
|
WorldEntity ordinary = Entity(2);
|
|
LandblockBuild build = Build(
|
|
LandblockId,
|
|
[animated, ordinary],
|
|
includeShell: false);
|
|
statics.Reconcile(build, Publication(build));
|
|
using var scene = new ArchRenderScene(generation);
|
|
journal.DrainTo(scene);
|
|
RenderProjectionId animatedId =
|
|
StaticRenderProjectionJournal.StaticEntityId(
|
|
LandblockId,
|
|
animated.Id);
|
|
Assert.True(scene.OpenQuery().TryGet(animatedId, out var before));
|
|
|
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
|
Assert.Single(journal.Pending.ToArray());
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.Register,
|
|
journal.Pending[0].Kind);
|
|
Assert.Equal(
|
|
RenderProjectionClass.ActiveAnimatedStatic,
|
|
journal.Pending[0].Record.ProjectionClass);
|
|
Assert.Equal(
|
|
before.OwnerIncarnation,
|
|
journal.Pending[0].Record.OwnerIncarnation);
|
|
journal.DrainTo(scene);
|
|
|
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
|
Assert.Equal(0, journal.Count);
|
|
|
|
animated.Rotation =
|
|
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
|
Assert.Single(journal.Pending.ToArray());
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.UpdateTransform,
|
|
journal.Pending[0].Kind);
|
|
Assert.True(statics.TryGet(animatedId, out var updated));
|
|
Assert.Equal(animated.Rotation, updated.Transform.Rotation);
|
|
RenderProjectionId ordinaryId =
|
|
StaticRenderProjectionJournal.StaticEntityId(
|
|
LandblockId,
|
|
ordinary.Id);
|
|
Assert.True(statics.TryGet(ordinaryId, out var unchanged));
|
|
Assert.Equal(
|
|
RenderProjectionClass.OutdoorStatic,
|
|
unchanged.ProjectionClass);
|
|
}
|
|
|
|
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);
|
|
}
|