The GL root cause was fixed in fcade06 (the gpu_us query-ring stale
errors). This closes the remaining design debt: a genuinely-failed
UploadMeshData was dropped permanently.
Exact mechanism (traced this session): UploadMeshData's catch returns
null, the staged item is already consumed, and _renderData stays empty -
but the prepared data lingers in _cpuMeshCache, so the #128 EnsureLoaded
re-arm hits PrepareMeshDataAsync's CPU-cache short-circuit
(ObjectMeshManager.cs:448-453) which returns the cached data WITHOUT
re-staging it for upload. The mesh stays invisible until CPU-cache
eviction - session-sticky under low cache pressure (the in-tower
scenario).
Fix: the per-frame Tick drain (WbMeshAdapter) now re-stages a failed
upload for the NEXT frame via ObjectMeshManager.UploadOrRequeue, bounded
by MaxUploadRetries (3). The attempt counter lives on the ObjectMeshData
object so it resets to 0 naturally on re-prepare. Re-stages are
collected and re-enqueued AFTER the drain loop, never inside it, so a
deterministic failure cannot spin the queue within a single frame; past
the cap it gives up with a loud [up-retry] ... giving up line - a
genuine GL defect now surfaces instead of the old silent permanent drop
or an unbounded retry storm. Retail loads content synchronously and has
no such failure mode; this converges the async pipeline toward that
guarantee.
The uncaught GenerateMipmaps path (open-question c) is INTENTIONALLY
left to surface errors - a blanket catch there would mask future real
defects (no-workarounds rule), and its trigger (fcade06) is retired.
No visual gate (robustness). Build green; App.Tests 264 + WbMeshAdapter
tests green. No GL-context test seam exists for the upload path, so the
bounded retry is verified by construction + the regression suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue119UpNullGfxObjDumpTests pins the dat truth: 0x010002B4 = 9 polys,
ALL NoPos, all surfaces Base1Solid; 0x010008A8 = 1 poly, NoPos,
Base1Solid|Translucent. Retail's skipNoTexture never draws either model
(the BR-1 build-time-skip <=> draw-time-skip equivalence), so
ObjectMeshManager's empty render-data cache is the CORRECT terminal state
- the only defect was the alarming "permanently invisible" log line,
reworded into an honest tripwire pointing at the dump test.
Second fact, same test (ShellModel_NoTexturedPolyIsDropped): on the
hall/tower shell 0x010014C3, ZERO textured polys are dropped by the
extraction gates (137/149 draw; the 12 dropped are the known #113
no-draw orphans) - the per-poly GfxObj extraction is exonerated for
building shells, kept green as a regression pin.
Net for #119: the missing tower-stair parts are NOT the up-null pair and
NOT a per-poly extraction drop. Remaining hypothesis space (interior
stair-cell flood admission, or a different model than assumed) needs the
re-gate to identify the exact tower; then the cell set + flood replay
headlessly like #118. ISSUES.md updated.
Suites: App 232, Core 1419+2skip (1416+3 new), UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User gate 2026-06-11: the filter (e46d3d9) removed the phantom staircase
everywhere (verified) but DOORS disappeared across Holtburg - the naive
PosNode/NegNode walk evidently misses polygons some models reference
another way. Doors > phantom stairs: filter application removed; the
CollectDrawingBspPolygonIds helper and the dat-fact tests (hall orphans
0+1, cottage 0..7) stay as apparatus for the holistic building-render
port. First diagnostic for re-landing: run the DrawingBSP histogram on a
door GfxObj. See
docs/research/2026-06-11-building-render-holistic-port-handoff.md.
Branch state after this commit: outdoor-scoped shell clip (927fd8f +
9ce335e) + retail straddle gate (414c3de) + all diagnostics; phantom
staircase VISIBLE again (known, documented); doors functional.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user gate + bisect overturned the coincident-cell attribution: the
phantom staircase persists in the PRE-session build (bisect screenshot at
the hall wall) and is drawn by the ENTITY pipeline, untouched by any clip.
Root cause (dat-proven, DumpHallModel_PolyFlagHistogram): retail renders a
GfxObj by TRAVERSING its drawing BSP (D3DPolyRender); polygons present in
the Polygons dictionary but referenced by NO DrawingBSP node are never
drawn - they are physics/no-draw geometry. The Holtburg meeting hall
(0x010014C3) keeps its exterior stair-ramp as dictionary polys 0+1: in
the PhysicsBSP (ACE walks The Sentry on it at z 117-118; invisible-but-
walkable in retail) but orphaned from the draw tree (true at ALL degrade
levels - the LOD theory is dead, Degrades[0] IS the base model). The hill
cottage (0x01000827) carries 8 such orphans. Our extraction iterated the
dictionary -> drew the collision skeleton: the wall staircase up close,
the flying stairs over the cottage roofline from afar (orphan ramp spans
world 221-232 at z 116-124.5; visible over the cottage roof from the west).
Fix: PrepareGfxObjMeshData filters to CollectDrawingBspPolygonIds(gfxObj)
when a drawing BSP exists; models without one draw everything (unchanged).
Physics untouched (collision keeps the full physics set - retail parity).
CellStruct extraction not touched (different conventions; no orphan
evidence there yet).
Dat-backed pins: Issue113DrawingBspFilterTests (hall orphans == 0+1,
cottage orphans == 0..7). Suites: App 226 / Core 1392 + the 4
pre-existing #99-era failures / UI 420 / Net 294.
Note: the earlier shell-clip enable (927fd8f, scoped 9ce335e) remains
correct and orthogonal - it crops interior CELL geometry to apertures
outdoors; this commit removes the phantom SHELL geometry at its source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: TextureAtlasManager.AddTexture only STAGES texture content (PBO
write + ManagedGLTextureArray._pendingUpdates); the actual TexSubImage3D
copies + mipmap regeneration happen in ProcessDirtyUpdates, which WB drives
once per frame via ObjectMeshManager.GenerateMipmaps() from its render loop
(WB GameScene.cs:975, just before the opaque pass). GameScene is the file we
replaced with GameWindow, so the call site was silently dropped — staged
updates only reached the GPU as a side effect of PBO growth (UpdateLayerInternal
flushes pending updates before orphaning the PBO). Every layer staged after an
array's LAST growth kept undefined TexStorage3D content behind a valid,
resident bindless sampler handle: white/garbage walls, zh==0, dat tripwires
silent — exactly the #105 signature. Only ObjectRenderBatch.BindlessTextureHandle
consumers are affected (EnvCellRenderer cell shells = indoor walls); entities
resolve via TextureCache (immediate TexImage2D) and terrain via TerrainAtlas
(immediate GenerateMipmap), which is why only indoor walls ever struck.
Fix: WbMeshAdapter.Tick() now calls _meshManager.GenerateMipmaps() after the
staged-upload drain — Tick runs before all draw passes (GameWindow OnRender),
the exact WB-equivalent position.
Evidence (ACDREAM_PROBE_TEXFLUSH=1 apparatus, kept env-gated):
- pre-fix (texflush-prefix.log): pending updates climb 0->48->...->142 and
park at 126 across 34/34 atlas arrays at standstill, forever (19 heartbeats);
brief dips only at PBO-growth crossings — the broken contract live.
- post-fix (texflush-postfix.log): every line after=0 — staged updates drain
the same frame, all 34 arrays clean.
Intermittency explained: background decode-completion order shuffles which
textures land in the never-flushed tail; whether a visible wall samples one is
per-run luck. Also explains the #110 correlation: znear=0.1 makes close-up
geometry newly visible -> more prepare/upload pressure indoors -> bigger tail
-> higher strike probability. The near plane is mechanism-innocent (re-land
follows as its own commit).
Baseline maintained: App 223 / UI 420 / Net 294 / Core 1377 green + 4
pre-existing #99-era failures + 1 skip; CornerFloodReplayTests (5) and
CameraCornerSealReplayTests (2) gates green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live session evidence narrowed #105 decisively: a wall section rendered as the
sky/clear color from session start, HAD collision (cell + physics fully
loaded), zero round-1 tripwires fired (all dat reads succeeded), and the hole
showed terrain or clear color depending on camera angle. So the wall mesh was
built and then lost between mesh-build and draw. New tripwires cover the
loss candidates in that window (all print ONLY on anomaly):
- [geom-null] ProcessQueueAsync EnvCell branch resolved null, with the
failing sub-step (prepare-null / cellstruct-missing /
env-read-failed) — a null here means the DEDUPLICATED cell
geometry never renders for ANY cell that shares it, and
nothing retries (RegisterCell fire-and-forgets the task).
- [geom-misroute] an EnvCell geom id (bit 33) whose pending request vanished
fell through to the generic path, where its hash-derived
low bits resolve to nothing -> silent null.
- [up-null] UploadGfxObjMeshData returned null and the EMPTY substitute
was cached in _renderData forever (permanently invisible).
Pair with the existing one-shot draw-side audit (ACDREAM_A8_AUDIT=1, light:
one line per unique cell/geom pair, prints renderData=null + bindless-handle
status) for full attribution on the next occurrence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ObjectMeshManager.Dispose never stopped its Task.Run(ProcessQueueAsync) decode
workers, and LandblockStreamer.Dispose abandoned its worker after a 2s join.
GameWindow.OnClosing then disposed the DatCollection, which unmaps the dats''
memory-mapped views (MemoryMappedBlockAllocator.DestroyMappedFile nulls
_viewPtr) — a worker still inside ReadBlock dereferences the dead view pointer:
an uncatchable AccessViolationException with ReadBlock on the stack, firing on
close/relaunch during decode storms. This is the recorded crash signature from
the 2026-06-09 white-walls session.
- ObjectMeshManager.Dispose: set IsDisposed under the queue lock, cancel+drain
pending requests, then wait (<=10s) for _activeWorkers==0; loud LogError if
workers outlive the wait. ProcessQueueAsync re-checks IsDisposed per dequeue;
Prepare*Async entries + enqueue blocks early-out when disposed.
- LandblockStreamer.Dispose: join 2s -> 15s with a loud [streamer] line on
timeout (cancellation honored between jobs; one landblock load bounds it).
- Also includes the [tex-skip] tripwire lines on ObjectMeshManager''s five
silent dat-miss exits (GfxObj + CellStruct texture chains) — part of the
white-walls attribution net (#105), zero output when healthy.
Verified: 3x close-mid-decode-storm smoke (in-world at ~8s, WM_CLOSE at ~11s),
clean exits, no crash signatures, no quiesce timeouts. Full suite: 294+218+420
green; Core 1338 green + 4 pre-existing physics failures (reproduced at bare
HEAD, unrelated). Investigation:
docs/research/2026-06-09-dat-reader-thread-safety-investigation.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.
- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
(load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).
Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.
Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End of Phase O extraction. Final cleanup:
- Dropped <ProjectReference> entries to WorldBuilder.Shared and
Chorizite.OpenGLSDLBackend from both AcDream.App.csproj and
AcDream.Core.csproj.
- Added Chorizite.Core NuGet PackageReference to AcDream.Core.csproj
(needed by Core.Rendering.Wb.TextureHelpers for TextureFormat enum;
previously transitive through the WB project ref).
- Added BCnEncoder.Net.ImageSharp (1.1.2) + SixLabors.ImageSharp (3.1.12)
as direct PackageReferences to AcDream.App.csproj — previously transitive
via Chorizite.OpenGLSDLBackend project; used directly by ObjectMeshManager.
Item A (BaseObjectRenderManager static fields):
- Inlined CurrentAtlas/CurrentVAO/CurrentIBO into a new RenderStateCache.cs
static class (AcDream.App.Rendering.Wb namespace) — the 4 consumers
(ManagedGLIndexBuffer, ManagedGLTexture, ManagedGLTextureArray, ParticleBatcher)
all reference RenderStateCache.* instead of BaseObjectRenderManager.*.
- Dropped using Chorizite.OpenGLSDLBackend.Lib from all 4 consumers and from
WbDrawDispatcher (which had it only as a dead import).
Item B (ActiveParticleEmitter.ObjectLandblock):
- ObjectLandblock? erased to object?; WorldBuilder.Shared.Models.ObjectId? erased
to ulong? — both fields are stored but never read by any consumer in our codebase.
- Dropped both WB using directives from ActiveParticleEmitter.cs.
Item C (IDatReaderWriter / IDatDatabase):
- Verbatim copy of both interfaces into IDatReaderWriter.cs in
AcDream.App.Rendering.Wb namespace — DatCollectionAdapter and ObjectMeshManager
already live in that namespace, so no using changes needed.
- Dropped using WorldBuilder.Shared.Services from DatCollectionAdapter.cs and
ObjectMeshManager.cs.
Additional extractions required by the reference drop:
- GeometryUtils.cs: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils
(float-precision overloads only; Vector3d double-precision overloads omitted —
ObjectMeshManager uses only the float versions).
- Dropped using WorldBuilder.Shared.Lib from ObjectMeshManager.cs.
WbMeshAdapter.cs cleanup (spec O-D12):
- Deleted _wbDats (DefaultDatReaderWriter) field + ctor init + Dispose call.
- Deleted the [indoor-upload] NULL_RESULT diagnostic block (lines ~205-262) —
its Phase 2 cell-resolution investigation is complete; its _wbDats.ResolveId
dependency goes with this commit.
- Deleted _pendingEnvCellRequests field + isPendingEnvCell tracking in Tick().
- Simplified Tick() to a clean drain loop.
Deleted SplitFormulaDivergenceTest.cs — one-time N.5b data-collection sweep;
job done.
Verified acceptance criteria:
- Zero <ProjectReference> to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj.
- Zero 'using WorldBuilder.*' / 'using Chorizite.OpenGLSDLBackend.*' in src/.
- DefaultDatReaderWriter referenced in zero places in src/ (comments only).
Build green (0 warnings, 0 errors).
Tests: 1154 total (-1 from deleted SplitFormulaDivergenceTest), 1146 pass,
8 pre-existing failures (unchanged from baseline — physics/input tests
unrelated to this change).
Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four fixes from T4 spec review:
1. Extracted InstanceData.cs (14-line struct) verbatim to
src/AcDream.App/Rendering/Wb/InstanceData.cs (per O-D1).
2. ObjectMeshManager.cs: replaced `using Chorizite.OpenGLSDLBackend.Lib;`
with `using AcDream.Core.Rendering.Wb;` (TextureHelpers comes from
our T2 Core extraction; InstanceData comes from new T4 cleanup).
3. EmbeddedResourceReader.GetEmbeddedResource promoted from `internal`
to `public` per O-D9 intent (the type promotion only changed the
class signature in T3; this finishes the spec).
4. OpenGLGraphicsDevice.cs: removed stale T3 interim comment at
lines 142-145 — T4 resolved the ParticleBatcher construction
via post-ctor assignment in WbMeshAdapter.cs:78.
Build green; tests green (1147 passing, 8 pre-existing failures
baseline maintained).
Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase O Task 4: extract the WB mesh pipeline (ObjectMeshManager + 7 support files)
from references/WorldBuilder into src/AcDream.App/Rendering/Wb/ and bridge dat I/O
through our DatCollection via a thin DatCollectionAdapter.
O-D7 adapter path taken: ObjectMeshManager has 26 _dats.X call sites (threshold 20),
so a DatCollectionAdapter : IDatReaderWriter is introduced rather than refactoring
ObjectMeshManager's internal dat access directly.
Files added (verbatim copies, namespace-only changes):
- ObjectMeshManager.cs — mesh pipeline hub; IDatReaderWriter field satisfied by adapter
- GlobalMeshBuffer.cs — single global VAO/VBO/IBO manager
- EdgeLineBuilder.cs — wireframe edge geometry from CellStruct polygons
- ModernRenderData.cs — ModernBatchData + LandblockMdiCommand structs
- TextureAtlasManager.cs — texture array grouping by (Width, Height, Format)
- ParticleBatcher.cs — GPU particle batching; T4 interim uses BaseObjectRenderManager
static fields from Chorizite.OpenGLSDLBackend.Lib (stays until T7)
- ParticleEmitterRenderer.cs — per-emitter particle lifecycle + rendering
- ActiveParticleEmitter.cs — wrapper holding renderer + part index + local offset
- DatCollectionAdapter.cs — NEW: bridges DatCollection → IDatReaderWriter; implements
ResolveId() via DatDatabase.TypeFromId + Tree.TryGetFile in HighRes→Portal→Language→Cell
order matching DefaultDatReaderWriter; DatDatabaseWrapper wraps DatDatabase as IDatDatabase
WbMeshAdapter.cs changes (T4 Step 6):
- _graphicsDevice switched from Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice to
extracted AcDream.App.Rendering.Wb.OpenGLGraphicsDevice
- ParticleBatcher = new ParticleBatcher(_graphicsDevice) restored (T3 had null! placeholder)
- ObjectMeshManager now constructed with new DatCollectionAdapter(dats) instead of _wbDats
- _wbDats field + its construction + disposal + [indoor-upload] NULL_RESULT diagnostic block
left intact — T7 cleanup removes these once WorldBuilder project ref is dropped
EmbeddedResourceReader.cs: replaced assembly manifest lookup (wrong prefix for our assembly)
with disk-based lookup mapping "Shaders.Particle.vert" → Rendering/Shaders/wb_particle.vert;
consistent with all other acdream shaders.
wb_particle.vert / wb_particle.frag: WB particle shaders copied verbatim with wb_ prefix
to distinguish from acdream's own particle.vert.
OpenGLGraphicsDevice.cs: ParticleBatcher property type updated to extracted ParticleBatcher;
setter changed from private to internal so WbMeshAdapter (same assembly) can assign post-ctor.
Build: green (0 errors, 0 warnings in AcDream.App).
Tests: 1147+8 baseline maintained (8 pre-existing failures unchanged).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>