diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
index 7f8f6126..fe0fbace 100644
--- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs
+++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
@@ -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;
/// 's separate flat-world safety path still
/// uses it for its own unrelated "did the flat terrain draw this frame"
/// diagnostic flag (Planes = drew, Skip = the PView walk ran
-/// instead; that path never produces Scissor).
+/// instead). S3 landing hygiene (H4) deleted the Scissor member: no
+/// producer ever wrote it ( only ever
+/// assigns Planes or Skip), because there is no GPU scissor
+/// consumer left anywhere in this contract, walk or flat.
///
public enum TerrainClipMode
{
- /// All outside_view slices have convex plane clips.
+ /// The flat-world path drew terrain this frame.
Planes,
- /// At least one outside_view slice requires scissor fallback.
- Scissor,
-
- /// No outside_view slice is visible; skip landscape indoors.
+ /// The PView walk ran instead of the flat-world path this frame;
+ /// flat terrain was not drawn.
Skip,
}
///
-/// 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 () only — no
+/// GPU pass reads it; there is no scissor consumer left in the walk.
///
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
diff --git a/src/AcDream.App/Rendering/ClipPlaneSet.cs b/src/AcDream.App/Rendering/ClipPlaneSet.cs
index dc4f0bd8..44f636ee 100644
--- a/src/AcDream.App/Rendering/ClipPlaneSet.cs
+++ b/src/AcDream.App/Rendering/ClipPlaneSet.cs
@@ -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;
///
/// 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.
///
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();
- UseScissorFallback = useScissorFallback;
+ IsPlaneOverflow = isPlaneOverflow;
IsNothingVisible = isNothingVisible;
- ScissorNdcAabb = scissorNdcAabb;
}
- /// Number of active clip planes, 0..8. 0 ⇒ inspect
- /// and to decide between "draw the AABB" and "draw nothing".
+ /// Number of active clip planes, 0..8. 0 ⇒ inspect
+ /// and to decide between "draw unclipped" and "draw nothing".
public int Count => _planes?.Length ?? 0;
/// The active clip-space planes (nx, ny, 0, d). Empty when 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();
- /// True ⇒ the convex-plane budget was exceeded; gate on
- /// instead (draw the box). Always false when > 0 or when the region is empty.
- public bool UseScissorFallback { get; }
+ /// True ⇒ Count == 0 because the region needs more than
+ /// half-planes (or is multi-polygon); the consumer draws UNCLIPPED
+ /// (ClipFrameAssembler.AppendOutsideSlice 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 > 0 or when the region is empty.
+ public bool IsPlaneOverflow { get; }
/// True ⇒ the region is not visible at all; the consumer draws NOTHING.
- /// Mutually exclusive with , and only meaningful when Count == 0.
+ /// Mutually exclusive with , and only meaningful when Count == 0.
public bool IsNothingVisible { get; }
- /// NDC axis-aligned scissor box (minX, minY, maxX, maxY). Valid (min <= max) only when
- /// is true. For the empty/nothing-visible case it is a degenerate
- /// inverted box so naive scissoring still draws nothing.
- public Vector4 ScissorNdcAabb { get; }
-
- /// The "nothing is visible" sentinel: Count == 0, not a scissor fallback, draw nothing.
+ /// The "nothing is visible" sentinel: Count == 0, not a plane overflow, draw nothing.
public static ClipPlaneSet Empty { get; } =
- new(Array.Empty(), 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(), isPlaneOverflow: false, isNothingVisible: true);
///
/// 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 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 the file header for the
+ /// full rule.
///
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 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(), useScissorFallback: true, isNothingVisible: false,
- scissorNdcAabb: new Vector4(minX, minY, maxX, maxY));
-
- private static ClipPlaneSet Scissor(ReadOnlySpan 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(), isPlaneOverflow: true, isNothingVisible: false);
///
/// Return the polygon wound CCW with collinear vertices removed. The PortalVisibilityBuilder
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
index 5b5cfd54..0ab6da15 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
@@ -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,
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 1ef0e31e..583ba3f5 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -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 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
diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
index 900b79c1..29da7261 100644
--- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs
+++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
@@ -241,8 +241,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
ICamera camera,
FrustumPlanes? frustum = null,
uint? neverCullLandblockId = null,
- ReadOnlySpan clipPlanes = default,
- Vector4? ndcClipAabb = null,
IReadOnlySet? 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
// ----------------------------------------------------------------
+ ///
+ /// S3 landing hygiene (H2): frustum-only now. Retail never view-clips
+ /// terrain (see '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 — — 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.
+ ///
internal static void CollectVisibleCells(
HashSet destination,
uint landblockId,
Vector3 worldOrigin,
float zMin,
float zMax,
- FrustumPlanes? frustum,
- Matrix4x4 viewProjection,
- ReadOnlySpan 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 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 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 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 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)
diff --git a/src/AcDream.App/Rendering/WorldPassSurface.cs b/src/AcDream.App/Rendering/WorldPassSurface.cs
index 6905e995..f7d56d59 100644
--- a/src/AcDream.App/Rendering/WorldPassSurface.cs
+++ b/src/AcDream.App/Rendering/WorldPassSurface.cs
@@ -20,10 +20,12 @@ internal interface IRenderFrameGlState
/// and heavily tested — is written once for both backends.
///
/// Everything else those executors do is delegation to a renderer. What is
-/// left is exactly this: the clip-frame publication, the doorway scissor,
-/// gl_ClipDistance 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.
+/// left is exactly this: the clip-frame publication, gl_ClipDistance
+/// 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 's implementation doc.)
///
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.
///
- /// 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.
- ///
/// 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.
+ ///
+ /// 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
+ /// (GlWorldPassSurface) was deleted at Campaign V slice V11 —
+ /// nothing was left to size a reservation for.
///
- void PrepareClipFrame(int terrainUploadCount);
+ void PrepareClipFrame();
///
/// Enables every gl_ClipDistance slot.
///
/// 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 1.0
- /// ("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).
+ /// divergence: only portal_depth.vert writes gl_ClipDistance
+ /// 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 's
+ /// doc). It still writes 1.0 ("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).
///
void EnableClipDistances();
@@ -73,11 +80,10 @@ internal interface IWorldPassSurface
}
///
-/// 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 vkCmdClearAttachments. The raw-GL implementation this used to
-/// sit alongside (GlWorldPassSurface) 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 vkCmdClearAttachments. The raw-GL
+/// implementation this used to sit alongside (GlWorldPassSurface) was
+/// deleted at Campaign V slice V11.
///
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);
diff --git a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
index 4a46e908..2da09ff1 100644
--- a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
+++ b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
@@ -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,
diff --git a/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs b/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs
index cc1a57d7..85a20921 100644
--- a/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs
@@ -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)
diff --git a/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs b/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs
deleted file mode 100644
index 2d3028dd..00000000
--- a/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs
+++ /dev/null
@@ -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;
-
-///
-/// #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
-/// (LScape::draw 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, IWorldPassSurface.BeginScissor/EndScissor, 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 (WalkFrameDriver'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 (ProjectToClip → ClipToRegion →
-/// ClipPlaneSet.From) that the KEPT punch-fan clip still consumes
-/// (RetailPViewPassExecutor.DrawWalkPunchFan reads
-/// clipAssembly.OutsideViewSlices[activeViewIndex].Planes — see
-/// ClipFrameLayoutTests'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.
-///
-[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();
- foreach (var p in view.Polygons) parts.Add(p.Vertices.Length.ToString());
- return $"[{string.Join(",", parts)}]";
- }
-
- ///
- /// 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.
- ///
- 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 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;
- }
-
- /// Highest boundary y of the polygon at vertical line x (NaN when
- /// the line misses the polygon).
- 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;
- }
-}
diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
index ebff31e9..702c55ec 100644
--- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
@@ -159,7 +159,9 @@ public sealed class RetailPViewPassExecutorTests
/// ClearClipRouting + the old DrawLandscapeSliceLate leaf,
/// one call per active landscape view) is deleted — DrawLandscapeDynamicsPhase now calls
/// exactly once,
- /// unconditionally, with no loop of any kind around it. This
+ /// conditional on
+ /// (see
+ /// — the L1 pin), with no loop of any kind around it. This
/// Assert.Single alone proved insufficient at fix round 1 (K1):
/// it counts DISTINCT call-site offsets, so it stays green even with a
/// foreach wrapped around the one call site (the exact round-1
@@ -201,7 +203,14 @@ public sealed class RetailPViewPassExecutorTests
/// the compiled foreach 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 's
+ /// own note): reads only
+ /// br/brtrue/brfalse-family single-target branches —
+ /// it does not decode a compiled switch jump table, but no C# loop
+ /// construct (for/foreach/while/do) ever
+ /// compiles to one, so this pin's blind spot is not a loop shape it
+ /// could miss.
///
[Fact]
public void DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch()
@@ -283,6 +292,65 @@ public sealed class RetailPViewPassExecutorTests
&& branch.TargetOffset > drawOffset);
}
+ ///
+ /// 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
+ /// WeatherTurnFired getter call and the DrawWeatherOnce
+ /// call — so if (clipAssembly.OutsideViewSlices.Length != 0 &&
+ /// walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx); still
+ /// passes it: the compiler evaluates OutsideViewSlices.Length != 0
+ /// FIRST, so ITS OWN brfalse lands BEFORE the getter call's
+ /// offset — outside L1's window — while calls[c-1] is still the
+ /// getter (the added condition reads OutsideViewSlices, a
+ /// property getter, then .Length, a non-call ldlen, so no
+ /// OTHER call intervenes before the getter). This pin widens the window
+ /// to start at the call immediately before the whole gate —
+ /// ,
+ /// 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 Assert.Single: production has exactly one, the forward
+ /// brfalse/brfalse.s 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 if
+ /// (clipAssembly.OutsideViewSlices.Length != 0 &&
+ /// walkDriver.WeatherTurnFired) — the branch count in the window
+ /// goes from 1 to 2 and Assert.Single fails (see the commit body
+ /// for the exact recorded failure text).
+ ///
+ [Fact]
+ public void DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce()
+ {
+ MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
+ "DrawLandscapeDynamicsPhase",
+ BindingFlags.Instance | BindingFlags.NonPublic)!;
+ IReadOnlyList 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 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.");
+ }
+
///
/// S3 chunk 4 fix round 2 (L8): the identical loop-shape question K1
/// asked of 's call
diff --git a/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs b/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs
index c681890f..e51bdcec 100644
--- a/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/TerrainParticleCellVisibilityTests.cs
@@ -3,6 +3,19 @@ using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
+///
+/// S3 landing hygiene (H2):
+/// 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 (),
+/// and that caller never actually passed either (grep). The doorway-clip/scissor
+/// rejection cases this file used to pin (RejectsCellsOutsideDoorwayClipPlanes,
+/// RejectsCellsOutsideDoorwayScissorAabb, UnionsCellsFromEveryLandscapeSlice)
+/// 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.
+///
public sealed class TerrainParticleCellVisibilityTests
{
[Fact]
@@ -16,9 +29,7 @@ public sealed class TerrainParticleCellVisibilityTests
Vector3.Zero,
zMin: 0f,
zMax: 20f,
- frustum: null,
- Matrix4x4.Identity,
- ReadOnlySpan.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.Empty);
+ frustum);
Assert.Empty(cells);
}
-
- [Fact]
- public void CollectVisibleCells_RejectsCellsOutsideDoorwayClipPlanes()
- {
- var cells = new HashSet();
- 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();
-
- TerrainModernRenderer.CollectVisibleCells(
- cells,
- 0xA9B4FFFFu,
- Vector3.Zero,
- zMin: 0f,
- zMax: 20f,
- frustum: null,
- Matrix4x4.Identity,
- ReadOnlySpan.Empty,
- ndcClipAabb: new Vector4(-4f, -4f, -2f, -2f));
-
- Assert.Empty(cells);
- }
-
- [Fact]
- public void CollectVisibleCells_UnionsCellsFromEveryLandscapeSlice()
- {
- var cells = new HashSet();
-
- TerrainModernRenderer.CollectVisibleCells(
- cells,
- 0xA9B4FFFFu,
- Vector3.Zero,
- zMin: 0f,
- zMax: 20f,
- frustum: null,
- Matrix4x4.Identity,
- ReadOnlySpan.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.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);
- }
}