GV7-1d-ii-b: two-object front split (GSFrontState + entry-point routing)

Instantiate a GSFrontState parser object under GSBackThreadMode::Pipelined
(SEAM-AUDIT.md $7): it owns all parse state and emits records into the back
renderer's channel; the back object executes them, installing record state
into its own members so the HW look-ahead heuristics read the same names
they always did. Mode 3 still drains per record (lockstep) — the pipelined
flip is the next commit.

- GS.cpp routes GIF transfers, SoftReset, CSR, readbacks, savestates, and
  the vsync PCRTC digestion to the front; present/TC/settings stay on the
  renderer. The front is created only when the back thread engaged, and is
  destroyed first (it drains the shared channel the back owns).
- Drained seams reach authoritative memory through m_mem_target: readback
  ReadImageX/SaveBMP, InvalidateLocalMem, savestate vm8 serialize/restore,
  TC readback/purge (now draining), plus back-side Reset/CLUT-reset and a
  PCRTC re-sync on Defrost.
- The draw executor on a split back aims m_draw_env/PRIM/m_context around
  the tail exactly as FlushDraw does on the front, and restores after.
- m_channel_shuffle_finish is written on both sides; the front's ApplyTEX0
  set becomes a one-shot edge OR-ed into the back-owned flag (a level
  install clobbered the draw path's own sets/clears — FlatOut 2 lost its
  channel-shuffle skip, caught by the vk hash gate).
- Kick-time IsCoverageAlphaSupported reproduces single-object mixed
  semantics: live PRIM/ALPHA from the front, last-executed-draw primclass/
  cached-ctx/alpha-minmax from the back (IsRTWrittenLive split).
- GSAllocateWrappedMemory drops its process-global singleton (close the
  fd/handle once the views are mapped) so two GSStates can each own a
  wrapped vm; also fixes a handle leak in the Windows free path.
- s_transfer_n moves to the submit side: transfer serials are
  front-assigned, and the vsync idle-frame check reads them on the MTGS
  thread.

