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>
13 KiB
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:
-
WB silently fails to build the
ObjectMeshData.PrepareEnvCellMeshDatareturns null when the Environment dat can't resolve, or whenPrepareCellStructMeshDatareturns null (texture issues, surface resolution failure). WB doesn't log; the failure is invisible. -
SetupParts.cellGeomIdis uploaded but its texture batches are empty.UploadGfxObjMeshDatareturning null at line 675 is treated as a non-fatal substitution — the render data has no draw batches, dispatcher silently draws nothing. -
Cell entity is culled before reaching the dispatcher.
visibleCellIdsfilter atWbDrawDispatcher.cs:317-319rejects entities whoseParentCellIdisn't in the visible set. If the cell entity'sParentCellId == envCellIdbut the visibility BFS doesn't include the player's current cell (becauseFindCameraCellreturns null when camera is in third-person above the building, etc.), the cell entity is skipped. -
Double-spawn conflict between WB's static-object SetupParts and acdream's per-stab entity hydration.
PrepareEnvCellMeshDataiteratesenvCell.StaticObjectsand adds each as a SetupPart. Meanwhile acdream already hydrates the same static objects as separateWorldEntityinstances atGameWindow.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. -
Transform composition bug.
ComposePartWorldMatrix(entityWorld, meshRef.PartTransform, partTransform)— if our cell entity'smeshRef.PartTransform == cellTransformand WB'spartTransformalready bakes the cell origin, the floor lands at2 × cellOrigin, far below or beside the actual cell. The user would describe this as "missing" because the floor is now outside the visible frustum. -
The cell entity's
MeshRefsonly has one entry, but WB expects multiple. The dispatcher iteratesentity.MeshRefs, but each MeshRef gets its ownTryGetRenderData(meshRef.GfxObjId)call. For cell entities we haveMeshRefs = { MeshRef(envCellId, cellTransform) }. When the lookup returns anIsSetup=truerender 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
- New file
src/AcDream.Core/Rendering/RenderingDiagnostics.cs— five staticboolproperties, each backed by an env-var read at startup, each runtime-settable from the DebugPanel. - DebugPanel section — new "Indoor rendering diagnostics" block in the existing DebugPanel "Diagnostics" group, with one checkbox per probe + a master "all" toggle.
- 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.
- WbMeshAdapter edits —
IncrementRefCountlogs an[indoor-upload] requested=trueline 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. - No GameWindow changes beyond passing the diagnostics class into the dispatcher (if not already accessible).
Capture procedure
- Build with the probe instrumentation.
dotnet buildgreen. - Launch with
ACDREAM_PROBE_INDOOR_ALL=1. Walk to Holtburg Inn, stand at the doorway, then step inside, then walk around the room. - Stop the client, grep
launch.logfor[indoor-*]lines. - 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] partCountincludes static-object IDs that ALSO appear inlandblock.Entities - H5 (transform double-apply) →
[indoor-xform] composedworld position lands at2 × cellOrigininstead ofcellOrigin - H6 (MeshRefs structure) → ruled out; probe data would still
surface it as
hit=true isSetup=true partCount=Nfollowed by allpartsHit=0
- H1 (null upload) →
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.RenderingDiagnosticsstatic class created with fiveboolproperties + masterIndoorAlltoggle, 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).
WbDrawDispatcheremits[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.WbMeshAdapteremits[indoor-upload]lines for EnvCell IDs: onerequestedline on firstIncrementRefCount, onecompletedline when WB's Tick drains the result (success or failure).dotnet buildclean.dotnet testclean (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 buildclean,dotnet testclean.- Visual verification: walking into Holtburg Inn renders interior floor + walls correctly.
- Roadmap updated.
- Probes left in place for future regressions but defaulted off.