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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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