T2 slice 1 (BR-4): multi-view UNION merge + retail 1-px vertex merge (the fixpoint floor)

(a) MergeBuildingFrame now UNIONS a building flood's views into cells
already present in the frame (retail Render::copy_view APPENDS every
clipped portal polygon as a new view_poly, Ghidra 0x0054dfc0 - a cell
visible through two apertures holds two views). The old first-wins
'ContainsKey -> continue' dropped the second aperture's views: the
multiview-loss-first-wins divergence, a named #109 suspect.

(b) ClipToRegion output now runs retail's post-divide vertex merge:
consecutive vertices closer than ~1 pixel collapse (copy_view's
|dx|<=1 && |dy|<=1 screen-unit merge), polygons that collapse below 3
distinct verts return empty (retail's '<3 survivors -> count 0'). This
is the flood's PHYSICAL fixpoint floor - re-clipping can only insert
sub-pixel slivers, which the merge removes, so accumulated views
converge instead of drifting. Unit note: builder has no viewport, so
1 px is expressed as NDC at reference 1080p (0.00185); coarser at higher
res, which only strengthens convergence. This is the prerequisite for
removing the MaxReprocessPerCell=16 drift cap (T2 slice 2) and the
EyeInsidePortalOpening rescue.

Gates: all 10 flood conformance tests green (CornerSweep monotone pin
included); App 226 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 11:20:12 +02:00
parent 579c8b06bc
commit cf8a2c379b
2 changed files with 75 additions and 8 deletions

View file

@ -144,17 +144,33 @@ public sealed class RetailPViewRenderer
}
}
// Append a per-building flood's cells + views into the frame. Each building cell belongs to exactly
// one building, so there is no cross-building overlap; ContainsKey is a safety dedup. OutsideView is
// NOT merged — the outdoor root already seeds full-screen terrain, and ConstructViewBuilding
// (BuildFromExterior) leaves OutsideView empty (it stops at exit portals once inside the building).
// T2 (BR-4): merge a per-building flood's cells + views into the frame as a
// UNION. Retail accumulates EVERY clipped portal polygon as a new view_poly
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
// a cell visible through two apertures holds two views, all consumed
// downstream). The old first-wins (`ContainsKey -> continue`) dropped the
// second building flood's views whenever a cell was already in the frame —
// the multiview-loss-first-wins divergence (a named #109 suspect: per-frame
// winner flips between apertures). CellView.Add dedups exact/collinear
// re-emissions (the dac8f6a CanonicalKey), so unioning is convergent.
// OutsideView is NOT merged — the outdoor root already seeds full-screen
// terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView
// empty (it stops at exit portals once inside the building).
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
{
foreach (uint cellId in src.OrderedVisibleCells)
{
if (target.CellViews.ContainsKey(cellId))
if (!src.CellViews.TryGetValue(cellId, out var srcView))
continue;
target.CellViews[cellId] = src.CellViews[cellId];
if (target.CellViews.TryGetValue(cellId, out var existing))
{
foreach (var p in srcView.Polygons)
existing.Add(p);
continue;
}
target.CellViews[cellId] = srcView;
target.OrderedVisibleCells.Add(cellId);
}
}