mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
@@ -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."));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+195
-98
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user