feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -1,4 +1,6 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using Arch.Core;
using ArchWorld = Arch.Core.World;
@ -37,6 +39,12 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
private RenderProjectionCounts _counts;
private ulong _lastAppliedJournalSequence;
private ulong _indexRevision = 1;
private ulong _directionalShadowTopologyRevision = 1;
private DirectionalShadowTransformChange[]? _directionalShadowTransformChanges;
private Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
_directionalShadowPartPoses;
private ulong _directionalShadowTransformRevision;
private int _directionalShadowTransformChangeCount;
private bool _disposed;
public ArchRenderScene(RenderSceneGeneration initialGeneration)
@ -76,6 +84,23 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
long lookupBytes =
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
long indexBytes = EstimateIndexBytes();
long directionalShadowJournalBytes =
_directionalShadowTransformChanges is null
? 0
: checked((long)_directionalShadowTransformChanges.Length
* Unsafe.SizeOf<DirectionalShadowTransformChange>());
if (_directionalShadowPartPoses is not null)
{
directionalShadowJournalBytes = checked(
directionalShadowJournalBytes
+ (long)_directionalShadowPartPoses.EnsureCapacity(0)
* (sizeof(int)
+ Unsafe.SizeOf<KeyValuePair<
RenderProjectionId,
DirectionalShadowPartPoseSnapshot>>())
+ _directionalShadowPartPoses.Values.Sum(static pose =>
(long)pose.Count * Unsafe.SizeOf<Matrix4x4>()));
}
return new RenderSceneMemoryAccounting(
EntityCount: _world.Size,
@ -86,7 +111,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ProjectionLookupCapacity: lookupCapacity,
EstimatedProjectionLookupBytes: lookupBytes,
EstimatedIndexBytes: indexBytes,
EstimatedJournalBufferBytes: 0,
EstimatedJournalBufferBytes: directionalShadowJournalBytes,
EstimatedSynchronizationSourceBytes: 0);
}
}
@ -163,7 +188,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ref _world.Get<RenderTransform>(entry.Entity);
ref RenderWorldBounds bounds =
ref _world.Get<RenderWorldBounds>(entry.Entity);
if (current == update.Transform && bounds == update.Bounds)
bool transformChanged = !TransformBitsEqual(current, update.Transform);
if (!transformChanged && bounds == update.Bounds)
continue;
_world.Set(
@ -171,6 +197,15 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
new PreviousRenderTransform(current.LocalToWorld));
_world.Set(entry.Entity, update.Transform);
_world.Set(entry.Entity, update.Bounds);
if (transformChanged
&& HasRefreshableDirectionalShadowTransforms(entry.ProjectionClass)
&& _directionalShadowTransformChanges is not null)
{
RenderProjectionRecord currentRecord = ReadRecord(in entry);
PublishDirectionalShadowTransformChange(
in currentRecord,
DirectionalShadowTransformChangeKind.DynamicSynchronization);
}
ref RenderDirtyMask dirty =
ref _world.Get<RenderDirtyMask>(entry.Entity);
@ -226,8 +261,10 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ClearIndices();
_counts = default;
_lastAppliedJournalSequence = 0;
ResetDirectionalShadowTransformChanges();
Generation = replacementGeneration;
AdvanceIndexRevision();
AdvanceDirectionalShadowTopologyRevision();
}
public void Dispose()
@ -239,6 +276,10 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ArchWorld.Destroy(_world);
_entries.Clear();
ClearIndices();
_directionalShadowTransformChanges = null;
_directionalShadowPartPoses = null;
_directionalShadowTransformRevision = 0;
_directionalShadowTransformChangeCount = 0;
_counts = default;
_disposed = true;
}
@ -284,6 +325,99 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
return _indexRevision;
}
ulong IRenderSceneQuerySource.GetDirectionalShadowTopologyRevision(
RenderSceneGeneration generation)
{
EnsureQueryGeneration(generation);
return _directionalShadowTopologyRevision;
}
ulong IRenderSceneQuerySource.GetDirectionalShadowTransformRevision(
RenderSceneGeneration generation)
{
EnsureQueryGeneration(generation);
EnsureDirectionalShadowTransformJournal();
return _directionalShadowTransformRevision;
}
DirectionalShadowTransformChanges
IRenderSceneQuerySource.CopyDirectionalShadowTransformChanges(
RenderSceneGeneration generation,
ulong afterRevision,
Span<DirectionalShadowTransformSnapshot> destination)
{
EnsureQueryGeneration(generation);
EnsureDirectionalShadowTransformJournal();
ulong latest = _directionalShadowTransformRevision;
if (afterRevision == latest)
return new DirectionalShadowTransformChanges(latest, 0, false);
if (afterRevision == 0
|| afterRevision > latest
|| latest - afterRevision
> checked((ulong)_directionalShadowTransformChangeCount))
{
return new DirectionalShadowTransformChanges(latest, 0, true);
}
int count = checked((int)(latest - afterRevision));
if (destination.Length < count)
return new DirectionalShadowTransformChanges(latest, 0, true);
DirectionalShadowTransformChange[] journal =
_directionalShadowTransformChanges!;
int updateTransformCount = 0;
int updateAppearanceCount = 0;
int dynamicSynchronizationCount = 0;
int activeAnimatedStaticCount = 0;
int liveDynamicRootCount = 0;
int equippedChildCount = 0;
for (int index = 0; index < count; index++)
{
ulong revision = checked(afterRevision + (ulong)index + 1UL);
DirectionalShadowTransformChange change =
journal[(int)(revision % (ulong)journal.Length)];
if (change.Revision != revision)
return new DirectionalShadowTransformChanges(latest, 0, true);
destination[index] = change.Projection;
switch (change.Kind)
{
case DirectionalShadowTransformChangeKind.UpdateTransform:
updateTransformCount++;
break;
case DirectionalShadowTransformChangeKind.UpdateAppearance:
updateAppearanceCount++;
break;
case DirectionalShadowTransformChangeKind.DynamicSynchronization:
dynamicSynchronizationCount++;
break;
default:
throw new InvalidOperationException(
$"Unknown directional-shadow change kind {change.Kind}.");
}
switch (change.Projection.ProjectionClass)
{
case RenderProjectionClass.ActiveAnimatedStatic:
activeAnimatedStaticCount++;
break;
case RenderProjectionClass.LiveDynamicRoot:
liveDynamicRootCount++;
break;
case RenderProjectionClass.EquippedChild:
equippedChildCount++;
break;
}
}
return new DirectionalShadowTransformChanges(
latest,
count,
false,
updateTransformCount,
updateAppearanceCount,
dynamicSynchronizationCount,
activeAnimatedStaticCount,
liveDynamicRootCount,
equippedChildCount);
}
bool IRenderSceneQuerySource.TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
@ -300,6 +434,30 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
return false;
}
int IRenderSceneQuerySource.CopyById(
RenderSceneGeneration generation,
ReadOnlySpan<RenderProjectionId> ids,
Span<RenderProjectionRecord> destination)
{
EnsureQueryGeneration(generation);
if (destination.Length < ids.Length)
{
throw new ArgumentException(
"The render-scene ID-copy destination is too small.",
nameof(destination));
}
for (int index = 0; index < ids.Length; index++)
{
if (!_entries.TryGetValue(ids[index], out SceneEntry entry))
{
throw new InvalidOperationException(
$"Render-scene projection {ids[index]} disappeared during a batched copy.");
}
destination[index] = ReadRecord(in entry);
}
return ids.Length;
}
int IRenderSceneQuerySource.CopyTo(
RenderSceneGeneration generation,
RenderProjectionClass? projectionClass,
@ -391,6 +549,16 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
RenderProjectionRecord prior = ReadRecord(in existing);
WriteRecord(existing.Entity, in record);
UpdateIndices(in prior, in record);
if (HasRefreshableDirectionalShadowTransforms(record.ProjectionClass))
{
if (!TransformBitsEqual(prior.Transform, record.Transform))
{
PublishDirectionalShadowTransformChange(
in record,
DirectionalShadowTransformChangeKind.UpdateTransform);
}
PublishDirectionalShadowPartPoseChangeIfNeeded(in record);
}
result.Applied++;
result.Updated++;
return;
@ -407,6 +575,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
record.ProjectionClass);
IncrementCount(record.ProjectionClass);
AddToIndices(in record);
SynchronizeDirectionalShadowPartPose(in record);
result.Applied++;
result.Registered++;
}
@ -465,6 +634,20 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
RenderProjectionRecord current = ReadRecord(in entry);
UpdateIndices(in prior, in current);
if (HasRefreshableDirectionalShadowTransforms(current.ProjectionClass))
{
if (kind is RenderProjectionDeltaKind.UpdateTransform
&& !TransformBitsEqual(prior.Transform, current.Transform))
{
PublishDirectionalShadowTransformChange(
in current,
DirectionalShadowTransformChangeKind.UpdateTransform);
}
else if (kind is RenderProjectionDeltaKind.UpdateAppearance)
{
PublishDirectionalShadowPartPoseChangeIfNeeded(in current);
}
}
result.Applied++;
result.Updated++;
}
@ -582,6 +765,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
private void Destroy(in SceneEntry entry)
{
RenderProjectionRecord record = ReadRecord(in entry);
_directionalShadowPartPoses?.Remove(record.Id);
RemoveFromIndices(in record);
_world.Destroy(entry.Entity);
DecrementCount(entry.ProjectionClass);
@ -603,6 +787,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
{
if (IndexMembershipEquals(in prior, in current))
{
if (!DirectionalShadowTopologyEquals(in prior, in current))
AdvanceDirectionalShadowTopologyRevision();
SynchronizeDirtyIndex(in current);
return;
}
@ -652,6 +838,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
if (record.DirtyMask != RenderDirtyMask.None)
_dirty.Add(record.Id);
AdvanceIndexRevision();
AdvanceDirectionalShadowTopologyRevision();
}
private void RemoveFromIndices(in RenderProjectionRecord record)
@ -668,6 +855,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
AdvanceIndexRevision();
AdvanceDirectionalShadowTopologyRevision();
}
private static bool IndexMembershipEquals(
@ -689,6 +877,235 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
&& left.SortKey == right.SortKey;
}
private static bool DirectionalShadowTopologyEquals(
in RenderProjectionRecord left,
in RenderProjectionRecord right)
{
const RenderProjectionFlags eligibilityFlags =
RenderProjectionFlags.Draw
| RenderProjectionFlags.SpatiallyResident
| RenderProjectionFlags.Translucent;
if (left.ProjectionClass != right.ProjectionClass
|| left.OwnerIncarnation != right.OwnerIncarnation
|| left.Source.ParentCellId != right.Source.ParentCellId
|| (left.Flags & eligibilityFlags) != (right.Flags & eligibilityFlags)
|| left.SortKey != right.SortKey
|| left.MeshSet.MeshCount != right.MeshSet.MeshCount
|| left.Material != right.Material
|| left.DegradeState != right.DegradeState
|| left.Source.AppearanceFingerprint
!= right.Source.AppearanceFingerprint
|| left.Source.DirectionalShadowTopologyFingerprint
!= right.Source.DirectionalShadowTopologyFingerprint
|| left.EntityPayload.IsBuildingShell
!= right.EntityPayload.IsBuildingShell
|| left.EntityPayload.CasterIdentity
!= right.EntityPayload.CasterIdentity
|| !PaletteEquals(
left.EntityPayload.PaletteOverride,
right.EntityPayload.PaletteOverride))
{
return false;
}
bool refreshableTransforms =
HasRefreshableDirectionalShadowTransforms(left.ProjectionClass);
if (!refreshableTransforms
&& (left.Transform != right.Transform
|| left.MeshSet != right.MeshSet
|| left.Source.GeometryFingerprint
!= right.Source.GeometryFingerprint))
{
return false;
}
IReadOnlyList<AcDream.Core.World.MeshRef>? leftMeshes =
left.EntityPayload.MeshRefs;
IReadOnlyList<AcDream.Core.World.MeshRef>? rightMeshes =
right.EntityPayload.MeshRefs;
if (ReferenceEquals(leftMeshes, rightMeshes))
return true;
if (leftMeshes is null
|| rightMeshes is null
|| leftMeshes.Count != rightMeshes.Count)
{
return false;
}
for (int meshIndex = 0; meshIndex < leftMeshes.Count; meshIndex++)
{
AcDream.Core.World.MeshRef leftMesh = leftMeshes[meshIndex];
AcDream.Core.World.MeshRef rightMesh = rightMeshes[meshIndex];
if (leftMesh.GfxObjId != rightMesh.GfxObjId
|| !SurfaceOverridesEqual(
leftMesh.SurfaceOverrides,
rightMesh.SurfaceOverrides)
|| (!refreshableTransforms
&& leftMesh.PartTransform != rightMesh.PartTransform))
{
return false;
}
}
return true;
}
private static bool HasRefreshableDirectionalShadowTransforms(
RenderProjectionClass projectionClass) =>
projectionClass is RenderProjectionClass.ActiveAnimatedStatic
or RenderProjectionClass.LiveDynamicRoot
or RenderProjectionClass.EquippedChild;
private static bool TransformBitsEqual(
in RenderTransform left,
in RenderTransform right)
{
Matrix4x4 leftMatrix = left.LocalToWorld;
Matrix4x4 rightMatrix = right.LocalToWorld;
ReadOnlySpan<Matrix4x4> leftSpan = MemoryMarshal.CreateReadOnlySpan(
in leftMatrix,
1);
ReadOnlySpan<Matrix4x4> rightSpan = MemoryMarshal.CreateReadOnlySpan(
in rightMatrix,
1);
return MemoryMarshal.AsBytes(leftSpan).SequenceEqual(
MemoryMarshal.AsBytes(rightSpan));
}
private void EnsureDirectionalShadowTransformJournal()
{
if (_directionalShadowTransformChanges is not null)
return;
_directionalShadowTransformChanges = new DirectionalShadowTransformChange[
DirectionalShadowTransformChangeJournal.Capacity];
_directionalShadowTransformRevision = 1;
_directionalShadowTransformChangeCount = 0;
_directionalShadowPartPoses = new Dictionary<
RenderProjectionId,
DirectionalShadowPartPoseSnapshot>();
foreach (SceneEntry entry in _entries.Values)
{
if (!HasRefreshableDirectionalShadowTransforms(entry.ProjectionClass))
continue;
RenderProjectionRecord record = ReadRecord(in entry);
SynchronizeDirectionalShadowPartPose(in record);
}
}
private void PublishDirectionalShadowTransformChange(
in RenderProjectionRecord projection,
DirectionalShadowTransformChangeKind kind)
{
DirectionalShadowTransformChange[]? journal =
_directionalShadowTransformChanges;
if (journal is null)
return;
if (_directionalShadowTransformRevision == ulong.MaxValue)
{
throw new InvalidOperationException(
"Directional-shadow transform revision space was exhausted.");
}
ulong revision = ++_directionalShadowTransformRevision;
journal[(int)(revision % (ulong)journal.Length)] =
new DirectionalShadowTransformChange(
revision,
DirectionalShadowTransformSnapshot.Capture(in projection),
kind);
if (_directionalShadowTransformChangeCount < journal.Length)
_directionalShadowTransformChangeCount++;
}
private void ResetDirectionalShadowTransformChanges()
{
if (_directionalShadowTransformChanges is null)
return;
_directionalShadowTransformRevision = 1;
_directionalShadowTransformChangeCount = 0;
_directionalShadowPartPoses!.Clear();
}
private void SynchronizeDirectionalShadowPartPose(
in RenderProjectionRecord record)
{
Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
poses = _directionalShadowPartPoses;
if (poses is null
|| !HasRefreshableDirectionalShadowTransforms(record.ProjectionClass))
{
return;
}
if (!poses.TryGetValue(record.Id, out DirectionalShadowPartPoseSnapshot? pose))
{
poses.Add(record.Id, DirectionalShadowPartPoseSnapshot.Capture(in record));
return;
}
pose.CaptureCurrent(in record);
}
private void PublishDirectionalShadowPartPoseChangeIfNeeded(
in RenderProjectionRecord record)
{
Dictionary<RenderProjectionId, DirectionalShadowPartPoseSnapshot>?
poses = _directionalShadowPartPoses;
if (poses is null)
return;
if (!poses.TryGetValue(record.Id, out DirectionalShadowPartPoseSnapshot? pose))
{
poses.Add(record.Id, DirectionalShadowPartPoseSnapshot.Capture(in record));
return;
}
if (!pose.CaptureCurrent(in record))
return;
PublishDirectionalShadowTransformChange(
in record,
DirectionalShadowTransformChangeKind.UpdateAppearance);
}
private static bool PaletteEquals(
AcDream.Core.World.PaletteOverride? left,
AcDream.Core.World.PaletteOverride? right)
{
if (ReferenceEquals(left, right))
return true;
if (left is null
|| right is null
|| left.BasePaletteId != right.BasePaletteId
|| left.SubPalettes.Count != right.SubPalettes.Count)
{
return false;
}
for (int index = 0; index < left.SubPalettes.Count; index++)
{
if (left.SubPalettes[index] != right.SubPalettes[index])
return false;
}
return true;
}
private static bool SurfaceOverridesEqual(
IReadOnlyDictionary<uint, uint>? left,
IReadOnlyDictionary<uint, uint>? right)
{
if (ReferenceEquals(left, right))
return true;
if (left is null || right is null || left.Count != right.Count)
return false;
foreach ((uint surfaceId, uint textureId) in left)
{
if (!right.TryGetValue(surfaceId, out uint candidate)
|| candidate != textureId)
{
return false;
}
}
return true;
}
private void SynchronizeDirtyIndex(
in RenderProjectionRecord record)
{
@ -709,6 +1126,17 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_indexRevision++;
}
private void AdvanceDirectionalShadowTopologyRevision()
{
if (_directionalShadowTopologyRevision == ulong.MaxValue)
{
throw new InvalidOperationException(
"Directional-shadow topology revision space was exhausted.");
}
_directionalShadowTopologyRevision++;
}
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
projectionClass is RenderProjectionClass.LiveDynamicRoot
or RenderProjectionClass.EquippedChild;
@ -925,10 +1353,71 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
hash.Add(record.Source.AppearanceFingerprint.Low);
hash.Add(record.Source.AppearanceFingerprint.High);
hash.Add(record.Source.CurrentProjectionFlags);
hash.Add((byte)record.EntityPayload.CasterIdentity);
}
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
private readonly record struct DirectionalShadowTransformChange(
ulong Revision,
DirectionalShadowTransformSnapshot Projection,
DirectionalShadowTransformChangeKind Kind);
private sealed class DirectionalShadowPartPoseSnapshot
{
private Matrix4x4[] _parts;
private DirectionalShadowPartPoseSnapshot(Matrix4x4[] parts) =>
_parts = parts;
internal int Count => _parts.Length;
internal static DirectionalShadowPartPoseSnapshot Capture(
in RenderProjectionRecord record)
{
IReadOnlyList<AcDream.Core.World.MeshRef>? meshes =
record.EntityPayload.MeshRefs;
var parts = new Matrix4x4[meshes?.Count ?? 0];
for (int index = 0; index < parts.Length; index++)
parts[index] = meshes![index].PartTransform;
return new DirectionalShadowPartPoseSnapshot(parts);
}
internal bool CaptureCurrent(in RenderProjectionRecord record)
{
IReadOnlyList<AcDream.Core.World.MeshRef>? meshes =
record.EntityPayload.MeshRefs;
int count = meshes?.Count ?? 0;
bool changed = _parts.Length != count;
if (changed)
_parts = new Matrix4x4[count];
for (int index = 0; index < count; index++)
{
Matrix4x4 current = meshes![index].PartTransform;
if (!MatrixBitsEqual(in _parts[index], in current))
{
_parts[index] = current;
changed = true;
}
}
return changed;
}
private static bool MatrixBitsEqual(
in Matrix4x4 left,
in Matrix4x4 right)
{
ReadOnlySpan<Matrix4x4> leftSpan = MemoryMarshal.CreateReadOnlySpan(
in left,
1);
ReadOnlySpan<Matrix4x4> rightSpan = MemoryMarshal.CreateReadOnlySpan(
in right,
1);
return MemoryMarshal.AsBytes(leftSpan).SequenceEqual(
MemoryMarshal.AsBytes(rightSpan));
}
}
private readonly record struct OutdoorStaticTag;
private readonly record struct IndoorCellStaticTag;

View file

@ -834,6 +834,22 @@ internal sealed class CurrentRenderSceneOracle :
geometry.Add(fingerprint.High);
}
internal static RenderSceneHash128
CreateDirectionalShadowTopologyFingerprint(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
StableRenderHash128 topology = StableRenderHash128.Create();
topology.Add(entity.MeshRefs.Count);
for (int meshIndex = 0; meshIndex < entity.MeshRefs.Count; meshIndex++)
{
MeshRef mesh = entity.MeshRefs[meshIndex];
topology.Add(mesh.GfxObjId);
AddSurfaceOverrides(ref topology, mesh.SurfaceOverrides);
}
return topology.Finish();
}
internal static RenderSceneHash128 CreateSurfaceOverrideFingerprint(
IReadOnlyDictionary<uint, uint>? overrides)
{

View file

@ -0,0 +1,542 @@
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);
}
}
}

View file

@ -1,5 +1,7 @@
using AcDream.App.Input;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
@ -26,6 +28,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
private readonly LiveEntityRuntime _runtime;
private readonly RenderProjectionJournal _journal;
private readonly IRenderTraversalOrderSource _traversalOrder;
private readonly ILocalPlayerIdentitySource? _localPlayer;
private readonly Dictionary<RuntimeEntityKey, TrackedProjection> _byKey = [];
private readonly List<LiveEntityRecord> _activeRootScratch = [];
private readonly List<TrackedProjection> _activeScratch = [];
@ -33,12 +36,14 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
public LiveRenderProjectionJournal(
LiveEntityRuntime runtime,
RenderProjectionJournal journal,
IRenderTraversalOrderSource traversalOrder)
IRenderTraversalOrderSource traversalOrder,
ILocalPlayerIdentitySource? localPlayer = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
_traversalOrder = traversalOrder
?? throw new ArgumentNullException(nameof(traversalOrder));
_localPlayer = localPlayer;
}
public int ProjectionCount => _byKey.Count;
@ -286,7 +291,14 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
ownerLandblockId,
fullCellId,
entity,
spatiallyVisible);
spatiallyVisible,
record.ProjectionKind is LiveEntityProjectionKind.Attached
? RenderCasterIdentityKind.EquippedChild
: RenderCasterIdentityClassifier.Classify(
record.Snapshot.Guid,
record.Snapshot.ItemType,
record.Snapshot.ObjectDescriptionFlags,
_localPlayer?.ServerGuid ?? 0u));
if (_traversalOrder.TryGetTraversalSortKey(
entity,
out RenderSortKey sortKey))
@ -336,6 +348,31 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
}
}
internal static class RenderCasterIdentityClassifier
{
private const uint PlayerDescriptionFlag = 0x8u;
private const uint PlayerGuidPrefix = 0x50000000u;
internal static RenderCasterIdentityKind Classify(
uint serverGuid,
uint? itemType,
uint? objectDescriptionFlags,
uint localPlayerGuid)
{
if (localPlayerGuid != 0 && serverGuid == localPlayerGuid)
return RenderCasterIdentityKind.LocalPlayer;
if ((objectDescriptionFlags.GetValueOrDefault()
& PlayerDescriptionFlag) != 0
|| (serverGuid & 0xFF000000u) == PlayerGuidPrefix)
{
return RenderCasterIdentityKind.RemotePlayer;
}
if ((itemType.GetValueOrDefault() & (uint)ItemType.Creature) != 0)
return RenderCasterIdentityKind.NonPlayerCreature;
return RenderCasterIdentityKind.OtherLiveDynamic;
}
}
internal sealed class LiveRenderProjectionResourceLifecycle(
ILiveRenderProjectionSink sink) : ILiveEntityResourceLifecycle
{

View file

@ -19,7 +19,9 @@ internal static class RenderProjectionRecordFactory
uint ownerLandblockId,
uint fullCellId,
WorldEntity entity,
bool spatiallyVisible)
bool spatiallyVisible,
RenderCasterIdentityKind casterIdentity =
RenderCasterIdentityKind.Unclassified)
{
ArgumentNullException.ThrowIfNull(entity);
CurrentRenderProjectionFingerprint fingerprint =
@ -80,11 +82,14 @@ internal static class RenderProjectionRecordFactory
fingerprint.Transform,
fingerprint.Geometry,
fingerprint.Appearance,
fingerprint.Flags),
fingerprint.Flags,
CurrentRenderSceneOracle
.CreateDirectionalShadowTopologyFingerprint(entity)),
new RenderEntityPayload(
entity.MeshRefs,
entity.PaletteOverride,
entity.IsBuildingShell));
entity.IsBuildingShell,
casterIdentity));
}
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(

View file

@ -222,7 +222,26 @@ internal readonly record struct RenderSourceMetadata(
RenderSceneHash128 TransformFingerprint,
RenderSceneHash128 GeometryFingerprint,
RenderSceneHash128 AppearanceFingerprint,
uint CurrentProjectionFlags = 0);
uint CurrentProjectionFlags = 0,
RenderSceneHash128 DirectionalShadowTopologyFingerprint = default);
/// <summary>
/// Render-only identity facts retained from the authoritative publication edge.
/// Outdoor DAT scenery has no tree discriminator, and the create-object payload
/// does not distinguish hostile monsters from other non-player creatures, so
/// neither narrower category is guessed here.
/// </summary>
internal enum RenderCasterIdentityKind : byte
{
Unclassified,
OutdoorStatic,
Building,
LocalPlayer,
RemotePlayer,
NonPlayerCreature,
OtherLiveDynamic,
EquippedChild,
}
/// <summary>
/// Borrowed immutable presentation payload captured at a scene publication
@ -234,7 +253,9 @@ internal readonly record struct RenderSourceMetadata(
internal readonly record struct RenderEntityPayload(
IReadOnlyList<MeshRef> MeshRefs,
PaletteOverride? PaletteOverride,
bool IsBuildingShell);
bool IsBuildingShell,
RenderCasterIdentityKind CasterIdentity =
RenderCasterIdentityKind.Unclassified);
internal readonly record struct RenderProjectionRecord(
RenderProjectionId Id,
@ -449,6 +470,66 @@ internal readonly record struct RenderSceneDigest(
RenderProjectionCounts Counts,
RenderSceneHash128 Hash);
internal static class DirectionalShadowTransformChangeJournal
{
// Larger than the measured 9,498-caster dense-Arwic row so one complete
// changed-pose publication fits without truncation. Overflow is explicit
// and makes the consumer take its exact full-refresh fallback.
internal const int Capacity = 16_384;
}
internal readonly struct DirectionalShadowTransformSnapshot
{
internal DirectionalShadowTransformSnapshot(
RenderProjectionId id,
RenderProjectionClass projectionClass,
RenderTransform transform,
RenderEntityPayload entityPayload)
{
Id = id;
ProjectionClass = projectionClass;
Transform = transform;
EntityPayload = entityPayload;
}
internal readonly RenderProjectionId Id;
internal readonly RenderProjectionClass ProjectionClass;
internal readonly RenderTransform Transform;
internal readonly RenderEntityPayload EntityPayload;
internal static DirectionalShadowTransformSnapshot Capture(
in RenderProjectionRecord projection) =>
new(
projection.Id,
projection.ProjectionClass,
projection.Transform,
projection.EntityPayload);
}
internal readonly record struct DirectionalShadowTransformChanges(
ulong LatestRevision,
int Count,
bool RequiresFullRefresh,
int UpdateTransformCount = 0,
int UpdateAppearanceCount = 0,
int DynamicSynchronizationCount = 0,
int ActiveAnimatedStaticCount = 0,
int LiveDynamicRootCount = 0,
int EquippedChildCount = 0);
// Transform-journal records carry the producer's already-current projection.
// Root matrices are values; MeshRef payloads are borrowed under the render
// publication ordering rule: a part-pose mutation must be followed by its
// UpdateAppearance publication before the frame opens a scene query. Consumers
// read newest-to-oldest, so repeated IDs always select the latest publication.
internal enum DirectionalShadowTransformChangeKind : byte
{
UpdateTransform,
UpdateAppearance,
DynamicSynchronization,
}
internal sealed class RenderSceneDigestBuffer
{
internal List<RenderProjectionRecord> Records { get; } = [];
@ -461,12 +542,26 @@ internal interface IRenderSceneQuerySource
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
ulong GetIndexRevision(RenderSceneGeneration generation);
ulong GetDirectionalShadowTopologyRevision(
RenderSceneGeneration generation);
ulong GetDirectionalShadowTransformRevision(
RenderSceneGeneration generation);
DirectionalShadowTransformChanges CopyDirectionalShadowTransformChanges(
RenderSceneGeneration generation,
ulong afterRevision,
Span<DirectionalShadowTransformSnapshot> destination);
bool TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
out RenderProjectionRecord record);
int CopyById(
RenderSceneGeneration generation,
ReadOnlySpan<RenderProjectionId> ids,
Span<RenderProjectionRecord> destination);
int CopyTo(
RenderSceneGeneration generation,
RenderProjectionClass? projectionClass,
@ -512,11 +607,30 @@ internal readonly struct RenderSceneQuery
public ulong IndexRevision =>
Source.GetIndexRevision(Generation);
public ulong DirectionalShadowTopologyRevision =>
Source.GetDirectionalShadowTopologyRevision(Generation);
public ulong DirectionalShadowTransformRevision =>
Source.GetDirectionalShadowTransformRevision(Generation);
public DirectionalShadowTransformChanges CopyDirectionalShadowTransformChanges(
ulong afterRevision,
Span<DirectionalShadowTransformSnapshot> destination) =>
Source.CopyDirectionalShadowTransformChanges(
Generation,
afterRevision,
destination);
public bool TryGet(
RenderProjectionId id,
out RenderProjectionRecord record) =>
Source.TryGet(Generation, id, out record);
public int CopyById(
ReadOnlySpan<RenderProjectionId> ids,
Span<RenderProjectionRecord> destination) =>
Source.CopyById(Generation, ids, destination);
public int CopyTo(Span<RenderProjectionRecord> destination) =>
Source.CopyTo(Generation, null, destination);

View file

@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Input;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.World;
using AcDream.Core.World;
@ -95,7 +96,8 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
public LiveRenderProjectionJournal BindLiveRuntime(
LiveEntityRuntime runtime,
IRenderTraversalOrderSource traversalOrder)
IRenderTraversalOrderSource traversalOrder,
ILocalPlayerIdentitySource? localPlayer = null)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(runtime);
@ -109,7 +111,8 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
_live = new LiveRenderProjectionJournal(
runtime,
_journal,
traversalOrder);
traversalOrder,
localPlayer);
return _live;
}

View file

@ -122,7 +122,9 @@ internal sealed class StaticRenderProjectionJournal :
&& accepted.Source.AppearanceFingerprint
== retained.Record.Source.AppearanceFingerprint
&& accepted.EntityPayload.IsBuildingShell
== retained.Record.EntityPayload.IsBuildingShell)
== retained.Record.EntityPayload.IsBuildingShell
&& accepted.EntityPayload.CasterIdentity
== retained.Record.EntityPayload.CasterIdentity)
{
accepted = accepted with
{
@ -245,7 +247,10 @@ internal sealed class StaticRenderProjectionJournal :
tracked.Record.Residency.OwnerLandblockId,
tracked.Record.Residency.FullCellId,
entity,
spatiallyVisible: true) with
spatiallyVisible: true,
casterIdentity: entity.IsBuildingShell
? RenderCasterIdentityKind.Building
: RenderCasterIdentityKind.OutdoorStatic) with
{
PreviousTransform = new PreviousRenderTransform(
tracked.Record.Transform.LocalToWorld),
@ -317,7 +322,10 @@ internal sealed class StaticRenderProjectionJournal :
landblockId,
fullCellId,
entity,
spatiallyVisible: true) with
spatiallyVisible: true,
casterIdentity: entity.IsBuildingShell
? RenderCasterIdentityKind.Building
: RenderCasterIdentityKind.OutdoorStatic) with
{
SortKey = sortKey,
};