Commit graph

1483 commits

Author SHA1 Message Date
Erik
b21bb28918 test(phys): §2b corner-seal replay — camera-penetration hypothesis REFUTED (openings, not walls)
Replays the captured corner/exit "escape" sweeps (corner-seal-capture.log) through
the real ResolveWithTransition with the camera probe call shape. Geometry-map
diagnostic proves every zero-contact traversal runs through a REAL opening: the
0170 exit-door portal for the viewer-outdoor frames; the 0171↔0173↔0172 doorway
chain (0173 = 20 cm threshold cell) for the corner-press frames. Captured eyes land
inside door-opening rectangles; the containment walk shows no path point in solid
cell volume; 8,703/14,230 indoor sweeps in the same capture collided correctly
(pull-in up to 2.77 m) — camera collision works, nothing penetrates.

The user-visible "background at the corner" is therefore NOT collision — it is the
§2a edge-on portal-clip collapse with the eye hovering at the doorway plane. Both
§4 siblings converge on one render-side mechanism: edge-on clip collapse near
opening planes. Next per the 2026-06-08 handoff §5.3: read retail's edge-on clip
oracle (PView::GetClip :432344, PView::ClipPortals :433572, polyClipFinish :702749)
before designing the fix.

Assertion fact = characterization pinning the verified behavior (openings pass
clean); Diagnostic fact = dispatch trace + room map + containment apparatus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:59:48 +02:00
Erik
df2ef7c598 docs: §4 outdoor full-world flap CLOSED — depth-mask leak close-out (evidence chain + fix + verification)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:22:02 +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
d877e4329a docs: §4 outdoor full-world flap — onset pinned to building-flood merge (handoff)
Evidence-chain handoff for the outdoor flap investigation: frame-exact
onset (pv-input flood 1->5 + the gl-state doorway-box fingerprint, same
frame), the full probe exoneration chain (camera matrix NaN-free at
6 dp, eye above terrain, cross-frame GL leak refuted, full-screen quad
planes can't cull, MergeNearbyBuildingFloods doesn't touch OutsideView,
ClipFrame capacity clean), the two surviving kill-mechanism suspects
(per-instance clip-slot routing under outdoor roots / terrain UBO
content at draw time), the decisive [clip-route] probe spec, the
user-validated repro protocol, and the probe-semantics gotchas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 08:19:54 +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
41fa3cbbc4 docs: #106 CLOSED — gate-4 verification + running-artifact attribution correction
Move #106 to Recently closed (user-verified collision + solid walls;
probe-verified 49 clean transitions incl. south A9B4->A9B3 at y=-0.19,
east A9B3->AAB3 at x=192.2, and room-by-room tracking through the
originally-failing A9B3 cottage). Records the three adjacent
pre-existing bugs the gate runs surfaced and fixed (legacy Resolve bare
ids, bogus-indoor-claim recovery, entry-hold streaming deadlock).

Correct the capture doc's attribution: the outdoor running distortion
was NOT fully the stale anchor — gate 4 shows residual background-color
screen artifacts persist with a correctly-following anchor. The
residual is the render §4 flap family (render digest), not membership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 06:59:24 +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
Erik
23adc9c9df fix(phys): #106 follow-up — legacy Resolve returns full prefixed cell ids (teleport bare-id wedge)
The #106 live gate run was sabotaged by a pre-existing bug the corrective
ACE teleport exposed: PhysicsEngine.Resolve (the legacy Phase-D resolve,
still used by the teleport-arrival snap at GameWindow.cs:4869 and the
player-mode-entry snap at :11295) returned BARE low-word cell ids on
every computed exit (ComputeOutdoorCellId, bestCell.CellId & 0xFFFF,
nextCellIndex, enterCellIndex). The teleport committed 0x0000013F into
PlayerMovementController.CellId, and a bare indoor id wedges the entire
membership chain:

- GetCellStruct(0x0000013F) misses (cells are keyed full-prefix) -> no
  indoor wall BSP -> walk through walls;
- the b3ce505 #98 gate reads "indoor primary" -> outdoor object radial
  sweep skipped -> NO object collision anywhere in the world;
- BuildCellSetAndPickContaining early-returns an unresolvable id forever
  (block 0x0000 is a real far-NW map block) -> membership frozen;
- render root never resolves -> interiors draw empty when stepping in.

Probe evidence: probe-cell-106-gate.log has exactly 2 [cell-transit]
lines for the whole session, both reason=teleport — the second one
(0xA9B3003C -> 0x0000013F) is the wedge. This is the L.2e "player CellId
tracked as bare low byte" finding (2026-05-12) finally biting; prefix
survival until now was a race artifact — Resolve only preserved the full
id when the landblock had not streamed in yet (passthrough exit), which
is why login snaps usually came out prefixed.

Fix: capture the matched landblock's key in Resolve's containment loop
and return lbPrefix | (targetCellId & 0xFFFF) on the computed exit —
the same full-32-bit convention Resolve's own doc comment states for
its inputs, and what both production callers (player snaps) require.
The passthrough exits (no landblock / step-reject) still return the
caller's id unchanged.

