896 lines
34 KiB
C#
896 lines
34 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class DirectionalShadowCasterFrameTests
|
|
{
|
|
[Fact]
|
|
public void Build_IncludesEveryHeadlineProjectionClassAndExcludesTrueTransparency()
|
|
{
|
|
RenderProjectionRecord[] statics =
|
|
[
|
|
Record(1, RenderProjectionClass.OutdoorStatic),
|
|
Record(2, RenderProjectionClass.OutdoorStatic, building: true),
|
|
Record(3, RenderProjectionClass.ActiveAnimatedStatic),
|
|
Record(4, RenderProjectionClass.OutdoorStatic,
|
|
extraFlags: RenderProjectionFlags.Translucent),
|
|
Record(5, RenderProjectionClass.OutdoorStatic, parentCell: 0x12340101),
|
|
];
|
|
RenderProjectionRecord[] dynamics =
|
|
[
|
|
Record(6, RenderProjectionClass.LiveDynamicRoot),
|
|
Record(7, RenderProjectionClass.EquippedChild),
|
|
];
|
|
var source = new QuerySource(statics, dynamics);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
|
|
frame.Build(new RenderSceneQuery(source, Generation));
|
|
|
|
DirectionalShadowCasterKind[] kinds = frame.Casters
|
|
.ToArray()
|
|
.Select(static value => value.Kind)
|
|
.ToArray();
|
|
Assert.Contains(DirectionalShadowCasterKind.OutdoorStatic, kinds);
|
|
Assert.Contains(DirectionalShadowCasterKind.Building, kinds);
|
|
Assert.Contains(DirectionalShadowCasterKind.AnimatedStatic, kinds);
|
|
Assert.Contains(DirectionalShadowCasterKind.LiveDynamic, kinds);
|
|
Assert.Contains(DirectionalShadowCasterKind.EquippedChild, kinds);
|
|
Assert.Equal(5, frame.Casters.Length);
|
|
Assert.Equal(1, frame.Stats.RejectedTransparent);
|
|
Assert.Equal(1, frame.Stats.RejectedIndoor);
|
|
Assert.True(frame.Casters[2].UsesCurrentAnimatedTransforms);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_ReportsEveryAuthoritativeCasterClassAndPreservesCountsOnStableFrame()
|
|
{
|
|
RenderProjectionRecord[] statics =
|
|
[
|
|
Record(101, RenderProjectionClass.OutdoorStatic),
|
|
Record(102, RenderProjectionClass.OutdoorStatic, building: true),
|
|
Record(103, RenderProjectionClass.ActiveAnimatedStatic),
|
|
];
|
|
RenderProjectionRecord[] dynamics =
|
|
[
|
|
Record(104, RenderProjectionClass.LiveDynamicRoot,
|
|
casterIdentity: RenderCasterIdentityKind.LocalPlayer),
|
|
Record(105, RenderProjectionClass.LiveDynamicRoot,
|
|
casterIdentity: RenderCasterIdentityKind.RemotePlayer),
|
|
Record(106, RenderProjectionClass.LiveDynamicRoot,
|
|
casterIdentity: RenderCasterIdentityKind.NonPlayerCreature),
|
|
Record(107, RenderProjectionClass.LiveDynamicRoot,
|
|
casterIdentity: RenderCasterIdentityKind.OtherLiveDynamic),
|
|
Record(108, RenderProjectionClass.EquippedChild,
|
|
casterIdentity: RenderCasterIdentityKind.EquippedChild),
|
|
];
|
|
var source = new QuerySource(statics, dynamics);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
|
|
frame.Build(new RenderSceneQuery(source, Generation));
|
|
DirectionalShadowCasterClassDiagnostics first = frame.Stats.CasterClasses;
|
|
|
|
Assert.Equal(0, first.TerrainCommands);
|
|
Assert.Equal(1, first.OutdoorStatics);
|
|
Assert.Equal(1, first.Buildings);
|
|
Assert.Equal(1, first.AnimatedStatics);
|
|
Assert.Equal(1, first.LocalPlayers);
|
|
Assert.Equal(1, first.RemotePlayers);
|
|
Assert.Equal(1, first.NonPlayerCreatures);
|
|
Assert.Equal(1, first.OtherLiveDynamics);
|
|
Assert.Equal(1, first.EquippedChildren);
|
|
|
|
frame.Build(new RenderSceneQuery(source, Generation));
|
|
|
|
Assert.Equal(first, frame.Stats.CasterClasses);
|
|
Assert.False(frame.Stats.TopologyRebuilt);
|
|
Assert.Equal(0, frame.Stats.Classifications);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_ReadsOnlyTwoResidentIndicesOnce_NoPViewCellOrCascadeRecull()
|
|
{
|
|
var source = new QuerySource(
|
|
[Record(20, RenderProjectionClass.OutdoorStatic)],
|
|
[Record(21, RenderProjectionClass.LiveDynamicRoot)]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
|
|
frame.Build(new RenderSceneQuery(source, Generation));
|
|
|
|
Assert.Equal(1, source.IndexCountReads);
|
|
Assert.Equal(2, source.IndexCopies);
|
|
Assert.Equal(
|
|
[RenderSceneIndex.OutdoorStatic, RenderSceneIndex.OutdoorDynamic],
|
|
source.CopiedIndices);
|
|
Assert.Equal(0, source.CellQueries);
|
|
Assert.Equal(2, frame.Stats.IndexCopies);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnchangedSecondFrame_ReusesSortedTopologyWithoutIndexCopiesOrClassification()
|
|
{
|
|
var source = new QuerySource(
|
|
[
|
|
Record(30, RenderProjectionClass.OutdoorStatic, sortKey: 30),
|
|
Record(31, RenderProjectionClass.ActiveAnimatedStatic, sortKey: 10),
|
|
],
|
|
[Record(32, RenderProjectionClass.LiveDynamicRoot, sortKey: 20)]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
|
|
frame.Build(in query);
|
|
long retained = frame.RetainedScratchBytes;
|
|
ulong[] firstOrder = frame.Casters
|
|
.ToArray()
|
|
.Select(static value => value.Projection.Id.RawValue)
|
|
.ToArray();
|
|
int indexReads = source.IndexCountReads;
|
|
int indexCopies = source.IndexCopies;
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal([31ul, 32ul, 30ul], firstOrder);
|
|
Assert.Equal([0, 1], frame.RefreshCasterSlots.ToArray());
|
|
Assert.Equal(retained, frame.RetainedScratchBytes);
|
|
Assert.Equal(1ul, frame.BuildSequence);
|
|
Assert.Equal(indexReads, source.IndexCountReads);
|
|
Assert.Equal(indexCopies, source.IndexCopies);
|
|
Assert.False(frame.Stats.TopologyRebuilt);
|
|
Assert.Equal(0, frame.Stats.IndexCopies);
|
|
Assert.Equal(0, frame.Stats.Classifications);
|
|
Assert.Equal(0, frame.Stats.DynamicTransformRefreshes);
|
|
Assert.Empty(frame.ChangedCasterPoses.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void TopologyRebuild_ReplacesRefreshSlotsAndRetainedAccountingIncludesThem()
|
|
{
|
|
RenderProjectionRecord[] statics =
|
|
[
|
|
Record(33, RenderProjectionClass.OutdoorStatic),
|
|
Record(34, RenderProjectionClass.OutdoorStatic),
|
|
Record(35, RenderProjectionClass.OutdoorStatic),
|
|
Record(36, RenderProjectionClass.OutdoorStatic),
|
|
];
|
|
var source = new QuerySource(statics, []);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
|
|
frame.Build(in query);
|
|
long staticRetainedBytes = frame.RetainedScratchBytes;
|
|
|
|
Assert.Empty(frame.RefreshCasterSlots.ToArray());
|
|
|
|
source.ReplaceStatics(
|
|
[
|
|
Record(33, RenderProjectionClass.ActiveAnimatedStatic),
|
|
Record(34, RenderProjectionClass.ActiveAnimatedStatic),
|
|
Record(35, RenderProjectionClass.ActiveAnimatedStatic),
|
|
Record(36, RenderProjectionClass.ActiveAnimatedStatic),
|
|
],
|
|
topologyChanged: true);
|
|
frame.Build(in query);
|
|
long animatedRetainedBytes = frame.RetainedScratchBytes;
|
|
|
|
Assert.True(animatedRetainedBytes > staticRetainedBytes);
|
|
Assert.Equal([0, 1, 2, 3], frame.RefreshCasterSlots.ToArray());
|
|
|
|
source.ReplaceStatics(statics, topologyChanged: true);
|
|
frame.Build(in query);
|
|
int projectionReads = source.ProjectionReads;
|
|
frame.Build(in query);
|
|
|
|
Assert.Empty(frame.RefreshCasterSlots.ToArray());
|
|
Assert.Equal(animatedRetainedBytes, frame.RetainedScratchBytes);
|
|
Assert.Equal(projectionReads, source.ProjectionReads);
|
|
Assert.Equal(0, frame.Stats.DynamicTransformRefreshes);
|
|
Assert.False(frame.Stats.TopologyRebuilt);
|
|
}
|
|
|
|
[Fact]
|
|
public void StableTopology_EmitsExactDynamicPoseWithoutOverwritingTopologyCaster()
|
|
{
|
|
RenderProjectionRecord original =
|
|
Record(40, RenderProjectionClass.LiveDynamicRoot);
|
|
var source = new QuerySource([], [original]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
|
|
float rootX = BitConverter.Int32BitsToSingle(0x41234567);
|
|
float partY = BitConverter.Int32BitsToSingle(0x40ABCDEF);
|
|
Matrix4x4 root = Matrix4x4.CreateTranslation(rootX, 2f, 3f);
|
|
Matrix4x4 part = Matrix4x4.CreateTranslation(4f, partY, 6f);
|
|
RenderProjectionRecord moved = original with
|
|
{
|
|
Transform = new RenderTransform(root),
|
|
EntityPayload = original.EntityPayload with
|
|
{
|
|
MeshRefs = [new MeshRef(40, part)],
|
|
},
|
|
};
|
|
source.ReplaceDynamics([moved], topologyChanged: false);
|
|
|
|
frame.Build(in query);
|
|
|
|
RenderProjectionRecord retained = frame.Casters[0].Projection;
|
|
Assert.Equal(original.Transform, retained.Transform);
|
|
Assert.Same(
|
|
original.EntityPayload.MeshRefs,
|
|
retained.EntityPayload.MeshRefs);
|
|
DirectionalShadowChangedPose changed =
|
|
Assert.Single(frame.ChangedCasterPoses.ToArray());
|
|
Assert.Equal(0, changed.CasterIndex);
|
|
Assert.Equal(
|
|
BitConverter.SingleToInt32Bits(rootX),
|
|
BitConverter.SingleToInt32Bits(
|
|
changed.Snapshot.Transform.LocalToWorld.M41));
|
|
Assert.Equal(
|
|
BitConverter.SingleToInt32Bits(partY),
|
|
BitConverter.SingleToInt32Bits(
|
|
changed.Snapshot.EntityPayload.MeshRefs[0].PartTransform.M42));
|
|
Assert.Equal(1ul, frame.BuildSequence);
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
}
|
|
|
|
[Fact]
|
|
public void StableTopology_RefreshesOnlyChangedCasterSlots()
|
|
{
|
|
RenderProjectionRecord first =
|
|
Record(41, RenderProjectionClass.LiveDynamicRoot);
|
|
RenderProjectionRecord second =
|
|
Record(42, RenderProjectionClass.EquippedChild);
|
|
var source = new QuerySource([], [first, second]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
RenderProjectionRecord moved = second with
|
|
{
|
|
Transform = new RenderTransform(
|
|
Matrix4x4.CreateTranslation(420f, 2f, 3f)),
|
|
};
|
|
|
|
source.ReplaceDynamics([first, moved], topologyChanged: false);
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
Assert.Equal(
|
|
[1],
|
|
frame.ChangedCasterPoses.ToArray()
|
|
.Select(static changed => changed.CasterIndex));
|
|
Assert.Equal(first.Transform, frame.Casters[0].Projection.Transform);
|
|
Assert.Equal(second.Transform, frame.Casters[1].Projection.Transform);
|
|
Assert.Equal(
|
|
moved.Transform,
|
|
frame.ChangedCasterPoses[0].Snapshot.Transform);
|
|
}
|
|
|
|
[Fact]
|
|
public void RepeatedSameId_ConsumesLatestJournalRecordWithoutSceneRead()
|
|
{
|
|
RenderProjectionRecord original =
|
|
Record(46, RenderProjectionClass.LiveDynamicRoot);
|
|
var source = new QuerySource([], [original]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
RenderProjectionRecord intermediate = original with
|
|
{
|
|
Transform = new RenderTransform(
|
|
Matrix4x4.CreateTranslation(100f, 101f, 102f)),
|
|
EntityPayload = original.EntityPayload with
|
|
{
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
46,
|
|
Matrix4x4.CreateTranslation(103f, 104f, 105f)),
|
|
],
|
|
},
|
|
};
|
|
float rootX = BitConverter.Int32BitsToSingle(0x41234567);
|
|
float partY = BitConverter.Int32BitsToSingle(0x40ABCDEF);
|
|
RenderProjectionRecord latest = intermediate with
|
|
{
|
|
Transform = new RenderTransform(
|
|
Matrix4x4.CreateTranslation(rootX, 201f, 202f)),
|
|
EntityPayload = intermediate.EntityPayload with
|
|
{
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
46,
|
|
Matrix4x4.CreateTranslation(203f, partY, 205f)),
|
|
],
|
|
},
|
|
};
|
|
source.PublishTransformRecord(in intermediate);
|
|
source.PublishTransformRecord(in latest);
|
|
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal(2, frame.Stats.CopiedTransformChanges);
|
|
Assert.Equal(1, frame.Stats.DedupedChangedCasterSlots);
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
Assert.Equal(0, source.BatchedProjectionCopies);
|
|
Assert.Equal(original.Transform, frame.Casters[0].Projection.Transform);
|
|
DirectionalShadowTransformSnapshot current =
|
|
frame.ChangedCasterPoses[0].Snapshot;
|
|
Assert.Equal(
|
|
BitConverter.SingleToInt32Bits(rootX),
|
|
BitConverter.SingleToInt32Bits(current.Transform.LocalToWorld.M41));
|
|
Assert.Equal(
|
|
BitConverter.SingleToInt32Bits(partY),
|
|
BitConverter.SingleToInt32Bits(
|
|
current.EntityPayload.MeshRefs[0].PartTransform.M42));
|
|
}
|
|
|
|
[Fact]
|
|
public void TransformJournalOverflow_FallsBackToExactFullDynamicRefresh()
|
|
{
|
|
RenderProjectionRecord first =
|
|
Record(43, RenderProjectionClass.LiveDynamicRoot);
|
|
RenderProjectionRecord second =
|
|
Record(44, RenderProjectionClass.EquippedChild);
|
|
var source = new QuerySource([], [first, second]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
source.PublishTransformChanges(
|
|
first.Id,
|
|
DirectionalShadowTransformChangeJournal.Capacity + 1);
|
|
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
Assert.Equal(1, source.BatchedProjectionCopies);
|
|
Assert.Equal(
|
|
[0, 1],
|
|
frame.ChangedCasterPoses.ToArray()
|
|
.Select(static changed => changed.CasterIndex));
|
|
Assert.Equal(2, frame.Stats.DynamicTransformRefreshes);
|
|
}
|
|
|
|
[Fact]
|
|
public void DenseChangedSet_UsesExactBulkRefreshInsteadOfSparseDictionaryWalk()
|
|
{
|
|
var dynamics = new RenderProjectionRecord[100];
|
|
for (int index = 0; index < dynamics.Length; index++)
|
|
{
|
|
dynamics[index] = Record(
|
|
checked((ulong)(4_500 + index)),
|
|
RenderProjectionClass.ActiveAnimatedStatic);
|
|
}
|
|
var source = new QuerySource(dynamics, []);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
for (int index = 0; index < 75; index++)
|
|
source.PublishTransformChanges(dynamics[index].Id, 1);
|
|
|
|
frame.Build(in query);
|
|
|
|
Assert.True(frame.Stats.DensityBulkRefresh);
|
|
Assert.Equal(75, frame.Stats.CopiedTransformChanges);
|
|
Assert.Equal(75, frame.Stats.DynamicTransformRefreshes);
|
|
Assert.Equal(0, frame.Stats.BatchedProjectionCopyCalls);
|
|
Assert.Equal(0, source.BatchedProjectionCopies);
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemovalAndRevisit_RebuildTopologyAndDiscardPriorTransformCursor()
|
|
{
|
|
RenderProjectionRecord original =
|
|
Record(45, RenderProjectionClass.LiveDynamicRoot);
|
|
var source = new QuerySource([], [original]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
source.ReplaceDynamics([], topologyChanged: true);
|
|
|
|
frame.Build(in query);
|
|
|
|
Assert.Empty(frame.Casters.ToArray());
|
|
Assert.Equal(2ul, frame.BuildSequence);
|
|
RenderProjectionRecord revisited = original with
|
|
{
|
|
Transform = new RenderTransform(
|
|
Matrix4x4.CreateTranslation(450f, 2f, 3f)),
|
|
};
|
|
source.ReplaceDynamics([revisited], topologyChanged: true);
|
|
frame.Build(in query);
|
|
int projectionReads = source.ProjectionReads;
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal(3ul, frame.BuildSequence);
|
|
Assert.Equal(revisited.Transform, frame.Casters[0].Projection.Transform);
|
|
Assert.Equal(projectionReads, source.ProjectionReads);
|
|
Assert.Empty(frame.ChangedCasterPoses.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void MembershipOrAppearanceRevision_RebuildsTopology()
|
|
{
|
|
RenderProjectionRecord original =
|
|
Record(50, RenderProjectionClass.OutdoorStatic);
|
|
var source = new QuerySource([original], []);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
|
|
RenderProjectionRecord changed = original with
|
|
{
|
|
EntityPayload = original.EntityPayload with
|
|
{
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(51, original.EntityPayload.MeshRefs[0].PartTransform)
|
|
{
|
|
SurfaceOverrides = new Dictionary<uint, uint>
|
|
{
|
|
[0x08000001] = 0x05000001,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
source.ReplaceStatics([changed], topologyChanged: true);
|
|
|
|
frame.Build(in query);
|
|
|
|
Assert.Equal(2ul, frame.BuildSequence);
|
|
Assert.True(frame.Stats.TopologyRebuilt);
|
|
Assert.Equal(2, frame.Stats.IndexCopies);
|
|
Assert.Equal(1, frame.Stats.Classifications);
|
|
Assert.Equal((uint)51, frame.Casters[0].Projection.EntityPayload.MeshRefs[0].GfxObjId);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmStableFrame_AllocatesZero()
|
|
{
|
|
var statics = new RenderProjectionRecord[9_500];
|
|
for (int index = 0; index < statics.Length; index++)
|
|
{
|
|
statics[index] = Record(
|
|
checked((ulong)(index + 60)),
|
|
RenderProjectionClass.OutdoorStatic);
|
|
}
|
|
var source = new QuerySource(
|
|
statics,
|
|
[Record(10_000, RenderProjectionClass.LiveDynamicRoot)]);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
frame.Build(in query);
|
|
int copies = source.IndexCopies;
|
|
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int iteration = 0; iteration < 256; iteration++)
|
|
frame.Build(in query);
|
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
|
|
Assert.Equal(0, allocated);
|
|
Assert.Equal(1ul, frame.BuildSequence);
|
|
Assert.Equal(copies, source.IndexCopies);
|
|
Assert.Equal(0, frame.Stats.Classifications);
|
|
Assert.Equal(0, frame.Stats.DynamicTransformRefreshes);
|
|
Assert.False(frame.Stats.TopologyRebuilt);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmDenseChangedFrames_AllocateZeroAndReadNoSceneRecords()
|
|
{
|
|
var dynamics = new RenderProjectionRecord[100];
|
|
for (int index = 0; index < dynamics.Length; index++)
|
|
{
|
|
dynamics[index] = Record(
|
|
checked((ulong)(20_000 + index)),
|
|
RenderProjectionClass.ActiveAnimatedStatic);
|
|
}
|
|
var source = new QuerySource(dynamics, []);
|
|
var frame = new DirectionalShadowCasterFrame();
|
|
RenderSceneQuery query = new(source, Generation);
|
|
frame.Build(in query);
|
|
source.EnsureTransformChangeCapacity(5_000);
|
|
for (int index = 0; index < 75; index++)
|
|
source.PublishTransformChanges(dynamics[index].Id, 1);
|
|
frame.Build(in query);
|
|
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int iteration = 0; iteration < 64; iteration++)
|
|
{
|
|
for (int index = 0; index < 75; index++)
|
|
source.PublishTransformChanges(dynamics[index].Id, 1);
|
|
frame.Build(in query);
|
|
}
|
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
|
|
Assert.Equal(0, allocated);
|
|
Assert.True(frame.Stats.DensityBulkRefresh);
|
|
Assert.Equal(75, frame.Stats.DynamicTransformRefreshes);
|
|
Assert.Equal(0, source.ProjectionReads);
|
|
Assert.Equal(0, source.BatchedProjectionCopies);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionTopologyFingerprint_ExcludesPoseButIncludesGeometryAndSurfaceOverrides()
|
|
{
|
|
var entity = new WorldEntity
|
|
{
|
|
Id = 1,
|
|
SourceGfxObjOrSetupId = 0x02000001,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
0x01000001,
|
|
Matrix4x4.CreateTranslation(1f, 2f, 3f)),
|
|
],
|
|
};
|
|
RenderSceneHash128 original = CurrentRenderSceneOracle
|
|
.CreateDirectionalShadowTopologyFingerprint(entity);
|
|
entity.MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
0x01000001,
|
|
Matrix4x4.CreateTranslation(10f, 20f, 30f)),
|
|
];
|
|
RenderSceneHash128 poseOnly = CurrentRenderSceneOracle
|
|
.CreateDirectionalShadowTopologyFingerprint(entity);
|
|
entity.MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
0x01000002,
|
|
Matrix4x4.CreateTranslation(10f, 20f, 30f))
|
|
{
|
|
SurfaceOverrides = new Dictionary<uint, uint>
|
|
{
|
|
[0x08000001] = 0x05000001,
|
|
},
|
|
},
|
|
];
|
|
RenderSceneHash128 changed = CurrentRenderSceneOracle
|
|
.CreateDirectionalShadowTopologyFingerprint(entity);
|
|
|
|
Assert.Equal(original, poseOnly);
|
|
Assert.NotEqual(original, changed);
|
|
}
|
|
|
|
private static readonly RenderSceneGeneration Generation =
|
|
RenderSceneGeneration.FromRaw(9);
|
|
|
|
private static RenderProjectionRecord Record(
|
|
ulong id,
|
|
RenderProjectionClass projectionClass,
|
|
bool building = false,
|
|
RenderProjectionFlags extraFlags = RenderProjectionFlags.None,
|
|
uint parentCell = 0,
|
|
ulong? sortKey = null,
|
|
RenderCasterIdentityKind casterIdentity =
|
|
RenderCasterIdentityKind.Unclassified)
|
|
{
|
|
Matrix4x4 transform = Matrix4x4.CreateTranslation((float)id, 0f, 0f);
|
|
return new RenderProjectionRecord() with
|
|
{
|
|
Id = RenderProjectionId.FromRaw(id),
|
|
ProjectionClass = projectionClass,
|
|
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
|
|
Transform = new RenderTransform(transform),
|
|
PreviousTransform = new PreviousRenderTransform(transform),
|
|
MeshSet = new RenderMeshSet(RenderAssetHandle.FromRaw(id), 1, 1),
|
|
Material = new RenderMaterialVariant(0, 0, 1),
|
|
Residency = new RenderSpatialResidency(
|
|
RenderSpatialBucket.FromRaw(id),
|
|
0x1234FFFF,
|
|
parentCell),
|
|
Bounds = new RenderWorldBounds(Vector3.Zero, Vector3.One),
|
|
Flags = RenderProjectionFlags.Draw
|
|
| RenderProjectionFlags.SpatiallyResident
|
|
| extraFlags,
|
|
SortKey = new RenderSortKey(sortKey ?? id),
|
|
Source = new RenderSourceMetadata(
|
|
LocalEntityId: (uint)id,
|
|
ServerGuid: projectionClass is RenderProjectionClass.LiveDynamicRoot
|
|
or RenderProjectionClass.EquippedChild
|
|
? (uint)id
|
|
: 0,
|
|
SourceId: (uint)id,
|
|
ParentCellId: parentCell,
|
|
EffectCellId: 0,
|
|
BuildingShellAnchorCellId: 0,
|
|
TransformFingerprint: default,
|
|
GeometryFingerprint: default,
|
|
AppearanceFingerprint: default),
|
|
EntityPayload = new RenderEntityPayload(
|
|
[new MeshRef((uint)id, transform)],
|
|
PaletteOverride: null,
|
|
IsBuildingShell: building,
|
|
CasterIdentity: casterIdentity),
|
|
};
|
|
}
|
|
|
|
private sealed class QuerySource : IRenderSceneQuerySource
|
|
{
|
|
private RenderProjectionRecord[] _statics;
|
|
private RenderProjectionRecord[] _dynamics;
|
|
|
|
public QuerySource(
|
|
RenderProjectionRecord[] statics,
|
|
RenderProjectionRecord[] dynamics)
|
|
{
|
|
_statics = statics;
|
|
_dynamics = dynamics;
|
|
}
|
|
|
|
public int IndexCountReads { get; private set; }
|
|
public int IndexCopies { get; private set; }
|
|
public int CellQueries { get; private set; }
|
|
public int ProjectionReads { get; private set; }
|
|
public int BatchedProjectionCopies { get; private set; }
|
|
public ulong TopologyRevision { get; private set; } = 1;
|
|
public ulong TransformRevision { get; private set; } = 1;
|
|
public List<RenderSceneIndex> CopiedIndices { get; } = [];
|
|
private readonly List<DirectionalShadowTransformSnapshot>
|
|
_transformChanges = [];
|
|
|
|
public RenderProjectionCounts GetCounts(RenderSceneGeneration generation) =>
|
|
throw new InvalidOperationException("The caster product must not enumerate the whole scene.");
|
|
|
|
public RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation)
|
|
{
|
|
IndexCountReads++;
|
|
return new RenderSceneIndexCounts(
|
|
OutdoorStatic: _statics.Length,
|
|
IndoorCellStatic: 0,
|
|
Dynamic: _dynamics.Length,
|
|
OutdoorDynamic: _dynamics.Length,
|
|
PortalStraddlingDynamic: 0,
|
|
Translucent: 0,
|
|
Selectable: 0,
|
|
LightCandidate: 0,
|
|
Dirty: 0);
|
|
}
|
|
|
|
public ulong GetIndexRevision(RenderSceneGeneration generation) => 1;
|
|
|
|
public ulong GetDirectionalShadowTopologyRevision(
|
|
RenderSceneGeneration generation) => TopologyRevision;
|
|
|
|
public ulong GetDirectionalShadowTransformRevision(
|
|
RenderSceneGeneration generation) => TransformRevision;
|
|
|
|
public DirectionalShadowTransformChanges
|
|
CopyDirectionalShadowTransformChanges(
|
|
RenderSceneGeneration generation,
|
|
ulong afterRevision,
|
|
Span<DirectionalShadowTransformSnapshot> destination)
|
|
{
|
|
if (afterRevision == TransformRevision)
|
|
{
|
|
return new DirectionalShadowTransformChanges(
|
|
TransformRevision,
|
|
0,
|
|
false);
|
|
}
|
|
ulong delta = TransformRevision - afterRevision;
|
|
if (afterRevision == 0
|
|
|| afterRevision > TransformRevision
|
|
|| delta > (ulong)_transformChanges.Count
|
|
|| delta > (ulong)destination.Length)
|
|
{
|
|
return new DirectionalShadowTransformChanges(
|
|
TransformRevision,
|
|
0,
|
|
true);
|
|
}
|
|
int count = checked((int)delta);
|
|
int start = _transformChanges.Count - count;
|
|
for (int index = 0; index < count; index++)
|
|
destination[index] = _transformChanges[start + index];
|
|
return new DirectionalShadowTransformChanges(
|
|
TransformRevision,
|
|
count,
|
|
false);
|
|
}
|
|
|
|
public bool TryGet(
|
|
RenderSceneGeneration generation,
|
|
RenderProjectionId id,
|
|
out RenderProjectionRecord record)
|
|
{
|
|
ProjectionReads++;
|
|
for (int index = 0; index < _statics.Length; index++)
|
|
{
|
|
if (_statics[index].Id == id)
|
|
{
|
|
record = _statics[index];
|
|
return true;
|
|
}
|
|
}
|
|
for (int index = 0; index < _dynamics.Length; index++)
|
|
{
|
|
if (_dynamics[index].Id == id)
|
|
{
|
|
record = _dynamics[index];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
record = default;
|
|
return false;
|
|
}
|
|
|
|
public int CopyById(
|
|
RenderSceneGeneration generation,
|
|
ReadOnlySpan<RenderProjectionId> ids,
|
|
Span<RenderProjectionRecord> destination)
|
|
{
|
|
BatchedProjectionCopies++;
|
|
if (destination.Length < ids.Length)
|
|
throw new ArgumentException("Destination is too small.", nameof(destination));
|
|
for (int outputIndex = 0; outputIndex < ids.Length; outputIndex++)
|
|
{
|
|
bool found = false;
|
|
for (int index = 0; index < _statics.Length; index++)
|
|
{
|
|
if (_statics[index].Id != ids[outputIndex])
|
|
continue;
|
|
destination[outputIndex] = _statics[index];
|
|
found = true;
|
|
break;
|
|
}
|
|
if (!found)
|
|
{
|
|
for (int index = 0; index < _dynamics.Length; index++)
|
|
{
|
|
if (_dynamics[index].Id != ids[outputIndex])
|
|
continue;
|
|
destination[outputIndex] = _dynamics[index];
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
throw new InvalidOperationException("Missing projection.");
|
|
}
|
|
return ids.Length;
|
|
}
|
|
|
|
public int CopyTo(
|
|
RenderSceneGeneration generation,
|
|
RenderProjectionClass? projectionClass,
|
|
Span<RenderProjectionRecord> destination) =>
|
|
throw new InvalidOperationException("The caster product must not enumerate the whole scene.");
|
|
|
|
public int CopyIndexTo(
|
|
RenderSceneGeneration generation,
|
|
RenderSceneIndex index,
|
|
Span<RenderProjectionRecord> destination)
|
|
{
|
|
IndexCopies++;
|
|
CopiedIndices.Add(index);
|
|
RenderProjectionRecord[] values = index switch
|
|
{
|
|
RenderSceneIndex.OutdoorStatic => _statics,
|
|
RenderSceneIndex.OutdoorDynamic => _dynamics,
|
|
_ => throw new InvalidOperationException(
|
|
$"Unexpected directional-shadow source index {index}."),
|
|
};
|
|
values.CopyTo(destination);
|
|
return values.Length;
|
|
}
|
|
|
|
public int GetCellCount(
|
|
RenderSceneGeneration generation,
|
|
uint fullCellId,
|
|
bool dynamic)
|
|
{
|
|
CellQueries++;
|
|
throw new InvalidOperationException("Directional shadows do not query PView cells.");
|
|
}
|
|
|
|
public int CopyCellTo(
|
|
RenderSceneGeneration generation,
|
|
uint fullCellId,
|
|
bool dynamic,
|
|
Span<RenderProjectionRecord> destination)
|
|
{
|
|
CellQueries++;
|
|
throw new InvalidOperationException("Directional shadows do not query PView cells.");
|
|
}
|
|
|
|
|
|
public void ReplaceStatics(
|
|
RenderProjectionRecord[] values,
|
|
bool topologyChanged)
|
|
{
|
|
_statics = values;
|
|
if (topologyChanged)
|
|
TopologyRevision++;
|
|
}
|
|
|
|
public void ReplaceDynamics(
|
|
RenderProjectionRecord[] values,
|
|
bool topologyChanged)
|
|
{
|
|
RenderProjectionRecord[] previous = _dynamics;
|
|
_dynamics = values;
|
|
if (topologyChanged)
|
|
TopologyRevision++;
|
|
else
|
|
{
|
|
for (int index = 0; index < values.Length; index++)
|
|
{
|
|
RenderProjectionRecord current = values[index];
|
|
RenderProjectionRecord prior = previous.FirstOrDefault(
|
|
value => value.Id == current.Id);
|
|
if (prior.Id == current.Id
|
|
&& prior.Transform == current.Transform
|
|
&& PartTransformsEqual(in prior, in current))
|
|
{
|
|
continue;
|
|
}
|
|
PublishTransformChanges(current.Id, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PublishTransformChanges(RenderProjectionId id, int count)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
|
RenderProjectionRecord record = Find(id);
|
|
for (int index = 0; index < count; index++)
|
|
{
|
|
_transformChanges.Add(
|
|
DirectionalShadowTransformSnapshot.Capture(in record));
|
|
TransformRevision++;
|
|
}
|
|
}
|
|
|
|
public void PublishTransformRecord(in RenderProjectionRecord record)
|
|
{
|
|
_transformChanges.Add(
|
|
DirectionalShadowTransformSnapshot.Capture(in record));
|
|
TransformRevision++;
|
|
}
|
|
|
|
public void EnsureTransformChangeCapacity(int capacity) =>
|
|
_transformChanges.EnsureCapacity(capacity);
|
|
|
|
private RenderProjectionRecord Find(RenderProjectionId id)
|
|
{
|
|
for (int index = 0; index < _statics.Length; index++)
|
|
{
|
|
if (_statics[index].Id == id)
|
|
return _statics[index];
|
|
}
|
|
for (int index = 0; index < _dynamics.Length; index++)
|
|
{
|
|
if (_dynamics[index].Id == id)
|
|
return _dynamics[index];
|
|
}
|
|
throw new InvalidOperationException("Missing projection.");
|
|
}
|
|
|
|
private static bool PartTransformsEqual(
|
|
in RenderProjectionRecord left,
|
|
in RenderProjectionRecord right)
|
|
{
|
|
IReadOnlyList<MeshRef> leftMeshes = left.EntityPayload.MeshRefs;
|
|
IReadOnlyList<MeshRef> rightMeshes = right.EntityPayload.MeshRefs;
|
|
if (leftMeshes.Count != rightMeshes.Count)
|
|
return false;
|
|
for (int index = 0; index < leftMeshes.Count; index++)
|
|
{
|
|
if (leftMeshes[index].PartTransform
|
|
!= rightMeshes[index].PartTransform)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|