acdream/src/AcDream.App/Rendering/Walk/WalkPView.cs
Erik 0d6cd5c06e fix(render): FW4 slice 5 - straddling cell statics'' particles emit pre-clear
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>
2026-08-30 19:58:04 +02:00

462 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (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);
}
}