acdream/docs/superpowers/plans/2026-05-31-phase-u4c-stabilize-portal-visibility.md
Erik fdeede8796 docs(render): Phase U.4c — annotate Task 3 with U.4c-1 evidence (H2 selected)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:14:22 +02:00

424 lines
25 KiB
Markdown

# 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: `<type>(render): Phase U.4c — <what>` + the trailer:
`Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`
---
## 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<uint, LoadedCell> 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<uint, LoadedCell> { [C0] = c0, [C1] = c1, [C2] = c2 };
return (c0, all);
}
[Fact]
public void Build_NearBoundaryIntermediatePortal_ExitCellStaysVisibleAcrossPose()
{
var (cam, all) = FlapChain();
Func<uint, LoadedCell?> 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) <noreply@anthropic.com>"
```
---
## 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
/// <summary>
/// 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).
/// </summary>
public IReadOnlyList<uint> VisibleCells = System.Array.Empty<uint>();
/// <summary>
/// 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.
/// </summary>
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<uint>();
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) <noreply@anthropic.com>"
```
---
## Task 3: Layer 2 — port retail InitCell `portal_side` sidedness (the fix; oracle-ported)
> **EVIDENCE UPDATE (2026-05-31, Task U.4c-1 ran):** the flap was characterized on real dat data —
> see [`docs/research/2026-05-31-u4c-flap-characterization.md`](../../research/2026-05-31-u4c-flap-characterization.md).
> The flap is a **direct `0xA9B40171→0xA9B40170` portal** side-test flip (the window cell is a
> direct neighbour, NOT multi-hop), and the evidence **selected H2 over H1**: our
> `CameraOnInteriorSide` derives the sidedness *sense* from the cell centroid, anti-correlated with
> the dat's authored per-portal `PortalSide` flag that retail's `InitCell` uses. The fix is to port
> the side test to use the dat `PortalSide` (in `portal.Flags`), not centroid-`InsideSide`. The
> stab_list PVS grounding (Layer 1 data, already plumbed in Task 2) stays as a correct improvement
> but is NOT the flap mechanism. The Task-1 synthetic test models the mechanism via a multi-hop
> `InsideSide` cull and must be re-authored to model the direct-portal `PortalSide`-vs-centroid
> divergence so it is a clean gate for the actual fix.
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) <noreply@anthropic.com>"
```
---
## 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<uint, LoadedCell> { [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<uint, LoadedCell?> 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) <noreply@anthropic.com>"
```
---
## 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<uint>`) + `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.