acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs
Erik ff607a1e04 fix(render): S3 chunk 4 round 1 — loop-shape weather pin, one weather gate, dead terrain-clip stack deleted, sky drawn once
Campaign OVERHAUL S3 chunk 4 fix round 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§10.4), on top of 6ba4b0b87. K1, K2, K3, K4, K6, K7, K8 (K5 was the lead's own plan-doc note, already
done).

K1 (blocking) — the Assert.Single pin over DrawLandscapeDynamicsPhase's DrawWeatherOnce call site
stayed green even wrapped in a foreach (it counts distinct call-site offsets, not loop shape). Added
a real LOOP-SHAPE pin reusing CompiledCallGraph.ReadBranches: asserts no backward branch (target
offset < its own offset) spans the DrawWeatherOnce call's IL offset — the shape every C#
for/foreach/while loop compiles to.

K2 (minor) — the Collect-time "OC" print fired on ctx.WeatherGateOpen alone while the Replay-time
draw independently re-derived clipAssembly.OutsideViewSlices.Length > 0, which could diverge on an
interior root whose landscape turn ran but whose reassembled outside-view slices ended up empty.
WalkFrameDriver now owns one WeatherTurnFired flag, set unconditionally inside OnWeatherTurn (so it
tracks "did the print's own gate hold" regardless of the transcript flag) and reset every
BeginFrame/AbortFrame; RetailPViewRenderer.DrawLandscapeDynamicsPhase gates DrawWeatherOnce on it
instead of re-deriving its own condition.

K3 (minor) — ClipFrame.SetTerrainClip (the terrain OutsideView writer) had no remaining caller after
chunk 4's original round and K4 below; deleted along with IWorldPassSurface.SetTerrainClip,
RhiWorldPassSurface.SetTerrainClip, the now-orphaned PublishTerrainClip helper, and PrepareClipFrame's
terrain-clip publish call. The terrain/sky shaders still declare the TerrainClip UBO (verified:
terrain_modern.vert, terrain_atmospheric.vert, sky.vert all read uTerrainClipCount/uTerrainClipPlanes),
so the section binding itself (WorldFrameSectionBinding.BindTerrainClip) stays — its existing
zeroed-ring fallback (for when nothing published the section) now binds that same all-zero disabled
block on every frame, identical bytes to the old permanent NoClip/Reset default.

K4 (major) — DrawWalkSky still looped the OUTDOOR case once per active landscape view under a doorway
scissor + BindTerrainClip + EnableClipDistances (the INTERIOR case already drew once unclipped, FW4
slice 6). Retail draws GameSky::Draw(sky,0) ONCE, unconditionally, before LScape::draw's block loop,
for both root kinds. Collapsed DrawWalkSky to one unconditional, unclipped draw; deleted
BeginDoorwayScissor and the RetailPViewPassExecutor.EnableClipDistances wrapper (both lost their only
caller). _surface.BeginScissor/EndScissor and IWorldPassSurface.EnableClipDistances stay:
RhiWorldPassSurface.ClearInteriorDepth still ends an active scissor, and WorldScenePassExecutor (the
separate flat-world path) still calls EnableClipDistances directly.

K6 (minor) — the punch-fan CPU/GPU equivalence pin hand-built a ClipViewSlice from ClipPlaneSet.From's
raw output, which could pass even if ClipFrameAssembler.Assemble's own packing/array-construction
diverged from that output. Rewrote it to build a real PortalVisibilityFrame and run it through
ClipFrameAssembler.Assemble, reading the planes back through assembly.OutsideViewSlices[0].Planes —
the exact outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)) path
ReassembleOutsideViewFromWalk (the walk's real interior-root producer) shares.

K7 (note) — restored the "a real look-in slice never reuses the reserved no-clip slot 0" assertion
the deleted VisibleClipSlotsInLookInTurn used to prove, via a new internal test-only accessor
(WalkFrameDriver.LookInSliceClipSlotAt) reading the same _lookInSlices storage
InteriorFloodViewClipPlanesAt resolves through _clipFrame.GetSlotPlanes(slice.ClipSlot).

K8 (note) — documented in DrawWeatherOnce's own comment that the weather mesh drawing before the rain
particles is this method's own call-order choice; retail's single GameSky::Draw(sky,1) imposes no
order between acdream's two substitutes.

Every new/changed pin's mutation was hand-verified this session (temporarily applied, ran the
specific test, confirmed the exact failing assertion, then reverted — see the parent task's structured
report for the four failing-assertion texts).

