unattached=0 in the last capture refuted the unattached hypothesis (the fix-1 pass is vacuous); the swirl outdoors rides a MATCHED attached emitter, so its owner is an OutdoorStatic keyed by a synthetic id. The matched-ids dump on an inside-vs-outside capture pair names the owner: the id that flips. Issue131SetupProbeTests dumps the outstage candidate setups from the dat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User gate on 20d1730: the candle is FIXED indoors ("now the candle
light is visible when I'm in the house when it is in front of the
opening") and the OUTDOOR sibling surfaced exactly as AP-34 recorded
("when I go out it is not showing unless I turn so the angle doesn't
put it in front of the opening"): under an OUTDOOR root the merged
building interiors draw AFTER the landscape stage (DrawEnvCellShells),
so a slice-drawn flame is overpainted by a punched aperture's interior
behind it.
Fix: outdoor roots SKIP the late-slice Scene-particle draw; attached
outdoor-static scene emitters draw in the POST-FRAME pass alongside the
T3 unattached pass, where depth is complete and flames composite
correctly against interiors. The owner-id set carries over from the
late slice (single full-screen slice outdoors); cell-pass and
dynamics-pass emitters keep their own passes (their owners are never in
the outdoor-static id set - no double-draw). Interior roots keep the
late-slice draw (their stage ends with the clear + seal discipline).
AP-34 row updated (the outdoor residual is now covered; the remaining
residual is translucent MESH batches within stage draw calls).
Portal swirl (#131): the user's "same results" on 20d1730 KILLS the
look-in-erasure hypothesis for the portal - the mesh now draws after
the look-ins and is still missing indoors. No further speculative fix;
the [outstage] probe now prints each outside-stage dynamic's
SourceGfxObjOrSetupId (portals have distinctive setups) and
[outstage-pt] lists up to 12 distinct UNMATCHED attached emitter owner
ids - the next capture identifies whether the portal entity reaches the
through-door draw at all, and where its emitters point.
Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's screenshot pair re-attributed both reports to ONE mechanism -
a compositing gap in the #124 look-in sub-pass:
- #131: the portal swirl (a TRANSLUCENT MESH, not only particles) stood
exactly in front of the hall's doorway. The slice drew it BEFORE the
look-in sub-pass; translucents write no depth, so the hall's interior
- drawn into its far-Z-punched aperture - overpainted the swirl.
Outdoors the look-ins are the post-stage merge path, so the swirl
survives ("stepping out it pops into existence").
- #132: the candle/lantern flame is an attached emitter in the slice's
Scene-particle pass - same pre-look-in placement, same erasure
whenever "the opening through a house" sat behind it; against a wall
nothing overdraws it. Background-dependence explained exactly.
Retail cannot exhibit this class: every alpha draw of the landscape
stage is collected and flushed ONCE after LScape::draw
(D3DPolyRender::FlushAlphaList, PView::DrawCells pc:432722) - i.e.
after all building look-ins.
Port (the two-phase split): DrawLandscapeThroughOutsideView now runs
EARLY per slice (sky, terrain, outdoor STATIC meshes - the look-in
punches need their depth to mark against, the #117 lesson), then the
#124 look-ins, then LATE per slice (outside-stage dynamics' meshes +
ALL attached scene particles + weather + SkyPostScene), then the #131
unattached pass. New RetailPViewLandscapeLateSliceContext carries the
dynamics survivors + the particle-owner set (statics + dynamics cone
survivors). GameWindow's slice handler split accordingly. Outdoor
roots: no look-ins live in the stage, so the net order is unchanged
(zero behavior change outdoors).
Register: AP-34 added - the two-phase split vs retail's single
deferred flush, with the residuals recorded (outdoor-root slice
particles still draw before merged building interiors - the unreported
outdoor sibling; building exteriors' own translucent batches draw
early).
The earlier #131 unattached-emitter pass (1d3f9a8) remains - it fixes
an independent hole (that class had NO indoor pass at all) - and now
runs at the end of the late phase.
Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user gate: swirl through the doorway, candle flame with
the opening behind it, far-building interiors (#124).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's capture run + a code read pinned it in one step: every
particle pass under an interior root is id-filtered (the landscape
slice's Scene pass, the per-cell pass, and the dynamics pass all
require AttachedObjectId != 0 plus owner-set membership). An UNATTACHED
emitter - AttachedObjectId == 0: portal swirls, campfires, ground
effects anchored at a position - drew NOWHERE when the viewer root was
interior. The outdoor root has the dedicated T3 pass for exactly this
class (its own comment records that "unattached ones had NO pass on
outdoor-node frames"); the identical hole on interior-root frames was
never plugged. Walking out flips to the outdoor root and the T3 pass
picks the swirl up - "appears when I walk out again", verbatim.
The [outstage] capture corroborated the rest of the chain healthy
under the interior root: outside-stage routing correct, cone PASS for
the portal-family dynamics, 57 attached emitters matched and drawn
through the doorway. Only the unattached class was orphaned.
Fix: RetailPViewDrawContext.DrawUnattachedSceneParticles - invoked ONCE
per interior-root frame at the END of the landscape stage:
- pre-clear, because drawn after the depth clear + seals an outdoor
emitter beyond the door plane z-fails against the seal's door-plane
stamp;
- after the #124 look-in sub-pass, so swirls blend over far-building
interiors;
- once per frame, not per slice - alpha particles must not double-draw
(the #121 lesson);
- mutually exclusive with the outdoor T3 pass by root kind (interior
invokes this; outdoor keeps T3).
Residual (documented in the issue): unattached INDOOR emitters now draw
pre-clear and get overpainted by the room's shells - the same
invisibility they had before this fix; the proper per-emitter cell
classification is a future port.
[outstage-pt] probe extended with the unattached emitter count (the
probe's blind spot was exactly where the bug hid).
Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user gate: the swirl through the doorway. #132 (candle
flame vs through-opening background) remains open - different
mechanism, background-dependent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two user reports from the #124 gate session, both axioms:
- #131: "the portal swirl is missing, when I look out from inside a
house. Appears when I walk out again." Mechanism frame: under an
interior root an outdoor dynamic's particles draw ONLY via the
landscape slice's Scene pass (#118 outside-stage routing; #121
excludes them from the last-pass particle callback) - if any link
fails, the swirl draws nowhere exactly when indoors. Desk-exonerated
already: filter key conventions uniform, the routing predicate
correct, sphere from vertex bounds.
- #132: "I have a candle ... when a wall is behind it it shows, but if
I turn a bit and the opening through a house is behind it candle
light disappears." Background-dependent => per-pixel depth/blend at
the aperture region, not owner culling. Possible overlap with the
#124 look-in sub-pass (new pre-clear content in those pixels) - the
pre-77cef4c check is in the issue.
Apparatus (env-gated, zero cost off): ACDREAM_PROBE_OUTSTAGE=1 ->
[outstage] per-slice outside-stage routing + cone verdict per dynamic
(print-on-change, RetailPViewRenderer) + [outstage-pt] slice
Scene-particle id set + live attached-emitter match count (GameWindow).
One capture standing inside looking at the portal pins which link
breaks.
Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From inside a building, looking out at ANOTHER building with an opening
showed its back walls missing (see-through to the world): per-building
look-in floods only ran for outdoor roots; under an interior root the
far building's interior never flooded.
Decomp anchor (named-retail, this session's read): retail runs the
look-in INSIDE the landscape stage for ANY root - LScape::draw is the
FIRST call of PView::DrawCells' outside-view branch (pc:432719),
strictly BEFORE the depth clear (pc:432732) and the exit-portal seals
(pc:432785). ConstructView(CBldPortal) (0x005a59a0) clips each aperture
via GetClip against the INSTALLED view - the accumulated doorway region
when looked into from inside - and build_draw_portals_only pass 1
far-Z punches ALL apertures before pass 2 floods + draws any interior
cell. The nested DrawCells has an empty outside view (PView ctor
draw_landscape=0): no recursive landscape/clear/seal.
Port:
- GameWindow's per-building gather (frustum pre-gate on
Building.PortalBounds) now runs for interior roots too; the root's
own doorway self-excludes via the seed eye-side test (the eye is on
its interior side).
- PortalVisibilityBuilder.BuildFromExterior/ConstructViewBuilding gain
seedRegion - the installed-view clip: interior-root look-ins seed
against the OutsideView polygons (a building not visible through the
doorway never floods); null = full screen (outdoor roots unchanged).
- RetailPViewRenderer.DrawBuildingLookIns: a landscape-stage sub-pass
(before ClearDepthForInterior + seals) - per building, punch ALL
apertures (new DrawLookInPortalPunch callback, always forceFarZ=true,
closing the ISSUES "forceFarZ keys on root kind, under-punches" gap),
then draw the flooded cells' shells + statics far->near. Look-in
frames are NEVER merged into the main frame: a merged cell would draw
post-clear and z-fail against the root's seal (the old ledger
portShape sketch was wrong on this point).
- Look-in cells join the Prepare + partition set so shells have batches
and statics route to ByCell (consumed only by the sub-pass; the main
cell-object pass iterates the main flood's cells).
Register: AP-33 added in the same commit - look-in statics draw WHOLE
(no per-part viewcone; over-include is the safe direction) and look-in
DYNAMICS are deferred (an NPC inside a far building stays invisible -
retail draws objects per overlapped cell in the landscape stage).
Pins: Issue124LookInSeedRegionTests on the real corner-building door -
a seed region containing the aperture floods (and never more than the
full-screen seed), a disjoint region floods NOTHING, and an
interior-side eye never seeds its own exit portal.
Suites: App 259+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user gate: far-building interiors visible through their
apertures from inside; #130 re-gate (top-edge strip) rides the same
launch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's re-gate refuted the scissor fix as THE strip (6c4b6d6 was a
real but sub-pixel under-coverage): the strip survived, screenshot at a
doorway, full width of the opening, top edge only, "very subtle".
Root cause (pinned by Issue130DoorwayStripTests.UnliftedGate_*): the
+0.02 m shell render lift. Cell shells DRAW 2 cm above the dat origin
(z-fight vs coplanar terrain); f35cb8b (the #119-residual fix,
2026-06-11) deliberately reverted the VISIBILITY graph to the physics
(unlifted) transform - but the OutsideView color gate (terrain/sky/
scissor through the doorway) and the seal/punch depth fans are
DRAW-space consumers and kept projecting the unlifted polygons. The
drawn lintel therefore sits one lift-projection above the gate's top
edge - measured 6.7 px at a 2.4 m doorway - and that band never
receives terrain/sky color while the seal also stamps 2 cm low.
A regression from f35cb8b, NOT from the W=0 clip port (987313a stays
exonerated). Vertical aperture edges are immune (the lift slides them
along themselves) - top edge only, exactly as reported; explains the
"also NOW" timing precisely.
Fix - draw space draws lifted, visibility stays physics (the f35cb8b
invariant, now symmetric):
- PortalVisibilityBuilder.Build gains drawLiftZ: the exit-portal branch
projects the OutsideView region with the lifted transform; flood
admission, side tests, and CellViews are untouched (default 0 keeps
every existing visibility test bit-identical).
- The seal/punch fans (DrawRetailPViewPortalDepthWrite) lift their
world verts to the drawn shell's space.
- One shared constant PortalVisibilityBuilder.ShellDrawLiftZ feeds the
shell registration (GameWindow:5604), the gate, and the fans.
Register: AP-32 ADDED - the +0.02 lift had NO row (a pre-register
deviation the 2026-06-12 sweep missed). The row records the split
invariant both ways: a draw-space consumer that forgets the lift
re-opens the #130 strip; a visibility consumer that picks the lifted
transform re-opens the #119-residual side-cull.
Pins: the lifted gate covers the drawn (lifted) aperture to 0.00 px
across the 147-combo sweep; the unlifted gate shows the 6.7 px strip
(sensitivity proof - if the lift is ever removed, this test says the
drawLiftZ plumbing can go too).
Suites: App 257+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user re-gate at a doorway with the lintel on screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's "thin strip of background color along the TOP outer edge of a
doorway, looking out from inside" is the landscape-slice scissor box, not
the W=0 clip port.
Mechanism (pinned headlessly, Issue130DoorwayStripTests, 147 eye/gaze
combos at the real Holtburg A9B4 0x0170 exit door):
- BeginDoorwayScissor converted the slice NDC AABB to pixels as
Floor(origin) + Ceiling(size). The far edge floor(min)+ceil(max-min)
lands up to ONE PIXEL SHORT of the true top/right edge at unlucky
fractional alignments (captured: top edge y=0.7938 @1080p -> row 968
cut; right edge column 1296 @1920 cut).
- The scissor brackets the ENTIRE landscape slice (sky, terrain, outdoor
statics, weather). The exit-portal SEAL stamps the full raw aperture at
true depth and the shell wall ends at the aperture edge, so the cut row
never receives any color write -> clear color, flickering with eye
movement as the fractional alignment shifts.
- This violated AD-17's own invariant (over-inclusion is safe,
UNDER-inclusion is the bug class). No register change: the fix restores
the row's documented doctrine.
Lead 1 (987313a W=0 clip port regression) REFUTED by the same harness:
the CPU polygon pipeline (ProjectToClip -> ClipToRegion merges ->
ClipPlaneSet planes) is sub-pixel exact against the raw aperture
projection (worst 0.54 px, 0.00 px aligned). For an all-in-front doorway
polygon the port is bit-identical to the old 1e-4 path by construction.
The EyeInsidePortalOpening rescue stays deleted.
Fix: conservative outer bound floor(min)/ceil(max) extracted to
NdcScissorRect.ToPixels (GL-free; containment property proven in the
header comment); BeginDoorwayScissor delegates.
Pins:
- NdcScissorRectTests: center-inside containment across 251 fractional
alignments x 2 framebuffer sizes + both captured regression cases.
- Issue130DoorwayStripTests: production flood + assembler at the real
exit door; asserts the scissor never cuts a plane-admitted fragment
(worstScissorGap 0.00 px post-fix, was 10.8 px capped) and the CPU
pipeline stays sub-pixel exact (canary 1.2 px).
Suites: App 252+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user visual gate at a cottage doorway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
The A6.P4 port, fused into one installment per the BR-2 half-port lesson
(registration and query are co-dependent: flood-registering shells under
the old radial query would re-open #98 through the vestibule).
REGISTRATION (ShadowObjectRegistry rewritten):
- Register/RegisterMultiPart/UpdatePosition compute the cell set via
CellTransit.BuildShadowCellSet (the C2 find_cell_list flood) seeded by
the entity's m_position cell id; the private 24m XY-grid rectangle and
its single-landblock clamp are deleted. Flood spheres follow retail's
CylSphere rule (base point + cyl radius, cap 10; BSP bounding-sphere
fallback - Ghidra 0x0052b9f0). Statics flood with the do_not_load
prune; dynamics (server spawns, isStatic:false) without.
- Keep-when-empty (SetPositionInternal num_cells gate, pc:283540): a
failed flood leaves the previous registration in place.
- RefloodLandblock: streaming-race hook re-runs the flood when a
landblock's cells hydrate (retail init_objects -> recalc_cross_cells,
Ghidra 0x0052b420/0x00515a30); wired at GameWindow's hydration tail.
- GameWindow sites pass the server position's full cell id as the seed
(spawn + UpdatePosition); the five static sites pass ParentCellId.
BUILDING CHANNEL (CSortCell.building shape):
- Building SHELLS are not shadow objects in retail (only caller of
find_building_collisions is CSortCell::find_collisions 0x005340aa;
one building per origin landcell, init_buildings 0x0052fd80 verified
verbatim + ACE cross-ref). IsBuildingShell entities skip the registry;
Transition.FindBuildingCollisions runs the shell part-0 BSP off
cache.GetBuilding(cellId) with bldg_check set around it
(find_building_collisions 0x006b5300), CollidedWithEnvironment on
non-Contact non-OK. BuildingPhysics.ModelId = pre-resolved part-0
GfxObj (0x02 Setups resolved at the CacheBuilding site).
- Placement/ethereal weakening: BSPQuery Path 1 passes center_solid=0
when BldgCheck && HitsInteriorCell (BSPTREE::find_collisions 0x0053a82e
+ placement_insert 0x005399d8) so doorway crossings don't hard-fail
against shell solids. SpherePath gains both retail fields;
HitsInteriorCell is rebuilt at every cell-array build
(build_cell_array reset 0x00509ef2 + find_cell_list/check_building_
transit set sites).
QUERY (retail per-cell order, transitional_insert 0x0050b6f0):
- TransitionalInsert per attempt: env -> building (LandCell only) ->
objects on the PRIMARY cell, then on OK the check_other_cells pass
(env -> building -> objects per OTHER overlapped cell) + the
carried-cell advance - the advance now happens AFTER all per-cell
object passes (the WF1 ordering divergence), with Adjusted/Slid
feeding the retry exactly like retail's OK_TS case.
- FindObjCollisionsInCell = CObjCell::find_obj_collisions (0x0052b750):
iterate ONLY the asked cell's list. DELETED: the radial 9-landblock
sweep, the +5m query pad, the b3ce505 indoor-primary gate, and the
isViewer exemption (the camera is bounded by interior cell-BSP env
collision - retail's own channel; CameraCornerSealReplayTests pins it
against real dat, and the new building-channel camera test pins the
outdoor stop).
TESTS: Core 1416/0/2 (was 1398 + 4 pre-existing #99-era fails + 1 skip),
App 225, UI 420, Net 294 - all green.
- 3 of the 4 #99-era reds flipped green as designed: the door apparatus
(Apparatus_Grounded_50cmOffCenter_FrontApproach_Blocks) and tick-13558
(indoor walkthrough) now assert the door BLOCKS; tick-22760 pins the
outdoor blocking invariant.
- The 4th (BSPStepUp D4) + 22760's lateral-slide delta are NOT cell-set
problems (probes prove the door is found + BSP-only dispatched;
BR-7 left both byte-identical) - filed as issue #116 (slide-response
family), D4 skipped with the issue reference.
- FindEnvCollisionsMultiCellTests migrated to the public entry (the A4
multi-cell halt now lives at the retail call site).
- New registry pins: per-cell query surface, outdoor-footprint-never-
indoor (#98 architectural), door-outdoor-cell membership, reflood.
- CameraCollisionIndoorTests rewritten against the building channel
(the isViewer-exemption pins died with the exemption).
Closes#99 (doors block both ways via registration-time cell membership
+ the straddle-spanning player cell array). #97 likely closed (the +5m
radial pad that produced phantom-collision candidates is gone) - verify
at T5. #98 stays closed ARCHITECTURALLY (outdoor footprints structurally
cannot reach interior cells; the cellar harness stays green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail CEnvCell::check_building_transit (Ghidra 0x0052c5d0) opens with
`if (other_portal_id >= 0)` on the SIGNED sign-extended portal id
(CBldPortal.other_portal_id is int, acclient.h:32098). Our BldPortalInfo
carried the dat reader's raw ushort and CheckBuildingTransit had no gate
at all, so a portal whose dat value is 0xFFFF (-1, "no reciprocal
portal") could admit its interior cell. BN's pseudo-C renders the
comparison unsigned - the sign-extension is Ghidra-proven (BR-7 verified
corrections, wf1-interior-collision.md).
- BldPortalInfo.OtherPortalId: ushort -> short; GameWindow construction
reinterprets the dat ushort via unchecked((short)).
- CheckBuildingTransit: negative-id portals rejected before any sphere
test; new multi-sphere overload matching retail's per-sphere loop
(0052c5fe, first-hit admits) with the hits_interior_cell output
(0052c650) the BR-7 building channel consumes next.
- Tests: negative-id skip vs positive-id admit on a leaf-root CellBSP;
multi-sphere plumbing + zero-sphere no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-gate rule (feedback_render_one_gate) is now structural:
- The per-frame ACME BFS (CellVisibility.ComputeVisibilityFromRoot) is
GONE from the frame. Its only production consumer was the
cameraInsideCell boolean - which is exactly 'viewerRoot is not null'
(the TryGetCell that produced viewerRoot already proves cells are
loaded; ComputeVisibilityFromRoot returned null iff root was null).
A full second visibility computation ran every frame to derive a
boolean we already had. The method + its tests remain as quarantined
non-production code (dual-live-visibility-computations, confirmed).
- The clipRoot==null mini-pipeline is DELETED (legacy-outdoor-branch-
remnant, adjusted-confirmed): the outdoor partition draw, the
Chebyshev look-in gather, the DrawPortal invocation and the dynamics
fallback. clipRoot is null only when NO viewer cell exists (pre-login,
fly/debug cameras, transient gaps) - those frames draw flat through
the dispatcher; every normal outdoor frame is the outdoor node.
- DELETED with it: InteriorRenderer (class file - its only caller was
the legacy branch), RetailPViewRenderer.DrawPortal +
RetailPViewPortalDrawContext (the look-in product; outdoor-root frames
flood buildings via MergeNearbyBuildingFloods inside DrawInside),
the _exteriorPortal*/_outdoorRootNoCells fields.
Per frame there is now exactly ONE visibility computation
(PortalVisibilityBuilder) and ONE render path (DrawInside).
Suites: build green, App 226 green, Core baseline (1398 + 4 pre-existing
#99-era).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports Render::viewconeCheck (Ghidra 0x0054c250): meshes are sphere-CULLED
per portal view, never hard-clipped. NEW ViewconeCuller lifts each
slice's <=8 clip-space half-planes to world-space eye-edge planes (the
view_vertex.plane analog, acclient.h:32483 - one matrix fold: L = VP
rows . P) and tests bounding spheres from the entity's cached AABB (the
dispatcher's own cull bounds source).
Gating now matches retail's shape end to end:
- Per-cell STATICS: sphere vs THEIR CELL's views - the statics-through-
walls fix (the cottage phantom staircase's actual draw path: a static
outside every view of its cell no longer paints through the wall).
- DYNAMICS (last pass): sphere vs their cell's views; outdoor/unresolved
vs the outside views (pass-all under the outdoor root). A dynamic in a
non-flooded room culls HERE - retail never reaches an object whose cell
is not in the draw list; the partition still routes it so the CULL is
what drops it, retail's shape exactly.
- OutdoorStatic (landscape pass): pre-filtered per outside slice; the
per-slice entity gl_ClipDistance routing is DELETED (entities draw
outside the clip bracket; terrain/sky keep their plane clip).
- PARTICLES: the scissor-AABB gate is DELETED; emitters gate through
their cone-surviving owners (candle-flames-through-walls fix).
- WEATHER: gated on the PLAYER being outside (retail is_player_outside -
an indoor player gets no rain even looking out a doorway). Closes
weather-gate-player-vs-viewer.
- UNATTACHED emitters (campfires) get their missing outdoor-root pass
(closes unattached-particles-dropped-outdoors).
Suites: App 226 green (flood gates included), Core baseline unchanged
(1398 + 4 pre-existing #99-era).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail floods a building's interior exactly when its shell DRAWS and an
aperture survives the view: DrawBuilding (Ghidra 0x0059f2a0) -> per-view
viewconeCheck on the shell -> portal-BSP walk -> ConstructView(CBldPortal)
side test + GetClip-vs-view + GetVisible. There is NO distance constant
anywhere on that chain (verifier-confirmed, flood-gate-shape adjusted).
Port:
- GameWindow's outdoor-node gather: per-BUILDING frustum pre-gate on the
aperture bounds (Building.PortalBounds - the tight flood-purposes
equivalent of the shell viewconeCheck), iterating the per-landblock
BuildingRegistries. Replaces the Chebyshev<=1 landblock cell-sweep.
Also the proper fix for the 2026-06-07 'FPS drops when I look out'
problem the Chebyshev hack approximated: dozens of AABB tests instead
of an O(all loaded cells) portal sweep.
- OutdoorBuildingSeedDistance 48f -> infinity (the binary visibility pop
at ~48 m - the confirmed #109 mechanism candidate - is gone; admission
is now the screen clip per portal, retail's GetClip gate).
- The legacy clipRoot==null look-in path keeps its 48 m: it is T4
deletion scope; improving doomed code wastes effort.
Closes the building-flood-seeding-48m-cutoff divergence (culling area,
adjusted-confirmed). Suites: App 226 green (flood gates included).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The complete retail drawing order in one installment (per the amended plan:
every installment is a COMPLETE retail behavior - the half-ported punch of
88be519 is re-landed here WITH the ordering that makes it correct):
static world (sky/terrain/weather/shells/scenery)
-> aperture depth writes (interior SEAL at true depth / outdoor+look-in
PUNCH to far-Z; PortalDepthMaskRenderer, DrawPortalPolyInternal
Ghidra 0x0059bc90)
-> interior cells WHOLE, far-to-near, drawn once (DrawCells Loop 2,
Ghidra 0x005a4840; use_built_mesh pc:427905)
-> per-cell STATIC object lists
-> ALL dynamics LAST (DrawDynamicsLast), depth-tested, never hard-clipped
InteriorEntityPartition: new contract - every ServerGuid != 0 entity goes
to Dynamics regardless of cell (indoor/outdoor/unresolved/hidden); ByCell
carries only dat-baked indoor statics of visible cells; Outdoor renamed
OutdoorStatic. Fixes the audit's livedynamic-invisible-under-interior-roots
divergence as a side effect (live entities are never dropped by the
visibility set; culling is T3's viewcone).
DELETED (retail has no counterpart): the gl_ClipDistance shell chop
(927fd8f enable + 9ce335e outdoor scoping + UseShellClipRouting + the
per-slice shell loop + clipShells param) - retail never clips cell
geometry; aperture exactness = punch/seal + z-buffer + this order. The
old per-slice scissored AABB depth clear is replaced by retail's single
gated full clear (ClearDepthForInterior). The interior-root LiveDynamic
top-up draw and the look-in's dynamics involvement are gone (one last
pass, no double-draws).
Closes at the T5 gate (expected): #114 (chop deleted), the char-eaten-by-
doorway regression (ordering), outdoor interiors-through-doorways (punch);
#108's render half (seal) - its membership half stays re-attributed.
Suites: build green, App 226 green (partition tests rewritten to the T1
contract), Core 1398 + 4 pre-existing #99-era + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Visual gate (2026-06-11) on the seal+punch build, then on the punch-reverted
build, isolated the truth:
- With the punch wired: #108 (cellar grass-sweep) gone BUT the player/NPCs
go transparent by exactly their overlap with any doorway viewed from
outside (the far-Z punch erases the depth of dynamic objects standing in
the aperture, so the interior paints over them).
- With ONLY the punch reverted (seal+full-clear kept): characters render
correctly AND #108 is BACK.
The punch is wired for OUTDOOR roots + the look-in path ONLY; it never runs
on a clean interior (cellar) frame. For it to have suppressed #108, the
cellar-transition frames must render through the OUTDOOR root -> the player
is being classified OUTDOOR mid-cellar (the known #112/#106 cellar
membership ping-pong). So:
- #108 is a MEMBERSHIP bug (render is downstream of membership); the punch
was MASKING it, harmfully. Re-attributed to the membership track.
- The interior-root SEAL addresses a case that is NOT #108 (confirmed: #108
isn't an interior-root frame), so it has no verified visible effect yet.
Per no-workarounds + verify-before-layering: reverted ALL of BR-2's depth
machinery (seal, punch, the per-slice->full-clear swap) to the pre-BR-2
baseline (restored from 6cba950). The phantom-site probe (6cba950) is kept.
PortalDepthMaskRenderer.cs is KEPT as a RESERVED, unwired primitive (it is
verified-correct; the depth discipline will be rebuilt during BR-3 with
dynamics-after-interior ordering, where it can be verified against the
shell-chop deletion).
What survives from this session's execution: BR-1 (already-equivalent,
695eca2) stands. #108 moves to membership. BR-2 to be re-approached under
BR-3 with correct ordering. No net production behavior change vs 6cba950.
Suites: build green, App 226 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror of commit 1's seal: the punch half of retail's invisible portal
depth writes (DrawPortalPolyInternal maxZ1, Ghidra 0x0059bc90;
ConstructView(CBldPortal) mode-1, pc:433827).
Generalized DrawRetailPViewExitPortalSeal -> DrawRetailPViewPortalDepthWrite
with retail's maxZ1/maxZ2 selector (forceFarZ):
- INTERIOR root: forceFarZ=false (SEAL, true depth) - unchanged from c1.
- OUTDOOR root + DrawPortal look-in: forceFarZ=true (PUNCH, far depth) -
erase the terrain depth inside a flooded building's entry aperture so
the interior shows THROUGH the doorway against the nearer front-ground.
Architectural note (divergence from retail ORDER, same RESULT): retail
draws the shell LAST (DrawBuilding Draw(part,1) punch -> draw interior ->
Draw(part,0) shell) so the shell closes everything outside the punch. Our
pipeline draws the building shell FIRST (it is an outdoor WorldEntity in
the landscape pass), so the outside-the-aperture wall occlusion is already
in the depth buffer when interiors draw - we need ONLY the punch for
in-aperture visibility, no shell reorder. The punch is confined to each
door polygon clipped to its slice (NOT a full clear), so it does not
reintroduce the 'cellar paints over everything' hazard that gated the old
outdoor ClearDepthSlice to null.
DrawExitPortalMasks is now wired on the outdoor-root DrawInside context and
the DrawPortal look-in context (both previously null -> no-op).
Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era.
NEEDS VISUAL GATE (batched with BR-3): outdoor interiors-through-doorways
must not bleed; cellar grass-sweep (#108) gone; tower stairs near+far.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the seal half of retail's invisible portal depth writes
(D3DPolyRender::DrawPortalPolyInternal, Ghidra 0x0059bc90; dispatched by
PView::DrawCells loop 1, Ghidra 0x005a4840 pc:432783-432786):
- NEW PortalDepthMaskRenderer: draws a portal polygon as a color-masked
triangle fan, depth-test ALWAYS + depth-write ON, at the polygon's TRUE
projected depth (retail maxZ2 seal) or forced to far-z 0.99999988
(retail maxZ1 punch - the constant from 0x0059bc90's tail; punch wiring
lands in BR-2 commit 2). Where retail software-clips the fan against
the installed view (polyClipFinish), we apply the SAME slice region via
gl_ClipDistance from the slice's <=8 clip-space half-planes. GL state
fully self-contained (set -> draw -> restore, no early-outs).
- DrawExitPortalMasks is now WIRED in production (was a null-callback
no-op since birth): for interior roots, every visible cell's portals
with OtherCellId==0xFFFF get their world-space polygon sealed per view
slice, far-to-near, after the landscape slices.
- ClearDepthSlice (per-slice scissored AABB clear - wrong shape, wrong
scope, no seal after it) is REPLACED by ClearDepthForInterior: ONE
full-buffer depth clear between the outside stage and the interior
stage, gated on any outside slice having drawn (retail's
portalsDrawnCount gate semantics staged as an open question, marked
inline). DepthMask(true) asserted at the clear site (c4df241 lesson).
Outdoor roots: no clear, no seals (interiors must depth-test against
terrain until the commit-2 punch).
Closes the mechanism behind #108 (outdoor grass sweeping across the
upstairs door opening - terrain depth seen through the doorway is now
re-stamped at the door plane so farther interior geometry z-fails inside
the aperture). Visual gate: BR-2/BR-3 batched checklist (cellar doorway
+ cottage wall + tower stairs near/far).
Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era
failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The entry snap set only the controller (physics stood on the grounded floor)
while the renderer kept drawing the entity at the server-restored height -
the user's "spawned 2 meters in the air" screenshot over a fully-correct
interior. The teleport-arrival path already does entity.SetPosition +
ParentCellId (GameWindow.cs:4914); the login path now matches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-run finding (wedge-107-gate-indoor-login.log + dat scan): ACE saved the
cell one room off (claim 0xA9B40172, position inside 0xA9B40171 — adjacent
rooms). FindVisibleChildCell corrects this fine when hydrated (proven by the
new AdjacentRoomClaim regression test), but the live entry committed the raw
claim: IsSpawnCellReady's "any cell struct in the landblock => claim is bogus,
proceed" disambiguator observed the MID-POPULATION state (interiors hydrate in
id order on the background worker; the render-thread predicate read the cache
mid-loop) and opened the gate before the claim and its stab neighbors were
cached. AdjustPosition then saw a null cell struct and silently passed the
claim through; the first movement demoted the player to outdoor inside the
house — the user-visible "transparent interior, see straight through walls"
(render is downstream of membership: an outdoor-classified viewer only sees
the interior through the doorway flood).
Fix: the hold now waits for THE CLAIMED CELL's struct, full stop
(IsSpawnCellReady simplification; HasAnyCellStructInLandblock removed).
Claims that can never hydrate are filtered by GameWindow against the dat's
LandBlockInfo.NumCells range (memoized IsSpawnClaimUnhydratable), and
PhysicsEngine.Resolve carries a loud lost-cell-equivalent safety net: an
indoor claim with NO cell struct AND NO CellSurface floor data demotes to the
outdoor landcell with a [spawn-adjust] line instead of committing raw
(retail GotoLostCell :283418; documented divergence). Partial hydration
(CellSurface present, struct pending) keeps the legacy floor-snap behavior —
HasCellSurface uses the file's masked-low-word norm so bare-id fixtures and
full-id production both resolve.
Baseline restored: Core 1381 (+4 new #107 conformance tests) + 4 pre-existing
#99-era failures + 1 skip; App 223 / UI 420 / Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause (capture resolve-107-login1.jsonl + dat conformance scan): ACE
restored the player with a POISONED (cell, position) pair — cell 0xA9B40162
(one building) with a position inside 0xA9B40171 (a different building 55 m
away). Our entry snap trusted the claim verbatim: the player stood
fake-grounded for minutes (isOnGround passthrough, no contact plane, no
walkable polygon — zero-move resolves short-circuit), the FIRST movement input
ran a real transition, the pick demoted the indoor claim to outdoor
mid-building, and the player fell 2.4 m through the cottage floor onto the
terrain underneath — wedged inside the building shell. The second wedge shape
(flood-fix-gate2.log) was the PortalSpace freeze: the teleport-arrival
detection gated on `differentLandblock || farAway>100m`, an invented
heuristic — ACE's same-landblock short-hop corrections matched neither, so
PortalSpace never exited and movement input stayed frozen all session.
Four legs, all retail-anchored:
1. PhysicsEngine.Resolve (the player snap path: login entry + teleport
arrival) now runs AdjustPosition first — retail SetPositionInternal step 1
(acclient :283892, AdjustPosition :280009): validate/correct the claimed
cell from the foot-sphere center BEFORE any physics. Corrections log one
[spawn-adjust] line.
2. AdjustPosition's previously-deferred indoor seen_outside →
adjust_to_outside sub-fallback (:280037-280046) is completed; CellPhysics
gains the SeenOutside flag (dat EnvCellFlags.SeenOutside) cached in
CacheCellStruct. The camera path does not reach this sub-branch in the
gated scenarios (CameraCornerSealReplayTests green).
3. PortalSpace arrival = ANY player position update (holtburger PlayerTeleport
handler conformant; recenter still only on landblock change). Verified
live: ACE sent a same-lb dist=69.8 correction that the old gate would have
frozen on — it now completes.
4. Outbound wire (cell, position) pairs are now SELF-CONSISTENT: derive the
landblock frame from the resolver's full cell id instead of welding a
position-derived landblock onto its low word — the old composition could
write exactly the poisoned pair shape into ACE's character save. Plus the
#106-gate-2 hold extension: an indoor spawn claim waits for the claimed
cell's hydration (IsSpawnCellReady) so the validation can act — the async
equivalent of retail's synchronous cell load.
Live verification (wedge-107-verify1.log): entry clean; ACE's same-lb teleport
correction completed (old code: permanent freeze); the teleport destination
itself carried ANOTHER poisoned claim (0xA9B40150) which [spawn-adjust]
corrected to 0xA9B40019; player fully controllable, walking across landcells.
3 new dat-backed conformance tests pin the poisoned-pair facts
(Issue107SpawnDiagnosticTests). Baseline: 1380+4 pre-existing #99-era
failures+1 skip / 223 / 420 / 294.
Pending user gate: park indoors, log out gracefully, relaunch — expect a clean
indoor spawn standing on the interior floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ROOT CAUSE (pinned by the [clip-route] probe run + [gl-state] tripwire, one
capture): EnvCellRenderer.RenderModernMDIInternal established the Transparent
pass state (Enable(Blend) + DepthMask(false)) BEFORE the batch pass-filter, so
a cell whose batches are ALL opaque (a plain cottage interior) hit the
totalDraws==0 early-out and returned without ever reaching the end-of-pass
restore. The frame ended with dmask=0 + blend=1; the NEXT frame's
glClear(GL_DEPTH_BUFFER_BIT) silently no-oped (depth clears honor glDepthMask),
and every world fragment — terrain, entities, player, sky — failed GL_LESS
against its own previous-frame depth ghost. Screen = the fog-tinted clear
color. Onset locks to the building-flood merge because that is the first frame
the flooded building shell draws; holds while merged (the leak re-arms every
frame); camera rotation recovers because the cell drops from the flood and the
restore-skipping path stops running.
Capture evidence (flap-cliproute-capture.log): all three draw-input suspects
exonerated — landscape scissor full-screen all run, terrain-UBO/region-SSBO
planes full-screen on both sides of every merge, all 41,373 instances on the
correct repacked slot with cullEnt=0 — while [gl-state] showed frames entering
with dmask=0 blend=1 for exactly the merged stretches (145,238 consecutive
frames in the held window, flipping with each merge boundary at the end-of-run
strobe cycles).
Fix (all paths root-cause, no suppression):
- EnvCellRenderer: move the pass-state establish BELOW the totalDraws==0
early-out so state is only set on a path that always reaches the restore;
hoist the globalVao==0 check (the second leak-shaped early-out) above the
state set.
- GameWindow frame clear: assert DepthMask(true) before glClear — the clear
DEPENDS on the depth write mask, so it sets the state it depends on
(feedback_render_self_contained_gl_state; this is the 4th instance of the
class, in the same function as the 1st).
Very likely the same family as the "parts of the screen flash while running
past cottages" and cottage enter/exit artifacts (every brief merge = a
1-frame no-op depth clear). Visual gate pending.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decisive probe between the two surviving suspects from the 2026-06-09
building-flood-merge handoff (docs/research/2026-06-09-flap-outdoor-fullworld-
building-flood-merge-handoff.md section 1), gated by ACDREAM_PROBE_CLIPROUTE=1,
all print-on-change:
- [clip-route] (RetailPViewRenderer.DrawLandscapeThroughOutsideView): the
outside slice slot + NDC AABB + planes, the CellIdToSlot routing table, the
region-SSBO bytes DECODED at the routed slot, and the terrain-UBO head —
captured after SetTerrainClip + UploadClipFrame + SetClipRouting, i.e.
exactly what the landscape draws consume. Pins/refutes suspect (b) and the
slot-repack half of suspect (a).
- [clip-route-disp] (WbDrawDispatcher.Draw, routed draws only): per-slot
instance histogram exactly as staged for binding=3 plus the count of
entities dropped by ResolveSlotForFrame CULL. Pins/refutes the
instance-routing half of suspect (a).
- [clip-route-scis] (GameWindow.DrawRetailPViewLandscapeSlice): the ACTUAL GL
scissor enable + box read back right after BeginDoorwayScissor — the whole
landscape pass (sky + terrain + outdoor entities + player) draws inside this
box, so a doorway-sized box here IS the full-world kill by construction.
Code-reading findings recorded while building the probe: the landscape pass is
scissored to slice.NdcAabb end-to-end (GameWindow.cs DrawRetailPViewLandscapeSlice),
and ResolveEntitySlot CULLs server entities with null ParentCellId while routing
is active — both now directly observable under the probe.
Throwaway apparatus — strip once §4 ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two probe additions from the §4 outdoor full-world flap investigation
(world turns to fog-tinted clear color at specific outdoor spots;
strobes then holds; camera rotation recovers; forward-walk triggers):
- [pv-input] gains terrZ=/eyeAbove= (terrain height under the camera
eye) — used to REFUTE the buried-eye hypothesis (eyeAbove stayed
+1.6..2.1 m through every held-flap block).
- [gl-state] (ACDREAM_PROBE_GLSTATE=1): snapshots the GL fixed-function
state entering the world passes (depth, blend, cull, scissor + box,
viewport, FBO, color/clip masks, glGetError), printing on change.
Delivered the frame-exact flap-onset marker: the leftover scissor box
flips from full-screen to a drifting 9x21 px doorway footprint at the
exact frame the nearby building flood merges in (pv-input flood 1->5).
Both probes are gated and zero-cost when off. Strip with the rest of
the §4 apparatus when the flap ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-3 never entered player mode: the new spawn-ground entry hold
(6dbbf95) waits for terrain under the spawn, but the K-fix1 streaming
gate skipped streaming entirely until the chase camera first engaged —
which requires player-mode entry. Circular wait; the user sat in the
fly camera with an empty world (the probe log has zero [cell-transit]
lines and no "auto-entered player mode").
K-fix1's intent was narrower than its implementation: suppress the
hardcoded-Holtburg center flash BEFORE LOGIN. The streaming tick even
has a dedicated in-world fly-camera branch (observer from
_lastLivePlayerLandblockId) that the outer gate made unreachable. The
fix gates streaming on pre-login only: once the live session reaches
InWorld, streaming runs (centered on the server-known player landblock
or the fly camera), terrain hydrates, the spawn-ground predicate flips,
and auto-entry fires. The world-geometry RENDER gate is untouched — the
pre-entry screen still shows sky only, exactly K-fix1's visual.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate-2 fallout chain: session 1's bare-id wedge poisoned ACE's save (the
client reported garbage cells while walking through walls), so session 2
logged in with (cell=0xA9B4013F inn interior, pos=(91.4,32.7)) — a
position that cell does not contain. Probe evidence: exactly one
[cell-transit] line all session; the player free-fell into an empty
world. Two holes, both fixed at the root:
1. CellTransit pick escape hatch — restores the #83/A1.7 + #90
verification that lived in PhysicsEngine.ResolveCellId before the
collide-then-pick rewrite moved membership into
BuildCellSetAndPickContaining: an indoor current cell that IS
hydrated but whose CellBSP no longer overlaps ANY part of the foot
sphere is a bogus claim (corrupt save, or walked out through an
unblocked gap). The portal BFS can never reach an exit portal from a
cell the sphere isn't in, so no candidates exist and the claim held
forever — wedging collision (ShadowObjectRegistry's #98 gate reads
"indoor primary" -> outdoor object sweep skipped), wall BSP, terrain,
and the render root. The pick now demotes to the outdoor column under
the sphere centre (the LandDefs.AdjustToOutside result already
computed for the pick — cross-block safe). Sphere-overlap
(BSPQuery.SphereIntersectsCellBsp, pseudo_c:317666 -> :323267), NOT
point-in: doorway push-back leaves the centre a few cm outside while
the sphere still overlaps — no demotion, #90's ping-pong stays dead.
An unhydrated cell cannot be verified — stale beats null while
streaming hydrates (retail-equivalent: stale curr_cell kept when the
pick finds nothing).
2. PlayerModeAutoEntry spawn-ground hold — player-mode entry now waits
for the terrain under the spawn position to stream in
(isSpawnGroundReady predicate, K.2 pattern). Entering earlier
integrates gravity against an empty world: indoor-claimed spawns got
no floor from any source and free-fell into the void; outdoor spawns
raced hydration by ~1s every login. Retail never has this state (it
loads cells synchronously) — the hold is the async-streaming
equivalent of that invariant. With the hold, the entry snap
(Resolve, stepUp=100) runs against hydrated cell floors + terrain
and re-seats a corrupt save's claim immediately.
Tests: IndoorSeed_SphereFullyOutsideHydratedCell_DemotesToOutdoorColumn
(the gate-2 wedge shape, red pre-fix), straddle + no-BSP guards (the #90
hysteresis and stale-beats-null), TryEnter_Armed_SpawnGroundNotReady_
DoesNotFire. Full suite: 294+218+420 green; Core 1375 green + the same
4 pre-existing door/#99-era failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Replace the outdoor root's single unified reverse-portal flood (whose root-level
portal-side test oscillated as the chase eye grazed a doorway — the measured
flood 2<->6) with retail's per-building floods.
- OutdoorCellNode.Build(uint): portal-less land root; floods only itself ->
full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed).
- PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a
building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via
DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded ->
consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4).
- RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby
cells by BuildingId and merge each per-building flood into the frame before
assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff.
- GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames.
Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership
stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject,
the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView
coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14.
Conformance-verified sound; the grazing-doorway flap is the visual acceptance test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ReferenceEquals(clipRoot, _outdoorNode) object-identity checks with the
documented LoadedCell.IsOutdoorNode flag (4 sites) so they survive R-A2 changing
the outdoor root's portals. Behavior-preserving (build + targeted suites green:
App PortalVisibilityBuilderTests 24/24, Core PlayerMovementControllerTests 14/14).
Right-sized from the planned 'collapse to one root': reading the live dispatch,
the viewerRoot ?? outdoorRoot split is already correct (viewerRoot feeds
cameraInsideCell/lighting via the older CellVisibility BFS; clipRoot is the render
root), and the 2026-06-07 cutover flip already made in-world frames single-path
DrawInside. The real flap fix is R-A2 (per-building floods). Dead exterior
DrawPortal look-in deletion deferred to R-A3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2026-06-08 AM "physics rest micro-jitter" diagnosis is refuted with primary
evidence (door-recheck 216K standstill records: 0 position re-snaps; player
byte-stable during the flap). Two adversarial verification sub-agents confirmed:
- Retail roots the render at the camera viewer_cell (swept from the player via
SmartBox::update_viewer 0x453ce0; DrawInside(viewer_cell) 0x453aa0) and toggles
DrawInside / LScape::draw -- so acdream's eye-cell rooting + inside/outside
toggle are RETAIL-FAITHFUL. The locked-design "root at player cell" is wrong.
- The flap is render membership instability, eye-motion-driven: the visible-cell
set oscillates (8<->3) as the eye sweeps monotonically. Root = the
re-enqueue-on-growth DRIFT (PortalVisibilityBuilder.cs:322, MaxReprocessPerCell
=16) re-clipping each grown cell every round -> sub-cm eye jitter flips membership.
Fix (spec, not yet implemented): verbatim port of retail's enqueue-once flood
(ConstructView + AddViewToPortals): enqueue once on first discovery, clip each
cell's portals once, union late growth in place (AddToCell) + draw-reorder
(FixCellList), never re-enqueue. Kills the drift; rooting/camera/seal untouched.
This commit lands VERIFIED GROUNDWORK + design only:
- spec: docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md
- findings: docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md
- [pv-input] probe gains rawPlayer + yaw (disambiguates the varying input)
- 4 GREEN physics rest-stability tests (prove rest is bit-stable -> flap not physics)
- apparatus: launch-flap-capture.ps1, analyze_flap_live.py, find_burst.py
- captured fixtures: tests/.../Fixtures/flap-doorway/0xA9B4017{0..5}.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cutover-flip follow-up (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>
The CUTOVER FLIP that kills the indoor FLAP. Replace the two-branch gate (clipRoot = playerIndoorGate && viewerRoot ? viewerRoot : null) with clipRoot = viewerRoot ?? _outdoorNode, so EVERY frame routes through the one RetailPViewRenderer.DrawInside path rooted at the viewer cell — interior EnvCell when the eye is indoors, the synthetic outdoor node when outdoors. There is no inside/outside branch to toggle as the 3rd-person eye crosses the doorway, so the flap (textures battling at every transition) dies by construction. Matches retail SmartBox::RenderNormalMode -> DrawInside(viewer_cell) (0x453aa0 -> 0x5a5860).
clipRoot is null only pre-spawn/login (viewerCellId==0 -> _outdoorNode null), so the outdoor LScape block still runs as the safety path and login keeps its live sky. playerIndoorGate stays computed for the [render-sig] probe.
Preserve the LiveDynamic entity draw (server entities with no resolved ParentCellId — the transient unpositioned case) for the outdoor-node root: the old outdoor branch drew it; DrawInside does not, so re-issue it after DrawInside to keep the spec section 10 byte-identical-outdoor guarantee (no live entity blinks out).
The old outdoor else block + DrawPortal/BuildFromExterior are now dead when clipRoot is non-null but are LEFT IN PLACE for the user visual gate (handoff section 4 Step D deletes them only after the user confirms). App 216/0, build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds the synthetic outdoor cell node (OutdoorCellNode.Build) every outdoor frame
from the nearby building-entrance portals (Chebyshev <=1 landblocks), stored in
_outdoorNode. NOT yet rooted — clipRoot/viewerRoot unchanged, so behaviour is
identical this commit. [outdoor-node] probe (ACDREAM_PROBE_FLAP) reports the live
portal count so the next (cutover) step can confirm real building entrances were
found before flipping the render root. App.Tests 214/214, build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.
- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
(load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).
Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.
Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail SmartBox::RenderNormalMode (0x453aa0:92665) branches DrawInside vs the
outdoor LScape::draw on is_player_outside (the PLAYER's cell, 0x451e80), then
roots DrawInside at the VIEWER cell. acdream keyed the whole branch off the
camera cell (clipRoot = visibility.CameraCell), so a 3rd-person chase camera
lagging in a doorway AFTER the player stepped outside took the DrawInside path
rooted at the threshold cell, where the exit-portal flood degenerates: grey
world + entities-through-walls. Now ShouldRenderIndoor(playerCellId,
viewerCellResolved) gates the branch on the player; the DrawInside root stays
the viewer cell (handoff invariant preserved).
SCOPE / HONESTY: this REDUCES the player-OUTSIDE doorway grey (visual-confirmed
reduced a lot) but does NOT fix the deeper symptom: when the player is in one
interior cell (cellar 0174) and the camera is in another (room 0171), the flood
roots at the camera cell and does NOT seal the player's cell, so the cellar
floor / interior walls drop to grey. That is the KNOWN R1-completion problem
(2026-06-05 Residual A handoff + 2026-06-02 design doc section 3: a
SHELL-SEALING / wrong-flood-root bug), not this branch.
Tests: Core 1331p / 4f (documented) / 1s, App 187p, build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase W single-viewpoint V1 (un-split). The render mode decision, indoor root, and portal
side-test now key on the collided-camera viewer cell + eye (RetailChaseCamera.ViewerCellId +
camPos) — retail RenderNormalMode -> DrawInside(viewer_cell) @92675; InitCell side-test vs
viewer.viewpoint @432991. Lighting / seen_outside / playerInsideCell stay on the PLAYER cell
(CurrCell), retail CellManager::ChangePosition @4559B0. The old per-render player-root +
eye-projection split (U.4c) is removed; the flap is avoided by the robust graph-tracked viewer
cell (no AABB, no grace). [flap-cam] probe extended with viewerCell vs playerCell. CurrCell
stays player-only (blue-hole fix intact). App 176 green; Core 1295/5 baseline (no new fails).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EntityPassesVisibleCellGate no longer returns true unconditionally for outdoor
scenery under a cell filter (was the headline #78 bleed). Outdoor scenery now
draws only via the unfiltered bucket (visibleCellIds: null) + ResolveEntitySlot's
OutsideView routing. The outdoor-root global Draw passes visibleCellIds: null
(no portal-cell scoping outdoors; retires VisibleCellIds as a render gate — peering
into buildings is R5). Updated the EntityClipTests case that pinned the old bypass
(Included -> Excluded). 174/174 App tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GameWindow.OnRender: when clipRoot != null, run only InteriorRenderer.DrawInside
(per-cell shells + per-cell objects + live-dynamics); the global entity pass +
global shell pass are no longer issued indoors. Outdoor scenery drawn clipped to
the doorway (after terrain, before the Z-clear). Outdoor root path unchanged.
pvFrame hoisted so the splice reads OrderedVisibleCells; per-frame 3-bucket
partition built on the indoor root. Retail RenderNormalMode @ 0x453aa0.
InteriorRenderer amended with a DrawableCells membership filter (an IsNothingVisible
cell can be in OrderedVisibleCells but absent from CellIdToSlot — iterate for ORDER,
filter for membership; matches the old envCellShellFilter set exactly).
Build green, 174/174 App tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opus adversarial review caught a real gap: the sky/weather MESH bled full-screen indoors in TerrainClipMode.Scissor (a multi-exit interior, or an OutsideView with >8 edges). The assembler only sets the binding=2 clip-plane UBO in Planes mode; in Scissor mode it leaves count==0, so sky.vert's gl_ClipDistance writes all +1 (no clip) and the mesh draws — which had NO scissor wrapper, only the no-op planes — covered the whole screen. The terrain and particle passes were already scissored; the sky/weather mesh was the one unguarded path.
Fix: scissor the WHOLE sky pre-scene + weather post-scene blocks (mesh + particles) to the OutsideView AABB when indoors. In Planes mode the scissor is a harmless over-include (the per-vertex clip planes are tighter and do the exact doorway clip); in Scissor mode it is the sole confinement, mirroring the terrain Scissor path; outdoors it is skipped (full-screen, bit-identical). Also hoisted the scissor-disable out of the particle null-check (cleaner, leak-free on the no-particle path) and corrected a stale 'weather does not write gl_ClipDistance' comment at the world-bracket close.
The single-convex-doorway case (Holtburg cottage) was already correct (Planes mode); this seals the multi-opening case. Build 0/0; App tests 171/171.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sky + weather (rain cylinder) are retail's LScape — 'the outside seen through the exit portal.' Retail PView::DrawCells (pseudo_c:432709) draws LScape clipped to the OutsideView when outside_view.view_count>0, then does a conditional Z-buffer-ONLY clear (432731) before the indoor cells. acdream now does the same:
- sky.vert writes gl_ClipDistance against the SAME binding=2 TerrainClip UBO the terrain reads. The OutsideView planes are screen-space (NDC) half-spaces encoded as clip-space planes (nx,ny,0,dw); the test dot(plane,gl_Position)>=0 reduces after perspective divide to nx*ndcX+ny*ndcY+dw>=0 — projection-INDEPENDENT — so the same plane set clips the sky EXACTLY despite its separate dome projection. count==0 (outdoor) → all distances +1 → full-screen, bit-identical. Lighting/fog math untouched.
- GameWindow: relocated the sky pre-scene + weather post-scene draws to their retail LScape positions, each in a local 8-plane clip bracket so sky.vert confines them to the doorway indoors / full-screen outdoors. Added the conditional doorway depth-ONLY Z-clear (no color → no blue hole), scissored to the OutsideView AABB. drawSkyThisFrame = seen_outside policy AND (outdoor OR exit-portal-in-view) — a sealed interior with no exit portal in view draws no sky (kills the full-screen-sky interim regression). Sky pre/post particle passes (particle.vert has no gl_ClipDistance) scissored to the doorway bbox.
- ClipFrameAssembly gains HasOutsideView + OutsideViewNdcAabb (the doorway NDC AABB, computed for BOTH Planes and Scissor terrain modes — unlike TerrainScissorNdcAabb which is Scissor-only).
- The pre-login goto SkipWorldGeometry moved BELOW the sky draw so the live sky still renders during the EnterWorld handshake (clipAssembly is null/no-clip pre-login → full-screen).
Build green; App tests 160/160. Stage 4 tests + verify-annotations follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the BFS visibility root to DataCache.CellGraph.CurrCell (the physics
membership answer written in W2 Task 1) rather than resolving independently
from a position via FindCameraCell. Closes the render/physics disagreement
that causes the "world from below" spawn-in flicker.
Changes:
- CellVisibility.GetVisibleCells: extracted BFS body into new private
GetVisibleCellsFromRoot(LoadedCell root, Vector3 cameraPos); existing
GetVisibleCells delegates to it after FindCameraCell (behavior unchanged).
- CellVisibility.ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos):
new public entry point; when root is null falls through to ComputeVisibility
(exact today's behavior), otherwise sets _lastCameraCell = root and delegates
to GetVisibleCellsFromRoot — cannot regress below baseline.
- GameWindow (line 7156): replaced ComputeVisibility(visRootPos) with
ComputeVisibilityFromRoot(physicsRoot, visRootPos) where physicsRoot is
resolved from _physicsEngine.DataCache.CellGraph.CurrCell via TryGetCell.
physicsRoot is null whenever CurrCell is null or its id is not yet in the
render registry, so the fallback fires until the cell loads.
- 6 new tests in CellVisibilityFromRootTests: null-root fallback equivalence
(3 cases), registered root → CameraCell == root (3 cases). All 160 App.Tests
pass, 0 regressions.
Visual verification PENDING — behavior change; do not claim it works visually.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>