Commit graph

1154 commits

Author SHA1 Message Date
Erik
3cf6bcc219 #119 decisive probe: ACDREAM_DUMP_ENTITY one-shot entity dump (H-A/H-B/H-C discriminator)
The broken-state log (user-session-capture2.log) shows meshMissing=0 /
entSeen==entDrawn WHILE broken stairs are on screen - the staircase is
DRAWN WRONG, not missing. This probe discriminates the three live
hypotheses in ONE launch (handoff 2026-06-11 s4):

- HYDRATE dump (GameWindow.BuildInteriorEntitiesForStreaming): per-part
  placement-frame translations + dropped-part accounting at the MOMENT
  MeshRefs are constructed. H-A (SetupMesh.Flatten identity fallback /
  silent gfx-null part drops under degraded dat reads) shows here as
  zero translations or built<43.
- DRAW dump (WbDrawDispatcher, first tuple per entity): live MeshRefs
  translation summary + per-part loaded flags + Tier-1 classification
  cache state (batch count + RestPose translation summary), re-emitted
  compactly on signature change. H-B (partial/stale cached batch set)
  shows as correct translations + odd batch count.
- WALK-REJECT lines (rate-limited): attributes 'entity never reaches
  the draw loop' to the specific gate (visibleCellIds/frustum).
- Correct everything -> H-C (draw-side compose), instrument next.

Targets: ACDREAM_DUMP_ENTITY=0x020003F2,0x020005D8 (the 43-part spiral
staircase Setup + the wall barrels; H-A predicts the user's 'barrel' IS
the collapsed staircase). Probe is inert when the env var is unset.
Parser in RenderingDiagnostics (diagnostic-owner pattern) + 5 unit tests.

Suites: App 242+1skip / Core 1427+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:01:08 +02:00
Erik
7bbb169c6c #128 STRUCTURAL: missing meshes re-request their load at the POINT OF USE - permanent invisibility becomes impossible
The registration-time re-arm was insufficient and the user proved it
(ran back from the lifestone -> broken stairs + exposed barrel again):
a preparation cancelled by landblock churn AFTER the last registration
event has no later event to re-fire it - crossing blocks loads/unloads
them repeatedly behind the player, so the cancel-after-last-register
window is routinely hit on any cross-country run.

The structural fix: the draw dispatcher touches every
missing-but-referenced mesh every frame (the meshMissing slow path) -
THAT is the one site a retry can never be missed from. Both miss paths
(per-MeshRef and per-Setup-part) now call WbMeshAdapter.EnsureLoaded
(idempotent passthrough to PrepareMeshDataAsync, which early-outs on
existing data and dedups pending tasks), deduped per Draw pass.
Retail-equivalence: retail loads synchronously - geometry is never
permanently absent; this converges the async pipeline to the same
guarantee regardless of cancellation/eviction timing.

Also fixes the #53-one-level-deeper hole found en route: a missing
SETUP PART did not mark the entity incomplete, so a partial batch set
could cache permanently for Setup-shaped render data.

New apparatus: [mesh-miss] once-per-id line under ACDREAM_WB_DIAG=1 -
any future missing mesh names itself instead of needing a live repro.

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:30:56 +02:00
Erik
120aeff720 #126 RETAIL-CORRECTED: restores commit the server Z - retail never re-derives position from surfaces
The user caught the process failure: two snap fixes were written without
reading retails restore code. The named decomp settles it -
CPhysicsObj::SetPositionInternal (0x00515bd0, pc:283892-283945) treats
the supplied Position as INPUT: AdjustPosition resolves which cell
CONTAINS it, CheckPositionInternal/find_valid_position VALIDATES it
through the collision transition, and the no-cell case goes
store_position + GotoLostCell. There is NO terrain or surface
re-grounding anywhere in the restore path. Trust + validate.

Both prior shapes diverged: grounding outdoor claims to terrainZ warped
a roof-deck logout (ACEs authoritative z=127.2 on the AAB3 tower)
through the roof into the building volume -> the transparent-interior
spawn on every login; the cell-walkable scan that replaced it missed
shell-geometry decks entirely (no EnvCell owns the deck surface) and
failed silently - the user logged in transparent at the tower bottom
again.

Fix: a zero-delta outdoor restore above terrain commits the claims Z
verbatim ([snap] line says so); the first physics tick validates and
settles against the REAL collision world (the BR-7 building channel
covers the deck). max(terrain, z) stays as the under-terrain sanity
bound - our recoverable stand-in for retails lost-cell machinery
(documented divergence, same class as the #107 demote).

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:22:17 +02:00
Erik
b94a7e8017 #126: outdoor restore grounds onto elevated walkables, not through them
A zero-delta RESTORE of an outdoor claim standing far above terrain
(logged out on a building roof deck - the AAB3 tower 0x010A slab at
z=127.2 over terrain 112) was grounded to TERRAIN unconditionally,
warping the player through the roof into the building interior,
outdoor-classified -> the transparent-interior spawn the user hit on
every login while the save sat on the roof. Retail restores settle via
AdjustPosition onto real surfaces, not the heightmap.

Fix: the snap outdoor branch, zero-delta shape only, when the claim z
exceeds terrain by more than step height: ground to the nearest CELL
WALKABLE at/below the claim z (the #111 WalkableFloorZNearest query -
real floors only, never the ceiling soup), keeping the outdoor cell id
(honest: a deck-stander center sits above the slab cell BSP - the same
state the user played in all afternoon). GfxObj-shell roofs without
cells not covered - file if a real case shows.

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:02:48 +02:00
Erik
f35cb8b164 #119-residual ROOT CAUSE: the +0.02 m render lift leaked into the portal-visibility graph - horizontal portals side-culled anyone standing on them
The live capture pinned it end to end. BuildInteriorEntitiesForStreaming
lifts the render-side cell transform +0.02 m Z (shell z-fighting vs
terrain - a DRAW concern) and passed that LIFTED transform to
BuildLoadedCell, so every plane in the visibility graph sat 2 cm high.
The portal side test's in-plane window is +-10 mm: an eye standing ON a
floor containing a HORIZONTAL portal (the tower's deck lip 010A->0107,
stair landings, cellar mouths) sits 0-10 mm above the TRUE plane = 10-20
mm BELOW the lifted plane -> outside the window -> the cell behind the
portal side-culled out of the flood. Captured live at the stair top:
root=0xAAB3010A eye z=126.803 vs the portal plane at 126.80, flood=1,
0x0107 (the whole tower interior incl. the staircase) dropped WHILE THE
GAZE LOOKED STRAIGHT AT IT - "stairs disappear and you can walk on
them", and the roof/edge flap as the gaze swung the marginal admissions.
Vertical doorways were immune (the lift slides their planes along
themselves) - exactly why this hit stairs/decks/floors and not doors.

Chase chain (the apparatus did all the work): [viewer] print-on-change
probe with eye@mm -> the user's climb capture -> [viewer-diff] naming
the dropped cells per flip -> headless replay of the exact captured
(eye,fwd) frame: healthy UNLIFTED, reproduces ONLY with the production
lift -> gate-by-gate diagnostic (side test dot=+0.003 unlifted vs
-0.017 lifted; clip + rescue exonerated; knife-edge z-sweep all-stable,
killing the float-chaos theory).

Fix: BuildLoadedCell receives the PHYSICS (unlifted) transform; the
drawn shells keep their lift. The seal/punch fans (which read the
visibility LoadedCell's WorldTransform) now stamp TRUE depth - MORE
consistent with the unlifted terrain they protect.

Pins: CapturedTopOfStairs_MainCellStaysInFlood - arm 1 (unlifted =
post-fix production) asserts the main cell admitted at the captured
frame; arm 2 (lifted) is the mechanism canary asserting the drop, with
instructions if it ever starts passing. Plus the gate-by-gate
diagnostic + knife-edge sweep as the investigation record.

Also this session: Issue127FloodFlipReplayTests (the captured 4 cm
outdoor flip pair replays STABLE across fovs/pre-gate arms - the
outdoor churn is NOT the flood math; remaining #127 = distant-building
admission churn, lower priority now that the tower-cell drops are
explained by the lift), and the [viewer-diff] probe (per-flip added/
removed cell naming - keep, it found this).

Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:26:06 +02:00
Erik
cd12d3dbbc capture run decoded: #126 spawn-through-roof + #127 bistable flood admissions + #128 session-sticky invisible staircase filed; [viewer] probe gains fwd=
The users tower capture (tower-viewer-capture.log, 551 [viewer] lines)
decodes into three distinct issues:

- #126 (HIGH, #107/#111 family): an OUTDOOR spawn claim on the tower
  roof (z=127.2) is grounded to TERRAIN z=112 - the player is warped
  through the roof into the tower interior, outdoor-classified ->
  the transparent-interior spawn. The snap outdoor branch must ground
  to the nearest WALKABLE surface (roofs/GfxObj floors), not terrain.
- #127 (HIGH, the flap mechanism): per-building flood admissions are
  BISTABLE per frame under the outdoor root - flood size oscillates
  +-1-3 cells at millimetre eye deltas (45<->52 standing on the roof,
  including a byte-static eye flip). Every oscillation = building
  interiors dropping in/out -> the roof/edge flap; running past a
  building = #123. Interior side shows the same family (flood 1<->3,
  outPolys 0<->1 during the climb).
- #128: the staircase was invisible the WHOLE climb under a HEALTHY
  interior root (0xAAB30107 FullScreen views - the cone cannot cull a
  root-cell static), while the SAME build rendered it perfectly in a
  different session (diag spawn + screenshot, meshMissing=0).
  Session-sticky nondeterminism; the barrel tracks this bug (a
  partial subset of staircase parts), NOT dat content (user axiom:
  no barrel in retail). Needs a diag-instrumented repro of the users
  session shape.

The [viewer] probe now logs the camera forward (fwd=) so the next
capture can be replayed headlessly - Build clip results depend on the
view-projection, not just the eye.

Suites: App 238+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:53:17 +02:00
Erik
a974504e6e #119-residual: [viewer] capture probe - the capture half of the tower capture-replay loop
One line per change of (root cell, flood size, OutsideView polys, player
cell), with the projection eye at mm precision on every line
(ACDREAM_PROBE_VIEWER=1, print-on-change, silent while stable). The
tower-ascent harness replays the captured production (eye, root) pairs
deterministically - replacing the synthetic helix that proved unphysical
in the roof-lip band (the real collided camera may never reach it).

Suites: App 238+1skip, Core 1422+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:41:05 +02:00
Erik
fcade06c46 #125: gpu_us query ring reads never-begun query objects - root cause of the WB_DIAG GL error cascade; fixed + live-verified
Root cause (by read, verified live): a glGenQueries name does not become
a QUERY OBJECT until its first glBeginQuery - GetQueryObject on a
never-begun name is GL_INVALID_OPERATION. The N.6 gpu_us ring assumed
ONE dispatcher Draw per frame with both passes always non-empty; the
pview pipeline issues MANY small Draws per frame (landscape slices,
per-cell static buckets, dynamics), where zero-draw passes routinely
skip BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read queued an
InvalidOperation EVERY frame - silently, until WB's diligent
texture-path glGetError checks ate the stale errors and treated their
own successful uploads as failures ([wb-error] + the sticky drop) and
ProcessDirtyUpdates' check threw (process death, tower-wbdiag3.log).
The GL-error-attribution trap, textbook form.

Fix: begun-flags per ring slot per target; the read path only queries
slots that were actually begun (a skipped pass contributes 0 ns).
Live verification (tower-wbdiag4.log, in-tower spawn): zero [wb-error]
(was 7), no crash, gpu_us reads real values (9-11 us) for the first
time under the pview pipeline, meshMissing=0 / entSeen==entDrawn.

Consequences: (1) the #119 missing-stairs mechanism theory via sticky
GL upload failures is RETIRED for normal runs (WB_DIAG off = no query
calls = no errors; clean runs confirmed zero wb-error) - and the
in-tower screenshot on the current build shows the spiral staircase
RENDERING, so the stairs were most plausibly a #120 flood-corruption
casualty (the tower threshold cells portal back to 0x0107 exactly in
the ping-pong window); user verdict pending. (2) The sticky-drop
defect (upload failure never retried) stays filed under #125 as
defense-in-depth debt - the trigger is gone but the design flaw isn't.

Suites: App 236, Core 1419+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:19:55 +02:00
Erik
63d14c3d6b #125 filed + WB-DIAG median crash fixed - the #119 stairs mechanism is a sticky GL upload failure
The in-tower ACDREAM_WB_DIAG launch (the saved character spawns inside
the #119 tower - a free deterministic repro lever) produced the
mechanism evidence in one run (tower-wbdiag3.log):

1. [wb-error] upload of 0x0100321D died on a GL InvalidOperation in
   ManagedGLTextureArray..ctor (new TextureAtlasManager) - caught,
   returns null, and the drop is STICKY: _preparationTasks.TryRemove
   runs BEFORE the upload, so a failed upload is never re-prepared.
   Permanently invisible mesh, one log line. This failure class is the
   likely #119 missing-stairs mechanism (dat + extraction +
   registration + dispatcher all exonerated by read/test this session).
2. The SAME GL error then fired UNCAUGHT in Tick -> GenerateMipmaps ->
   ProcessDirtyUpdatesInternal and killed the process. Both render-
   thread - not thread affinity. Filed as #125 (HIGH) with the open
   question of GL error attribution (a stale error queued by an earlier
   unchecked call lands on WB's diligent glGetError checks).

Also fixed here: WbDrawDispatcher.MedianMicros crashed with
IndexOutOfRange on the first diag flush when exactly 1 sample was
recorded (copy[copy.Length - nz/2] with nz==1) - the same off-by-one
GameWindow's TerrainDiagMedianMicros twin fixed; same fix applied.
ACDREAM_WB_DIAG=1 is usable again.

Suites: App 236, Core 1419+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:08:46 +02:00
Erik
c4464739d2 #121: dynamics-owner particle pass - world portals visible again; re-gate ledger in ISSUES
Fix: dynamics' ATTACHED emitters (portal swirls on server-spawned portal
entities, creature effects) fell through EVERY particle filter under the
unified pview path - the landscape slice filter carries outdoor statics
(+ the #118 outside-stage dynamics), the per-cell callback carries cell
statics, and T4 deleted the clipRoot==null global pass from normal
frames. T5 never checked portals; the user's re-gate caught it ("all
portals that were previously showing are now gone"). DrawDynamicsLast
now hands its cone-surviving dynamics (minus outside-stage entities,
whose emitters already drew in the landscape slice - alpha particles
must not double-draw) to a new DrawDynamicsParticles callback;
GameWindow draws Scene-pass emitters filtered to those owner ids,
mirroring DrawRetailPViewCellParticles. Retail shape: emitters draw
with their owner object.

Re-gate ledger (user verdicts are axioms):
- #117 CLOSED ("Yes solved"), #118 CLOSED ("Yes solved" + NPC-through-
  door "Yes fixed").
- #108 REOPENED narrowed: cellar-ascent eye-below-grade window only
  (grass covers the exit door until the head pops over ground level);
  fix belongs on the membership/viewer side - the depth-gated punch
  stays (DO-NOT-RETRY).
- #119 user split: phantom walkable stairs at the hill cottage (#113
  family), tower missing stairs + barrel (#119 proper), hill-house
  transparent-on-entry (#112 - re-check after the #120 fix; the
  ping-pong fired at exactly A9B3 0103/010F).
- #120 FIXED pending re-gate (dede7e4).
- NEW #122 window oscillation on entry (re-check after #120 first),
  NEW #123 buildings transiently disappear running close past,
  NEW #124 far-building back walls missing through openings (lead:
  per-building look-in floods run only for outdoor roots -
  NearbyBuildingCells is null for interior roots; retail runs the
  look-in inside LScape::draw for ANY root).

Suites: App 236, Core 1419+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:36:58 +02:00
Erik
dede7e491c #120: CellView containment rejection - the reciprocal ping-pong converges; tripwire firings reproduced + killed
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>
2026-06-11 17:32:21 +02:00
Erik
8d93665053 #119: the [up-null] lead is EXONERATED (dat-proven) - both GfxObjs are legitimately no-draw models
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>
2026-06-11 16:55:45 +02:00
Erik
5a80a2ee24 #118: outdoor dynamics draw in the outside stage under interior roots - the house-exit clip+vanish was the SEAL z-killing the player
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>
2026-06-11 16:49:29 +02:00
Erik
478c549b9e #117: depth-gate the aperture punch - stencil mark+punch (z-buffered equivalent of retail's painter's order)
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>
2026-06-11 16:05:48 +02:00
Erik
2d15084243 #120: arm the propagation tripwire for self-attribution + two convergence regression pins
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>
2026-06-11 15:57:25 +02:00
Erik
ca4b482f8b T6 (BR-7) C4: straddle-only outside-add (A6.P5 widening DELETED) + #90 stickiness removed
The two remaining flagged workarounds retired, per the BR-7 plan +
the WF1 [MEDIUM] correction (re-gate, do NOT delete the outside-add):

1. A6.P5 hasExitPortal topology widening DELETED. Outdoor cells enter the
   collision cell array ONLY on the retail straddle gate - |dist| <
   radius + F_EPSILON against an exterior portal plane
   (CEnvCell::find_transit_cells Ghidra 0x0052c820, gate 0052c9d6,
   live-binary verified) - the same flag that already gated the
   membership pick (#112 rider). The widening existed so outdoor-
   registered doors stayed findable from indoor cells under the old flat
   registry query; with per-cell shadow lists the door is found in the
   straddle-admitted outdoor cell's own list (tick-13558 pin holds).
   The hasExitPortal out-param + plumbing deleted from
   FindTransitCellsSphere; the AddAllOutsideCells call in
   BuildCellSetAndPickContaining re-gated on exitOutsideStraddle
   (once-per-walk = retail CELLARRAY.added_outside).

2. #90 ResolveCellId sphere-overlap stickiness REMOVED (the 4ca3596
   workaround, deferred-to-A6.P4 in the physics digest). It was dead
   code: the method's only caller is FindEnvCollisions' cache-null TEST
   fallback, and the indoor branch (where the stickiness lived) required
   a non-null DataCache. Production membership flows exclusively through
   the collide-then-pick advance whose ordered-array hysteresis (current
   cell at index 0, interior-wins-break) is the retail mechanism the
   workaround approximated. ResolveCellId reduced to the bare
   prefix-preserving outdoor re-derive, documented test-only.

Test updates (pins of the deleted behaviors inverted to retail):
- A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell (asserted the
  topology widening verbatim) -> DeepInteriorSphere_NoStraddle_
  AddsNoOutdoorCells: a deep-interior sphere admits NO outdoor cells.
- A6P5_BuildCellSetFromAlcove... -> AlcoveSphere_StraddlesExitPortal_
  ReachesDoorOutdoorCell (the captured alcove position genuinely
  straddles - the retail-positive half).
- Issue112MembershipTests straddle pin + the second-sphere straddle test
  updated to the single-flag signature.

Suites: Core 1416/0/2, App 225, UI 420, Net 294 - green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:44:49 +02:00
Erik
dbfbf8506c T6 (BR-7) C3: per-cell shadow architecture - flood registration, building channel, per-cell query; b3ce505 stopgap DELETED (closes #99)
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>
2026-06-11 14:37:50 +02:00
Erik
abf36e2743 T6 (BR-7) C2: BuildShadowCellSet - the registration-side portal flood
Verbatim port of CObjCell::find_cell_list (Ghidra 0x0052b4e0, pc:308742)
as invoked by calc_cross_cells / calc_cross_cells_static (0x00515230 /
0x00515160) - the flood retail runs at SHADOW REGISTRATION time, minus
the containing-cell pick (registration passes a null out-cell):

- Seed: indoor -> exactly that one cell (added even when unloaded, like
  retail's null-pointer add_cell; the walk is then skipped per the
  0052b576 seed-pointer gate); outdoor -> block-crossing
  AddAllOutsideCells.
- Growing-array walk: indoor cells via FindTransitCellsSphere
  (sphere-vs-neighbor-BSP admission; exterior straddle -> outside cells
  once per walk, retail CELLARRAY.added_outside); outdoor cells via the
  CLandCell leg (0x00533800) = add_all_outside_cells (same once-guard)
  + the building bridge CSortCell -> CBuildingObj ->
  check_building_transit (0x00534060/0x006b5230/0x0052c5d0) - how an
  outdoor-positioned door reaches the vestibule's shadow list at
  registration. No XY grid, no visibility lists (the spec's
  VisibleCellIds rule was REFUTED by the WF1 verification).
- Static prune (do_not_load_cells, 0052b66e): indoor-seeded statics keep
  only {seed} + seed.stab_list (VisibleCellIds) - also strips outdoor
  cells, matching retail (interior statics never shadow into landcells;
  outdoor spheres reach them through their own array's building bridge).

11 unit tests: seeds (indoor/outdoor/unloaded), neighbor-BSP admission,
building bridge incl. the C1 negative-portal-id gate, exterior straddle
on/off, static prune drop/keep, zero-sphere no-op.

Consumed by C3 (ShadowObjectRegistry rewrite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:02:08 +02:00
Erik
6ec4cde9a4 T6 (BR-7) C1: signed OtherPortalId + the >=0 building-transit gate
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>
2026-06-11 13:56:16 +02:00
Erik
4a307d33b5 T4 (BR-6): ONE visibility gate - ACME BFS deleted from the frame, legacy second render path deleted
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>
2026-06-11 13:03:06 +02:00
Erik
a6aec8c32f T3 (BR-5): viewconeCheck port - per-view sphere culling for statics/dynamics/particles, weather player-gate, unattached outdoor emitters
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>
2026-06-11 12:56:48 +02:00
Erik
88f3ce1fa0 T2 slice 3 (BR-4): draw-driven flood gating - per-building frustum pre-gate, 48m seed cap DELETED
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>
2026-06-11 12:46:46 +02:00
Erik
529dfcfee9 T2 slice 2 (BR-4): in-place growth propagation, strict reciprocal cull, retail seed in-plane reject - two retail constants REFUTED by the gate and documented
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>
2026-06-11 12:42:41 +02:00
Erik
cf8a2c379b T2 slice 1 (BR-4): multi-view UNION merge + retail 1-px vertex merge (the fixpoint floor)
(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>
2026-06-11 11:20:12 +02:00
Erik
579c8b06bc T1 (fused BR-2/3): retail frame order - dynamics last, punch+seal, shell chop deleted
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>
2026-06-11 11:16:27 +02:00
Erik
88be519ec0 revert(render): BR-2 depth discipline - the gate proved #108 is MEMBERSHIP, not depth
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>
2026-06-11 10:35:57 +02:00
Erik
4ac547f6eb BR-2 commit 2: far-Z PUNCH on building entry apertures (outdoor + look-in)
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>
2026-06-11 08:15:52 +02:00
Erik
6d4cac2418 BR-2 commit 1: exit-portal depth SEALS + retail full depth clear (the #108 machinery)
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>
2026-06-11 08:03:10 +02:00
Erik
6cba95047c BR-2 task 1: phantom-site probe (ACDREAM_PROBE_PHANTOM)
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>
2026-06-11 06:27:44 +02:00
Erik
124c6cb2af revert(render): #113 - un-apply the DrawingBSP poly filter (door regression); keep helper + dat pins
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>
2026-06-10 21:47:44 +02:00
Erik
e46d3d9273 fix(render): #113 root cause #2 - GfxObj meshes draw only DrawingBSP-referenced polys (the REAL phantom staircase)
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>
2026-06-10 20:52:52 +02:00
Erik
9ce335eb17 fix(render): #114 - scope the PView shell clip to outdoor-eye roots (first user gate findings)
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>
2026-06-10 20:22:29 +02:00
Erik
414c3deaf4 fix(phys): #112 residual - retail straddle gate for outdoor-cell admission (live-binary verified)
The oracle read the #112 residual was waiting on, settled against the
LIVE 2013 client (cdb attach, CEnvCell::find_transit_cells @ 0052c820;
BN pseudo-C was ambiguous and partly wrong per
feedback_bn_decomp_field_names - it invented portal_side tests in this
branch): retail admits outdoor transit cells from an indoor cell IFF a
path sphere STRADDLES an exterior portal polygon plane,
|dist| < radius + F_EPSILON(0.000199999995, @ 007c8c70). The flag at
[esp+18h] (set 0052c925, x87 decode fcompp/test ah,41h +
fcomp/test ah,5/jp) gates the add_all_outside_cells call (0052c9d6 je).
Graph reachability alone NEVER admits outdoor cells in retail.

Port (CellTransit):
- FindTransitCellsSphere: exitOutside now carries the retail straddle
  semantics; new hasExitPortal out carries the old topology-only flag.
- BuildCellSetAndPickContaining: the collision cell SET keeps the A6.P5
  topology widening on hasExitPortal (outdoor-registered doors must stay
  findable from indoor cells until #99/A6.P4 ships per-cell shadow
  lists - the 2026-05-25 door capture scenario), but the membership
  PICK's outdoor branch is gated on the retail flag. Membership is now
  retail-identical in both regimes: straddle -> outdoor candidates valid;
  no straddle -> outdoor ignored -> retail keep-curr. This is what stops
  deep-interior containment gaps in ANY house from demoting to outdoor
  (the #112 transparent-interior shape) - the systemic protection the
  user asked for, without house-by-house verification.

The at-doorway A9B3 gap demote is RETAIL-FAITHFUL (gap point is 0.23m
from 0x104s door plane < 0.48 foot radius -> retail straddles + demotes
+ self-heals inward): DocumentsResidual renamed to
...DemotesRetailFaithfully, expectation unchanged. New conformance pins:
deep-gap keep-curr (A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell)
+ function-level gate semantics on real dat geometry
(FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail).

Tests: Core 1391 green (+2) / App 224 / UI 420 / Net 294; pre-existing
4 #99-era failures unchanged; P1 membership goldens + A6.P5 door-set
tests explicitly green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:52:24 +02:00
Erik
927fd8fde2 fix(render): #113 - enable GL clip distances for the PView shell pass (phantom exterior staircase)
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>
2026-06-10 16:26:55 +02:00
Erik
2d6954ee44 fix(phys): #112 - remove the non-retail escape-hatch demote from the pick; lateral stab-graph recovery + retail keep-curr
Root cause (oracle: CLandCell::point_in_cell :316941 = terrain-poly only;
find_cell_list null-result keep-curr pc:308788-308825; CEnvCell::
check_building_transit :309827 = sphere_intersects_cell per portal-adjacent
cell): retail KEEPS curr_cell when nothing contains the centre — including
inside a house's containment gaps. Our 6dbbf95 escape hatch instead demoted
any hydrated indoor claim the sphere no longer overlaps to the outdoor
column; at the A9B3 hill cottage's real interior gap this stranded the
player outdoor-classified deep indoors, where re-promotion is portal-
adjacent-only (retail-identical) -> the outdoor flood rendered the interior
transparent (the user's "sometimes transparent" walk).

The hatch's actual target - poisoned (cell, position) SAVES - has been
handled at the SNAP by PhysicsEngine.Resolve's AdjustPosition validation
since #107/#111, so the per-tick pick reverts to retail semantics:
1. lateral recovery first - when the sphere no longer overlaps the claim,
   search the claim's stab list for a containing cell (retail
   find_visible_child_cell :311444, the same recovery AdjustPosition uses);
   the #111 adjacent-claim shape now self-heals laterally (dat-backed test:
   pick(seed 0x172) at a 0x171-interior point -> 0x171);
2. else KEEP curr_cell (retail null-result).

Two old tests asserting the hatch demote rewritten to the retail semantics
(tests-can-codify-bugs); P1 retail-golden conformance gates explicitly green
(FindCellListConformance + ThresholdPortalCrossing + CottageDoorway +
CameraCornerSeal = 11/11). New Issue112MembershipTests: the lateral-recovery
fact + a DocumentsResidual fact pinning the remaining at-doorway gap demote
(via the NORMAL outdoor-candidate path; open oracle read = retail's
add_all_outside_cells gate in CEnvCell::find_transit_cells pc:317499 -
sphere-proximity vs graph-reachability). Core 1383 + 4 pre-existing #99
failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:03:49 +02:00
Erik
2735695a6b fix(phys): #111 - snap the player ENTITY at login entry, parity with the teleport-arrival path
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>
2026-06-10 14:28:29 +02:00
Erik
5706e0e10a fix(phys): #111 - ground the validated claim via PHYSICS walkable polygons, not the CellSurface triangle soup
Two grounding selection rules failed against live ACE restores before the
right one: (1) first-hit SampleFloorZ returned 0x171's 99.475 ceiling TOP face
over its 94.0 floor (issue111-verify2.log) - the player committed onto the
roof level, and the session's heartbeats poisoned ACE's save with z=99.475;
(2) nearest-to-reference self-confirmed that poison (the reference SAT on the
ceiling face, issue111-verify3.log). Root insight: ceiling/roof top faces are
upward-facing and XY-projectable - geometrically indistinguishable from
floors in the render-ish CellSurface soup. The PHYSICS walkable set (plane
normal.Z >= PhysicsGlobals.FloorZ over the claim's Resolved cell-local
polygons - retail BSPTREE::find_walkable's filter) contains only real floors:
PhysicsEngine.WalkableFloorZNearest transforms into the cell frame, drops on
each walkable plane under the XY, picks nearest the reference.

Verified live (issue111-verify4.log): ACE restored the roof-poisoned
(0xA9B40171, z=99.475); the snap validated the claim and grounded to
z=94.000 - the first fully clean indoor login of the arc:
[snap] claim=0xA9B40171 VALIDATED -> grounded to its walkable floor z=94.000
[cell-transit] 0x00000000 -> 0xA9B40171 pos=(155.525,12.416,94.000)

Baseline: Core 1381 + 4 pre-existing #99 failures + 1 skip; App/UI/Net green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:21:59 +02:00
Erik
5f1eb7c4b1 fix(phys): #111 - a validated indoor claim is authoritative at the snap; stop the whole-landblock bestCell floor-pick from clobbering it
The [snap] apparatus (issue111-snap1.log) caught the mechanism live: ACE
restored a CLEAN pair (0xA9B40171, on-floor) which AdjustPosition validated -
and the legacy Resolve then committed 0xA9B4013F instead: its bestCell floor-
pick scans EVERY CellSurface in the landblock (123 at Holtburg) for "any floor
under this XY nearest currentZ" and breaks same-height ties by iteration
order. The wrong cell then fails containment on the first movement -> outdoor
demote inside the building -> the #111 transparent interior. This free-pick
also explains the earlier "committed verbatim" mystery (the winning tie
happened to echo the input pair) AND seeded the ACE poison loop: our outbound
heartbeats reported the clobbered cell, ACE persisted it, and the NEXT login
inherited it (this run's [spawn-adjust] rejecting 0xA9B4013F is exactly that
echo coming back).

Fix (retail SetPositionInternal shape): when AdjustPosition VALIDATED an
indoor claim, the cell choice is settled - the snap grounds Z onto the
validated cell's own floor (find_valid_position's settle role, :283426) and
returns; it never re-picks the cell from floor geometry. Claims whose cell has
no own floor surface under the XY (thresholds, stair lips) fall through to
the legacy path unchanged; mover-shaped calls (delta != 0, tests) untouched.

[snap] diagnostic kept (snaps only - one line per login/teleport).
Baseline: Core 1381+4 pre-existing #99 failures+1 skip; App/UI/Net green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:12:21 +02:00
Erik
e4f6750e09 fix(phys): #107 gate-run regression — auto-entry hold opened on a mid-population cache read; wait for the claimed cell itself
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>
2026-06-10 13:10:01 +02:00
Erik
1090189d39 fix(phys): #107 indoor-login spawn wedge — validate the server (cell,pos) pair at the player snap (retail AdjustPosition) + unfreeze same-landblock teleport arrivals + self-consistent wire pairs
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>
2026-06-10 12:52:47 +02:00
Erik
d4b5c71e66 fix(render): re-land near plane 0.1m (retail Render::znear) — #110 resolved, closes the §4 corner see-through; close #105/#110
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>
2026-06-10 12:14:00 +02:00
Erik
c78720127a fix(render): #105 white indoor walls — restore WB's per-frame staged-texture flush dropped in the N.4/O-T4 extraction
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>
2026-06-10 12:10:00 +02:00
Erik
8bd3492612 revert(render): near plane back to 1.0m pending #110 - 0.1 correlated with missing indoor textures
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>
2026-06-10 11:02:55 +02:00
Erik
137b4f2d25 fix(render): near plane 1.0m -> 0.1m (retail Render::znear) - corner see-through-wall; file #107-#109
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>
2026-06-10 10:48:53 +02:00
Erik
dac8f6ad1f fix(render): §4 flood strobe — homogeneous reciprocal clip + collinear-aware region dedup
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>
2026-06-10 10:26:01 +02:00
Erik
c4df241690 fix(render): §4 outdoor full-world flap — empty Transparent pass leaked DepthMask(false), no-oping the frame depth clear
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>
2026-06-10 09:13:14 +02:00
Erik
682cba36f1 diag(render): §4 flap [clip-route] probe — slot routing + clip-buffer content + landscape scissor
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>
2026-06-10 08:37:09 +02:00
Erik
fafe5d66e8 diag(render): §4 flap apparatus — eye-vs-terrain field + GL-state tripwire
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>
2026-06-10 08:18:26 +02:00
Erik
e6913ac2f9 fix(app): #106 gate-3 — unblock in-world streaming before chase entry (entry-hold deadlock)
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>
2026-06-10 06:30:40 +02:00
Erik
6dbbf953c6 fix(phys): #106 gate-2 — bogus-indoor-claim recovery + spawn-ground entry hold
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>
2026-06-09 23:47:11 +02:00