mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
co-authored by
Claude
parent
94de4fd55c
commit
84a1de62b5
+84
-54
@@ -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
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -292,6 +292,7 @@ private:
|
||||
|
||||
bool NeedsBlending();
|
||||
bool IsRTWritten();
|
||||
bool IsRTWrittenLive(const GIFRegALPHA& ALPHA) override;
|
||||
bool IsDepthAlwaysPassing();
|
||||
bool IsUsingCsInBlend();
|
||||
bool IsUsingAsInBlend();
|
||||
|
||||
Reference in New Issue
Block a user