diff --git a/pcsx2/GS/GSBackQueue.h b/pcsx2/GS/GSBackQueue.h index 6c853d0b0e..caeb68f93b 100644 --- a/pcsx2/GS/GSBackQueue.h +++ b/pcsx2/GS/GSBackQueue.h @@ -11,9 +11,12 @@ #include "GS/GSVertexKick.h" #include "GS/Renderers/Common/GSVertex.h" +#include "common/Threading.h" + #include #include #include +#include // GV-7: self-contained records crossing the GS front (GIF parse / vertex kick / // draw buffering) → back (local memory, texture cache, draw, present) boundary. @@ -354,4 +357,35 @@ namespace GSBackQueue static_assert(std::is_trivially_copyable_v); using RecordRing = SpscRing; + + // GV7-1d-ii: everything shared between the producing (front) and consuming + // (back) sides of the split. In single-object modes the GSState uses its own + // channel; under the two-object pipelined split the front parser object + // points at the back object's channel, so records, pool nodes, and drain + // waits all target one shared instance. The channel's storage owner (the + // back object) frees the pooled arrays in its destructor; the producer must + // be destroyed or drained first. + struct Channel + { + RecordRing ring; + Threading::WorkSema sema; + + // Set while the back thread is running. Read/written only on the MTGS + // thread (start/stop/drain all happen there), so a plain bool is enough. + bool consumer_running = false; + + // Draw-node pool: the producer acquires (free ring first, then arena + // growth up to the cap, then backpressure), the consumer releases after + // the draw executes. Free-ring capacity == arena cap, so Release can + // never fail. + static constexpr u32 kMaxDrawNodes = 64; + std::vector draw_arena; + SpscRing draw_free; + + // Transfer payload pool: the producer stages into the current node, the + // consumer releases rotated-out nodes via RELEASE_PAYLOAD records. + static constexpr u32 kMaxPayloadNodes = 8; + std::vector payload_arena; + SpscRing payload_free; + }; } // namespace GSBackQueue diff --git a/pcsx2/GS/GSState.cpp b/pcsx2/GS/GSState.cpp index 2a6e78b6e2..34911153ae 100644 --- a/pcsx2/GS/GSState.cpp +++ b/pcsx2/GS/GSState.cpp @@ -108,11 +108,7 @@ GSState::GSState() { Console.WriteLn("GS: back-thread mode %d (record path active).", static_cast(GSConfig.BackThreadMode)); - // Adopt m_tr's staging buffer as payload node 0 — from here on - // m_tr.buff always aliases the current node's buffer. - GSBackQueue::PayloadNode* node = new GSBackQueue::PayloadNode{m_tr.buff}; - m_payload_arena.push_back(node); - m_tr_payload_node = node; + AdoptTransferBuffer(); if (GSConfig.BackThreadMode >= GSBackThreadMode::Lockstep) { @@ -176,8 +172,10 @@ GSState::~GSState() _aligned_free(m_draw_index.buff); // GV7-1c: every mode drains before teardown, so all pool nodes hold their - // own arrays here (records in flight would alias them otherwise). - for (GSBackQueue::DrawNode* node : m_draw_node_arena) + // own arrays here (records in flight would alias them otherwise). Only this + // object's own channel storage is freed — a front object pointing at the + // back's channel has empty arenas of its own. + for (GSBackQueue::DrawNode* node : m_chan_storage.draw_arena) { if (node->vb.buff) _aligned_free(node->vb.buff); @@ -192,7 +190,7 @@ GSState::~GSState() // ~GSTransferBuffer must not free it a second time. if (m_back_records) m_tr.buff = nullptr; - for (GSBackQueue::PayloadNode* node : m_payload_arena) + for (GSBackQueue::PayloadNode* node : m_chan_storage.payload_arena) { _aligned_free(node->buff); delete node; @@ -462,14 +460,14 @@ GSBackQueue::DrawNode* GSState::AcquireDrawNode() // the back thread. for (;;) { - if (GSBackQueue::DrawNode** slot = m_draw_node_free.Peek()) + if (GSBackQueue::DrawNode** slot = m_chan->draw_free.Peek()) { GSBackQueue::DrawNode* node = *slot; - m_draw_node_free.Pop(); + m_chan->draw_free.Pop(); return node; } - if (m_draw_node_arena.size() < MAX_DRAW_NODES) + if (m_chan->draw_arena.size() < GSBackQueue::Channel::kMaxDrawNodes) break; std::this_thread::yield(); @@ -486,31 +484,41 @@ GSBackQueue::DrawNode* GSState::AcquireDrawNode() if (!node->vb.buff || !node->vb.buff_copy || !node->ib.buff) pxFailRel("GS: draw-node pool allocation failed"); node->vb.maxcount = m_vertex->maxcount; - m_draw_node_arena.push_back(node); + m_chan->draw_arena.push_back(node); return node; } void GSState::ReleaseDrawNode(GSBackQueue::DrawNode* node) { // Cannot fail: the free ring's capacity equals the arena cap. - GSBackQueue::DrawNode** slot = m_draw_node_free.BeginPush(); + GSBackQueue::DrawNode** slot = m_chan->draw_free.BeginPush(); pxAssert(slot); *slot = node; - m_draw_node_free.CommitPush(); + m_chan->draw_free.CommitPush(); +} + +void GSState::AdoptTransferBuffer() +{ + // Run by the staging object at construction: m_tr's original heap buffer + // becomes payload node 0, and from here on m_tr.buff always aliases the + // current node's buffer. The channel's storage owner frees it. + GSBackQueue::PayloadNode* node = new GSBackQueue::PayloadNode{m_tr.buff}; + m_chan->payload_arena.push_back(node); + m_tr_payload_node = node; } GSBackQueue::PayloadNode* GSState::AcquirePayloadNode() { for (;;) { - if (GSBackQueue::PayloadNode** slot = m_payload_free.Peek()) + if (GSBackQueue::PayloadNode** slot = m_chan->payload_free.Peek()) { GSBackQueue::PayloadNode* node = *slot; - m_payload_free.Pop(); + m_chan->payload_free.Pop(); return node; } - if (m_payload_arena.size() < MAX_PAYLOAD_NODES) + if (m_chan->payload_arena.size() < GSBackQueue::Channel::kMaxPayloadNodes) break; std::this_thread::yield(); @@ -521,7 +529,7 @@ GSBackQueue::PayloadNode* GSState::AcquirePayloadNode() static_cast(_aligned_malloc(alloc_size, 32))}; if (!node->buff) pxFailRel("GS: payload pool allocation failed"); - m_payload_arena.push_back(node); + m_chan->payload_arena.push_back(node); return node; } @@ -548,15 +556,16 @@ void GSState::RotateTransferPayload() void GSState::ExecReleasePayloadRecord(const GSBackQueue::ReleasePayloadRecord& rec) { // Cannot fail: the free ring's capacity equals the arena cap. - GSBackQueue::PayloadNode** slot = m_payload_free.BeginPush(); + GSBackQueue::PayloadNode** slot = m_chan->payload_free.BeginPush(); pxAssert(slot); *slot = rec.node; - m_payload_free.CommitPush(); + m_chan->payload_free.CommitPush(); } void GSState::StartBackThread() { m_back_thread_exit.store(false, std::memory_order_release); + m_chan->consumer_running = true; m_back_thread = std::thread(&GSState::BackThreadLoop, this); Console.WriteLn("GS: back thread started (%s).", m_back_lockstep ? "lockstep" : "pipelined"); } @@ -566,17 +575,20 @@ void GSState::StopBackThread() if (!m_back_thread.joinable()) return; - m_back_sema.WaitForEmpty(); + m_chan->sema.WaitForEmpty(); m_back_thread_exit.store(true, std::memory_order_release); - m_back_sema.NotifyOfWork(); + m_chan->sema.NotifyOfWork(); m_back_thread.join(); + m_chan->consumer_running = false; m_back_queued = false; } void GSState::DrainBackQueue() { - if (m_back_queued) - m_back_sema.WaitForEmpty(); + // Keyed on the channel, not this object's producer flag, so drains work + // from either side of the two-object split. + if (m_chan->consumer_running) + m_chan->sema.WaitForEmpty(); } void GSState::BackThreadLoop() @@ -585,15 +597,15 @@ void GSState::BackThreadLoop() for (;;) { - m_back_sema.WaitForWorkWithSpin(); + m_chan->sema.WaitForWorkWithSpin(); if (m_back_thread_exit.load(std::memory_order_acquire)) break; - while (GSBackQueue::RecordSlot* slot = m_back_ring.Peek()) + while (GSBackQueue::RecordSlot* slot = m_chan->ring.Peek()) { ExecRecordSlot(*slot); - m_back_ring.Pop(); + m_chan->ring.Pop(); } } } diff --git a/pcsx2/GS/GSState.h b/pcsx2/GS/GSState.h index ca4482ae52..92d997d2cc 100644 --- a/pcsx2/GS/GSState.h +++ b/pcsx2/GS/GSState.h @@ -553,28 +553,32 @@ public: // executor tails against live state; any other mode builds records. bool m_back_records = false; + // GV7-1d-ii: the front<->back channel (record ring + wake semaphore + pool + // arenas/free rings, GSBackQueue.h). Single-object modes use this object's + // own storage; the two-object pipelined split points the front parser + // object's m_chan at the back object's channel. The destructor frees + // m_chan_storage's pooled arrays — only ever this object's own storage, so + // a front pointing elsewhere frees nothing it doesn't own. + GSBackQueue::Channel m_chan_storage; + GSBackQueue::Channel* m_chan = &m_chan_storage; + // GV7-1c: draw-node pool. Acquire is front-side (free ring first, then arena // growth up to the ring capacity, then backpressure); Release is the consume // site (inline modes: FlushPrim right after the executor returns; pipelined: - // the back thread after DrawRecordTail). Arena entries are front-owned and - // freed in the destructor — safe because every mode drains before teardown. - static constexpr u32 MAX_DRAW_NODES = 64; - std::vector m_draw_node_arena; - GSBackQueue::SpscRing m_draw_node_free; + // the back thread after DrawRecordTail). GSBackQueue::DrawNode* AcquireDrawNode(); void ReleaseDrawNode(GSBackQueue::DrawNode* node); // GV7-1c: transfer payload pool (record modes only; mode 0 keeps // GSTransferBuffer's own allocation untouched). m_tr.buff aliases the // current node's 4MB buffer; RotateTransferPayload runs at transfer Init and - // swaps to a fresh node once records reference the current one. The ctor - // adopts m_tr's original buffer as node 0 (the dtor nulls m_tr.buff before - // the arena walk so it isn't freed twice). - static constexpr u32 MAX_PAYLOAD_NODES = 8; - std::vector m_payload_arena; - GSBackQueue::SpscRing m_payload_free; + // swaps to a fresh node once records reference the current one. + // AdoptTransferBuffer (run by the staging object at construction) hands + // m_tr's original buffer to the channel as node 0 (the dtor nulls m_tr.buff + // before the arena walk so it isn't freed twice). GSBackQueue::PayloadNode* m_tr_payload_node = nullptr; bool m_tr_payload_referenced = false; + void AdoptTransferBuffer(); GSBackQueue::PayloadNode* AcquirePayloadNode(); void RotateTransferPayload(); void ExecReleasePayloadRecord(const GSBackQueue::ReleasePayloadRecord& rec); @@ -589,8 +593,6 @@ public: // the MTGS thread and HW draws would issue GL calls from the wrong thread. bool m_back_queued = false; bool m_back_lockstep = false; - GSBackQueue::RecordRing m_back_ring; - Threading::WorkSema m_back_sema; std::thread m_back_thread; std::atomic m_back_thread_exit{false}; @@ -606,13 +608,13 @@ public: { for (;;) { - GSBackQueue::RecordSlot* slot = m_back_ring.BeginPush(); + GSBackQueue::RecordSlot* slot = m_chan->ring.BeginPush(); if (slot) { slot->type = type; std::memcpy(slot->As(), &rec, sizeof(T)); - m_back_ring.CommitPush(); - m_back_sema.NotifyOfWork(); + m_chan->ring.CommitPush(); + m_chan->sema.NotifyOfWork(); break; } std::this_thread::yield(); // ring full — backpressure @@ -624,7 +626,7 @@ public: // fps on MQ65 with plain WaitForEmpty) — it's the bisect rung, not a // shipping mode. if (m_back_lockstep) - m_back_sema.WaitForEmptyWithSpin(); + m_chan->sema.WaitForEmptyWithSpin(); } GSVector4i GetTEX0Rect(GSDrawingContext prev_ctx);