feat(render) Campaign FW1: port the PView interior flood

WalkPView transcribes ConstructView @0x005a57b0, InitCell @0x005a4b70,
InsCellTodoList @0x005a4f50 (nearest-first pop), ClipPortals @0x005a5520,
OtherPortalClip @0x005a5400 (inverted ==0 sidedness), AddViewToPortals
@0x005a52d0 (update_count watermark, duplicate-reach arm), AddToCell
@0x005a4d90, SetOtherSeen @0x005a4e30, FixCellList/AdjustCellPlace/
AdjustDrawList/AdjustCellView @0x005a5250/0x005a5010/0x005a4e90/
0x005a5770, plus set_view and PView::GetClip @0x005a4320 over the
WalkScreenClip chain. Resolves and documents the portal-flag convention
(inflag=1 = the portal FACES the viewer and feeds max_indist; inflag=0 +
armed seen = an opening — the same side condition building look-ins
require), which the PDB names obscure. WalkWorld supplies the cell model
and frame-context seam. Six synthetic-world flood tests pass on first
run: traversal, facing rejection with the exact distance key, exit-view
raising, entry-portal no-ping-pong, chain ordering, unloaded skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 09:58:28 +02:00
parent 11ca527fb9
commit 161ffaf4e9
3 changed files with 729 additions and 0 deletions

View file

@ -0,0 +1,448 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW1 — retail <c>PView</c>'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):
/// <c>inflag</c> (+4) == 1 marks a portal whose polygon FACES the viewer as
/// a visible surface (it feeds max_indist); <c>inflag</c> == 0 with
/// <c>seen</c> (+0) == 1 marks an OPENING on the viewer's look-through side
/// (InitCell's <c>side == portal_side</c> arm — the same side condition
/// <c>ConstructView(CBldPortal)</c> REQUIRES for building look-ins).
/// ClipPortals' traversal gate <c>seen != 0 &amp;&amp; inflag != 1</c>
/// 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.
/// </summary>
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<WalkCell> CellDrawList = new();
private readonly List<TodoEntry> _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];
/// <summary>Retail <c>PView.draw_landscape</c>: exit portals raise
/// <see cref="OutsideView"/> only when set.</summary>
public bool DrawLandscape = true;
/// <summary>Retail <c>Render::PortalList</c> as ClipPortals installs it —
/// the last processed cell's top view (consumed by the landscape draw).</summary>
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);
}
}
/// <summary>Retail passes other_portal_id as a ZERO-EXTENDED 16-bit read:
/// 1 becomes 0xFFFF, InitCell/AddToCell's never-matching sentinel.</summary>
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<WalkScreenPoint> 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);
}
}

View file

@ -0,0 +1,89 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>A cell-local portal polygon with its plane (retail
/// <c>CPolygon</c>: vertices + <c>plane</c> @+0x20).</summary>
public sealed class WalkPolygon
{
public Vector3[] Vertices = [];
public WalkPlane Plane;
}
/// <summary>Retail <c>CCellPortal</c> (stride 0x18). The exit-to-landscape
/// sentinel is <c>OtherCellId == 0xFFFFFFFF</c>; <c>OtherPortalId == -1</c>
/// means "no reciprocal".</summary>
public struct WalkCellPortal
{
public uint OtherCellId;
public int PolygonIndex;
public int PortalSide;
public int OtherPortalId;
public bool ExactMatch;
}
/// <summary>
/// The flood's cell model (retail <c>CEnvCell</c> 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).
/// </summary>
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<WalkPortalView> PortalViews = new();
public WalkCell?[] CachedNeighbors = [];
public WalkPortalView TopView => PortalViews[NumView - 1];
/// <summary><c>CEnvCell::curr_view_push</c> @0x005a5090: push one
/// view-recursion level (lazy slot, exactly three counters reset).</summary>
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--;
}
/// <summary>
/// The per-frame context retail keeps in globals (<c>Render::FrameCurrent</c>
/// after <c>positionPush(3, cell.pos)</c>, the projection state, the visible
/// cell registry, and the <c>cliplandscape</c> toggle — .data default 1).
/// </summary>
public interface IWalkFrameContext
{
/// <summary>The eye position in the cell's local frame (retail: the
/// pushed frame's <c>viewer.viewpoint</c>).</summary>
Vector3 ViewpointIn(WalkCell cell);
/// <summary>Object(cell-local)→clip matrix for projecting the cell's
/// portal polygons (retail: the pushed frame composed with
/// WorldToView·ViewToClip).</summary>
Matrix4x4 ObjectToClip(WalkCell cell);
/// <summary><c>CEnvCell::GetVisible</c>: the committed/visible cell
/// registry. Returning null skips the portal silently (retail behavior —
/// but implementations should count the miss for the fail-loud rule).</summary>
WalkCell? GetVisible(uint cellId);
IWalkRayCaster Rays { get; }
Vector3 WorldViewpoint { get; }
float ViewportWidth { get; }
float ViewportHeight { get; }
/// <summary>Retail global <c>cliplandscape</c> (.data @0x00820f4c = 1).</summary>
bool ClipLandscape => true;
}

View file

@ -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<uint, WalkCell> 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));
}
}