Two changes for visual-gate-#1 follow-up. After the pool aliasing fix
(prior commit), walls + objects render cleanly but three residual symptoms
remain: missing floor, purple wall tint, no sky through windows. This
commit addresses one and adds the probe for the second.
Sky fix:
The blanket `!cameraInsideCell` skip of the sky pass was inherited from
when the indoor-cell concept was sealed dungeons. With Phase A8's
RenderInsideOutAcdream pipeline, cottages render through their portals
to outside — and the user expects sky visible through windows + doorways.
WB's VisibilityManager.RenderInsideOut assumes sky has already been
rendered as the far-depth backdrop before stencil setup. New gate:
`!cameraInsideCell || cameraInsideBuilding`. Sky renders inside cottages
(building → portals), skipped inside true dungeons (no portals). The
Step 4 stencil-gated outdoor pass composites terrain + scenery through
portal silhouettes on top of the sky.
Per-cell audit probe (ACDREAM_A8_AUDIT=1):
One-shot dump per (cellId, gfxObjId) pair in the active snapshot:
- renderData null/non-null status
- batches count + total IndexCount
- per-batch CullMode + IsTransparent + IsAdditive + bindless-handle-zero
The first visual gate showed tris=135 for 18 cells — way too low if cell
meshes were complete (expected ~20+ tris/cell). The audit dump will
identify whether (a) some cells aren't uploading, (b) some batches have
zero indices, or (c) batches' CullModes are getting them culled at
typical viewing angles. Without this probe, we'd be back to speculation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defense-in-depth apparatus per the 2026-05-27 handoff's option-1 recommendation.
The audit-found pool aliasing bug (prior commit) is the primary fix; this probe
is the safety net for any unidentified residual issue when the visual gate runs.
EmitDrawOrderProbe now logs the full GL state at each step boundary of
RenderInsideOutAcdream — stencil test/func/ref/mask/op, depth func/mask, cull
face/mode, blend src/dst, color writemask, current VAO, current program. An
operator can read the log offline and compare line-by-line against WB's
expected state at VisibilityManager.cs:73-239. Any divergence pinpoints the
bug's GL-state shape; matching state confirms the issue is elsewhere
(instance data, mesh upload, etc.).
EmitEnvCellProbe now logs pool diagnostics — total pool size + snapshot's
PostPreparePoolIndex high-water mark. A spike in poolTotal across stationary-
camera frames, or a divergence between poolHwm and cell-count, signals
pool-management regression. The fix-the-bug-first principle keeps this probe
dormant by default; enable via ACDREAM_PROBE_VIS=1 only when investigating.
Heavy (~10 GL queries per step × 5-10 steps per frame), but gated.
86/86 App tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-Wave-5 indoor branch chaos (flickering, missing walls, GPU 100%,
~10 FPS) is caused by two interconnected pool-management bugs in
EnvCellRenderer that line-by-line WB comparison surfaced in 30 minutes.
Neither was found by the five post-Wave-5 speculative fixes because none
of them inspected the pool path.
Bug #1 — GetPooledList missing list.Clear():
The reuse branch returned pool lists with prior-frame data still inside.
PrepareRenderBatches' merge phase pattern `gfxDict[k] = list; list.AddRange(...)`
assumes empty lists. Without Clear(), lists grow unbounded each frame, GPU
draws cumulative instance counts, and per-instance transforms become a stew
of past + present data. Mirrors WB ObjectRenderManagerBase.cs:1221-1233.
Bug #2 — Render uses snapshot.BatchedByCell.Count instead of PostPreparePoolIndex:
The snapshot author dropped WB's PostPreparePoolIndex field calling it
"scenery-only," then "compensated" in Render by setting _poolIndex to the
cell count. The cell count has no relation to the pool — Prepare may have
used 50+ pool lists for an 18-cell scene. Render's filter-path GetPooledList
then returns lists that ARE in snapshot.BatchedByCell, corrupting the snapshot
mid-Render. Restoring PostPreparePoolIndex (WB VisibilitySnapshot.cs:31)
correctly places Render's pool cursor past the snapshot's owned region.
Bug #3 (minor) — PopulateRecursive hardcoded isSetup:false for nested parts:
Setup IDs use high-byte 0x02 (per retail). WB ObjectRenderManagerBase.cs:813
checks `(partId >> 24) == 0x02` to detect nested Setups. Our port always
passed isSetup:false, silently dropping any nested Setup (its TryGetRenderData
returns IsSetup=true, Render's `!IsSetup` guard skips the draw). Probably
rare in EnvCells but fixed for completeness.
Regression coverage:
- GetPooledList_ReusedList_IsClearedBeforeReturn — would have failed pre-fix
- GetPooledList_FreshList_IsAlwaysEmpty — sanity check
- Snapshot_PostPreparePoolIndex_IsInitSettable — compile-time guarantee
- Snapshot_PostPreparePoolIndex_DefaultsToZero — defensive default
86/86 App tests pass. Build green. The fix is the audit's primary
deliverable; the GL state probe option-1 apparatus follows in a separate
commit as defense-in-depth for any unidentified residual issue.
Full audit + WB cross-reference in
docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 5 visual-gate failures with speculative fixes that each addressed
plausible-looking symptoms without resolving the chaos (texture flicker,
missing walls, GPU 100%, ~10 FPS), this commit stops the speculation and
ships a kill-switch that reverts default behavior to pre-A8.
The user's verbatim authorization at session start said "no quickfixes
or fixes that might cause issues down the line ... no band-aids." The
post-Wave-5 fix stream WAS band-aids — each fix was pattern-matched
against possible RR7-era causes without confirming the actual root
cause from evidence. Five failures in a row is the signal to stop.
ACDREAM_A8_INDOOR_BRANCH gate:
- Unset or != "1" (DEFAULT): cameraInsideBuilding forced false. Outdoor
Draw(All) path runs for indoor cells too. Pre-A8 depth-clear-if-inside
workaround at line ~7314 is restored. Visual behavior = pre-A8.
- Set to "1": indoor branch (RenderInsideOutAcdream) runs. All A8 code
exercises. Probes ([envcells]/[stencil]/[draworder]/[buildings]) emit.
All Phase A8 scaffolding (Waves 1-5 + post-Wave-5 fix commits) remains
in tree, accessible for the next-session apparatus to test against.
~1,830 LOC of WB-extracted infrastructure preserved.
Handoff doc at docs/research/2026-05-28-a8-wb-port-shipped-but-broken-handoff.md
captures the full chronicle: which fixes were applied, what each
visual-gate launch reported, the root-cause hypotheses tested and
falsified, the remaining unknowns, and the recommended apparatus
approaches (frame-replay harness / per-step GL state probe / WB-renderer
side-by-side / mesh-data audit).
Next session's mission: NO MORE LIVE LAUNCHES until apparatus is built.
This is the same trap the issue #98 saga fell into before the
trajectory-replay harness shipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report after Step 5 disable + ColorMask cleanup + cull-cache fix +
terrain-skip fix: "Still chaos, GPU 100%. House missing lots of walls."
Root cause finally found: Pre-A8 indoor walls came from IsBuildingShell
entities (landblock-baked GfxObj slabs that represent cottage exterior
walls — NOT part of any cell's CellStruct). They were drawn via
Dispatcher.Draw(set: IndoorPass) in the pre-A8 outdoor path.
WB's algorithm assumes its StaticObjectManager.Render in Step 4 handles
these (its partition lumps shells with outdoor statics). Our EntitySet
partition (RR2) puts IsBuildingShell into IndoorPass (alongside cell
stabs), NOT OutdoorScenery — because logically shells ARE indoor walls.
A8's RenderInsideOutAcdream Step 4 calls Draw(set: OutdoorScenery)
which EXCLUDES IsBuildingShell. So cottage exterior wall slabs never
render in A8. EnvCellRenderer provides the floor + interior CellStruct
walls, but the shell slabs (exterior walls visible from inside) are
gone. Symptom: "missing walls" because half the cottage walls are
landblock-baked shells, not cell mesh.
FIX: insert a Draw(set: IndoorPass) call between Step 3 (cells) and
Step 4 (stencil-gated outdoor) when cameraInsideBuilding. Uses
currentEnvCellIds as the cell filter — narrows cell stabs to camera-
building cells; building shells (no ParentCellId) pass through and
all render. Depth-tested (DepthFunc.Less) so cottage-A's near walls
occlude cottage-B's far walls; no stencil so shells render
unconditionally inside the camera building.
Build green.
This was the root cause behind the 4 days of RR7 failures. The
handoff doc even noted "Building shells render (they ARE GfxObj
entities with proper mesh refs after hydration)" — but the new
RenderInsideOutAcdream code DIDN'T call IndoorPass, only
OutdoorScenery. Hence shells never rendered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report after Step 5 disable + cull-cache fix: "Cant see anything,
flickering colors, sometimes textures sometimes inside, house missing
lots of walls. 10 FPS. GPU 100%."
Root cause: terrain was being drawn TWICE per indoor frame:
1. Line 7283 _terrain.Draw — UNCONDITIONAL pre-A8 pass; writes full-
screen terrain color + depth at terrain Z.
2. Step 4 inside RenderInsideOutAcdream — stencil-gated terrain at
portal silhouettes only (matching WB VisibilityManager:143).
Pre-A8 papered over the Z conflict with a depth-clear-if-inside
workaround (cleared depth between terrain and cottage) which we
DELETED as part of Wave 4. Without it, when Step 3 writes the cell
geometry with DepthFunc.Less, the cottage walls at Z=92-94 (where
they meet the ground) Z-FIGHT against the terrain already in the
depth buffer from pass 1. Symptom: flickering walls + missing
sections + GPU saturated drawing terrain twice.
Fix: gate the line-7200 terrain draw on `!cameraInsideBuilding`.
When indoor, Step 4 is the SOLE terrain pass — stencil-gated to
portal silhouettes only. No double-draw, no Z-fighting, no need
for the deleted depth-clear workaround.
Outdoor mode unchanged (pass 1 still fires, Step 4 isn't taken).
Build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report from second visual gate (Step 5 disabled, ColorMask fixed):
"Cant see anything, flickering colors, sometimes I see textures and
sometimes I see inside, the house is missing lots of walls. 10 FPS."
Root cause: EnvCellRenderer._currentVao and _currentCullMode are STATIC
caches that let SetCullMode / BindVertexArray skip redundant GL state
changes when "already" in the right state. But other consumers
(WbDrawDispatcher, terrain renderer, the Step 1+2 stencil pipeline)
change the actual GL state without updating these caches. The cache
lies, the per-batch SetCullMode in RenderModernMDIInternal skips its
glCullFace call, and the cottage's mixed-cull-mode batches end up
rendering with whatever stale cull state was leaked from the prior
consumer. Walls with backface-only geometry vanish. The flicker is
the state alternating depending on which Render call set the cache
this frame.
WB invalidates these caches at line 404-410 of EnvCellRenderManager.cs:
CurrentVAO = 0;
CurrentIBO = 0;
CurrentAtlas = 0;
CurrentInstanceBuffer = 0;
CurrentCullMode = null;
Our port missed this. Adding _currentVao = 0; _currentCullMode = null;
at Render entry forces each Render call to re-establish the GL state
it expects. (We only track Vao + CullMode in our minimal port; IBO/
Atlas/InstanceBuffer aren't cached in our class.)
Build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
R1 + R2 + R3 + R3.5 v1 + R3.5 v2 all shipped this session (ed72704 →
2bfeafd). 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 (ed72704 → 2bfeafd) 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>
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>
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>
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>
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>
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>
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>