Skip to content

Fix intermittent exit AV: RenderThread::Terminate kicked over a busy frame - #32

Merged
sheazywi merged 2 commits into
devfrom
fix/shutdown-av
Oct 19, 2026
Merged

sheazywi merged 2 commits into
devfrom
fix/shutdown-av

Conversation

@sheazywi

Copy link
Copy Markdown
Member

What

RenderThread::Terminate() now waits for the in-flight frame before its final pump. The last submitted frame then runs on the render thread, and a final kick lets the thread exit. Applied to both the Windows and Linux render threads. m_IsRunning is now std::atomic<bool>.

Why

This fixes the intermittent 0xC0000005 in Release Editor.exe at exit. It hit 1–5 of every 16 golden-harness launches, and also some manual closes during a run.

Terminate() used to do m_IsRunning = false; Pump(); with no wait first. If the render thread was still Busy with frame N-1, Pump()'s Set(Kick) overwrote Busy. The render thread then set Idle, saw !IsRunning() and returned, so frame N never ran on it. Renderer::Shutdown then replays the leftover command queue on the main thread. By then StorageImageClear::Shutdown has nulled s_Layout and s_RendererData has been deleted. CmdSetPipelineLayout(*s_Layout) reads null+0x148, and the process faults. The fault RVA (0x605D21) disassembles to exactly that read inside the inlined CommandBufferVK::SetPipelineLayout.

Whether it fires depends on whether the render thread has finished frame N-1 by the time ~Application reaches Terminate. That is why it never reproduced under a debugger.

Neither interleaving can deadlock after the final kick. If the render thread is waiting, it runs the already-executed (empty) queue and exits. If it has already seen !IsRunning(), it has exited and the kick is a no-op. Nothing waits for Idle after that kick.

Verification

  • Built Release and Debug (Windows).
  • golden_run.py --scenes Benchmark --features --config release: 16/16 OK on the fixed build. No Editor.exe AV in the Application event log, and no CoreCLR dumps (DOTNET_DbgEnableMiniDump=1). Baseline builds the same day crashed 5/16 (p1B), 1/16 (p1A), plus one exit-time AV (p1B2). The p1B "mid-run" deaths were manual window closes, which take the same Terminate path.
  • Linux: the same edit was mirrored in LinuxRenderThread.cpp, but not compiled on Linux.

Regeneration

Not needed (no files added or removed).

Docs

.claude/docs/Threading.md: added the "never Kick() while the render thread is Busy" rule and the Terminate sequence.

…minate

Terminate() kicked while the render thread could still be Busy with the
previous frame. Set(Kick) overwrote Busy, the render thread set Idle when done
and exited, so the last submitted frame never ran on it and Renderer::Shutdown
replayed the whole frame on the main thread after the renderer was torn down.

Wait for Idle first, pump the last frame on the render thread, then kick once
more so it observes IsRunning() == false and returns. m_IsRunning is atomic.
Copilot AI balanced review requested due to automatic review settings October 19, 2026 18:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sheazywi
sheazywi merged commit 47a3970 into dev Oct 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants