acdream/docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md
Erik ea60d1fb7d docs(spec): Phase A8 — full WB RenderInsideOut + RenderOutsideIn port (design)
Re-brainstormed after RR0 falsification showed R3+R3.5 introduced
Issues A+C (rendering all 16 BFS-reachable cells at full screen extent
caused co-planar Z-fight + grace-state leak from outside view). The
prior design's "WB-faithful restructure" was insufficient — it kept
the BFS-wide cell rendering. Retail and WB both solve indoor visibility
with per-portal recursive culling.

This design ports WB's full pipeline:
  - RenderInsideOut Steps 1-5 (including 3-stencil-bit cross-building)
  - RenderOutsideIn (cottage interiors visible through windows from outside)
  - Per-building cell association (Building + BuildingRegistry, plus
    LoadedCell.BuildingId for O(1) cell→building lookups)
  - Single strict cameraInsideBuilding gate (no grace for render path)
  - Stencil-gated sky inside indoor branch (acdream enhancement)

12 tasks (RR1-RR12), 8-10 sessions estimated. M1.5 indoor scope ships fully.

Supersedes:
  docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md
  docs/superpowers/plans/2026-05-26-phase-a8-restructure.md
(both will be footer-marked in RR1 cleanup)

Reverts in RR1: R3 (60f07bc), R3.5 v1 (38d5374), R3.5 v2 (2bfeafd).
R1+R2 (data layer + dispatcher partition) stay — orthogonal infrastructure.

RR2 spike resolves the BuildingInfo data shape + interior-portal walk
algorithm against WB PortalRenderManager:518-551 before RR3 implements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:29:14 +02:00

29 KiB
Raw Blame History

Phase A8 — Full WorldBuilder RenderInsideOut + RenderOutsideIn port (design)

Date: 2026-05-26 (PM, post-RR0 re-brainstorm) Phase: A8 — Indoor-cell visibility culling — SCOPE EXPANSION after RR0 falsified the prior design's "stencil-mask only" assumption. Status: Design approved 2026-05-26. Ready for superpowers:writing-plans. Branch: claude/strange-albattani-3fc83c (worktree) Supersedes: docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md (footer-marked) and docs/superpowers/plans/2026-05-26-phase-a8-restructure.md (footer-marked).

Required predecessor reading:


TL;DR

The prior design (2026-05-26-phase-a8-restructure-design.md) assumed Issues A and C from R4 visual verification were either pre-existing or fixable by removing a depth-clear workaround. The RR0 falsification spike disproved both assumptions: A and C are caused by R3's stencil pipeline wire-in — specifically by rendering 16 cells of a Holtburg cottage cluster simultaneously at full screen extent. R3 implements WB's Step 4 (stencil-gate outdoor) but skips WB's Step 3 architecture (render only the camera-building's cells, not BFS-extended).

The fix is to port WB's full RenderInsideOut and RenderOutsideIn with per-building cell scoping, including Step 5 (3-stencil-bit cross-building visibility). This requires:

  1. New data model: Building data class + BuildingRegistry per landblock + LoadedCell.BuildingId field (Option C of the data-model brainstorm — both directions indexed for O(1) lookups).
  2. New code path: BuildingLoader walks LandBlockInfo.Buildings at landblock load time, computes per-building cell sets via the portal graph.
  3. Render-frame restructure: single strict cameraInsideBuilding gate (replaces both cameraInsideCell lenient and cameraReallyInside strict); IndoorPass walks the camera-building's cells (not full BFS); stencil-gated sky for windows; Step 5 for cross-building visibility; RenderOutsideIn for outdoor camera looking into cottage windows.
  4. Revert R3, R3.5 v1, R3.5 v2 (the half-port). Keep R1 (IsBuildingShell flag) and R2 (EntitySet partition) — they remain useful infrastructure.

Twelve tasks (RR1RR12), ~8-10 sessions (1.5-2 weeks calendar). M1.5 indoor-world acceptance scope ships fully.


Why the scope expanded

Per RR0 evidence:

Branch Issue C Issue A #78
HEAD (R3.5 v2) YES YES (fixed by R3)
R3 baseline (60f07bc) YES YES (fixed by R3)
main (7034be9, no A8) NO NO flicker YES, constant

R3's stencil-mask approach fixes #78 by stencil-gating outdoor visibility — but the underlying "render all 16 BFS-reachable cells at full screen" is structurally wrong. Co-planar cell meshes (cottage floor + cellar ceiling, both with +0.02m EnvCell render lift) Z-fight. During exit-grace, the same 16-cell pool leaks from outside view. R3 is half a WB port. Retail and WB both solve this with per-portal recursive culling (retail via polygon-clip scissor, WB via per-building stencil scoping).

The "minimum viable indoor visibility" turns out to be larger than the prior design assumed. We are committing to the full port now rather than iterating through more half-fixes.


Brainstorm outcomes

# Question Decision
Q1 Scope: include WB Step 5 (cross-building visibility)? Yes — full RenderInsideOut including Step 5
Q2 Data model for per-building cell association? Option C — Building registry + cell-side BuildingId field (both directions O(1))
Q3 Outdoor→indoor visibility (RenderOutsideIn)? Yes — include in this phase
(implicit) What about R3 + R3.5 v1 + R3.5 v2? Revert (3 commits); R1+R2 stay
(implicit) Grace mechanism for render path? Drop in render frame (use strict cameraInsideBuilding); grace stays alive in CellVisibility for non-render consumers

Architecture

One new gate flag (replaces the two-flag asymmetry)

bool cameraInsideBuilding = visibility?.CameraCell is not null
    && CellVisibility.PointInCell(camPos, visibility.CameraCell)
    && visibility.CameraCell.BuildingId is not null;

Strict (PointInCell), no grace, AND requires the camera's cell to actually be a building cell. A cell with BuildingId == null is an outdoor cell or dungeon cell not tagged by LandBlockInfo.Buildings; those flow through the outdoor render path.

Drives: sky pre-scene, initial terrain, stencil-pipeline branch entry, weather post-scene, sky-PES debug. playerInsideCell (used for lighting in third-person chase mode) stays separate — different semantics (player position, not camera).

Two render paths

cameraInsideBuilding == false — outdoor path (matches WB RenderOutsideIn):

  1. Sky drawn normally
  2. Terrain drawn normally
  3. RenderOutsideIn: for each visible building in the frustum (per BuildingRegistry.All() filtered by frustum):
    • Stencil-mark its exit portals (single-bit stencil 1)
    • Render its EnvCellIds through stencil (visible only at portal silhouettes)
    • Use occlusion query (prev-frame result) to skip buildings whose portals weren't visible last frame
  4. Draw(set: All) outdoor entities (existing pre-A8 behavior)
  5. Weather

cameraInsideBuilding == true — indoor path (matches WB RenderInsideOut):

  1. Skip initial sky (gated on !cameraInsideBuilding)
  2. Skip initial terrain (gated on !cameraInsideBuilding)
  3. No depth-clear (the old depth-clear-if-inside block is deleted)
  4. MarkAndPunch — stencil bit 1 + far-depth at the camera-buildings' exit portals (lookup via registry.GetBuildingsContainingCell(visibility.CameraCell.CellId))
  5. IndoorPass — render the camera-buildings' cells via WbDrawDispatcher.Draw(cellIds: camBuildings.SelectMany(b => b.EnvCellIds)). Stencil off, DepthFunc.Less. This is the structural fix.
  6. EnableOutdoorPass — stencil read Equal(1, 0x01)
  7. Stencil-gated sky_skyRenderer.RenderSky with DepthMask off (acdream enhancement; closes the "fog haze through windows" complaint)
  8. Stencil-gated terrain re-draw
  9. Stencil-gated OutdoorScenery
  10. Step 5 — for each "other building" (visible per frustum but NOT containing camera):
    • Read prev-frame occlusion query result
    • Begin new occlusion query
    • Mark stencil bit 2 at this other-building's portals (StencilFunc.Equal(3, 0x01), StencilMask(0x02)) — bit 2 set only where bit 1 already set
    • End occlusion query
    • Clear depth where bits 1+2 set (re-draw portal mesh with DepthFunc.Always, StencilFunc.Equal(3, 0x03))
    • Render this other-building's EnvCellIds through stencil == 3
    • Reset bit 2 for the next iteration
  11. DisableStencil
  12. LiveDynamic — player + NPCs + dropped items, depth-test only

Data model

Building (new class)

src/AcDream.App/Rendering/Wb/Building.cs

namespace AcDream.App.Rendering.Wb;

public sealed class Building
{
    public required uint BuildingId { get; init; }
    public required HashSet<uint> EnvCellIds { get; init; }
    public required IReadOnlyList<Vector3[]> ExitPortalPolygons { get; init; }

    // Step 5 occlusion-query state (mutable, per-frame):
    public uint QueryId;       // GL query id, lazily created
    public bool QueryStarted;  // true after first BeginQuery
    public bool WasVisible;    // result from prev-frame query; drives whether to render this frame
}

BuildingRegistry (new class, per-landblock)

src/AcDream.App/Rendering/Wb/BuildingRegistry.cs

public sealed class BuildingRegistry
{
    // Index 1: cell-id → list of buildings containing that cell.
    // Returns a list because a cell may be shared between buildings (rare; matches WB's API shape).
    private readonly Dictionary<uint, List<Building>> _byCellId = new();

    // Index 2: building-id → Building.
    private readonly Dictionary<uint, Building> _byBuildingId = new();

    public void Add(Building b) { /* keep both indexes in sync */ }

    public IReadOnlyList<Building> GetBuildingsContainingCell(uint cellId) =>
        _byCellId.TryGetValue(cellId, out var list) ? list : Array.Empty<Building>();

    public Building? GetById(uint buildingId) =>
        _byBuildingId.TryGetValue(buildingId, out var b) ? b : null;

    public IEnumerable<Building> All() => _byBuildingId.Values;

    public int Count => _byBuildingId.Count;
}

LoadedCell.BuildingId (extension)

src/AcDream.App/Rendering/CellVisibility.cs

public sealed class LoadedCell
{
    // ... existing fields ...

    /// <summary>
    /// 2026-05-26 (Phase A8): the building this cell belongs to, if any.
    /// Set at landblock load time by <see cref="BuildingLoader"/> from
    /// <c>LandBlockInfo.Buildings</c>. Null when the cell isn't part of any
    /// building (outdoor surface cells, or dungeon cells not enumerated
    /// in Buildings).
    /// </summary>
    public uint? BuildingId { get; init; }
}

BuildingLoader (new static)

src/AcDream.App/Rendering/Wb/BuildingLoader.cs

Pure factory. Inputs: LandBlockInfo + an existing dictionary IReadOnlyDictionary<uint, LoadedCell> cellsByCellId. Outputs: a BuildingRegistry.

Algorithm:

  1. For each BuildingInfo in info.Buildings: a. Allocate a sequential BuildingId. b. Walk its Portals (list of CBldPortal-equivalent items in DatReaderWriter). Each portal points to one indoor cell (OtherCellId). c. Build the building's EnvCellIds set by adding each portal's OtherCellId AND any cells reachable from those via interior portals only (no exit portals — those connect to outdoor). Walk the portal graph BFS-style; mark visited; stop when no new cells found OR hitting an exit portal. d. Build the building's ExitPortalPolygons by collecting portal polygons from each cell whose OtherCellId == 0xFFFF (existing PortalMeshBuilder.BuildTriangles logic, but per-building scoped).
  2. Return the registry.

Open question RR2 resolves: does WB's per-building cell set include ALL cells reachable from the building's entry portals (via any interior portal), or only the cells DIRECTLY in BuildingInfo.Portals? This needs verifying against WB's code (references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:518-551). The algorithm above assumes "all interior-reachable cells" which is the conservative/correct interpretation; RR2's spike confirms before RR3 implements.

Cell-side mutation

LoadedCell is a sealed class with init properties. To set BuildingId after BuildingLoader runs, we either:

  • Add an internal setter (and document the lifecycle: "set once during landblock load, never mutated after")
  • Or recreate the LoadedCell instances (expensive — many references)
  • Or move BuildingId to a side-table on BuildingRegistry (gives up cell-side O(1) lookup → Option A, not chosen)

Decision: internal setter ({ get; internal set; }). Documented as "set exactly once by BuildingLoader.Apply(registry, cells) immediately after LandblockLoader produces the cells; never mutated thereafter." The narrowness keeps it safe.


Render-frame data flow (frame-by-frame)

Outdoor frame (cameraInsideBuilding == false)

Pre-frame: glClear(Color | Depth) at frame start (line 6900). Stencil also cleared in this restructure (was not before; the missing stencil clear is potentially load-bearing — see Risk Register).

1. Sky pre-scene (existing _skyRenderer.RenderSky)
   - Writes color + depth at far plane
2. Terrain
   - Writes color + depth at terrain Z
3. RenderOutsideIn — for each visible building:
   - Stencil bit 1 at building's exit portals
   - Stencil-gated render of building's EnvCells (occlusion-query-driven)
   - Reset stencil between iterations
4. Draw(set: All) — outdoor entities (trees, stabs, dynamic)
5. Weather post-scene

Indoor frame (cameraInsideBuilding == true)

Pre-frame: glClear(Color | Depth | Stencil)

1. Skip sky pre-scene (gated on !cameraInsideBuilding)
2. Skip terrain (gated on !cameraInsideBuilding)
3. (No depth-clear — block deleted)

4. MarkAndPunch
   - Stencil bit 1 + depth=1.0 at camera-buildings' exit portal silhouettes
   - GL state on exit: stencil off, depth normal, masks all

5. IndoorPass — WbDrawDispatcher.Draw(set: IndoorPass, cellIds: camBuildings.SelectMany(b => b.EnvCellIds))
   - Walks ONLY the camera-buildings' cells (typically 1-3 cells for a Holtburg cottage cluster)
   - + building shells (IsBuildingShell from R1)
   - Stencil off, DepthFunc.Less
   - Writes cottage wall/floor/ceiling depth that protects against outdoor leak

6. EnableOutdoorPass
   - StencilFunc.Equal(1, 0x01), StencilMask 0x00

7. Stencil-gated sky
   - DepthMask off; _skyRenderer.RenderSky; DepthMask on
   - Sky color writes through punched depth=1.0 only where stencil bit 1 is set (portal silhouettes)
   - DepthMask off so the punched depth survives for step 8

8. Stencil-gated terrain re-draw
   - Terrain Z (with f48c74a -0.01 nudge) overwrites the punched 1.0 at portal pixels
   - Color: terrain near-field through window; beyond terrain horizon, sky color from step 7 survives

9. Stencil-gated OutdoorScenery
   - WbDrawDispatcher.Draw(set: OutdoorScenery)
   - Stabs/procedural scenery visible at portal silhouettes

10. Step 5 — for each other building in otherBuildings:
    a. Read prev-frame occlusion query result → b.WasVisible
    b. Begin new occlusion query
    c. Mark stencil bit 2 at this building's portals
       - StencilFunc.Equal(3, 0x01), StencilMask 0x02
       - Replace where bit 1 already set
    d. End occlusion query
    e. Clear depth where stencil == 3 (re-draw portals with DepthFunc.Always)
    f. Render this building's EnvCells where stencil == 3
       - WbDrawDispatcher.Draw(set: IndoorPass, cellIds: building.EnvCellIds)
    g. Reset bit 2 for next iteration
       - Replace where bit 2 set with bit 2 cleared

11. DisableStencil

12. LiveDynamic — player + NPCs + dropped items
    - Stencil off, depth-test only

Components & file map

File Status Purpose
src/AcDream.App/Rendering/Wb/Building.cs NEW Building data class (cell ids + portal polygons + occlusion-query state)
src/AcDream.App/Rendering/Wb/BuildingRegistry.cs NEW Per-landblock registry; two indexes (byCellId, byBuildingId)
src/AcDream.App/Rendering/Wb/BuildingLoader.cs NEW Static factory: reads LandBlockInfo.Buildings + interior-portal walk; produces registry; sets LoadedCell.BuildingId
src/AcDream.App/Rendering/CellVisibility.cs (LoadedCell) MODIFY Add BuildingId: uint? (init + internal setter)
src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs EXTEND Add 3-bit mode for Step 5; add occlusion-query helpers; building-portal upload helper
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs MINOR New overload Draw(... cellIds: IEnumerable<uint>, ...) to walk an explicit cell list (used by IndoorPass + Step 5)
src/AcDream.App/Rendering/GameWindow.cs RESTRUCTURE Render frame: single cameraInsideBuilding gate; new indoor branch with Steps 1-5; new outdoor branch with RenderOutsideIn pass; landblock-load wires BuildingLoader
src/AcDream.App/Rendering/Shaders/portal_stencil.{vert,frag} LIKELY OK Re-used as-is for Step 5 (uniform/state changes are in the C# pipeline class, not the shader)
tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs NEW Mock LandBlockInfo; assert cell mapping + interior-portal walk + exit-portal extraction
tests/AcDream.App.Tests/Rendering/Wb/BuildingRegistryTests.cs NEW Two-way indexing invariants
tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs EXTEND 3-bit mode + occlusion-query state machine
tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs EXTEND New Draw(cellIds:) overload tests
docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md FOOTER-MARK Mark as SUPERSEDED by this doc; brief note pointing here
docs/superpowers/plans/2026-05-26-phase-a8-restructure.md FOOTER-MARK Same — SUPERSEDED
docs/ISSUES.md UPDATE (in RR12) Move #78 to closed; close #102 (subsumed by Step 5); file new follow-ups if any
CLAUDE.md UPDATE (in RR12) Update A8 paragraph to SHIPPED with full description

Commits to revert at the start of execution (RR1):

  • 2bfeafd R3.5 v2 (depth-clear gate)
  • 38d5374 R3.5 v1 (stencil-branch gate)
  • 60f07bc R3 (stencil-pipeline wire-in)

Commits kept:

  • ed72704 R1 — IsBuildingShell tag (still useful — drives EntitySet partition + Step 5 building-shell rendering)
  • 55f26f2 R2 — EntitySet partition (still useful — IndoorPass/OutdoorScenery/LiveDynamic semantics unchanged)
  • Tasks 1-6 infrastructure (PortalPolygons field, IndoorCellStencilPipeline, portal_stencil shaders, ProbeVisibilityEnabled)

Uncommitted local change to commit before reverts: the [vis] probe wired in GameWindow.cs (RenderingDiagnostics gate). Useful long-term diagnostic; commit as a standalone preserving improvement.


Testing strategy

Unit tests (TDD where feasible)

  • BuildingLoaderTests — fixture LandBlockInfo with synthetic Buildings array; assert:
    • Single-portal building maps to one cell
    • Multi-portal building maps to multiple cells
    • Interior-portal walk reaches all connected cells
    • Exit portal extraction collects right polygons
    • Empty Buildings → empty registry
  • BuildingRegistryTests — add/lookup two-way invariants; cell shared between buildings handled; Count accurate.
  • IndoorCellStencilPipelineTests — 3-bit mode portal-triangle math (existing pattern); occlusion-query state machine if mockable (BeginQuery / EndQuery / read-back lifecycle).
  • WbDrawDispatcherEntitySetTests — new Draw(cellIds:) overload: explicit cell list filters indoor entities correctly; combined with EntitySet partition works as expected.

Visual verification matrix (per RR8, RR10, RR12)

Scenario Acceptance
Cottage interior (ground floor) Walls solid; no see-through to outside; sky visible through windows (Issue B closure)
Cottage cellar Walls solid; cottage floor SOLID, no transparent floor showing cellar (Issue C closure); no grass overlay through stairs from inside
Holtburg Inn (multi-room) All walls solid; no see-through to adjacent rooms (no cross-room portal leak); sky through windows
Dungeon (no buildings) Corridor walls solid; indoor lighting; cells without BuildingId render via fallback path
Exit transition (indoor → outdoor) Clean — no through-ground flicker (Issue A closure); no missing walls on adjacent cottages
Entry transition (outdoor → indoor) Clean — no transparent floor; no texture flicker
Cross-building (Step 5) Stand inside Holtburg Inn, look through window at a cottage across the street with the cottage's window facing you; cottage interior visible through its own window through the inn's window
Looking-into-windows from outside (RenderOutsideIn) Stand outside a cottage, look at its window; cottage interior visible (cottage walls, furniture, NPCs inside)
No regression on #100 No transparent rectangles around cottages

Risk register

Risk Likelihood Mitigation
LandBlockInfo.Buildings portal data shape differs from WB's expectations Medium RR2 spike dumps Holtburg landblock's Buildings array structure before RR3 designs/implements
Interior-portal walk algorithm has subtle rules I don't capture in BuildingLoader Medium RR2 reads WB PortalRenderManager:518-551 verbatim; algorithm matches WB
3-bit stencil + occlusion-query has GPU-specific quirks Medium RR6 mirrors WB's exact GL state sequence; tests on dev machine; comments document WB line refs
Occlusion queries cause CPU stall if read same-frame Low WB uses prev-frame results (asynchronous); we mirror this
Step 5 logic complex, multiple iterations needed High RR8 gate isolates Steps 1-4 first; if A+C+#78 closed at RR8, Step 5 can ship as a follow-on without blocking M1.5 indoor acceptance
Some Holtburg cottages don't have BuildingInfo entries (older content) Medium BuildingLoader handles missing data: cells without a Building get BuildingId == null, flow through the outdoor render path
Stencil-buffer clear missing at frame start (line 6900 currently only clears Color+Depth) High Restructure adds ClearBufferMask.StencilBufferBit to the per-frame clear. Validate no other code relied on stencil persisting between frames
LoadedCell immutability broken by internal set on BuildingId Low Documented narrowly — BuildingLoader.Apply is the sole writer, runs once after LandblockLoader
RR2 spike reveals BuildingInfo structure incompatible with the design Low-Medium Re-brainstorm gate at RR2 outcome; design doc updated; subsequent tasks adapted

Falsifiability

The visual matrix is the acceptance test. If any scenario fails at RR8 or RR12, the failure mode determines next step:

  • Issue C or A reproduces → IndoorPass partition wrong; debug with [vis] probe; check camBuildings.EnvCellIds actually narrows to the camera's cottage cluster
  • Sky-through-window fails → step 7 GL state issue; check DepthMask + stencil interaction
  • Cross-building (Step 5) fails → likely 3-bit stencil math wrong; compare per-iteration GL state against WB line-by-line
  • RenderOutsideIn fails → check building-frustum culling + per-building portal mesh upload

Each failure has a deterministic next investigation step. We avoid the "speculative fix → another iteration" anti-pattern.


Task breakdown

# Task Sub-skill Est. session Outputs
RR1 Cleanup: commit [vis] probe (uncommitted); revert R3+R3.5 v1+R3.5 v2; footer-mark old design + plan as SUPERSEDED (mechanical) 0.5 1 probe commit, 3 revert commits, 2 footer-marks
RR2 Spike: dump LandBlockInfo.Buildings for Holtburg cottages (e.g. landblock 0xA9B40000); read WB PortalRenderManager:518-551; write findings doc 2026-05-2X-a8-buildings-data-shape.md locking the data shape + interior-portal walk algorithm (research) 0.5-1 Findings doc
RR3 TDD-implement Building, BuildingRegistry, BuildingLoader (+ unit tests) impl 1 3 new files + 2 test files
RR4 Wire BuildingRegistry into landblock load + LoadedCell.BuildingId propagation impl 0.5 GameWindow.cs ctor wiring + LoadedCell change
RR5 Extend WbDrawDispatcher with Draw(cellIds:) overload (TDD) impl 0.5 Dispatcher + test extension
RR6 Extend IndoorCellStencilPipeline for 3-bit mode + occlusion-query helpers impl 1 Pipeline class extension + tests
RR7 Restructure render frame: WB-faithful Steps 1-4 + stencil-gated sky + outdoor branch (no Step 5 yet) + initial sky/terrain gates impl 1 GameWindow.cs render-frame block rewrite
RR8 Visual verification gate: Steps 1-4 working (cottage / cellar / inn / dungeon / sky-through-windows / clean transitions). Closes #78 + R4 Issues A+C. (visual) 0.5 RR8 findings doc; gate decision
RR9 Implement Step 5 (cross-building visibility) impl 1-2 Pipeline + GameWindow.cs additions
RR10 Visual verification gate: Step 5 working (inn→window→cottage across street) (visual) 0.5 RR10 findings doc
RR11 Implement RenderOutsideIn (outdoor camera looking into cottage windows) impl 1 Outdoor-branch addition
RR12 Final visual matrix + ship docs (close #78; update CLAUDE.md A8 paragraph; supersede old docs in their footers) (visual + docs) 0.5 ISSUES.md + CLAUDE.md commits

Total estimate: 8-10 sessions (~1.5-2 weeks calendar).

Gate decisions:

  • After RR2 outcome: if LandBlockInfo.Buildings structure incompatible → re-brainstorm.
  • After RR8 outcome: if any of #78, Issue A, Issue C still reproduces → debug into RR7 implementation BEFORE proceeding to RR9 (Step 5 amplifies bugs).
  • After RR10 outcome: if Step 5 visibly broken → file as a known issue and proceed to RR11+RR12. Step 5 is a polish-tier feature; Steps 1-4 carry M1.5.

Alternatives considered

Alt 1 — cameraInsideCell (lenient) gate everywhere

Re-use the existing lenient flag. Means grace frames stay treated as "inside" for the render frame. Reintroduces every grace-state bug seen in R3.5. Rejected: the entire RR0 analysis points to dropping grace for the render path.

Alt 2 — LoadedCell.BuildingId as the SOLE data source (Option B from brainstorm)

Cells carry building-id; no registry; rebuilds the per-building cell set per frame by scanning all cells. Performance fine at our scale, but the API becomes awkward (no building.EnvCellIds to iterate). Rejected per the data-model brainstorm.

Alt 3 — Per-cell Z-offset to break co-planar Z-fight

Apply glPolygonOffset differently per cell to resolve the cottage-floor / cellar-ceiling tie. Doesn't address the structural "render 16 cells at full screen" issue — just patches one of its symptoms. Rejected as another half-fix in the R3 lineage.

Alt 4 — Retail polygon-clip scissor port

Most faithful to retail (PView::DrawCells at acclient_2013_pseudo_c.txt:432709). Implements per-portal screen-space clipping recursively. Multi-week scope. WB's stencil approach is a known-good equivalent observable behavior; preferring WB matches "rendering: WB is acdream's base" per CLAUDE.md.

Alt 5 — Defer all A8 work; live with #78 on main

Revert R1+R2+R3+R3.5; ship M1.5 without indoor visibility fix; come back later. Rejected per user direction ("Try to expand A8 scope to a full WB port now").


Open follow-ups (post-RR12)

  • #78 — closed by this phase
  • #102 (cross-cell-portal far-side visibility, WB Step 5 deferral) — closed by Step 5 in this phase
  • #103 (cellar terrain Z-fight from outside, out-to-in artifact) — pre-existing; may be addressed by RenderOutsideIn if the cellar entrance is treated as a building portal; verify in RR12
  • Grace-mechanism cleanup — non-render consumers may still benefit from grace in CellVisibility.FindCameraCell; auditing them is out of scope. Future cleanup phase.

Self-review notes

  • Placeholder scan: no TBDs/TODOs/incomplete sections. RR2 explicitly resolves the "open question" about WB's interior-portal walk algorithm via the spike + findings doc; that's a CONCRETE next step, not a placeholder.
  • Internal consistency: cameraInsideBuilding semantics stated once and used consistently across all sections. BuildingId lifecycle stated once (set at load by BuildingLoader.Apply, never mutated thereafter). Step numbering (1-12) consistent between architecture, data-flow, and task breakdown sections.
  • Scope check: twelve tasks, ~8-10 sessions, decomposed by surface (data model → dispatcher → pipeline → render frame → Step 5 → RenderOutsideIn → ship). Each task is single-PR-sized. No scope sprawl.
  • Ambiguity check:
    • "render only the camera-buildings' cells" — explicit about USING the registry, not BFS
    • "stencil-gated sky with DepthMask off + on" — explicit about depth-buffer effect
    • "RR2 spike resolves the interior-portal walk algorithm" — explicit about what's settled vs deferred
    • "RR8 gate decision" — explicit about what failure→action mapping looks like

Notes for the writing-plans skill

When this design enters superpowers:writing-plans:

  • Each RR# task above maps to one plan task.
  • TDD where feasible (RR3, RR5, RR6 have clear unit-test surfaces).
  • GL integration tasks (RR7, RR9, RR11) are visual-verification-only.
  • Spike (RR2) is research-only; output is a findings doc, no production code.
  • Add to the spec reviewer prompt for RR7, RR9, RR11: "Does the implementation match WB's RenderInsideOut/RenderOutsideIn at references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-330 line-by-line? Reviewer must cross-reference."