The retail frame walk now drives every production static draw. In RetailPViewRenderer.DrawInside, when the concrete executor + the packed product + the FW3.1 walk registries are all wired (all production compositions - anything less throws): - A pre-walk events-only collection pass (the shadow sink generalized to WalkVisitedSetCollector) gathers the frame's visited cells, buildings, and landscape-cell turns; the visited cells union into prepareCells so EnvCellRenderer prepares every shell the driver draws. - DrawWalkDrivenStatics runs the WalkFrameDriver over the production world data (WalkProductionWorldData over RenderSceneQuery + the building registry): sky, terrain slices, outdoor statics at their landscape-cell turns, buildings (alpha barrier -> punch/look-in passes -> shell) in retail order, interior clear+seals as leaf closures (the old tail block's drain reasoning moves with them), flood cells shell-then-contents. Landscape/cell-stage particle owners re-source from the walk's visited sets - retail gates particles per cell turn (ShouldDrawParticles @0x0050FE60), which this is; the old sphere filter was the approximation. - DrawLandscapeDynamicsPhase + DrawBuildingLookInDynamics carry the dynamics-only remainder (LookInObject now dynamic-classified, late outside-dynamics + weather, particle unions); DrawDynamicsLast and the outdoor flush are unchanged. - The product builder stops emitting LandscapeOutdoorStatic / LandscapeBuildingShell / CellStatic (methods deleted, dead index tracking removed); LookInObject loads cells with includeStatics: false. The old static path survives ONLY behind !walkActive for the standalone/diagnostic executor-fake path that keeps 15 retail-ordering regression tests exercising the barrier/punch/seal machinery; no production composition can reach it. Its deletion is FW4 scope (the plan's "deleting the patch apparatus") - recorded in the plan. Transitional risks recorded in code/report: the two-pass walk cost (FW3.4 measures), the interior slice-count reconciliation between the old clip assembly and the walk's own exit-view survival, and the outdoor merged-flood punch coverage now riding the walk's own building-BSP punches (retail-faithful per FW1; the owner visual gate verifies). Suites (lead-verified): full Release build 0 warnings; hermetic 6,750/0 (baseline minus the three deleted route tests); Walk lane 201/1; InstalledDat Walk conformance 40/1 untouched. The two IL-branch tests the implementation round reported failing pass in every lead run - the recurring parallel-load flake pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
518 lines
18 KiB
C#
518 lines
18 KiB
C#
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.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);
|
|
|
|
// 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,
|
|
[],
|
|
[Cell],
|
|
EmptyCellSource.Instance,
|
|
[],
|
|
RootIsOutdoor: true);
|
|
builder.Build(exchange, frameSequence, in input);
|
|
}
|
|
}
|
|
|
|
[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;
|
|
}
|
|
}
|