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>