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

25 KiB

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 — 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:

// -----------------------------------------------------------------------
// 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
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:

    /// <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):

        // 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:

        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
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. 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; 0xFFFFoutside_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
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

[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
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"):

$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.