chore(render): S3 landing hygiene — scissor-era dead state, stale comments, the #130 harness, a conjunction-proof weather-gate pin
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>
This commit is contained in:
parent
b3ef4027a2
commit
99d9c6d564
11 changed files with 209 additions and 640 deletions
|
|
@ -11,8 +11,9 @@
|
|||
// reverse cell_draw_list object lists
|
||||
//
|
||||
// Slot 0 is always no-clip. A slice whose polygon cannot be represented by the
|
||||
// <=8 plane budget uses slot 0 and its NDC AABB; the renderer uses scissor for
|
||||
// passes that need that fallback. Empty regions are omitted entirely.
|
||||
// <=8 plane budget uses slot 0 too (ClipPlaneSet.IsPlaneOverflow) and draws
|
||||
// unclipped — there is no GPU scissor consumer anywhere in the walk (S3
|
||||
// chunk 4 fix round 2, L2). Empty regions are omitted entirely.
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
|
|
@ -27,23 +28,25 @@ namespace AcDream.App.Rendering;
|
|||
/// <see cref="WorldSceneRenderer"/>'s separate flat-world safety path still
|
||||
/// uses it for its own unrelated "did the flat terrain draw this frame"
|
||||
/// diagnostic flag (<c>Planes</c> = drew, <c>Skip</c> = the PView walk ran
|
||||
/// instead; that path never produces <c>Scissor</c>).
|
||||
/// instead). S3 landing hygiene (H4) deleted the <c>Scissor</c> member: no
|
||||
/// producer ever wrote it (<see cref="WorldSceneRenderer"/> only ever
|
||||
/// assigns <c>Planes</c> or <c>Skip</c>), because there is no GPU scissor
|
||||
/// consumer left anywhere in this contract, walk or flat.
|
||||
/// </summary>
|
||||
public enum TerrainClipMode
|
||||
{
|
||||
/// <summary>All outside_view slices have convex plane clips.</summary>
|
||||
/// <summary>The flat-world path drew terrain this frame.</summary>
|
||||
Planes,
|
||||
|
||||
/// <summary>At least one outside_view slice requires scissor fallback.</summary>
|
||||
Scissor,
|
||||
|
||||
/// <summary>No outside_view slice is visible; skip landscape indoors.</summary>
|
||||
/// <summary>The PView walk ran instead of the flat-world path this frame;
|
||||
/// flat terrain was not drawn.</summary>
|
||||
Skip,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained
|
||||
/// for passes that cannot write gl_ClipDistance and must use scissor.
|
||||
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is
|
||||
/// retained for diagnostics (<see cref="WorldRenderDiagnostics"/>) only — no
|
||||
/// GPU pass reads it; there is no scissor consumer left in the walk.
|
||||
/// </summary>
|
||||
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
//
|
||||
// Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region,
|
||||
// in NDC) into a small set of clip-space half-space planes for the GPU's
|
||||
// gl_ClipDistance, OR a scissor AABB when the region can't be expressed as one
|
||||
// convex plane set.
|
||||
// gl_ClipDistance, or a signal that the region OVER-includes (draws
|
||||
// unclipped) because it cannot be expressed as one convex plane set.
|
||||
//
|
||||
// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
|
||||
// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
|
||||
|
|
@ -20,23 +20,24 @@
|
|||
// would clip away the others → a real visibility bug (under-inclusion).
|
||||
//
|
||||
// Therefore From() NEVER emits a single polygon's planes when the CellView holds
|
||||
// several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor:
|
||||
// the scissor is a superset of the true region, so it OVER-includes (draws a few
|
||||
// extra pixels) but never hides anything. Over-inclusion is safe; under-inclusion
|
||||
// is the bug class.
|
||||
// several. Multi-polygon (and >8-edge) regions instead report a plane overflow:
|
||||
// the consumer (ClipFrameAssembler.AppendOutsideSlice) reads Count == 0 and
|
||||
// IsNothingVisible == false, appends an EMPTY plane array (slot 0, no clip), and
|
||||
// the polygon draws whole (the punch fan covers the whole fan). That OVER-includes
|
||||
// (draws a few extra pixels/triangles) but never hides anything. Over-inclusion is
|
||||
// safe; under-inclusion is the bug class. S3 chunk 4 (fix round 2, L2) deleted the
|
||||
// only mechanism that ever read a scissor AABB from this type — there is no GPU
|
||||
// scissor consumer left anywhere in the walk — so an overflow region simply draws
|
||||
// unclipped; it does not carry a bounding box for anyone to scissor against.
|
||||
//
|
||||
// === The three Count==0 states (how a consumer tells them apart) =============
|
||||
// Count == 0 can mean three different things; the consumer MUST distinguish:
|
||||
// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
|
||||
// The cell/region isn't visible at all → DRAW NOTHING. The
|
||||
// ScissorNdcAabb is a degenerate inverted box (min > max) so that a
|
||||
// consumer which naively scissors on it still draws nothing.
|
||||
// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false.
|
||||
// The convex-plane budget was exceeded (multi-polygon or >8 edges)
|
||||
// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle).
|
||||
// There is no third Count==0 state produced by From(). (A separate "no-clip,
|
||||
// pass-all" slot 0 is constructed by the consumer directly, not via From().)
|
||||
// When Count > 0, Planes carries the convex gate and the scissor fields are unused.
|
||||
// === The two Count==0 states (how a consumer tells them apart) ===============
|
||||
// Count == 0 can mean two different things; the consumer MUST distinguish:
|
||||
// (a) Empty — IsNothingVisible == true, IsPlaneOverflow == false.
|
||||
// The cell/region isn't visible at all → DRAW NOTHING.
|
||||
// (b) Overflow — IsNothingVisible == false, IsPlaneOverflow == true.
|
||||
// The convex-plane budget was exceeded (multi-polygon or >8
|
||||
// edges) → DRAW UNCLIPPED (over-include, never a scissor).
|
||||
// When Count > 0, Planes carries the convex gate and both flags above are false.
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -46,8 +47,8 @@ namespace AcDream.App.Rendering;
|
|||
|
||||
/// <summary>
|
||||
/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a
|
||||
/// scissor AABB fallback. See the file header for the convexity rule and the three
|
||||
/// Count==0 states.
|
||||
/// plane-overflow signal (draw unclipped). See the file header for the convexity
|
||||
/// rule and the two Count==0 states.
|
||||
/// </summary>
|
||||
public readonly struct ClipPlaneSet
|
||||
{
|
||||
|
|
@ -69,16 +70,15 @@ public readonly struct ClipPlaneSet
|
|||
private const float MinPolygonArea = 1e-7f;
|
||||
private readonly Vector4[] _planes;
|
||||
|
||||
private ClipPlaneSet(Vector4[] planes, bool useScissorFallback, bool isNothingVisible, Vector4 scissorNdcAabb)
|
||||
private ClipPlaneSet(Vector4[] planes, bool isPlaneOverflow, bool isNothingVisible)
|
||||
{
|
||||
_planes = planes ?? Array.Empty<Vector4>();
|
||||
UseScissorFallback = useScissorFallback;
|
||||
IsPlaneOverflow = isPlaneOverflow;
|
||||
IsNothingVisible = isNothingVisible;
|
||||
ScissorNdcAabb = scissorNdcAabb;
|
||||
}
|
||||
|
||||
/// <summary>Number of active clip planes, 0..8. 0 ⇒ inspect <see cref="UseScissorFallback"/>
|
||||
/// and <see cref="IsNothingVisible"/> to decide between "draw the AABB" and "draw nothing".</summary>
|
||||
/// <summary>Number of active clip planes, 0..8. 0 ⇒ inspect <see cref="IsPlaneOverflow"/>
|
||||
/// and <see cref="IsNothingVisible"/> to decide between "draw unclipped" and "draw nothing".</summary>
|
||||
public int Count => _planes?.Length ?? 0;
|
||||
|
||||
/// <summary>The active clip-space planes (nx, ny, 0, d). Empty when <see cref="Count"/> is 0.
|
||||
|
|
@ -89,30 +89,26 @@ public readonly struct ClipPlaneSet
|
|||
// its frame-scoped slice instead of cloning every plane payload a second time.
|
||||
internal Vector4[] PlaneArray => _planes ?? Array.Empty<Vector4>();
|
||||
|
||||
/// <summary>True ⇒ the convex-plane budget was exceeded; gate on <see cref="ScissorNdcAabb"/>
|
||||
/// instead (draw the box). Always false when <see cref="Count"/> > 0 or when the region is empty.</summary>
|
||||
public bool UseScissorFallback { get; }
|
||||
/// <summary>True ⇒ Count == 0 because the region needs more than <see cref="MaxPlanes"/>
|
||||
/// half-planes (or is multi-polygon); the consumer draws UNCLIPPED
|
||||
/// (<c>ClipFrameAssembler.AppendOutsideSlice</c> emits an empty plane array, so the region
|
||||
/// lands on the no-clip slot and — for the punch fans — the fan draws whole) — over-include,
|
||||
/// never a scissor. Always false when <see cref="Count"/> > 0 or when the region is empty.</summary>
|
||||
public bool IsPlaneOverflow { get; }
|
||||
|
||||
/// <summary>True ⇒ the region is not visible at all; the consumer draws NOTHING.
|
||||
/// Mutually exclusive with <see cref="UseScissorFallback"/>, and only meaningful when Count == 0.</summary>
|
||||
/// Mutually exclusive with <see cref="IsPlaneOverflow"/>, and only meaningful when Count == 0.</summary>
|
||||
public bool IsNothingVisible { get; }
|
||||
|
||||
/// <summary>NDC axis-aligned scissor box (minX, minY, maxX, maxY). Valid (min <= max) only when
|
||||
/// <see cref="UseScissorFallback"/> is true. For the empty/nothing-visible case it is a degenerate
|
||||
/// inverted box so naive scissoring still draws nothing.</summary>
|
||||
public Vector4 ScissorNdcAabb { get; }
|
||||
|
||||
/// <summary>The "nothing is visible" sentinel: Count == 0, not a scissor fallback, draw nothing.</summary>
|
||||
/// <summary>The "nothing is visible" sentinel: Count == 0, not a plane overflow, draw nothing.</summary>
|
||||
public static ClipPlaneSet Empty { get; } =
|
||||
new(Array.Empty<Vector4>(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb);
|
||||
|
||||
// Inverted box (min > max) — any sane AABB intersection against it is empty.
|
||||
private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f);
|
||||
new(Array.Empty<Vector4>(), isPlaneOverflow: false, isNothingVisible: true);
|
||||
|
||||
/// <summary>
|
||||
/// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges
|
||||
/// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor;
|
||||
/// empty/degenerate → <see cref="Empty"/>. See the file header for the full rule.
|
||||
/// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → plane overflow
|
||||
/// (draw unclipped); empty/degenerate → <see cref="Empty"/>. See the file header for the
|
||||
/// full rule.
|
||||
/// </summary>
|
||||
public static ClipPlaneSet From(CellView region)
|
||||
{
|
||||
|
|
@ -120,9 +116,8 @@ public readonly struct ClipPlaneSet
|
|||
return Empty;
|
||||
|
||||
// MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes.
|
||||
// Over-include via the union AABB (safe). region.Min/Max already track the union.
|
||||
if (region.Polygons.Count > 1)
|
||||
return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
|
||||
return Overflow();
|
||||
|
||||
return From(region.Polygons[0]);
|
||||
}
|
||||
|
|
@ -152,17 +147,16 @@ public readonly struct ClipPlaneSet
|
|||
{
|
||||
int count = NormalizeAndMerge(input, verts);
|
||||
|
||||
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
|
||||
// meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
|
||||
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area ⇒ nothing visible.
|
||||
if (count < 3)
|
||||
return Empty;
|
||||
|
||||
ReadOnlySpan<Vector2> normalized = verts[..count];
|
||||
|
||||
// A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
|
||||
// on ITS own AABB (still a superset of the polygon → over-include, safe).
|
||||
// A single convex polygon with too many edges to fit the hardware budget ⇒ plane
|
||||
// overflow (draw unclipped — still a superset of the polygon → over-include, safe).
|
||||
if (count > MaxPlanes)
|
||||
return Scissor(normalized);
|
||||
return Overflow();
|
||||
|
||||
// 3..8 edges: emit one inward half-space plane per edge (CCW formula). This array is
|
||||
// the retained GPU-routing payload and therefore the one necessary allocation.
|
||||
|
|
@ -179,7 +173,7 @@ public readonly struct ClipPlaneSet
|
|||
// dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
|
||||
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
|
||||
}
|
||||
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
|
||||
return new ClipPlaneSet(planes, isPlaneOverflow: false, isNothingVisible: false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -188,22 +182,8 @@ public readonly struct ClipPlaneSet
|
|||
}
|
||||
}
|
||||
|
||||
private static ClipPlaneSet Scissor(float minX, float minY, float maxX, float maxY) =>
|
||||
new(Array.Empty<Vector4>(), useScissorFallback: true, isNothingVisible: false,
|
||||
scissorNdcAabb: new Vector4(minX, minY, maxX, maxY));
|
||||
|
||||
private static ClipPlaneSet Scissor(ReadOnlySpan<Vector2> verts)
|
||||
{
|
||||
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
|
||||
foreach (var v in verts)
|
||||
{
|
||||
if (v.X < minX) minX = v.X;
|
||||
if (v.X > maxX) maxX = v.X;
|
||||
if (v.Y < minY) minY = v.Y;
|
||||
if (v.Y > maxY) maxY = v.Y;
|
||||
}
|
||||
return Scissor(minX, minY, maxX, maxY);
|
||||
}
|
||||
private static ClipPlaneSet Overflow() =>
|
||||
new(Array.Empty<Vector4>(), isPlaneOverflow: true, isNothingVisible: false);
|
||||
|
||||
/// <summary>
|
||||
/// Return the polygon wound CCW with collinear vertices removed. The PortalVisibilityBuilder
|
||||
|
|
|
|||
|
|
@ -140,8 +140,8 @@ public RetailPViewPassExecutor(
|
|||
ClipFrameAssembly reuseAssembly) =>
|
||||
ClipFrameAssembler.BeginWalkFrame(_clipFrame, outdoorRoot, reuseAssembly);
|
||||
|
||||
public void PrepareClipFrame(int terrainUploadCount) =>
|
||||
_surface.PrepareClipFrame(terrainUploadCount);
|
||||
public void PrepareClipFrame() =>
|
||||
_surface.PrepareClipFrame();
|
||||
|
||||
public void PrepareCellBatches(
|
||||
RetailPViewFrameInput frame,
|
||||
|
|
|
|||
|
|
@ -413,10 +413,11 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
// FW4 slice 1: the ONE clip-region publication, after any walk
|
||||
// reassembly so the walk-derived outside-view slots are included
|
||||
// (moved from directly after AssembleClipFrame; the count is
|
||||
// reservation metadata the RHI arm ignores).
|
||||
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
||||
passes.PrepareClipFrame(terrainUploadCount);
|
||||
// (moved from directly after AssembleClipFrame). S3 landing hygiene
|
||||
// (H2) deleted the GL-era arena-reservation count this call used to
|
||||
// compute and pass — PrepareClipFrame() takes nothing now (see
|
||||
// IWorldPassSurface.PrepareClipFrame's doc).
|
||||
passes.PrepareClipFrame();
|
||||
|
||||
// Production prepares exactly the one walk's visited-cell set.
|
||||
HashSet<uint> prepareCells = drawableCells;
|
||||
|
|
@ -643,8 +644,9 @@ internal sealed class RetailPViewRenderer
|
|||
// + DrawLandscapeSliceLate, one call per active landscape view) is
|
||||
// DELETED — retail draws the
|
||||
// weather mesh and its rain particles ONCE, unclipped, after
|
||||
// LScape::draw's whole landblock loop (GameSky::Draw(sky,1)
|
||||
// @0x00506ff0), never once per doorway aperture. DrawWeatherOnce
|
||||
// LScape::draw's whole landblock loop (GameSky::Draw(sky,1) call site
|
||||
// @0x00506396 — the callee itself is @0x00506ff0), never once per
|
||||
// doorway aperture. DrawWeatherOnce
|
||||
// is the ONE call that remains — it now also submits the rain
|
||||
// particles (moved from the deleted loop's per-slice, per-doorway
|
||||
// ParticleRenderPass.SkyPostScene draw) as ONE unclipped
|
||||
|
|
|
|||
|
|
@ -241,8 +241,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
ICamera camera,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint? neverCullLandblockId = null,
|
||||
ReadOnlySpan<Vector4> clipPlanes = default,
|
||||
Vector4? ndcClipAabb = null,
|
||||
IReadOnlySet<uint>? inViewLandcells = null)
|
||||
{
|
||||
if (_alloc.LoadedCount == 0) return;
|
||||
|
|
@ -289,10 +287,7 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
data.WorldOrigin,
|
||||
data.AabbMin.Z,
|
||||
data.AabbMax.Z,
|
||||
frustum,
|
||||
viewProjection,
|
||||
clipPlanes,
|
||||
ndcClipAabb);
|
||||
frustum);
|
||||
}
|
||||
}
|
||||
if (_visibleSlots.Count == 0) return;
|
||||
|
|
@ -485,16 +480,23 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
// Private helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// S3 landing hygiene (H2): frustum-only now. Retail never view-clips
|
||||
/// terrain (see <see cref="DrawLandCells"/>'s doc); the CPU/GPU
|
||||
/// clip-region equivalence check this used to also run (a doorway
|
||||
/// slice's clip-space planes plus its NDC-AABB scissor fallback) had
|
||||
/// exactly one caller — <see cref="Draw"/> — and that caller's own two
|
||||
/// clip-region parameters were themselves dead (grep: no production
|
||||
/// caller ever passed either), so the equivalence helper and its
|
||||
/// homogeneous-plane subroutine are deleted along with them.
|
||||
/// </summary>
|
||||
internal static void CollectVisibleCells(
|
||||
HashSet<uint> destination,
|
||||
uint landblockId,
|
||||
Vector3 worldOrigin,
|
||||
float zMin,
|
||||
float zMax,
|
||||
FrustumPlanes? frustum,
|
||||
Matrix4x4 viewProjection,
|
||||
ReadOnlySpan<Vector4> clipPlanes,
|
||||
Vector4? ndcClipAabb = null)
|
||||
FrustumPlanes? frustum)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
const float cellSize = AcDream.Core.Physics.TerrainSurface.CellSize;
|
||||
|
|
@ -517,21 +519,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
continue;
|
||||
}
|
||||
|
||||
// Retail publishes landcell in_view from the clipped landscape
|
||||
// view, not merely from the camera frustum. The modern renderer
|
||||
// expresses each doorway slice as homogeneous clip-space planes
|
||||
// plus its scissor AABB; use both products here so particle
|
||||
// simulation follows the same visible terrain slice as the GPU.
|
||||
if (!IsAabbVisibleThroughClipRegion(
|
||||
cellMin,
|
||||
cellMax,
|
||||
viewProjection,
|
||||
clipPlanes,
|
||||
ndcClipAabb))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint low = AcDream.Core.Physics.TerrainSurface.ComputeOutdoorCellLowId(
|
||||
cellX * cellSize,
|
||||
cellY * cellSize);
|
||||
|
|
@ -540,75 +527,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsAabbVisibleThroughClipRegion(
|
||||
Vector3 min,
|
||||
Vector3 max,
|
||||
Matrix4x4 viewProjection,
|
||||
ReadOnlySpan<Vector4> clipPlanes,
|
||||
Vector4? ndcClipAabb)
|
||||
{
|
||||
Vector4 aabb = ndcClipAabb.GetValueOrDefault();
|
||||
bool hasScissorConstraint = ndcClipAabb.HasValue
|
||||
&& (aabb.X > -1f || aabb.Y > -1f || aabb.Z < 1f || aabb.W < 1f);
|
||||
if (clipPlanes.IsEmpty && !hasScissorConstraint)
|
||||
return true;
|
||||
|
||||
Span<Vector4> clipCorners = stackalloc Vector4[8];
|
||||
for (int corner = 0; corner < clipCorners.Length; corner++)
|
||||
{
|
||||
var world = new Vector4(
|
||||
(corner & 1) == 0 ? min.X : max.X,
|
||||
(corner & 2) == 0 ? min.Y : max.Y,
|
||||
(corner & 4) == 0 ? min.Z : max.Z,
|
||||
1f);
|
||||
clipCorners[corner] = Vector4.Transform(world, viewProjection);
|
||||
}
|
||||
|
||||
for (int planeIndex = 0; planeIndex < clipPlanes.Length; planeIndex++)
|
||||
{
|
||||
if (IsAabbOutsideHomogeneousPlane(clipCorners, clipPlanes[planeIndex]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasScissorConstraint)
|
||||
return true;
|
||||
|
||||
Span<Vector4> scissorPlanes = stackalloc Vector4[4]
|
||||
{
|
||||
new( 1f, 0f, 0f, -aabb.X),
|
||||
new(-1f, 0f, 0f, aabb.Z),
|
||||
new( 0f, 1f, 0f, -aabb.Y),
|
||||
new( 0f, -1f, 0f, aabb.W),
|
||||
};
|
||||
for (int planeIndex = 0; planeIndex < scissorPlanes.Length; planeIndex++)
|
||||
{
|
||||
if (IsAabbOutsideHomogeneousPlane(clipCorners, scissorPlanes[planeIndex]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsAabbOutsideHomogeneousPlane(
|
||||
ReadOnlySpan<Vector4> clipCorners,
|
||||
Vector4 plane)
|
||||
{
|
||||
// A linear half-space reaches its maximum over the transformed AABB at
|
||||
// one of the eight corners. If every corner is negative, no point in
|
||||
// the cell box can survive this GPU clip plane.
|
||||
for (int corner = 0; corner < clipCorners.Length; corner++)
|
||||
{
|
||||
if (Vector4.Dot(plane, clipCorners[corner]) >= 0f)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnsureCapacity(int newCapacity)
|
||||
{
|
||||
if (newCapacity <= _alloc.Capacity)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ internal interface IRenderFrameGlState
|
|||
/// and heavily tested — is written once for both backends.
|
||||
///
|
||||
/// <para>Everything else those executors do is delegation to a renderer. What is
|
||||
/// left is exactly this: the clip-frame publication, the doorway scissor,
|
||||
/// <c>gl_ClipDistance</c> enablement, and retail's interior depth clear. Four
|
||||
/// concerns, each of which genuinely differs between GL and Vulkan, and none of
|
||||
/// which is expressible on the pinned RHI contract.</para>
|
||||
/// left is exactly this: the clip-frame publication, <c>gl_ClipDistance</c>
|
||||
/// enablement, and retail's interior depth clear. Three concerns, each of
|
||||
/// which genuinely differs between GL and Vulkan, and none of which is
|
||||
/// expressible on the pinned RHI contract. (S3 landing hygiene, H4: the
|
||||
/// doorway scissor this used to also cover was retired at S3 chunk 4 fix
|
||||
/// round 2 — see <see cref="ClearInteriorDepth"/>'s implementation doc.)</para>
|
||||
/// </summary>
|
||||
internal interface IWorldPassSurface
|
||||
{
|
||||
|
|
@ -31,13 +33,6 @@ internal interface IWorldPassSurface
|
|||
/// Publishes this frame's per-cell clip-region table, and routes it to the
|
||||
/// renderers that read it.
|
||||
///
|
||||
/// <para><paramref name="terrainUploadCount"/> is how many distinct terrain
|
||||
/// clip blocks the frame will issue. GL reserves that many arena records
|
||||
/// before the first draw, because reallocating the arena while an earlier
|
||||
/// slice can still reference it is the hazard the reservation exists for. The
|
||||
/// RHI arm ignores it: a ring allocation is distinct memory by construction
|
||||
/// and lives until the frame retires.</para>
|
||||
///
|
||||
/// <para>S3 chunk 4 fix round 1 (K3) deleted the walk's per-frame terrain
|
||||
/// screen-space clip publish this used to also carry — its only writer
|
||||
/// was retired along with the per-outside-view-slice terrain/sky/weather
|
||||
|
|
@ -45,17 +40,29 @@ internal interface IWorldPassSurface
|
|||
/// declaration of that clip block along with the section binder that used
|
||||
/// to re-assert it, so this contract now publishes exactly one section,
|
||||
/// the per-cell clip-region table.</para>
|
||||
///
|
||||
/// <para>S3 landing hygiene (H2) deleted the GL-era reservation-count
|
||||
/// parameter this used to also take (how many distinct terrain clip
|
||||
/// blocks the frame would issue): the RHI arm always ignored it (a ring
|
||||
/// allocation is distinct memory by construction and lives until the
|
||||
/// frame retires), and the last GL implementation that read it
|
||||
/// (<c>GlWorldPassSurface</c>) was deleted at Campaign V slice V11 —
|
||||
/// nothing was left to size a reservation for.</para>
|
||||
/// </summary>
|
||||
void PrepareClipFrame(int terrainUploadCount);
|
||||
void PrepareClipFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Enables every <c>gl_ClipDistance</c> slot.
|
||||
///
|
||||
/// <para>Vulkan activates every element the shader declares and has no
|
||||
/// enable, so this is a no-op there — and that is safe rather than a
|
||||
/// divergence: all three world vertex shaders already write <c>1.0</c>
|
||||
/// ("keep everything") into every slot past the active count, so a frame with
|
||||
/// no clip planes clips nothing on either backend (plan §5.5.14 item 4).</para>
|
||||
/// divergence: only <c>portal_depth.vert</c> writes <c>gl_ClipDistance</c>
|
||||
/// in the world pass now (S3 landing hygiene, H4 — S3 chunk 4 fix round 2
|
||||
/// (L3) deleted the sky and terrain shaders' own declaration of the block
|
||||
/// this contract used to also publish; see <see cref="PrepareClipFrame"/>'s
|
||||
/// doc). It still writes <c>1.0</c> ("keep everything") into every slot
|
||||
/// past its active count, so a frame with no clip planes clips nothing on
|
||||
/// either backend (plan §5.5.14 item 4).</para>
|
||||
/// </summary>
|
||||
void EnableClipDistances();
|
||||
|
||||
|
|
@ -73,11 +80,10 @@ internal interface IWorldPassSurface
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The clip tables are ring sections published on the world pass scope, the
|
||||
/// scissor is dynamic state on the borrowed encoder, and the depth clear is a
|
||||
/// scoped <c>vkCmdClearAttachments</c>. The raw-GL implementation this used to
|
||||
/// sit alongside (<c>GlWorldPassSurface</c>) was deleted at Campaign V slice
|
||||
/// V11.
|
||||
/// The clip table is a ring section published on the world pass scope, and the
|
||||
/// depth clear is a scoped <c>vkCmdClearAttachments</c>. The raw-GL
|
||||
/// implementation this used to sit alongside (<c>GlWorldPassSurface</c>) was
|
||||
/// deleted at Campaign V slice V11.
|
||||
/// </summary>
|
||||
internal sealed class RhiWorldPassSurface : IWorldPassSurface
|
||||
{
|
||||
|
|
@ -95,12 +101,8 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
|
|||
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
|
||||
}
|
||||
|
||||
public void PrepareClipFrame(int terrainUploadCount)
|
||||
public void PrepareClipFrame()
|
||||
{
|
||||
// The reservation count has no RHI counterpart: each publication below
|
||||
// takes its own ring slice, which is distinct memory that outlives every
|
||||
// draw recorded against it in this frame.
|
||||
_ = terrainUploadCount;
|
||||
_scope.Sections.ClipRegions = Publish(
|
||||
_clipFrame.RegionBytes,
|
||||
GpuRingUsage.Storage);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
_environmentCells.SetClipRouting(null);
|
||||
}
|
||||
|
||||
public void PrepareFlatWorldClip() => _surface.PrepareClipFrame(1);
|
||||
public void PrepareFlatWorldClip() => _surface.PrepareClipFrame();
|
||||
|
||||
public void DrawFlatSky(
|
||||
in WorldCameraFrame camera,
|
||||
|
|
|
|||
|
|
@ -47,9 +47,8 @@ public class ClipPlaneSetTests
|
|||
var fromPolygon = ClipPlaneSet.From(polygon);
|
||||
|
||||
Assert.Equal(fromView.Count, fromPolygon.Count);
|
||||
Assert.Equal(fromView.UseScissorFallback, fromPolygon.UseScissorFallback);
|
||||
Assert.Equal(fromView.IsPlaneOverflow, fromPolygon.IsPlaneOverflow);
|
||||
Assert.Equal(fromView.IsNothingVisible, fromPolygon.IsNothingVisible);
|
||||
Assert.Equal(fromView.ScissorNdcAabb, fromPolygon.ScissorNdcAabb);
|
||||
Assert.Equal(fromView.Planes, fromPolygon.Planes);
|
||||
}
|
||||
|
||||
|
|
@ -75,14 +74,14 @@ public class ClipPlaneSetTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void From_NineEdgePolygon_FallsBackToScissor()
|
||||
public void From_NineEdgePolygon_FallsBackToPlaneOverflow()
|
||||
{
|
||||
var poly = RegularNgonCellView(n: 9, radius: 0.6f);
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
Assert.True(cps.UseScissorFallback || cps.Count <= 8);
|
||||
if (cps.UseScissorFallback)
|
||||
Assert.True(cps.IsPlaneOverflow || cps.Count <= 8);
|
||||
if (cps.IsPlaneOverflow)
|
||||
{
|
||||
Assert.Equal(0, cps.Count); // AABB carries the gate
|
||||
Assert.Equal(0, cps.Count); // draws unclipped, no plane gate
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +94,7 @@ public class ClipPlaneSetTests
|
|||
// --- Multi-polygon safety (the under-inclusion guard) ---------------------
|
||||
|
||||
[Fact]
|
||||
public void From_MultiplePolygons_FallsBackToUnionScissor_NeverEmitsOnePolygonsPlanes()
|
||||
public void From_MultiplePolygons_FallsBackToPlaneOverflow_NeverEmitsOnePolygonsPlanes()
|
||||
{
|
||||
// Two disjoint squares: a CONVEX plane set can never represent their union.
|
||||
var cv = new CellView();
|
||||
|
|
@ -111,29 +110,19 @@ public class ClipPlaneSetTests
|
|||
var cps = ClipPlaneSet.From(cv);
|
||||
|
||||
// MUST NOT emit a single polygon's convex planes (that would hide the other).
|
||||
Assert.True(cps.UseScissorFallback);
|
||||
Assert.True(cps.IsPlaneOverflow);
|
||||
Assert.Equal(0, cps.Count);
|
||||
Assert.Empty(cps.Planes);
|
||||
|
||||
// Scissor AABB is the UNION of both polygons (over-include, safe).
|
||||
Assert.Equal(-0.8f, cps.ScissorNdcAabb.X, 5); // minX
|
||||
Assert.Equal(-0.8f, cps.ScissorNdcAabb.Y, 5); // minY
|
||||
Assert.Equal(0.8f, cps.ScissorNdcAabb.Z, 5); // maxX
|
||||
Assert.Equal(0.8f, cps.ScissorNdcAabb.W, 5); // maxY
|
||||
|
||||
// The far corner of the second square is inside the union AABB → not hidden.
|
||||
Assert.True(0.7f >= cps.ScissorNdcAabb.X && 0.7f <= cps.ScissorNdcAabb.Z);
|
||||
Assert.True(0.7f >= cps.ScissorNdcAabb.Y && 0.7f <= cps.ScissorNdcAabb.W);
|
||||
}
|
||||
|
||||
// --- Distinguishing the three Count==0 states ----------------------------
|
||||
// --- Distinguishing the two Count==0 states -------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Empty_IsNothingVisible_NotScissorFallback()
|
||||
public void Empty_IsNothingVisible_NotPlaneOverflow()
|
||||
{
|
||||
var e = ClipPlaneSet.From(new CellView());
|
||||
Assert.Equal(0, e.Count);
|
||||
Assert.False(e.UseScissorFallback); // NOT "draw the AABB box"
|
||||
Assert.False(e.IsPlaneOverflow); // NOT "draws unclipped"
|
||||
Assert.True(e.IsNothingVisible); // "draw nothing"
|
||||
Assert.Empty(e.Planes);
|
||||
}
|
||||
|
|
@ -143,23 +132,17 @@ public class ClipPlaneSetTests
|
|||
{
|
||||
var e = ClipPlaneSet.Empty;
|
||||
Assert.Equal(0, e.Count);
|
||||
Assert.False(e.UseScissorFallback);
|
||||
Assert.False(e.IsPlaneOverflow);
|
||||
Assert.True(e.IsNothingVisible);
|
||||
// Degenerate AABB (min > max) so a consumer that naively scissors on it draws nothing.
|
||||
Assert.True(e.ScissorNdcAabb.X > e.ScissorNdcAabb.Z);
|
||||
Assert.True(e.ScissorNdcAabb.Y > e.ScissorNdcAabb.W);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScissorFallback_IsNotNothingVisible()
|
||||
public void PlaneOverflow_IsNotNothingVisible()
|
||||
{
|
||||
var poly = RegularNgonCellView(n: 9, radius: 0.6f);
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
Assert.True(cps.UseScissorFallback);
|
||||
Assert.False(cps.IsNothingVisible); // "draw the AABB box", not "draw nothing"
|
||||
// The fallback AABB bounds the 9-gon (radius 0.6).
|
||||
Assert.True(cps.ScissorNdcAabb.Z - cps.ScissorNdcAabb.X > 0.5f);
|
||||
Assert.True(cps.ScissorNdcAabb.W - cps.ScissorNdcAabb.Y > 0.5f);
|
||||
Assert.True(cps.IsPlaneOverflow);
|
||||
Assert.False(cps.IsNothingVisible); // "draws unclipped", not "draw nothing"
|
||||
}
|
||||
|
||||
// --- Plane-sign correctness for every edge -------------------------------
|
||||
|
|
@ -242,13 +225,13 @@ public class ClipPlaneSetTests
|
|||
Assert.Equal(4, cps.Count); // the redundant collinear edge is merged away
|
||||
}
|
||||
|
||||
// --- Exactly 8 edges fit (octagon); 9 spills to scissor (after merge fails to help).
|
||||
// --- 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.UseScissorFallback);
|
||||
Assert.False(cps.IsPlaneOverflow);
|
||||
Assert.Equal(8, cps.Count);
|
||||
var center = new Vector4(0, 0, 0, 1);
|
||||
foreach (var p in cps.Planes)
|
||||
|
|
|
|||
|
|
@ -1,326 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// #130 — background-color strip along the TOP outer edge of a doorway when
|
||||
/// looking out from inside. Mechanism model (2026-06-12 evidence sweep): for
|
||||
/// an interior root the SEAL stamps the FULL raw dat portal polygon at true
|
||||
/// depth (PortalDepthMaskRenderer, root-cell slice = full screen), while
|
||||
/// terrain/sky COLOR used to be gated per fragment by the OutsideView region —
|
||||
/// the same dat polygon run through ProjectToClip → ClipToRegion (1-px
|
||||
/// MergeSubPixelVertices) → ClipPlaneSet.From (0.5° collinear merge) → planes,
|
||||
/// with a Floor/Ceil pixel scissor (the sky's own doorway scissor bracket)
|
||||
/// on the slice AABB on top. Every one of those passes could only SHRINK the gate, so any
|
||||
/// shave showed as a strip of clear color between the gate's top edge and the
|
||||
/// aperture's rasterized top edge.
|
||||
///
|
||||
/// RETIRED MECHANISM (S3 chunk 4 fix round 2, L7): the scissor half of this
|
||||
/// story no longer applies. Retail draws the sky and the landscape unclipped
|
||||
/// (<c>LScape::draw</c> never installs a view before either — S3 chunk 3/4's
|
||||
/// own findings), and acdream now matches that: the sky's own doorway
|
||||
/// scissor bracket, <c>IWorldPassSurface.BeginScissor/EndScissor</c>, and
|
||||
/// every per-slice terrain/sky/weather clip they used to bracket are deleted
|
||||
/// outright. An
|
||||
/// interior root's aperture exactness comes instead from the depth clear, the
|
||||
/// exit seals, and the interior repaint (<c>WalkFrameDriver</c>'s interior
|
||||
/// turn draws the real cell geometry back over whatever the unclipped
|
||||
/// landscape painted through the doorway) — see the S3 chunk 4 plan section
|
||||
/// for the retail citations.
|
||||
///
|
||||
/// What remains below still pins something production reads: the outside-view
|
||||
/// polygon pipeline (<c>ProjectToClip</c> → <c>ClipToRegion</c> →
|
||||
/// <c>ClipPlaneSet.From</c>) that the KEPT punch-fan clip still consumes
|
||||
/// (<c>RetailPViewPassExecutor.DrawWalkPunchFan</c> reads
|
||||
/// <c>clipAssembly.OutsideViewSlices[activeViewIndex].Planes</c> — see
|
||||
/// <c>ClipFrameLayoutTests</c>'s punch-fan equivalence pin for the synthetic-
|
||||
/// view version of this same proof). This harness measures the PLANE gap
|
||||
/// headlessly at the real Holtburg corner building exit door (A9B4 0x0170,
|
||||
/// the HouseExitWalkReplay door): project the aperture, run the production
|
||||
/// flood + assembler, then walk sample points just inside the aperture's top
|
||||
/// edge downward until the plane gate admits them.
|
||||
///
|
||||
/// VERDICT (2026-06-12, 147 eye/gaze combos, plane half only — the original
|
||||
/// sweep also measured a scissor gap, since retired): the CPU polygon
|
||||
/// pipeline is sub-pixel exact (worst 0.54 px) — the W=0 clip port 987313a
|
||||
/// and both merge passes are EXONERATED. PIN 2 below still asserts that
|
||||
/// bound.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public class Issue130DoorwayStripTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
public Issue130DoorwayStripTests(ITestOutputHelper output) => _out = output;
|
||||
|
||||
private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u;
|
||||
|
||||
// Production projection convention (CornerFloodReplayTests.ViewProjFor):
|
||||
// FovY 1.2 rad, 1280x720 viewport, near 1, far 5000. The flood clip is
|
||||
// near-independent so near/far exactness is not load-bearing.
|
||||
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
|
||||
{
|
||||
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
|
||||
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
|
||||
return view * proj;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExitDoorTopEdge_GateCoversTheDrawnApertureWithinPixelTolerance()
|
||||
{
|
||||
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
|
||||
|
||||
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;
|
||||
|
||||
// Find the exit portal (OtherCellId == 0xFFFF) and its world polygon.
|
||||
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, "0x0170 has no exit portal polygon");
|
||||
|
||||
var localPoly = root.PortalPolygons[exitIdx];
|
||||
// Campaign FW3.3: ShellDrawLiftZ is retired — shells, seal fans, and
|
||||
// the gate all live in the ONE dat space, so the drawn aperture IS
|
||||
// the physics polygon. This test survives as the coverage proof
|
||||
// (gate covers the drawn hole within tolerance across the sweep);
|
||||
// the historical lifted-vs-unlifted strip test's premise (two
|
||||
// spaces) no longer exists and that test is deleted.
|
||||
var worldPoly = new Vector3[localPoly.Length];
|
||||
for (int i = 0; i < localPoly.Length; i++)
|
||||
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
||||
|
||||
Vector3 centroid = Vector3.Zero;
|
||||
foreach (var w in worldPoly) centroid += w;
|
||||
centroid /= worldPoly.Length;
|
||||
|
||||
// Inward direction: the portal plane normal signed toward the cell
|
||||
// interior (ClipPlanes carries InsideSide from the load).
|
||||
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);
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"exit portal idx={exitIdx} verts={localPoly.Length} centroid=({centroid.X:F2},{centroid.Y:F2},{centroid.Z:F2}) inward=({worldNormal.X:F2},{worldNormal.Y:F2},{worldNormal.Z:F2})"));
|
||||
for (int i = 0; i < worldPoly.Length; i++)
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$" poly[{i}] world=({worldPoly[i].X:F3},{worldPoly[i].Y:F3},{worldPoly[i].Z:F3})"));
|
||||
|
||||
float worstPlaneGapPx = 0f;
|
||||
string worstDesc = "(none)";
|
||||
|
||||
// Eye sweep: back off the doorway along the inward normal at several
|
||||
// distances/heights/lateral offsets; gaze at the centroid plus raised /
|
||||
// lowered targets (NDC alignment of the top edge varies with gaze).
|
||||
var lateral = Vector3.Normalize(Vector3.Cross(worldNormal, Vector3.UnitZ));
|
||||
float[] dists = { 0.6f, 1.0f, 1.6f, 2.4f, 3.5f };
|
||||
float[] heights = { 0.9f, 1.4f, 1.7f };
|
||||
float[] laterals = { -0.8f, 0f, 0.8f };
|
||||
float[] gazeRaise = { -0.4f, 0f, 0.4f, 0.9f };
|
||||
|
||||
int evaluated = 0;
|
||||
foreach (float d in dists)
|
||||
foreach (float h in heights)
|
||||
foreach (float lat in laterals)
|
||||
foreach (float gz in gazeRaise)
|
||||
{
|
||||
var eye = centroid + worldNormal * d + lateral * lat;
|
||||
eye.Z = centroid.Z - 1.0f + h; // door centroid sits mid-opening; bias to floor-ish
|
||||
var look = centroid + new Vector3(0, 0, gz);
|
||||
var viewProj = ViewProjFor(eye, look);
|
||||
|
||||
// Aperture truth: the seal's footprint = the raw polygon's projection.
|
||||
var clip = new Vector4[worldPoly.Length];
|
||||
float minW = float.MaxValue;
|
||||
for (int i = 0; i < worldPoly.Length; i++)
|
||||
{
|
||||
clip[i] = Vector4.Transform(new Vector4(worldPoly[i], 1f), viewProj);
|
||||
minW = MathF.Min(minW, clip[i].W);
|
||||
}
|
||||
if (minW <= 0.05f) continue; // eye in/behind the door plane — out of #130's scenario
|
||||
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 pv = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
||||
buildingMembership: null);
|
||||
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
||||
if (asm.OutsideViewSlices.Length == 0)
|
||||
{
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"d={d} h={h} lat={lat} gz={gz}: NO outside slice (outPolys={pv.OutsideView.Polygons.Count})"));
|
||||
continue;
|
||||
}
|
||||
evaluated++;
|
||||
|
||||
(float planeGapPx, float atX) =
|
||||
MeasureTopEdgeGap(aperture, asm.OutsideViewSlices, 1080);
|
||||
|
||||
if (planeGapPx > worstPlaneGapPx)
|
||||
{
|
||||
worstDesc = FormattableString.Invariant(
|
||||
$"d={d} h={h} lat={lat} gz={gz} minW={minW:F2} atX={atX:F3} slices={asm.OutsideViewSlices.Length} outVerts={DescribePolys(pv.OutsideView)} apVerts={aperture.Length}");
|
||||
worstPlaneGapPx = planeGapPx;
|
||||
}
|
||||
|
||||
if (planeGapPx > 0.55f)
|
||||
{
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"GAP d={d} h={h} lat={lat} gz={gz}: planeGap={planeGapPx:F2}px atX={atX:F3} outVerts={DescribePolys(pv.OutsideView)}"));
|
||||
float apTop = TopBoundaryY(aperture, atX);
|
||||
foreach (var slice in asm.OutsideViewSlices)
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$" slice slot={slice.Slot} planes={slice.Planes.Length} aabb=({slice.NdcAabb.X:F4},{slice.NdcAabb.Y:F4},{slice.NdcAabb.Z:F4},{slice.NdcAabb.W:F4}) apTopAtX={apTop:F4}"));
|
||||
foreach (var poly in pv.OutsideView.Polygons)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(" outPoly:");
|
||||
foreach (var v in poly.Vertices)
|
||||
sb.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
||||
_out.WriteLine(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_out.WriteLine(FormattableString.Invariant(
|
||||
$"evaluated={evaluated} worstPlaneGapPx={worstPlaneGapPx:F2} @ {worstDesc}"));
|
||||
|
||||
Assert.True(evaluated > 100, $"sweep degenerated: only {evaluated} eye/gaze combos evaluated");
|
||||
// PIN (canary): the CPU polygon pipeline (ProjectToClip → ClipToRegion
|
||||
// merges → ClipPlaneSet planes) stays sub-pixel exact against the raw
|
||||
// aperture projection. Observed 0.54 px worst (2026-06-12); the
|
||||
// production vertex-merge floor is ~1 px — beyond 1.2 px means a new
|
||||
// under-inclusion shaver entered the pipeline.
|
||||
Assert.True(worstPlaneGapPx <= 1.2f, FormattableString.Invariant(
|
||||
$"plane gate under-covers the aperture top edge by {worstPlaneGapPx:F2}px @ {worstDesc}"));
|
||||
}
|
||||
|
||||
private static string DescribePolys(CellView view)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var p in view.Polygons) parts.Add(p.Vertices.Length.ToString());
|
||||
return $"[{string.Join(",", parts)}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For sample x positions across the aperture's projected top edge, find the
|
||||
/// aperture boundary's top y, then walk downward until the plane gate
|
||||
/// admits the point. Returns the worst gap in 1080p pixels, and the x of
|
||||
/// the worst gap. S3 chunk 4 fix round 2 (L7): the scissor-gap half this
|
||||
/// used to measure alongside the plane gap is deleted — the scissor
|
||||
/// mechanism it modeled no longer exists in production.
|
||||
/// </summary>
|
||||
private static (float planeGapPx, float atX) MeasureTopEdgeGap(
|
||||
Vector2[] aperture, ClipViewSlice[] slices, int fbH,
|
||||
ITestOutputHelper? debug = null)
|
||||
{
|
||||
const float Inset = 1e-4f; // dodge exact-boundary ambiguity
|
||||
const float StepY = 0.0002f; // ~0.1 px at 1080p
|
||||
const float CapY = 0.02f; // stop searching beyond ~10 px
|
||||
|
||||
float minX = float.MaxValue, maxX = float.MinValue;
|
||||
foreach (var v in aperture) { minX = MathF.Min(minX, v.X); maxX = MathF.Max(maxX, v.X); }
|
||||
float span = maxX - minX;
|
||||
if (span <= 0.01f) return (0, 0);
|
||||
|
||||
float worstPlane = 0, atX = 0;
|
||||
const int Samples = 160;
|
||||
for (int s = 0; s <= Samples; s++)
|
||||
{
|
||||
float x = minX + span * (0.01f + 0.98f * s / Samples);
|
||||
if (MathF.Abs(x) > 0.98f) continue; // off screen — no pixel exists there
|
||||
float topY = TopBoundaryY(aperture, x);
|
||||
if (float.IsNaN(topY) || MathF.Abs(topY) > 0.98f) continue; // off screen / no boundary
|
||||
|
||||
var p = new Vector2(x, topY - Inset);
|
||||
|
||||
float planeGap = GapBelow(p, q => AnySliceAdmitsPlanes(slices, q), StepY, CapY);
|
||||
|
||||
if (debug is not null && planeGap > 0.005f)
|
||||
debug.WriteLine(FormattableString.Invariant(
|
||||
$" sample x={x:F4} apTop={topY:F4} planeGap={planeGap * fbH / 2f:F2}px"));
|
||||
|
||||
if (planeGap > worstPlane) { worstPlane = planeGap; atX = x; }
|
||||
}
|
||||
// NDC y → pixels at the given framebuffer height.
|
||||
return (worstPlane * fbH / 2f, atX);
|
||||
}
|
||||
|
||||
private static float GapBelow(Vector2 start, Func<Vector2, bool> admitted, float step, float cap)
|
||||
{
|
||||
if (admitted(start)) return 0f;
|
||||
for (float dy = step; dy <= cap; dy += step)
|
||||
{
|
||||
if (admitted(new Vector2(start.X, start.Y - dy)))
|
||||
return dy;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
// Production semantics: each OutsideView polygon is one slice; the union of
|
||||
// slices is drawn. A slice with planes gates per fragment via
|
||||
// gl_ClipDistance (dot((nx,ny,0,d),(x,y,z,1)) >= 0 for an NDC point);
|
||||
// a planeless slice (>8-edge zero-plane fallback, ClipFrameAssembler.cs) admits its whole NDC AABB.
|
||||
private static bool AnySliceAdmitsPlanes(ClipViewSlice[] slices, Vector2 p)
|
||||
{
|
||||
foreach (var slice in slices)
|
||||
{
|
||||
if (slice.Planes.Length == 0)
|
||||
{
|
||||
if (p.X >= slice.NdcAabb.X && p.Y >= slice.NdcAabb.Y
|
||||
&& p.X <= slice.NdcAabb.Z && p.Y <= slice.NdcAabb.W)
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
bool inside = true;
|
||||
foreach (var pl in slice.Planes)
|
||||
{
|
||||
if (pl.X * p.X + pl.Y * p.Y + pl.W < 0f) { inside = false; break; }
|
||||
}
|
||||
if (inside) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Highest boundary y of the polygon at vertical line x (NaN when
|
||||
/// the line misses the polygon).</summary>
|
||||
private static float TopBoundaryY(Vector2[] poly, float x)
|
||||
{
|
||||
float best = float.NaN;
|
||||
for (int i = 0; i < poly.Length; i++)
|
||||
{
|
||||
var a = poly[i];
|
||||
var b = poly[(i + 1) % poly.Length];
|
||||
if (MathF.Abs(a.X - b.X) < 1e-9f)
|
||||
{
|
||||
if (MathF.Abs(a.X - x) < 1e-6f)
|
||||
{
|
||||
float hi = MathF.Max(a.Y, b.Y);
|
||||
if (float.IsNaN(best) || hi > best) best = hi;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
float t = (x - a.X) / (b.X - a.X);
|
||||
if (t < 0f || t > 1f) continue;
|
||||
float y = a.Y + t * (b.Y - a.Y);
|
||||
if (float.IsNaN(best) || y > best) best = y;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
|
@ -159,7 +159,9 @@ public sealed class RetailPViewPassExecutorTests
|
|||
/// <c>ClearClipRouting</c> + the old <c>DrawLandscapeSliceLate</c> leaf,
|
||||
/// one call per active landscape view) is deleted — <c>DrawLandscapeDynamicsPhase</c> now calls
|
||||
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/> exactly once,
|
||||
/// unconditionally, with no loop of any kind around it. This
|
||||
/// conditional on <see cref="AcDream.App.Rendering.Walk.WalkFrameDriver.WeatherTurnFired"/>
|
||||
/// (see <see cref="DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired"/>
|
||||
/// — the L1 pin), with no loop of any kind around it. This
|
||||
/// <c>Assert.Single</c> alone proved insufficient at fix round 1 (K1):
|
||||
/// it counts DISTINCT call-site offsets, so it stays green even with a
|
||||
/// <c>foreach</c> wrapped around the one call site (the exact round-1
|
||||
|
|
@ -201,7 +203,14 @@ public sealed class RetailPViewPassExecutorTests
|
|||
/// the compiled <c>foreach</c> emits a backward branch (the
|
||||
/// condition-check jump back to the loop body) whose span now contains
|
||||
/// the call's offset, so this test fails; restore the single
|
||||
/// unconditional call to make it pass again.
|
||||
/// unconditional call to make it pass again. Note for reviewers (added
|
||||
/// at S3 landing hygiene, H4, matching <see cref="DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch"/>'s
|
||||
/// own note): <see cref="CompiledCallGraph.ReadBranches"/> reads only
|
||||
/// <c>br</c>/<c>brtrue</c>/<c>brfalse</c>-family single-target branches —
|
||||
/// it does not decode a compiled <c>switch</c> jump table, but no C# loop
|
||||
/// construct (<c>for</c>/<c>foreach</c>/<c>while</c>/<c>do</c>) ever
|
||||
/// compiles to one, so this pin's blind spot is not a loop shape it
|
||||
/// could miss.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch()
|
||||
|
|
@ -283,6 +292,65 @@ public sealed class RetailPViewPassExecutorTests
|
|||
&& branch.TargetOffset > drawOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 landing hygiene (H5): strengthens the L1 pin above against a
|
||||
/// conjoined gate the post-hoc three-lens review found it does not
|
||||
/// reject. L1's check (b) only looks at branches STRICTLY BETWEEN the
|
||||
/// <c>WeatherTurnFired</c> getter call and the <c>DrawWeatherOnce</c>
|
||||
/// call — so <c>if (clipAssembly.OutsideViewSlices.Length != 0 &&
|
||||
/// walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);</c> still
|
||||
/// passes it: the compiler evaluates <c>OutsideViewSlices.Length != 0</c>
|
||||
/// FIRST, so ITS OWN <c>brfalse</c> lands BEFORE the getter call's
|
||||
/// offset — outside L1's window — while <c>calls[c-1]</c> is still the
|
||||
/// getter (the added condition reads <c>OutsideViewSlices</c>, a
|
||||
/// property getter, then <c>.Length</c>, a non-call <c>ldlen</c>, so no
|
||||
/// OTHER call intervenes before the getter). This pin widens the window
|
||||
/// to start at the call immediately before the whole gate —
|
||||
/// <see cref="RetailPViewPassExecutor.DrawUnattachedSceneParticles"/>,
|
||||
/// the outdoor-emitters call the method's own comment names as the last
|
||||
/// call before the gate — and counts EVERY branch in that wider window
|
||||
/// with <c>Assert.Single</c>: production has exactly one, the forward
|
||||
/// <c>brfalse</c>/<c>brfalse.s</c> right after the getter. A conjoined
|
||||
/// gate's extra, earlier condition adds a second branch this wider
|
||||
/// window catches but L1's narrower one cannot.
|
||||
/// MUTATION: change the gate to <c>if
|
||||
/// (clipAssembly.OutsideViewSlices.Length != 0 &&
|
||||
/// walkDriver.WeatherTurnFired)</c> — the branch count in the window
|
||||
/// goes from 1 to 2 and <c>Assert.Single</c> fails (see the commit body
|
||||
/// for the exact recorded failure text).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce()
|
||||
{
|
||||
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
||||
"DrawLandscapeDynamicsPhase",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||
int particlesIndex = RequiredCallIndex(
|
||||
calls,
|
||||
typeof(RetailPViewPassExecutor),
|
||||
nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles));
|
||||
int drawIndex = RequiredCallIndex(
|
||||
calls,
|
||||
typeof(RetailPViewPassExecutor),
|
||||
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
||||
int particlesOffset = calls[particlesIndex].Offset;
|
||||
int drawOffset = calls[drawIndex].Offset;
|
||||
|
||||
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
||||
CompiledBranch onlyGuard = Assert.Single(
|
||||
branches,
|
||||
branch => branch.Offset > particlesOffset && branch.Offset < drawOffset);
|
||||
|
||||
Assert.True(
|
||||
onlyGuard.OpCode == OpCodes.Brfalse || onlyGuard.OpCode == OpCodes.Brfalse_S,
|
||||
$"Expected the sole branch between DrawUnattachedSceneParticles and "
|
||||
+ $"DrawWeatherOnce to be a brfalse, was {onlyGuard.OpCode}.");
|
||||
Assert.True(
|
||||
onlyGuard.TargetOffset > drawOffset,
|
||||
"Expected the guard branch to skip forward past DrawWeatherOnce.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 4 fix round 2 (L8): the identical loop-shape question K1
|
||||
/// asked of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>'s call
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@ using AcDream.App.Rendering;
|
|||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// S3 landing hygiene (H2): <see cref="TerrainModernRenderer.CollectVisibleCells"/>
|
||||
/// is frustum-only now — its clip-plane-list and NDC-AABB scissor
|
||||
/// parameters and the CPU/GPU clip-region equivalence check they fed had
|
||||
/// exactly one production caller (<see cref="TerrainModernRenderer.Draw"/>),
|
||||
/// and that caller never actually passed either (grep). The doorway-clip/scissor
|
||||
/// rejection cases this file used to pin (<c>RejectsCellsOutsideDoorwayClipPlanes</c>,
|
||||
/// <c>RejectsCellsOutsideDoorwayScissorAabb</c>, <c>UnionsCellsFromEveryLandscapeSlice</c>)
|
||||
/// pinned a mechanism nothing in production ever fed — deleted with the
|
||||
/// parameters. What remains still pins the one thing this method's callers
|
||||
/// actually rely on: it publishes the visible terrain cell set per landblock,
|
||||
/// respecting an explicit frustum when one is given.
|
||||
/// </summary>
|
||||
public sealed class TerrainParticleCellVisibilityTests
|
||||
{
|
||||
[Fact]
|
||||
|
|
@ -16,9 +29,7 @@ public sealed class TerrainParticleCellVisibilityTests
|
|||
Vector3.Zero,
|
||||
zMin: 0f,
|
||||
zMax: 20f,
|
||||
frustum: null,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty);
|
||||
frustum: null);
|
||||
|
||||
Assert.Equal(64, cells.Count);
|
||||
Assert.Contains(0xA9B40001u, cells);
|
||||
|
|
@ -40,80 +51,8 @@ public sealed class TerrainParticleCellVisibilityTests
|
|||
new Vector3(100f, 100f, 100f),
|
||||
zMin: 100f,
|
||||
zMax: 120f,
|
||||
frustum,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty);
|
||||
frustum);
|
||||
|
||||
Assert.Empty(cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollectVisibleCells_RejectsCellsOutsideDoorwayClipPlanes()
|
||||
{
|
||||
var cells = new HashSet<uint>();
|
||||
var planes = new[] { new Vector4(-1f, 0f, 0f, -2f) };
|
||||
|
||||
TerrainModernRenderer.CollectVisibleCells(
|
||||
cells,
|
||||
0xA9B4FFFFu,
|
||||
Vector3.Zero,
|
||||
zMin: 0f,
|
||||
zMax: 20f,
|
||||
frustum: null,
|
||||
Matrix4x4.Identity,
|
||||
planes);
|
||||
|
||||
Assert.Empty(cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollectVisibleCells_RejectsCellsOutsideDoorwayScissorAabb()
|
||||
{
|
||||
var cells = new HashSet<uint>();
|
||||
|
||||
TerrainModernRenderer.CollectVisibleCells(
|
||||
cells,
|
||||
0xA9B4FFFFu,
|
||||
Vector3.Zero,
|
||||
zMin: 0f,
|
||||
zMax: 20f,
|
||||
frustum: null,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty,
|
||||
ndcClipAabb: new Vector4(-4f, -4f, -2f, -2f));
|
||||
|
||||
Assert.Empty(cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollectVisibleCells_UnionsCellsFromEveryLandscapeSlice()
|
||||
{
|
||||
var cells = new HashSet<uint>();
|
||||
|
||||
TerrainModernRenderer.CollectVisibleCells(
|
||||
cells,
|
||||
0xA9B4FFFFu,
|
||||
Vector3.Zero,
|
||||
zMin: 0f,
|
||||
zMax: 20f,
|
||||
frustum: null,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty,
|
||||
ndcClipAabb: new Vector4(1f, -1_000f, 23f, 1_000f));
|
||||
TerrainModernRenderer.CollectVisibleCells(
|
||||
cells,
|
||||
0xA9B4FFFFu,
|
||||
Vector3.Zero,
|
||||
zMin: 0f,
|
||||
zMax: 20f,
|
||||
frustum: null,
|
||||
Matrix4x4.Identity,
|
||||
ReadOnlySpan<Vector4>.Empty,
|
||||
ndcClipAabb: new Vector4(49f, -1_000f, 71f, 1_000f));
|
||||
|
||||
Assert.Equal(16, cells.Count);
|
||||
Assert.Contains(0xA9B40001u, cells);
|
||||
Assert.Contains(0xA9B40011u, cells);
|
||||
Assert.DoesNotContain(0xA9B40009u, cells);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue