fix(rendering): preserve exact scene traversal order

This commit is contained in:
Erik 2026-07-25 02:03:58 +02:00
parent 06e7754619
commit e0f36caa70
12 changed files with 500 additions and 77 deletions

View file

@ -55,10 +55,13 @@ public sealed class LiveRenderProjectionJournalTests
Load(harness.State, LandblockId);
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(
RenderProjectionDeltaKind.UpdateFlags,
harness.Journal.Pending[1].Kind);
harness.Journal.Pending[2].Kind);
const uint secondLandblock = 0x1235FFFFu;
const uint secondCell = 0x12350100u;
@ -66,13 +69,16 @@ public sealed class LiveRenderProjectionJournalTests
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, secondCell));
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(
RenderProjectionDeltaKind.Rebucket,
harness.Journal.Pending[2].Kind);
harness.Journal.Pending[4].Kind);
Assert.Equal(
incarnation,
harness.Journal.Pending[2].Record.OwnerIncarnation);
harness.Journal.Pending[4].Record.OwnerIncarnation);
Assert.Equal(1, harness.Projections.ProjectionCount);
}
@ -357,7 +363,10 @@ public sealed class LiveRenderProjectionJournalTests
var runtime = new LiveEntityRuntime(state, resources);
var journal = new RenderProjectionJournal(
RenderSceneGeneration.FromRaw(1));
var projections = new LiveRenderProjectionJournal(runtime, journal);
var projections = new LiveRenderProjectionJournal(
runtime,
journal,
new GpuWorldRenderTraversalOrderSource(state));
sink = projections;
var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1));
return new Harness(state, runtime, journal, projections, scene);

View file

@ -262,6 +262,79 @@ public sealed class RenderScenePViewFrameProductTests
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)
{
bool dynamic = entity.ServerGuid != 0;

View file

@ -137,6 +137,74 @@ public sealed class StaticRenderProjectionJournalTests
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]
public void StaleFarPublication_DoesNotMutateProjectionJournal()
{
@ -307,14 +375,16 @@ public sealed class StaticRenderProjectionJournalTests
private static GpuLandblockSpatialPublication Publication(
LandblockBuild build,
bool requiresActivation = true) =>
bool requiresActivation = true,
uint renderTraversalOrder = 1) =>
new(
build.LandblockId,
build.Landblock,
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
?? [],
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
requiresActivation);
requiresActivation,
renderTraversalOrder);
private static LandblockBuild Build(
uint landblockId,