From 211350b8a67b93c0c3ea3613e7d6329f168cb7b0 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 31 May 2026 09:53:38 +0200 Subject: [PATCH] =?UTF-8?q?docs(render):=20Phase=20U.4c=20=E2=80=94=20impl?= =?UTF-8?q?ementation=20plan=20(stabilize=20portal=20visibility)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five tasks: (1) RED apparatus reproducing the doorway flap on a synthetic C0->C1->C2(exit) chain; (2) Layer 1 LoadedCell.VisibleCells + SeenOutside plumbing; (3) oracle-ported PVS grounding of set membership (the fix, gated by task 1); (4) seen_outside invariants (sealed=empty, threshold=stable); (5) live [vis] + visual gate. Task 3 is a faithful port (add_views 433382 / InitCell 432896 / ClipPortals 433572), pseudocode-first, not fabricated. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...1-phase-u4c-stabilize-portal-visibility.md | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-31-phase-u4c-stabilize-portal-visibility.md diff --git a/docs/superpowers/plans/2026-05-31-phase-u4c-stabilize-portal-visibility.md b/docs/superpowers/plans/2026-05-31-phase-u4c-stabilize-portal-visibility.md new file mode 100644 index 0000000..ad6cad7 --- /dev/null +++ b/docs/superpowers/plans/2026-05-31-phase-u4c-stabilize-portal-visibility.md @@ -0,0 +1,412 @@ +# Phase U.4c — Stabilize Portal Visibility (fix the threshold "flap") Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the indoor portal-visibility **set** stable across camera pose so the cottage doorway no longer flaps terrain + building-shells off, by grounding the set in the per-cell precomputed PVS (`stab_list`) + the `seen_outside` flag — the retail mechanism. + +**Architecture:** Three layers. (1) `LoadedCell` carries two already-in-process stable inputs (`VisibleCells` = `stab_list`, `SeenOutside` flag). (2) `PortalVisibilityBuilder` grounds set membership in the camera cell's PVS so a brittle per-frame portal-side test can no longer drop the exit-portal cell. (3) The `SeenOutside` flag is the stability anchor + test oracle. The per-frame portal-clip walk and every rendering consumer (`ClipFrameAssembler`, `ClipFrame`, shaders, `EnvCellRenderer`, terrain) are unchanged — U.4c changes only *what feeds* them. + +**Tech Stack:** C# / .NET 10, `System.Numerics`, xUnit. GL-free CPU code only (no shader/GPU work — that shipped in U.3/U.4). Retail oracle: `docs/research/named-retail/acclient_2013_pseudo_c.txt`. + +**Spec:** [`docs/superpowers/specs/2026-05-31-phase-u4c-stabilize-portal-visibility-design.md`](../specs/2026-05-31-phase-u4c-stabilize-portal-visibility-design.md) — read it first. + +**Branch:** `claude/thirsty-goldberg-51bb9b` (continue; **do NOT** create a new branch/worktree, **do NOT** drop the two `git stash` entries). + +--- + +## Workflow rules (apply to every task) + +- This is AC-specific behavior. Follow the mandated workflow: **grep named → decompile → pseudocode → port → conformance-test**. For Task 3 the algorithm body is **ported from the cited decomp lines**, not invented. If the decomp and a guess disagree, the decomp wins. +- Build + test green before every commit: + - Build: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` + - App tests: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug` +- App-test baseline at session start: **151/151 passing** (the Core suite has documented pre-existing static-leak flakiness — ignore Core unless a task touches Core; no Core production files are touched here). +- Commit messages: `(render): Phase U.4c — ` + the trailer: + `Co-Authored-By: Claude Opus 4.8 (1M context) ` + +--- + +## File Structure + +| File | Change | Responsibility | +|------|--------|----------------| +| `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs` | Modify | Add the flap-reproduction regression test (Task 1) + `SeenOutside`/PVS-grounding tests (Tasks 3-4). | +| `src/AcDream.App/Rendering/CellVisibility.cs` | Modify | Add `LoadedCell.VisibleCells` + `LoadedCell.SeenOutside` fields (Task 2). | +| `src/AcDream.App/Rendering/GameWindow.cs` | Modify | Populate the two new fields at the existing EnvCell-build site (~5696) from `envCell.VisibleCells` + `envCell.Flags` (Task 2). | +| `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` | Modify | Ground set membership in the camera cell's PVS; faithful port of retail's `add_views`/`InitCell`/`ClipPortals` grounding (Task 3). | +| `src/AcDream.App/Rendering/ClipFrameAssembler.cs` | (optional, Task 6) | Cosmetic only: branch the 3 `Count==0` states before `AppendSlot`. | + +No new files. No new project references. No GL/shader changes. + +--- + +## Task 1: Apparatus — reproduce the flap in a GL-free unit test (RED) + +The flap = the exit-portal cell drops from the visible set when an intermediate portal's per-frame side test flips as the camera moves a few cm, emptying `OutsideView`. Reproduce that deterministically so the fix has a falsifiable gate. This task adds **no production code** — it produces a RED test that documents the bug (the same discipline that #103 lacked at the unit level). + +**Files:** +- Test: `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs` + +- [ ] **Step 1: Add the flap-reproduction test** + +Append this to `PortalVisibilityBuilderTests` (it reuses the existing `Cell` / `Quad` helpers in that file). Build a `ViewProj` from each camera pose so the side test and the projection agree: + +```csharp +// ----------------------------------------------------------------------- +// Phase U.4c: the threshold "flap". A chain camera(C0) -> mid(C1) -> exit(C2) +// where the C0->C1 portal's clip plane sits just in front of the camera. +// BOTH poses are legitimately inside C0 and SHOULD see the exit window; they +// straddle the C0->C1 side-test boundary by a few cm. Pre-fix, the pose just +// behind the plane hard-culls C0->C1 (CameraOnInteriorSide), C2 is never +// reached, and OutsideView empties — the flap. The fix must keep the exit +// cell visible (OutsideView non-empty) at BOTH poses. +// ----------------------------------------------------------------------- +private static Matrix4x4 ViewProjAt(Vector3 eye) +{ + var view = Matrix4x4.CreateLookAt(eye, eye + new Vector3(0, 0, -1), Vector3.UnitY); + var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f); + return view * proj; +} + +private static (LoadedCell cam, Dictionary all) FlapChain() +{ + const uint C0 = 0x0001, C1 = 0x0002, C2 = 0x0003; + // C0 -> C1 portal at z=-1, with a clip plane (normal +Z, InsideSide=0) at z=-1. + // dot = camZ + D; with D = 1 the plane is at camZ = -1: inside iff camZ >= -1 - eps. + var c0 = Cell(C0, new CellPortalInfo((ushort)C1, 0, 0, 0)); + c0.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -1f)); + c0.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 1f, InsideSide = 0 }); + // C1 -> C2 (no clip plane → never culled), C2 has the exit window. + var c1 = Cell(C1, new CellPortalInfo((ushort)C2, 0, 0, 0)); + c1.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); + var c2 = Cell(C2, new CellPortalInfo(0xFFFF, 0, 0, 0)); + c2.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -7f)); + var all = new Dictionary { [C0] = c0, [C1] = c1, [C2] = c2 }; + return (c0, all); +} + +[Fact] +public void Build_NearBoundaryIntermediatePortal_ExitCellStaysVisibleAcrossPose() +{ + var (cam, all) = FlapChain(); + Func lookup = id => all.TryGetValue(id, out var c) ? c : null; + + // Pose A: a few cm IN FRONT of the C0->C1 plane (camZ = -0.9 >= -1 → inside). + var poseA = new Vector3(0, 0, -0.9f); + var frameA = PortalVisibilityBuilder.Build(cam, poseA, lookup, ViewProjAt(poseA)); + + // Pose B: a few cm BEHIND it (camZ = -1.1 < -1 → pre-fix the side test culls C0->C1). + var poseB = new Vector3(0, 0, -1.1f); + var frameB = PortalVisibilityBuilder.Build(cam, poseB, lookup, ViewProjAt(poseB)); + + // The exit cell — and therefore OutsideView — must be present at BOTH poses. + Assert.False(frameA.OutsideView.IsEmpty, "pose A should see the exit window"); + Assert.False(frameB.OutsideView.IsEmpty, + "pose B (a few cm away) must ALSO see the exit window — this is the flap: " + + "an intermediate side-test flip must not drop the exit cell from the set"); + Assert.Contains(0x0003u, frameB.OrderedVisibleCells); +} +``` + +- [ ] **Step 2: Run the test and confirm it FAILS (reproduces the flap)** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~Build_NearBoundaryIntermediatePortal"` +Expected: **FAIL** at the pose-B assertion (`OutsideView` empty / `0x0003` absent). If it PASSES, the fixture didn't reproduce the cull — **tune the geometry** (move `poseB` a touch further behind the plane, or nudge `ClipPlanes[0].D`) until pose B is RED while pose A is GREEN, then re-confirm. The RED-at-B / GREEN-at-A split is the whole point — do not proceed until you have it. + +- [ ] **Step 3: Commit the RED apparatus** + +```bash +git add tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs +git commit -m "test(render): Phase U.4c — reproduce the doorway flap (RED apparatus) + +Synthetic C0->C1->C2(exit) chain; two camera poses straddle the C0->C1 +side-test boundary by a few cm. Pre-fix, pose B hard-culls C0->C1 and the +exit cell drops -> OutsideView empties (the flap). Gates the U.4c fix. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 2: Layer 1 — `LoadedCell` carries the stable PVS + `SeenOutside` + +Plumb the two already-in-process inputs onto the render-side cell. No new dat parsing. + +**Files:** +- Modify: `src/AcDream.App/Rendering/CellVisibility.cs` (add two fields to `LoadedCell`) +- Modify: `src/AcDream.App/Rendering/GameWindow.cs:5696` (populate them at hydration) + +- [ ] **Step 1: Add the two fields to `LoadedCell`** + +In `CellVisibility.cs`, inside `public sealed class LoadedCell`, after the `BuildingId` property, add: + +```csharp + /// + /// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail + /// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially + /// visible from this cell, precomputed by the AC content tools. Refreshed only at + /// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878). + /// PortalVisibilityBuilder grounds set membership in it so a brittle per-frame + /// portal-side test can't drop a potentially-visible cell from the visible set. + /// Empty when the dat carried no stab list (degenerate / old cell). + /// + public IReadOnlyList VisibleCells = System.Array.Empty(); + + /// + /// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees + /// the exterior (an exit portal is reachable from it). Retail gates the landscape + /// data + draw decision on the camera cell's value (RenderNormalMode decomp:92649, + /// grab_visible_cells decomp:311878). The stable anchor for the terrain-draw test. + /// + public bool SeenOutside; +``` + +- [ ] **Step 2: Populate them at the hydration site** + +In `GameWindow.cs`, in the EnvCell-build method, locate the `var loaded = new LoadedCell { ... }` initializer at ~5696. Immediately BEFORE it, derive the two values from `envCell` (the local already in scope that supplied `envCell.CellPortals`): + +```csharp + // Phase U.4c: surface the stable PVS + seen-outside flag onto the render cell. + // Both come straight off the dat EnvCell — no new parsing (PhysicsDataCache + // already reads VisibleCells the same way; A8CellAudit reads the flag). + uint lbPrefix = envCellId & 0xFFFF0000u; + var visibleCells = new List(); + if (envCell.VisibleCells is not null) + foreach (var lowId in envCell.VisibleCells) + visibleCells.Add(lbPrefix | lowId); + bool seenOutside = envCell.Flags.HasFlag(AcDream.Core.Dats.EnvCellFlags.SeenOutside); +``` + +> NOTE: confirm the exact `EnvCellFlags` namespace/type by grepping the symbol used at `tools/A8CellAudit/Program.cs:200` (`envCell.Flags.HasFlag(EnvCellFlags.SeenOutside)`) and match its `using`/qualified name. Do not guess the namespace. + +Then add the two fields to the initializer: + +```csharp + var loaded = new LoadedCell + { + CellId = envCellId, + WorldPosition = cellOrigin, + WorldTransform = cellTransform, + InverseWorldTransform = inverse, + LocalBoundsMin = boundsMin, + LocalBoundsMax = boundsMax, + Portals = portals, + ClipPlanes = clipPlanes, + PortalPolygons = portalPolygons, // Phase A8 + VisibleCells = visibleCells, // Phase U.4c + SeenOutside = seenOutside, // Phase U.4c + }; +``` + +- [ ] **Step 3: Build green** + +Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` +Expected: build succeeds (the existing flap test from Task 1 is still RED — that's correct; the fix is Task 3). + +- [ ] **Step 4: Commit** + +```bash +git add src/AcDream.App/Rendering/CellVisibility.cs src/AcDream.App/Rendering/GameWindow.cs +git commit -m "feat(render): Phase U.4c — LoadedCell carries stab_list PVS + seen_outside + +VisibleCells (full ids) + SeenOutside, populated at the EnvCell-build site from +envCell.VisibleCells + envCell.Flags. Mirrors retail CEnvCell.stab_list / +seen_outside (acclient.h ~30925). Data already in-process; render path no longer +drops it. Consumed by the builder in U.4c-3. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 3: Layer 2 — ground set membership in the PVS (the fix; oracle-ported, gated by Task 1) + +This is the AC-algorithm port. **Read the oracle first, write pseudocode, then port** — do not invent the algorithm. + +**Files:** +- Modify: `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` +- Test gate: `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs` (Task 1's test goes GREEN) + +- [ ] **Step 1: Read the oracle + write a pseudocode note** + +Read, in `docs/research/named-retail/acclient_2013_pseudo_c.txt`: +- `PView::DrawInside` 433793 — calls `add_views(num_stabs, stab_list)` before `ConstructView`. +- `PView::add_views` 433382 — `for each stab: GetVisible(id); curr_view_push(cell)` (seeds a live view accumulator on every PVS cell). +- `PView::InitCell` 432896 — per-portal **sidedness** classification (the retail analog of our `CameraOnInteriorSide`; note its plane-dot + `portal_side` compare + ~0.0002 epsilon, lines 432928-432968). +- `PView::ClipPortals` 433572 — only `seen && !inflag` portals propagate; `0xFFFF` → `outside_view` (433662-433676). +- `PView::AddViewToPortals` 433446 — enqueue on first discovery (`ecx_5==0`); re-incorporate on view-growth (`AddToCell`/`FixCellList`, 433494-433502). + +Compare against our `PortalVisibilityBuilder.Build` (the walk 116-228) and `CameraOnInteriorSide` (237-244). Identify the **divergence** that lets our exit cell drop where retail's stays present. Two candidates the apparatus disambiguates: +- **H1 (set grounding):** retail keeps the cell present via `add_views`/`visible_cell_table`; our pure-walk set has no such anchor. Fix = the camera cell's `VisibleCells` PVS grounds set membership. +- **H2 (side test):** our `CameraOnInteriorSide` (centroid-derived `InsideSide`, ±0.01) is a less-stable reimplementation than retail's `InitCell` sidedness; fix = port `InitCell`'s sidedness faithfully. + +Write a short note to `docs/research/2026-05-31-u4c-pview-grounding-pseudocode.md`: the retail traversal in pseudocode + the identified divergence + which fix the apparatus selects. + +- [ ] **Step 2: Confirm the Task-1 test is RED against current code** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~Build_NearBoundaryIntermediatePortal"` +Expected: FAIL at pose B (carried over from Task 1). + +- [ ] **Step 3: Port the grounding into the builder** + +Implement the fix selected in Step 1, ported from the cited decomp. The design intent (spec §5.2): **the camera cell's PVS (`VisibleCells`) is the authority on set membership; the per-frame portal-side test refines clip regions but must not drop a PVS-member cell from the visible set.** Concretely, ground the traversal so that: +- every cell in `cameraCell.VisibleCells` is a participant (resolvable + reachable), so the exit cell cannot be culled out of existence by a near-boundary side-test flip; and +- the `0xFFFF` exit contribution to `OutsideView` is gathered from PVS-member cells. + +Keep the change minimal and faithful — prefer porting `InitCell` sidedness (H2) if that alone makes the apparatus GREEN; add the PVS membership grounding (H1) as the structural net. **Do not add a hysteresis / last-frame band-aid** (forbidden). **Do not** introduce a full-screen "draw everything when unsure" fallback as the common-case path — over-include is a §6 *degenerate-data* backstop only. + +Cite the decomp anchors in code comments (function + address). The builder stays GL-free; its public signature + `PortalVisibilityFrame` shape are unchanged. + +- [ ] **Step 4: Run the full builder test suite — flap GREEN, no regression** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~PortalVisibilityBuilderTests"` +Expected: ALL PASS — the new flap test GREEN **and** every prior builder test still GREEN. In particular `Builder_BackFacingPortal_NotTraversed`, `Builder_SealedCellar_NoExitPortal_OutsideViewEmpty`, and `Builder_Cellar_WindowClippedToStairwell_NotFullWindow` must still pass (a fix that over-includes — drawing terrain when genuinely sealed, or failing to narrow the window — breaks these; that is the guardrail against a band-aid). + +- [ ] **Step 5: Full App suite green** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug` +Expected: 152/152 (151 baseline + the new flap test). + +- [ ] **Step 6: Commit** + +```bash +git add src/AcDream.App/Rendering/PortalVisibilityBuilder.cs tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs docs/research/2026-05-31-u4c-pview-grounding-pseudocode.md +git commit -m "fix(render): Phase U.4c — ground portal visibility set in the PVS (closes the flap) + +Port of retail PView grounding (add_views 433382 / InitCell 432896 / ClipPortals +433572): the camera cell's stab_list PVS is the authority on visible-set +membership, so a near-boundary CameraOnInteriorSide flip no longer drops the +exit-portal cell -> OutsideView no longer spuriously empties -> no TerrainMode.Skip +flap. Flap regression test GREEN; sealed-cellar + window-narrowing tests still +GREEN (no over-include band-aid). Pseudocode note + decomp anchors inline. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 4: Layer 3 — `SeenOutside` invariants + assembler-unchanged confirmation + +Lock the stable terrain-decision invariants (spec §5.3) so a future change can't silently re-introduce the flap or over-draw. + +**Files:** +- Test: `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs` + +- [ ] **Step 1: Add the invariant tests** + +```csharp +[Fact] // windowless interior: no exit portal in the PVS chain → no terrain (correctly empty) +public void Build_WindowlessInterior_OutsideViewEmpty() +{ + // C0 -> C1, neither has a 0xFFFF portal. Even with PVS grounding, OutsideView + // must be empty — terrain is correctly absent in a sealed room (matches retail + // DrawCells gating on outside_view.view_count, decomp:432715). + const uint C0 = 0x0001, C1 = 0x0002; + var c0 = Cell(C0, new CellPortalInfo((ushort)C1, 0, 0, 0)); + c0.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f)); + c0.VisibleCells = new uint[] { C0, C1 }; + var c1 = Cell(C1); // no portals, no exit + c1.VisibleCells = new uint[] { C0, C1 }; + var all = new Dictionary { [C0] = c0, [C1] = c1 }; + var frame = PortalVisibilityBuilder.Build( + c0, new Vector3(0, 0, -0.5f), id => all.TryGetValue(id, out var c) ? c : null, ViewProjAt(new Vector3(0, 0, -0.5f))); + Assert.True(frame.OutsideView.IsEmpty, + "a windowless interior must draw no terrain — over-include here would be a band-aid"); +} + +[Fact] // threshold stability: the exit cell stays present across a swept pose (no polys=0/1 flap) +public void Build_ThresholdSweep_OutsideViewStableNonEmpty() +{ + var (cam, all) = FlapChain(); + cam.VisibleCells = new uint[] { 0x0001, 0x0002, 0x0003 }; // PVS reaches the exit cell + Func lookup = id => all.TryGetValue(id, out var c) ? c : null; + // Sweep the camera across the C0->C1 boundary; OutsideView must never empty. + for (float z = -0.8f; z >= -1.2f; z -= 0.05f) + { + var eye = new Vector3(0, 0, z); + var frame = PortalVisibilityBuilder.Build(cam, eye, lookup, ViewProjAt(eye)); + Assert.False(frame.OutsideView.IsEmpty, $"flap at camera z={z}: OutsideView empty mid-sweep"); + } +} +``` + +> If the Task-3 fix grounds set membership on `cameraCell.VisibleCells`, the `FlapChain` fixture must set `cam.VisibleCells` (as above) for the grounding to engage. If Task 3 instead landed on the H2 side-test port (no PVS dependency for this fixture), these tests still pass; keep both — they pin the §5.3 invariants either way. + +- [ ] **Step 2: Run them** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~PortalVisibilityBuilderTests"` +Expected: ALL PASS. + +- [ ] **Step 3: Confirm the assembler is untouched & still correct** + +Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~ClipFrameAssemblerTests"` +Expected: ALL PASS — `ClipFrameAssembler` consumes the now-stable `PortalVisibilityFrame` unchanged (U.4c added no assembler changes). + +- [ ] **Step 4: Commit** + +```bash +git add tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs +git commit -m "test(render): Phase U.4c — seen_outside invariants (windowless empty, threshold stable) + +Pins spec 5.3: a sealed interior draws no terrain (no over-include band-aid); +a threshold cell keeps a stable non-empty OutsideView across a swept pose (no +polys=0/1 flap). Assembler tests confirm the consumer is unchanged. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 5: Visual gate (acceptance — STOP for visual verification) + +The real gate. Unit tests on synthetic data did not catch #103; the live scene is authoritative. + +**Files:** none (run + observe). + +- [ ] **Step 1: Build green** + +Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` + +- [ ] **Step 2: Launch with the visibility probe to a fresh UTF-16 log** + +PowerShell (per CLAUDE.md "Running the client"): +```powershell +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1"; $env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount"; $env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_PROBE_VIS = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "u4c-vis.log" +``` +Run in the background; give it ~8 s to reach in-world. + +- [ ] **Step 3: User crosses the threshold (visual verification)** + +Ask the user to walk `+Acdream` through a Holtburg cottage doorway — cellar → ground floor → outside — from several angles and zoom levels, watching for terrain / building-shell flicker at the threshold. **This is the acceptance test; only the user can confirm it.** + +- [ ] **Step 4: Read the probe log (the apparatus gate)** + +Read `u4c-vis.log` (UTF-16): PowerShell `Select-String "\[vis\]" u4c-vis.log` (or bash `tr -d '\0' < u4c-vis.log | grep '\[vis\]'`). +Expected: for the threshold cell, `OutsideView` stays **non-empty and narrowing** — NO `outside(polys=0)` frames interleaved with `outside(polys=1)` for the same cell. That, plus the user's "seamless" confirmation, is the gate. + +- [ ] **Step 5: On pass — update roadmap/issues + commit; decide push with the user** + +Update the M1.5 block + `docs/ISSUES.md` (the flap closed), commit the docs, and ask the user whether to push the branch. On fail — capture the `[vis]` divergence and return to Task 3 Step 1 with the new evidence (do NOT band-aid). + +--- + +## Task 6 (optional): cosmetic sweeps — only if trivial + +Take only if zero-risk and quick; otherwise defer. Each is behaviour-neutral with its own commit. +- `ClipFrameAssembler` / `ClipFrame.AppendSlot`: branch the 3 `Count==0` states (`IsNothingVisible` / `UseScissorFallback` / trivial) before calling `AppendSlot`, per the U.4 review note. +- Remove orphaned `LandblockEntriesWithoutAnimatedIndex`; remove dead `BuildingShellAnchorPass/Reject` counters. + +Gate: build + full App suite green; no behaviour change. + +--- + +## Self-review notes (author) + +- **Spec coverage:** §5.1 → Task 2; §5.2 → Task 3; §5.3 → Task 4; §7 unit gate → Tasks 1/3/4; §7 visual gate → Task 5; §8 staging maps 1:1 (U.4c-1≈Task1+Task3.Step1, U.4c-2≈Task2, U.4c-3≈Task3, U.4c-4≈Task4, U.4c-5≈Task5); §8 optional sweeps → Task 6. +- **Type consistency:** `VisibleCells` (`IReadOnlyList`) + `SeenOutside` (`bool`) used identically in Tasks 2/3/4; `CellView.IsEmpty`/`MinX`/`MaxX`/`Polygons`, `PortalVisibilityFrame.OutsideView`/`OrderedVisibleCells`/`CellViews`, `CellPortalInfo(OtherCellId, PolygonId, Flags, OtherPortalId)`, `PortalClipPlane{Normal,D,InsideSide}` all match the live source read during planning. +- **No-guess guard:** Task 3's algorithm body is explicitly a port from cited decomp lines, pseudocode-first — not fabricated here (CLAUDE.md no-guessing mandate). `EnvCellFlags` namespace is flagged for grep-confirmation, not guessed.