Tests: Resolve_IndoorStay_ReturnsFullPrefixedCellId (the teleport
shape, red pre-fix) + Resolve_OutdoorStay_ReturnsFullPrefixedCellId;
Resolve_LeaveIndoorCell_TransitionsToOutdoor's unmasked
`CellId < 0x100` assertion codified the bare behavior — now masked +
asserts the prefix. Full suite: 294+218+420 green; Core 1371 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:34:30 +02:00
Erik
7078264291 fix(phys): #106 — outdoor membership crosses landblock boundaries (LandDefs global-lcoord port)
The player's outdoor cell froze at the last in-block cell the moment they
walked over a landblock boundary (10,449-frame playerCell freeze in the
2026-06-09 capture; whole neighbouring-block interiors unenterable, plus
the running-distortion from the stale render anchor). Root cause: the
add_all_outside_cells port clamped BOTH the candidate proposal and the
find_cell_list containing-cell pick to the current landblock's 8x8 grid,
in a frame that silently assumed the current block sits at world origin.
One step over the line -> zero candidates -> FindCellSet returns
currentCellId forever.

Retail has no such clamp. Its cell math runs in a GLOBAL landcell grid
(lcoord 0..2039 spanning the map): get_outside_lcoord = blockid_to_lcoord
+ floor(blockLocalPos/24) with no bounds besides the map edge, and
lcoord_to_gid re-derives the landblock id from the lcoord's upper bits —
crossings are inherent, never special-cased.

The fix, decomp-cited throughout:
- New AcDream.Core.Physics.LandDefs: in_bounds (pc:68509),
  blockid_to_lcoord (pc:68520), inbound_valid_cellid (pc:163438),
  gid_to_lcoord (pc:163500), lcoord_to_gid (pc:171859),
  get_outside_lcoord (pc:438690), adjust_to_outside (pc:438719).
  Cross-checked against ACE LandDefs.cs; three artifacts documented and
  avoided: BN's int8_t mis-render of block_y, BN's dropped 192f
  BlockLength constant, and ACE add_cell_block's "FIXME!" same-block
  guard (an ACE divergence, not retail).
- CellTransit.AddAllOutsideCells rewritten as the faithful sphere
  variant (pc:317499 @0x00533630): adjust_to_outside re-seats the
  (cell, position) pair cross-block, check_add_cell_boundary (pc:317229)
  adds up to 3 neighbours by global lcoord, add_outside_cell (pc:317056)
  has no same-block filter. adjust_to_outside failure breaks the sphere
  loop (pc:533699 verbatim).
- BuildCellSetAndPickContaining: the outdoor containing-cell pick is now
  the global XY-column under the sphere centre (AdjustToOutside), not
  the [0,8)-clamped current-prefix reconstruction. Interior-wins order
  and current-cell-first hysteresis unchanged.
- World->block-local frame conversion via the landblock origin already
  registered in CellGraph (new TryGetTerrainOrigin); Zero fallback
  preserves the legacy anchor-block assumption for unregistered terrain.
- Cross-landblock building entry comes free: the candidate snapshot now
  contains neighbour-block landcells, so GetBuilding/CheckBuildingTransit
  fire for cottages across the line (the capture's one failing entry).

Investigated FIRST per the pickup brief: the b3ce505 #98 stopgap gate is
definitively exonerated — it is a collision-object query gate that fires
only for indoor primary cells; no membership path touches
ShadowObjectRegistry.

Tests: 31 new (25 LandDefs conformance incl. capture-geometry goldens
0xA9B40031 -> 0xA9B30038/0xA9B30034 and the northbound return; 4
AddAllOutsideCells cross-block; 3 FindCellSet membership goldens incl.
the non-anchor-frame origin conversion). Full suite: 294+218+420 green;
Core 1369 green + the 4 pre-existing door/#99-era failures + 1 skip
(unchanged from baseline).

Pseudocode + artifact notes:
docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md.
Remaining acceptance: live boundary walk with ACDREAM_PROBE_CELL=1
(ISSUES.md #106).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:10:59 +02:00
Erik
12fb408972 docs: #106 pinned — outdoor membership freezes at landblock boundaries
The 53MB flap-probe capture (user live-reproduced the broken-house entry)
plus 3-agent analysis settles the day: playerCell froze at 0xA9B40031 for
10,449 frames spanning ~130m of outdoor walking into landblock A9B3 and a
stand INSIDE an A9B3 cottage. Within-landblock outdoor flips are 96/96
clean; all 10 successful indoor entries were same-landblock buildings; the
single cross-landblock entry failed. The render flood independently drew the
A9B3 interior cells the whole time — rendering is downstream and healthy;
membership is the broken layer (feedback_render_downstream_of_membership,
proven again). The stale render anchor also explains the outdoor running
distortion; the capture refutes flood-level causes outdoors (26,960/26,960
outdoor frames rigid at outPolys=1 vis=1).

Files #106 (HIGH, physics/membership) with fix pointers: ResolveCellId /
AddAllOutsideCells cross-landblock proposal, the b3ce505 outdoor-sweep gate
(possible stopgap fallout, like #99), retail find_cell_list :308742 +
LandDefs.get_outside_lcoord. Reframes #105: largely superseded by #106;
residual (single wall missing while membership indoor-correct) stays open
with all tripwires armed. Handoff:
docs/research/2026-06-09-105-capture-analysis-membership-landblock-pin.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 22:38:19 +02:00
Erik
0a38d934fd diag(render): #105 round 3 — finalize-replace + late-register tripwires on EnvCellRenderer
Live evidence narrowed #105 to the pending->committed instance hand-off:
walls missing from BOTH inside and outside views while the same cells'' props
draw and collision works = the wall-shell INSTANCES never reach the committed
draw set. FinalizeLandblock uses REPLACE semantics (lb.Instances =
lb.PendingInstances), and with two-tier streaming a landblock can finalize
while a promote job is still registering its cells on the worker thread —
the partial pending set commits, the remainder lands in a fresh pending list,
and the next finalize REPLACES the committed set with only the remainder.
Whoever registered first is silently lost: per-session-random (drain timing),
per-building-persistent, from session start.

- [finalize-replace] a finalize that DISCARDS already-committed instances
- [late-register]    a RegisterCell landing after its landblock finalized

Both print only on the suspect interleavings. Next occurrence proves or
kills the theory; the fix (merge semantics + registration/finalize atomicity)
follows the evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 22:07:43 +02:00
Erik
cbba71f8a9 diag(render): #105 round 2 — tripwires on the upload/registration loss paths
Live session evidence narrowed #105 decisively: a wall section rendered as the
sky/clear color from session start, HAD collision (cell + physics fully
loaded), zero round-1 tripwires fired (all dat reads succeeded), and the hole
showed terrain or clear color depending on camera angle. So the wall mesh was
built and then lost between mesh-build and draw. New tripwires cover the
loss candidates in that window (all print ONLY on anomaly):

- [geom-null]     ProcessQueueAsync EnvCell branch resolved null, with the
                  failing sub-step (prepare-null / cellstruct-missing /
                  env-read-failed) — a null here means the DEDUPLICATED cell
                  geometry never renders for ANY cell that shares it, and
                  nothing retries (RegisterCell fire-and-forgets the task).
- [geom-misroute] an EnvCell geom id (bit 33) whose pending request vanished
                  fell through to the generic path, where its hash-derived
                  low bits resolve to nothing -> silent null.
- [up-null]       UploadGfxObjMeshData returned null and the EMPTY substitute
                  was cached in _renderData forever (permanently invisible).

Pair with the existing one-shot draw-side audit (ACDREAM_A8_AUDIT=1, light:
one line per unique cell/geom pair, prints renderData=null + bindless-handle
status) for full attribution on the next occurrence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:52:01 +02:00
Erik
8dc707d43b docs: dat-reader investigation handoff + file #105 (white walls, tripwired)
Records the 2026-06-09 dat-reader thread-safety investigation: concurrent
READS on Chorizite.DatReaderWriter 2.1.7 exonerated (source audit + 1.1M-read
hammer, b3920d8); the real crash was dispose-during-read at teardown (fixed,
8fadf77); the white-walls mechanism remains open as #105 with every silent
dat-miss exit tripwired (7433b70) so the next occurrence self-attributes.

Also corrects project lore: the A.1-era rule that all dat reads must stay on
one thread does not hold for the 2.1.7 read path, and both investigation
subagents'' claimed ReadBlock instance-field race does not exist in the
shipped source — verify agent claims against source before acting on them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:29:06 +02:00
Erik
b3920d83f6 test(conformance): dat-reader concurrency hammer — concurrent READS exonerated
Settles the long-standing lore that DatCollection is not thread-safe for reads,
for Chorizite.DatReaderWriter 2.1.7: replays acdream''s real four-population
access pattern (render / streamer / decode-pool / raw) against the live dats —
golden FNV-1a fingerprints taken single-threaded, then 8 threads x 25 shuffled
passes over ~2900 files spanning the cell heightmap/LBI/EnvCell set and the
portal texture chain (Environment -> Surface -> SurfaceTexture -> RenderSurface
incl. highres probes). Two layers: raw TryGetFileBytes (BTree + ReadBlock, no
caching) and typed TryGet with FileCachingStrategy.Never (full production
unpack path: ArrayPool + DatBinReader + ObjectFactory).

Result: ~1.1M concurrent reads, ZERO anomalies — byte-identical to golden.
Matches the line-level audit of the release/2.1.7 source (ReadBlock keeps all
cursor state in locals over a stable read-only mmap view; locked LRU BTree
node cache; ConcurrentDictionary file cache; fresh DatBinReader per call).
The real crash bug was dispose-during-read at teardown (fixed in 8fadf77).
Keep this as the regression guard for any future dat-reader version bump.

Skips cleanly when the dats are absent (CI), matching suite convention.
Full evidence: docs/research/2026-06-09-dat-reader-thread-safety-investigation.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:28:32 +02:00
Erik
7433b704fb diag(render): tripwires on every silent dat-miss path (white-walls attribution, #105)
The intermittent white-cottage-walls failure has NEVER produced a log line:
every dat-read failure on the walls-relevant paths exits silently, and the
failed result is cached for the session (mesh batches build once). Today it
reproduced on a probe-free launch with a 35-line, zero-error log — so the
prior heavy-probes-starve-the-dat-reader framing is not the whole story.

Tripwires (print ONLY on anomaly; zero cost healthy; keep until #105 closes):
- [dat-miss]  DatDatabaseWrapper.TryGet — a miss for an id whose BTree entry
  EXISTS (re-probed under the same lock); legit not-found fallbacks stay quiet.
- [tex-miss]  TextureCache.DecodeFromDats x3 — render-thread decode fell back
  to magenta (Surface / SurfaceTexture / RenderSurface miss).
- [cell-miss] GameWindow interior hydration x2 — EnvCell or Environment read
  returned null, so a cell''s WALLS are silently never registered while its
  statics still draw (the exact observed geometry signature).

Color discriminates the layer on the next occurrence: magenta = TextureCache;
see-through + [tex-skip]/[dat-miss] = mesh build; see-through + [cell-miss] =
hydration; broken with NO tripwire output = GL-side upload/residency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:28:32 +02:00
Erik
8fadf770fe fix(render): quiesce dat readers before teardown — kill the shutdown AccessViolation
ObjectMeshManager.Dispose never stopped its Task.Run(ProcessQueueAsync) decode
workers, and LandblockStreamer.Dispose abandoned its worker after a 2s join.
GameWindow.OnClosing then disposed the DatCollection, which unmaps the dats''
memory-mapped views (MemoryMappedBlockAllocator.DestroyMappedFile nulls
_viewPtr) — a worker still inside ReadBlock dereferences the dead view pointer:
an uncatchable AccessViolationException with ReadBlock on the stack, firing on
close/relaunch during decode storms. This is the recorded crash signature from
the 2026-06-09 white-walls session.

- ObjectMeshManager.Dispose: set IsDisposed under the queue lock, cancel+drain
  pending requests, then wait (<=10s) for _activeWorkers==0; loud LogError if
  workers outlive the wait. ProcessQueueAsync re-checks IsDisposed per dequeue;
  Prepare*Async entries + enqueue blocks early-out when disposed.
- LandblockStreamer.Dispose: join 2s -> 15s with a loud [streamer] line on
  timeout (cancellation honored between jobs; one landblock load bounds it).
- Also includes the [tex-skip] tripwire lines on ObjectMeshManager''s five
  silent dat-miss exits (GfxObj + CellStruct texture chains) — part of the
  white-walls attribution net (#105), zero output when healthy.

Verified: 3x close-mid-decode-storm smoke (in-world at ~8s, WM_CLOSE at ~11s),
clean exits, no crash signatures, no quiesce timeouts. Full suite: 294+218+420
green; Core 1338 green + 4 pre-existing physics failures (reproduced at bare
HEAD, unrelated). Investigation:
docs/research/2026-06-09-dat-reader-thread-safety-investigation.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 21:27:22 +02:00
Erik
d0bd28543b docs(memory): distill CLAUDE.md render+physics banners into claude-memory digests
Collapse ~285 lines of dated render-flap + A6/#98/door/#100/#101 physics saga banners into two pointers to new claude-memory digests (project_render_pipeline_digest + project_physics_collision_digest), each carrying current-truth + a DO-NOT-RETRY table. Add an always-loaded memory pointer to the How-to-operate section: read the digests before domain work; update the digest, don't add new dated banners here. ~380 fewer lines in the always-loaded instruction file; no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:20:17 +02:00
Erik
a1b12dff40 docs(render): R-A2b shipped + flap residual (sec 4) + texture red-herring handoff
R-A2b (485e44d) killed the 0171<->0173 churn (maxPop 16->1, measured). Visible flap residual is sec 4 (edge-on openings render-side + corner camera-seal). Camera-damping tried+failed+reverted. The white-walls scare was a RED HERRING: heavy per-frame probes (ACDREAM_PROBE_FLAP) starve the thread-unsafe dat-reader so texture-decode loses the race -> white; a clean launch (no probes) fixes it. The dat-reader thread-safety bug is the real underlying issue (filed). Repo clean at HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:44:33 +02:00
Erik
485e44d163 fix(render): R-A2b — cull back portal like retail (InitCell side test), kill the indoor flap cycle
Pinned (flap-sidechk.log): the indoor doorway flap is a 0171<->0173 flood cycle. Back portals show camInterior=False (our side test already agrees with retail) but were traversed when eyeIn=True because the side-cull had an  bypass (added 2026-06-05 for the void). Within 1.75m of a doorway that bypass kept the BACK portal alive -> mutual re-contribution -> re-enqueue churn (maxPop=16) -> eye-sensitive flood depth -> grey flap + dropped floor.

Fix (Option B1): drop the bypass from the side-cull in Build + BuildFromExterior so back portals cull by the side test alone, exactly like retail PView::InitCell (:432962, no eye-in-opening bypass). The forward-portal clip-empty void rescue is a SEPARATE branch and is untouched (Build_EyeStandingInInteriorPortal_FloodsNeighbour stays green). New RED->GREEN test Build_BackFacingPortal_EyeStandingInOpening_StillCulled; full App suite 218 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:06:57 +02:00
Erik
89a2032c8e diag(render): [pv-trace] sidechk — pin back-portal traversal (B1 bypass vs B2 side-test) for R-A2b
Logs camInterior/eyeIn/D per portal under the existing PortalBuildTrace so the 0173->0171 back-portal traversal can be attributed to B1 (EyeInsidePortalOpening bypass) or B2 (CameraOnInteriorSide convention). Throwaway; stripped in Phase 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:46:14 +02:00
Erik
7b8a490da9 docs(render): R-A2b plan — back-portal side-cull (Option B), verify-first B1/B2 pin
Reading retail InitCell (:432896) side test during writing-plans showed retail's flood is acyclic (the back portal fails the side test, so 0171<->0173 can't cycle). Our flood traverses the back portal -> the cycle -> the churn. Option B (user-chosen): cull the back portal like retail, keep the forward-portal void rescue, remove the dead cap. Phase 1 pins WHY the back portal is traversed (B1 eyeInsideOpening bypass vs B2 CameraOnInteriorSide convention) before the fix; spec REVISION updated A->B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:25:28 +02:00
Erik
3fd71a123c docs(render): R-A2b spec — revive bounded-propagation, churn confirmed at flap-time
The indoor doorway flap is the portal flood's re-enqueue churn (0171<->0173 mutual re-contribution; drifted near-duplicate regions AddRegion won't dedup -> grew -> re-enqueue, capped at MaxReprocessPerCell=16 -> eye-sensitive flood depth -> grey flash). Confirmed live: launch-churn-confirm.log shows maxPop=16 on 44% of frames during a doorway walk-through. The 2026-06-08 'maxPop=1, churn refuted' verdict was a camera-turn-at-rest capture (wrong reproduction); its DO-NOT is overturned.

Fix (Option A, user-approved): contributions already covered by the neighbour's accumulated view don't grow it (no re-enqueue); only the uncovered remainder propagates -- retail's 'redundant -> empty before copy_view' (copy_view confirmed to just append). Remove MaxReprocessPerCell; keep re-processing of genuinely-new slices. Scope: PortalVisibilityBuilder only. Revives 2026-06-08 spec+plan (banners redirected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:48:53 +02:00
Erik
8f879bd7d9 docs(render): calibrated indoor-flap handoff — MEASURED vs HYPOTHESIS vs OPEN
Separates what is measured (eye smooth + 1um at rest -> not jitter) from the leading-but-unproven hypothesis (clip edge-on) and the NOT-ruled-out alternative (camera position: retail eye collided/head-on 93%, ours floats edge-on). The one 'clean' pass had ratio 4.2x back-and-forth, so the flood claim is indicated not proven. Lists the verify-first steps before any R-A2b fix. Counters this session's pattern of overclaiming then refuting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:16:51 +02:00
Erik
8c78f1f07a docs(render): plan status — R-A4 ruled out by measurement; remaining work is R-A2b (indoor-flood edge-on robustness)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:10:06 +02:00
Erik
c7069cf0b6 diag(camera): add F6 in/out eye to [flap-sweep] probe
Logs SweepEye input (desiredEye) vs output (eye) at micrometre precision. Used to prove the indoor flap is NOT the camera: the eye is smooth (clean one-way pass = 3/18 direction-changes over 25.7k frames) and ~1um stable at rest, yet the visible-cell count oscillates 414x with 648 clip=0 near edge-on doorways. The flap is the flood/clip's edge-on behaviour, not the eye.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:09:10 +02:00
Erik
3c178b28e6 diag(cdb): flap-cam-measure.cdb — retail eye-hover + CameraManager smoothing capture
Attaches to live retail, dumps CameraManager/SmartBox type offsets, captures viewer eye origin per frame (pub+sought) to measure boom jitter vs acdream. Used to pin the indoor flicker to the camera's published/swept eye (retail settled ~tens of um vs acdream ~1.3mm).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:14:54 +02:00
Erik
2ec189c106 fix(render): R-A2 seam fix — flood null-BuildingId cells instead of dropping them
MergeNearbyBuildingFloods skipped cells whose BuildingId is null; the pre-R-A2 outdoor-node reverse-portal flood reached them, so dropping left holes at building/terrain seams. Key by (BuildingId ?? CellId) so unstamped/outdoor-adjacent exit-portal cells still seed a per-entrance flood; cells without an exit portal contribute nothing as before. App Rendering 207/207.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:08:14 +02:00
Erik
c62663d7cb feat(render): R-A2 — per-building floods (the flap fix)
Replace the outdoor root's single unified reverse-portal flood (whose root-level
portal-side test oscillated as the chase eye grazed a doorway — the measured
flood 2<->6) with retail's per-building floods.

- OutdoorCellNode.Build(uint): portal-less land root; floods only itself ->
  full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed).
- PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a
  building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via
  DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded ->
  consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4).
- RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby
  cells by BuildingId and merge each per-building flood into the frame before
  assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff.
- GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames.

Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership
stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject,
the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView
coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14.

Conformance-verified sound; the grazing-doorway flap is the visual acceptance test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:44:43 +02:00
Erik
7fe98098f5 refactor(render): R-A1 — canonicalize outdoor-root detection on IsOutdoorNode
Replace ReferenceEquals(clipRoot, _outdoorNode) object-identity checks with the
documented LoadedCell.IsOutdoorNode flag (4 sites) so they survive R-A2 changing
the outdoor root's portals. Behavior-preserving (build + targeted suites green:
App PortalVisibilityBuilderTests 24/24, Core PlayerMovementControllerTests 14/14).

