fix(render): quiesce dat readers before teardown — kill the shutdown AccessViolation
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>
This commit is contained in:
parent
d0bd28543b
commit
8fadf770fe
2 changed files with 73 additions and 7 deletions
|
|
@ -329,7 +329,15 @@ public sealed class LandblockStreamer : IDisposable
|
|||
if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;
|
||||
_cancel.Cancel();
|
||||
_inbox.Writer.TryComplete();
|
||||
_worker?.Join(TimeSpan.FromSeconds(2));
|
||||
// Generous join: the owner disposes the DatCollection after this, which
|
||||
// unmaps the dats' memory-mapped views — an abandoned worker mid-dat-read
|
||||
// would take the process down with an AccessViolation in
|
||||
// MemoryMappedBlockAllocator.ReadBlock (dat-race investigation 2026-06-09).
|
||||
// Cancellation is honored between jobs, so the wait is bounded by one
|
||||
// landblock load; 15s only ever elapses if the worker is genuinely hung.
|
||||
if (_worker is not null && !_worker.Join(TimeSpan.FromSeconds(15)))
|
||||
Console.Error.WriteLine(
|
||||
"[streamer] worker did not stop within 15s — dat teardown may race an in-flight load");
|
||||
_cancel.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue