docs(render): ARCHITECTURE RESET — indoor render is a 3-gate patchwork; handoff + unified-PView target
A week on the indoor render (Phase U.4 → U.4c → 2026-05-31) fixed the flap but
produced NO shippable progress: walls/ceiling don't seal, outdoor terrain is
visible from inside (#78), the enclosure reads grey/transparent. Root cause is
ARCHITECTURAL, not a bug.
Evidence this session (direct, via the new [shell] probe + screenshots) RULED OUT
every subsystem except the gating architecture: the interior cell shells render
fine (geometry/texture/opaque/depth all correct, zh=0 tr=0); the visibility
traversal computes correct sets + non-empty portal clips; cull mode is fine; the
camera/eye thread was a detour. The residual is that OUTDOOR geometry is not gated
to portal openings when indoors, and acdream enforces visibility THREE inconsistent
ways (TerrainClipMode / per-cell shell clip / entity ParentCellId filter with an
outdoor-stab bypass) instead of retail's ONE PView gate.
This commit is the reset handoff + documentation, not a code fix:
- docs/research/2026-05-31-render-architecture-reset-handoff.md — canonical: honest
state, evidence ledger (ruled-out / do-not-repeat), the mapped 3-gate patchwork,
the retail PView target (one traversal → one gate for ALL geometry), the reset
mission, and a copy-paste pickup prompt.
- docs/architecture/acdream-architecture.md — new "Render Pipeline" SSOT section
(current divergence + unified-PView target + the one rule: compute visibility
once, enforce it once). (Doc has pre-existing corruption below this section —
flagged for separate cleanup.)
- Apparatus: ACDREAM_PROBE_SHELL → [shell] (EnvCellRenderer per-cell prepared/drawn
geometry + flags) added to RenderingDiagnostics + EnvCellRenderer. Throwaway.
- docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md —
spec for e099b4c (camera collision; now parked as orthogonal to the seam).
Next session: STOP point-fixing; do the architecture reset to a single PView gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e099b4c4a3
commit
0013819fa1
4 changed files with 417 additions and 0 deletions
236
docs/research/2026-05-31-render-architecture-reset-handoff.md
Normal file
236
docs/research/2026-05-31-render-architecture-reset-handoff.md
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
# Render Architecture Reset — assessment + handoff (2026-05-31)
|
||||||
|
|
||||||
|
> **Read this first, in full, before touching any render code.** This is a deliberate
|
||||||
|
> reset after a week of point-fixing the indoor render with **no shippable progress**.
|
||||||
|
> The next session's job is NOT another symptom fix — it is to bring acdream's render
|
||||||
|
> pipeline up to a **single, coherent, retail-faithful design (PView)** and close the
|
||||||
|
> indoor seams *by construction*.
|
||||||
|
|
||||||
|
## 1. The honest state
|
||||||
|
|
||||||
|
A full week (Phase U.4 → U.4c → this session) has gone into the indoor render. Net
|
||||||
|
result: the **doorway "flap" is fixed** (real, kept), but the indoor world is **still
|
||||||
|
broken** — walls/ceiling don't visually seal, you see outdoor terrain from inside the
|
||||||
|
cellar ("the world from below"), and the enclosure reads as a grey/transparent void.
|
||||||
|
|
||||||
|
The reason there's been no progress is **architectural, not a bug**: the render pipeline
|
||||||
|
is a **patchwork of three inconsistent visibility gates** bolted onto a half-finished
|
||||||
|
port of retail's PView. Seams are *structural* — fixing one place (the flap, the camera,
|
||||||
|
the shells) pops a seam in another. We have been chasing symptoms across different
|
||||||
|
subsystems (visibility → camera collision → shell rendering) instead of fixing the
|
||||||
|
architecture. That stops now.
|
||||||
|
|
||||||
|
**Mandate from the user (2026-05-31):** *"produce me a solid engine that produces a
|
||||||
|
retail-faithful experience… look over what we have done, check our design and if it
|
||||||
|
matches a solid engine design, and solve all our issues."* This handoff is the
|
||||||
|
assessment; the next session executes the reset.
|
||||||
|
|
||||||
|
## 2. Evidence ledger — what is RULED OUT and CONFIRMED (do not re-litigate)
|
||||||
|
|
||||||
|
This week was not all waste: it eliminated, **with direct evidence**, every subsystem
|
||||||
|
*except* the gating architecture. Do not re-investigate these.
|
||||||
|
|
||||||
|
**CONFIRMED (direct evidence):**
|
||||||
|
- **The interior cell shells RENDER correctly.** `[shell]` probe (ACDREAM_PROBE_SHELL):
|
||||||
|
every visible cell (`0xA9B40170-0175`) is prepared and drawn — `gfx=1`, `idx` 24–270,
|
||||||
|
`tr=0` (not translucent), `zh=0` (textures present), opaque pass, depth-on. So the
|
||||||
|
shells are NOT the problem (geometry, textures, cull, depth all fine).
|
||||||
|
- **The visibility traversal computes correct results.** `[flap]` probe: portals
|
||||||
|
traverse (`TRV`), project, and clip to **non-empty** screen regions (`clip=4/5`),
|
||||||
|
`vis=3` cells, `terrain=Skip`/`outPolys=0` correct for a windowless cellar.
|
||||||
|
- **The residual is OUTDOOR geometry not being gated to portal openings when indoors**
|
||||||
|
(issue #78): screenshots show outdoor terrain + scenery rendered and visible from
|
||||||
|
*inside* the cellar (from below), and the interior not sealing. The cell mesh renders
|
||||||
|
but does not occlude / the outside is not clipped to the window.
|
||||||
|
|
||||||
|
**RULED OUT (with evidence) — do NOT chase again:**
|
||||||
|
- The camera / eye position as the root (eye-displacement was reduced by `e099b4c` but
|
||||||
|
the residuals persisted unchanged — the camera thread was a **detour**).
|
||||||
|
- Cull mode (cell shells already render double-sided, `EnvCellRenderer.cs:1168`).
|
||||||
|
- Cell-shell geometry missing / textures missing (`[shell]` proves both present).
|
||||||
|
- The portal side-test / PortalSide (`b5f2bf2`, byte-identical no-op).
|
||||||
|
- PVS / stab_list grounding (H1, `639f20f` plumbed, not the fix).
|
||||||
|
- Camera zoom as the cause (errors persist at minimum zoom).
|
||||||
|
|
||||||
|
## 3. The current render architecture (mapped) — the patchwork
|
||||||
|
|
||||||
|
Per-frame, inside `GameWindow.OnRender` (~`:7290-7471`):
|
||||||
|
|
||||||
|
```
|
||||||
|
CellVisibility.ComputeVisibility(visRootPos = PLAYER pos) → visible-cell SET + VisibleCellIds + CameraCell
|
||||||
|
│
|
||||||
|
PortalVisibilityBuilder.Build(clipRoot, visRootPos=PLAYER, envCellViewProj=EYE) ← a partial port of retail ConstructView
|
||||||
|
│ → PortalVisibilityFrame { OutsideView, per-cell CellViews, OrderedVisibleCells }
|
||||||
|
│ (side-test + ordering use the PLAYER; projection uses the EYE — a split)
|
||||||
|
│
|
||||||
|
ClipFrameAssembler.Assemble(...) → TerrainClipMode {Skip|Scissor|Planes} + CellIdToSlot (per-cell clip regions) + OutdoorVisible
|
||||||
|
│
|
||||||
|
├── TerrainModernRenderer.Draw gated by TerrainClipMode (GATE #1)
|
||||||
|
├── EnvCellRenderer.Render(shells) gated by envCellShellFilter + per-cell clip slot (GATE #2)
|
||||||
|
└── WbDrawDispatcher.Draw(entities) gated by ParentCellId ∈ visibleCellIds … BUT outdoor stabs have ParentCellId==null and ALWAYS PASS (GATE #3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is structurally fragile (the root of all the seams):**
|
||||||
|
1. **Three separate gates** (terrain / shells / entities) must independently agree to
|
||||||
|
produce a seamless interior. They don't. Retail has **one** gate (PView).
|
||||||
|
2. **Outdoor geometry is not gated to portal openings.** Terrain has its own
|
||||||
|
Skip/Scissor/Planes path (and the Scissor fallback *over-includes*), and outdoor
|
||||||
|
stabs/scenery (`ParentCellId==null`) **bypass the gate entirely** → you see the
|
||||||
|
outdoor world from inside (#78). Retail draws the landscape **only** through exit
|
||||||
|
portals, clipped to the opening.
|
||||||
|
3. **No single source of truth.** "What is visible and where on screen" is reassembled
|
||||||
|
from three consumers of one builder, instead of being the builder's authoritative,
|
||||||
|
uniformly-enforced output.
|
||||||
|
4. **Player-rooted set vs eye-projected clip split** adds a second axis of fragility
|
||||||
|
(the flap, the residual degeneracies) — secondary, but real.
|
||||||
|
|
||||||
|
This is the crux: **we compute visibility once but enforce it three different,
|
||||||
|
inconsistent ways.**
|
||||||
|
|
||||||
|
## 4. The retail-faithful target — PView: one traversal, one gate
|
||||||
|
|
||||||
|
Retail (decomp `docs/research/named-retail/acclient_2013_pseudo_c.txt`) renders the
|
||||||
|
world through **PView**, a single portal-clipped traversal:
|
||||||
|
|
||||||
|
- Root at the **viewer's cell** (`CellManager::ChangePosition` tracks `curr_cell`).
|
||||||
|
- `PView::ConstructView` (`:433750`) walks the portal graph; `InitCell` (`:432896`)
|
||||||
|
computes each cell's **screen-space clip region** (the portal opening it's seen
|
||||||
|
through, recursively intersected with the parent's region); cells go on
|
||||||
|
`cell_draw_list` closest-first.
|
||||||
|
- **Exit portals** add the **outside** (landscape) to the view, **clipped to the
|
||||||
|
exit-portal opening**.
|
||||||
|
- `PView::DrawCells` (`:432715`) renders each cell **clipped to its region**, and the
|
||||||
|
landscape **only where an exit portal reveals it**.
|
||||||
|
- The viewer (eye) is **collided** (`SmartBox::update_viewer` `:92761`) so the
|
||||||
|
projection stays well-conditioned.
|
||||||
|
- `CEnvCell::find_visible_child_cell` (`:311397`) + `seen_outside` gate the
|
||||||
|
landscape-keep.
|
||||||
|
- **Outdoors**, PView is trivial (the LandCell + neighbours, no indoor clipping).
|
||||||
|
|
||||||
|
The defining property: **ONE visibility structure gates ALL geometry** — interior cell
|
||||||
|
shells, interior statics, and the outside (terrain + scenery) — all clipped to their
|
||||||
|
PView-derived regions. That is *why retail is seamless by construction.*
|
||||||
|
|
||||||
|
Reference port to crib (acdream owns it but **never invokes it**): WorldBuilder's
|
||||||
|
`RenderInsideOut` / `VisibilityManager`
|
||||||
|
(`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`).
|
||||||
|
|
||||||
|
## 5. The solid design for acdream (the reset target)
|
||||||
|
|
||||||
|
We already have the hard part (the `PortalVisibilityBuilder` ConstructView port + the
|
||||||
|
`ClipFrame` clip-plane machinery + per-cell clip slots). The reset is a
|
||||||
|
**consolidation, not a rewrite**: make the PView output the **single gate for every
|
||||||
|
geometry type**.
|
||||||
|
|
||||||
|
1. **One gate.** Every draw — cell shells, interior statics, terrain, outdoor scenery —
|
||||||
|
is clipped to its PView region:
|
||||||
|
- Interior cell `C` → its per-cell clip region (already ≈done in `EnvCellRenderer`).
|
||||||
|
- Interior static in cell `C` → cell `C`'s region (route via the same per-instance
|
||||||
|
clip slot).
|
||||||
|
- **Outside** (terrain + outdoor scenery + `ParentCellId==null` stabs) → the
|
||||||
|
**`OutsideView` region** (union of exit-portal openings). **Empty `OutsideView`
|
||||||
|
(windowless interior) ⇒ draw NO outdoor geometry.** **No `ParentCellId==null`
|
||||||
|
bypass.**
|
||||||
|
- **Outdoor root** (camera/player in a LandCell, no indoor cell) ⇒ gate = everything
|
||||||
|
(today's normal path).
|
||||||
|
2. **Delete the ad-hoc gates.** Collapse GATE #1/#2/#3 into the single clip-to-region.
|
||||||
|
Remove the `ParentCellId==null` always-pass in `WbDrawDispatcher`. Remove the
|
||||||
|
terrain Scissor over-include fallback in favour of retail's exact portal clip.
|
||||||
|
3. **Resolve the set/clip split.** Decide one rooting story (the flap fix proved the
|
||||||
|
*set* must be player-rooted; the *clip/projection* is the eye's — confirm this is
|
||||||
|
retail-faithful under the unified gate, or collide the eye so they coincide).
|
||||||
|
4. **U.5 (eye outside, looking into a building)** is the same machinery with the root at
|
||||||
|
the eye's cell and the interior shown through the building's portals — design it in,
|
||||||
|
don't special-case it.
|
||||||
|
|
||||||
|
Result, by construction: #78 (outdoor gated to openings), transparent walls (interior
|
||||||
|
uniformly clipped), grey enclosure (no spurious ungated gaps), terrain-through-floor
|
||||||
|
(no outdoor outside the window) all resolve together — because there is one consistent
|
||||||
|
rule instead of three.
|
||||||
|
|
||||||
|
## 6. Next-session mission (the reset — in order)
|
||||||
|
|
||||||
|
1. **Audit** the current render data-flow against §3; confirm the three-gate divergence
|
||||||
|
in the live code.
|
||||||
|
2. **Verify the target against retail**: read PView in full
|
||||||
|
(`ConstructView`/`InitCell`/`ClipPortals`/`AddViewToPortals`/`DrawCells`/
|
||||||
|
`grab_visible_cells`/`find_visible_child_cell`) **and** WB `RenderInsideOut`. Do not
|
||||||
|
trust this doc's summary alone — confirm the one-gate model from the source.
|
||||||
|
3. **Design** the unified gate (`superpowers:brainstorming` → spec). This is
|
||||||
|
architectural; brainstorm it, don't free-solo.
|
||||||
|
4. **Implement** the single gate; delete the ad-hoc gates/bypasses.
|
||||||
|
5. **Verify** at the visual gate: flap stays fixed; walls/ceiling solid; no
|
||||||
|
terrain-through-floor; no grey; outdoor visible *only* through windows/doors, clipped.
|
||||||
|
|
||||||
|
**Operating rules for the reset:** evidence-first; no point-fixes; no new gate bolted on
|
||||||
|
to mask a seam (that is exactly what produced this mess). If a fix starts to look like a
|
||||||
|
fourth special-case gate, stop — it belongs in the one gate.
|
||||||
|
|
||||||
|
## 7. What the week produced (status of each artifact)
|
||||||
|
|
||||||
|
- **KEEP — `0ee328a` flap fix:** root indoor visibility at the player's cell. A correct,
|
||||||
|
retail-faithful PView piece (the set must be player-rooted). Keep.
|
||||||
|
- **ORTHOGONAL — `e099b4c` camera-collision viewer-gate-exemption:** retail-faithful but
|
||||||
|
**not** the seam fix (the camera thread was a detour). **Open decision:** keep (it's a
|
||||||
|
real, faithful improvement) or revert to keep the reset branch focused. Recommendation:
|
||||||
|
**park it** — keep the commit but treat camera collision as out-of-scope for the reset;
|
||||||
|
revisit only after the unified gate lands. Its sibling diagnosis (`3066460`) +
|
||||||
|
spec (`docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md`)
|
||||||
|
+ the `CameraCollisionIndoorTests` are accurate for what they cover but are about an
|
||||||
|
orthogonal subsystem.
|
||||||
|
- **APPARATUS (throwaway, gated, useful for the reset):**
|
||||||
|
- `ACDREAM_PROBE_FLAP` → `[flap]`/`[flap-cam]`/`[flap-sweep]` (visibility + eye + camera sweep).
|
||||||
|
- `ACDREAM_PROBE_SHELL` → `[shell]` (per-cell shell render: prepared? geometry? flags?).
|
||||||
|
Strip both once the reset lands.
|
||||||
|
- **Disproven hypotheses (do NOT repeat):** H1 (PVS grounding), H2 (PortalSide),
|
||||||
|
zoom-confound, back-face-cull, eye-displacement-as-root, shell-geometry/texture-missing.
|
||||||
|
|
||||||
|
## 8. Git / safety
|
||||||
|
- Branch `claude/thirsty-goldberg-51bb9b`, **UNPUSHED** (push pending with the user).
|
||||||
|
- Two `git stash` entries preserved (#98/#101 WIP) — **do NOT drop.**
|
||||||
|
- Throwaway capture logs `u4c-*.log` are untracked — ignore/delete, do not commit.
|
||||||
|
|
||||||
|
## 9. Pickup prompt (copy-paste for the next session)
|
||||||
|
|
||||||
|
```
|
||||||
|
RENDER ARCHITECTURE RESET — acdream indoor pipeline. Continue on branch
|
||||||
|
claude/thirsty-goldberg-51bb9b (do NOT branch/worktree; preserve the 2 git stashes).
|
||||||
|
|
||||||
|
READ FIRST, in full: docs/research/2026-05-31-render-architecture-reset-handoff.md, then
|
||||||
|
the "Render Pipeline" section of docs/architecture/acdream-architecture.md.
|
||||||
|
|
||||||
|
State both altitudes: Working toward M1.5 "indoor world feels right." After a week of
|
||||||
|
point-fixing with no shippable progress, this is an ARCHITECTURE RESET, not a symptom fix.
|
||||||
|
|
||||||
|
THE PROBLEM (root cause, evidence in the handoff): acdream's render pipeline is a
|
||||||
|
patchwork of THREE inconsistent visibility gates (terrain TerrainClipMode / shell
|
||||||
|
per-cell clip / entity ParentCellId filter with an outdoor-stab bypass) bolted onto a
|
||||||
|
half-finished PView port. Seams are structural. The interior cell shells RENDER FINE
|
||||||
|
(proven); the residual is that OUTDOOR geometry isn't gated to portal openings when
|
||||||
|
indoors (#78) and the three gates don't agree.
|
||||||
|
|
||||||
|
THE TARGET (retail PView, §4-5 of the handoff): ONE portal-visibility traversal whose
|
||||||
|
output is the SINGLE GATE for ALL geometry — interior shells, interior statics, AND the
|
||||||
|
outside (terrain + scenery) all clipped to their PView regions; outside drawn ONLY
|
||||||
|
through exit-portal openings; empty OutsideView ⇒ no outdoor; no ParentCellId==null
|
||||||
|
bypass; outdoor root ⇒ gate=everything. We already own the builder (PortalVisibilityBuilder
|
||||||
|
= ConstructView port) + ClipFrame; this is CONSOLIDATION, not a rewrite.
|
||||||
|
|
||||||
|
MISSION (in order): (1) audit the current 3-gate data-flow in GameWindow.OnRender
|
||||||
|
~:7290-7471; (2) VERIFY the one-gate model against retail PView (ConstructView :433750,
|
||||||
|
InitCell :432896, DrawCells :432715, find_visible_child_cell :311397) + WB RenderInsideOut
|
||||||
|
(references/WorldBuilder/.../VisibilityManager.cs) — confirm from source, not the handoff
|
||||||
|
summary; (3) brainstorm + spec the unified gate (superpowers:brainstorming); (4) implement
|
||||||
|
the single gate, delete the ad-hoc gates/bypasses; (5) visual-verify ALL indoor issues
|
||||||
|
resolve together.
|
||||||
|
|
||||||
|
DO NOT REPEAT (evidence-disproven, handoff §2): camera/eye as root, cull mode, shell
|
||||||
|
geometry/texture missing, H1 PVS grounding, H2 PortalSide, zoom-confound. DO NOT bolt on
|
||||||
|
a fourth gate to mask a seam. Evidence-first; no point-fixes; brainstorm the architecture
|
||||||
|
before coding; only stop for visual verification.
|
||||||
|
|
||||||
|
Apparatus ready: ACDREAM_PROBE_FLAP ([flap]/[flap-cam]/[flap-sweep]) + ACDREAM_PROBE_SHELL
|
||||||
|
([shell]). Open decision: keep or revert e099b4c (camera collision, orthogonal — parked).
|
||||||
|
Launch per CLAUDE.md "Running the client"; Holtburg cottage cellar/stairs is the test
|
||||||
|
scenario; visual verification at that threshold is the acceptance gate.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Camera-collision indoor engagement — the shared-root residual fix (2026-05-31)
|
||||||
|
|
||||||
|
**Phase:** M1.5 "indoor world feels right" — post-flap-fix residuals.
|
||||||
|
**Status:** design (pre-approved by user 2026-05-31; "most thorough + retail-faithful").
|
||||||
|
**Predecessors:** flap fix `0ee328a`; diagnosis `docs/research/2026-05-31-camera-collision-indoor-diagnosis.md`; camera-collision research `docs/research/2026-05-29-a8f-camera-collision-handoff.md`.
|
||||||
|
|
||||||
|
## Problem (one shared root, three faces)
|
||||||
|
|
||||||
|
After the U.4c flap fix (indoor visibility rooted at the **player's** cell; the 3rd-person
|
||||||
|
camera **eye** still drives the per-frame projection), three residuals remain — transparent
|
||||||
|
outer walls (#2), terrain-through-floor (#78), "stairs everything grey" (#3). Evidence
|
||||||
|
(`u4c-fix.log`, current code state): the eye is **outside the player's cell ~90% of frames**,
|
||||||
|
at full chase distance (~3.4 m) — i.e. **the shipped camera collision is not engaging in
|
||||||
|
interior cells.** When the eye is displaced, it projects the player-traversed portal openings
|
||||||
|
to garbage/off-screen NDC (74% of frames), collapsing `OutsideView` to empty → terrain `Skip`
|
||||||
|
(grey, #3) or tripping the giant-scissor fallback → over-include (bleed, #78/#2). The projection
|
||||||
|
**from the eye is correct** (the OutsideView is a screen-space clip and the screen is the eye's
|
||||||
|
view) — the fault is the eye being in the wrong place. So the root fix is **keeping the eye in
|
||||||
|
valid space**, which resolves all three faces at once.
|
||||||
|
|
||||||
|
## Why the shipped collision doesn't engage (code-verified)
|
||||||
|
|
||||||
|
`PhysicsCameraCollisionProbe.SweepEye` → `PhysicsEngine.ResolveWithTransition(... cellId =
|
||||||
|
playerCell, moverFlags = IsViewer|PathClipped|FreeRotate|PerfectClip ...)`. The camera eye
|
||||||
|
sweeps UP+BACK from the head-pivot (~2.6 m back, ~2.25 m up). In an acdream cottage the
|
||||||
|
enclosing geometry (walls/floor/roof) lives in a **landblock-baked exterior-shell GfxObj**
|
||||||
|
registered `cellScope=0` (outdoor/landblock-wide shadow list) — established by issue #98
|
||||||
|
(the cottage floor that capped the player's head sphere is GfxObj `0x01000A2B`, not cell BSP).
|
||||||
|
`ShadowObjectRegistry.GetNearbyObjects` has the **issue-#98 indoor gate** at
|
||||||
|
`ShadowObjectRegistry.cs:480`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if ((primaryCellId & 0xFFFFu) >= 0x0100u) // primary cell is indoor
|
||||||
|
return; // skip the outdoor radial sweep entirely
|
||||||
|
```
|
||||||
|
|
||||||
|
So while the viewer sphere's primary cell is the indoor cottage cell, the sweep is **gated
|
||||||
|
away from the exterior-shell GfxObj** — the only geometry that encloses the cottage in our
|
||||||
|
data model. The eye therefore finds nothing to stop it and flies to full distance.
|
||||||
|
|
||||||
|
## Retail behavior (decomp-verified — the faithfulness anchor)
|
||||||
|
|
||||||
|
`SmartBox::update_viewer` @ `acclient_2013_pseudo_c.txt:92761-92892`:
|
||||||
|
- roots the viewer `CTransition` at the **player/pivot cell** (`init_path(t, cell_1, pivot,
|
||||||
|
sought)`; `cell_1` = `AdjustPosition`'s pivot cell, fallback `player->cell`);
|
||||||
|
- `init_object(player, 0x5c)` = `IsViewer|PathClipped|FreeRotate|PerfectClip` (acdream matches);
|
||||||
|
- sweeps `viewer_sphere` (0.3 m) via `find_valid_position` and publishes the **stopped**
|
||||||
|
`sphere_path.curr_pos` + `curr_cell`; fallbacks `AdjustPosition` → snap-to-player.
|
||||||
|
|
||||||
|
So retail bounds the viewer by **whatever the swept transition hits inside the player's cell.**
|
||||||
|
Retail's interior EnvCells are **self-enclosing** (walls + ceiling in the cell's own geometry),
|
||||||
|
so the viewer is stopped by interior geometry, and retail's "structural separation" (`CObjCell::
|
||||||
|
find_cell_list` `:308751-308769` adds outdoor GfxObjs only to outdoor cells' shadow lists)
|
||||||
|
holds *because* the interior is self-contained.
|
||||||
|
|
||||||
|
**acdream's divergence:** our cottages are NOT self-enclosing — the enclosure is the landblock
|
||||||
|
shell GfxObj (per #98). So for our data model, the shell GfxObj **is** the enclosure the viewer
|
||||||
|
must collide. Letting the viewer reach it is the faithful analog of retail's
|
||||||
|
viewer-bounded-by-enclosure.
|
||||||
|
|
||||||
|
## The fix (Option A — viewer-exempt the #98 gate)
|
||||||
|
|
||||||
|
Thread the mover flags (or a derived `isViewer` bool) from the `FindObjCollisions` call site
|
||||||
|
(`ObjectInfo.State` is already on the `Transition`) down through to
|
||||||
|
`ShadowObjectRegistry.GetNearbyObjects`, and change the gate to:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Issue #98 gate is correct ONLY for the player foot/head capsule (it stops the cottage-floor
|
||||||
|
// GfxObj from capping the head sphere from the cellar below). The camera viewer (IsViewer, a
|
||||||
|
// single 0.3 m sphere, no capsule, no contact-plane) must reach the landblock-baked building
|
||||||
|
// shell that forms the cottage enclosure — retail's update_viewer bounds the viewer by the
|
||||||
|
// cell enclosure (acclient_2013_pseudo_c.txt:92761); in acdream's data model that enclosure is
|
||||||
|
// the shell GfxObj. find_obj_collisions has no indoor gate (acclient_2013_pseudo_c.txt:308918).
|
||||||
|
if ((primaryCellId & 0xFFFFu) >= 0x0100u && !isViewer)
|
||||||
|
return;
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetNearbyObjects` gains an `isViewer` parameter (default `false` → existing callers keep the
|
||||||
|
gate). Only `PhysicsCameraCollisionProbe.SweepEye` passes `IsViewer`, so only the camera sweep
|
||||||
|
changes behavior.
|
||||||
|
|
||||||
|
### Why not the alternatives
|
||||||
|
- **Re-scope the cottage shell registration to the indoor cell** (`cellScope` = cottage cell):
|
||||||
|
requires per-GfxObj→cell adjacency we don't have and isn't how retail models it; would also
|
||||||
|
re-expose the player to the #98 head-cap. Rejected.
|
||||||
|
- **Make interior cells self-enclosing (faithful hydration of ceiling/walls into cell BSP):**
|
||||||
|
the genuinely-retail-faithful end state, but a large, #98-adjacent, high-risk refactor of
|
||||||
|
cottage hydration. **Documented out-of-scope follow-on** (file as a residual after the visual
|
||||||
|
gate). Option A is the low-risk bridge that matches retail's *observable* behavior for our
|
||||||
|
data model.
|
||||||
|
- **Retail fallback chain (`AdjustPosition` → snap-to-player):** faithful completeness, but not
|
||||||
|
load-bearing for this residual (our sweep succeeds and returns a position; it doesn't *fail*).
|
||||||
|
Out-of-scope follow-on.
|
||||||
|
|
||||||
|
## Tests (TDD)
|
||||||
|
1. **GREEN the RED test** `CameraCollisionIndoorTests.SweepEye_IndoorCellExteriorGfxObjWall_
|
||||||
|
NotReachedFromIndoorContext_CurrentlyFails`: after the fix, the viewer sweep stops at the
|
||||||
|
shell wall → `pulledIn ≥ 0.5` (rename to drop `_CurrentlyFails`).
|
||||||
|
2. **#98 regression guard (new):** an `IsViewer` sweep in the cellar toward the cottage-floor
|
||||||
|
GfxObj behaves correctly (the viewer single-sphere is not a head-capsule, so the #98 cap does
|
||||||
|
not apply); assert the player's gate behavior is untouched (an `IsPlayer` sweep at the same
|
||||||
|
site still hits the gate / keeps the #98 fix). Use the cottage GfxObj fixture
|
||||||
|
(`RegisterCottageGfxObj`, `0x01000A2B`).
|
||||||
|
3. Full `dotnet build` + `dotnet test` green; with-fix failure set ⊆ baseline (the pre-existing
|
||||||
|
static-leak flakiness is independent).
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
- Build + tests green; RED→GREEN; #98 guard passes.
|
||||||
|
- **Visual gate (the one user step):** walk `+Acdream` into a Holtburg cottage / cellar / stairs
|
||||||
|
and a window room with `ACDREAM_PROBE_FLAP=1`. Expect: the eye stops at walls/ceiling (no
|
||||||
|
transparent outer walls, no grey, no terrain-through-floor); `[flap-sweep]` shows `pulledIn > 0`
|
||||||
|
+ `eyeInRoot=Y` for indoor cells; `[flap-cam]` shows `terrain=Planes` through windows (not
|
||||||
|
`Skip`/`Scissor`-fallback). The visual gate is the **arbiter on real geometry** — if the eye
|
||||||
|
still flies free in the main-floor residual cells (0174/0175), the residual is a different
|
||||||
|
cause (interior-BSP completeness) and we iterate with the live `[flap-sweep]` data.
|
||||||
|
|
||||||
|
## Out of scope (documented residuals)
|
||||||
|
- **U.5** outside-camera → building-interior peering, and the legitimate eye-through-an-open-
|
||||||
|
doorway exit (both deferred in the Phase U spec).
|
||||||
|
- **Interior-cell self-enclosure hydration** (the deeper retail divergence; faithful end state).
|
||||||
|
- **Viewer fallback chain** (`AdjustPosition` → snap-to-player) for the find-valid-position-fails
|
||||||
|
path.
|
||||||
|
- The throwaway `[flap]`/`[flap-cam]`/`[flap-sweep]` apparatus is kept for the visual gate and
|
||||||
|
stripped once the indoor residuals close.
|
||||||
|
|
@ -933,6 +933,48 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
_lastFrameStats.TrianglesDrawn += (dc.renderData.Batches.Count > 0
|
_lastFrameStats.TrianglesDrawn += (dc.renderData.Batches.Count > 0
|
||||||
? dc.renderData.Batches[0].IndexCount / 3
|
? dc.renderData.Batches[0].IndexCount / 3
|
||||||
: 0) * dc.count;
|
: 0) * dc.count;
|
||||||
|
|
||||||
|
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
|
||||||
|
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
|
||||||
|
// present in the prepared snapshot, and its geometry/flags. Answers why the
|
||||||
|
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
|
||||||
|
// prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
|
||||||
|
// (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
|
||||||
|
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
|
||||||
|
if (renderPass == WbRenderPass.Opaque
|
||||||
|
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder(256);
|
||||||
|
sb.Append("[shell] filter=").Append(filter?.Count ?? -1)
|
||||||
|
.Append(" drawCalls=").Append(drawCalls.Count)
|
||||||
|
.Append(" inst=").Append(allInstances.Count)
|
||||||
|
.Append(" tris=").Append(_lastFrameStats.TrianglesDrawn);
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
foreach (var cellId in filter)
|
||||||
|
{
|
||||||
|
if (!snapshot.BatchedByCell.TryGetValue(cellId, out var gfxDict))
|
||||||
|
{
|
||||||
|
sb.Append(" [0x").Append(cellId.ToString("X8")).Append(":NOSNAP]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int gfxN = 0, tf = 0, batch = 0, idx = 0, tr = 0, zh = 0;
|
||||||
|
foreach (var (gfxObjId, transforms) in gfxDict)
|
||||||
|
{
|
||||||
|
gfxN++; tf += transforms.Count;
|
||||||
|
var rd = _meshManager.TryGetRenderData(gfxObjId);
|
||||||
|
if (rd != null)
|
||||||
|
foreach (var b in rd.Batches)
|
||||||
|
{ batch++; idx += b.IndexCount; if (b.IsTransparent) tr++; if (b.BindlessTextureHandle == 0) zh++; }
|
||||||
|
}
|
||||||
|
sb.Append(" [0x").Append(cellId.ToString("X8"))
|
||||||
|
.Append(":gfx=").Append(gfxN).Append(" tf=").Append(tf)
|
||||||
|
.Append(" batch=").Append(batch).Append(" idx=").Append(idx)
|
||||||
|
.Append(" tr=").Append(tr).Append(" zh=").Append(zh).Append(']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.Console.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,21 @@ public static class RenderingDiagnostics
|
||||||
public static bool ProbeFlapEnabled { get; set; } =
|
public static bool ProbeFlapEnabled { get; set; } =
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Issue #78 (2026-05-31) cell-shell render probe. When true,
|
||||||
|
/// <c>EnvCellRenderer.Render</c> emits one <c>[shell]</c> line per opaque-pass
|
||||||
|
/// call: per visible (filtered) cell — is it present in the prepared snapshot,
|
||||||
|
/// how many gfxObjs + instances, and per-gfxObj batch count / index count /
|
||||||
|
/// translucent / zero-bindless-handle (missing texture) — plus the pass totals.
|
||||||
|
/// This directly answers WHY interior walls/ceilings don't appear: no geometry
|
||||||
|
/// prepared for the cell (cell absent / 0 instances), drawn-but-invisible
|
||||||
|
/// (zeroHandle / translucent against the clear color), or prepared+drawn (so the
|
||||||
|
/// fault is elsewhere — depth/occlusion). Throwaway apparatus — strip once the
|
||||||
|
/// indoor-enclosure render is fixed. Initial state from <c>ACDREAM_PROBE_SHELL=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeShellEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "1";
|
||||||
|
|
||||||
// Cell-change gate for EmitVis. The probe fires once per distinct root cell
|
// Cell-change gate for EmitVis. The probe fires once per distinct root cell
|
||||||
// so launch.log stays readable under motion (the per-frame call is a no-op
|
// so launch.log stays readable under motion (the per-frame call is a no-op
|
||||||
// when the root is unchanged). Sentinel 0 = "no root yet" — the first real
|
// when the root is unchanged). Sentinel 0 = "no root yet" — the first real
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue