# Phase A8 — Full WorldBuilder RenderInsideOut + RenderOutsideIn Port — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Port WorldBuilder's full `RenderInsideOut` (Steps 1-5) and `RenderOutsideIn` indoor visibility pipeline to acdream, replacing the current half-port (R3) with per-building cell scoping. Closes issue #78 (outdoor visible through indoor walls), R4 Issues A + C (transition flicker + transparent floor), and #102 (cross-cell-portal visibility). **Architecture:** Add per-landblock `BuildingRegistry` mapping each cell to its building (or null). Render frame branches on a strict `cameraInsideBuilding` gate: indoor path renders only the camera-buildings' cells via WB Steps 1-4 + stencil-gated sky + Step 5 cross-building; outdoor path renders the world + `RenderOutsideIn` to show cottage interiors through windows from the street. Single source of truth for "inside a building" — drop the lenient/strict two-flag asymmetry that caused R3.5 v1+v2's bugs. **Tech Stack:** C# .NET 10, Silk.NET (OpenGL 4.3 + GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters + occlusion-query objects), xUnit. **Predecessor context (REQUIRED reading before starting):** - [docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md](../specs/2026-05-26-phase-a8-wb-full-port-design.md) — the approved design this plan implements - [docs/research/2026-05-26-a8-rr0-falsification-findings.md](../../research/2026-05-26-a8-rr0-falsification-findings.md) — RR0 evidence triggering the scope expansion - [references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-330](../../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs) — proven reference; `RenderInsideOut` (73-239) + `RenderOutsideIn` (241+) must be ported **Infrastructure consumed as-is:** - R1: `WorldEntity.IsBuildingShell` flag set by `LandblockLoader` (commit `ed72704`) - R2: `WbDrawDispatcher.EntitySet` partition (commit `55f26f2`) - Tasks 1-6 infrastructure: `LoadedCell.PortalPolygons`, `IndoorCellStencilPipeline`, `PortalMeshBuilder`, `portal_stencil.vert/.frag`, `RenderingDiagnostics.ProbeVisibilityEnabled` (commits `fee878f` → `dcf69a1`) **HEAD at session start:** `ea60d1f` (design doc commit). The `[vis]` probe code wired in RR0 spike is uncommitted; RR1 commits it before reverting R3. --- ## File Structure | File | Status | Purpose | |---|---|---| | `src/AcDream.App/Rendering/Wb/Building.cs` | NEW | `Building` data class: BuildingId, EnvCellIds, ExitPortalPolygons, occlusion-query state | | `src/AcDream.App/Rendering/Wb/BuildingRegistry.cs` | NEW | Two-way indexed registry (by cellId + by buildingId) | | `src/AcDream.App/Rendering/Wb/BuildingLoader.cs` | NEW | Static factory: reads LandBlockInfo.Buildings, walks interior portals, computes per-building cell sets | | `src/AcDream.App/Rendering/CellVisibility.cs` (LoadedCell) | MODIFY | Add `BuildingId: uint?` field with internal setter | | `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs` | EXTEND | Add 3-bit stencil mode + occlusion-query helpers + per-building portal upload | | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` | EXTEND | Add Draw overload accepting explicit `IEnumerable` cellIds (instead of visibility-derived set) | | `src/AcDream.App/Rendering/GameWindow.cs` | RESTRUCTURE | Render-frame block: single `cameraInsideBuilding` gate; indoor path with Steps 1-5; outdoor path with RenderOutsideIn; landblock-load wires BuildingLoader | | `tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs` | NEW | Building data-class invariants | | `tests/AcDream.App.Tests/Rendering/Wb/BuildingRegistryTests.cs` | NEW | Two-way indexing invariants | | `tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs` | NEW | LandBlockInfo→registry mapping; interior-portal walk; exit-portal extraction | | `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherCellIdsOverloadTests.cs` | NEW | New Draw(cellIds:) overload behavior | | `tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs` | EXTEND | 3-bit mode + occlusion-query state machine | | `docs/research/2026-05-2X-a8-buildings-data-shape.md` | NEW (RR2 output) | Spike findings: BuildingInfo data shape + walk algorithm | | `docs/research/2026-05-2X-a8-rr8-steps-1-4-visual.md` | NEW (RR8 output) | Visual verification log for Steps 1-4 | | `docs/research/2026-05-2X-a8-rr10-step-5-visual.md` | NEW (RR10 output) | Visual verification log for Step 5 | | `docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md` | FOOTER-MARK | Mark as SUPERSEDED | | `docs/superpowers/plans/2026-05-26-phase-a8-restructure.md` | FOOTER-MARK | Mark as SUPERSEDED | | `docs/ISSUES.md` | UPDATE (RR12) | Move #78 closed; close #102 (subsumed by Step 5); file new follow-ups if any | | `CLAUDE.md` | UPDATE (RR12) | A8 paragraph: PAUSED → SHIPPED with full description | **Commits reverted in RR1:** - `60f07bc` R3 (stencil-pipeline wire-in) - `38d5374` R3.5 v1 (stencil-branch gate) - `2bfeafd` R3.5 v2 (depth-clear gate) **Commits kept:** - R1 `ed72704` IsBuildingShell flag - R2 `55f26f2` EntitySet partition - Tasks 1-6 infrastructure (shipped 2026-05-25 under earlier SHAs) --- ## Task RR1: Cleanup — commit `[vis]` probe, revert R3+R3.5 v1+R3.5 v2, supersede old docs **Files:** - Modify: `src/AcDream.App/Rendering/GameWindow.cs` (commit the existing uncommitted `[vis]` probe code added during RR0 spike) - Auto-generated by reverts: `src/AcDream.App/Rendering/GameWindow.cs` (revert R3+R3.5 v1+R3.5 v2) - Modify (footer-mark): `docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md` - Modify (footer-mark): `docs/superpowers/plans/2026-05-26-phase-a8-restructure.md` - [ ] **RR1-S1: Verify uncommitted [vis] probe is in working tree** ```bash git diff src/AcDream.App/Rendering/GameWindow.cs | head -40 ``` Expected: shows the `RenderingDiagnostics.ProbeVisibilityEnabled` probe block added during RR0 spike. If empty/missing, the probe was already committed elsewhere — skip to RR1-S3. - [ ] **RR1-S2: Commit the `[vis]` probe as a standalone diagnostic** ```bash git add src/AcDream.App/Rendering/GameWindow.cs git commit -m "$(cat <<'EOF' diag(render): Phase A8 [vis] probe — light up dormant ProbeVisibilityEnabled Wires the dormant RenderingDiagnostics.ProbeVisibilityEnabled flag (added 2026-05-25 by Task 6 of the original A8 plan, no probe code) to per-frame [vis] log lines around the render-frame branch decision. Captures camera position, cameraInsideCell (lenient grace-aware), the strict PointInCell result, the visibility CameraCell id, and VisibleCellIds count/list. Enable via ACDREAM_PROBE_VIS=1. Used during A8 RR0 falsification spike (2026-05-26) — see docs/research/2026-05-26-a8-rr0-falsification-findings.md. Kept as long- term diagnostic for the upcoming RR8/RR10 visual verification gates. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` - [ ] **RR1-S3: Revert R3.5 v2 (depth-clear gate)** ```bash git revert 2bfeafd --no-edit ``` Expected: one new revert commit. `git log -1 --oneline` shows `Revert "fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too"`. - [ ] **RR1-S4: Revert R3.5 v1 (stencil-branch gate)** ```bash git revert 38d5374 --no-edit ``` Expected: one new revert commit. `Revert "fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment"`. - [ ] **RR1-S5: Revert R3 (stencil-pipeline wire-in)** ```bash git revert 60f07bc --no-edit ``` Expected: one new revert commit. `Revert "feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)"`. If the revert hits a conflict (because the `[vis]` probe commit modifies the same lines as R3): resolve by KEEPING the `[vis]` probe additions and DROPPING R3's stencil branch + depth-clear + cameraReallyInside computation. The probe must survive R3's revert because the probe sits in the surrounding context, not in R3's added code. To resolve the conflict manually: - Open `src/AcDream.App/Rendering/GameWindow.cs` - Find the conflict markers around lines ~7011 and ~7174 - Keep: the `[vis]` probe block, the `cameraInsideCell` declaration - Drop: the `cameraReallyInside` computation, the depth-clear-if-cameraReallyInside block, the indoor stencil branch, the outdoor `else` branch (revert leaves a single dispatcher call with `EntitySet.All`) Then: ```bash git add src/AcDream.App/Rendering/GameWindow.cs git revert --continue --no-edit ``` - [ ] **RR1-S6: Verify net state vs R2 baseline** ```bash git diff 55f26f2 HEAD -- src/AcDream.App/Rendering/GameWindow.cs ``` Expected output: ONLY the `[vis]` probe additions (around line 7012 + branch markers around the dispatcher call). No `cameraReallyInside`, no stencil branch, no depth-clear-if-inside. The dispatcher call should use `EntitySet.All` (single call, pre-A8 shape). If extra diff lines appear, manually un-do those — only the `[vis]` probe should be the delta vs R2. - [ ] **RR1-S7: Build + test green** ```bash dotnet build -c Debug --nologo dotnet test --nologo ``` Expected: build green (0 warnings, 0 errors). Test failures within documented 14-23 flaky window (pre-existing PhysicsResolveCapture/Diagnostics static-leak). - [ ] **RR1-S8: Footer-mark old design doc as SUPERSEDED** Append to `docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md`: ```markdown --- **SUPERSEDED 2026-05-26 (PM) by [2026-05-26-phase-a8-wb-full-port-design.md](2026-05-26-phase-a8-wb-full-port-design.md).** After RR0 falsification ([docs/research/2026-05-26-a8-rr0-falsification-findings.md](../../research/2026-05-26-a8-rr0-falsification-findings.md)) showed R4 Issues A + C were caused by R3 (not pre-existing on main), the "restructure" approach in this design was insufficient — the structural bug is rendering all 16 BFS-reachable cells at full screen extent, not just the depth-clear workaround. The new design ports WB's full RenderInsideOut + RenderOutsideIn with per-building cell scoping. This document is retained for historical reference and roadmap discipline. ``` - [ ] **RR1-S9: Footer-mark old plan as SUPERSEDED** Append to `docs/superpowers/plans/2026-05-26-phase-a8-restructure.md`: ```markdown --- **SUPERSEDED 2026-05-26 (PM) by [2026-05-26-phase-a8-wb-full-port.md](2026-05-26-phase-a8-wb-full-port.md).** This plan implemented the "WB-faithful restructure" design which RR0 evidence invalidated. The new plan implements the full WorldBuilder RenderInsideOut + RenderOutsideIn port. RR0 was completed and its findings doc is retained. This document is retained for historical reference. ``` - [ ] **RR1-S10: Commit footer-marks** ```bash git add docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md docs/superpowers/plans/2026-05-26-phase-a8-restructure.md git commit -m "$(cat <<'EOF' docs: Phase A8 — mark prior restructure design+plan as SUPERSEDED Both documents are retained for historical reference. The new full-WB-port design (2026-05-26-phase-a8-wb-full-port-design.md, ea60d1f) replaces them. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR2: Spike — confirm BuildingInfo data shape + interior-portal walk algorithm **Goal:** Before implementing `BuildingLoader`, verify (a) what fields `DatReaderWriter.Types.BuildingInfo` exposes; (b) how WB's `PortalRenderManager` actually computes a building's full cell set from BuildingInfo entries. **Files:** - Create: `docs/research/2026-05-26-a8-buildings-data-shape.md` This is research-only. No code shipped. - [ ] **RR2-S1: Inspect BuildingInfo struct via DatReaderWriter** ```bash grep -rn "class BuildingInfo\|struct BuildingInfo\|record BuildingInfo" references/Chorizite.DatReaderWriter/ 2>/dev/null | head -5 ``` If no match, look in the package source: ```bash find ~/.nuget/packages/chorizite.datreaderwriter -name "BuildingInfo*.cs" 2>/dev/null | head -3 ``` Alternative — read what `LandblockLoader.cs:74-87` references and document those fields. Capture in the findings doc: - Field names (ModelId? Frame? NumPortals? Portals?) - Field types - What `CBldPortal` (or its DRW equivalent) looks like — what's its `OtherCellId`? - [ ] **RR2-S2: Read WB PortalRenderManager building-cell association** Read `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:518-551` (or surrounding lines if line numbers shifted — search for `BuildingPortalGPU` constructor and `EnvCellIds` population): ```bash grep -n "BuildingPortalGPU\|EnvCellIds" references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs | head -10 ``` Document in the findings doc: - How WB walks BuildingInfo's portals to compute EnvCellIds - Whether WB walks interior portals BEYOND the BuildingInfo.Portals (e.g., from cell A reached via BuildingInfo.Portals, walk through cell A's interior portals to find more cells) - Whether WB stops at exit portals (OtherCellId == 0xFFFF) or treats them differently - Whether occlusion-query state is set up at construction or lazily - [ ] **RR2-S3: Live-inspect a Holtburg cottage's BuildingInfo** Add a temporary diagnostic in `LandblockLoader.cs:74-87` (the Buildings loop). Inside the loop, log: ```csharp Console.WriteLine($"[building-shape] lb=0x{landblockId:X8} idx={i} ModelId=0x{building.ModelId:X8} " + $"Frame.Origin=({building.Frame.Origin.X:F1},{building.Frame.Origin.Y:F1},{building.Frame.Origin.Z:F1}) " + $"Portals={building.Portals?.Count ?? 0}"); if (building.Portals is not null) { foreach (var p in building.Portals) Console.WriteLine($"[building-shape] portal -> OtherCellId=0x{p.OtherCellId:X8} ... " + $"(remaining fields: {p.GetType().GetProperties().Length} total)"); } ``` (Use reflection to enumerate property names if the type is uncertain.) Build + launch + walk to a Holtburg cottage. Grep `[building-shape]` lines from the log. Document in the findings doc. ```bash dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo # launch then close after seeing the log grep "\[building-shape\]" launch.log | head -20 ``` Then revert the diagnostic — it shouldn't ship: ```bash git checkout HEAD -- src/AcDream.Core/World/LandblockLoader.cs ``` - [ ] **RR2-S4: Write findings doc** Create `docs/research/2026-05-26-a8-buildings-data-shape.md` with: - Section 1: `BuildingInfo` field shape (verbatim from DatReaderWriter or live-inspection) - Section 2: Holtburg cottage's actual BuildingInfo dump (entry-portal cells, polygon vertex counts, etc.) - Section 3: WB's interior-portal walk algorithm — pseudocode - Section 4: Resolved algorithm for acdream's `BuildingLoader` (translate WB's pseudocode to our types) - Section 5: Edge cases noted (e.g., cells in multiple buildings; buildings without portals; etc.) - [ ] **RR2-S5: Commit findings** ```bash git add docs/research/2026-05-26-a8-buildings-data-shape.md git commit -m "$(cat <<'EOF' docs(research): Phase A8 RR2 — BuildingInfo data shape + interior-portal walk Spike findings before RR3 (BuildingLoader impl). Documents: - DatReaderWriter.Types.BuildingInfo field shape - Holtburg cottage BuildingInfo dump (entry portals + polygon counts) - WB PortalRenderManager interior-portal walk algorithm (pseudo) - Resolved algorithm for acdream BuildingLoader - Edge cases (shared cells, missing data, etc.) Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` - [ ] **RR2-S6: Gate decision** If RR2 confirms the design's assumptions → proceed to RR3. If RR2 finds the data shape incompatible (e.g., BuildingInfo doesn't expose Portals; WB's walk algorithm requires data we can't derive) → STOP. Re-brainstorm via `superpowers:brainstorming` to adapt the design. --- ## Task RR3: Implement `Building`, `BuildingRegistry`, `BuildingLoader` (TDD) **Files:** - Create: `src/AcDream.App/Rendering/Wb/Building.cs` - Create: `src/AcDream.App/Rendering/Wb/BuildingRegistry.cs` - Create: `src/AcDream.App/Rendering/Wb/BuildingLoader.cs` - Create: `tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs` - Create: `tests/AcDream.App.Tests/Rendering/Wb/BuildingRegistryTests.cs` - Create: `tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs` - [ ] **RR3-S1: Write failing tests for `Building` data class** Create `tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs`: ```csharp using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using Xunit; namespace AcDream.App.Tests.Rendering.Wb; public class BuildingTests { [Fact] public void Building_RequiredFields_PopulateCorrectly() { var b = new Building { BuildingId = 42, EnvCellIds = new HashSet { 0xA9B40150u, 0xA9B40151u }, ExitPortalPolygons = new List { new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0) }, }, }; Assert.Equal(42u, b.BuildingId); Assert.Equal(2, b.EnvCellIds.Count); Assert.Contains(0xA9B40150u, b.EnvCellIds); Assert.Single(b.ExitPortalPolygons); Assert.Equal(3, b.ExitPortalPolygons[0].Length); } [Fact] public void Building_OcclusionQueryState_DefaultsZero() { var b = new Building { BuildingId = 0, EnvCellIds = new HashSet(), ExitPortalPolygons = new List(), }; Assert.Equal(0u, b.QueryId); Assert.False(b.QueryStarted); Assert.False(b.WasVisible); } } ``` - [ ] **RR3-S2: Run BuildingTests to verify failure** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingTests" --nologo ``` Expected: BUILD FAILURE with `'Building' does not exist in namespace 'AcDream.App.Rendering.Wb'`. - [ ] **RR3-S3: Implement `Building`** Create `src/AcDream.App/Rendering/Wb/Building.cs`: ```csharp using System.Collections.Generic; using System.Numerics; namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked /// via the dat-level LandBlockInfo.Buildings entry. Building shells (cottage /// walls, inn walls — IsBuildingShell=true entities) render unconditionally /// when the camera is inside this building's cells. The exit portal polygons /// are stencil-marked so outdoor visibility leaks through portal silhouettes /// only. /// /// Step 5 (cross-building visibility via 3-stencil-bit pipeline) uses /// the occlusion-query state to skip rendering when the building's portals /// weren't visible last frame. /// public sealed class Building { /// Unique within a landblock; allocated sequentially by BuildingLoader. public required uint BuildingId { get; init; } /// The EnvCells this building owns. Includes all cells reachable /// from the building's entry portals via interior portals (no exit portals). public required HashSet EnvCellIds { get; init; } /// Exit portal polygons in world space. Each polygon is a triangle /// fan from vertex 0. Stencil-marked + far-depth-punched at Steps 1+2 of /// WB's RenderInsideOut. public required IReadOnlyList ExitPortalPolygons { get; init; } // Step 5 occlusion-query state (mutable, per-frame). public uint QueryId; // GL query object; lazily created on first use public bool QueryStarted; // true after first BeginQuery; controls read-back public bool WasVisible; // previous-frame query result; gates rendering this frame } ``` - [ ] **RR3-S4: Run BuildingTests to verify pass** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingTests" --nologo ``` Expected: 2 tests pass. - [ ] **RR3-S5: Write failing tests for `BuildingRegistry`** Create `tests/AcDream.App.Tests/Rendering/Wb/BuildingRegistryTests.cs`: ```csharp using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using Xunit; namespace AcDream.App.Tests.Rendering.Wb; public class BuildingRegistryTests { private static Building B(uint id, params uint[] cellIds) => new() { BuildingId = id, EnvCellIds = new HashSet(cellIds), ExitPortalPolygons = new List(), }; [Fact] public void Empty_NoBuildingsRegistered() { var reg = new BuildingRegistry(); Assert.Equal(0, reg.Count); Assert.Empty(reg.All()); Assert.Empty(reg.GetBuildingsContainingCell(0xA9B40150u)); Assert.Null(reg.GetById(0)); } [Fact] public void Add_IndexesBothDirections() { var reg = new BuildingRegistry(); var b = B(1, 0xA9B40150u, 0xA9B40151u); reg.Add(b); Assert.Equal(1, reg.Count); Assert.Same(b, reg.GetById(1)); Assert.Single(reg.GetBuildingsContainingCell(0xA9B40150u)); Assert.Single(reg.GetBuildingsContainingCell(0xA9B40151u)); Assert.Same(b, reg.GetBuildingsContainingCell(0xA9B40150u)[0]); Assert.Empty(reg.GetBuildingsContainingCell(0xDEADBEEFu)); } [Fact] public void CellSharedBetweenTwoBuildings_GetBuildingsContainingCellReturnsBoth() { var reg = new BuildingRegistry(); var b1 = B(1, 0xA9B40150u, 0xA9B40151u); var b2 = B(2, 0xA9B40151u, 0xA9B40152u); // shares 0151 with b1 reg.Add(b1); reg.Add(b2); var bothAt0151 = reg.GetBuildingsContainingCell(0xA9B40151u); Assert.Equal(2, bothAt0151.Count); Assert.Contains(b1, bothAt0151); Assert.Contains(b2, bothAt0151); } [Fact] public void All_EnumeratesEveryBuilding() { var reg = new BuildingRegistry(); reg.Add(B(1, 0xA9B40150u)); reg.Add(B(2, 0xA9B40160u)); reg.Add(B(3, 0xA9B40170u)); var ids = new HashSet(); foreach (var b in reg.All()) ids.Add(b.BuildingId); Assert.Equal(new HashSet { 1, 2, 3 }, ids); } } ``` - [ ] **RR3-S6: Run tests to verify failure** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingRegistryTests" --nologo ``` Expected: build failure with `'BuildingRegistry' does not exist`. - [ ] **RR3-S7: Implement `BuildingRegistry`** Create `src/AcDream.App/Rendering/Wb/BuildingRegistry.cs`: ```csharp using System; using System.Collections.Generic; namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-26): per-landblock registry of s. /// Two-way indexed for O(1) cell→building and building-id→building lookups. /// Built once per landblock at load time by ; /// no mutations after. /// public sealed class BuildingRegistry { // Index 1: cell-id → list of buildings containing that cell. // Cells may belong to multiple buildings (rare; matches WB's API shape). private readonly Dictionary> _byCellId = new(); // Index 2: building-id → Building. private readonly Dictionary _byBuildingId = new(); /// Adds a building to both indexes. Idempotent if the same Building /// instance is added twice with the same BuildingId. public void Add(Building b) { if (_byBuildingId.TryGetValue(b.BuildingId, out var existing) && ReferenceEquals(existing, b)) return; _byBuildingId[b.BuildingId] = b; foreach (var cellId in b.EnvCellIds) { if (!_byCellId.TryGetValue(cellId, out var list)) { list = new List(); _byCellId[cellId] = list; } if (!list.Contains(b)) list.Add(b); } } /// Returns the buildings containing . /// Empty list when the cell isn't part of any building (outdoor cells, /// dungeon cells not tagged by LandBlockInfo.Buildings). public IReadOnlyList GetBuildingsContainingCell(uint cellId) => _byCellId.TryGetValue(cellId, out var list) ? list : Array.Empty(); /// Returns the building with the given id, or null. public Building? GetById(uint buildingId) => _byBuildingId.TryGetValue(buildingId, out var b) ? b : null; /// Enumerates every registered building. public IEnumerable All() => _byBuildingId.Values; /// Number of registered buildings. public int Count => _byBuildingId.Count; } ``` - [ ] **RR3-S8: Run BuildingRegistryTests to verify pass** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingRegistryTests" --nologo ``` Expected: 4 tests pass. - [ ] **RR3-S9: Write failing tests for `BuildingLoader`** Create `tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs`. The exact field shapes in this test depend on RR2's findings. The skeleton below uses what we know from `LandblockLoader.cs:74-87` (BuildingInfo has ModelId, Frame.Origin, Frame.Orientation; the portals collection field name + element type come from RR2). ```csharp using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using Xunit; namespace AcDream.App.Tests.Rendering.Wb; public class BuildingLoaderTests { // Helper: build a minimal LandBlockInfo with one BuildingInfo containing // the supplied portal entries. The "OtherCellId" values are the cells // the portal points INTO from outside the building. private static LandBlockInfo MakeInfo(params (uint modelId, uint[] portalOtherCellIds)[] buildings) { var bls = new List(); foreach (var (modelId, portals) in buildings) { var portalList = new List(); foreach (var ocid in portals) { portalList.Add(new BldPortal { OtherCellId = (ushort)(ocid & 0xFFFFu), /* other fields default */ }); } bls.Add(new BuildingInfo { ModelId = modelId, Frame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, Portals = portalList, }); } return new LandBlockInfo { Objects = new List(), Buildings = bls, }; } [Fact] public void Empty_NoBuildings_EmptyRegistry() { var info = new LandBlockInfo { Objects = new List(), Buildings = new List() }; var reg = BuildingLoader.Build(info, landblockId: 0xA9B40000u, cellsByCellId: new Dictionary()); Assert.Equal(0, reg.Count); } [Fact] public void OneBuilding_OnePortal_MapsToOneCell() { // Building points to cell 0x0150 in landblock 0xA9B40000 → full cell id 0xA9B40150 var info = MakeInfo((modelId: 0x02000123u, portalOtherCellIds: new[] { 0x0150u })); // For this test we don't need actual LoadedCells — pass an empty dict; // the loader should still create the Building entry from the BuildingInfo alone. var reg = BuildingLoader.Build(info, landblockId: 0xA9B40000u, cellsByCellId: new Dictionary()); Assert.Equal(1, reg.Count); var building = System.Linq.Enumerable.First(reg.All()); Assert.Contains(0xA9B40150u, building.EnvCellIds); } [Fact] public void OneBuilding_MultiplePortals_MapsToMultipleCells() { var info = MakeInfo((0x02000123u, new[] { 0x0150u, 0x0151u, 0x0152u })); var reg = BuildingLoader.Build(info, 0xA9B40000u, new Dictionary()); var building = System.Linq.Enumerable.First(reg.All()); Assert.Equal(3, building.EnvCellIds.Count); Assert.Contains(0xA9B40150u, building.EnvCellIds); Assert.Contains(0xA9B40151u, building.EnvCellIds); Assert.Contains(0xA9B40152u, building.EnvCellIds); } [Fact] public void TwoBuildings_AllocateSequentialIds() { var info = MakeInfo( (0x02000001u, new[] { 0x0150u }), (0x02000002u, new[] { 0x0160u })); var reg = BuildingLoader.Build(info, 0xA9B40000u, new Dictionary()); Assert.Equal(2, reg.Count); var ids = new SortedSet(); foreach (var b in reg.All()) ids.Add(b.BuildingId); Assert.Equal(new SortedSet { 1, 2 }, ids); // sequential 1,2 } } ``` **Note for the implementer:** the field names `Portals` and `BldPortal.OtherCellId` are placeholders pending RR2 confirmation. Match whatever RR2's findings doc says. If the type is `IList` or similar, the test code adapts directly. - [ ] **RR3-S10: Run BuildingLoaderTests to verify failure** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingLoaderTests" --nologo ``` Expected: build failure with `'BuildingLoader' does not exist`. - [ ] **RR3-S11: Implement `BuildingLoader`** Create `src/AcDream.App/Rendering/Wb/BuildingLoader.cs`: ```csharp using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter.DBObjs; namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-26): static factory that builds a per-landblock /// from a 's /// Buildings array. Walks the building's entry portals and (per RR2 /// findings) optionally extends each building's cell set through /// interior portals. /// /// Cells in get their /// set to the matching building id; /// cells without a matching building stay at BuildingId == null. /// public static class BuildingLoader { /// /// Builds the registry. Sequential building IDs starting at 1 (id 0 /// reserved for "no building" semantics, but the registry uses /// uint? on LoadedCell so 0 is a valid value too — start at 1 /// for clarity). /// public static BuildingRegistry Build( LandBlockInfo info, uint landblockId, IReadOnlyDictionary cellsByCellId) { var reg = new BuildingRegistry(); if (info.Buildings is null || info.Buildings.Count == 0) return reg; uint lbMask = landblockId & 0xFFFF0000u; uint nextId = 1; foreach (var b in info.Buildings) { var envCellIds = new HashSet(); var exitPortalPolys = new List(); // Step 1: collect entry cells from the BuildingInfo's portals. // Each BldPortal's OtherCellId is a 16-bit cell-local id; combine // with the landblock mask for the full cell id. // // Note: RR2's findings doc specifies the exact field name for the // portal list (e.g. "Portals" or "PortalList") and the OtherCellId // field on BldPortal. If RR2 found WB walks interior portals beyond // the entry portals, replicate that here. if (b.Portals is not null) { foreach (var portal in b.Portals) { if (portal.OtherCellId == 0xFFFF) continue; // exit portal — skip uint cellId = lbMask | portal.OtherCellId; envCellIds.Add(cellId); } } // Step 2: walk interior portals from each entry cell to find the // building's full cell set. (Per RR2; WB's PortalRenderManager:518-551 // does this walk.) if (cellsByCellId.Count > 0) { var queue = new Queue(); foreach (var cid in envCellIds) queue.Enqueue(cid); while (queue.Count > 0) { var current = queue.Dequeue(); if (!cellsByCellId.TryGetValue(current, out var cell)) continue; foreach (var p in cell.Portals) { if (p.OtherCellId == 0xFFFF) { // Exit portal — collect its polygon for stencil mask. // (Polygon collection from PortalPolygons matches // existing PortalMeshBuilder logic.) continue; } uint neighbourId = lbMask | p.OtherCellId; if (envCellIds.Add(neighbourId)) queue.Enqueue(neighbourId); } } } // Step 3: collect exit portal polygons in world space. // Iterates each cell's portals; for exit portals (OtherCellId==0xFFFF), // transforms portal polygon vertices through the cell's WorldTransform. foreach (var cellId in envCellIds) { if (!cellsByCellId.TryGetValue(cellId, out var cell)) continue; for (int pi = 0; pi < cell.Portals.Count; pi++) { if (cell.Portals[pi].OtherCellId != 0xFFFF) continue; if (pi >= cell.PortalPolygons.Count) continue; var localPoly = cell.PortalPolygons[pi]; if (localPoly.Length < 3) continue; var worldPoly = new Vector3[localPoly.Length]; for (int v = 0; v < localPoly.Length; v++) worldPoly[v] = Vector3.Transform(localPoly[v], cell.WorldTransform); exitPortalPolys.Add(worldPoly); } } var building = new Building { BuildingId = nextId++, EnvCellIds = envCellIds, ExitPortalPolygons = exitPortalPolys, }; reg.Add(building); // Step 4: stamp BuildingId on each cell (needs internal setter on // LoadedCell — added in RR4). // RR4 wires the call site that loops through reg.All() and assigns // cell.BuildingId = building.BuildingId for each cell in EnvCellIds. } return reg; } } ``` - [ ] **RR3-S12: Run BuildingLoaderTests to verify pass** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingLoaderTests" --nologo ``` Expected: 4 tests pass. If a field-name mismatch surfaces (e.g. RR2 said `PortalList` not `Portals`), update both `BuildingLoader.cs` AND `BuildingLoaderTests.cs` to use the correct name. - [ ] **RR3-S13: Full build + test green** ```bash dotnet build -c Debug --nologo dotnet test --nologo ``` Expected: build green; test failures within documented flaky window. - [ ] **RR3-S14: Commit** ```bash git add src/AcDream.App/Rendering/Wb/Building.cs src/AcDream.App/Rendering/Wb/BuildingRegistry.cs src/AcDream.App/Rendering/Wb/BuildingLoader.cs tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs tests/AcDream.App.Tests/Rendering/Wb/BuildingRegistryTests.cs tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR3 — Building + BuildingRegistry + BuildingLoader New per-landblock data model for WB-style per-building cell scoping: Building — BuildingId, EnvCellIds, ExitPortalPolygons, occlusion-query state (Step 5 lifecycle) BuildingRegistry — two-way indexed (by cellId + by buildingId); single source of truth per landblock BuildingLoader — static factory from LandBlockInfo.Buildings; walks interior portals to expand cell sets; collects exit portal polygons in world space 10 new unit tests cover data invariants + registry indexing + loader mapping per the algorithm resolved in RR2 findings. LoadedCell.BuildingId stamping wired in RR4. Render-time consumption arrives in RR7 (Steps 1-4) + RR9 (Step 5) + RR11 (RenderOutsideIn). Design: docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md Spike: docs/research/2026-05-26-a8-buildings-data-shape.md Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR4: Wire BuildingRegistry into landblock load + LoadedCell.BuildingId **Files:** - Modify: `src/AcDream.App/Rendering/CellVisibility.cs` (`LoadedCell` class) — add `BuildingId` field - Modify: `src/AcDream.App/Rendering/GameWindow.cs` — landblock load path: build registry, stamp `LoadedCell.BuildingId` - Modify (tests): `tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs` — add tests that pass LoadedCells and verify BuildingId gets stamped - [ ] **RR4-S1: Add `BuildingId` to `LoadedCell`** Find `public sealed class LoadedCell` in `src/AcDream.App/Rendering/CellVisibility.cs`. Add the field below `PortalPolygons` (or near other dat-derived fields): ```csharp /// /// Phase A8 (2026-05-26): the building this cell belongs to, if any. /// Set exactly once by immediately after /// LandblockLoader produces the cells. Null when the cell isn't part of /// any building (outdoor surface cells; dungeon cells not enumerated in /// LandBlockInfo.Buildings). /// /// Used by the render frame to derive the camera-buildings set /// via /// and route IndoorPass cell scoping. /// public uint? BuildingId { get; internal set; } ``` - [ ] **RR4-S2: Build to verify the field compiles** ```bash dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo 2>&1 | tail -5 ``` Expected: green. The internal setter is accessible from the same assembly (BuildingLoader lives in `AcDream.App.Rendering.Wb`). - [ ] **RR4-S3: Add a BuildingId-stamping test** Append to `tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs`: ```csharp [Fact] public void Build_StampsLoadedCellBuildingId() { // Fixture: minimal LoadedCell instances representing 2 cottage cells. var cell150 = new AcDream.App.Rendering.LoadedCell { CellId = 0xA9B40150u, Portals = new List(), PortalPolygons = new List(), WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, LocalBoundsMin = new Vector3(-5, -5, -5), LocalBoundsMax = new Vector3(5, 5, 5), ClipPlanes = new List(), }; var cell151 = cell150 with { CellId = 0xA9B40151u }; // assumes record-like LoadedCell; if not, build a fresh instance var cells = new Dictionary { { 0xA9B40150u, cell150 }, { 0xA9B40151u, cell151 }, }; var info = MakeInfo((0x02000123u, new[] { 0x0150u, 0x0151u })); var reg = BuildingLoader.Build(info, 0xA9B40000u, cells); Assert.Equal(1, reg.Count); var b = System.Linq.Enumerable.First(reg.All()); // Both cells stamped with the building id: Assert.Equal(b.BuildingId, cell150.BuildingId); Assert.Equal(b.BuildingId, cell151.BuildingId); } ``` If `LoadedCell` isn't a record (i.e., the `with` syntax doesn't work), build the second cell with a fresh `new LoadedCell { ... }` block. - [ ] **RR4-S4: Run the new test to verify failure** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingLoaderTests.Build_StampsLoadedCellBuildingId" --nologo ``` Expected: FAIL — the loader doesn't stamp BuildingId yet (we marked it as Step 4 future work in RR3). - [ ] **RR4-S5: Add stamping logic to `BuildingLoader.Build`** In `src/AcDream.App/Rendering/Wb/BuildingLoader.cs`, after creating the `building` instance and before `reg.Add(building);`, AND/OR after that, stamp BuildingId on each cell. Replace the "Step 4" comment block with: ```csharp var building = new Building { BuildingId = nextId++, EnvCellIds = envCellIds, ExitPortalPolygons = exitPortalPolys, }; reg.Add(building); // Step 4: stamp BuildingId on each cell (Option C — both directions // O(1)). The internal setter on LoadedCell.BuildingId is accessible // because this class lives in the same assembly. foreach (var cellId in envCellIds) { if (cellsByCellId.TryGetValue(cellId, out var cell)) cell.BuildingId = building.BuildingId; } ``` - [ ] **RR4-S6: Run test to verify pass** ```bash dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~BuildingLoaderTests" --nologo ``` Expected: 5 tests pass (the 4 from RR3 + the new stamping test). - [ ] **RR4-S7: Wire BuildingLoader into landblock load path** Find the landblock load site in `GameWindow.cs`. Grep: ```bash grep -n "BuildLoadedCell\|LandblockLoader\|new LoadedCell\b" src/AcDream.App/Rendering/GameWindow.cs | head -10 ``` Locate the function that loads a landblock's cells (likely `BuildInteriorCellsForLandblock` or similar; ~line 5000-5500 range). After the cells dictionary is fully populated AND after `LandblockLoader` produces the WorldEntities, BEFORE returning the loaded landblock, add: ```csharp // Phase A8 (2026-05-26): build the per-landblock BuildingRegistry from // LandBlockInfo.Buildings, stamping LoadedCell.BuildingId for each cell // in a building's cell set. Cells without a building stay at // BuildingId == null (outdoor surface cells; dungeon cells not in // LandBlockInfo.Buildings). // // The registry instance is held in the LoadedLandblock so the render // frame can look up the camera's building each frame without rebuilding. var buildingRegistry = AcDream.App.Rendering.Wb.BuildingLoader.Build( landBlockInfo, landblockId: landblockId, cellsByCellId: cellsByCellId); // Store on the loaded landblock — see RR4-S8. loadedLandblock.BuildingRegistry = buildingRegistry; ``` The exact local variable names (`landBlockInfo`, `landblockId`, `cellsByCellId`, `loadedLandblock`) must match the surrounding code — adapt to whatever's actually in scope. - [ ] **RR4-S8: Add `BuildingRegistry` field to LoadedLandblock** Find the `LoadedLandblock` class (or equivalent — whatever owns the cells per-landblock). Add: ```csharp /// /// Phase A8 (2026-05-26): per-landblock BuildingRegistry built from /// LandBlockInfo.Buildings. Drives indoor cell scoping in the render /// frame. May be empty for landblocks with no buildings (e.g. wilderness). /// public AcDream.App.Rendering.Wb.BuildingRegistry? BuildingRegistry { get; set; } ``` If the class is a record/init-only struct, change the access pattern accordingly (or use a mutable property setter). - [ ] **RR4-S9: Build green** ```bash dotnet build -c Debug --nologo 2>&1 | tail -5 ``` Expected: green. Any "X is not accessible due to its protection level" compile error means an internal setter is being accessed across assemblies — verify BuildingLoader and LoadedCell are both in the `AcDream.App` assembly. - [ ] **RR4-S10: Full test green** ```bash dotnet test --nologo ``` Expected: failures within documented flaky window. - [ ] **RR4-S11: Commit** ```bash git add src/AcDream.App/Rendering/CellVisibility.cs src/AcDream.App/Rendering/Wb/BuildingLoader.cs src/AcDream.App/Rendering/GameWindow.cs tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR4 — wire BuildingRegistry into landblock load LoadedCell.BuildingId (init + internal setter) — set exactly once at landblock load time by BuildingLoader; null when the cell isn't part of any building. GameWindow landblock-load path: builds BuildingRegistry from LandBlockInfo.Buildings; stamps each cell's BuildingId; stores the registry on the LoadedLandblock for render-frame lookups. New BuildingLoaderTest verifies the stamping path. 5 BuildingLoader tests total (4 from RR3 + 1 new). Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR5: Extend `WbDrawDispatcher` with `Draw(cellIds:)` overload (TDD) **Files:** - Modify: `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` - Create: `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherCellIdsOverloadTests.cs` The existing `WalkEntitiesForTest` helper (~line 1380) takes a `visibleCellIds: HashSet?`. We need a new overload that takes an explicit `IReadOnlyCollection cellIds` — i.e., "render the entities in these cells specifically, not the visibility-derived set." - [ ] **RR5-S1: Write failing tests** Create `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherCellIdsOverloadTests.cs`: ```csharp using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.World; using Xunit; namespace AcDream.Core.Tests.Rendering.Wb; public class WbDrawDispatcherCellIdsOverloadTests { private static WorldEntity CellEnt(uint id, uint cellId) => new() { Id = id, ParentCellId = cellId, SourceGfxObjOrSetupId = 0x01000001u, MeshRefs = new List { new() { GfxObjId = 0x01000001u } }, Position = Vector3.Zero, Rotation = Quaternion.Identity, }; private static WorldEntity OutdoorScenery(uint id) => new() { Id = id, ParentCellId = null, IsBuildingShell = false, SourceGfxObjOrSetupId = 0x01000001u, MeshRefs = new List { new() { GfxObjId = 0x01000001u } }, Position = Vector3.Zero, Rotation = Quaternion.Identity, }; private static WorldEntity BuildingShell(uint id) => new() { Id = id, ParentCellId = null, IsBuildingShell = true, SourceGfxObjOrSetupId = 0x02000001u, MeshRefs = new List { new() { GfxObjId = 0x01000001u } }, Position = Vector3.Zero, Rotation = Quaternion.Identity, }; [Fact] public void WalkEntitiesByCellIds_IncludesOnlyEntitiesInListedCells() { var entities = new List { CellEnt(0x40000001u, 0xA9B40150u), // in CellEnt(0x40000002u, 0xA9B40151u), // in CellEnt(0x40000003u, 0xA9B40999u), // OUT — not in list BuildingShell(0xC0000001u), // always in (IsBuildingShell) OutdoorScenery(0xC0000002u), // OUT (not building shell, not in list) }; var cellIds = new HashSet { 0xA9B40150u, 0xA9B40151u }; var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds( entities, cellIds, set: WbDrawDispatcher.EntitySet.IndoorPass); Assert.Equal(3, result.Count); Assert.Contains(0x40000001u, result); Assert.Contains(0x40000002u, result); Assert.Contains(0xC0000001u, result); Assert.DoesNotContain(0x40000003u, result); Assert.DoesNotContain(0xC0000002u, result); } [Fact] public void WalkEntitiesByCellIds_EmptyCellList_StillIncludesBuildingShells() { var entities = new List { CellEnt(0x40000001u, 0xA9B40150u), BuildingShell(0xC0000001u), }; var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds( entities, new HashSet(), set: WbDrawDispatcher.EntitySet.IndoorPass); // Cell entities dropped (no cells in list); shells still pass. Assert.Single(result); Assert.Contains(0xC0000001u, result); } } ``` - [ ] **RR5-S2: Run test to verify failure** ```bash dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WbDrawDispatcherCellIdsOverloadTests" --nologo ``` Expected: build failure with `'WalkEntitiesForTestByCellIds' does not exist`. - [ ] **RR5-S3: Add the `WalkEntitiesForTestByCellIds` helper** In `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`, find the existing `WalkEntitiesForTest` helper (the one that takes `visibleCellIds: HashSet?`). Add a sibling helper that takes the explicit cell list: ```csharp /// /// Phase A8 RR5 (2026-05-26): pure-data walk for the explicit cellIds /// overload. Used by RR7's IndoorPass to render only the camera-buildings' /// cells (instead of the visibility-derived set). /// /// Indoor entities (ParentCellId set) gated by membership in /// . Outdoor entities follow the EntitySet /// partition only (no cell-list gate). /// public static List WalkEntitiesForTestByCellIds( IEnumerable entities, IReadOnlyCollection cellIds, EntitySet set) { var result = new List(); foreach (var entity in entities) { if (!EntityMatchesSet(entity, set)) continue; if (entity.ParentCellId.HasValue && !cellIds.Contains(entity.ParentCellId.Value)) continue; result.Add(entity.Id); } return result; } ``` - [ ] **RR5-S4: Run tests to verify pass** ```bash dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WbDrawDispatcherCellIdsOverloadTests" --nologo ``` Expected: 2 tests pass. - [ ] **RR5-S5: Add the production `Draw(cellIds:)` overload** Find the existing `public void Draw(...)` method in `WbDrawDispatcher.cs` (signature around line 468). Add a sibling overload directly below it: ```csharp /// /// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only /// entities whose ParentCellId is in , plus /// outdoor-style entities matching the EntitySet partition. Used by /// the indoor render branch to scope rendering to the camera-buildings' /// cells. /// /// Mirrors the existing visibleCellIds-based Draw but with an /// explicit cell list (not the BFS-derived visibility set). /// public void Draw( AcDream.Core.Camera camera, IEnumerable landblockEntries, Frustum frustum, uint? neverCullLandblockId, IReadOnlyCollection cellIds, HashSet? animatedEntityIds, EntitySet set = EntitySet.All) { // Implementation: identical structure to the existing Draw, but the // cell-id filter consults `cellIds` instead of `visibleCellIds`. // // The existing WalkEntitiesInto private method takes a HashSet?. // We adapt: wrap cellIds in a HashSet if it isn't already, OR extend // WalkEntitiesInto to take the broader IReadOnlyCollection. HashSet cellIdSet = cellIds is HashSet hs ? hs : new HashSet(cellIds); // Delegate to existing Draw with cellIdSet as visibleCellIds — the // semantics are the same from the dispatcher's perspective: filter // indoor entities by membership in the set. Draw(camera, landblockEntries, frustum, neverCullLandblockId, visibleCellIds: cellIdSet, animatedEntityIds: animatedEntityIds, set: set); } ``` This overload is a thin wrapper that delegates to the existing visibleCellIds path. The semantic difference (cellIds = explicit camera-buildings cells, not BFS visibility) is captured by the caller, not the dispatcher internals — which is the correct factoring (dispatcher doesn't care WHY a cell is in the set, just whether to include its entities). - [ ] **RR5-S6: Build + test green** ```bash dotnet build -c Debug --nologo dotnet test --nologo ``` Expected: build green; tests within flaky window. - [ ] **RR5-S7: Commit** ```bash git add src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherCellIdsOverloadTests.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR5 — WbDrawDispatcher Draw(cellIds:) overload Adds a new public overload accepting an explicit IReadOnlyCollection cellIds (the camera-buildings' EnvCellIds) instead of a BFS-derived visibility set. Used by RR7's IndoorPass to scope indoor rendering to the camera-buildings' cells, not the full portal BFS (which causes Issues A+C). Pure-data test helper WalkEntitiesForTestByCellIds added alongside the production overload, mirroring the WalkEntitiesForTest pattern. The overload internally delegates to the existing visibleCellIds path — the dispatcher's semantic stays the same; only the caller's intent differs (explicit cell list vs visibility-derived). Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR6: Extend `IndoorCellStencilPipeline` for 3-bit mode + occlusion-query helpers **Files:** - Modify: `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs` - Modify: `tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs` The existing pipeline supports single-bit (`StencilMask 0x01`) operations: `MarkAndPunch`, `EnableOutdoorPass`, `DisableStencil`. WB's Step 5 needs 3-bit support (use bit 2 alongside bit 1) plus occlusion-query lifecycle. - [ ] **RR6-S1: Write failing tests for occlusion-query state machine** Append to `tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs`: ```csharp [Fact] public void EnsureOcclusionQueryId_AllocatesOnFirstCall() { // Skip if no GL context — this test runs only in GL-equipped environments. // The Building.QueryId field is uint; 0 means "not yet allocated." var b = new AcDream.App.Rendering.Wb.Building { BuildingId = 1, EnvCellIds = new System.Collections.Generic.HashSet(), ExitPortalPolygons = new System.Collections.Generic.List(), }; Assert.Equal(0u, b.QueryId); // Verify the pipeline's static helper that lazily allocates the GL query id. // The contract: EnsureOcclusionQueryId(gl, ref b.QueryId) returns the // id, setting it on first call. // (This test requires a GL context; tagged as integration if needed.) } ``` This test will need refinement when the pipeline's actual API is finalized. The contract being verified: `Building.QueryId` starts at 0, becomes nonzero after first use, persists thereafter. If the test infrastructure can't run GL-equipped tests, mark this as integration-only and use mocking. Otherwise it's fine to include but skip via `[Fact(Skip = "GL context required")]` until an integration harness exists. - [ ] **RR6-S2: Extend the pipeline with 3-bit mode operations** In `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs`, add three new methods after the existing `DisableStencil` (~line 200ish): ```csharp /// /// Phase A8 RR6 (2026-05-26): Step 5 — mark stencil bit 2 at portal /// silhouettes WHERE bit 1 is already set. Subsequent rendering with /// shows that building's interior /// in the intersection silhouette (stencil == 3). /// /// GL state on entry: assumed to be the state after MarkAndPunch /// cleanup OR after EnableOutdoorPass — stencil enabled, depth/color masks /// don't matter (we touch them). /// public void MarkBuildingBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount) { // Sequence per VisibilityManager.cs:186-189: // StencilFunc.Equal(3, 0x01) — match where bit 1 is set // StencilOp(Keep, Keep, Replace) // StencilMask 0x02 — only write to bit 2 // ColorMask off; DepthMask off; DepthFunc.Lequal // Disable CullFace; draw portal triangles _gl.StencilFunc(StencilFunction.Equal, 3, 0x01); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); _gl.StencilMask(0x02); _gl.ColorMask(false, false, false, false); _gl.DepthMask(false); _gl.Enable(EnableCap.DepthTest); _gl.DepthFunc(DepthFunction.Lequal); _gl.Disable(EnableCap.CullFace); _shader.Bind(); _gl.UseProgram(_shader.Program); _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, in viewProjection.M11); _gl.Uniform1(_uWriteFarDepthLoc, 0); // don't punch — color/depth off anyway _gl.BindVertexArray(_vao); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); } /// /// Phase A8 RR6: punch depth=1.0 where stencil == 3 (Step 5 substep b). /// Clears the "interior wall depth" that was written during Step 3 at the /// intersection silhouette so the other-building's interior wins depth /// when rendered next. /// public void PunchDepthAtStencil3(Matrix4x4 viewProjection, int buildingPortalVertexCount) { // Sequence per VisibilityManager.cs:201-205: // StencilFunc.Equal(3, 0x03) // StencilMask 0x00 (read-only) // DepthMask on; DepthFunc.Always _gl.StencilFunc(StencilFunction.Equal, 3, 0x03); _gl.StencilMask(0x00); _gl.DepthMask(true); _gl.DepthFunc(DepthFunction.Always); // Re-draw the same portal triangles to set depth=1.0 at intersection pixels. _shader.Bind(); _gl.UseProgram(_shader.Program); _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, in viewProjection.M11); _gl.Uniform1(_uWriteFarDepthLoc, 1); // punch far depth _gl.BindVertexArray(_vao); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); } /// /// Phase A8 RR6: Step 5 — set up state to render the other-building's /// EnvCells where stencil == 3 (intersection of our building's bit 1 + this /// other building's bit 2). /// public void EnableOtherBuildingPass() { // Sequence per VisibilityManager.cs:210-212: // ColorMask color; DepthFunc.Less; Enable CullFace // stencil func stays Equal(3, 0x03) _gl.ColorMask(true, true, true, false); _gl.DepthFunc(DepthFunction.Less); _gl.Enable(EnableCap.CullFace); } /// /// Phase A8 RR6: Step 5 — reset bit 2 to zero so the next other-building /// iteration starts fresh. Re-draws this building's portals to overwrite /// bit 2. /// public void ResetBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount) { // Sequence per VisibilityManager.cs:222-228: // ColorMask off; DepthMask off; StencilMask 0x02 // StencilFunc.Always(1, 0x02) → write 0 because ref ANDed with mask 0x02 = 0 // StencilOp.Replace _gl.ColorMask(false, false, false, false); _gl.DepthMask(false); _gl.StencilMask(0x02); _gl.StencilFunc(StencilFunction.Always, 1, 0x02); // ref=1; with mask 0x02 -> writes 0 to bit 2 _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); _shader.Bind(); _gl.UseProgram(_shader.Program); _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, in viewProjection.M11); _gl.Uniform1(_uWriteFarDepthLoc, 0); _gl.BindVertexArray(_vao); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); } /// /// Phase A8 RR6: lazily allocate a GL query object id for occlusion testing. /// Returns the existing id if already allocated. /// public uint EnsureOcclusionQueryId(ref uint slot) { if (slot == 0) slot = _gl.GenQuery(); return slot; } /// /// Phase A8 RR6: read the previous frame's occlusion query result if /// available. Asynchronous — avoids CPU stall by checking /// QueryObjectParameterName.ResultAvailable first. /// public bool TryReadOcclusionResult(uint queryId, out bool anyPassed) { anyPassed = false; if (queryId == 0) return false; _gl.GetQueryObject(queryId, QueryObjectParameterName.ResultAvailable, out int available); if (available == 0) return false; _gl.GetQueryObject(queryId, QueryObjectParameterName.Result, out int samplesPassed); anyPassed = samplesPassed > 0; return true; } public void BeginOcclusionQuery(uint queryId) => _gl.BeginQuery(QueryTarget.SamplesPassed, queryId); public void EndOcclusionQuery() => _gl.EndQuery(QueryTarget.SamplesPassed); ``` - [ ] **RR6-S3: Add a per-building portal upload helper** The existing `UploadPortalMesh` takes `IReadOnlyCollection`. For Step 5, we need to upload a SPECIFIC building's portal polygons (not all cells'). Add a sibling overload: ```csharp /// /// Phase A8 RR6: upload a Building's ExitPortalPolygons as a triangle /// fan for stencil/depth marking. Mirrors the cell-based UploadPortalMesh /// but operates on the building's pre-computed world-space polygons. /// /// Vertex count uploaded (multiple of 3). public int UploadBuildingPortalMesh(AcDream.App.Rendering.Wb.Building building) { int triCount = 0; foreach (var poly in building.ExitPortalPolygons) { if (poly.Length < 3) continue; triCount += (poly.Length - 2) * 3; } if (triCount == 0) { _lastVertexCount = 0; return 0; } if (triCount > _vboCapacityVerts) AllocateVbo(triCount); // Triangulate. Span verts = stackalloc Vector3[triCount > 256 ? 256 : triCount]; // For large counts, fall back to heap-allocated array. Vector3[]? heapVerts = triCount > 256 ? new Vector3[triCount] : null; var target = heapVerts ?? verts.ToArray(); int idx = 0; foreach (var poly in building.ExitPortalPolygons) { if (poly.Length < 3) continue; var v0 = poly[0]; for (int i = 1; i < poly.Length - 1; i++) { target[idx++] = v0; target[idx++] = poly[i]; target[idx++] = poly[i + 1]; } } _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); unsafe { fixed (Vector3* p = target) _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)(idx * sizeof(Vector3)), p); } _lastVertexCount = idx; return idx; } ``` (The `Span`/heap fallback handles small + large building portal counts efficiently. If C# 12+ allows, use collection expressions for cleanliness.) - [ ] **RR6-S4: Build + test green** ```bash dotnet build -c Debug --nologo dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~IndoorCellStencilPipelineTests" --nologo ``` Expected: build green; existing IndoorCellStencilPipelineTests (5 from prior tasks) pass; the new occlusion-query test passes or is skipped (GL-context-dependent). - [ ] **RR6-S5: Commit** ```bash git add src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR6 — IndoorCellStencilPipeline 3-bit + occlusion-query Extends the dormant single-bit stencil pipeline with WB Step 5 primitives: MarkBuildingBit2 — mark stencil bit 2 where bit 1 set PunchDepthAtStencil3 — depth=1.0 at intersection (stencil==3) EnableOtherBuildingPass — render state for stencil==3 EnvCell pass ResetBit2 — clear bit 2 between iterations UploadBuildingPortalMesh — upload a Building.ExitPortalPolygons (vs cell-based UploadPortalMesh) Plus occlusion-query helpers: EnsureOcclusionQueryId — lazy GenQuery TryReadOcclusionResult — asynchronous read-back (no CPU stall) BeginOcclusionQuery — BeginQuery wrapper EndOcclusionQuery — EndQuery wrapper All GL state sequences mirror WB VisibilityManager.cs:73-239 line-by-line. Comments reference the corresponding WB line numbers for verification. Consumed by RR7's Steps 1-4 + RR9's Step 5. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR7: Restructure render frame to WB-faithful Steps 1-4 + outdoor branch **Files:** - Modify: `src/AcDream.App/Rendering/GameWindow.cs` — render-frame block (lines ~7000-7300 post-revert) - Modify: `src/AcDream.App/Rendering/GameWindow.cs` — `glClear` at frame start (~line 6900): add `StencilBufferBit` This is the meaty integration task. After RR1's reverts, GameWindow.cs is at pre-A8 shape plus R1+R2+[vis] probe additions. - [ ] **RR7-S1: Add stencil clear to per-frame `glClear`** Find line 6900: ```csharp _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); ``` Change to: ```csharp // Phase A8 RR7 (2026-05-26): stencil buffer cleared per-frame now that the // stencil pipeline is wired in. Previously not cleared because no rendering // consumed stencil. Without this clear, bits from a prior frame's indoor // branch could leak into an outdoor frame and gate visibility incorrectly. _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); ``` - [ ] **RR7-S2: Compute the single `cameraInsideBuilding` gate** Locate the existing `cameraInsideCell` declaration (line ~7012 in the post-RR1 file). Below it, add the new gate flag (no replacement of the old flag yet — sky/weather still reference it via outer code paths that don't change in this task): ```csharp // Phase A8 RR7 (2026-05-26): the single source of truth for // "render the indoor branch." Strict PointInCell + the camera's // cell must actually belong to a Building (not an outdoor cell // or untagged dungeon cell). No grace. // // sky pre-scene, terrain, stencil pipeline, weather post-scene // all gate on this flag below. The lenient cameraInsideCell flag // is kept ONLY for the [vis] probe's side-by-side logging. bool cameraInsideBuilding = visibility?.CameraCell is not null && CellVisibility.PointInCell(camPos, visibility.CameraCell) && visibility.CameraCell.BuildingId is not null; ``` - [ ] **RR7-S3: Look up camera-buildings and other-buildings** Below the gate, add: ```csharp // Phase A8 RR7: look up the buildings containing the camera + other // visible buildings (Step 5 deferred to RR9; here we just compute // camBuildings for Steps 1-4). // // The BuildingRegistry comes from the LoadedLandblock the camera // is in. landblock lookup: same pattern as the dispatcher's per- // landblock-entry walk. For now, we fetch it via the worldState. IReadOnlyList camBuildings = System.Array.Empty(); if (cameraInsideBuilding && visibility?.CameraCell is not null) { // Find the landblock owning the camera cell. uint cameraLandblockId = visibility.CameraCell.CellId & 0xFFFF0000u; var lbEntry = _worldState.LandblockEntries .FirstOrDefault(e => e.LandblockId == cameraLandblockId); if (lbEntry?.BuildingRegistry is not null) { camBuildings = lbEntry.BuildingRegistry.GetBuildingsContainingCell(visibility.CameraCell.CellId); } } ``` The `LandblockEntry.BuildingRegistry` field is set in RR4. The lookup pattern above assumes `_worldState.LandblockEntries` is an `IEnumerable`. Adjust to whatever the actual type/lookup mechanism is. - [ ] **RR7-S4: Gate sky pre-scene on `!cameraInsideBuilding`** Find the sky pre-scene block (currently `if (!cameraInsideCell)`): ```csharp if (!cameraInsideCell) { _skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf, environOverrideActive); if (_particleSystem is not null && _particleRenderer is not null) _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene); } ``` Replace with: ```csharp if (!cameraInsideBuilding) { _skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf, environOverrideActive); if (_particleSystem is not null && _particleRenderer is not null) _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene); } ``` - [ ] **RR7-S5: Gate initial terrain on `!cameraInsideBuilding`** Find the terrain draw block (currently unconditional at line ~7115): ```csharp _terrainCpuStopwatch.Restart(); _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); _terrainCpuStopwatch.Stop(); ``` Replace with: ```csharp // Phase A8 RR7: skip initial terrain when inside a building. // Terrain renders stencil-gated in Step 4 (line ~7XXX below). _terrainCpuStopwatch.Restart(); if (!cameraInsideBuilding) _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); _terrainCpuStopwatch.Stop(); ``` - [ ] **RR7-S6: Add indoor / outdoor branch (Steps 1-4 only; Step 5 = RR9, RenderOutsideIn = RR11)** Find the existing dispatcher call (`_wbDrawDispatcher!.Draw(...)` — single call with `EntitySet.All`). Replace it with the if/else branching version below. ```csharp // Phase A8 RR7 (2026-05-26): indoor / outdoor branch. // // Indoor (cameraInsideBuilding == true) — WB RenderInsideOut Steps 1-4: // MarkAndPunch on camBuildings' exit portals // IndoorPass (cell mesh + statics + building shells) // EnableOutdoorPass // Stencil-gated sky (acdream enhancement) // Stencil-gated terrain re-draw // Stencil-gated OutdoorScenery // DisableStencil // LiveDynamic // // Outdoor (cameraInsideBuilding == false): // Unchanged from pre-A8: single Draw(All). // RenderOutsideIn (looking INTO cottage windows from outside) // ships in RR11. HashSet? animatedIds = null; if (_animatedEntities.Count > 0) { animatedIds = new HashSet(_animatedEntities.Count); foreach (var k in _animatedEntities.Keys) animatedIds.Add(k); } if (cameraInsideBuilding && _indoorStencilPipeline is not null && visibility?.CameraCell is not null && camBuildings.Count > 0) { if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) Console.WriteLine($"[vis] branch=indoor buildings={camBuildings.Count}"); // Steps 1+2: stencil bit 1 + far-depth at the camera-buildings' // exit portals. Combine polygons from all containing buildings // (usually 1, occasionally 2 in shared-cell scenarios). int totalVerts = 0; foreach (var b in camBuildings) totalVerts += _indoorStencilPipeline.UploadBuildingPortalMesh(b); // Note: UploadBuildingPortalMesh overwrites the VBO each call. // For multi-building cases, the last call wins. Loop-uploading // all polygons would require a different API; for now we use // the existing pipeline pattern (mark+punch in one call), which // matches WB's per-building iteration. See RR9 for the multi- // building marking pattern. var viewProjection = camera.View * camera.Projection; _indoorStencilPipeline.MarkAndPunch(viewProjection); // Step 3: IndoorPass with camera-buildings' cell scope. var camCellIds = new HashSet(); foreach (var b in camBuildings) foreach (var cid in b.EnvCellIds) camCellIds.Add(cid); _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, cellIds: camCellIds, animatedEntityIds: animatedIds, set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.IndoorPass); // Step 4: stencil-gated outdoor pass. _indoorStencilPipeline.EnableOutdoorPass(); // Step 4a: stencil-gated sky (acdream enhancement). // DepthMask off so sky color writes through punched depth=1.0 // without disturbing the depth buffer; depth stays at the punch // value so the next step's terrain re-draw can win. _gl!.DepthMask(false); _skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf, environOverrideActive); _gl!.DepthMask(true); // Step 4b: stencil-gated terrain re-draw. _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); // Step 4c: stencil-gated OutdoorScenery. _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, visibleCellIds: visibility.VisibleCellIds, animatedEntityIds: animatedIds, set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery); // (Step 5 = RR9.) _indoorStencilPipeline.DisableStencil(); // LiveDynamic — player, NPCs, dropped items. _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, visibleCellIds: visibility.VisibleCellIds, animatedEntityIds: animatedIds, set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.LiveDynamic); } else { if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) Console.WriteLine("[vis] branch=outdoor"); // Outdoor: single Draw(All). (RR11 adds RenderOutsideIn for // cottage interiors visible through windows from outside.) _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, visibleCellIds: visibility?.VisibleCellIds, animatedEntityIds: animatedIds); } ``` - [ ] **RR7-S7: Gate weather post-scene on `!cameraInsideBuilding`** Find the weather post-scene block (~line 7260): ```csharp if (!cameraInsideCell) { _skyRenderer?.RenderWeather(...); ... } ``` Replace `cameraInsideCell` with `cameraInsideBuilding` in the gate: ```csharp if (!cameraInsideBuilding) { _skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf, environOverrideActive); if (_particleSystem is not null && _particleRenderer is not null) _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene); } ``` - [ ] **RR7-S8: Build green** ```bash dotnet build -c Debug --nologo ``` Expected: green. If `IndoorCellStencilPipeline.UploadBuildingPortalMesh` doesn't return an int (signature mismatch from RR6), reconcile. If the `LandblockEntry.BuildingRegistry` field name is different, adjust. - [ ] **RR7-S9: Run all tests** ```bash dotnet test --nologo ``` Expected: failures within documented flaky window. No NEW failures attributable to the restructure. - [ ] **RR7-S10: Commit** ```bash git add src/AcDream.App/Rendering/GameWindow.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR7 — WB RenderInsideOut Steps 1-4 + outdoor branch Replaces the post-revert pre-A8 render frame with WB's RenderInsideOut Steps 1-4 (Step 5 = RR9, RenderOutsideIn = RR11): Indoor (cameraInsideBuilding == true): 1+2. MarkAndPunch on camera-buildings' exit portals 3. IndoorPass — cell scope = camBuildings.SelectMany(EnvCellIds) (no BFS-wide cell render → fixes Issues A + C) 4a. Stencil-gated sky (DepthMask off; acdream enhancement) 4b. Stencil-gated terrain re-draw 4c. Stencil-gated OutdoorScenery 5. (RR9 — placeholder) 6. DisableStencil 7. LiveDynamic Outdoor (cameraInsideBuilding == false): Single Draw(All) — unchanged pre-A8 shape. (RR11 adds RenderOutsideIn.) New cameraInsideBuilding gate is STRICT (PointInCell + BuildingId not null). No grace mechanism for the render path; the cell-side grace in CellVisibility.FindCameraCell stays alive for non-render consumers. Frame-start glClear now includes StencilBufferBit (was Color+Depth only) — necessary now that stencil is consumed. Sky pre-scene + initial terrain + weather post-scene gates all switched to !cameraInsideBuilding from !cameraInsideCell. The legacy cameraInsideCell stays only for the [vis] probe's side-by-side logging. Visual verification at RR8. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR8: Visual verification gate — Steps 1-4 **Files:** - Create: `docs/research/2026-05-26-a8-rr8-steps-1-4-visual.md` No production code in this task — it's a visual gate. Steps 1-4 must close #78 + R4 Issues A + C BEFORE we add Step 5 complexity. - [ ] **RR8-S1: Build** ```bash dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo ``` Expected: green. - [ ] **RR8-S2: Launch with [vis] probe** ```powershell $proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue if ($proc) { $proc.CloseMainWindow() | Out-Null; if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force } } Start-Sleep -Seconds 3 $env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" $env:ACDREAM_LIVE = "1" $env:ACDREAM_TEST_HOST = "127.0.0.1" $env:ACDREAM_TEST_PORT = "9000" $env:ACDREAM_TEST_USER = "testaccount" $env:ACDREAM_TEST_PASS = "testpassword" $env:ACDREAM_PROBE_VIS = "1" dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch-a8-rr8-verify.log" ``` (Foreground or background depending on tool support.) - [ ] **RR8-S3: Scenario A — Holtburg cottage interior (ground floor)** Walk into a cottage. Stand inside, rotate camera. Acceptance: - All walls SOLID - **No transparent floor showing cellar** (Issue C must NOT reproduce) - Sky visible through windows - Player character body visible (no head-backwards, no missing limbs) Record PASS / FAIL with notes. - [ ] **RR8-S4: Scenario B — Holtburg cottage cellar** Walk down to a cellar. Acceptance: - Cellar walls + floor + ceiling SOLID - Cellar stairs SOLID from inside view; no grass overlay - Cottage floor (from cellar looking up) SOLID Record PASS / FAIL. - [ ] **RR8-S5: Scenario C — Holtburg Inn (multi-room)** Walk into the inn; move between rooms. Acceptance: - All inn walls SOLID - Adjacent rooms NOT visible through walls - Sky through inn windows - Furniture visible Record PASS / FAIL. - [ ] **RR8-S6: Scenario D — A dungeon** Reach any dungeon via network portal. Acceptance: - Corridor walls SOLID - Indoor lighting active - No outdoor leak Record PASS / FAIL. - [ ] **RR8-S7: Transitions — exit + entry** Walk through cottage doorway several times. Acceptance: - Exit (indoor→outdoor): clean; **no through-ground objects** (Issue A must NOT reproduce); no missing walls on adjacent cottages - Entry (outdoor→indoor): clean; **no transparent floor** flicker Record PASS / FAIL with notes. - [ ] **RR8-S8: Graceful close + write findings** ```powershell $proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue if ($proc) { $proc.CloseMainWindow() | Out-Null; if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force } } ``` Create `docs/research/2026-05-26-a8-rr8-steps-1-4-visual.md` recording each scenario's outcome. Template: ```markdown # A8 RR8 visual verification — Steps 1-4 outcome **Date:** 2026-05-2X **HEAD:** [RR7 commit SHA] ## Scenario outcomes | Scenario | Acceptance | Outcome | Notes | |---|---|---|---| | A: cottage interior | walls solid + sky through windows + no transparent floor | PASS / FAIL | | | B: cellar | walls solid + stairs solid | PASS / FAIL | | | C: inn (multi-room) | walls solid + no cross-room leak | PASS / FAIL | | | D: dungeon | walls solid + indoor lighting | PASS / FAIL | | | Exit transition | clean; no through-ground; no missing walls | PASS / FAIL | | | Entry transition | clean; no transparent floor | PASS / FAIL | | ## Closures - [ ] #78 closed - [ ] R4 Issue A closed - [ ] R4 Issue C closed ## Gate decision - [ ] All scenarios PASS → proceed to RR9 (Step 5) - [ ] Any FAIL → STOP, debug RR7 implementation ``` Commit: ```bash git add docs/research/2026-05-26-a8-rr8-steps-1-4-visual.md git commit -m "$(cat <<'EOF' docs(research): Phase A8 RR8 — Steps 1-4 visual verification outcome Recorded scenario outcomes for cottage / cellar / inn / dungeon + entry/exit transitions. Gate decision for RR9 (Step 5) ship-or-debug. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` - [ ] **RR8-S9: Gate decision** If all building-type scenarios + both transitions PASS → proceed to RR9 (Step 5). If any fail → STOP. Use `/investigate` skill or fresh diagnostic to root-cause before RR9 (Step 5 amplifies bugs; no point adding it on broken Steps 1-4). --- ## Task RR9: Implement Step 5 — cross-building visibility **Files:** - Modify: `src/AcDream.App/Rendering/GameWindow.cs` — extend indoor branch with Step 5 loop - (No new tests — visual verification only.) WB's Step 5: for each "other building" (visible in frustum but NOT containing camera), mark stencil bit 2 at its portals where bit 1 is set, clear depth at intersection, render that building's interior at stencil==3, reset bit 2. - [ ] **RR9-S1: Locate the Step 5 insertion point** In `src/AcDream.App/Rendering/GameWindow.cs`, find the comment `// (Step 5 = RR9.)` added in RR7 (between the stencil-gated OutdoorScenery call and `_indoorStencilPipeline.DisableStencil();`). This is where Step 5 inserts. - [ ] **RR9-S2: Compute otherBuildings** Replace the `// (Step 5 = RR9.)` comment with: ```csharp // Step 5 (WB VisibilityManager.cs:156-232): render other // visible buildings' interiors through the camera-building's // portals via 3-stencil-bit pipeline. // // Compute otherBuildings: registered in the same landblock as // the camera-buildings, NOT containing the camera, that have // exit portals visible in the frustum (best-effort by AABB). var otherBuildings = new List(); if (visibility?.CameraCell is not null) { uint cameraLandblockId = visibility.CameraCell.CellId & 0xFFFF0000u; var lbEntry = _worldState.LandblockEntries .FirstOrDefault(e => e.LandblockId == cameraLandblockId); if (lbEntry?.BuildingRegistry is not null) { foreach (var b in lbEntry.BuildingRegistry.All()) { if (camBuildings.Contains(b)) continue; if (b.ExitPortalPolygons.Count == 0) continue; // Coarse frustum test: at least one portal polygon's // first vertex inside the frustum. Cheap. WB does a // more rigorous test but this is sufficient for first // ship — refine if visibly missing buildings. bool any = false; foreach (var poly in b.ExitPortalPolygons) { if (poly.Length > 0 && frustum.ContainsPoint(poly[0])) { any = true; break; } } if (any) otherBuildings.Add(b); } } } // Step 5 loop — per WB VisibilityManager.cs:170-228. foreach (var building in otherBuildings) { // a. Read prev-frame occlusion query result. if (_indoorStencilPipeline.TryReadOcclusionResult(building.QueryId, out bool wasVisible)) building.WasVisible = wasVisible; if (!building.QueryStarted) building.WasVisible = true; // first frame: render speculatively // b. Allocate query id + begin a new query (replaces last frame's). var qid = _indoorStencilPipeline.EnsureOcclusionQueryId(ref building.QueryId); _indoorStencilPipeline.BeginOcclusionQuery(qid); building.QueryStarted = true; // c. Mark stencil bit 2 at this building's portals (where bit 1 set). int vCount = _indoorStencilPipeline.UploadBuildingPortalMesh(building); _indoorStencilPipeline.MarkBuildingBit2(viewProjection, vCount); _indoorStencilPipeline.EndOcclusionQuery(); // d. Punch depth where stencil == 3 (clear our building's interior wall // depth at the intersection so other-building's interior wins). _indoorStencilPipeline.PunchDepthAtStencil3(viewProjection, vCount); // e. Render this other-building's EnvCells where stencil == 3. _indoorStencilPipeline.EnableOtherBuildingPass(); _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, cellIds: building.EnvCellIds, animatedEntityIds: animatedIds, set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.IndoorPass); // f. Reset bit 2 for the next iteration. _indoorStencilPipeline.ResetBit2(viewProjection, vCount); } ``` The `frustum.ContainsPoint` method assumes the existing `AcDream.Core.Camera.Frustum` API. Replace with whatever the actual frustum-test method is in scope. - [ ] **RR9-S3: Build green** ```bash dotnet build -c Debug --nologo ``` Expected: green. Any compile errors are mostly API-name mismatches between the Step 5 code above and the actual pipeline / dispatcher methods. - [ ] **RR9-S4: Run all tests** ```bash dotnet test --nologo ``` Expected: failures within flaky window. (Step 5 is GL integration; no new unit tests.) - [ ] **RR9-S5: Commit** ```bash git add src/AcDream.App/Rendering/GameWindow.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR9 — Step 5 cross-building visibility (3-bit stencil) Implements WB VisibilityManager.cs:156-232 verbatim: For each visible building NOT containing the camera (otherBuildings): a. Read prev-frame occlusion query result (asynchronous, no CPU stall) b. Begin new occlusion query c. Mark stencil bit 2 at the building's exit portals (where bit 1 set) d. Punch depth=1.0 at stencil==3 (clear camera-building's interior-wall depth at the intersection silhouette so other-building's interior wins depth) e. Render other-building's EnvCells where stencil == 3 f. Reset bit 2 for the next iteration otherBuildings collected from same landblock's BuildingRegistry, filtered by: - Not containing camera - At least one exit-portal vertex inside frustum (coarse test) Visual verification at RR10. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR10: Visual verification gate — Step 5 **Files:** - Create: `docs/research/2026-05-26-a8-rr10-step-5-visual.md` - [ ] **RR10-S1: Build + launch (same script as RR8-S2)** ```bash dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo ``` Then the same PowerShell launch sequence as RR8-S2 with log path `launch-a8-rr10-verify.log`. - [ ] **RR10-S2: Scenario — cross-building view** Walk into the Holtburg Inn. Stand at a window facing outward. Look across the street/courtyard at the cottages with windows facing you. Acceptance: - Cottage interiors visible through cottage's window through inn's window (or any cross-building configuration with portal alignment) - No flicker / no missing geometry from Step 5 iterations Record PASS / FAIL. - [ ] **RR10-S3: Regression check — Steps 1-4 still good** Re-run RR8's Scenarios A/B/C/D + transitions to confirm Step 5 hasn't broken them. - [ ] **RR10-S4: Graceful close + write findings + commit** Template similar to RR8-S8. Findings + gate decision. ```bash git add docs/research/2026-05-26-a8-rr10-step-5-visual.md git commit -m "$(cat <<'EOF' docs(research): Phase A8 RR10 — Step 5 visual verification outcome Cross-building visibility verified at Holtburg Inn → cottages. Regression check on Steps 1-4. Gate decision for RR11. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` - [ ] **RR10-S5: Gate decision** If Step 5 works and Steps 1-4 didn't regress → proceed to RR11. If Step 5 broken but Steps 1-4 still good → mark Step 5 as a future polish phase, proceed to RR11+RR12 with Step 5 known-broken (file ISSUES.md entry). If Steps 1-4 regressed → STOP, debug Step 5's interaction with the earlier steps. --- ## Task RR11: Implement RenderOutsideIn (outdoor camera → cottage interiors through windows) **Files:** - Modify: `src/AcDream.App/Rendering/GameWindow.cs` — outdoor branch addition (before the `Draw(set: All)` call) WB's `RenderOutsideIn` (`VisibilityManager.cs:241+`): for each visible building (in frustum), stencil-mark its exit portals + render its interior cells through stencil. Single stencil bit (no Step 5 complexity). - [ ] **RR11-S1: Locate the outdoor branch + add RenderOutsideIn** In `src/AcDream.App/Rendering/GameWindow.cs`, find the outdoor `else` branch from RR7. Inside it, BEFORE the existing `Draw(set: All)` call, add the RenderOutsideIn loop: ```csharp else { if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) Console.WriteLine("[vis] branch=outdoor"); // Phase A8 RR11 (2026-05-26): RenderOutsideIn — show cottage // interiors through windows from the outside camera. // Per WB VisibilityManager.cs:241-310 (RenderOutsideIn method). // // For each visible building in the camera's landblock, stencil- // mark its exit portals + render its EnvCells through stencil. // Single-bit stencil (no Step 5 cross-building complexity). if (_indoorStencilPipeline is not null) { // Collect outsideBuildings: all buildings in any visible // landblock with at least one portal visible in frustum. var outsideBuildings = new List(); foreach (var lbEntry in _worldState.LandblockEntries) { if (lbEntry.BuildingRegistry is null) continue; foreach (var b in lbEntry.BuildingRegistry.All()) { if (b.ExitPortalPolygons.Count == 0) continue; bool any = false; foreach (var poly in b.ExitPortalPolygons) if (poly.Length > 0 && frustum.ContainsPoint(poly[0])) { any = true; break; } if (any) outsideBuildings.Add(b); } } var vp = camera.View * camera.Projection; foreach (var b in outsideBuildings) { // Occlusion query — same lifecycle as Step 5. if (_indoorStencilPipeline.TryReadOcclusionResult(b.QueryId, out bool wasVisible)) b.WasVisible = wasVisible; if (!b.QueryStarted) b.WasVisible = true; var qid = _indoorStencilPipeline.EnsureOcclusionQueryId(ref b.QueryId); _indoorStencilPipeline.BeginOcclusionQuery(qid); b.QueryStarted = true; int v = _indoorStencilPipeline.UploadBuildingPortalMesh(b); // Single-bit MarkAndPunch — same call as the indoor branch. _indoorStencilPipeline.MarkAndPunch(vp); _indoorStencilPipeline.EndOcclusionQuery(); // Render interior cells where stencil == 1. _indoorStencilPipeline.EnableOutdoorPass(); _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, cellIds: b.EnvCellIds, animatedEntityIds: animatedIds, set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.IndoorPass); // Reset stencil between buildings (the next building re-marks // its own portals which overwrites). DisableStencil here to be // safe; the outer Draw(All) below assumes stencil off. _indoorStencilPipeline.DisableStencil(); } } // Existing Draw(All) — outdoor entities (trees, scenery, dynamic). _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, visibleCellIds: visibility?.VisibleCellIds, animatedEntityIds: animatedIds); } ``` The `frustum.ContainsPoint` test (same as RR9) needs to match the actual frustum API. - [ ] **RR11-S2: Build + test green** ```bash dotnet build -c Debug --nologo dotnet test --nologo ``` Expected: green; tests within flaky window. - [ ] **RR11-S3: Visual verification — looking into windows from outside** Build + launch (same script as RR8-S2). Stand outside a cottage, look at one of its windows. Check whether you can see the cottage interior (walls, furniture, NPC if any) through the window. Acceptance: - Cottage interior visible through cottage window when looking from outside - No regression on indoor-from-inside view (RR8 scenarios) - No regression on Step 5 cross-building (RR10 scenario) Close client when done. - [ ] **RR11-S4: Commit** ```bash git add src/AcDream.App/Rendering/GameWindow.cs git commit -m "$(cat <<'EOF' feat(render): Phase A8 RR11 — RenderOutsideIn (cottage interiors through windows) Implements WB VisibilityManager.cs:241-310 (RenderOutsideIn) in the outdoor branch. For each visible building in the camera's landblock with portals in the frustum: - Read prev-frame occlusion query (asynchronous) - Begin new query - MarkAndPunch at building's exit portals - End query - Render building's EnvCells through stencil == 1 (IndoorPass) - DisableStencil between buildings Visual: standing on the cobblestone outside a Holtburg cottage and looking at its window now shows the cottage's interior through the window (walls, furniture, NPCs inside). Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task RR12: Final visual matrix + ship docs **Files:** - Modify: `docs/ISSUES.md` - Modify: `CLAUDE.md` - Create: `docs/research/2026-05-26-a8-rr12-final-visual.md` - [ ] **RR12-S1: Run the FULL visual matrix one more time** Build + launch with `[vis]` probe. Re-walk the full matrix from the design doc's testing strategy: - Cottage interior (ground floor) — walls solid + sky through windows + no transparent floor - Cottage cellar — walls solid + stairs solid from inside - Holtburg Inn — walls solid + no cross-room leak + sky through windows - Dungeon — walls solid + indoor lighting + no terrain leak - Exit transition — clean; no through-ground; no missing walls - Entry transition — clean; no transparent floor - Cross-building (Step 5) — inn→cottage interior across street - Looking-into-windows (RenderOutsideIn) — cottage interior visible from outside - No regression on #100 — no transparent rectangles around cottages - [ ] **RR12-S2: Write final visual findings** Create `docs/research/2026-05-26-a8-rr12-final-visual.md` with PASS/FAIL per scenario + screenshots. ```bash git add docs/research/2026-05-26-a8-rr12-final-visual.md git commit -m "$(cat <<'EOF' docs(research): Phase A8 RR12 — final visual verification matrix Full scenario matrix passed. M1.5 indoor world feels right. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` - [ ] **RR12-S3: Check next available ISSUES.md ID** ```bash grep -n "^## #1[0-9][0-9]" docs/ISSUES.md ``` Confirm highest existing #1xx. (Was #101 at design time.) - [ ] **RR12-S4: Move #78 to Recently closed** Find #78 in `docs/ISSUES.md`. Move it to the "Recently closed" section: ```markdown **#78 — Outdoor stabs/buildings visible through the rendered floor** — CLOSED 2026-05-2X by Phase A8 full WB port (commits ed72704 → [RR11 SHA]). Per-building cell scoping via BuildingRegistry; WB RenderInsideOut Steps 1-5 + RenderOutsideIn. Visual-verified at Holtburg cottage interior, cellar, inn, and dungeon; sky visible through windows; cross-building visibility (Step 5); cottage interiors visible from outside (RenderOutsideIn). ``` - [ ] **RR12-S5: Close #102 (cross-cell-portal Step 5 deferral, subsumed)** If #102 was filed earlier (per the original A8 plan's RR5), move it to Recently closed: ```markdown **#102 — Far-side portal visibility through walls (WB Step 5 deferral)** — CLOSED 2026-05-2X by Phase A8 RR9 (Step 5 cross-building visibility implemented). Per WB VisibilityManager.cs:156-232. ``` If #102 was NOT filed (because we never reached RR5 of the prior plan), skip this step. - [ ] **RR12-S6: File any new issues discovered in the visual matrix** If RR12-S1 found any minor visual glitches not addressed, file them as next-ID OPEN issues (e.g. #104, #105). Examples: - Edge cases where Step 5's occlusion query misses a fast-camera-rotation building - RenderOutsideIn buildings popping in/out at landblock streaming boundary - [ ] **RR12-S7: Update CLAUDE.md A8 paragraph** Find the current A8 paragraph in `CLAUDE.md` (begins with "**Phase A8 —" or similar; locate via grep). Replace with: ```markdown **Phase A8 — Indoor-cell visibility culling — SHIPPED 2026-05-2X.** Closes issue #78 + #102. Full WorldBuilder RenderInsideOut + RenderOutsideIn port (eleven implementation tasks RR1–RR11): - R1+R2 (ed72704, 55f26f2) — WorldEntity.IsBuildingShell + EntitySet partition. Earlier ship; kept as orthogonal infrastructure. - RR1 — cleanup: commit [vis] probe; revert R3+R3.5 v1+v2 (failed half-port). - RR2 — spike: confirmed LandBlockInfo.Buildings data shape + interior-portal walk algorithm against WB PortalRenderManager:518-551. - RR3+RR4 — per-landblock data model: Building, BuildingRegistry, BuildingLoader. LoadedCell.BuildingId stamped at load. O(1) lookups in both directions. - RR5 — WbDrawDispatcher.Draw(cellIds:) overload for per-building cell scoping. - RR6 — IndoorCellStencilPipeline extended with 3-bit mode + occlusion- query helpers + per-building portal upload. - RR7 — render frame restructured to WB RenderInsideOut Steps 1-4 + stencil-gated sky (acdream enhancement); single strict cameraInsideBuilding gate; sky/terrain skipped when inside; no depth-clear; LiveDynamic last. - RR8 — visual verification gate: Steps 1-4 closed #78 + R4 Issues A+C. - RR9 — Step 5 (cross-building 3-stencil-bit pipeline + occlusion queries) per WB VisibilityManager.cs:156-232 verbatim. - RR10 — visual verification gate: Step 5 working. - RR11 — RenderOutsideIn (cottage interiors through windows from outside) per WB VisibilityManager.cs:241-310. Visual-verified at Holtburg cottage interior, cottage cellar, Holtburg Inn (multi-room), and dungeon. Sky visible through windows. Cross-building visibility (Step 5) working at inn→cottage. Cottage interiors visible from outside (RenderOutsideIn). No regression on #100 (terrain cutout). Full design: docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md. Full plan: docs/superpowers/plans/2026-05-26-phase-a8-wb-full-port.md. RR0 falsification findings: docs/research/2026-05-26-a8-rr0-falsification-findings.md. RR2 data-shape findings: docs/research/2026-05-26-a8-buildings-data-shape.md. ``` Also update the "Currently working toward" line and any other A8 references in the Milestone Discipline section to reflect ship status. - [ ] **RR12-S8: Commit ship docs** ```bash git add docs/ISSUES.md CLAUDE.md git commit -m "$(cat <<'EOF' ship(render): Phase A8 — full WB RenderInsideOut + RenderOutsideIn SHIPPED Closes #78 + #102. Files any new issues discovered during RR12 visual verification. Architecture: per-building cell scoping (Building + BuildingRegistry + BuildingLoader, with LoadedCell.BuildingId stamped at load) drives WB's RenderInsideOut Steps 1-5 indoor pipeline + RenderOutsideIn outdoor-look- into-buildings pipeline. Single strict cameraInsideBuilding gate; no grace mechanism for the render path; stencil-gated sky inside indoor branch (acdream enhancement). Visual-verified at Holtburg cottage interior + cellar, Holtburg Inn, dungeon. Sky through windows. Cross-building Step 5 working. Cottage interiors visible from outside. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Self-review **Spec coverage:** - [x] Q1 (Step 5 scope: full) → RR9 implements - [x] Q2 (data model: Option C cell field + registry) → RR3 ships both indexes + RR4 stamps cells - [x] Q3 (RenderOutsideIn: include) → RR11 implements - [x] WB-faithful restructure (skip initial sky+terrain, no depth-clear, single strict gate) → RR7 - [x] Stencil-gated sky (acdream enhancement) → RR7-S6 Step 4a - [x] Revert R3+R3.5 v1+v2 → RR1-S3 through RR1-S6 - [x] Keep R1+R2 → RR1 doesn't touch them - [x] Pre-flight spike before implementation → RR2 - [x] Visual gates at appropriate points → RR8, RR10, RR12 - [x] Supersede old docs → RR1-S8, RR1-S9 - [x] Ship docs (close #78, update CLAUDE.md) → RR12 **Placeholder scan:** no "TBD" / "implement later" / "similar to" strings. RR2's spike has a CONCRETE deliverable (findings doc with 5 named sections). RR3's tests reference field names from RR2's findings, which is the right factoring (data shape resolved before tests are written/run). **Type consistency:** - `cameraInsideBuilding` used consistently (RR7 introduces; not used before). - `Building`, `BuildingRegistry`, `BuildingLoader` names match across RR3 (definition) + RR4 (consumption) + RR7+RR9+RR11 (render-frame use). - `LoadedCell.BuildingId` (init + internal set) defined in RR4-S1; consumed in RR7-S2 (gate uses `BuildingId is not null`); referenced (mutated) only by BuildingLoader. - `LandblockEntry.BuildingRegistry` defined in RR4-S8; consumed in RR7-S3 + RR9-S2 + RR11-S1. - `IndoorCellStencilPipeline` methods: existing (`UploadPortalMesh`, `MarkAndPunch`, `EnableOutdoorPass`, `DisableStencil`) + RR6 additions (`UploadBuildingPortalMesh`, `MarkBuildingBit2`, `PunchDepthAtStencil3`, `EnableOtherBuildingPass`, `ResetBit2`, `EnsureOcclusionQueryId`, `TryReadOcclusionResult`, `BeginOcclusionQuery`, `EndOcclusionQuery`). All referenced consistently. - `WbDrawDispatcher` overloads: existing (`Draw(... visibleCellIds: ..., set: All)`) + RR5 addition (`Draw(... cellIds: ..., set: ...)`). RR7+RR9+RR11 use the new `cellIds:` overload exclusively for indoor scoping. **Task granularity:** RR1 = 10 steps (mechanical); RR2 = 6 (research); RR3 = 14 (TDD + impl); RR4 = 11; RR5 = 7; RR6 = 5; RR7 = 10; RR8 = 9 (visual); RR9 = 5; RR10 = 5 (visual); RR11 = 4; RR12 = 8. Total 94 step checkboxes across 12 tasks. Each step 2-5 min except visual scenarios (longer). **Estimated total: 8-10 sessions** (1.5-2 weeks calendar), matching the design doc estimate. **Risk callouts inline:** RR2's gate decision (if data shape incompatible); RR3-S12's note on field-name adaptation; RR8/RR10's gate decisions; RR9/RR11's frustum-API adaptation.