acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs
Erik 651badc2b9 fix(render): S3 chunk 3 round 1 - slot key, deferred cross-block terrain batches, per-frame terrain diagnostic
Fixes the three-lens review blockers against 671eb3ad4 (S3 section 9.6 F1-F6).

F1 - slot key (blocking, retail). TerrainModernRenderer.DrawLandCells
normalizes every incoming landblockId to (id & 0xFFFF0000u) | 0xFFFFu
before the _idToSlot lookup: the walk hands 0xXXYY0000
(WalkLandBlock.LandblockId) but AddLandblock stores under the DAT id
0xXXYYFFFF (LandblockRenderPublisher.LandblockId) - every walk lookup
was missing and the walk path drew NO terrain. Unit-tested end-to-end
through a real RecordingGpuDevice-backed TerrainModernRenderer
(TerrainWalkSlotKeyNormalizationTests): AddLandblock(0xA9B4FFFF, ...)
is found by a 0xA9B40000 lookup, an unknown landblock is a silent
per-entry no-op, and a batch mixing a known and unknown entry submits
only the known one.

F2 - deferred cross-block batching (blocking, driver). Retail's
DrawSortCell always follows DrawLandCell (LC/SC strictly alternate,
never two LC in a row - S3 section 9 R1), so chunk 3's "merge
consecutive same-landblock LandCell events" rule never actually
merged anything; the driver review flagged batching as inert.
WalkFrameDriver.Replay now keeps ONE pending terrain batch across
landblocks ((landblockId, side, cellIndex) entries, cleared at
Replay's own start); a LandCell event only appends; every OTHER event
kind that will itself submit GPU work (StreamMark, Sky, CellShell,
PunchFan, AlphaBarrier, LandscapeFlush, ClearInteriorDepth,
ExitSeals) flushes the pending batch first; a StaticParticles/
CellParticles turn asks the new ParticleSystem.
HasRenderableEmittersInCell (an allocation-free sibling of
CopyRenderableEmittersInCell) and, when the cell has no renderable
emitter, submits nothing and does NOT flush either - the whole point
of the deferred rule. The end of Replay flushes the remainder. This
is order-preserving by construction: a flush always lands at the
exact point the unbatched draw would have, so GPU submission order -
and therefore pixels - is identical to the unbatched baseline; only
the number of small terrain draw calls shrinks.
TerrainModernRenderer.DrawLandCellRuns becomes DrawLandCells(
viewProjection, IReadOnlyList<(uint LandblockId, int SideCellCount,
int CellIndex)>) - one MultiDrawIndexedIndirect over every entry's
runs, unknown slots skipped per-entry. IWalkFrameLeafRenderer.
DrawLandCellBatch drops its separate landblockId parameter to match
(a batch can span several landblocks now) and gains
HasRenderableEmittersInCell.

Batch-count demonstration: driven through a real WalkFrameDriver
Replay (OnLandCellTurn_MergesAcrossLandblocksOverAnEmptyParticleTurn_
RealSubmissionsSplit), 4 LandCell turns across 3 distinct landblocks,
separated only by an empty particle turn, a real StreamMark, and a
building's alpha barrier, submit as exactly 3 DrawLandCellBatch calls
(2+1+1) instead of 4 - the empty particle turn's non-flush merges two
otherwise-separate cross-landblock entries. At production scale the
same mechanism is expected to cut the terrace-edge frame's ~578
individual DrawLandCell events (S3 section 9's captured transcript
count) to "tens" of submitted batches, per the contract's own
expectation: most terrain cells have no particle owner nearby, so the
strict LC/[empty-SC]/LC/[empty-SC]/... run collapses into one batch
per region bounded by real content (a building, a StreamMark-worthy
cell, or a genuine emitter) rather than per cell.

F3 - per-frame terrain diagnostic (blocking, build/test). The walk
leaf no longer brackets each batch with TerrainDrawDiagnosticsController
.Begin()/Complete() (a per-batch Stopwatch Restart/Stop pair that was
pushing one timing SAMPLE per batch, not per frame).
RetailPViewPassExecutor.DrawWalkLandCellBatch instead times its own
call with a raw Stopwatch.GetTimestamp() delta (no allocation) and
hands the ticks to the controller's new AccumulateWalkBatch;
RetailPViewRenderer.DrawWalkDrivenStatics calls the new
CompleteWalkTerrainFrame() exactly once, immediately after
driver.Replay finishes - "the end of the walk replay", where the
deleted whole-stage terrain leaf's own Begin()/Complete() bracket
used to close - which pushes ONE elapsed-time sample (even a
zero-batch frame pushes a zero sample: one sample per frame, not per
landscape turn) and publishes on the existing 5-second cadence.
TerrainRenderDiagnosticFacts gains a Draws field alongside
VisibleSlots (both were the same field before); TerrainModernRenderer
tracks its own per-frame WalkVisibleSlotCount/WalkDrawCount (a
HashSet<int>/int cleared in BeginFrame, populated by DrawLandCells),
and the diagnostics source reports those whenever the walk drew at
least one batch this frame, falling back to the non-walk Draw()
path's VisibleSlots otherwise (the two paths never both run in the
same frame). The [TERRAIN-DIAG] line's meaning (cpu_us per frame) is
unchanged, so the S3 section 9.5 before/after compare stays valid.

F4 - driver pins for the LandCell position (major). Three RunFrame-
level pins replace the deleted TERRAIN:0 pins: an outdoor-root
sequence (SKY, then one LANDCELL, driving RetailFrameWalk.
DrawLandscape directly with a one-view/zero-vertex WalkPortalView so
WalkLandscape.CheckBlocks' admission stays the same deterministic
"CY-only" test RetailFrameWalkTests already relies on, while still
satisfying WalkFrameDriver's real >=1-active-view fail-loud guard);
an interior-root test with one real exit view and one populated
block (SKY, LANDCELL, LFLUSH, SEALS, SHELL...) built on the existing
RunFrame_InteriorFloodWithExitView_... fixture; and the T4 batching
pin re-expressed for the F2 rule (OnLandCellTurn_
MergesAcrossLandblocksOverAnEmptyParticleTurn_RealSubmissionsSplit,
described above). The fake leaf's DrawLandCellBatch now logs
LANDCELL:<lb>:<side>:<idx>[,...] per batch and gains
HasRenderableEmittersInCell backed by an opt-out CellsWithoutEmitters
set (default true - has-emitters - so every pre-existing pin in the
file keeps its old unconditional-submission behavior unchanged).

F5 - no code change: the walk's in-view gate is unchanged; no
whole-block terrain re-added.

F6 - minor/notes: DrawLandCells' own comment now states the walk's
CheckBlocks/landcell_check admission is the sole terrain culling
authority (retail has no separate terrain frustum test); the
HandleLandscapeTurn comment's inverted claim is corrected (a FARTHER
building's punch survived because NEARER terrain was drawn BEFORE
it, not after - the interleave now draws it after, matching retail);
the "flat/directional-shadow paths" claim is corrected to the one
actual caller, WorldScenePassExecutor.DrawFlatTerrain (a directional-
shadow receiver selects its pipeline inside the SAME DrawRhi call,
not through a second caller); the cathedral order-trace token gains
the LOD side/index (":LC<lb>/<side>:<idx>"); T2's vacuous "no
TERRAIN event" assertion in RetailFrameWalkTests is replaced by a
comment pointing at the F4 driver-level pins; and the stale
"Confirmed OH5 defect" row in oh1-construction-landscape-contract.md
is retired with "FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round
1)".