Right-sized from the planned 'collapse to one root': reading the live dispatch,
the viewerRoot ?? outdoorRoot split is already correct (viewerRoot feeds
cameraInsideCell/lighting via the older CellVisibility BFS; clipRoot is the render
root), and the 2026-06-07 cutover flip already made in-world frames single-path
DrawInside. The real flap fix is R-A2 (per-building floods). Dead exterior
DrawPortal look-in deletion deferred to R-A3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:25:58 +02:00
Erik
6996e5645c docs(render): mark bounded-propagation plan + spec SUPERSEDED (churn refuted by measurement)
Point both at the Option-A full-retail-port handoff so a fresh session can't follow the dead plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:20:24 +02:00
Erik
fe87e9794a docs(render): FLAP settled by live-retail measurement — full retail port DECIDED (Option A) + exhaustive handoff
Attached cdb to the live 2013 retail client at the Holtburg doorway + read the decomp.
The indoor flap is a STRUCTURAL divergence, settled by measurement (not inference):

- Retail has ONE render path: DrawInside(viewer_cell) every frame. NO inside/outside
  branch (RenderNormalMode's outside branch is dead code; is_player_outside only gates
  sky/lighting). "Entering a building" is not a render event — only the camera sweep
  resolving a different viewer_cell. Same path before/after threshold -> no seam.
- Retail's eye JITTERS ~36um at rest yet membership is stable -> robustness is
  STRUCTURAL: many small per-building floods (~7/frame, ~2 cells each, via terrain BSP
  -> DrawPortal -> ConstructView(CBldPortal)), not one giant knife-edge flood.
- Our 3 divergences: (D1) invented inside/outside branch (GameWindow.cs:7498,
  clipRoot = viewerRoot ?? _outdoorNode :7396); (D2) synthetic _outdoorNode; (D3) one
  unified flood.

DECISION (user-approved): Option A — rip out branch + outdoor node, root always at the
real viewer_cell, one DrawInside, per-building rendering. Phased, conformance-tested,
visual-gated.

REFUTED by measurement (do not retry): bounded-propagation/churn (maxPop=1, 0/63k
reciprocals empty); byte-stable eye (retail's jitters ~36um — rest-snap cd974b2 failed +
regressed, reverted 9b1857a).

Lands the canonical exhaustive handoff for a FRESH session
(docs/research/2026-06-08-full-retail-render-port-OPTION-A-handoff.md), the CLAUDE.md
READ-THIS-FIRST banner, and reusable cdb apparatus. No project code changed; working tree
at the known-good baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:19:34 +02:00
Erik
9b1857ac52 Revert "fix(camera): rest-snap render position — kills the indoor doorway standing-still flicker"
This reverts commit cd974b29bc.
2026-06-08 15:08:25 +02:00
Erik
cd974b29bc fix(camera): rest-snap render position — kills the indoor doorway standing-still flicker
Root cause (pinned live, flap-churn.log at the Holtburg cottage doorway): the physics
body is byte-stable at rest (rawPlayer = 1 distinct value), but
PlayerMovementController.ComputeRenderPosition's Lerp(prev, curr, alpha) dithers the
render position by microns — the two physics-tick snapshots lag the settled body
(per-frame resolve edge-settles the resting sphere against the doorframe after the last
tick wrote curr) while the leftover-accumulator alpha varies every frame. The grazing-
doorframe camera-collision sweep (PhysicsCameraCollisionProbe.SweepEye) amplifies that
~1000x into a ~1.3 mm eye jitter (eye 17 distinct, RenderPosition 15 distinct) that trips
the PortalVisibilityBuilder clip -> the standing-still flicker (blue void / grass over the
cellar entrance) the user reported.

Fix: at rest (body velocity below RestVelocityEpsilonSq) render AT the authoritative
byte-stable body position instead of interpolating between two stale tick snapshots, so the
camera's pivot input is byte-stable and the sweep output stops jittering. Mirrors retail (a
resting object renders bit-stable) + the boom convergence snap
(RetailChaseCamera.ApplyConvergenceSnap, d2212cf), one layer earlier. Sub-tick interpolation
is preserved during motion (velocity above epsilon).

This SUPERSEDES the committed bounded-propagation plan: the live pin proved ZERO portal
re-enqueue churn during the flap (maxPop=1 across 13k oscillating frames; 0/63k reciprocals
ever clipped empty), so the flap was never the churn the spec hypothesized. The
ACDREAM_PROBE_PORTAL_CHURN apparatus did its job (refuted the hypothesis before the wrong
fix was built); plan/spec/memory updates to follow.

TDD: extracted the rest-snap into an internal-static pure ComputeRenderPosition; RED rest-
snap test (stale prev!=curr + varying alpha dithers) -> GREEN after the gate; motion test
guards interpolation; precondition test confirms a settled body's velocity is below the
gate threshold. 29 controller+cellar + 62 camera+portal tests green, no regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:59:59 +02:00
Erik
b3a9884dff diag(render): launch-flap-churn.ps1 — Phase 1 portal-churn pin capture script
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:56:44 +02:00
Erik
a866c510e3 test(render): deterministic re-pop anchor for the bounded-propagation pin
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:52:46 +02:00
Erik
e6fe4c611a diag(render): [portal-churn] probe — per-Build re-enqueue + reciprocal pre/post
Step 4 summary-emit adapted from the plan: the plan's Invariant($"a" + $"b" + sb) form

passes a string to FormattableString.Invariant (which requires a FormattableString) and

does not compile; merged the two interpolated fragments into one literal and appended the

already-invariant-formatted reciprocal detail outside the Invariant call. Same output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:51:48 +02:00
Erik
687040ba52 feat(render-diag): add ACDREAM_PROBE_PORTAL_CHURN flag for the bounded-propagation pin
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:49:17 +02:00
Erik
a3dadbf664 docs(render): implementation plan — portal-flood bounded-propagation (instrument+pin, then fix)
Phase 1 (fully specified): add the [portal-churn] probe (per-Build re-enqueue +
reciprocal pre/post), a deterministic re-pop anchor test, and a live doorway
capture to PIN the exact divergence (where acdream's redundant reciprocal
back-contribution stays non-empty where retail clips to empty) — a float-drift
runtime fact, not derivable from decomp.

Phase 2 (evidence-gated outline): port the bound (redundant contributions don't
add propagatable slices; remove MaxReprocessPerCell), keeping re-processing +
Build_ViewGrowthAfterDoneCell green. Gets its own no-placeholder plan after the
Phase 1 pin — apparatus-first, not a deferred placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:38:02 +02:00
Erik
ab6ed905f1 docs(render): correct flap spec — enqueue-once REFUTED, bounded-propagation port
The writing-plans decomp pass read FixCellList (433407) -> AdjustCellView (433741)
-> ClipPortals(update_count) + AddViewToPortals, proving retail RE-PROCESSES a
grown-after-drawn cell. So the approved "enqueue-once / no re-process" approach is
wrong (it would break Build_ViewGrowthAfterDoneCell for the right reason — that test
is actually retail-faithful).

Corrected approach (user chose the faithful moderate port over an epsilon-dedup
band-aid): KEEP re-processing on growth, but BOUND it the way retail does — each
view slice processed once (monotonic update_count watermark) and redundant
reciprocal back-contributions clip to EMPTY (OtherPortalClip -> no copy_view -> no
new slice), so the reciprocal/drift loop can't churn. acdream churns because its
reciprocal yields a drifted non-empty sliver, bounded only by the
MaxReprocessPerCell=16 hack. Remove the cap; bound structurally.

Scope unchanged: PortalVisibilityBuilder only; no rooting/camera/clip-math-rewrite/
seal change. One open precision (exact line where acdream's sliver becomes
non-empty — float-drift-dependent on real geometry) deferred to the plan's first
task: instrument PortalVisibilityBuilder (per-pop re-pop count + reciprocal-clip
in/out + grew), capture at the doorway, pin it, THEN fix.

Spec updated in place with a REVISION banner; superseded enqueue-once body retained
for the audit trail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:33:06 +02:00
Erik
6c3a96b26e diag(render): flap re-diagnosed as portal-flood re-clip DRIFT; physics + camera REFUTED
The 2026-06-08 AM "physics rest micro-jitter" diagnosis is refuted with primary
evidence (door-recheck 216K standstill records: 0 position re-snaps; player
byte-stable during the flap). Two adversarial verification sub-agents confirmed:

- Retail roots the render at the camera viewer_cell (swept from the player via
  SmartBox::update_viewer 0x453ce0; DrawInside(viewer_cell) 0x453aa0) and toggles
  DrawInside / LScape::draw -- so acdream's eye-cell rooting + inside/outside
  toggle are RETAIL-FAITHFUL. The locked-design "root at player cell" is wrong.
- The flap is render membership instability, eye-motion-driven: the visible-cell
  set oscillates (8<->3) as the eye sweeps monotonically. Root = the
  re-enqueue-on-growth DRIFT (PortalVisibilityBuilder.cs:322, MaxReprocessPerCell
  =16) re-clipping each grown cell every round -> sub-cm eye jitter flips membership.

Fix (spec, not yet implemented): verbatim port of retail's enqueue-once flood
(ConstructView + AddViewToPortals): enqueue once on first discovery, clip each
cell's portals once, union late growth in place (AddToCell) + draw-reorder
(FixCellList), never re-enqueue. Kills the drift; rooting/camera/seal untouched.

This commit lands VERIFIED GROUNDWORK + design only:
- spec: docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md
- findings: docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md
- [pv-input] probe gains rawPlayer + yaw (disambiguates the varying input)
- 4 GREEN physics rest-stability tests (prove rest is bit-stable -> flap not physics)
- apparatus: launch-flap-capture.ps1, analyze_flap_live.py, find_burst.py
- captured fixtures: tests/.../Fixtures/flap-doorway/0xA9B4017{0..5}.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:21:46 +02:00
Erik
d6aa526dd3 diag(render/physics): flap root-caused to physics rest µm-jitter; refute prior diagnoses
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:16:12 +02:00
Erik
d0b65c4170 docs(render): re-scope flap fix to retail enqueue-once traversal port (not an overlap band-aid)
Per senior-eng direction: the retail-faithful fix is to stop diverging from PView::
AddViewToPortals (first-discovery enqueue + AddToCell/FixCellList in-place growth, no
re-enqueue/re-clip), removing acdream's MaxReprocessPerCell re-enqueue fixpoint and its
documented per-round ProjectToClip drift. Drops the overlap-predicate approach. Viewpoint
bit-stability (the ~1-8um player RenderPosition jitter) is the contingency next step only
if a residual flap survives the visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:51:51 +02:00
Erik
d9d69394bb docs(render): spec — portal-flood membership stability (indoor flap root-cause fix)
Confirmed root cause via primary evidence (determinism test + 6dp jitter probe + retail
grounding): the flap is portal-flood set-membership flipping because the drift-prone
ClipToRegion vertex count gates membership while the player RenderPosition micro-jitters
(~1-8um) into a grazing portal's knife-edge clip. Design: gate membership on a stable
side-test + view-region overlap, not the vertex count. Refutes the 2026-06-07 see-through/
EnvCell/outdoor-node handoff (ModelId GfxObj 0x01000A2B is the solid exterior; outside is
stable; root is stable 0170).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:23:34 +02:00
Erik
5c6e53b0a4 docs: add verify-first kickoff prompt to the render-residuals handoff (treat the diagnosis as a suspect statement)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:12:38 +02:00
Erik
ef2186147d docs: cutover flip shipped — see-through + oscillation DIAGNOSED (evidence-based handoff)
The flip killed the branch-toggle flap (one path, zero OutdoorRoot frames). It exposed two residuals now PROVEN via a live [bshell] probe, not guessed: (1) oscillation = the outdoor-node flood membership swings 1<->~13 building cells frame-to-frame, so the walls (EnvCell shells) blink; (2) see-through = EnvCell wall polys are single-sided for SidesType==CounterClockwise, so from outside you see their culled back. The ModelId building shells DO render (6/6 with mesh) but are a partial frame, not the walls — the skip-all-interiors experiment proved the walls are the EnvCell shells. Fixes identified (stabilise flood + build back faces) but not implemented; full do-not-retry list + open pre-flip-reconciliation question in the doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:49:00 +02:00
Erik
774cb22713 Revert "fix(render): outdoor look-in draws interior cells only through real doorway apertures (no see-through walls)"
This reverts commit 0030dacaaa.
2026-06-07 21:16:11 +02:00
Erik
0030dacaaa fix(render): outdoor look-in draws interior cells only through real doorway apertures (no see-through walls)
Cutover-flip follow-up: see-through buildings from outside. When the outdoor-node flood reaches a building, each interior cell is meant to draw clipped to its doorway aperture. But DrawEnvCellShells falls back to the no-clip slot 0 (full-screen) when a cell's aperture degenerates — screen-covering when you get close, or edge-on. Indoors that fallback is load-bearing (it seals the room the camera stands in; near walls hide the over-draw). From OUTSIDE it paints the building interior across the whole screen, depth-tested, so it shows wherever the solid exterior does not cover — the see-through walls, appearing 'past a threshold' exactly where the aperture degenerates.

Fix: for the outdoor-node root only, skip a flooded interior cell with no real plane-clip slot (HasRealClipSlot). From outside, 'no real aperture' means 'do not paint this interior', not 'paint it everywhere'. Interior roots keep the seal-everything slot-0 fallback unchanged. Applied to DrawEnvCellShells AND DrawCellObjectLists so a skipped cell shows neither walls nor furniture; the dead DrawPortal exterior look-in gets the same gate.

Root cause traced over the WB EnvCell render path: CellMesh.cs is physics-only; ObjectMeshManager.PrepareCellStructMeshData builds double-sided walls, so this was never a culling bug. App 216/0, build green. Visual gate pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:08:43 +02:00
Erik
88caa0dc8b fix(render): outdoor-root skips the full-screen depth clear (cellar/buildings no longer draw over the world)
Cutover-flip follow-up (issues 1+2 from the visual gate). After the flip an outdoor-node frame ran DrawInside with a full-screen OutsideView slice (Step A) whose ClearDepthSlice wiped the ENTIRE depth buffer AFTER terrain/exteriors/player drew — so the flooded building interior shells (cellars) drew over everything: the cellar painted in front of the player from outside, and distant building interiors showed through the ground in the open field.

The depth clear is a doorway look-in trick (clear a small door region so the cell seen through it draws over the terrain drawn through it). It is wrong for the full-screen base terrain of the outdoor root. Skip it there (ClearDepthSlice=null when clipRoot is the outdoor node); interiors now depth-test against terrain + exteriors and appear only through real door openings. Interior roots keep the doorway clear unchanged.

App 216/0, build green. Visual gate pending (player must be outdoors to exercise the outdoor root).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:18:33 +02:00