diff --git a/docs/launch-options.md b/docs/launch-options.md
index fe1f77e8..83ffba72 100644
--- a/docs/launch-options.md
+++ b/docs/launch-options.md
@@ -304,7 +304,6 @@ issue is closed, the strip was missed; delete both.
| Flag | Owning investigation | Value | What it does | Side effects | Read by |
|---|---|---|---|---|---|
-| `ACDREAM_CLIP_DEBUG` | #176 | `=1` | forces the EnvCell SHELL pass to map every instance to clip slot 0 (no-clip) instead of its cell's portal-slice region | ALTERS RENDERED OUTPUT: shells draw whole/unclipped instead of trimmed — a visual isolation mode, not a log-only probe; no DebugPanel mirror | `RenderingDiagnostics.ClipDebugNoShellTrim` |
| `ACDREAM_DUMP_APPEARANCE` | #5 | `="1"` | Logs every `0xF625` ObjDescEvent + `0xF7DB` UpdateObject with body length, target guid, hex preview — used to debug remote-player appearance asymmetry | print-only (`Console.WriteLine`) | `WorldSession` static field `DumpAppearanceEnabled` (`WorldSession.cs:792-793`), raw scattered read, issue #5 diagnostic |
| `ACDREAM_DUMP_OPCODES` | #5 | `="1"` | Logs first occurrence of each genuinely-unhandled inbound opcode (deduped by opcode) | print-only. Must stay the LAST else-if in the dispatch chain per comment (else it would intercept handled opcodes) — currently correct. | `WorldSession` static field `DumpOpcodesEnabled` (`WorldSession.cs:788-789`, consumed `WorldSession.cs:2391-2398`), issue #5 diagnostic. Also mirrored (display-only, non-functional) via `DebugPanel.cs:241`/`DebugVM.cs:227`. |
| `ACDREAM_DUMP_SCENERY_Z` | #48 | `=1` | Per-spawn Z-placement diagnostic for procedural scenery (trees/bushes/rocks), added for issue #48 (the "trees-in-sky" bug). | **NOT print-only** — this is a real behavior fork, not just added logging. `LandblockBuildFactory.cs:167-178`: when the flag is on, the streaming worker calls a **separate, duplicate scenery-building method** (`BuildSceneryEntitiesForStreaming`, a full parallel reimplementation of GfxObj/Setup mesh resolution + placement inline in this file) instead of production's `LandblockPhysicsContentBuilder.HydrateProceduralScenery`. Any visual/measurement run taken with this flag set is exercising a different scenery-placement code path than production, which can drift from it silently. | `RuntimeOptions.DumpSceneryZ` → `SessionPlayerComposition.cs:280` → `LandblockBuildFactory.cs:23,42,168,335` |
@@ -314,7 +313,7 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_PROBE_CELLSET` | a6.p5 | `=1` | gates `PhysicsDiagnostics.LogCellSetBuild`, one `[cellset-build]` line per `BuildCellSetAndPickContaining` call (seed cell, sphere XY, candidate list) from `CellTransit.cs:1468` | print-only; builds a `StringBuilder` of the candidate id list only when the flag is on | `PhysicsDiagnostics.ProbeCellSetEnabled` |
| `ACDREAM_PROBE_CELL_CACHE` | indoor walking phase d | `=1` | gates one `[cell-cache]` line per EnvCell first-cached in `PhysicsDataCache.CacheCellStruct` (poly counts, BSP root structure) | print-only; fires at most once per EnvCell (cache is no-op after first population); no DebugPanel mirror | `PhysicsDiagnostics.ProbeCellCacheEnabled` |
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
-| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
+| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-scis]` lines: outside-slice clip routing + region-SSBO bytes, actual GL/RHI scissor state (S3 review fix round 1, F5: the third producer, `[clip-route-disp]` — WbDrawDispatcher's per-entity clip-slot routing histogram — is deleted with the routing state it reported on) | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |
diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs
index 1231a9d6..4222e2b0 100644
--- a/src/AcDream.App/Rendering/ClipFrame.cs
+++ b/src/AcDream.App/Rendering/ClipFrame.cs
@@ -1,29 +1,37 @@
// ClipFrame.cs
//
-// 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
+// Phase U.3: the per-frame CPU plane store — AppendSlot/GetSlotPlanes — that
+// WalkFrameDriver.InteriorFloodViewClipPlanesAt resolves through for the
+// exit-seal/punch-fan KEEP clips (S3 §8/§10). 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 (the shader side obeys) =====================================
+// S3 review fix round 1 (F5): the mesh SSBO clip-region table (binding=2)
+// this file used to also publish for mesh_modern.vert (and its four sibling
+// mesh/particle vertex shaders) is DELETED, not merely dormant — the ONE
+// WbDrawDispatcher API that could ever have armed a non-zero clip slot for
+// an instance had ZERO production callers, so every instance mapped to slot
+// 0 (no-clip) in every shipped build, and the shaders' own per-cell fetch +
+// clip-distance loop are now gone too (see mesh_modern.vert's own doc
+// comment). RegionBytes/RegionBytesForTest below —
+// the GPU-shaped byte blob PrepareClipFrame used to publish for that dead
+// consumer — stay as UNTOUCHED RESIDUE this round (WorldPassSurface.cs's
+// PrepareClipFrame doc comment names it); AppendSlot/GetSlotPlanes are the
+// part every consumer left (the exit seals) actually needs.
+//
+// === The byte layout AppendSlot/GetSlotPlanes still use internally ===========
// 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).
//
-// 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.
+// The one CONSUMER 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 arena,
@@ -40,9 +48,12 @@ using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
///
-/// 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 /
+/// Per-frame CPU plane store (/
+/// ) the exit-seal/punch-fan KEEP clips read
+/// through WalkFrameDriver.InteriorFloodViewClipPlanesAt. See the file
+/// header for the internal std430 byte layout, for why / are untouched residue
+/// (S3 review fix round 1, F5), and for /
/// 's separate remaining purpose.
/// Per-instance slot buffers (binding=3) are owned by each renderer, not here.
///
@@ -84,7 +95,7 @@ public sealed class ClipFrame : IDisposable
// ---- CPU-side state ------------------------------------------------------
- // Packed std430 bytes for clipRegions[]. Always holds at least slot 0.
+ // Packed std430 bytes, one CellClip per slot. Always holds at least slot 0.
private byte[] _regionBytes;
private int _slotCount;
diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
index fe0fbace..46a01e39 100644
--- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs
+++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
@@ -47,8 +47,23 @@ public enum TerrainClipMode
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is
/// retained for diagnostics () only — no
/// GPU pass reads it; there is no scissor consumer left in the walk.
+///
+/// S3 review fix round 1 (F1):
+/// distinguishes the "this walk view's own polygon collapsed" case (retail
+/// polyClipFinish under a degenerate view leaves <3 vertices —
+/// 0x59BDBC-0x59BDBF) from the pre-existing Slot == 0, Planes ==
+/// [] "plane overflow" case (a convex region needing >8 half-planes —
+/// draws UNCLIPPED, over-include, never a scissor). Both states share
+/// Slot == 0 and an empty array, so a consumer
+/// MUST check this flag rather than infer intent from Planes.Length:
+/// NothingVisible == true means DRAW NOTHING;
+/// NothingVisible == false with empty means DRAW
+/// UNCLIPPED. See 's
+/// index-alignment doc comment for why every walk view must occupy exactly
+/// one slice slot.
///
-public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
+public readonly record struct ClipViewSlice(
+ int Slot, Vector4 NdcAabb, Vector4[] Planes, bool NothingVisible = false);
///
/// Populated clip buffers plus routing data consumed by the render walk.
@@ -438,6 +453,25 @@ public static class ClipFrameAssembler
/// Must run AFTER the walk's Collect and BEFORE
/// PrepareClipFrame publishes the clip regions — appended slots
/// join the same single publication.
+ ///
+ /// S3 review fix round 1 (F1, BLOCKING): keeps exactly ONE slice
+ /// per walk view, index-aligned with 's own
+ /// ViewCount. RetailFrameWalk.DrawBuilding pins
+ /// ActiveViewIndex = v over these SAME walk views (retail
+ /// building_view = portal_view_num @0x0059f3bf) and the punch
+ /// leaf () indexes
+ /// by that index. The
+ /// shared helper SKIPS a view whose
+ /// polygon collapses () —
+ /// correct for 's "empty regions are omitted
+ /// entirely" legacy policy, but wrong here: skipping would make the
+ /// slice array SHORTER than the view list, so every later view's fan
+ /// would read the WRONG view's planes (off by however many views
+ /// collapsed before it). A collapsed view instead gets its own slice
+ /// with set (slot 0, empty
+ /// planes, empty AABB) — the punch leaf draws nothing for that view
+ /// specifically, never a neighboring view's fan under the wrong clip.
+ ///
///
public static void ReassembleOutsideViewFromWalk(
ClipFrameAssembly assembly,
@@ -497,13 +531,24 @@ public static class ClipFrameAssembler
if (poly.MaxY > unionMaxY) unionMaxY = poly.MaxY;
}
- AppendOutsideSlice(
+ bool appended = AppendOutsideSlice(
frame,
poly,
outsideSlicesList,
ref outsideMaxPlaneCount,
ref outsideHasScissorFallback,
ref scissorFallbacks);
+
+ if (!appended)
+ {
+ // F1: this walk view's own polygon collapsed (empty after
+ // projection, or ClipPlaneSet.IsNothingVisible — a line/point
+ // with no screen coverage). It still occupies slot v so the
+ // NEXT view's slice lands at index v+1, not v — see this
+ // method's own doc comment.
+ outsideSlicesList.Add(
+ new ClipViewSlice(0, default, System.Array.Empty(), NothingVisible: true));
+ }
}
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
index c1f0fea3..4ff89f69 100644
--- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
+++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
@@ -365,9 +365,10 @@ public sealed class PortalTunnelPresentation : IDisposable
{
UploadRetailLight();
// The dispatcher is shared with the world pass. Portal space is its
- // own CreatureMode scene: it has no world-cell clip routing and no
- // world point lights, only the distant light installed above.
- _dispatcher.ClearClipRouting();
+ // own CreatureMode scene: no world point lights, only the distant
+ // light installed above (S3 review fix round 1, F5: the former
+ // per-frame clip-routing reset call here is deleted with the routing
+ // state it reset — every instance is slot 0 regardless).
_dispatcher.SetSceneLights(null);
var entries = new (uint, Vector3, Vector3, IReadOnlyList, IReadOnlyDictionary?)[]
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs
index 79d15f09..20e7a0dc 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs
@@ -99,7 +99,21 @@ internal sealed partial class RetailPViewPassExecutor
/// @0x0059bc90's far-Z punch through PortalDepthMaskRenderer,
/// clipped by the pinned view's slice planes (retail
/// building_view @0x0059f3bf). FW3.3: fans draw at the dat
- /// aperture verbatim (the ShellDrawLiftZ retirement).
+ /// aperture verbatim (the ShellDrawLiftZ retirement).
+ ///
+ /// S3 review fix round 1 (F1, BLOCKING): indexes one-for-one with the
+ /// walk's own views ( keeps the
+ /// arrays index-aligned). An out-of-range index means the two arrays
+ /// desynchronized — drawing unclipped in that case would be a real
+ /// visibility bug (the fan could punch through walls no view actually
+ /// exposes), so this FAILS LOUD instead. A slice flagged means the pinned view's own
+ /// polygon collapsed (retail's polyClipFinish under a degenerate
+ /// view leaves <3 vertices → no fan, 0x59BDBC-0x59BDBF): this
+ /// draws NOTHING for it, matching retail.
internal void DrawWalkPunchFan(
RetailPViewFrameInput frame,
ClipFrameAssembly clipAssembly,
@@ -111,10 +125,23 @@ internal sealed partial class RetailPViewPassExecutor
Vector3[] vertices = worldPolygon.Vertices;
if (vertices.Length < 3)
return;
+
ReadOnlySpan slices = clipAssembly.OutsideViewSlices;
- ReadOnlySpan planes = (uint)activeViewIndex < (uint)slices.Length
- ? slices[activeViewIndex].Planes
- : default;
+ if ((uint)activeViewIndex >= (uint)slices.Length)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(activeViewIndex),
+ activeViewIndex,
+ $"punch fan pinned to walk view {activeViewIndex} but the "
+ + $"reassembled outside view only holds {slices.Length} "
+ + "index-aligned slice(s) — ReassembleOutsideViewFromWalk "
+ + "desynchronized from RetailFrameWalk.DrawBuilding's own "
+ + "view count (fail-loud rule; never draw unclipped).");
+ }
+
+ ClipViewSlice slice = slices[activeViewIndex];
+ if (slice.NothingVisible)
+ return;
Span world = stackalloc Vector3[32];
int count = Math.Min(vertices.Length, world.Length);
@@ -123,7 +150,7 @@ internal sealed partial class RetailPViewPassExecutor
_portalDepthMask.DrawDepthFan(
world[..count],
frame.ViewProjection,
- planes,
+ slice.Planes,
forceFarZ: true);
}
}
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
index 0ab6da15..a28a8be4 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
@@ -11,8 +11,8 @@ namespace AcDream.App.Rendering;
// 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
+// the RHI surface's NDC-to-pixel scissor conversion, and that scissor
+// mechanism itself is deleted in the same round (no producer of a narrowed
// rectangle remains anywhere in the walk).
internal sealed class RetailPViewCellSource : IRetailPViewCellSource
@@ -170,8 +170,8 @@ public RetailPViewPassExecutor(
/// 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 (the walk's own screen-space
- /// terrain-clip writer, ClearClipRouting, and the old
- /// DrawLandscapeSliceLate leaf, one call per active landscape
+ /// terrain-clip writer, its per-frame clip-routing reset call, 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
@@ -223,7 +223,6 @@ public RetailPViewPassExecutor(
if (!ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
return;
- DisableClipDistances();
_sky?.RenderWeather(
frame.Camera,
frame.CameraWorldPosition,
@@ -270,8 +269,6 @@ public RetailPViewPassExecutor(
// emitter owns one shadow in its OWN current cell, so this is a cell
// lookup, not an owner union — a hidden/suspended owner's emitter
// still draws here.
- DisableClipDistances();
-
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.DrawForCell(
@@ -281,9 +278,6 @@ public RetailPViewPassExecutor(
cellId,
clipSlot: 0);
}
-
- _entities.ClearClipRouting();
- DisableClipDistances();
}
public void ClearInteriorDepth()
@@ -320,7 +314,6 @@ public RetailPViewPassExecutor(
// The former once-per-OutsideView-slice submission with that slice's
// hardware clip slot made effects vanish by view direction (zero
// outside slices in view = zero submissions) — invented behavior.
- DisableClipDistances();
_particleRenderer.DrawForOwners(
frame.Camera,
frame.CameraWorldPosition,
@@ -342,7 +335,6 @@ public RetailPViewPassExecutor(
if (_particles is null || _particleRenderer is null)
return;
- DisableClipDistances();
// Retail never clips cell particles to a portal view: the owner
// cell's walls own occlusion via the depth test at the alpha flush.
// CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) draws an
@@ -354,7 +346,6 @@ public RetailPViewPassExecutor(
ParticleRenderPass.Scene,
cellId,
clipSlot: 0);
- DisableClipDistances();
}
public void EmitDiagnostics(
@@ -444,17 +435,31 @@ public RetailPViewPassExecutor(
// 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
+ // producer of a narrowed rectangle). The interface's own scissor
+ // begin/end pair 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
+ // the pass.
+ //
+ // S3 review fix round 1 (F3): round 2's own comment above claimed "the
// KEEP clips (exit seals, punch fans) bracket their own draws with
- // Enable/DisableClipDistances below.
-
- private void DisableClipDistances() => _surface.DisableClipDistances();
+ // Enable/DisableClipDistances below" — FALSE. Neither
+ // DrawExitPortalMask/DrawPortalDepthWrite nor
+ // RetailPViewPassExecutor.WalkLeaf.cs's DrawWalkPunchFan ever called
+ // Enable/DisableClipDistances; every one of this file's six
+ // DisableClipDistances() call sites sat around the UNCLIPPED particle
+ // draws (DrawWeatherOnce, DrawLandscapeStaticParticles x2,
+ // DrawUnattachedSceneParticles, DrawCellParticles x2) — bodies that were
+ // already no-ops on the only backend
+ // (RhiWorldPassSurface.EnableClipDistances/DisableClipDistances, see
+ // WorldPassSurface.cs). A no-op call bracketing an unclipped draw is not
+ // a clip bracket at all; it asserted a mechanism this file never had.
+ // Deleted outright with this correction, along with the private
+ // DisableClipDistances() wrapper (a method that does nothing on the
+ // only backend is a false mechanism too). IWorldPassSurface.
+ // EnableClipDistances/DisableClipDistances stay on the INTERFACE —
+ // WorldScenePassExecutor (the separate flat-world path) still calls
+ // them — only this file's now-pointless use of them is gone.
}
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 583ba3f5..290fee08 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -640,9 +640,9 @@ internal sealed class RetailPViewRenderer
}
// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
- // (the walk's own screen-space terrain-clip writer + ClearClipRouting
- // + DrawLandscapeSliceLate, one call per active landscape view) is
- // DELETED — retail draws the
+ // (the walk's own screen-space terrain-clip writer + its per-frame
+ // clip-routing reset call + 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) call site
// @0x00506396 — the callee itself is @0x00506ff0), never once per
diff --git a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
index cec1ba8c..7cf118a2 100644
--- a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
+++ b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
@@ -46,34 +46,17 @@ layout(std430, binding = 1) readonly buffer BatchBuffer {
BatchData Batches[];
};
-// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
-// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
-//
-// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
-// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
-// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
-//
-// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
-// instance buffer and indexed by the IDENTICAL per-instance index
-// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
-// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
-// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
-//
-// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
-// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
-// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
-// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
-// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
-struct CellClip {
- uint count;
- uint _p0;
- uint _p1;
- uint _p2;
- vec4 planes[8];
-};
-layout(std430, binding = 2) readonly buffer ClipRegionBuf {
- CellClip clipRegions[];
-};
+// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
+// (Phase U.3's binding=2 CellClip region SSBO) is deleted — the CPU-side
+// routing that could ever select a non-zero slot for an instance
+// (WbDrawDispatcher's per-frame clip-routing arming call) had ZERO production callers, so every
+// instance has always mapped to slot 0 (no-clip) in every shipped build; a
+// shader-side clip test against a table that only ever holds the reserved
+// no-clip slot clips nothing. binding=3 — PER-RENDERER per-instance slot
+// index, parallel to the binding=0 instance buffer and indexed by the
+// IDENTICAL per-instance index (gl_BaseInstanceARB + gl_InstanceID) — stays
+// declared (the CPU side still writes it, always 0) but is no longer read
+// here; the instance-buffer layout it occupies is S5's to revisit.
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
uint instanceClipSlot[];
};
@@ -133,16 +116,6 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
vec2 instanceSelectionLighting[];
};
-// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
-// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
-// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
-// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
-// they pass everything (no clipping) when the slot's count < 8.
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
-
uniform mat4 uViewProjection;
// Absolute transform prefix in the shared shadow/world pose arena. Every
// parallel per-instance array remains local to this submission, so only the
@@ -352,9 +325,9 @@ void main() {
// Campaign VM VM6: weather-driven foliage sway. acdreamFoliageDisplace is
// a no-op unless b.flags carries the cutout (0x2) or trunk (0x4)
// classification bit — see foliage_wind.glsl. Applied before gl_Position
- // so clip distances, lighting, and the fragment stage all see the
- // displaced position; the four directional-shadow caster vertex shaders
- // call the identical include so the shadow moves with the same vertex.
+ // so lighting and the fragment stage both see the displaced position; the
+ // four directional-shadow caster vertex shaders call the identical
+ // include so the shadow moves with the same vertex.
worldPos.xyz = acdreamFoliageDisplace(
worldPos.xyz,
model[3].xyz,
@@ -363,18 +336,6 @@ void main() {
uAtmosphereWindAmplitude);
gl_Position = uViewProjection * worldPos;
- // Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
- // SAME instanceIndex used for the binding=0 transform above, so the slot
- // travels with the instance through the MDI BaseInstance offsets. Slot 0
- // (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
- // +1.0 ⇒ nothing is clipped.
- uint _slot = instanceClipSlot[instanceIndex];
- CellClip _c = clipRegions[_slot];
- for (uint i = 0u; i < _c.count; ++i)
- gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
- for (uint i = _c.count; i < 8u; ++i)
- gl_ClipDistance[i] = 1.0;
-
vWorldPos = worldPos.xyz;
vNormal = normalize(mat3(model) * aNormal);
vAmbientLocalLit = accumulateAmbientLocalLights(
diff --git a/src/AcDream.App/Rendering/Shaders/mesh_detail.vert b/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
index 9c8b8dad..6c56e6df 100644
--- a/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
+++ b/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
@@ -23,16 +23,17 @@ layout(std430, binding = 1) readonly buffer BatchBuffer {
BatchData Batches[];
};
-struct CellClip {
- uint count;
- uint _p0;
- uint _p1;
- uint _p2;
- vec4 planes[8];
-};
-layout(std430, binding = 2) readonly buffer ClipRegionBuf {
- CellClip clipRegions[];
-};
+// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
+// (Phase U.3's binding=2 CellClip region SSBO) is deleted — the CPU-side
+// routing that could ever select a non-zero slot for an instance
+// (WbDrawDispatcher's per-frame clip-routing arming call) had ZERO production callers, so every
+// instance has always mapped to slot 0 (no-clip) in every shipped build; a
+// shader-side clip test against a table that only ever holds the reserved
+// no-clip slot clips nothing. binding=3 — PER-RENDERER per-instance slot
+// index, parallel to the binding=0 instance buffer and indexed by the
+// IDENTICAL per-instance index — stays declared (the CPU side still writes
+// it, always 0) but is no longer read here; the instance-buffer layout it
+// occupies is S5's to revisit.
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
uint instanceClipSlot[];
};
@@ -56,11 +57,6 @@ layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
float instanceAlpha[];
};
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
-
uniform mat4 uViewProjection;
uniform int uDrawIDOffset;
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
@@ -84,13 +80,6 @@ void main() {
gl_Position = uViewProjection * worldPos;
vWorldPos = worldPos.xyz;
- uint slot = instanceClipSlot[instanceIndex];
- CellClip clip = clipRegions[slot];
- for (uint i = 0u; i < clip.count; ++i)
- gl_ClipDistance[i] = dot(clip.planes[i], gl_Position);
- for (uint i = clip.count; i < 8u; ++i)
- gl_ClipDistance[i] = 1.0;
-
// No distance term: VM2 found the fade only exists in
// D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon
// path) and only when the static noFadeDetail (0x00820e38, initialised
diff --git a/src/AcDream.App/Rendering/Shaders/mesh_modern.vert b/src/AcDream.App/Rendering/Shaders/mesh_modern.vert
index b7fd426c..b834886f 100644
--- a/src/AcDream.App/Rendering/Shaders/mesh_modern.vert
+++ b/src/AcDream.App/Rendering/Shaders/mesh_modern.vert
@@ -42,34 +42,17 @@ layout(std430, binding = 1) readonly buffer BatchBuffer {
BatchData Batches[];
};
-// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
-// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
-//
-// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
-// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
-// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
-//
-// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
-// instance buffer and indexed by the IDENTICAL per-instance index
-// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
-// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
-// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
-//
-// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
-// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
-// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
-// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
-// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
-struct CellClip {
- uint count;
- uint _p0;
- uint _p1;
- uint _p2;
- vec4 planes[8];
-};
-layout(std430, binding = 2) readonly buffer ClipRegionBuf {
- CellClip clipRegions[];
-};
+// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
+// (Phase U.3's binding=2 CellClip region SSBO) is deleted — the CPU-side
+// routing that could ever select a non-zero slot for an instance
+// (WbDrawDispatcher's per-frame clip-routing arming call) had ZERO production callers, so every
+// instance has always mapped to slot 0 (no-clip) in every shipped build; a
+// shader-side clip test against a table that only ever holds the reserved
+// no-clip slot clips nothing. binding=3 — PER-RENDERER per-instance slot
+// index, parallel to the binding=0 instance buffer and indexed by the
+// IDENTICAL per-instance index (gl_BaseInstanceARB + gl_InstanceID) — stays
+// declared (the CPU side still writes it, always 0) but is no longer read
+// here; the instance-buffer layout it occupies is S5's to revisit.
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
uint instanceClipSlot[];
};
@@ -129,16 +112,6 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
vec2 instanceSelectionLighting[];
};
-// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
-// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
-// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
-// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
-// they pass everything (no clipping) when the slot's count < 8.
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
-
uniform mat4 uViewProjection;
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
@@ -311,18 +284,6 @@ void main() {
vec4 worldPos = model * vec4(aPosition, 1.0);
gl_Position = uViewProjection * worldPos;
- // Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
- // SAME instanceIndex used for the binding=0 transform above, so the slot
- // travels with the instance through the MDI BaseInstance offsets. Slot 0
- // (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
- // +1.0 ⇒ nothing is clipped.
- uint _slot = instanceClipSlot[instanceIndex];
- CellClip _c = clipRegions[_slot];
- for (uint i = 0u; i < _c.count; ++i)
- gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
- for (uint i = _c.count; i < 8u; ++i)
- gl_ClipDistance[i] = 1.0;
-
vWorldPos = worldPos.xyz;
vNormal = normalize(mat3(model) * aNormal);
vLit = accumulateLights(vNormal, vWorldPos, instanceIndex); // A7: per-vertex Gouraud (per-object lights)
diff --git a/src/AcDream.App/Rendering/Shaders/particle.vert b/src/AcDream.App/Rendering/Shaders/particle.vert
index 7df885dd..ca37a142 100644
--- a/src/AcDream.App/Rendering/Shaders/particle.vert
+++ b/src/AcDream.App/Rendering/Shaders/particle.vert
@@ -14,24 +14,18 @@ layout(location = 5) in vec4 aColor;
// (ACDREAM_TEXTURE_HANDLE, injected by
// tools/ShaderCompiler/VulkanGlslPreamble.cs).
layout(location = 6) in uint aTextureIndex;
+// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
+// this attribute fed (the binding=2 CellClip region SSBO + the
+// gl_ClipDistance write below) is deleted — the CPU-side routing that could
+// ever select a non-zero slot for an instance (WbDrawDispatcher.
+// the per-frame clip-routing arming call) had ZERO production callers, so every particle has always
+// carried clip slot 0 (no-clip) in every shipped build; a shader-side clip
+// test against a table that only ever holds the reserved no-clip slot clips
+// nothing. The attribute itself stays declared (the CPU side still writes
+// it, always 0) but is no longer read here; the vertex layout it occupies
+// is S5's to revisit.
layout(location = 7) in uint aClipSlot;
-struct CellClip {
- uint count;
- uint _p0;
- uint _p1;
- uint _p2;
- vec4 planes[8];
-};
-layout(std430, binding = 2) readonly buffer ClipRegionBuf {
- CellClip clipRegions[];
-};
-
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
-
uniform mat4 uViewProjection;
out vec2 vTex;
@@ -52,9 +46,4 @@ void main() {
// stage, which is the only form Vulkan can express.
vTextureIndex = aTextureIndex;
gl_Position = uViewProjection * vec4(world, 1.0);
- CellClip clip = clipRegions[aClipSlot];
- for (uint i = 0u; i < clip.count; ++i)
- gl_ClipDistance[i] = dot(clip.planes[i], gl_Position);
- for (uint i = clip.count; i < 8u; ++i)
- gl_ClipDistance[i] = 1.0;
}
diff --git a/src/AcDream.App/Rendering/Shaders/particle_mesh.vert b/src/AcDream.App/Rendering/Shaders/particle_mesh.vert
index 6d6542f1..4d40c2e6 100644
--- a/src/AcDream.App/Rendering/Shaders/particle_mesh.vert
+++ b/src/AcDream.App/Rendering/Shaders/particle_mesh.vert
@@ -5,24 +5,18 @@ layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in mat4 aModel;
layout(location = 7) in vec4 aColor;
+// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
+// this attribute fed (the binding=2 CellClip region SSBO + the
+// gl_ClipDistance write below) is deleted — the CPU-side routing that could
+// ever select a non-zero slot for an instance (WbDrawDispatcher.
+// the per-frame clip-routing arming call) had ZERO production callers, so every particle mesh has
+// always carried clip slot 0 (no-clip) in every shipped build; a shader-side
+// clip test against a table that only ever holds the reserved no-clip slot
+// clips nothing. The attribute itself stays declared (the CPU side still
+// writes it, always 0) but is no longer read here; the vertex layout it
+// occupies is S5's to revisit.
layout(location = 8) in uint aClipSlot;
-struct CellClip {
- uint count;
- uint _p0;
- uint _p1;
- uint _p2;
- vec4 planes[8];
-};
-layout(std430, binding = 2) readonly buffer ClipRegionBuf {
- CellClip clipRegions[];
-};
-
-out gl_PerVertex {
- vec4 gl_Position;
- float gl_ClipDistance[8];
-};
-
uniform mat4 uViewProjection;
out vec2 vTexCoord;
@@ -32,9 +26,4 @@ void main() {
vTexCoord = aTexCoord;
vColor = aColor;
gl_Position = uViewProjection * aModel * vec4(aPosition, 1.0);
- CellClip clip = clipRegions[aClipSlot];
- for (uint i = 0u; i < clip.count; ++i)
- gl_ClipDistance[i] = dot(clip.planes[i], gl_Position);
- for (uint i = clip.count; i < 8u; ++i)
- gl_ClipDistance[i] = 1.0;
}
diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv
index 8ffa95f0..1fa7572d 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv
index 03ed2c55..50650461 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv
index a9833999..3ab0bbb5 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/particle.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/particle.vert.spv
index 9a52c2bf..f8f07394 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/particle.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/particle.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/particle_mesh.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/particle_mesh.vert.spv
index ec6aec3e..c2ef7d4a 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/particle_mesh.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/particle_mesh.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
index 842ec7f5..d272f155 100644
--- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
+++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
@@ -215,7 +215,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "c03ba560fbc2c7ce12e67326dcc79114f740999423fade53cd2d18804bf8f81a",
+ "sourceSha256": "e552edb7ef42e9e32812824c075310f2d99eb5826e527db5c22b54890ef02088",
"compiled": true
},
{
@@ -231,7 +231,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "3b03a153a439aa54fcd0bcb575274c8e1da888b8ae85ee15fae020ccb1d3bf49",
+ "sourceSha256": "0c1eddb0fc892cb76ef4da47f727cec2516052d63247fa2682918bfd60b84fa8",
"compiled": true
},
{
@@ -247,7 +247,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "2c37aa0fd1ee4af5ee1f6541b00e6a7226d4173988e748b1fb1c271b0a9b91b2",
+ "sourceSha256": "dba51ce23f2b621683fc78db73f608a0c000479b8e827cd3ecc8731d46c6ee64",
"compiled": true
},
{
@@ -263,7 +263,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "2bdd7114223164916a87e7d2e6df8a21fdcbd1cad082fafb3102b8a0fd4a2a8b",
+ "sourceSha256": "f85089aaf92ee63d95f2e9feecd65d81e1aa942dceca5b62fa0747f9aae06f4c",
"compiled": true
},
{
@@ -279,7 +279,7 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "582e9be4bca8bd1807d5a2152b211d1d4dd1ed54a6efe4240f52d402e628be75",
+ "sourceSha256": "14ef8815265fee45bccaf08dcf4f0adc3c1135305fe123cb9085a3a2880fe08a",
"compiled": true
},
{
diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs
index c4d4844d..daa254ff 100644
--- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs
+++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs
@@ -1723,7 +1723,28 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// trailing per-frame weather OC (§11.6 H1), which has no EC
// counterpart at all; see WalkTranscriptDump.PrintEnvCellShell's
// own doc comment for the same citation.
- WalkTranscriptDump.PrintEnvCellShell(cells[i]);
+ //
+ // S3 review fix round 1 (F4a): retail's breakpoint sits inside
+ // the setup_view/DrawEnvCell LOOP (PView::DrawCells
+ // @0x005a4ab1-0x005a4acc), which iterates the cell's live
+ // portal_view slices — @0x0059f170's own entry, above, fires
+ // once PER live view, not once per cell. Read the SliceCount
+ // directly off the route CaptureCellViewRoute just captured
+ // (_lookInTurns[viewRouteIndex]) rather than through
+ // InteriorFloodViewSliceCountAt's flood-index indirection: this
+ // loop also runs for a building's LOOK-IN flood
+ // (WalkDrawStage.LookInStatic), whose cells never populate
+ // InteriorFloodCells (that list is the interior ROOT flood
+ // only — see its own doc comment) — indexing it here would
+ // throw ArgumentOutOfRangeException on a real look-in turn. A
+ // cell with zero live views (an untracked/degenerate route)
+ // prints zero EC lines, matching setup_view never iterating an
+ // empty view list. The SHELL DRAW event below is unchanged —
+ // the frame stamp still dedupes it to one submission regardless
+ // of how many times this prints.
+ int liveViewCount = _lookInTurns[viewRouteIndex].SliceCount;
+ for (int view = 0; view < liveViewCount; view++)
+ WalkTranscriptDump.PrintEnvCellShell(cells[i]);
if (_cellShellsDrawnThisFrame.Add(cells[i]))
{
MarkIfGrown();
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
index 8154724e..dc4842ee 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
@@ -145,25 +145,18 @@ public sealed unsafe partial class EnvCellRenderer
for (int i = 0; i < uniqueInstanceCount; i++)
_gpuInstanceTransforms[i] = allInstances[i].Transform;
- // Phase U.4: per-instance clip slots, laid out parallel to the transforms
- // so instanceClipSlot[BaseInstance + gl_InstanceID] tracks Instances[].
+ // S3 review fix round 1 (F5): per-instance clip slots, laid out
+ // parallel to the transforms so instanceClipSlot[BaseInstance +
+ // gl_InstanceID] tracks Instances[] — always slot 0 (no-clip). The
+ // former Phase U.4 cellId→slot routing this array carried (see
+ // EnvCellRenderer.cs's own doc comment) is deleted, not merely
+ // inactive: the arming method's only caller ever passed null, so
+ // this branch always ran in every shipped build. The vertex-layout input
+ // itself (instanceClipSlot[]) stays fed 0 — see mesh_modern.vert's
+ // own doc comment for why the shader no longer reads it either.
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
- if (_cellIdToSlot is null
- || AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
- {
- Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
- }
- else
- {
- for (int i = 0; i < uniqueInstanceCount; i++)
- {
- _clipSlotData[i] =
- _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
- ? (uint)slot
- : 0u;
- }
- }
+ Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
// Campaign VM VM1 follow-up: per-instance opacity multiplier, laid out
// parallel to the transforms exactly like _clipSlotData above. EnvCell
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
index 271e00fe..f41083eb 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
@@ -214,23 +214,13 @@ public sealed partial class EnvCellRenderer :
}
}
- // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
- // non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
- // _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
- // gated to that cell's portal-clip region. When null (U.3 path), every
- // instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0
- // (no-clip) — but the caller's Render filter already restricts the draw to the
- // map's keys, so that fallback should not fire in practice.
- private IReadOnlyDictionary? _cellIdToSlot;
-
- ///
- /// Phase U.4: install the per-frame cellId→slot map used to gate cell shells
- /// to their portal-clip regions. Call once per frame BEFORE
- /// . Pass null to revert to
- /// the U.3 no-clip behavior (every shell instance → slot 0).
- ///
- public void SetClipRouting(IReadOnlyDictionary? cellIdToSlot)
- => _cellIdToSlot = cellIdToSlot;
+ // S3 review fix round 1 (F5): the Phase U.4 per-frame cellId→CellClip-slot
+ // map this renderer's own arming method used to install is deleted — its
+ // only caller ever passed null (WorldScenePassExecutor's
+ // BeginFrame/AbortFrame), so RenderModernMDIInternal's instanceClipSlot
+ // write always took the "every instance maps to slot 0" branch in every
+ // shipped build. See RenderModernMDIInternal's own doc comment
+ // (EnvCellRenderer.Rhi.cs) for where that write now lives, unconditionally.
///
/// A7 Fix D (D-2): hand the renderer this frame's point-light snapshot
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs
index 9b4375db..1cef8b9d 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs
@@ -255,9 +255,11 @@ public sealed partial class WbDrawDispatcher
/// _groups/_packedGroups or publishing selection itself (see
/// this file's type doc comment).
///
- /// A culled entity ( returns
- /// Culled: true — not visible through the active clip route)
- /// contributes nothing, matching every other classifier's cull gate.
+ /// S3 review fix round 1 (F5):
+ /// never culls (the clip-slot routing it used to gate on had zero
+ /// production callers, so it is deleted, not merely inactive) — the
+ /// cull check below is retained for shape parity with every other
+ /// classifier's cull gate but never actually fires.
///
internal void ClassifyEntityForWalk(
in RenderProjectionRecord projection,
@@ -293,9 +295,7 @@ public sealed partial class WbDrawDispatcher
tupleLandblockId,
animated: liveDynamic);
- (uint slot, bool culled) = ResolveSlotForFrame(
- _clipRoutingActive, entity.ServerGuid, entity.ParentCell,
- _cellIdToSlot, _outdoorSlot, _outdoorVisible);
+ (uint slot, bool culled) = ResolveSlotForFrame();
if (culled)
return;
@@ -826,9 +826,9 @@ public sealed partial class WbDrawDispatcher
/// the caller's firstAdmission gate skips it) or the single
/// RetailWholeMeshSlot sentinel, so looping was never anything but
/// one iteration in practice. (the entity's OWN
- /// classic clip-routing slot, always 0 while _clipRoutingActive is
- /// never armed by the walk path — see )
- /// is therefore the ONE clip slot every surviving batch carries.
+ /// clip slot — always 0, see 's own doc
+ /// comment for why the routing that used to vary this is deleted) is
+ /// therefore the ONE clip slot every surviving batch carries.
///
private void EmitClassifiedBatches(
ObjectRenderData renderData,
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
index ba713392..cbefe579 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
@@ -570,27 +570,27 @@ public sealed partial class WbDrawDispatcher : IDisposable
// GlWorldPassSurface still calls the setter unconditionally.
private uint _sharedClipRegionSsbo;
- // Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before
- // each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root /
- // no portal frame), every instance maps to slot 0 (no-clip) and no instance is
- // culled — identical to U.3. When active, each instance's slot is resolved by
- // ResolveEntitySlot per the U.4 policy (cell-owned entities to their cell slot;
- // outdoor-owned entities to OutsideView; non-visible/unresolved indoors culled).
- private bool _clipRoutingActive;
- private IReadOnlyDictionary? _cellIdToSlot;
- private int _outdoorSlot;
- private bool _outdoorVisible;
+ // S3 review fix round 1 (F5): Phase U.4's per-frame clip-slot routing
+ // fields and their install/reset methods are deleted outright — the ONE
+ // method that could ever arm routing has ZERO production callers, so
+ // the "when active" branch of the U.4 policy this comment used to
+ // describe NEVER RAN in any shipped build: every instance has always
+ // mapped to slot 0 (no-clip), unconditionally, on every path, matching
+ // the U.3 default this section header originally described as only the
+ // FALLBACK case. ResolveSlotForFrame below now says so directly instead
+ // of gating on a flag nothing ever set true.
// Phase U.4: the clip slot of the entity currently being classified in Draw's
// per-entity loop. Set once per entity (before ClassifyBatches / ApplyCacheHit),
// read by the two matrix-append sites (AppendInstanceToGroup + ClassifyBatches)
- // so every group's Slots[] stays in lockstep with its Matrices[]. Defaults to 0
- // (no-clip) on the U.3 / outdoor path.
+ // so every group's Slots[] stays in lockstep with its Matrices[]. Always 0
+ // (no-clip) — see ResolveSlotForFrame's own doc comment.
private uint _currentEntitySlot;
- // Phase U.4: true when the current entity resolved to the CULL sentinel
- // (cell not visible, or outdoor stab while no outdoors is visible). Persisted
- // across the entity's tuples; the per-tuple body skips all instance emission.
+ // Phase U.4: whether the current entity was culled by clip-slot routing.
+ // Persisted across the entity's tuples; the per-tuple body skips all
+ // instance emission when true. Always false — see ResolveSlotForFrame's
+ // own doc comment.
private bool _currentEntityCulled;
// Per-frame scratch arrays — Tasks 9-10 fully wire these.
@@ -855,186 +855,20 @@ public sealed partial class WbDrawDispatcher : IDisposable
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
///
- /// Phase U.4: install the per-frame clip-slot routing for an INDOOR root.
- /// Call once per frame BEFORE when the camera's root cell is
- /// non-null; the next resolves each instance's binding=3
- /// clip slot via the U.4 policy (cell-owned entities to their cell slot,
- /// outdoor-owned entities to OutsideView, non-visible/unresolved indoors culled).
- /// Pair with on outdoor-root frames so the
- /// dispatcher reverts to the U.3 no-clip-everything behavior.
+ /// S3 review fix round 1 (F5): the per-instance clip-slot decision for
+ /// one entity, returning the (Slot, Culled) pair the per-entity
+ /// loop body consumes. Every instance is slot 0 (no-clip) and nothing is
+ /// ever clip-culled — the U.4 "routing active" policy this used to gate
+ /// on (ResolveEntitySlot: indoor entities to their cell's slot,
+ /// outdoor entities to the OutsideView slot, everything else culled) is
+ /// DELETED, not merely disabled: the ONE method that could ever arm it
+ /// had ZERO production callers, so that branch never ran in any shipped
+ /// build. internal static (no parameters
+ /// needed any more) so it stays unit-testable — see
+ /// WbDrawDispatcherClipSlotTests' history for why the removed
+ /// per-branch tests existed and no longer apply.
///
- /// cellId → CellClip slot. A cell absent from the map
- /// is NOT visible → its cell-static instances are culled.
- /// Slot for outdoor scenery / building shells while
- /// indoors (the OutsideView slot, or 0 for no-clip over-include).
- /// False ⇒ cull outdoor scenery / shells this frame
- /// (the OutsideView is empty).
- public void SetClipRouting(IReadOnlyDictionary cellIdToSlot, int outdoorSlot, bool outdoorVisible)
- {
- ArgumentNullException.ThrowIfNull(cellIdToSlot);
- _clipRoutingActive = true;
- _cellIdToSlot = cellIdToSlot;
- _outdoorSlot = outdoorSlot;
- _outdoorVisible = outdoorVisible;
- }
-
- ///
- /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip),
- /// nothing is culled by clip routing. Call on outdoor-root frames (camera
- /// outdoors) and any frame without a portal-visibility result.
- ///
- public void ClearClipRouting()
- {
- _clipRoutingActive = false;
- _cellIdToSlot = null;
- _outdoorSlot = 0;
- _outdoorVisible = false;
- }
-
- // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change
- // signature + monotonic sequence + reusable histogram. See RenderingDiagnostics
- // .ProbeClipRouteEnabled for the full probe contract.
- private string? _lastClipRouteDispSig;
- private long _clipRouteDispSeq;
- private readonly SortedDictionary _clipRouteHist = new();
-
- // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3.
- // grp.Slots is laid out 1:1 with grp.Matrices (binding=0), so this IS the slot content
- // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish
- // into cullEnt) when the building flood merges, this line shows it directly.
- private void EmitClipRouteDispatchProbe(int culledEntities)
- {
- _clipRouteHist.Clear();
- int total = 0;
- foreach (var grp in _groups.Values)
- {
- var slots = grp.Slots;
- for (int i = 0; i < slots.Count; i++)
- {
- _clipRouteHist.TryGetValue(slots[i], out int c);
- _clipRouteHist[slots[i]] = c + 1;
- total++;
- }
- }
-
- var sb = new System.Text.StringBuilder(128);
- sb.Append(System.FormattableString.Invariant(
- $"outdoorSlot={_outdoorSlot} outdoorVis={(_outdoorVisible ? 'Y' : 'n')} inst={total} cullEnt={culledEntities} slots={{"));
- bool first = true;
- foreach (var kv in _clipRouteHist)
- {
- if (!first) sb.Append(',');
- first = false;
- sb.Append(System.FormattableString.Invariant($"{kv.Key}:{kv.Value}"));
- }
- sb.Append('}');
-
- string sig = sb.ToString();
- _clipRouteDispSeq++;
- if (sig == _lastClipRouteDispSig)
- return;
- _lastClipRouteDispSig = sig;
- Console.WriteLine($"[clip-route-disp] n={_clipRouteDispSeq} {sig}");
- }
-
- // Phase U.4 CULL sentinel returned by ResolveEntitySlot: the entity's instances
- // are dropped entirely (not emitted into the binding=0 instance buffer NOR the
- // binding=3 slot buffer), matching the existing frustum / visible-cell cull.
- // Internal (not private) so the clip-slot unit tests can assert against it
- // directly — see WbDrawDispatcherClipSlotTests.
- internal const int ClipSlotCull = -1;
-
- ///
- /// Phase U.4: resolve the clip slot for one entity per the slot/gate policy.
- /// Returns to drop the entity's instances entirely.
- ///
- /// - Indoor ParentCellId: the cell's slot, or CULL when hidden.
- /// - Outdoor ParentCellId or ParentCellId == null static scenery: the OutsideView slot
- /// when , else CULL.
- /// - ServerGuid != 0 with ParentCellId == null: CULL while routing is active.
- ///
- /// Only called when _clipRoutingActive (indoor root). On the U.3 / outdoor
- /// path every instance is slot 0 and nothing is culled — see
- /// , which gates on that flag.
- ///
- /// INVARIANT: and the keys of
- /// MUST live in the same FULL cell-id space
- /// (lbMask | OtherCellId, e.g. 0xA9B40164). A bare-low-byte
- /// ParentCellId (e.g. 0x64) would never match a full-id key and would
- /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in
- /// CLAUDE.md where player CellId was tracked without its landblock prefix.
- ///
- ///
- /// internal static + pure (reads no instance state) so the clip-slot
- /// unit tests exercise every branch without a GL context. The caller hands in
- /// the routing fields it would otherwise read from _cellIdToSlot etc.
- ///
- ///
- internal static int ResolveEntitySlot(
- uint serverGuid,
- uint? parentCellId,
- IReadOnlyDictionary cellIdToSlot,
- int outdoorSlot,
- bool outdoorVisible)
- {
- // Live-dynamic entities are not a global indoor overlay. When they
- // have current cell ownership, route them through the same visible
- // cell/OutsideView graph as every other object. Parentless live objects
- // are unresolved indoors, so cull them while clip routing is active.
- if (parentCellId is uint parentCell)
- {
- if (IsIndoorCellId(parentCell))
- {
- if (!cellIdToSlot.ContainsKey(parentCell))
- return ClipSlotCull;
-
- return cellIdToSlot[parentCell];
- }
-
- return outdoorVisible ? outdoorSlot : ClipSlotCull;
- }
-
- if (serverGuid != 0)
- return ClipSlotCull;
-
- // Outdoor scenery / building shell (no ParentCellId). Indoor root: gate to
- // the OutsideView slot, or cull when nothing outdoors is visible.
- return outdoorVisible ? outdoorSlot : ClipSlotCull;
- }
-
- private static bool IsIndoorCellId(uint cellId)
- {
- uint low = cellId & 0xFFFFu;
- return low >= 0x0100u && low != 0xFFFFu;
- }
-
- ///
- /// Phase U.4: the call-site clip-slot decision for one entity, returning the
- /// (Slot, Culled) pair the per-entity loop body consumes. Wraps
- /// with the
- /// gate: when routing is INACTIVE (outdoor root / no portal frame), every entity
- /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so
- /// the resolver (and ) is bypassed entirely.
- /// When active, a CULL sentinel maps to (0, culled=true) — the slot value
- /// is never emitted for a culled entity.
- /// internal static + pure so the whole policy (including the routing-
- /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests.
- ///
- internal static (uint Slot, bool Culled) ResolveSlotForFrame(
- bool clipRoutingActive,
- uint serverGuid,
- uint? parentCellId,
- IReadOnlyDictionary? cellIdToSlot,
- int outdoorSlot,
- bool outdoorVisible)
- {
- if (!clipRoutingActive)
- return (0u, false);
-
- int resolved = ResolveEntitySlot(serverGuid, parentCellId, cellIdToSlot!, outdoorSlot, outdoorVisible);
- bool culled = resolved == ClipSlotCull;
- return (culled ? 0u : (uint)resolved, culled);
- }
+ internal static (uint Slot, bool Culled) ResolveSlotForFrame() => (0u, false);
public static Matrix4x4 ComposePartWorldMatrix(
Matrix4x4 entityWorld,
@@ -1408,11 +1242,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
uint? populateEntityId = null;
uint populateLandblockId = 0;
- // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by
- // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity —
- // cheap enough to count unconditionally; emission below is probe-gated.
- int probeCulledEntities = 0;
-
// Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a
// FLAT list of batches across all MeshRefs of an entity, so a single
// ApplyCacheHit call already drew every batch. _walkScratch yields
@@ -1506,16 +1335,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
}
currentEntityIncomplete = false;
- // Phase U.4: resolve this entity's clip slot ONCE per entity
- // (constant across its tuples). On the U.3 / outdoor path
- // (_clipRoutingActive false) every entity is slot 0, never culled.
- // The whole decision (including the routing-active gate) lives in
- // the pure ResolveSlotForFrame helper so it's unit-testable.
- (_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame(
- _clipRoutingActive, entity.ServerGuid, entity.ParentCell,
- _cellIdToSlot, _outdoorSlot, _outdoorVisible);
- if (_currentEntityCulled)
- probeCulledEntities++;
+ // S3 review fix round 1 (F5): resolve this entity's clip slot
+ // ONCE per entity (constant across its tuples) — always slot
+ // 0, never culled. See ResolveSlotForFrame's own doc comment
+ // for why the U.4 "routing active" branch this used to gate
+ // on is deleted rather than merely inactive.
+ (_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame();
// Fix B: select this entity's up-to-8 point/spot lights ONCE (the set
// is constant across the entity's parts/tuples), by the entity's
@@ -1890,14 +1715,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
populateEntityId, populateLandblockId, _cache,
_populateScratch, _populateSelectionScratch);
- // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance
- // histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the
- // culled-entity count. Routed draws only (the landscape pass under DrawInside) so the
- // unrouted per-cell bucket draws don't oscillate the print-on-change signature.
- // Emitted BEFORE the MeshSourceReady / totalInstances early-outs so an
- // all-culled frame still reports (inst=0).
- if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive)
- EmitClipRouteDispatchProbe(probeCulledEntities);
+ // S3 review fix round 1 (F5): the §4 flap [clip-route-disp] probe
+ // (2026-06-10, throwaway) is deleted with the routing state it
+ // reported on — it could only ever print "cullEnt=0" now that
+ // ResolveSlotForFrame never culls anything.
ExecuteClassifiedGroups(
vp,
@@ -3464,8 +3285,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
HashSet? visibleCellIds,
EntitySet set)
{
- // No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics / outdoor
- // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating.
+ // No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics /
+ // outdoor scenery) ⇒ every entity passes; no other gate narrows it further — S3
+ // review fix round 1 (F5) deleted the clip-slot routing this comment used to cite
+ // (ResolveEntitySlot never had a production caller).
if (visibleCellIds is null)
return true;
@@ -3476,7 +3299,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
// ParentCellId == null (outdoor scenery / building shell): NOT a member of any interior cell,
// so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional
// `return true`). When such entities must draw (through the doorway), the caller passes
- // visibleCellIds: null and relies on ResolveEntitySlot's OutsideView routing instead.
+ // visibleCellIds: null instead.
return false;
}
diff --git a/src/AcDream.App/Rendering/WorldPassSurface.cs b/src/AcDream.App/Rendering/WorldPassSurface.cs
index dc28ba59..005e3402 100644
--- a/src/AcDream.App/Rendering/WorldPassSurface.cs
+++ b/src/AcDream.App/Rendering/WorldPassSurface.cs
@@ -30,16 +30,21 @@ internal interface IRenderFrameGlState
internal interface IWorldPassSurface
{
///
- /// Publishes this frame's per-cell clip-region table, and routes it to the
- /// renderers that read it.
+ /// Publishes this frame's per-cell clip-region table.
///
/// 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.
+ /// to re-assert it, so this contract published exactly one section, the
+ /// per-cell clip-region table. S3 review fix round 1 (F5): that ONE
+ /// remaining section now has no shader reader left either (the mesh/
+ /// particle vertex shaders' own per-cell fetch is deleted, per
+ /// 's own doc comment) — the publish
+ /// itself stays here as untouched residue this round (a CPU-side
+ /// byte-blob write nothing on the GPU consumes), named so a future round
+ /// can finish the deletion without re-discovering it.
///
/// S3 landing hygiene (H2) deleted the GL-era reservation-count
/// parameter this used to also take (how many distinct terrain clip
@@ -56,19 +61,21 @@ internal interface IWorldPassSurface
///
/// Vulkan activates every element the shader declares and has no
/// enable, so this is a no-op there — and that is safe rather than a
- /// divergence. Two shader families still write gl_ClipDistance in
- /// the world pass: portal_depth.vert (the exit seals and punch fans,
- /// planes handed per draw) and the mesh/particle vertex shaders
+ /// divergence. Only ONE shader family writes gl_ClipDistance in the
+ /// world pass now: portal_depth.vert (the exit seals and punch
+ /// fans, planes handed per draw through the TerrainClip UBO). S3 review
+ /// fix round 1 (F5) deleted the mesh/particle vertex shaders'
/// (mesh_modern, mesh_atmospheric, mesh_detail,
- /// particle, particle_mesh), which read the per-cell
- /// clip-region table this contract publishes through
- /// . The walk feeds every ordinary part and
- /// every particle slot 0 (no clip), so in the walk path that table is
- /// dormant; the sky and terrain shaders no longer declare any clip block
- /// at all (S3 chunk 4 fix round 2, L3). Every writer sets 1.0
- /// ("keep everything") in each slot past its active count, so a frame
- /// with no clip planes clips nothing on either backend (plan §5.5.14
- /// item 4).
+ /// particle, particle_mesh) own per-cell fetch and
+ /// gl_ClipDistance loop outright: the per-cell clip-region table
+ /// used to publish for them had no path
+ /// that could ever arm a non-zero slot for an instance (the ONE
+ /// WbDrawDispatcher API that could have armed it had ZERO production
+ /// callers), so that table was dormant in every path, not merely the walk's —
+ /// AD-17's correction is complete. Every writer left (portal_depth.vert
+ /// alone) sets 1.0 ("keep everything") in each slot past its
+ /// active count, so a frame with no clip planes clips nothing on either
+ /// backend (plan §5.5.14 item 4).
///
void EnableClipDistances();
@@ -126,8 +133,8 @@ internal sealed class RhiWorldPassSurface : IWorldPassSurface
public void ClearInteriorDepth()
{
- // S3 chunk 4 fix round 2 (L2): BeginScissor/EndScissor are deleted —
- // no producer of a narrowed scissor rectangle remains anywhere in the
+ // S3 chunk 4 fix round 2 (L2): the interface's own scissor begin/end
+ // pair is 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
diff --git a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
index 2da09ff1..c758fe92 100644
--- a/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
+++ b/src/AcDream.App/Rendering/WorldScenePassExecutor.cs
@@ -106,8 +106,6 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
{
_visibleParticleOwners.Clear();
_clipFrame.Reset();
- _entities.ClearClipRouting();
- _environmentCells.SetClipRouting(null);
}
public void PrepareFlatWorldClip() => _surface.PrepareClipFrame();
@@ -279,9 +277,7 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
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);
diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
index 6c2637fd..0a43478b 100644
--- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
+++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
@@ -270,21 +270,13 @@ public static class RenderingDiagnostics
? ldm
: 0;
- ///
- /// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1) — throwaway
- /// diagnostic. When true, the EnvCell SHELL pass maps every instance to clip
- /// slot 0 (the reserved no-clip region) instead of its cell's portal-slice
- /// region — i.e. shells draw WHOLE, retail's shape (retail never clips cell
- /// geometry; aperture exactness comes from the seal/punch depth writes +
- /// far→near order, PView::DrawCells 0x005a4840). Discriminator for the
- /// camera-against-wall stripe/hatch pattern: a knife-edge slice region (eye
- /// on a portal plane / inside a wall) yields a clip plane near-parallel to
- /// large surfaces → interpolated gl_ClipDistance ≈ 0 across them → per-pixel
- /// sign dither = the hatch. Stripes gone with this on = the shell trim is
- /// the mechanism. Entity/dispatcher clip routing is NOT affected.
- ///
- public static bool ClipDebugNoShellTrim { get; set; } =
- Environment.GetEnvironmentVariable("ACDREAM_CLIP_DEBUG") == "1";
+ // S3 review fix round 1 (F5): ClipDebugNoShellTrim (ACDREAM_CLIP_DEBUG)
+ // is deleted — the #176 stripe-hunt isolation it toggled between "clip
+ // shells to their cell's portal-slice region" and "map every instance to
+ // slot 0" is moot now that EnvCellRenderer's per-cell clip routing is
+ // deleted outright (its arming method never had a live caller): shells
+ // always draw WHOLE, unconditionally, the same behavior this flag used
+ // to force.
/// Parse ACDREAM_PROBE_SEAMDRAW: "1"/"true"/empty → the default #176
/// Facility Hub set; otherwise a comma-separated hex cell-id list (same forgiving
diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
index 38f3b409..4c5f27a2 100644
--- a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
@@ -194,46 +194,23 @@ public class ClipFrameLayoutTests
// AppendSlot_FromClipPlaneSet_AxisAlignedSquare_PacksFourPlanes pins
// bit-exact GPU packing of ClipPlaneSet's own output — neither asks
// whether a point ON an edge has ~0 distance under the planes actually
- // read back through each KEEP item's real production accessor. These
- // two tests close that gap, one per KEEP item (exit seals, punch fans).
-
- ///
- /// KEEP item 1 — exit seals: RetailPViewPassExecutor.DrawExitPortalMask
- /// reads its clip planes through WalkFrameDriver.
- /// InteriorFloodViewClipPlanesAt → ,
- /// the packed-byte round trip ClipFrameAssembler.Assemble builds
- /// via . For a synthetic
- /// quad "view" (a stand-in for one live portal view), every edge
- /// midpoint has near-zero signed distance under the GPU-read-back
- /// planes AND non-negative distance under every plane (retail's
- /// polyClipFinish keeps the whole convex region, never clips a
- /// point that sits ON the boundary) — proving the bytes
- /// InteriorFloodViewClipPlanesAt hands the seal leaf really are
- /// the CPU view polygon's own edge planes, not merely SOME bytes that
- /// happen to round-trip.
- ///
- [Fact]
- public void GetSlotPlanes_ExitSealPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView()
- {
- Vector2[] verts =
- [
- new(-1f, -0.5f), new(1f, -0.5f), new(1f, 0.5f), new(-1f, 0.5f),
- ];
- var cv = new CellView();
- cv.Add(new ViewPolygon(verts));
- ClipPlaneSet cps = ClipPlaneSet.From(cv);
- Assert.Equal(4, cps.Count);
-
- // The EXACT production path: ClipFrameAssembler.Assemble packs the
- // ClipPlaneSet into a slot; the seal leaf reads it back through
- // ClipFrame.GetSlotPlanes (WalkFrameDriver.InteriorFloodViewClipPlanesAt).
- var frame = ClipFrame.NoClip();
- int slot = frame.AppendSlot(cps);
- ReadOnlySpan gpuPlanes = frame.GetSlotPlanes(checked((uint)slot));
- Assert.Equal(4, gpuPlanes.Length);
-
- AssertEveryEdgeMidpointLiesOnSomeGpuPlane(verts, gpuPlanes);
- }
+ // read back through each KEEP item's real production accessor.
+ //
+ // KEEP item 1 — exit seals — moved to
+ // AcDream.App.Tests.Rendering.Walk.WalkFrameDriverClipSealTests
+ // (S3 review fix round 1, F2): the pin that used to live here drove
+ // ClipFrame.AppendSlot(ClipPlaneSet) directly through
+ // ClipFrameAssembler.Assemble's packed-byte round trip — an overload
+ // and a method with ZERO production callers (production interior roots
+ // never call Assemble; RetailPViewPassExecutor.DrawExitPortalMask
+ // reads its planes through WalkFrameDriver.InteriorFloodViewClipPlanesAt,
+ // which resolves through the driver's own CaptureViews/AppendClipSlot).
+ // The rewritten pin drives THAT real producer — it needs
+ // WalkFrameDriverTests' own DispatcherFixture/TestContext harness, so it
+ // lives alongside them rather than duplicating that machinery here.
+ //
+ // KEEP item 2 — punch fans stays below (it already drove the exact pair
+ // that runs: BeginWalkFrame + ReassembleOutsideViewFromWalk).
///
/// KEEP item 2 — punch fans: RetailPViewPassExecutor.DrawWalkPunchFan
diff --git a/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs b/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs
index 8ad712aa..b051e846 100644
--- a/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs
@@ -90,8 +90,9 @@ public sealed class EntityClipTests
// R1 (bleed fix #78): an outdoor entity (ParentCellId == null) with a non-null cell filter
// does NOT pass — it is not a member of any interior cell. (Was an unconditional return-true
// bypass, the headline outdoor-scenery bleed.) When such entities must draw through the
- // doorway, the caller passes visibleCellIds: null and the OutsideView clip-slot routing
- // (ResolveEntitySlot) gates them instead.
+ // doorway, the caller passes visibleCellIds: null instead (S3 review fix round 1, F5: the
+ // OutsideView clip-slot routing this comment used to cite as the alternate gate never had a
+ // production caller and is deleted).
var visibleCellIds = new HashSet { 0xA9B40170u };
var entity = Entity(parentCellId: null);
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
index 19e25259..f5f2bcd1 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs
@@ -40,14 +40,30 @@ public sealed class VulkanShaderManifestTests
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
["mesh_modern.frag.spv"] = "b702b644862aca31ce1fb0677adc5872b39c4ea87f595a89363b44d10f2cc50e",
- ["mesh_modern.vert.spv"] = "7ca5fb241c4f0248884ba8fa88fbae17a7d5cbc80efe4ac4a9ffd0254012ead8",
+ // mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
+ // F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
+ // gl_ClipDistance loop, and the gl_PerVertex redeclaration are
+ // deleted outright — the ONE WbDrawDispatcher API that could
+ // ever arm per-instance clip routing had ZERO production
+ // callers, so every instance has always mapped to slot
+ // 0 (no-clip) in every shipped build; the shader-side clip test
+ // clipped nothing. binding=3 instanceClipSlot[] stays declared,
+ // unread (fed 0; the instance-buffer layout is S5's).
+ ["mesh_modern.vert.spv"] = "f9ed4ee7140ccd136130559dbea68545f733f6022f52b085b6df4bcb787223c6",
["particle.frag.spv"] = "680da227704e0b3afa9b5226a7d73dd65aa9d8759d081cf4d5009d30e148726b",
// Re-pinned 2026-08-27: portal-view clip slots now travel with
// deferred billboard particles, matching retail PortalList draws.
- ["particle.vert.spv"] = "bf0f6b7b26a6b237e4abb2973b9959a38338868fd8304b3863f593ad56d61c35",
+ // Re-pinned again 2026-09-03 (S3 review fix round 1, F5): same
+ // deletion as mesh_modern.vert above — the binding=2 ClipRegionBuf
+ // fetch, gl_ClipDistance loop, and gl_PerVertex redeclaration are
+ // gone; the aClipSlot vertex attribute stays declared, unread.
+ ["particle.vert.spv"] = "95ce6ecf834930a92da5c5fe9aef513b38b5ba104704b98c1606af71fe17eaf3",
["particle_mesh.frag.spv"] = "7696b1dc0613b5a724c55df465173f613ae047da9675895b149b7c71b009cc7c",
// Same contract for full-mesh particle geometry.
- ["particle_mesh.vert.spv"] = "b5b3e0f583e00b78b56e297e60e5050013a26b3a74dbec64f6e5b286b751f0fa",
+ // Re-pinned again 2026-09-03 (S3 review fix round 1, F5): same
+ // ClipRegionBuf/gl_ClipDistance/gl_PerVertex deletion as
+ // particle.vert above.
+ ["particle_mesh.vert.spv"] = "043482b97c2ed036511692f89c75a0a6c298aba48cb519e5e3aff7fe7ba6371b",
["portal_depth.frag.spv"] = "96755196d4d0da7be4792107557465778be2ebefb5584834cc75bf90ec55a6cc",
// Re-pinned 2026-09-02 (Campaign OVERHAUL S1 hygiene): the b3b7d922
// investigation checkpoint removed the #117 mark-pass NDC bias and the
diff --git a/tests/AcDream.App.Tests/Rendering/ParticleBindlessInstanceTests.cs b/tests/AcDream.App.Tests/Rendering/ParticleBindlessInstanceTests.cs
index 9d355c3d..28c5a79d 100644
--- a/tests/AcDream.App.Tests/Rendering/ParticleBindlessInstanceTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ParticleBindlessInstanceTests.cs
@@ -44,8 +44,11 @@ public sealed class ParticleBindlessInstanceTests
// index rather than by a null handle, because Vulkan's descriptor array
// cannot be asked whether an element was ever written.
Assert.Contains("layout(location = 6) in uint aTextureIndex;", vertex);
+ // S3 review fix round 1 (F5): aClipSlot stays declared (the CPU side
+ // still writes it, always 0) but the per-cell clip-region SSBO fetch
+ // it used to index is deleted outright — WbDrawDispatcher's arming
+ // call for that routing had ZERO production callers.
Assert.Contains("layout(location = 7) in uint aClipSlot;", vertex);
- Assert.Contains("clipRegions[aClipSlot]", vertex);
Assert.Contains("flat out uint vTextureIndex;", vertex);
Assert.Contains("vTextureIndex = aTextureIndex;", vertex);
Assert.Contains("#extension GL_ARB_bindless_texture : require", fragment);
diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
index 702c55ec..f2206913 100644
--- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
@@ -122,8 +122,8 @@ public sealed class RetailPViewPassExecutorTests
/// 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 (the walk's own
- /// screen-space terrain-clip writer + ClearClipRouting + the old
- /// DrawLandscapeSliceLate leaf) is deleted outright (§10.2): retail
+ /// screen-space terrain-clip writer + its per-frame clip-routing reset
+ /// call + 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
@@ -155,8 +155,8 @@ public sealed class RetailPViewPassExecutorTests
///
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
- /// (the walk's own screen-space terrain-clip writer +
- /// ClearClipRouting + the old DrawLandscapeSliceLate leaf,
+ /// (the walk's own screen-space terrain-clip writer + its per-frame
+ /// clip-routing reset call + the old DrawLandscapeSliceLate leaf,
/// one call per active landscape view) is deleted — DrawLandscapeDynamicsPhase now calls
/// exactly once,
/// conditional on
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverClipSealTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverClipSealTests.cs
new file mode 100644
index 00000000..0405c7f0
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverClipSealTests.cs
@@ -0,0 +1,172 @@
+using System.Numerics;
+using AcDream.App.Rendering;
+using AcDream.App.Rendering.Walk;
+
+namespace AcDream.App.Tests.Rendering.Walk;
+
+///
+/// S3 review fix round 1 (F2, major): the exit-seal CPU/GPU equivalence pin,
+/// rewritten to drive the PRODUCER that actually runs. The prior pin (still
+/// at ClipFrameLayoutTests.GetSlotPlanes_ExitSealPath_... before this
+/// commit) drove ClipFrame.AppendSlot(ClipPlaneSet) directly — an
+/// overload with ZERO production callers. Production seal planes come from
+/// , which resolves
+/// through the driver's private CaptureViews/AppendClipSlot (the
+/// SAME pixel-space capture
+/// runs during a real interior flood — RetailPViewPassExecutor.
+/// DrawExitPortalMask reads it back through exactly this accessor). This
+/// file drives that real path directly (
+/// + the cast, the same minimal-harness pattern
+/// WalkFrameDriverTranscriptTests uses) — reusing 's own private fixture types via the shared
+/// partial class.
+///
+public sealed partial class WalkFrameDriverTests
+{
+ private static WalkScreenPoint[] NdcToPixelPoints(Vector2[] ndcVerts, float width, float height)
+ {
+ var points = new WalkScreenPoint[ndcVerts.Length];
+ for (int i = 0; i < ndcVerts.Length; i++)
+ {
+ float px = (ndcVerts[i].X + 1f) * width / 2f;
+ float py = (1f - ndcVerts[i].Y) * height / 2f;
+ points[i] = new WalkScreenPoint(px, py, 0f, 1f);
+ }
+ return points;
+ }
+
+ ///
+ /// KEEP item 1 — exit seals, rewritten through the real producer.
+ /// AppendClipSlot's per-edge plane formula (normalize + inward
+ /// perpendicular, winding-corrected) is IDENTICAL to 's own — two independent
+ /// implementations of the same retail idea — so an exact plane-for-plane
+ /// match against ClipPlaneSet.From's output proves the walk's own
+ /// capture reproduces the CPU polygon's true edge planes, not merely
+ /// bytes that happen to satisfy a looser geometric check.
+ /// MUTATION: flip the winding selection (ccw = area2 < 0f
+ /// instead of >= 0f) — every plane's sign inverts, failing the
+ /// exact-match assertion. Drop the normalize (Vector2.Normalize
+ /// removed from the per-edge formula) — the plane magnitude scales by
+ /// the (unnormalized) edge length, failing the exact-match assertion.
+ /// See
+ /// for the third mutation (deleting the AABB overflow branch).
+ ///
+ [Fact]
+ public void ExitSealPath_PlanesMatchClipPlaneSetsIndependentComputation_ThroughCaptureViewsAndAppendClipSlot()
+ {
+ using var fx = new DispatcherFixture();
+ var ctx = new TestContext();
+ using ClipFrame clipFrame = ClipFrame.NoClip();
+ var driver = new WalkFrameDriver(
+ fx.Dispatcher, new RecordingLeafRenderer(new List()), new FakeWorldData(), clipFrame: clipFrame);
+
+ const uint cellId = 0x100u;
+ var cell = new WalkCell { CellId = cellId };
+ cell.PushView();
+
+ // A non-axis-aligned convex quad, CCW in NDC — distinct from the
+ // punch-fan KEEP-2 pin's own verts (ClipFrameLayoutTests).
+ Vector2[] verts =
+ [
+ new(-0.3f, 0.5f), new(-0.6f, -0.2f), new(0.2f, -0.5f), new(0.6f, 0.3f),
+ ];
+ Assert.True(WalkCopyView.Append(
+ cell.TopView,
+ NdcToPixelPoints(verts, ctx.ViewportWidth, ctx.ViewportHeight),
+ ctx.Rays,
+ ctx.WorldViewpoint));
+ ctx.Cells[cellId] = cell;
+
+ driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
+ ((IWalkEventSink)driver).OnInteriorFloodDrawTurn(new[] { cellId }, outsideViewCount: 1);
+ driver.EndFrame();
+
+ Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
+ ReadOnlySpan planes = driver.InteriorFloodViewClipPlanesAt(0, 0);
+
+ Vector4[] expected = ClipPlaneSet.From(new ViewPolygon(verts)).PlaneArray;
+ Assert.Equal(expected.Length, planes.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].X, planes[i].X, 4);
+ Assert.Equal(expected[i].Y, planes[i].Y, 4);
+ Assert.Equal(expected[i].Z, planes[i].Z, 4);
+ Assert.Equal(expected[i].W, planes[i].W, 4);
+ }
+ }
+
+ ///
+ /// F2's first overflow case: a 9-vertex portal view (over = 8) exit-seals to exactly the 4 AABB
+ /// planes that CONTAIN every one of the 9 source vertices (over-include,
+ /// never under-include) — AppendClipSlot's conservative fallback
+ /// for a view too complex for the convex half-plane budget, rather than
+ /// falling back to slot 0 (which would erase the aperture entirely).
+ /// MUTATION: delete the if (ndc.Length > ClipFrame.MaxPlanes)
+ /// branch — the per-edge loop below stack-allocates exactly
+ /// (8) plane slots, so a 9-edge polygon
+ /// indexes past the end and throws, failing this test loudly.
+ ///
+ [Fact]
+ public void ExitSealPath_NineVertexView_UsesFourAabbPlanesContainingEveryVertex()
+ {
+ using var fx = new DispatcherFixture();
+ var ctx = new TestContext();
+ using ClipFrame clipFrame = ClipFrame.NoClip();
+ var driver = new WalkFrameDriver(
+ fx.Dispatcher, new RecordingLeafRenderer(new List()), new FakeWorldData(), clipFrame: clipFrame);
+
+ const uint cellId = 0x100u;
+ var cell = new WalkCell { CellId = cellId };
+ cell.PushView();
+
+ const int n = 9;
+ var verts = new Vector2[n];
+ for (int i = 0; i < n; i++)
+ {
+ float angle = i * MathF.Tau / n;
+ verts[i] = new Vector2(0.7f * MathF.Cos(angle), 0.7f * MathF.Sin(angle));
+ }
+ Assert.True(WalkCopyView.Append(
+ cell.TopView,
+ NdcToPixelPoints(verts, ctx.ViewportWidth, ctx.ViewportHeight),
+ ctx.Rays,
+ ctx.WorldViewpoint));
+ // No collinear-merge stole a vertex — a genuine 9-gon reached AppendClipSlot.
+ Assert.Equal(n, cell.TopView.View.Polys[0].VertexCount);
+ ctx.Cells[cellId] = cell;
+
+ driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
+ ((IWalkEventSink)driver).OnInteriorFloodDrawTurn(new[] { cellId }, outsideViewCount: 1);
+ driver.EndFrame();
+
+ ReadOnlySpan planes = driver.InteriorFloodViewClipPlanesAt(0, 0);
+ Assert.Equal(4, planes.Length);
+
+ float minX = verts.Min(v => v.X), maxX = verts.Max(v => v.X);
+ float minY = verts.Min(v => v.Y), maxY = verts.Max(v => v.Y);
+
+ // Over-include: every source vertex satisfies every plane.
+ foreach (Vector2 v in verts)
+ {
+ var clip = new Vector4(v.X, v.Y, 0f, 1f);
+ for (int p = 0; p < planes.Length; p++)
+ Assert.True(Vector4.Dot(planes[p], clip) >= -1e-3f);
+ }
+
+ // Exactly the four axis-aligned NDC bounds, matching AppendClipSlot's own formula.
+ Vector4[] expected =
+ [
+ new(1f, 0f, 0f, -minX), new(-1f, 0f, 0f, maxX),
+ new(0f, 1f, 0f, -minY), new(0f, -1f, 0f, maxY),
+ ];
+ for (int i = 0; i < 4; i++)
+ {
+ Assert.Equal(expected[i].X, planes[i].X, 3);
+ Assert.Equal(expected[i].Y, planes[i].Y, 3);
+ Assert.Equal(expected[i].Z, planes[i].Z, 3);
+ Assert.Equal(expected[i].W, planes[i].W, 2);
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
index 7fed1612..b69aad2c 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
@@ -83,6 +83,21 @@ public sealed class WalkTraceConformanceTests
public void OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount)
=> WalkTraceReplayContext.AppendFloodTurns(Tokens, cells);
+
+ /// S3 review fix round 1 (F4b): retail's trailing per-frame
+ /// weather turn (GameSky::Draw(sky,1) @0x00506ff0, fired by
+ /// RetailFrameWalk.DrawLandscape when ctx.WeatherGateOpen
+ /// holds) prints its own "OC" line
+ /// (WalkFrameDriver.OnWeatherTurn's production sibling) —
+ /// before this fix the interface's default no-op left the replay
+ /// side of the eight-kind signature silently blind to it, so
+ ///
+ /// had to drop the SAME line from the oracle side just to keep both
+ /// sides comparable (a pin that could never fail on the weather
+ /// turn's own placement/value). Recording it here the SAME way
+ /// records every other "OC" (identical token
+ /// shape) lets the oracle side compare literally instead.
+ public void OnWeatherTurn(uint viewerCellId) => Tokens.Add($"OC:{viewerCellId:x8}");
}
private static DatCollection OpenDats()
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
index 53a7fe9b..588c8671 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
@@ -51,6 +51,15 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
public WalkTraceReplayContext(WalkOraclePose pose, Dictionary cells)
{
_cells = cells;
+ // S3 review fix round 1 (F4b): the SAME cell id the harness hands
+ // WalkFrame's own cameraCellId (every call site in
+ // WalkTraceConformanceTests passes pose.CellId) — production wires
+ // the identical invariant (RetailPViewFrameInput.ViewerCellId ==
+ // WalkFrame's cameraCellId, see WalkFrameDriverTranscriptTests'
+ // own citation). Needed so a trailing weather "OC" this context's
+ // WeatherGateOpen now legitimately opens carries the SAME cell id
+ // retail's own capture recorded.
+ ViewerCellId = pose.CellId;
WorldViewpoint = pose.Origin;
var rotation = new Quaternion(pose.Q1, pose.Q2, pose.Q3, pose.Q0);
// Basis convention RE-pinned 2026-08-30 (second pass): storage
@@ -101,6 +110,26 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
public WalkPlane CyPlane { get; }
public IWalkFrameContext CellContext => this;
+ /// S3 review fix round 1 (F4b): overrides
+ /// 's default 0 — see the
+ /// constructor's own doc comment for why this equals the harness's own
+ /// WalkFrame cameraCellId argument.
+ public uint ViewerCellId { get; }
+
+ /// S3 review fix round 1 (F4b): overrides
+ /// 's default false with retail's
+ /// own gate — SmartBox::is_player_outside @0x00451e80,
+ /// (cellId & 0xFFFF) < 0x100 — so an outdoor-rooted
+ /// fixture's trailing weather turn fires during replay exactly where
+ /// retail's own capture recorded it, and an interior-rooted fixture's
+ /// never does (retail's own gate is unconditionally false whenever the
+ /// viewer's cell id has local part >= 0x100, regardless of whether an
+ /// exit view survives). This decomp-port context has no App-level
+ /// render-toggle concept to AND against — see the interface member's own
+ /// doc comment for why that is safe here (a pure walk conformance
+ /// harness assumes both toggles on, matching retail's default).
+ public bool WeatherGateOpen => (ViewerCellId & 0xFFFFu) < 0x100u;
+
public void SetActiveView(WalkPortalView views, int index)
{
WalkViewPoly poly = views.View.Polys[index];
@@ -200,20 +229,19 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
/// reason with LC/SC). Round 2 fixes this: the oracle side now reads its
/// own CAPTURED EC/OC lines verbatim, in the order retail's
/// cdb breakpoints actually recorded them — a real comparison against a
- /// real trace, not a derivation compared to itself. The one exclusion is
- /// the trailing per-frame weather OC (§11.6 H1): the LAST event
- /// of the frame's raw list, when its cell id matches the frame's own "P"
- /// line () AND that P cell itself looks
- /// outdoor (local id < 0x100 — retail's own weather gate,
- /// SmartBox::is_player_outside) — an interior root's OWN flood can
- /// ALSO end the frame with a real OC matching the P cell (its nearest
- /// cell's object-list turn draws last, and that nearest cell is often
- /// the camera's own root cell: cathedral-arrival.walk's real trailing
- /// "OC f4180108" at DI:f4180108/P-cell f4180108), so position and value
- /// alone are not enough — it owns no DC and is not a walk turn at all
- /// (see WalkOracleTraceTests for a dedicated
- /// assertion that a COMPLETE outdoor frame's raw event list really does
- /// end with it).
+ /// real trace, not a derivation compared to itself.
+ ///
+ ///
+ /// S3 review fix round 1 (F4b): round 2's own exclusion of the trailing
+ /// per-frame weather OC is GONE — 's Recorder now implements
+ /// IWalkEventSink.OnWeatherTurn (the interface's default was a
+ /// silent no-op), so the replay side records that exact "OC" line too,
+ /// at the exact point RetailFrameWalk.DrawLandscape fires it. Both
+ /// sides now carry the weather turn's own trailing OC literally — the
+ /// eight-kind signature pins its placement and value like every other
+ /// line, rather than the two sides mutually agreeing to stay silent
+ /// about it.
///
///
public static string Signature8(IReadOnlyList tokens) => string.Join("|", tokens);
@@ -254,30 +282,13 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
tokens.Add($"EC:{e.CellId!.Value:x8}");
break;
case WalkOracleEventKind.ObjectCellTurn:
- // §11.6 H1's trailing weather OC: the LAST event of the
- // whole frame, matching the frame's own "P" cell — not a
- // flood turn, excluded from the comparison (see this
- // method's own class doc for the citation). The extra
- // "P cell is itself an outdoor land cell" check matters:
- // an INTERIOR root's OWN flood draws its NEAREST cell's
- // object list LAST (PView::DrawCells's reversed loop,
- // index 0 = nearest = last) — when that nearest cell IS
- // the camera's own root cell, its real OC turn ALSO sits
- // at the frame's last position AND shares the P cell's
- // id (cathedral-arrival.walk: DI:f4180108, P cell
- // f4180108, real trailing "OC f4180108" — position AND
- // value alone would misclassify it). Retail's weather
- // never draws for an interior-rooted P cell at all
- // (SmartBox::is_player_outside is false whenever the
- // viewer's own cell id has local part >= 0x100), so
- // requiring the P cell itself look outdoor closes that
- // hole without reopening G8's original one.
- bool trailingWeatherOc = i == events.Count - 1
- && frame.Pose is not null
- && e.CellId!.Value == frame.Pose.CellId
- && (frame.Pose.CellId & 0xFFFFu) < 0x100u;
- if (!trailingWeatherOc)
- tokens.Add($"OC:{e.CellId!.Value:x8}");
+ // S3 review fix round 1 (F4b): every ObjectCellTurn now
+ // reads literally, trailing weather OC included — see
+ // this method's own class doc for why the former
+ // position/value exclusion is gone (the replay side now
+ // records the same line through
+ // WalkTraceConformanceTests.Recorder.OnWeatherTurn).
+ tokens.Add($"OC:{e.CellId!.Value:x8}");
break;
}
}
diff --git a/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs b/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
index 0b4dadc4..e1791cd1 100644
--- a/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
@@ -1,6 +1,10 @@
+using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
+using AcDream.App.Rendering.Gpu;
+using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Walk;
+using AcDream.App.Tests.Rendering.Gpu;
using Xunit;
namespace AcDream.App.Tests.Rendering;
@@ -117,6 +121,269 @@ public class WalkOutsideViewReassemblyTests
Assert.Equal(0, asm.OutdoorSlot);
}
+ ///
+ /// S3 review fix round 1 (F1, BLOCKING): a two-view walk whose FIRST
+ /// polygon collapses (all three points exactly collinear — zero area,
+ /// well under ClipPlaneSet's MinPolygonArea) must still
+ /// produce a length-2 slice array, index-aligned with the walk's own
+ /// view count: slice[0] is the collapsed view (flagged
+ /// ), slice[1] is the SECOND
+ /// polygon's real edge planes — not the (nonexistent) first view's, and
+ /// not shifted down by one. Constructs the
+ /// directly (bypassing 's own pixel-
+ /// space collinearity filter, which is not the same test as
+ /// ClipPlaneSet's NDC-area gate) so the collapse is exact and
+ /// deterministic.
+ ///
+ [Fact]
+ public void FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned()
+ {
+ ClipFrameAssembly asm = AssembledWithOldOutside();
+
+ var walkView = new WalkPortalView();
+ // View 0: three EXACTLY collinear pixel points -> zero-area triangle,
+ // collinear before and after the affine pixel->NDC transform.
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(100f, 100f) });
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(200f, 100f) });
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(300f, 100f) });
+ walkView.View.Polys.Add(new WalkViewPoly(3, 0, 100f, 300f, 100f, 100f));
+
+ // View 1: a normal quad.
+ int secondBase = walkView.View.Vertices.Count;
+ Vector2[] secondQuad =
+ [
+ new(600f, 400f), new(900f, 400f), new(900f, 650f), new(600f, 650f),
+ ];
+ foreach (Vector2 pixel in secondQuad)
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = pixel });
+ walkView.View.Polys.Add(new WalkViewPoly(4, secondBase, 600f, 900f, 400f, 650f));
+
+ walkView.ViewCount = 2;
+
+ ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
+
+ Assert.Equal(2, asm.OutsideViewSlices.Length);
+
+ ClipViewSlice collapsed = asm.OutsideViewSlices[0];
+ Assert.True(collapsed.NothingVisible);
+ Assert.Equal(0, collapsed.Slot);
+ Assert.Empty(collapsed.Planes);
+ Assert.Equal(default, collapsed.NdcAabb);
+
+ ClipViewSlice survivor = asm.OutsideViewSlices[1];
+ Assert.False(survivor.NothingVisible);
+ // The second polygon's own edge planes, converted from pixel space
+ // (px=600..900, py=400..650) into the assembler's standard NDC.
+ var expectedNdcVerts = new Vector2[secondQuad.Length];
+ for (int i = 0; i < secondQuad.Length; i++)
+ {
+ expectedNdcVerts[i] = new Vector2(
+ secondQuad[i].X / W * 2f - 1f,
+ 1f - secondQuad[i].Y / H * 2f);
+ }
+ AssertEveryEdgeMidpointLiesOnSomeGpuPlane(expectedNdcVerts, survivor.Planes);
+ }
+
+ ///
+ /// F1's punch-leaf half: RetailPViewPassExecutor.DrawWalkPunchFan
+ /// reading the SAME index-aligned array produced above. Uses a recording
+ /// surface so the assertion inspects the
+ /// actual GPU submission (or its absence), not just the CPU slice.
+ /// activeViewIndex=1 (the survivor) submits a fan with the survivor's
+ /// planes; activeViewIndex=0 (the collapsed view) submits NOTHING;
+ /// activeViewIndex=2 (out of range) throws (fail-loud rule).
+ ///
+ [Fact]
+ public void PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange()
+ {
+ ClipFrameAssembly asm = AssembledWithOldOutside();
+ var walkView = new WalkPortalView();
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(100f, 100f) });
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(200f, 100f) });
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(300f, 100f) });
+ walkView.View.Polys.Add(new WalkViewPoly(3, 0, 100f, 300f, 100f, 100f));
+ int secondBase = walkView.View.Vertices.Count;
+ Vector2[] secondQuad =
+ [
+ new(600f, 400f), new(900f, 400f), new(900f, 650f), new(600f, 650f),
+ ];
+ foreach (Vector2 pixel in secondQuad)
+ walkView.View.Vertices.Add(new WalkViewVertex { Point = pixel });
+ walkView.View.Polys.Add(new WalkViewPoly(4, secondBase, 600f, 900f, 400f, 650f));
+ walkView.ViewCount = 2;
+ ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
+ Assert.Equal(2, asm.OutsideViewSlices.Length);
+
+ using var device = new RecordingGpuDevice();
+ var frames = new GpuDeviceFrameLifetime(device);
+ var scope = new VulkanWorldPassScope(sampleCount: 1);
+ using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
+
+ // DrawWalkPunchFan only reads _portalDepthMask, worldPolygon.Vertices,
+ // clipAssembly.OutsideViewSlices, and frame.ViewProjection — a bare
+ // (constructor-bypassed) executor with only that one field set
+ // exercises the real leaf without needing a full GL/DAT renderer
+ // graph. Same GetUninitializedObject pattern WorldRenderCompositionTests
+ // already uses for stubbing App-layer types.
+ var executor = (RetailPViewPassExecutor)System.Runtime.CompilerServices.RuntimeHelpers
+ .GetUninitializedObject(typeof(RetailPViewPassExecutor));
+ typeof(RetailPViewPassExecutor)
+ .GetField("_portalDepthMask", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
+ .SetValue(executor, portalDepthMask);
+
+ var worldPolygon = new WalkPolygon
+ {
+ Vertices = new[]
+ {
+ new Vector3(-1f, -1f, 0f), new Vector3(1f, -1f, 0f), new Vector3(0f, 1f, 0f),
+ },
+ };
+
+ RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
+ rootCell: null!,
+ nearbyBuildingCells: null,
+ viewerEyePos: Vector3.Zero,
+ viewProjection: Matrix4x4.Identity,
+ cells: null!,
+ camera: null!,
+ cameraWorldPosition: Vector3.Zero,
+ frustum: null,
+ playerLandblockId: null,
+ animatedEntityIds: null,
+ renderCenterLbX: 0,
+ renderCenterLbY: 0,
+ renderRadius: 0,
+ landblockEntries: Array.Empty<(uint, Vector3, Vector3, IReadOnlyList, IReadOnlyDictionary?)>(),
+ renderSky: false,
+ renderWeather: false,
+ dayFraction: 0f,
+ activeDayGroup: null,
+ skyKeyframe: default,
+ environOverrideActive: false,
+ viewerCellId: 0,
+ playerCellId: 0,
+ playerViewPosition: Vector3.Zero,
+ cameraView: Matrix4x4.Identity,
+ cameraCellResolution: default);
+
+ void RunDraw(int activeViewIndex)
+ {
+ frames.BeginFrame();
+ portalDepthMask.BeginFrame(frameSlot: 0);
+ using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
+ GpuPassDescription.BackbufferClear(
+ "s3-f1-punch-leaf", Vector4.Zero, sampleCount: 1)))
+ using (scope.Publish(pass))
+ {
+ executor.DrawWalkPunchFan(frame, asm, worldPolygon, activeViewIndex);
+ }
+ frames.EndFrame();
+ }
+
+ // activeViewIndex = 0: the collapsed view. Draws nothing.
+ int drawsBefore = device.Calls.OfType().Count();
+ RunDraw(0);
+ int drawsAfterCollapsed = device.Calls.OfType().Count();
+ Assert.Equal(drawsBefore, drawsAfterCollapsed);
+
+ // activeViewIndex = 1: the surviving view. Submits exactly one fan draw.
+ RunDraw(1);
+ int drawsAfterSurvivor = device.Calls.OfType().Count();
+ Assert.Equal(drawsAfterCollapsed + 1, drawsAfterSurvivor);
+
+ // activeViewIndex = 2: out of range. Fails loud instead of drawing unclipped.
+ var ex = Assert.Throws(() =>
+ {
+ frames.BeginFrame();
+ portalDepthMask.BeginFrame(frameSlot: 0);
+ using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
+ GpuPassDescription.BackbufferClear(
+ "s3-f1-punch-leaf-oob", Vector4.Zero, sampleCount: 1)))
+ using (scope.Publish(pass))
+ {
+ executor.DrawWalkPunchFan(frame, asm, worldPolygon, 2);
+ }
+ });
+ Assert.Contains("activeViewIndex", ex.Message);
+ }
+
+ /// Same CPU/GPU equivalence check as
+ /// ClipFrameLayoutTests.AssertEveryEdgeMidpointLiesOnSomeGpuPlane
+ /// (private there, duplicated here): every edge midpoint of the source
+ /// NDC polygon must be non-negative under every plane and ~zero under at
+ /// least one (its own edge's plane).
+ private static void AssertEveryEdgeMidpointLiesOnSomeGpuPlane(
+ Vector2[] verts, System.ReadOnlySpan gpuPlanes)
+ {
+ const float eps = 1e-4f;
+ for (int i = 0; i < verts.Length; i++)
+ {
+ Vector2 a = verts[i];
+ Vector2 b = verts[(i + 1) % verts.Length];
+ Vector2 mid = (a + b) / 2f;
+ var clip = new Vector4(mid.X, mid.Y, 0f, 1f);
+
+ float minAbsDistance = float.PositiveInfinity;
+ foreach (Vector4 plane in gpuPlanes)
+ {
+ float distance = Vector4.Dot(plane, clip);
+ Assert.True(
+ distance >= -eps,
+ $"edge {i} midpoint ({mid.X},{mid.Y}) must be inside-or-on every "
+ + $"GPU plane; plane {plane} gave distance {distance}");
+ minAbsDistance = MathF.Min(minAbsDistance, MathF.Abs(distance));
+ }
+ Assert.True(
+ minAbsDistance < eps,
+ $"edge {i} midpoint ({mid.X},{mid.Y}) should lie ~on its OWN GPU plane; "
+ + $"the closest plane was only {minAbsDistance} away");
+ }
+ }
+
+ ///
+ /// S3 review fix round 1 (F2): the outside-view sibling of
+ /// WalkFrameDriverClipSealTests.ExitSealPath_NineVertexView_... —
+ /// a 9-vertex outside view is too complex for the <=8-plane budget
+ /// (), but unlike the exit-seal
+ /// path (which falls back to a conservative 4-plane AABB) the outside
+ /// view/punch-fan path has NO scissor consumer at all (S3 chunk 4 fix
+ /// round 2, L2) — it draws fully UNCLIPPED:
+ /// stays 0 with an EMPTY array, and
+ /// critically is FALSE — this
+ /// is the "draw unclipped" state F1 distinguishes from the "draw
+ /// nothing" collapsed-view state.
+ ///
+ [Fact]
+ public void NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible()
+ {
+ ClipFrameAssembly asm = AssembledWithOldOutside();
+
+ const int n = 9;
+ var verts = new Vector2[n];
+ for (int i = 0; i < n; i++)
+ {
+ float angle = i * MathF.Tau / n;
+ verts[i] = new Vector2(0.6f * MathF.Cos(angle), 0.6f * MathF.Sin(angle));
+ }
+ var pixelPoints = new WalkScreenPoint[n];
+ for (int i = 0; i < n; i++)
+ {
+ pixelPoints[i] = new WalkScreenPoint(
+ (verts[i].X + 1f) * W / 2f, (1f - verts[i].Y) * H / 2f, 0f, 1f);
+ }
+ var walkView = new WalkPortalView();
+ Assert.True(WalkCopyView.Append(walkView, pixelPoints, new StubRays(), Vector3.Zero));
+ // No collinear-merge stole a vertex.
+ Assert.Equal(n, walkView.View.Polys[0].VertexCount);
+
+ ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
+
+ ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
+ Assert.False(slice.NothingVisible);
+ Assert.Equal(0, slice.Slot);
+ Assert.Empty(slice.Planes);
+ }
+
[Fact]
public void TwoWalkViews_ProduceTwoSlicesWithDistinctSlots()
{
diff --git a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs
index a596f2cc..b4833bb5 100644
--- a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs
@@ -1,185 +1,28 @@
-using System.Collections.Generic;
using AcDream.App.Rendering.Wb;
using Xunit;
namespace AcDream.App.Tests.Rendering.Wb;
+///
+/// S3 review fix round 1 (F5): every test this file used to hold
+/// (RawResolve_* against WbDrawDispatcher.ResolveEntitySlot,
+/// ForFrame_RoutingActive_* against the active branch of
+/// ResolveSlotForFrame) pinned the Phase U.4 per-instance clip-slot
+/// routing policy — deleted outright because the ONE method that could ever
+/// arm it had ZERO production callers, so that
+/// policy never ran in any shipped build. ResolveEntitySlot and the
+/// ClipSlotCull sentinel are deleted with it; the ONE surviving
+/// behavior — every instance is slot 0, nothing is ever culled — is what
+/// this file now pins.
+///
public sealed class WbDrawDispatcherClipSlotTests
{
- private const uint VisibleCellA = 0xA9B4_0164u;
- private const uint VisibleCellB = 0xA9B4_0165u;
- private const uint NotVisibleCell = 0xA9B4_0999u;
- private const uint OutdoorCell = 0xA9B4_0020u;
-
- private const int SlotA = 3;
- private const int SlotB = 7;
- private const int OutsideViewSlot = 11;
-
- private static IReadOnlyDictionary Routing() => new Dictionary
- {
- [VisibleCellA] = SlotA,
- [VisibleCellB] = SlotB,
- };
-
[Fact]
- public void RawResolve_LiveEntity_WithVisibleIndoorParent_GetsThatCellSlot()
+ public void ResolveSlotForFrame_AlwaysReturnsSlotZeroNeverCulled()
{
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0x5000_000Au, parentCellId: VisibleCellA,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
+ var r = WbDrawDispatcher.ResolveSlotForFrame();
- Assert.Equal(SlotA, slot);
- }
-
- [Fact]
- public void RawResolve_LiveEntity_WithHiddenIndoorParent_IsCulled()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0x5000_000Au, parentCellId: NotVisibleCell,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
- }
-
- [Fact]
- public void RawResolve_LiveEntity_WithOutdoorParent_UsesOutsideViewWhenVisible()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0x5000_000Au, parentCellId: OutdoorCell,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(OutsideViewSlot, slot);
- }
-
- [Fact]
- public void RawResolve_LiveEntity_WithParentNull_IsCulledWhenRoutingActive()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0x5000_000Au, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
- }
-
- [Fact]
- public void RawResolve_CellStatic_InVisibleSet_GetsThatCellSlot()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0u, parentCellId: VisibleCellB,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(SlotB, slot);
- }
-
- [Fact]
- public void RawResolve_CellStatic_NotInVisibleSet_IsCulled()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0u, parentCellId: NotVisibleCell,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
- }
-
- [Fact]
- public void RawResolve_OutdoorStab_OutdoorsVisible_GetsOutsideViewSlot()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0u, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal(OutsideViewSlot, slot);
- }
-
- [Fact]
- public void RawResolve_OutdoorStab_OutdoorsNotVisible_IsCulled()
- {
- int slot = WbDrawDispatcher.ResolveEntitySlot(
- serverGuid: 0u, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: false);
-
- Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
- }
-
- [Fact]
- public void ForFrame_RoutingInactive_EveryEntityIsSlot0AndNotCulled()
- {
- var live = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: false, serverGuid: 0x5000_000Au, parentCellId: null,
- cellIdToSlot: null, outdoorSlot: OutsideViewSlot, outdoorVisible: true);
- Assert.Equal(0u, live.Slot);
- Assert.False(live.Culled);
-
- var wouldCull = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: false, serverGuid: 0u, parentCellId: NotVisibleCell,
- cellIdToSlot: null, outdoorSlot: OutsideViewSlot, outdoorVisible: false);
- Assert.Equal(0u, wouldCull.Slot);
- Assert.False(wouldCull.Culled);
- }
-
- [Fact]
- public void ForFrame_RoutingActive_LiveEntityVisible_GetsCellSlotNotCulled()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0x5000_000Au, parentCellId: VisibleCellA,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal((uint)SlotA, r.Slot);
- Assert.False(r.Culled);
- }
-
- [Fact]
- public void ForFrame_RoutingActive_LiveEntityParentNull_Culled()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0x5000_000Au, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.True(r.Culled);
Assert.Equal(0u, r.Slot);
- }
-
- [Fact]
- public void ForFrame_RoutingActive_CellStaticVisible_GetsCellSlotNotCulled()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0u, parentCellId: VisibleCellA,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal((uint)SlotA, r.Slot);
Assert.False(r.Culled);
}
-
- [Fact]
- public void ForFrame_RoutingActive_CellStaticNotVisible_Culled()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0u, parentCellId: NotVisibleCell,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.True(r.Culled);
- Assert.Equal(0u, r.Slot);
- }
-
- [Fact]
- public void ForFrame_RoutingActive_OutdoorStabVisible_GetsOutsideViewSlot()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0u, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
-
- Assert.Equal((uint)OutsideViewSlot, r.Slot);
- Assert.False(r.Culled);
- }
-
- [Fact]
- public void ForFrame_RoutingActive_OutdoorStabNotVisible_Culled()
- {
- var r = WbDrawDispatcher.ResolveSlotForFrame(
- clipRoutingActive: true, serverGuid: 0u, parentCellId: null,
- cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: false);
-
- Assert.True(r.Culled);
- Assert.Equal(0u, r.Slot);
- }
}