mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
GV7-1d-ii-a: extract the front<->back channel from GSState
Move the record ring, wake semaphore, and both pool arenas/free rings into GSBackQueue::Channel. Each GSState owns channel storage and works through a m_chan pointer (defaulting to its own storage), so the upcoming two-object pipelined split can aim a front parser object at the back object's channel without touching any record or pool logic. DrainBackQueue keys on the channel's consumer_running flag instead of the producer flag, making drains work from either side; payload node-0 adoption becomes an explicit AdoptTransferBuffer() run by the staging object. The destructor frees only its own channel storage. No behavior change in any mode. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
6d998af9ff
commit
94de4fd55c
@@ -11,9 +11,12 @@
|
||||
#include "GS/GSVertexKick.h"
|
||||
#include "GS/Renderers/Common/GSVertex.h"
|
||||
|
||||
#include "common/Threading.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
// 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<ReleasePayloadRecord>);
|
||||
|
||||
using RecordRing = SpscRing<RecordSlot, 512>;
|
||||
|
||||
// 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<DrawNode*> draw_arena;
|
||||
SpscRing<DrawNode*, kMaxDrawNodes> 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<PayloadNode*> payload_arena;
|
||||
SpscRing<PayloadNode*, kMaxPayloadNodes> payload_free;
|
||||
};
|
||||
} // namespace GSBackQueue
|
||||
|
||||
+39
-27
@@ -108,11 +108,7 @@ GSState::GSState()
|
||||
{
|
||||
Console.WriteLn("GS: back-thread mode %d (record path active).", static_cast<int>(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<u8*>(_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-17
@@ -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<GSBackQueue::DrawNode*> m_draw_node_arena;
|
||||
GSBackQueue::SpscRing<GSBackQueue::DrawNode*, MAX_DRAW_NODES> 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<GSBackQueue::PayloadNode*> m_payload_arena;
|
||||
GSBackQueue::SpscRing<GSBackQueue::PayloadNode*, MAX_PAYLOAD_NODES> 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<bool> 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<T>(), &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);
|
||||
|
||||
Reference in New Issue
Block a user