WalkBuildingPortals transcribes the portal-only drawing-BSP walk (BSPTREE/BSPNODE::build_draw_portals_only @0x00539860/@0x0053c100, BSPPORTAL::portal_draw_portals_only @0x0053d870 - opposite-child-first = far-to-near emission, IN_PLANE PORT arm emits nothing), PView::DrawPortal @0x005a5ab0 (stab add/remove_views around the look-in, DrawCells on pass-2 success), and the CBldPortal PView::ConstructView overload @0x005a59a0 (side must EQUAL the authored portal_side, clip survival, Visible destination, punch on pass 1, flood recursion on pass 2). Punch surfaces via the pass sink for FW1 conformance; the depth-fan submission itself is FW2. Six tests: BSP emission order both viewer sides, in-plane suppression, pass-1 punch-no-flood, pass-2 flood + DC event, sidedness rejection, unloaded destination skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
259 lines
9.9 KiB
C#
259 lines
9.9 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.App.Rendering.Walk;
|
|
|
|
/// <summary>Retail <c>CBldPortal</c>: one building portal — the authored
|
|
/// side gate, the destination interior cell, its reciprocal portal index,
|
|
/// and the stab list of cells whose view stacks are pushed around the
|
|
/// look-in (<c>PView::add_views</c>/<c>remove_views</c>).</summary>
|
|
public struct WalkBldPortal
|
|
{
|
|
public int PortalSide;
|
|
public uint OtherCellId;
|
|
public int OtherPortalId;
|
|
public bool ExactMatch;
|
|
public uint[] StabList;
|
|
}
|
|
|
|
/// <summary>One <c>CPortalPoly</c> reference on a PORT BSP node: the index
|
|
/// into the building's portal list plus the portal polygon (building-local).</summary>
|
|
public struct WalkPortalRef
|
|
{
|
|
public int PortalIndex;
|
|
public WalkPolygon Polygon;
|
|
}
|
|
|
|
/// <summary>A node of the building's drawing BSP as the portal-only walk
|
|
/// sees it (retail tags: 'FAIL' leaf = stop, 'PORT' = portal node with
|
|
/// in_portals, anything else = plain splitting node).</summary>
|
|
public sealed class WalkBspNode
|
|
{
|
|
public WalkPlane SplittingPlane;
|
|
public WalkBspNode? PosNode;
|
|
public WalkBspNode? NegNode;
|
|
public bool IsFail;
|
|
public WalkPortalRef[]? InPortals; // non-null = PORT node
|
|
|
|
public bool IsPortal => InPortals is not null;
|
|
}
|
|
|
|
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
|
|
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
|
|
public sealed class WalkBuilding
|
|
{
|
|
/// <summary><c>Position.objcell_id</c> — the id the oracle's BLD events
|
|
/// carry (CBuildingObj+0x4C).</summary>
|
|
public uint PositionCellId;
|
|
|
|
public WalkBldPortal[] Portals = [];
|
|
|
|
/// <summary>The drawing BSP of part 0's GfxObj (portal-only view).</summary>
|
|
public WalkBspNode? DrawingBsp;
|
|
|
|
/// <summary><c>part->gfxobj[deg_level] != 0</c> — a degraded-out slot
|
|
/// skips the whole building AFTER publishing the portal list.</summary>
|
|
public bool HasGeometry = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign FW1 — the building look-in machinery, ported from the first
|
|
/// decomp appendix report 3 (docs/research/2026-08-30-fw-walk-pseudocode-appendix.md):
|
|
/// <c>BSPTREE/BSPNODE::build_draw_portals_only</c> @0x00539860/@0x0053c100,
|
|
/// <c>BSPPORTAL::portal_draw_portals_only</c> @0x0053d870,
|
|
/// <c>PView::DrawPortal</c> @0x005a5ab0, and the CBldPortal
|
|
/// <c>PView::ConstructView</c> overload @0x005a59a0.
|
|
///
|
|
/// The invisible-panel primitive (<c>DrawPortalPolyInternal</c> @0x0059bc90
|
|
/// — punch far-Z / seal own-depth) is a GPU submission and belongs to FW2's
|
|
/// ordered stream; here it surfaces as the <see cref="IWalkPortalPassSink"/>
|
|
/// punch callback so FW1 conformance can observe when retail would draw it.
|
|
/// </summary>
|
|
public static class WalkBuildingPortals
|
|
{
|
|
/// <summary>What the portal passes report outward: punches (pass 1) and
|
|
/// look-in cell floods (pass 2, the oracle's <c>DC ov=…</c> events).</summary>
|
|
public interface IWalkPortalPassSink
|
|
{
|
|
/// <summary>Pass 1 drew the portal polygon as a far-Z punch
|
|
/// (<c>DrawPortalPolyInternal(poly, 1)</c>).</summary>
|
|
void OnPunch(WalkPolygon polygon);
|
|
|
|
/// <summary>Pass 2 completed a look-in flood and retail called
|
|
/// <c>PView::DrawCells</c> — the DC event with the flood's list.</summary>
|
|
void OnDrawCells(WalkPView pview);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>BSPTREE::build_draw_portals_only</c> @0x00539860 + the node/portal
|
|
/// walkers: dispatch the root, then walk plane-side ordered — the child
|
|
/// OPPOSITE the viewer first, so portals emit far-to-near. PORT nodes
|
|
/// emit every in_portal on the POSITIVE and NEGATIVE arms; the IN_PLANE
|
|
/// arm (|d| ≤ ε) visits the positive child and emits NOTHING.
|
|
/// </summary>
|
|
public static void BuildDrawPortalsOnly(
|
|
WalkBspNode? root, int pass, Vector3 viewpointInBuilding,
|
|
Action<WalkPortalRef, int> emitPortal)
|
|
{
|
|
if (root is null || root.IsFail) return;
|
|
Walk(root, pass, viewpointInBuilding, emitPortal);
|
|
}
|
|
|
|
private static void Walk(
|
|
WalkBspNode node, int pass, Vector3 viewpoint,
|
|
Action<WalkPortalRef, int> emitPortal)
|
|
{
|
|
while (true)
|
|
{
|
|
float d = Vector3.Dot(node.SplittingPlane.Normal, viewpoint) + node.SplittingPlane.D;
|
|
int side = d > WalkVisibilityMath.Epsilon ? 0
|
|
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
|
|
|
|
WalkBspNode? next;
|
|
if (node.IsPortal)
|
|
{
|
|
if (side == 0)
|
|
{
|
|
Visit(node.NegNode, pass, viewpoint, emitPortal);
|
|
foreach (WalkPortalRef portal in node.InPortals!)
|
|
emitPortal(portal, pass);
|
|
next = node.PosNode;
|
|
}
|
|
else if (side == 1)
|
|
{
|
|
Visit(node.PosNode, pass, viewpoint, emitPortal);
|
|
foreach (WalkPortalRef portal in node.InPortals!)
|
|
emitPortal(portal, pass);
|
|
next = node.NegNode;
|
|
}
|
|
else
|
|
{
|
|
Visit(node.PosNode, pass, viewpoint, emitPortal);
|
|
next = node.NegNode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (side == 0)
|
|
{
|
|
Visit(node.NegNode, pass, viewpoint, emitPortal);
|
|
next = node.PosNode;
|
|
}
|
|
else
|
|
{
|
|
Visit(node.PosNode, pass, viewpoint, emitPortal);
|
|
next = node.NegNode;
|
|
}
|
|
}
|
|
|
|
if (next is null || next.IsFail) return;
|
|
node = next; // retail's tail-continue
|
|
}
|
|
}
|
|
|
|
private static void Visit(
|
|
WalkBspNode? child, int pass, Vector3 viewpoint,
|
|
Action<WalkPortalRef, int> emitPortal)
|
|
{
|
|
if (child is null || child.IsFail) return;
|
|
Walk(child, pass, viewpoint, emitPortal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>PView::DrawPortal</c> @0x005a5ab0 for one emitted portal polygon:
|
|
/// resolve the CBldPortal, push view slots on its stab cells
|
|
/// (<c>add_views</c>), run the CBldPortal ConstructView, on pass 2
|
|
/// success run the look-in DrawCells, then pop the stab views. The
|
|
/// GPU-state backup/restore and the building-frame re-push are
|
|
/// submission concerns (FW2); the CPU state here is complete.
|
|
/// </summary>
|
|
public static bool DrawPortal(
|
|
WalkPView pview, WalkBuilding building, in WalkPortalRef portalRef,
|
|
int pass, IWalkBuildingFrameContext ctx, IWalkPortalPassSink sink)
|
|
{
|
|
ref readonly WalkBldPortal bldPortal = ref building.Portals[portalRef.PortalIndex];
|
|
AddViews(bldPortal.StabList, ctx);
|
|
bool ok = ConstructBuildingView(
|
|
pview, building, in bldPortal, portalRef.Polygon, pass, ctx, sink);
|
|
if (ok && pass != 1)
|
|
sink.OnDrawCells(pview);
|
|
RemoveViews(bldPortal.StabList, ctx);
|
|
return ok;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The CBldPortal <c>PView::ConstructView</c> overload @0x005a59a0:
|
|
/// the viewer's side of the portal plane must EQUAL the authored
|
|
/// portal_side (IN_PLANE within ±ε fails both gates); the polygon must
|
|
/// survive the active-view clip with ≥3 points; the destination cell
|
|
/// must be Visible; the clipped view is appended to its top slot. Pass 1
|
|
/// punches the polygon; pass ≠ 1 recurses into the interior flood.
|
|
/// </summary>
|
|
public static bool ConstructBuildingView(
|
|
WalkPView pview, WalkBuilding building, in WalkBldPortal bldPortal,
|
|
WalkPolygon polygon, int pass, IWalkBuildingFrameContext ctx,
|
|
IWalkPortalPassSink sink)
|
|
{
|
|
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
|
float d = Vector3.Dot(polygon.Plane.Normal, viewpoint) + polygon.Plane.D;
|
|
int side = d > WalkVisibilityMath.Epsilon ? 0
|
|
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
|
|
if (bldPortal.PortalSide != 0)
|
|
{
|
|
if (side != 1) return false;
|
|
}
|
|
else if (side != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<WalkScreenPoint> clipped = stackalloc WalkScreenPoint[64];
|
|
int n = ctx.ClipBuildingPolygon(building, polygon, side, clipped);
|
|
if (n == 0) return false;
|
|
|
|
WalkCell? cell = ctx.GetVisible(bldPortal.OtherCellId);
|
|
if (cell is null) return false;
|
|
if (!WalkCopyView.Append(
|
|
cell.TopView, clipped[..n], ctx.Rays, ctx.WorldViewpoint))
|
|
return false;
|
|
|
|
if (pass != 2)
|
|
sink.OnPunch(polygon); // DrawPortalPolyInternal(poly, pass == 1)
|
|
if (pass != 1)
|
|
pview.ConstructView(cell, ToEntryIndex(bldPortal.OtherPortalId), ctx.CellContext);
|
|
return true;
|
|
}
|
|
|
|
private static int ToEntryIndex(int otherPortalId)
|
|
=> otherPortalId < 0 ? 0xFFFF : otherPortalId;
|
|
|
|
private static void AddViews(uint[] stabList, IWalkBuildingFrameContext ctx)
|
|
{
|
|
foreach (uint id in stabList)
|
|
ctx.GetVisible(id)?.PushView();
|
|
}
|
|
|
|
private static void RemoveViews(uint[] stabList, IWalkBuildingFrameContext ctx)
|
|
{
|
|
foreach (uint id in stabList)
|
|
ctx.GetVisible(id)?.PopView();
|
|
}
|
|
}
|
|
|
|
/// <summary>The building half of the frame context: building-local
|
|
/// viewpoint and projection (retail pushes the building's object frame
|
|
/// before the portal pass), plus the shared cell context.</summary>
|
|
public interface IWalkBuildingFrameContext
|
|
{
|
|
Vector3 ViewpointInBuilding(WalkBuilding building);
|
|
|
|
/// <summary>Project + clip one building-local portal polygon against
|
|
/// the ACTIVE view (retail: GetClip with do_clip=1 in the building
|
|
/// frame). Returns the surviving count.</summary>
|
|
int ClipBuildingPolygon(
|
|
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output);
|
|
|
|
WalkCell? GetVisible(uint cellId);
|
|
IWalkRayCaster Rays { get; }
|
|
Vector3 WorldViewpoint { get; }
|
|
IWalkFrameContext CellContext { get; }
|
|
}
|