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

13 KiB
Raw Blame History

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 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 — 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) 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). 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. 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)`
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 editsIncrementRefCount 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.