acdream/docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md
Erik e798cb7898 docs(spec): expand probe design with concrete line formats + code sites
User feedback: "add the probes you need. Better info, better code."
Original spec had a single ACDREAM_PROBE_INDOOR=1 with vague "log
lookup results" guidance. Replaced with five individually-toggleable
probes, each with:

- Specific env var name + DebugPanel checkbox name.
- Concrete log-line format.
- Exact code site to instrument.
- The hypothesis it disambiguates.

Probe set:
- ACDREAM_PROBE_INDOOR_WALK   — dispatcher entity walk per cell
- ACDREAM_PROBE_INDOOR_LOOKUP — render-data lookup hit/miss + SetupParts
- ACDREAM_PROBE_INDOOR_UPLOAD — WB upload result (requested + completed)
- ACDREAM_PROBE_INDOOR_XFORM  — composed world transform for cell geom
- ACDREAM_PROBE_INDOOR_CULL   — visibility/frustum filter decisions

Plus ACDREAM_PROBE_INDOOR_ALL master toggle.

Implementation outline added: new RenderingDiagnostics static class
(mirrors L.2a's PhysicsDiagnostics pattern), DebugPanel subsection,
edits to WbDrawDispatcher + WbMeshAdapter.

Acceptance criteria refreshed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:07:54 +02:00

248 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Indoor Cell Rendering Fix — Design
**Status:** Brainstormed 2026-05-19. Pivoted mid-brainstorm — see §1.5 for
the corrected root-cause analysis. Awaiting user review.
**Scope:** Diagnose + fix the actual break in the EnvCell rendering chain.
**Out of scope this phase:** Cell collision symptoms (no wall collision
exiting, weird open-air collisions). Filed as a follow-up phase pending
user repro data.
---
## 1. Symptom
Walking into Holtburg Inn: the exterior building stab renders (walls visible
from inside), but the interior cell's own room mesh — floor, inner walls,
ceiling — is missing. The user can walk through the empty interior with no
floor visible underfoot.
## 1.5 What the root cause is NOT (corrected mid-brainstorm)
Initial hypothesis: N.5 retirement (commit
[`dcae2b6`](../../../#) 2026-05-08) deleted the legacy cell-mesh drain path
with the assumption "WB handles EnvCell geometry through its own pipeline,"
and that assumption was wrong.
**Closer inspection during brainstorm proved that assumption is correct.**
WB's `ObjectMeshManager.PrepareMeshData(id, isSetup)` at
[`ObjectMeshManager.cs:557`](../../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:557)
dispatches on the **dat record type** (not on the `isSetup` parameter).
When the id resolves to a `DBObjType.EnvCell`, it routes to
`PrepareEnvCellMeshData(id, envCell, ct)` at line 1186, which produces an
`ObjectMeshData` with `IsSetup=true`, `SetupParts` = [static objects +
cellGeometry], `EnvCellGeometry` = the floor/wall/ceiling room mesh.
The dispatcher correctly handles `IsSetup=true` at
[`WbDrawDispatcher.cs:607-621`](../../../src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:607) —
it iterates `SetupParts`, looks up each part's render data, composes
transforms, and draws each.
`DefaultDatReaderWriter` loads region cell dats during construction
([`DefaultDatReaderWriter.cs:66-89`](../../../references/WorldBuilder/WorldBuilder.Shared/Services/DefaultDatReaderWriter.cs:66))
so `ResolveId(envCellId)` will find the cell record.
`LandblockSpawnAdapter.OnLandblockLoaded` iterates `landblock.Entities` and
calls `_adapter.IncrementRefCount(meshRef.GfxObjId)` for each
([`LandblockSpawnAdapter.cs:75-80`](../../../src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs:75)).
Cell entities have `ServerGuid == 0` (atlas-tier), so they pass the filter
at line 73. Their `MeshRef.GfxObjId == envCellId` reaches `IncrementRefCount`.
**The chain looks structurally intact.** Floors SHOULD render today. They
don't. Therefore the failure is subtler than "we never invoke the load."
## 2. Real failure point — to be determined by diagnostics
Six untested hypotheses, in rough order of probability:
1. **WB silently fails to build the `ObjectMeshData`.** `PrepareEnvCellMeshData`
returns null when the Environment dat can't resolve, or when
`PrepareCellStructMeshData` returns null (texture issues, surface
resolution failure). WB doesn't log; the failure is invisible.
2. **`SetupParts.cellGeomId` is uploaded but its texture batches are empty.**
`UploadGfxObjMeshData` returning null at line 675 is treated as a
non-fatal substitution — the render data has no draw batches, dispatcher
silently draws nothing.
3. **Cell entity is culled before reaching the dispatcher.** `visibleCellIds`
filter at `WbDrawDispatcher.cs:317-319` rejects entities whose
`ParentCellId` isn't in the visible set. If the cell entity's
`ParentCellId == envCellId` but the visibility BFS doesn't include the
player's current cell (because `FindCameraCell` returns null when camera
is in third-person above the building, etc.), the cell entity is
skipped.
4. **Double-spawn conflict between WB's static-object SetupParts and
acdream's per-stab entity hydration.** `PrepareEnvCellMeshData` iterates
`envCell.StaticObjects` and adds each as a SetupPart. Meanwhile acdream
already hydrates the same static objects as separate `WorldEntity`
instances at [`GameWindow.cs:5390-5439`](../../../src/AcDream.App/Rendering/GameWindow.cs:5390).
WB might be holding extra ref counts on those GfxObj IDs that block
eviction or cause cache thrash. Unlikely to cause "missing floor" but
worth ruling out.
5. **Transform composition bug.** `ComposePartWorldMatrix(entityWorld,
meshRef.PartTransform, partTransform)` — if our cell entity's
`meshRef.PartTransform == cellTransform` and WB's `partTransform`
already bakes the cell origin, the floor lands at `2 × cellOrigin`,
far below or beside the actual cell. The user would describe this
as "missing" because the floor is now outside the visible frustum.
6. **The cell entity's `MeshRefs` only has one entry, but WB expects
multiple.** The dispatcher iterates `entity.MeshRefs`, but each MeshRef
gets its own `TryGetRenderData(meshRef.GfxObjId)` call. For cell
entities we have `MeshRefs = { MeshRef(envCellId, cellTransform) }`.
When the lookup returns an `IsSetup=true` render data, the dispatcher
does the right thing (line 607-621) — iterates SetupParts. So this
should work; ruling out.
## 3. Solution
### Phase 1 — Diagnostics (this phase's work)
Five probes, each individually toggleable via env-var + DebugPanel
checkbox. The probes live in a new
`AcDream.Core.Rendering.RenderingDiagnostics` static class (mirroring
the `AcDream.Core.Physics.PhysicsDiagnostics` pattern shipped in L.2a)
so they're discoverable from one place and survive across the
Core / App seam.
Each probe is **rate-limited**: by default, one line per (envCellId,
frame-modulo-30) — i.e., once per second per cell at 30 Hz — to avoid
log spam. When `ACDREAM_PROBE_INDOOR_VERBOSE=1` is also set, the
rate-limit drops and every frame logs.
| Env var (and DebugPanel mirror) | Probe | Code location | Line format |
|---|---|---|---|
| `ACDREAM_PROBE_INDOOR_WALK` | Cell-entity dispatcher walk | `WbDrawDispatcher.WalkVisibleEntities` (rate-limited per cellId) | `[indoor-walk] cellEnt=0xID pos=(x,y,z) parentCell=0xID landblockVisible=B aabbVisible=B cellInVis=B drawn=B` |
| `ACDREAM_PROBE_INDOOR_LOOKUP` | Render-data lookup for cell entities | `WbDrawDispatcher.DrawAccumulated` per cell entity | `[indoor-lookup] cellId=0xID hit=B isSetup=B partCount=N hasEnvCellGeom=B partsHit=N partsMiss=N` |
| `ACDREAM_PROBE_INDOOR_UPLOAD` | WB upload result for envCellId | `WbMeshAdapter.IncrementRefCount` (on first call per id) + a callback hooked into `_meshManager.Tick()` for completion | `[indoor-upload] cellId=0xID requested=true completed=B partsCount=N cellGeomVerts=N error="..."` |
| `ACDREAM_PROBE_INDOOR_XFORM` | Composed world transform for cell-geometry SetupPart | `WbDrawDispatcher` inside the `IsSetup` branch at line 607-621, for partGfxObjId matching `(envCellId | 0x1_00000000UL)` | `[indoor-xform] cellId=0xID cellOrigin=(x,y,z) entityWorld=(...) partTransform=(...) composed=(x,y,z y-axis,z-axis) detExpected≈1 detActual=F` |
| `ACDREAM_PROBE_INDOOR_CULL` | Visibility / cull decision per cell entity | `WbDrawDispatcher.WalkVisibleEntities` (the two filter sites at lines 304-305 and 317-319) | `[indoor-cull] cellEnt=0xID reason="visibleCellIds-miss" or "frustum" or "served" details="..."` |
The five probes can be enabled independently or together. The user's
common case is `ACDREAM_PROBE_INDOOR_ALL=1` which sets all five at
once.
#### Implementation outline
1. **New file** `src/AcDream.Core/Rendering/RenderingDiagnostics.cs` —
five static `bool` properties, each backed by an env-var read at
startup, each runtime-settable from the DebugPanel.
2. **DebugPanel section** — new "Indoor rendering diagnostics" block
in the existing DebugPanel "Diagnostics" group, with one checkbox
per probe + a master "all" toggle.
3. **WbDrawDispatcher edits** — instrument the walk and the IsSetup
draw branch. The walk probe needs to know whether the entity passed
the cell-visibility filter; the cull probe needs the same data.
Cleanest: emit BOTH lines in one place when either probe is on.
4. **WbMeshAdapter edits** — `IncrementRefCount` logs an `[indoor-upload]
requested=true` line when the id is recognized as an EnvCell
(high-bit check `(id & 0xFFFF) >= 0x0100`). On Tick(), when a
completion drains for an envCellId, log the result line with the
actual ObjectMeshData/ObjectRenderData fields.
5. **No GameWindow changes** beyond passing the diagnostics class
into the dispatcher (if not already accessible).
#### Capture procedure
1. Build with the probe instrumentation. `dotnet build` green.
2. Launch with `ACDREAM_PROBE_INDOOR_ALL=1`. Walk to Holtburg Inn,
stand at the doorway, then step inside, then walk around the room.
3. Stop the client, grep `launch.log` for `[indoor-*]` lines.
4. The captured log identifies WHICH hypothesis matches:
- **H1 (null upload)** → `[indoor-upload] completed=false`
- **H2 (empty batches)** → `[indoor-upload] cellGeomVerts=0`
- **H3 (cull bug)** → `[indoor-cull] reason="visibleCellIds-miss"`
- **H4 (double-spawn)** → `[indoor-lookup] partCount` includes
static-object IDs that ALSO appear in `landblock.Entities`
- **H5 (transform double-apply)** → `[indoor-xform] composed`
world position lands at `2 × cellOrigin` instead of `cellOrigin`
- **H6 (MeshRefs structure)** → ruled out; probe data would still
surface it as `hit=true isSetup=true partCount=N` followed by
all `partsHit=0`
### Phase 2 — Fix the specific break (next phase)
Once the probe identifies the failure point, implement the surgical
fix. Likely shapes per hypothesis:
| Hypothesis | Fix shape |
|---|---|
| H1 — WB returns null | Add WB logging or pre-check the dat resolution path in WbMeshAdapter |
| H2 — Empty batches | Investigate WB texture pipeline; possibly a missing texture in the cell's surface list |
| H3 — Cull bug | Fix `ParentCellId` assignment OR loosen the visibility filter for cell entities |
| H4 — Double-spawn | Stop WB from spawning static-object parts in EnvCell setups (filter them in PrepareEnvCellMeshData, or skip acdream's per-stab hydration when WB handles the cell) |
| H5 — Transform double-apply | Replace `MeshRef.PartTransform = cellTransform` with `entity.Position+Rotation = cellPosition` |
| H6 — MeshRefs structure | Already ruled out in §2 |
Phase 2's actual code change is small and well-targeted once Phase 1
gives us a definite answer.
## 4. Why NOT build a separate cell renderer
The original brainstorm proposed adapting `_pendingCellMeshes` data into
WB via a new `UploadCellMesh` adapter method. **That solution is wrong** —
it would duplicate work WB already does, fragment the rendering pipeline,
and bypass WB's existing GPU memory management. Worse, it would hide
whatever the actual bug is, not fix it.
## 5. Edge cases
| Scenario | Behavior |
|---|---|
| Visible during diagnostic capture | Probe is heavy (per-frame, per-entity). Bounded by short walk; runtime-toggle off when done. |
| Probe spam in production | Default OFF, mirrored to DebugPanel. Same pattern as L.2a `ACDREAM_PROBE_RESOLVE` / `ACDREAM_PROBE_CELL`. |
| Concurrent landblock stream | Probe records per frame across all loaded cells — useful for cross-cell comparison ("does cell X load but cell Y not?"). |
## 6. Testing strategy
**Unit tests:** none in Phase 1. The probe is diagnostic, not behavioral.
**Visual verification (user-driven, end-to-end):**
- Add probe, launch client, walk into Holtburg Inn.
- Read probe output to identify which hypothesis matches.
- Brief Phase 2 in a new design (or amend this one) once the failure
point is known.
**Phase 2 unit tests:** depend on the fix shape. If H5 (transform
double-apply), tests verify the world matrix composition. If H3 (cull
bug), tests verify visibility BFS for indoor entities.
## 7. What's NOT in this phase
- Cell collision symptoms — investigated separately.
- Particle/fire emitter integration — already shipped.
- Light registration — already shipped.
- Stab-leak-through-walls — deferred.
## 8. Acceptance criteria
**Phase 1 (this phase):**
- [ ] `AcDream.Core.Rendering.RenderingDiagnostics` static class created
with five `bool` properties + master `IndoorAll` toggle, each backed
by an env-var read at startup and runtime-settable.
- [ ] DebugPanel "Diagnostics" group has a new "Indoor rendering"
subsection with six checkboxes (five probes + master).
- [ ] `WbDrawDispatcher` emits `[indoor-walk]`, `[indoor-lookup]`,
`[indoor-xform]`, `[indoor-cull]` lines when the respective probe
is on. Rate-limited to ~1/sec per cell unless verbose mode active.
- [ ] `WbMeshAdapter` emits `[indoor-upload]` lines for EnvCell IDs:
one `requested` line on first `IncrementRefCount`, one `completed`
line when WB's Tick drains the result (success or failure).
- [ ] `dotnet build` clean. `dotnet test` clean (the diagnostics-only
change should not affect any test).
- [ ] Probe captured at Holtburg Inn confirms which hypothesis matches.
Capture procedure documented in §3 above.
- [ ] Phase 2 design (amended spec or new spec) documents the surgical
fix matched to the identified hypothesis.
**Phase 2 (next phase, driven by Phase 1 output):**
- [ ] `dotnet build` clean, `dotnet test` clean.
- [ ] Visual verification: walking into Holtburg Inn renders interior
floor + walls correctly.
- [ ] Roadmap updated.
- [ ] Probes left in place for future regressions but defaulted off.