diff --git a/src/AcDream.App/Rendering/Walk/WalkPView.cs b/src/AcDream.App/Rendering/Walk/WalkPView.cs
new file mode 100644
index 00000000..3e5f7984
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/WalkPView.cs
@@ -0,0 +1,448 @@
+using System.Numerics;
+
+namespace AcDream.App.Rendering.Walk;
+
+///
+/// Campaign FW1 — retail PView's interior flood, transcribed from the
+/// flood-read appendix (docs/research/2026-08-30-fw-flood-pseudocode-appendix.md)
+/// plus the raw bodies re-read for AddToCell @0x005a4d90, AdjustDrawList
+/// @0x005a4e90, AdjustCellPlace @0x005a5010, FixCellList @0x005a5250,
+/// AdjustCellView @0x005a5770, and ConstructView @0x005a57b0.
+///
+/// PORTAL-FLAG CONVENTION (resolved 2026-08-30 — the PDB names mislead):
+/// inflag (+4) == 1 marks a portal whose polygon FACES the viewer as
+/// a visible surface (it feeds max_indist); inflag == 0 with
+/// seen (+0) == 1 marks an OPENING on the viewer's look-through side
+/// (InitCell's side == portal_side arm — the same side condition
+/// ConstructView(CBldPortal) REQUIRES for building look-ins).
+/// ClipPortals' traversal gate seen != 0 && inflag != 1
+/// therefore selects exactly the armed openings; the entry portal is forced
+/// {seen=1, inflag=1} so the flood never walks back out of it. Transcribe;
+/// do not "fix" the names.
+///
+public sealed class WalkPView
+{
+ private static int _masterTimestamp;
+
+ private readonly struct TodoEntry(WalkCell cell, float dist)
+ {
+ public readonly WalkCell Cell = cell;
+ public readonly float Dist = dist;
+ }
+
+ public readonly WalkPortalView OutsideView = new();
+ public readonly List CellDrawList = new();
+ private readonly List _todo = new();
+ private readonly WalkPortalView _otherPortalScratch = new(); // retail's static temp_view
+ private Vector2[] _activeViewVerts = new Vector2[32];
+ private int _activeViewVertCount;
+ private readonly WalkScreenPoint[] _projectScratch = new WalkScreenPoint[64];
+ private readonly WalkScreenPoint[] _clipScratch = new WalkScreenPoint[64];
+
+ /// Retail PView.draw_landscape: exit portals raise
+ /// only when set.
+ public bool DrawLandscape = true;
+
+ /// Retail Render::PortalList as ClipPortals installs it —
+ /// the last processed cell's top view (consumed by the landscape draw).
+ public WalkPortalView? PortalList { get; private set; }
+
+ // ------------------------------------------------------------------
+ // ConstructView (CEnvCell overload) @0x005a57b0 — the flood.
+ // ------------------------------------------------------------------
+ public void ConstructView(WalkCell seed, int throughPortalIndex, IWalkFrameContext ctx)
+ {
+ OutsideView.ResetForPush(); // outside_view.view_count = 0 (counters only)
+ _masterTimestamp++;
+ _todo.Clear();
+ CellDrawList.Clear();
+ InitCell(seed, throughPortalIndex, ctx);
+ InsCellTodoList(seed, 0f);
+ while (_todo.Count > 0)
+ {
+ WalkCell cell = _todo[^1].Cell;
+ _todo.RemoveAt(_todo.Count - 1);
+ CellDrawList.Add(cell);
+ cell.TopView.CellViewDone = true;
+ if (ClipPortals(cell, 0, ctx))
+ AddViewToPortals(cell, ctx);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // InitCell @0x005a4b70 (Ghidra-verified raw re-read 2026-08-30).
+ // ------------------------------------------------------------------
+ public bool InitCell(WalkCell cell, int entryPortalIndex, IWalkFrameContext ctx)
+ {
+ WalkPortalView top = cell.TopView;
+ if (top.ViewCount == 0) return false;
+
+ Vector3 viewpoint = ctx.ViewpointIn(cell);
+ top.CellViewDone = false;
+ top.ViewTimestamp = _masterTimestamp;
+ if (top.PortalFlags.Length < cell.Portals.Length)
+ top.PortalFlags = new WalkPortalFlags[cell.Portals.Length];
+
+ float maxDistSquared = 0f;
+ bool anyLookThrough = false; // retail var_4 (uninitialized there; benign — init false)
+
+ for (int i = 0; i < cell.Portals.Length; i++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[i];
+ WalkPolygon poly = cell.PortalPolygons[cell.Portals[i].PolygonIndex];
+
+ if (i == entryPortalIndex && !flags.InView)
+ {
+ // Entered-through portal: forced facing + armed — never re-traversed.
+ flags.InView = true;
+ flags.Seen = true;
+ }
+ else
+ {
+ flags.Seen = false;
+ float d = Vector3.Dot(poly.Plane.Normal, viewpoint) + poly.Plane.D;
+ if (d <= WalkVisibilityMath.Epsilon && d >= -WalkVisibilityMath.Epsilon)
+ {
+ flags.InView = false; // IN_PLANE: neither surface nor opening
+ anyLookThrough = true;
+ }
+ else
+ {
+ int side = d > WalkVisibilityMath.Epsilon ? 0 : 1;
+ if (side == cell.Portals[i].PortalSide)
+ {
+ flags.InView = false; // viewer on the look-through side (opening)
+ anyLookThrough = true;
+ }
+ else
+ {
+ flags.InView = true; // portal polygon faces the viewer
+ }
+ }
+ }
+
+ if (flags.InView)
+ {
+ foreach (Vector3 v in poly.Vertices)
+ {
+ float dx = viewpoint.X - v.X;
+ float dy = viewpoint.Y - v.Y;
+ float dz = viewpoint.Z - v.Z;
+ float d2 = dx * dx + dy * dy + dz * dz;
+ if (d2 > maxDistSquared) maxDistSquared = d2;
+ }
+ }
+ }
+ top.MaxInDistSquared = maxDistSquared;
+
+ // Fixup: arm every opening (retail runs this once per view with a
+ // vestigial set_view per pass; the flag writes are idempotent).
+ if (anyLookThrough && top.ViewCount > 0)
+ {
+ for (int j = 0; j < cell.Portals.Length; j++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[j];
+ if (!flags.InView && !flags.Seen) flags.Seen = true;
+ }
+ }
+
+ top.UpdateCount = top.ViewCount;
+ return true;
+ }
+
+ // ------------------------------------------------------------------
+ // InsCellTodoList @0x005a4f50 — descending by dist from index 0;
+ // END = nearest; pop-from-END = nearest-first (the Ghidra-corrected
+ // polarity: the sink stops under the first STRICTLY greater key).
+ // ------------------------------------------------------------------
+ public void InsCellTodoList(WalkCell cell, float dist)
+ {
+ int pos = _todo.Count;
+ while (pos > 0 && !(dist < _todo[pos - 1].Dist))
+ pos--;
+ _todo.Insert(pos, new TodoEntry(cell, dist));
+ }
+
+ // ------------------------------------------------------------------
+ // ClipPortals @0x005a5520.
+ // ------------------------------------------------------------------
+ public bool ClipPortals(WalkCell cell, int startView, IWalkFrameContext ctx)
+ {
+ WalkPortalView top = cell.TopView;
+ PortalList = top; // set BEFORE any early-out (retail)
+ if (cell.Portals.Length <= 0) return false;
+
+ // Pass 1: which portals are live; resolve+cache neighbors.
+ bool anyLive = false;
+ for (int j = 0; j < cell.Portals.Length; j++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[j];
+ if (!flags.Seen || flags.InView) continue;
+ ref WalkCellPortal portal = ref cell.Portals[j];
+ if (cell.CachedNeighbors[j] is null && portal.OtherCellId != 0xFFFFFFFFu)
+ {
+ cell.CachedNeighbors[j] = ctx.GetVisible(portal.OtherCellId);
+ if (cell.CachedNeighbors[j] is null) continue; // not loaded: silently dead
+ }
+ anyLive = true;
+ }
+ if (!anyLive) return false;
+
+ // Pass 2: clip every live portal against each view in the window.
+ for (int i = startView; i < top.ViewCount; i++)
+ {
+ SetView(top, i);
+ for (int j = 0; j < cell.Portals.Length; j++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[j];
+ if (!flags.Seen || flags.InView) continue;
+ ref WalkCellPortal portal = ref cell.Portals[j];
+ int n = GetClip(
+ cell, portal.PortalSide,
+ cell.PortalPolygons[portal.PolygonIndex],
+ doClip: true, ctx, _clipScratch);
+ if (n == 0) continue;
+
+ if (portal.OtherCellId == 0xFFFFFFFFu)
+ {
+ if (DrawLandscape)
+ {
+ if (ctx.ClipLandscape)
+ WalkCopyView.Append(
+ OutsideView, _clipScratch.AsSpan(0, n),
+ ctx.Rays, ctx.WorldViewpoint);
+ else
+ WalkCopyView.AppendFullViewportQuad(
+ OutsideView, ctx.Rays, ctx.WorldViewpoint,
+ ctx.ViewportWidth, ctx.ViewportHeight);
+ }
+ }
+ else if (cell.CachedNeighbors[j] is WalkCell neighbor)
+ {
+ if (!portal.ExactMatch && portal.OtherPortalId >= 0)
+ {
+ n = OtherPortalClip(cell, j, n, ctx);
+ SetView(top, i); // restore after the far-frame excursion
+ if (n == 0) continue;
+ }
+ if (neighbor.NumView != 0)
+ WalkCopyView.Append(
+ neighbor.TopView, _clipScratch.AsSpan(0, n),
+ ctx.Rays, ctx.WorldViewpoint);
+ }
+ }
+ }
+ return true;
+ }
+
+ // ------------------------------------------------------------------
+ // OtherPortalClip @0x005a5400 — the double clip for non-exact_match
+ // portals: snapshot the near-clipped poly as a temp view, then re-clip
+ // against the FAR cell's own portal polygon with INVERTED sidedness
+ // (a literal == 0 test, not an XOR).
+ // ------------------------------------------------------------------
+ private int OtherPortalClip(
+ WalkCell cell, int portalIndex, int count, IWalkFrameContext ctx)
+ {
+ _otherPortalScratch.ResetForPush();
+ if (!WalkCopyView.Append(
+ _otherPortalScratch, _clipScratch.AsSpan(0, count),
+ ctx.Rays, ctx.WorldViewpoint))
+ return 0;
+ ref WalkCellPortal portal = ref cell.Portals[portalIndex];
+ WalkCell far = cell.CachedNeighbors[portalIndex]
+ ?? throw new InvalidOperationException(
+ "OtherPortalClip requires a resolved neighbor (ClipPortals pass 1 contract).");
+ ref WalkCellPortal farPortal = ref far.Portals[portal.OtherPortalId];
+ SetView(_otherPortalScratch, 0);
+ return GetClip(
+ far, farPortal.PortalSide == 0 ? 1 : 0,
+ far.PortalPolygons[farPortal.PolygonIndex],
+ doClip: true, ctx, _clipScratch);
+ }
+
+ // ------------------------------------------------------------------
+ // AddViewToPortals @0x005a52d0.
+ // ------------------------------------------------------------------
+ public void AddViewToPortals(WalkCell cell, IWalkFrameContext ctx)
+ {
+ for (int j = 0; j < cell.Portals.Length; j++)
+ {
+ ref WalkCellPortal portal = ref cell.Portals[j];
+ WalkCell? neighbor = cell.CachedNeighbors[j];
+ ref WalkPortalFlags flags = ref cell.TopView.PortalFlags[j];
+ if (neighbor is null || flags.InView || !flags.Seen || neighbor.NumView == 0)
+ continue;
+ WalkPortalView neighborTop = neighbor.TopView;
+ if (neighborTop.ViewCount == 0) continue;
+
+ if (neighborTop.UpdateCount == 0)
+ {
+ // First touch this flood: schedule the neighbor.
+ if (InitCell(neighbor, ToEntryIndex(portal.OtherPortalId), ctx))
+ InsCellTodoList(neighbor, neighborTop.MaxInDistSquared);
+ }
+ else if (neighborTop.UpdateCount != neighborTop.ViewCount)
+ {
+ // Duplicate reach with NEW views since last processed.
+ AddToCell(neighbor, ToEntryIndex(portal.OtherPortalId));
+ if (neighborTop.CellViewDone)
+ FixCellList(neighbor, cell, ctx);
+ neighborTop.UpdateCount = neighborTop.ViewCount; // fresh re-read after recursion
+ }
+ else
+ {
+ continue; // nothing new; NO SetOtherSeen either
+ }
+
+ if (portal.OtherPortalId >= 0) // full-width signed test (−1 sentinel skips)
+ SetOtherSeen(cell, j);
+ }
+ }
+
+ /// Retail passes other_portal_id as a ZERO-EXTENDED 16-bit read:
+ /// −1 becomes 0xFFFF, InitCell/AddToCell's never-matching sentinel.
+ private static int ToEntryIndex(int otherPortalId)
+ => otherPortalId < 0 ? 0xFFFF : otherPortalId;
+
+ // ------------------------------------------------------------------
+ // AddToCell @0x005a4d90 — the duplicate-reach flag refresh over the
+ // NEW views window only.
+ // ------------------------------------------------------------------
+ public void AddToCell(WalkCell cell, int entryPortalIndex)
+ {
+ WalkPortalView top = cell.TopView;
+ for (int i = top.UpdateCount; i < top.ViewCount; i++)
+ {
+ // retail set_view(top.view, i) here is side-effect-only (vestigial)
+ for (int j = 0; j < cell.Portals.Length; j++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[j];
+ if (j == entryPortalIndex && !flags.InView) flags.InView = true;
+ if (!flags.InView && !flags.Seen) flags.Seen = true;
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // SetOtherSeen @0x005a4e30: fill the neighbor's backlink and arm its
+ // back-portal when that portal faces the neighbor's viewer.
+ // ------------------------------------------------------------------
+ public void SetOtherSeen(WalkCell cell, int portalIndex)
+ {
+ WalkCell? neighbor = cell.CachedNeighbors[portalIndex];
+ if (neighbor is null) return;
+ int backIndex = cell.Portals[portalIndex].OtherPortalId;
+ ref WalkCellPortal backPortal = ref neighbor.Portals[backIndex];
+ if (neighbor.CachedNeighbors[backIndex] is null)
+ neighbor.CachedNeighbors[backIndex] = cell;
+ ref WalkPortalFlags backFlags = ref neighbor.TopView.PortalFlags[backIndex];
+ if (backFlags.InView) backFlags.Seen = true;
+ }
+
+ // ------------------------------------------------------------------
+ // FixCellList @0x005a5250 = AdjustCellPlace + AdjustCellView.
+ // ------------------------------------------------------------------
+ public void FixCellList(WalkCell moved, WalkCell reachedThrough, IWalkFrameContext ctx)
+ {
+ AdjustCellPlace(moved, reachedThrough);
+ AdjustCellView(moved, ctx);
+ }
+
+ // AdjustCellPlace @0x005a5010: re-place, then recurse through the
+ // reaching cell's armed facing portals.
+ private void AdjustCellPlace(WalkCell moved, WalkCell reachedThrough)
+ {
+ WalkPortalView top = reachedThrough.TopView;
+ if (!AdjustDrawList(moved, reachedThrough)) return;
+ for (int i = 0; i < reachedThrough.Portals.Length; i++)
+ {
+ ref WalkPortalFlags flags = ref top.PortalFlags[i];
+ if (flags.Seen && flags.InView && reachedThrough.CachedNeighbors[i] is WalkCell next)
+ AdjustCellPlace(reachedThrough, next);
+ }
+ }
+
+ // AdjustDrawList @0x005a4e90: if `moved` appears in the draw list BEFORE
+ // `reachedThrough`, insert `reachedThrough` at `moved`'s slot (shifting
+ // the range up) so the walk-from-the-end draws `reachedThrough` earlier
+ // (farther). Appends `reachedThrough` when absent. Returns true when a
+ // move happened.
+ private bool AdjustDrawList(WalkCell moved, WalkCell reachedThrough)
+ {
+ for (int i = 0; i < CellDrawList.Count; i++)
+ {
+ uint id = CellDrawList[i].CellId;
+ if (id == reachedThrough.CellId) break; // already earlier: nothing to do
+ if (id != moved.CellId) continue;
+
+ int at = i;
+ while (at < CellDrawList.Count && CellDrawList[at].CellId != reachedThrough.CellId)
+ at++;
+ if (at == CellDrawList.Count)
+ CellDrawList.Add(reachedThrough); // conceptual append (grown below by the shift)
+ for (int k = at; k > i; k--)
+ CellDrawList[k] = CellDrawList[k - 1];
+ CellDrawList[i] = reachedThrough;
+ return true;
+ }
+ return false;
+ }
+
+ // AdjustCellView @0x005a5770: incremental re-flood over the new-views
+ // window only (the update_count watermark).
+ private void AdjustCellView(WalkCell cell, IWalkFrameContext ctx)
+ {
+ if (ClipPortals(cell, cell.TopView.UpdateCount, ctx))
+ AddViewToPortals(cell, ctx);
+ }
+
+ // ------------------------------------------------------------------
+ // set_view @0x0054d0e0 (the slice GetClip consumes: the installed
+ // view's screen vertices).
+ // ------------------------------------------------------------------
+ public void SetView(WalkPortalView portalView, int polyIndex)
+ {
+ WalkViewPoly poly = portalView.View.Polys[polyIndex];
+ if (_activeViewVerts.Length < poly.VertexCount)
+ _activeViewVerts = new Vector2[poly.VertexCount];
+ for (int k = 0; k < poly.VertexCount; k++)
+ _activeViewVerts[k] = portalView.View.Vertices[poly.VertexIndex + k].Point;
+ _activeViewVertCount = poly.VertexCount;
+ }
+
+ // ------------------------------------------------------------------
+ // PView::GetClip @0x005a4320: project, order by sidedness, optionally
+ // clip against the installed view. Returns the surviving count (0 on
+ // full rejection — the caller's pre-zero contract).
+ // ------------------------------------------------------------------
+ public int GetClip(
+ WalkCell cell, int side, WalkPolygon polygon, bool doClip,
+ IWalkFrameContext ctx, Span output)
+ {
+ int n = polygon.Vertices.Length;
+ Matrix4x4 objectToClip = ctx.ObjectToClip(cell);
+ for (int i = 0; i < n; i++)
+ {
+ _projectScratch[i] = WalkScreenClip.TransformToScreen(
+ polygon.Vertices[i], objectToClip, ctx.ViewportWidth, ctx.ViewportHeight);
+ }
+ // Sidedness != POSITIVE reverses the winding (IN_PLANE takes the
+ // reversed branch too, though flood callers never pass it).
+ if (side != 0)
+ {
+ for (int i = 0; i < n / 2; i++)
+ (_projectScratch[i], _projectScratch[n - 1 - i])
+ = (_projectScratch[n - 1 - i], _projectScratch[i]);
+ }
+ if (!doClip)
+ {
+ _projectScratch.AsSpan(0, n).CopyTo(output);
+ return n;
+ }
+ return WalkScreenClip.ClipAgainstView(
+ _projectScratch.AsSpan(0, n),
+ _activeViewVerts.AsSpan(0, _activeViewVertCount),
+ output);
+ }
+}
diff --git a/src/AcDream.App/Rendering/Walk/WalkWorld.cs b/src/AcDream.App/Rendering/Walk/WalkWorld.cs
new file mode 100644
index 00000000..84a30759
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/WalkWorld.cs
@@ -0,0 +1,89 @@
+using System.Numerics;
+
+namespace AcDream.App.Rendering.Walk;
+
+/// A cell-local portal polygon with its plane (retail
+/// CPolygon: vertices + plane @+0x20).
+public sealed class WalkPolygon
+{
+ public Vector3[] Vertices = [];
+ public WalkPlane Plane;
+}
+
+/// Retail CCellPortal (stride 0x18). The exit-to-landscape
+/// sentinel is OtherCellId == 0xFFFFFFFF; OtherPortalId == -1
+/// means "no reciprocal".
+public struct WalkCellPortal
+{
+ public uint OtherCellId;
+ public int PolygonIndex;
+ public int PortalSide;
+ public int OtherPortalId;
+ public bool ExactMatch;
+}
+
+///
+/// The flood's cell model (retail CEnvCell as the walk sees it).
+/// Retail stores the walk state ON the cell (num_view / portal_view stack /
+/// cached neighbor pointers); this port keeps the same placement so the
+/// transcription stays line-by-line — production adapters construct these
+/// from the committed cell registry (FW3 wiring).
+///
+public sealed class WalkCell
+{
+ public uint CellId;
+ public WalkCellPortal[] Portals = [];
+ public WalkPolygon[] PortalPolygons = [];
+ public uint[] StabList = [];
+
+ // ---- walk state (retail: fields on CEnvCell) ----
+ public int NumView;
+ public readonly List PortalViews = new();
+ public WalkCell?[] CachedNeighbors = [];
+
+ public WalkPortalView TopView => PortalViews[NumView - 1];
+
+ /// CEnvCell::curr_view_push @0x005a5090: push one
+ /// view-recursion level (lazy slot, exactly three counters reset).
+ public void PushView()
+ {
+ while (PortalViews.Count <= NumView)
+ PortalViews.Add(new WalkPortalView());
+ PortalViews[NumView].ResetForPush();
+ NumView++;
+ if (CachedNeighbors.Length < Portals.Length)
+ CachedNeighbors = new WalkCell?[Portals.Length];
+ }
+
+ public void PopView() => NumView--;
+}
+
+///
+/// The per-frame context retail keeps in globals (Render::FrameCurrent
+/// after positionPush(3, cell.pos), the projection state, the visible
+/// cell registry, and the cliplandscape toggle — .data default 1).
+///
+public interface IWalkFrameContext
+{
+ /// The eye position in the cell's local frame (retail: the
+ /// pushed frame's viewer.viewpoint).
+ Vector3 ViewpointIn(WalkCell cell);
+
+ /// Object(cell-local)→clip matrix for projecting the cell's
+ /// portal polygons (retail: the pushed frame composed with
+ /// WorldToView·ViewToClip).
+ Matrix4x4 ObjectToClip(WalkCell cell);
+
+ /// CEnvCell::GetVisible: the committed/visible cell
+ /// registry. Returning null skips the portal silently (retail behavior —
+ /// but implementations should count the miss for the fail-loud rule).
+ WalkCell? GetVisible(uint cellId);
+
+ IWalkRayCaster Rays { get; }
+ Vector3 WorldViewpoint { get; }
+ float ViewportWidth { get; }
+ float ViewportHeight { get; }
+
+ /// Retail global cliplandscape (.data @0x00820f4c = 1).
+ bool ClipLandscape => true;
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkPViewFloodTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkPViewFloodTests.cs
new file mode 100644
index 00000000..18f4e79f
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkPViewFloodTests.cs
@@ -0,0 +1,192 @@
+using System.Numerics;
+using AcDream.App.Rendering.Walk;
+
+namespace AcDream.App.Tests.Rendering.Walk;
+
+public sealed class WalkPViewFloodTests
+{
+ private sealed class TestContext : IWalkFrameContext
+ {
+ private sealed class Caster : IWalkRayCaster
+ {
+ public Vector3 RayThrough(float screenX, float screenY)
+ => new(screenX, screenY, 100f);
+ }
+
+ public readonly Dictionary Cells = new();
+ private readonly Matrix4x4 _viewProj;
+
+ 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;
+ }
+
+ // All test cells sit at identity transforms: cell-local == world.
+ 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;
+ }
+
+ private static WalkPolygon Quad(float z, float half = 0.5f, bool facingViewer = true) => new()
+ {
+ Vertices =
+ [
+ new Vector3(-half, -half, z), new Vector3(half, -half, z),
+ new Vector3(half, half, z), new Vector3(-half, half, z),
+ ],
+ // Plane through the quad: for z=-2 facing +z, N=(0,0,1), D=2 (eye at
+ // origin sits on the POSITIVE side: d = +2).
+ Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
+ };
+
+ private static WalkCell Cell(
+ TestContext ctx, uint id, params (WalkCellPortal Portal, WalkPolygon Polygon)[] portals)
+ {
+ var cell = new WalkCell
+ {
+ CellId = id,
+ Portals = portals.Select(p => p.Portal).ToArray(),
+ PortalPolygons = portals.Select(p => p.Polygon).ToArray(),
+ };
+ cell.PushView(); // add_views/stab-list stand-in: one pushed slot
+ ctx.Cells[id] = cell;
+ return cell;
+ }
+
+ private static WalkPView SeedAndFlood(TestContext ctx, WalkCell seed)
+ {
+ var pview = new WalkPView();
+ WalkCopyView.AppendFullViewportQuad(
+ seed.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
+ pview.ConstructView(seed, 0xFFFF, ctx);
+ return pview;
+ }
+
+ [Fact]
+ public void Flood_traverses_a_look_through_portal_into_the_neighbor()
+ {
+ var ctx = new TestContext();
+ // Eye on the positive side of the portal plane (d=+2 → side 0);
+ // PortalSide=0 → side == PortalSide → an OPENING (look-through).
+ WalkCell seed = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+ WalkCell neighbor = Cell(ctx, 0x101,
+ (new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
+ Quad(-2f)));
+
+ WalkPView pview = SeedAndFlood(ctx, seed);
+
+ Assert.Equal(new[] { 0x100u, 0x101u }, pview.CellDrawList.Select(c => c.CellId));
+ Assert.Equal(1, neighbor.TopView.ViewCount); // the clipped view arrived
+ Assert.True(neighbor.TopView.CellViewDone); // and the neighbor was processed
+ }
+
+ [Fact]
+ public void Facing_portal_is_not_traversed_but_feeds_the_distance_key()
+ {
+ var ctx = new TestContext();
+ // PortalSide=1 while the eye computes side 0 → the portal FACES the
+ // viewer (a visible surface, not an opening).
+ WalkCell seed = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
+ Quad(-2f)));
+ Cell(ctx, 0x101,
+ (new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+
+ WalkPView pview = SeedAndFlood(ctx, seed);
+
+ Assert.Equal(new[] { 0x100u }, pview.CellDrawList.Select(c => c.CellId));
+ // max_indist = squared distance to the farthest facing-portal vertex:
+ // (±0.5, ±0.5, −2) from the origin → 0.25 + 0.25 + 4.
+ Assert.Equal(4.5f, seed.TopView.MaxInDistSquared, 3);
+ }
+
+ [Fact]
+ public void Exit_portal_raises_the_outside_view_only_when_landscape_is_drawn()
+ {
+ var ctx = new TestContext();
+ WalkCell seed = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 0, OtherPortalId = -1 },
+ Quad(-2f)));
+
+ WalkPView pview = SeedAndFlood(ctx, seed);
+ Assert.Equal(1, pview.OutsideView.ViewCount);
+
+ // draw_landscape == 0 discards exit views entirely.
+ seed.PopView();
+ seed.PushView();
+ var noLandscape = new WalkPView { DrawLandscape = false };
+ WalkCopyView.AppendFullViewportQuad(
+ seed.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
+ noLandscape.ConstructView(seed, 0xFFFF, ctx);
+ Assert.Equal(0, noLandscape.OutsideView.ViewCount);
+ }
+
+ [Fact]
+ public void Flood_never_walks_back_through_the_entry_portal()
+ {
+ var ctx = new TestContext();
+ // Two cells looking into each other; both sides traversable by
+ // geometry. Without the entry-portal force, the flood would ping-pong.
+ WalkCell a = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+ WalkCell b = Cell(ctx, 0x101,
+ (new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+
+ WalkPView pview = SeedAndFlood(ctx, a);
+
+ Assert.Equal(2, pview.CellDrawList.Count);
+ Assert.Equal(2, pview.CellDrawList.Select(c => c.CellId).Distinct().Count());
+ }
+
+ [Fact]
+ public void Deeper_chain_floods_in_nearest_first_pop_order()
+ {
+ var ctx = new TestContext();
+ // seed → mid (portal at z=-2) → far (portal at z=-4): the draw list
+ // appends in pop order (nearest first), so the end-first draw walk
+ // is far-to-near.
+ WalkCell seed = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+ WalkCell mid = Cell(ctx, 0x101,
+ (new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
+ Quad(-2f)),
+ (new WalkCellPortal { OtherCellId = 0x102, PolygonIndex = 1, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-4f, half: 0.4f)));
+ WalkCell far = Cell(ctx, 0x102,
+ (new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 1 },
+ Quad(-4f, half: 0.4f)));
+
+ WalkPView pview = SeedAndFlood(ctx, seed);
+
+ Assert.Equal(
+ new[] { 0x100u, 0x101u, 0x102u },
+ pview.CellDrawList.Select(c => c.CellId));
+ Assert.Equal(1, far.TopView.ViewCount);
+ }
+
+ [Fact]
+ public void Unloaded_neighbor_is_silently_skipped()
+ {
+ var ctx = new TestContext();
+ WalkCell seed = Cell(ctx, 0x100,
+ (new WalkCellPortal { OtherCellId = 0x0DEAD, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
+ Quad(-2f)));
+
+ WalkPView pview = SeedAndFlood(ctx, seed);
+
+ Assert.Equal(new[] { 0x100u }, pview.CellDrawList.Select(c => c.CellId));
+ }
+}