Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/docs/Threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ outside the main loop (e.g. during a resize).
`Renderer::WaitAndRender(RenderThread*)` is the render thread's body: it swaps the double-buffered
command queues (`Renderer::SwapQueues`) and executes the submission queue.

**Never `Kick()` while the render thread is `Busy`.** `Set(Kick)` overwrites `Busy`; the render thread
then sets `Idle` when it finishes and the kick is lost. Every kick must follow
`BlockUntilRenderComplete()`. `Terminate()` used to break this rule. When the previous frame was still
running at exit, the final submitted frame never ran on the render thread, and
`Renderer::Shutdown` replayed it on the main thread after the renderer was torn down. The result was an
intermittent exit AV in `CmdSetPipelineLayout` from the nulled `StorageImageClear` layout.
`Terminate()` now waits, pumps that last frame on the render thread, then kicks once more so the thread
sees `IsRunning() == false` and returns.

---

## JobSystem
Expand Down
14 changes: 13 additions & 1 deletion Core/Platform/Linux/LinuxRenderThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,23 @@ namespace Lux {

void RenderThread::Terminate()
{
m_IsRunning = false;
// The frame kicked last may still be executing. Kicking over its Busy state let the render
// thread set Idle and exit with the final submitted frame never run, so Renderer::Shutdown
// replayed that whole frame on the main thread after the renderer was torn down (null
// StorageImageClear layout -> AV in CmdSetPipelineLayout).
BlockUntilRenderComplete();

// Run the final submitted frame on the render thread while the renderer is still alive.
Pump();

m_IsRunning = false;
if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded)
{
// Release the render thread from its wait for the next kick. It executes the empty render
// queue (nothing was submitted since the pump), then sees m_IsRunning and returns.
Kick();
m_RenderThread.Join();
}

s_RenderThreadID = std::thread::id();
}
Expand Down
14 changes: 13 additions & 1 deletion Core/Platform/Windows/WindowsRenderThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,23 @@ namespace Lux {

void RenderThread::Terminate()
{
m_IsRunning = false;
// The frame kicked last may still be executing. Kicking over its Busy state let the render
// thread set Idle and exit with the final submitted frame never run, so Renderer::Shutdown
// replayed that whole frame on the main thread after the renderer was torn down (null
// StorageImageClear layout -> AV in CmdSetPipelineLayout).
BlockUntilRenderComplete();

// Run the final submitted frame on the render thread while the renderer is still alive.
Pump();

m_IsRunning = false;
if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded)
{
// Release the render thread from its wait for the next kick. It executes the empty render
// queue (nothing was submitted since the pump), then sees m_IsRunning and returns.
Kick();
m_RenderThread.Join();
}

s_RenderThreadID = std::thread::id();
}
Expand Down
4 changes: 2 additions & 2 deletions Core/Source/Lux/Core/RenderThread.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ namespace Lux {
~RenderThread();

void Run();
bool IsRunning() const { return m_IsRunning; }
bool IsRunning() const { return m_IsRunning.load(std::memory_order_acquire); }
void Terminate();

void Wait(State waitForState);
Expand All @@ -72,7 +72,7 @@ namespace Lux {

Thread m_RenderThread;

bool m_IsRunning = false;
std::atomic<bool> m_IsRunning = false;

std::atomic<uint32_t> m_AppThreadFrame = 0;
};
Expand Down
Loading