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

@ -28,6 +28,12 @@ internal sealed class StaticRenderProjectionJournal :
private readonly RenderProjectionJournal _journal;
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
_byLandblock = [];
private readonly Dictionary<RenderProjectionId, TrackedProjection>
_trackedById = [];
private readonly Dictionary<WorldEntity, RenderProjectionId> _idByEntity =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<uint, List<WorldEntity>> _entitiesByLandblock =
[];
private readonly List<RenderProjectionRecord> _candidates = [];
private readonly List<RenderProjectionId> _removed = [];
private ulong _nextIncarnation = 1;
@ -87,7 +93,10 @@ internal sealed class StaticRenderProjectionJournal :
for (int i = 0; i < _candidates.Count; i++)
{
RenderProjectionRecord candidate = _candidates[i];
if (current.TryGetValue(candidate.Id, out TrackedProjection retained))
if (current.TryGetValue(
candidate.Id,
out TrackedProjection? retained)
&& retained is not null)
{
_removed.Remove(candidate.Id);
RenderProjectionRecord accepted = candidate with
@ -107,14 +116,14 @@ internal sealed class StaticRenderProjectionJournal :
OwnerIncarnation = NextIncarnation(),
};
_journal.Register(in accepted);
current[candidate.Id] = new TrackedProjection(accepted);
retained.Record = accepted;
continue;
}
if (accepted != retained.Record)
{
_journal.AppendDifference(retained.Record, accepted);
current[candidate.Id] = new TrackedProjection(accepted);
retained.Record = accepted;
}
continue;
}
@ -124,7 +133,9 @@ internal sealed class StaticRenderProjectionJournal :
OwnerIncarnation = NextIncarnation(),
};
_journal.Register(in registered);
current.Add(registered.Id, new TrackedProjection(registered));
var tracked = new TrackedProjection(registered);
current.Add(registered.Id, tracked);
_trackedById.Add(registered.Id, tracked);
ProjectionCount++;
}
@ -135,11 +146,13 @@ internal sealed class StaticRenderProjectionJournal :
TrackedProjection omitted = current[id];
_journal.Unregister(id, omitted.Record.OwnerIncarnation);
current.Remove(id);
_trackedById.Remove(id);
ProjectionCount--;
}
if (current.Count == 0)
_byLandblock.Remove(landblockId);
ReplaceStaticEntityMap(landblockId, publication.Landblock.Entities);
}
public void Retire(GpuLandblockRetirement retirement)
@ -166,17 +179,66 @@ internal sealed class StaticRenderProjectionJournal :
}
ProjectionCount -= current.Count;
foreach (RenderProjectionId id in current.Keys)
_trackedById.Remove(id);
RemoveStaticEntityMap(landblockId);
}
public void Clear(RenderSceneGeneration replacementGeneration)
{
_byLandblock.Clear();
_trackedById.Clear();
_idByEntity.Clear();
_entitiesByLandblock.Clear();
_candidates.Clear();
_removed.Clear();
ProjectionCount = 0;
_journal.Clear(replacementGeneration);
}
public void SynchronizeActiveAnimatedSources(
IReadOnlyList<WorldEntity> activeEntities)
{
ArgumentNullException.ThrowIfNull(activeEntities);
for (int i = 0; i < activeEntities.Count; i++)
{
WorldEntity entity = activeEntities[i];
if (!_idByEntity.TryGetValue(
entity,
out RenderProjectionId id)
|| !_trackedById.TryGetValue(
id,
out TrackedProjection? tracked))
{
continue;
}
RenderProjectionRecord current =
RenderProjectionRecordFactory.ProjectEntity(
id,
RenderProjectionClass.ActiveAnimatedStatic,
tracked.Record.OwnerIncarnation,
tracked.Record.Residency.OwnerLandblockId,
tracked.Record.Residency.FullCellId,
entity,
spatiallyVisible: true) with
{
PreviousTransform = new PreviousRenderTransform(
tracked.Record.Transform.LocalToWorld),
};
if (tracked.Record.ProjectionClass
!= RenderProjectionClass.ActiveAnimatedStatic)
{
_journal.Register(in current);
}
else
{
_journal.AppendDifference(tracked.Record, current);
}
tracked.Record = current;
}
}
internal bool TryGet(
RenderProjectionId id,
out RenderProjectionRecord record)
@ -184,7 +246,10 @@ internal sealed class StaticRenderProjectionJournal :
foreach (Dictionary<RenderProjectionId, TrackedProjection> projections
in _byLandblock.Values)
{
if (projections.TryGetValue(id, out TrackedProjection tracked))
if (projections.TryGetValue(
id,
out TrackedProjection? tracked)
&& tracked is not null)
{
record = tracked.Record;
return true;
@ -310,8 +375,43 @@ internal sealed class StaticRenderProjectionJournal :
private static uint Canonicalize(uint landblockId) =>
(landblockId & 0xFFFF0000u) | 0xFFFFu;
private readonly record struct TrackedProjection(
RenderProjectionRecord Record);
private void ReplaceStaticEntityMap(
uint landblockId,
IReadOnlyList<WorldEntity> entities)
{
RemoveStaticEntityMap(landblockId);
var retained = new List<WorldEntity>();
for (int i = 0; i < entities.Count; i++)
{
WorldEntity entity = entities[i];
if (entity.ServerGuid != 0)
continue;
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
if (!_trackedById.ContainsKey(id))
continue;
_idByEntity.Add(entity, id);
retained.Add(entity);
}
if (retained.Count > 0)
_entitiesByLandblock.Add(landblockId, retained);
}
private void RemoveStaticEntityMap(uint landblockId)
{
if (!_entitiesByLandblock.Remove(
landblockId,
out List<WorldEntity>? entities))
{
return;
}
for (int i = 0; i < entities.Count; i++)
_idByEntity.Remove(entities[i]);
}
private sealed class TrackedProjection(RenderProjectionRecord record)
{
public RenderProjectionRecord Record { get; set; } = record;
}
private sealed class ProjectionRecordComparer :
IComparer<RenderProjectionRecord>