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:
parent
b9cbf5e040
commit
b3427554c3
10 changed files with 397 additions and 129 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue