The armed tripwire self-attributed on the re-gate launch
(regate-118-119-launch.log): a pure TWO-CELL reciprocal ping-pong, 64 laps
each - chain root=0xA9B4015C eye=(109.995,37.158,96.249) cells 0162x64
015Cx64, and root=0xA9B3010F eye=(175.771,-107.310,118.814) cells 0103x64
010Fx64 (A9B3 = the hill cottage the user reports going all-transparent on
entry - likely the same mechanism, verify at the next gate).
Mechanism: with the eye within PortalSideEpsilon (+-1 cm; the T2
refuted-to-tighten root-lag tolerance - retail's is 0.0002) of a portal
plane, the in-plane case counts as interior for BOTH cells, so views lap
A->B->A...; each lap re-clips through two near-edge-on apertures whose
intersection numerics wobble by more than CellView's 1e-3 dedup grid, so
every lap keys as NEW and the in-place growth recurses to the depth-128
failsafe. The prior convergence sweeps could not reproduce because they
only load the corner building 0x016F-0x0175 - both firing pairs are
outside that set. Issue120ReciprocalPingPongTests loads the full
landblock's interior cells and drives the +-epsilon window directly:
deterministic firings + 65-polygon CellView piles pre-fix.
Fix (the handoff's own predicted class - dedup admitting near-duplicates
per lap, NOT a limit tune): CellView.Add rejects a polygon CONTAINED in
one already stored (convex edge test, DedupGridNdc slack). A round-trip
re-emission is, in exact arithmetic, a SUBSET of the polygon that
originated it - containment rejection makes union growth strictly
area-increasing, so no new visible area means no propagation. Bonus:
back-emission into a full-screen view (the root cell) now always rejects.
The corner-flood completeness pins stay green - no real region is dropped.
Suites: App 236 (232+4), Core 1419+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue119UpNullGfxObjDumpTests pins the dat truth: 0x010002B4 = 9 polys,
ALL NoPos, all surfaces Base1Solid; 0x010008A8 = 1 poly, NoPos,
Base1Solid|Translucent. Retail's skipNoTexture never draws either model
(the BR-1 build-time-skip <=> draw-time-skip equivalence), so
ObjectMeshManager's empty render-data cache is the CORRECT terminal state
- the only defect was the alarming "permanently invisible" log line,
reworded into an honest tripwire pointing at the dump test.
Second fact, same test (ShellModel_NoTexturedPolyIsDropped): on the
hall/tower shell 0x010014C3, ZERO textured polys are dropped by the
extraction gates (137/149 draw; the 12 dropped are the known #113
no-draw orphans) - the per-poly GfxObj extraction is exonerated for
building shells, kept green as a regression pin.
Net for #119: the missing tower-stair parts are NOT the up-null pair and
NOT a per-poly extraction drop. Remaining hypothesis space (interior
stair-cell flood admission, or a different model than assumed) needs the
re-gate to identify the exact tower; then the cell set + flood replay
headlessly like #118. ISSUES.md updated.
Suites: App 232, Core 1419+2skip (1416+3 new), UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (pinned by the new deterministic exit-walk harness, NOT guessed):
under an interior render root, the exit-portal SEAL stamps the door fan at
TRUE depth after the gated full depth clear, and T1's "ALL dynamics last"
pass then drew the outdoor-classified player depth-tested - every fragment
beyond the door plane z-failed against the seal across the whole aperture.
Harness measured the full window: from the moment the sphere center crosses
the plane until the eye follows (~2.6 m of camera lag, ~2.2 s at walk speed)
the player is invisible; while straddling, the beyond-plane body half clips
at the plane. The handoff's three cone-level candidates are all EXONERATED:
the cone walk passes every step; (eye, ViewerCellId) come from the same
SweepEye call with camera-update-before-visibility-read in the same frame;
the side-test window is sub-epsilon under healthy resolution.
Retail oracle (grep-named-first): PView::DrawCells 0x005a4840 runs
LScape::draw FIRST (pc:432719), then the gated depth clear (pc:432731-32)
and the exit-portal seals (pc:432785-86); outdoor cell objects draw inside
the landscape stage (DrawBlock 0x005a17c0 -> DrawSortCell pc:430124), and
an object draws once per overlapped shadow cell (pc:430056-64) - the
straddling body composes from both stages, neither half clips.
Fix: RetailPViewRenderer assigns dynamics to the OUTSIDE stage under an
interior root when outdoor-classified OR sphere-straddling an exit-portal
plane of their flood-visible cell (DynamicDrawsInOutsideStage - pure, the
harness drives it as the ordering contract); they ride the landscape slice
draw (pre-clear, seal-protected) with the same per-slice cone test as
outdoor statics. Indoor dynamics keep the last pass (retail loop C);
straddlers draw in both (retail shadow dual-draw). Outdoor roots keep
all-dynamics-last - the BR-2 punch-after-dynamics lesson (88be519) stands.
Apparatus: HouseExitWalkReplayTests - dat-backed corner-building exit walk
driving the production stack headlessly (RetailChaseCamera damping ->
healthy-sweep viewer resolution -> PortalVisibilityBuilder.Build ->
ClipFrameAssembler -> ViewconeCuller -> the DrawDynamicsLast predicate +
a CPU seal-depth model). 5 tests: cone pin, seal-depth pin, straddle
dual-draw pin, per-step table, stale-root window quantifier (#118 cand 2).
Suites: App 232 (227+5), Core 1416+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T5 reported doors/interiors visible through terrain hills and through
nearer buildings, always in aperture-shaped regions. Root cause, decomp-
settled: retail's DrawPortalPolyInternal (Ghidra 0x0059bc90) draws the
punch with DEPTHTEST_ALWAYS + per-vertex far-Z (0.99999899, maxZ1 bit0)
- it UNCONDITIONALLY stomps any occluder depth at aperture pixels.
Retail is safe only because its outdoor pass is painter's-ordered
far->near: anything nearer (hills, closer houses) draws AFTER the punch
and re-covers it. Our z-buffered MDI frame has no such global order
(one terrain pass + one shells pass), so the faithful GL-state port of
the punch was unsafe by construction - the far house's aperture punch
erased the near house's wall depth / the hill's depth, and the interior
+ door entities (dynamics drawn last) painted through.
Fix - the z-buffer-correct equivalent of the painter's-order guarantee:
punch only where the aperture polygon itself is VISIBLE.
PortalDepthMaskRenderer's punch path is now two passes:
A) stencil-mark: aperture fan at its (slightly biased) true depth,
depth LEQUAL, no depth write -> stencil=1 where the aperture wins
against everything drawn so far (terrain + all shells precede
DrawExitPortalMasks in the frame, so the buffer holds the real
occluders);
B) far-Z punch with depth ALWAYS, stencil-gated EQUAL 1, zeroing the
stencil as it goes (self-cleaning; no frame-level stencil state).
The mark bias (0.0005 NDC ~ 6 cm at 5 m) keeps #108's case covered:
terrain hugging the door plane still punches; a hill or another house
meters nearer no longer does. The SEAL path (interior roots) stays
retail-verbatim single-pass - it runs right after the gated full depth
clear, so there is nothing nearer to stomp.
Also: WindowOptions now requests 8 stencil bits explicitly (was the
GLFW platform default), and PortalDepthMaskRenderer's stale "RESERVED -
not wired" banner is corrected (T1 wired it via
DrawRetailPViewPortalDepthWrite).
Acceptance rides the focused post-T5 re-gate (downhill door check +
behind-house openings check + #108 cellar stays clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Investigation: retail's growth propagation RECURSES natively too
(AddViewToPortals -> FixCellList -> AdjustCellView -> AddViewToPortals,
Ghidra 0x005a52d0/0x005a5250/0x005a5770, no depth guard) - the in-place
recursion shape is faithful; retail's safety is fast convergence. Our
depth-128 firing means slow/non-saturating growth (each lap of a portal
cycle nests one recursion level), not necessarily a true infinite loop.
Two dat-backed sweeps over the corner-building cell set could NOT
reproduce the T5 firing:
- PortalPlaneCrossings_InPlacePropagationConverges: +/-6cm eye sweep
across every portal plane, seeded from both sides.
- InCellDirectionSweep_InPlacePropagationConverges: 3024 builds, in-cell
eye grid x 8 yaw x 3 pitch (the walking-and-turning regime).
Both pass with 0 firings -> production-only ingredients suspected (full
lookup graph - one T5 firing was 0x0162, another building - and/or the
real camera path).
Armed: PortalVisibilityBuilder.ConvergenceTripwireCount (test
observable, both Build + look-in sites) + DumpPropagationChain - on the
next firing the log carries root cell, eye, per-cell frequency summary,
and the 24-entry chain tail, so the cycle's structure (A<->B ping-pong
vs 3-cycle laps) reads directly off the output. Both sweeps stay as
regression pins.
App tests: 227 green (was 225; +2 pins).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The A6.P4 port, fused into one installment per the BR-2 half-port lesson
(registration and query are co-dependent: flood-registering shells under
the old radial query would re-open #98 through the vestibule).
REGISTRATION (ShadowObjectRegistry rewritten):
- Register/RegisterMultiPart/UpdatePosition compute the cell set via
CellTransit.BuildShadowCellSet (the C2 find_cell_list flood) seeded by
the entity's m_position cell id; the private 24m XY-grid rectangle and
its single-landblock clamp are deleted. Flood spheres follow retail's
CylSphere rule (base point + cyl radius, cap 10; BSP bounding-sphere
fallback - Ghidra 0x0052b9f0). Statics flood with the do_not_load
prune; dynamics (server spawns, isStatic:false) without.
- Keep-when-empty (SetPositionInternal num_cells gate, pc:283540): a
failed flood leaves the previous registration in place.
- RefloodLandblock: streaming-race hook re-runs the flood when a
landblock's cells hydrate (retail init_objects -> recalc_cross_cells,
Ghidra 0x0052b420/0x00515a30); wired at GameWindow's hydration tail.
- GameWindow sites pass the server position's full cell id as the seed
(spawn + UpdatePosition); the five static sites pass ParentCellId.
BUILDING CHANNEL (CSortCell.building shape):
- Building SHELLS are not shadow objects in retail (only caller of
find_building_collisions is CSortCell::find_collisions 0x005340aa;
one building per origin landcell, init_buildings 0x0052fd80 verified
verbatim + ACE cross-ref). IsBuildingShell entities skip the registry;
Transition.FindBuildingCollisions runs the shell part-0 BSP off
cache.GetBuilding(cellId) with bldg_check set around it
(find_building_collisions 0x006b5300), CollidedWithEnvironment on
non-Contact non-OK. BuildingPhysics.ModelId = pre-resolved part-0
GfxObj (0x02 Setups resolved at the CacheBuilding site).
- Placement/ethereal weakening: BSPQuery Path 1 passes center_solid=0
when BldgCheck && HitsInteriorCell (BSPTREE::find_collisions 0x0053a82e
+ placement_insert 0x005399d8) so doorway crossings don't hard-fail
against shell solids. SpherePath gains both retail fields;
HitsInteriorCell is rebuilt at every cell-array build
(build_cell_array reset 0x00509ef2 + find_cell_list/check_building_
transit set sites).
QUERY (retail per-cell order, transitional_insert 0x0050b6f0):
- TransitionalInsert per attempt: env -> building (LandCell only) ->
objects on the PRIMARY cell, then on OK the check_other_cells pass
(env -> building -> objects per OTHER overlapped cell) + the
carried-cell advance - the advance now happens AFTER all per-cell
object passes (the WF1 ordering divergence), with Adjusted/Slid
feeding the retry exactly like retail's OK_TS case.
- FindObjCollisionsInCell = CObjCell::find_obj_collisions (0x0052b750):
iterate ONLY the asked cell's list. DELETED: the radial 9-landblock
sweep, the +5m query pad, the b3ce505 indoor-primary gate, and the
isViewer exemption (the camera is bounded by interior cell-BSP env
collision - retail's own channel; CameraCornerSealReplayTests pins it
against real dat, and the new building-channel camera test pins the
outdoor stop).
TESTS: Core 1416/0/2 (was 1398 + 4 pre-existing #99-era fails + 1 skip),
App 225, UI 420, Net 294 - all green.
- 3 of the 4 #99-era reds flipped green as designed: the door apparatus
(Apparatus_Grounded_50cmOffCenter_FrontApproach_Blocks) and tick-13558
(indoor walkthrough) now assert the door BLOCKS; tick-22760 pins the
outdoor blocking invariant.
- The 4th (BSPStepUp D4) + 22760's lateral-slide delta are NOT cell-set
problems (probes prove the door is found + BSP-only dispatched;
BR-7 left both byte-identical) - filed as issue #116 (slide-response
family), D4 skipped with the issue reference.
- FindEnvCollisionsMultiCellTests migrated to the public entry (the A4
multi-cell halt now lives at the retail call site).
- New registry pins: per-cell query surface, outdoor-footprint-never-
indoor (#98 architectural), door-outdoor-cell membership, reflood.
- CameraCollisionIndoorTests rewritten against the building channel
(the isViewer-exemption pins died with the exemption).
Closes#99 (doors block both ways via registration-time cell membership
+ the straddle-spanning player cell array). #97 likely closed (the +5m
radial pad that produced phantom-collision candidates is gone) - verify
at T5. #98 stays closed ARCHITECTURALLY (outdoor footprints structurally
cannot reach interior cells; the cellar harness stays green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail CEnvCell::check_building_transit (Ghidra 0x0052c5d0) opens with
`if (other_portal_id >= 0)` on the SIGNED sign-extended portal id
(CBldPortal.other_portal_id is int, acclient.h:32098). Our BldPortalInfo
carried the dat reader's raw ushort and CheckBuildingTransit had no gate
at all, so a portal whose dat value is 0xFFFF (-1, "no reciprocal
portal") could admit its interior cell. BN's pseudo-C renders the
comparison unsigned - the sign-extension is Ghidra-proven (BR-7 verified
corrections, wf1-interior-collision.md).
- BldPortalInfo.OtherPortalId: ushort -> short; GameWindow construction
reinterprets the dat ushort via unchecked((short)).
- CheckBuildingTransit: negative-id portals rejected before any sphere
test; new multi-sphere overload matching retail's per-sphere loop
(0052c5fe, first-hit admits) with the hits_interior_cell output
(0052c650) the BR-7 building channel consumes next.
- Tests: negative-id skip vs positive-id admit on a leaf-root CellBSP;
multi-sphere plumbing + zero-sphere no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-gate rule (feedback_render_one_gate) is now structural:
- The per-frame ACME BFS (CellVisibility.ComputeVisibilityFromRoot) is
GONE from the frame. Its only production consumer was the
cameraInsideCell boolean - which is exactly 'viewerRoot is not null'
(the TryGetCell that produced viewerRoot already proves cells are
loaded; ComputeVisibilityFromRoot returned null iff root was null).
A full second visibility computation ran every frame to derive a
boolean we already had. The method + its tests remain as quarantined
non-production code (dual-live-visibility-computations, confirmed).
- The clipRoot==null mini-pipeline is DELETED (legacy-outdoor-branch-
remnant, adjusted-confirmed): the outdoor partition draw, the
Chebyshev look-in gather, the DrawPortal invocation and the dynamics
fallback. clipRoot is null only when NO viewer cell exists (pre-login,
fly/debug cameras, transient gaps) - those frames draw flat through
the dispatcher; every normal outdoor frame is the outdoor node.
- DELETED with it: InteriorRenderer (class file - its only caller was
the legacy branch), RetailPViewRenderer.DrawPortal +
RetailPViewPortalDrawContext (the look-in product; outdoor-root frames
flood buildings via MergeNearbyBuildingFloods inside DrawInside),
the _exteriorPortal*/_outdoorRootNoCells fields.
Per frame there is now exactly ONE visibility computation
(PortalVisibilityBuilder) and ONE render path (DrawInside).
Suites: build green, App 226 green, Core baseline (1398 + 4 pre-existing
#99-era).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports Render::viewconeCheck (Ghidra 0x0054c250): meshes are sphere-CULLED
per portal view, never hard-clipped. NEW ViewconeCuller lifts each
slice's <=8 clip-space half-planes to world-space eye-edge planes (the
view_vertex.plane analog, acclient.h:32483 - one matrix fold: L = VP
rows . P) and tests bounding spheres from the entity's cached AABB (the
dispatcher's own cull bounds source).
Gating now matches retail's shape end to end:
- Per-cell STATICS: sphere vs THEIR CELL's views - the statics-through-
walls fix (the cottage phantom staircase's actual draw path: a static
outside every view of its cell no longer paints through the wall).
- DYNAMICS (last pass): sphere vs their cell's views; outdoor/unresolved
vs the outside views (pass-all under the outdoor root). A dynamic in a
non-flooded room culls HERE - retail never reaches an object whose cell
is not in the draw list; the partition still routes it so the CULL is
what drops it, retail's shape exactly.
- OutdoorStatic (landscape pass): pre-filtered per outside slice; the
per-slice entity gl_ClipDistance routing is DELETED (entities draw
outside the clip bracket; terrain/sky keep their plane clip).
- PARTICLES: the scissor-AABB gate is DELETED; emitters gate through
their cone-surviving owners (candle-flames-through-walls fix).
- WEATHER: gated on the PLAYER being outside (retail is_player_outside -
an indoor player gets no rain even looking out a doorway). Closes
weather-gate-player-vs-viewer.
- UNATTACHED emitters (campfires) get their missing outdoor-root pass
(closes unattached-particles-dropped-outdoors).
Suites: App 226 green (flood gates included), Core baseline unchanged
(1398 + 4 pre-existing #99-era).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail floods a building's interior exactly when its shell DRAWS and an
aperture survives the view: DrawBuilding (Ghidra 0x0059f2a0) -> per-view
viewconeCheck on the shell -> portal-BSP walk -> ConstructView(CBldPortal)
side test + GetClip-vs-view + GetVisible. There is NO distance constant
anywhere on that chain (verifier-confirmed, flood-gate-shape adjusted).
Port:
- GameWindow's outdoor-node gather: per-BUILDING frustum pre-gate on the
aperture bounds (Building.PortalBounds - the tight flood-purposes
equivalent of the shell viewconeCheck), iterating the per-landblock
BuildingRegistries. Replaces the Chebyshev<=1 landblock cell-sweep.
Also the proper fix for the 2026-06-07 'FPS drops when I look out'
problem the Chebyshev hack approximated: dozens of AABB tests instead
of an O(all loaded cells) portal sweep.
- OutdoorBuildingSeedDistance 48f -> infinity (the binary visibility pop
at ~48 m - the confirmed #109 mechanism candidate - is gone; admission
is now the screen clip per portal, retail's GetClip gate).
- The legacy clipRoot==null look-in path keeps its 48 m: it is T4
deletion scope; improving doomed code wastes effort.
Closes the building-flood-seeding-48m-cutoff divergence (culling area,
adjusted-confirmed). Suites: App 226 green (flood gates included).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IN (retail-faithful):
- Growth propagation is IN PLACE, never by re-enqueue: retail
AddViewToPortals (Ghidra 0x005a52d0) enqueues only on first discovery
(InsCellTodoList); growth into a popped cell runs AdjustCellView -
re-clip ONLY the new views through that cell's portals immediately.
ProcessCellPortals + the processedViewCounts watermark port exactly
that; the MaxReprocessPerCell=16 drift cap is DELETED (the 1-px merge
is the physical fixpoint floor). Depth-128 tripwire logs loudly if the
convergence invariant ever breaks (failsafe, not control flow). Same
restructure in BuildFromExterior.
- Reciprocal-empty culls strictly (retail OtherPortalClip returning
nothing = the opening is invisible from the neighbour's side); the
eye-in-opening pre-clip restore is gone.
- Look-in seeds: retail ConstructView(CBldPortal) IN_PLANE reject at the
TRUE F_EPSILON (SeedInPlaneEpsilon=0.0002, const @0x007c8c70) + the
full-screen substitute rescue DELETED (the verifier-flagged non-retail
bypass that admitted floods retail strictly rejects).
REFUTED BY THE CONFORMANCE GATE (attempted, reverted, documented inline):
- PortalSideEpsilon 0.0002: retail's tight epsilon assumes the viewer
cell transits the instant the eye crosses a plane; our root can lag
~1 cm at pressed corners (CornerFloodReplay failed at every step -
0x0171/0x0173 chain dead). 0.01 KEPT as the documented root-lag
tolerance; tighten only with eye-exact viewer tracking + cdstW.
- Deleting the clip-empty eye-in-opening rescue: same gate, same total
failure - our ProjectToClip near-eye behavior (EyePlaneW=1e-4) diverges
from retail polyClipFinish's UNPINNED cdstW constant. Rescue KEPT as
the documented cdstW-gap compensation; re-attempt only after pinning
cdstW from the binary.
Gates: App 226/226 green (CornerFloodReplay + MeetingHallFlood + the
collapsed-portal pin all pass); Core baseline unchanged (1398 + 4
pre-existing #99-era). CloneViewPolygons orphan removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(a) MergeBuildingFrame now UNIONS a building flood's views into cells
already present in the frame (retail Render::copy_view APPENDS every
clipped portal polygon as a new view_poly, Ghidra 0x0054dfc0 - a cell
visible through two apertures holds two views). The old first-wins
'ContainsKey -> continue' dropped the second aperture's views: the
multiview-loss-first-wins divergence, a named #109 suspect.
(b) ClipToRegion output now runs retail's post-divide vertex merge:
consecutive vertices closer than ~1 pixel collapse (copy_view's
|dx|<=1 && |dy|<=1 screen-unit merge), polygons that collapse below 3
distinct verts return empty (retail's '<3 survivors -> count 0'). This
is the flood's PHYSICAL fixpoint floor - re-clipping can only insert
sub-pixel slivers, which the merge removes, so accumulated views
converge instead of drifting. Unit note: builder has no viewport, so
1 px is expressed as NDC at reference 1080p (0.00185); coarser at higher
res, which only strengthens convergence. This is the prerequisite for
removing the MaxReprocessPerCell=16 drift cap (T2 slice 2) and the
EyeInsidePortalOpening rescue.
Gates: all 10 flood conformance tests green (CornerSweep monotone pin
included); App 226 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The complete retail drawing order in one installment (per the amended plan:
every installment is a COMPLETE retail behavior - the half-ported punch of
88be519 is re-landed here WITH the ordering that makes it correct):
static world (sky/terrain/weather/shells/scenery)
-> aperture depth writes (interior SEAL at true depth / outdoor+look-in
PUNCH to far-Z; PortalDepthMaskRenderer, DrawPortalPolyInternal
Ghidra 0x0059bc90)
-> interior cells WHOLE, far-to-near, drawn once (DrawCells Loop 2,
Ghidra 0x005a4840; use_built_mesh pc:427905)
-> per-cell STATIC object lists
-> ALL dynamics LAST (DrawDynamicsLast), depth-tested, never hard-clipped
InteriorEntityPartition: new contract - every ServerGuid != 0 entity goes
to Dynamics regardless of cell (indoor/outdoor/unresolved/hidden); ByCell
carries only dat-baked indoor statics of visible cells; Outdoor renamed
OutdoorStatic. Fixes the audit's livedynamic-invisible-under-interior-roots
divergence as a side effect (live entities are never dropped by the
visibility set; culling is T3's viewcone).
DELETED (retail has no counterpart): the gl_ClipDistance shell chop
(927fd8f enable + 9ce335e outdoor scoping + UseShellClipRouting + the
per-slice shell loop + clipShells param) - retail never clips cell
geometry; aperture exactness = punch/seal + z-buffer + this order. The
old per-slice scissored AABB depth clear is replaced by retail's single
gated full clear (ClearDepthForInterior). The interior-root LiveDynamic
top-up draw and the look-in's dynamics involvement are gone (one last
pass, no double-draws).
Closes at the T5 gate (expected): #114 (chop deleted), the char-eaten-by-
doorway regression (ordering), outdoor interiors-through-doorways (punch);
#108's render half (seal) - its membership half stays re-attributed.
Suites: build green, App 226 green (partition tests rewritten to the T1
contract), Core 1398 + 4 pre-existing #99-era + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Visual gate (2026-06-11) on the seal+punch build, then on the punch-reverted
build, isolated the truth:
- With the punch wired: #108 (cellar grass-sweep) gone BUT the player/NPCs
go transparent by exactly their overlap with any doorway viewed from
outside (the far-Z punch erases the depth of dynamic objects standing in
the aperture, so the interior paints over them).
- With ONLY the punch reverted (seal+full-clear kept): characters render
correctly AND #108 is BACK.
The punch is wired for OUTDOOR roots + the look-in path ONLY; it never runs
on a clean interior (cellar) frame. For it to have suppressed #108, the
cellar-transition frames must render through the OUTDOOR root -> the player
is being classified OUTDOOR mid-cellar (the known #112/#106 cellar
membership ping-pong). So:
- #108 is a MEMBERSHIP bug (render is downstream of membership); the punch
was MASKING it, harmfully. Re-attributed to the membership track.
- The interior-root SEAL addresses a case that is NOT #108 (confirmed: #108
isn't an interior-root frame), so it has no verified visible effect yet.
Per no-workarounds + verify-before-layering: reverted ALL of BR-2's depth
machinery (seal, punch, the per-slice->full-clear swap) to the pre-BR-2
baseline (restored from 6cba950). The phantom-site probe (6cba950) is kept.
PortalDepthMaskRenderer.cs is KEPT as a RESERVED, unwired primitive (it is
verified-correct; the depth discipline will be rebuilt during BR-3 with
dynamics-after-interior ordering, where it can be verified against the
shell-chop deletion).
What survives from this session's execution: BR-1 (already-equivalent,
695eca2) stands. #108 moves to membership. BR-2 to be re-approached under
BR-3 with correct ordering. No net production behavior change vs 6cba950.
Suites: build green, App 226 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror of commit 1's seal: the punch half of retail's invisible portal
depth writes (DrawPortalPolyInternal maxZ1, Ghidra 0x0059bc90;
ConstructView(CBldPortal) mode-1, pc:433827).
Generalized DrawRetailPViewExitPortalSeal -> DrawRetailPViewPortalDepthWrite
with retail's maxZ1/maxZ2 selector (forceFarZ):
- INTERIOR root: forceFarZ=false (SEAL, true depth) - unchanged from c1.
- OUTDOOR root + DrawPortal look-in: forceFarZ=true (PUNCH, far depth) -
erase the terrain depth inside a flooded building's entry aperture so
the interior shows THROUGH the doorway against the nearer front-ground.
Architectural note (divergence from retail ORDER, same RESULT): retail
draws the shell LAST (DrawBuilding Draw(part,1) punch -> draw interior ->
Draw(part,0) shell) so the shell closes everything outside the punch. Our
pipeline draws the building shell FIRST (it is an outdoor WorldEntity in
the landscape pass), so the outside-the-aperture wall occlusion is already
in the depth buffer when interiors draw - we need ONLY the punch for
in-aperture visibility, no shell reorder. The punch is confined to each
door polygon clipped to its slice (NOT a full clear), so it does not
reintroduce the 'cellar paints over everything' hazard that gated the old
outdoor ClearDepthSlice to null.
DrawExitPortalMasks is now wired on the outdoor-root DrawInside context and
the DrawPortal look-in context (both previously null -> no-op).
Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era.
NEEDS VISUAL GATE (batched with BR-3): outdoor interiors-through-doorways
must not bleed; cellar grass-sweep (#108) gone; tower stairs near+far.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the seal half of retail's invisible portal depth writes
(D3DPolyRender::DrawPortalPolyInternal, Ghidra 0x0059bc90; dispatched by
PView::DrawCells loop 1, Ghidra 0x005a4840 pc:432783-432786):
- NEW PortalDepthMaskRenderer: draws a portal polygon as a color-masked
triangle fan, depth-test ALWAYS + depth-write ON, at the polygon's TRUE
projected depth (retail maxZ2 seal) or forced to far-z 0.99999988
(retail maxZ1 punch - the constant from 0x0059bc90's tail; punch wiring
lands in BR-2 commit 2). Where retail software-clips the fan against
the installed view (polyClipFinish), we apply the SAME slice region via
gl_ClipDistance from the slice's <=8 clip-space half-planes. GL state
fully self-contained (set -> draw -> restore, no early-outs).
- DrawExitPortalMasks is now WIRED in production (was a null-callback
no-op since birth): for interior roots, every visible cell's portals
with OtherCellId==0xFFFF get their world-space polygon sealed per view
slice, far-to-near, after the landscape slices.
- ClearDepthSlice (per-slice scissored AABB clear - wrong shape, wrong
scope, no seal after it) is REPLACED by ClearDepthForInterior: ONE
full-buffer depth clear between the outside stage and the interior
stage, gated on any outside slice having drawn (retail's
portalsDrawnCount gate semantics staged as an open question, marked
inline). DepthMask(true) asserted at the clear site (c4df241 lesson).
Outdoor roots: no clear, no seals (interiors must depth-test against
terrain until the commit-2 punch).
Closes the mechanism behind #108 (outdoor grass sweeping across the
upstairs door opening - terrain depth seen through the doorway is now
re-stamped at the door plane so farther interior geometry z-fails inside
the aperture). Visual gate: BR-2/BR-3 batched checklist (cellar doorway
+ cottage wall + tower stairs near/far).
Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era
failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BR-1 pre-check left the #113 phantom residual with two surviving
suspects, both cell-side: (a) flood-admitted cells whose shell draws with
a pass-all slice (NoClipSlice fallback when the assembler handed no slot,
or an assembler slot-0 scissor-fallback slice), and (b) cell entity
buckets drawn unclipped + un-viewcone'd by design.
[phantom-shell]: per shell-pass cell, print-on-change - clip-enable
state, slot presence, every drawn slice's slot + plane count with
PASS-ALL flagged. [phantom-objs]: per object-list cell, print-on-change
- entity bucket size. Env-gated ACDREAM_PROBE_PHANTOM=1, zero cost off,
throwaway (strip when the phantom closes).
Repro protocol: launch with the probe on, stand at the hall bisect spot
(world ~216,-108 looking at the AAB3 meeting hall west face) where the
phantom is visible, read which mechanism fires for stair cells
0xAAB30100..0x106. Shells pass-all -> BR-2/BR-3 close it; statics ->
BR-5 closes it.
Build green; App suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User gate 2026-06-11: the filter (e46d3d9) removed the phantom staircase
everywhere (verified) but DOORS disappeared across Holtburg - the naive
PosNode/NegNode walk evidently misses polygons some models reference
another way. Doors > phantom stairs: filter application removed; the
CollectDrawingBspPolygonIds helper and the dat-fact tests (hall orphans
0+1, cottage 0..7) stay as apparatus for the holistic building-render
port. First diagnostic for re-landing: run the DrawingBSP histogram on a
door GfxObj. See
docs/research/2026-06-11-building-render-holistic-port-handoff.md.
Branch state after this commit: outdoor-scoped shell clip (927fd8f +
9ce335e) + retail straddle gate (414c3de) + all diagnostics; phantom
staircase VISIBLE again (known, documented); doors functional.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user gate + bisect overturned the coincident-cell attribution: the
phantom staircase persists in the PRE-session build (bisect screenshot at
the hall wall) and is drawn by the ENTITY pipeline, untouched by any clip.
Root cause (dat-proven, DumpHallModel_PolyFlagHistogram): retail renders a
GfxObj by TRAVERSING its drawing BSP (D3DPolyRender); polygons present in
the Polygons dictionary but referenced by NO DrawingBSP node are never
drawn - they are physics/no-draw geometry. The Holtburg meeting hall
(0x010014C3) keeps its exterior stair-ramp as dictionary polys 0+1: in
the PhysicsBSP (ACE walks The Sentry on it at z 117-118; invisible-but-
walkable in retail) but orphaned from the draw tree (true at ALL degrade
levels - the LOD theory is dead, Degrades[0] IS the base model). The hill
cottage (0x01000827) carries 8 such orphans. Our extraction iterated the
dictionary -> drew the collision skeleton: the wall staircase up close,
the flying stairs over the cottage roofline from afar (orphan ramp spans
world 221-232 at z 116-124.5; visible over the cottage roof from the west).
Fix: PrepareGfxObjMeshData filters to CollectDrawingBspPolygonIds(gfxObj)
when a drawing BSP exists; models without one draw everything (unchanged).
Physics untouched (collision keeps the full physics set - retail parity).
CellStruct extraction not touched (different conventions; no orphan
evidence there yet).
Dat-backed pins: Issue113DrawingBspFilterTests (hall orphans == 0+1,
cottage orphans == 0..7). Suites: App 226 / Core 1392 + the 4
pre-existing #99-era failures / UI 420 / Net 294.
Note: the earlier shell-clip enable (927fd8f, scoped 9ce335e) remains
correct and orthogonal - it crops interior CELL geometry to apertures
outdoors; this commit removes the phantom SHELL geometry at its source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-06-11 user gate on 927fd8f: the OUTDOOR half works (phantom
meeting-hall staircase GONE at the original spot) but enabling the clip
for INDOOR roots exposed that our indoor clip regions are admission-
quality, not draw-quality - chopped interior stairs, a neighbour rooms
barrel visible through a clipped-away wall, missing candle-holder
geometry, inner walls vanishing while passing building exits. Retail
crops indoors too, but with pixel-exact recursively-clipped regions;
ours have knife-edge cases indoors that were invisible until the GL
enable made them cut real geometry.
Scope: DrawInside enables the shell clip only when RootCell.IsOutdoorNode
(the regime Issue113MeetingHallFloodTests validates); DrawPortal (from-
outside look-in) keeps it on; indoor roots draw unclipped - yesterdays
user-accepted state. Filed #114 for bringing indoor regions to retail
crop quality (also the home of the remaining gate findings to re-test:
entry-transparency at the hilltop cottage, particles visible through
buildings, camera feel in cramped interiors).
App 224 green; no Core/UI/Net surface touched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Attribution (dat-evidenced, supersedes the misplaced-cell hypothesis):
the phantom staircase is the Holtburg MEETING HALL (AAB3 building[0],
model 0x010014C3 at AAB3-local (36,84,116)), NOT an A9B3 building - the
user stood at the A9B3/AAB3 boundary (cell-transit trail in
issue112-gate1.log) and clicked through the hall to the NPC behind it.
The hall's interior stair cells (0x100..0x106, ring climbing z 116->124.5
to the deck hatch) have geometry coincident with the shell's west wall
(both at local x=29.0). Our outdoor per-building flood admits them with
CORRECT tight clip regions (4-6 planes, door-aperture NDC boxes -
Issue113MeetingHallFloodTests proves it), but DrawEnvCellShells drew them
WHOLE: mesh_modern.vert writes gl_ClipDistance from the routed CellClip
slot, and gl_ClipDistance is ignored unless GL_CLIP_DISTANCEi is enabled -
which no caller ever did for the shell pass (born inert in 1405dd8).
Interior staircase painted across the exterior wall; unpickable because
it is cell geometry, not an entity.
Retail oracle: cell geometry IS clipped to the accumulated portal view -
Render::set_view (:343750) installs the view polygon edge planes,
DrawEnvCell submits every cell polygon with planeMask=0xffffffff (:427922)
through ACRender::polyClipFinish. Characters/meshes are NOT poly-clipped
(viewconeCheck path) - entity routing stays cleared, comment scoped.
Fix: enable GL_CLIP_DISTANCE0..7 around exactly the shell pass
(self-contained per feedback_render_self_contained_gl_state; no early-outs
between set and restore). Slot-0 fallback slices (>8-plane regions) still
draw pass-all - the assembler's scissor fallback remains unimplemented and
documented; the new flood test pins 0 such slices at the hall.
Refuted along the way (full evidence in Issue113PhantomStairsDumpTests):
- ONE misplaced interior EnvCell unifying #113+#112+collision gaps: all 17
A9B3 cottage cells share an identical dat Position (nothing to misplace);
the #112 gap is a real 20cm doorway micro-gap 0.23m outside threshold
cell 0x104 (straddles its exterior portal plane at foot radius 0.48);
missing object collision remains #99/A6.P4.
- A9B3 dat content near the spot: no stair geometry in shell (balcony at
z119 + turret roof only), cells (flat 116/118.8), statics, or stabs.
Tests: Core 1389 green (+6 dump facts) / App 224 (+1 flood replay) /
UI 420 / Net 294; pre-existing 4 #99-era failures unchanged.
Visual gate pending: user re-check of the hall west face vs retail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The entry snap set only the controller (physics stood on the grounded floor)
while the renderer kept drawing the entity at the server-restored height -
the user's "spawned 2 meters in the air" screenshot over a fully-correct
interior. The teleport-arrival path already does entity.SetPosition +
ParentCellId (GameWindow.cs:4914); the login path now matches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-run finding (wedge-107-gate-indoor-login.log + dat scan): ACE saved the
cell one room off (claim 0xA9B40172, position inside 0xA9B40171 — adjacent
rooms). FindVisibleChildCell corrects this fine when hydrated (proven by the
new AdjacentRoomClaim regression test), but the live entry committed the raw
claim: IsSpawnCellReady's "any cell struct in the landblock => claim is bogus,
proceed" disambiguator observed the MID-POPULATION state (interiors hydrate in
id order on the background worker; the render-thread predicate read the cache
mid-loop) and opened the gate before the claim and its stab neighbors were
cached. AdjustPosition then saw a null cell struct and silently passed the
claim through; the first movement demoted the player to outdoor inside the
house — the user-visible "transparent interior, see straight through walls"
(render is downstream of membership: an outdoor-classified viewer only sees
the interior through the doorway flood).
Fix: the hold now waits for THE CLAIMED CELL's struct, full stop
(IsSpawnCellReady simplification; HasAnyCellStructInLandblock removed).
Claims that can never hydrate are filtered by GameWindow against the dat's
LandBlockInfo.NumCells range (memoized IsSpawnClaimUnhydratable), and
PhysicsEngine.Resolve carries a loud lost-cell-equivalent safety net: an
indoor claim with NO cell struct AND NO CellSurface floor data demotes to the
outdoor landcell with a [spawn-adjust] line instead of committing raw
(retail GotoLostCell :283418; documented divergence). Partial hydration
(CellSurface present, struct pending) keeps the legacy floor-snap behavior —
HasCellSurface uses the file's masked-low-word norm so bare-id fixtures and
full-id production both resolve.
Baseline restored: Core 1381 (+4 new #107 conformance tests) + 4 pre-existing
#99-era failures + 1 skip; App 223 / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (capture resolve-107-login1.jsonl + dat conformance scan): ACE
restored the player with a POISONED (cell, position) pair — cell 0xA9B40162
(one building) with a position inside 0xA9B40171 (a different building 55 m
away). Our entry snap trusted the claim verbatim: the player stood
fake-grounded for minutes (isOnGround passthrough, no contact plane, no
walkable polygon — zero-move resolves short-circuit), the FIRST movement input
ran a real transition, the pick demoted the indoor claim to outdoor
mid-building, and the player fell 2.4 m through the cottage floor onto the
terrain underneath — wedged inside the building shell. The second wedge shape
(flood-fix-gate2.log) was the PortalSpace freeze: the teleport-arrival
detection gated on `differentLandblock || farAway>100m`, an invented
heuristic — ACE's same-landblock short-hop corrections matched neither, so
PortalSpace never exited and movement input stayed frozen all session.
Four legs, all retail-anchored:
1. PhysicsEngine.Resolve (the player snap path: login entry + teleport
arrival) now runs AdjustPosition first — retail SetPositionInternal step 1
(acclient :283892, AdjustPosition :280009): validate/correct the claimed
cell from the foot-sphere center BEFORE any physics. Corrections log one
[spawn-adjust] line.
2. AdjustPosition's previously-deferred indoor seen_outside →
adjust_to_outside sub-fallback (:280037-280046) is completed; CellPhysics
gains the SeenOutside flag (dat EnvCellFlags.SeenOutside) cached in
CacheCellStruct. The camera path does not reach this sub-branch in the
gated scenarios (CameraCornerSealReplayTests green).
3. PortalSpace arrival = ANY player position update (holtburger PlayerTeleport
handler conformant; recenter still only on landblock change). Verified
live: ACE sent a same-lb dist=69.8 correction that the old gate would have
frozen on — it now completes.
4. Outbound wire (cell, position) pairs are now SELF-CONSISTENT: derive the
landblock frame from the resolver's full cell id instead of welding a
position-derived landblock onto its low word — the old composition could
write exactly the poisoned pair shape into ACE's character save. Plus the
#106-gate-2 hold extension: an indoor spawn claim waits for the claimed
cell's hydration (IsSpawnCellReady) so the validation can act — the async
equivalent of retail's synchronous cell load.
Live verification (wedge-107-verify1.log): entry clean; ACE's same-lb teleport
correction completed (old code: permanent freeze); the teleport destination
itself carried ANOTHER poisoned claim (0xA9B40150) which [spawn-adjust]
corrected to 0xA9B40019; player fully controllable, walking across landcells.
3 new dat-backed conformance tests pin the poisoned-pair facts
(Issue107SpawnDiagnosticTests). Baseline: 1380+4 pre-existing #99-era
failures+1 skip / 223 / 420 / 294.
Pending user gate: park indoors, log out gracefully, relaunch — expect a clean
indoor spawn standing on the interior floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 137b4f2 payload, re-landed now that #110 is resolved: the missing-indoor-
textures correlation was the pre-existing #105 staged-texture-flush drop
(fixed in c787201), not a near-plane mechanism. znear=0.1 merely raised #105's
trigger probability — a closer near plane makes close-up geometry newly
visible, inflating per-frame prepare/upload pressure indoors and growing the
never-flushed tail. Exactly the handoff's only-credible-link hypothesis,
verified instead of assumed.
Retail: Render::SetFOVRad sets znear=0.1 flat (decomp :342173, initializer
:1101867). 0.1 < the 0.3m camera-collision sphere, so a wall the collided eye
presses against no longer falls inside the near plane — the §4 corner
see-through-wall closes.
Verification on the 0.1 arm (the arm that struck 2-of-3 on 2026-06-10):
nearplane-reland-1.log — [tex-flush] after=0 on all 45 lines, 68,291 [shell]
lines with zero zh>0 batches, all four dat tripwires silent, no [wb-error].
ISSUES.md: #105 + #110 moved to Recently closed with root cause + evidence.
Pending user re-gate: corner press (wall stays solid) + distance scan for
z-shimmer (none expected; retail ships 0.1 with D24).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: TextureAtlasManager.AddTexture only STAGES texture content (PBO
write + ManagedGLTextureArray._pendingUpdates); the actual TexSubImage3D
copies + mipmap regeneration happen in ProcessDirtyUpdates, which WB drives
once per frame via ObjectMeshManager.GenerateMipmaps() from its render loop
(WB GameScene.cs:975, just before the opaque pass). GameScene is the file we
replaced with GameWindow, so the call site was silently dropped — staged
updates only reached the GPU as a side effect of PBO growth (UpdateLayerInternal
flushes pending updates before orphaning the PBO). Every layer staged after an
array's LAST growth kept undefined TexStorage3D content behind a valid,
resident bindless sampler handle: white/garbage walls, zh==0, dat tripwires
silent — exactly the #105 signature. Only ObjectRenderBatch.BindlessTextureHandle
consumers are affected (EnvCellRenderer cell shells = indoor walls); entities
resolve via TextureCache (immediate TexImage2D) and terrain via TerrainAtlas
(immediate GenerateMipmap), which is why only indoor walls ever struck.
Fix: WbMeshAdapter.Tick() now calls _meshManager.GenerateMipmaps() after the
staged-upload drain — Tick runs before all draw passes (GameWindow OnRender),
the exact WB-equivalent position.
Evidence (ACDREAM_PROBE_TEXFLUSH=1 apparatus, kept env-gated):
- pre-fix (texflush-prefix.log): pending updates climb 0->48->...->142 and
park at 126 across 34/34 atlas arrays at standstill, forever (19 heartbeats);
brief dips only at PBO-growth crossings — the broken contract live.
- post-fix (texflush-postfix.log): every line after=0 — staged updates drain
the same frame, all 34 arrays clean.
Intermittency explained: background decode-completion order shuffles which
textures land in the never-flushed tail; whether a visible wall samples one is
per-run luck. Also explains the #110 correlation: znear=0.1 makes close-up
geometry newly visible -> more prepare/upload pressure indoors -> bigger tail
-> higher strike probability. The near plane is mechanism-innocent (re-land
follows as its own commit).
Baseline maintained: App 223 / UI 420 / Net 294 / Core 1377 green + 4
pre-existing #99-era failures + 1 skip; CornerFloodReplayTests (5) and
CameraCornerSealReplayTests (2) gates green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bisect (user-gated): two consecutive runs on 0.1 lost indoor textures; the 1.0 bisect run rendered clean. #105 tripwires silent on the bad runs (GL-side). No known mechanism links the near plane to texturing - #110 filed to investigate (RenderDoc / flip-testing) before re-landing retail's znear=0.1, which the corner see-through fix depends on. Comments on all four cameras point at #110 so the retail value is not re-landed blind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The collided camera eye sits 0.3m from walls (viewer_sphere radius); a 1.0m near plane clipped the wall face away, so pressing the camera into a corner showed the clear color through the wall (gate result: unchanged by the flood fix - it was never a flood bug). Retail sets Render::znear = 0.1 flat in SetFOVRad (decomp :342173, initializer :1101867). All four cameras aligned. Also files #107 (indoor spawn wedge, 3-for-3), #108 (cellar-up terrain sweep across door opening), #109 (exit-door texture/background oscillation) from the 2026-06-10 visual gate; gate confirms the dac8f6a flood fix: room-room + indoor-outdoor transitions clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE BUG (pinned deterministically by the new CornerFloodReplayTests harness — real
Holtburg cells, captured corner-press scenario): a smooth 2 cm/step monotonic eye
sweep across the 0172↔0173↔0171 doorway produced a NON-monotonic flood — on ~10 of
61 steps the player's room (0172) vanished from the flood entirely or collapsed to
a sub-pixel sliver, taking its downstream chain (016F, the outside view) with it.
Live, those isolated frames are the §4 background strobe: openings/passages flash
the clear color during transitions, and the corner press shows background at the
angles that park the eye near the doorway plane.
TWO root causes, both fixed:
1. ApplyReciprocalClip ran the reciprocal portal polygon through the legacy
divide-first ProjectToNdc + 2D Intersect path, justified by "the reciprocal is
never near the eye." That assumption is exactly false at doorways/corners: the
reciprocal IS the same opening whose plane the eye presses against (2-60 cm).
ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge
there — 2 cm eye moves flipped its output between a no-op and a duplicated-
vertex hairline that ground the healthy region down to <3 distinct vertices.
FIX: route the reciprocal through the SAME homogeneous pipeline as the forward
clip (ProjectToClip + ClipToRegion) — which is what retail does:
PView::OtherPortalClip (decomp:433524-433563) runs the reciprocal through the
very same GetClip(finish=1) → ACRender::polyClipFinish homogeneous clipper.
Also ported retail's skip: exact_match portals (CCellPortal.exact_match,
acclient.h:32300; PView::ClipPortals :433689) bypass the reciprocal clip —
both sides share the same polygon, so re-clipping is redundant.
2. CellView.CanonicalKey missed COLLINEAR re-emissions: the homogeneous region
clipper legitimately inserts intersection vertices ON a subject edge when a
region edge grazes it, so BFS re-clip rounds re-emit the SAME geometric region
with 1-2 extra collinear edge vertices — keyed as distinct, defeating the
dedup and accumulating duplicate polygons (this was the real mechanism behind
the historical "float drift defeats the dedup" rationale that had parked the
reciprocal on the unstable path). FIX: canonicalize away collinear snapped
points (exact integer cross-products on the 1e-3 NDC grid) so the key is
purely a function of the region's corners.
Conformance: CornerSweep_FloodIsCompleteAndMonotone pins the fixed behavior —
61-step monotonic eye sweep ⇒ full flood every step, outside view always reached,
player-room region monotone (was: clean shrink 4.000→2.879 with zero drops, vs
~10 glitch steps before). Diagnostic facts (trace diff, hop microscope, primitive
scratch) retained as the apparatus.
Suites: App 223 green (incl. Build_AppliesReciprocalOtherPortalClip, now passing
with proper tightening AND dedup), Core 1377 green + the 4 pre-existing #99-era
failures + 1 skip, UI 420, Net 294. Visual gate pending: corner press, room↔room,
cellar↔floor, indoor↔outdoor transitions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ROOT CAUSE (pinned by the [clip-route] probe run + [gl-state] tripwire, one
capture): EnvCellRenderer.RenderModernMDIInternal established the Transparent
pass state (Enable(Blend) + DepthMask(false)) BEFORE the batch pass-filter, so
a cell whose batches are ALL opaque (a plain cottage interior) hit the
totalDraws==0 early-out and returned without ever reaching the end-of-pass
restore. The frame ended with dmask=0 + blend=1; the NEXT frame's
glClear(GL_DEPTH_BUFFER_BIT) silently no-oped (depth clears honor glDepthMask),
and every world fragment — terrain, entities, player, sky — failed GL_LESS
against its own previous-frame depth ghost. Screen = the fog-tinted clear
color. Onset locks to the building-flood merge because that is the first frame
the flooded building shell draws; holds while merged (the leak re-arms every
frame); camera rotation recovers because the cell drops from the flood and the
restore-skipping path stops running.
Capture evidence (flap-cliproute-capture.log): all three draw-input suspects
exonerated — landscape scissor full-screen all run, terrain-UBO/region-SSBO
planes full-screen on both sides of every merge, all 41,373 instances on the
correct repacked slot with cullEnt=0 — while [gl-state] showed frames entering
with dmask=0 blend=1 for exactly the merged stretches (145,238 consecutive
frames in the held window, flipping with each merge boundary at the end-of-run
strobe cycles).
Fix (all paths root-cause, no suppression):
- EnvCellRenderer: move the pass-state establish BELOW the totalDraws==0
early-out so state is only set on a path that always reaches the restore;
hoist the globalVao==0 check (the second leak-shaped early-out) above the
state set.
- GameWindow frame clear: assert DepthMask(true) before glClear — the clear
DEPENDS on the depth write mask, so it sets the state it depends on
(feedback_render_self_contained_gl_state; this is the 4th instance of the
class, in the same function as the 1st).
Very likely the same family as the "parts of the screen flash while running
past cottages" and cottage enter/exit artifacts (every brief merge = a
1-frame no-op depth clear). Visual gate pending.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decisive probe between the two surviving suspects from the 2026-06-09
building-flood-merge handoff (docs/research/2026-06-09-flap-outdoor-fullworld-
building-flood-merge-handoff.md section 1), gated by ACDREAM_PROBE_CLIPROUTE=1,
all print-on-change:
- [clip-route] (RetailPViewRenderer.DrawLandscapeThroughOutsideView): the
outside slice slot + NDC AABB + planes, the CellIdToSlot routing table, the
region-SSBO bytes DECODED at the routed slot, and the terrain-UBO head —
captured after SetTerrainClip + UploadClipFrame + SetClipRouting, i.e.
exactly what the landscape draws consume. Pins/refutes suspect (b) and the
slot-repack half of suspect (a).
- [clip-route-disp] (WbDrawDispatcher.Draw, routed draws only): per-slot
instance histogram exactly as staged for binding=3 plus the count of
entities dropped by ResolveSlotForFrame CULL. Pins/refutes the
instance-routing half of suspect (a).
- [clip-route-scis] (GameWindow.DrawRetailPViewLandscapeSlice): the ACTUAL GL
scissor enable + box read back right after BeginDoorwayScissor — the whole
landscape pass (sky + terrain + outdoor entities + player) draws inside this
box, so a doorway-sized box here IS the full-world kill by construction.
Code-reading findings recorded while building the probe: the landscape pass is
scissored to slice.NdcAabb end-to-end (GameWindow.cs DrawRetailPViewLandscapeSlice),
and ResolveEntitySlot CULLs server entities with null ParentCellId while routing
is active — both now directly observable under the probe.
Throwaway apparatus — strip once §4 ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two probe additions from the §4 outdoor full-world flap investigation
(world turns to fog-tinted clear color at specific outdoor spots;
strobes then holds; camera rotation recovers; forward-walk triggers):
- [pv-input] gains terrZ=/eyeAbove= (terrain height under the camera
eye) — used to REFUTE the buried-eye hypothesis (eyeAbove stayed
+1.6..2.1 m through every held-flap block).
- [gl-state] (ACDREAM_PROBE_GLSTATE=1): snapshots the GL fixed-function
state entering the world passes (depth, blend, cull, scissor + box,
viewport, FBO, color/clip masks, glGetError), printing on change.
Delivered the frame-exact flap-onset marker: the leftover scissor box
flips from full-screen to a drifting 9x21 px doorway footprint at the
exact frame the nearby building flood merges in (pv-input flood 1->5).
Both probes are gated and zero-cost when off. Strip with the rest of
the §4 apparatus when the flap ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-3 never entered player mode: the new spawn-ground entry hold
(6dbbf95) waits for terrain under the spawn, but the K-fix1 streaming
gate skipped streaming entirely until the chase camera first engaged —
which requires player-mode entry. Circular wait; the user sat in the
fly camera with an empty world (the probe log has zero [cell-transit]
lines and no "auto-entered player mode").
K-fix1's intent was narrower than its implementation: suppress the
hardcoded-Holtburg center flash BEFORE LOGIN. The streaming tick even
has a dedicated in-world fly-camera branch (observer from
_lastLivePlayerLandblockId) that the outer gate made unreachable. The
fix gates streaming on pre-login only: once the live session reaches
InWorld, streaming runs (centered on the server-known player landblock
or the fly camera), terrain hydrates, the spawn-ground predicate flips,
and auto-entry fires. The world-geometry RENDER gate is untouched — the
pre-entry screen still shows sky only, exactly K-fix1's visual.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-2 fallout chain: session 1's bare-id wedge poisoned ACE's save (the
client reported garbage cells while walking through walls), so session 2
logged in with (cell=0xA9B4013F inn interior, pos=(91.4,32.7)) — a
position that cell does not contain. Probe evidence: exactly one
[cell-transit] line all session; the player free-fell into an empty
world. Two holes, both fixed at the root:
1. CellTransit pick escape hatch — restores the #83/A1.7 + #90
verification that lived in PhysicsEngine.ResolveCellId before the
collide-then-pick rewrite moved membership into
BuildCellSetAndPickContaining: an indoor current cell that IS
hydrated but whose CellBSP no longer overlaps ANY part of the foot
sphere is a bogus claim (corrupt save, or walked out through an
unblocked gap). The portal BFS can never reach an exit portal from a
cell the sphere isn't in, so no candidates exist and the claim held
forever — wedging collision (ShadowObjectRegistry's #98 gate reads
"indoor primary" -> outdoor object sweep skipped), wall BSP, terrain,
and the render root. The pick now demotes to the outdoor column under
the sphere centre (the LandDefs.AdjustToOutside result already
computed for the pick — cross-block safe). Sphere-overlap
(BSPQuery.SphereIntersectsCellBsp, pseudo_c:317666 -> :323267), NOT
point-in: doorway push-back leaves the centre a few cm outside while
the sphere still overlaps — no demotion, #90's ping-pong stays dead.
An unhydrated cell cannot be verified — stale beats null while
streaming hydrates (retail-equivalent: stale curr_cell kept when the
pick finds nothing).
2. PlayerModeAutoEntry spawn-ground hold — player-mode entry now waits
for the terrain under the spawn position to stream in
(isSpawnGroundReady predicate, K.2 pattern). Entering earlier
integrates gravity against an empty world: indoor-claimed spawns got
no floor from any source and free-fell into the void; outdoor spawns
raced hydration by ~1s every login. Retail never has this state (it
loads cells synchronously) — the hold is the async-streaming
equivalent of that invariant. With the hold, the entry snap
(Resolve, stepUp=100) runs against hydrated cell floors + terrain
and re-seats a corrupt save's claim immediately.
Tests: IndoorSeed_SphereFullyOutsideHydratedCell_DemotesToOutdoorColumn
(the gate-2 wedge shape, red pre-fix), straddle + no-BSP guards (the #90
hysteresis and stale-beats-null), TryEnter_Armed_SpawnGroundNotReady_
DoesNotFire. Full suite: 294+218+420 green; Core 1375 green + the same
4 pre-existing door/#99-era failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live evidence narrowed #105 to the pending->committed instance hand-off:
walls missing from BOTH inside and outside views while the same cells'' props
draw and collision works = the wall-shell INSTANCES never reach the committed
draw set. FinalizeLandblock uses REPLACE semantics (lb.Instances =
lb.PendingInstances), and with two-tier streaming a landblock can finalize
while a promote job is still registering its cells on the worker thread —
the partial pending set commits, the remainder lands in a fresh pending list,
and the next finalize REPLACES the committed set with only the remainder.
Whoever registered first is silently lost: per-session-random (drain timing),
per-building-persistent, from session start.
- [finalize-replace] a finalize that DISCARDS already-committed instances
- [late-register] a RegisterCell landing after its landblock finalized
Both print only on the suspect interleavings. Next occurrence proves or
kills the theory; the fix (merge semantics + registration/finalize atomicity)
follows the evidence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live session evidence narrowed #105 decisively: a wall section rendered as the
sky/clear color from session start, HAD collision (cell + physics fully
loaded), zero round-1 tripwires fired (all dat reads succeeded), and the hole
showed terrain or clear color depending on camera angle. So the wall mesh was
built and then lost between mesh-build and draw. New tripwires cover the
loss candidates in that window (all print ONLY on anomaly):
- [geom-null] ProcessQueueAsync EnvCell branch resolved null, with the
failing sub-step (prepare-null / cellstruct-missing /
env-read-failed) — a null here means the DEDUPLICATED cell
geometry never renders for ANY cell that shares it, and
nothing retries (RegisterCell fire-and-forgets the task).
- [geom-misroute] an EnvCell geom id (bit 33) whose pending request vanished
fell through to the generic path, where its hash-derived
low bits resolve to nothing -> silent null.
- [up-null] UploadGfxObjMeshData returned null and the EMPTY substitute
was cached in _renderData forever (permanently invisible).
Pair with the existing one-shot draw-side audit (ACDREAM_A8_AUDIT=1, light:
one line per unique cell/geom pair, prints renderData=null + bindless-handle
status) for full attribution on the next occurrence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The intermittent white-cottage-walls failure has NEVER produced a log line:
every dat-read failure on the walls-relevant paths exits silently, and the
failed result is cached for the session (mesh batches build once). Today it
reproduced on a probe-free launch with a 35-line, zero-error log — so the
prior heavy-probes-starve-the-dat-reader framing is not the whole story.
Tripwires (print ONLY on anomaly; zero cost healthy; keep until #105 closes):
- [dat-miss] DatDatabaseWrapper.TryGet — a miss for an id whose BTree entry
EXISTS (re-probed under the same lock); legit not-found fallbacks stay quiet.
- [tex-miss] TextureCache.DecodeFromDats x3 — render-thread decode fell back
to magenta (Surface / SurfaceTexture / RenderSurface miss).
- [cell-miss] GameWindow interior hydration x2 — EnvCell or Environment read
returned null, so a cell''s WALLS are silently never registered while its
statics still draw (the exact observed geometry signature).
Color discriminates the layer on the next occurrence: magenta = TextureCache;
see-through + [tex-skip]/[dat-miss] = mesh build; see-through + [cell-miss] =
hydration; broken with NO tripwire output = GL-side upload/residency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ObjectMeshManager.Dispose never stopped its Task.Run(ProcessQueueAsync) decode
workers, and LandblockStreamer.Dispose abandoned its worker after a 2s join.
GameWindow.OnClosing then disposed the DatCollection, which unmaps the dats''
memory-mapped views (MemoryMappedBlockAllocator.DestroyMappedFile nulls
_viewPtr) — a worker still inside ReadBlock dereferences the dead view pointer:
an uncatchable AccessViolationException with ReadBlock on the stack, firing on
close/relaunch during decode storms. This is the recorded crash signature from
the 2026-06-09 white-walls session.
- ObjectMeshManager.Dispose: set IsDisposed under the queue lock, cancel+drain
pending requests, then wait (<=10s) for _activeWorkers==0; loud LogError if
workers outlive the wait. ProcessQueueAsync re-checks IsDisposed per dequeue;
Prepare*Async entries + enqueue blocks early-out when disposed.
- LandblockStreamer.Dispose: join 2s -> 15s with a loud [streamer] line on
timeout (cancellation honored between jobs; one landblock load bounds it).
- Also includes the [tex-skip] tripwire lines on ObjectMeshManager''s five
silent dat-miss exits (GfxObj + CellStruct texture chains) — part of the
white-walls attribution net (#105), zero output when healthy.
Verified: 3x close-mid-decode-storm smoke (in-world at ~8s, WM_CLOSE at ~11s),
clean exits, no crash signatures, no quiesce timeouts. Full suite: 294+218+420
green; Core 1338 green + 4 pre-existing physics failures (reproduced at bare
HEAD, unrelated). Investigation:
docs/research/2026-06-09-dat-reader-thread-safety-investigation.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pinned (flap-sidechk.log): the indoor doorway flap is a 0171<->0173 flood cycle. Back portals show camInterior=False (our side test already agrees with retail) but were traversed when eyeIn=True because the side-cull had an bypass (added 2026-06-05 for the void). Within 1.75m of a doorway that bypass kept the BACK portal alive -> mutual re-contribution -> re-enqueue churn (maxPop=16) -> eye-sensitive flood depth -> grey flap + dropped floor.
Fix (Option B1): drop the bypass from the side-cull in Build + BuildFromExterior so back portals cull by the side test alone, exactly like retail PView::InitCell (:432962, no eye-in-opening bypass). The forward-portal clip-empty void rescue is a SEPARATE branch and is untouched (Build_EyeStandingInInteriorPortal_FloodsNeighbour stays green). New RED->GREEN test Build_BackFacingPortal_EyeStandingInOpening_StillCulled; full App suite 218 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Logs camInterior/eyeIn/D per portal under the existing PortalBuildTrace so the 0173->0171 back-portal traversal can be attributed to B1 (EyeInsidePortalOpening bypass) or B2 (CameraOnInteriorSide convention). Throwaway; stripped in Phase 4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Logs SweepEye input (desiredEye) vs output (eye) at micrometre precision. Used to prove the indoor flap is NOT the camera: the eye is smooth (clean one-way pass = 3/18 direction-changes over 25.7k frames) and ~1um stable at rest, yet the visible-cell count oscillates 414x with 648 clip=0 near edge-on doorways. The flap is the flood/clip's edge-on behaviour, not the eye.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MergeNearbyBuildingFloods skipped cells whose BuildingId is null; the pre-R-A2 outdoor-node reverse-portal flood reached them, so dropping left holes at building/terrain seams. Key by (BuildingId ?? CellId) so unstamped/outdoor-adjacent exit-portal cells still seed a per-entrance flood; cells without an exit portal contribute nothing as before. App Rendering 207/207.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the outdoor root's single unified reverse-portal flood (whose root-level
portal-side test oscillated as the chase eye grazed a doorway — the measured
flood 2<->6) with retail's per-building floods.
- OutdoorCellNode.Build(uint): portal-less land root; floods only itself ->
full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed).
- PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a
building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via
DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded ->
consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4).
- RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby
cells by BuildingId and merge each per-building flood into the frame before
assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff.
- GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames.
Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership
stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject,
the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView
coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14.
Conformance-verified sound; the grazing-doorway flap is the visual acceptance test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ReferenceEquals(clipRoot, _outdoorNode) object-identity checks with the
documented LoadedCell.IsOutdoorNode flag (4 sites) so they survive R-A2 changing
the outdoor root's portals. Behavior-preserving (build + targeted suites green:
App PortalVisibilityBuilderTests 24/24, Core PlayerMovementControllerTests 14/14).
Right-sized from the planned 'collapse to one root': reading the live dispatch,
the viewerRoot ?? outdoorRoot split is already correct (viewerRoot feeds
cameraInsideCell/lighting via the older CellVisibility BFS; clipRoot is the render
root), and the 2026-06-07 cutover flip already made in-world frames single-path
DrawInside. The real flap fix is R-A2 (per-building floods). Dead exterior
DrawPortal look-in deletion deferred to R-A3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 4 summary-emit adapted from the plan: the plan's Invariant($"a" + $"b" + sb) form
passes a string to FormattableString.Invariant (which requires a FormattableString) and
does not compile; merged the two interpolated fragments into one literal and appended the
already-invariant-formatted reciprocal detail outside the Invariant call. Same output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2026-06-08 AM "physics rest micro-jitter" diagnosis is refuted with primary
evidence (door-recheck 216K standstill records: 0 position re-snaps; player
byte-stable during the flap). Two adversarial verification sub-agents confirmed:
- Retail roots the render at the camera viewer_cell (swept from the player via
SmartBox::update_viewer 0x453ce0; DrawInside(viewer_cell) 0x453aa0) and toggles
DrawInside / LScape::draw -- so acdream's eye-cell rooting + inside/outside
toggle are RETAIL-FAITHFUL. The locked-design "root at player cell" is wrong.
- The flap is render membership instability, eye-motion-driven: the visible-cell
set oscillates (8<->3) as the eye sweeps monotonically. Root = the
re-enqueue-on-growth DRIFT (PortalVisibilityBuilder.cs:322, MaxReprocessPerCell
=16) re-clipping each grown cell every round -> sub-cm eye jitter flips membership.
Fix (spec, not yet implemented): verbatim port of retail's enqueue-once flood
(ConstructView + AddViewToPortals): enqueue once on first discovery, clip each
cell's portals once, union late growth in place (AddToCell) + draw-reorder
(FixCellList), never re-enqueue. Kills the drift; rooting/camera/seal untouched.
This commit lands VERIFIED GROUNDWORK + design only:
- spec: docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md
- findings: docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md
- [pv-input] probe gains rawPlayer + yaw (disambiguates the varying input)
- 4 GREEN physics rest-stability tests (prove rest is bit-stable -> flap not physics)
- apparatus: launch-flap-capture.ps1, analyze_flap_live.py, find_burst.py
- captured fixtures: tests/.../Fixtures/flap-doorway/0xA9B4017{0..5}.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cutover-flip follow-up: see-through buildings from outside. When the outdoor-node flood reaches a building, each interior cell is meant to draw clipped to its doorway aperture. But DrawEnvCellShells falls back to the no-clip slot 0 (full-screen) when a cell's aperture degenerates — screen-covering when you get close, or edge-on. Indoors that fallback is load-bearing (it seals the room the camera stands in; near walls hide the over-draw). From OUTSIDE it paints the building interior across the whole screen, depth-tested, so it shows wherever the solid exterior does not cover — the see-through walls, appearing 'past a threshold' exactly where the aperture degenerates.
Fix: for the outdoor-node root only, skip a flooded interior cell with no real plane-clip slot (HasRealClipSlot). From outside, 'no real aperture' means 'do not paint this interior', not 'paint it everywhere'. Interior roots keep the seal-everything slot-0 fallback unchanged. Applied to DrawEnvCellShells AND DrawCellObjectLists so a skipped cell shows neither walls nor furniture; the dead DrawPortal exterior look-in gets the same gate.
Root cause traced over the WB EnvCell render path: CellMesh.cs is physics-only; ObjectMeshManager.PrepareCellStructMeshData builds double-sided walls, so this was never a culling bug. App 216/0, build green. Visual gate pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cutover-flip follow-up (issues 1+2 from the visual gate). After the flip an outdoor-node frame ran DrawInside with a full-screen OutsideView slice (Step A) whose ClearDepthSlice wiped the ENTIRE depth buffer AFTER terrain/exteriors/player drew — so the flooded building interior shells (cellars) drew over everything: the cellar painted in front of the player from outside, and distant building interiors showed through the ground in the open field.
The depth clear is a doorway look-in trick (clear a small door region so the cell seen through it draws over the terrain drawn through it). It is wrong for the full-screen base terrain of the outdoor root. Skip it there (ClearDepthSlice=null when clipRoot is the outdoor node); interiors now depth-test against terrain + exteriors and appear only through real door openings. Interior roots keep the doorway clear unchanged.
App 216/0, build green. Visual gate pending (player must be outdoors to exercise the outdoor root).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>