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
|
|
@ -375,7 +375,9 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
liveEntities,
|
liveEntities,
|
||||||
static runtime => runtime.Clear());
|
static runtime => runtime.Clear());
|
||||||
LiveRenderProjectionJournal? liveRenderProjections =
|
LiveRenderProjectionJournal? liveRenderProjections =
|
||||||
renderSceneShadow?.BindLiveRuntime(liveEntities);
|
renderSceneShadow?.BindLiveRuntime(
|
||||||
|
liveEntities,
|
||||||
|
new GpuWorldRenderTraversalOrderSource(worldState));
|
||||||
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
||||||
|
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrow derived adapter over <see cref="GpuWorldState"/>'s canonical spatial
|
||||||
|
/// traversal. It exposes no bucket mutation or gameplay lookup to the render
|
||||||
|
/// scene.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GpuWorldRenderTraversalOrderSource(
|
||||||
|
GpuWorldState world) : IRenderTraversalOrderSource
|
||||||
|
{
|
||||||
|
private readonly GpuWorldState _world =
|
||||||
|
world ?? throw new ArgumentNullException(nameof(world));
|
||||||
|
|
||||||
|
public bool TryGetTraversalSortKey(
|
||||||
|
WorldEntity entity,
|
||||||
|
out RenderSortKey sortKey) =>
|
||||||
|
_world.TryGetRenderTraversalSortKey(entity, out sortKey);
|
||||||
|
}
|
||||||
|
|
@ -24,16 +24,20 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
|
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly RenderProjectionJournal _journal;
|
private readonly RenderProjectionJournal _journal;
|
||||||
|
private readonly IRenderTraversalOrderSource _traversalOrder;
|
||||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||||
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||||
private readonly List<TrackedProjection> _activeScratch = [];
|
private readonly List<TrackedProjection> _activeScratch = [];
|
||||||
|
|
||||||
public LiveRenderProjectionJournal(
|
public LiveRenderProjectionJournal(
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
RenderProjectionJournal journal)
|
RenderProjectionJournal journal,
|
||||||
|
IRenderTraversalOrderSource traversalOrder)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
||||||
|
_traversalOrder = traversalOrder
|
||||||
|
?? throw new ArgumentNullException(nameof(traversalOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ProjectionCount => _byLocalId.Count;
|
public int ProjectionCount => _byLocalId.Count;
|
||||||
|
|
@ -257,7 +261,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
: fullCellId == 0
|
: fullCellId == 0
|
||||||
? 0
|
? 0
|
||||||
: Canonicalize(fullCellId);
|
: Canonicalize(fullCellId);
|
||||||
return RenderProjectionRecordFactory.ProjectEntity(
|
RenderProjectionRecord projected =
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
ProjectionId(entity.Id),
|
ProjectionId(entity.Id),
|
||||||
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||||
? RenderProjectionClass.EquippedChild
|
? RenderProjectionClass.EquippedChild
|
||||||
|
|
@ -268,6 +273,21 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
fullCellId,
|
fullCellId,
|
||||||
entity,
|
entity,
|
||||||
spatiallyVisible);
|
spatiallyVisible);
|
||||||
|
if (_traversalOrder.TryGetTraversalSortKey(
|
||||||
|
entity,
|
||||||
|
out RenderSortKey sortKey))
|
||||||
|
{
|
||||||
|
return projected with { SortKey = sortKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spatial withdrawal is not logical destruction. Retain the last
|
||||||
|
// resident order while the projection is inactive so a portal/rebucket
|
||||||
|
// edge does not manufacture an unrelated ordering mutation.
|
||||||
|
return _byLocalId.TryGetValue(
|
||||||
|
entity.Id,
|
||||||
|
out TrackedProjection? retained)
|
||||||
|
? projected with { SortKey = retained.Projection.SortKey }
|
||||||
|
: projected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Remove(TrackedProjection tracked)
|
private void Remove(TrackedProjection tracked)
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,35 @@ internal readonly record struct RenderDegradeState(byte Level, uint Revision);
|
||||||
|
|
||||||
internal readonly record struct RenderSortKey(ulong Value);
|
internal readonly record struct RenderSortKey(ulong Value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only seam over the spatial owner's exact resident traversal. The
|
||||||
|
/// render scene consumes this derived order only; it does not acquire spatial
|
||||||
|
/// or gameplay ownership.
|
||||||
|
/// </summary>
|
||||||
|
internal interface IRenderTraversalOrderSource
|
||||||
|
{
|
||||||
|
bool TryGetTraversalSortKey(
|
||||||
|
WorldEntity entity,
|
||||||
|
out RenderSortKey sortKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class RenderTraversalSortKey
|
||||||
|
{
|
||||||
|
public static RenderSortKey Compose(
|
||||||
|
uint landblockOrder,
|
||||||
|
int entityIndex)
|
||||||
|
{
|
||||||
|
if (landblockOrder == 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(landblockOrder));
|
||||||
|
if (entityIndex < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(entityIndex));
|
||||||
|
|
||||||
|
return new RenderSortKey(
|
||||||
|
((ulong)landblockOrder << 32)
|
||||||
|
| checked((uint)entityIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable source facts required to compare the derived presentation scene
|
/// Immutable source facts required to compare the derived presentation scene
|
||||||
/// with the accepted current path. These are diagnostics/render inputs only;
|
/// with the accepted current path. These are diagnostics/render inputs only;
|
||||||
|
|
|
||||||
|
|
@ -254,8 +254,6 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_expected.Sort(CandidateKeyComparer.Instance);
|
|
||||||
_actual.Sort(CandidateKeyComparer.Instance);
|
|
||||||
RenderSceneHash128 expectedDigest = BuildDigest(_expected);
|
RenderSceneHash128 expectedDigest = BuildDigest(_expected);
|
||||||
RenderSceneHash128 actualDigest = BuildDigest(_actual);
|
RenderSceneHash128 actualDigest = BuildDigest(_actual);
|
||||||
string? mismatch = CompareKeys();
|
string? mismatch = CompareKeys();
|
||||||
|
|
@ -641,8 +639,6 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_packedExpected.Sort(PackedInputKeyComparer.Instance);
|
|
||||||
_packedActual.Sort(PackedInputKeyComparer.Instance);
|
|
||||||
RenderSceneHash128 expectedDigest =
|
RenderSceneHash128 expectedDigest =
|
||||||
BuildPackedInputDigest(_packedExpected);
|
BuildPackedInputDigest(_packedExpected);
|
||||||
RenderSceneHash128 actualDigest =
|
RenderSceneHash128 actualDigest =
|
||||||
|
|
@ -873,44 +869,6 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CandidateKeyComparer : IComparer<CandidateKey>
|
|
||||||
{
|
|
||||||
public static CandidateKeyComparer Instance { get; } = new();
|
|
||||||
|
|
||||||
private CandidateKeyComparer()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Compare(CandidateKey left, CandidateKey right)
|
|
||||||
{
|
|
||||||
int value = left.Route.CompareTo(right.Route);
|
|
||||||
if (value != 0) return value;
|
|
||||||
value = left.RouteIndex.CompareTo(right.RouteIndex);
|
|
||||||
if (value != 0) return value;
|
|
||||||
value = left.CellId.CompareTo(right.CellId);
|
|
||||||
if (value != 0) return value;
|
|
||||||
return left.ProjectionId.CompareTo(right.ProjectionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PackedInputKeyComparer :
|
|
||||||
IComparer<PackedInputKey>
|
|
||||||
{
|
|
||||||
public static PackedInputKeyComparer Instance { get; } = new();
|
|
||||||
|
|
||||||
private PackedInputKeyComparer()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Compare(PackedInputKey left, PackedInputKey right)
|
|
||||||
{
|
|
||||||
int value = left.DrawSequence.CompareTo(right.DrawSequence);
|
|
||||||
if (value != 0) return value;
|
|
||||||
value = left.ProjectionId.CompareTo(right.ProjectionId);
|
|
||||||
if (value != 0) return value;
|
|
||||||
return left.MeshRefIndex.CompareTo(right.MeshRefIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -92,17 +92,22 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
|
||||||
internal RenderSceneQuery Query => _scene.OpenQuery();
|
internal RenderSceneQuery Query => _scene.OpenQuery();
|
||||||
|
|
||||||
public LiveRenderProjectionJournal BindLiveRuntime(
|
public LiveRenderProjectionJournal BindLiveRuntime(
|
||||||
LiveEntityRuntime runtime)
|
LiveEntityRuntime runtime,
|
||||||
|
IRenderTraversalOrderSource traversalOrder)
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
ArgumentNullException.ThrowIfNull(runtime);
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
ArgumentNullException.ThrowIfNull(traversalOrder);
|
||||||
if (_live is not null)
|
if (_live is not null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"The shadow render scene is already bound to a live runtime.");
|
"The shadow render scene is already bound to a live runtime.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_live = new LiveRenderProjectionJournal(runtime, _journal);
|
_live = new LiveRenderProjectionJournal(
|
||||||
|
runtime,
|
||||||
|
_journal,
|
||||||
|
traversalOrder);
|
||||||
return _live;
|
return _live;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,24 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
if (!publication.RequiresActivation)
|
if (!publication.RequiresActivation)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (publication.RenderTraversalOrder == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Landblock 0x{landblockId:X8} has no render traversal order.");
|
||||||
|
}
|
||||||
_candidates.Clear();
|
_candidates.Clear();
|
||||||
for (int i = 0; i < publication.Landblock.Entities.Count; i++)
|
for (int i = 0; i < publication.Landblock.Entities.Count; i++)
|
||||||
{
|
{
|
||||||
WorldEntity entity = publication.Landblock.Entities[i];
|
WorldEntity entity = publication.Landblock.Entities[i];
|
||||||
if (entity.ServerGuid == 0)
|
if (entity.ServerGuid == 0)
|
||||||
_candidates.Add(ProjectStaticEntity(landblockId, entity));
|
{
|
||||||
|
_candidates.Add(ProjectStaticEntity(
|
||||||
|
landblockId,
|
||||||
|
entity,
|
||||||
|
RenderTraversalSortKey.Compose(
|
||||||
|
publication.RenderTraversalOrder,
|
||||||
|
i)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (build.EnvCells is { } envCells)
|
if (build.EnvCells is { } envCells)
|
||||||
|
|
@ -237,6 +249,7 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
{
|
{
|
||||||
PreviousTransform = new PreviousRenderTransform(
|
PreviousTransform = new PreviousRenderTransform(
|
||||||
tracked.Record.Transform.LocalToWorld),
|
tracked.Record.Transform.LocalToWorld),
|
||||||
|
SortKey = tracked.Record.SortKey,
|
||||||
};
|
};
|
||||||
if (tracked.Record.ProjectionClass
|
if (tracked.Record.ProjectionClass
|
||||||
!= RenderProjectionClass.ActiveAnimatedStatic)
|
!= RenderProjectionClass.ActiveAnimatedStatic)
|
||||||
|
|
@ -290,7 +303,8 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
|
|
||||||
private RenderProjectionRecord ProjectStaticEntity(
|
private RenderProjectionRecord ProjectStaticEntity(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
WorldEntity entity)
|
WorldEntity entity,
|
||||||
|
RenderSortKey sortKey)
|
||||||
{
|
{
|
||||||
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
||||||
uint fullCellId = entity.ParentCellId ?? landblockId;
|
uint fullCellId = entity.ParentCellId ?? landblockId;
|
||||||
|
|
@ -303,7 +317,10 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
landblockId,
|
landblockId,
|
||||||
fullCellId,
|
fullCellId,
|
||||||
entity,
|
entity,
|
||||||
spatiallyVisible: true);
|
spatiallyVisible: true) with
|
||||||
|
{
|
||||||
|
SortKey = sortKey,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RenderProjectionRecord ProjectEnvCellShell(
|
private static RenderProjectionRecord ProjectEnvCellShell(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -18,7 +20,8 @@ internal sealed record GpuLandblockSpatialPublication(
|
||||||
LoadedLandblock Landblock,
|
LoadedLandblock Landblock,
|
||||||
IReadOnlyList<ulong> AdditionalRenderIds,
|
IReadOnlyList<ulong> AdditionalRenderIds,
|
||||||
IReadOnlyList<WorldEntity> StaticEntities,
|
IReadOnlyList<WorldEntity> StaticEntities,
|
||||||
bool RequiresActivation = true);
|
bool RequiresActivation = true,
|
||||||
|
uint RenderTraversalOrder = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
/// 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();
|
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, LandblockStreamTier> _tierByLandblock = new();
|
||||||
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
|
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
|
||||||
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
|
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
|
||||||
|
|
@ -252,32 +263,127 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (!_availability.IsWorldAvailable)
|
if (!_availability.IsWorldAvailable)
|
||||||
yield break;
|
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;
|
IReadOnlyDictionary<uint, WorldEntity>? byId = null;
|
||||||
if (includeAnimatedIndex)
|
if (includeAnimatedIndex)
|
||||||
{
|
{
|
||||||
if (!_animatedIndexByLandblock.TryGetValue(kvp.Key, out var cached)
|
if (!_animatedIndexByLandblock.TryGetValue(landblockId, out var cached)
|
||||||
|| !ReferenceEquals(cached.Source, kvp.Value))
|
|| !ReferenceEquals(cached.Source, landblock))
|
||||||
{
|
{
|
||||||
var rebuilt = new Dictionary<uint, WorldEntity>(kvp.Value.Entities.Count);
|
var rebuilt = new Dictionary<uint, WorldEntity>(landblock.Entities.Count);
|
||||||
foreach (var entity in kvp.Value.Entities)
|
foreach (var entity in landblock.Entities)
|
||||||
rebuilt[entity.Id] = entity;
|
rebuilt[entity.Id] = entity;
|
||||||
|
|
||||||
cached = new AnimatedEntityIndex(kvp.Value, rebuilt);
|
cached = new AnimatedEntityIndex(landblock, rebuilt);
|
||||||
_animatedIndexByLandblock[kvp.Key] = cached;
|
_animatedIndexByLandblock[landblockId] = cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
byId = cached.EntitiesById;
|
byId = cached.EntitiesById;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_aabbs.TryGetValue(kvp.Key, out var aabb))
|
if (_aabbs.TryGetValue(landblockId, out var aabb))
|
||||||
yield return (kvp.Key, aabb.Min, aabb.Max, kvp.Value.Entities, byId);
|
yield return (landblockId, aabb.Min, aabb.Max, landblock.Entities, byId);
|
||||||
else
|
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>
|
/// <summary>
|
||||||
/// Total live entities currently parked in the pending bucket waiting
|
/// Total live entities currently parked in the pending bucket waiting
|
||||||
/// for their landblock to arrive. Useful diagnostic for verifying the
|
/// for their landblock to arrive. Useful diagnostic for verifying the
|
||||||
|
|
@ -689,7 +795,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
retained,
|
retained,
|
||||||
Array.Empty<ulong>(),
|
Array.Empty<ulong>(),
|
||||||
Array.Empty<WorldEntity>(),
|
Array.Empty<WorldEntity>(),
|
||||||
RequiresActivation: false);
|
RequiresActivation: false,
|
||||||
|
RenderTraversalOrder:
|
||||||
|
GetRenderTraversalOrder(retained.LandblockId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile the only fallible logical-owner edge before consuming any
|
// Reconcile the only fallible logical-owner edge before consuming any
|
||||||
|
|
@ -727,18 +835,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
tier = LandblockStreamTier.Near;
|
tier = LandblockStreamTier.Near;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_loaded.Remove(landblock.LandblockId, out displaced))
|
if (RemoveLoadedLandblock(landblock.LandblockId, out displaced))
|
||||||
{
|
{
|
||||||
RemoveLoadedLandblockFromFlatView(displaced);
|
RemoveLoadedLandblockFromFlatView(displaced);
|
||||||
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaded[landblock.LandblockId] = landblock;
|
AddLoadedLandblock(landblock.LandblockId, landblock);
|
||||||
AddLoadedLandblockToFlatView(landblock);
|
AddLoadedLandblockToFlatView(landblock);
|
||||||
_tierByLandblock[landblock.LandblockId] = tier;
|
_tierByLandblock[landblock.LandblockId] = tier;
|
||||||
return CreateSpatialPublication(
|
return CreateSpatialPublication(
|
||||||
_loaded[landblock.LandblockId],
|
_loaded[landblock.LandblockId],
|
||||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>());
|
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>(),
|
||||||
|
GetRenderTraversalOrder(landblock.LandblockId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -804,7 +913,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
IEnumerable<ulong> additionalRenderIds)
|
IEnumerable<ulong> additionalRenderIds,
|
||||||
|
uint renderTraversalOrder)
|
||||||
{
|
{
|
||||||
ulong[] renderIds = additionalRenderIds as ulong[]
|
ulong[] renderIds = additionalRenderIds as ulong[]
|
||||||
?? additionalRenderIds.ToArray();
|
?? additionalRenderIds.ToArray();
|
||||||
|
|
@ -815,7 +925,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
landblock.LandblockId,
|
landblock.LandblockId,
|
||||||
landblock,
|
landblock,
|
||||||
renderIds,
|
renderIds,
|
||||||
staticEntities);
|
staticEntities,
|
||||||
|
RenderTraversalOrder: renderTraversalOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1027,7 +1138,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_animatedIndexByLandblock.Remove(canonical);
|
_animatedIndexByLandblock.Remove(canonical);
|
||||||
|
|
||||||
_tierByLandblock.Remove(canonical);
|
_tierByLandblock.Remove(canonical);
|
||||||
if (_loaded.Remove(canonical, out LoadedLandblock? removed))
|
if (RemoveLoadedLandblock(canonical, out LoadedLandblock? removed))
|
||||||
RemoveLoadedLandblockFromFlatView(removed);
|
RemoveLoadedLandblockFromFlatView(removed);
|
||||||
|
|
||||||
for (int i = 0; i < retainedLive.Count; i++)
|
for (int i = 0; i < retainedLive.Count; i++)
|
||||||
|
|
@ -1453,7 +1564,8 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
canonical,
|
canonical,
|
||||||
_loaded[canonical],
|
_loaded[canonical],
|
||||||
effectiveRenderIds,
|
effectiveRenderIds,
|
||||||
staticEntities);
|
staticEntities,
|
||||||
|
RenderTraversalOrder: GetRenderTraversalOrder(canonical));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrainVisibilityTransitions()
|
private void DrainVisibilityTransitions()
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,13 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Load(harness.State, LandblockId);
|
Load(harness.State, LandblockId);
|
||||||
|
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Equal(2, harness.Journal.Count);
|
Assert.Equal(3, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
harness.Journal.Pending[1].Kind);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
RenderProjectionDeltaKind.UpdateFlags,
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
harness.Journal.Pending[1].Kind);
|
harness.Journal.Pending[2].Kind);
|
||||||
|
|
||||||
const uint secondLandblock = 0x1235FFFFu;
|
const uint secondLandblock = 0x1235FFFFu;
|
||||||
const uint secondCell = 0x12350100u;
|
const uint secondCell = 0x12350100u;
|
||||||
|
|
@ -66,13 +69,16 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, secondCell));
|
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, secondCell));
|
||||||
harness.Projections.SynchronizeActiveSources();
|
harness.Projections.SynchronizeActiveSources();
|
||||||
|
|
||||||
Assert.Equal(3, harness.Journal.Count);
|
Assert.Equal(5, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
harness.Journal.Pending[3].Kind);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
RenderProjectionDeltaKind.Rebucket,
|
RenderProjectionDeltaKind.Rebucket,
|
||||||
harness.Journal.Pending[2].Kind);
|
harness.Journal.Pending[4].Kind);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
incarnation,
|
incarnation,
|
||||||
harness.Journal.Pending[2].Record.OwnerIncarnation);
|
harness.Journal.Pending[4].Record.OwnerIncarnation);
|
||||||
Assert.Equal(1, harness.Projections.ProjectionCount);
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,7 +363,10 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
var runtime = new LiveEntityRuntime(state, resources);
|
var runtime = new LiveEntityRuntime(state, resources);
|
||||||
var journal = new RenderProjectionJournal(
|
var journal = new RenderProjectionJournal(
|
||||||
RenderSceneGeneration.FromRaw(1));
|
RenderSceneGeneration.FromRaw(1));
|
||||||
var projections = new LiveRenderProjectionJournal(runtime, journal);
|
var projections = new LiveRenderProjectionJournal(
|
||||||
|
runtime,
|
||||||
|
journal,
|
||||||
|
new GpuWorldRenderTraversalOrderSource(state));
|
||||||
sink = projections;
|
sink = projections;
|
||||||
var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1));
|
var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1));
|
||||||
return new Harness(state, runtime, journal, projections, scene);
|
return new Harness(state, runtime, journal, projections, scene);
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,79 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
Assert.Equal(2, logs.Count);
|
Assert.Equal(2, logs.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Controller_RejectsEqualMembershipInDifferentTraversalOrder()
|
||||||
|
{
|
||||||
|
WorldEntity first =
|
||||||
|
Entity(1_000_001, 0x8000_0001, parentCell: null);
|
||||||
|
WorldEntity second =
|
||||||
|
Entity(1_000_002, 0x8000_0002, parentCell: null);
|
||||||
|
var oracle = new CurrentRenderSceneOracle();
|
||||||
|
var partition = new InteriorEntityPartition.Result();
|
||||||
|
InteriorEntityPartition.Partition(
|
||||||
|
partition,
|
||||||
|
[],
|
||||||
|
[Entry([first, second])],
|
||||||
|
oracle);
|
||||||
|
oracle.BeginPViewFrame();
|
||||||
|
oracle.ObservePViewBucket(
|
||||||
|
CurrentRenderPViewRoute.DynamicLast,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
[first, second]);
|
||||||
|
oracle.CompletePViewFrame();
|
||||||
|
oracle.BeginDispatcherFrame();
|
||||||
|
oracle.ObserveDispatcherDraw(
|
||||||
|
WbDrawDispatcher.EntitySet.All,
|
||||||
|
entitiesWalked: 2,
|
||||||
|
[
|
||||||
|
(first, 0, Landblock),
|
||||||
|
(second, 0, Landblock),
|
||||||
|
]);
|
||||||
|
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation);
|
||||||
|
RenderProjectionRecord firstRecord = Project(first) with
|
||||||
|
{
|
||||||
|
SortKey = new RenderSortKey(2),
|
||||||
|
};
|
||||||
|
RenderProjectionRecord secondRecord = Project(second) with
|
||||||
|
{
|
||||||
|
SortKey = new RenderSortKey(1),
|
||||||
|
};
|
||||||
|
shadow.Journal.Register(in firstRecord);
|
||||||
|
shadow.Journal.Register(in secondRecord);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var controller = new RenderScenePViewFrameProductController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
PortalVisibilityFrame portal = Portal(Cell);
|
||||||
|
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||||
|
ViewconeCuller viewcone =
|
||||||
|
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||||
|
|
||||||
|
controller.BuildAndCompare(
|
||||||
|
portal,
|
||||||
|
clip,
|
||||||
|
viewcone,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
EmptyCellSource.Instance,
|
||||||
|
[],
|
||||||
|
Landblock,
|
||||||
|
rootIsOutdoor: true);
|
||||||
|
|
||||||
|
RenderFrameProductComparisonSnapshot snapshot =
|
||||||
|
controller.Snapshot;
|
||||||
|
Assert.Equal(1, snapshot.MismatchCount);
|
||||||
|
Assert.Contains(
|
||||||
|
$"ProjectionId = {LiveRenderProjectionJournal.ProjectionId(first.Id)}",
|
||||||
|
snapshot.FirstMismatch);
|
||||||
|
Assert.Contains(
|
||||||
|
$"ProjectionId = {LiveRenderProjectionJournal.ProjectionId(second.Id)}",
|
||||||
|
snapshot.FirstMismatch);
|
||||||
|
Assert.Equal(1, snapshot.PackedInputMismatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static RenderProjectionRecord Project(WorldEntity entity)
|
private static RenderProjectionRecord Project(WorldEntity entity)
|
||||||
{
|
{
|
||||||
bool dynamic = entity.ServerGuid != 0;
|
bool dynamic = entity.ServerGuid != 0;
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,74 @@ public sealed class StaticRenderProjectionJournalTests
|
||||||
StaticRenderProjectionJournal.StaticEntityId(otherLandblock, 42));
|
StaticRenderProjectionJournal.StaticEntityId(otherLandblock, 42));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reconcile_PreservesResidentTraversalOrderInSortKeys()
|
||||||
|
{
|
||||||
|
const uint secondLandblock = 0x5678FFFFu;
|
||||||
|
var journal = new RenderProjectionJournal(Generation(10));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
WorldEntity first = Entity(99);
|
||||||
|
WorldEntity liveGap = Entity(77, serverGuid: 0x80000077u);
|
||||||
|
WorldEntity second = Entity(1);
|
||||||
|
LandblockBuild firstBuild = Build(
|
||||||
|
LandblockId,
|
||||||
|
[first, liveGap, second],
|
||||||
|
includeShell: false);
|
||||||
|
LandblockBuild secondBuild = Build(
|
||||||
|
secondLandblock,
|
||||||
|
[Entity(500)],
|
||||||
|
includeShell: false);
|
||||||
|
|
||||||
|
statics.Reconcile(firstBuild, Publication(firstBuild));
|
||||||
|
statics.Reconcile(
|
||||||
|
secondBuild,
|
||||||
|
Publication(secondBuild, renderTraversalOrder: 2));
|
||||||
|
|
||||||
|
Assert.True(statics.TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
first.Id),
|
||||||
|
out RenderProjectionRecord firstRecord));
|
||||||
|
Assert.True(statics.TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
second.Id),
|
||||||
|
out RenderProjectionRecord secondRecord));
|
||||||
|
Assert.True(statics.TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
secondLandblock,
|
||||||
|
500),
|
||||||
|
out RenderProjectionRecord laterLandblockRecord));
|
||||||
|
Assert.True(firstRecord.SortKey.Value < secondRecord.SortKey.Value);
|
||||||
|
Assert.True(
|
||||||
|
secondRecord.SortKey.Value
|
||||||
|
< laterLandblockRecord.SortKey.Value);
|
||||||
|
Assert.Equal(
|
||||||
|
2u,
|
||||||
|
unchecked((uint)secondRecord.SortKey.Value));
|
||||||
|
|
||||||
|
LandblockBuild reordered = Build(
|
||||||
|
LandblockId,
|
||||||
|
[second, first],
|
||||||
|
includeShell: false);
|
||||||
|
statics.Reconcile(reordered, Publication(reordered));
|
||||||
|
|
||||||
|
Assert.True(statics.TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
first.Id),
|
||||||
|
out firstRecord));
|
||||||
|
Assert.True(statics.TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
second.Id),
|
||||||
|
out secondRecord));
|
||||||
|
Assert.True(secondRecord.SortKey.Value < firstRecord.SortKey.Value);
|
||||||
|
Assert.Equal(
|
||||||
|
2uL,
|
||||||
|
laterLandblockRecord.SortKey.Value >> 32);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StaleFarPublication_DoesNotMutateProjectionJournal()
|
public void StaleFarPublication_DoesNotMutateProjectionJournal()
|
||||||
{
|
{
|
||||||
|
|
@ -307,14 +375,16 @@ public sealed class StaticRenderProjectionJournalTests
|
||||||
|
|
||||||
private static GpuLandblockSpatialPublication Publication(
|
private static GpuLandblockSpatialPublication Publication(
|
||||||
LandblockBuild build,
|
LandblockBuild build,
|
||||||
bool requiresActivation = true) =>
|
bool requiresActivation = true,
|
||||||
|
uint renderTraversalOrder = 1) =>
|
||||||
new(
|
new(
|
||||||
build.LandblockId,
|
build.LandblockId,
|
||||||
build.Landblock,
|
build.Landblock,
|
||||||
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
|
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
|
||||||
?? [],
|
?? [],
|
||||||
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
|
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
|
||||||
requiresActivation);
|
requiresActivation,
|
||||||
|
renderTraversalOrder);
|
||||||
|
|
||||||
private static LandblockBuild Build(
|
private static LandblockBuild Build(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
|
public sealed class GpuWorldStateRenderTraversalTests
|
||||||
|
{
|
||||||
|
private const uint FirstLandblock = 0x1234FFFFu;
|
||||||
|
private const uint SecondLandblock = 0x1235FFFFu;
|
||||||
|
private const uint ReusedLandblock = 0x1236FFFFu;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TraversalKeys_MirrorResidentLandblockAndMutableBucketOrder()
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
WorldEntity first = Entity(1, 0x80000001u);
|
||||||
|
WorldEntity moved = Entity(2, 0x80000002u);
|
||||||
|
WorldEntity secondLandblockEntity = Entity(3, 0x80000003u);
|
||||||
|
state.AddLandblock(Landblock(FirstLandblock, first, moved));
|
||||||
|
state.AddLandblock(Landblock(
|
||||||
|
SecondLandblock,
|
||||||
|
secondLandblockEntity));
|
||||||
|
|
||||||
|
AssertSort(state, first, landblockOrder: 1, entityIndex: 0);
|
||||||
|
AssertSort(state, moved, landblockOrder: 1, entityIndex: 1);
|
||||||
|
AssertSort(
|
||||||
|
state,
|
||||||
|
secondLandblockEntity,
|
||||||
|
landblockOrder: 2,
|
||||||
|
entityIndex: 0);
|
||||||
|
|
||||||
|
state.RemoveLiveEntityProjection(first);
|
||||||
|
|
||||||
|
AssertSort(state, moved, landblockOrder: 1, entityIndex: 0);
|
||||||
|
|
||||||
|
WorldEntity replacement = Entity(4, 0x80000004u);
|
||||||
|
state.AddLandblock(Landblock(SecondLandblock, replacement));
|
||||||
|
|
||||||
|
Assert.False(state.TryGetRenderTraversalSortKey(
|
||||||
|
secondLandblockEntity,
|
||||||
|
out _));
|
||||||
|
AssertSort(state, replacement, landblockOrder: 2, entityIndex: 0);
|
||||||
|
|
||||||
|
state.RemoveLandblock(FirstLandblock);
|
||||||
|
WorldEntity reused = Entity(5, 0x80000005u);
|
||||||
|
state.AddLandblock(Landblock(ReusedLandblock, reused));
|
||||||
|
|
||||||
|
AssertSort(state, reused, landblockOrder: 1, entityIndex: 0);
|
||||||
|
Assert.Equal(
|
||||||
|
[ReusedLandblock, SecondLandblock],
|
||||||
|
state.LandblockEntriesWithoutAnimatedIndex
|
||||||
|
.Select(static entry => entry.LandblockId)
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TraversalKey_IsUnavailableWhileProjectionIsPending()
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
WorldEntity pending = Entity(1, 0x80000001u);
|
||||||
|
|
||||||
|
state.PlaceLiveEntityProjection(FirstLandblock, pending);
|
||||||
|
|
||||||
|
Assert.False(state.TryGetRenderTraversalSortKey(pending, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertSort(
|
||||||
|
GpuWorldState state,
|
||||||
|
WorldEntity entity,
|
||||||
|
uint landblockOrder,
|
||||||
|
int entityIndex)
|
||||||
|
{
|
||||||
|
Assert.True(state.TryGetRenderTraversalSortKey(
|
||||||
|
entity,
|
||||||
|
out RenderSortKey actual));
|
||||||
|
Assert.Equal(
|
||||||
|
RenderTraversalSortKey.Compose(landblockOrder, entityIndex),
|
||||||
|
actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoadedLandblock Landblock(
|
||||||
|
uint landblockId,
|
||||||
|
params WorldEntity[] entities) =>
|
||||||
|
new(
|
||||||
|
landblockId,
|
||||||
|
new LandBlock(),
|
||||||
|
entities,
|
||||||
|
PhysicsDatBundle.Empty);
|
||||||
|
|
||||||
|
private static WorldEntity Entity(uint id, uint serverGuid) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ServerGuid = serverGuid,
|
||||||
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
|
Position = Vector3.Zero,
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue