From d2db8d5b220122b73768022f8360378b0acc703e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 26 May 2026 09:42:53 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=20A8=20REVERT=20handoff=20?= =?UTF-8?q?=E2=80=94=20full=20session=20story=20+=20pickup=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the 3-round visual verification failure of the original A8 plan, the architectural taxonomy gap that surfaced (cottage walls are landblock-baked stabs with ParentCellId == null, not cell mesh, so the binary IndoorOnly/OutdoorOnly partition mis-classifies them), and what the re-plan must consider. Bottom line: the WB stencil approach is correct in principle and the infrastructure (Tasks 1-6: PortalPolygons field, RenderingDiagnostics flag, portal_stencil shaders, IndoorCellStencilPipeline, PortalMeshBuilder, EntitySet enum) is correct and tested. The integration (Task 7) made a wrong architectural assumption about entity classification. Reverted by fef6c61, 96f8bd2, c897a17. Includes detailed pickup prompt for the re-plan session: re-investigate entity taxonomy (6 distinct classes documented), spike distinguisher options (AABB-encloses-camera heuristic recommended for first ship), re-plan Task 7 with MarkAndPunch-first GL order + separate live-entity pass + 3-building visual verification requirement. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/research/2026-05-26-a8-revert-handoff.md | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 docs/research/2026-05-26-a8-revert-handoff.md diff --git a/docs/research/2026-05-26-a8-revert-handoff.md b/docs/research/2026-05-26-a8-revert-handoff.md new file mode 100644 index 0000000..7700f5e --- /dev/null +++ b/docs/research/2026-05-26-a8-revert-handoff.md @@ -0,0 +1,447 @@ +# Phase A8 — Indoor-cell visibility culling — REVERTED. Handoff for re-plan. + +**Date:** 2026-05-26 (session began 2026-05-25 PM, continued into 2026-05-26) +**Status:** Task 7 integration REVERTED after three rounds of visual verification surfaced cascading bugs. Infrastructure (Tasks 1-6) RETAINED — all dead-but-correct code, ready to be re-integrated under a different design. +**Branch:** `claude/strange-albattani-3fc83c` (worktree) +**Predecessor handoff:** [docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md](2026-05-25-issue-100-shipped-and-culling-handoff.md) +**Original plan:** [docs/superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md](../superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md) +**Investigation report (Phase 1):** [docs/research/2026-05-25-issue-78-visibility-culling-investigation.md](2026-05-25-issue-78-visibility-culling-investigation.md) + +--- + +## TL;DR + +We tried to close issue #78 (outdoor stabs visible through inn floor/walls) and the cellar-stairs grass-overlay artifact by porting WorldBuilder's stencil-based `RenderInsideOut` pipeline. The plan executed cleanly through 7 tasks (1029 lines of plan, 11 commits including hardening + fixes), and the H1 hypothesis from the investigation was correct — the cellar-stairs artifact IS a culling problem, NOT a Z-fight problem, confirmed by the camera-rotation falsification test. + +But the WB stencil approach has **architectural assumptions that don't match acdream's data model**, and three rounds of visual verification surfaced compounding bugs that aren't fixable by patching: + +1. **Round 1** (commit `41c2e67` initial integration) — character disappeared indoors. Root cause: player/NPCs have `ParentCellId == null`, got classified as outdoor scenery, stencil-gated to portal silhouettes only. +2. **Round 2** (commit `a2ad5c1` animated-entity fix) — character now visible, but closed doors leaked outside, walls between rooms showed far-side portal openings, character body bled to terrain where it overlapped a portal silhouette on screen. +3. **Round 3** (commit `b76f6d1` order swap — Mark+Punch BEFORE indoor draw, matching WB's actual order) — closed doors now correctly blocked, BUT cottage walls completely disappeared, character rendered head-inside-out, see-through everything. Root cause: cottage walls are **landblock-baked stabs** (`LandBlockInfo.Objects`) with `ParentCellId == null`, classified as outdoor scenery, stencil-gated → visible only at portal silhouettes (windows/doors). + +The integration commits `41c2e67`, `a2ad5c1`, `b76f6d1` are now reverted by `fef6c61`, `96f8bd2`, `c897a17`. Tasks 1-6 (infrastructure: `PortalPolygons` field, `RenderingDiagnostics.ProbeVisibilityEnabled`, portal_stencil shaders, `IndoorCellStencilPipeline`, `PortalMeshBuilder`, `WbDrawDispatcher.EntitySet` enum) remain committed and tested. They're dormant — nothing in the runtime invokes them — but they're correct, tested, and ready for a different integration design. + +**Current HEAD: `fef6c61`** — render frame back to pre-A8 behavior (terrain → depth-clear-if-inside → dispatcher with all entities). Build green, all infrastructure tests passing (26 dispatcher + 5 stencil-pipeline + 2 PortalPolygons data-class + 1 ProbeVisibilityEnabled toggle). + +The next session needs to **re-investigate the entity taxonomy** before re-planning the integration. The plan's binary `IndoorOnly vs OutdoorOnly` partition is wrong; AC's data model has at least four distinct entity classes that need different treatment. + +--- + +## What was tried (chronological) + +### Phase 1: Investigation (REPORT-ONLY, before any code) + +Dispatched four parallel research agents: +1. Retail decomp visibility chain (`PView::DrawCells`, `RenderInsideOut`, `CEnvCell::find_visible_child_cell`) +2. WorldBuilder `VisibilityManager.RenderInsideOut` reference implementation +3. acdream's existing visibility code (`CellVisibility`, `WbDrawDispatcher`, `TerrainModernRenderer`, render frame integration points) +4. ISSUES.md context for #78, #95, and the lighting family + +Findings consolidated in [`docs/research/2026-05-25-issue-78-visibility-culling-investigation.md`](2026-05-25-issue-78-visibility-culling-investigation.md). Two main outputs: +- Confirmed retail and WB use different mechanisms (retail = screen-space polygon-clip scissor, WB = stencil mask), but achieve the same observable behavior. WB's stencil approach is the right fit for acdream's modern GL pipeline. +- Three approach options sketched: A (WB stencil port), B (retail polygon-clip — multi-week), C (binary gate — workaround). + +User chose **Approach A** (WB stencil port). + +### Phase 1a: Falsification test (visual) + +User stood in a Holtburg cottage cellar at the artifact spot and rotated the camera in place. Reported: **no flickering around the edges.** This confirmed H1 (culling) over H2 (Z-fight). The artifact IS a rendered polygon that needs to be culled, not a depth-precision issue. + +### Phase 2: Plan written + +[`docs/superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md`](../superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md). 8 tasks, TDD-shaped where unit-testable. Architecture: split entities into `IndoorOnly` (`ParentCellId.HasValue`) and `OutdoorOnly` (`ParentCellId == null`); stencil-mark current building's exit portals; gate terrain + outdoor entities by `glStencilFunc(Equal, 1, 0x01)`. + +### Phase 3: Subagent-driven execution + +Tasks 1-7 implemented by Sonnet subagents, each with two-stage review (spec-compliance + code-quality). Task 4 was sent back once for over-engineering (the implementer added speculative `pos.w` clamp and `out FragColor` declarations not in the spec; subtractively reverted in commit `344034b`). Task 5 received a hardening pass (`a1c393e`) for explicit `Enable(DepthTest)`, `readonly` fields, and an `AllocateVbo` comment. + +All 7 implementation tasks shipped clean. Built green, ~36 unit tests added across tasks, all passing. + +### Phase 4: Visual verification — three rounds, three regressions + +**Round 1 — commit `41c2e67` (initial integration)** + +User scenarios: +- Cellar stairs: not visible from outside-to-in (but this turned out to be a NOT-A8 artifact — separate) +- Inn walls: solid (no see-through buildings) ✅ +- Character: **DISAPPEARED inside cottages** +- Character at doorway: only parts of body visible, **head rendered backwards** +- Flickering on enter/exit + +Diagnosis: animated entities (player, NPCs) have `ParentCellId == null` (server-spawned, not statically tied to a cell). EntitySet partition classified them as OutdoorOnly, so the stencil-gated outdoor pass only let them render where stencil bit 1 was set (= portal silhouettes). Walking around inside, character body crossed in and out of portal silhouettes → partial body visible briefly at doorways, head-on-backwards artifacts where stencil clipped one part of body but not another, fully invisible most of the time. + +**Round 2 — commit `a2ad5c1` (animated-entity fix)** + +Fix: `animatedEntityIds` overrides the partition. Animated entities go into `IndoorOnly` (stencil OFF), excluded from `OutdoorOnly`. + +User scenarios: +- Character: **VISIBLE** ✅ +- Closed door: **OUTSIDE STILL VISIBLE through closed door** ❌ +- Door from adjacent room: **VISIBLE THROUGH WALL** between rooms ❌ +- Character at door opening overlap: **outside bleeds through character body** where body covers the portal silhouette on screen ❌ + +Diagnosis: my plan had the GL state order WRONG. I had `IndoorOnly draw → MarkAndPunch → terrain stencil-gated`. The `MarkAndPunch` step writes `gl_FragDepth = 1.0` at all stencil-1 pixels, destroying any indoor depth that was just written there. Then terrain at 0.99 wins every depth test at portal-silhouette pixels. WB's actual order is `MarkAndPunch FIRST → indoor cells → terrain stencil-gated`. With WB's order, indoor cells write depth AFTER the punch, so their depth survives and correctly occludes the subsequent stencil-gated terrain pass. + +**Round 3 — commit `b76f6d1` (order swap to match WB)** + +Fix: swap `IndoorOnly draw` and `MarkAndPunch` so MarkAndPunch runs first. + +User scenarios: +- Closed door: **NOW BLOCKS OUTSIDE** ✅ +- Door from adjacent room through wall: **STILL VISIBLE** ❌ (worse than expected) +- Character at door: **TOTALLY BROKEN** — character rendered head-inside-out, see-through to distant outdoor objects through where walls should be ❌ + +Screenshot evidence: user stood on what appeared to be the upper floor of a Holtburg cottage. Visible in the frame: wood stairs/floor (indoor cell mesh), player character in armor, and a small window-shaped opening showing outdoor terrain (correct portal behavior). Beyond that: GREY expanse (clear color) with NPCs and decorations floating in space (= distant outdoor entities visible THROUGH where walls should be). + +Diagnosis (the showstopper): **cottage walls are landblock-baked stabs** stored in `LandBlockInfo.Objects`, NOT in the EnvCell's mesh or `StaticObjects`. They're `WorldEntity` instances with `ParentCellId == null`. The EntitySet partition treats them as outdoor scenery and stencil-gates them. Result: cottage interior walls only render at portal silhouettes — i.e., framed in the window openings. The rest of the wall area is just the cleared framebuffer (grey), with distant entities (which DO render unconditionally because they happen to be in screen positions not occluded by walls that don't exist) bleeding through. + +The head-inside-out artifact is a cascade — with the depth buffer state and framebuffer being so broken (walls absent, terrain stencil-gated in unexpected places, depth punched then partially overwritten by terrain), the character mesh rendering interacts with these broken depths in ways producing the impossible-anatomy effect. I don't have a single-call explanation; it's "the depth + stencil state is so far from sane that character vertex shader + fragment depth tests produce nonsense." + +### Phase 5: REVERT + +Decision: continuing to patch was going to keep surfacing edge cases. The fundamental issue (EntitySet partition by `ParentCellId.HasValue` is wrong) requires re-design, not patching. + +Three revert commits: +- `c897a17` reverts `b76f6d1` (order swap) +- `96f8bd2` reverts `a2ad5c1` (animated-entity fix) +- `fef6c61` reverts `41c2e67` (Task 7 integration) + +After reverts: HEAD = `fef6c61`. GameWindow.cs render frame is back to pre-A8 (terrain → depth-clear-if-inside → dispatcher with all entities). Build green. All infrastructure tests passing. + +--- + +## What was kept (the infrastructure) + +Six commits NOT reverted. All internally consistent, all tested, all dormant (nothing invokes them at runtime): + +| Commit | What it adds | Status | +|---|---|---| +| `fee878f` | `LoadedCell.PortalPolygons: List` field | dormant, tested | +| `d834188` | `BuildLoadedCell` populates `PortalPolygons` from `cellStruct.Polygons[portal.PolygonId].VertexIds` | dormant (data populated, nothing reads it) | +| `6577c0a` | `RenderingDiagnostics.ProbeVisibilityEnabled` flag + DebugVM mirror | dormant (no probe code uses it) | +| `2d31d49`→`344034b`→`f3d7b13` | `portal_stencil.vert/.frag` shader pair | dormant (no code loads them) | +| `3973596`→`a1c393e` | `IndoorCellStencilPipeline` class + `PortalMeshBuilder` static helper, with hardening | dormant, 5 unit tests pass | +| `dcf69a1` | `WbDrawDispatcher.EntitySet { All, IndoorOnly, OutdoorOnly }` enum + `set` parameter on `Draw` + `WalkEntitiesForTest` helper | dormant (`Draw` always called with default `EntitySet.All`), 26 dispatcher tests pass | + +These are all correct and useful. They don't need to be re-shipped in the re-plan — they're ready for a new integration to consume. + +**However**, the re-plan may want to reshape some of them: +- `EntitySet` enum's binary `IndoorOnly/OutdoorOnly` partition is the load-bearing wrong assumption. The re-plan likely needs more partition values (e.g. `IndoorStatic`, `BuildingShell`, `OutdoorScenery`, `LiveDynamic`) or a different mechanism entirely. The enum can be extended or replaced. +- `IndoorCellStencilPipeline` is correct as a primitive but its current usage assumption ("mark exit portals, gate outdoor passes") may need refinement. For example, it might want a "draw building-shell stabs unconditionally THEN stencil-gate outdoor scenery" split. + +--- + +## Root cause taxonomy (the architectural lesson) + +acdream's `WorldEntity` data model has more entity classes than the plan accounted for. The classes encountered: + +| Class | `ParentCellId` | Source | Examples | Stencil treatment needed | +|---|---|---|---|---| +| **Cell mesh** | set | `EnvCell` geometry | inn walls, dungeon corridor walls, cellar floor | Always render (unconditional) | +| **Cell statics** | set | `EnvCell.StaticObjects` | inn furniture, dungeon braziers | Always render (unconditional) | +| **Building shell stab** | **null** | `LandBlockInfo.Objects` | cottage walls/roof, smithy walls | Always render WHEN camera is inside the building | +| **Outdoor scenery stab** | null | `LandBlockInfo.Objects` | trees, fences, lampposts, rocks, hitching posts | Stencil-gate (only visible through portals from inside) | +| **Live animated** | null | server `CreateObject` + in `animatedEntityIds` | player, NPCs, monsters, doors mid-animation | Always render (unconditional) | +| **Live static** | null | server `CreateObject`, NOT animated | dropped items, sigils, idle doors after animation ends | Probably always render? Hard to say | + +The plan's binary `IndoorOnly = HasValue, OutdoorOnly = !HasValue` partition lumps "building shell stab" with "outdoor scenery stab" — both have null `ParentCellId`. But they need OPPOSITE stencil treatment. + +**The 3rd-round disaster came from this conflation specifically.** When camera is inside a cottage, the cottage's walls (building shell stab) need to render UNCONDITIONALLY (just like cell mesh would). My plan classified them with the trees and lampposts → stencil-gated → invisible. + +WB handles this via a "building" concept: `BuildingPortalGPU` tracks which `EnvCellIds` belong to each building, and the building's portal mesh + occlusion is treated separately from generic scenery. acdream doesn't have this concept — all landblock stabs go into the same `WorldEntity` pool with no "is-building-shell" flag. + +### Why Tasks 1-6 review missed this + +The spec / code review focused on: +- Spec compliance (did the implementation match the spec?) +- Code quality (well-structured, clean, etc.) + +Neither addressed: **is the spec's architectural premise correct?** The plan stated the partition as a binary based on `ParentCellId`, the reviewers verified the implementation followed that, but no one questioned whether the premise was right. Investigation (Phase 1) didn't catch it either — the audit focused on the EXISTING code paths and didn't go deep on the `WorldEntity` lifecycle / classification. + +This is the kind of issue where the plan's "self-review" step + investigation's "what we've ruled out" section should have included an entity-taxonomy audit. Future plans for rendering-pipeline changes should include: "List every kind of `WorldEntity` and what classification it gets, then verify the pipeline treats each correctly." + +### A second architectural issue (deferred but real) + +Even with the cottage-walls case solved, the WB stencil approach has a known limitation that the predecessor handoff already flagged: **all exit portals in `VisibleCellIds` get marked**, including portals on cells far from the camera. From inside a cottage, if the camera looks at a wall, the portals BEHIND the wall (on the other side of the room) ARE marked in stencil (their silhouettes project to screen positions covered by the wall). Then far-depth is punched at those positions. Then terrain stencil-gated wins over indoor wall depth → "outdoor visible through window on the other side of the room behind a wall." + +In Round 2 testing, this surfaced as "I can see the door of the adjacent room through the wall." It's a real geometric over-marking issue. + +WB handles it with a 3-stencil-bit pipeline ("Step 5" in WB's RenderInsideOut). My plan explicitly DEFERRED Step 5. With Round 2's order, the issue was masked because indoor wall depth was being destroyed by the late MarkAndPunch anyway, so the punch's far-depth happened to coincide with the bug. With Round 3's order, the punch happens before walls draw, so walls correctly write depth — but now the far-side-portal issue is unmasked. + +The re-plan needs to address Step 5 OR accept it as a documented limitation OR find a different mechanism (camera-frustum portal filtering, occlusion query for portals behind walls, etc.). + +--- + +## Things the re-plan must consider (the "do-not-miss" list) + +1. **Building shell stabs are NOT outdoor scenery.** They have `ParentCellId == null` but must render unconditionally when the camera is inside the building. The fix is one of: + - (a) **AABB-encloses-camera heuristic**: when an entity's `[AabbMin, AabbMax]` contains `cameraPos`, treat it as building shell. Quick to implement, ~30 min. Works for cottages and inns. Edge cases: very tall buildings with low camera, or buildings the camera isn't quite inside. + - (b) **Tag building stabs at hydration time**: when reading `LandBlockInfo.Objects`, identify objects that have associated `EnvCellIds` (i.e., they're the building shell of those cells). Add `WorldEntity.IsBuildingShell: bool` (or similar). Correct, but requires understanding LandBlockInfo's structure. + - (c) **WB-style building concept**: full `BuildingPortalGPU.EnvCellIds` model. Heavy lift; probably overkill for first ship. + - **Recommended: (a) for first ship, (b) as a follow-up if the heuristic has misses.** + +2. **Live entities (player, NPCs, dropped items) need a "always render" path.** Today's `animatedEntityIds` covers the animated subset. Dropped items / idle doors are NOT animated but ARE live. The cleanest model: add `WorldEntity.IsLiveDynamic` flag set at hydration when the entity has a `ServerGuid` (vs landblock-baked). All live entities skip stencil-gating entirely. + +3. **GL state order matters: MarkAndPunch BEFORE indoor cells.** Confirmed by Round 3. The far-depth punch must run before indoor geometry draws, so indoor geometry writes depth on top of the 1.0 punch and correctly occludes the subsequent stencil-gated terrain. The Plan had the order wrong; the order-swap (Round 3) is the correct order. Re-plan must reflect this. + +4. **Animated/live entities should draw AFTER all stencil work**, with stencil disabled, so their depth never interacts with the punch or the stencil-gated pass. Round 2 showed character body bleeding to terrain when drawn BEFORE the punch (depth destroyed by punch). Drawing them last fixes this naturally. + +5. **The "far-side portal visible through wall" problem (WB Step 5)** is real and won't be fixed by the order swap alone. Either implement Step 5 (complex), accept it as a known limitation for first ship, or add a camera-frustum filter on portal triangles (only stencil-mark portals the camera could plausibly see directly). + +6. **Cellar-stairs grass artifact from outside-to-in is NOT A8 scope.** This was reported by the user in Round 1 and persisted across all rounds. From outside, no stencil work runs; the artifact is purely terrain-Z-fight against the cellar geometry. The cellar floor is meters below terrain Z; #100's 1cm shader nudge doesn't help. File as a separate issue OR roll into a future "deep-cell terrain occlusion" phase. + +7. **Closed doors must block outdoor visibility.** The Round 3 order successfully delivers this — door entities (`ParentCellId == null` but inside the building's AABB) need to draw in the indoor pass AFTER MarkAndPunch, so their depth wins over terrain. Doors actually map cleanly to the building-shell stab solution: a door is functionally part of the building when closed. + +8. **The `EntitySet` enum may need refactoring or replacement.** Today it has `All, IndoorOnly, OutdoorOnly`. The taxonomy suggests at least: + - `All` (pre-A8 default) + - `IndoorPass` — cell mesh + cell statics + building shell stabs + live entities (essentially everything that should draw unconditionally when inside) + - `OutdoorPass` — outdoor scenery only, stencil-gated + - `LivePass` (optional) — separate pass for live entities at the very end, no stencil + + Or replace the enum with a callback / filter delegate. The current enum is a quick prototype; the production design should reflect the actual taxonomy. + +9. **Visual verification scenarios must cover MORE buildings.** Round 1 tested at the inn (which has cell mesh walls); Round 3 tested at a cottage (which has stab walls). Different bugs surfaced. The re-plan's Task 8 must explicitly test: inn, cottage (interior + cellar), dungeon (portal-entry), and at least one mid-size building with multiple rooms. Each likely has a different geometry classification mix. + +10. **The flickering on enter/exit** reported across all rounds is unexplained. Likely the `CellSwitchGraceFrameCount = 3` interacting with stencil setup timing — when camera transits a cell boundary, the visibility result toggles between cells over the grace frames, and the stencil mask flips with it. Investigate during re-plan. + +--- + +## Existing apparatus the next session inherits + +### Code (committed, dormant) + +- **`src/AcDream.App/Rendering/CellVisibility.cs`** — `LoadedCell.PortalPolygons` field populated by `BuildLoadedCell`. The data is ready; nothing reads it. +- **`src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs`** — `PortalMeshBuilder.BuildTriangles(...)` (pure-math, tested) + `IndoorCellStencilPipeline` (GL class, untested at runtime but the GL state machine has been reviewed twice). The `MarkAndPunch` GL sequence is correct per WB; the cleanup state is correct for either pre- or post-indoor-draw scheduling. Re-usable as-is. +- **`src/AcDream.App/Rendering/Shaders/portal_stencil.vert/.frag`** — minimal MVP + `gl_FragDepth = 1.0` writer. Re-usable. +- **`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`** — `EntitySet` enum + `WalkEntitiesForTest` helper + partition logic in `WalkEntitiesInto`. **The current partition is the load-bearing wrong assumption.** Re-plan likely modifies this. +- **`src/AcDream.Core/Rendering/RenderingDiagnostics.cs`** — `ProbeVisibilityEnabled` flag. Re-usable. + +### Tests (passing) + +- `tests/AcDream.App.Tests/Rendering/CellVisibilityPortalPolygonsTests.cs` — 2 tests, data-class invariants +- `tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs` — 5 tests, triangle-fan math +- `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs` — 3 tests, EntitySet partition +- `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsVisibilityTests.cs` — 1 test, flag toggle + +### Documents + +- The original plan: [`docs/superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md`](../superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md) — read for Task 1-6 implementation reference; **do NOT re-execute Task 7** as written. +- The investigation report: [`docs/research/2026-05-25-issue-78-visibility-culling-investigation.md`](2026-05-25-issue-78-visibility-culling-investigation.md) — the H1 confirmation + WB/retail/acdream code anchors still apply. +- The original handoff: [`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](2026-05-25-issue-100-shipped-and-culling-handoff.md) — the family map (#78 + cellar-stairs + #95) is unchanged. + +### Reference anchors (still valid) + +- **WB stencil:** `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239` (RenderInsideOut). **Note: WB's order is MarkAndPunch FIRST, then indoor cells — confirmed by Round 3.** +- **WB building concept:** `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs` — `BuildingPortalGPU.EnvCellIds` is the "this stab belongs to a building" association we're missing. +- **Retail:** `acclient_2013_pseudo_c.txt:432709` (`PView::DrawCells`, `outside_view.view_count > 0` gate). Polygon-clip scissor, not stencil — equivalent observable behavior. + +### Issue state + +- **#78** — still OPEN. Not fixed by A8 attempt. +- **Cellar-stairs artifact (NEW evidence for #78)** — still happening from outside-to-in (NOT A8 scope) AND from inside (was A8 scope; not fixed). +- **#95** (portal-graph blowup at network hubs) — out of scope, separate work. +- **#79/#80/#81/#93/#94** (indoor lighting family) — unchanged. + +--- + +## Pickup prompt for the next session + +``` +Phase A8 — Indoor-cell visibility culling — RE-PLAN after revert. + +Read first (in this order — REQUIRED): + 1. docs/research/2026-05-26-a8-revert-handoff.md (this doc — full + story of the 3-round visual verification failure + reverts) + 2. docs/superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md + (original plan — reference Tasks 1-6 implementation; do NOT + re-execute Task 7 as written) + 3. docs/research/2026-05-25-issue-78-visibility-culling-investigation.md + (original investigation; H1 culling diagnosis is confirmed) + 4. CLAUDE.md — find "currently working toward" to refresh state + +State both altitudes: + Currently working toward: M1.5 — Indoor world feels right + Current phase: A8 — Indoor-cell visibility culling RE-PLAN + Previous attempt at HEAD: fef6c61 (reverts of 41c2e67, a2ad5c1, b76f6d1) + Infrastructure preserved: Tasks 1-6 commits (fee878f → dcf69a1 + a1c393e) + Test baseline: build green; 36 A8-infrastructure tests pass dormant + +Session flow: + +### Phase 1 — RE-INVESTIGATE the entity taxonomy (USE /investigate skill) + +DO NOT skip to planning. The original plan's binary IndoorOnly/OutdoorOnly +partition was the load-bearing wrong assumption. Before any new plan: + + a. Read src/AcDream.App/Rendering/GameWindow.cs around the entity + hydration paths (BuildInteriorEntitiesForStreaming around line + 5409+, and the LandBlockInfo.Objects iteration). Document every + code path that constructs a WorldEntity and what ParentCellId it + gets. + + b. Enumerate the actual entity classes that exist in acdream's runtime: + - Cell mesh (ParentCellId set, from EnvCell) + - Cell statics (ParentCellId set, from EnvCell.StaticObjects) + - Building shell stab (ParentCellId == null, from LandBlockInfo.Objects, + represents inn walls / cottage walls / etc) + - Outdoor scenery stab (ParentCellId == null, from LandBlockInfo.Objects, + represents trees / fences / lampposts) + - Live animated (ParentCellId == null, server-spawned, in + animatedEntityIds — player, NPCs, monsters, mid-animation doors) + - Live static (ParentCellId == null, server-spawned, NOT animated — + dropped items, idle doors after animation ends, sigils) + + c. For each class, determine: how can the renderer distinguish it from + the other null-ParentCellId classes? Today only animatedEntityIds + separates one class. The re-plan needs distinguishers for the others. + Options: + - WorldEntity.IsBuildingShell (set at LandBlockInfo hydration) + - WorldEntity.IsLiveDynamic (set when ServerGuid != 0) + - AABB-encloses-camera heuristic (runtime, no new field) + - WB-style building association (per-cell building registry) + Spike each option's cost + correctness. + + d. Read WB's reference to confirm how WB handles each class. + references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs + has the BuildingPortalGPU.EnvCellIds association we're missing. + references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/StaticObjectRenderManager.cs + might show how outdoor scenery is treated separately from buildings. + + e. Decide the entity-distinguisher approach. The cheapest option that + handles cottages + inns + dungeons is likely AABB-encloses-camera + for building shells, animatedEntityIds for live animated, and + accept "dropped items invisible from inside" as a known limitation + for first ship (defer to a follow-up task with a real IsLiveDynamic + flag). + + f. Re-confirm the GL state order: MarkAndPunch FIRST, then indoor + cells (including building shells + live animated), then terrain + stencil-gated, then outdoor scenery stencil-gated. Confirmed by + Round 3 of the original A8 attempt. + + g. Decide whether to address the "far-side portal visible through wall" + issue (WB Step 5 territory) in this phase or defer. The simplest + ship-now approximation: only stencil-mark portals on the CAMERA'S + OWN CELL (not BFS-extended VisibleCellIds). This restricts stencil + to portals directly adjacent to the camera. Loses cross-cell-portal + visibility (probably acceptable for first ship). + +Phase 1 output: a short report (<400 tokens chat, full doc to +docs/research/YYYY-MM-DD-a8-entity-taxonomy.md). Plus a fix-shape +sketch covering all 6 entity classes. Get user approval before Phase 2. + +### Phase 2 — Re-PLAN Task 7 (USE superpowers:writing-plans skill) + +The re-plan replaces Task 7 (and may reshape `EntitySet` enum semantics +or add new partition values). Expected shape: + + - Task R1: Add the entity distinguisher (e.g. AABB-encloses-camera + helper on WbDrawDispatcher, or new WorldEntity.IsBuildingShell field + if going that route). + - Task R2: Update WbDrawDispatcher.EntitySet partition to use the + distinguisher. May rename enum values to reflect new taxonomy. + Update unit tests. + - Task R3: Add a third dispatcher call for live entities AFTER the + stencil work. Either new EntitySet value or a flag parameter. + - Task R4: Re-wire GameWindow render frame with MarkAndPunch FIRST + order. Three (or four) dispatcher calls when inside: + 1. Indoor pass (cell mesh + cell statics + building shell stabs) + 2. MarkAndPunch + 3. Terrain stencil-gated + 4. Outdoor scenery pass (stencil-gated) + 5. Live entity pass (no stencil, AFTER everything) + - Task R5: Visual verification — MUST test at cottage interior + cellar, + inn interior, AND a dungeon (portal-entry). Each likely surfaces + different bugs. + - Task R6: Ship docs (close #78, update CLAUDE.md A8 paragraph) — + only if all three visual scenarios pass clean. + +The infrastructure from Tasks 1-6 is ready. The re-plan only needs to +ship the new integration. Keep tasks small (TDD-shaped where possible); +the GL integration tasks are visual-verification-only by nature. + +### Phase 3 — Implement (USE superpowers:subagent-driven-development) + +Same pattern as original. Fresh Sonnet subagent per task with two-stage +review. CRITICAL: spec reviewer must ALSO check "does the spec's +architectural premise match the actual entity taxonomy?" Don't repeat +the original's mistake of reviewing implementation without questioning +the spec's foundational assumption. + +## Constraints + +- Per CLAUDE.md "no workarounds" rule — fix the root cause, do not + patch symptom sites. The re-plan IS the root-cause fix for the + taxonomy issue; Round 1-3 patches were band-aids that didn't address + the underlying classification gap. +- Visual verification is the acceptance test. Test at AT LEAST THREE + building types (cottage, inn, dungeon) before declaring success. +- The cellar-stairs grass artifact FROM OUTSIDE is NOT A8 scope (no + stencil work happens when camera is outside). File as a separate + issue if not already filed, with a note that it's a deep-cell + terrain Z-fight (not solvable by #100's 1cm nudge). +- The "far-side portal visible through wall" issue may be addressed + in this phase or deferred to A8.P2. Decide explicitly during Phase 1. +- DON'T re-revert the infrastructure. Tasks 1-6 commits are kept + intentionally; the re-plan consumes them. The only thing being + re-shipped is the integration design. + +## What success looks like + +After this re-plan ships: + - Standing inside a Holtburg cottage (any room), all walls are + SOLID — no see-through to outdoor objects, no see-through to + adjacent rooms. + - Standing inside Holtburg Inn, same. No outdoor stabs through + walls/floor (#78's primary acceptance). + - Standing in cottage cellar, no grass overlay on stair geometry + (the cellar-stairs in-to-out half of the artifact; the + out-to-in half is separate). + - Player character + NPCs are FULLY VISIBLE indoors at all camera + angles. No partial body, no head-backwards, no flickering on + enter/exit (or document any residual flickering as a known + issue). + - Closed doors BLOCK outdoor visibility. Open doors SHOW outdoor + through the opening, occluded properly by surrounding wall. + - No regression on issue #100 (no transparent rectangles around + cottages). + - dotnet build green; dotnet test failures within the documented + 14-23 flaky window. + +## Reference repo hierarchy reminder + +Per CLAUDE.md "Reference repos: cross-check the relevant ones" — +for the entity taxonomy / building shell question: + - WB's PortalRenderManager + StaticObjectRenderManager (how WB + splits buildings from outdoor scenery) + - WB's VisibilityManager (the proven stencil pipeline with the + correct GL state order) + - Retail decomp for CLandBlock::init_buildings (the data-model + source — how retail tags building objects vs. scenery) + - ACE (server) has minimal coverage here — buildings are + client-side decoration + +Cross-reference WB + retail. The acdream-specific question is HOW +acdream's WorldEntity model can express the building-vs-scenery +distinction. +``` + +--- + +## Files state at session end + +``` +Branch: claude/strange-albattani-3fc83c +HEAD: fef6c61 Revert "feat(render): Phase A8 — wire stencil pipeline into render frame" +Parent: 96f8bd2 Revert "fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass" +Grandparent: c897a17 Revert "fix(render): Phase A8 — mark-and-punch BEFORE indoor draw (correct WB order)" +Before reverts: b76f6d1 fix(render): Phase A8 — mark-and-punch BEFORE indoor draw +Infrastructure base: dcf69a1 → a1c393e → 3973596 → 344034b/f3d7b13 → 2d31d49 → 6577c0a → d834188 → fee878f + +Working tree: clean +Build: green (0 warnings, 0 errors) +Tests: 26 WbDrawDispatcher + 5 IndoorCellStencilPipeline + 2 PortalPolygons + 1 ProbeVisibility = 34 A8 infrastructure tests passing +Untracked: launch-a8-verify*.log (session logs, can be deleted) +``` + +The reverts are NEW commits (not destructive history rewrites — original commits remain in history for evidence). The re-plan can `git log b76f6d1..fef6c61` to see exactly what was reverted, or `git diff dcf69a1..fef6c61` to see the net effect on the codebase (should be: only test file is at slightly different state; everything else from Tasks 1-6 is in place).