fix(rendering): preserve exact scene traversal order
This commit is contained in:
parent
06e7754619
commit
e0f36caa70
12 changed files with 500 additions and 77 deletions
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -18,7 +20,8 @@ internal sealed record GpuLandblockSpatialPublication(
|
|||
LoadedLandblock Landblock,
|
||||
IReadOnlyList<ulong> AdditionalRenderIds,
|
||||
IReadOnlyList<WorldEntity> StaticEntities,
|
||||
bool RequiresActivation = true);
|
||||
bool RequiresActivation = true,
|
||||
uint RenderTraversalOrder = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
||||
|
|
@ -83,6 +86,14 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
}
|
||||
|
||||
private readonly Dictionary<uint, LoadedLandblock> _loaded = new();
|
||||
// The old renderer consumes landblocks in Dictionary entry-slot order.
|
||||
// Make that formerly implicit ordering explicit so both the accepted path
|
||||
// and the retained render scene share one stable source. Slots are reused
|
||||
// LIFO, matching Dictionary's remove/add behavior, while enumeration skips
|
||||
// retired holes.
|
||||
private readonly List<uint> _renderTraversalLandblockSlots = [];
|
||||
private readonly Stack<int> _freeRenderTraversalLandblockSlots = [];
|
||||
private readonly Dictionary<uint, int> _renderTraversalSlotByLandblock = [];
|
||||
private readonly Dictionary<uint, LandblockStreamTier> _tierByLandblock = new();
|
||||
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
|
||||
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
|
||||
|
|
@ -252,32 +263,127 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
if (!_availability.IsWorldAvailable)
|
||||
yield break;
|
||||
|
||||
foreach (var kvp in _loaded)
|
||||
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
|
||||
{
|
||||
uint landblockId = _renderTraversalLandblockSlots[slot];
|
||||
if (landblockId == 0)
|
||||
continue;
|
||||
LoadedLandblock landblock = _loaded[landblockId];
|
||||
IReadOnlyDictionary<uint, WorldEntity>? byId = null;
|
||||
if (includeAnimatedIndex)
|
||||
{
|
||||
if (!_animatedIndexByLandblock.TryGetValue(kvp.Key, out var cached)
|
||||
|| !ReferenceEquals(cached.Source, kvp.Value))
|
||||
if (!_animatedIndexByLandblock.TryGetValue(landblockId, out var cached)
|
||||
|| !ReferenceEquals(cached.Source, landblock))
|
||||
{
|
||||
var rebuilt = new Dictionary<uint, WorldEntity>(kvp.Value.Entities.Count);
|
||||
foreach (var entity in kvp.Value.Entities)
|
||||
var rebuilt = new Dictionary<uint, WorldEntity>(landblock.Entities.Count);
|
||||
foreach (var entity in landblock.Entities)
|
||||
rebuilt[entity.Id] = entity;
|
||||
|
||||
cached = new AnimatedEntityIndex(kvp.Value, rebuilt);
|
||||
_animatedIndexByLandblock[kvp.Key] = cached;
|
||||
cached = new AnimatedEntityIndex(landblock, rebuilt);
|
||||
_animatedIndexByLandblock[landblockId] = cached;
|
||||
}
|
||||
|
||||
byId = cached.EntitiesById;
|
||||
}
|
||||
|
||||
if (_aabbs.TryGetValue(kvp.Key, out var aabb))
|
||||
yield return (kvp.Key, aabb.Min, aabb.Max, kvp.Value.Entities, byId);
|
||||
if (_aabbs.TryGetValue(landblockId, out var aabb))
|
||||
yield return (landblockId, aabb.Min, aabb.Max, landblock.Entities, byId);
|
||||
else
|
||||
yield return (kvp.Key, Vector3.Zero, Vector3.Zero, kvp.Value.Entities, byId);
|
||||
yield return (landblockId, Vector3.Zero, Vector3.Zero, landblock.Entities, byId);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryGetRenderTraversalSortKey(
|
||||
WorldEntity entity,
|
||||
out RenderSortKey sortKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (_projectionLocations.TryGetValue(
|
||||
entity,
|
||||
out ProjectionLocation location)
|
||||
&& location.IsLoaded
|
||||
&& _renderTraversalSlotByLandblock.TryGetValue(
|
||||
location.LandblockId,
|
||||
out int landblockSlot))
|
||||
{
|
||||
sortKey = RenderTraversalSortKey.Compose(
|
||||
checked((uint)landblockSlot + 1),
|
||||
location.BucketIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
sortKey = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private uint GetRenderTraversalOrder(uint landblockId)
|
||||
{
|
||||
if (!_renderTraversalSlotByLandblock.TryGetValue(
|
||||
landblockId,
|
||||
out int slot))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Loaded landblock 0x{landblockId:X8} has no render traversal slot.");
|
||||
}
|
||||
|
||||
return checked((uint)slot + 1);
|
||||
}
|
||||
|
||||
private void AddLoadedLandblock(
|
||||
uint landblockId,
|
||||
LoadedLandblock landblock)
|
||||
{
|
||||
if (!_loaded.TryAdd(landblockId, landblock))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Landblock 0x{landblockId:X8} is already loaded.");
|
||||
}
|
||||
|
||||
int slot;
|
||||
if (_freeRenderTraversalLandblockSlots.TryPop(out int freeSlot))
|
||||
{
|
||||
slot = freeSlot;
|
||||
if (_renderTraversalLandblockSlots[slot] != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Render traversal slot {slot} was not free.");
|
||||
}
|
||||
_renderTraversalLandblockSlots[slot] = landblockId;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot = _renderTraversalLandblockSlots.Count;
|
||||
_renderTraversalLandblockSlots.Add(landblockId);
|
||||
}
|
||||
|
||||
if (!_renderTraversalSlotByLandblock.TryAdd(landblockId, slot))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Landblock 0x{landblockId:X8} already owns a render traversal slot.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveLoadedLandblock(
|
||||
uint landblockId,
|
||||
[NotNullWhen(true)]
|
||||
out LoadedLandblock? landblock)
|
||||
{
|
||||
if (!_loaded.Remove(landblockId, out landblock))
|
||||
return false;
|
||||
if (!_renderTraversalSlotByLandblock.Remove(
|
||||
landblockId,
|
||||
out int slot)
|
||||
|| _renderTraversalLandblockSlots[slot] != landblockId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Loaded landblock 0x{landblockId:X8} has inconsistent render traversal state.");
|
||||
}
|
||||
|
||||
_renderTraversalLandblockSlots[slot] = 0;
|
||||
_freeRenderTraversalLandblockSlots.Push(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total live entities currently parked in the pending bucket waiting
|
||||
/// for their landblock to arrive. Useful diagnostic for verifying the
|
||||
|
|
@ -689,7 +795,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
retained,
|
||||
Array.Empty<ulong>(),
|
||||
Array.Empty<WorldEntity>(),
|
||||
RequiresActivation: false);
|
||||
RequiresActivation: false,
|
||||
RenderTraversalOrder:
|
||||
GetRenderTraversalOrder(retained.LandblockId));
|
||||
}
|
||||
|
||||
// Reconcile the only fallible logical-owner edge before consuming any
|
||||
|
|
@ -727,18 +835,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
tier = LandblockStreamTier.Near;
|
||||
}
|
||||
|
||||
if (_loaded.Remove(landblock.LandblockId, out displaced))
|
||||
if (RemoveLoadedLandblock(landblock.LandblockId, out displaced))
|
||||
{
|
||||
RemoveLoadedLandblockFromFlatView(displaced);
|
||||
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
||||
}
|
||||
|
||||
_loaded[landblock.LandblockId] = landblock;
|
||||
AddLoadedLandblock(landblock.LandblockId, landblock);
|
||||
AddLoadedLandblockToFlatView(landblock);
|
||||
_tierByLandblock[landblock.LandblockId] = tier;
|
||||
return CreateSpatialPublication(
|
||||
_loaded[landblock.LandblockId],
|
||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>());
|
||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>(),
|
||||
GetRenderTraversalOrder(landblock.LandblockId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -804,7 +913,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
|
||||
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong> additionalRenderIds)
|
||||
IEnumerable<ulong> additionalRenderIds,
|
||||
uint renderTraversalOrder)
|
||||
{
|
||||
ulong[] renderIds = additionalRenderIds as ulong[]
|
||||
?? additionalRenderIds.ToArray();
|
||||
|
|
@ -815,7 +925,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
landblock.LandblockId,
|
||||
landblock,
|
||||
renderIds,
|
||||
staticEntities);
|
||||
staticEntities,
|
||||
RenderTraversalOrder: renderTraversalOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1027,7 +1138,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
_animatedIndexByLandblock.Remove(canonical);
|
||||
|
||||
_tierByLandblock.Remove(canonical);
|
||||
if (_loaded.Remove(canonical, out LoadedLandblock? removed))
|
||||
if (RemoveLoadedLandblock(canonical, out LoadedLandblock? removed))
|
||||
RemoveLoadedLandblockFromFlatView(removed);
|
||||
|
||||
for (int i = 0; i < retainedLive.Count; i++)
|
||||
|
|
@ -1453,7 +1564,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
canonical,
|
||||
_loaded[canonical],
|
||||
effectiveRenderIds,
|
||||
staticEntities);
|
||||
staticEntities,
|
||||
RenderTraversalOrder: GetRenderTraversalOrder(canonical));
|
||||
}
|
||||
|
||||
private void DrainVisibilityTransitions()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue