refactor(render): delete the packed pview product
This commit is contained in:
parent
78547718c7
commit
8d8aa715ee
5 changed files with 101 additions and 2335 deletions
|
|
@ -354,7 +354,6 @@ internal sealed class FrameRootCompositionPhase
|
|||
// contract cleanup later in this campaign.
|
||||
CurrentRenderSceneOracle? currentRenderSceneOracle = null;
|
||||
RenderSceneShadowComparisonController? renderSceneShadowComparison = null;
|
||||
RenderScenePViewFrameProductController? renderFrameProduct = null;
|
||||
{
|
||||
// Campaign V slice V6j: the world scene is composed on BOTH arms.
|
||||
// Every renderer below now exists on Vulkan too; what forks is one
|
||||
|
|
@ -607,8 +606,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"Lifecycle automation requires the canonical physics cache."),
|
||||
currentRenderSceneOracle,
|
||||
renderSceneShadowComparison,
|
||||
renderFrameProduct);
|
||||
renderSceneShadowComparison);
|
||||
lifecycleAutomation =
|
||||
new WorldLifecycleAutomationController(
|
||||
() => session.WorldReveal.Snapshot,
|
||||
|
|
|
|||
|
|
@ -47,8 +47,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_renderSceneOracle;
|
||||
private readonly IRenderSceneShadowSnapshotSource?
|
||||
_renderSceneShadow;
|
||||
private readonly IRenderFrameProductSnapshotSource?
|
||||
_renderFrameProduct;
|
||||
|
||||
public WorldLifecycleResourceSnapshotSource(
|
||||
GpuWorldState world,
|
||||
|
|
@ -69,8 +67,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
ResidencyManager residency,
|
||||
PhysicsDataCache physics,
|
||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
||||
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations
|
||||
|
|
@ -99,7 +96,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_renderSceneOracle = renderSceneOracle;
|
||||
_renderSceneShadow = renderSceneShadow;
|
||||
_renderFrameProduct = renderFrameProduct;
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
|
|
@ -151,9 +147,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
RenderSceneShadow:
|
||||
_renderSceneShadow?.CaptureCheckpointSnapshot()
|
||||
?? RenderSceneShadowComparisonSnapshot.Disabled,
|
||||
RenderFrameProduct:
|
||||
_renderFrameProduct?.Snapshot
|
||||
?? RenderFrameProductComparisonSnapshot.Disabled,
|
||||
RenderFrameProduct: RenderFrameProductComparisonSnapshot.Disabled,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// Retained lifecycle-automation schema for releases that still deserialize
|
||||
/// the former packed-frame comparison block. Campaign FW4 removed that
|
||||
/// producer; new snapshots always publish <see cref="Disabled"/>.
|
||||
/// </summary>
|
||||
internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||
bool Enabled,
|
||||
ulong ProductFrameSequence,
|
||||
ulong CurrentPViewFrameSequence,
|
||||
ulong ComparisonCount,
|
||||
ulong SuccessfulComparisonCount,
|
||||
int ExpectedCandidateCount,
|
||||
int ActualCandidateCount,
|
||||
long MismatchCount,
|
||||
string? FirstMismatch,
|
||||
RenderSceneHash128 ExpectedDigest,
|
||||
RenderSceneHash128 ActualDigest,
|
||||
ulong PackedInputComparisonCount,
|
||||
ulong PackedInputSuccessfulComparisonCount,
|
||||
int PackedInputExpectedCount,
|
||||
int PackedInputActualCount,
|
||||
long PackedInputMismatchCount,
|
||||
string? PackedInputFirstMismatch,
|
||||
RenderSceneHash128 PackedInputExpectedDigest,
|
||||
RenderSceneHash128 PackedInputActualDigest,
|
||||
ulong ClassifiedOutputComparisonCount,
|
||||
ulong ClassifiedOutputSuccessfulComparisonCount,
|
||||
int ClassifiedOutputExpectedDrawCount,
|
||||
int ClassifiedOutputActualDrawCount,
|
||||
long ClassifiedOutputMismatchCount,
|
||||
string? ClassifiedOutputFirstMismatch,
|
||||
RenderSceneHash128 ClassifiedOutputExpectedDigest,
|
||||
RenderSceneHash128 ClassifiedOutputActualDigest,
|
||||
ulong SelectionOutputComparisonCount,
|
||||
ulong SelectionOutputSuccessfulComparisonCount,
|
||||
int SelectionOutputExpectedPartCount,
|
||||
int SelectionOutputActualPartCount,
|
||||
long SelectionOutputMismatchCount,
|
||||
string? SelectionOutputFirstMismatch,
|
||||
RenderSceneHash128 SelectionOutputExpectedDigest,
|
||||
RenderSceneHash128 SelectionOutputActualDigest,
|
||||
int PackedClassificationProjectionCount,
|
||||
int PackedClassificationStaticRebuildCount,
|
||||
int PackedClassificationSameFrameReuseCount,
|
||||
int PackedClassificationCrossFrameReuseCount,
|
||||
int PackedClassificationAnimatedCount,
|
||||
int PackedClassificationRetiredCount,
|
||||
long PackedClassificationRetainedPayloadBytes,
|
||||
RenderFrameDiagnosticCounts ProductCounts)
|
||||
{
|
||||
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
||||
new(
|
||||
Enabled: false,
|
||||
ProductFrameSequence: 0,
|
||||
CurrentPViewFrameSequence: 0,
|
||||
ComparisonCount: 0,
|
||||
SuccessfulComparisonCount: 0,
|
||||
ExpectedCandidateCount: 0,
|
||||
ActualCandidateCount: 0,
|
||||
MismatchCount: 0,
|
||||
FirstMismatch: null,
|
||||
ExpectedDigest: RenderSceneHash128.Empty,
|
||||
ActualDigest: RenderSceneHash128.Empty,
|
||||
PackedInputComparisonCount: 0,
|
||||
PackedInputSuccessfulComparisonCount: 0,
|
||||
PackedInputExpectedCount: 0,
|
||||
PackedInputActualCount: 0,
|
||||
PackedInputMismatchCount: 0,
|
||||
PackedInputFirstMismatch: null,
|
||||
PackedInputExpectedDigest: RenderSceneHash128.Empty,
|
||||
PackedInputActualDigest: RenderSceneHash128.Empty,
|
||||
ClassifiedOutputComparisonCount: 0,
|
||||
ClassifiedOutputSuccessfulComparisonCount: 0,
|
||||
ClassifiedOutputExpectedDrawCount: 0,
|
||||
ClassifiedOutputActualDrawCount: 0,
|
||||
ClassifiedOutputMismatchCount: 0,
|
||||
ClassifiedOutputFirstMismatch: null,
|
||||
ClassifiedOutputExpectedDigest: RenderSceneHash128.Empty,
|
||||
ClassifiedOutputActualDigest: RenderSceneHash128.Empty,
|
||||
SelectionOutputComparisonCount: 0,
|
||||
SelectionOutputSuccessfulComparisonCount: 0,
|
||||
SelectionOutputExpectedPartCount: 0,
|
||||
SelectionOutputActualPartCount: 0,
|
||||
SelectionOutputMismatchCount: 0,
|
||||
SelectionOutputFirstMismatch: null,
|
||||
SelectionOutputExpectedDigest: RenderSceneHash128.Empty,
|
||||
SelectionOutputActualDigest: RenderSceneHash128.Empty,
|
||||
PackedClassificationProjectionCount: 0,
|
||||
PackedClassificationStaticRebuildCount: 0,
|
||||
PackedClassificationSameFrameReuseCount: 0,
|
||||
PackedClassificationCrossFrameReuseCount: 0,
|
||||
PackedClassificationAnimatedCount: 0,
|
||||
PackedClassificationRetiredCount: 0,
|
||||
PackedClassificationRetainedPayloadBytes: 0,
|
||||
ProductCounts: default);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,679 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RenderScenePViewFrameProductTests
|
||||
{
|
||||
private sealed class RejectAllLookInViews(uint cellId) : IWalkLookInViewSource
|
||||
{
|
||||
public IReadOnlyList<uint> LookInCellTurns { get; } = [cellId];
|
||||
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius) => false;
|
||||
}
|
||||
|
||||
private const uint Landblock = 0xA9B4FFFF;
|
||||
private const uint Cell = 0xA9B40170;
|
||||
private static readonly RenderSceneGeneration Generation =
|
||||
RenderSceneGeneration.FromRaw(1);
|
||||
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners,
|
||||
// Builder_OrdersMultiSliceBuildingShellsAfterLookIns, and
|
||||
// Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes are
|
||||
// DELETED — all three asserted route-range interleaving for
|
||||
// LandscapeOutdoorStatic/LandscapeBuildingShell/CellStatic, which
|
||||
// RenderScenePViewFrameBuilder no longer emits at all (WalkFrameDriver
|
||||
// draws every outdoor static, building shell, and cell static — look-in
|
||||
// included — directly through OrderedDrawStream; plan §FW3
|
||||
// "FW3.2b-2 — the production rooting", item 3). There is no successor
|
||||
// assertion to write: the routes themselves are retired, not relocated.
|
||||
|
||||
[Fact]
|
||||
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
|
||||
{
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): re-expressed against
|
||||
// the DYNAMIC index (LiveDynamicRoot / DynamicCandidates) instead of
|
||||
// the now-dead OutdoorStatic index — LoadSceneIndices' dirty-record
|
||||
// refresh + sort-key reorder mechanism this test proves is SHARED
|
||||
// between the two indices; the OutdoorStatic side was deleted along
|
||||
// with BuildOutdoorRoutes (its only reader).
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord later = Record(
|
||||
0x0100_0000_0000_0012,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
RenderProjectionRecord earlier = Record(
|
||||
0x0100_0000_0000_0011,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, later),
|
||||
RenderProjectionDelta.Register(Generation, 2, earlier),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
|
||||
Build(frameSequence: 1);
|
||||
RenderFrameView first = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[earlier.Id, later.Id],
|
||||
first.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in first);
|
||||
}
|
||||
|
||||
scene.ClearDirty();
|
||||
ulong retainedRevision = scene.OpenQuery().IndexRevision;
|
||||
Matrix4x4 movedTransform =
|
||||
Matrix4x4.CreateTranslation(0.25f, 0, 0);
|
||||
RenderProjectionRecord moved = later with
|
||||
{
|
||||
Transform = new RenderTransform(movedTransform),
|
||||
PreviousTransform =
|
||||
new PreviousRenderTransform(
|
||||
later.Transform.LocalToWorld),
|
||||
Bounds = new RenderWorldBounds(
|
||||
new Vector3(-0.75f, -1, -1),
|
||||
new Vector3(1.25f, 1, 1)),
|
||||
};
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
Generation,
|
||||
3,
|
||||
moved),
|
||||
]);
|
||||
Assert.Equal(
|
||||
retainedRevision,
|
||||
scene.OpenQuery().IndexRevision);
|
||||
|
||||
Build(frameSequence: 2);
|
||||
RenderFrameView second = exchange.BorrowLatest(Generation, 2);
|
||||
try
|
||||
{
|
||||
RenderProjectionRecord projected =
|
||||
second.EntityCandidates.ToArray()
|
||||
.Single(candidate =>
|
||||
candidate.Projection.Id == later.Id)
|
||||
.Projection;
|
||||
Assert.Equal(
|
||||
movedTransform,
|
||||
projected.Transform.LocalToWorld);
|
||||
Assert.Equal(
|
||||
[earlier.Id, later.Id],
|
||||
second.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in second);
|
||||
}
|
||||
|
||||
scene.ClearDirty();
|
||||
RenderProjectionRecord reordered = moved with
|
||||
{
|
||||
SortKey = new RenderSortKey(0),
|
||||
};
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
Generation,
|
||||
4,
|
||||
reordered),
|
||||
]);
|
||||
Assert.True(
|
||||
scene.OpenQuery().IndexRevision > retainedRevision);
|
||||
|
||||
Build(frameSequence: 3);
|
||||
RenderFrameView third = exchange.BorrowLatest(Generation, 3);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[later.Id, earlier.Id],
|
||||
third.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in third);
|
||||
}
|
||||
|
||||
void Build(ulong frameSequence)
|
||||
{
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
query,
|
||||
new RenderSceneDigest(
|
||||
query.Generation,
|
||||
query.Counts,
|
||||
default),
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
null,
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
RootIsOutdoor: true,
|
||||
RootFloodCells: new HashSet<uint> { Cell });
|
||||
builder.Build(exchange, frameSequence, in input);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_KeysLookInDynamicsToWalkTurnsAndExcludesThemFromDynamicLast()
|
||||
{
|
||||
const uint lookInCell = 0xA9B4_0171;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord rootDynamic = Record(
|
||||
0x0100_0000_0000_0021,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord lookInDynamic = Record(
|
||||
0x0100_0000_0000_0022,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: lookInCell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0100_0000_0000_0023,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 3, outdoorDynamic),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
query,
|
||||
new RenderSceneDigest(query.Generation, query.Counts, default),
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
LookInCellTurns: [lookInCell],
|
||||
WalkLookInViews: null,
|
||||
DrawableCells: [Cell, lookInCell],
|
||||
Cells: EmptyCellSource.Instance,
|
||||
AnimatedEntityIds: [],
|
||||
RootIsOutdoor: false,
|
||||
RootFloodCells: new HashSet<uint> { Cell });
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.DoesNotContain(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route == RenderFrameCandidateRoute.LookInObject);
|
||||
|
||||
RenderFrameCandidateRange outside = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeOutsideDynamic);
|
||||
RenderProjectionId[] outsideIds = frame.RouteCandidates
|
||||
.Slice(outside.Offset, outside.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id)
|
||||
.ToArray();
|
||||
Assert.Contains(outdoorDynamic.Id, outsideIds);
|
||||
Assert.DoesNotContain(lookInDynamic.Id, outsideIds);
|
||||
|
||||
RenderFrameCandidateRange dynamicLast = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
|
||||
RenderProjectionId[] lastIds = frame.RouteCandidates
|
||||
.Slice(dynamicLast.Offset, dynamicLast.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id)
|
||||
.ToArray();
|
||||
Assert.Contains(rootDynamic.Id, lastIds);
|
||||
Assert.DoesNotContain(lookInDynamic.Id, lastIds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in frame);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_CarriesExactWalkConeForPerPartDispatcherAdmission()
|
||||
{
|
||||
const uint lookInCell = 0xA9B4_0171;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord lookInDynamic = Record(
|
||||
0x0100_0000_0000_0022,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: lookInCell);
|
||||
RenderProjectionRecord rootDynamic = Record(
|
||||
0x0100_0000_0000_0023,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0100_0000_0000_0024,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, lookInDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 2, rootDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 3, outdoorDynamic),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
query,
|
||||
new RenderSceneDigest(query.Generation, query.Counts, default),
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
LookInCellTurns: [lookInCell],
|
||||
WalkLookInViews: new RejectAllLookInViews(lookInCell),
|
||||
DrawableCells: [Cell, lookInCell],
|
||||
Cells: EmptyCellSource.Instance,
|
||||
AnimatedEntityIds: [],
|
||||
RootIsOutdoor: false,
|
||||
RootFloodCells: new HashSet<uint> { Cell });
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.DoesNotContain(
|
||||
frame.RouteRanges.ToArray(),
|
||||
candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject);
|
||||
Assert.DoesNotContain(
|
||||
frame.RouteCandidates.ToArray(),
|
||||
candidate => candidate.Id == rootDynamic.Id);
|
||||
Assert.DoesNotContain(
|
||||
frame.RouteCandidates.ToArray(),
|
||||
candidate => candidate.Id == outdoorDynamic.Id);
|
||||
Assert.NotNull(frame.WalkLookInViews);
|
||||
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
|
||||
routeIndex: 0,
|
||||
center: Vector3.Zero,
|
||||
radius: 1f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in frame);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
||||
{
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): no static entities here
|
||||
// — LandscapeOutdoorStatic/CellStatic no longer flow through the
|
||||
// packed product at all (WalkFrameDriver owns every static now), so
|
||||
// a static observed on the LEGACY oracle side with nothing on the
|
||||
// packed side would report a permanent mismatch unrelated to what
|
||||
// this test is proving (withdrawal detection on the SURVIVING
|
||||
// DynamicLast route).
|
||||
WorldEntity outdoorDynamic =
|
||||
Entity(1_000_012, 0x8000_0012, parentCell: null);
|
||||
WorldEntity cellDynamic =
|
||||
Entity(1_000_013, 0x8000_0013, parentCell: Cell);
|
||||
WorldEntity[] entities = [outdoorDynamic, cellDynamic];
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var partition = new InteriorEntityPartition.Result();
|
||||
InteriorEntityPartition.Partition(
|
||||
partition,
|
||||
[Cell],
|
||||
[Entry(entities)],
|
||||
oracle);
|
||||
oracle.BeginPViewFrame();
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
0,
|
||||
0,
|
||||
[outdoorDynamic, cellDynamic]);
|
||||
oracle.CompletePViewFrame();
|
||||
oracle.BeginDispatcherFrame();
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 2,
|
||||
[
|
||||
(outdoorDynamic, 0, Landblock),
|
||||
(cellDynamic, 0, Landblock),
|
||||
]);
|
||||
|
||||
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,
|
||||
[],
|
||||
Landblock,
|
||||
rootIsOutdoor: true);
|
||||
|
||||
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
|
||||
Assert.True(matched.Enabled);
|
||||
Assert.Equal(1uL, matched.ComparisonCount);
|
||||
Assert.Equal(1uL, matched.SuccessfulComparisonCount);
|
||||
Assert.Equal(2, matched.ExpectedCandidateCount);
|
||||
Assert.Equal(2, matched.ActualCandidateCount);
|
||||
Assert.Equal(0, matched.MismatchCount);
|
||||
Assert.Null(matched.FirstMismatch);
|
||||
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
|
||||
Assert.Equal(1uL, matched.PackedInputComparisonCount);
|
||||
Assert.Equal(1uL, matched.PackedInputSuccessfulComparisonCount);
|
||||
Assert.Equal(2, matched.PackedInputExpectedCount);
|
||||
Assert.Equal(2, matched.PackedInputActualCount);
|
||||
Assert.Equal(0, matched.PackedInputMismatchCount);
|
||||
Assert.Null(matched.PackedInputFirstMismatch);
|
||||
Assert.Equal(
|
||||
matched.PackedInputExpectedDigest,
|
||||
matched.PackedInputActualDigest);
|
||||
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,
|
||||
[],
|
||||
Landblock,
|
||||
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=2 actual=1", mismatch.FirstMismatch);
|
||||
Assert.Equal(1, mismatch.PackedInputMismatchCount);
|
||||
Assert.Contains(
|
||||
"field=count expected=2 actual=1",
|
||||
mismatch.PackedInputFirstMismatch);
|
||||
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;
|
||||
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,
|
||||
bool isBuildingShell = false,
|
||||
uint buildingShellAnchorCellId = 0)
|
||||
{
|
||||
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: buildingShellAnchorCellId,
|
||||
TransformFingerprint: default,
|
||||
GeometryFingerprint: default,
|
||||
AppearanceFingerprint: default),
|
||||
new RenderEntityPayload(
|
||||
[new MeshRef((uint)id, Matrix4x4.Identity)],
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: isBuildingShell));
|
||||
}
|
||||
|
||||
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 ClipFrameAssembly TwoSliceClip(uint cellId)
|
||||
{
|
||||
var first = new ClipViewSlice(
|
||||
Slot: 0,
|
||||
NdcAabb: new Vector4(-1, -1, 0, 1),
|
||||
Planes: []);
|
||||
var second = new ClipViewSlice(
|
||||
Slot: 1,
|
||||
NdcAabb: new Vector4(0, -1, 1, 1),
|
||||
Planes: []);
|
||||
var assembly = new ClipFrameAssembly();
|
||||
assembly.SetOutsideViewSlices([first, second]);
|
||||
assembly.CellIdToViewSlices[cellId] = [first];
|
||||
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;
|
||||
}
|
||||
|
||||
private sealed class DictionaryCellSource : IRetailPViewCellSource
|
||||
{
|
||||
private readonly Dictionary<uint, LoadedCell> _cells;
|
||||
|
||||
public DictionaryCellSource(params LoadedCell[] cells) =>
|
||||
_cells = cells.ToDictionary(static cell => cell.CellId);
|
||||
|
||||
public LoadedCell? Find(uint cellId) =>
|
||||
_cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue