docs: cutover flip shipped — see-through + oscillation DIAGNOSED (evidence-based handoff)

The flip killed the branch-toggle flap (one path, zero OutdoorRoot frames). It exposed two residuals now PROVEN via a live [bshell] probe, not guessed: (1) oscillation = the outdoor-node flood membership swings 1<->~13 building cells frame-to-frame, so the walls (EnvCell shells) blink; (2) see-through = EnvCell wall polys are single-sided for SidesType==CounterClockwise, so from outside you see their culled back. The ModelId building shells DO render (6/6 with mesh) but are a partial frame, not the walls — the skip-all-interiors experiment proved the walls are the EnvCell shells. Fixes identified (stabilise flood + build back faces) but not implemented; full do-not-retry list + open pre-flip-reconciliation question in the doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-07 21:49:00 +02:00
parent 774cb22713
commit ef2186147d

View file

@ -0,0 +1,125 @@
# Handoff — Cutover FLIP shipped; see-through + oscillation DIAGNOSED (evidence-based) — 2026-06-07 (PM)
> **CANONICAL PICKUP for the render-unification residuals.** Worktree `thirsty-goldberg-51bb9b`,
> branch `claude/thirsty-goldberg-51bb9b`, HEAD `774cb22`. The cutover flip is SHIPPED (one render
> path, no branch-toggle flap). It exposed two residuals — **see-through building walls** and
> **oscillation** — whose root causes are now PROVEN with a live probe (not guessed). The fixes are
> identified but NOT yet implemented. Read §3 (diagnosis) and §5 (do-not-retry) before touching code.
---
## 1. What shipped (committed, keep)
The CUTOVER FLIP from `2026-06-07-render-unification-cutover-flip-handoff.md` landed:
| Commit | What |
|---|---|
| `5379f6e` | Step A — `PortalVisibilityBuilder.Build` seeds a full-screen `OutsideView` when the root is the outdoor node (`LoadedCell.IsOutdoorNode`, set by `OutdoorCellNode.Build`). +2 UnifiedFloodTests, +2 flag assertions. |
| `445e861` | Step B — the flip: `GameWindow.cs:~7387` `clipRoot = viewerRoot ?? _outdoorNode`. Drops the `playerIndoorGate` gate. ONE path, no inside/outside branch. Preserves the `LiveDynamic` draw for the outdoor root. |
| `88caa0d` | depth-clear fix — `ClearDepthSlice = null` for the outdoor root (the full-screen depth clear was painting the cellar over the player; fixed). |
| `774cb22` | Revert of `0030dac` (the slot-0 skip — a FAILED fix, see §5). |
**The flip's PRIMARY goal succeeded:** `[render-sig]` shows `branch=RetailPViewInside` every frame,
**zero `OutdoorRoot` frames** across a whole session. The two-branch-toggle flap is gone by
construction. Baselines: build green, App.Tests 216/0.
---
## 2. The two residuals the flip exposed (user-observed)
1. **See-through building walls from outside** — standing outside a building you see *into* it through
the walls (doors closed).
2. **Oscillation** — the interior/walls flicker between "showing nothing", "see-through", and "full
interior" frame-to-frame while standing still.
---
## 3. ROOT CAUSE — proven by a live `[bshell]` probe (NOT guessed)
A throttled probe in `RetailPViewRenderer.DrawInside` (now stripped; re-add from git history of this
doc's session if needed) logged, for the outdoor-node root on a loaded frame at the Holtburg cottage:
```
[bshell] total=6 withMesh=6 inOutdoorPartition=6 envCellsFlooded=1 outdoorEntities=637
```
Interpretation (each number is decisive):
- **`total=6 withMesh=6 inOutdoorPartition=6`** — there ARE 6 building `ModelId` "shell" entities
(`WorldEntity.IsBuildingShell`, created in `LandblockLoader.cs:75-91` from `LandBlockInfo.Buildings[].ModelId`),
ALL carry meshes, ALL land in `partition.Outdoor` (they have `ParentCellId==null`;
`InteriorEntityPartition` line 47 → Outdoor; `WbDrawDispatcher.EntityPassesVisibleCellGate` returns
`true` for null `visibleCellIds`). **So the `ModelId` exterior DOES render.**
- **BUT** the earlier "skip all interior shell draws for the outdoor root" experiment (uncommitted,
reverted) made the building **fully see-through** — i.e. drawing ONLY the `ModelId` shells is NOT a
solid building. **Therefore the `ModelId` Setup is a partial frame, and the building's actual WALLS
are the EnvCell shell geometry** (`ObjectMeshManager.PrepareCellStructMeshData`, drawn by
`DrawEnvCellShells`).
- **`envCellsFlooded=1`** — in this frame the outdoor-node flood reached **ZERO** building interior
cells (only the node itself). Earlier `[render-sig]` frames at the same spot showed `ids=[node + ~12
building cells]` (≈13). **So the flood membership swings between 1 and ~13 frame-to-frame.**
### The two residuals, explained
1. **Oscillation = flood instability gating the walls.** The flip made wall-drawing depend on the
portal flood reaching each building's interior cells. That flood is unstable (1 ↔ ~13), so the
EnvCell walls blink in and out. ("showing nothing" = flood=1, no interior; "full interior /
see-through" = flood reached the building.)
2. **See-through = single-sided EnvCell walls.** Even when the walls DO draw, the EnvCell wall polys
are single-sided for `SidesType==CounterClockwise` (interior-facing). `PrepareCellStructMeshData`
(ObjectMeshManager ~1299-1310) builds the back face only for `SidesType==None` (front twice
reversed) and `SidesType==Clockwise` (neg surface). A `CounterClockwise` wall = front face only →
from outside you see its culled back → see-through.
---
## 4. Fix path (identified, NOT implemented)
Two independent fixes, both needed:
- **F1 — Stabilise the flood membership** so a building's interior cells are CONSISTENTLY in/out of
the visible set (no 1↔13 swing). This is the same metastability family as the indoor flicker. Likely
levers: ground the outdoor-node flood's building membership in the cell `stab_list`/PVS (stable,
precomputed) instead of the per-frame portal-side test + projection; or hysteresis on which buildings
are flooded. Probe to re-add: `envCellsFlooded` per frame (RLE it; it should be constant when standing
still).
- **F2 — Make the EnvCell walls solid from outside.** Either build the missing back faces for
`SidesType==CounterClockwise` walls in `PrepareCellStructMeshData`, or render those shells
double-sided (`CullMode.None`) when the viewer is outside the cell. Verify against retail: dump a real
Holtburg cell's wall-poly `SidesType` distribution first.
**Open research question (reconcile before F2):** pre-flip the buildings looked SOLID from outside.
What drew the solid walls pre-flip — a global EnvCell-shell render, the `DrawPortal`/`BuildFromExterior`
look-in, or were the `ModelId` shells solid then? Find what the flip replaced. The old outdoor `else`
block (`GameWindow.cs:~7557-7663`, now dead-when-clipRoot-non-null but still present) is the place to
read. This answers whether F2 is "build back faces" or "restore a pre-flip draw".
---
## 5. DO NOT RETRY (failed this session, with evidence)
- **Slot-0 skip** (`0030dac`, reverted `774cb22`): "for the outdoor root, skip flooded cells whose
clip degenerated to no-clip slot 0." Made the oscillation WORSE — slot-0-ness flickers per frame, so
cells blinked. Wrong: the see-through is not the slot-0 fallback.
- **Skip-all-interiors experiment** (uncommitted, reverted): "outdoor root draws terrain + ModelId
exteriors only, no EnvCell shells." Made buildings FULLY see-through + flashing — proved the `ModelId`
Setup is not the walls (the walls are the EnvCell shells). Do not ship this.
- **Backface-culling-of-shells hypothesis** (never coded): plausible but the cull mode is already
data-driven (`poly.SidesType`); the real gap is single-sided geometry (no back face built), not a
cull-state bug.
- The subagent hypothesis "ModelId exterior occludes; interior overdraws it; fix = gate DrawEnvCellShells
off for the outdoor root" is **disproven** — that gate IS the skip-all-interiors experiment, which
removed the walls entirely.
---
## 6. State + how to resume
- HEAD `774cb22`, tree clean, build green, App.Tests 216/0. The flip + depth-clear are committed; the
branch renders with the two residuals (see-through + oscillation).
- The flip is on this BRANCH only (main is unaffected). To get a stable client meanwhile, revert the
flip commits (`445e861` Step B is the behaviour change; reverting it alone restores the pre-flip
outdoor path — verify Step A `5379f6e` is inert without it).
- Re-add the `[bshell]` / `envCellsFlooded` probe (see this session's git reflog for the exact code) to
watch flood stability while working F1.
- Memory: `project_indoor_flap_rootcause` (update with this corrected diagnosis),
`reference_render_pipeline_state`, `feedback_render_downstream_of_membership` (the oscillation IS a
membership/flood-stability bug, per that note).