acdream/tests/AcDream.App.Tests/Rendering/DirectionalShadowCasterFrameTests.cs

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;
}
}
}