GV7-2: drain the back queue at mid-frame MTGS-thread device seams

Sync-point audit fixes for pipelined mode: GSUpdateConfig's non-reopen
branches, SaveSnapshotToMemory, and capture begin/end all touch renderer
or GSDevice state from the MTGS thread while the back thread may be
mid-draw on the same device. Each now drains queued records first (the
front only parses on the MTGS thread, so nothing new queues during the
operation). DrainBackQueue becomes public for the GS.cpp seam.

Audit conclusions (no code needed): WaitGS callers never touch back-owned
state EE-side (state access travels through MTGS ring packets into already
drained seams); SIGNAL/FINISH are GIFRegHandlerNull in GSState — CSR
semantics are entirely EE-side; a back-thread assert failure aborts the
process on Linux, so it cannot deadlock the front.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes identical to GV-0
baselines, modes 0 and 3, vk and sw.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-19 14:58:49 -07:00
co-authored by Claude
parent e7736345bb
commit 5aeb3dd8bc
3 changed files with 20 additions and 1 deletions
+6
View File
@@ -864,6 +864,12 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
if (!g_gs_renderer)
return;
// GV7-2: everything below mutates renderer/device state the back thread may
// be reading mid-draw (settings, ImGui font textures, TC purges). The front
// only parses on this (MTGS) thread, so a single drain up front quiesces the
// back thread for the whole apply.
g_gs_renderer->DrainBackQueue();
// Handle OSD scale changes by pushing a window resize through.
if (new_config.OsdScale != old_config.OsdScale)
ImGuiManager::RequestScaleUpdate();
+5 -1
View File
@@ -47,6 +47,11 @@ public:
GSBackQueue::Channel* GetBackChannel() { return m_chan; }
bool IsBackThreadRunning() const { return m_chan->consumer_running; }
// GV7-2: external sync points (settings apply, screenshot-to-memory) that
// touch renderer/device state from the MTGS thread must drain queued records
// first — the back thread may otherwise be mid-draw on the same GSDevice.
void DrainBackQueue();
static constexpr int GetSaveStateSize(int version);
private:
@@ -631,7 +636,6 @@ public:
void StartBackThread();
void StopBackThread();
void DrainBackQueue();
void BackThreadLoop();
void ExecRecordSlot(const GSBackQueue::RecordSlot& slot);
virtual void ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec);
+9
View File
@@ -1056,6 +1056,9 @@ void GSSetDisplayAlignment(GSDisplayAlignment alignment)
bool GSRenderer::BeginCapture(std::string filename, const GSVector2i& size)
{
// GV7-2: capture start/stop can run mid-frame on the MTGS thread; teardown
// frees download textures on the device the back thread may be drawing on.
DrainBackQueue();
const GSVector2i capture_resolution = (size.x != 0 && size.y != 0) ?
size :
(GSConfig.VideoCaptureAutoResolution ?
@@ -1069,6 +1072,7 @@ bool GSRenderer::BeginCapture(std::string filename, const GSVector2i& size)
void GSRenderer::EndCapture()
{
DrainBackQueue(); // see BeginCapture
GSCapture::EndCapture();
}
@@ -1085,6 +1089,11 @@ bool GSRenderer::IsIdleFrame() const
bool GSRenderer::SaveSnapshotToMemory(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders,
u32* width, u32* height, std::vector<u32>* pixels)
{
// GV7-2: mid-frame screenshot issues device calls (CreateRenderTarget /
// StretchRect) on the MTGS thread; the back thread may be mid-draw on the
// same device. The vsync-path callers are already post-drain (no-op there).
DrainBackQueue();
GSTexture* const current = g_gs_device->GetCurrent();
if (!current)
{