feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -0,0 +1,896 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue