fix(render): §4 flood strobe — homogeneous reciprocal clip + collinear-aware region dedup
THE BUG (pinned deterministically by the new CornerFloodReplayTests harness — real Holtburg cells, captured corner-press scenario): a smooth 2 cm/step monotonic eye sweep across the 0172↔0173↔0171 doorway produced a NON-monotonic flood — on ~10 of 61 steps the player's room (0172) vanished from the flood entirely or collapsed to a sub-pixel sliver, taking its downstream chain (016F, the outside view) with it. Live, those isolated frames are the §4 background strobe: openings/passages flash the clear color during transitions, and the corner press shows background at the angles that park the eye near the doorway plane. TWO root causes, both fixed: 1. ApplyReciprocalClip ran the reciprocal portal polygon through the legacy divide-first ProjectToNdc + 2D Intersect path, justified by "the reciprocal is never near the eye." That assumption is exactly false at doorways/corners: the reciprocal IS the same opening whose plane the eye presses against (2-60 cm). ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge there — 2 cm eye moves flipped its output between a no-op and a duplicated- vertex hairline that ground the healthy region down to <3 distinct vertices. FIX: route the reciprocal through the SAME homogeneous pipeline as the forward clip (ProjectToClip + ClipToRegion) — which is what retail does: PView::OtherPortalClip (decomp:433524-433563) runs the reciprocal through the very same GetClip(finish=1) → ACRender::polyClipFinish homogeneous clipper. Also ported retail's skip: exact_match portals (CCellPortal.exact_match, acclient.h:32300; PView::ClipPortals :433689) bypass the reciprocal clip — both sides share the same polygon, so re-clipping is redundant. 2. CellView.CanonicalKey missed COLLINEAR re-emissions: the homogeneous region clipper legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices — keyed as distinct, defeating the dedup and accumulating duplicate polygons (this was the real mechanism behind the historical "float drift defeats the dedup" rationale that had parked the reciprocal on the unstable path). FIX: canonicalize away collinear snapped points (exact integer cross-products on the 1e-3 NDC grid) so the key is purely a function of the region's corners. Conformance: CornerSweep_FloodIsCompleteAndMonotone pins the fixed behavior — 61-step monotonic eye sweep ⇒ full flood every step, outside view always reached, player-room region monotone (was: clean shrink 4.000→2.879 with zero drops, vs ~10 glitch steps before). Diagnostic facts (trace diff, hop microscope, primitive scratch) retained as the apparatus. Suites: App 223 green (incl. Build_AppliesReciprocalOtherPortalClip, now passing with proper tightening AND dedup), Core 1377 green + the 4 pre-existing #99-era failures + 1 skip, UI 420, Net 294. Visual gate pending: corner press, room↔room, cellar↔floor, indoor↔outdoor transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
482b0dea1b
commit
dac8f6ad1f
3 changed files with 565 additions and 18 deletions
|
|
@ -97,10 +97,21 @@ public sealed class CellView
|
|||
private const float DedupGridNdc = 1e-3f;
|
||||
|
||||
// Canonical key for a view polygon: vertices snapped to the NDC grid, consecutive snap-duplicates
|
||||
// removed (including wrap-around), then rotated to start at the lexicographically smallest vertex so
|
||||
// a rotated emission of the same cycle yields the same key. Returns null when fewer than 3 distinct
|
||||
// removed (including wrap-around), COLLINEAR points removed (exact integer cross-products on the
|
||||
// snapped grid), then rotated to start at the lexicographically smallest vertex so a rotated
|
||||
// emission of the same cycle yields the same key. Returns null when fewer than 3 distinct
|
||||
// snapped vertices survive (a degenerate sliver, not a real region). Winding is already CCW for every
|
||||
// builder input (ClipToRegion / EnsureCcw), so the cyclic order is canonical without a reversal step.
|
||||
//
|
||||
// §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper
|
||||
// (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop)
|
||||
// legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so
|
||||
// BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices.
|
||||
// Without collinear canonicalization those re-emissions key as distinct, defeating the dedup and
|
||||
// accumulating duplicate polygons (the pre-2026-06-06 unbounded-growth hang in miniature, and the
|
||||
// exact reason the reciprocal clip was previously parked on the unstable divide-first path).
|
||||
// Dropping collinear snapped points makes the key purely a function of the region's CORNERS, so
|
||||
// any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates.
|
||||
private static string? CanonicalKey(Vector2[]? verts)
|
||||
{
|
||||
if (verts is null || verts.Length < 3) return null;
|
||||
|
|
@ -112,6 +123,30 @@ public sealed class CellView
|
|||
if (pts.Count == 0 || pts[^1] != q) pts.Add(q);
|
||||
}
|
||||
if (pts.Count >= 2 && pts[^1] == pts[0]) pts.RemoveAt(pts.Count - 1);
|
||||
|
||||
// Remove collinear points: for consecutive (prev, cur, next) around the cycle, drop cur when
|
||||
// cross(cur-prev, next-cur) == 0 — exact in integer grid coordinates (deltas ≤ ~4000, products
|
||||
// ≤ ~1.6e7, no overflow). Loop to a fixpoint: removing one point can make its neighbour
|
||||
// collinear. Degenerate inputs (all points on one line) reduce below 3 → rejected below.
|
||||
bool removed = true;
|
||||
while (removed && pts.Count >= 3)
|
||||
{
|
||||
removed = false;
|
||||
for (int i = 0; i < pts.Count && pts.Count >= 3; i++)
|
||||
{
|
||||
var prev = pts[(i + pts.Count - 1) % pts.Count];
|
||||
var cur = pts[i];
|
||||
var next = pts[(i + 1) % pts.Count];
|
||||
long cross = (long)(cur.X - prev.X) * (next.Y - cur.Y)
|
||||
- (long)(cur.Y - prev.Y) * (next.X - cur.X);
|
||||
if (cross == 0)
|
||||
{
|
||||
pts.RemoveAt(i);
|
||||
removed = true;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pts.Count < 3) return null;
|
||||
|
||||
int n = pts.Count;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue