542 lines
21 KiB
C#
542 lines
21 KiB
C#
namespace AcDream.App.Rendering.Scene;
|
|
|
|
/// <summary>
|
|
/// Projection-level membership. Opaque versus alpha-cutout remains an exact
|
|
/// mesh-batch decision in the dispatcher; this product deliberately does not
|
|
/// guess from an entity's texture set.
|
|
/// </summary>
|
|
internal enum DirectionalShadowCasterKind : byte
|
|
{
|
|
OutdoorStatic,
|
|
Building,
|
|
AnimatedStatic,
|
|
LiveDynamic,
|
|
EquippedChild,
|
|
}
|
|
|
|
internal readonly record struct DirectionalShadowCaster(
|
|
RenderProjectionRecord Projection,
|
|
DirectionalShadowCasterKind Kind)
|
|
{
|
|
public bool UsesCurrentAnimatedTransforms =>
|
|
Projection.ProjectionClass
|
|
is RenderProjectionClass.ActiveAnimatedStatic
|
|
or RenderProjectionClass.LiveDynamicRoot
|
|
or RenderProjectionClass.EquippedChild;
|
|
}
|
|
|
|
internal readonly struct DirectionalShadowChangedPose
|
|
{
|
|
internal DirectionalShadowChangedPose(
|
|
int casterIndex,
|
|
in DirectionalShadowTransformSnapshot snapshot)
|
|
{
|
|
CasterIndex = casterIndex;
|
|
Snapshot = snapshot;
|
|
}
|
|
|
|
internal readonly int CasterIndex;
|
|
internal readonly DirectionalShadowTransformSnapshot Snapshot;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Accepted caster counts by the strongest class proven at render publication.
|
|
/// TerrainCommands is populated by the terrain command producer. OutdoorStatics
|
|
/// includes trees and all other outdoor DAT scenery; NonPlayerCreatures includes
|
|
/// hostile monsters and non-hostile NPC creatures because neither source carries
|
|
/// a narrower authoritative render-only discriminator.
|
|
/// </summary>
|
|
internal readonly record struct DirectionalShadowCasterClassDiagnostics(
|
|
int TerrainCommands,
|
|
int OutdoorStatics,
|
|
int Buildings,
|
|
int AnimatedStatics,
|
|
int LocalPlayers,
|
|
int RemotePlayers,
|
|
int NonPlayerCreatures,
|
|
int OtherLiveDynamics,
|
|
int EquippedChildren);
|
|
|
|
internal readonly record struct DirectionalShadowCasterBuildStats(
|
|
int SourceOutdoorStatics,
|
|
int SourceOutdoorDynamics,
|
|
int Accepted,
|
|
int RejectedNotDrawable,
|
|
int RejectedNotResident,
|
|
int RejectedTransparent,
|
|
int RejectedIndoor,
|
|
int RejectedMissingMesh,
|
|
int IndexCopies,
|
|
int Classifications,
|
|
int DynamicTransformRefreshes,
|
|
bool TopologyRebuilt,
|
|
int CopiedTransformChanges = 0,
|
|
int DedupedChangedCasterSlots = 0,
|
|
bool TransformJournalFullRefresh = false,
|
|
int UpdateTransformChanges = 0,
|
|
int UpdateAppearanceChanges = 0,
|
|
int DynamicSynchronizationChanges = 0,
|
|
int ActiveAnimatedStaticChanges = 0,
|
|
int LiveDynamicRootChanges = 0,
|
|
int EquippedChildChanges = 0,
|
|
bool DensityBulkRefresh = false,
|
|
int BatchedProjectionCopyCalls = 0)
|
|
{
|
|
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reusable, streaming-bounded caster product. It copies and classifies the
|
|
/// render scene's two resident outdoor indices only when the scene's shadow
|
|
/// topology revision changes. Stable frames retain those topology records and
|
|
/// emit only deduplicated slim root/part pose changes for prepared matrix slots.
|
|
/// </summary>
|
|
internal sealed class DirectionalShadowCasterFrame
|
|
{
|
|
private RenderProjectionRecord[] _outdoorStaticScratch = [];
|
|
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
|
|
private DirectionalShadowCaster[] _casters = [];
|
|
private int[] _refreshCasterSlots = [];
|
|
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
|
|
private bool[] _changedCasterFlags = [];
|
|
private RenderProjectionId[] _casterIds = [];
|
|
private RenderProjectionClass[] _casterClasses = [];
|
|
private RenderProjectionId[] _denseIdScratch = [];
|
|
private RenderProjectionRecord[] _denseRecordScratch = [];
|
|
private readonly DirectionalShadowTransformSnapshot[] _transformChangeScratch =
|
|
new DirectionalShadowTransformSnapshot[
|
|
DirectionalShadowTransformChangeJournal.Capacity];
|
|
private readonly Dictionary<RenderProjectionId, int> _refreshCasterSlotById = [];
|
|
private int _casterCount;
|
|
private int _refreshCasterSlotCount;
|
|
private int _changedCasterPoseCount;
|
|
private ulong _topologyRevision;
|
|
private ulong _transformRevision;
|
|
private DirectionalShadowTransformChanges _lastTransformChanges;
|
|
private bool _lastDensityBulkRefresh;
|
|
private int _lastBatchedProjectionCopyCalls;
|
|
|
|
public RenderSceneGeneration Generation { get; private set; }
|
|
|
|
public ulong BuildSequence { get; private set; }
|
|
|
|
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
|
|
_casters.AsSpan(0, _casterCount);
|
|
|
|
internal ReadOnlySpan<int> RefreshCasterSlots =>
|
|
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
|
|
|
|
internal ReadOnlySpan<DirectionalShadowChangedPose> ChangedCasterPoses =>
|
|
_changedCasterPoses.AsSpan(0, _changedCasterPoseCount);
|
|
|
|
internal ulong TransformRevision => _transformRevision;
|
|
|
|
public DirectionalShadowCasterBuildStats Stats { get; private set; }
|
|
|
|
public long RetainedScratchBytes =>
|
|
checked(
|
|
(long)_outdoorStaticScratch.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
|
+ (long)_outdoorDynamicScratch.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
|
+ (long)_casters.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
|
|
+ (long)_refreshCasterSlots.Length * sizeof(int)
|
|
+ (long)_changedCasterPoses.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
|
DirectionalShadowChangedPose>()
|
|
+ _changedCasterFlags.Length
|
|
+ (long)_casterIds.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
|
RenderProjectionId>()
|
|
+ (long)_casterClasses.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
|
RenderProjectionClass>()
|
|
+ (long)_denseIdScratch.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionId>()
|
|
+ (long)_denseRecordScratch.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
|
+ (long)_transformChangeScratch.Length
|
|
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
|
DirectionalShadowTransformSnapshot>()
|
|
+ (long)_refreshCasterSlotById.EnsureCapacity(0)
|
|
* (sizeof(int)
|
|
+ System.Runtime.CompilerServices.Unsafe.SizeOf<
|
|
KeyValuePair<RenderProjectionId, int>>()));
|
|
|
|
public void Build(in RenderSceneQuery query)
|
|
{
|
|
ulong topologyRevision = query.DirectionalShadowTopologyRevision;
|
|
if (BuildSequence != 0
|
|
&& Generation == query.Generation
|
|
&& _topologyRevision == topologyRevision)
|
|
{
|
|
int refreshes = RefreshChangedTransforms(in query);
|
|
Stats = Stats with
|
|
{
|
|
IndexCopies = 0,
|
|
Classifications = 0,
|
|
DynamicTransformRefreshes = refreshes,
|
|
TopologyRebuilt = false,
|
|
CopiedTransformChanges = _lastTransformChanges.Count,
|
|
DedupedChangedCasterSlots = _changedCasterPoseCount,
|
|
TransformJournalFullRefresh =
|
|
_lastTransformChanges.RequiresFullRefresh,
|
|
DensityBulkRefresh = _lastDensityBulkRefresh,
|
|
BatchedProjectionCopyCalls = _lastBatchedProjectionCopyCalls,
|
|
UpdateTransformChanges =
|
|
_lastTransformChanges.UpdateTransformCount,
|
|
UpdateAppearanceChanges =
|
|
_lastTransformChanges.UpdateAppearanceCount,
|
|
DynamicSynchronizationChanges =
|
|
_lastTransformChanges.DynamicSynchronizationCount,
|
|
ActiveAnimatedStaticChanges =
|
|
_lastTransformChanges.ActiveAnimatedStaticCount,
|
|
LiveDynamicRootChanges =
|
|
_lastTransformChanges.LiveDynamicRootCount,
|
|
EquippedChildChanges =
|
|
_lastTransformChanges.EquippedChildCount,
|
|
};
|
|
return;
|
|
}
|
|
|
|
RenderSceneIndexCounts counts = query.IndexCounts;
|
|
EnsureCapacity(ref _outdoorStaticScratch, counts.OutdoorStatic);
|
|
EnsureCapacity(ref _outdoorDynamicScratch, counts.OutdoorDynamic);
|
|
int staticCount = query.CopyIndexTo(
|
|
RenderSceneIndex.OutdoorStatic,
|
|
_outdoorStaticScratch.AsSpan(0, counts.OutdoorStatic));
|
|
int dynamicCount = query.CopyIndexTo(
|
|
RenderSceneIndex.OutdoorDynamic,
|
|
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
|
|
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
|
|
_casterCount = 0;
|
|
|
|
int rejectedNotDrawable = 0;
|
|
int rejectedNotResident = 0;
|
|
int rejectedTransparent = 0;
|
|
int rejectedIndoor = 0;
|
|
int rejectedMissingMesh = 0;
|
|
int outdoorStatics = 0;
|
|
int buildings = 0;
|
|
int animatedStatics = 0;
|
|
int localPlayers = 0;
|
|
int remotePlayers = 0;
|
|
int nonPlayerCreatures = 0;
|
|
int otherLiveDynamics = 0;
|
|
int equippedChildren = 0;
|
|
for (int i = 0; i < staticCount; i++)
|
|
Add(_outdoorStaticScratch[i]);
|
|
for (int i = 0; i < dynamicCount; i++)
|
|
Add(_outdoorDynamicScratch[i]);
|
|
|
|
Array.Sort(
|
|
_casters,
|
|
0,
|
|
_casterCount,
|
|
DirectionalShadowCasterComparer.Instance);
|
|
int refreshCasterCount = 0;
|
|
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
|
{
|
|
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
|
refreshCasterCount++;
|
|
}
|
|
EnsureCapacity(ref _refreshCasterSlots, refreshCasterCount);
|
|
EnsureCapacity(ref _changedCasterPoses, refreshCasterCount);
|
|
EnsureCapacity(ref _changedCasterFlags, _casterCount);
|
|
EnsureCapacity(ref _casterIds, _casterCount);
|
|
EnsureCapacity(ref _casterClasses, _casterCount);
|
|
EnsureCapacity(ref _denseIdScratch, refreshCasterCount);
|
|
EnsureCapacity(ref _denseRecordScratch, refreshCasterCount);
|
|
_refreshCasterSlotCount = 0;
|
|
_changedCasterPoseCount = 0;
|
|
_refreshCasterSlotById.Clear();
|
|
_refreshCasterSlotById.EnsureCapacity(refreshCasterCount);
|
|
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
|
{
|
|
_casterIds[casterIndex] = _casters[casterIndex].Projection.Id;
|
|
_casterClasses[casterIndex] =
|
|
_casters[casterIndex].Projection.ProjectionClass;
|
|
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
|
{
|
|
_refreshCasterSlots[_refreshCasterSlotCount++] = casterIndex;
|
|
_refreshCasterSlotById.Add(
|
|
_casterIds[casterIndex],
|
|
casterIndex);
|
|
}
|
|
}
|
|
Generation = query.Generation;
|
|
_topologyRevision = topologyRevision;
|
|
_transformRevision = query.DirectionalShadowTransformRevision;
|
|
_lastTransformChanges = default;
|
|
_lastDensityBulkRefresh = false;
|
|
_lastBatchedProjectionCopyCalls = 0;
|
|
BuildSequence = checked(BuildSequence + 1);
|
|
Stats = new DirectionalShadowCasterBuildStats(
|
|
staticCount,
|
|
dynamicCount,
|
|
_casterCount,
|
|
rejectedNotDrawable,
|
|
rejectedNotResident,
|
|
rejectedTransparent,
|
|
rejectedIndoor,
|
|
rejectedMissingMesh,
|
|
IndexCopies: 2,
|
|
Classifications: _casterCount,
|
|
DynamicTransformRefreshes: 0,
|
|
TopologyRebuilt: true)
|
|
{
|
|
CasterClasses = new DirectionalShadowCasterClassDiagnostics(
|
|
TerrainCommands: 0,
|
|
outdoorStatics,
|
|
buildings,
|
|
animatedStatics,
|
|
localPlayers,
|
|
remotePlayers,
|
|
nonPlayerCreatures,
|
|
otherLiveDynamics,
|
|
equippedChildren),
|
|
};
|
|
return;
|
|
|
|
void Add(in RenderProjectionRecord projection)
|
|
{
|
|
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
|
{
|
|
rejectedNotDrawable++;
|
|
return;
|
|
}
|
|
if ((projection.Flags & RenderProjectionFlags.SpatiallyResident) == 0)
|
|
{
|
|
rejectedNotResident++;
|
|
return;
|
|
}
|
|
// Transparent means a true blended projection. ClipMap/foliage is
|
|
// retained here and separated from opaque batches later.
|
|
if ((projection.Flags & RenderProjectionFlags.Translucent) != 0)
|
|
{
|
|
rejectedTransparent++;
|
|
return;
|
|
}
|
|
if (projection.Source.ParentCellId != 0
|
|
&& InteriorEntityPartition.IsIndoorCellId(
|
|
projection.Source.ParentCellId))
|
|
{
|
|
rejectedIndoor++;
|
|
return;
|
|
}
|
|
if (projection.MeshSet.MeshCount <= 0
|
|
|| projection.EntityPayload.MeshRefs is null
|
|
|| projection.EntityPayload.MeshRefs.Count == 0)
|
|
{
|
|
rejectedMissingMesh++;
|
|
return;
|
|
}
|
|
|
|
DirectionalShadowCasterKind kind = Classify(in projection);
|
|
_casters[_casterCount++] = new DirectionalShadowCaster(
|
|
projection,
|
|
kind);
|
|
switch (kind)
|
|
{
|
|
case DirectionalShadowCasterKind.OutdoorStatic:
|
|
outdoorStatics++;
|
|
break;
|
|
case DirectionalShadowCasterKind.Building:
|
|
buildings++;
|
|
break;
|
|
case DirectionalShadowCasterKind.AnimatedStatic:
|
|
animatedStatics++;
|
|
break;
|
|
case DirectionalShadowCasterKind.EquippedChild:
|
|
equippedChildren++;
|
|
break;
|
|
case DirectionalShadowCasterKind.LiveDynamic:
|
|
switch (projection.EntityPayload.CasterIdentity)
|
|
{
|
|
case RenderCasterIdentityKind.LocalPlayer:
|
|
localPlayers++;
|
|
break;
|
|
case RenderCasterIdentityKind.RemotePlayer:
|
|
remotePlayers++;
|
|
break;
|
|
case RenderCasterIdentityKind.NonPlayerCreature:
|
|
nonPlayerCreatures++;
|
|
break;
|
|
default:
|
|
otherLiveDynamics++;
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(kind), kind, "Unknown shadow caster kind.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private int RefreshChangedTransforms(in RenderSceneQuery query)
|
|
{
|
|
_changedCasterPoseCount = 0;
|
|
ulong latest = query.DirectionalShadowTransformRevision;
|
|
if (latest == _transformRevision)
|
|
{
|
|
_lastTransformChanges = new DirectionalShadowTransformChanges(
|
|
latest,
|
|
0,
|
|
false);
|
|
_lastDensityBulkRefresh = false;
|
|
_lastBatchedProjectionCopyCalls = 0;
|
|
return 0;
|
|
}
|
|
|
|
DirectionalShadowTransformChanges changes =
|
|
query.CopyDirectionalShadowTransformChanges(
|
|
_transformRevision,
|
|
_transformChangeScratch);
|
|
_lastTransformChanges = changes;
|
|
if (changes.RequiresFullRefresh)
|
|
{
|
|
_lastBatchedProjectionCopyCalls = 1;
|
|
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
|
{
|
|
int casterIndex = _refreshCasterSlots[index];
|
|
_denseIdScratch[index] = _casters[casterIndex].Projection.Id;
|
|
}
|
|
int copied = query.CopyById(
|
|
_denseIdScratch.AsSpan(0, _refreshCasterSlotCount),
|
|
_denseRecordScratch.AsSpan(0, _refreshCasterSlotCount));
|
|
if (copied != _refreshCasterSlotCount)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Dense directional-shadow refresh returned an incomplete record batch.");
|
|
}
|
|
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
|
{
|
|
int casterIndex = _refreshCasterSlots[index];
|
|
RefreshOne(in _denseRecordScratch[index], casterIndex);
|
|
DirectionalShadowTransformSnapshot snapshot =
|
|
DirectionalShadowTransformSnapshot.Capture(
|
|
in _denseRecordScratch[index]);
|
|
_changedCasterPoses[_changedCasterPoseCount++] =
|
|
new DirectionalShadowChangedPose(casterIndex, in snapshot);
|
|
}
|
|
_lastDensityBulkRefresh = false;
|
|
_transformRevision = changes.LatestRevision;
|
|
return _changedCasterPoseCount;
|
|
}
|
|
_lastBatchedProjectionCopyCalls = 0;
|
|
|
|
try
|
|
{
|
|
ReadOnlySpan<DirectionalShadowTransformSnapshot> records =
|
|
_transformChangeScratch.AsSpan(0, changes.Count);
|
|
// Newest-first makes repeated publications of the same projection
|
|
// resolve to the latest exact root/part payload without an ECS read.
|
|
for (int index = records.Length - 1; index >= 0; index--)
|
|
{
|
|
if (!_refreshCasterSlotById.TryGetValue(
|
|
records[index].Id,
|
|
out int casterIndex)
|
|
|| _changedCasterFlags[casterIndex])
|
|
{
|
|
continue;
|
|
}
|
|
_changedCasterFlags[casterIndex] = true;
|
|
ValidateStablePose(in records[index], casterIndex);
|
|
_changedCasterPoses[_changedCasterPoseCount++] =
|
|
new DirectionalShadowChangedPose(
|
|
casterIndex,
|
|
in records[index]);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
for (int index = 0; index < _changedCasterPoseCount; index++)
|
|
{
|
|
_changedCasterFlags[
|
|
_changedCasterPoses[index].CasterIndex] = false;
|
|
}
|
|
}
|
|
_lastDensityBulkRefresh = _refreshCasterSlotCount >= 64
|
|
&& _changedCasterPoseCount
|
|
>= checked((_refreshCasterSlotCount * 3) / 4);
|
|
_transformRevision = changes.LatestRevision;
|
|
return _changedCasterPoseCount;
|
|
}
|
|
|
|
private void ValidateStablePose(
|
|
in DirectionalShadowTransformSnapshot current,
|
|
int casterIndex)
|
|
{
|
|
if (current.Id != _casterIds[casterIndex]
|
|
|| current.ProjectionClass != _casterClasses[casterIndex])
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Stable directional-shadow topology changed caster "
|
|
+ $"{_casterIds[casterIndex]} identity or class.");
|
|
}
|
|
}
|
|
|
|
private void RefreshOne(
|
|
in RenderProjectionRecord current,
|
|
int casterIndex)
|
|
{
|
|
DirectionalShadowCaster retained = _casters[casterIndex];
|
|
if (current.Id != retained.Projection.Id
|
|
|| current.ProjectionClass != retained.Projection.ProjectionClass)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Stable directional-shadow topology changed caster "
|
|
+ $"{retained.Projection.Id} identity or class.");
|
|
}
|
|
_casters[casterIndex] = retained with { Projection = current };
|
|
}
|
|
|
|
private static DirectionalShadowCasterKind Classify(
|
|
in RenderProjectionRecord projection)
|
|
{
|
|
if (projection.EntityPayload.IsBuildingShell)
|
|
return DirectionalShadowCasterKind.Building;
|
|
return projection.ProjectionClass switch
|
|
{
|
|
RenderProjectionClass.OutdoorStatic =>
|
|
DirectionalShadowCasterKind.OutdoorStatic,
|
|
RenderProjectionClass.ActiveAnimatedStatic =>
|
|
DirectionalShadowCasterKind.AnimatedStatic,
|
|
RenderProjectionClass.LiveDynamicRoot =>
|
|
DirectionalShadowCasterKind.LiveDynamic,
|
|
RenderProjectionClass.EquippedChild =>
|
|
DirectionalShadowCasterKind.EquippedChild,
|
|
_ => throw new InvalidOperationException(
|
|
$"Outdoor shadow index carried unsupported {projection.ProjectionClass}."),
|
|
};
|
|
}
|
|
|
|
private static void EnsureCapacity<T>(ref T[] values, int required)
|
|
{
|
|
if (required < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(required));
|
|
if (values.Length >= required)
|
|
return;
|
|
int capacity = values.Length == 0 ? 4 : values.Length;
|
|
while (capacity < required)
|
|
capacity = checked(capacity * 2);
|
|
Array.Resize(ref values, capacity);
|
|
}
|
|
|
|
private sealed class DirectionalShadowCasterComparer
|
|
: IComparer<DirectionalShadowCaster>
|
|
{
|
|
public static DirectionalShadowCasterComparer Instance { get; } = new();
|
|
|
|
public int Compare(DirectionalShadowCaster left, DirectionalShadowCaster right)
|
|
{
|
|
int order = left.Projection.SortKey.Value.CompareTo(
|
|
right.Projection.SortKey.Value);
|
|
return order != 0
|
|
? order
|
|
: left.Projection.Id.CompareTo(right.Projection.Id);
|
|
}
|
|
}
|
|
}
|