Gates: gs_vertex_tests 21/21; gsrunner PNG hashes bit-identical to GV-0
baselines for modes 0/1/2/3 x vk+sw over all 10 dumps (mode 3 exercises
Defrost + readback seams through the front object).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-19 14:20:49 -07:00
co-authored by Claude
parent 94de4fd55c
commit 84a1de62b5
5 changed files with 302 additions and 67 deletions
+84 -54
View File
@@ -208,6 +208,17 @@ static void GSClampUpscaleMultiplier(Pcsx2Config::GSOptions& config)
config.UpscaleMultiplier = static_cast<float>(max_upscale_multiplier);
}
// GV7-1d-ii: the front parser object of the two-object split (GSState.h).
// Non-null only when GSBackThreadMode::Pipelined engaged; all GIF-parse entry
// points below route to it, while draw/present/TC stay on g_gs_renderer.
std::unique_ptr<GSFrontState> g_gs_front;
// The object GIF data, parse-side resets, readbacks, and savestates route to.
static __fi GSState* GSParseTarget()
{
return g_gs_front ? static_cast<GSState*>(g_gs_front.get()) : static_cast<GSState*>(g_gs_renderer.get());
}
static bool OpenGSRenderer(GSRendererType renderer, u8* basemem)
{
// Must be done first, initialization routines in GSState use GSIsHardwareRenderer().
@@ -232,6 +243,18 @@ static bool OpenGSRenderer(GSRendererType renderer, u8* basemem)
g_gs_renderer->SetRegsMem(basemem);
g_gs_renderer->ResetPCRTC();
g_gs_renderer->UpdateRenderFixes();
// GV7-1d-ii: instantiate the front parser only when the back thread really
// engaged (the renderer ctor falls back to inline records on a non-Vulkan
// HW device).
if (GSConfig.BackThreadMode == GSBackThreadMode::Pipelined && g_gs_renderer->IsBackThreadRunning())
{
g_gs_front = std::make_unique<GSFrontState>(g_gs_renderer.get());
g_gs_front->SetRegsMem(basemem);
g_gs_front->ResetPCRTC();
Console.WriteLn("GS: front parser object active (two-object split).");
}
g_perfmon.Reset();
return true;
}
@@ -240,6 +263,10 @@ static void CloseGSRenderer()
{
GSTextureReplacements::Shutdown();
// The front must go first: its destructor drains the shared channel, and
// the back object owns that channel and the pooled arrays.
g_gs_front.reset();
if (g_gs_renderer)
{
g_gs_renderer->Destroy();
@@ -252,7 +279,7 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
{
Console.WriteLn("Reopening GS with %s device", recreate_device ? "new" : "existing");
g_gs_renderer->Flush(GSState::GSFlushReason::GSREOPEN);
GSParseTarget()->Flush(GSState::GSFlushReason::GSREOPEN);
if (recreate_device && !recreate_renderer)
{
@@ -282,7 +309,7 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
std::unique_ptr<u8[]> fd_data;
if (recreate_renderer)
{
if (g_gs_renderer->Freeze(&fd, true) != 0)
if (GSParseTarget()->Freeze(&fd, true) != 0)
{
Console.Error("(GSreopen) Failed to get GS freeze size");
return false;
@@ -290,7 +317,7 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
fd_data = std::make_unique<u8[]>(fd.size);
fd.data = fd_data.get();
if (g_gs_renderer->Freeze(&fd, false) != 0)
if (GSParseTarget()->Freeze(&fd, false) != 0)
{
Console.Error("(GSreopen) Failed to freeze GS");
return false;
@@ -335,7 +362,7 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
return false;
}
if (g_gs_renderer->Defrost(&fd) != 0)
if (GSParseTarget()->Defrost(&fd) != 0)
{
Console.Error("(GSreopen) Failed to defrost");
return false;
@@ -388,7 +415,10 @@ void GSclose()
void GSreset(bool hardware_reset)
{
// Back first (drains the channel, resets memory/TC), then the front parser.
g_gs_renderer->Reset(hardware_reset);
if (g_gs_front)
g_gs_front->Reset(hardware_reset);
// Restart video capture if it's been started.
// Otherwise we get a buildup of audio frames from the CPU thread.
@@ -404,76 +434,79 @@ void GSreset(bool hardware_reset)
void GSgifSoftReset(u32 mask)
{
g_gs_renderer->SoftReset(mask);
GSParseTarget()->SoftReset(mask);
}
void GSwriteCSR(u32 csr)
{
g_gs_renderer->WriteCSR(csr);
GSParseTarget()->WriteCSR(csr);
}
void GSInitAndReadFIFO(u8* mem, u32 size)
{
GL_PERF("Init and read FIFO %u qwc", size);
g_gs_renderer->InitReadFIFO(mem, size);
g_gs_renderer->ReadFIFO(mem, size);
GSParseTarget()->InitReadFIFO(mem, size);
GSParseTarget()->ReadFIFO(mem, size);
}
void GSReadLocalMemoryUnsync(u8* mem, u32 qwc, u64 BITBLITBUF, u64 TRXPOS, u64 TRXREG)
{
g_gs_renderer->ReadLocalMemoryUnsync(mem, qwc, GIFRegBITBLTBUF{BITBLITBUF}, GIFRegTRXPOS{TRXPOS}, GIFRegTRXREG{TRXREG});
GSParseTarget()->ReadLocalMemoryUnsync(mem, qwc, GIFRegBITBLTBUF{BITBLITBUF}, GIFRegTRXPOS{TRXPOS}, GIFRegTRXREG{TRXREG});
}
void GSgifTransfer(const u8* mem, u32 size)
{
g_gs_renderer->Transfer<3>(mem, size);
GSParseTarget()->Transfer<3>(mem, size);
}
void GSgifTransfer1(u8* mem, u32 addr)
{
g_gs_renderer->Transfer<0>(const_cast<u8*>(mem) + addr, (0x4000 - addr) / 16);
GSParseTarget()->Transfer<0>(const_cast<u8*>(mem) + addr, (0x4000 - addr) / 16);
}
void GSgifTransfer2(u8* mem, u32 size)
{
g_gs_renderer->Transfer<1>(const_cast<u8*>(mem), size);
GSParseTarget()->Transfer<1>(const_cast<u8*>(mem), size);
}
void GSgifTransfer3(u8* mem, u32 size)
{
g_gs_renderer->Transfer<2>(const_cast<u8*>(mem), size);
GSParseTarget()->Transfer<2>(const_cast<u8*>(mem), size);
}
void GSvsync(u32 field, bool registers_written)
{
// Update this here because we need to check if the pending draw affects the current frame, so our regs need to be updated.
g_gs_renderer->PCRTCDisplays.SetVideoMode(g_gs_renderer->GetVideoMode());
g_gs_renderer->PCRTCDisplays.EnableDisplays(g_gs_renderer->m_regs->PMODE, g_gs_renderer->m_regs->SMODE2, g_gs_renderer->isReallyInterlaced());
g_gs_renderer->PCRTCDisplays.SetRects(0, g_gs_renderer->m_regs->DISP[0].DISPLAY, g_gs_renderer->m_regs->DISP[0].DISPFB);
g_gs_renderer->PCRTCDisplays.SetRects(1, g_gs_renderer->m_regs->DISP[1].DISPLAY, g_gs_renderer->m_regs->DISP[1].DISPFB);
g_gs_renderer->PCRTCDisplays.CheckSameSource();
g_gs_renderer->PCRTCDisplays.CalculateDisplayOffset(g_gs_renderer->m_scanmask_used);
g_gs_renderer->PCRTCDisplays.CalculateFramebufferOffset(g_gs_renderer->m_scanmask_used, g_gs_renderer->m_regs->DISP[0].DISPFB, g_gs_renderer->m_regs->DISP[1].DISPFB);
GSState* const front = GSParseTarget();
front->PCRTCDisplays.SetVideoMode(front->GetVideoMode());
front->PCRTCDisplays.EnableDisplays(front->m_regs->PMODE, front->m_regs->SMODE2, front->isReallyInterlaced());
front->PCRTCDisplays.SetRects(0, front->m_regs->DISP[0].DISPLAY, front->m_regs->DISP[0].DISPFB);
front->PCRTCDisplays.SetRects(1, front->m_regs->DISP[1].DISPLAY, front->m_regs->DISP[1].DISPFB);
front->PCRTCDisplays.CheckSameSource();
front->PCRTCDisplays.CalculateDisplayOffset(front->m_scanmask_used);
front->PCRTCDisplays.CalculateFramebufferOffset(front->m_scanmask_used, front->m_regs->DISP[0].DISPFB, front->m_regs->DISP[1].DISPFB);
// The PCRTC record must precede the vsync-flushed draw records — those draws
// see the fresh display state, mid-frame draws saw the previous frame's.
g_gs_renderer->SubmitPcrtcSync();
front->SubmitPcrtcSync();
// Do not move the flush into the VSync() method. It's here because EE transfers
// get cleared in HW VSync, and may be needed for a buffered draw (FFX FMVs).
g_gs_renderer->Flush(GSState::VSYNC);
front->Flush(GSState::VSYNC);
g_gs_renderer->SubmitVsync(field, registers_written);
if (g_gs_front)
g_gs_front->MirrorPostVsyncState();
}
int GSfreeze(FreezeAction mode, freezeData* data)
{
if (mode == FreezeAction::Save)
{
return g_gs_renderer->Freeze(data, false);
return GSParseTarget()->Freeze(data, false);
}
else if (mode == FreezeAction::Size)
{
return g_gs_renderer->Freeze(data, true);
return GSParseTarget()->Freeze(data, true);
}
else // if (mode == FreezeAction::Load)
{
@@ -487,12 +520,18 @@ int GSfreeze(FreezeAction mode, freezeData* data)
if (GSCapture::IsCapturing())
GSCapture::Flush();
return g_gs_renderer->Defrost(data);
return GSParseTarget()->Defrost(data);
}
}
void GSQueueSnapshot(const std::string& path, u32 gsdump_frames)
{
// GV7-1d-ii known gap: the GSDump transfer hook sits on the parse path, so
// under the two-object split the front's transfers would be missing from
// the dump (GV7-2 item). Warn rather than write a corrupt dump silently.
if (g_gs_front)
Console.Warning("GS: dump recording under GSBackThreadMode=Pipelined is not yet supported; expect an incomplete dump.");
if (g_gs_renderer)
g_gs_renderer->QueueSnapshot(path, gsdump_frames);
}
@@ -854,6 +893,8 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
// renderer-specific options (e.g. auto flush, TC offset)
g_gs_renderer->UpdateSettings(old_config);
if (g_gs_front)
g_gs_front->UpdateSettings(old_config);
// reload texture cache when trilinear filtering or TC options change
if (
@@ -929,14 +970,13 @@ bool GSSaveSnapshotToMemory(u32 window_width, u32 window_height, bool apply_aspe
#ifdef _WIN32
static HANDLE s_fh = NULL;
void* GSAllocateWrappedMemory(size_t size, size_t repeat)
{
pxAssertRel(!s_fh, "Has no file mapping");
s_fh = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, nullptr);
if (s_fh == NULL)
// No static handle: the mapped views keep the section alive, so the handle
// closes before returning and multiple wrapped allocations can coexist
// (the GV7 two-object split runs two GSStates, each with a wrapped vm).
const HANDLE fh = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, nullptr);
if (fh == NULL)
{
Console.Error("Failed to create file mapping of size %zu. WIN API ERROR:%u", size, GetLastError());
return nullptr;
@@ -955,7 +995,7 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
// Everything except the last needs the placeholders split to map over them. Then map the same file over the region.
u8* addr = base + i * size;
if ((i != (repeat - 1) && !VirtualFreeEx(GetCurrentProcess(), addr, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) ||
!MapViewOfFile3(s_fh, GetCurrentProcess(), addr, 0, size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0))
!MapViewOfFile3(fh, GetCurrentProcess(), addr, 0, size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0))
{
Console.Error("Failed to map repeat %zu of size %zu.", i, size);
okay = false;
@@ -968,6 +1008,7 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
if (okay)
{
DbgCon.WriteLn("fifo_alloc(): Mapped %zu repeats of %zu bytes at %p.", repeat, size, base);
CloseHandle(fh);
return base;
}
@@ -975,15 +1016,12 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
}
Console.Error("Failed to reserve VA space of size %zu. WIN API ERROR:%u", size, GetLastError());
CloseHandle(s_fh);
s_fh = NULL;
CloseHandle(fh);
return nullptr;
}
void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
{
pxAssertRel(s_fh, "Has a file mapping");
for (size_t i = 0; i < repeat; i++)
{
u8* addr = (u8*)ptr + i * size;
@@ -991,7 +1029,6 @@ void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
}
VirtualFreeEx(GetCurrentProcess(), ptr, 0, MEM_RELEASE);
s_fh = NULL;
}
#else
@@ -1001,15 +1038,14 @@ void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
#include <fcntl.h>
#include <unistd.h>
static int s_shm_fd = -1;
void* GSAllocateWrappedMemory(size_t size, size_t repeat)
{
pxAssert(s_shm_fd == -1);
// No static fd: the mappings keep the shm object alive, so the descriptor
// closes before returning and multiple wrapped allocations can coexist
// (the GV7 two-object split runs two GSStates, each with a wrapped vm).
const char* file_name = "/GS.mem";
s_shm_fd = shm_open(file_name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (s_shm_fd != -1)
const int fd = shm_open(file_name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd != -1)
{
shm_unlink(file_name); // file is deleted but descriptor is still open
}
@@ -1019,33 +1055,27 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
return nullptr;
}
if (ftruncate(s_shm_fd, repeat * size) < 0)
if (ftruncate(fd, repeat * size) < 0)
fprintf(stderr, "Failed to reserve memory due to %s\n", strerror(errno));
void* fifo = mmap(nullptr, size * repeat, PROT_READ | PROT_WRITE, MAP_SHARED, s_shm_fd, 0);
void* fifo = mmap(nullptr, size * repeat, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
for (size_t i = 1; i < repeat; i++)
{
void* base = (u8*)fifo + size * i;
u8* next = (u8*)mmap(base, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s_shm_fd, 0);
u8* next = (u8*)mmap(base, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
if (next != base)
fprintf(stderr, "Fail to mmap contiguous segment\n");
}
close(fd);
return fifo;
}
void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
{
pxAssert(s_shm_fd >= 0);
if (s_shm_fd < 0)
return;
munmap(ptr, size * repeat);
close(s_shm_fd);
s_shm_fd = -1;
}
#endif
+142 -11
View File
@@ -96,7 +96,7 @@ constexpr int GSState::GetSaveStateSize(int version)
return size;
}
GSState::GSState()
GSState::GSState(GSBackQueue::Channel* shared_chan)
: m_vt(this)
{
// m_nativeres seems to be a hack. Unfortunately it impacts draw call number which make debug painful in the replayer.
@@ -104,7 +104,20 @@ GSState::GSState()
m_nativeres = GSConfig.UpscaleMultiplier == 1.0f;
m_mipmap = GSConfig.Mipmap;
m_back_records = GSConfig.BackThreadMode != GSBackThreadMode::Off;
if (m_back_records)
if (shared_chan)
{
// Front parser object of the two-object split: records go to the back
// object's channel, whose thread is already running (GS.cpp only
// creates a front once the back engaged). The back object owns the
// thread and the pooled arrays; this object only stages and pushes.
pxAssertRel(m_back_records && shared_chan->consumer_running, "GS front object requires a running back thread");
m_chan = shared_chan;
m_back_queued = true;
// Still lockstep until the pipelined flip (GV7-1d-ii-c).
m_back_lockstep = true;
AdoptTransferBuffer();
}
else if (m_back_records)
{
Console.WriteLn("GS: back-thread mode %d (record path active).", static_cast<int>(GSConfig.BackThreadMode));
@@ -197,6 +210,54 @@ GSState::~GSState()
}
}
GSFrontState::GSFrontState(GSState* back)
: GSState(back->GetBackChannel())
, m_back(back)
{
m_mem_target = back;
back->m_split_back = true;
}
GSFrontState::~GSFrontState()
{
// Pool nodes referenced by in-flight records belong to the back's channel
// and outlive us, but drain anyway so teardown never depends on ordering.
DrainBackQueue();
}
void GSFrontState::Draw()
{
pxFailRel("Draw() called on the GS front parser object");
}
bool GSFrontState::IsCoverageAlphaSupported()
{
// The kick cull path evaluates this per AA1 prim. Single-object semantics
// are a mixed read: PRIM and the blending ALPHA reg are the LIVE parse
// state, while the primclass / cached ctx / alpha min-max come from the
// LAST EXECUTED draw. Reproduce exactly: live parts from this (front)
// object, last-draw parts from the back — drained here under lockstep.
// Under true pipelining the back-side parts become stale heuristic reads;
// revisit at the pipelined flip (1dii-DESIGN.md).
if (!(PRIM->AA1 && (m_back->m_vt.m_primclass == GS_LINE_CLASS || m_back->m_vt.m_primclass == GS_TRIANGLE_CLASS)))
return false; // IsCoverageAlpha(), with the last draw's primclass
if (GSGetCurrentRenderer() == GSRendererType::Null)
return false;
if (!GSIsHardwareRenderer())
return true; // SW: IsCoverageAlpha() alone
return m_back->IsRTWrittenLive(m_context->ALPHA) && g_gs_device->Features().aa1;
}
void GSFrontState::MirrorPostVsyncState()
{
// The vsync executed on the (drained) back object on this thread; Merge
// decremented the back's scanmask copy. Re-mirror so next frame's front
// PCRTC digestion sees what a single object would have.
m_scanmask_used = m_back->m_scanmask_used;
}
std::string GSState::GetDrawDumpPath(const char* format, ...)
{
std::va_list ap;
@@ -2731,6 +2792,11 @@ void GSState::FlushWrite()
rec.draw_serial = s_n;
rec.first_slice = (m_tr.start == 0);
// Front side: transfer serials are front-assigned — the front decides
// transfer order, and the vsync idle-frame check reads this on the MTGS
// thread.
s_transfer_n++;
// The record references a slice of the pooled staging buffer; the next
// transfer Init rotates it out instead of reusing it.
m_tr_payload_referenced = m_back_records;
@@ -2814,7 +2880,6 @@ void GSState::ExecTransferRecord(const GSBackQueue::TransferRecord& rec)
wi(m_mem, m_exec_tr_x, m_exec_tr_y, rec.payload, rec.len, blit, pos, reg);
g_perfmon.Put(GSPerfMon::Swizzle, rec.stat_len);
s_transfer_n++;
}
// This function decides if the context has changed in a way which warrants flushing the draw.
@@ -2980,6 +3045,16 @@ void GSState::FlushPrim()
rec.channel_shuffle_finish = m_channel_shuffle_finish;
rec.packed_uv_hack_flag = m_isPackedUV_HackFlag;
// m_channel_shuffle_finish is written on BOTH sides: the front's
// ApplyTEX0 sets it as a one-shot "abort shuffle skip" message, while
// the draw path sets AND clears it as back-persistent shuffle state.
// On the split front the capture above delivers the message, so clear
// our copy (edge semantics) — the executor ORs it into the back's
// authoritative copy instead of level-installing. On a single object
// the member IS the authoritative copy: leave it alone.
if (m_mem_target != this)
m_channel_shuffle_finish = false;
if (m_back_queued)
{
// The consumer releases the node after the tail runs.
@@ -3060,10 +3135,35 @@ void GSState::ExecDrawRecord(const GSBackQueue::DrawRecord& rec)
m_backed_up_ctx = rec.backed_up_ctx;
m_dirty_gs_regs = rec.dirty_gs_regs;
m_state_flush_reason = static_cast<GSFlushReason>(rec.flush_reason);
m_channel_shuffle_finish = rec.channel_shuffle_finish;
// Split back: OR the front's one-shot abort edge into the back-owned
// shuffle state (a level-install would clobber the draw path's own
// sets/clears). Single object: identity re-install of the live value.
if (m_split_back)
m_channel_shuffle_finish |= rec.channel_shuffle_finish;
else
m_channel_shuffle_finish = rec.channel_shuffle_finish;
m_isPackedUV_HackFlag = rec.packed_uv_hack_flag;
// On a split back object nobody ran FlushDraw here — aim the draw pointers
// at the installed draw env exactly as FlushDraw does on the front, and
// restore after, so non-draw records execute with pointers at m_env like
// serial execution between draws. On a single object FlushDraw owns both
// (its restore must stay AFTER FlushPrim's carry-over rebuild).
if (m_split_back)
{
m_draw_env = &m_prev_env;
PRIM = &m_prev_env.PRIM;
UpdateContext();
}
DrawRecordTail(rec.draw_serial);
if (m_split_back)
{
m_draw_env = &m_env;
PRIM = &m_env.PRIM;
UpdateContext();
}
}
// The draw executor's tail: everything from vertex trace to Draw() + perfmon,
@@ -3436,6 +3536,9 @@ void GSState::Write(const u8* mem, int len)
rec.draw_serial = s_n;
rec.first_slice = true;
// Front-assigned, like the staged path in FlushWrite.
s_transfer_n++;
if (m_back_queued)
PushRecord(GSBackQueue::RecordType::Transfer, rec);
else
@@ -3483,11 +3586,15 @@ void GSState::InitReadFIFO(u8* mem, int len)
const int sy = m_env.TRXPOS.SSAY;
const GSVector4i r(sx, sy, sx + w, sy + h);
// CheckWriteOverlap above may have flushed pending draws into records;
// they must land in local memory before the TC readback and image read.
DrainBackQueue();
if (m_tr.x == sx && m_tr.y == sy)
InvalidateLocalMem(m_env.BITBLTBUF, r);
m_mem_target->InvalidateLocalMem(m_env.BITBLTBUF, r);
// Read the image all in one go.
m_mem.ReadImageX(m_tr.x, m_tr.y, m_tr.buff, m_tr.total, m_env.BITBLTBUF, m_env.TRXPOS, m_env.TRXREG);
m_mem_target->m_mem.ReadImageX(m_tr.x, m_tr.y, m_tr.buff, m_tr.total, m_env.BITBLTBUF, m_env.TRXPOS, m_env.TRXREG);
if (GSConfig.SaveRT && GSConfig.ShouldDump(s_n, g_perfmon.GetFrame()))
{
@@ -3496,7 +3603,7 @@ void GSState::InitReadFIFO(u8* mem, int len)
s_n, (int)m_env.BITBLTBUF.SBP, (int)m_env.BITBLTBUF.SBW, GSUtil::GetPSMName(m_env.BITBLTBUF.SPSM),
r.left, r.top, r.right, r.bottom));
m_mem.SaveBMP(s, m_env.BITBLTBUF.SBP, m_env.BITBLTBUF.SBW, m_env.BITBLTBUF.SPSM, r.right, r.bottom);
m_mem_target->m_mem.SaveBMP(s, m_env.BITBLTBUF.SBP, m_env.BITBLTBUF.SBW, m_env.BITBLTBUF.SPSM, r.right, r.bottom);
}
}
@@ -3882,7 +3989,7 @@ void GSState::ReadLocalMemoryUnsync(u8* mem, int qwc, GIFRegBITBLTBUF BITBLTBUF,
if (m_tr.start == 0)
{
m_mem.ReadImageX(tb.x, tb.y, m_tr.buff, m_tr.total, BITBLTBUF, TRXPOS, TRXREG);
m_mem_target->m_mem.ReadImageX(tb.x, tb.y, m_tr.buff, m_tr.total, BITBLTBUF, TRXPOS, TRXREG);
m_tr.start += m_tr.total;
}
@@ -4132,8 +4239,12 @@ int GSState::Freeze(freezeData* fd, bool sizeonly)
Flush(GSFlushReason::SAVESTATE);
// The flush may have pushed draw records; they must land before the local
// memory bytes are serialized.
DrainBackQueue();
if (GSConfig.UserHacks_ReadTCOnClose)
ReadbackTextureCache();
m_mem_target->ReadbackTextureCache();
u8* data = fd->data;
@@ -4190,7 +4301,7 @@ int GSState::Freeze(freezeData* fd, bool sizeonly)
WriteState(data, &m_tr.end);
WriteState(data, &m_tr.write);
// End of version 9 changes.
WriteState(data, m_mem.m_vm8, m_mem.m_vmsize);
WriteState(data, m_mem_target->m_mem.m_vm8, m_mem_target->m_mem.m_vmsize);
for (GIFPath& path : m_path)
{
@@ -4236,8 +4347,15 @@ int GSState::Defrost(const freezeData* fd)
Flush(GSFlushReason::LOADSTATE);
DrainBackQueue();
Reset(true);
// Two-object split: the back renderer's local memory / TC / persistent draw
// state must reset too before its m_vm8 is refilled below.
if (m_mem_target != this)
m_mem_target->Reset(true);
ReadState(&m_env.PRIM, data);
if (version <= 6)
@@ -4321,7 +4439,7 @@ int GSState::Defrost(const freezeData* fd)
m_tr.write = true;
}
ReadState(m_mem.m_vm8, data, m_mem.m_vmsize);
ReadState(m_mem_target->m_mem.m_vm8, data, m_mem_target->m_mem.m_vmsize);
for (GIFPath& path : m_path)
{
@@ -4353,12 +4471,19 @@ int GSState::Defrost(const freezeData* fd)
// Force CLUT to be reloaded.
m_mem.m_clut.Reset();
if (m_mem_target != this)
m_mem_target->m_mem.m_clut.Reset();
(PRIM->CTXT == 0) ? ApplyTEX0<0>(m_context->TEX0) : ApplyTEX0<1>(m_context->TEX0);
g_perfmon.SetFrame(0);
ResetPCRTC();
// Two-object split: refresh the back's PCRTC copy now instead of leaving it
// stale until the next vsync (draw heuristics read it per draw).
if (m_mem_target != this)
SubmitPcrtcSync();
return 0;
}
@@ -7565,6 +7690,12 @@ bool GSState::IsCoverageAlphaSupported()
return false;
}
bool GSState::IsRTWrittenLive(const GIFRegALPHA& ALPHA)
{
pxFailRel("Not implemented");
return false;
}
GIFRegTEX0 GSState::GetTex0Layer(u32 lod)
{
// Shortcut
+63 -1
View File
@@ -30,11 +30,23 @@ class GSState : public GSAlignedClass<32>
// GSVertexTrace::Update consumes the per-buffer fused FindMinMax accumulator
// (m_vertex->fmm_*) directly.
friend class GSVertexTrace;
// GV7-1d-ii: the front parser object delegates protected queries/seams to
// the back renderer through a GSState*.
friend class GSFrontState;
public:
GSState();
// GV7-1d-ii: shared_chan aims this object at another GSState's channel — the
// front parser object of the two-object split passes the back object's
// channel so its records land in the consumed ring. Default (nullptr) uses
// this object's own channel storage, exactly as before.
GSState(GSBackQueue::Channel* shared_chan = nullptr);
virtual ~GSState();
// GV7-1d-ii: channel/back-thread visibility for the front-object lifecycle
// in GS.cpp (create the front only when the back thread actually engaged).
GSBackQueue::Channel* GetBackChannel() { return m_chan; }
bool IsBackThreadRunning() const { return m_chan->consumer_running; }
static constexpr int GetSaveStateSize(int version);
private:
@@ -315,6 +327,10 @@ protected:
bool IsCoverageAlpha();
bool IsCoverageAlphaFixedOne();
virtual bool IsCoverageAlphaSupported();
// GV7-1d-ii: back-half of the split front's kick-time coverage-alpha query
// (HW only): cached-ctx/alpha-minmax from this object's last executed draw,
// the caller's live ALPHA passed in.
virtual bool IsRTWrittenLive(const GIFRegALPHA& ALPHA);
void CalcAlphaMinMax(const int tex_min, const int tex_max);
void CorrectATEAlphaMinMax(const u32 atst, const int aref);
@@ -562,6 +578,19 @@ public:
GSBackQueue::Channel m_chan_storage;
GSBackQueue::Channel* m_chan = &m_chan_storage;
// GV7-1d-ii: the object owning local memory, the CLUT palette, and the
// texture cache for this session. Single-object modes: this. On the front
// parser object it points at the back renderer, so the drained seams
// (readbacks, savestates) reach the authoritative m_mem/TC while every
// register decision stays front-side. Only ever dereferenced after a drain.
GSState* m_mem_target = this;
// GV7-1d-ii: set on the back renderer when a front parser object exists.
// The draw executor then aims m_draw_env/PRIM/m_context around the tail
// itself (on a single object FlushDraw owns that aiming, and the front's
// carry-over rebuild depends on FlushDraw's restore happening after).
bool m_split_back = false;
// 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:
@@ -665,6 +694,39 @@ public:
GIFRegTEX0 GetTex0Layer(u32 lod);
};
// GV7-1d-ii: the front parser object of the two-object pipelined split
// (SEAM-AUDIT.md §7). Owns all parse state (env, vertex kick, draw buffering,
// transfer staging, CLUT decision) and emits records into the back renderer's
// channel; the back object executes them on the back thread, installing record
// state into its own members. The front never draws, and reaches the
// authoritative local memory / texture cache only through m_mem_target after a
// drain. Created by GS.cpp only when the back thread engaged under
// GSBackThreadMode::Pipelined.
class GSFrontState final : public GSState
{
public:
GSFrontState(GSState* back);
~GSFrontState() override;
void Draw() override;
// Parse-path virtuals must answer exactly as the back renderer would (they
// steer kick/flush decisions); the overridden implementations only read
// session-constant config/device caps, so cross-object calls are safe.
bool IsCoverageAlphaSupported() override;
// Once per frame, after the (drained) vsync executed on the back object:
// re-mirror present-side state the back mutated (Merge's scanmask
// decrement) so next frame's front digestion sees what a single object
// would have.
void MirrorPostVsyncState();
private:
GSState* m_back;
};
extern std::unique_ptr<GSFrontState> g_gs_front;
// We put this in the header because of Multi-ISA.
inline void GSState::ExpandDIMX(GSVector4i* dimx, const GIFRegDIMX DIMX)
{
+12 -1
View File
@@ -54,11 +54,14 @@ void GSRendererHW::Destroy()
void GSRendererHW::PurgeTextureCache(bool sources, bool targets, bool hash_cache)
{
// Queued draw records reach the TC; retire them before mutating it.
DrainBackQueue();
g_texture_cache->RemoveAll(sources, targets, hash_cache);
}
void GSRendererHW::ReadbackTextureCache()
{
DrainBackQueue();
g_texture_cache->ReadbackAll();
}
@@ -2148,6 +2151,15 @@ bool GSRendererHW::NeedsBlending()
}
bool GSRendererHW::IsRTWritten()
{
return IsRTWrittenLive(m_context->ALPHA);
}
// GV7-1d-ii: ALPHA is a parameter so the split front object can evaluate the
// kick-time coverage-alpha query with ITS live blending regs while the cached
// ctx / alpha min-max stay this (the back) object's last-executed-draw state —
// exactly the mixed live/stale read a single object performs.
bool GSRendererHW::IsRTWrittenLive(const GIFRegALPHA& ALPHA)
{
const GIFRegTEST TEST = m_cached_ctx.TEST;
const bool only_z_written = (TEST.ATE && TEST.ATST == ATST_NEVER && TEST.AFAIL == AFAIL_ZB_ONLY);
@@ -2155,7 +2167,6 @@ bool GSRendererHW::IsRTWritten()
return false;
const u32 written_bits = (~m_cached_ctx.FRAME.FBMSK & GSLocalMemory::m_psm[m_cached_ctx.FRAME.PSM].fmsk);
const GIFRegALPHA ALPHA = m_context->ALPHA;
return (
// A not masked
(written_bits & 0xFF000000u) != 0) ||
+1
View File
@@ -292,6 +292,7 @@ private:
bool NeedsBlending();
bool IsRTWritten();
bool IsRTWrittenLive(const GIFRegALPHA& ALPHA) override;
bool IsDepthAlwaysPassing();
bool IsUsingCsInBlend();
bool IsUsingAsInBlend();