App hermetic lane: 6,795/6,795 (up from 671eb3ad4's 6,786 baseline -
net +9 tests: 3 F1 slot-key tests, 2 F4a/b driver RunFrame pins, 3
TerrainDrawDiagnosticsController walk-frame tests, plus the T4->F4c
rewrite and the RetailPViewPassExecutorTests split are net neutral).
InstalledDat lane: 241 passed, the same 3 accepted failures (2
pre-existing #383 layout fixture-drift tests, 1 TowerAscent
Status=KnownFailure) - unchanged from baseline. Core Vfx tests:
109/109 (108 baseline + 1 new HasRenderableEmittersInCell lifecycle
pin mirroring CopyRenderableEmittersInCell's own add/move/remove
test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 09:57:34 +02:00

1874 lines
80 KiB
C#

using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using AcDream.Core.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Lib.IO;
using Microsoft.Extensions.Logging.Abstractions;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// Campaign FW stage FW3.2b-1: <see cref="WalkFrameDriver"/>'s headless
/// referee suite — proves that driving <see cref="RetailFrameWalk"/> with the
/// driver as its <see cref="IWalkEventSink"/> produces retail's own turn
/// order (all shells then all contents in reverse flood order,
/// flush-before-every-leaf-action,
/// content-before-punch) through the REAL <see cref="WbDrawDispatcher.SubmitOrderedStream"/>
/// onto a <see cref="RecordingGpuDevice"/> — never a mock of the submission
/// path itself. No production wiring is exercised (<c>WorldSceneRenderer</c>
/// still does not construct this driver); every world-data/leaf-renderer
/// dependency here is a synthetic fake per plan §FW3.2b-1.
/// </summary>
public sealed class WalkFrameDriverTests
{
// ── Shared ordered log: BOTH the fake leaf renderer and the fake trace
// write into ONE list, so a single sequence assertion proves the FULL
// interleave (stream flushes interleaved with sky/terrain/shell/punch/
// alpha-barrier), not just each half in isolation. ─────────────────────
private sealed class RecordingLeafRenderer(
List<string> log,
RetailAlphaQueue? alpha = null) : IWalkFrameLeafRenderer
{
public readonly List<WalkPolygon> Punches = new();
public readonly List<uint> Shells = new();
public readonly List<int> AlphaPendingAtBarrier = new();
public readonly List<(uint LandblockId, int SideCellCount, int CellIndex)[]> LandCellBatches = new();
/// <summary>S3 chunk 3 fix round 1 (F2/F4c): cell ids
/// <see cref="HasRenderableEmittersInCell"/> reports as having NO
/// renderable emitter. Empty by default so EVERY pre-existing pin in
/// this file keeps its old "every StaticParticles/CellParticles turn
/// submits unconditionally" behavior unchanged; a test proving the
/// new "genuinely empty turn submits nothing and does not flush"
/// rule opts specific cells OUT via this set.</summary>
public readonly HashSet<uint> CellsWithoutEmitters = new();
/// <summary>S3 chunk 2: the exit-seal polygon count this fake
/// reports back to the driver (B2 — <see cref="DrawExitSeals"/>
/// returns the submitted count so the driver can re-arm its
/// persistent <c>PortalsDrawnCount</c> for the NEXT <c>ov&gt;0</c>
/// frame's clear decision, R4). Defaults to 1 so a test that
/// doesn't care about the re-arm mechanism sees an ordinary
/// "some seal fired" outcome; a test proving "flood with no exit
/// portal never clears" sets this to 0.</summary>
public int SealPolygonsSubmitted = 1;
public void DrawSky() => log.Add("SKY");
public void DrawLandCellBatch(
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells)
{
LandCellBatches.Add(cells.ToArray());
log.Add("LANDCELL:" + string.Join(
',', cells.Select(c => $"{c.LandblockId:x8}:{c.SideCellCount}:{c.CellIndex}")));
}
public bool HasRenderableEmittersInCell(uint cellId) => !CellsWithoutEmitters.Contains(cellId);
public void DrawCellShell(uint cellId)
{
Shells.Add(cellId);
log.Add($"SHELL:{cellId:x8}");
}
public void FlushLandscape() => log.Add("LFLUSH");
public void ClearInteriorDepth() => log.Add("CLEAR");
public int DrawExitSeals()
{
log.Add("SEALS");
return SealPolygonsSubmitted;
}
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex)
{
Punches.Add(worldPolygon);
log.Add($"PUNCH:{worldPolygon.Vertices.Length}@v{activeViewIndex}");
}
public void AlphaBarrier()
{
if (alpha is null)
{
log.Add("ALPHA");
return;
}
AlphaPendingAtBarrier.Add(alpha.PendingCount);
log.Add($"ALPHA:{alpha.PendingCount}");
alpha.Flush();
}
public void DrawStaticParticles(uint cellId) => log.Add($"PARTICLES:{cellId:x8}");
public void DrawCellParticles(uint cellId) => log.Add($"CELL-PARTICLES:{cellId:x8}");
}
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
{
public void OnFlush(int commandCount, IReadOnlyList<WalkDrawStage> stages) =>
log.Add($"FLUSH:{commandCount}:{string.Join(',', stages.Distinct())}");
}
private sealed class FakeWorldData : IWalkFrameWorldData
{
public readonly Dictionary<uint, WalkFrameStaticRecords> CellStaticsByCell = new();
public readonly Dictionary<uint, WalkFrameStaticRecords> CellDynamicsByCell = new();
public readonly Dictionary<uint, WalkFrameStaticRecords> OutdoorStaticsByCell = new();
public readonly Dictionary<uint, WalkFrameStaticRecords> OutdoorDynamicsByCell = new();
public readonly Dictionary<WalkBuilding, WalkFrameStaticRecords> ShellByBuilding = new();
public readonly Dictionary<WalkBuilding, Matrix4x4> WorldTransformByBuilding = new();
public WalkFrameStaticRecords GetCellStatics(uint cellId) =>
CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetCellDynamics(uint cellId) =>
CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) =>
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) =>
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) =>
ShellByBuilding.GetValueOrDefault(building, WalkFrameStaticRecords.Empty);
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) =>
WorldTransformByBuilding.GetValueOrDefault(building, Matrix4x4.Identity);
}
// ── The walk-level test context (interior flood + building portal pass) ─
private sealed class Caster : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY) => new(screenX, screenY, 100f);
}
private sealed class TestContext : IWalkFrameContext, IRetailFrameWalkContext
{
public readonly Dictionary<uint, WalkCell> Cells = new();
public readonly Dictionary<WalkBuilding, float> ViewerDistances = new();
private readonly Matrix4x4 _viewProj;
private static readonly Vector2[] RootQuad =
[
new(0, 480), new(640, 480), new(640, 0), new(0, 0),
];
public TestContext()
{
Matrix4x4 view = Matrix4x4.CreateLookAt(
Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
Matrix4x4 proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
_viewProj = view * proj;
}
public Vector3 ViewpointIn(WalkCell cell) => Vector3.Zero;
public Matrix4x4 ObjectToClip(WalkCell cell) => _viewProj;
public WalkCell? GetVisible(uint cellId) => Cells.GetValueOrDefault(cellId);
public IWalkRayCaster Rays { get; } = new Caster();
public Vector3 WorldViewpoint => Vector3.Zero;
public float ViewportWidth => 640f;
public float ViewportHeight => 480f;
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
public float ViewerDistanceTo(WalkBuilding building) =>
ViewerDistances.GetValueOrDefault(building, 0f);
public IWalkFrameContext CellContext => this;
public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f);
public void SetActiveView(WalkPortalView views, int index) { }
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
{
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
for (int i = 0; i < polygon.Vertices.Length; i++)
projected[i] = WalkScreenClip.TransformToScreen(
polygon.Vertices[i], _viewProj, ViewportWidth, ViewportHeight);
if (side != 0)
projected.Reverse();
return WalkScreenClip.ClipAgainstView(projected, RootQuad, output);
}
}
private static WalkPolygon Quad(float z, bool facingViewer = true) => new()
{
Vertices =
[
new Vector3(-0.5f, -0.5f, z), new Vector3(0.5f, -0.5f, z),
new Vector3(0.5f, 0.5f, z), new Vector3(-0.5f, 0.5f, z),
],
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
};
// ── Deliverable: RunFrame drives an interior two-cell flood using
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
// then ALL object cells far-to-near. ─────────────────────────────────
// ── Deliverable (S3 chunk 2, superseding the 2026-08-30 decomp
// correction): PView::DrawCells @0x005a4840's actual DRAW order for an
// interior root's OWN flood is NOT the order its DrawInside/DrawCells
// EVENTS fire in (breakpoint-entry order, matching the FW0 oracle
// traces) — retail draws LScape::draw FIRST (pc:432719, only when exit
// views survived), then the landscape flush + device-stamp advance,
// then a GATED depth clear (pc:432731-432732 — R4: read-then-zero
// portalsDrawnCount @0x005a489c-0x005a489e, clear iff nonzero), then
// the exit-portal seals (pc:432785-432786), and ONLY THEN the flood's
// own cells far-to-near. This case has a surviving exit view (ov=1): DC
// records the flood list (no draw), the landscape turn runs (sky,
// terrain), THEN flush, THEN — since this is a FRESH driver's very
// first ov&gt;0 flood, so portalsDrawnCount starts at 0 (R4's
// "first-frame no-clear quirk") — the clear is SKIPPED, then seals,
// then all shells and all contents in reverse order. ──────────────────
[Fact]
public void RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObjA = 0x0200_0001UL;
const ulong gfxObjB = 0x0200_0002UL;
InjectRenderData(fx.Manager, gfxObjA, MakeFlatMesh(
MakeBatch(0x08100001u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, gfxObjB, MakeFlatMesh(
MakeBatch(0x08100002u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
var ctx = new TestContext();
var cell1 = new WalkCell
{
CellId = 0x100,
StabList = [0x101u],
Portals =
[
new WalkCellPortal
{
OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
},
// The exit portal (retail's "world beyond the door") — this
// is what raises ov to 1 and makes DrawInside run the
// landscape turn before clear+seals+the flood cells.
new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 1, PortalSide = 0, OtherPortalId = -1,
},
],
PortalPolygons = [Quad(-2f), Quad(-3f)],
};
var cell2 = new WalkCell
{
CellId = 0x101,
Portals = [new WalkCellPortal
{
OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell1.CellId] = cell1;
ctx.Cells[cell2.CellId] = cell2;
var worldData = new FakeWorldData();
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
new[] { MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
new[] { MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)]) }, 0x8C04u);
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
using ClipFrame clipFrame = ClipFrame.NoClip();
var driver = new WalkFrameDriver(
fx.Dispatcher, leaf, worldData, trace, clipFrame);
var walk = new RetailFrameWalk();
// A minimal, no-op landscape (1x1 window, the one slot unpublished)
// — matches RetailFrameWalkTests' own exit-view fixture. LScape::draw
// still runs its full turn (sky/terrain) against it; there's simply
// nothing published to iterate blocks/cells/buildings for.
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
using DrawScope draw = fx.BeginDraw();
driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: landscape,
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
Assert.Equal(
new[]
{
// No CLEAR: R4's first-frame no-clear quirk — this driver's
// PortalsDrawnCount starts at 0, so the read-then-zero
// decision sees "not armed" even though ov=1.
"SKY", "LFLUSH", "SEALS",
"SHELL:00000101", "SHELL:00000100",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
},
log);
Assert.DoesNotContain("CLEAR", log);
// This frame's own seals (RecordingLeafRenderer's default
// SealPolygonsSubmitted) armed the counter for a NEXT ov>0 frame.
Assert.Equal(1, driver.PortalsDrawnCount);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(2, mdiCalls.Count);
Assert.All(mdiCalls, c => Assert.Equal(1u, c.DrawCount));
// Nothing dropped: every populated record reached exactly one indirect draw.
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
// Exit seals replay through the exact portal_view slices captured by
// this walk. The legacy PortalVisibilityFrame is not consulted.
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(1));
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(0, 0).Length);
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(1, 0).Length);
Assert.True(clipFrame.SlotCount >= 3);
}
[Fact]
public void InteriorFloodAfterLandscape_RearmsRetailPartStampForPostClearCellRepaint()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObj = 0x0200_0021UL;
const uint outdoorCellId = 0xF4180009u;
const uint interiorCellId = 0xF4180112u;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
RenderProjectionRecord sharedPart = MakeRecord(
0x4F41806Cu,
0,
Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[outdoorCellId] =
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
worldData.CellStaticsByCell[interiorCellId] =
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
var ctx = new TestContext();
var interiorCell = new WalkCell { CellId = interiorCellId };
interiorCell.PushView();
WalkCopyView.AppendFullViewportQuad(
interiorCell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[interiorCellId] = interiorCell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(log),
worldData,
new RecordingTrace(log));
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var landscapeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
landscapeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
sink.OnLandscapeViews(landscapeViews);
sink.OnLandscapeCellTurn(outdoorCellId);
// A landscape turn ran (activeViewCount: 1 above), so this models
// retail's ov>0 case.
sink.OnInteriorFloodDrawTurn([interiorCellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(2, mdiCalls.Count);
Assert.All(mdiCalls, call => Assert.Equal(1u, call.DrawCount));
Assert.Equal(2, mdiCalls.Sum(call => (int)call.DrawCount));
Assert.Equal(2, log.Count(entry => entry == "FLUSH:1:OutdoorStatic"
|| entry == "FLUSH:1:CellStatic"));
}
// ── Deliverable (T1, flipped for S3 chunk 2): the ov==0 interior case —
// no exit view survives, so DrawInside never runs the landscape turn,
// and PView::DrawCells' whole landscape-flush/stamp/clear/seal turn
// (S3 §8.1 R3: all four gated on outside_view.view_count > 0) never
// runs either — straight to the flood's own cells after the
// (draw-nothing) DC event. ──────────────────────────────────────────
[Fact]
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObjA = 0x0200_0003UL;
const ulong gfxObjB = 0x0200_0004UL;
InjectRenderData(fx.Manager, gfxObjA, MakeFlatMesh(
MakeBatch(0x08100003u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, gfxObjB, MakeFlatMesh(
MakeBatch(0x08100004u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
var ctx = new TestContext();
var cell1 = new WalkCell
{
CellId = 0x100,
StabList = [0x101u],
Portals = [new WalkCellPortal
{
OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
var cell2 = new WalkCell
{
CellId = 0x101,
Portals = [new WalkCellPortal
{
OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell1.CellId] = cell1;
ctx.Cells[cell2.CellId] = cell2;
var worldData = new FakeWorldData();
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
new[] { MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
new[] { MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)]) }, 0x8C04u);
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, trace);
var walk = new RetailFrameWalk();
using DrawScope draw = fx.BeginDraw();
driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: new WalkLandscape(),
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
// at all. T1 (flipped): LFLUSH/CLEAR/SEALS and the device-stamp
// advance are ALSO absent now — S3 §8.1 R3 gates all four on
// outside_view.view_count > 0, and it is 0 here.
Assert.Equal(
new[]
{
"SHELL:00000101", "SHELL:00000100",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
},
log);
Assert.Equal(0, driver.PortalsDrawnCount);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(2, mdiCalls.Count);
Assert.All(mdiCalls, c => Assert.Equal(1u, c.DrawCount));
// Nothing dropped: every populated record reached exactly one indirect draw.
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
}
// ── T2 (S3 chunk 2, R4): a fresh driver's first ov>0 interior flood
// skips the gated clear (PortalsDrawnCount starts at 0); that same
// frame's own exit seals arm the counter at Replay, so the driver's
// SECOND ov>0 flood — even with a different flood cell, since the
// counter is driver-scoped, not cell-scoped — sees it nonzero and
// clears. ────────────────────────────────────────────────────────────
[Fact]
public void OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint cellId = 0xF4180200u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
// R4: PortalsDrawnCount starts at 0 for a fresh driver — the
// 0x005a489c-0x005a489e read-then-zero decision sees "not armed" on
// this driver's very first ov>0 interior-root flood, so the gated
// depth clear (pc:432731-432732) is skipped even though a landscape
// turn just ran.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var views1 = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
views1, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(views1);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[]
{
"SKY", "LFLUSH", "SEALS",
"SHELL:f4180200", "CELL-PARTICLES:f4180200",
},
log);
Assert.DoesNotContain("CLEAR", log);
Assert.Equal(1, driver.PortalsDrawnCount);
log.Clear();
// Frame 1's exit seals reported SealPolygonsSubmitted (default 1) at
// Replay, arming PortalsDrawnCount for THIS frame's read-then-zero.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var views2 = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
views2, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(views2);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[]
{
"SKY", "LFLUSH", "CLEAR", "SEALS",
"SHELL:f4180200", "CELL-PARTICLES:f4180200",
},
log);
}
// ── T2 (S3 chunk 2, R4): a driver whose flood never reaches an exit
// portal (DrawExitSeals reports 0 submitted every turn) never clears,
// across any number of ov>0 frames — retail's counter is fed ONLY by
// actually-submitted seal fans. ─────────────────────────────────────
[Fact]
public void OnInteriorFloodDrawTurn_FloodWithNoExitPortal_NeverClearsAcrossFrames()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log) { SealPolygonsSubmitted = 0 };
var ctx = new TestContext();
const uint cellId = 0xF4180201u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
for (int frame = 0; frame < 3; frame++)
{
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var views = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
views, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(views);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
}
Assert.DoesNotContain("CLEAR", log);
Assert.Equal(3, log.Count(entry => entry == "SEALS"));
Assert.Equal(0, driver.PortalsDrawnCount);
}
// ── T3 (S3 chunk 2, R1): a building look-in's own DrawCells re-enters
// with ov==0 unconditionally and neither ARMS nor CONSUMES the
// persistent PortalsDrawnCount counter — retail calls DrawCells
// re-entrantly there with no clear/seal step at all. ────────────────
[Fact]
public void LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint rootCellId = 0xF4180301u;
const uint lookInCellId = 0xF4180302u;
var rootCell = new WalkCell { CellId = rootCellId };
rootCell.PushView();
WalkCopyView.AppendFullViewportQuad(
rootCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[rootCellId] = rootCell;
var lookInCell = new WalkCell { CellId = lookInCellId };
lookInCell.PushView();
WalkCopyView.AppendFullViewportQuad(
lookInCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[lookInCellId] = lookInCell;
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
// Arm PortalsDrawnCount with one throwaway ov>0 interior-root flood.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var rootViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
rootViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(rootViews);
sink.OnInteriorFloodDrawTurn([rootCellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(1, driver.PortalsDrawnCount);
log.Clear();
// A building look-in's own DrawCells re-enters with ov==0
// unconditionally (R1) — no LFLUSH/clear/seal step at all ("DrawCells
// re-entrantly there with no clear/seal step"), so it must leave the
// already-armed counter alone.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [lookInCellId]));
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.DoesNotContain("LFLUSH", log);
Assert.DoesNotContain("CLEAR", log);
Assert.DoesNotContain("SEALS", log);
Assert.Equal(1, driver.PortalsDrawnCount);
}
// ── Deliverable: a building turn's alpha barrier precedes its portal
// pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0:
// FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk]
// -> CPhysicsPart::Draw(parts,0) [the shell] @0x0059f30b-0x0059f345);
// the punch pass runs with nothing of THIS building's own queued yet
// (the shell is not appended until the whole portal pass completes);
// the look-in DC turn draws shell-then-contents exactly like an ordinary
// interior flood; the building's own shell content is appended and
// flushed only AFTER the portal pass, at frame end; and the punch
// polygon reaches the leaf renderer transformed building-local ->
// world. ─────────────────────────────────────────────────────────────
[Fact]
public void BeginEndFrame_BuildingTurnWithPunchAndLookIn_OrdersAlphaBarrierPortalPassThenShell()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong shellGfxObj = 0x0200_0010UL;
const ulong interiorGfxObj = 0x0200_0011UL;
const ulong dynamicGfxObj = 0x0200_0012UL;
InjectRenderData(fx.Manager, shellGfxObj, MakeFlatMesh(
MakeBatch(0x08100010u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, interiorGfxObj, MakeFlatMesh(
MakeBatch(0x08100011u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
InjectRenderData(fx.Manager, dynamicGfxObj, MakeFlatMesh(
MakeBatch(0x08100012u, TranslucencyKind.Opaque, 6, 8, 3, 3)));
var ctx = new TestContext();
var interior = new WalkCell
{
CellId = 0x104,
Portals = [new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[interior.CellId] = interior;
var building = new WalkBuilding
{
PositionCellId = 0xA9B4000Fu,
Portals =
[
new WalkBldPortal
{
PortalSide = 0, OtherCellId = 0x104, OtherPortalId = 0,
StabList = [0x104u],
},
],
// Viewpoint (0,0,0) is on the NEGATIVE side of this splitting
// plane (d=-5): the single PORT node's side==1 arm emits its
// portal exactly once per pass (WalkBuildingPortals.Walk).
DrawingBsp = new WalkBspNode
{
SplittingPlane = new WalkPlane(new Vector3(1, 0, 0), -5f),
InPortals = [new WalkPortalRef { PortalIndex = 0, Polygon = Quad(-2f) }],
},
};
ctx.ViewerDistances[building] = 12.5f;
var worldData = new FakeWorldData();
worldData.ShellByBuilding[building] = new WalkFrameStaticRecords(
new[] { MakeRecord(201, 0, Vector3.Zero, [new MeshRef((uint)shellGfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.CellStaticsByCell[0x104] = new WalkFrameStaticRecords(
new[] { MakeRecord(202, 0, Vector3.Zero, [new MeshRef((uint)interiorGfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.CellDynamicsByCell[0x104] = new WalkFrameStaticRecords(
new[] { MakeRecord(203, 0, Vector3.Zero, [new MeshRef((uint)dynamicGfxObj, Matrix4x4.Identity)], parentCellId: 0x104) }, 0x8C04u);
Matrix4x4 buildingWorld = Matrix4x4.CreateTranslation(10f, 0f, 0f);
worldData.WorldTransformByBuilding[building] = buildingWorld;
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
using ClipFrame clipFrame = ClipFrame.NoClip();
var driver = new WalkFrameDriver(
fx.Dispatcher, leaf, worldData, trace, clipFrame);
var walk = new RetailFrameWalk();
var activeView = new WalkPortalView();
activeView.ResetForPush();
WalkCopyView.AppendFullViewportQuad(
activeView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
Assert.Equal(1, activeView.ViewCount);
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
walk.DrawBuilding(building, activeView, ctx, driver);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
// Campaign OVERHAUL S2 chunk 6: a building's shell fires no
// particle turn of its own any more — interior emitters draw at
// their own cell's CellParticles turn instead.
new[]
{
"ALPHA", "PUNCH:4@v0", "SHELL:00000104",
"FLUSH:1:LookInStatic", "FLUSH:1:Dynamic",
"CELL-PARTICLES:00000104",
"FLUSH:1:BuildingShell",
},
log);
Assert.Equal([0x104u], driver.LookInCellTurns);
Assert.Equal([0x104u], driver.LookInCells);
Assert.Collection(
leaf.Shells,
shell => Assert.Equal(0x104u, shell));
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
Vector2 capturedCenter = Vector2.Zero;
for (int edge = 0; edge < capturedPoly.VertexCount; edge++)
{
capturedCenter += capturedView.View.Vertices[
capturedPoly.VertexIndex + edge].Point;
}
capturedCenter /= capturedPoly.VertexCount;
Vector3 insideCone = ctx.Rays.RayThrough(capturedCenter.X, capturedCenter.Y);
Assert.True(driver.SphereVisibleInLookInTurn(
0, in insideCone, 0.1f));
Assert.False(driver.SphereVisibleInLookInTurn(
0, new Vector3(10_000f, 0f, 10f), 0.1f));
uint clipSlot = Assert.Single(driver.VisibleClipSlotsInLookInTurn(
0, in insideCone, 0.1f, testSphere: true));
Assert.NotEqual(0u, clipSlot);
Assert.Equal(2, clipFrame.SlotCount);
// The punch polygon reached the leaf renderer in WORLD space: the
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
// (10,0,0) under the caller-supplied building world transform.
WalkPolygon punch = Assert.Single(leaf.Punches);
Assert.Equal(new Vector3(9.5f, -0.5f, -2f), punch.Vertices[0]);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(3, mdiCalls.Count);
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
}
[Fact]
public void RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint cellId = 0xF4180112u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
leaf,
new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
// Neither call models a surviving exit view (ov==0 both times — no
// WalkEvent.Landscape turn ran in this synthetic double-entry
// scenario), so per S3 §8.1 R3 the device-stamp advance never fires
// from either call: this isolates the frame-stamp shell dedup from
// the landscape-flush/clear/seal gate entirely.
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 0);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 0);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId], leaf.Shells);
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
Assert.Equal(0, log.Count(entry => entry == "LFLUSH"));
Assert.Equal(0, log.Count(entry => entry == "CLEAR"));
Assert.Equal(0, log.Count(entry => entry == "SEALS"));
Assert.Equal(0, driver.PortalsDrawnCount);
}
// Campaign OVERHAUL S2 chunk 6 pin — the portal-haze bug this chunk
// fixes: a hidden/suspended owner (a portalling player materializing
// into its arrival cell) publishes NO registry rows there, so
// GetCellStatics/GetCellDynamics report empty records for that cell —
// yet retail's CPhysicsObj::add_particle_shadow_to_cell (0x00514a70)
// still draws that owner's emitter at the cell's own object-list turn,
// because particle draw membership is the emitter's OWN cell, never a
// registry/owner lookup. The walk must fire the cell's particle turn
// regardless of whether any static/dynamic record names an owner there.
[Fact]
public void EmitCellContentsTurn_FiresCellParticlesEvenWhenTheCellHasNoStaticOrDynamicRecords()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint arrivalCellId = 0x8A020141u;
var arrivalCell = new WalkCell { CellId = arrivalCellId };
ctx.Cells[arrivalCellId] = arrivalCell;
// FakeWorldData has NO entry at all for the arrival cell —
// GetCellStatics/GetCellDynamics both fall back to
// WalkFrameStaticRecords.Empty, exactly the "hidden owner, no
// registry rows" scenario.
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
// No landscape turn modeled here — ov==0.
sink.OnInteriorFloodDrawTurn([arrivalCellId], outsideViewCount: 0);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Contains("CELL-PARTICLES:8a020141", log);
}
[Fact]
public void LandscapeStampBoundary_RearmsWholeShellForPostClearRootRepaint()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
const uint cellId = 0xF4180112u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(new List<string>()),
new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
// R4 (the "first-frame no-clear quirk", pinned explicitly by
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClear...):
// PortalsDrawnCount starts at 0 for a fresh driver, so ITS first
// ov>0 flood would skip the clear. Prime the counter with one
// throwaway ov>0 flood (this driver's own exit seals arm it) so the
// documented scenario below models a STEADY-STATE interior frame,
// where the clear actually fires.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var primerViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
primerViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(primerViews);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(1, driver.PortalsDrawnCount);
var leaf = new RecordingLeafRenderer(log);
driver.RebindFrame(leaf, clipFrame: null);
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
// LScape::draw has begun. A building look-in reached this cell before
// PView::DrawCells advances m_nFrameStamp and clears interior depth.
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var landscapeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
landscapeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
sink.OnLandscapeViews(landscapeViews);
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [cellId]));
// The same shell must draw again after the retail stamp increment and
// full depth clear; otherwise the pre-clear color survives unpaired
// with depth and bleeds through the root's walls. ov=1 (the
// landscape turn above ran), and PortalsDrawnCount is armed from
// the primer frame's own seals, so the clear actually fires here.
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId, cellId], leaf.Shells);
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
Assert.Contains("CLEAR", log);
Assert.True(
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
"The look-in shell must precede the interior clear.");
Assert.True(
log.LastIndexOf("SHELL:f4180112") > log.IndexOf("SEALS"),
"The rearmed root shell must repaint after the clear and seals.");
}
// ── Fail-loud: a DrawCells turn with no preceding DrawInside/Building
// turn is a walk/driver desync, not a silent skip. ─────────────────────
[Fact]
public void Emit_DrawCellsBeforeAnyDrawInsideOrBuildingTurn_ThrowsRatherThanSilentlyDropping()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>(
() => ((IWalkEventSink)driver).Emit(WalkEvent.DrawCells(0, [0x100u])));
}
// ── Fail-loud: BeginFrame is not re-entrant. ────────────────────────────
[Fact]
public void BeginFrame_CalledWhileAFrameIsAlreadyOpen_Throws()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>(
() => driver.BeginFrame(
ctx, Matrix4x4.Identity, Vector3.Zero));
driver.EndFrame();
// EndFrame cleared the open-frame guard: BeginFrame is usable again.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
driver.EndFrame();
}
// ── Campaign FW3.4a fail-loud: Replay without a completed Collect (no
// BeginFrame/EndFrame at all, or BeginFrame with no matching EndFrame)
// has nothing recorded to draw — throw rather than silently drawing
// nothing, which would look like an empty frame instead of a misuse. ──
[Fact]
public void Replay_WithNoPrecedingCollect_Throws()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw();
Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass));
}
[Fact]
public void Replay_WhileCollectIsStillOpen_Throws()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass));
}
// ── Deliverable: Collect + Replay called as the SPLIT PAIR (never
// RunFrame) — the shape RetailPViewRenderer uses, since it must run other
// frame work (PrepareCellBatches/BuildAndBorrow) between the two. Proves
// the pair alone — with no GPU work happening until Replay — reproduces
// the same turn order RunFrame's combined call would. ──────────────────
[Fact]
public void CollectThenReplay_AsSeparateCalls_PerformsNoGpuWorkUntilReplay()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(
fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData(), new RecordingTrace(log));
var walk = new RetailFrameWalk();
// Outdoor root (camera cell low word < 0x100): a minimal, no-op
// landscape — LScape::draw still runs its sky/terrain turn against
// it even though nothing is published to walk cells/buildings for.
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
using DrawScope draw = fx.BeginDraw();
driver.Collect(
walk, cameraCellId: 0u, cameraCell: null, landscape, ctx,
Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
// No GPU calls at all yet — Collect is CPU-only.
Assert.Empty(log);
Assert.Empty(fx.Device.Calls);
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { "SKY" }, log);
}
[Fact]
public void RebindFrame_ReusesTheDriverAndRoutesTheNextFrameToTheNewLeaf()
{
using var fx = new DispatcherFixture();
var firstLog = new List<string>();
var secondLog = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(
fx.Dispatcher, new RecordingLeafRenderer(firstLog), new FakeWorldData());
var walk = new RetailFrameWalk();
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
using DrawScope draw = fx.BeginDraw();
driver.Collect(
walk, 0u, null, landscape, ctx,
Matrix4x4.Identity, Vector3.Zero);
driver.Replay(draw.Frame, draw.Pass);
driver.RebindFrame(new RecordingLeafRenderer(secondLog), clipFrame: null);
driver.Collect(
walk, 0u, null, landscape, ctx,
Matrix4x4.Identity, Vector3.Zero);
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { "SKY" }, firstLog);
Assert.Equal(new[] { "SKY" }, secondLog);
}
[Fact]
public void RebindFrame_DiscardsAnIncompletePriorFrameAndRecovers()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var ctx = new TestContext();
var driver = new WalkFrameDriver(
fx.Dispatcher, new RecordingLeafRenderer(new List<string>()), new FakeWorldData());
var walk = new RetailFrameWalk();
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
driver.RebindFrame(new RecordingLeafRenderer(log), clipFrame: null);
using DrawScope draw = fx.BeginDraw();
driver.Collect(
walk, 0u, null, landscape, ctx,
Matrix4x4.Identity, Vector3.Zero);
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { "SKY" }, log);
}
// ── Deliverable: an outdoor landscape-cell turn with no building appends
// straight to the stream (no shell call — outdoor cells have no EnvCell
// shell), and the accumulated content flushes at frame end. ───────────
[Fact]
public void OnLandscapeCellTurn_AppendsOutdoorStaticsWithNoShellCallAndFlushesAtFrameEnd()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObj = 0x0200_0020UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100020u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var ctx = new TestContext();
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0x8C040005u] = new WalkFrameStaticRecords(
new[] { MakeRecord(301, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.OutdoorDynamicsByCell[0x8C040005u] = new WalkFrameStaticRecords(
new[] { MakeRecord(302, 0x50000001, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
var driver = new WalkFrameDriver(
fx.Dispatcher, new RecordingLeafRenderer(log), worldData, new RecordingTrace(log));
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
((IWalkEventSink)driver).OnLandscapeCellTurn(0x8C040005u);
Assert.Empty(log); // no GPU work at Collect time; nothing recorded to the log yet
driver.EndFrame();
Assert.Empty(log); // still nothing — EndFrame closes Collect, it does not Replay
driver.Replay(draw.Frame, draw.Pass);
// FW4 #132 positional invariant: the cell's emitters submit at its
// own landscape turn, after its meshes flush. Campaign OVERHAUL S2
// chunk 6: the leaf draws by CELL, not by owner set.
Assert.Equal(
new[] { "FLUSH:2:OutdoorStatic,Dynamic", "PARTICLES:8c040005" },
log);
var visibleCells = new HashSet<uint> { 0xDEAD_BEEFu };
driver.CopyVisibleCellsTo(visibleCells);
Assert.Equal(new[] { 0x8C040005u }, visibleCells);
GpuRecordedMultiDrawIndirect[] mdi =
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().ToArray();
Assert.Equal(2, mdi.Length);
Assert.Equal(2u, mdi.Aggregate(0u, static (sum, call) => sum + call.DrawCount));
}
// Campaign OVERHAUL S2 chunk 6: the mesh-dedup shape (a shadow alias's
// record entering two landscape cells' dictionaries) is a MESH-only
// concern — retail's per-part render membership is whole-object per
// cell. Particles are NOT part of that alias: an emitter owns exactly
// one shadow in its own current cell (add_particle_shadow_to_cell
// 0x00514a70), so each VISITED landscape cell fires its own independent
// StaticParticles turn with no cross-cell dedupe needed or performed.
[Fact]
public void OnLandscapeCellTurn_MultiCellShadowAlias_DrawsMeshOnceButParticlesPerVisitedCell()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObj = 0x0200_0021UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var ctx = new TestContext();
var worldData = new FakeWorldData();
RenderProjectionRecord shadowAlias = MakeRecord(
303,
0x50000002,
Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
WalkFrameStaticRecords aliases = new(
new[] { shadowAlias },
0xF07Fu);
worldData.OutdoorDynamicsByCell[0xF07F0040u] = aliases;
worldData.OutdoorDynamicsByCell[0xF0800001u] = aliases;
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(log),
worldData,
new RecordingTrace(log));
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
((IWalkEventSink)driver).OnLandscapeCellTurn(0xF07F0040u);
((IWalkEventSink)driver).OnLandscapeCellTurn(0xF0800001u);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[] { "FLUSH:1:Dynamic", "PARTICLES:f07f0040", "PARTICLES:f0800001" },
log);
GpuRecordedMultiDrawIndirect[] mdi =
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().ToArray();
Assert.Single(mdi);
Assert.Equal(1u, mdi[0].DrawCount);
}
[Fact]
public void CoarseLandscapeCellTurn_ExpandsToEveryCoveredAuthoritativeOwnerCell()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0200_0022UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100022u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0xE43D0040u] = new WalkFrameStaticRecords(
new[] { MakeRecord(304, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(new List<string>()),
worldData);
var ctx = new TestContext();
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
// A 2x2 coarse grid's cell 3 covers authoritative x/y cells 4..7,
// including original cell 64. The old cellIndex+1 lookup only asked
// for ids 1..4 and silently lost this record.
((IWalkEventSink)driver).OnLandscapeCellTurn(0xE43DFFFFu, 2, 3);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Contains(0xE43D0040u, driver.VisitedLandscapeCellIds);
GpuRecordedMultiDrawIndirect call = Assert.Single(
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Equal(1u, call.DrawCount);
}
[Fact]
public void Collect_DoesNotExposeNearAlphaToEarlierBuildingBarrier()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0200_0023UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100023u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0xE43D0001u] = new WalkFrameStaticRecords(
new[] { MakeRecord(305, 0, new Vector3(0, 0, -20), [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
worldData.OutdoorStaticsByCell[0xE43D0002u] = new WalkFrameStaticRecords(
new[] { MakeRecord(306, 0, new Vector3(0, 0, -2), [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xE43Du);
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
var ctx = new TestContext();
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
((IWalkEventSink)driver).OnLandscapeCellTurn(0xE43D0001u);
((IWalkEventSink)driver).OnBuildingTurn(new WalkBuilding());
((IWalkEventSink)driver).OnLandscapeCellTurn(0xE43D0002u);
driver.EndFrame();
Assert.Equal(0, fx.AlphaQueue.PendingCount);
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { 1 }, leaf.AlphaPendingAtBarrier);
Assert.Equal(1, fx.AlphaQueue.PendingCount);
}
// ── F4(a) (S3 chunk 3 fix round 1 §9.6): an outdoor-root sequence — the
// LandCell terrain turn precedes its own cell's object-list turn, and
// (since nothing else intervenes) the whole frame's terrain stays one
// PENDING batch until Replay's own end. Drives RetailFrameWalk.
// DrawLandscape directly with a ZERO-view WalkPortalView — the SAME
// deterministic "CY-only" admission technique RetailFrameWalkTests'
// own outdoor tests use (a real WalkFrame root's 1-view default quad
// depends on the production ray-caster's screen geometry, which this
// suite's synthetic Caster does not model faithfully enough to predict
// block/cell admission from). A side=1 block's own object-list turn
// (WalkFrameDriver.OnLandscapeCellTurn's coarse-cell expansion) fires a
// StaticParticles turn for every one of the underlying 64 owner
// buckets; marking them all "no emitters" keeps this test's sequence
// to exactly SKY + one LANDCELL, the same way a genuinely empty turn
// stays silent (F2). ────────────────────────────────────────────────
private static IEnumerable<uint> CoarseLandscapeBuckets(uint landblockPrefix)
{
for (int x = 0; x < 8; x++)
for (int y = 0; y < 8; y++)
yield return landblockPrefix | (uint)(x * 8 + y + 1);
}
/// <summary>A ONE-view <see cref="WalkPortalView"/> whose single polygon
/// has ZERO vertices — <c>WalkLandscape.CheckBlocks</c> reads only
/// <c>poly.VertexCount</c> to build its edge-plane list, so this reaches
/// the SAME permissive <c>edgeCount == 0</c> ("CY-only") admission test
/// RetailFrameWalkTests' own outdoor fixtures use via a bare
/// zero-VIEW <see cref="WalkPortalView"/> — but with <c>ViewCount == 1</c>,
/// satisfying <see cref="WalkFrameDriver"/>'s fail-loud
/// "a Landscape turn needs at least one active view" guard, which a
/// driver-level test (unlike a bare <c>IWalkEventSink</c> recorder) must
/// pass through <c>RetailFrameWalk.DrawLandscape</c>'s real
/// <c>Emit(WalkEvent.Landscape(...))</c> call.</summary>
private static WalkPortalView OneDegenerateView()
{
var view = new WalkPortalView { ViewCount = 1 };
view.View.Polys.Add(new WalkViewPoly(0, 0, 0, 0, 0, 0));
return view;
}
[Fact]
public void OutdoorRoot_LandCellPrecedesItsOwnCellsObjectTurn_ThenFlushesAtReplaysEnd()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
var walk = new RetailFrameWalk();
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
var block = new WalkLandBlock
{
LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
};
block.EnsureCellArrays();
landscape.Blocks[0] = block;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { "SKY", "LANDCELL:f4180000:1:0" }, log);
Assert.Equal(new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF4180000u, 1, 0) },
Assert.Single(leaf.LandCellBatches));
}
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
// surviving exit view — the SAME fixture as
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
// above, with one populated land block added to the (previously
// unpublished) landscape. Sequence: SKY, LANDCELL…, LFLUSH, SEALS,
// SHELL… — the LandCell terrain turn(s) drawn through the interior
// root's own exit view precede the landscape-flush/seal/flood-cell
// turns, exactly like the outdoor case above. ────────────────────────
[Fact]
public void InteriorRootWithExitView_DrawsLandCellThroughTheExitViewBeforeFlushSealsAndFlood()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObjA = 0x0200_0025UL;
const ulong gfxObjB = 0x0200_0026UL;
InjectRenderData(fx.Manager, gfxObjA, MakeFlatMesh(
MakeBatch(0x08100025u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, gfxObjB, MakeFlatMesh(
MakeBatch(0x08100026u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
var ctx = new TestContext();
var cell1 = new WalkCell
{
CellId = 0x100,
StabList = [0x101u],
Portals =
[
new WalkCellPortal
{
OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
},
// The exit portal (retail's "world beyond the door") — this
// is what raises ov to 1 and drives the landscape (and now
// its own LandCell turns) before clear+seals+the flood cells.
new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 1, PortalSide = 0, OtherPortalId = -1,
},
],
PortalPolygons = [Quad(-2f), Quad(-3f)],
};
var cell2 = new WalkCell
{
CellId = 0x101,
Portals = [new WalkCellPortal
{
OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell1.CellId] = cell1;
ctx.Cells[cell2.CellId] = cell2;
var worldData = new FakeWorldData();
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
new[] { MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)]) }, 0x8C04u);
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
new[] { MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)]) }, 0x8C04u);
var leaf = new RecordingLeafRenderer(log);
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
var trace = new RecordingTrace(log);
using ClipFrame clipFrame = ClipFrame.NoClip();
var driver = new WalkFrameDriver(
fx.Dispatcher, leaf, worldData, trace, clipFrame);
var walk = new RetailFrameWalk();
// The same 1x1 landscape RunFrame_InteriorFloodWithExitView... uses,
// but with a real block published in its one slot so the exit view
// has something to admit.
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
var block = new WalkLandBlock
{
LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
};
block.EnsureCellArrays();
landscape.Blocks[0] = block;
using DrawScope draw = fx.BeginDraw();
driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: landscape,
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
Assert.Equal(
new[]
{
"SKY", "LANDCELL:f4180000:1:0", "LFLUSH", "SEALS",
"SHELL:00000101", "SHELL:00000100",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
},
log);
}
// ── T4 (S3 chunk 3 §9.3), re-expressed for fix round 1's F2 rule
// (§9.6 F4c): the batch is now a single PENDING list Replay keeps
// across the WHOLE frame, not a same-landblock lookahead merge — two
// LandCell turns of DIFFERENT landblocks with only an empty particle
// turn between them still merge into ONE DrawLandCellBatch call; a
// StreamMark (a real cell with statics) OR a building's own alpha
// barrier between two LandCell turns splits the batch; the fake leaf's
// HasRenderableEmittersInCell reports "no emitters" for one cell (via
// CellsWithoutEmitters) and "has emitters" (the default) for another,
// so both arms of the F2 particle-turn gate are covered in one test. ──
[Fact]
public void OnLandCellTurn_MergesAcrossLandblocksOverAnEmptyParticleTurn_RealSubmissionsSplit()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0200_0024UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100024u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[0xBBBB0002u] = new WalkFrameStaticRecords(
new[] { MakeRecord(310, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xBBBBu);
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
// The 0xAAAA0001 particle turn has no world-data records AND is
// marked without emitters -> a genuinely empty turn (F2's other
// arm: 0xBBBB0002 below keeps the default "has emitters").
leaf.CellsWithoutEmitters.Add(0xAAAA0001u);
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnLandscapeViews(new WalkPortalView());
sink.OnLandCellTurn(0xF4180000u, 8, 0); // landblock A
sink.OnLandscapeCellTurn(0xAAAA0001u); // empty particle turn: no emitters -> no submit, no flush
sink.OnLandCellTurn(0xF3180000u, 8, 0); // landblock B, DIFFERENT -> still merges (F2)
sink.OnLandscapeCellTurn(0xBBBB0002u); // real content -> StreamMark splits; has emitters -> submits
sink.OnLandCellTurn(0xF3180000u, 8, 1); // new batch, started after the StreamMark split
sink.OnBuildingTurn(new WalkBuilding()); // the building's own alpha barrier splits again
sink.OnLandCellTurn(0xF2180000u, 8, 0); // final batch, flushed at Replay's own end
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[]
{
"LANDCELL:f4180000:8:0,f3180000:8:0",
"PARTICLES:bbbb0002",
"LANDCELL:f3180000:8:1",
"ALPHA",
"LANDCELL:f2180000:8:0",
},
log);
Assert.DoesNotContain("PARTICLES:aaaa0001", log);
Assert.Equal(3, leaf.LandCellBatches.Count);
Assert.Equal(
new (uint LandblockId, int SideCellCount, int CellIndex)[]
{
(0xF4180000u, 8, 0), (0xF3180000u, 8, 0),
},
leaf.LandCellBatches[0]);
Assert.Equal(
new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF3180000u, 8, 1) },
leaf.LandCellBatches[1]);
Assert.Equal(
new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF2180000u, 8, 0) },
leaf.LandCellBatches[2]);
}
// ── Fixture (mirrors WalkStaticStreamPopulatorTests' DispatcherFixture —
// FW3.2a's own referee) ─────────────────────────────────────────────────
private static RenderProjectionRecord MakeRecord(
uint localEntityId,
uint serverGuid,
Vector3 position,
IReadOnlyList<MeshRef> meshRefs,
bool isBuildingShell = false,
uint parentCellId = 0u) =>
new(
Id: RenderProjectionId.FromRaw(localEntityId),
ProjectionClass: RenderProjectionClass.OutdoorStatic,
OwnerIncarnation: RenderOwnerIncarnation.FromRaw(1),
Transform: new RenderTransform(Matrix4x4.CreateTranslation(position)),
PreviousTransform: default,
MeshSet: default,
Material: default,
Residency: default,
Bounds: default,
Flags: RenderProjectionFlags.Draw,
DegradeState: default,
SortKey: new RenderSortKey(0),
DirtyMask: default,
Source: new RenderSourceMetadata(
LocalEntityId: localEntityId,
ServerGuid: serverGuid,
SourceId: 0,
ParentCellId: parentCellId,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default),
EntityPayload: new RenderEntityPayload(
MeshRefs: meshRefs,
PaletteOverride: null,
IsBuildingShell: isBuildingShell));
private static ObjectRenderBatch MakeBatch(
uint surfaceId,
TranslucencyKind translucency,
uint firstIndex,
int baseVertex,
int indexCount,
uint textureSlotIndex,
uint textureLayer = 0,
CullMode cullMode = CullMode.CounterClockwise) =>
new()
{
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
Translucency = translucency,
FirstIndex = firstIndex,
BaseVertex = (uint)baseVertex,
IndexCount = indexCount,
TextureSlot = new GpuTextureSlot(textureSlotIndex),
TextureIndex = (int)textureLayer,
};
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
new() { Batches = new List<ObjectRenderBatch>(batches) };
private static void InjectRenderData(ObjectMeshManager manager, ulong id, ObjectRenderData data)
{
FieldInfo field = typeof(ObjectMeshManager).GetField(
"_renderData", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException(
"ObjectMeshManager._renderData field not found — test relies on this exact name.");
var dict = (ConcurrentDictionary<ulong, ObjectRenderData>)field.GetValue(manager)!;
dict[id] = data;
}
private readonly struct DrawScope : IDisposable
{
private readonly IDisposable _publication;
private readonly IGpuPassEncoder _pass;
private readonly RetailAlphaQueue? _alpha;
public DrawScope(
IGpuFrame frame,
IGpuPassEncoder pass,
IDisposable publication,
RetailAlphaQueue? alpha = null)
{
Frame = frame;
_pass = pass;
_publication = publication;
_alpha = alpha;
}
public IGpuFrame Frame { get; }
public IGpuPassEncoder Pass => _pass;
public void Dispose()
{
if (_alpha?.IsCollecting == true)
_alpha.EndFrame();
_publication.Dispose();
_pass.Dispose();
}
}
private sealed class DispatcherFixture : IDisposable
{
private readonly WbMeshAdapter _meshAdapter;
private readonly TextureCache _textures;
public DispatcherFixture()
{
Device = new RecordingGpuDevice();
FrameLifetime = new GpuDeviceFrameLifetime(Device);
Scope = new VulkanWorldPassScope(sampleCount: 1);
_textures = new TextureCache(Device, new NoopDatReaderWriter());
_meshAdapter = new WbMeshAdapter(
Device,
new NoopDatReaderWriter(),
new NullPreparedAssetSource(),
NullLogger<WbMeshAdapter>.Instance,
Device.Retirement);
var entitySpawnAdapter = new EntitySpawnAdapter(
_textures,
_ => throw new NotSupportedException("Not exercised by these tests."));
Dispatcher = new WbDrawDispatcher(
Device,
FrameLifetime,
Scope,
_textures,
_meshAdapter,
entitySpawnAdapter,
new EntityClassificationCache(),
new AcDream.Core.Rendering.TranslucencyFadeManager(),
alphaQueue: AlphaQueue);
}
public RecordingGpuDevice Device { get; }
public GpuDeviceFrameLifetime FrameLifetime { get; }
public VulkanWorldPassScope Scope { get; }
public WbDrawDispatcher Dispatcher { get; }
public RetailAlphaQueue AlphaQueue { get; } = new();
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
public DrawScope BeginDraw(bool beginAlpha = false)
{
if (beginAlpha)
{
Dispatcher.BeginFrame(frameSlot: 0);
AlphaQueue.BeginFrame();
}
FrameLifetime.BeginFrame();
IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass(
GpuPassDescription.BackbufferClear(
"fw3-2b-1-walk-frame-driver-test", Vector4.Zero, sampleCount: 1));
IDisposable publication = Scope.Publish(pass);
Device.Clear();
return new DrawScope(
frame,
pass,
publication,
beginAlpha ? AlphaQueue : null);
}
public void Dispose()
{
if (AlphaQueue.IsCollecting)
AlphaQueue.AbortFrame();
Dispatcher.Dispose();
_meshAdapter.Dispose();
_textures.Dispose();
Device.Dispose();
}
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;
public CacheStats DecodedTextureCacheStats => default;
public PreparedAssetPresence Probe(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Missing;
public PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken = default) =>
PreparedAssetReadResult.Missing;
public void Dispose()
{
}
}
private sealed class NoopDatReaderWriter : IDatReaderWriter
{
private readonly StubDatabase _portal = new();
private readonly StubDatabase _highRes = new();
private readonly StubDatabase _language = new();
private readonly StubDatabase _cell = new();
public string SourceDirectory => string.Empty;
public IDatDatabase Portal => _portal;
public IDatDatabase Cell => _cell;
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
new(new Dictionary<uint, IDatDatabase>());
public IDatDatabase HighRes => _highRes;
public IDatDatabase Language => _language;
public IDatDatabase Local => _language;
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
new(new Dictionary<uint, uint>());
public int PortalIteration => 0;
public int CellIteration => 0;
public int HighResIteration => 0;
public int LanguageIteration => 0;
public bool TryGetFileBytes(
uint regionId,
uint fileId,
ref byte[] bytes,
out int bytesRead)
{
bytesRead = 0;
return false;
}
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
Array.Empty<uint>();
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
Array.Empty<IDatReaderWriter.IdResolution>();
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
public bool TrySave<T>(
uint regionId,
T obj,
int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
[return: MaybeNull]
public T Get<T>(uint fileId) where T : IDBObj => default;
public bool TryGet<T>(
uint fileId,
[MaybeNullWhen(false)] out T value) where T : IDBObj
{
value = default;
return false;
}
public void Dispose()
{
}
private sealed class StubDatabase : IDatDatabase
{
public DatDatabase Db => throw new NotSupportedException();
public int Iteration => 0;
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
Array.Empty<uint>();
public bool TryGet<T>(
uint fileId,
[MaybeNullWhen(false)] out T value) where T : IDBObj
{
value = default;
return false;
}
public bool TryGetFileBytes(
uint fileId,
[MaybeNullWhen(false)] out byte[] value)
{
value = null;
return false;
}
public bool TryGetFileBytes(
uint fileId,
ref byte[] bytes,
out int bytesRead)
{
bytesRead = 0;
return false;
}
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
public void Dispose()
{
}
}
}
}