acdream/docs/research/2026-05-26-a8-revert-handoff.md
Erik d2db8d5b22 docs: Phase A8 REVERT handoff — full session story + pickup prompt
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) <noreply@anthropic.com>
2026-05-26 09:42:53 +02:00

34 KiB

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 Original plan: docs/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


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. 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. 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<Vector3[]> 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)
2d31d49344034bf3d7b13 portal_stencil.vert/.frag shader pair dormant (no code loads them)
3973596a1c393e 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.csLoadedCell.PortalPolygons field populated by BuildLoadedCell. The data is ready; nothing reads it.
  • src/AcDream.App/Rendering/IndoorCellStencilPipeline.csPortalMeshBuilder.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.csEntitySet 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.csProbeVisibilityEnabled 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

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.csBuildingPortalGPU.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).