Commit graph

1585 commits

Author SHA1 Message Date
Erik
2f4520ee12 docs(D.2b): mark D.2b + D.4 shipped (Spec 1 — markup engine + retail vitals)
Roadmap: D.2b (custom retail-look backend) and D.4 (dat sprites + 9-slice +
DrawSprite) both shipped this session via the Spec-1 work — the UiHost-based
markup engine (MarkupDocument + ControlsIni + IUiRegistry) rendering a
markup-driven retail Vitals panel (8-piece dat chrome + red/gold/blue bars).
Records the direct-RenderSurface decode finding + the confirmed chrome sprite
ids. Remaining D.2b polish (gradient bar sprite, AcFont/D.3, input integration,
LayoutDesc importer, D.5 panels) noted inline.

Full suite green (2413 passed / 0 failed / 3 pre-existing skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:50:42 +02:00
Erik
019350fa31 feat(D.2b): IUiRegistry plugin UI surface + buffered drain into UiHost
Adds the plugin-facing UI registration surface (Task 9, final D.2b task).
Plugins call host.Ui.AddMarkupPanel(path, binding) from Enable(); calls are
buffered in BufferedUiRegistry before the GL window opens, then drained into
UiHost.Root in GameWindow.OnLoad inside the RetailUi block after the first-
party vitals panel. Faulty plugin markup is isolated (try/catch per panel,
logged + skipped). IPluginHost.Ui added; AppPluginHost wired; StubHost in
Core.Tests updated; BufferedUiRegistryTests confirms drain-once semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:46:37 +02:00
Erik
07bf6cbf60 feat(D.2b): MarkupDocument (XML -> UiElement tree); vitals panel from vitals.xml
Implements Task 8 of the D.2b retail-UI plan. MarkupDocument.Build() parses
KSML-style panel markup into a live UiNineSlicePanel subtree, resolving
{Binding} attribute expressions against a supplied object via reflection.
Color format is #AARRGGBB (alpha-first, matching controls.ini). Handles
<panel> root (geometry + optional title label) and <meter> children (fill,
label, bar color). Future element kinds (label, button, image) extend the
switch without touching existing code.

vitals.xml encodes the just-approved vitals panel layout (health red #FFC70D0D,
stamina gold #FFD49E1F, mana blue #FF1F33D9); ships next to the binary via
PreserveNewest csproj rule. GameWindow.cs drops the 35-line hand-built panel
block in favour of a 4-line File.ReadAllText + MarkupDocument.Build call —
identical tree, identical render, now data-driven.

2 new tests (Build_CreatesPanelWithMeterFillLabelAndGeometry,
Build_NullBindingValuesYieldNullFillAndLabel) + 11 total targeted green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:38:07 +02:00
Erik
97bd1d2f09 feat(D.2b): controls.ini stylesheet loader + apply title color
Adds ControlsIni — a minimal flat-INI reader for retail's controls.ini
(#AARRGGBB alpha-first color tokens; case-insensitive section/key lookup;
missing file returns an empty sheet with no throw). Wires the [title]
color token into the vitals panel's UiLabel in GameWindow.OnLoad, with
hardcoded white as the fallback. Visually a no-op (retail's [title] color
is white), but proves the stylesheet plumbing end-to-end (D.2b §7).
Three unit tests cover section parsing, #AARRGGBB decode, and graceful
missing-file handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:31:55 +02:00
Erik
b18403da02 feat(D.2b): wire UiHost + live retail Vitals panel (render-only); retire TS-30
Wires the dormant AcDream.App/UI retained-mode tree into GameWindow under
ACDREAM_RETAIL_UI=1: an 8-piece dat-sprite UiNineSlicePanel framing three
UiMeter vital bars bound to the existing VitalsVM. Render-only (UiHost input not
yet bridged to the InputDispatcher — next sub-phase). Coexists with the ImGui
devtools path; no regression there.

Visually verified against a live retail client: the bars match retail's vitals
structure (three stacked horizontal bars, current/max numbers centered) — so the
earlier "orbs" assumption was wrong (retail vitals ARE bars), and stamina is GOLD
not cyan (the #10F0F0 research note was wrong). UiMeter gains a centered numeric
Label (stub debug font for now). Spec §8 + the markup example corrected to match.

Bookkeeping: retired divergence row TS-30 (flat-rect panels -> real dat chrome)
and added IA-15 (our UiHost/markup engine vs keystone.dll's LayoutDesc tree).

Remaining polish (filed, §15): glassy gradient bar fill sprite + the retail dat
font for the numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:56:57 +02:00
Erik
064ef41ce4 feat(D.2b): UiMeter vital bar + fill-geometry tests
Adds UiMeter, the horizontal vital-bar widget for the D.2b retail-look
UI toolkit. Solid-color fill for Spec 1; the retail orb sprite + scissor
crop path is reserved for a later sub-phase. Five unit tests (1 Fact +
4 Theory) cover half-fill geometry and clamping at -1/0/1/2 fractions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:38:07 +02:00
Erik
0bf790c8bf feat(D.2b): UiNineSlicePanel — 8-piece retail window frame + geometry test
Implements the retail floating-window bevel as a UiPanel subclass using
RetailChromeSprites: 4 tiled edges + 4 stretched corners + tiled center fill,
matching the 8-piece border layout confirmed by the D.2b Step-0 prove-out.
Resolver delegate keeps GL out of unit tests. Geometry verified by
ComputeFrameRects_PlacesCornersEdgesAndCenter (1/1 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:36:11 +02:00
Erik
8e91805206 feat(D.2b): Step-0 chrome sprites confirmed + direct-RenderSurface upload path
Step-0 prove-out result: retail UI chrome sprites are RenderSurface objects
(0x06xxxxxx) that must be decoded DIRECTLY, not via the Surface->SurfaceTexture
chain GetOrUpload uses for world materials (which produced 1x1 magenta/garbage).
Added TextureCache.GetOrUploadRenderSurface(id, out w, out h) — Portal/HighRes
TryGet<RenderSurface> -> DecodeRenderSurface(palette:null) -> upload, separately
cached. This is the path UI chrome + (later) dat fonts use.

Confirmed the universal floating-window bevel is an 8-piece border + center fill:
  center  0x06004CC2 (48x48)
  edges   0x060074BF/C1 (10x5 horiz)  0x060074C0/C2 (5x10 vert)
  corners 0x060074C3..C6 (5x5)
Recorded in RetailChromeSprites.cs (edge/corner->position mapping is a best
guess pending the LayoutDesc 0x21000040 parse; visually confirmed at panel
render). The memory-note ids were right; only the decode path was wrong.

Temporary prove-out harness (added to GameWindow.OnRender) removed. proveout*.log
gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:32:27 +02:00
Erik
66888d2c8e fix(textures): DecodeSolidColor null-safe against null ColorValue
A Base1Solid (or OrigTextureId==0) Surface can carry a null ColorValue;
DecodeSolidColor dereferenced it (color.Alpha) and threw NullReferenceException.
It is called directly from TextureCache.DecodeFromDats, OUTSIDE
DecodeRenderSurface's try/catch, so the NRE crashed the whole client. Surfaced
by the D.2b chrome prove-out feeding UI surface ids. Guard null -> Magenta
(the decoder's existing "undecodable" sentinel). Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:36:07 +02:00
Erik
c9eef1d7cd feat(D.2b): textured-sprite path in TextRenderer + UV-rect DrawSprite
Add uUseTexture==2 (RGBA modulate) branch to ui_text.frag so dat sprites
can be drawn through the existing 2D batcher without touching the font path.

TextRenderer gains _spriteBufs (per-GL-handle List<float>), DrawSprite(), and
a Flush block that issues one draw call per distinct texture with uUseTexture=2.
Also adds DepthMask(false) in the state-save block (restored to true after) to
prevent the transparent-quad pass from writing depth and corrupting the 3D scene
if the UI is flushed mid-frame.

TextureCache gains GetOrUpload(surfaceId, out width, out height) — caches pixel
dimensions alongside the GL handle so UI 9-slice geometry can compute slice UVs
from the source image size without a second decode.

UiRenderContext gains a DrawSprite forwarder that applies the current 2D
translate stack, matching the DrawRect / DrawRectOutline pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:28:29 +02:00
Erik
626d06ebc1 feat(D.2b): RuntimeOptions.RetailUi + AcDir toggles
Adds two startup-time env toggles that Phase D.2b's retail-UI panel
frame will read:
- ACDREAM_RETAIL_UI=1  → opts.RetailUi (bool, default false)
- ACDREAM_AC_DIR=<path> → opts.AcDir   (string?, default null)

Both follow the existing helper conventions (IsExactlyOne / NullIfEmpty).
No call sites broke because the only construction site in RuntimeOptions.cs
already uses named arguments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:25:21 +02:00
Erik
35152248f1 docs(D.2b): implementation plan — retail panel frame + live Vitals
9-task TDD plan against the re-grounded spec, building on the existing
AcDream.App/UI scaffold: RuntimeOptions toggles, textured-sprite path in
TextRenderer (+ frag uUseTexture=2, + TextureCache size overload), Step-0 chrome
prove-out, UiNineSlicePanel + UiMeter widgets, wire UiHost + live Vitals
(render-only) retiring TS-30, controls.ini loader, MarkupDocument (XML ->
UiElement tree), and the IUiRegistry plugin surface. Exact code per step; pure
parsers TDD'd in AcDream.App.Tests, GL/visual bits user-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:21:56 +02:00
Erik
d50023f6d9 docs(D.2b): re-ground spec onto existing AcDream.App/UI scaffold
A direct read of src/AcDream.App/UI/ found a complete (dormant) retained-mode
toolkit the grounding workflow missed: UiRoot (input routing, focus, capture,
drag-drop, tooltip, click detection, world fall-through), UiElement,
UiPanel/UiLabel/UiButton, UiHost (Tick/Draw + WireMouse/WireKeyboard),
UiRenderContext, retail-faithful UiEvent codes. It's never wired into GameWindow,
and UiPanel.cs is the exact file divergence row TS-30 cites.

So the retail UI is this existing UiRoot tree — NOT an IPanelHost/IPanelRenderer
backend. Rewrote the architecture sections: Spec 1 now WIRES the dormant UiHost
and adds only the gaps (DrawSprite + frag uUseTexture=2, UiNineSlicePanel,
UiMeter, MarkupDocument that builds a UiElement subtree, ControlsIni). Input
machinery already exists in UiRoot; deferring it is now about integrating two
input consumers, not a missing contract. Plugin contract becomes a UiElement/
markup subtree added to UiRoot (IUiRegistry on IPluginHost), not IPanel.

Net: strictly less new code, more faithful, retires TS-30 by subclassing the
file it cites. Added §0 documenting the correction + the process lesson
(subsystem-discovery must glob by directory, not by the parent's framing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:13:30 +02:00
Erik
de9229eed5 docs(D.2b): design spec — retail panel frame + live Vitals (Approach C)
Brainstormed design for the D.2b retail-look UI backend: our own KSML-style
markup + controls.ini stylesheet + retained-mode toolkit on Silk.NET (no
embedded browser, zero external deps — Approach C, chosen over Ultralight/CEF
and RmlUi for memory/dep-weight/faithfulness).

Spec 1 scope: an 8-piece dat-sprite window frame + live Vitals bars bound to
the existing VitalsVM, gated behind ACDREAM_RETAIL_UI=1, rendered via a reused
TextRenderer batch. Render-only (input/hit-test, AcFont glyphs, anchor solver,
LayoutDesc importer all deferred).

Grounded by a read-only research workflow (7 readers + gap-critic). The critic
corrected several stale memory/plan-doc facts now baked into the spec's
do-not-trust list: VitalsVM is a sealed class (not the old record); chrome
sprite IDs are unverified (Step-0 dat prove-out resolves them empirically);
controls.ini exists and #FFDBD6A8 is editbox text not a bg; DatCollection reads
are thread-safe; KSML is rich-text not the layout language (we mirror
ElementDesc).

Phase D.2b / Milestone M5 (parallelizable with M3/M4 — opened as a parallel
track while M1.5 stays the active critical-path milestone). Retires divergence
row TS-30 + adds one IA row when the chrome ships.

Also gitignores the /.superpowers/ visual-companion scratch dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:00:14 +02:00
Erik
f6a30f4aae handoff: doorway artifacts #130/#129 + #113 re-check + UN-2 desk work (queue, leads, apparatus, pickup prompt) 2026-06-12 12:51:32 +02:00
Erik
c007f5a962 CLAUDE.md: adopt the condensed structure, updated to current truth (-503/+176)
Reconciles the wip/main-local-claudemd-condensation distillation (made
~2026-06-03 by a parallel session) onto post-merge main:

- Current state section: ONE status block (<=5 lines + pointers, by
  rule) + canonical reading order + the two digest entry points + the
  divergence register; replaces status sediment scattered through
  Goal/Roadmap sections.
- Kept from the merged main (the condensation predated them): the
  memory/digest rule in How to operate, the divergence-register
  section + phase-checklist item, de-dated milestone rules.
- Dropped: shipped-phase ship-notes, stale next-phase candidate lists,
  the superseded reference_render_pipeline_state pointer.
- Also salvaged from the wip branch: .gitignore entries (.obsidian/,
  claude-memory junction) + pdb_extract.py __main__ guard. The wip's
  TextureDump edit predates main's args support (discarded) and its
  physics-probe edits were STRIP-marked leftovers (discarded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:42:27 +02:00
Erik
3c3293aebb divergence register -> docs/architecture (living doc) + CLAUDE.md rules: same-commit row discipline, symptom-scan trigger, phase-checklist hook 2026-06-12 12:25:47 +02:00
Erik
ebf61f9eeb retail divergence register: 108 audited rows (14 IA / 27 AD / 31 DA / 30 TS / 6 UN) - deviations found by audit, not playtesting 2026-06-12 12:11:29 +02:00
Erik
0664cba925 #112 CLOSED: threshold tick-skip absorbing state fixed by the retail growing-walk port (user-gated 2026-06-12) 2026-06-12 11:45:41 +02:00
Erik
be03146e30 #112 ROOT CAUSE: outdoor-seed pick lacked retail's growing-array walk - threshold tick-skip became absorbing
The instrumented capture (cottage-112-capture1.log) + dat replay pinned
the transparent-cottage mechanism end to end:

1. The A9B3 cottage's entry cell 0x104 is a 0.22 m-wide THRESHOLD band
   (x 184.68->184.46 at y~82). A running player (~13-16 cm/tick at
   30 Hz) can cross it BETWEEN two physics ticks - the tick where the
   centre is inside 0x104 never happens.
2. Our outdoor-seed branch ran CheckBuildingTransit over a landcell
   snapshot and STOPPED - building-admitted entry cells were never
   expanded. The tick after the skip (centre in 0x100, a deep room not
   building-portal-adjacent) found no containing candidate -> the pick
   kept the outdoor landcell FOREVER (absorbing): the user walked the
   whole interior classified outdoor (render faithfully drew an outdoor
   frame = transparent walls), promoting only on touching
   portal-adjacent 0x102's own volume minutes later (captured:
   0xA9B3003C -> 0xA9B30102 with no transitions in between).
3. Retail cannot strand: CObjCell::find_cell_list (0x0052b4e0) runs ONE
   growing-array walk for EVERY seed (0052b576-0052b5ab,
   cells[i]->find_transit_cells vtable dispatch over the GROWING array)
   - the landcell's building bridge admits 0x104 (the foot sphere still
   overlaps the band one tick after the skip) and the walk expands
   0x104's portals to 0x100 where containment wins. Recovery fires one
   tick after any skip.

Fix: BuildCellSetAndPickContaining now runs retail's single growing
walk for both seeds with per-cell-type dispatch (landcells ->
CLandCell::find_transit_cells 0x00533800 -> CSortCell 0x00534060 ->
check_building_transit 0x0052c5d0; envcells -> FindTransitCellsSphere
with the straddle gate + once-per-walk outside add). The old indoor
branch behavior is preserved (seed at index 0, hysteresis, straddle-
gated outdoor pick); the outdoor branch gains the expansion + the
indoor branch gains the retail landcell bridge dispatch for
straddle-admitted landcells.

Pins (dat-backed, Issue112MembershipTests): tick-skip recovery one tick
past the threshold (RED pre-fix); run-speed entry replay across tick
phases never strands outdoor; threshold-gap outdoor-seed keeps outdoor
(over-fix guard); entry-walk replay diagnostic prints the full
promotion chain (0x3C -> 0x104 -> 0x100 -> 0x103 -> 0x100 -> 0x102).

Suites: App 246+1skip / Core 1438+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:35:52 +02:00
Erik
756ea61e30 file #129 (door/doorway leak through terrain at distance) + #130 (background strip at doorway top edge) 2026-06-12 09:06:02 +02:00
Erik
0b214d673a #119 + #128 CLOSED: tower stairs/barrel resolution chain recorded (user-gated 2026-06-12) 2026-06-12 09:01:27 +02:00
Erik
6a9b529113 #119: entity bounds from dat vertex data - works for every case, not just multi-part
The 1ca412d part-offset expansion fixed the staircase but still rested
on the 5 m promise one level down: a SINGLE part whose mesh extends
more than 5 m from its own origin (offset 0 -> box +-5 m) keeps the
gaze-dependent vanish. Per the user's mandate ("it must work for every
case"), the bound now derives from the dat VERTEX data - the same
vertices that get drawn - so no synthetic containment promise remains.

Oracle context (read this session): retail has NO whole-entity
visibility volume - CPhysicsPart::Draw (0x0050d7a0) viewcone-checks
each part's dat-authored CGfxObj.drawing_sphere at the part's own
world position (RenderDeviceD3D::DrawMesh 0x005a0860). Retail's bound
IS data; ours was a promise. Our per-ENTITY granularity stays (a
deliberate batching-era choice, WB-owned per the inventory) but the
volume is now data-derived and conservative: visually identical by
construction, never culls what retail would draw.

- GfxObjBounds: per-GfxObj vertex AABB, cached by id (parts repeat
  heavily); LocalBoundsAccumulator: union of part-transformed AABB
  corners (conservative-correct under any affine transform).
- WorldEntity.SetLocalBounds + RefreshAabb preferred path: rotate the
  root-local bounds' 8 corners into world axes + DefaultAabbRadius
  margin (absorbs animated-pose drift vs the rest-pose bounds; keeps
  small objects at their historical box size). Offset heuristic stays
  as the fallback for boundless fixtures.
- All four hydration sites wired (outdoor stabs, scenery incl. baked
  scale, interior cell statics, server live spawns).

Tests: tall-single-part coverage (the case 1ca412d could not see),
rotation-following, accumulator union. Suites: App 246+1skip / Core
1434+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:39:05 +02:00
Erik
1ca412d07b #119: entity bounds must cover the parts - the gaze-dependent staircase vanish
User re-gate after 2163308/987313a: run-from-town stairs FIXED, barrel
GONE - but the stairs still vanish by VIEWING ANGLE (visible climbing
down, gone climbing up; same at the tower top). The gate3 probe data
exonerates everything downstream: the entity always draws with correct
batches when it reaches the dispatcher (cache hit:119, restZ correct,
zero WALK-REJECTs, never clip-culled) - so the vanish lives in the one
gaze-dependent gate the probe cannot see: the bounds-based cullers.

WorldEntity.RefreshAabb was a fixed +-5 m box around the entity ANCHOR.
The staircase's 43 parts spiral 15 m ABOVE the anchor, and BOTH
visibility gates derive from the box: the dispatcher's per-entity
frustum cull AND RetailPViewRenderer.EntitySphere (the viewcone sphere
= this box's bounding sphere). Looking up the spiral put the anchor's
neighborhood out of view -> the whole entity culled while 15 m of it
stood in front of the camera; looking down kept the anchor in view ->
visible. Exactly the reported asymmetry.

Fix: expand the box by the largest MeshRef part-translation magnitude
(rotation-invariant, so entity.Rotation needs no handling; identity-
part entities get offset 0 - behavior unchanged; scenery scale is
already baked into the part transforms).

Suites: App 246+1skip / Core 1431+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:58:17 +02:00
Erik
987313aa54 knife-edge port: polyClipFinish W=0 eye-plane clip + degenerate-view propagation; EyeInsidePortalOpening rescue DELETED
Ports retail ACRender::polyClipFinish (0x006b6d00, pc:702749) near-eye
semantics into PortalProjection.ProjectToClip - the fundamental fix for
the in-plane portal clip family (climb strobes, tower-top roof/floor
flap while turning; live-corroborated this session: [viewer-diff]
0xAAB30108 strobing 27x mid-climb, whole interior dropping at the top).
Pseudocode: docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md.

Three legs, all decomp-driven:

1. ProjectToClip clips at w >= 0 EXACTLY (was EyePlaneW=1e-4), with
   retail's any-negative-w gate. Boundary intersections land at w == 0
   (homogeneous directions), so a portal the eye is CROSSING yields the
   correct unbounded half-region that the bounded view-region clip cuts
   to the screen. A w=0 vertex cannot survive a bounded region clip
   into the divide (direction fails some edge of any bounded convex
   region); the measure-zero corner case is guarded non-finite->empty.

2. CellView.CanonicalKey keys ALL-COLLINEAR (zero-area) views as their
   snapped segment ("L:" + extremes) instead of rejecting them - retail
   PROPAGATES degenerate views (ClipPortals decomp:433651-433711
   forwards any count!=0 GetClip output, no area gate anywhere), keeping
   the cell behind an exactly-in-plane portal in the draw list (cells
   draw whole; onward floods die naturally). Rejection dropped the
   whole chain for the frame - the parked-eye knife-edge band. Finite
   key space unchanged -> dedup + strict-growth convergence intact.

3. The EyeInsidePortalOpening rescue is DELETED (the T2-documented
   compensation for the 1e-4 divergence) along with EyeStandingPerpDist
   + PointInPoly2D. Empty clip = no flood, period (retail's rule).
   CornerFloodReplay - the gate that REFUTED the previous deletion
   attempt - passes WITHOUT the rescue under the W=0 port.

Harness criterion corrected to retail's rules (it codified the rescue):
cells fully BEHIND the camera are not required (all-behind portals clip
empty in retail); monotone area holds per root regime; the two
manufactured exact-on-plane steps assert root-only (boundary root pick
is ambiguous; the in-plane portal there is ~perpendicular to the gaze =
genuinely off-screen). Build_CollapsedInteriorPortalNearEye test
inverted to pin the retail empty-clip rule (it pinned the rescue).

New pins: eye-crossing portal -> w==0 boundary verts + half-region (not
sliver); gaze-along-plane degenerate view accepted + segment-key dedup;
non-finite guard. Replay harnesses (CornerFloodReplay, Issue120,
TowerAscent, HouseExit, Issue127) all green.

Suites: App 246+1skip / Core 1430+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:44:23 +02:00
Erik
2163308032 #119 ROOT CAUSE: interior-id X-byte collision + player-landblock cache hints = cross-entity batch serving
The decisive probe (3cf6bcc) caught it live in ONE session: a 43-part
staircase entity (src=0x020003F2, healthy MeshRefs tZ=[0.35..15.15])
drew with cache=hit:3 restZero=3 - THREE batches belonging to a 1-part
entity - then under a different hint the correct hit:119. Two
compounding bugs:

1. interiorIdBase = 0x40000000 | (landblockId & 0x00FFFF00) resolved to
   0x40YYFF00 for landblock keys 0xXXYYFFFF - the landblock X byte
   DISCARDED. Every landblock in a map Y-row shared one id space:
   Holtburg town A9B3's 9th interior stab == the AAB3 tower's spiral
   staircase, both 0x40B3FF09. Fixed to 0x40000000|(lbX<<16)|(lbY<<8)
   (the scenery 0x80XXYY## scheme).

2. The Tier-1 classification cache's #53 tuple key (EntityId,
   LandblockHint) was fed the PLAYER's landblock at bucket-draw time
   (RetailPViewRenderer.DrawEntityBucket fabricates its tuple with
   ctx.PlayerLandblockId), so colliding ids from different landblocks
   shared a key: whichever entity classified first under a hint won,
   and the loser wore its batches all session (static fast path never
   re-classifies). Also: bucket-hinted entries were never swept by
   InvalidateLandblock(owner) - stale entries survived owner unload.
   Fixed: ResolveCacheLandblockHint derives the hint from the entity's
   owning cell (ParentCellId landblock, canonical 0xXXYYFFFF), falling
   back to the tuple id for ownerless paths (outdoor stabs/scenery,
   where the tuple IS the owner).

Explains the session-shaped repro exactly: town-login + run to the
tower hydrates/classifies town interiors first -> the tower staircase
cache-hits the town twin's batches (stairs missing/partial + a wrong
object near the floor - the "water barrel"); login-inside classifies
the tower first -> usually clean. meshMissing=0 / entSeen==entDrawn
both ways (everything draws, wrong batches). Likely also feeds #113's
distance-dependent phantom staircase (the town twin wearing the
tower's staircase batches).

3 new cache tests pin the collision contract + hint derivation.
Suites: App green / Core 1430+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:43:45 +02:00
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
d82f070b88 docs: tower-stairs fundamental handoff - the broken-state log kills all mesh-absence theories
The users final broken-state session (user-session-capture2.log,
standing in front of broken stairs) reports meshMissing=0 and
entSeen==entDrawn: the staircase is DRAWN WRONG, not missing. The
handoff records the 8 verified fixes shipped today (none was this bug),
the ranked hypothesis space (H-A hydration-time MeshRef corruption via
SetupMesh.Flatten identity fallback - predicts the barrel IS the
collapsed staircase; H-B Tier-1 partial-batch cache; H-C draw compose),
the decisive one-launch probe design, the polyClipFinish/cdstW port
spec for the climb strobes + top flap (read done, constant pinned), the
apparatus inventory, and the paste-ready pickup prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:46:35 +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
2eca7f5033 docs: #119-residual root cause (render lift in the visibility graph) recorded in ISSUES 2026-06-11 19:27:14 +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
899145e1d7 #119-residual: tower-ascent harness pins the roof-lip flood gap; barrel claim RETRACTED (user axiom: not in retail)
User verdict on the post-#120 build: "Barrel is gone and more stairs
exist" - the #120 fix partially cured the tower, and the earlier
"legit dat barrels on the landings" claim is RETRACTED (USER AXIOM: the
barrel is NOT in the tower in retail; what the user saw was itself a
render artifact of the corrupted floods, and what the 0x020005D8 cell
statics actually render as is unverified - do not assume barrel).

Remaining tower bugs, both PINNED by TowerAscentReplayTests (the #118
exit-walk pattern, vertical - a helix ascent with the gaze locked ON
the staircase, so a cull has no gaze excuse):
- steps 195-201 (eye z 126.9-127.3, the roof-lip band between the main
  cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve
  OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 =
  the outdoor node alone): the eye is above every side aperture's useful
  view and ON/INSIDE the roof aperture's plane, so BuildFromExterior's
  seed side-test / in-plane reject refuses every exit portal. The tower
  interior never floods -> the staircase (a 0x0107 static) cone-culls
  while staying walkable (user symptom 1), and the roof-lip cell
  geometry flaps as the live eye bobs across the band's edges (user
  symptom 2). One mechanism, both symptoms.
- The pin is committed as a SKIPPED red test
  (TowerAscent_StaircaseStaysConeVisible_EveryStep; the skip reason
  carries the defect) so the suite stays green - un-skip with the fix.
- TowerAscent_RootDoesNotPingPong + the per-step diagnostic stay active.

Fix direction (oracle-first, next): determine which side diverges from
retail - (a) viewer-cell resolution (retail curr_cell may keep the eye
INTERIOR through the band: keep-curr above open-top cells / cell BSP
classifying the parapet bowl as inside 0x010A, where our resolution
demotes to outdoor), or (b) exterior seed admission (retail
ConstructView(CBldPortal) Sidedness with an in-plane eye). Grep the
named decomp for both before touching either layer.

Suites: App 238 + 1 skip (236+3 new, 1 pinned), Core 1419+2skip,
UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:34:45 +02:00
Erik
0c55b473dd docs: #125 root-cause-fixed status + #119 decoded/likely-fixed-by-#120 ledger update 2026-06-11 18:20:36 +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
1b8c9f1f50 #119: tower decoded - the "missing stairs" are Setup 0x020003F2 (43-part spiral staircase); the "barrel" is a legit dat static orphaned by it
The user stood in the tower and logged out; the next login [snap] pinned
it: cell 0xAAB30107, AAB3 building[1] (model 0x01001117, NOT the #113
meeting hall 0x010014C3). Issue119TowerDumpTests decodes the dat truth:

- The tower interior cell 0x0107 has ZERO ramp polys - the stairs are
  NOT cell geometry. They are cell STATICS: Setup 0x020003F2 at the
  exact tower center = a 43-part spiral staircase (5 corner platforms
  0x01000E2A + 38 steps 0x01000E2B/2C/2D/2F/31/32, placement frames
  spiraling z 0.35..15.15, all parts fully drawable - 0 NoPos polys).
- The four 0x020005D8 statics (1 part, 0x01001774, 24 polys - barrel-
  shaped) sit along the wall at ascending heights: legitimate dat
  barrels on the stair landings. With the staircase missing, the
  bottom one reads as "a barrel in the middle where it's not supposed
  to be" - the barrel is NOT extraneous, the stairs around it are gone.

Pipeline reads (all correct by read, no errors logged):
BuildInteriorEntitiesForStreaming flattens Setup statics per part
(SetupMesh.Flatten -> 43 MeshRefs with placement transforms);
LandblockSpawnAdapter registers per MeshRef GfxObj id; the dispatcher
walks per MeshRef and composes PartTransform * entityWorld; the
ConsoleErrorLogger (wb-error) is wired and silent. Remaining suspects
are runtime-state: the #55 meshMissing population (parts never
finishing PrepareMeshDataAsync) or a draw-level drop - the saved
character now spawns INSIDE the tower, so a WB_DIAG launch at spawn
reads the dispatcher counters directly on the exact content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:59:52 +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
acaaeae434 docs: session handoff - T6/BR-7 shipped + T5 verdict + post-T5 state, with the next-session prompt
docs/research/2026-06-11-t6-br7-shipped-t5-gate-post-t5-handoff.md:
the 9-commit ledger with decomp anchors, the per-cell shadow
architecture summary, the T5 gate verdict (collision half 100% passed;
#117-#120 filed), the #117 fix detail (depth-gated punch - re-gate
pending), the #118 narrowing + exit-walk harness design, the #119
up-null lead, the #120 armed tripwire, watchouts/DO-NOT-RETRY
additions, new apparatus inventory, the next-session work order, and
the paste-ready prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:17:41 +02:00
Erik
0e6e24faf6 docs: #118 narrowed - partition + per-tick ParentCellId exonerated; decision-stack candidates + exit-walk harness design recorded
Read-level exonerations: the local player routes to Dynamics correctly
(ServerGuid set), and its entity ParentCellId syncs per tick from the
controller - neither is the vanish mechanism. Live candidates are the
doorway-crossing decision stack: (a) eye/cell incoherence under camera
damping (the verified #115/BR-8a divergence - we damp from our own
damped eye while the root comes from the swept cell), (b) the
exit-portal side test culling the OutsideView when the eye is
epsilon-outside while the root is still interior (retail's
AdjustPosition demotes the viewer cell the same moment), (c) the
aperture-cone tightness for an outdoor player with an indoor viewer.
Next step is the deterministic exit-walk harness (all-CPU drive of the
production decision stack over the corner-building cells); designed in
the issue entry, queued for a focused session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:11:54 +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
af5d424df0 docs: T5 comprehensive gate verdict - PARTIAL PASS; #108/#109/#97 closed, #117-#120 filed
The single comprehensive visual gate (2026-06-11, user-driven):

CONFIRMED (user axioms): doors block both ways incl. off-center (#99
visual pass), cellar descent/ascent clean + #108 grass-sweep GONE, inn
2nd floor clean (#97 CLOSED), interiors stable through doorways incl.
edge-on, #109 far-door oscillation GONE, formerly-popping stairs now
stable at all ranges. The entire T6/BR-7 collision port passed 100%.

REMAINING (render, filed at mechanism level - no live whack-a-mole):
- #117 aperture-shaped see-through: doors/interiors visible through
  terrain hills and through nearer buildings (the far-Z punch erases
  occluder depth at aperture pixels). Decomp direction:
  DrawPortalPolyInternal depth state vs draw order.
- #118 character clipped + vanishes momentarily on house exit
  (viewer-indoor/player-outdoor transition frames; local-player
  partition / aperture-clip suspects).
- #119 old-tower stairs partially invisible + extraneous water barrel
  (pre-existing); T5-log lead: [up-null] 0x010002B4 + 0x010008A8
  cached EMPTY render data, permanently invisible.
- #120 [pv-ERROR] in-place propagation tripwire at depth 128 on the
  cottage interior cells (T2 convergence invariant break, self-detected
  during the gate) - investigate FIRST.

Rain-indoors not verifiable (clear weather).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:46:30 +02:00
Erik
60c10707a3 docs: T6 (BR-7) ship closeout - #99/#90 closed, #97 likely-closed note, #116 filed, plan stamped
- ISSUES.md: #99 DONE (per-cell shadow architecture, dbfbf85+ca4b482;
  visual rides T5); #90 DONE (stickiness workaround removed, retail
  ordered-pick owns doorway hysteresis); #97 likely-closed note (the +5m
  pad producer deleted - verify at T5); #116 filed (slide-response
  family: tick-22760 lateral-slide loss + BSPStepUp D4 first-frame
  behavior, both pre-dating BR-7, oracle-first fix shape).
- Port plan: execution stamp updated to BR-2..BR-7 ALL CODE-COMPLETE
  with the per-commit map; BR-7 section marked code-complete; remaining
  = T5, the single comprehensive user visual gate.
- Memory digests (physics + render + MEMORY.md index) updated in the
  same session per the digest protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:49:18 +02:00