feat(rendering): compare scene-built PView candidates
Build a same-frame candidate product from incremental render-scene indices and compare its exact PView routes against the accepted current path without changing the production draw source.
This commit is contained in:
parent
8c638654be
commit
e346f8bbaf
13 changed files with 1187 additions and 9 deletions
|
|
@ -377,6 +377,7 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
|
||||
RenderSceneShadowComparisonSnapshot.Disabled,
|
||||
RenderFrameProductComparisonSnapshot.Disabled,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ public sealed class RenderFrameProductTests
|
|||
0x01000001,
|
||||
Matrix4x4.Identity,
|
||||
SelectionMesh()));
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
0,
|
||||
0,
|
||||
[outdoor]);
|
||||
var digest = new RenderSceneDigest(
|
||||
Generation,
|
||||
new RenderProjectionCounts(Total: 4, 1, 2, 1, 0, 0),
|
||||
|
|
@ -70,8 +75,17 @@ public sealed class RenderFrameProductTests
|
|||
Assert.Equal(2, view.Classifications.Length);
|
||||
Assert.Equal(1, view.LightSets.Length);
|
||||
Assert.Equal(1, view.SelectionParts.Length);
|
||||
Assert.Equal(outdoor.Id, view.RouteCandidates[0].Id);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1),
|
||||
new RenderFrameCandidateRange(
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1),
|
||||
view.RouteRanges[0]);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1, 1),
|
||||
view.DiagnosticCounts);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,353 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RenderScenePViewFrameProductTests
|
||||
{
|
||||
private const uint Landblock = 0xA9B4FFFF;
|
||||
private const uint Cell = 0xA9B40170;
|
||||
private static readonly RenderSceneGeneration Generation =
|
||||
RenderSceneGeneration.FromRaw(1);
|
||||
|
||||
[Fact]
|
||||
public void Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners()
|
||||
{
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord outdoor = Record(
|
||||
0x0100_0000_0000_0001,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord hidden = Record(
|
||||
0x0100_0000_0000_0002,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
flags: RenderProjectionFlags.Hidden
|
||||
| RenderProjectionFlags.SpatiallyResident);
|
||||
RenderProjectionRecord withdrawn = Record(
|
||||
0x0100_0000_0000_0003,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
flags: RenderProjectionFlags.Draw);
|
||||
RenderProjectionRecord shell = Record(
|
||||
0x0200_0000_0000_0004,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord cellStatic = Record(
|
||||
0x0100_0000_0000_0005,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0300_0000_0000_0006,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
RenderProjectionRecord cellDynamic = Record(
|
||||
0x0300_0000_0000_0007,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord[] records =
|
||||
[
|
||||
outdoor,
|
||||
hidden,
|
||||
withdrawn,
|
||||
shell,
|
||||
cellStatic,
|
||||
outdoorDynamic,
|
||||
cellDynamic,
|
||||
];
|
||||
var deltas = new RenderProjectionDelta[records.Length];
|
||||
for (int index = 0; index < records.Length; index++)
|
||||
{
|
||||
deltas[index] = RenderProjectionDelta.Register(
|
||||
Generation,
|
||||
(ulong)index + 1,
|
||||
records[index]);
|
||||
}
|
||||
scene.Apply(deltas);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[outdoor.Id, hidden.Id],
|
||||
view.OutdoorStaticCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(
|
||||
[cellStatic.Id],
|
||||
view.CellStaticCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(
|
||||
[outdoorDynamic.Id, cellDynamic.Id],
|
||||
view.DynamicCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(5, view.Transforms.Length);
|
||||
Assert.Equal(5, view.RouteCandidates.Length);
|
||||
Assert.Equal(3, view.RouteRanges.Length);
|
||||
Assert.DoesNotContain(
|
||||
view.RouteCandidates.ToArray(),
|
||||
item => item.Id == withdrawn.Id || item.Id == shell.Id);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(
|
||||
OutdoorStaticCandidates: 2,
|
||||
CellStaticCandidates: 1,
|
||||
DynamicCandidates: 2,
|
||||
TransformCount: 5,
|
||||
OpaqueClassificationCount: 0,
|
||||
AlphaClassificationCount: 0,
|
||||
LightSetCount: 0,
|
||||
SelectionPartCount: 0,
|
||||
RouteCandidateCount: 5),
|
||||
view.DiagnosticCounts);
|
||||
Assert.Same(portal, view.PortalFrame);
|
||||
Assert.Same(clip, view.ClipAssembly);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
||||
{
|
||||
WorldEntity outdoor = Entity(10, serverGuid: 0, parentCell: null);
|
||||
WorldEntity cellStatic = Entity(11, serverGuid: 0, parentCell: Cell);
|
||||
WorldEntity outdoorDynamic =
|
||||
Entity(1_000_012, 0x8000_0012, parentCell: null);
|
||||
WorldEntity cellDynamic =
|
||||
Entity(1_000_013, 0x8000_0013, parentCell: Cell);
|
||||
WorldEntity[] entities =
|
||||
[outdoor, cellStatic, outdoorDynamic, cellDynamic];
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var partition = new InteriorEntityPartition.Result();
|
||||
InteriorEntityPartition.Partition(
|
||||
partition,
|
||||
[Cell],
|
||||
[Entry(entities)],
|
||||
oracle);
|
||||
oracle.BeginPViewFrame();
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
|
||||
0,
|
||||
0,
|
||||
[outdoor]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
0,
|
||||
0,
|
||||
[cellStatic]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
0,
|
||||
0,
|
||||
[outdoorDynamic, cellDynamic]);
|
||||
oracle.CompletePViewFrame();
|
||||
|
||||
using var shadow = new RenderSceneShadowRuntime(Generation);
|
||||
foreach (WorldEntity entity in entities)
|
||||
{
|
||||
RenderProjectionRecord record = Project(entity);
|
||||
shadow.Journal.Register(in record);
|
||||
}
|
||||
shadow.DrainUpdateBoundary();
|
||||
var logs = new List<string>();
|
||||
var controller = new RenderScenePViewFrameProductController(
|
||||
shadow,
|
||||
oracle,
|
||||
logs.Add);
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
|
||||
controller.BuildAndCompare(
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
rootIsOutdoor: true);
|
||||
|
||||
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
|
||||
Assert.True(matched.Enabled);
|
||||
Assert.Equal(1uL, matched.ComparisonCount);
|
||||
Assert.Equal(1uL, matched.SuccessfulComparisonCount);
|
||||
Assert.Equal(4, matched.ExpectedCandidateCount);
|
||||
Assert.Equal(4, matched.ActualCandidateCount);
|
||||
Assert.Equal(0, matched.MismatchCount);
|
||||
Assert.Null(matched.FirstMismatch);
|
||||
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
|
||||
Assert.Empty(logs);
|
||||
|
||||
RenderProjectionId withdrawnId =
|
||||
LiveRenderProjectionJournal.ProjectionId(cellDynamic.Id);
|
||||
shadow.Journal.Unregister(
|
||||
withdrawnId,
|
||||
RenderOwnerIncarnation.FromRaw(cellDynamic.Id));
|
||||
shadow.DrainUpdateBoundary();
|
||||
controller.BuildAndCompare(
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
rootIsOutdoor: true);
|
||||
|
||||
RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot;
|
||||
Assert.Equal(2uL, mismatch.ComparisonCount);
|
||||
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
|
||||
Assert.Equal(1, mismatch.MismatchCount);
|
||||
Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
|
||||
Assert.Single(logs);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Project(WorldEntity entity)
|
||||
{
|
||||
bool dynamic = entity.ServerGuid != 0;
|
||||
RenderProjectionId id = dynamic
|
||||
? LiveRenderProjectionJournal.ProjectionId(entity.Id)
|
||||
: StaticRenderProjectionJournal.StaticEntityId(
|
||||
Landblock,
|
||||
entity.Id);
|
||||
RenderProjectionClass projectionClass = dynamic
|
||||
? RenderProjectionClass.LiveDynamicRoot
|
||||
: entity.ParentCellId is uint parentCell
|
||||
&& InteriorEntityPartition.IsIndoorCellId(parentCell)
|
||||
? RenderProjectionClass.IndoorCellStatic
|
||||
: RenderProjectionClass.OutdoorStatic;
|
||||
return RenderProjectionRecordFactory.ProjectEntity(
|
||||
id,
|
||||
projectionClass,
|
||||
RenderOwnerIncarnation.FromRaw(entity.Id),
|
||||
Landblock,
|
||||
entity.ParentCellId ?? Landblock,
|
||||
entity,
|
||||
spatiallyVisible: true);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Record(
|
||||
ulong id,
|
||||
RenderProjectionClass projectionClass,
|
||||
uint? parentCell = null,
|
||||
RenderProjectionFlags flags =
|
||||
RenderProjectionFlags.Draw
|
||||
| RenderProjectionFlags.SpatiallyResident)
|
||||
{
|
||||
uint fullCellId = parentCell ?? Landblock;
|
||||
Matrix4x4 transform = Matrix4x4.Identity;
|
||||
return new RenderProjectionRecord(
|
||||
RenderProjectionId.FromRaw(id),
|
||||
projectionClass,
|
||||
RenderOwnerIncarnation.FromRaw(id),
|
||||
new RenderTransform(transform),
|
||||
new PreviousRenderTransform(transform),
|
||||
new RenderMeshSet(RenderAssetHandle.FromRaw(id), 1, 1),
|
||||
new RenderMaterialVariant(0, 0, 1),
|
||||
new RenderSpatialResidency(
|
||||
RenderSpatialBucket.FromRaw(fullCellId),
|
||||
Landblock,
|
||||
fullCellId),
|
||||
new RenderWorldBounds(
|
||||
new Vector3(-1),
|
||||
new Vector3(1)),
|
||||
flags,
|
||||
default,
|
||||
new RenderSortKey(id),
|
||||
RenderDirtyMask.All,
|
||||
new RenderSourceMetadata(
|
||||
LocalEntityId: (uint)id,
|
||||
ServerGuid: projectionClass
|
||||
is RenderProjectionClass.LiveDynamicRoot ? (uint)id : 0,
|
||||
SourceId: 1,
|
||||
ParentCellId: parentCell ?? 0,
|
||||
EffectCellId: 0,
|
||||
BuildingShellAnchorCellId: 0,
|
||||
TransformFingerprint: default,
|
||||
GeometryFingerprint: default,
|
||||
AppearanceFingerprint: default));
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(
|
||||
uint id,
|
||||
uint serverGuid,
|
||||
uint? parentCell) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x0200_0001,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs =
|
||||
[
|
||||
new MeshRef(0x0100_0001, Matrix4x4.Identity),
|
||||
],
|
||||
ParentCellId = parentCell,
|
||||
};
|
||||
|
||||
private static PortalVisibilityFrame Portal(uint cellId)
|
||||
{
|
||||
var frame = new PortalVisibilityFrame();
|
||||
frame.OrderedVisibleCells.Add(cellId);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static ClipFrameAssembly FullScreenClip(uint cellId)
|
||||
{
|
||||
var slice = new ClipViewSlice(
|
||||
Slot: 0,
|
||||
NdcAabb: new Vector4(-1, -1, 1, 1),
|
||||
Planes: []);
|
||||
var assembly = new ClipFrameAssembly();
|
||||
assembly.SetOutsideViewSlices([slice]);
|
||||
assembly.CellIdToViewSlices[cellId] = [slice];
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static (
|
||||
uint LandblockId,
|
||||
Vector3 AabbMin,
|
||||
Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)
|
||||
Entry(IReadOnlyList<WorldEntity> entities) =>
|
||||
(
|
||||
Landblock,
|
||||
Vector3.Zero,
|
||||
Vector3.One,
|
||||
entities,
|
||||
null);
|
||||
|
||||
private sealed class EmptyCellSource : IRetailPViewCellSource
|
||||
{
|
||||
public static EmptyCellSource Instance { get; } = new();
|
||||
|
||||
private EmptyCellSource()
|
||||
{
|
||||
}
|
||||
|
||||
public LoadedCell? Find(uint cellId) => null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue