feat(render) Campaign FW1: flood decomp appendix + the visibility math port
The second decomp round (flood bookkeeping/propagation, view-clip support, landscape visibility) is archived Ghidra-arbitrated - it caught a load-bearing BN inversion (InsCellTodoList pops NEAREST-first, which is what makes the draw list far-to-near) and three more traps (the 192m elided constant, the min/max double positional swap, the copy_view cross order - the walk doc section 6 is corrected). WalkVisibilityMath ports get_pt_limit @0x0054b840, get_clip_height @0x0054cff0, corner/block_plane_check @0x0054b930/@0x0054d060, block_check @0x0054dc50, and viewconeCheck @0x0054c250 with retail boundary semantics (strict cull, inclusive partial, touch-out=Outside / touch-in=EntirelyInside) under 23 focused tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0926b55eaf
commit
368c480bc2
4 changed files with 1138 additions and 2 deletions
211
src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs
Normal file
211
src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW1 — retail's CPU visibility primitives, ported from the named
|
||||
/// 2013 decomp (extractions:
|
||||
/// docs/research/2026-08-30-fw-flood-pseudocode-appendix.md, report 4; all
|
||||
/// x87-ambiguous branches Ghidra-arbitrated). Two families:
|
||||
///
|
||||
/// 1. The landscape interval test — a grid corner's vertical column is
|
||||
/// classified against each active clip plane as a signed height
|
||||
/// (<c>Render::get_pt_limit</c> @0x0054b840), and a landblock's z-slab
|
||||
/// is tested against its four corner columns plane by plane
|
||||
/// (<c>corner_plane_check</c> @0x0054b930, <c>block_plane_check</c>
|
||||
/// @0x0054d060, <c>block_check</c> @0x0054dc50).
|
||||
/// 2. The sphere-vs-view test (<c>Render::viewconeCheck</c> @0x0054c250).
|
||||
///
|
||||
/// Both consume the same installed state: the CY near plane (built by
|
||||
/// <c>Render::update_viewpoint</c>) plus the active view's edge planes
|
||||
/// (installed by <c>Render::set_view</c>).
|
||||
/// </summary>
|
||||
public static class WalkVisibilityMath
|
||||
{
|
||||
/// <summary>Retail F_EPSILON (raw float 0x3951B717).</summary>
|
||||
public const float Epsilon = 0.000199999995f;
|
||||
|
||||
/// <summary>get_pt_limit sky cap: a clip height at or above this means
|
||||
/// the column holds nothing (up-normal arm) / everything (down-normal
|
||||
/// arm).</summary>
|
||||
public const float SkyHeight = 1000f;
|
||||
|
||||
/// <summary>Sentinel: the column is wholly inside the plane's positive
|
||||
/// half-space.</summary>
|
||||
public const float InsideColumn = 0f;
|
||||
|
||||
/// <summary>Sentinel: the column is wholly outside (static init 1000+1).</summary>
|
||||
public const float OutsideColumn = 1001f;
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::get_pt_limit</c> @0x0054b840: classify the vertical column
|
||||
/// at (x, y) against one plane. Returns <see cref="InsideColumn"/> /
|
||||
/// <see cref="OutsideColumn"/>, a positive h (inside only BELOW h —
|
||||
/// down-pointing normal), or a negative −h (inside only ABOVE h —
|
||||
/// up-pointing normal). Vertical planes (|N.z| ≤ ε) collapse to
|
||||
/// all-in/all-out via the plane side of (x, y, 0).
|
||||
/// </summary>
|
||||
public static float GetPointLimit(float x, float y, in WalkPlane plane)
|
||||
{
|
||||
Vector3 n = plane.Normal;
|
||||
if (n.Z > Epsilon)
|
||||
{
|
||||
float h = -((x * n.X + y * n.Y + plane.D) / n.Z);
|
||||
if (h >= SkyHeight) return OutsideColumn;
|
||||
return h > 0f ? -h : InsideColumn;
|
||||
}
|
||||
if (n.Z < -Epsilon)
|
||||
{
|
||||
float h = -((x * n.X + y * n.Y + plane.D) / n.Z);
|
||||
if (h <= 0f) return OutsideColumn;
|
||||
return h >= SkyHeight ? InsideColumn : h;
|
||||
}
|
||||
// Vertical plane: no z dependence; test point (x, y, 0), retail
|
||||
// Plane::which_side semantics (NEGATIVE = strictly below -epsilon).
|
||||
float d = x * n.X + y * n.Y + plane.D;
|
||||
return d < -Epsilon ? OutsideColumn : InsideColumn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::get_clip_height</c> @0x0054cff0: fill one corner's
|
||||
/// interval vector — bounds[0] vs the CY plane, bounds[1..edgeCount] vs
|
||||
/// each active edge plane (the loop is INCLUSIVE of edgeCount entries:
|
||||
/// edgeCount + 1 floats are written).
|
||||
/// </summary>
|
||||
public static void FillClipHeights(
|
||||
float x, float y, in WalkPlane cyPlane, ReadOnlySpan<WalkPlane> edgePlanes,
|
||||
Span<float> bounds)
|
||||
{
|
||||
bounds[0] = GetPointLimit(x, y, cyPlane);
|
||||
for (int i = 0; i < edgePlanes.Length; i++)
|
||||
bounds[i + 1] = GetPointLimit(x, y, edgePlanes[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::corner_plane_check</c> @0x0054b930: one corner's encoding
|
||||
/// vs the block z-slab [minZ, maxZ]. Boundary semantics are retail's:
|
||||
/// touching the clip height on the OUT side is Outside; on the IN side
|
||||
/// is EntirelyInside.
|
||||
/// </summary>
|
||||
public static WalkBoundingType CornerPlaneCheck(float bound, float minZ, float maxZ)
|
||||
{
|
||||
if (bound == OutsideColumn) return WalkBoundingType.Outside;
|
||||
if (bound != InsideColumn)
|
||||
{
|
||||
if (bound <= 0f)
|
||||
{
|
||||
// inside is z >= h, h = -bound
|
||||
float h = -bound;
|
||||
if (h > minZ)
|
||||
{
|
||||
if (maxZ <= h) return WalkBoundingType.Outside;
|
||||
return WalkBoundingType.PartiallyInside;
|
||||
}
|
||||
}
|
||||
else if (bound < maxZ)
|
||||
{
|
||||
// inside is z <= h, h = bound; the block top pokes above h
|
||||
if (bound <= minZ) return WalkBoundingType.Outside;
|
||||
return WalkBoundingType.PartiallyInside;
|
||||
}
|
||||
}
|
||||
return WalkBoundingType.EntirelyInside;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::block_plane_check</c> @0x0054d060: four corners vs one
|
||||
/// plane. Outside/EntirelyInside require unanimity; anything mixed is
|
||||
/// PartiallyInside.
|
||||
/// </summary>
|
||||
public static WalkBoundingType BlockPlaneCheck(
|
||||
float b1, float b2, float b3, float b4, float minZ, float maxZ)
|
||||
{
|
||||
WalkBoundingType c1 = CornerPlaneCheck(b1, minZ, maxZ);
|
||||
WalkBoundingType c2 = CornerPlaneCheck(b2, minZ, maxZ);
|
||||
WalkBoundingType c3 = CornerPlaneCheck(b3, minZ, maxZ);
|
||||
WalkBoundingType c4 = CornerPlaneCheck(b4, minZ, maxZ);
|
||||
if (c1 == WalkBoundingType.Outside)
|
||||
{
|
||||
if (c2 == WalkBoundingType.Outside
|
||||
&& c3 == WalkBoundingType.Outside
|
||||
&& c4 == WalkBoundingType.Outside)
|
||||
{
|
||||
return WalkBoundingType.Outside;
|
||||
}
|
||||
}
|
||||
else if (c1 == WalkBoundingType.EntirelyInside
|
||||
&& c2 == WalkBoundingType.EntirelyInside
|
||||
&& c3 == WalkBoundingType.EntirelyInside
|
||||
&& c4 == WalkBoundingType.EntirelyInside)
|
||||
{
|
||||
return WalkBoundingType.EntirelyInside;
|
||||
}
|
||||
return WalkBoundingType.PartiallyInside;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::block_check</c> @0x0054dc50: the block-vs-view test over
|
||||
/// four corner interval vectors. Retail's call site passes
|
||||
/// (max_zval, min_zval) and an internal double positional swap hands
|
||||
/// corner_plane_check (min, max) — this port takes (maxZ, minZ) to
|
||||
/// mirror the caller's argument order and performs the same swap, so
|
||||
/// call sites read like the decomp. Any plane with all four corners
|
||||
/// outside culls; any partial plane demotes stickily; EntirelyInside
|
||||
/// requires unanimity on every plane. planeCount = the active view's
|
||||
/// edge count (bounds[0] is the CY plane, tested first with early-out).
|
||||
/// </summary>
|
||||
public static WalkBoundingType BlockCheck(
|
||||
ReadOnlySpan<float> corner00, ReadOnlySpan<float> corner01,
|
||||
ReadOnlySpan<float> corner10, ReadOnlySpan<float> corner11,
|
||||
int planeCount, float maxZ, float minZ)
|
||||
{
|
||||
WalkBoundingType result = BlockPlaneCheck(
|
||||
corner00[0], corner01[0], corner10[0], corner11[0], minZ, maxZ);
|
||||
if (result == WalkBoundingType.Outside) return WalkBoundingType.Outside;
|
||||
for (int k = 1; k <= planeCount; k++)
|
||||
{
|
||||
WalkBoundingType r = BlockPlaneCheck(
|
||||
corner00[k], corner01[k], corner10[k], corner11[k], minZ, maxZ);
|
||||
if (r == WalkBoundingType.Outside) return WalkBoundingType.Outside;
|
||||
if (r == WalkBoundingType.PartiallyInside)
|
||||
result = WalkBoundingType.PartiallyInside;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Render::viewconeCheck</c> @0x0054c250 (plane-test half): a sphere
|
||||
/// (center already in viewer-block space, radius already scaled) vs the
|
||||
/// CY plane plus the active view's edge planes. Cull is STRICT
|
||||
/// (d < −r); the partial flag is INCLUSIVE (d ≤ r) — a sphere exactly
|
||||
/// tangent from inside counts Partial, not EntirelyInside. The retail
|
||||
/// body's side effects (publishing local_object_center/radius) belong
|
||||
/// to the walk context, not this math.
|
||||
/// </summary>
|
||||
public static WalkBoundingType ViewconeCheck(
|
||||
Vector3 center, float radius, in WalkPlane cyPlane,
|
||||
ReadOnlySpan<WalkPlane> edgePlanes)
|
||||
{
|
||||
float d = Vector3.Dot(cyPlane.Normal, center) + cyPlane.D;
|
||||
if (d < -radius) return WalkBoundingType.Outside;
|
||||
bool partial = d <= radius;
|
||||
foreach (ref readonly WalkPlane plane in edgePlanes)
|
||||
{
|
||||
d = Vector3.Dot(plane.Normal, center) + plane.D;
|
||||
if (d < -radius) return WalkBoundingType.Outside;
|
||||
if (d <= radius) partial = true;
|
||||
}
|
||||
return partial ? WalkBoundingType.PartiallyInside : WalkBoundingType.EntirelyInside;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retail <c>Plane</c>: dot(N, p) + d, positive side = inside.</summary>
|
||||
public readonly record struct WalkPlane(Vector3 Normal, float D);
|
||||
|
||||
/// <summary>Retail <c>BoundingType</c>, values used raw by the walk.</summary>
|
||||
public enum WalkBoundingType
|
||||
{
|
||||
Outside = 0,
|
||||
PartiallyInside = 1,
|
||||
EntirelyInside = 2,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue