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:
parent
2163308032
commit
987313aa54
7 changed files with 357 additions and 130 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue