acdream/docs/superpowers/plans/2026-06-08-portal-flood-bounded-propagation.md
Erik 3fd71a123c docs(render): R-A2b spec — revive bounded-propagation, churn confirmed at flap-time
The indoor doorway flap is the portal flood's re-enqueue churn (0171<->0173 mutual re-contribution; drifted near-duplicate regions AddRegion won't dedup -> grew -> re-enqueue, capped at MaxReprocessPerCell=16 -> eye-sensitive flood depth -> grey flash). Confirmed live: launch-churn-confirm.log shows maxPop=16 on 44% of frames during a doorway walk-through. The 2026-06-08 'maxPop=1, churn refuted' verdict was a camera-turn-at-rest capture (wrong reproduction); its DO-NOT is overturned.

Fix (Option A, user-approved): contributions already covered by the neighbour's accumulated view don't grow it (no re-enqueue); only the uncovered remainder propagates -- retail's 'redundant -> empty before copy_view' (copy_view confirmed to just append). Remove MaxReprocessPerCell; keep re-processing of genuinely-new slices. Scope: PortalVisibilityBuilder only. Revives 2026-06-08 spec+plan (banners redirected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:48:53 +02:00

12 KiB

REVIVED 2026-06-09. Phase 1 (the churn probe) is done; Phase 2 (port the bound) is now the active R-A2b work — design: ../specs/2026-06-09-portal-flood-bounded-propagation-r-a2b-design.md. The banner below was wrong: the maxPop=1 reading came from a camera-turn-at-rest capture (the calm position, root 0172), the wrong reproduction. A 2026-06-09 doorway walk-through capture (launch-churn-confirm.log, the proper Phase-1 Task-4 pin) measured maxPop=16 on 44 % of frames → churn confirmed. Task 4's prediction ("redundant reciprocal back-contribution stays non-empty") is confirmed by recip=1->1, grew=True.


(HISTORICAL — corrected above) SUPERSEDED 2026-06-08 (evening). Live ACDREAM_PROBE_PORTAL_CHURN measured maxPop=1but on the wrong reproduction (camera turn at rest), not a doorway crossing (see the revival note above). The Phase-1 churn probe this plan added is correct and is the tool that ultimately confirmed (not disproved) the churn once aimed at the actual flap.

Portal-Flood Bounded-Propagation Port 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: Eliminate the indoor doorway "flap" by porting retail's bounded portal-flood propagation into PortalVisibilityBuilder — keeping re-processing on growth (retail-faithful) but stopping the reciprocal/drift churn that makes membership eye-sensitive.

