diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs
index ed186465..1970c8f6 100644
--- a/src/AcDream.App/Composition/FrameRootComposition.cs
+++ b/src/AcDream.App/Composition/FrameRootComposition.cs
@@ -368,15 +368,12 @@ internal sealed class FrameRootCompositionPhase
d.RenderDiagnosticLog);
IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
- var worldFramebufferSource =
- new SilkRetailPViewFramebufferSource(d.Window);
IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuFrameLifetime,
- live.ClipFrame,
- worldFramebufferSource);
+ live.ClipFrame);
var worldFrameEnvironment =
new RuntimeWorldFrameEnvironmentPreparation(
d.Options,
diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs
index a447d097..eb4c700c 100644
--- a/src/AcDream.App/Rendering/ClipFrame.cs
+++ b/src/AcDream.App/Rendering/ClipFrame.cs
@@ -1,39 +1,38 @@
// ClipFrame.cs
//
-// Phase U.3: the per-frame container for the SHARED per-frame clip data
-// consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert (UBO
-// binding=2). This is the "shared" half of the U.3 clip mechanism; the
-// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
-// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
-// buffer — it is NOT here.
+// Phase U.3: the per-frame container for the mesh SSBO clip-region table
+// (binding=2) that mesh_modern.vert reads. The per-instance slot index
+// buffer (SSBO binding=3) is PER-RENDERER and owned by each renderer
+// (WbDrawDispatcher / EnvCellRenderer), parallel to its instance buffer —
+// it is NOT here.
//
-// === The contract (both shader sides obey) ===================================
+// === The contract (the shader side obeys) =====================================
// binding=2 mesh SSBO holds an array of CellClip, one per "slot":
// struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; };
// std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16
// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0).
-// binding=2 terrain UBO holds the single OutsideView region:
-// layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; };
-// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes.
//
-// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a
-// terrain count of 0. Everything renders exactly as before. U.4 populates real
-// slots from a PortalVisibilityFrame (one CellClip per visible cell) and sets the
-// terrain OutsideView planes, then points each renderer's per-instance slot
-// buffer at the right slots.
+// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0).
+// Everything renders exactly as before. U.4 populates real slots from a
+// PortalVisibilityFrame (one CellClip per visible cell), then points each
+// renderer's per-instance slot buffer at the right slots.
+//
+// The one CONSUMER outside the mesh path that still reads a slot's packed
+// bytes directly is PortalDepthMaskRenderer's exit-seal/punch-fan draw
+// (S3 §8/§10 KEEP): it shares the SAME std140 144-byte layout
+// (TerrainUboBytes) and binding (TerrainClipUboBinding = 2) the walk's own
+// screen-space clip gate used to occupy before S3 chunk 4 fix round 2 (L3)
+// deleted that gate outright — the terrain and sky shaders no longer declare
+// any block at that binding.
//
// Pure CPU byte-packing. The GL upload machinery this file used to carry
-// alongside the packing (a per-flight-slot region SSBO + terrain UBO arena,
+// alongside the packing (a per-flight-slot region SSBO arena,
// reservation/upload-once bookkeeping, and their disposal) was deleted at
// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame
// in WorldPassSurface.cs) reads RegionBytes below and copies it into a frame
-// ring allocation instead, so nothing here owns a GPU resource anymore. S3
-// chunk 4 fix round 1 (K3) deleted SetTerrainClip (its only writer) — the
-// terrain UBO half of PrepareClipFrame's publication is gone with it;
-// WorldFrameSectionBinding.BindTerrainClip's existing zeroed-ring fallback
-// binds TerrainBytes's permanent all-zero state instead. The byte layout is
-// asserted by ClipFrameLayoutTests so a silent std430/std140 drift can't
-// reach the GPU.
+// ring allocation instead, so nothing here owns a GPU resource anymore. The
+// byte layout is asserted by ClipFrameLayoutTests so a silent std430 drift
+// can't reach the GPU.
using System;
using System.Numerics;
using System.Runtime.InteropServices;
@@ -41,15 +40,15 @@ using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
///
-/// Per-frame container for the SHARED clip data: the binding=2 mesh SSBO (one
-/// CellClip per slot, slot 0 reserved no-clip) and the binding=2 terrain
-/// UBO (the single OutsideView region). See the file header for the exact
-/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
-/// each renderer, not here.
+/// Per-frame container for the mesh SSBO clip-region table (binding=2, one
+/// CellClip per slot, slot 0 reserved no-clip). See the file header for
+/// the exact std430 byte layout and for /
+/// 's separate remaining purpose.
+/// Per-instance slot buffers (binding=3) are owned by each renderer, not here.
///
public sealed class ClipFrame : IDisposable
{
- // ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
+ // ---- Layout constants (mirror mesh_modern.vert) ----
/// Max planes per clip region — matches the shader's planes[8]
/// and GL's guaranteed GL_MAX_CLIP_DISTANCES >= 8.
@@ -63,17 +62,24 @@ public sealed class ClipFrame : IDisposable
/// count + 3 pad uints).
public const int CellClipPlanesOffset = 16;
- /// std140 size of the terrain UBO block: int count padded to 16, then
- /// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by
- /// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.
+ /// std140 size of PortalDepthMaskRenderer's exit-seal/punch-fan
+ /// clip block: int count padded to 16, then 8 × 16 (vec4 planes) = 144
+ /// bytes. Same number as the mesh SSBO stride by coincidence of the
+ /// 16-byte vec4 rule, but a DIFFERENT layout family. S3 chunk 4 fix round 2
+ /// (L3) retired this constant's other former use — the walk's own
+ /// screen-space clip gate, which terrain_modern.vert and sky.vert used to
+ /// declare at the same binding before the gate itself was deleted.
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
- /// UBO binding index for the terrain OutsideView clip region
- /// (terrain_modern.vert binding=2). Read directly by both the RHI world-pass
- /// section binder (WorldFrameSectionBinding.BindTerrainClip) and
- /// PortalDepthMaskRenderer, so unlike the mesh SSBO binding (which the
- /// RHI arm addresses through its own GpuBindingModel.StorageClipRegions
- /// instead) this one is still genuinely shared.
+ /// UBO binding index for PortalDepthMaskRenderer's
+ /// exit-seal/punch-fan clip block (portal_depth.vert binding=2) — the
+ /// one production consumer left at this binding after S3 chunk 4 fix
+ /// round 2 (L3) deleted the walk's own screen-space clip gate (which used
+ /// to share this same binding number in terrain_modern.vert and
+ /// sky.vert). Unlike the mesh SSBO binding (which the RHI arm addresses
+ /// through its own GpuBindingModel.StorageClipRegions instead) this
+ /// one stays a restated raw constant — see
+ /// VulkanPipelineLayouts.UniformTerrainClip's matching doc.
public const uint TerrainClipUboBinding = 2;
// ---- CPU-side state ------------------------------------------------------
@@ -82,9 +88,6 @@ public sealed class ClipFrame : IDisposable
private byte[] _regionBytes;
private int _slotCount;
- // Packed std140 bytes for the terrain UBO (always TerrainUboBytes long).
- private readonly byte[] _terrainBytes = new byte[TerrainUboBytes];
-
///
/// The GL arm's per-flight-slot region/terrain buffer rings this used to
/// report on were deleted at Campaign V slice V11: every publication now
@@ -98,14 +101,12 @@ public sealed class ClipFrame : IDisposable
{
_regionBytes = regionBytes;
_slotCount = slotCount;
- // Terrain defaults to count 0 (ungated). _terrainBytes is already all
- // zeros, which encodes count=0 + zeroed (unused) planes.
}
///
- /// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain
- /// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4
- /// replaces this with a frame built from real portal visibility.
+ /// The U.3 default frame: exactly slot 0 (no-clip, count 0). The whole
+ /// scene renders ungated — identical to pre-U.3. U.4 replaces this with a
+ /// frame built from real portal visibility.
///
public static ClipFrame NoClip()
{
@@ -120,10 +121,10 @@ public sealed class ClipFrame : IDisposable
///
/// Phase U.4: reset this frame back to the NoClip state — exactly slot 0
- /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new
- /// frame. The single long-lived _clipFrame in GameWindow is reset +
- /// re-packed every frame by , then published
- /// through one frame-ring allocation per section (see
+ /// (no-clip, count 0) — WITHOUT allocating a new frame. The single
+ /// long-lived _clipFrame in GameWindow is reset + re-packed every
+ /// frame by , then published through one
+ /// frame-ring allocation per section (see
/// RhiWorldPassSurface.PrepareClipFrame).
///
public void Reset()
@@ -136,9 +137,6 @@ public sealed class ClipFrame : IDisposable
EnsureRegionCapacity(CellClipStrideBytes);
Array.Clear(_regionBytes, 0, CellClipStrideBytes);
_slotCount = 1;
-
- // Terrain back to count 0 (ungated) until SetTerrainClip is called again.
- Array.Clear(_terrainBytes);
}
///
@@ -230,18 +228,13 @@ public sealed class ClipFrame : IDisposable
count * sizeof(float) * 4));
}
- // S3 chunk 4 fix round 1 (K3): SetTerrainClip (the terrain OutsideView
- // writer) is deleted — no production caller has written a non-empty
- // terrain clip since S3 chunk 4's original round deleted the walk's
- // per-outside-view-slice sky/terrain-clip loop, and fix round 1's K4
- // deletes the sky loop's own now-dead SetTerrainClip wrapper too. Every
- // KEEP clip (exit seals, punch fans) already read their planes through
- // GetSlotPlanes above / ClipViewSlice.Planes directly, never through
- // this terrain-specific slot. _terrainBytes therefore stays at its
- // NoClip/Reset default (count 0, all zero) for the life of a frame —
- // see WorldPassSurface.cs's PrepareClipFrame for how the TerrainClip
- // UBO section still gets bound as that all-zero disabled block even
- // with nothing left to publish it.
+ // S3 chunk 4 fix round 2 (L3): the walk's screen-space clip gate is gone
+ // — terrain_modern.vert and sky.vert no longer declare a block at this
+ // binding, and the last CPU-side publisher of one was already removed a
+ // round earlier. Every KEEP clip (exit seals, punch fans) reads its
+ // planes through GetSlotPlanes above / ClipViewSlice.Planes directly,
+ // never through a terrain-specific slot — this class carries no terrain
+ // state at all any more.
///
/// No-op: this container owns no GPU resource of its own on the RHI arm —
@@ -308,15 +301,9 @@ public sealed class ClipFrame : IDisposable
internal ReadOnlySpan RegionBytes =>
_regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes);
- /// The packed std140 terrain-clip block. See .
- internal ReadOnlySpan TerrainBytes => _terrainBytes;
-
// ---- Test seams ----------------------------------------------------------
/// Test seam: the packed std430 region bytes (slot 0..SlotCount-1).
/// Read-only snapshot used by ClipFrameLayoutTests to assert the byte layout.
internal ReadOnlySpan RegionBytesForTest => RegionBytes;
-
- /// Test seam: the packed std140 terrain UBO bytes.
- internal ReadOnlySpan TerrainBytesForTest => TerrainBytes;
}
diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
index 026dd683..7f8f6126 100644
--- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs
+++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
@@ -19,7 +19,15 @@ using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// How the landscape-through-outside_view pass should be interpreted.
+/// S3 chunk 4 fix round 2 (L2): retired from the walk's own clip assembly —
+/// no longer tracks a terrain clip mode at
+/// all, because the walk draws the sky, terrain and weather unclipped, one
+/// call each, matching retail (LScape::draw never installs a view
+/// before any of the three). The type stays only because
+/// '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).
///
public enum TerrainClipMode
{
@@ -61,8 +69,6 @@ public sealed class ClipFrameAssembly
public int OutdoorSlot { get; internal set; }
public bool OutdoorVisible { get; internal set; }
- public TerrainClipMode TerrainMode { get; internal set; }
- public Vector4 TerrainScissorNdcAabb { get; internal set; }
public bool HasOutsideView { get; internal set; }
public Vector4 OutsideViewNdcAabb { get; internal set; }
@@ -279,8 +285,6 @@ public static class ClipFrameAssembler
assembly.SetOutsideViewSlices(assembly.CopySlices(slices));
assembly.OutdoorSlot = 0;
assembly.OutdoorVisible = true;
- assembly.TerrainMode = TerrainClipMode.Scissor;
- assembly.TerrainScissorNdcAabb = fullScreen;
assembly.HasOutsideView = true;
assembly.OutsideViewNdcAabb = fullScreen;
assembly.OutsidePlaneCount = 0;
@@ -290,8 +294,6 @@ public static class ClipFrameAssembler
{
assembly.OutdoorSlot = 0;
assembly.OutdoorVisible = false;
- assembly.TerrainMode = TerrainClipMode.Skip;
- assembly.TerrainScissorNdcAabb = Vector4.Zero;
assembly.HasOutsideView = false;
assembly.OutsideViewNdcAabb = Vector4.Zero;
assembly.OutsidePlaneCount = 0;
@@ -370,53 +372,36 @@ public static class ClipFrameAssembler
foreach (var poly in pvFrame.OutsideView.Polygons)
{
- var cps = ClipPlaneSet.From(poly);
- if (cps.IsNothingVisible)
- continue;
-
- int slot;
- Vector4[] planes;
- if (cps.Count > 0)
- {
- planes = cps.PlaneArray;
- slot = frame.AppendSlot(planes);
- if (cps.Count > outsideMaxPlaneCount)
- outsideMaxPlaneCount = cps.Count;
- }
- else
- {
- planes = System.Array.Empty();
- slot = 0;
- outsideHasScissorFallback = true;
- scissorFallbacks++;
- }
-
- outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
+ AppendOutsideSlice(
+ frame,
+ poly,
+ outsideSlicesList,
+ ref outsideMaxPlaneCount,
+ ref outsideHasScissorFallback,
+ ref scissorFallbacks);
}
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
bool outdoorVisible = outsideViewSlices.Length > 0;
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
- TerrainClipMode terrainMode = !outdoorVisible
- ? TerrainClipMode.Skip
- : (outsideHasScissorFallback ? TerrainClipMode.Scissor : TerrainClipMode.Planes);
Vector4 outsideViewNdcAabb = outdoorVisible
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
: Vector4.Zero;
- Vector4 terrainScissor = terrainMode == TerrainClipMode.Scissor
- ? outsideViewNdcAabb
- : Vector4.Zero;
assembly.SetOutsideViewSlices(outsideViewSlices);
assembly.OutdoorSlot = outdoorSlot;
assembly.OutdoorVisible = outdoorVisible;
- assembly.TerrainMode = terrainMode;
- assembly.TerrainScissorNdcAabb = terrainScissor;
assembly.HasOutsideView = outdoorVisible;
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
- assembly.OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0;
+ // S3 chunk 4 fix round 2 (L2): equivalent to the deleted
+ // terrainMode == TerrainClipMode.Planes gate without needing
+ // TerrainClipMode at all — a scissor-fallback slice can only exist
+ // when outdoorVisible is already true (both branches above append a
+ // slice), so this reduces to the same three cases (no slices -> 0;
+ // any scissor fallback -> 0; all-planes -> outsideMaxPlaneCount).
+ assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
assembly.ScissorFallbacks = scissorFallbacks;
return assembly;
}
@@ -509,55 +494,82 @@ public static class ClipFrameAssembler
if (poly.MaxY > unionMaxY) unionMaxY = poly.MaxY;
}
- var cps = ClipPlaneSet.From(poly);
- if (cps.IsNothingVisible)
- continue;
-
- int slot;
- Vector4[] planes;
- if (cps.Count > 0)
- {
- planes = cps.PlaneArray;
- slot = frame.AppendSlot(planes);
- if (cps.Count > outsideMaxPlaneCount)
- outsideMaxPlaneCount = cps.Count;
- }
- else
- {
- planes = System.Array.Empty();
- slot = 0;
- outsideHasScissorFallback = true;
- scissorFallbacks++;
- }
-
- outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
+ AppendOutsideSlice(
+ frame,
+ poly,
+ outsideSlicesList,
+ ref outsideMaxPlaneCount,
+ ref outsideHasScissorFallback,
+ ref scissorFallbacks);
}
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
bool outdoorVisible = outsideViewSlices.Length > 0;
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
- TerrainClipMode terrainMode = !outdoorVisible
- ? TerrainClipMode.Skip
- : (outsideHasScissorFallback ? TerrainClipMode.Scissor : TerrainClipMode.Planes);
Vector4 outsideViewNdcAabb = outdoorVisible
? new Vector4(unionMinX, unionMinY, unionMaxX, unionMaxY)
: Vector4.Zero;
- Vector4 terrainScissor = terrainMode == TerrainClipMode.Scissor
- ? outsideViewNdcAabb
- : Vector4.Zero;
assembly.SetOutsideViewSlices(outsideViewSlices);
assembly.OutdoorSlot = outdoorSlot;
assembly.OutdoorVisible = outdoorVisible;
- assembly.TerrainMode = terrainMode;
- assembly.TerrainScissorNdcAabb = terrainScissor;
assembly.HasOutsideView = outdoorVisible;
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
- assembly.OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0;
+ // S3 chunk 4 fix round 2 (L2): see Assemble's matching comment — the
+ // same reduction applies here.
+ assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
assembly.ScissorFallbacks = scissorFallbacks;
}
+ ///
+ /// S3 chunk 4 fix round 2 (L4): the ONE place a single outside_view
+ /// becomes an appended clip slot plus its
+ /// — both 's
+ /// outside_view loop and 's
+ /// outside_view loop call this instead of each carrying its own copy of
+ /// the plane/scissor-fallback bookkeeping (a prior round's three-lens
+ /// review found the duplication let a CPU/GPU equivalence pin exercise
+ /// one copy while production ran the other — see ClipFrameLayoutTests'
+ /// punch-fan pin). A polygon entirely outside every plane
+ /// () appends nothing and
+ /// returns false; the caller's own loop simply moves to the next
+ /// polygon either way, so the return value only matters to a caller that
+ /// needs to know.
+ ///
+ private static bool AppendOutsideSlice(
+ ClipFrame frame,
+ in ViewPolygon poly,
+ List outsideSlicesList,
+ ref int maxPlaneCount,
+ ref bool hasScissorFallback,
+ ref int scissorFallbacks)
+ {
+ var cps = ClipPlaneSet.From(poly);
+ if (cps.IsNothingVisible)
+ return false;
+
+ int slot;
+ Vector4[] planes;
+ if (cps.Count > 0)
+ {
+ planes = cps.PlaneArray;
+ slot = frame.AppendSlot(planes);
+ if (cps.Count > maxPlaneCount)
+ maxPlaneCount = cps.Count;
+ }
+ else
+ {
+ planes = System.Array.Empty();
+ slot = 0;
+ hasScissorFallback = true;
+ scissorFallbacks++;
+ }
+
+ outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
+ return true;
+ }
+
private static Vector4 AabbOf(ViewPolygon poly) =>
new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY);
diff --git a/src/AcDream.App/Rendering/Gpu/GpuBindingModel.cs b/src/AcDream.App/Rendering/Gpu/GpuBindingModel.cs
index 51ee71b7..618848cf 100644
--- a/src/AcDream.App/Rendering/Gpu/GpuBindingModel.cs
+++ b/src/AcDream.App/Rendering/Gpu/GpuBindingModel.cs
@@ -93,7 +93,8 @@ internal static class GpuBindingModel
/// is 144 bytes: too large for the 96-byte push-constant block (and for
/// Vulkan's guaranteed 128-byte ceiling), and there is no RHI verb for setting
/// a uniform array. A small uniform buffer is the Vulkan-legal home. Binding 2
- /// is taken by the terrain clip block, so this is 3.
+ /// is taken by PortalDepthMaskRenderer's exit-seal/punch-fan clip block
+ /// (), so this is 3.
///
public const uint UniformTerrainTiling = 3;
@@ -107,8 +108,8 @@ internal static class GpuBindingModel
/// block, so a loose uniform mat4 uSkyView; is unspellable, and this
/// set is 256 bytes in std140 — three matrices alone are twice the entire
/// 96-byte push-constant block. A uniform buffer is the only legal home.
- /// Bindings 1, 2 and 3 are taken by SceneLighting, the terrain clip block
- /// and terrain tiling, so this is 4.
+ /// Bindings 1, 2 and 3 are taken by SceneLighting, PortalDepthMaskRenderer's
+ /// clip block and terrain tiling, so this is 4.
///
public const uint UniformSkyParams = 4;
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanPipelineLayouts.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanPipelineLayouts.cs
index 3da35bd7..08e8ff20 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanPipelineLayouts.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanPipelineLayouts.cs
@@ -371,13 +371,17 @@ internal static unsafe class VulkanPipelineLayouts
}
///
- /// The terrain screen-space clip block's uniform binding.
+ /// PortalDepthMaskRenderer's exit-seal/punch-fan clip block's uniform
+ /// binding (portal_depth.vert).
///
/// does not name this number — it only
- /// records, twice, that "binding 2 is taken by the terrain clip block" while
- /// explaining why terrain tiling is 3 and sky params are 4. The number itself
- /// has been pinned by terrain_modern.vert and sky.vert since
- /// Phase U.3. Restating it here rather than promoting it into the frozen
+ /// records that binding 2 is taken here while explaining why terrain tiling
+ /// is 3 and sky params are 4. Pinned since Phase U.3; S3 chunk 4 fix round 2
+ /// (L3) deleted the walk's OWN screen-space clip gate, which used to share
+ /// this exact binding number in terrain_modern.vert and
+ /// sky.vert — neither shader declares a block at this binding any
+ /// more, leaving portal_depth.vert the one production consumer.
+ /// Restating the number here rather than promoting it into the frozen
/// binding model keeps slice V6i-2 out of the pinned contract; a later slice
/// entitled to change §3.3 should move it.
///
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
index 5e4a5819..5beb3de2 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
@@ -38,11 +38,13 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// Scissor does NOT flip with the viewport. The V3 audit called
/// this out as a concrete acceptance item (plan §4.10, item 1):
/// vkCmdSetScissor is always top-left-origin regardless of viewport sign,
-/// and NdcScissorRect.ToPixels emits GL bottom-left rectangles. So the
-/// scissor rectangle needs an explicit Y flip against the attachment height,
-/// while the viewport needs none. Getting this wrong shows up as a doorway
-/// aperture clipped from the wrong edge — visible, but only in a scene that has
-/// one.
+/// while every caller of passes a GL-convention
+/// bottom-left rectangle (the pass encoder's own full-attachment default,
+/// set once at pass begin — S3 chunk 4 fix round 2 deleted the one caller that
+/// used to pass anything narrower). So the scissor rectangle needs an explicit
+/// Y flip against the attachment height, while the viewport needs none.
+/// Getting this wrong shows up as a doorway aperture clipped from the wrong
+/// edge — visible, but only in a scene that has one.
///
/// Clip space itself needs nothing. acdream's cameras already build
/// projections with Matrix4x4.CreatePerspectiveFieldOfView, which is the
diff --git a/src/AcDream.App/Rendering/NdcScissorRect.cs b/src/AcDream.App/Rendering/NdcScissorRect.cs
deleted file mode 100644
index f26eb0c6..00000000
--- a/src/AcDream.App/Rendering/NdcScissorRect.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// NdcScissorRect.cs
-//
-// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound).
-// The scissor that brackets a landscape/doorway slice is a fallback BOUND on
-// the slice's view region (AD-17 in the divergence register): it must CONTAIN
-// every fragment the per-fragment plane clip would keep. Under-inclusion is
-// the bug class — the #130 doorway top-edge background strip was this box
-// computed as Floor(origin) + Ceiling(size), whose far edge
-// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge
-// at unlucky fractional alignments, scissoring away the aperture's top/right
-// pixel row for the whole slice (sky, terrain, statics, weather) while the
-// seal still stamps it — a strip of clear color no later pass can fill.
-//
-// Correct outer bound: floor both mins, ceil both maxes, width = difference.
-// A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in
-// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒
-// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So
-// [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by
-// at most one pixel per edge — safe per AD-17's doctrine (the wall shell /
-// plane clip repaints or kills the surplus).
-using System;
-using System.Numerics;
-
-namespace AcDream.App.Rendering;
-
-public static class NdcScissorRect
-{
- /// Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a
- /// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to
- /// the screen so a region extending past an edge still yields a valid box.
- /// Width/height are at least 1.
- public static (int X, int Y, int Width, int Height) ToPixels(
- Vector4 ndcAabb, int fbWidth, int fbHeight)
- {
- float nx0 = Math.Clamp(ndcAabb.X, -1f, 1f);
- float ny0 = Math.Clamp(ndcAabb.Y, -1f, 1f);
- float nx1 = Math.Clamp(ndcAabb.Z, -1f, 1f);
- float ny1 = Math.Clamp(ndcAabb.W, -1f, 1f);
- int px0 = (int)MathF.Floor((nx0 * 0.5f + 0.5f) * fbWidth);
- int py0 = (int)MathF.Floor((ny0 * 0.5f + 0.5f) * fbHeight);
- int px1 = (int)MathF.Ceiling((nx1 * 0.5f + 0.5f) * fbWidth);
- int py1 = (int)MathF.Ceiling((ny1 * 0.5f + 0.5f) * fbHeight);
- return (px0, py0, Math.Max(1, px1 - px0), Math.Max(1, py1 - py0));
- }
-}
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
index 100a8116..5b5cfd54 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
@@ -6,29 +6,14 @@ using AcDream.App.Rendering.Wb;
using AcDream.Core.Rendering;
using AcDream.Core.Vfx;
using AcDream.Core.World;
-using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
-internal readonly record struct RetailPViewFramebufferSize(int Width, int Height);
-
-internal interface IRetailPViewFramebufferSource
-{
- RetailPViewFramebufferSize Capture();
-}
-
-internal sealed class SilkRetailPViewFramebufferSource(IWindow window) :
- IRetailPViewFramebufferSource
-{
- private readonly IWindow _window = window
- ?? throw new ArgumentNullException(nameof(window));
-
- public RetailPViewFramebufferSize Capture()
- {
- var size = _window.FramebufferSize;
- return new RetailPViewFramebufferSize(size.X, size.Y);
- }
-}
+// S3 chunk 4 fix round 2 (L2): RetailPViewFramebufferSize / IRetailPViewFramebufferSource
+// / SilkRetailPViewFramebufferSource are deleted — their only consumer was
+// RhiWorldPassSurface.BeginScissor's NDC-to-pixel conversion, and BeginScissor
+// itself is deleted in the same round (no producer of a narrowed scissor
+// rectangle remains anywhere in the walk).
internal sealed class RetailPViewCellSource : IRetailPViewCellSource
{
@@ -184,9 +169,10 @@ public RetailPViewPassExecutor(
/// arg2==1 runs AFTER LScape::draw's landblock loop
/// finishes (@0x00506396), not once per active landscape view. This call
/// is UNCLIPPED and sets no scissor — S3 chunk 4 (§10.2) deleted the
- /// former per-outside-view-slice loop (SetTerrainClip +
- /// ClearClipRouting + the old DrawLandscapeSliceLate leaf,
- /// one call per active landscape view) that used to run before this
+ /// former per-outside-view-slice loop (the walk's own screen-space
+ /// terrain-clip writer, ClearClipRouting, and the old
+ /// DrawLandscapeSliceLate leaf, one call per active landscape
+ /// view) that used to run before this
/// call and re-submit the rain mesh once per doorway aperture; this is
/// now the ONLY weather call site, matching retail's ONE unclipped
/// GameSky::Draw(sky,1).
@@ -447,16 +433,28 @@ public RetailPViewPassExecutor(
return submitted;
}
- // S3 chunk 4 fix round 1 (K4): BeginDoorwayScissor and the
- // EnableClipDistances wrapper are deleted — their only caller was
- // DrawWalkSky's per-outside-view-slice loop (RetailPViewPassExecutor.
- // WalkLeaf.cs), itself deleted by the same fix (retail draws the sky
- // ONCE, unclipped, exactly like the terrain and the weather — see
- // DrawWalkSky's own doc comment). _surface.BeginScissor/EndScissor and
- // IWorldPassSurface.EnableClipDistances stay: RhiWorldPassSurface.
- // ClearInteriorDepth still ends an active scissor, and
- // WorldScenePassExecutor (the separate flat-world path, out of this
- // chunk's scope) still calls EnableClipDistances directly.
+ // S3 chunk 4 fix round 1 (K4): the sky's own doorway-scissor bracket and
+ // the EnableClipDistances wrapper around it are deleted — their only
+ // caller was DrawWalkSky's per-outside-view-slice loop
+ // (RetailPViewPassExecutor.WalkLeaf.cs), itself deleted by the same fix
+ // (retail draws the sky ONCE, unclipped, exactly like the terrain and
+ // the weather — see DrawWalkSky's own doc comment).
+ //
+ // S3 chunk 4 fix round 2 (L2): round 1's own comment here asserted that
+ // the interior depth clear still had a live scissor to end at that
+ // point — a mechanism that no longer existed ANYWHERE by then (K4 had
+ // already deleted the sky's own scissor bracket, the last production
+ // producer of a narrowed rectangle). IWorldPassSurface.BeginScissor/
+ // EndScissor and their RhiWorldPassSurface bodies are deleted outright
+ // in this round; the interior depth clear no longer ends anything
+ // because nothing narrows a rectangle any more — the pass encoder sets
+ // the full-attachment scissor once, at pass begin
+ // (VulkanGpuPassEncoder.cs:87), and it stays that way for the life of
+ // the pass. IWorldPassSurface.EnableClipDistances/DisableClipDistances
+ // stay: WorldScenePassExecutor (the separate flat-world path, out of
+ // this chunk's scope) still calls EnableClipDistances directly, and the
+ // KEEP clips (exit seals, punch fans) bracket their own draws with
+ // Enable/DisableClipDistances below.
private void DisableClipDistances() => _surface.DisableClipDistances();
}
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index cbde681f..1ef0e31e 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -639,8 +639,9 @@ internal sealed class RetailPViewRenderer
}
// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
- // (SetTerrainClip + ClearClipRouting + DrawLandscapeSliceLate, one
- // call per active landscape view) is DELETED — retail draws the
+ // (the walk's own screen-space terrain-clip writer + ClearClipRouting
+ // + 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
diff --git a/src/AcDream.App/Rendering/Shaders/sky.vert b/src/AcDream.App/Rendering/Shaders/sky.vert
index 28a94d1a..d984d0f7 100644
--- a/src/AcDream.App/Rendering/Shaders/sky.vert
+++ b/src/AcDream.App/Rendering/Shaders/sky.vert
@@ -85,32 +85,18 @@ layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
vec4 uCameraAndTime;
};
-// === Phase W Stage 4: sky/weather portal clip (the OutsideView region) ========
-// The sky + weather (rain cylinder) meshes are "the outside seen through a
-// doorway" — retail draws them as part of LScape, clipped to the exit-portal
-// region (PView::DrawCells @ 0x005a4840). acdream gates them with the SAME
-// binding=2 TerrainClip UBO the terrain shader reads (ClipFrame.SetTerrainClip →
-// the OutsideView convex planes). The planes are SCREEN-SPACE (NDC) half-spaces
-// encoded as clip-space planes (nx, ny, 0, dw) with the test
-// dot(plane, gl_Position) >= 0. After the perspective divide that reduces to
-// nx*ndcX + ny*ndcY + dw >= 0 — INDEPENDENT of the projection matrix. So the same
-// plane set clips the sky correctly even though the sky uses its OWN dome
-// projection (uSkyProjection / uSkyView, translation-zeroed) rather than the
-// camera view-proj. uTerrainClipCount == 0 (outdoor / no exit portal visible)
-// ungates the sky entirely (the second loop sets all 8 distances to +1.0 ⇒
-// full-screen sky, bit-identical to pre-Stage-4). Host enables GL_CLIP_DISTANCE0..7
-// only around the sky/weather draws.
-layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
- int uTerrainClipCount;
- vec4 uTerrainClipPlanes[8];
-};
-
-// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
-// (mirrors terrain_modern.vert). Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8.
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
+// Retail draws the sky ONCE per frame, UNCLIPPED (LScape::draw @0x00506330
+// -> GameSky::Draw(sky,0) @0x0050633c, before draw_check_blocks — no view is
+// ever installed for it, and the landscape itself is view-CULLED per cell,
+// never GPU-clipped, RenderDeviceD3D::DrawBlock @0x005a17c0). S3 chunk 4 fix
+// round 2 (L3/L5) deletes the screen-space clip UBO and gl_ClipDistance
+// writes this shader used to declare at binding=2 to gate the sky/weather to
+// a doorway aperture — that mechanism's only CPU-side writer was already
+// deleted at fix round 1's K3, and its own doorway scissor bracket at fix
+// round 1's K4; an interior root's aperture exactness comes from the depth
+// clear, the exit seals, and the interior repaint instead (WalkFrameDriver's
+// interior turn draws the real cell geometry back over whatever the
+// unclipped sky/terrain painted through the doorway).
out vec2 vTex;
out vec3 vTint;
@@ -165,17 +151,4 @@ void main() {
float fogEnd = uFogParams.y;
float span = max(fogEnd - fogStart, 1e-3);
vFogFactor = clamp((fogEnd - dist) / span, 0.0, 1.0);
-
- // Phase W Stage 4: clip the sky/weather to the OutsideView (doorway) region.
- // With uTerrainClipCount == 0 (outdoor / no exit portal in view) the first loop
- // is skipped and the second sets all 8 distances to +1.0 ⇒ no clipping ⇒
- // full-screen sky. Indoors with an exit portal visible, the OutsideView planes
- // confine the sky to the doorway opening — exactly, per-fragment, matching the
- // terrain (no scissor approximation). plane.z is 0 (a screen-space slab), so the
- // sky's depth / dome radius is irrelevant. gl_Position here is the sky's own
- // dome-projected clip position; the NDC-plane test is projection-independent.
- for (int i = 0; i < uTerrainClipCount; ++i)
- gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
- for (int i = uTerrainClipCount; i < 8; ++i)
- gl_ClipDistance[i] = 1.0;
}
diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
index 699a7c54..842ec7f5 100644
--- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
+++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
@@ -311,7 +311,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "9102b156bd4fd667831353640b2feee2ddde66e88579a4e425566c9b67304b64",
+ "sourceSha256": "51b42232202cd005439e95c50fd325a3849359b3357ab7550af9a4bb404279e4",
"compiled": true
},
{
@@ -327,7 +327,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "882979f63c858760e977ecafb708416ab01f7adabe4bb38c0630020c087f1c28",
+ "sourceSha256": "42d1dbd0dbd950862e8672f79a3f429f771bae55fd2b5721c3e551a0ea9563b6",
"compiled": true
},
{
@@ -343,7 +343,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "a3f8592d482793622f7a69c08f8ba828a4e071f2fb5e1763f865be84797bceb7",
+ "sourceSha256": "faf6855222cb2b09da6697c93a93f35cbc57c27bbc124055a6d235c72ef1a23a",
"compiled": true
},
{
diff --git a/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv
index e69c1a43..19ec723c 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv
index 171b4460..0cc9e4d3 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/terrain_atmospheric.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.vert.spv
index bbf61fad..18339f79 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert b/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert
index 706a085b..fe8021f1 100644
--- a/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert
+++ b/src/AcDream.App/Rendering/Shaders/terrain_atmospheric.vert
@@ -40,33 +40,15 @@ layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
vec4 uCameraAndTime;
};
-// === Phase U.3: terrain screen-space clip gate (OutsideView region) ===========
-// Terrain is a single global region (the OutsideView), so it needs one set of
-// clip planes, not a per-instance slot table like the mesh shader. A std140 UBO
-// at binding=2 carries it. The UBO binding namespace is distinct from the SSBO
-// binding namespace, so this does NOT collide with the mesh shader's SSBO
-// binding=2 — and within THIS shader binding=1 (SceneLighting) is the only other
-// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
-// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
-// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
-//
-// Campaign V slice V6i-2: ACDREAM_UBO_SET is what puts this in set 1 under the
-// Vulkan dialect and expands to nothing under GL. Omitting it left the block at
-// set 0 binding 2, which the storage layout declares as a STORAGE buffer — see
-// plan §5.5.12 finding 2, measured on the committed SPIR-V rather than inferred.
-// sky.vert declares the SAME block correctly and is the precedent.
-layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
- int uTerrainClipCount;
- vec4 uTerrainClipPlanes[8];
-};
-
-// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal.
-// Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8. Host enables GL_CLIP_DISTANCE0..7
-// once at startup; unused planes are set to +1.0 below so they pass everything.
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
+// Retail never view-clips terrain — LScape::draw draws whole landblocks, and
+// the walk's own CellInView admission already decided which cells reach here
+// (RenderDeviceD3D::DrawBlock @0x005a17c0; S3 chunk 3's own findings). S3
+// chunk 4 fix round 2 (L3/L5) deletes the Phase U.3 terrain screen-space clip
+// gate this shader used to declare at binding=2 along with its
+// gl_ClipDistance writes — that mechanism's only CPU-side writer was already
+// deleted at fix round 1's K3; an interior root's aperture exactness comes
+// from the depth clear, the exit seals, and the interior repaint instead
+// (WalkFrameDriver's interior turn).
out vec2 vBaseUV;
out vec3 vWorldNormal;
@@ -199,12 +181,4 @@ void main() {
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
gl_Position = uViewProjection * vec4(terrainPos, 1.0);
-
- // Phase U.3: terrain clip gate against the single OutsideView region. With
- // uTerrainClipCount == 0 (U.3 default) the first loop is skipped and the
- // second sets all 8 distances to +1.0 ⇒ no clipping ⇒ identical terrain.
- for (int i = 0; i < uTerrainClipCount; ++i)
- gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
- for (int i = uTerrainClipCount; i < 8; ++i)
- gl_ClipDistance[i] = 1.0;
}
diff --git a/src/AcDream.App/Rendering/Shaders/terrain_modern.vert b/src/AcDream.App/Rendering/Shaders/terrain_modern.vert
index dbf24f40..b613370e 100644
--- a/src/AcDream.App/Rendering/Shaders/terrain_modern.vert
+++ b/src/AcDream.App/Rendering/Shaders/terrain_modern.vert
@@ -38,33 +38,15 @@ layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
vec4 uCameraAndTime;
};
-// === Phase U.3: terrain screen-space clip gate (OutsideView region) ===========
-// Terrain is a single global region (the OutsideView), so it needs one set of
-// clip planes, not a per-instance slot table like the mesh shader. A std140 UBO
-// at binding=2 carries it. The UBO binding namespace is distinct from the SSBO
-// binding namespace, so this does NOT collide with the mesh shader's SSBO
-// binding=2 — and within THIS shader binding=1 (SceneLighting) is the only other
-// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
-// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
-// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
-//
-// Campaign V slice V6i-2: ACDREAM_UBO_SET is what puts this in set 1 under the
-// Vulkan dialect and expands to nothing under GL. Omitting it left the block at
-// set 0 binding 2, which the storage layout declares as a STORAGE buffer — see
-// plan §5.5.12 finding 2, measured on the committed SPIR-V rather than inferred.
-// sky.vert declares the SAME block correctly and is the precedent.
-layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
- int uTerrainClipCount;
- vec4 uTerrainClipPlanes[8];
-};
-
-// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal.
-// Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8. Host enables GL_CLIP_DISTANCE0..7
-// once at startup; unused planes are set to +1.0 below so they pass everything.
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
+// Retail never view-clips terrain — LScape::draw draws whole landblocks, and
+// the walk's own CellInView admission already decided which cells reach here
+// (RenderDeviceD3D::DrawBlock @0x005a17c0; S3 chunk 3's own findings). S3
+// chunk 4 fix round 2 (L3/L5) deletes the Phase U.3 terrain screen-space clip
+// gate this shader used to declare at binding=2 along with its
+// gl_ClipDistance writes — that mechanism's only CPU-side writer was already
+// deleted at fix round 1's K3; an interior root's aperture exactness comes
+// from the depth clear, the exit seals, and the interior repaint instead
+// (WalkFrameDriver's interior turn).
out vec2 vBaseUV;
out vec3 vWorldNormal;
@@ -180,12 +162,4 @@ void main() {
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
gl_Position = uViewProjection * vec4(terrainPos, 1.0);
-
- // Phase U.3: terrain clip gate against the single OutsideView region. With
- // uTerrainClipCount == 0 (U.3 default) the first loop is skipped and the
- // second sets all 8 distances to +1.0 ⇒ no clipping ⇒ identical terrain.
- for (int i = 0; i < uTerrainClipCount; ++i)
- gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
- for (int i = uTerrainClipCount; i < 8; ++i)
- gl_ClipDistance[i] = 1.0;
}
diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.Rhi.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.Rhi.cs
index eeced25f..9f681ead 100644
--- a/src/AcDream.App/Rendering/Sky/SkyRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.Rhi.cs
@@ -255,7 +255,6 @@ public sealed unsafe partial class SkyRenderer
SkyParams.SizeInBytes);
WorldFrameSectionBinding.BindSceneLighting(encoder, scope.Sections, frame);
- WorldFrameSectionBinding.BindTerrainClip(encoder, scope.Sections, frame);
encoder.DrawIndexed((uint)sub.IndexCount, 1, 0, 0, 0);
}
diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
index 4ca62c12..cedd4c7d 100644
--- a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
@@ -256,7 +256,6 @@ public sealed unsafe partial class TerrainModernRenderer
encoder.BindIndexBuffer(RequireIndexStore(), 0, GpuIndexType.UInt32);
BindTilingTable(encoder);
WorldFrameSectionBinding.BindSceneLighting(encoder, scope.Sections, frame);
- WorldFrameSectionBinding.BindTerrainClip(encoder, scope.Sections, frame);
// Campaign VM VM6 review fix round 4 (item 4): bind on BINDABLE,
// not Enabled — same rule as WbDrawDispatcher.BindDirectionalShadowReceiver.
// TryGetCurrentFrameBinding now returns true for a disabled-content
diff --git a/src/AcDream.App/Rendering/WorldPassScope.cs b/src/AcDream.App/Rendering/WorldPassScope.cs
index 6f953898..58c3345d 100644
--- a/src/AcDream.App/Rendering/WorldPassScope.cs
+++ b/src/AcDream.App/Rendering/WorldPassScope.cs
@@ -21,16 +21,20 @@ internal readonly record struct GpuBufferSection(
}
///
-/// Campaign V slice V6j: the three sections GL binds frame-globally and Vulkan
+/// Campaign V slice V6j: the sections GL binds frame-globally and Vulkan
/// cannot.
///
-/// On GL the SceneLighting UBO (set 1 binding 1), the per-cell clip regions
-/// (set 0 binding 2) and the terrain clip block (set 1 binding 2) are each bound
-/// once, to a global binding point, and every consumer inherits them. Vulkan has
-/// no global binding points: a descriptor set is bound per draw, and a renderer
-/// that binds its own buffers selects the descriptor scope those sections have to
-/// land in (plan §5.5.14 item 2). So the writers PUBLISH here and each renderer
-/// binds them inside the pass, after its own binds.
+/// On GL the SceneLighting UBO (set 1 binding 1) and the per-cell clip
+/// regions (set 0 binding 2) are each bound once, to a global binding point,
+/// and every consumer inherits them. Vulkan has no global binding points: a
+/// descriptor set is bound per draw, and a renderer that binds its own
+/// buffers selects the descriptor scope those sections have to land in (plan
+/// §5.5.14 item 2). So the writers PUBLISH here and each renderer binds them
+/// inside the pass, after its own binds. S3 chunk 4 fix round 2 (L3) deleted
+/// the third section this class used to carry (the terrain screen-space clip
+/// block, set 1 binding 2) — no shader declares it any more, and the
+/// unrelated KEEP clip block that still uses binding 2 (the exit-seal/punch
+/// fan clip) is fed directly by its own renderer, not through here.
///
/// Borrowed for the frame that publishes it — the sections are ring slices
/// and die when the frame retires.
@@ -43,14 +47,10 @@ internal sealed class WorldFrameSections
/// Set 0 binding 2 — the per-cell CellClip table.
public GpuBufferSection ClipRegions { get; set; }
- /// Set 1 binding 2 — TerrainClip.
- public GpuBufferSection TerrainClip { get; set; }
-
public void Reset()
{
SceneLighting = default;
ClipRegions = default;
- TerrainClip = default;
}
}
@@ -135,7 +135,10 @@ internal interface IWorldPassScope
/// Each helper falls back to a zeroed ring slice when nothing published the
/// section. That is the same rule the GL arm already states for its own bindings
/// — bind at least one element so the shader never reads an unbound buffer —
-/// applied to the three sections whose publisher runs outside the renderer.
+/// applied to the two sections whose publisher runs outside the renderer. S3
+/// chunk 4 fix round 2 (L3) deleted the third helper this used to carry —
+/// the terrain screen-space clip block's binder — with its last two callers;
+/// no shader declares that block any more.
///
internal static class WorldFrameSectionBinding
{
@@ -182,27 +185,6 @@ internal static class WorldFrameSectionBinding
section.SizeBytes);
}
- internal static void BindTerrainClip(
- IGpuPassEncoder encoder,
- WorldFrameSections sections,
- IGpuFrame frame)
- {
- GpuBufferSection section = sections.TerrainClip;
- if (!section.IsValid)
- {
- section = Zeroed(
- frame,
- ClipFrame.TerrainUboBytes,
- GpuRingUsage.Uniform);
- }
-
- encoder.BindUniformBuffer(
- ClipFrame.TerrainClipUboBinding,
- section.Buffer!,
- section.OffsetBytes,
- section.SizeBytes);
- }
-
private static GpuBufferSection Zeroed(
IGpuFrame frame,
int byteCount,
diff --git a/src/AcDream.App/Rendering/WorldPassSurface.cs b/src/AcDream.App/Rendering/WorldPassSurface.cs
index 5ac23b23..6905e995 100644
--- a/src/AcDream.App/Rendering/WorldPassSurface.cs
+++ b/src/AcDream.App/Rendering/WorldPassSurface.cs
@@ -1,4 +1,3 @@
-using System.Numerics;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Rendering;
@@ -39,30 +38,16 @@ internal interface IWorldPassSurface
/// 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): this no longer also publishes the
- /// terrain clip block — ClipFrame.SetTerrainClip, its only
- /// writer, is deleted, so stays at
- /// its permanent NoClip/Reset default (count 0, all zero) for the life of
- /// a frame. The terrain/sky shaders still declare the TerrainClip
- /// UBO (binding=2), so 's
- /// existing zeroed-ring fallback (used whenever nothing published the
- /// section) now binds that all-zero disabled block on every frame instead
- /// of only when something raced the publish — the observable result is
- /// identical either way, since a zeroed publish and a zeroed fallback are
- /// the same bytes.
+ /// 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
+ /// clip loop — and fix round 2 (L3) deleted the terrain/sky shaders' own
+ /// 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.
///
void PrepareClipFrame(int terrainUploadCount);
- ///
- /// Re-asserts the terrain clip block at its binding.
- ///
- /// GL needs this because binding points are global and the sky and mesh
- /// shaders read the same uniform binding between two terrain slices. On the
- /// RHI arm every consumer binds the published section inside the pass, so
- /// there is no ambient binding to re-assert and this is a no-op.
- ///
- void BindTerrainClip();
-
///
/// Enables every gl_ClipDistance slot.
///
@@ -77,15 +62,6 @@ internal interface IWorldPassSurface
/// Disables every gl_ClipDistance slot. See .
void DisableClipDistances();
- ///
- /// Scissors to a doorway slice's screen-space bounding box. Returns whether a
- /// scissor is now active, so the caller can pair it with .
- ///
- bool BeginScissor(Vector4 ndcAabb);
-
- /// Restores the full drawable rectangle.
- void EndScissor();
-
///
/// Retail's interior depth clear, between the landscape slice and the
/// interior cells (PView::DrawCells @ 0x005A4840). Retail gates it
@@ -108,18 +84,15 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
private readonly IWorldPassScope _scope;
private readonly ICurrentGpuFrameSource _frames;
private readonly ClipFrame _clipFrame;
- private readonly IRetailPViewFramebufferSource _framebuffer;
public RhiWorldPassSurface(
IWorldPassScope scope,
ICurrentGpuFrameSource frames,
- ClipFrame clipFrame,
- IRetailPViewFramebufferSource framebuffer)
+ ClipFrame clipFrame)
{
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
- _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer));
}
public void PrepareClipFrame(int terrainUploadCount)
@@ -131,17 +104,6 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
_scope.Sections.ClipRegions = Publish(
_clipFrame.RegionBytes,
GpuRingUsage.Storage);
- // S3 chunk 4 fix round 1 (K3): the terrain-clip publish half is
- // deleted — SetTerrainClip (its only writer) is gone, so publishing
- // ClipFrame.TerrainBytes here would only ever republish permanent
- // zero bytes. WorldFrameSectionBinding.BindTerrainClip's existing
- // zeroed-ring fallback (for when nothing published the section)
- // binds that same all-zero disabled block instead.
- }
-
- /// No-op: there is no ambient binding to re-assert. See the interface.
- public void BindTerrainClip()
- {
}
/// No-op: Vulkan activates every declared clip distance. See the interface.
@@ -154,33 +116,17 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
{
}
- public bool BeginScissor(Vector4 ndcAabb)
- {
- RetailPViewFramebufferSize framebuffer = _framebuffer.Capture();
- var box = NdcScissorRect.ToPixels(
- ndcAabb,
- framebuffer.Width,
- framebuffer.Height);
- // GL convention on the way in; the backend performs its own Y flip.
- _scope.RequireEncoder().SetScissor(box.X, box.Y, box.Width, box.Height);
- return true;
- }
-
- public void EndScissor() =>
- _scope.RequireEncoder().SetScissor(
- 0,
- 0,
- _scope.AttachmentWidth,
- _scope.AttachmentHeight);
-
public void ClearInteriorDepth()
{
- // The GL arm drops the scissor before clearing so the clear covers the
- // whole target; vkCmdClearAttachments takes its own rectangle and is not
- // scissored, so the same coverage comes for free — but the scissor still
- // has to come off, because the interior cells drawn after it are not
- // confined to the doorway slice that was active.
- EndScissor();
+ // S3 chunk 4 fix round 2 (L2): BeginScissor/EndScissor are deleted —
+ // no producer of a narrowed scissor rectangle remains anywhere in the
+ // walk (K4 deleted the doorway scissor that used to bracket the sky;
+ // fix round 1 already deleted the terrain/weather per-slice scissor
+ // loop). VulkanGpuPassEncoder's constructor sets the full-attachment
+ // scissor exactly once, at pass begin (VulkanGpuPassEncoder.cs:87),
+ // and nothing narrows it after that any more, so
+ // vkCmdClearAttachments's own unscissored clear and every draw after
+ // it already cover the whole target with no bracket needed here.
_scope.ClearInteriorDepth();
}
diff --git a/src/AcDream.App/Rendering/WorldRenderDiagnostics.cs b/src/AcDream.App/Rendering/WorldRenderDiagnostics.cs
index c0dddf62..079ff766 100644
--- a/src/AcDream.App/Rendering/WorldRenderDiagnostics.cs
+++ b/src/AcDream.App/Rendering/WorldRenderDiagnostics.cs
@@ -243,14 +243,10 @@ internal sealed class WorldRenderDiagnostics
$" ssbo[{slice.Slot}]: OUT-OF-RANGE len={regionBytes.Length}"));
}
- ReadOnlySpan terrainBytes = clipFrame.TerrainBytesForTest;
- int terrainCount = BitConverter.ToInt32(terrainBytes[..4]);
- float p0 = BitConverter.ToSingle(terrainBytes.Slice(16, 4));
- float p1 = BitConverter.ToSingle(terrainBytes.Slice(20, 4));
- float p2 = BitConverter.ToSingle(terrainBytes.Slice(24, 4));
- float p3 = BitConverter.ToSingle(terrainBytes.Slice(28, 4));
- text.Append(FormattableString.Invariant(
- $" ubo: n={terrainCount} p0=({p0:F3},{p1:F3},{p2:F3},{p3:F3})"));
+ // S3 chunk 4 fix round 2 (L3): the "ubo: n=... p0=..." segment this
+ // used to append read ClipFrame's own screen-space clip gate bytes,
+ // deleted along with that gate — no shader declares the block any
+ // more, so there is nothing left here to read.
string signature = text.ToString();
_clipRouteSequence++;
@@ -342,7 +338,11 @@ internal sealed class WorldRenderDiagnostics
+ $"playerInRoot={(playerInRoot ? "Y" : "n")} "
+ $"eye=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}) "
+ $"player=({playerPosition.X:F2},{playerPosition.Y:F2},{playerPosition.Z:F2}) "
- + $"terrain={result.ClipAssembly.TerrainMode} "
+ // S3 chunk 4 fix round 2 (L2): the "terrain=" field read
+ // ClipFrameAssembly.TerrainMode, deleted along with the
+ // walk's own terrain-clip-mode tracking (the walk draws
+ // terrain unclipped, one call, matching retail — there is no
+ // longer a mode to report here).
+ $"outVisible={result.ClipAssembly.OutdoorVisible}");
}
}
@@ -402,15 +402,17 @@ internal sealed class WorldRenderDiagnostics
text.Append(" skyFrame=").Append(drawSkyThisFrame ? 'Y' : 'n');
text.Append(" zclear=").Append(depthClear ? 'Y' : 'n');
text.Append(" sceneParticles=").Append(sceneParticles);
+ // S3 chunk 4 fix round 2 (L2): "outMode=" read
+ // ClipFrameAssembly.TerrainMode, deleted with the walk's own
+ // terrain-clip-mode tracking.
if (clipAssembly is not null)
{
text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length);
text.Append(" outPolys=").Append(clipAssembly.OutsideViewSlices.Length);
- text.Append(" outMode=").Append(clipAssembly.TerrainMode);
}
else
{
- text.Append(" outSlices=0 outPolys=0 outMode=none");
+ text.Append(" outSlices=0 outPolys=0");
}
text.Append(" ids=").Append(FormatIds(visibleCells, false));
diff --git a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
index 9e18b364..4a46e908 100644
--- a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
+++ b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
@@ -1,240 +1,238 @@
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb;
-using AcDream.Core.Rendering;
-using AcDream.Core.Vfx;
-using AcDream.Core.World;
-
-namespace AcDream.App.Rendering;
-
-internal interface IWorldScenePassExecutor
-{
- HashSet? TerrainVisibleCellIds { get; }
-
- void BeginFrame();
-
- void PrepareFlatWorldClip();
-
- void DrawFlatSky(
- in WorldCameraFrame camera,
- in RenderFrameFoundation foundation,
- DayGroupData? activeDayGroup,
- float dayFraction);
-
- void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId);
-
- void DrawFlatEntities(
- in WorldCameraFrame camera,
- IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
- System.Numerics.Vector3 AabbMax,
- IReadOnlyList Entities,
- IReadOnlyDictionary? AnimatedById)> entries,
- uint? playerLandblockId,
- HashSet animatedEntityIds);
-
- string DrawPostWorldParticles(
- LoadedCell? clipRoot,
- ClipFrameAssembly? clipAssembly,
- in WorldCameraFrame camera,
- string currentSignature);
-
- void DrawFlatWeather(
- in WorldCameraFrame camera,
- in RenderFrameFoundation foundation,
- DayGroupData? activeDayGroup,
- float dayFraction);
-
- void DisableClipDistances();
-
- void AbortFrame();
-}
-
-///
-/// Concrete leaf for the flat-world safety path and the post-world particle
-/// and weather passes. Retail PView frames remain owned by
-/// and .
-///
-/// Campaign V slice V6j: backend-neutral. Everything it does is either
-/// delegation to a renderer or one of the four concerns
-/// owns, so one implementation serves both
-/// backends and the retail ordering is written once.
-///
-internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
-{
- private readonly IWorldPassSurface _surface;
- private readonly IRenderFrameGlState _frameGlState;
- private readonly ClipFrame _clipFrame;
- private readonly WbDrawDispatcher _entities;
- private readonly EnvCellRenderer _environmentCells;
- private readonly TerrainModernRenderer? _terrain;
- private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
- private readonly SkyRenderer? _sky;
+using AcDream.Core.Rendering;
+using AcDream.Core.Vfx;
+using AcDream.Core.World;
+
+namespace AcDream.App.Rendering;
+
+internal interface IWorldScenePassExecutor
+{
+ HashSet? TerrainVisibleCellIds { get; }
+
+ void BeginFrame();
+
+ void PrepareFlatWorldClip();
+
+ void DrawFlatSky(
+ in WorldCameraFrame camera,
+ in RenderFrameFoundation foundation,
+ DayGroupData? activeDayGroup,
+ float dayFraction);
+
+ void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId);
+
+ void DrawFlatEntities(
+ in WorldCameraFrame camera,
+ IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
+ System.Numerics.Vector3 AabbMax,
+ IReadOnlyList Entities,
+ IReadOnlyDictionary? AnimatedById)> entries,
+ uint? playerLandblockId,
+ HashSet animatedEntityIds);
+
+ string DrawPostWorldParticles(
+ LoadedCell? clipRoot,
+ ClipFrameAssembly? clipAssembly,
+ in WorldCameraFrame camera,
+ string currentSignature);
+
+ void DrawFlatWeather(
+ in WorldCameraFrame camera,
+ in RenderFrameFoundation foundation,
+ DayGroupData? activeDayGroup,
+ float dayFraction);
+
+ void DisableClipDistances();
+
+ void AbortFrame();
+}
+
+///
+/// Concrete leaf for the flat-world safety path and the post-world particle
+/// and weather passes. Retail PView frames remain owned by
+/// and .
+///
+/// Campaign V slice V6j: backend-neutral. Everything it does is either
+/// delegation to a renderer or one of the four concerns
+/// owns, so one implementation serves both
+/// backends and the retail ordering is written once.
+///
+internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
+{
+ private readonly IWorldPassSurface _surface;
+ private readonly IRenderFrameGlState _frameGlState;
+ private readonly ClipFrame _clipFrame;
+ private readonly WbDrawDispatcher _entities;
+ private readonly EnvCellRenderer _environmentCells;
+ private readonly TerrainModernRenderer? _terrain;
+ private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
+ private readonly SkyRenderer? _sky;
private readonly ParticleSystem? _particles;
private readonly ParticleRenderer? _particleRenderer;
- private readonly HashSet _visibleParticleOwners = [];
- private readonly HashSet _noExcludedParticleOwners = [];
-
- public WorldScenePassExecutor(
- IWorldPassSurface surface,
- IRenderFrameGlState frameGlState,
- ClipFrame clipFrame,
- WbDrawDispatcher entities,
- EnvCellRenderer environmentCells,
- TerrainModernRenderer? terrain,
+ private readonly HashSet _visibleParticleOwners = [];
+ private readonly HashSet _noExcludedParticleOwners = [];
+
+ public WorldScenePassExecutor(
+ IWorldPassSurface surface,
+ IRenderFrameGlState frameGlState,
+ ClipFrame clipFrame,
+ WbDrawDispatcher entities,
+ EnvCellRenderer environmentCells,
+ TerrainModernRenderer? terrain,
TerrainDrawDiagnosticsController terrainDiagnostics,
SkyRenderer? sky,
ParticleSystem? particles,
ParticleRenderer? particleRenderer)
- {
- _surface = surface ?? throw new ArgumentNullException(nameof(surface));
- _frameGlState = frameGlState
- ?? throw new ArgumentNullException(nameof(frameGlState));
- _clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
- _entities = entities ?? throw new ArgumentNullException(nameof(entities));
- _environmentCells = environmentCells
- ?? throw new ArgumentNullException(nameof(environmentCells));
- _terrain = terrain;
- _terrainDiagnostics = terrainDiagnostics
- ?? throw new ArgumentNullException(nameof(terrainDiagnostics));
- _sky = sky;
+ {
+ _surface = surface ?? throw new ArgumentNullException(nameof(surface));
+ _frameGlState = frameGlState
+ ?? throw new ArgumentNullException(nameof(frameGlState));
+ _clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
+ _entities = entities ?? throw new ArgumentNullException(nameof(entities));
+ _environmentCells = environmentCells
+ ?? throw new ArgumentNullException(nameof(environmentCells));
+ _terrain = terrain;
+ _terrainDiagnostics = terrainDiagnostics
+ ?? throw new ArgumentNullException(nameof(terrainDiagnostics));
+ _sky = sky;
_particles = particles;
_particleRenderer = particleRenderer;
- }
-
- public HashSet? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
-
- public void BeginFrame()
- {
- _visibleParticleOwners.Clear();
- _clipFrame.Reset();
- _entities.ClearClipRouting();
- _environmentCells.SetClipRouting(null);
- }
-
- public void PrepareFlatWorldClip() => _surface.PrepareClipFrame(1);
-
- public void DrawFlatSky(
- in WorldCameraFrame camera,
- in RenderFrameFoundation foundation,
- DayGroupData? activeDayGroup,
- float dayFraction)
- {
- _surface.BindTerrainClip();
- _surface.EnableClipDistances();
- Exception? drawFailure = null;
- try
- {
- _sky?.RenderSky(
- camera.Camera,
- camera.Position,
- dayFraction,
- activeDayGroup,
- foundation.Sky,
- foundation.EnvironOverrideActive);
- }
- catch (Exception error)
- {
- drawFailure = error;
- throw;
- }
- finally
- {
- try
- {
- DisableClipDistances();
- }
- catch (Exception closeFailure) when (drawFailure is not null)
- {
- throw new AggregateException(
- "Sky drawing failed and its clip-distance bracket could not be closed.",
- drawFailure,
- closeFailure);
- }
- }
-
- if (_particles is not null && _particleRenderer is not null)
- {
- _particleRenderer.Draw(
- camera.Camera,
- camera.Position,
- ParticleRenderPass.SkyPreScene);
- }
- }
-
- public void DrawFlatTerrain(
- in WorldCameraFrame camera,
- uint? playerLandblockId)
- {
- _surface.EnableClipDistances();
- _terrainDiagnostics.Begin();
- _terrain?.Draw(
- camera.Camera,
- camera.Frustum,
- neverCullLandblockId: playerLandblockId);
- _terrainDiagnostics.Complete();
- }
-
- public void DrawFlatEntities(
- in WorldCameraFrame camera,
- IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
- System.Numerics.Vector3 AabbMax,
- IReadOnlyList Entities,
- IReadOnlyDictionary? AnimatedById)> entries,
- uint? playerLandblockId,
- HashSet animatedEntityIds) =>
- _entities.Draw(
- camera.Camera,
- entries,
- camera.Frustum,
- neverCullLandblockId: playerLandblockId,
- visibleCellIds: null,
- animatedEntityIds: animatedEntityIds);
-
- public string DrawPostWorldParticles(
- LoadedCell? clipRoot,
- ClipFrameAssembly? clipAssembly,
- in WorldCameraFrame camera,
- string currentSignature)
- {
- if (_particles is null || _particleRenderer is null)
- return currentSignature;
-
- if (clipRoot is null)
- {
- if (clipAssembly is not null)
- {
- _particleRenderer.DrawForOwners(
- camera.Camera,
- camera.Position,
- ParticleRenderPass.Scene,
- _visibleParticleOwners,
- includeUnattached: true,
- excludedAttachedOwnerIds: _noExcludedParticleOwners);
- return AppendSignature(currentSignature, "filtered");
- }
-
- _particleRenderer.Draw(
- camera.Camera,
- camera.Position,
- ParticleRenderPass.Scene);
- return AppendSignature(currentSignature, "global");
- }
-
- // Every PView root, including the outdoor sentinel, now submits scene
- // particles inside LScape::draw. Replaying them here is both a duplicate
- // and too late: it occurs after nested building cells, allowing exterior
- // waterfall/foliage alpha to repaint an indoor/outdoor transition.
- return currentSignature;
- }
-
- public void DrawFlatWeather(
- in WorldCameraFrame camera,
- in RenderFrameFoundation foundation,
- DayGroupData? activeDayGroup,
- float dayFraction)
- {
- _surface.BindTerrainClip();
- _surface.EnableClipDistances();
- Exception? drawFailure = null;
+ }
+
+ public HashSet? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
+
+ public void BeginFrame()
+ {
+ _visibleParticleOwners.Clear();
+ _clipFrame.Reset();
+ _entities.ClearClipRouting();
+ _environmentCells.SetClipRouting(null);
+ }
+
+ public void PrepareFlatWorldClip() => _surface.PrepareClipFrame(1);
+
+ public void DrawFlatSky(
+ in WorldCameraFrame camera,
+ in RenderFrameFoundation foundation,
+ DayGroupData? activeDayGroup,
+ float dayFraction)
+ {
+ _surface.EnableClipDistances();
+ Exception? drawFailure = null;
+ try
+ {
+ _sky?.RenderSky(
+ camera.Camera,
+ camera.Position,
+ dayFraction,
+ activeDayGroup,
+ foundation.Sky,
+ foundation.EnvironOverrideActive);
+ }
+ catch (Exception error)
+ {
+ drawFailure = error;
+ throw;
+ }
+ finally
+ {
+ try
+ {
+ DisableClipDistances();
+ }
+ catch (Exception closeFailure) when (drawFailure is not null)
+ {
+ throw new AggregateException(
+ "Sky drawing failed and its clip-distance bracket could not be closed.",
+ drawFailure,
+ closeFailure);
+ }
+ }
+
+ if (_particles is not null && _particleRenderer is not null)
+ {
+ _particleRenderer.Draw(
+ camera.Camera,
+ camera.Position,
+ ParticleRenderPass.SkyPreScene);
+ }
+ }
+
+ public void DrawFlatTerrain(
+ in WorldCameraFrame camera,
+ uint? playerLandblockId)
+ {
+ _surface.EnableClipDistances();
+ _terrainDiagnostics.Begin();
+ _terrain?.Draw(
+ camera.Camera,
+ camera.Frustum,
+ neverCullLandblockId: playerLandblockId);
+ _terrainDiagnostics.Complete();
+ }
+
+ public void DrawFlatEntities(
+ in WorldCameraFrame camera,
+ IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
+ System.Numerics.Vector3 AabbMax,
+ IReadOnlyList Entities,
+ IReadOnlyDictionary? AnimatedById)> entries,
+ uint? playerLandblockId,
+ HashSet animatedEntityIds) =>
+ _entities.Draw(
+ camera.Camera,
+ entries,
+ camera.Frustum,
+ neverCullLandblockId: playerLandblockId,
+ visibleCellIds: null,
+ animatedEntityIds: animatedEntityIds);
+
+ public string DrawPostWorldParticles(
+ LoadedCell? clipRoot,
+ ClipFrameAssembly? clipAssembly,
+ in WorldCameraFrame camera,
+ string currentSignature)
+ {
+ if (_particles is null || _particleRenderer is null)
+ return currentSignature;
+
+ if (clipRoot is null)
+ {
+ if (clipAssembly is not null)
+ {
+ _particleRenderer.DrawForOwners(
+ camera.Camera,
+ camera.Position,
+ ParticleRenderPass.Scene,
+ _visibleParticleOwners,
+ includeUnattached: true,
+ excludedAttachedOwnerIds: _noExcludedParticleOwners);
+ return AppendSignature(currentSignature, "filtered");
+ }
+
+ _particleRenderer.Draw(
+ camera.Camera,
+ camera.Position,
+ ParticleRenderPass.Scene);
+ return AppendSignature(currentSignature, "global");
+ }
+
+ // Every PView root, including the outdoor sentinel, now submits scene
+ // particles inside LScape::draw. Replaying them here is both a duplicate
+ // and too late: it occurs after nested building cells, allowing exterior
+ // waterfall/foliage alpha to repaint an indoor/outdoor transition.
+ return currentSignature;
+ }
+
+ public void DrawFlatWeather(
+ in WorldCameraFrame camera,
+ in RenderFrameFoundation foundation,
+ DayGroupData? activeDayGroup,
+ float dayFraction)
+ {
+ _surface.EnableClipDistances();
+ Exception? drawFailure = null;
try
{
_sky?.RenderWeather(
@@ -244,63 +242,63 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
activeDayGroup,
foundation.Sky,
foundation.EnvironOverrideActive);
- }
- catch (Exception error)
- {
- drawFailure = error;
- throw;
- }
- finally
- {
- try
- {
- DisableClipDistances();
- }
- catch (Exception closeFailure) when (drawFailure is not null)
- {
- throw new AggregateException(
- "Weather drawing failed and its clip-distance bracket could not be closed.",
- drawFailure,
- closeFailure);
- }
- }
-
- if (_particles is not null && _particleRenderer is not null)
- {
- _particleRenderer.Draw(
- camera.Camera,
- camera.Position,
- ParticleRenderPass.SkyPostScene);
- }
- }
-
+ }
+ catch (Exception error)
+ {
+ drawFailure = error;
+ throw;
+ }
+ finally
+ {
+ try
+ {
+ DisableClipDistances();
+ }
+ catch (Exception closeFailure) when (drawFailure is not null)
+ {
+ throw new AggregateException(
+ "Weather drawing failed and its clip-distance bracket could not be closed.",
+ drawFailure,
+ closeFailure);
+ }
+ }
+
+ if (_particles is not null && _particleRenderer is not null)
+ {
+ _particleRenderer.Draw(
+ camera.Camera,
+ camera.Position,
+ ParticleRenderPass.SkyPostScene);
+ }
+ }
+
public void DisableClipDistances() => _surface.DisableClipDistances();
-
- public void AbortFrame()
- {
- List? failures = null;
- TryAbort(_frameGlState.RestoreFrameDefaults);
- TryAbort(_clipFrame.Reset);
- TryAbort(_entities.ClearClipRouting);
- TryAbort(_entities.AbortCurrentRenderSceneObserverFrame);
- TryAbort(() => _environmentCells.SetClipRouting(null));
- _visibleParticleOwners.Clear();
- if (failures is { Count: > 0 })
- throw new AggregateException("World scene pass abort failed.", failures);
-
- void TryAbort(Action operation)
- {
- try
- {
- operation();
- }
- catch (Exception error)
- {
- (failures ??= []).Add(error);
- }
- }
- }
-
- private static string AppendSignature(string current, string value) =>
- current == "none" ? value : current + "+" + value;
-}
+
+ public void AbortFrame()
+ {
+ List? failures = null;
+ TryAbort(_frameGlState.RestoreFrameDefaults);
+ TryAbort(_clipFrame.Reset);
+ TryAbort(_entities.ClearClipRouting);
+ TryAbort(_entities.AbortCurrentRenderSceneObserverFrame);
+ TryAbort(() => _environmentCells.SetClipRouting(null));
+ _visibleParticleOwners.Clear();
+ if (failures is { Count: > 0 })
+ throw new AggregateException("World scene pass abort failed.", failures);
+
+ void TryAbort(Action operation)
+ {
+ try
+ {
+ operation();
+ }
+ catch (Exception error)
+ {
+ (failures ??= []).Add(error);
+ }
+ }
+ }
+
+ private static string AppendSignature(string current, string value) =>
+ current == "none" ? value : current + "+" + value;
+}
diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs
index 900fa1ac..1c8a9c8b 100644
--- a/tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs
@@ -38,7 +38,6 @@ public class ClipFrameAssemblerTests
Assert.Equal(new Vector4(-1f, -1f, 1f, 1f), slice.NdcAabb);
Assert.True(assembly.OutdoorVisible);
Assert.True(assembly.HasOutsideView);
- Assert.Equal(TerrainClipMode.Scissor, assembly.TerrainMode);
Assert.Equal(1, frame.SlotCount);
}
@@ -59,7 +58,6 @@ public class ClipFrameAssemblerTests
Assert.Empty(assembly.OutsideViewSlices);
Assert.False(assembly.OutdoorVisible);
Assert.False(assembly.HasOutsideView);
- Assert.Equal(TerrainClipMode.Skip, assembly.TerrainMode);
Assert.Equal(Vector4.Zero, assembly.OutsideViewNdcAabb);
Assert.Equal(0, assembly.ScissorFallbacks);
Assert.Equal(1, frame.SlotCount);
@@ -93,7 +91,6 @@ public class ClipFrameAssemblerTests
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(asm.OutdoorSlot, asm.OutsideViewSlices[0].Slot);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
}
@@ -117,7 +114,6 @@ public class ClipFrameAssemblerTests
Assert.DoesNotContain(cellB, asm.CellIdToSlot.Keys);
Assert.False(asm.OutdoorVisible);
Assert.Empty(asm.OutsideViewSlices);
- Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
Assert.Equal(0, asm.OutsidePlaneCount);
}
@@ -138,11 +134,9 @@ public class ClipFrameAssemblerTests
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Equal(2, asm.OutsideViewSlices.Length);
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + cell + two outside slices
- Assert.Equal(Vector4.Zero, asm.TerrainScissorNdcAabb);
}
[Fact]
@@ -167,7 +161,6 @@ public class ClipFrameAssemblerTests
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cell slices + outside slice
Assert.Equal(0, asm.ScissorFallbacks);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
}
[Fact]
@@ -179,7 +172,6 @@ public class ClipFrameAssemblerTests
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.Single(asm.OutsideViewSlices);
var expected = new Vector4(
@@ -198,14 +190,12 @@ public class ClipFrameAssemblerTests
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.Equal(2, asm.OutsideViewSlices.Length);
var expected = new Vector4(
pv.OutsideView.MinX, pv.OutsideView.MinY,
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
Assert.Equal(expected, asm.OutsideViewNdcAabb);
- Assert.Equal(Vector4.Zero, asm.TerrainScissorNdcAabb);
}
[Fact]
@@ -219,7 +209,6 @@ public class ClipFrameAssemblerTests
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.False(asm.HasOutsideView);
- Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
Assert.Equal(Vector4.Zero, asm.OutsideViewNdcAabb);
}
@@ -246,7 +235,6 @@ public class ClipFrameAssemblerTests
Assert.Contains(0xA9B40200, asm2.CellIdToSlot.Keys);
Assert.DoesNotContain(0xA9B40100, asm2.CellIdToSlot.Keys);
Assert.False(asm2.OutdoorVisible);
- Assert.Equal(TerrainClipMode.Skip, asm2.TerrainMode);
}
[Fact]
@@ -295,7 +283,6 @@ public class ClipFrameAssemblerTests
Assert.Empty(cleared.CellIdToViewSlices);
Assert.Empty(cleared.PerCellPlaneCounts);
Assert.Empty(cleared.OutsideViewSlices);
- Assert.Equal(TerrainClipMode.Skip, cleared.TerrainMode);
}
[Fact]
diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
index dca1080a..38f3b409 100644
--- a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
@@ -1,15 +1,16 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
+using AcDream.App.Rendering.Walk;
using Xunit;
namespace AcDream.App.Tests.Rendering;
///
-/// Phase U.3: CPU-side proof that packs the shared clip
-/// data in the EXACT std430 (mesh SSBO) / std140 (terrain UBO) byte layout the
-/// shaders read. A silent layout drift here would mis-clip at U.4 with no build
-/// error — these tests are the gate that catches it.
+/// Phase U.3: CPU-side proof that packs the mesh SSBO
+/// clip-region table in the EXACT std430 byte layout mesh_modern.vert reads. A
+/// silent layout drift here would mis-clip at U.4 with no build error — these
+/// tests are the gate that catches it.
///
/// Layout under test (mesh CellClip, std430):
/// offset 0 : uint count
@@ -20,7 +21,11 @@ namespace AcDream.App.Tests.Rendering;
/// ...
/// offset 16 + i*16 : vec4 planes[i]
/// stride 144 bytes per slot.
-/// Terrain UBO (std140): int count at 0 (padded to 16), vec4 planes[8] at 16.
+///
+/// /
+/// share this file only because they happen to reuse the identical std140
+/// 144-byte shape for a DIFFERENT, still-live consumer — see their own doc
+/// comments (S3 chunk 4 fix round 2, L3).
///
public class ClipFrameLayoutTests
{
@@ -41,11 +46,12 @@ public class ClipFrameLayoutTests
Assert.Equal(16, ClipFrame.CellClipPlanesOffset);
Assert.Equal(8, ClipFrame.MaxPlanes);
Assert.Equal(144, ClipFrame.TerrainUboBytes);
- // Binding contract: mesh clip regions on SSBO binding=2, terrain on UBO binding=2.
- // The mesh side's binding index moved off ClipFrame at Campaign V slice
- // V11 — the RHI arm addresses it through GpuBindingModel.StorageClipRegions
- // instead of a raw GL binding constant (see ClipFrame's BeginFrame doc
- // comment); the terrain UBO binding is still genuinely shared, so it stays.
+ // Binding contract: mesh clip regions on SSBO binding=2, PortalDepthMaskRenderer's
+ // exit-seal/punch-fan clip block on UBO binding=2. The mesh side's binding
+ // index moved off ClipFrame at Campaign V slice V11 — the RHI arm addresses
+ // it through GpuBindingModel.StorageClipRegions instead of a raw GL binding
+ // constant (see ClipFrame's BeginFrame doc comment); the portal-depth UBO
+ // binding is still genuinely shared, so it stays.
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
Assert.Equal(2u, ClipFrame.TerrainClipUboBinding);
}
@@ -66,17 +72,6 @@ public class ClipFrameLayoutTests
Assert.Equal(0, b);
}
- [Fact]
- public void NoClip_TerrainBytes_Count0_AllZeros()
- {
- var frame = ClipFrame.NoClip();
- var t = frame.TerrainBytesForTest;
- Assert.Equal(ClipFrame.TerrainUboBytes, t.Length);
- Assert.Equal(0, ReadInt(t, 0)); // count 0 ⇒ terrain ungated
- foreach (var b in t)
- Assert.Equal(0, b);
- }
-
[Fact]
public void AppendSlot_WritesCountAndPlanes_AtStd430Offsets()
{
@@ -180,11 +175,9 @@ public class ClipFrameLayoutTests
AssertPlaneAt(bytes, baseOff + ClipFrame.CellClipPlanesOffset + i * 16, cps.Planes[i]);
}
- // S3 chunk 4 fix round 1 (K3): SetTerrainClip_WritesCountAndPlanes_AtStd140Offsets
- // is deleted along with ClipFrame.SetTerrainClip itself (no production
- // writer remains). NoClip_TerrainBytes_Count0_AllZeros above still pins
- // the permanent all-zero state SetTerrainClip used to be the only way
- // to move off of.
+ // S3 chunk 4 fix round 2 (L3): the walk's screen-space clip gate and its
+ // own std140 layout pin are deleted outright — no shader declares that
+ // block any more, so there is nothing left to pack or pin.
private static void AssertPlaneAt(System.ReadOnlySpan bytes, int offset, Vector4 expected)
{
@@ -246,39 +239,62 @@ public class ClipFrameLayoutTests
/// KEEP item 2 — punch fans: RetailPViewPassExecutor.DrawWalkPunchFan
/// reads its clip planes through clipAssembly.OutsideViewSlices
/// [activeViewIndex].Planes — 's
- /// Planes field. S3 chunk 4 fix round 1 (K6): this pin now builds
- /// that slice through the REAL production assembly —
- /// ClipFrameAssembler.Assemble's own
- /// outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly),
- /// planes)) line, the exact construction
- /// ReassembleOutsideViewFromWalk (the walk's real interior-root
- /// producer) shares — instead of hand-constructing a
- /// directly from 's raw output: a hand-built slice
- /// could pass even if Assemble's own packing/array-construction diverged
- /// from that raw output, which is exactly the gap a prior round's
- /// three-lens review found (a hand-built ClipViewSlice is not
- /// proof the production path builds the same one). Same synthetic-view
- /// helper as the exit-seal pin, a different (non-axis-aligned) synthetic
- /// polygon so the two pins are not testing the identical input.
+ /// Planes field. S3 chunk 4 fix round 2 (L4): a prior round's pin
+ /// here built the slice through ClipFrameAssembler.Assemble — but
+ /// Assemble has ZERO production callers (it exists only for
+ /// isolated research/replay tests, per this file's own class header);
+ /// production builds an interior root's outside-view slices through
+ /// BeginWalkFrame + ReassembleOutsideViewFromWalk
+ /// (RetailPViewPassExecutor.cs's BeginWalkFrame call,
+ /// RetailPViewRenderer.cs's ReassembleOutsideViewFromWalk
+ /// call), so a pin through Assemble proves nothing about the code
+ /// that actually runs. This pin now drives that EXACT pair: a synthetic
+ /// holding one pixel-space polygon (built
+ /// the way WalkCopyViewTests builds views), fed through
+ /// BeginWalkFrame(frame, outdoorRoot: false) then
+ /// ReassembleOutsideViewFromWalk — both of which now call the
+ /// SAME shared ClipFrameAssembler.AppendOutsideSlice helper
+ /// Assemble would have (L4's own de-duplication), so this pin
+ /// exercises the helper through the producer that actually runs.
+ /// MUTATION: perturbing planes[0].W INSIDE that shared helper (the
+ /// one place both producers pack a plane) breaks this pin — see the
+ /// commit body for the recorded failing assertion text. The pin's own
+ /// source contains no Assemble( call (grep-checked in the commit
+ /// body). Same non-axis-aligned synthetic polygon as before, kept
+ /// distinct from the exit-seal pin's input.
///
[Fact]
public void ClipViewSlicePlanes_PunchFanPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView()
{
+ const float ViewportWidth = 640f, ViewportHeight = 480f;
Vector2[] verts =
[
new(0f, 0.6f), new(-0.6f, -0.4f), new(0.5f, -0.5f), new(0.7f, 0.2f),
];
- // The EXACT production assembly path: ClipFrameAssembler.Assemble
- // packs the outside_view polygon into a slot and constructs the
- // ClipViewSlice DrawWalkPunchFan reads back through
- // clipAssembly.OutsideViewSlices[activeViewIndex].Planes.
- var pvFrame = new PortalVisibilityFrame();
- pvFrame.OutsideView.Add(new ViewPolygon(verts));
+ // Pixel-space points (origin top-left, +Y down) that ReassembleOutsideViewFromWalk's
+ // own inverse transform (px = (ndc+1)*W/2, py = (1-ndc)*H/2) maps back to `verts`.
+ var pixelPoints = new WalkScreenPoint[verts.Length];
+ for (int i = 0; i < verts.Length; i++)
+ {
+ float px = (verts[i].X + 1f) * ViewportWidth / 2f;
+ float py = (1f - verts[i].Y) * ViewportHeight / 2f;
+ pixelPoints[i] = new WalkScreenPoint(px, py, 0f, 1f);
+ }
- var frame = ClipFrame.NoClip();
- ClipFrameAssembly assembly = ClipFrameAssembler.Assemble(frame, pvFrame);
+ var walkView = new WalkPortalView();
+ Assert.True(WalkCopyView.Append(
+ walkView, pixelPoints, new SyntheticRayCaster(), Vector3.Zero));
+
+ // The EXACT production pair: BeginWalkFrame seeds an interior root's
+ // empty outside-view assembly; ReassembleOutsideViewFromWalk (the
+ // walk's real producer) fills it from the walk's own pixel-space
+ // view — the same two calls RetailPViewPassExecutor.cs and
+ // RetailPViewRenderer.cs make.
+ ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
+ ClipFrame.NoClip(), outdoorRoot: false);
+ ClipFrameAssembler.ReassembleOutsideViewFromWalk(
+ assembly, walkView, ViewportWidth, ViewportHeight);
ClipViewSlice slice = Assert.Single(assembly.OutsideViewSlices);
Assert.True(slice.Planes.Length >= 3);
@@ -286,6 +302,16 @@ public class ClipFrameLayoutTests
AssertEveryEdgeMidpointLiesOnSomeGpuPlane(verts, slice.Planes);
}
+ /// Trivial ray caster: only WalkPortalView's stored pixel
+ /// points feed
+ /// (via NDC conversion); the per-vertex plane a ray caster would seed is
+ /// unused by that path, so any non-degenerate direction works.
+ private sealed class SyntheticRayCaster : IWalkRayCaster
+ {
+ public Vector3 RayThrough(float screenX, float screenY) =>
+ Vector3.Normalize(new Vector3(screenX, screenY, 1000f));
+ }
+
///
/// CPU/GPU equivalence: a point on a convex polygon's edge must sit
/// (a) non-negative under EVERY plane (still inside-or-on the region —
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderDescriptorContractTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderDescriptorContractTests.cs
index 8d0c8487..f8c2d782 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderDescriptorContractTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderDescriptorContractTests.cs
@@ -133,12 +133,15 @@ public sealed class VulkanShaderDescriptorContractTests
|| module.StartsWith("terrain_atmospheric.", StringComparison.Ordinal);
///
- /// The regression itself, named. TerrainClip is the only uniform block
- /// terrain_modern.vert declares besides SceneLighting, so
- /// asserting the module's uniform bindings as a set pins it exactly.
+ /// The regression itself, named — corrected for S3 chunk 4 fix round 2 (L3).
+ /// terrain_modern.vert used to also declare a screen-space clip block
+ /// at set 1 binding 2; that gate is deleted outright (no shader writer, no
+ /// GPU reader) and SceneLighting is now the ONLY uniform block the
+ /// module declares, so asserting the module's uniform bindings as a set
+ /// pins that directly.
///
[Fact]
- public void TerrainVertexShaderDeclaresItsClipBlockInTheUniformSet()
+ public void TerrainVertexShaderDeclaresOnlySceneLightingInTheUniformSet()
{
uint[] uniformBindings =
[
@@ -152,9 +155,7 @@ public sealed class VulkanShaderDescriptorContractTests
.Order(),
];
- Assert.Equal(
- [GpuBindingModel.UniformSceneLighting, VulkanPipelineLayouts.UniformTerrainClip],
- uniformBindings);
+ Assert.Equal([GpuBindingModel.UniformSceneLighting], uniformBindings);
}
///
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
index d5cca20d..19e25259 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
@@ -62,9 +62,19 @@ public sealed class VulkanShaderManifestTests
// SkyFogRuleTests). A deliberate default-path change, reviewed
// with the world-fog-range fix in the same commit.
["sky.frag.spv"] = "1c4ae77056837cbdc188f8cfcc4b0e8851647cdfaf398f25d8c8ff489ef84d57",
- ["sky.vert.spv"] = "3b51945fa4ff1be1604144df92866bdd47aade22f9dd90267591ef36adb28cde",
+ // sky.vert re-pinned 2026-09-03 (Campaign OVERHAUL S3 chunk 4 fix
+ // round 2, L3): the screen-space TerrainClip UBO (binding=2) and
+ // its gl_ClipDistance writes are deleted outright — retail draws
+ // the sky ONCE, unclipped (GameSky::Draw(sky,0) @0x0050633c), and
+ // that mechanism's only CPU-side writer was already retired at
+ // fix round 1's K3, leaving the shader-side declaration dead.
+ ["sky.vert.spv"] = "7d67a9e3624d198b370d402b5c12e4ce925bf9b8e646ef5123636a86d5985ab5",
["terrain_modern.frag.spv"] = "7b3cdb01b837ed77ee20559a81c1ce5c9d5395300efcc072560ab0be3c5a1af9",
- ["terrain_modern.vert.spv"] = "9f4cb221ea6aed94a8d23af6cb8e3f3ed96c3cce6e50d135a72d3b55667b1557",
+ // terrain_modern.vert re-pinned 2026-09-03 (Campaign OVERHAUL S3
+ // chunk 4 fix round 2, L3): same deletion as sky.vert above —
+ // retail never view-clips terrain (LScape::draw draws whole
+ // landblocks; RenderDeviceD3D::DrawBlock @0x005a17c0).
+ ["terrain_modern.vert.spv"] = "8a73d89ef0e51e550327b9ff8c24857e309103b1d491030cf0d4d8594b45068c",
["ui_text.frag.spv"] = "37a281bf80441cb425eaa3ad8e0b3a43cfa21b74b60973ed4201718b9dc102df",
["ui_text.vert.spv"] = "018ac64477cf7d4c3fc0c5878951b148c7bfeb6ee3a7eebb02381d7904877798",
["vk_probe.frag.spv"] = "c2dedbcc6dcc89744707b4b47138f1c31b38ef9088e584f1da07dd6953586c42",
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
index 29bb1219..2d4147d9 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
@@ -18,7 +18,7 @@ namespace AcDream.App.Tests.Rendering.Gpu.Vk;
///
/// The scissor is the trap. It does NOT flip with the viewport:
/// vkCmdSetScissor is always top-left-origin regardless of viewport sign,
-/// while NdcScissorRect.ToPixels emits GL bottom-left rectangles. The V3
+/// while every caller passes a GL-convention bottom-left rectangle. The V3
/// audit flagged this explicitly as a V6 acceptance item, and getting it wrong
/// shows up as a doorway aperture clipped from the wrong edge — which only a
/// scene containing one would reveal.
diff --git a/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs b/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs
index 22c32fc3..2d3028dd 100644
--- a/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Issue130DoorwayStripTests.cs
@@ -14,29 +14,44 @@ namespace AcDream.App.Tests.Rendering;
/// 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 is gated per fragment by the OutsideView region — the
-/// same dat polygon run through ProjectToClip → ClipToRegion (1-px
+/// 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 (BeginDoorwayScissor) on the slice AABB on
-/// top. Every one of those passes can only SHRINK the gate, so any shave shows
-/// as a strip of clear color between the gate's top edge and the aperture's
-/// rasterized top edge (the shell wall starts above it; the seal z-kills
-/// everything beyond; nothing re-covers).
+/// 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.
///
-/// This harness measures that 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 gate admits them.
-/// Plane-gap and scissor-gap are measured separately (mechanism attribution).
+/// 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.
///
-/// VERDICT (2026-06-12, 147 eye/gaze combos): the CPU polygon pipeline is
-/// sub-pixel exact (worst 0.54 px) — the W=0 clip port 987313a and both merge
-/// passes are EXONERATED. The strip was the scissor box: the old
-/// Floor(origin)+Ceiling(size) form cut up to 1 px off the TOP/RIGHT edges at
-/// unlucky fractional alignments (captured live by this harness: top edge
-/// y=0.7938 at 1080p → row 968 cut; right edge x=0.3503 at 1920 → column 1296
-/// cut). Fixed by the conservative NdcScissorRect bound; the assertions below
-/// pin both properties.
+/// 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
@@ -108,7 +123,7 @@ public class Issue130DoorwayStripTests
_out.WriteLine(FormattableString.Invariant(
$" poly[{i}] world=({worldPoly[i].X:F3},{worldPoly[i].Y:F3},{worldPoly[i].Z:F3})"));
- float worstPlaneGapPx = 0f, worstScissorGapPx = 0f;
+ float worstPlaneGapPx = 0f;
string worstDesc = "(none)";
// Eye sweep: back off the doorway along the inward normal at several
@@ -155,21 +170,20 @@ public class Issue130DoorwayStripTests
}
evaluated++;
- (float planeGapPx, float scissorGapPx, float atX) =
- MeasureTopEdgeGap(aperture, asm.OutsideViewSlices, 1920, 1080);
+ (float planeGapPx, float atX) =
+ MeasureTopEdgeGap(aperture, asm.OutsideViewSlices, 1080);
- if (planeGapPx > worstPlaneGapPx || scissorGapPx > worstScissorGapPx)
+ if (planeGapPx > worstPlaneGapPx)
{
worstDesc = FormattableString.Invariant(
- $"d={d} h={h} lat={lat} gz={gz} minW={minW:F2} atX={atX:F3} slices={asm.OutsideViewSlices.Length} mode={asm.TerrainMode} outVerts={DescribePolys(pv.OutsideView)} apVerts={aperture.Length}");
- worstPlaneGapPx = MathF.Max(worstPlaneGapPx, planeGapPx);
- worstScissorGapPx = MathF.Max(worstScissorGapPx, scissorGapPx);
+ $"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 || scissorGapPx > 0.55f)
+ if (planeGapPx > 0.55f)
{
_out.WriteLine(FormattableString.Invariant(
- $"GAP d={d} h={h} lat={lat} gz={gz}: planeGap={planeGapPx:F2}px scissorGap={scissorGapPx:F2}px atX={atX:F3} mode={asm.TerrainMode} outVerts={DescribePolys(pv.OutsideView)}"));
+ $"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(
@@ -185,15 +199,10 @@ public class Issue130DoorwayStripTests
}
_out.WriteLine(FormattableString.Invariant(
- $"evaluated={evaluated} worstPlaneGapPx={worstPlaneGapPx:F2} worstScissorGapPx={worstScissorGapPx:F2} @ {worstDesc}"));
+ $"evaluated={evaluated} worstPlaneGapPx={worstPlaneGapPx:F2} @ {worstDesc}"));
Assert.True(evaluated > 100, $"sweep degenerated: only {evaluated} eye/gaze combos evaluated");
- // PIN 1 (#130): the scissor box never cuts a fragment the plane gate
- // admits — conservative containment (AD-17's over-include doctrine).
- // One probe step is ~0.11 px; anything beyond it is a real cut row.
- Assert.True(worstScissorGapPx <= 0.15f, FormattableString.Invariant(
- $"scissor under-covers the plane-admitted region by {worstScissorGapPx:F2}px @ {worstDesc}"));
- // PIN 2 (canary): the CPU polygon pipeline (ProjectToClip → ClipToRegion
+ // 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
@@ -211,12 +220,14 @@ public class Issue130DoorwayStripTests
///
/// For sample x positions across the aperture's projected top edge, find the
- /// aperture boundary's top y, then walk downward until the gate admits the
- /// point. Returns the worst gaps in 1080p pixels (plane gate and modeled
- /// scissor gate measured independently), and the x of the worst plane gap.
+ /// 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 scissorGapPx, float atX) MeasureTopEdgeGap(
- Vector2[] aperture, ClipViewSlice[] slices, int fbW, int fbH,
+ 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
@@ -226,9 +237,9 @@ public class Issue130DoorwayStripTests
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, 0);
+ if (span <= 0.01f) return (0, 0);
- float worstPlane = 0, worstScissor = 0, atX = 0;
+ float worstPlane = 0, atX = 0;
const int Samples = 160;
for (int s = 0; s <= Samples; s++)
{
@@ -240,22 +251,15 @@ public class Issue130DoorwayStripTests
var p = new Vector2(x, topY - Inset);
float planeGap = GapBelow(p, q => AnySliceAdmitsPlanes(slices, q), StepY, CapY);
- // The scissor question is "does the box cut pixels the PLANES would
- // draw" — measure it from the planes-admitted top, not the aperture
- // top (at slanted corners the aperture top can sit legitimately
- // outside the gate polygon's column).
- var pPlanes = new Vector2(p.X, p.Y - planeGap - Inset);
- float scissorGap = GapBelow(pPlanes, q => AnySliceAdmitsScissor(slices, q, fbW, fbH), StepY, CapY);
- if (debug is not null && scissorGap > 0.005f)
+ 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 pPlanes=({pPlanes.X:F4},{pPlanes.Y:F4}) scissorGap={scissorGap * fbH / 2f:F2}px"));
+ $" sample x={x:F4} apTop={topY:F4} planeGap={planeGap * fbH / 2f:F2}px"));
if (planeGap > worstPlane) { worstPlane = planeGap; atX = x; }
- worstScissor = MathF.Max(worstScissor, scissorGap);
}
// NDC y → pixels at the given framebuffer height.
- return (worstPlane * fbH / 2f, worstScissor * fbH / 2f, atX);
+ return (worstPlane * fbH / 2f, atX);
}
private static float GapBelow(Vector2 start, Func admitted, float step, float cap)
@@ -272,7 +276,7 @@ public class Issue130DoorwayStripTests
// 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 (scissor fallback) admits its whole NDC AABB.
+ // 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)
@@ -294,22 +298,6 @@ public class Issue130DoorwayStripTests
return false;
}
- // Production scissor (BeginDoorwayScissor → NdcScissorRect.ToPixels): a
- // point is admitted when its pixel falls inside some slice's scissor box.
- private static bool AnySliceAdmitsScissor(ClipViewSlice[] slices, Vector2 p, int fbW, int fbH)
- {
- int pixX = (int)MathF.Floor((p.X * 0.5f + 0.5f) * fbW);
- int pixY = (int)MathF.Floor((p.Y * 0.5f + 0.5f) * fbH);
- foreach (var slice in slices)
- {
- var box = NdcScissorRect.ToPixels(slice.NdcAabb, fbW, fbH);
- if (pixX >= box.X && pixX < box.X + box.Width
- && pixY >= box.Y && pixY < box.Y + box.Height)
- 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)
diff --git a/tests/AcDream.App.Tests/Rendering/NdcScissorRectTests.cs b/tests/AcDream.App.Tests/Rendering/NdcScissorRectTests.cs
deleted file mode 100644
index 2dc084ff..00000000
--- a/tests/AcDream.App.Tests/Rendering/NdcScissorRectTests.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using System.Numerics;
-using AcDream.App.Rendering;
-using Xunit;
-
-namespace AcDream.App.Tests.Rendering;
-
-///
-/// #130: the doorway-slice scissor must be a CONSERVATIVE outer bound of its
-/// NDC AABB (AD-17: over-inclusion safe, under-inclusion is the bug class).
-/// The old Floor(origin)+Ceiling(size) form put the far edge at
-/// floor(min)+ceil(max−min), up to one pixel short of the true max edge —
-/// the doorway top-edge background strip.
-///
-public class NdcScissorRectTests
-{
- /// Containment property: every pixel whose CENTER lies inside the
- /// NDC box is inside the scissor box, across a dense grid of fractional
- /// alignments at two framebuffer sizes.
- [Theory]
- [InlineData(1920, 1080)]
- [InlineData(2560, 1440)]
- public void EveryCenterInsidePixel_IsInsideTheBox(int fbW, int fbH)
- {
- for (int i = 0; i < 251; i++)
- {
- // Sweep fractional alignments of all four edges.
- float f = i / 251f;
- float minX = -0.83f + f * 0.0031f;
- float minY = -0.71f + f * 0.0047f;
- float maxX = 0.339f + f * 0.0043f;
- float maxY = 0.7938f + f * 0.0029f;
- var box = NdcScissorRect.ToPixels(new Vector4(minX, minY, maxX, maxY), fbW, fbH);
-
- // Pixel-space extremes of center-inside pixels.
- float x0 = (minX * 0.5f + 0.5f) * fbW, x1 = (maxX * 0.5f + 0.5f) * fbW;
- float y0 = (minY * 0.5f + 0.5f) * fbH, y1 = (maxY * 0.5f + 0.5f) * fbH;
- int loX = (int)MathF.Ceiling(x0 - 0.5f), hiX = (int)MathF.Floor(x1 - 0.5f);
- int loY = (int)MathF.Ceiling(y0 - 0.5f), hiY = (int)MathF.Floor(y1 - 0.5f);
-
- Assert.True(box.X <= loX, $"left cut: box.X={box.X} > loX={loX} (minX={minX})");
- Assert.True(box.Y <= loY, $"bottom cut: box.Y={box.Y} > loY={loY} (minY={minY})");
- Assert.True(box.X + box.Width > hiX, $"right cut: box ends {box.X + box.Width} <= hiX={hiX} (maxX={maxX})");
- Assert.True(box.Y + box.Height > hiY, $"top cut: box ends {box.Y + box.Height} <= hiY={hiY} (maxY={maxY})");
- // Over-inclusion stays bounded (≤1 px per edge).
- Assert.True(box.X >= loX - 1 && box.Y >= loY - 1);
- Assert.True(box.X + box.Width <= hiX + 2 && box.Y + box.Height <= hiY + 2);
- }
- }
-
- [Fact]
- public void CapturedRegression_TopEdgeRow968_At1080p()
- {
- // Issue130DoorwayStripTests live capture: aperture top y=0.7938 →
- // pixel row 968 (center 968.5 < 968.65). The old formula ended the box
- // at row 967 — the visible strip.
- var box = NdcScissorRect.ToPixels(new Vector4(-0.339f, -0.743f, 0.339f, 0.7938f), 1920, 1080);
- Assert.True(box.Y + box.Height > 968, $"top row 968 cut: box ends at {box.Y + box.Height}");
- }
-
- [Fact]
- public void CapturedRegression_RightColumn1296_At1920()
- {
- // Issue130DoorwayStripTests live capture: gate right edge x=0.3507 →
- // pixel column 1296 admitted by the plane gate; the old formula ended
- // the box at column 1295.
- var box = NdcScissorRect.ToPixels(new Vector4(-0.2845f, -1.0f, 0.3507f, 0.2630f), 1920, 1080);
- Assert.True(box.X + box.Width > 1296, $"right column 1296 cut: box ends at {box.X + box.Width}");
- }
-
- [Fact]
- public void DegenerateAndOffscreenBoxes_StayValid()
- {
- // Past-the-edge regions clamp to the screen and keep min 1 px size.
- var box = NdcScissorRect.ToPixels(new Vector4(0.999f, 0.999f, 1.5f, 1.5f), 1920, 1080);
- Assert.True(box.Width >= 1 && box.Height >= 1);
- var inverted = NdcScissorRect.ToPixels(new Vector4(1f, 1f, -1f, -1f), 1920, 1080);
- Assert.True(inverted.Width >= 1 && inverted.Height >= 1);
- }
-}
diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
index b793bfac..ebff31e9 100644
--- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Reflection.Emit;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Sky;
@@ -120,9 +121,10 @@ public sealed class RetailPViewPassExecutorTests
/// WalkFrameDriverTranscriptTests: Collect_OutdoorRoot_...
/// and Collect_InteriorRoot_...). This method now draws the
/// weather MESH and the rain PARTICLES only — the former per-outside-
- /// view-slice loop that used to run before this call (SetTerrainClip
- /// + ClearClipRouting + the old DrawLandscapeSliceLate leaf)
- /// is deleted outright (§10.2): retail draws the weather mesh and its
+ /// view-slice loop that used to run before this call (the walk's own
+ /// screen-space terrain-clip writer + ClearClipRouting + the old
+ /// DrawLandscapeSliceLate leaf) is deleted outright (§10.2): retail
+ /// draws the weather mesh and its
/// rain particles ONCE, unclipped, never once per doorway aperture.
/// MUTATION: re-inlining a
/// WalkTranscriptDump.PrintObjectCellTurn call back into this
@@ -153,9 +155,9 @@ public sealed class RetailPViewPassExecutorTests
///
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
- /// (SetTerrainClip + ClearClipRouting + the old
- /// DrawLandscapeSliceLate leaf, one call per active landscape
- /// view) is deleted — DrawLandscapeDynamicsPhase now calls
+ /// (the walk's own screen-space terrain-clip writer +
+ /// 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
/// Assert.Single alone proved insufficient at fix round 1 (K1):
@@ -222,6 +224,103 @@ public sealed class RetailPViewPassExecutorTests
&& callOffset < branch.Offset);
}
+ ///
+ /// S3 chunk 4 fix round 2 (L1, BLOCKING). Round 1's three-lens review
+ /// found that neither existing pin above actually looks at the
+ /// production CONDITION gating DrawWeatherOnce: restoring the
+ /// pre-fix gate if (clipAssembly.OutsideViewSlices.Length != 0) —
+ /// the exact regression K2 was supposed to close — leaves both green,
+ /// because both only ask "is the call site shaped correctly", never
+ /// "does the call site read WalkFrameDriver.WeatherTurnFired".
+ /// This pin reads the compiled condition directly: with c the
+ /// index of the DrawWeatherOnce call, (a) calls[c-1] must
+ /// be the WeatherTurnFired getter — the LAST call before the draw
+ /// — and (b) exactly one branch must sit strictly between that getter
+ /// call and the draw call, be a brfalse/brfalse.s, and
+ /// jump FORWARD past the draw call — the compiled shape of
+ /// if (walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);
+ /// and nothing else (an inverted test, an unconditional call, or a
+ /// different condition entirely all fail one of the two checks).
+ /// MUTATION M1 (restores the pre-fix regression): change the gate back
+ /// to if (clipAssembly.OutsideViewSlices.Length != 0) — check (a)
+ /// fails because calls[c-1] is no longer the
+ /// WeatherTurnFired getter. MUTATION M2 (drops the gate
+ /// entirely): make the call unconditional — check (b) fails because no
+ /// branch sits between the getter call and the draw call (in fact the
+ /// getter call itself disappears with the gate, so check (a) fails
+ /// first). MUTATION M3 (inverts the condition): change the gate to
+ /// if (!walkDriver.WeatherTurnFired) — calls[c-1] is still
+ /// the getter (check (a) passes), but the compiler emits a
+ /// brtrue/brtrue.s to skip the draw instead of a
+ /// brfalse/brfalse.s, so check (b)'s opcode filter finds
+ /// nothing and Assert.Single fails on zero matches.
+ ///
+ [Fact]
+ public void DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired()
+ {
+ MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
+ "DrawLandscapeDynamicsPhase",
+ BindingFlags.Instance | BindingFlags.NonPublic)!;
+ IReadOnlyList calls = CompiledCallGraph.Read(method);
+ int callIndex = RequiredCallIndex(
+ calls,
+ typeof(RetailPViewPassExecutor),
+ nameof(RetailPViewPassExecutor.DrawWeatherOnce));
+ Assert.True(callIndex > 0, "Expected a call before DrawWeatherOnce — the gate condition.");
+
+ CompiledCall condition = calls[callIndex - 1];
+ Assert.Equal(typeof(AcDream.App.Rendering.Walk.WalkFrameDriver), condition.Target.DeclaringType);
+ Assert.Equal("get_WeatherTurnFired", condition.Target.Name);
+
+ int conditionOffset = condition.Offset;
+ int drawOffset = calls[callIndex].Offset;
+ IReadOnlyList branches = CompiledCallGraph.ReadBranches(method);
+ Assert.Single(
+ branches,
+ branch => branch.Offset > conditionOffset
+ && branch.Offset < drawOffset
+ && (branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S)
+ && branch.TargetOffset > drawOffset);
+ }
+
+ ///
+ /// S3 chunk 4 fix round 2 (L8): the identical loop-shape question K1
+ /// asked of 's call
+ /// site, applied to 's
+ /// own call — retail draws the sky
+ /// dome exactly once per frame too (K4's own doc comment on
+ /// DrawWalkSky), so nothing may wrap this call in a loop either.
+ /// Note for reviewers: 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. MUTATION: wrapping the call in
+ /// for (int i = 0; i < 2; i++) { _sky?.RenderSky(...); } makes
+ /// this fail; restoring the single unconditional call makes it pass
+ /// again.
+ ///
+ [Fact]
+ public void DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch()
+ {
+ MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
+ "DrawWalkSky",
+ BindingFlags.Instance | BindingFlags.NonPublic)!;
+ IReadOnlyList calls = CompiledCallGraph.Read(method);
+ int callIndex = RequiredCallIndex(
+ calls,
+ typeof(SkyRenderer),
+ nameof(SkyRenderer.RenderSky));
+ int callOffset = calls[callIndex].Offset;
+
+ IReadOnlyList branches = CompiledCallGraph.ReadBranches(method);
+ Assert.DoesNotContain(
+ branches,
+ branch => branch.TargetOffset < branch.Offset
+ && branch.TargetOffset <= callOffset
+ && callOffset < branch.Offset);
+ }
+
///
/// S3 chunk 1 fix round 2 (§11.6 H1): is
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs
index 9ce4e67d..54f21cbb 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs
@@ -723,13 +723,22 @@ public sealed partial class WalkFrameDriverTests
/// separately at Replay — on an interior root whose landscape turn ran
/// with the gate open but whose reassembled outside-view slices ended
/// up empty, the transcript could report a weather turn the frame never
- /// actually drew. is exactly the flag
- /// RetailPViewRenderer.DrawLandscapeDynamicsPhase now gates
- /// DrawWeatherOnce on, so this test's flag assertion doubles as
- /// the draw-side pin: gate closed -> the flag stays false (so
- /// DrawWeatherOnce would not fire either) AND no "OC" line
- /// prints; gate open -> the flag becomes true (so
- /// DrawWeatherOnce would fire) AND exactly one "OC" line prints.
+ /// actually drew. is
+ /// exactly the flag RetailPViewRenderer.DrawLandscapeDynamicsPhase
+ /// gates DrawWeatherOnce on: gate closed -> the flag stays
+ /// false; gate open -> the flag becomes true AND exactly one "OC"
+ /// line prints. CORRECTION (S3 chunk 4 fix round 2, L1): this test's
+ /// flag assertion does NOT double as the draw-side pin — it builds a
+ /// bare and never touches
+ /// DrawLandscapeDynamicsPhase, so it cannot see whether that
+ /// method actually reads this flag (round 1 shipped with the pre-fix
+ /// gate clipAssembly.OutsideViewSlices.Length != 0 silently
+ /// restored, and every test in this file stayed green). The real
+ /// draw-side pin is
+ /// RetailPViewPassExecutorTests.DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired,
+ /// which reads the compiled call graph of
+ /// DrawLandscapeDynamicsPhase itself and asserts it calls
+ /// DrawWeatherOnce gated on THIS getter.
///
[Theory]
[InlineData(false)]
diff --git a/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs b/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
index ab07b7b6..0b4dadc4 100644
--- a/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
@@ -61,7 +61,6 @@ public class WalkOutsideViewReassemblyTests
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.True(asm.OutdoorVisible);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(slice.Slot, asm.OutdoorSlot);
@@ -101,7 +100,6 @@ public class WalkOutsideViewReassemblyTests
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.Equal(new Vector4(-1f, -1f, 1f, 1f), slice.NdcAabb);
- Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
}
[Fact]
@@ -115,7 +113,6 @@ public class WalkOutsideViewReassemblyTests
Assert.Empty(asm.OutsideViewSlices);
Assert.False(asm.OutdoorVisible);
Assert.False(asm.HasOutsideView);
- Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
Assert.Equal(0, asm.OutsidePlaneCount);
Assert.Equal(0, asm.OutdoorSlot);
}