Commit graph

1238 commits

Author SHA1 Message Date
Erik
9c5991061f fix(render): Phase A8 — Step 5 gate-off-by-default + restore GL state
First user-driven visual gate (1,595 indoor frames, 109 other-buildings)
reported textures flickering / can barely move / client crashed
indoors. Root causes:

1. Step 5 (cross-building visibility) iterates EVERY loaded other-
   building per frame with NO frustum culling. At Holtburg that's 109
   other-buildings × 5 GL draws each = ~545 extra draws/frame on top
   of the 4 setup steps. Each Render() within Step 5c also re-uploads
   the instance SSBO via glBufferData(orphan) + glBufferSubData and
   a glMemoryBarrier. Combined with rapid 109-iteration back-to-back
   state churn, the driver hits TDR and crashes.

   GATE: Step 5 is now OFF BY DEFAULT. Set ACDREAM_A8_STEP5=1 to opt
   in once we add per-building frustum culling on otherBuildings.
   Cross-building visibility is a polish feature; M1.5 indoor walking
   doesn't require it.

2. WB's RenderInsideOut cleanup at line 234-238 exits with
   ColorMask(t,t,t,FALSE) — alpha-bit OFF — matching WB's editor
   pipeline expectations. acdream's subsequent rendering (particles,
   anything writing alpha) needs alpha-bit ON. The flicker symptom
   is consistent with subsequent passes mis-writing alpha.

   FIX: cleanup now restores ColorMask(t,t,t,t), DepthMask(true),
   DepthFunc.Less, Enable(CullFace) — all to acdream defaults so the
   outer render frame sees a clean slate. Step 5's loop also leaves
   DepthMask/CullFace in non-default states; defensive restore makes
   this safe whether Step 5 ran or not.

Build green. Tests unchanged.

Expectation for next relaunch: indoor frames hit only Steps 1+2+3+4
(camera-building stencil + cell render + stencil-gated outdoor scenery).
Cross-building visibility (Step 5) is intentionally inert. Flicker
should be resolved by the ColorMask alpha restore. Perf should be
closer to pre-A8 outdoor (one extra full-screen pass + a small
stencil mask).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:06:07 +02:00
Erik
5d41876ba6 fix(render): Phase A8 — normalize _buildingRegistries key (RR7.2 root cause)
Second visual-gate probe data: [envcells]/[buildings] firing 3711 times
each (indoor branch FIRED), but [stencil]=0 and [draworder]=2x (only
Steps 3+4, no Steps 1+2+5). [buildings] sample:
  camCell=0xA9B40143 camBldgs=[] otherBldgs=109 totalKnown=110

The registry HAS 110 buildings loaded but lookup returns empty. Root
cause: storage key mismatch. lb.LandblockId encodes 0xXXYY_FFFF (low 16
bits = 0xFFFF for the landblock's own LandBlockInfo dat id), while the
runtime lookup at the gate derives 0xXXYY_0000 via cellId & 0xFFFF0000u.
Same bug RR7.2 (`efe3520`, reverted by `9aaae02`) tried to fix — landed
here properly:

- Storage key now `lb.LandblockId & 0xFFFF0000u` (was lb.LandblockId).
- Both RemoveLandblock callbacks use `id & 0xFFFF0000u` to match.

Build green.

After this fix, [buildings] should show camBldgs=[0x1] (or similar)
when the player is inside a cottage, [envcells] cells/tris should be
non-zero, and the [stencil] / [draworder] step 1 + 2 + 5 should fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:20:27 +02:00
Erik
0fc6003c2a fix(render): Phase A8 — stamp BuildingId on already-loaded cells too
First visual-gate launch showed 8,737 [vis] lines (player at Holtburg
cottage cell 0xA9B40143, inside=True really=True) but ZERO [buildings] /
[envcells] / [stencil] / [draworder] probe emissions. Root cause: same as
the original RR7.1 saga — BuildingLoader.Build was passed only the
per-frame drainedCells dict, missing cells loaded on PRIOR frames. Those
cells stayed with BuildingId=null, the strict cameraInsideBuilding gate
returned false, the indoor branch never fired.

Fix: in ApplyLoadedTerrainLocked, merge drainedCells with the cells
already registered in _cellVisibility for the same landblock prefix
before passing to BuildingLoader. The richer dict ensures the stamping
loop in BuildingLoader.Build covers EVERY cell in this landblock.

Added IReadOnlyList<LoadedCell> GetCellsForLandblock(uint lbId) on
CellVisibility — minimal API expose; existing _cellsByLandblock dict
was already the right shape (lbId = upper 16 bits).

Build green. Tests unchanged.

Next: relaunch the client. With the fix, [buildings] probe should fire
with camBldgs=[0x1,...] when the player is inside a Holtburg cottage,
[envcells] should report cells>=1 tris>=1 per indoor frame, and the
indoor branch should be exercising the WB-faithful Steps 1-5 pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:18:07 +02:00
Erik
8532c84f57 feat(render): Phase A8 Wave 5 — probe trail ([envcells]/[stencil]/[draworder]/[buildings])
Probe emitters wired (replaces the Task 8 stubs). All gated on
ACDREAM_PROBE_VIS=1 (everything) or ACDREAM_PROBE_ENVCELL=1
([envcells] only):

- [envcells] frame=N cells=N tris=N ourBldgs=N otherBldgs=N filterCnt=N
  Fires once per Render call inside RenderInsideOutAcdream Step 3.
  Reads CellsRendered + TrianglesDrawn from EnvCellRenderer.Stats.

- [stencil] op={mark|punch} bld=0xHHHHHHHH verts=N
  Fires after every IndoorCellStencilPipeline.RenderBuildingStencilMask
  call (Steps 1, 2, 5a, 5b, 5d) — surfaces LastStencil* probe fields
  added in Wave 1's Task 7 extension.

- [draworder] frame=N step=Xy stencil={on|off} depthFn=0xHHH depthMask={true|false}
  Fires at each step boundary (entry to Step 1/2/3/4/5{a,b,c,d}).
  Reads live GL state via glGetInteger so divergence between assumed
  vs actual state is immediately visible.

- [buildings] camCell=0xHHHHHHHH camBldgs=[0x1,0x2,...] otherBldgs=N totalKnown=N
  Fires once per indoor frame at the top of RenderInsideOutAcdream.
  totalKnown sums BuildingRegistry.Count across all loaded landblocks.

Per-frame counter _phaseA8DrawOrderFrame incremented once per render
tick after the existing [vis] probe block (line 7104).

New env-var flag ACDREAM_PROBE_ENVCELL in RenderingDiagnostics +
ProbeEnvCellEnabled property (true OR ProbeVisibilityEnabled).

Mandatory acceptance criteria (process rule "no visual-gate launch
without probe data first") to check FROM the log BEFORE asking the
user for visual verification:
  - [buildings] camBldgs=[0x...] non-empty when inside a cottage
  - [envcells] cells>=1 tris>=1 filterCnt>=1 for at least one indoor frame
  - [stencil] op=mark verts>0 fires per camera-building
  - [draworder] shows the full Step 1 → 2 → 3 → 4 → 5{a,b,c,d} cycle

Build green. 82/82 App.Tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:14:45 +02:00
Erik
f9a644a366 feat(render): Phase A8 Wave 4 — RenderInsideOutAcdream byte-for-byte WB port
Six surgical edits in GameWindow.cs (+275 LOC):

1. _indoorStencilPipeline field + ctor init (line 172 + 1788). Uses
   the portal_stencil.{vert,frag} shaders. Disposed at line 10595.

2. Strict cameraInsideBuilding gate (line 7079-7097): visibility.CameraCell
   PointInCell + BuildingId != null. camBuildings + otherBuildings lists
   populated from _buildingRegistries.GetBuildingsContainingCell / .All().

3. envCellViewProj compute + _envCellFrustum.Update + _envCellRenderer
   .PrepareRenderBatches (line 7192) — once per frame, before sky.

4. Frame clear now includes StencilBufferBit (line 6947) so stencil starts
   at 0 each frame. RR7 missed this.

5. Old "depth clear when inside" workaround (was lines 7210-7215) DELETED.
   Replaced with one-line marker pointing at RenderInsideOutAcdream.

6. Indoor-vs-outdoor branch (line 7284-7298): on cameraInsideBuilding,
   call RenderInsideOutAcdream. Otherwise, existing Dispatcher.Draw(set: All).
   The outdoor path retains pre-A8 behavior exactly.

7. RenderInsideOutAcdream method (line 10587-10761): byte-for-byte port of
   WB VisibilityManager.RenderInsideOut at
   references/WorldBuilder/.../VisibilityManager.cs:73-239. Substitutions:
     portalManager.RenderBuildingStencilMask -> _indoorStencilPipeline.RenderBuildingStencilMask
     envCellManager.Render(pass, filter)     -> _envCellRenderer.Render(pass, filter)
     terrainManager.Render(...)              -> _terrain?.Draw(camera, frustum, neverCullLb)
     sceneryManager + staticObjectManager    -> _wbDrawDispatcher.Draw(set: OutdoorScenery)
     sceneryShader.Bind()                    -> _meshShader.Use()
   Step 1 + 2 (camera-building portals stencil mark + far-depth punch).
   Step 3 (cells of camera-buildings, opaque + transparent).
   Step 4 (stencil-gated terrain + scenery).
   Step 5 (cross-building visibility via 3-bit stencil + occlusion query).

8. Four EmitXxxProbe stub methods (Task 9 fills them with real output).

LiveDynamic (player + NPCs + dropped items) is NOT YET drawn separately;
Task 9 follow-up may add the LiveDynamic dispatch call after stencil
disable. Pre-A8 behavior had no separate LiveDynamic pass either —
dynamic entities flow through Dispatcher.Draw(All) on the outdoor path.

Subagent deviation from spec: `camera` parameter typed as
AcDream.App.Rendering.ICamera (the actual type GameWindow uses) rather
than AcDream.Core.Rendering.Camera (which doesn't exist).

Build green. 82/82 App.Tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:13:16 +02:00
Erik
4b4f687070 feat(render): Phase A8 Wave 3 — wire EnvCellRenderer into landblock streaming
Six surgical edits to GameWindow.cs (+1 MeshManager accessor on WbMeshAdapter):

1. Field declarations (line 166-167): _envCellRenderer + _envCellFrustum.
2. Ctor init (line 1775-1778): construct WbFrustum + EnvCellRenderer,
   Initialize with the existing _meshShader (loaded from mesh_modern.vert/frag).
3. BuildInteriorEntitiesForStreaming (line 5444): _envCellRenderer.RegisterCell(...)
   replaces the cell-as-WorldEntity creation block. staticObjects is empty —
   cell stabs continue as WorldEntity records via the dispatcher's IndoorPass.
4. ApplyLoadedTerrainLocked (line 5885): _envCellRenderer.FinalizeLandblock(...)
   immediately after _buildingRegistries[lb.LandblockId] = ... — atomically
   commits the landblock's per-cell instance store.
5. RemoveLandblock callbacks (lines 1861 + 8955): mirror the existing
   _buildingRegistries.Remove(id) sites so EnvCellRenderer's storage clears
   in lockstep.
6. Dispose (line 10595): _envCellRenderer?.Dispose() after _wbDrawDispatcher.

Plan revision (vs original plan.md Task 6): we keep the static-object stab
WorldEntity hydration (lines 5440-5489) instead of deleting it — stabs need
WorldEntity records for interaction (clicking) and physics. EnvCellRenderer
receives empty staticObjects so it only renders cell geometry; stab rendering
continues unchanged through the dispatcher.

Build green. 23/23 EnvCellRenderer + WbFrustum + EnvCellSceneryInstance
tests pass. App.Tests baseline holds (82/82). Pre-existing Core.Tests
static-leak flakiness (8-19 failures, documented baseline) unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:03:17 +02:00
Erik
aad9ed4cdb fix(render): Phase A8 — EnvCellRenderer uses acdream Shader (not GLSLShader)
Task 5's subagent took GLSLShader (WB's abstract shader). Our existing
GameWindow wire-in uses the legacy AcDream.App.Rendering.Shader class
loaded once at startup for mesh_modern.{vert,frag} and shared with
WbDrawDispatcher. Matching that convention keeps the wire-in trivial
and avoids a second shader compile.

API mapping (acdream Shader is the surface here):
  Bind()                      -> Use()
  SetUniform(name, int)       -> SetInt(name, int)
  SetUniform(name, Vector4)   -> SetVec4(name, Vector4)
  SetUniform(name, Matrix4x4) -> SetMatrix4(name, Matrix4x4)  (unused)

Build green. 23/23 Wave 1+2 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:57:29 +02:00
Erik
f16b8e9812 feat(render): Phase A8 Wave 2 — EnvCellRenderer (WB EnvCellRenderManager port)
The core port. 1013 LOC of WB-faithful rendering algorithm:

- GetEnvCellGeomId        : WB EnvCellRenderManager.cs:94-103 verbatim
- PrepareRenderBatches    : WB EnvCellRenderManager.cs:247-373 verbatim
                            (parallel frustum-cull, per-cell slow path,
                            ThreadLocal merge, atomic snapshot swap)
- Render(filter:)         : WB EnvCellRenderManager.cs:395-511 verbatim
                            (filter-driven gfxObj group + draw call build)
- RenderModernMDIInternal : WB BaseObjectRenderManager.cs:709-848
                            (single-slot variant; resize buffers,
                            group by cull mode + additive, MDI draw)
- PopulatePartGroups      : WB EnvCellRenderManager.cs:572-580 verbatim
                            (Setup part recursion via PopulateRecursive)
- RegisterCell / FinalizeLandblock / RemoveLandblock — streaming seam
  (no WB analog; bridges acdream's existing StreamingController +
  LandblockStreamer to the renderer's per-cell instance store)

Documented deviations from WB:
- Drop _useModernRendering branch (Phase N.5 mandatory modern path)
- Drop SelectedInstance/HoveredInstance highlights (no editor state)
- _activeSnapshotGlobalGroups/GfxObjIds as sibling fields on the class
  rather than on the snapshot (EnvCellVisibilitySnapshot per Task 4 spec
  only carries BatchedByCell + VisibleLandblocks; global groups only
  used in the unfiltered Render(pass) path which we don't take)
- ConcurrentDictionary<uint, EnvCellLandblock> keyed by full 32-bit
  landblock id (WB uses ushort packed key; acdream uses full id throughout)

10 unit tests (GetEnvCellGeomId determinism + bit-33 dedup flag +
NeedsPrepare + dispose semantics + RemoveLandblock idempotence). Build
green; 23/23 Wave 1+2 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:55:15 +02:00
Erik
fc68d6d01f feat(render): Phase A8 Wave 1 — WB scaffolding extraction + stencil low-level method
Five tasks shipped together (interdependent at build time):

Task 1: WbRenderPass enum — verbatim port of WB RenderPass.cs:1-22
Task 2: WbFrustum + WbBoundingBox + FrustumTestResult — verbatim port
  of WB Frustum.cs (98 LOC) with namespace + BoundingBox-type adaptations.
  +7 unit tests.
Task 3: EnvCellSceneryInstance + EnvCellLandblock — verbatim port of WB
  SceneryInstance.cs:1-161, renamed scope-narrow. Dropped editor-only
  fields (DisqualificationReason, ParticleEmitters, IsQueuedForUpload,
  InstanceBufferOffset, InstanceCount, MdiCommands, IsTransformOnlyUpdate)
  + InstanceId narrowed uint (we don't use ObjectId's editor methods).
  +5 unit tests.
Task 4: EnvCellVisibilitySnapshot — direct port of WB VisibilitySnapshot
  narrowed to BatchedByCell + VisibleLandblocks only.
Task 7: IndoorCellStencilPipeline.RenderBuildingStencilMask — new
  low-level WB-faithful entry mirroring PortalRenderManager:471-484.
  No surrounding GL state setup (caller's responsibility). Probe fields
  LastStencilVertexCount / LastStencilWasFarPunch / LastStencilBuildingId
  for the [stencil] probe emitter in Task 9.

Build green, 18 tests pass (7 new Frustum + 5 new SceneryInstance + 6
existing stencil pipeline). Ready for Wave 2 (EnvCellRenderer port).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:46:07 +02:00
Erik
95f0d5267b docs(plan): Phase A8 WB RenderInsideOut port — implementation plan
Replaces the four reverted RR7 variants from 2026-05-27 with a
verbatim port of WB VisibilityManager.RenderInsideOut.

Plan covers 10 tasks across 5 dependency waves:
- Wave 1 (tasks 1-4, 7): extract WbRenderPass, WbFrustum,
  EnvCellSceneryInstance/EnvCellLandblock, EnvCellVisibilitySnapshot;
  add IndoorCellStencilPipeline.RenderBuildingStencilMask
- Wave 2 (task 5): build EnvCellRenderer with inline RenderModernMDI
- Wave 3 (task 6): wire EnvCellRenderer into landblock streaming
- Wave 4 (task 8): port RenderInsideOutAcdream byte-for-byte
- Wave 5 (task 9): probe trail [envcells]/[stencil]/[draworder]/[buildings]
- Wave 6 (task 10): probe-gated visual verification launch

Process rules carved from RR7 saga:
- No visual gate without probe data first
- No partial WB ports (Steps 1-5 ship together)
- No conceptual adaptations
- Trust-but-verify after every subagent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:41:54 +02:00
Erik
3e9ff7accb docs(research): A8 RR7 reverted — full WB-port handoff for next session
Four RR7 variants shipped + reverted in one session (RR7, RR7.1, RR7.2,
RR7.3). The root architectural mismatch: RR7 routed cell-mesh rendering
through ObjectMeshManager / WbDrawDispatcher.Draw(IndoorPass) — a per-
GfxObj batched pipeline. WB uses a separate EnvCellRenderManager (862
LOC) for cells; we never extracted it. Indoor branch fires correctly
after RR7.2 + RR7.3 but interior cell geometry doesn't render.

User direction (verbatim, 2026-05-27): port WB verbatim. No band-aids.
Visual test launch only when fix is ready; probe data verified first.

Handoff captures:
- Session log of all four RR7 attempts + why each failed
- Why WB over retail (modern GL fit + existing Phase N.4/N.5/O
  commitment to WB as rendering base)
- The full WB RenderInsideOut algorithm spec (Steps 1-5, line refs)
- 5-phase next-session plan (extract EnvCellRenderManager + deps,
  wire into landblock load, replicate RenderInsideOut byte-for-byte,
  probe trail mandatory before visual gate, single visual gate)
- Process rules carved from this session's mistakes (no visual gate
  without probe data first, no partial WB ports, no conceptual
  adaptations, trust-but-verify, slow at brainstorm not implement)

RR3-RR6 infrastructure remains shipped + tested in isolation
(Building/Registry/Loader/Dispatcher cellIds overload/Stencil pipeline).
Branch is at pre-A8 visual ("looks good") with infrastructure dormant.

Next session opens cold against the pickup prompt at the bottom of
the handoff doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:10:33 +02:00
Erik
4fa3390592 Revert "feat(render): Phase A8 RR7 — WB RenderInsideOut Steps 1-4 + outdoor branch"
This reverts commit 3d28d701a2.
2026-05-27 14:07:13 +02:00
Erik
21dc72b010 Revert "fix(render): Phase A8 RR7.1 — stamp BuildingId on cells loaded across multiple frames"
This reverts commit a1a3e0ee3e.
2026-05-27 14:07:13 +02:00
Erik
9aaae02610 Revert "fix(render): Phase A8 RR7.2 — _buildingRegistries key mismatch"
This reverts commit efe35201fc.
2026-05-27 14:07:13 +02:00
Erik
07c5981824 Revert "fix(render): Phase A8 RR7.3 — dat-driven BFS in BuildingLoader"
This reverts commit 56673e1b1e.
2026-05-27 14:07:13 +02:00
Erik
56673e1b1e fix(render): Phase A8 RR7.3 — dat-driven BFS in BuildingLoader
RR7.2 fix made the indoor branch fire (119K frames vs 0), but visual
verification showed missing interior textures — the inn's floor + lower
wall sections rendered as fog-color clear instead of cell-mesh polygons.
Root cause: BFS short-circuited at registry-build time on intermediate
cells that hadn't yet streamed in. The Holtburg Inn has 2 entry portals
+ 209 interior leaves; if any intermediate cell wasn't loaded when lbInfo
arrived, BFS stopped, EnvCellIds was a tiny subset of the building's true
cells, camCellIds at the gate excluded most inn cells, and IndoorPass
skipped their mesh entities → flat fog-color floor.

Fix: walk the dat directly in BFS via `dats.Get<EnvCell>(cellId)
  .CellPortals` (matches WB PortalService.cs:67-79). BFS now completes
deterministically at registry-build time regardless of cell load
ordering. Exit-portal polygon collection (Step C) also gets a dat
fallback so the stencil mask is complete on first indoor frame.

BuildingLoader.Build signature gains two optional params:
  - dats: DatCollection? — null in unit tests preserves old behavior
  - landblockOrigin: Vector3 — translation for dat-side polygons

Tests: 11/11 pass (unit-test path unchanged via dats == null).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:18:57 +02:00
Erik
efe35201fc fix(render): Phase A8 RR7.2 — _buildingRegistries key mismatch
RR7.1 fixed cell-timing but the indoor branch STILL fired 0 times in
the v2 visual gate (125,476 inside=True frames, all routed outdoor).
Real root cause: a key-form mismatch between storage and lookup.

Storage at line ~5886 used `_buildingRegistries[lb.LandblockId]`. But
lb.LandblockId is the LandBlock dat-file id (e.g. 0xA9B4FFFF — the
0xFFFF low word identifies the file as terrain). Lookups at the gate
(line ~7090) and the drain late-stamp (line ~5708) used
`cell.CellId & 0xFFFF0000u` (e.g. 0xA9B40000). 0xA9B4FFFF ≠ 0xA9B40000
so TryGetValue always missed; camBuildings stayed empty; the gate
fell to the outdoor branch unconditionally.

Fix: normalize all four sites to the masked form
(`& 0xFFFF0000u`) — storage at the build call, both Remove callbacks
in the streaming-controller setup, and the lookups (already correct).

User-visible symptom that surfaced the v2 launch:
  - sky + ground missing through windows
  - buildings + objects still visible
This pattern (stencil-gated outdoor passes failing while ungated
indoor pass works) was actually the OUTDOOR branch running with the
indoor visibility set — `visibleCellIds` filtered out terrain cells
and the sky pre-scene was gated off too because cameraInsideBuilding
was True (correctly) but camBuildings was empty (incorrectly).

Wait — re-reading the indoor branch's gate: it requires
camBuildings.Count > 0 too, so with the key mismatch it took the
outdoor branch. The sky+terrain visibility pattern user reported is
the outdoor branch where sky-pre-scene was correctly gated off by
!cameraInsideBuilding (cameraInsideBuilding is what computes the
ROUTING; it doesn't have to match the actual branch taken when the
extra `camBuildings.Count > 0` filter trips). So initial-sky was
skipped (cameraInsideBuilding=true) but indoor branch didn't fire
either — outdoor branch with no initial sky = the dark window
visual. RR7.2 closes both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:00:28 +02:00
Erik
a1a3e0ee3e fix(render): Phase A8 RR7.1 — stamp BuildingId on cells loaded across multiple frames
RR7 visual gate (2026-05-27) revealed the indoor branch NEVER fired even
when the strict gate's PointInCell + non-null CameraCell hit: 17,748
inside=True frames, 0 branch=indoor decisions. Root cause: RR4 wired
BuildingLoader.Build with the per-frame drainedCells dict — cells that
streamed in on earlier frames (the common case, since cells arrive
asynchronously over many frames after the landblock-info completion)
were not in drainedCells, so the BFS short-circuited and the registry's
EnvCellIds set was systematically incomplete. Cells loaded ahead of
lbInfo arrival never got their BuildingId stamped.

Fix has two parts:

1. CellVisibility.AllLoadedCells — new public IReadOnlyDictionary
   exposing the existing private _cellLookup. BuildingLoader.Build at
   landblock-info-arrival now walks the full cell set, not just this
   frame's drain.

2. _pendingCells drain loop — late-stamps BuildingId on each arriving
   cell if its landblock's BuildingRegistry already exists. Covers cells
   that arrive AFTER the registry-build pass.

Together these handle all four timing cases:
  - Cells loaded before lbInfo arrives  → stamped in BuildingLoader.Build
  - Cells loaded with lbInfo (same frame) → stamped in BuildingLoader.Build
  - Cells loaded after lbInfo arrives    → stamped in drain loop
  - lbInfo never arrives (LB has no info) → registry never built, cells
                                            stay at BuildingId == null
                                            (intended — flow through outdoor
                                            render path)

Probe data from the failed gate launch confirmed cell 0xA9B40150
(cottage idx=6 cellar from the #98 saga) was reached as the camera cell
with visN=16 visible neighbours, but BuildingId stayed null. This fix
gets the indoor branch fired in that scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:45:45 +02:00
Erik
3d28d701a2 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 each indoor frame.

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
and UpdateSkyPes path.

IndoorCellStencilPipeline constructed in OnLoad (portal_stencil.vert/frag,
shader-compile exception caught + logged; indoor branch falls back to
outdoor on null). Added to Dispose chain.

camBuildings looked up via _buildingRegistries dict (NOT
LandblockEntry.BuildingRegistry — per Code Structure Rule #2, the registry
lives on GameWindow keyed by landblock id).

Visual verification at RR8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:33:26 +02:00
Erik
6a7894ac35 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) <noreply@anthropic.com>
2026-05-27 11:22:15 +02:00
Erik
3361933ce6 feat(render): Phase A8 RR5 — WbDrawDispatcher Draw(cellIds:) overload
Adds a new public overload accepting an explicit IReadOnlyCollection<uint>
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) <noreply@anthropic.com>
2026-05-27 11:18:21 +02:00
Erik
f8d0499d8b 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 (outdoor surface cells; dungeon cells not
    enumerated in LandBlockInfo.Buildings).

  GameWindow landblock-load path: builds BuildingRegistry from
    LandBlockInfo.Buildings; stamps each cell's BuildingId; stores the
    registry on _buildingRegistries[landblockId] (GameWindow-level dict)
    for render-frame lookups. Note: LoadedLandblock is AcDream.Core.World
    (a sealed record) — adding an App-type field there would violate
    Code Structure Rule #2, so the registry is stored in a new
    GameWindow-level dictionary instead. Cleanup wired in both
    removeTerrain lambdas (OnLoad + OnResize paths).

  drainedCells dict: the existing _pendingCells drain loop is extended
    to also build a local CellId→LoadedCell dict; BuildingLoader.Build
    uses this dict for the stamping pass so no second iteration is needed.

  New BuildingLoaderTest verifies the stamping path. 5 BuildingLoader
  tests total (4 from RR3 + 1 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:13:48 +02:00
Erik
f125fdb220 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) <noreply@anthropic.com>
2026-05-27 11:08:43 +02:00
Erik
f44a9bf943 docs(research): Phase A8 RR2 — BuildingInfo data shape + interior-portal walk
Spike findings before RR3 (BuildingLoader impl). Documents:
  - DatReaderWriter.Types.BuildingInfo field shape (verbatim ilspy decomp
    of DRW 2.1.7 — type is BuildingInfo with field BuildingPortal, not the
    plan's tentative BldPortal; same OtherCellId semantics)
  - WB PortalService.GetPortalsByBuilding interior-portal walk algorithm
    (BFS through EnvCell.CellPortals; 0xFFFF == exit-portal sentinel)
  - Holtburg town landblock 0xA9B4FFFF live BuildingInfo dump: 12 buildings,
    1-10 portals each, including the cottage from the #98 cellar saga at
    idx=6 (cells 0xA9B40145/014C/014E/014F/0150)
  - Resolved BuildingLoader algorithm + 2 minor rename corrections vs the
    plan's RR3 pseudocode (BuildingPortal not BldPortal; defensive 0xFFFF
    skip kept matching WB)
  - 6 edge cases (empty portals, shared cells, unloaded interiors, etc.)

Gate decision: data shape compatible — proceed to RR3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:01:39 +02:00
Erik
a5d2244467 docs(handoff): Phase A8 RR1 shipped — pickup prompt for RR2 spike
Session-end handoff capturing:
- RR0 findings + design + plan + RR1 cleanup all shipped (8 commits)
- Working tree at logical R2-baseline + [vis] probe
- RR2 (BuildingInfo spike) is next; ~30-60 min; human-in-the-loop step
  for live-inspect
- Canonical doc-read order for fresh session
- Pickup prompt with state-both-altitudes header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:46:32 +02:00
Erik
29e306b0f6 docs: Phase A8 — mark prior restructure design+plan as SUPERSEDED
Both documents retained for historical reference. The new full-WB-port
design + plan (2026-05-26-phase-a8-wb-full-port-design.md + plan, ea60d1f +
651e7e2) replace them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:08:48 +02:00
Erik
fd721afdf9 Revert "feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)"
This reverts commit 60f07bc21b.
2026-05-27 10:08:10 +02:00
Erik
b93103885a Revert "fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment"
This reverts commit 38d537491f.
2026-05-27 10:07:15 +02:00
Erik
664ca9cb16 Revert "fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too"
This reverts commit 2bfeafd358.
2026-05-27 10:07:15 +02:00
Erik
84c4a70296 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) <noreply@anthropic.com>
2026-05-27 10:07:10 +02:00
Erik
651e7e22fb docs(plan): Phase A8 — full WB RenderInsideOut + RenderOutsideIn port plan
12-task implementation plan (RR1-RR12, 94 step checkboxes total):
  RR1  — Cleanup: commit [vis] probe; revert R3+R3.5 v1+v2; supersede old docs
  RR2  — Spike: confirm BuildingInfo shape + WB interior-portal walk algorithm
  RR3  — Implement Building + BuildingRegistry + BuildingLoader (TDD, 10 tests)
  RR4  — Wire registry into landblock load + LoadedCell.BuildingId
  RR5  — WbDrawDispatcher.Draw(cellIds:) overload (TDD)
  RR6  — IndoorCellStencilPipeline 3-bit + occlusion-query helpers
  RR7  — Render frame: WB Steps 1-4 + outdoor branch + stencil-gated sky
  RR8  — Visual verification gate: Steps 1-4 close #78 + Issues A+C
  RR9  — Step 5 (3-stencil-bit cross-building + occlusion queries)
  RR10 — Visual verification gate: Step 5
  RR11 — RenderOutsideIn (cottage interiors through windows from outside)
  RR12 — Final visual matrix + ship docs (close #78, #102; update CLAUDE.md)

Each task: bite-sized 2-5 min steps; exact code snippets; commit per task.
Visual gates at RR8, RR10, RR12 ensure each layer works before adding the
next. Risk register handles RR2 data-shape uncertainty + RR9/RR11 frustum
API adaptation.

Estimated 8-10 sessions (~1.5-2 weeks calendar). Closes M1.5 indoor world
acceptance scope.

Design: docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:57:45 +02:00
Erik
ea60d1fb7d docs(spec): Phase A8 — full WB RenderInsideOut + RenderOutsideIn port (design)
Re-brainstormed after RR0 falsification showed R3+R3.5 introduced
Issues A+C (rendering all 16 BFS-reachable cells at full screen extent
caused co-planar Z-fight + grace-state leak from outside view). The
prior design's "WB-faithful restructure" was insufficient — it kept
the BFS-wide cell rendering. Retail and WB both solve indoor visibility
with per-portal recursive culling.

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:29:14 +02:00
Erik
f9bab501df docs(research): Phase A8 RR0 — Issues A + C caused by R3, NOT pre-existing
Three-branch falsification spike per the design's RR0:

  HEAD (2bfeafd, R3.5 v2):     Issue C YES; Issue A YES (varies by building)
  R3 baseline (60f07bc):        Issue C YES; Issue A YES (same as HEAD)
  main (7034be9, no A8 work):   Issue C NO; Issue A NO flicker; BUT
                                 constant #78 symptom (houses-below-terrain
                                 visible from inside)

Diagnosis: R3 (stencil pipeline wire-in) successfully fixes the original
#78 main symptom but introduces Issues A and C as new transition artifacts.
R3.5 v1+v2 patches didn't help (R3 baseline shows same A+C as HEAD).

Per the design's decision gate (Outcome 2): PAUSE plan; re-brainstorm
via superpowers:brainstorming to address A+C without re-introducing #78
constant leak.

The original restructure design assumed A+C might be pre-existing and
could be filed as separate out-of-A8-scope issues. RR0 invalidates that.
The restructure must address them OR the brainstorm needs a third option
between "stencil-gate everything" (causes A+C) and "no stencil work"
(causes #78).

Open questions for the re-brainstorm captured in the findings doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:33:32 +02:00
Erik
769a003138 docs(plan): Phase A8 — render-frame restructure implementation plan
Six-task plan implementing the approved design:
  RR0 — pre-restructure falsification spike (3-branch repro of A + C)
  RR1 — revert R3.5 v1 + v2 (38d5374 + 2bfeafd)
  RR2 — restructure render frame to WB-faithful order
  RR3 — verify SkyRenderer doesn't toggle stencil state
  RR4 — visual verification matrix (4 buildings + transitions + sky)
  RR5 — ship docs (close #78, file new follow-ups, update CLAUDE.md)

Bite-sized steps (2-5 min each) with exact code snippets, commands,
and expected outcomes. TDD where applicable; GL integration tasks are
visual-verification-only by nature.

Each task has explicit decision gates:
  RR0-S5 — outcome 2 (A8-caused) triggers re-brainstorm
  RR3-S1 — dirty SkyRenderer triggers wrapper variant
  RR4-S9 — building-type failure triggers /investigate

Design: docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:46 +02:00
Erik
732f766d1b docs(spec): Phase A8 — render-frame restructure to WB-faithful order (design)
Brainstorm-approved design for the A8 R3.5 → restructure pivot. Replaces
the R3.5 v1+v2 frankenstein (terrain twice + depth-clear workaround) with
WB's RenderInsideOut order verbatim: skip initial sky+terrain when inside,
delete the depth-clear, add a stencil-gated sky step inside the indoor
branch so windows show real sky (closes R4 Issue B).

Unifies the two-flag asymmetry (cameraInsideCell lenient + cameraReallyInside
strict) into a single strict cameraInside flag via PointInCell. Grace
mechanism in CellVisibility stays alive for non-render consumers.

Six tasks ahead, in order:
  RR0 — pre-restructure falsification spike (Issues A + C on main?)
  RR1 — revert R3.5 v1+v2 (38d5374 + 2bfeafd)
  RR2 — restructure render frame to WB-faithful order
  RR3 — verify SkyRenderer doesn't toggle stencil state
  RR4 — visual verification matrix (cottage/cellar/inn/dungeon + transitions)
  RR5 — ship docs (close #78; file new follow-ups if pre-existing on main)

Next: superpowers:writing-plans to produce the per-task plan.

Note: the design references two predecessor docs that are currently
untracked in this worktree (entity-taxonomy + phase-a8-replan). Their
contents are read-stable on disk; committing them is a separate concern
(they belong to the prior session's work). The handoff doc this design
continues from is at f90fa2f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:49:18 +02:00
Erik
f90fa2f863 docs(handoff): Phase A8 R3.5 paused for restructure — fresh-session handoff
R1 + R2 + R3 + R3.5 v1 + R3.5 v2 all shipped this session (ed727042bfeafd). Primary #78 fix works (cottage walls solid from inside). Three
transition / sky issues remain that resist symptom-level patching:

  A — Exit indoor→outdoor: "objects through ground + building parts missing"
  B — Inside through window: "sky doesn't render"
  C — Entry outdoor→indoor: "floor transparent showing cellar + wrong texture"

Root cause: architectural mismatch with WB's RenderInsideOut reference.
We draw initial terrain unconditionally + depth-clear-if-inside as a
workaround; WB skips initial terrain when inside and renders terrain
ONLY at the stencil-gated step. The R3.5 v1+v2 patches were symptom
fixes that kept producing new edge cases — the exact "patching symptoms"
anti-pattern the predecessor revert handoff called out.

Handoff doc captures: what shipped, what works, what doesn't (with
verbatim user reports), the architectural diagnosis (WB vs our pipeline),
the recommended next-session approach (brainstorm → write-plan → execute
with the full superpowers workflow), and a self-contained pickup prompt.

No code changes in this commit — handoff is doc-only. The 5 implementation
commits (ed727042bfeafd) remain at HEAD; next session decides whether
to revert R3.5 v1+v2 for a cleaner diff vs the R3 baseline, or layer
the restructure on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:52:46 +02:00
Erik
2bfeafd358 fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too
R3.5 v1 only gated the stencil branch on `cameraReallyInside`; the
depth-clear-if-inside at line ~7129 stayed on `cameraInsideCell`. During
grace frames after exit:

  cameraInsideCell   = true  (grace, holds previous cell for 3 frames)
  cameraReallyInside = false (PointInCell on camera pos returns false)

So depth-clear FIRED (writing depth = 1.0 globally) but the OUTDOOR branch
ran (single Draw(All) on every entity). With depth cleared, terrain's
depth = 1.0 — every entity below terrain (cellar geometry, basement
GfxObjs, anything at world Z < terrain Z) won the depth test and rendered
THROUGH the ground. User reported: "stand outside or pass outside → flicker
where objects are visible through ground and walls of other buildings are
missing."

v2 fix: unify depth-related gates on `cameraReallyInside`. During grace
frames depth-clear is now ALSO skipped; terrain depth survives; the
outdoor pass renders normally with proper terrain occlusion. Sky /
lighting / particles continue to use `cameraInsideCell` for smooth
grace-aware transitions.

The two-flag split is now explicit:
  cameraInsideCell    → sky, lighting (smooth, grace-aware)
  cameraReallyInside  → depth-clear, stencil branch (strict, no grace)

Closes the persistent transition flicker observed in R4 visual
verification after v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:40:35 +02:00
Erik
38d537491f fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment
Closes the transition-flicker symptom observed during R4 visual verification:
brief 1-3 frames after exiting a building where outdoor scenery rendered
with wrong stencil mask, "walls disappear and buildings show under ground"
shimmer, and sky stayed suppressed.

Root cause: CellVisibility.FindCameraCell holds the previous CameraCell
for ~3 grace frames after the camera physically exits the cell volume
(see _cellSwitchGraceFrames). The grace mechanism prevents flicker at
the doorway threshold for sky/lighting/depth-clear, but the new R3
stencil branch was using `cameraInsideCell` directly — so during grace
frames it ran MarkAndPunch with the previous cell's portals (now behind/
beside the camera) and the IndoorPass + stencil-gated outdoor produced
the garbage frame.

Fix: compute `cameraReallyInside` via the stricter
CellVisibility.PointInCell containment check and use it (instead of
`cameraInsideCell`) as the gate for the stencil branch. Sky, depth-clear,
lighting, and particles continue to use `cameraInsideCell` so their
smooth grace-aware behavior is unchanged.

Handoff item #10 (docs/research/2026-05-26-a8-revert-handoff.md) flagged
this exact concern: "Likely the CellSwitchGraceFrameCount = 3 interacting
with stencil setup timing." Confirmed and closed.

Visual-verification of the fix is part of R4 (re-run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:07:36 +02:00
Erik
60f07bc21b feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)
Replaces the pre-A8 single dispatcher call with the WB RenderInsideOut
order when cameraInsideCell:

  1. Terrain draws normally (color + depth)
  2. depth-clear-if-inside (depth = 1.0 globally)
  3. MarkAndPunch — stencil bit 1 at camera's-own-cell exit portals
  4. IndoorPass — cell mesh + cell statics + building shells, stencil OFF
  5. EnableOutdoorPass + re-draw terrain + OutdoorScenery, stencil-gated
  6. DisableStencil + LiveDynamic, depth-test only

Outdoor (cameraInsideCell == false) path unchanged: single Draw(set: All).

Step 5 (WB's 3-stencil-bit cross-cell-portal pipeline) is DEFERRED — we
mark only the camera's own cell's exit portals via [visibility.CameraCell],
not the BFS-extended VisibleCellIds. Trade-off documented in
docs/research/2026-05-26-a8-entity-taxonomy.md §"open questions".

Adds IndoorCellStencilPipeline field + ctor wiring + Dispose. Field types
the partition consumers from R2; the ParentCellId / IsBuildingShell /
ServerGuid distinctions are now consumed at runtime.

Visual verification at cottage interior / cottage cellar / inn interior /
dungeon is R4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:46:45 +02:00
Erik
55f26f2a9c feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
a three-way taxonomy-aware partition:

  IndoorPass     — cell mesh + cell statics + building shells
                   (ParentCellId.HasValue OR IsBuildingShell), live-dynamic
                   excluded
  OutdoorScenery — outdoor scenery only (ParentCellId == null AND
                   !IsBuildingShell), live-dynamic excluded
  LiveDynamic    — ServerGuid != 0 (player, NPCs, dropped items)

Centralizes the membership predicate in EntityMatchesSet to keep the three
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.

R1's IsBuildingShell flag is now consumed at render time. Integration into
the render frame ships in R3.

Tests rebuilt from scratch — 7 cases cover the new partition truth table.
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
default EntitySet.All.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:42:09 +02:00
Erik
ed72704f7b feat(world): Phase A8 R1 — tag WorldEntity.IsBuildingShell at LandblockLoader
Adds a bool flag at the WorldEntity data layer set by LandblockLoader from
the source dat array: LandBlockInfo.Buildings → true (cottage walls, inn
walls, smithy walls); LandBlockInfo.Objects → false (trees, lampposts,
rocks, hitching posts).

Retail anchor: CLandBlock::init_buildings reads a separate BuildInfo**
array from objects (acclient.h:31893 num_buildings / buildings field;
acclient_2013_pseudo_c.txt:313854 init_buildings entry). WorldBuilder
preserves the same distinction via SceneryInstance.IsBuilding
(StaticObjectRenderManager.cs:334). Today acdream's loader reads both
arrays into the same WorldEntity pool with no tag, destroying the
distinction (the comment at GameWindow.cs:5175 already acknowledges this
gap for scenery suppression). This commit closes the gap.

Render-time consumption arrives in R2 (EntitySet partition refactor).
Two new LandblockLoader tests lock the tagging behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:31:11 +02:00
Erik
d2db8d5b22 docs: Phase A8 REVERT handoff — full session story + pickup prompt
Documents the 3-round visual verification failure of the original
A8 plan, the architectural taxonomy gap that surfaced (cottage walls
are landblock-baked stabs with ParentCellId == null, not cell mesh,
so the binary IndoorOnly/OutdoorOnly partition mis-classifies them),
and what the re-plan must consider.

Bottom line: the WB stencil approach is correct in principle and the
infrastructure (Tasks 1-6: PortalPolygons field, RenderingDiagnostics
flag, portal_stencil shaders, IndoorCellStencilPipeline,
PortalMeshBuilder, EntitySet enum) is correct and tested. The
integration (Task 7) made a wrong architectural assumption about
entity classification. Reverted by fef6c61, 96f8bd2, c897a17.

Includes detailed pickup prompt for the re-plan session: re-investigate
entity taxonomy (6 distinct classes documented), spike distinguisher
options (AABB-encloses-camera heuristic recommended for first ship),
re-plan Task 7 with MarkAndPunch-first GL order + separate live-entity
pass + 3-building visual verification requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:42:53 +02:00
Erik
fef6c619a9 Revert "feat(render): Phase A8 — wire stencil pipeline into render frame"
This reverts commit 41c2e67cd8.
2026-05-26 09:38:37 +02:00
Erik
96f8bd2bd7 Revert "fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass"
This reverts commit a2ad5c1ac4.
2026-05-26 09:38:37 +02:00
Erik
c897a179fa Revert "fix(render): Phase A8 — mark-and-punch BEFORE indoor draw (correct WB order)"
This reverts commit b76f6d112e.
2026-05-26 09:38:36 +02:00
Erik
b76f6d112e fix(render): Phase A8 — mark-and-punch BEFORE indoor draw (correct WB order)
Second visual verification surfaced three depth-ordering bugs all from
one cause: the IndoorOnly dispatcher Draw ran BEFORE MarkAndPunch, so
the far-depth punch (gl_FragDepth = 1.0 at stencil=1 portal silhouettes)
overwrote any indoor depth that had been written there. Result:

  • Closed doors leaked outside terrain — door mesh wrote depth 0.6 at
    the portal silhouette, then the punch overwrote it to 1.0, then
    terrain at 0.99 won the depth test.
  • Walls between rooms leaked the far-side door/window opening —
    same mechanism: wall depth at the far-portal silhouette destroyed
    by the punch.
  • Animated character body bled to terrain where it overlapped a
    portal silhouette on screen — same mechanism: character depth
    destroyed by the punch.

Re-reading WB's RenderInsideOut (VisibilityManager.cs:73-239) confirms
the correct order is mark-and-punch FIRST, then indoor cells. Indoor
geometry drawn AFTER the punch wins the depth test against 1.0 and
correctly occludes the subsequent stencil-gated outdoor pass.

The swap is a single block move; MarkAndPunch was already correctly
leaving the GL state stencil-disabled for the indoor pass to follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:18:13 +02:00
Erik
a2ad5c1ac4 fix(render): Phase A8 — animated entities exempt from stencil-gated outdoor pass
Visual verification of A8 (commit 41c2e67) surfaced a showstopper:
player + NPCs disappeared when the camera entered a building. Root
cause: live server-spawned entities (animated player/NPCs/monsters)
have ParentCellId == null. The EntitySet partition classified them
as "outdoor" and stencil-gated them in the OutdoorOnly pass — so
they only rendered where stencil bit 1 was set (portal silhouettes),
producing partial-body and head-backwards artifacts at doorway
transits and full invisibility everywhere else inside.

Fix: animatedEntityIds overrides the ParentCellId-based partition.
Animated entities always belong in the IndoorOnly pass (stencil OFF),
never in OutdoorOnly. Three changes:
- WalkEntitiesInto full-walk path: compute isAnimated up front, use
  it in both partition checks
- WalkEntitiesInto animated-only path: skip the entire path on
  OutdoorOnly (every iterated entity is animated by definition)
- WalkEntitiesForTest: add optional animatedEntityIds parameter,
  mirror the new partition logic

Two new tests cover:
- EntitySet_IndoorOnly_IncludesAnimatedEntitiesEvenWithNullParentCellId
- EntitySet_OutdoorOnly_ExcludesAnimatedEntities

Known remaining limitation: dropped items / static-but-live objects
have ParentCellId == null AND are NOT in animatedEntityIds, so they
still classify as outdoor scenery and stencil-gate. Addressing this
requires a "live entity" flag on WorldEntity — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:55:45 +02:00
Erik
41c2e67cd8 feat(render): Phase A8 — wire stencil pipeline into render frame
Replaces the pre-A8 "terrain-always + depth-clear-when-inside" pattern
with WB's stencil-aware ordering when cameraInsideCell:

  1. Upload portal triangle mesh from VisibleCellIds → LoadedCell.
  2. Draw indoor entities (EntitySet.IndoorOnly) — stencil OFF.
  3. Mark portal stencil + punch far depth (MarkAndPunch).
  4. Draw terrain — stencil-gated to portal silhouettes.
  5. Draw outdoor entities (EntitySet.OutdoorOnly) — stencil-gated.
  6. DisableStencil before particles/weather/UI.

Outdoor path unchanged (EntitySet.All, no stencil work).

Adds CellVisibility.TryGetCell(uint) for the VisibleCellIds → LoadedCell
materialization. Removes the now-redundant DepthBufferBit Clear that
was the old approximation.

Retail oracle: PView::DrawCells at
acclient_2013_pseudo_c.txt:432709 ("outside_view.view_count > 0" gate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:29:45 +02:00
Erik
dcf69a1feb feat(render): Phase A8 — WbDrawDispatcher.EntitySet partition
Adds EntitySet { All, IndoorOnly, OutdoorOnly } and a Draw parameter to
partition the per-entity walk by ParentCellId presence. EntitySet.All
preserves pre-A8 behavior; IndoorOnly drops null-ParentCellId entities;
OutdoorOnly drops ParentCellId.HasValue entities. The visibleCellIds
filter is still applied on top.

Used by Task 7 to split the render frame's single Draw call into two
(indoor stencil-OFF, outdoor stencil-gated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:20:45 +02:00
Erik
a1c393ee14 hardening(render): Phase A8 — IndoorCellStencilPipeline robustness
Three small improvements from Task 5 code review:
- MarkAndPunch now enables DepthTest explicitly (was relying on
  GameWindow's startup enable; this makes the method self-contained).
- Uniform location fields marked readonly (set once in ctor).
- AllocateVbo gets a comment noting that mid-session reallocation is
  safe because the VAO bakes the VBO association at ConfigureVao time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:16:42 +02:00