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>
248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# 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.
|