feat(render) Campaign FW1: port the view machinery (xformStart, polyClipFinish, copy_view)

WalkScreenClip ports PrimD3DRender::xformStart @0x0059b990 (homogeneous
viewport coords, y-flip, no divide) and ACRender::polyClipFinish
@0x006b6d00 (w>=cdstW plane then last-to-first edge passes, inside =
side<=0 homogeneous 2D cross, reverse-scan passes with original-winding
restore, <3 early-outs). WalkViews ports the view_type/portal_view_type
data model and Render::copy_view @0x0054dfc0 exactly: in-place divide,
the keep/last/stl/second pruning bookkeeping with all three closing
wrap checks, <3 reject leaving dest untouched, cap 31, pool-base reset
at view_count==0, retail fabs on copy, and edge planes
N=normalize(cross(ray[k+1],ray[k])), d=-dot(N,eye) behind an
IWalkRayCaster seam. Thirteen new tests; Walk namespace 85/85.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 09:50:03 +02:00
parent 368c480bc2
commit 11ca527fb9
4 changed files with 718 additions and 0 deletions

View file

@ -0,0 +1,176 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Retail <c>Vec2Dscreen</c>: homogeneous viewport coordinates as produced
/// by <c>PrimD3DRender::xformStart</c> @0x0059b990 — X/Y are viewport-scaled
/// but NOT perspective-divided (divide by W to get pixels), Z is raw clip z,
/// W is raw clip w. <c>copy_view</c> performs the divide;
/// <c>polyClipFinish</c> clips pre-divide homogeneously.
/// </summary>
public struct WalkScreenPoint
{
public float X, Y, Z, W;
public WalkScreenPoint(float x, float y, float z, float w)
{
X = x; Y = y; Z = z; W = w;
}
}
/// <summary>
/// Campaign FW1 — the screen-space projection/clip chain, ported from the
/// flood-read appendix (docs/research/2026-08-30-fw-flood-pseudocode-appendix.md,
/// report 3; Ghidra-arbitrated — BN's literal rendering inverts the edge
/// inside test and the w-clip plane).
/// </summary>
public static class WalkScreenClip
{
/// <summary>The w-clip plane constant <c>cdstW</c> (= retail F_EPSILON).</summary>
public const float MinW = WalkVisibilityMath.Epsilon;
/// <summary>
/// <c>PrimD3DRender::xformStart</c> @0x0059b990 (toScreen path): object
/// space → homogeneous viewport coordinates. x=(bw/2)(x_clip+w),
/// y=(bh/2)(wy_clip) — y flipped, origin top-left — z/w raw clip.
/// <paramref name="objectToClip"/> is the concatenated object→clip
/// matrix (row-vector convention, v * M).
/// </summary>
public static WalkScreenPoint TransformToScreen(
Vector3 point, in Matrix4x4 objectToClip, float viewportWidth, float viewportHeight)
{
Vector4 clip = Vector4.Transform(new Vector4(point, 1f), objectToClip);
return new WalkScreenPoint(
clip.X * viewportWidth * 0.5f + clip.W * viewportWidth * 0.5f,
clip.W * viewportHeight * 0.5f - clip.Y * viewportHeight * 0.5f,
clip.Z,
clip.W);
}
/// <summary>
/// <c>ACRender::polyClipFinish</c> @0x006b6d00: Sutherland-Hodgman clip
/// of a homogeneous screen polygon against the active view — first the
/// w ≥ <see cref="MinW"/> plane (only when some w is below it), then
/// every view edge, iterated LAST-to-FIRST as vertex pairs
/// (v[0], v[n1]), (v[n1], v[n2]), …, (v[1], v[0]). Edge INSIDE is
/// side ≤ 0 with the homogeneous 2D cross
/// side(p) = (p.x a.x·p.w)·ey (p.y a.y·p.w)·ex. Each pass scans
/// its input in REVERSE; the output keeps the ORIGINAL winding (retail's
/// pass-parity bookkeeping collapses to reversing per pass and
/// un-reversing at the end — this port appends reversed per pass and
/// restores at the end, observably identical). Returns the surviving
/// count, or 0 the moment any stage drops below 3 vertices — in which
/// case <paramref name="output"/> content is unspecified (retail never
/// writes the out count on that path; callers pre-zero it).
/// </summary>
public static int ClipAgainstView(
ReadOnlySpan<WalkScreenPoint> input,
ReadOnlySpan<Vector2> viewEdgeVertices,
Span<WalkScreenPoint> output)
{
// Working buffers sized for retail's ≤32-vertex contract plus clip growth.
Span<WalkScreenPoint> bufferA = stackalloc WalkScreenPoint[64];
Span<WalkScreenPoint> bufferB = stackalloc WalkScreenPoint[64];
Span<WalkScreenPoint> current = bufferA;
int count = input.Length;
input.CopyTo(current);
// Track how many reversing passes ran so the final copy can restore
// the original winding exactly as retail's parity dance does.
int reversals = 0;
// Pass 0: the w-plane, only when some vertex is below cdstW.
bool anyBelow = false;
for (int i = 0; i < count; i++)
if (current[i].W < MinW) { anyBelow = true; break; }
if (anyBelow)
{
count = ClipPassW(current[..count], bufferB);
if (count < 3) return 0;
Span<WalkScreenPoint> swap = current;
current = bufferB;
bufferB = swap;
reversals++;
}
// Edge passes: pairs (a, b) = (v[0], v[n-1]), (v[n-1], v[n-2]) … (v[1], v[0]).
int n = viewEdgeVertices.Length;
for (int e = n - 1; e >= 0; e--)
{
Vector2 a = viewEdgeVertices[e == n - 1 ? 0 : e + 1];
Vector2 b = viewEdgeVertices[e];
count = ClipPassEdge(current[..count], a, b, bufferB);
if (count < 3) return 0;
Span<WalkScreenPoint> swap = current;
current = bufferB;
bufferB = swap;
reversals++;
}
// Restore original winding: each pass reversed the order once.
if ((reversals & 1) != 0)
{
for (int i = 0; i < count; i++)
output[i] = current[count - 1 - i];
}
else
{
current[..count].CopyTo(output);
}
return count;
}
private static int ClipPassW(ReadOnlySpan<WalkScreenPoint> pts, Span<WalkScreenPoint> outPts)
{
int outCount = 0;
// Reverse traversal starting from the wrap pair (pts[0], pts[n-1]).
WalkScreenPoint prev = pts[0];
float sPrev = prev.W - MinW;
bool inPrev = sPrev >= 0f;
for (int i = pts.Length - 1; i >= 0; i--)
{
WalkScreenPoint cur = pts[i];
float s = cur.W - MinW;
bool inCur = s >= 0f;
if (inPrev != inCur)
outPts[outCount++] = Lerp(prev, cur, sPrev / (sPrev - s));
if (inCur)
outPts[outCount++] = cur;
prev = cur; sPrev = s; inPrev = inCur;
}
return outCount;
}
private static int ClipPassEdge(
ReadOnlySpan<WalkScreenPoint> pts, Vector2 a, Vector2 b, Span<WalkScreenPoint> outPts)
{
float ex = b.X - a.X;
float ey = b.Y - a.Y;
float Side(in WalkScreenPoint p) => (p.X - a.X * p.W) * ey - (p.Y - a.Y * p.W) * ex;
int outCount = 0;
WalkScreenPoint prev = pts[0];
float s0 = Side(prev);
float sPrev = s0;
bool inPrev = s0 <= 0f; // INSIDE = side <= 0 (Ghidra-verified)
for (int i = pts.Length - 1; i >= 0; i--)
{
WalkScreenPoint cur = pts[i];
float s = i != 0 ? Side(cur) : s0; // final pair reuses point 0's side
bool inCur = s <= 0f;
if (inPrev != inCur)
outPts[outCount++] = Lerp(prev, cur, sPrev / (sPrev - s));
if (inCur)
outPts[outCount++] = cur;
prev = cur; sPrev = s; inPrev = inCur;
}
return outCount;
}
private static WalkScreenPoint Lerp(in WalkScreenPoint p, in WalkScreenPoint q, float t)
=> new(
p.X + (q.X - p.X) * t,
p.Y + (q.Y - p.Y) * t,
p.Z + (q.Z - p.Z) * t,
p.W + (q.W - p.W) * t);
}