fix #130 (the real strip): drawn-shell lift vs draw-space portal consumers
The user's re-gate refuted the scissor fix as THE strip (6c4b6d6was a real but sub-pixel under-coverage): the strip survived, screenshot at a doorway, full width of the opening, top edge only, "very subtle". Root cause (pinned by Issue130DoorwayStripTests.UnliftedGate_*): the +0.02 m shell render lift. Cell shells DRAW 2 cm above the dat origin (z-fight vs coplanar terrain);f35cb8b(the #119-residual fix, 2026-06-11) deliberately reverted the VISIBILITY graph to the physics (unlifted) transform - but the OutsideView color gate (terrain/sky/ scissor through the doorway) and the seal/punch depth fans are DRAW-space consumers and kept projecting the unlifted polygons. The drawn lintel therefore sits one lift-projection above the gate's top edge - measured 6.7 px at a 2.4 m doorway - and that band never receives terrain/sky color while the seal also stamps 2 cm low. A regression fromf35cb8b, NOT from the W=0 clip port (987313astays exonerated). Vertical aperture edges are immune (the lift slides them along themselves) - top edge only, exactly as reported; explains the "also NOW" timing precisely. Fix - draw space draws lifted, visibility stays physics (thef35cb8binvariant, now symmetric): - PortalVisibilityBuilder.Build gains drawLiftZ: the exit-portal branch projects the OutsideView region with the lifted transform; flood admission, side tests, and CellViews are untouched (default 0 keeps every existing visibility test bit-identical). - The seal/punch fans (DrawRetailPViewPortalDepthWrite) lift their world verts to the drawn shell's space. - One shared constant PortalVisibilityBuilder.ShellDrawLiftZ feeds the shell registration (GameWindow:5604), the gate, and the fans. Register: AP-32 ADDED - the +0.02 lift had NO row (a pre-register deviation the 2026-06-12 sweep missed). The row records the split invariant both ways: a draw-space consumer that forgets the lift re-opens the #130 strip; a visibility consumer that picks the lifted transform re-opens the #119-residual side-cull. Pins: the lifted gate covers the drawn (lifted) aperture to 0.00 px across the 147-combo sweep; the unlifted gate shows the 6.7 px strip (sensitivity proof - if the lift is ever removed, this test says the drawLiftZ plumbing can go too). Suites: App 257+1skip / Core 1439+2skip / UI 420 / Net 294 green. Awaiting the user re-gate at a doorway with the lintel on screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4ba714835d
commit
5135066733
6 changed files with 191 additions and 38 deletions
|
|
@ -77,9 +77,16 @@ public class Issue130DoorwayStripTests
|
|||
Assert.True(exitIdx >= 0, "0x0170 has no exit portal polygon");
|
||||
|
||||
var localPoly = root.PortalPolygons[exitIdx];
|
||||
// DRAWN space: the shell that rasterizes the aperture (and the seal fan)
|
||||
// draws +ShellDrawLiftZ above the physics transform — the gate must be
|
||||
// compared against the drawn hole, not the physics polygon (#130: the
|
||||
// unlifted gate left a 2 cm background strip under the drawn lintel).
|
||||
var worldPoly = new Vector3[localPoly.Length];
|
||||
for (int i = 0; i < localPoly.Length; i++)
|
||||
{
|
||||
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
||||
worldPoly[i].Z += PortalVisibilityBuilder.ShellDrawLiftZ;
|
||||
}
|
||||
|
||||
Vector3 centroid = Vector3.Zero;
|
||||
foreach (var w in worldPoly) centroid += w;
|
||||
|
|
@ -137,7 +144,8 @@ public class Issue130DoorwayStripTests
|
|||
for (int i = 0; i < clip.Length; i++)
|
||||
aperture[i] = new Vector2(clip[i].X / clip[i].W, clip[i].Y / clip[i].W);
|
||||
|
||||
var pv = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj);
|
||||
var pv = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
||||
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
||||
if (asm.OutsideViewSlices.Length == 0)
|
||||
{
|
||||
|
|
@ -194,6 +202,103 @@ public class Issue130DoorwayStripTests
|
|||
$"plane gate under-covers the aperture top edge by {worstPlaneGapPx:F2}px @ {worstDesc}"));
|
||||
}
|
||||
|
||||
/// <summary>Sensitivity proof + regression documentation: a gate built in
|
||||
/// PHYSICS space (drawLiftZ 0) against the DRAWN (lifted) aperture shows a
|
||||
/// multi-pixel strip at a close doorway — the user-visible #130 strip
|
||||
/// (f35cb8b split the lift out of the visibility transform; the OutsideView
|
||||
/// kept gating drawn color in unlifted space). If this stops failing-by-gap,
|
||||
/// the lift is gone and the production drawLiftZ plumbing can go too.</summary>
|
||||
[Fact]
|
||||
public void UnliftedGate_LeavesTheStripAtTheDrawnTopEdge()
|
||||
{
|
||||
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var cells = CornerFloodReplayTests.LoadBuilding(dats);
|
||||
var root = cells[ExitCellId];
|
||||
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
|
||||
|
||||
int exitIdx = -1;
|
||||
for (int i = 0; i < root.Portals.Count; i++)
|
||||
{
|
||||
if (root.Portals[i].OtherCellId == 0xFFFF && i < root.PortalPolygons.Count
|
||||
&& root.PortalPolygons[i].Length >= 3)
|
||||
{ exitIdx = i; break; }
|
||||
}
|
||||
Assert.True(exitIdx >= 0);
|
||||
|
||||
var localPoly = root.PortalPolygons[exitIdx];
|
||||
var worldPoly = new Vector3[localPoly.Length];
|
||||
Vector3 centroid = Vector3.Zero;
|
||||
for (int i = 0; i < localPoly.Length; i++)
|
||||
{
|
||||
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
||||
worldPoly[i].Z += PortalVisibilityBuilder.ShellDrawLiftZ; // drawn space
|
||||
centroid += worldPoly[i];
|
||||
}
|
||||
centroid /= worldPoly.Length;
|
||||
|
||||
var plane = root.ClipPlanes[exitIdx];
|
||||
var worldNormal = Vector3.TransformNormal(plane.Normal, root.WorldTransform);
|
||||
var cellCenterWorld = Vector3.Transform(
|
||||
(root.LocalBoundsMin + root.LocalBoundsMax) * 0.5f, root.WorldTransform);
|
||||
if (Vector3.Dot(worldNormal, cellCenterWorld - centroid) < 0)
|
||||
worldNormal = -worldNormal;
|
||||
worldNormal = Vector3.Normalize(worldNormal);
|
||||
|
||||
// d=2.4 m, eye low (0.9 m above the opening's base), gaze at the
|
||||
// centroid — the main sweep's clean case, where the aperture top edge
|
||||
// projects ON SCREEN (y≈0.79; a closer/higher eye pushes the lintel
|
||||
// past the screen top and the seam becomes unmeasurable).
|
||||
var eye = centroid + worldNormal * 2.4f;
|
||||
eye.Z = centroid.Z - 1.0f + 0.9f;
|
||||
var viewProj = ViewProjFor(eye, centroid);
|
||||
|
||||
var clip = new Vector4[worldPoly.Length];
|
||||
for (int i = 0; i < worldPoly.Length; i++)
|
||||
clip[i] = Vector4.Transform(new Vector4(worldPoly[i], 1f), viewProj);
|
||||
var aperture = new Vector2[clip.Length];
|
||||
for (int i = 0; i < clip.Length; i++)
|
||||
aperture[i] = new Vector2(clip[i].X / clip[i].W, clip[i].Y / clip[i].W);
|
||||
|
||||
var pvUnlifted = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj); // drawLiftZ 0
|
||||
var asmUnlifted = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pvUnlifted);
|
||||
Assert.True(asmUnlifted.OutsideViewSlices.Length > 0);
|
||||
(float unliftedGapPx, _, _) = MeasureTopEdgeGap(aperture, asmUnlifted.OutsideViewSlices, 1920, 1080);
|
||||
|
||||
var pvLifted = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
||||
var asmLifted = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pvLifted);
|
||||
Assert.True(asmLifted.OutsideViewSlices.Length > 0);
|
||||
(float liftedGapPx, _, _) = MeasureTopEdgeGap(aperture, asmLifted.OutsideViewSlices, 1920, 1080);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"top-edge gap vs the DRAWN aperture at d=2.4 m: unliftedGate={unliftedGapPx:F2}px liftedGate={liftedGapPx:F2}px"));
|
||||
var dbg = new System.Text.StringBuilder(" aperture(LIFTED):");
|
||||
foreach (var v in aperture) dbg.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
||||
_out.WriteLine(dbg.ToString());
|
||||
foreach (var poly in pvUnlifted.OutsideView.Polygons)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(" unliftedGatePoly:");
|
||||
foreach (var v in poly.Vertices) sb.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
||||
_out.WriteLine(sb.ToString());
|
||||
}
|
||||
foreach (var poly in pvLifted.OutsideView.Polygons)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(" liftedGatePoly:");
|
||||
foreach (var v in poly.Vertices) sb.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
||||
_out.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
// The strip the user saw: physics-space gate vs drawn hole, several px.
|
||||
Assert.True(unliftedGapPx > 2.0f, FormattableString.Invariant(
|
||||
$"expected the unlifted gate to show the strip (>2px), got {unliftedGapPx:F2}px"));
|
||||
// The fix: a gate in drawn space covers the drawn hole.
|
||||
Assert.True(liftedGapPx <= 1.2f, FormattableString.Invariant(
|
||||
$"lifted gate still under-covers by {liftedGapPx:F2}px"));
|
||||
}
|
||||
|
||||
private static string DescribePolys(CellView view)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue