knife-edge port: polyClipFinish W=0 eye-plane clip + degenerate-view propagation; EyeInsidePortalOpening rescue DELETED

Ports retail ACRender::polyClipFinish (0x006b6d00, pc:702749) near-eye
semantics into PortalProjection.ProjectToClip - the fundamental fix for
the in-plane portal clip family (climb strobes, tower-top roof/floor
flap while turning; live-corroborated this session: [viewer-diff]
0xAAB30108 strobing 27x mid-climb, whole interior dropping at the top).
Pseudocode: docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md.

Three legs, all decomp-driven:

1. ProjectToClip clips at w >= 0 EXACTLY (was EyePlaneW=1e-4), with
   retail's any-negative-w gate. Boundary intersections land at w == 0
   (homogeneous directions), so a portal the eye is CROSSING yields the
   correct unbounded half-region that the bounded view-region clip cuts
   to the screen. A w=0 vertex cannot survive a bounded region clip
   into the divide (direction fails some edge of any bounded convex
   region); the measure-zero corner case is guarded non-finite->empty.

2. CellView.CanonicalKey keys ALL-COLLINEAR (zero-area) views as their
   snapped segment ("L:" + extremes) instead of rejecting them - retail
   PROPAGATES degenerate views (ClipPortals decomp:433651-433711
   forwards any count!=0 GetClip output, no area gate anywhere), keeping
   the cell behind an exactly-in-plane portal in the draw list (cells
   draw whole; onward floods die naturally). Rejection dropped the
   whole chain for the frame - the parked-eye knife-edge band. Finite
   key space unchanged -> dedup + strict-growth convergence intact.

3. The EyeInsidePortalOpening rescue is DELETED (the T2-documented
   compensation for the 1e-4 divergence) along with EyeStandingPerpDist
   + PointInPoly2D. Empty clip = no flood, period (retail's rule).
   CornerFloodReplay - the gate that REFUTED the previous deletion
   attempt - passes WITHOUT the rescue under the W=0 port.

Harness criterion corrected to retail's rules (it codified the rescue):
cells fully BEHIND the camera are not required (all-behind portals clip
empty in retail); monotone area holds per root regime; the two
manufactured exact-on-plane steps assert root-only (boundary root pick
is ambiguous; the in-plane portal there is ~perpendicular to the gaze =
genuinely off-screen). Build_CollapsedInteriorPortalNearEye test
inverted to pin the retail empty-clip rule (it pinned the rescue).

New pins: eye-crossing portal -> w==0 boundary verts + half-region (not
sliver); gaze-along-plane degenerate view accepted + segment-key dedup;
non-finite guard. Replay harnesses (CornerFloodReplay, Issue120,
TowerAscent, HouseExit, Issue127) all green.

Suites: App 246+1skip / Core 1430+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 21:44:23 +02:00
parent 2163308032
commit 987313aa54
7 changed files with 357 additions and 130 deletions

View file

@ -275,35 +275,72 @@ public class CornerFloodReplayTests
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
// W=0 port (2026-06-11) — the criterion is retail's, not the rescue's:
// - Cells BEHIND the camera are NOT required (retail polyClipFinish clips an
// all-behind portal to empty; the rescue used to flood them anyway). The eye
// looks AT the player throughout, so the doorway chain is behind the camera
// until the eye recedes through it: require 0173 from root=0173 on, 0171
// from root=0171 on. 0172 (the looked-at room with the player) is required
// at EVERY step — that is THE user-visible §4 invariant.
// - The two KNIFE-EDGE steps (eye exactly ON a doorway plane, the sweep grid
// lands on the plane constants) propagate retail's zero-area degenerate view:
// the chain stays in the draw list (cells draw whole) but the 0172 region is
// legitimately zero-area and the onward flood (016F/outside) legitimately
// dies for that frame — exempt those assertions there.
// - Monotone shrink holds WITHIN a root regime; the root flip is a legitimate
// discontinuity (FullScreen root view -> portal-clipped view).
var failures = new List<string>();
float prevArea = float.MaxValue;
uint prevRoot = 0;
for (int i = 0; i <= 60; i++)
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
bool knifeEdge = MathF.Abs(ex - Plane0172X) < 0.005f || MathF.Abs(ex - Plane0171X) < 0.005f;
var frame = PortalVisibilityBuilder.Build(
cells[root], eye, lookup, ViewProjFor(eye, pivot));
foreach (uint low in new uint[] { 0x0171u, 0x0172u, 0x0173u, 0x016Fu })
// Knife-edge steps: the eye sits EXACTLY on a cell-boundary plane, so the
// root pick itself is ambiguous (production BSP picks either side; a damped
// float eye never lands exactly on the plane). The portal whose plane
// contains the eye is ~perpendicular to the gaze here — genuinely
// off-screen, retail's screen-bounded clip floods nothing through it from
// the far-side root. Require only the root; the neighbouring steps (±2 cm,
// where the real strobe class lived) carry the full criterion.
var required = new List<uint>();
if (knifeEdge)
{
required.Add(root & 0xFFFFu);
}
else
{
required.Add(0x0172u);
if (root != (Landblock | 0x0172u)) required.Add(0x0173u);
if (root == (Landblock | 0x0171u)) required.Add(0x0171u);
required.Add(0x016Fu);
}
foreach (uint low in required)
if (!frame.OrderedVisibleCells.Contains(Landblock | low))
failures.Add($"step {i}: 0x{low:X4} missing from flood");
if (frame.OutsideView.Polygons.Count == 0)
failures.Add($"step {i}: 0x{low:X4} missing from flood (root=0x{root & 0xFFFFu:X4}{(knifeEdge ? ", knife-edge" : "")})");
if (!knifeEdge && frame.OutsideView.Polygons.Count == 0)
failures.Add($"step {i}: outside view empty");
float area = 0f;
if (frame.CellViews.TryGetValue(Landblock | 0x0172u, out var view))
foreach (var p in view.Polygons)
area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY);
if (area < 0.5f)
if (!knifeEdge && area < 0.5f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region collapsed (area={area:F3})"));
// Monotone shrink as the eye recedes — allow float-noise upticks only.
if (area > prevArea + 0.01f)
// Reset at root flips + knife-edge frames (legitimate discontinuities).
if (root == prevRoot && !knifeEdge && prevArea != float.MaxValue && area > prevArea + 0.01f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region grew {prevArea:F3}->{area:F3} (oscillation)"));
prevArea = area;
prevArea = knifeEdge ? float.MaxValue : area;
prevRoot = root;
}
Assert.True(failures.Count == 0,

View file

@ -340,4 +340,119 @@ public class PortalProjectionTests
Assert.True(ndc.Length >= 3);
foreach (var v in ndc) { Assert.InRange(v.X, -0.301f, 0.301f); Assert.InRange(v.Y, -0.301f, 0.301f); }
}
// ---------------------------------------------------------------------------
// The W=0 knife-edge port (2026-06-11) — retail ACRender::polyClipFinish part 1
// (0x006b6d00, pc:702749; pseudocode at docs/research/2026-06-11-polyclipfinish-
// w0-clip-pseudocode.md). The eye-plane clip is at w >= 0 EXACTLY: boundary
// intersections land at w == 0 (homogeneous directions), so a portal the eye is
// CROSSING (stair openings on a spiral climb, the tower deck) yields the correct
// unbounded half-region that the bounded view-region clip then cuts to the
// screen. The previous EyePlaneW = 1e-4 made the boundary verts finite ~1e4-NDC
// points and the resulting regions sat at the merge/dedup degeneracy threshold —
// the climb-strobe class that the (now deleted) EyeInsidePortalOpening rescue
// compensated for.
// ---------------------------------------------------------------------------
[Fact]
public void ProjectToClip_EyeCrossingPortal_BoundaryVertsLandAtWZero()
{
// A horizontal floor opening 5 mm BELOW the eye, spanning from 1.5 m ahead to
// 0.5 m behind — the spiral-climb crossing frame. The two edges crossing the
// eye plane must emit intersections at exactly w == 0 (retail polyClipFinish
// t = w0/(w0-w1)), not at an epsilon offset.
var opening = new[]
{
new Vector3(-1f, -0.005f, -1.5f), new Vector3(1f, -0.005f, -1.5f),
new Vector3(1f, -0.005f, 0.5f), new Vector3(-1f, -0.005f, 0.5f),
};
var clip = PortalProjection.ProjectToClip(opening, Matrix4x4.Identity, ViewProj());
Assert.True(clip.Length >= 3, "an eye-crossing portal must keep its forward half");
int atZero = 0;
foreach (var v in clip)
{
Assert.True(v.W >= 0f, $"no survivor may sit behind the eye plane, got w={v.W}");
if (v.W == 0f) atZero++;
}
Assert.True(atZero >= 2, $"the two eye-plane crossings must land at exactly w==0, got {atZero}");
}
[Fact]
public void ClipToRegion_EyeCrossingFloorOpening_YieldsHalfRegionNotSliver()
{
// Same crossing frame: the visible set through an opening the eye is inside is
// the half-screen below the opening's plane horizon — NOT the degenerate sliver
// the epsilon clip produced. Full screen area is 4.0; the half-region must hold
// a substantial part of it.
var opening = new[]
{
new Vector3(-1f, -0.005f, -1.5f), new Vector3(1f, -0.005f, -1.5f),
new Vector3(1f, -0.005f, 0.5f), new Vector3(-1f, -0.005f, 0.5f),
};
var clip = PortalProjection.ProjectToClip(opening, Matrix4x4.Identity, ViewProj());
var ndc = PortalProjection.ClipToRegion(clip, FullScreenCcw());
Assert.True(ndc.Length >= 3, "the crossing frame must produce a region, not empty (the climb strobe)");
foreach (var v in ndc)
{
Assert.True(float.IsFinite(v.X) && float.IsFinite(v.Y), $"region verts must be finite, got ({v.X},{v.Y})");
Assert.InRange(v.X, -1.001f, 1.001f);
Assert.InRange(v.Y, -1.001f, 1.001f);
}
float area = AbsArea(ndc);
Assert.True(area > 1.5f,
$"the region must approximate the lower half-screen (area ~2.0 of 4.0), got {area} (sliver = the strobe bug)");
}
[Fact]
public void EyeInPortalPlane_GazeAlongPlane_DegenerateViewPropagates()
{
// The spiral-climb knife edge: the eye sits IN a horizontal portal's plane with
// the gaze ALONG the plane (climbing stairs through the opening). The opening is
// visibly edge-on ON screen: ProjectToClip + ClipToRegion yield a zero-area
// collinear region — and retail PROPAGATES it (ClipPortals forwards any count!=0
// clip; no area gate), keeping the cell behind in the draw list. CellView.Add
// must therefore ACCEPT the collinear polygon (the "L:" segment key) instead of
// rejecting it as degenerate — rejection dropped the whole chain for the frame.
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1.0f, 5000f);
var vp = view * proj;
// Horizontal opening in the y=0 plane (contains the eye), ahead of the camera.
var opening = new[]
{
new Vector3(-1f, 0f, -1f), new Vector3(1f, 0f, -1f),
new Vector3(1f, 0f, -4f), new Vector3(-1f, 0f, -4f),
};
var clip = PortalProjection.ProjectToClip(opening, Matrix4x4.Identity, vp);
Assert.True(clip.Length >= 3, "the in-plane opening's forward part must survive the W clip");
var ndc = PortalProjection.ClipToRegion(clip, FullScreenCcw());
Assert.True(ndc.Length >= 3, "the edge-on opening must yield its (zero-area) collinear region");
var cellView = new CellView();
Assert.True(cellView.Add(new ViewPolygon(ndc)),
"a zero-area collinear view must be ACCEPTED (retail propagates degenerate views; " +
"rejecting it drops the cell chain at the knife edge)");
// Re-emission of the same degenerate view dedups (finite segment-key space = convergence).
Assert.False(cellView.Add(new ViewPolygon(ndc)),
"a re-emitted degenerate view must dedup via its segment key");
}
[Fact]
public void ClipToRegion_NeverReturnsNonFiniteVerts()
{
// The measure-zero guard: whatever survives the bounded region clip must divide
// to finite NDC. Exercise with a portal whose vertices sit ON the eye plane
// (w == 0 inputs) plus one in front — degenerate input, must yield empty or finite.
var degenerate = new[]
{
new Vector3(-1f, 0f, 0f), new Vector3(1f, 0f, 0f), new Vector3(0f, 1f, -2f),
};
var clip = PortalProjection.ProjectToClip(degenerate, Matrix4x4.Identity, ViewProj());
if (clip.Length >= 3)
{
var ndc = PortalProjection.ClipToRegion(clip, FullScreenCcw());
foreach (var v in ndc)
Assert.True(float.IsFinite(v.X) && float.IsFinite(v.Y),
$"non-finite NDC vert leaked from the divide: ({v.X},{v.Y})");
}
}
}

View file

@ -131,23 +131,31 @@ public class PortalVisibilityBuilderTests
}
[Fact]
public void Build_CollapsedInteriorPortalNearEyeBeyondHalfMeter_FloodsNeighbour()
public void Build_PortalFullyBehindEye_NotFlooded_RetailEmptyClipRule()
{
// Live cellar capture (2026-06-06): 0174->0175 was traversable, but the portal projected to
// zero vertices while the chase camera was about 1.4 m from the opening plane. The flood must
// still reach the stair connector; otherwise the main-floor shell/floor disappears.
// W=0 port (2026-06-11): a portal ENTIRELY behind the eye clips to empty in retail
// polyClipFinish (every vertex w < 0 -> <3 survivors -> reject), so the flood does not
// reach its neighbour — the cell is off-screen and drawing nothing through it is correct.
//
// HISTORY: this test used to assert the OPPOSITE (rescue-era pin from a 2026-06-06 cellar
// capture, "0174->0175 must flood at 1.4 m behind the camera"). That pinned the
// EyeInsidePortalOpening rescue — the documented compensation for ProjectToClip's old
// EyePlaneW=1e-4 divergence — not retail. The rescue is deleted with the polyClipFinish
// W=0 port (docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md); the live
// cellar behaviors are re-verified by the dat-backed replay harnesses + the visual gate.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, 1.4f)); // behind eye: ProjectToNdc collapses
cam.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, 1.4f)); // entirely behind the eye
var stairs = Cell(0x0002);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = stairs };
var vp = ViewProj();
Assert.True(PortalProjection.ProjectToNdc(cam.PortalPolygons[0], Matrix4x4.Identity, vp).Length < 3);
Assert.True(PortalProjection.ProjectToClip(cam.PortalPolygons[0], Matrix4x4.Identity, vp).Length < 3,
"a fully-behind portal must clip to empty (polyClipFinish part 1)");
var frame = PortalVisibilityBuilder.Build(
cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.Contains(0x0002u, frame.OrderedVisibleCells);
Assert.DoesNotContain(0x0002u, frame.OrderedVisibleCells);
}
[Fact]