feat(rendering): index incremental scene queries

Maintain packed render worksets as lifecycle deltas arrive and synchronize active animated statics without scanning the resident world. This keeps Slice F non-drawing while preparing exact spatial and feature queries for shadow comparison.

Release: 8,206 passed, 5 skipped.
This commit is contained in:
Erik 2026-07-24 22:12:26 +02:00
parent 58e7c2eb99
commit ff5d86175f
8 changed files with 745 additions and 16 deletions

View file

@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using AcDream.App.Rendering;
using Arch.Core;
using ArchWorld = Arch.Core.World;
@ -18,6 +19,19 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
private readonly int _ownerThreadId;
private readonly Dictionary<RenderProjectionId, SceneEntry> _entries = [];
private readonly HashSet<RenderProjectionId> _outdoorStatics = [];
private readonly HashSet<RenderProjectionId> _indoorCellStatics = [];
private readonly HashSet<RenderProjectionId> _dynamics = [];
private readonly HashSet<RenderProjectionId> _outdoorDynamics = [];
private readonly HashSet<RenderProjectionId> _portalStraddlingDynamics = [];
private readonly HashSet<RenderProjectionId> _translucent = [];
private readonly HashSet<RenderProjectionId> _selectable = [];
private readonly HashSet<RenderProjectionId> _lightCandidates = [];
private readonly HashSet<RenderProjectionId> _dirty = [];
private readonly Dictionary<uint, HashSet<RenderProjectionId>>
_cellStatics = [];
private readonly Dictionary<uint, HashSet<RenderProjectionId>>
_cellDynamics = [];
private ArchWorld _world;
private RenderProjectionCounts _counts;
private ulong _lastAppliedJournalSequence;
@ -59,6 +73,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
int lookupCapacity = _entries.EnsureCapacity(0);
long lookupBytes =
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
long indexBytes = EstimateIndexBytes();
return new RenderSceneMemoryAccounting(
EntityCount: _world.Size,
@ -68,7 +83,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
EstimatedChunkPayloadBytes: estimatedChunkPayloadBytes,
ProjectionLookupCapacity: lookupCapacity,
EstimatedProjectionLookupBytes: lookupBytes,
EstimatedIndexBytes: 0,
EstimatedIndexBytes: indexBytes,
EstimatedJournalBufferBytes: 0,
EstimatedSynchronizationSourceBytes: 0);
}
@ -158,6 +173,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ref RenderDirtyMask dirty =
ref _world.Get<RenderDirtyMask>(entry.Entity);
dirty |= RenderDirtyMask.Transform | RenderDirtyMask.WorldBounds;
_dirty.Add(update.Id);
}
}
@ -205,6 +221,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
ArchWorld.Destroy(_world);
_world = CreateWorld();
_entries.Clear();
ClearIndices();
_counts = default;
_lastAppliedJournalSequence = 0;
Generation = replacementGeneration;
@ -218,10 +235,22 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
EnsureMutationThread();
ArchWorld.Destroy(_world);
_entries.Clear();
ClearIndices();
_counts = default;
_disposed = true;
}
public void ClearDirty()
{
EnsureMutationThread();
foreach (RenderProjectionId id in _dirty)
{
if (_entries.TryGetValue(id, out SceneEntry entry))
_world.Set(entry.Entity, RenderDirtyMask.None);
}
_dirty.Clear();
}
RenderProjectionCounts IRenderSceneQuerySource.GetCounts(
RenderSceneGeneration generation)
{
@ -229,6 +258,22 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
return _counts;
}
RenderSceneIndexCounts IRenderSceneQuerySource.GetIndexCounts(
RenderSceneGeneration generation)
{
EnsureQueryGeneration(generation);
return new RenderSceneIndexCounts(
_outdoorStatics.Count,
_indoorCellStatics.Count,
_dynamics.Count,
_outdoorDynamics.Count,
_portalStraddlingDynamics.Count,
_translucent.Count,
_selectable.Count,
_lightCandidates.Count,
_dirty.Count);
}
bool IRenderSceneQuerySource.TryGet(
RenderSceneGeneration generation,
RenderProjectionId id,
@ -276,6 +321,41 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
return count;
}
int IRenderSceneQuerySource.CopyIndexTo(
RenderSceneGeneration generation,
RenderSceneIndex index,
Span<RenderProjectionRecord> destination)
{
EnsureQueryGeneration(generation);
HashSet<RenderProjectionId> source = Index(index);
return CopyIdsTo(source, destination);
}
int IRenderSceneQuerySource.GetCellCount(
RenderSceneGeneration generation,
uint fullCellId,
bool dynamic)
{
EnsureQueryGeneration(generation);
Dictionary<uint, HashSet<RenderProjectionId>> index =
dynamic ? _cellDynamics : _cellStatics;
return index.TryGetValue(fullCellId, out var ids) ? ids.Count : 0;
}
int IRenderSceneQuerySource.CopyCellTo(
RenderSceneGeneration generation,
uint fullCellId,
bool dynamic,
Span<RenderProjectionRecord> destination)
{
EnsureQueryGeneration(generation);
Dictionary<uint, HashSet<RenderProjectionId>> index =
dynamic ? _cellDynamics : _cellStatics;
return index.TryGetValue(fullCellId, out var ids)
? CopyIdsTo(ids, destination)
: 0;
}
private static ArchWorld CreateWorld() =>
ArchWorld.Create(
archetypeCapacity: InitialArchetypeCapacity,
@ -298,7 +378,9 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
if (incarnationOrder == 0
&& record.ProjectionClass == existing.ProjectionClass)
{
RenderProjectionRecord prior = ReadRecord(in existing);
WriteRecord(existing.Entity, in record);
UpdateIndices(in prior, in record);
result.Applied++;
result.Updated++;
return;
@ -314,6 +396,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
record.OwnerIncarnation,
record.ProjectionClass);
IncrementCount(record.ProjectionClass);
AddToIndices(in record);
result.Applied++;
result.Registered++;
}
@ -326,6 +409,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
if (!TryGetCurrent(in record, ref result, out SceneEntry entry))
return;
RenderProjectionRecord prior = ReadRecord(in entry);
switch (kind)
{
case RenderProjectionDeltaKind.UpdateTransform:
@ -336,6 +420,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_world.Set(entry.Entity, record.Source);
OrDirty(
entry.Entity,
record.Id,
RenderDirtyMask.Transform
| RenderDirtyMask.WorldBounds
| RenderDirtyMask.SortKey);
@ -345,22 +430,30 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
_world.Set(entry.Entity, record.Material);
_world.Set(entry.Entity, record.DegradeState);
_world.Set(entry.Entity, record.Source);
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
OrDirty(
entry.Entity,
record.Id,
RenderDirtyMask.Appearance);
break;
case RenderProjectionDeltaKind.UpdateFlags:
_world.Set(entry.Entity, record.Flags);
_world.Set(entry.Entity, record.Source);
OrDirty(entry.Entity, RenderDirtyMask.Flags);
OrDirty(entry.Entity, record.Id, RenderDirtyMask.Flags);
break;
case RenderProjectionDeltaKind.Rebucket:
_world.Set(entry.Entity, record.Residency);
_world.Set(entry.Entity, record.Source);
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
OrDirty(
entry.Entity,
record.Id,
RenderDirtyMask.SpatialResidency);
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
RenderProjectionRecord current = ReadRecord(in entry);
UpdateIndices(in prior, in current);
result.Applied++;
result.Updated++;
}
@ -474,14 +567,196 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
private void Destroy(in SceneEntry entry)
{
RenderProjectionRecord record = ReadRecord(in entry);
RemoveFromIndices(in record);
_world.Destroy(entry.Entity);
DecrementCount(entry.ProjectionClass);
}
private void OrDirty(Entity entity, RenderDirtyMask value)
private void OrDirty(
Entity entity,
RenderProjectionId id,
RenderDirtyMask value)
{
ref RenderDirtyMask dirty = ref _world.Get<RenderDirtyMask>(entity);
dirty |= value;
_dirty.Add(id);
}
private void UpdateIndices(
in RenderProjectionRecord prior,
in RenderProjectionRecord current)
{
RemoveFromIndices(in prior);
AddToIndices(in current);
}
private void AddToIndices(in RenderProjectionRecord record)
{
bool dynamic = IsDynamic(record.ProjectionClass);
bool staticProjection = !dynamic;
bool indoor = InteriorEntityPartition.IsIndoorCellId(
record.Source.ParentCellId == 0
? null
: record.Source.ParentCellId);
if (staticProjection)
{
if (indoor)
{
_indoorCellStatics.Add(record.Id);
AddCell(_cellStatics, record.Residency.FullCellId, record.Id);
}
else
{
_outdoorStatics.Add(record.Id);
}
}
else
{
_dynamics.Add(record.Id);
if (indoor)
AddCell(_cellDynamics, record.Residency.FullCellId, record.Id);
else
_outdoorDynamics.Add(record.Id);
if ((record.Flags & RenderProjectionFlags.PortalStraddling) != 0)
_portalStraddlingDynamics.Add(record.Id);
}
if ((record.Flags & RenderProjectionFlags.Translucent) != 0)
_translucent.Add(record.Id);
if ((record.Flags & RenderProjectionFlags.Selectable) != 0)
_selectable.Add(record.Id);
if ((record.Flags & RenderProjectionFlags.LightCandidate) != 0)
_lightCandidates.Add(record.Id);
if (record.DirtyMask != RenderDirtyMask.None)
_dirty.Add(record.Id);
}
private void RemoveFromIndices(in RenderProjectionRecord record)
{
_outdoorStatics.Remove(record.Id);
_indoorCellStatics.Remove(record.Id);
_dynamics.Remove(record.Id);
_outdoorDynamics.Remove(record.Id);
_portalStraddlingDynamics.Remove(record.Id);
_translucent.Remove(record.Id);
_selectable.Remove(record.Id);
_lightCandidates.Remove(record.Id);
_dirty.Remove(record.Id);
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
}
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
projectionClass is RenderProjectionClass.LiveDynamicRoot
or RenderProjectionClass.EquippedChild;
private static void AddCell(
Dictionary<uint, HashSet<RenderProjectionId>> index,
uint fullCellId,
RenderProjectionId id)
{
if (fullCellId == 0)
return;
if (!index.TryGetValue(fullCellId, out HashSet<RenderProjectionId>? ids))
{
ids = [];
index.Add(fullCellId, ids);
}
ids.Add(id);
}
private static void RemoveCell(
Dictionary<uint, HashSet<RenderProjectionId>> index,
uint fullCellId,
RenderProjectionId id)
{
if (!index.TryGetValue(fullCellId, out HashSet<RenderProjectionId>? ids))
return;
ids.Remove(id);
if (ids.Count == 0)
index.Remove(fullCellId);
}
private HashSet<RenderProjectionId> Index(RenderSceneIndex index) =>
index switch
{
RenderSceneIndex.OutdoorStatic => _outdoorStatics,
RenderSceneIndex.IndoorCellStatic => _indoorCellStatics,
RenderSceneIndex.Dynamic => _dynamics,
RenderSceneIndex.OutdoorDynamic => _outdoorDynamics,
RenderSceneIndex.PortalStraddlingDynamic =>
_portalStraddlingDynamics,
RenderSceneIndex.Translucent => _translucent,
RenderSceneIndex.Selectable => _selectable,
RenderSceneIndex.LightCandidate => _lightCandidates,
RenderSceneIndex.Dirty => _dirty,
_ => throw new ArgumentOutOfRangeException(
nameof(index),
index,
null),
};
private int CopyIdsTo(
HashSet<RenderProjectionId> source,
Span<RenderProjectionRecord> destination)
{
if (destination.Length < source.Count)
{
throw new ArgumentException(
$"Destination holds {destination.Length} records; {source.Count} required.",
nameof(destination));
}
int count = 0;
foreach (RenderProjectionId id in source)
{
if (!_entries.TryGetValue(id, out SceneEntry entry))
{
throw new InvalidOperationException(
$"Render index retained missing {id}.");
}
destination[count++] = ReadRecord(in entry);
}
return count;
}
private long EstimateIndexBytes()
{
long slots =
_outdoorStatics.EnsureCapacity(0)
+ _indoorCellStatics.EnsureCapacity(0)
+ _dynamics.EnsureCapacity(0)
+ _outdoorDynamics.EnsureCapacity(0)
+ _portalStraddlingDynamics.EnsureCapacity(0)
+ _translucent.EnsureCapacity(0)
+ _selectable.EnsureCapacity(0)
+ _lightCandidates.EnsureCapacity(0)
+ _dirty.EnsureCapacity(0);
foreach (HashSet<RenderProjectionId> ids in _cellStatics.Values)
slots += ids.EnsureCapacity(0);
foreach (HashSet<RenderProjectionId> ids in _cellDynamics.Values)
slots += ids.EnsureCapacity(0);
long cellLookupSlots =
_cellStatics.EnsureCapacity(0)
+ _cellDynamics.EnsureCapacity(0);
return checked(slots * 24L + cellLookupSlots * 40L);
}
private void ClearIndices()
{
_outdoorStatics.Clear();
_indoorCellStatics.Clear();
_dynamics.Clear();
_outdoorDynamics.Clear();
_portalStraddlingDynamics.Clear();
_translucent.Clear();
_selectable.Clear();
_lightCandidates.Clear();
_dirty.Clear();
_cellStatics.Clear();
_cellDynamics.Clear();
}
private void IncrementCount(RenderProjectionClass projectionClass)