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
|
|
@ -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 RenderProjectionJournal _journal;
|
||||
private readonly IRenderTraversalOrderSource _traversalOrder;
|
||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||
private readonly List<TrackedProjection> _activeScratch = [];
|
||||
|
||||
public LiveRenderProjectionJournal(
|
||||
LiveEntityRuntime runtime,
|
||||
RenderProjectionJournal journal)
|
||||
RenderProjectionJournal journal,
|
||||
IRenderTraversalOrderSource traversalOrder)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
||||
_traversalOrder = traversalOrder
|
||||
?? throw new ArgumentNullException(nameof(traversalOrder));
|
||||
}
|
||||
|
||||
public int ProjectionCount => _byLocalId.Count;
|
||||
|
|
@ -257,7 +261,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
: fullCellId == 0
|
||||
? 0
|
||||
: Canonicalize(fullCellId);
|
||||
return RenderProjectionRecordFactory.ProjectEntity(
|
||||
RenderProjectionRecord projected =
|
||||
RenderProjectionRecordFactory.ProjectEntity(
|
||||
ProjectionId(entity.Id),
|
||||
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||
? RenderProjectionClass.EquippedChild
|
||||
|
|
@ -268,6 +273,21 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
fullCellId,
|
||||
entity,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -178,6 +178,35 @@ internal readonly record struct RenderDegradeState(byte Level, uint Revision);
|
|||
|
||||
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>
|
||||
/// Immutable source facts required to compare the derived presentation scene
|
||||
/// 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 actualDigest = BuildDigest(_actual);
|
||||
string? mismatch = CompareKeys();
|
||||
|
|
@ -641,8 +639,6 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
}
|
||||
}
|
||||
|
||||
_packedExpected.Sort(PackedInputKeyComparer.Instance);
|
||||
_packedActual.Sort(PackedInputKeyComparer.Instance);
|
||||
RenderSceneHash128 expectedDigest =
|
||||
BuildPackedInputDigest(_packedExpected);
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -92,17 +92,22 @@ internal sealed class RenderSceneShadowRuntime : IDisposable
|
|||
internal RenderSceneQuery Query => _scene.OpenQuery();
|
||||
|
||||
public LiveRenderProjectionJournal BindLiveRuntime(
|
||||
LiveEntityRuntime runtime)
|
||||
LiveEntityRuntime runtime,
|
||||
IRenderTraversalOrderSource traversalOrder)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(traversalOrder);
|
||||
if (_live is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The shadow render scene is already bound to a live runtime.");
|
||||
}
|
||||
|
||||
_live = new LiveRenderProjectionJournal(runtime, _journal);
|
||||
_live = new LiveRenderProjectionJournal(
|
||||
runtime,
|
||||
_journal,
|
||||
traversalOrder);
|
||||
return _live;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,12 +63,24 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
if (!publication.RequiresActivation)
|
||||
return;
|
||||
|
||||
if (publication.RenderTraversalOrder == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Landblock 0x{landblockId:X8} has no render traversal order.");
|
||||
}
|
||||
_candidates.Clear();
|
||||
for (int i = 0; i < publication.Landblock.Entities.Count; i++)
|
||||
{
|
||||
WorldEntity entity = publication.Landblock.Entities[i];
|
||||
if (entity.ServerGuid == 0)
|
||||
_candidates.Add(ProjectStaticEntity(landblockId, entity));
|
||||
{
|
||||
_candidates.Add(ProjectStaticEntity(
|
||||
landblockId,
|
||||
entity,
|
||||
RenderTraversalSortKey.Compose(
|
||||
publication.RenderTraversalOrder,
|
||||
i)));
|
||||
}
|
||||
}
|
||||
|
||||
if (build.EnvCells is { } envCells)
|
||||
|
|
@ -237,6 +249,7 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
{
|
||||
PreviousTransform = new PreviousRenderTransform(
|
||||
tracked.Record.Transform.LocalToWorld),
|
||||
SortKey = tracked.Record.SortKey,
|
||||
};
|
||||
if (tracked.Record.ProjectionClass
|
||||
!= RenderProjectionClass.ActiveAnimatedStatic)
|
||||
|
|
@ -290,7 +303,8 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
|
||||
private RenderProjectionRecord ProjectStaticEntity(
|
||||
uint landblockId,
|
||||
WorldEntity entity)
|
||||
WorldEntity entity,
|
||||
RenderSortKey sortKey)
|
||||
{
|
||||
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
||||
uint fullCellId = entity.ParentCellId ?? landblockId;
|
||||
|
|
@ -303,7 +317,10 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
landblockId,
|
||||
fullCellId,
|
||||
entity,
|
||||
spatiallyVisible: true);
|
||||
spatiallyVisible: true) with
|
||||
{
|
||||
SortKey = sortKey,
|
||||
};
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord ProjectEnvCellShell(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue