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:
parent
579c8b06bc
commit
cf8a2c379b
2 changed files with 75 additions and 8 deletions
|
|
@ -129,8 +129,59 @@ public static class PortalProjection
|
|||
float w = poly[i].W;
|
||||
ndc[i] = new Vector2(poly[i].X / w, poly[i].Y / w);
|
||||
}
|
||||
EnsureCcw(ndc);
|
||||
return ndc;
|
||||
|
||||
// T2 (BR-4): retail's post-divide vertex merge — Render::copy_view
|
||||
// (Ghidra 0x0054dfc0) collapses consecutive vertices closer than ~1
|
||||
// PIXEL (|dx|<=1 && |dy|<=1 screen units) after the perspective divide.
|
||||
// This is the flood's physical fixpoint floor: re-clipping a view can
|
||||
// only insert sub-pixel sliver vertices, which this merge removes, so
|
||||
// accumulated views converge instead of drifting (the drift is what
|
||||
// forced the MaxReprocessPerCell=16 cap). Unit approximation: the
|
||||
// builder has no viewport, so 1 px is expressed in NDC at a reference
|
||||
// 1080p (2/1080 ≈ 0.00185); at higher resolutions the merge is merely
|
||||
// slightly coarser than retail's, which only strengthens convergence.
|
||||
var merged = MergeSubPixelVertices(ndc);
|
||||
if (merged.Length < 3)
|
||||
return System.Array.Empty<Vector2>();
|
||||
|
||||
EnsureCcw(merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses
|
||||
// runs of consecutive near-identical vertices, including across the
|
||||
// wrap-around. A polygon that collapses below 3 distinct vertices is
|
||||
// degenerate (sub-pixel sliver) and returns empty — exactly retail's
|
||||
// "<3 surviving verts → output count 0".
|
||||
private const float VertexMergeEpsilonNdc = 2f / 1080f;
|
||||
|
||||
private static Vector2[] MergeSubPixelVertices(Vector2[] poly)
|
||||
{
|
||||
if (poly.Length < 3) return poly;
|
||||
var kept = new List<Vector2>(poly.Length);
|
||||
foreach (var v in poly)
|
||||
{
|
||||
if (kept.Count > 0)
|
||||
{
|
||||
var prev = kept[^1];
|
||||
if (MathF.Abs(v.X - prev.X) <= VertexMergeEpsilonNdc
|
||||
&& MathF.Abs(v.Y - prev.Y) <= VertexMergeEpsilonNdc)
|
||||
continue;
|
||||
}
|
||||
kept.Add(v);
|
||||
}
|
||||
// Wrap-around: last ≈ first.
|
||||
while (kept.Count >= 2)
|
||||
{
|
||||
var first = kept[0];
|
||||
var last = kept[^1];
|
||||
if (MathF.Abs(first.X - last.X) <= VertexMergeEpsilonNdc
|
||||
&& MathF.Abs(first.Y - last.Y) <= VertexMergeEpsilonNdc)
|
||||
kept.RemoveAt(kept.Count - 1);
|
||||
else
|
||||
break;
|
||||
}
|
||||
return kept.Count == poly.Length ? poly : kept.ToArray();
|
||||
}
|
||||
|
||||
// One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue