perf(rendering): retain synchronous frame scratch

Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
This commit is contained in:
Erik 2026-07-25 05:28:30 +02:00
parent b9cbf5e040
commit b3427554c3
10 changed files with 397 additions and 129 deletions

View file

@ -97,6 +97,21 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
private readonly Dictionary<uint, LandblockStreamTier> _tierByLandblock = new();
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
// H-a4: synchronous render borrows. The former yield properties created
// fresh iterator state at every per-frame call site.
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>
_landblockEntriesView = [];
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>
_landblockEntriesWithoutAnimatedIndexView = [];
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)>
_landblockBoundsView = [];
private bool _landblockEntriesViewDirty = true;
private bool _landblockEntriesWithoutAnimatedIndexViewDirty = true;
private bool _landblockBoundsViewDirty = true;
private sealed record AnimatedEntityIndex(
LoadedLandblock Source,
@ -201,6 +216,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
public void SetLandblockAabb(uint landblockId, Vector3 min, Vector3 max)
{
_aabbs[landblockId] = (min, max);
InvalidateLandblockRenderViews(entries: true, bounds: true);
}
/// <summary>
@ -219,49 +235,77 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
/// explicitly invalidate exactly the affected index generation.
/// </para>
/// </summary>
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
=> EnumerateLandblockEntries(includeAnimatedIndex: true);
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
=> GetLandblockEntriesView(includeAnimatedIndex: true);
/// <summary>
/// Per-landblock render entries without the animated lookup dictionary.
/// Static render passes use this to avoid rebuilding an index they cannot
/// consume.
/// </summary>
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntriesWithoutAnimatedIndex
=> EnumerateLandblockEntries(includeAnimatedIndex: false);
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntriesWithoutAnimatedIndex
=> GetLandblockEntriesView(includeAnimatedIndex: false);
/// <summary>
/// Lightweight bounds-only enumeration for overlays and diagnostics.
/// Does not walk entity lists.
/// </summary>
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds
{
get
{
if (!_availability.IsWorldAvailable)
yield break;
return Array.Empty<(uint, Vector3, Vector3)>();
if (!_landblockBoundsViewDirty)
return _landblockBoundsView;
foreach (var kvp in _loaded)
_landblockBoundsView.Clear();
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
{
if (_aabbs.TryGetValue(kvp.Key, out var aabb))
yield return (kvp.Key, aabb.Min, aabb.Max);
uint landblockId = _renderTraversalLandblockSlots[slot];
if (landblockId == 0)
continue;
if (_aabbs.TryGetValue(landblockId, out var aabb))
_landblockBoundsView.Add((landblockId, aabb.Min, aabb.Max));
else
yield return (kvp.Key, Vector3.Zero, Vector3.Zero);
_landblockBoundsView.Add(
(landblockId, Vector3.Zero, Vector3.Zero));
}
_landblockBoundsViewDirty = false;
return _landblockBoundsView;
}
}
private IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> EnumerateLandblockEntries(
private IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> GetLandblockEntriesView(
bool includeAnimatedIndex)
{
if (!_availability.IsWorldAvailable)
yield break;
{
return Array.Empty<(uint, Vector3, Vector3,
IReadOnlyList<WorldEntity>,
IReadOnlyDictionary<uint, WorldEntity>?)>();
}
List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> view =
includeAnimatedIndex
? _landblockEntriesView
: _landblockEntriesWithoutAnimatedIndexView;
bool dirty = includeAnimatedIndex
? _landblockEntriesViewDirty
: _landblockEntriesWithoutAnimatedIndexViewDirty;
if (!dirty)
return view;
view.Clear();
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
{
@ -287,10 +331,29 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
}
if (_aabbs.TryGetValue(landblockId, out var aabb))
yield return (landblockId, aabb.Min, aabb.Max, landblock.Entities, byId);
view.Add(
(landblockId, aabb.Min, aabb.Max, landblock.Entities, byId));
else
yield return (landblockId, Vector3.Zero, Vector3.Zero, landblock.Entities, byId);
view.Add(
(landblockId, Vector3.Zero, Vector3.Zero, landblock.Entities, byId));
}
if (includeAnimatedIndex)
_landblockEntriesViewDirty = false;
else
_landblockEntriesWithoutAnimatedIndexViewDirty = false;
return view;
}
private void InvalidateLandblockRenderViews(bool entries, bool bounds)
{
if (entries)
{
_landblockEntriesViewDirty = true;
_landblockEntriesWithoutAnimatedIndexViewDirty = true;
}
if (bounds)
_landblockBoundsViewDirty = true;
}
internal bool TryGetRenderTraversalSortKey(
@ -361,6 +424,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
throw new InvalidOperationException(
$"Landblock 0x{landblockId:X8} already owns a render traversal slot.");
}
InvalidateLandblockRenderViews(entries: true, bounds: true);
}
private bool RemoveLoadedLandblock(
@ -381,6 +445,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
_renderTraversalLandblockSlots[slot] = 0;
_freeRenderTraversalLandblockSlots.Push(slot);
InvalidateLandblockRenderViews(entries: true, bounds: true);
return true;
}
@ -726,6 +791,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
int bucketIndex = entities.Count;
entities.Add(entity);
_animatedIndexByLandblock.Remove(landblockId);
InvalidateLandblockRenderViews(entries: true, bounds: false);
AddFlatEntity(entity);
SetProjectionLocation(entity, landblockId, isLoaded: true, bucketIndex);
AdjustVisibleLiveProjection(entity, +1);
@ -765,6 +831,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
entities.RemoveAt(lastIndex);
_animatedIndexByLandblock.Remove(location.LandblockId);
InvalidateLandblockRenderViews(entries: true, bounds: false);
RemoveFlatEntity(entity);
RemoveProjectionLocation(entity);
AdjustVisibleLiveProjection(entity, -1);
@ -1463,6 +1530,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
}
_loaded[canonical] = NormalizeLoadedLandblock(lb, retainedLive);
_animatedIndexByLandblock.Remove(canonical);
InvalidateLandblockRenderViews(entries: true, bounds: false);
_tierByLandblock[canonical] = LandblockStreamTier.Far;
}
@ -1595,6 +1663,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
AdjustVisibleLiveProjection(entity, +1);
}
_animatedIndexByLandblock.Remove(canonical);
InvalidateLandblockRenderViews(entries: true, bounds: false);
_tierByLandblock[canonical] = LandblockStreamTier.Near;
ulong[] effectiveRenderIds = additionalRenderIds?.ToArray() ?? Array.Empty<ulong>();
WorldEntity[] staticEntities = entities