Merge yaps2/main: GV7 GS front/back thread split (17 commits)

Brings the complete GV7 campaign: GSBackQueue SPSC record ring, draw/
transfer/PCRTC/vsync records with inline executors, the GSFrontState
two-object front split, lockstep + pipelined back-thread modes (default
Off), mid-frame MTGS-thread drain seams, back-thread affinity fix, and
the Qt/Big Picture GSBackThreadMode settings UI.

Conflict resolutions:
- pcsx2/GS/GS.cpp: adjacent additions unioned (Android Tekken 5 Mali
  override + GV7 g_gs_front/GSParseTarget). GSAllocateWrappedMemory
  unioned semantically: keeps the iOS-safe HostSys::CreateSharedMemory
  routing AND drops the static fd so two wrapped allocations coexist
  (the GV7 two-object split runs two GSStates, each with a wrapped vm);
  CreateSharedMemory already unlinks/memfds the name and the iOS
  file-backed fallback O_EXCL-retries per-attempt paths, so coexisting
  allocations cannot collide.
- GraphicsAdvancedSettingsTab.ui: take gsBackThreadMode tabstop; drop
  yaps2's stale "rov" tabstop (no such widget in either tree).
- FullscreenUI_Settings.cpp: GS Back Thread setting inserted outside the
  ARMSX2 !__APPLE__ guard around exclusive fullscreen.

Audit: ARMSX2 one-liners in GV7-rewritten files survived
(GSClut CreateFeedbackTarget, GSState SaveTransferImages); ARMSX2's
Merge()-path additions (RetroArch shader chain, FastMAD fallback) run
post-drain on the vsync path, so no unaudited device seams.

Gates on merged tree: recompiler_tests 1359/1359, gs_vertex_tests 21/21
(incl. new gs_backqueue suite), full build incl. armsx2-qt.
This commit is contained in:
Brian Degenhardt
2026-07-19 16:19:06 -07:00
23 changed files with 1965 additions and 411 deletions
+14
View File
@@ -508,6 +508,7 @@ static void PrintCommandLineHelp(const char* progname)
std::fprintf(stderr, " -loop <count>: Loops dump playback N times. Defaults to 1. 0 will loop infinitely.\n");
std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Defaults to Auto.\n");
std::fprintf(stderr, " -swthreads <threads>: Sets the number of threads for the software renderer.\n");
std::fprintf(stderr, " -backthread <mode>: GS back-thread mode (0=off, 1=inline-records, 2=lockstep, 3=pipelined). Defaults to 0.\n");
std::fprintf(stderr, " -window: Forces a window to be displayed.\n");
std::fprintf(stderr, " -surfaceless: Disables showing a window.\n");
std::fprintf(stderr, " -logfile <filename>: Writes emu log to filename.\n");
@@ -706,6 +707,19 @@ bool GSRunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& pa
s_settings_interface.SetIntValue("EmuCore/GS", "Renderer", static_cast<int>(type));
continue;
}
else if (CHECK_ARG_PARAM("-backthread"))
{
const int mode = StringUtil::FromChars<int>(argv[++i]).value_or(-1);
if (mode < 0 || mode > 3)
{
Console.Error("Invalid GS back-thread mode (0=off, 1=inline-records, 2=lockstep, 3=pipelined)");
return false;
}
Console.WriteLn("Setting GS back-thread mode to %d.", mode);
s_settings_interface.SetIntValue("EmuCore/GS", "GSBackThreadMode", mode);
continue;
}
else if (CHECK_ARG_PARAM("-swthreads"))
{
const int swthreads = StringUtil::FromChars<int>(argv[++i]).value_or(0);
@@ -198,6 +198,40 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="gsBackThreadModeLabel">
<property name="text">
<string>GS Back Thread:</string>
</property>
<property name="buddy">
<cstring>gsBackThreadMode</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="gsBackThreadMode">
<item>
<property name="text">
<string>Disabled (Default)</string>
</property>
</item>
<item>
<property name="text">
<string>Inline Records (Debug)</string>
</property>
</item>
<item>
<property name="text">
<string>Lockstep (Debug)</string>
</property>
</item>
<item>
<property name="text">
<string>Pipelined (Second GS Thread)</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
@@ -365,6 +399,7 @@
<tabstop>gsDumpCompression</tabstop>
<tabstop>texturePreloading</tabstop>
<tabstop>exclusiveFullscreenControl</tabstop>
<tabstop>gsBackThreadMode</tabstop>
<tabstop>extendedUpscales</tabstop>
<tabstop>spinGPUDuringReadbacks</tabstop>
<tabstop>spinCPUDuringReadbacks</tabstop>
@@ -244,6 +244,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* settings_dialog,
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_advanced.disableShaderCache, "EmuCore/GS", "DisableShaderCache", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_advanced.disableVertexShaderExpand, "EmuCore/GS", "DisableVertexShaderExpand", false);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_advanced.gsDownloadMode, "EmuCore/GS", "HWDownloadMode", static_cast<int>(GSHardwareDownloadMode::Enabled));
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_advanced.gsBackThreadMode, "EmuCore/GS", "GSBackThreadMode", static_cast<int>(GSBackThreadMode::Off));
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_advanced.ntscFrameRate, "EmuCore/GS", "FrameRateNTSC", 59.94f);
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_advanced.palFrameRate, "EmuCore/GS", "FrameRatePAL", 50.00f);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_advanced.spinCPUDuringReadbacks, "EmuCore/GS", "HWSpinCPUForReadbacks", false);
@@ -797,6 +798,12 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* settings_dialog,
"Can result in a large speed boost on slower systems, at the cost of many broken graphical effects. "
"If games are broken and you have this option enabled, please disable it first."));
dialog()->registerWidgetHelp(m_advanced.gsBackThreadMode, tr("GS Back Thread"), tr("Disabled"),
tr("Pipelined splits GS emulation across two threads: one parses GIF data and builds vertices while the other runs draws, "
"the texture cache, and the GPU device. Can significantly reduce GS thread time on multi-core systems with spare cores, "
"but competes for cores with the EE/VU threads. The Inline Records and Lockstep modes are debugging tools and much "
"slower — do not use them for play."));
dialog()->registerWidgetHelp(m_advanced.ntscFrameRate, tr("NTSC Frame Rate"), tr("59.94 Hz"),
tr("Determines what frame rate NTSC games run at."));
+1
View File
@@ -540,6 +540,7 @@ set(pcsx2GSSources
set(pcsx2GSHeaders
GS/GSAlignedClass.h
GS/GSBlock.h
GS/GSBackQueue.h
GS/GSCapture.h
GS/GSClut.h
GS/GSDrawingContext.h
+13
View File
@@ -496,6 +496,18 @@ enum class GSDepthFeedbackMode : u8
DepthAsRT = 3,
};
// GV-7 GS front/back split. Off = today's single-threaded path with no record
// round-trip; InlineRecords = build + execute every record on the calling
// thread (the GV7-0 shape — validation / bisect rung); Lockstep = back thread
// runs but the front drains after every record; Pipelined = the real thing.
enum class GSBackThreadMode : u8
{
Off = 0,
InlineRecords = 1,
Lockstep = 2,
Pipelined = 3,
};
enum class AchievementOverlayPosition : u8
{
TopLeft,
@@ -929,6 +941,7 @@ struct Pcsx2Config
TriFiltering TriFilter = DEFAULT_TRILINEAR_FILTERING_MODE;
s8 OverrideTextureBarriers = -1;
GSDepthFeedbackMode DepthFeedbackMode = GSDepthFeedbackMode::Auto;
GSBackThreadMode BackThreadMode = GSBackThreadMode::Off;
// RetroArch (.slangp) shader chain, applied at present after ShadeBoost/FXAA via
// librashader. Disabled or an empty preset skips the chain entirely (zero cost),
+107 -57
View File
@@ -241,6 +241,17 @@ static void ApplyAndroidGameDBOverrides()
}
#endif
// 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().
@@ -265,6 +276,26 @@ 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). Unsynchronized HW downloads read local memory from the EE
// thread with no drain — that session runs single-object (lockstep).
if (GSConfig.BackThreadMode == GSBackThreadMode::Pipelined && g_gs_renderer->IsBackThreadRunning())
{
if (GSConfig.HWDownloadMode == GSHardwareDownloadMode::Unsynchronized && GSConfig.UseHardwareRenderer())
{
Console.Warning("GS: pipelined mode is unsupported with unsynchronized HW downloads — running lockstep.");
}
else
{
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, pipelined).");
}
}
g_perfmon.Reset();
return true;
}
@@ -273,6 +304,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();
@@ -285,7 +320,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)
{
@@ -315,7 +350,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;
@@ -323,7 +358,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;
@@ -371,7 +406,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;
@@ -427,6 +462,11 @@ void GSclose()
void GSreset(bool hardware_reset)
{
// Front first: its Reset flushes pending buffered draws into records; the
// back's Reset then drains (executing them, like serial pre-reset draws)
// before resetting memory/TC.
if (g_gs_front)
g_gs_front->Reset(hardware_reset);
g_gs_renderer->Reset(hardware_reset);
// Restart video capture if it's been started.
@@ -443,44 +483,44 @@ 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);
}
// Manual frameskip target (Android). Set from the UI thread via the JNI
@@ -535,29 +575,36 @@ bool GSGetPresentCapSuspended()
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.
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);
g_gs_renderer->VSync(field, registers_written, g_gs_renderer->IsIdleFrame());
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)
{
@@ -571,12 +618,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);
}
@@ -899,6 +952,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();
@@ -938,6 +997,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 (
@@ -1019,14 +1080,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;
@@ -1045,7 +1105,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;
@@ -1058,6 +1118,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;
}
@@ -1065,15 +1126,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;
@@ -1081,7 +1139,6 @@ void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
}
VirtualFreeEx(GetCurrentProcess(), ptr, 0, MEM_RELEASE);
s_fh = NULL;
}
#else
@@ -1091,17 +1148,16 @@ 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);
// Route fd creation through HostSys::CreateSharedMemory so iOS gets the
// file-backed fallback the helper already provides for the rest of the
// codebase. The bare shm_open("/GS.mem", ...) the prior implementation
// used is rejected by the iOS sandbox, which returned -1 and propagated
// nullptr up to GSLocalMemory.
// 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).
// Creation routes through HostSys::CreateSharedMemory so iOS gets the
// file-backed fallback (the bare shm_open the prior implementation used
// is rejected by the iOS sandbox); the helper unlinks the name (or uses
// memfd) immediately, so coexisting allocations never collide on it, and
// it ftruncates to the requested size before returning.
const std::string file_name = HostSys::GetFileMappingName("GS.mem");
void* const handle = HostSys::CreateSharedMemory(file_name.c_str(), repeat * size);
if (!handle)
@@ -1112,32 +1168,26 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
size, repeat, repeat * size);
return nullptr;
}
s_shm_fd = static_cast<int>(reinterpret_cast<intptr_t>(handle));
const int fd = static_cast<int>(reinterpret_cast<intptr_t>(handle));
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);
HostSys::DestroySharedMemory(reinterpret_cast<void*>(static_cast<intptr_t>(s_shm_fd)));
s_shm_fd = -1;
}
#endif
+391
View File
@@ -0,0 +1,391 @@
// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "GS/GS.h"
#include "GS/GSRegs.h"
#include "GS/GSVector.h"
#include "GS/GSDrawingContext.h"
#include "GS/GSDrawingEnvironment.h"
#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.
// Every record carries its register snapshot, so the consumer needs no live
// register state machine. Records are built by the front-side seam functions
// (FlushWrite / Move / ...) and consumed by GSState::Exec*Record — executed
// inline today, and on the back thread once the GV7-1 queue lands.
// Seam classification: scratchpad/gv7-2026-07/SEAM-AUDIT.md.
namespace GSBackQueue
{
// One slice of a HOST->LOCAL transfer (today's FlushWrite body, or the
// whole-packet fast path in GSState::Write). A logical transfer is one
// first_slice record followed by zero or more continuation slices; the
// executor owns the write cursor across slices.
struct TransferRecord
{
GIFRegBITBLTBUF blit; // m_tr.m_blit: wi() blit argument + partial-end fixup
GIFRegBITBLTBUF env_blit; // invalidate rect + wi selection: m_env.BITBLTBUF in
// FlushWrite, m_tr.m_blit on the Write fast path —
// they diverge if BITBLTBUF is rewritten mid-transfer
GIFRegTRXPOS pos;
GIFRegTRXREG reg;
GSVector4i rect; // m_tr.rect
const u8* payload;
int len; // bytes handed to wi()
int stat_len; // bytes counted for the Swizzle perfmon stat (the Write
// fast path counts the raw packet length, which can
// exceed the transfer total — preserved exactly)
int end; // m_tr.end as of this slice (partial-end fixup input)
int total; // m_tr.total
int init_x, init_y; // write-cursor init, consumed when first_slice
u64 draw_serial; // s_n at build time (upload-queue entry stamping)
bool first_slice; // initialises the cursor + pushes the upload-queue entry
};
// LOCAL->LOCAL blit. The executor installs these registers and runs the
// virtual Move chain (HW hack -> TC move -> software blit) unchanged.
struct MoveRecord
{
GIFRegBITBLTBUF blit;
GIFRegTRXPOS pos;
GIFRegTRXREG reg;
u64 draw_serial; // consumed once GV7-0d makes serials record-carried
};
// CLUT palette load. The decision chain (WriteTest / CanLoadCLUT /
// InvalidateRange dirty tracking) is register/address-only and stays
// front-side; this record triggers the back-side palette-byte read from
// local memory into the CLUT buffer.
struct ClutLoadRecord
{
GIFRegTEX0 TEX0; // post-CPSM-mask, as installed in m_env.CTXT[i]
GIFRegTEXCLUT TEXCLUT;
};
// Vertex/index buffer sets — the DRAW record payload. Hoisted from GSState
// (front fills them at kick time, the draw executor consumes and mutates
// them in place); the GV7-1 pool hands ownership across the boundary.
struct VertexBuff
{
GSVertex* buff;
GSVertex* buff_copy; // same size buffer to copy/modify the original buffer
u32 head, tail, next, maxcount; // head: first vertex, tail: last vertex + 1, next: last indexed + 1
u32 xy_tail;
GSVector4i xy[4];
GSVector4i xyhead;
// Scalar mirror of xy[] for the outcode cull fast path: written wherever
// xy[] is written, outcodes re-derived on scissor change (RefreshKickMirror).
GSVertexKernels::CullMirrorEntry kick_ring[4];
// Fused vertex-trace bounds (aarch64 only): FindMinMax min/max accumulated
// at index emission over this buffer's referenced vertices. fmm_watermark is
// the first vertex position not yet folded in (clamped on rewinds/compaction
// so re-referenced positions re-accumulate); fmm_valid means the accumulator
// covers every emitted index of the pending draw. Reset lazily at the first
// emission of a draw (itail == n).
GSVertexKernels::FmmAcc fmm_acc;
u32 fmm_watermark;
bool fmm_valid;
};
struct IndexBuff
{
u16* buff;
u32 tail;
};
// GV7-1c: one pooled vertex+index buffer set. On the record path, FlushPrim
// hands the live heap arrays to a node (struct copy + array exchange: the
// parse slot takes the node's recycled arrays as its fresh buffers), so the
// DRAW record's payload stays valid until consumed while the front keeps
// parsing into the same GSState buffer slots it always did. The consumer
// releases the node after the draw executes.
struct DrawNode
{
VertexBuff vb;
IndexBuff ib;
};
// GV7-1c: pooled transfer staging buffer (4MB, the GSTransferBuffer size).
// In record modes m_tr.buff aliases the current node's buffer: the front
// stages into it and TRANSFER records reference slices of it (disjoint
// ranges, so front-appends and back-reads never overlap). At the next
// transfer Init after any slice referenced the buffer, the front rotates to
// a fresh node and emits a RELEASE_PAYLOAD record behind the slices — FIFO
// guarantees they were consumed by the time the release executes.
struct PayloadNode
{
u8* buff;
};
struct ReleasePayloadRecord
{
PayloadNode* node;
};
// PCRTC digest state — hoisted from GSState (GSvsync writes it once per
// frame from the privileged registers; the Draw() heuristics and the Merge
// circuit read it back-side, so it ships whole in PCRTC_SYNC records).
struct GSPCRTCRegs
{
struct PCRTCDisplay
{
bool enabled;
int FBP;
int FBW;
int PSM;
int DBY;
int DBX;
GSRegDISPFB prevFramebufferReg;
GSVector2i prevDisplayOffset;
GSVector2i displayOffset;
GSVector4i displayRect;
GSVector2i magnification;
GSVector2i prevFramebufferOffsets;
GSVector2i framebufferOffsets;
GSVector4i framebufferRect;
__fi int Block() const { return FBP << 5; }
};
int videomode = 0;
int interlaced = 0;
int FFMD = 0;
bool PCRTCSameSrc = false;
bool toggling_field = false;
PCRTCDisplay PCRTCDisplays[2] = {};
bool IsAnalogue();
// Calculates which display is closest to matching zero offsets in either direction.
GSVector2i NearestToZeroOffset();
void SetVideoMode(GSVideoMode videoModeIn);
// Enable each of the displays.
void EnableDisplays(GSRegPMODE pmode, GSRegSMODE2 smode2, bool smodetoggle);
void CheckSameSource();
bool FrameWrap();
// If the start point of both frames match, we can do a single read
bool FrameRectMatch();
GSVector2i GetResolution();
GSVector4i GetFramebufferRect(int display);
int GetFramebufferBitDepth();
GSVector2i GetFramebufferSize(int display);
// Sets up the rectangles for both the framebuffer read and the displays for the merge circuit.
void SetRects(int display, GSRegDISPLAY displayReg, GSRegDISPFB framebufferReg);
// Calculate framebuffer read offsets, should be considered if only one circuit is enabled, or difference is more than 1 line.
// Only considered if "Anti-blur" is enabled.
void CalculateFramebufferOffset(bool scanmask, GSRegDISPFB framebuffer0Reg, GSRegDISPFB framebuffer1Reg);
// Used in software mode to align the buffer when reading. Offset is accounted for (block aligned) by GetOutput.
void RemoveFramebufferOffset(int display);
// If the two displays are offset from each other, move them to the correct offsets.
// If using screen offsets, calculate the positions here.
void CalculateDisplayOffset(bool scanmask);
};
// Once-per-frame PCRTC digest, shipped BEFORE the vsync-flushed draw
// records so those draws see the fresh display state, exactly like today
// (GSvsync digests, then flushes). Mid-frame draws keep seeing the previous
// frame's digest, also like today.
struct PcrtcSyncRecord
{
GSPCRTCRegs displays;
u8 scanmask_used; // pre-decrement value; Merge's decrement stays back-side
};
// End of frame: the whole VSync() body (Merge -> present -> capture ->
// perfmon frame tick) runs back-side.
struct VsyncRecord
{
u32 field;
bool registers_written;
bool idle_frame;
};
// One flushed draw (today's FlushPrim tail: vertex trace -> texel rounding ->
// Draw() -> perfmon). Self-contained: the executor installs the env snapshots
// and scalars, then runs the tail against the referenced buffers, which it
// owns and may mutate in place (texel rounding, HW draw rewrites). The
// carry-over window is captured front-side before the record is built.
struct DrawRecord
{
// Installed into the consumer's m_prev_env: the draw's own environment,
// exactly as FlushBuffers staged it before the flush.
GSDrawingEnvironment draw_env;
// Next-draw peek for the HW look-ahead heuristics: the live m_env/m_v at
// build time — bit-exact with today because FlushBuffers installs buffer
// i+1's env into m_env before flushing buffer i.
GSDrawingEnvironment next_env;
GSVertex next_v;
GSVector4i draw_rect; // temp_draw_rect at flush
VertexBuff* vertex; // = &node->vb/&node->ib on the record path
IndexBuff* index;
DrawNode* node; // released by the consumer after the tail runs (null in tests)
u64 draw_serial; // front-assigned s_n
int backed_up_ctx;
u32 dirty_gs_regs;
int flush_reason; // GSState::GSFlushReason (class-scoped enum, stored widened)
bool channel_shuffle_finish;
bool packed_uv_hack_flag;
};
// ------------------------------------------------------------------
// GV7-1: the front->back SPSC ring.
// ------------------------------------------------------------------
// Discriminator for ring slots.
enum class RecordType : u8
{
Transfer,
Move,
ClutLoad,
PcrtcSync,
Vsync,
Draw,
ReleasePayload,
};
// Single-producer/single-consumer ring. Producer = the MTGS ("front") thread,
// consumer = the GS back thread. Indices are free-running u32s over a
// power-of-two slot count; acquire/release only — no RMW, so this is
// armv8.0-safe (compiles to plain LDAR/STLR).
template <typename SlotT, u32 kCount>
class SpscRing
{
static_assert(kCount != 0 && (kCount & (kCount - 1)) == 0, "slot count must be a power of two");
public:
SpscRing()
: m_slots(std::make_unique<SlotT[]>(kCount))
{
}
static constexpr u32 Capacity() { return kCount; }
u32 Size() const { return m_tail.load(std::memory_order_acquire) - m_head.load(std::memory_order_acquire); }
bool IsEmpty() const { return Size() == 0; }
// Producer side. BeginPush returns the slot to fill in place (records are
// built directly in the ring — no intermediate copy), or nullptr when the
// ring is full (caller applies backpressure). CommitPush publishes the
// slot to the consumer.
SlotT* BeginPush()
{
const u32 tail = m_tail.load(std::memory_order_relaxed);
if (tail - m_head.load(std::memory_order_acquire) == kCount)
return nullptr;
return &m_slots[tail & (kCount - 1)];
}
void CommitPush()
{
m_tail.store(m_tail.load(std::memory_order_relaxed) + 1, std::memory_order_release);
}
// Consumer side. Peek returns the oldest unconsumed slot (nullptr when
// empty); Pop retires it, releasing the slot back to the producer.
SlotT* Peek()
{
const u32 head = m_head.load(std::memory_order_relaxed);
if (head == m_tail.load(std::memory_order_acquire))
return nullptr;
return &m_slots[head & (kCount - 1)];
}
void Pop()
{
m_head.store(m_head.load(std::memory_order_relaxed) + 1, std::memory_order_release);
}
private:
std::unique_ptr<SlotT[]> m_slots;
alignas(64) std::atomic<u32> m_head{0}; // consumer cursor
alignas(64) std::atomic<u32> m_tail{0}; // producer cursor
};
// Tagged slot sized for the largest record (DRAW). All records are trivially
// copyable (asserted below), so slots are reused with no destructor
// bookkeeping.
struct RecordSlot
{
RecordType type;
alignas(alignof(DrawRecord)) u8 data[sizeof(DrawRecord)];
template <typename T>
T* As()
{
static_assert(std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(data));
return reinterpret_cast<T*>(data);
}
template <typename T>
const T* As() const
{
static_assert(std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(data));
return reinterpret_cast<const T*>(data);
}
};
static_assert(std::is_trivially_copyable_v<TransferRecord>);
static_assert(std::is_trivially_copyable_v<MoveRecord>);
static_assert(std::is_trivially_copyable_v<ClutLoadRecord>);
static_assert(std::is_trivially_copyable_v<PcrtcSyncRecord>);
static_assert(std::is_trivially_copyable_v<VsyncRecord>);
static_assert(std::is_trivially_copyable_v<DrawRecord>);
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
+6 -2
View File
@@ -216,12 +216,16 @@ bool GSClut::WriteTest(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
return m_write.IsDirty(TEX0, TEXCLUT);
}
void GSClut::Write(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
void GSClut::WriteDecision(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
{
m_write.TEX0 = TEX0;
m_write.TEXCLUT = TEXCLUT;
m_read.dirty = true;
m_write.dirty = 0;
}
void GSClut::WriteLoad(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
{
m_read.dirty = true;
(this->*m_wc[TEX0.CSM][TEX0.CPSM][TEX0.PSM])(TEX0, TEXCLUT);
}
+4 -1
View File
@@ -118,7 +118,10 @@ public:
void SetNextCLUTTEX0(u64 CBP);
bool CanLoadCLUT(const GIFRegTEX0& TEX0, const bool update_CBP = false);
bool WriteTest(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
void Write(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
// GV-7 split of the old Write(): the decision state update (front side)
// and the palette load from local memory (back-executable).
void WriteDecision(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
void WriteLoad(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
//void Read(const GIFRegTEX0& TEX0);
void Read32(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA);
void GetAlphaMinMax32(int& amin, int& amax);
+936 -231
View File
File diff suppressed because it is too large Load Diff
+195 -98
View File
@@ -7,6 +7,7 @@
#include "GS/GSPerfMon.h"
#include "GS/GSLocalMemory.h"
#include "GS/GSVertexKick.h"
#include "GS/GSBackQueue.h"
#include "GS/GSDrawingContext.h"
#include "GS/GSDrawingEnvironment.h"
#include "GS/Renderers/Common/GSVertex.h"
@@ -15,6 +16,13 @@
#include "GS/GSVector.h"
#include "GSAlignedClass.h"
#include "common/Threading.h"
#include <atomic>
#include <cstring>
#include <thread>
#include <vector>
class GSDumpBase;
class GSState : public GSAlignedClass<32>
@@ -22,11 +30,28 @@ 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; }
// 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:
@@ -131,6 +156,11 @@ private:
} m_tr;
protected:
// Executor-owned HOST->LOCAL write cursor (advanced by wi() across transfer
// slices; mirrored back into m_tr.x/y inline for savestate coherence).
int m_exec_tr_x = 0;
int m_exec_tr_y = 0;
static constexpr int INVALID_ALPHA_MINMAX = 500;
static constexpr int MAX_DRAW_BUFFERS = 3;
@@ -141,37 +171,13 @@ protected:
int m_current_buffer_idx = 0;
bool m_recent_buffer_switch = false;
struct GSVertexBuff
{
GSVertex* buff;
GSVertex* buff_copy; // same size buffer to copy/modify the original buffer
u32 head, tail, next, maxcount; // head: first vertex, tail: last vertex + 1, next: last indexed + 1
u32 xy_tail;
GSVector4i xy[4];
GSVector4i xyhead;
// Scalar mirror of xy[] for the outcode cull fast path: written wherever
// xy[] is written, outcodes re-derived on scissor change (RefreshKickMirror).
GSVertexKernels::CullMirrorEntry kick_ring[4];
// Fused vertex-trace bounds (aarch64 only): FindMinMax min/max accumulated
// at index emission over this buffer's referenced vertices. fmm_watermark is
// the first vertex position not yet folded in (clamped on rewinds/compaction
// so re-referenced positions re-accumulate); fmm_valid means the accumulator
// covers every emitted index of the pending draw. Reset lazily at the first
// emission of a draw (itail == n).
GSVertexKernels::FmmAcc fmm_acc;
u32 fmm_watermark;
bool fmm_valid;
};
// Definitions hoisted to GSBackQueue.h (DRAW record payload types).
using GSVertexBuff = GSBackQueue::VertexBuff;
using GSIndexBuff = GSBackQueue::IndexBuff;
GSVertexBuff m_vertex_buffers[MAX_DRAW_BUFFERS];
GSVertexBuff* m_vertex = nullptr;
struct GSIndexBuff
{
u16* buff;
u32 tail;
};
GSIndexBuff m_index_buffers[MAX_DRAW_BUFFERS];
GSIndexBuff* m_index;
@@ -326,6 +332,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);
@@ -415,9 +425,13 @@ public:
GSVector4i m_r = {};
GSVector4i m_r_no_scissor = {};
static u64 s_n;
static u64 s_last_transfer_draw_n;
static u64 s_transfer_n;
// GV7-1d-ii-c: per-object serial counters (were process statics). The
// front assigns draw/transfer order and carries serials in records; the
// back installs them at execution, so its TC/heuristic reads see the
// executing draw's serial, not the front's runahead position.
u64 s_n = 0;
u64 s_last_transfer_draw_n = 0;
u64 s_transfer_n = 0;
GSPerfMon m_perfmon_frame; // Track stat across a frame.
GSPerfMon m_perfmon_draw; // Track stat across a draw.
@@ -495,74 +509,10 @@ public:
std::vector<size_t> m_drawlist;
std::vector<GSVector4i> m_drawlist_bbox;
struct GSPCRTCRegs
{
struct PCRTCDisplay
{
bool enabled;
int FBP;
int FBW;
int PSM;
int DBY;
int DBX;
GSRegDISPFB prevFramebufferReg;
GSVector2i prevDisplayOffset;
GSVector2i displayOffset;
GSVector4i displayRect;
GSVector2i magnification;
GSVector2i prevFramebufferOffsets;
GSVector2i framebufferOffsets;
GSVector4i framebufferRect;
// Definition hoisted to GSBackQueue.h (PCRTC_SYNC record payload type).
using GSPCRTCRegs = GSBackQueue::GSPCRTCRegs;
__fi int Block() const { return FBP << 5; }
};
int videomode = 0;
int interlaced = 0;
int FFMD = 0;
bool PCRTCSameSrc = false;
bool toggling_field = false;
PCRTCDisplay PCRTCDisplays[2] = {};
bool IsAnalogue();
// Calculates which display is closest to matching zero offsets in either direction.
GSVector2i NearestToZeroOffset();
void SetVideoMode(GSVideoMode videoModeIn);
// Enable each of the displays.
void EnableDisplays(GSRegPMODE pmode, GSRegSMODE2 smode2, bool smodetoggle);
void CheckSameSource();
bool FrameWrap();
// If the start point of both frames match, we can do a single read
bool FrameRectMatch();
GSVector2i GetResolution();
GSVector4i GetFramebufferRect(int display);
int GetFramebufferBitDepth();
GSVector2i GetFramebufferSize(int display);
// Sets up the rectangles for both the framebuffer read and the displays for the merge circuit.
void SetRects(int display, GSRegDISPLAY displayReg, GSRegDISPFB framebufferReg);
// Calculate framebuffer read offsets, should be considered if only one circuit is enabled, or difference is more than 1 line.
// Only considered if "Anti-blur" is enabled.
void CalculateFramebufferOffset(bool scanmask, GSRegDISPFB framebuffer0Reg, GSRegDISPFB framebuffer1Reg);
// Used in software mode to align the buffer when reading. Offset is accounted for (block aligned) by GetOutput.
void RemoveFramebufferOffset(int display);
// If the two displays are offset from each other, move them to the correct offsets.
// If using screen offsets, calculate the positions here.
void CalculateDisplayOffset(bool scanmask);
} PCRTCDisplays;
GSPCRTCRegs PCRTCDisplays;
public:
/// Returns the appropriate directory for draw dumping.
@@ -608,6 +558,114 @@ public:
virtual void Move();
// GV-7 front/back seam (SEAM-AUDIT.md): the front builds a self-contained
// record, the Exec*Record executor consumes it — inline today, on the back
// thread once GV7-1 lands. The executor owns the HOST->LOCAL write cursor
// across transfer slices.
void ExecTransferRecord(const GSBackQueue::TransferRecord& rec);
void SubmitMove();
void ExecMoveRecord(const GSBackQueue::MoveRecord& rec);
void SubmitClutLoad(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT);
void ExecClutLoadRecord(const GSBackQueue::ClutLoadRecord& rec);
void ExecDrawRecord(const GSBackQueue::DrawRecord& rec);
void DrawRecordTail(u64 draw_serial);
void SubmitPcrtcSync();
void ExecPcrtcSyncRecord(const GSBackQueue::PcrtcSyncRecord& rec);
// GV7-1: sampled from GSConfig.BackThreadMode at construction (the option is
// restart-required, so it can't change under a live GSState). Off = the
// front-side seam functions skip the record round-trip entirely and call the
// 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-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:
// 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.
// 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);
// GV7-1d: the back thread (modes Lockstep and, for now, Pipelined — true
// pipelining needs the front-object split, so Pipelined runs lockstep until
// then). Lockstep = drain after every push, which is what makes executing
// against the shared single-object state safe. VSYNC records are NOT queued:
// present runs on the MTGS thread after a drain, so the back thread never
// touches the GSDevice on present paths (and for SW, at all). Queued modes
// engage only for Vulkan and SW renderers — a GL device is context-bound to
// the MTGS thread and HW draws would issue GL calls from the wrong thread.
bool m_back_queued = false;
bool m_back_lockstep = false;
std::thread m_back_thread;
std::atomic<bool> m_back_thread_exit{false};
void StartBackThread();
void StopBackThread();
void BackThreadLoop();
void ExecRecordSlot(const GSBackQueue::RecordSlot& slot);
virtual void ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec);
template <typename T>
void PushRecord(GSBackQueue::RecordType type, const T& rec)
{
for (;;)
{
GSBackQueue::RecordSlot* slot = m_chan->ring.BeginPush();
if (slot)
{
slot->type = type;
std::memcpy(slot->As<T>(), &rec, sizeof(T));
m_chan->ring.CommitPush();
m_chan->sema.NotifyOfWork();
break;
}
std::this_thread::yield(); // ring full — backpressure
}
// Spin-then-sleep: records usually execute in microseconds, so the spin
// catches nearly every drain without the futex round-trip. Lockstep is
// still per-record synchronization and inherently slow (measured 30->6
// fps on MQ65 with plain WaitForEmpty) — it's the bisect rung, not a
// shipping mode.
if (m_back_lockstep)
m_chan->sema.WaitForEmptyWithSpin();
}
GSVector4i GetTEX0Rect(GSDrawingContext prev_ctx);
void CheckWriteOverlap(bool req_write, bool req_read);
void Write(const u8* mem, int len);
@@ -644,6 +702,45 @@ 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;
// Kick-time coverage-alpha query. Mixed live/stale semantics (see the
// implementation); needs last-flushed-draw state that only exists after
// that draw EXECUTED, so it drains the back queue — memoized per
// (draw epoch, live ALPHA) so at most one drain per AA1 draw.
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;
// IsCoverageAlphaSupported memo (see above).
u64 m_cov_epoch = ~0ULL;
u64 m_cov_alpha = 0;
bool m_cov_answer = false;
};
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)
{
+28
View File
@@ -629,6 +629,25 @@ void GSRenderer::EndPresentFrame()
ImGuiManager::NewFrame();
}
void GSRenderer::SubmitVsync(u32 field, bool registers_written)
{
GSBackQueue::VsyncRecord rec;
rec.field = field;
rec.registers_written = registers_written;
rec.idle_frame = IsIdleFrame(); // front-computable: compares serials against the last frame's
// VSYNC is never queued: present runs on the MTGS thread behind a drain, so
// the back thread stays off the GSDevice on present paths entirely (which
// is also what keeps SW + GL-present devices legal in queued modes).
DrainBackQueue();
ExecVsyncRecord(rec);
}
void GSRenderer::ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec)
{
VSync(rec.field, rec.registers_written, rec.idle_frame);
}
void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
{
if (GSConfig.ShouldDump(s_n, g_perfmon.GetFrame()))
@@ -1109,6 +1128,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 ?
@@ -1122,6 +1144,7 @@ bool GSRenderer::BeginCapture(std::string filename, const GSVector2i& size)
void GSRenderer::EndCapture()
{
DrainBackQueue(); // see BeginCapture
GSCapture::EndCapture();
}
@@ -1138,6 +1161,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)
{
+2
View File
@@ -41,6 +41,8 @@ public:
virtual void UpdateRenderFixes();
virtual void VSync(u32 field, bool registers_written, bool idle_frame);
void SubmitVsync(u32 field, bool registers_written);
void ExecVsyncRecord(const GSBackQueue::VsyncRecord& rec) override;
virtual bool CanUpscale() { return false; }
virtual float GetUpscaleMultiplier() { return 1.0f; }
virtual float GetTextureScaleFactor() { return 1.0f; }
+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();
@@ -556,7 +556,7 @@ bool GSRendererHWFunctions::SwPrimRender(GSRendererHW& hw, bool invalidate_tc, b
uq.blit.DBP = hw.m_cached_ctx.FRAME.Block();
uq.blit.DBW = hw.m_cached_ctx.FRAME.FBW;
uq.blit.DPSM = hw.m_cached_ctx.FRAME.PSM;
uq.draw = GSState::s_n;
uq.draw = hw.s_n;
uq.rect = bbox;
hw.m_draw_transfers.push_back(uq);
}
+18 -18
View File
@@ -430,7 +430,7 @@ GSVector4i GSTextureCache::TranslateAlignedRectByPage(u32 tbp, u32 tebp, u32 tbw
// The width is mismatched to the page.
if (!is_invalidation && GSConfig.UserHacks_TextureInsideRt < GSTextureInRtMode::MergeTargets)
{
DevCon.Warning("Uneven pages mess up sbp %x dbp %x spgw %d dpgw %d src fmt %d dst fmt %d src_rect %d, %d, %d, %d draw %lld", sbp, tbp, src_pgw, dst_pgw, spsm, tpsm, in_rect.x, in_rect.y, in_rect.z, in_rect.w, GSState::s_n);
DevCon.Warning("Uneven pages mess up sbp %x dbp %x spgw %d dpgw %d src fmt %d dst fmt %d src_rect %d, %d, %d, %d draw %lld", sbp, tbp, src_pgw, dst_pgw, spsm, tpsm, in_rect.x, in_rect.y, in_rect.z, in_rect.w, GSRendererHW::GetInstance()->s_n);
return GSVector4i::zero();
}
@@ -1423,7 +1423,7 @@ GSTextureCache::Source* GSTextureCache::LookupSource(const bool is_color, const
// Also is we have already found a target which we had to offset in to by using a region or exact address,
// it's probable that's more correct than being inside (Tomb Raider Legends + Project Snowblind)
// Vakyrie Profile 2 also has some in draws which get done on a different target due to a slight offset, so we need to make sure we have the newer one.
if (!overlaps || (found_t && (GSState::s_n - dst->m_last_draw) < (GSState::s_n - t->m_last_draw)))
if (!overlaps || (found_t && (GSRendererHW::GetInstance()->s_n - dst->m_last_draw) < (GSRendererHW::GetInstance()->s_n - t->m_last_draw)))
continue;
// If the BP is offset in to a page and the format does not match, trying to match up the correct position is very difficult since we don't swizzle.
@@ -1735,7 +1735,7 @@ GSTextureCache::Source* GSTextureCache::LookupSource(const bool is_color, const
match = false;
// Different swizzle, different width, and dirty, so probably not what we want.
// DevCon.Warning("Expected %x Got %x shuffle %d draw %d", psm, t_psm, possible_shuffle, GSState::s_n);
// DevCon.Warning("Expected %x Got %x shuffle %d draw %d", psm, t_psm, possible_shuffle, GSRendererHW::GetInstance()->s_n);
if (match)
{
// It is a complex to convert the code in shader. As a reference, let's do it on the CPU,
@@ -2365,7 +2365,7 @@ void GSTextureCache::CombineAlignedInsideTargets(Target* target, GSTextureCache:
const bool valid_color = t->m_valid_rgb;
const bool valid_alpha = (t->m_valid_alpha_high || t->m_valid_alpha_low) && (GSUtil::GetChannelMask(t->m_TEX0.PSM) & 0x8);
GL_CACHE("Combining %x-%x in to %x-%x draw %lld", t->m_TEX0.TBP0, t->m_end_block, target->m_TEX0.TBP0, target->m_end_block, GSState::s_n);
GL_CACHE("Combining %x-%x in to %x-%x draw %lld", t->m_TEX0.TBP0, t->m_end_block, target->m_TEX0.TBP0, target->m_end_block, GSRendererHW::GetInstance()->s_n);
if (target->m_type == RenderTarget)
{
@@ -2463,7 +2463,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
{
bool can_use = true;
if (new_dst && ((GSState::s_n - new_dst->m_last_draw) < (GSState::s_n - t->m_last_draw) && new_dst->m_TEX0.TBP0 <= bp))
if (new_dst && ((GSRendererHW::GetInstance()->s_n - new_dst->m_last_draw) < (GSRendererHW::GetInstance()->s_n - t->m_last_draw) && new_dst->m_TEX0.TBP0 <= bp))
{
DevCon.Warning("Ignoring target at %x as one at %x is newer", t->m_TEX0.TBP0, new_dst->m_TEX0.TBP0);
i++;
@@ -2589,12 +2589,12 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
const GSLocalMemory::psm_t& s_psm = GSLocalMemory::m_psm[TEX0.PSM];
const u32 widthpage_offset = (std::abs(static_cast<int>(bp - t->m_TEX0.TBP0)) >> 5) % std::max(t->m_TEX0.TBW, 1U);
const bool is_aligned_ok = widthpage_offset == 0 || ((min_rect.width() <= static_cast<int>((t->m_TEX0.TBW - widthpage_offset) * 64) && (t->m_TEX0.TBW == TEX0.TBW || TEX0.TBW == 1)) && bp >= t->m_TEX0.TBP0);
const bool no_target_or_newer = (!new_dst || ((GSState::s_n - new_dst->m_last_draw) < (GSState::s_n - t->m_last_draw)));
const bool no_target_or_newer = (!new_dst || ((GSRendererHW::GetInstance()->s_n - new_dst->m_last_draw) < (GSRendererHW::GetInstance()->s_n - t->m_last_draw)));
const bool width_match = (t->m_TEX0.TBW == TEX0.TBW || (TEX0.TBW == 1 && draw_rect.w <= GSLocalMemory::m_psm[t->m_TEX0.PSM].pgs.y));
const bool ds_offset = !ds || offset != 0;
const bool is_double_buffer = TEX0.TBP0 == ((((t->m_end_block + 1) - t->m_TEX0.TBP0) / 2) + t->m_TEX0.TBP0);
const bool source_match = src && src->m_TEX0.TBP0 <= bp && src->m_end_block > bp && src->m_TEX0.TBW == TEX0.TBW && src->m_from_target && src->m_from_target == t && t->Inside(bp, TEX0.TBW, TEX0.PSM, min_rect);
const bool was_used_last_draw = t->m_last_draw == (GSState::s_n - 1);
const bool was_used_last_draw = t->m_last_draw == (GSRendererHW::GetInstance()->s_n - 1);
// if it's a shuffle, some games tend to offset back by a page, such as Tomb Raider, for no disernable reason, but it then causes problems.
// This can also happen horizontally (Catwoman moves everything one page left with shuffles), but this is too messy to deal with right now.
const bool overlaps = t->Overlaps(bp, TEX0.TBW, TEX0.PSM, min_rect) || (is_shuffle && src && GSLocalMemory::m_psm[src->m_TEX0.PSM].bpp == 8 && t->Overlaps(bp, TEX0.TBW, TEX0.PSM, min_rect + GSVector4i(0, 0, 0, s_psm.pgs.y - (min_rect.w & (s_psm.pgs.y - 1)))));
@@ -2612,7 +2612,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
}
// I know what you're thinking, and I hate the guy who wrote it too (me). Project Snowblind, Tomb Raider etc decide to offset where they're drawing using a channel shuffle, and this gets messy, so best just to kill the old target.
if (is_shuffle && src && src->m_TEX0.PSM == PSMT8 && GSRendererHW::GetInstance()->m_context->FRAME.FBW == 1 && t->m_last_draw != (GSState::s_n - 1) && src->m_from_target && (src->m_from_target->m_TEX0.TBP0 == src->m_TEX0.TBP0 || (((src->m_TEX0.TBP0 - src->m_from_target->m_TEX0.TBP0) >> 5) % std::max(src->m_from_target->m_TEX0.TBW, 1U) == 0)) && widthpage_offset && src->m_from_target != t)
if (is_shuffle && src && src->m_TEX0.PSM == PSMT8 && GSRendererHW::GetInstance()->m_context->FRAME.FBW == 1 && t->m_last_draw != (GSRendererHW::GetInstance()->s_n - 1) && src->m_from_target && (src->m_from_target->m_TEX0.TBP0 == src->m_TEX0.TBP0 || (((src->m_TEX0.TBP0 - src->m_from_target->m_TEX0.TBP0) >> 5) % std::max(src->m_from_target->m_TEX0.TBW, 1U) == 0)) && widthpage_offset && src->m_from_target != t)
{
if (iteration == 0)
{
@@ -2671,7 +2671,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
const bool all_dirty = dirty_rect.eq(t->m_valid);
if (!is_shuffle && !dirty_rect.rempty() && (!preserve_alpha && !preserve_rgb) && (GSState::s_n - 3) > t->m_last_draw)
if (!is_shuffle && !dirty_rect.rempty() && (!preserve_alpha && !preserve_rgb) && (GSRendererHW::GetInstance()->s_n - 3) > t->m_last_draw)
{
GL_INS("TC: Deleting RT BP 0x%x BW %d PSM %s due to dirty areas not preserved (Likely change in target)", t->m_TEX0.TBP0, t->m_TEX0.TBW, GSUtil::GetPSMName(t->m_TEX0.PSM));
InvalidateSourcesFromTarget(t);
@@ -2681,7 +2681,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
continue;
}
if (!all_dirty && ((translated_rect.w <= t->m_valid.w) || widthpage_offset == 0 || (GSState::s_n - 3) <= t->m_last_draw))
if (!all_dirty && ((translated_rect.w <= t->m_valid.w) || widthpage_offset == 0 || (GSRendererHW::GetInstance()->s_n - 3) <= t->m_last_draw))
{
if (TEX0.TBW == t->m_TEX0.TBW && !is_shuffle && widthpage_offset == 0 && ((min_rect.w + 63) / 64) > 1)
{
@@ -2699,7 +2699,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
}
}
//DevCon.Warning("Here draw %d wanted %x PSM %x got %x PSM %x offset of %d pages width %d pages draw width %d", GSState::s_n, bp, TEX0.PSM, t->m_TEX0.TBP0, t->m_TEX0.PSM, (bp - t->m_TEX0.TBP0) >> 5, t->m_TEX0.TBW, draw_rect.width());
//DevCon.Warning("Here draw %d wanted %x PSM %x got %x PSM %x offset of %d pages width %d pages draw width %d", GSRendererHW::GetInstance()->s_n, bp, TEX0.PSM, t->m_TEX0.TBP0, t->m_TEX0.PSM, (bp - t->m_TEX0.TBP0) >> 5, t->m_TEX0.TBW, draw_rect.width());
new_dst = t;
new_dst->m_32_bits_fmt |= (psm_s.bpp != 16);
@@ -2754,7 +2754,7 @@ GSTextureCache::Target* GSTextureCache::LookupDrawTarget(GIFRegTEX0 TEX0, const
t->m_texture = nullptr;
}
GL_CACHE("TC: Deleting Z draw %d", GSState::s_n);
GL_CACHE("TC: Deleting Z draw %d", GSRendererHW::GetInstance()->s_n);
InvalidateSourcesFromTarget(t);
i = rev_list.erase(i);
delete t;
@@ -3374,7 +3374,7 @@ GSTextureCache::Target* GSTextureCache::CreateTarget(GIFRegTEX0 TEX0, const GSVe
dst->readbacks_since_draw = 0;
dst->m_last_draw = GSState::s_n;
dst->m_last_draw = GSRendererHW::GetInstance()->s_n;
if (dst->m_dirty.empty() && GSLocalMemory::m_psm[TEX0.PSM].depth == 0 && (GSUtil::GetChannelMask(TEX0.PSM) & 0x8))
dst->m_rt_alpha_scale = true;
@@ -3520,7 +3520,7 @@ bool GSTextureCache::PreloadTarget(GIFRegTEX0 TEX0, const GSVector2i& size, cons
for (auto iter = transfers.rbegin(); iter != transfers.rend(); ++iter)
{
if (iter->draw < (GSState::s_n - 1))
if (iter->draw < (GSRendererHW::GetInstance()->s_n - 1))
break;
if (iter->transfer_type == GSRendererHW::GetInstance()->EEGS_TransferType::Clear && iter->blit.DBP == TEX0.TBP0 && iter->blit.DBW == TEX0.TBW)
@@ -3751,7 +3751,7 @@ bool GSTextureCache::PreloadTarget(GIFRegTEX0 TEX0, const GSVector2i& size, cons
if (texture_height > dst->m_unscaled_size.y && !dst->ResizeTexture(dst->m_unscaled_size.x, texture_height, true))
{
// Resize failed, probably ran out of VRAM, better luck next time. Fall back to CPU.
DevCon.Warning("Failed to resize target on preload? Draw %lld", GSState::s_n);
DevCon.Warning("Failed to resize target on preload? Draw %lld", GSRendererHW::GetInstance()->s_n);
i++;
continue;
}
@@ -4001,7 +4001,7 @@ GSTextureCache::Target* GSTextureCache::LookupDisplayTarget(GIFRegTEX0 TEX0, con
// Make sure the target is inside the texture
if (t->m_TEX0.TBP0 <= bp_adj && bp_adj <= t->UnwrappedEndBlock() && (half_buffer_match || t->Inside(bp_adj, TEX0.TBW, TEX0.PSM, GSVector4i::loadh(size))))
{
if (dst && (GSState::s_n - dst->m_last_draw) < (GSState::s_n - t->m_last_draw))
if (dst && (GSRendererHW::GetInstance()->s_n - dst->m_last_draw) < (GSRendererHW::GetInstance()->s_n - t->m_last_draw))
continue;
if (TEX0.TBW != t->m_TEX0.TBW && t->m_TEX0.TBW > 1 && t->m_age > 0)
@@ -5437,7 +5437,7 @@ bool GSTextureCache::Move(u32 SBP, u32 SBW, u32 SPSM, int sx, int sy, u32 DBP, u
// Make sure the copy doesn't go out of bounds (it shouldn't).
if ((scaled_dx + scaled_w) > dst->m_texture->GetWidth() || (scaled_dy + scaled_h) > dst->m_texture->GetHeight())
return false;
GL_CACHE("TC: HW Move after draw %lld 0x%x[BW:%u PSM:%s] to 0x%x[BW:%u PSM:%s] <%d,%d->%d,%d> -> <%d,%d->%d,%d>", GSState::s_n, SBP, SBW,
GL_CACHE("TC: HW Move after draw %lld 0x%x[BW:%u PSM:%s] to 0x%x[BW:%u PSM:%s] <%d,%d->%d,%d> -> <%d,%d->%d,%d>", GSRendererHW::GetInstance()->s_n, SBP, SBW,
GSUtil::GetPSMName(SPSM), DBP, DBW, GSUtil::GetPSMName(DPSM), sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h);
const bool cover_whole_target = dst->m_type == RenderTarget && GSVector4i(dx, dy, dx + w, dy + h).rintersect(dst->m_valid).eq(dst->m_valid);
@@ -5859,7 +5859,7 @@ GSVector2i GSTextureCache::GetTargetSize(u32 bp, u32 fbw, u32 psm, s32 min_width
}
}
DbgCon.WriteLn("TC: New size at %x %u %u: %ux%u draw %lld", bp, fbw, psm, min_width, min_height, GSState::s_n);
DbgCon.WriteLn("TC: New size at %x %u %u: %ux%u draw %lld", bp, fbw, psm, min_width, min_height, GSRendererHW::GetInstance()->s_n);
m_target_heights.push_front(search);
return GSVector2i(min_width, min_height);
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include "GS/Renderers/OpenGL/GLState.h"
#include "GS/Renderers/Common/GSGPUProfile.h"
#include "GS/GSState.h"
#include "GS/Renderers/Common/GSRenderer.h"
#include "GS/GSGL.h"
#include "GS/GSPerfMon.h"
#include "GS/GSUtil.h"
@@ -4004,7 +4005,7 @@ void GSDeviceOGL::DebugMessageCallback(GLenum gl_source, GLenum gl_type, GLuint
// Don't spam noisy information on the terminal
if (gl_severity != GL_DEBUG_SEVERITY_NOTIFICATION && gl_source != GL_DEBUG_SOURCE_APPLICATION)
{
Console.Error("T:%s\tID:%d\tS:%s\t=> %s", type.c_str(), GSState::s_n, severity.c_str(), message.c_str());
Console.Error("T:%s\tID:%d\tS:%s\t=> %s", type.c_str(), g_gs_renderer ? g_gs_renderer->s_n : 0, severity.c_str(), message.c_str());
}
}
+1 -1
View File
@@ -1669,7 +1669,7 @@ void GSRendererSW::SharedData::UpdateSource()
}
}
if (GSConfig.SaveTexture && GSConfig.ShouldDump(s_n, g_perfmon.GetFrame()))
if (GSConfig.SaveTexture && GSConfig.ShouldDump(g_gs_renderer->s_n, g_perfmon.GetFrame()))
{
const u64 frame = g_perfmon.GetFrame();
+9
View File
@@ -3319,6 +3319,15 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad
"EmuCore/GS", "HWDownloadMode", static_cast<int>(GSHardwareDownloadMode::Enabled), s_hw_download, std::size(s_hw_download),
true);
}
static constexpr const char* s_back_thread_modes[] = {
FSUI_NSTR("Disabled (Default)"),
FSUI_NSTR("Inline Records (Debug)"),
FSUI_NSTR("Lockstep (Debug)"),
FSUI_NSTR("Pipelined (Second GS Thread)"),
};
DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_MICROCHIP, "GS Back Thread"),
FSUI_CSTR("Pipelined splits GS emulation across two threads on multi-core systems. The debug modes are much slower — do not use them for play."),
"EmuCore/GS", "GSBackThreadMode", static_cast<int>(GSBackThreadMode::Off), s_back_thread_modes, std::size(s_back_thread_modes), true);
#if !defined(__APPLE__)
DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_EXPAND, "Allow Exclusive Fullscreen"),
FSUI_CSTR("Overrides the driver's heuristics for enabling exclusive fullscreen, or direct flip/scanout."), "EmuCore/GS",

Some files were not shown because too many files have changed in this diff Show More