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:
Erik 2026-09-03 18:14:38 +02:00
parent b3ef4027a2
commit 99d9c6d564
11 changed files with 209 additions and 640 deletions

View file

@ -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);

View file

@ -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"/> &gt; 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"/> &gt; 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 &lt;= 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

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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);

View file

@ -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,