No register row added or removed — every change here deletes an acdream-only rule or repairs a pin;
none introduces a new deviation.

Full solution build: 0 warnings/0 errors. App hermetic (Lane!=InstalledDat/PreparedPackage/Live/
Manual/Timing/Windows/Linux/SystemFont & Purpose!=Diagnostic & Status!=KnownFailure): 6831/6831.
InstalledDat: 244 pass/1 skip/4 known — identical to 6ba4b0b87's own baseline (2x #383 layout tests,
TowerAscent, and the pre-existing #458 WalkLandscape.CheckBlocks block-visibility divergence,
unrelated to and untouched by this round).

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

1974 lines
86 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 partial 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;
/// <summary>S3 chunk 4 (O1/O2/O3): overrides <see
/// cref="IRetailFrameWalkContext"/>'s default-false/-zero weather-turn
/// members so a test can arm the gate and pin the printed "OC" cell
/// id. Real production values wire <c>RetailPViewFrameInput.ViewerCellId</c>
/// / <c>RetailPViewPassExecutor.ShouldDrawWeatherOnce(...)</c> — see
/// <c>WalkProductionFrameContext</c>.</summary>
public uint ViewerCellId { get; set; }
public bool WeatherGateOpen { get; set; }
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));
// S3 chunk 4 fix round 1 (K7): restores the pin the deleted
// VisibleClipSlotsInLookInTurn used to prove — a real look-in slice
// is packed into its own GPU clip slot, never left on the reserved
// no-clip slot 0 (the exit-seal KEEP reads planes through the SAME
// slot mechanism, _clipFrame.GetSlotPlanes(slice.ClipSlot)).
uint clipSlot = driver.LookInSliceClipSlotAt(0);
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);
// Every covered bucket but ONE reports no renderable emitter, so the
// block's object turns submit nothing (no flush) except bucket
// 0xF4180001, whose particle turn is a real submission — it must land
// AFTER the coarse cell's own terrain (retail DrawLandCell → DrawSortCell).
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
leaf.CellsWithoutEmitters.Remove(0xF4180001u);
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", "PARTICLES:f4180001" }, 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), then again for fix round 2's F10 rule (§9.7 F12): the
// batch is a single PENDING list Replay keeps across the WHOLE frame.
// Two LandCell turns of DIFFERENT landblocks merge into ONE
// DrawLandCellBatch call across EITHER of two things that used to (or,
// for a real StreamMark, still might look like they should) split it:
// an intervening EMPTY particle turn (F2, unchanged) AND an intervening
// StreamMark from a cell that DOES grow the stream but has no
// renderable emitter of its own (F10, new — proves the split doesn't
// come back just because content was appended). A cell with BOTH real
// stream content AND a renderable emitter still splits, because its
// StaticParticles turn itself is a flush point (F10) — the split just
// no longer comes from the StreamMark that precedes it. A building's
// own alpha barrier and its portal pass's punch fan each split too
// (F10: both remain flush points). ─────────────────────────────────────
[Fact]
public void OnLandCellTurn_MergesAcrossLandblocksOverStreamMarksAndEmptyParticleTurns_RealFlushPointsSplit()
{
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();
// Has REAL stream content (so its own turn records a genuine
// StreamMark event) but NO renderable emitter — the F10 case: the
// StreamMark must not split the pending terrain batch even though
// the ordered stream visibly grew.
worldData.OutdoorStaticsByCell[0xAAAA0002u] = new WalkFrameStaticRecords(
new[] { MakeRecord(311, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) },
0xAAAAu);
// Has real stream content AND a renderable emitter (default) — its
// StaticParticles turn is itself a flush point.
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, unchanged).
// 0xAAAA0002 has world-data records (grows the stream, so Collect
// records a real StreamMark) but is ALSO marked without emitters ->
// the F10 case. 0xBBBB0002 keeps the default "has emitters".
leaf.CellsWithoutEmitters.Add(0xAAAA0001u);
leaf.CellsWithoutEmitters.Add(0xAAAA0002u);
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, new RecordingTrace(log));
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
// A REAL full-viewport landscape view: with an empty view the classifier
// admits nothing, the stream never grows, and no StreamMark ever fires
// (the round-2 review caught exactly that vacuity).
var landscapeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
landscapeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(landscapeViews);
sink.OnLandCellTurn(0xF4180000u, 8, 0); // landblock A
sink.OnLandscapeCellTurn(0xAAAA0001u); // empty particle turn: no growth, no emitters -> no submit, no flush (F2)
sink.OnLandCellTurn(0xF3180000u, 8, 0); // landblock B, DIFFERENT -> still merges (F2)
sink.OnLandscapeCellTurn(0xAAAA0002u); // grows the stream (a real StreamMark fires) but no emitters -> the StreamMark does NOT flush (F10), and the particle turn doesn't either
sink.OnLandCellTurn(0xF2180001u, 8, 0); // landblock C -> still merges straight across that StreamMark (F10)
sink.OnLandscapeCellTurn(0xBBBB0002u); // grows the stream too, but HAS emitters (default) -> its StaticParticles turn flushes (F10), not the StreamMark ahead of it
sink.OnLandCellTurn(0xF3180000u, 8, 1); // new batch, started after that flush
sink.OnBuildingTurn(new WalkBuilding()); // the building's own alpha barrier flushes (F10)
sink.OnLandCellTurn(0xF2180000u, 8, 0); // new batch
sink.OnPunchGeometry(new WalkBuilding(), Quad(0f), 0); // the portal pass's punch fan flushes (F10)
sink.OnLandCellTurn(0xF1180000u, 8, 0); // final batch, flushed at Replay's own end
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[]
{
"FLUSH:1:OutdoorStatic", // 0xAAAA0002's statics: the StreamMark draws its range but does NOT flush the terrain batch (F10)
"FLUSH:1:OutdoorStatic", // 0xBBBB0002's statics: same
"LANDCELL:f4180000:8:0,f3180000:8:0,f2180001:8:0",
"PARTICLES:bbbb0002",
"LANDCELL:f3180000:8:1",
"ALPHA",
"LANDCELL:f2180000:8:0",
"PUNCH:4@v0",
"LANDCELL:f1180000:8:0",
},
log);
Assert.DoesNotContain("PARTICLES:aaaa0001", log);
Assert.DoesNotContain("PARTICLES:aaaa0002", log);
// Two genuine StreamMarks fired (the ordered stream grew twice) — the
// F10 rule is exercised, not assumed.
Assert.Equal(2, log.Count(entry => entry.StartsWith("FLUSH:", StringComparison.Ordinal)));
Assert.Equal(4, leaf.LandCellBatches.Count);
Assert.Equal(
new (uint LandblockId, int SideCellCount, int CellIndex)[]
{
(0xF4180000u, 8, 0), (0xF3180000u, 8, 0), (0xF2180001u, 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]);
Assert.Equal(
new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF1180000u, 8, 0) },
leaf.LandCellBatches[3]);
}
// ── New punch-order pin (S3 chunk 3 fix round 2, §9.7 F12): a
// PunchFan flushes the pending terrain batch (F10 — PunchFan's
// DEPTHTEST_ALWAYS + write is exactly the ordering this chunk exists to
// preserve, §9.1 R4). LandCell(far), a building turn whose portal pass
// emits a PunchFan, LandCell(near) -> the leaf log reads
// LANDCELL(far batch), PUNCH, LANDCELL(near batch): the far terrain is
// flushed and drawn BEFORE the punch stamps its far-Z, and the near
// terrain starts a NEW batch drawn strictly AFTER it — so the nearer
// terrain still overwrites the punched depth, exactly as retail's own
// interleave requires. ──────────────────────────────────────────────────
[Fact]
public void OnPunchGeometry_FlushesPendingTerrainBatch_FarTerrainBeforePunchNearTerrainAfter()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnLandscapeViews(new WalkPortalView());
sink.OnLandCellTurn(0xF4180000u, 8, 0); // far land cell
sink.OnPunchGeometry(new WalkBuilding(), Quad(0f), 0); // the building's far-Z punch
sink.OnLandCellTurn(0xF4180000u, 8, 1); // near land cell (same landblock — the point is ORDER, not the landblock key)
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(
new[]
{
"LANDCELL:f4180000:8:0",
"PUNCH:4@v0",
"LANDCELL:f4180000:8:1",
},
log);
}
// ── 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()
{
}
}
}
}