acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs
Erik f6b4584bf3 feat(render): S2 chunk 6 — particle emitters draw by their own cell (add_particle_shadow_to_cell)
Owner G2 finding: the purple cloud around an arriving character no longer
drew. The server keeps the player Hidden until acdream sends LoginComplete at
reveal completion (retail-correct); the Hidden-state script's emitters spawn
in the arrival cell and are view-eligible when the world appears, but the
walk drew an owner's emitters only through the owner's registry rows, and a
hidden owner's shadow is suspended. Retail's
CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) gives an emitter one
shadow in its OWN current cell, drawn at that cell's turn regardless of the
parent's hidden state (add_shadows_to_cells 0x00514aed skips the flood for
state & 0x1000).

Port: ParticleSystem keeps a per-pass cell -> renderable-handles index
(maintained at every renderable/OwnerCellId change) and
CopyRenderableEmittersInCell; ParticleRenderer.DrawForCell; the walk draws
particles BY CELL at the existing turns (interior CellParticles, landscape
LandscapeCellParticles), the events fire for every visited cell, and every
owner-union particle path is deleted (UnionOwners/UnionNewOwners for
particles, the outdoor drawn-owner dedupe, the executor's owner
classification sets, the context ParticleOwnerIds members). The post-replay
per-cell pass double-submitted the root flood's emitters and is deleted: an
emitter draws once, at its cell's replay turn. AD-117 item 4 becomes a port
note (the index lives in the particle system; an emitter is not a physics
object in acdream). The temporary [pes-spawn]/[pes-vis] traces are removed
and the ACDREAM_DUMP_PLAYSCRIPT row restored.

Verified: timed arrival route logs/selfgate-20260903-062522-haze-chunk6,
frame h02-arrive-400ms shows the cloud at the character in Facility Hub.
Gates (Release): Core 4,987/4,987; Content 214/214; Runtime 1,884/1,884; App
hermetic lane 6,760/6,760; App InstalledDat 217 pass / 2 pre-existing #383.

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

1375 lines
56 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 void DrawSky() => log.Add("SKY");
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
public void DrawCellShell(uint cellId)
{
Shells.Add(cellId);
log.Add($"SHELL:{cellId:x8}");
}
public void ClearInteriorDepth() => log.Add("CLEAR");
public void DrawExitSeals() => log.Add("SEALS");
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 (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 depth clear (pc:432731-432732), 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 (flush no-op, sky, terrain), THEN
// clear, seals, then all shells and all contents in reverse order. ───
[Fact]
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
{
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[]
{
"SKY", "TERRAIN:0", "CLEAR", "SEALS",
"SHELL:00000101", "SHELL:00000100",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
},
log);
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);
sink.OnInteriorFloodDrawTurn([interiorCellId]);
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: the ov==0 interior case — no exit view survives, so
// DrawInside never runs the landscape turn at all; retail's clear+seals
// still run unconditionally for the interior root's own flood, straight
// after the (draw-nothing) DC event. ───────────────────────────────────
[Fact]
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeButStillClearsAndSeals()
{
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 — but CLEAR/SEALS still fire unconditionally.
Assert.Equal(
new[]
{
"CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
},
log);
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));
}
// ── 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);
sink.OnInteriorFloodDrawTurn([cellId]);
sink.OnInteriorFloodDrawTurn([cellId]);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId], leaf.Shells);
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
Assert.Equal(2, log.Count(entry => entry == "CLEAR"));
Assert.Equal(2, log.Count(entry => entry == "SEALS"));
}
// 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);
sink.OnInteriorFloodDrawTurn([arrivalCellId]);
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 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);
// 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.
sink.OnInteriorFloodDrawTurn([cellId]);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId, cellId], leaf.Shells);
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
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", "TERRAIN:0" }, 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", "TERRAIN:0" }, firstLog);
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, 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", "TERRAIN:0" }, 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);
}
// ── 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()
{
}
}
}
}