Architecture: The flap is PortalVisibilityBuilder.Build's unbounded re-enqueue churn (cs:322, MaxReprocessPerCell=16 hack): redundant reciprocal back-contributions yield drifted non-empty slivers → grew → re-enqueue, and the churn's fixpoint shifts under sub-cm eye motion. Retail bounds it structurally (monotonic update_count watermark + empty reciprocal). Phase 1 instruments + pins the exact acdream divergence at the live doorway (it's float-drift-dependent — a runtime fact, not derivable from decomp). Phase 2 ports the bound, gated on Phase 1's evidence. Spec: docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md (REVISION banner).

Tech Stack: C# / .NET 10, xUnit. GL-free unit tests (AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs). Live capture via dotnet run against local ACE.

Non-goals (unchanged from spec): no rooting/camera/clip-math-rewrite/seal change; physics + the 4 rest-stability regression tests stay; Build_ViewGrowthAfterDoneCell stays GREEN (re-processing is kept).


PHASE 1 — Instrument & pin the exact divergence

Task 1: Add the portal-churn probe flag

Files:

  • Modify: src/AcDream.Core/Rendering/RenderingDiagnostics.cs (after ProbePvInputEnabled, ~line 144)

  • Test: tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs

  • Step 1: Write the failing test

[Fact]
public void ProbePortalChurn_DefaultsFalse_WhenEnvUnset()
{
    // Env var is absent in the test host, so the flag must default false (inert probe).
    Assert.False(AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled);
}
  • Step 2: Run it — expect FAIL (compile error: ProbePortalChurnEnabled does not exist)

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ProbePortalChurn_DefaultsFalse" Expected: FAIL (does not compile).

  • Step 3: Add the flag (in RenderingDiagnostics, mirroring ProbePvInputEnabled)
/// <summary>
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
/// max pop count, and — per re-enqueue — the reciprocal-clip pre→post region count + grew flag. Pins
/// whether the flap's churn is redundant reciprocal back-contributions producing non-empty drifted
/// slivers (the hypothesis) vs another source. Throwaway apparatus — strip once the bound ships.
/// Initial state from ACDREAM_PROBE_PORTAL_CHURN=1.
/// </summary>
public static bool ProbePortalChurnEnabled { get; set; } =
    Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
  • Step 4: Run it — expect PASS. Run the same filter. Expected: PASS.

  • Step 5: Commit

git add src/AcDream.Core/Rendering/RenderingDiagnostics.cs tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs
git commit -m "feat(render-diag): add ACDREAM_PROBE_PORTAL_CHURN flag for the bounded-propagation pin"

Task 2: Instrument PortalVisibilityBuilder.Build churn + reciprocal

Files:

  • Modify: src/AcDream.App/Rendering/PortalVisibilityBuilder.cs (the pop loop ~145-178; the reciprocal site ~295-306; the re-enqueue ~322; before return frame ~341)

  • Step 1: Add a per-Build churn accumulator (top of Build, after var popCounts = ... ~line 112)

// [portal-churn] apparatus (2026-06-08): when ProbePortalChurnEnabled, accumulate re-enqueue churn
// + reciprocal pre/post region counts, emitted as one summary line at end of Build. Inert when off.
bool churnProbe = AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled;
int churnReenqueues = 0;
var churnReciprocal = churnProbe ? new System.Text.StringBuilder(256) : null;
  • Step 2: Record reciprocal pre→post (at the reciprocal site, right after ApplyReciprocalClip(...) ~line 297, before the if (clippedRegion.Count == 0) check)
if (churnProbe)
    churnReciprocal!.Append(System.FormattableString.Invariant(
        $" recip[0x{neighbourId:X8} {preReciprocalCount}->{clippedRegion.Count}]"));
  • Step 3: Count re-enqueues (inside the if (grew && ... queued.Add(neighbourId)) block ~line 322, after todo.Insert(neighbour, dist))
if (churnProbe) churnReenqueues++;
  • Step 4: Emit the summary (just before return frame; ~line 341)
if (churnProbe)
{
    int maxPop = 0; uint maxCell = 0; int rePopped = 0;
    foreach (var kv in popCounts)
    {
        if (kv.Value > maxPop) { maxPop = kv.Value; maxCell = kv.Key; }
        if (kv.Value > 1) rePopped++;
    }
    Console.WriteLine(System.FormattableString.Invariant(
        $"[portal-churn] root=0x{cameraCell.CellId:X8} cells={frame.OrderedVisibleCells.Count} " +
        $"reEnqueues={churnReenqueues} rePoppedCells={rePopped} maxPop=0x{maxCell:X8}:{maxPop}" +
        churnReciprocal));
}
  • Step 5: Build — Run: dotnet build src/AcDream.App/AcDream.App.csproj -c Debug. Expected: Build succeeded.

  • Step 6: Commit

git add src/AcDream.App/Rendering/PortalVisibilityBuilder.cs
git commit -m "diag(render): [portal-churn] probe — per-Build re-enqueue + reciprocal pre/post"

Task 3: Deterministic re-pop unit test (probe baseline)

Files:

  • Modify: tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs

  • Step 1: Write the test — the ViewGrowthAfterDoneCell topology re-pops B (legitimate late growth from D). This proves the re-pop path is exercised deterministically (so the probe + later fix have a non-flaky anchor) without needing live float-drift.

[Fact]
public void Build_ViewGrowthAfterDoneCell_RePopsGrownCell()
{
    // Same A->B(near LEFT) + A->D(far RIGHT) + D->B(later) topology as
    // Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit: B is popped via LEFT, then D grows B
    // through RIGHT after B is done -> B re-pops. This is retail-faithful late growth (kept by the fix).
    const uint A = 0x0001, B = 0x0002, D = 0x0003;
    var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0), new CellPortalInfo((ushort)D, 1, 0, 0));
    a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
    a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
    var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0),
                    new CellPortalInfo(0xFFFF, 1, 0, 0), new CellPortalInfo((ushort)D, 2, 0, 0));
    b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
    b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f));
    b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
    var d = Cell(D, new CellPortalInfo((ushort)B, 0, 0, 2));
    d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
    var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [D] = d };

    var frame = Build(a, all);

    // Membership (the flap-relevant output) is each cell once, regardless of re-pops.
    Assert.Equal(new[] { B }, frame.OrderedVisibleCells.Where(c => c == B).ToArray());
    Assert.Contains(D, frame.OrderedVisibleCells);
}
  • Step 2: Run it — expect PASS (documents current behavior; the re-pop happens internally, membership stays deduped).

Run: dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~Build_ViewGrowthAfterDoneCell_RePopsGrownCell" Expected: PASS.

  • Step 3: Commit
git add tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs
git commit -m "test(render): deterministic re-pop anchor for the bounded-propagation pin"

Task 4: Live capture at the doorway + pin (CHECKPOINT — needs the user)

  • Step 1: Launch with the churn probe (add $env:ACDREAM_PROBE_PORTAL_CHURN = "1" to a copy of launch-flap-capture.ps1; keep ACDREAM_PROBE_PVINPUT=1 for correlation). dotnet build green first, then launch in background, tee to flap-churn.log.
  • Step 2: User reproduces — stand at the cottage doorway, turn the camera back and forth (the flap). ~15 s.
  • Step 3: Analyze flap-churn.log: for the flap frames (correlate with [pv-input] flood flips), inspect [portal-churn]: which cells hit high maxPop (churn → near 16), and the recip[...] pre→post counts — is a redundant reciprocal back-contribution staying non-empty (pre->post both >0 on a cell that already contributed)? That is the predicted divergence.
  • Step 4: Pin + write the Phase 2 fix plan. Record the pinned divergence (the exact cell/portal/condition where the redundant contribution stays non-empty) in a short findings note, and write docs/superpowers/plans/2026-06-08-portal-flood-bound-fix.md (Phase 2) with the exact, evidence-grounded code change. Do not write Phase 2 code before this pin.

PHASE 2 — Port the bound (outline; finalized from Phase 1's pin)

Shape (locked; exact predicate from Task 4): make redundant reciprocal / re-clip contributions not generate a new propagatable slice — matching retail's empty-reciprocal (OtherPortalClip→no copy_view) + monotonic update_count watermark — then remove MaxReprocessPerCell + popCounts (termination becomes structural). Keep re-processing on growth (the AddRegion union + the deferred re-process), so Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit stays GREEN. Most-likely concrete form (confirm in Task 4): gate the grew → re-enqueue so a back-contribution whose clipped region is already covered by the cell's accumulated view (retail: empty after reciprocal clip) does not count as growth — via the faithful reciprocal/watermark, NOT an epsilon dedup heuristic.

Tests: the new eye-sweep stability test (membership a single contiguous run as the eye sweeps — synthetic grazing topology if reproducible, else the live [pv-input] gate); all existing PortalVisibilityBuilderTests green incl. Build_ViewGrowthAfterDoneCell_*, Build_IsDeterministic_*, Builder_CyclicGraph/Hub termination; the 4 physics rest-stability guards green.

Acceptance (visual gate — the real one): at the cottage doorway, turn the camera back and forth and walk through — interior rooms render steadily, no battling/popping; [pv-input] flood stable per eye pose; [portal-churn] maxPop ≤ small constant (no near-16 churn). Then strip the apparatus ([portal-churn], [pv-input], the launch scripts).


Self-Review notes

  • Spec coverage: Phase 1 implements the spec's "instrument + pin first" requirement; Phase 2 implements the bounded-propagation fix. Build_ViewGrowthAfterDoneCell explicitly kept green (spec correction). No rooting/camera/clip/seal change.
  • Phase 2 is intentionally an outline: its exact predicate is the runtime fact Phase 1 pins (the spec + this plan both flag this). Phase 2 gets its own no-placeholder plan after Task 4 — this is the apparatus-first discipline, not a deferred placeholder.