The cathedral falls'' true identity, pinned by the emitter/owner dumps
and the owner''s retail-vs-ACE oracle (retail renders them perfectly):
they are EnvCell STAB-LIST statics (interior-cell dat objects, ids
minted by InteriorEntityIdAllocator) whose geometry hangs out over the
lake. Our cell-statics route submitted every interior flood cell''s
static particle owners POST-clear ("walls own depth") - true for
content that stays inside, but a straddler''s outside half meets
seal/cleared depth and splats across the vista.
Retail''s mechanism is the outdoor shadow-cell draw: an object
overlapping outdoor landcells draws during LScape::draw (DrawBlock
pc:430056-430064, once per overlapped shadow cell), so its particles
submit in the landscape scope and drain at the pre-clear boundary
flush over true terrain depth; the cell''s own turn skips them
(drawn-once). This slice ports that rule for particles: after Collect,
the interior flood cells'' static records are classified by the shared
exit-plane straddle test (extracted as SphereStraddlesExitPlane from
the #118 dynamic split); straddlers'' owners submit in the pre-clear
closure and the post-replay cell-owners union subtracts them, so every
owner emits exactly once. Also adds the [walk-emit] probe dump that
falsified the unattached-route theory.
Hermetic 6,762/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
462 lines
20 KiB
C#
462 lines
20 KiB
C#
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 && 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 (AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame)
|
||
{
|
||
Console.WriteLine(
|
||
$"[walk-portal] init cell={cell.CellId:x8} i={i} "
|
||
+ $"dest={cell.Portals[i].OtherCellId:x8} d={d:F3} "
|
||
+ $"pside={cell.Portals[i].PortalSide} "
|
||
+ $"inview={(flags.InView ? 1 : 0)}");
|
||
}
|
||
}
|
||
|
||
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 (AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame)
|
||
{
|
||
Console.WriteLine(
|
||
$"[walk-portal] clip cell={cell.CellId:x8} view={i} j={j} "
|
||
+ $"dest={portal.OtherCellId:x8} n={n}");
|
||
}
|
||
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);
|
||
}
|
||
}
|