Chunk H of Campaign OVERHAUL v2's S3 walk-ownership program (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10.6), landing the three post-hoc three-lens findings on S3 chunk 4 round 2. Pure dead-state deletion, comment truth, one test deletion, one stronger pin — no admission, order, or behavior change; no new flag or probe; no register row (AD-17's correction is the lead's own, already on the campaign branch). H1 — ClipPlaneSet.cs: deleted ScissorNdcAabb (no production reader — only AppendOutsideSlice consumes ClipPlaneSet, and it reads only IsNothingVisible/Count/PlaneArray) and renamed UseScissorFallback -> IsPlaneOverflow with a doc stating the true consumer contract: Count==0 overflow means the region draws UNCLIPPED via the no-clip slot, never a scissor box. Rewrote the file header's convexity-rule and Count==0-states sections to the two states that exist (Empty / overflow) instead of three, and simplified the private Scissor(...) AABB-computing helpers into a parameterless Overflow() factory since nothing reads the AABB any more. ClipPlaneSetTests.cs: renamed every UseScissorFallback reference, deleted the AABB assertions (From_MultiplePolygons's four ScissorNdcAabb.X/Y/Z/W checks, Empty_StaticProperty_DrawsNothing's degenerate-box check, ScissorFallback_IsNotNothingVisible's bounds check) that pinned the deleted consumer contract. H2 — TerrainModernRenderer.cs: deleted Draw's clipPlanes/ndcClipAabb parameters (grep: TerrainModernRenderer.Draw has exactly one production caller, WorldScenePassExecutor.DrawFlatTerrain, and it never passed either), which made CollectVisibleCells's own clipPlanes/ndcClipAabb/ viewProjection parameters and the IsAabbVisibleThroughClipRegion + IsAabbOutsideHomogeneousPlane helper pair they fed entirely dead — deleted along with the stale "Retail publishes landcell in_view from the clipped landscape view" comment; CollectVisibleCells is frustum-only now, matching DrawLandCells's own "retail never view-clips terrain" doc. Deleted the terrainUploadCount computation at RetailPViewRenderer.cs (the DrawInside/DrawLandscapeDynamicsPhase caller) and PrepareClipFrame's GL-era reservation-count parameter end to end: IWorldPassSurface.PrepareClipFrame(int), RhiWorldPassSurface's matching body and its now-pointless "_ = terrainUploadCount" discard, RetailPViewPassExecutor.PrepareClipFrame's pass-through, and WorldScenePassExecutor.PrepareFlatWorldClip's hardcoded `1` argument — PrepareClipFrame() takes nothing on every arm. TerrainParticleCellVisibilityTests.cs: deleted the three tests that only existed to pin the removed clip-plane/scissor mechanism (RejectsCellsOutsideDoorwayClipPlanes, RejectsCellsOutsideDoorwayScissorAabb, UnionsCellsFromEveryLandscapeSlice) and updated the two surviving frustum tests to the new signature. H3 — deleted tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs outright. #130 was a background-color strip along a doorway's top edge from a since-fixed under-inclusion in the CPU clip pipeline; its scissor half was already retired at S3 chunk 4 fix round 2 (L7), and its surviving plane-gap half exercised only PortalVisibilityBuilder.Build and ClipFrameAssembler.Assemble, both confirmed zero-production-caller legacy paths (grep) — the one production stage it actually touched, ClipPlaneSet.From, is already pinned through the real producers (BeginWalkFrame/ReassembleOutsideViewFromWalk) by the K6/L4 punch-fan equivalence pin in ClipFrameLayoutTests, so nothing production-relevant lost coverage. H4 — comment truth pass: - ClipFrameAssembler.cs: rewrote the file-header "renderer uses scissor for passes that need that fallback" line and the TerrainClipMode summary's "that path never produces Scissor" clause. Deleted the TerrainClipMode enum's Scissor member — grepped every reader first (WorldRenderDiagnostics.cs, WorldSceneDiagnosticsController.cs, WorldSceneRendererTests.cs all only pass the value through to a string Append or a mock parameter; WorldSceneRenderer.cs, the only writer, sets only Planes (default) or Skip, never Scissor) — kept Planes/Skip for the flat path and corrected Skip's stale doc ("No outside_view slice is visible; skip landscape indoors", a leftover from the pre-round-2 walk meaning) to what the flat path's own writer actually means now ("the PView walk ran instead of the flat-world path this frame"). Corrected ClipViewSlice's doc: the AABB is retained for WorldRenderDiagnostics only, not for a scissor consumer. - WorldPassSurface.cs: "Four concerns... the clip-frame publication, the doorway scissor, gl_ClipDistance enablement, and retail's interior depth clear" -> three concerns (the doorway scissor was retired at L2), with a pointer to ClearInteriorDepth's own truthful doc. EnableClipDistances' "all three world vertex shaders already write 1.0" -> only portal_depth.vert writes gl_ClipDistance now (grep: sky.vert and terrain_modern.vert/terrain_atmospheric.vert no longer declare it at all — L3 deleted the block outright; portal_depth.vert still writes 1.0 past its active clip count). RhiWorldPassSurface's class doc dropped "the scissor is dynamic state on the borrowed encoder" (no producer left). - RetailPViewPassExecutorTests.cs: DrawLandscapeDynamicsPhase_CallsDraw WeatherOnceExactlyOnce's doc corrected "unconditionally" (false since K2) to "conditional on WalkFrameDriver.WeatherTurnFired, see the L1 pin". Added the switch-table scope note (matching L8's own note on DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch) to the K1 pin's doc (DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosing BackwardBranch). - RetailPViewRenderer.cs: the DrawWeatherOnce citation cited the callee address (GameSky::Draw(sky,1) @0x00506ff0) where §10.1 cites the call site (@0x00506396) — corrected to cite the call site with the callee address alongside it. H5 — MUTATION CHECK (mandatory, run against the production gate at RetailPViewRenderer.DrawLandscapeDynamicsPhase): Added DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce, which widens the L1 pin's window to start at the DrawUnattachedSceneParticles call (the last call before the whole gate) and asserts EXACTLY ONE branch total in that window — the L1 pin's own window (getter-call to draw-call) misses a conjoined gate's extra, earlier condition because that condition's own brfalse lands BEFORE the getter call's offset. - Mutation 1 (the conjoined gate this pin exists to reject): changed the gate to `if (clipAssembly.OutsideViewSlices.Length != 0 && walkDriver.WeatherTurnFired)`, built, ran the new pin — FAILED with: Assert.Single() Failure: The collection contained 2 matching items Expected: (predicate expression) Collection: [CompiledBranch { Offset = 7, OpCode = brfalse.s, TargetOffset = 17 }, CompiledBranch { Offset = 24, OpCode = brfalse.s, TargetOffset = 42 }, CompiledBranch { Offset = 33, OpCode = brfalse.s, TargetOffset = 42 }] Match indices: 1, 2 (offset 7 = the DrawUnattachedSceneParticles block's own guard, correctly excluded since it precedes that call's offset; offsets 24 and 33 are the conjoined gate's two brfalse's, both inside the widened window — exactly the failure this pin exists to catch). Restored the single-flag gate; rebuilt; the new pin and all 13 sibling tests in RetailPViewPassExecutorTests pass again (14/14). - Mutation M1 (the existing round-2 regression: restore the pre-fix gate `if (clipAssembly.OutsideViewSlices.Length != 0)`), built, ran the full suite — the L1 pin (DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired) still FAILS, as required, with: Assert.Equal() Failure: Values differ Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver) Actual: typeof(AcDream.App.Rendering.ClipFrameAssembly) (the new H5 pin does NOT fail under M1 — a single-flag gate still produces exactly one branch in the widened window, which is correct: catching "wrong condition source" is L1's job, catching "an extra conjoined condition" is H5's). Restored the correct gate; rebuilt; 14/14 green again. H6 grep sweep (extends §10.5's L6 sweep per the plan): grep -rnE "ScissorNdcAabb|UseScissorFallback|TerrainClipMode\.Scissor|ndcClipAabb|terrainUploadCount" --include=*.cs --include=*.vert --include=*.frag src tests -> empty (every mention, including in this commit's own explanatory comments, was rephrased to avoid the literal deleted identifiers). The base L6 sweep (SetTerrainClip|BeginDoorwayScissor|BindTerrainClip| TerrainBytes|active scissor|TerrainClipMode\.Scissor|NdcScissorRect) is also still empty. Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings / 0 errors. Hermetic lane -> 6825/6825 passed, 0 failed. InstalledDat lane (ACDREAM_DAT_DIR set) -> 243 passed / 4 known failures (2x #383 layout tests, TowerAscentReplayTests's TowerAscent KnownFailure, WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff #458 KnownFailure) / 1 skipped / 248 total — one fewer test than before H3, as expected from Issue130DoorwayStripTests's deletion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
260 lines
9 KiB
C#
260 lines
9 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class ClipPlaneSetTests
|
|
{
|
|
// A CellView holding one regular n-gon centred at the origin, wound CCW.
|
|
private static CellView RegularNgonCellView(int n, float radius)
|
|
{
|
|
var verts = new Vector2[n];
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
// CCW: increasing angle.
|
|
float a = MathF.Tau * i / n;
|
|
verts[i] = new Vector2(radius * MathF.Cos(a), radius * MathF.Sin(a));
|
|
}
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(verts));
|
|
return cv;
|
|
}
|
|
|
|
private static CellView SquareCellView(float min, float max)
|
|
{
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(min, min), new Vector2(max, min), new Vector2(max, max), new Vector2(min, max),
|
|
}));
|
|
return cv;
|
|
}
|
|
|
|
[Fact]
|
|
public void From_ViewPolygon_MatchesSinglePolygonCellView()
|
|
{
|
|
var polygon = new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.7f, -0.4f), new Vector2(0.2f, -0.4f),
|
|
new Vector2(0.7f, 0.3f), new Vector2(-0.5f, 0.6f),
|
|
});
|
|
var view = new CellView();
|
|
Assert.True(view.Add(polygon));
|
|
|
|
var fromView = ClipPlaneSet.From(view);
|
|
var fromPolygon = ClipPlaneSet.From(polygon);
|
|
|
|
Assert.Equal(fromView.Count, fromPolygon.Count);
|
|
Assert.Equal(fromView.IsPlaneOverflow, fromPolygon.IsPlaneOverflow);
|
|
Assert.Equal(fromView.IsNothingVisible, fromPolygon.IsNothingVisible);
|
|
Assert.Equal(fromView.Planes, fromPolygon.Planes);
|
|
}
|
|
|
|
// --- The three required tests (verbatim intent from the plan) -------------
|
|
|
|
[Fact]
|
|
public void From_AxisAlignedSquare_FourPlanes_PointInsideHasPositiveDistances()
|
|
{
|
|
var sq = new CellView();
|
|
sq.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f), new Vector2(0.5f, 0.5f), new Vector2(-0.5f, 0.5f),
|
|
}));
|
|
var cps = ClipPlaneSet.From(sq);
|
|
Assert.Equal(4, cps.Count);
|
|
|
|
var clip = new Vector4(0, 0, 0, 1); // NDC (0,0), inside
|
|
foreach (var p in cps.Planes)
|
|
Assert.True(Vector4.Dot(p, clip) >= 0);
|
|
|
|
var outClip = new Vector4(0.9f, 0, 0, 1); // NDC (0.9,0), outside the square
|
|
Assert.Contains(cps.Planes, p => Vector4.Dot(p, outClip) < 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void From_NineEdgePolygon_FallsBackToPlaneOverflow()
|
|
{
|
|
var poly = RegularNgonCellView(n: 9, radius: 0.6f);
|
|
var cps = ClipPlaneSet.From(poly);
|
|
Assert.True(cps.IsPlaneOverflow || cps.Count <= 8);
|
|
if (cps.IsPlaneOverflow)
|
|
{
|
|
Assert.Equal(0, cps.Count); // draws unclipped, no plane gate
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void From_EmptyRegion_IsEmpty()
|
|
{
|
|
Assert.Equal(0, ClipPlaneSet.From(new CellView()).Count);
|
|
}
|
|
|
|
// --- Multi-polygon safety (the under-inclusion guard) ---------------------
|
|
|
|
[Fact]
|
|
public void From_MultiplePolygons_FallsBackToPlaneOverflow_NeverEmitsOnePolygonsPlanes()
|
|
{
|
|
// Two disjoint squares: a CONVEX plane set can never represent their union.
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.8f, -0.8f), new Vector2(-0.4f, -0.8f), new Vector2(-0.4f, -0.4f), new Vector2(-0.8f, -0.4f),
|
|
}));
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(0.4f, 0.4f), new Vector2(0.8f, 0.4f), new Vector2(0.8f, 0.8f), new Vector2(0.4f, 0.8f),
|
|
}));
|
|
|
|
var cps = ClipPlaneSet.From(cv);
|
|
|
|
// MUST NOT emit a single polygon's convex planes (that would hide the other).
|
|
Assert.True(cps.IsPlaneOverflow);
|
|
Assert.Equal(0, cps.Count);
|
|
Assert.Empty(cps.Planes);
|
|
}
|
|
|
|
// --- Distinguishing the two Count==0 states -------------------------------
|
|
|
|
[Fact]
|
|
public void Empty_IsNothingVisible_NotPlaneOverflow()
|
|
{
|
|
var e = ClipPlaneSet.From(new CellView());
|
|
Assert.Equal(0, e.Count);
|
|
Assert.False(e.IsPlaneOverflow); // NOT "draws unclipped"
|
|
Assert.True(e.IsNothingVisible); // "draw nothing"
|
|
Assert.Empty(e.Planes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_StaticProperty_DrawsNothing()
|
|
{
|
|
var e = ClipPlaneSet.Empty;
|
|
Assert.Equal(0, e.Count);
|
|
Assert.False(e.IsPlaneOverflow);
|
|
Assert.True(e.IsNothingVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlaneOverflow_IsNotNothingVisible()
|
|
{
|
|
var poly = RegularNgonCellView(n: 9, radius: 0.6f);
|
|
var cps = ClipPlaneSet.From(poly);
|
|
Assert.True(cps.IsPlaneOverflow);
|
|
Assert.False(cps.IsNothingVisible); // "draws unclipped", not "draw nothing"
|
|
}
|
|
|
|
// --- Plane-sign correctness for every edge -------------------------------
|
|
|
|
[Fact]
|
|
public void From_Square_EveryEdgePlane_IsPositiveAtCenter_NegativeJustOutsideThatEdge()
|
|
{
|
|
var cps = ClipPlaneSet.From(SquareCellView(-0.5f, 0.5f));
|
|
Assert.Equal(4, cps.Count);
|
|
|
|
var center = new Vector4(0, 0, 0, 1);
|
|
// Inside ⇒ EVERY plane non-negative.
|
|
foreach (var p in cps.Planes)
|
|
Assert.True(Vector4.Dot(p, center) >= 0, $"plane {p} should be >=0 at center");
|
|
|
|
// For each of the four cardinal directions just outside the square, at least one
|
|
// plane goes negative (the edge facing that direction).
|
|
foreach (var pt in new[]
|
|
{
|
|
new Vector4(0.6f, 0, 0, 1), new Vector4(-0.6f, 0, 0, 1),
|
|
new Vector4(0, 0.6f, 0, 1), new Vector4(0, -0.6f, 0, 1),
|
|
})
|
|
{
|
|
Assert.Contains(cps.Planes, p => Vector4.Dot(p, pt) < 0);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void From_Triangle_ThreePlanes_AllZeroZComponent()
|
|
{
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(0f, 0.5f), new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f), // CCW
|
|
}));
|
|
var cps = ClipPlaneSet.From(cv);
|
|
Assert.Equal(3, cps.Count);
|
|
foreach (var p in cps.Planes)
|
|
{
|
|
Assert.Equal(0f, p.Z, 6); // clip-space planes are (nx, ny, 0, d)
|
|
// Normal is unit length in xy.
|
|
Assert.Equal(1f, MathF.Sqrt(p.X * p.X + p.Y * p.Y), 4);
|
|
}
|
|
}
|
|
|
|
// --- Winding normalization: a CW square must still yield inside-positive planes.
|
|
|
|
[Fact]
|
|
public void From_ClockwiseSquare_NormalizesWinding_InsideStillPositive()
|
|
{
|
|
var cv = new CellView();
|
|
// Same square but wound CW (reverse order). Builder normally EnsureCcw's,
|
|
// but From must be robust to either winding.
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.5f, -0.5f), new Vector2(-0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, -0.5f),
|
|
}));
|
|
var cps = ClipPlaneSet.From(cv);
|
|
Assert.Equal(4, cps.Count);
|
|
var center = new Vector4(0, 0, 0, 1);
|
|
foreach (var p in cps.Planes)
|
|
Assert.True(Vector4.Dot(p, center) >= 0, $"plane {p} must be >=0 at center even for CW input");
|
|
}
|
|
|
|
// --- Collinear-edge merge: a square with a midpoint added on one edge → 4 planes.
|
|
|
|
[Fact]
|
|
public void From_SquareWithCollinearMidpoint_MergesToFourPlanes()
|
|
{
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.5f, -0.5f),
|
|
new Vector2(0f, -0.5f), // collinear midpoint on the bottom edge
|
|
new Vector2(0.5f, -0.5f),
|
|
new Vector2(0.5f, 0.5f),
|
|
new Vector2(-0.5f, 0.5f),
|
|
}));
|
|
var cps = ClipPlaneSet.From(cv);
|
|
Assert.Equal(4, cps.Count); // the redundant collinear edge is merged away
|
|
}
|
|
|
|
// --- Exactly 8 edges fit (octagon); 9 spills to plane overflow (after merge fails to help).
|
|
|
|
[Fact]
|
|
public void From_RegularOctagon_FitsInEightPlanes()
|
|
{
|
|
var cps = ClipPlaneSet.From(RegularNgonCellView(n: 8, radius: 0.6f));
|
|
Assert.False(cps.IsPlaneOverflow);
|
|
Assert.Equal(8, cps.Count);
|
|
var center = new Vector4(0, 0, 0, 1);
|
|
foreach (var p in cps.Planes)
|
|
Assert.True(Vector4.Dot(p, center) >= 0);
|
|
}
|
|
|
|
// --- Degenerate single polygon (all collinear after merge) → nothing convex → empty.
|
|
|
|
[Fact]
|
|
public void From_DegenerateSinglePolygon_IsNothingVisible()
|
|
{
|
|
// A ViewPolygon needs >=3 verts to be added, but they can be (nearly) collinear,
|
|
// leaving <3 distinct edges after merge → no convex region → treat as nothing visible.
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.5f, -0.5f), new Vector2(0f, 0f), new Vector2(0.5f, 0.5f),
|
|
}));
|
|
var cps = ClipPlaneSet.From(cv);
|
|
Assert.Equal(0, cps.Count);
|
|
// Degenerate sliver carries no area → safest is "nothing visible" (over-include would
|
|
// need a real AABB; a zero-area line has none).
|
|
Assert.True(cps.IsNothingVisible);
|
|
}
|
|
|
|
}
|