2026-01-12 05:18:12 -05:00
|
|
|
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
2024-07-30 13:42:36 +02:00
|
|
|
// SPDX-License-Identifier: GPL-3.0+
|
2022-12-12 20:05:53 +10:00
|
|
|
|
2023-05-13 14:30:41 +10:00
|
|
|
#include "Config.h"
|
|
|
|
|
#include "Counters.h"
|
|
|
|
|
#include "ImGui/FullscreenUI.h"
|
|
|
|
|
#include "ImGui/ImGuiManager.h"
|
2023-07-09 01:40:05 +10:00
|
|
|
#include "GS/GS.h"
|
|
|
|
|
#include "GS/GSCapture.h"
|
|
|
|
|
#include "GS/GSExtra.h"
|
|
|
|
|
#include "GS/GSGL.h"
|
|
|
|
|
#include "GS/GSLzma.h"
|
|
|
|
|
#include "GS/GSPerfMon.h"
|
|
|
|
|
#include "GS/GSUtil.h"
|
|
|
|
|
#include "GS/MultiISA.h"
|
2023-05-13 14:30:41 +10:00
|
|
|
#include "Host.h"
|
|
|
|
|
#include "Input/InputManager.h"
|
2023-06-24 17:46:36 +10:00
|
|
|
#include "MTGS.h"
|
2023-05-13 14:30:41 +10:00
|
|
|
#include "pcsx2/GS.h"
|
2026-07-12 21:19:26 -07:00
|
|
|
#include "GS/Renderers/Null/GSDeviceNone.h"
|
2023-05-13 14:30:41 +10:00
|
|
|
#include "GS/Renderers/Null/GSRendererNull.h"
|
|
|
|
|
#include "GS/Renderers/HW/GSRendererHW.h"
|
2026-07-21 20:42:35 -04:00
|
|
|
#include "GS/Renderers/HW/GSHwHack.h"
|
2026-07-22 09:56:36 -07:00
|
|
|
#include "GS/Renderers/HW/GSDrawLog.h"
|
2023-05-13 14:30:41 +10:00
|
|
|
#include "GS/Renderers/HW/GSTextureReplacements.h"
|
2024-01-28 20:49:35 +10:00
|
|
|
#include "VMManager.h"
|
2023-05-13 14:30:41 +10:00
|
|
|
|
|
|
|
|
#ifdef ENABLE_OPENGL
|
|
|
|
|
#include "GS/Renderers/OpenGL/GSDeviceOGL.h"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifdef __APPLE__
|
|
|
|
|
#include "GS/Renderers/Metal/GSMetalCPPAccessible.h"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifdef ENABLE_VULKAN
|
|
|
|
|
#include "GS/Renderers/Vulkan/GSDeviceVK.h"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
|
|
|
|
#include "GS/Renderers/DX11/GSDevice11.h"
|
|
|
|
|
#include "GS/Renderers/DX12/GSDevice12.h"
|
|
|
|
|
#include "GS/Renderers/DX11/D3D.h"
|
|
|
|
|
|
|
|
|
|
#endif
|
2011-02-19 03:36:30 +00:00
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
#include "common/Console.h"
|
2022-05-20 00:46:33 +10:00
|
|
|
#include "common/FileSystem.h"
|
2026-07-18 17:17:12 +02:00
|
|
|
#include "common/HostSys.h"
|
2022-05-20 00:46:33 +10:00
|
|
|
#include "common/Path.h"
|
2023-12-11 01:28:55 +10:00
|
|
|
#include "common/SmallString.h"
|
2021-12-19 00:43:50 +10:00
|
|
|
#include "common/StringUtil.h"
|
2021-05-13 17:46:58 +02:00
|
|
|
|
2025-12-27 20:58:33 +07:00
|
|
|
#include "IconsFontAwesome.h"
|
2024-07-01 00:21:03 +10:00
|
|
|
|
2023-01-24 13:59:12 +10:00
|
|
|
#include "fmt/format.h"
|
|
|
|
|
|
2026-07-08 21:46:23 -04:00
|
|
|
#include <atomic>
|
2021-05-12 11:35:43 +02:00
|
|
|
#include <fstream>
|
2026-07-14 02:33:44 -04:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <string_view>
|
2021-05-12 11:35:43 +02:00
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
Pcsx2Config::GSOptions GSConfig;
|
2010-01-28 02:53:10 +00:00
|
|
|
|
2023-12-31 17:45:13 +10:00
|
|
|
static GSRendererType GSCurrentRenderer;
|
|
|
|
|
|
|
|
|
|
GSRendererType GSGetCurrentRenderer()
|
|
|
|
|
{
|
|
|
|
|
return GSCurrentRenderer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GSIsHardwareRenderer()
|
|
|
|
|
{
|
|
|
|
|
// Null gets flagged as hw.
|
|
|
|
|
return (GSCurrentRenderer != GSRendererType::SW);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-22 02:24:31 +02:00
|
|
|
std::string GetDefaultAdapter()
|
|
|
|
|
{
|
|
|
|
|
// Will be treated as empty.
|
|
|
|
|
return "(Default)";
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-10 22:04:56 +10:00
|
|
|
static RenderAPI GetAPIForRenderer(GSRendererType renderer)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
|
|
|
|
switch (renderer)
|
2011-02-17 03:24:37 +00:00
|
|
|
{
|
2026-07-12 21:19:26 -07:00
|
|
|
// Null renderer pairs with the deviceless None host device — headless runs
|
|
|
|
|
// (eerunner A/B, CI) must not require a working Vulkan/GL context.
|
|
|
|
|
case GSRendererType::Null:
|
|
|
|
|
return RenderAPI::None;
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
case GSRendererType::OGL:
|
2022-10-10 22:04:56 +10:00
|
|
|
return RenderAPI::OpenGL;
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2021-10-21 18:45:27 +10:00
|
|
|
case GSRendererType::VK:
|
2022-10-10 22:04:56 +10:00
|
|
|
return RenderAPI::Vulkan;
|
2021-10-21 18:45:27 +10:00
|
|
|
|
2022-04-03 14:49:49 +10:00
|
|
|
#ifdef _WIN32
|
|
|
|
|
case GSRendererType::DX11:
|
2022-10-10 22:04:56 +10:00
|
|
|
return RenderAPI::D3D11;
|
2022-03-19 22:19:16 +10:00
|
|
|
|
|
|
|
|
case GSRendererType::DX12:
|
2022-10-10 22:04:56 +10:00
|
|
|
return RenderAPI::D3D12;
|
2022-04-03 14:49:49 +10:00
|
|
|
#endif
|
|
|
|
|
|
2021-11-19 00:25:51 -06:00
|
|
|
#ifdef __APPLE__
|
|
|
|
|
case GSRendererType::Metal:
|
2022-10-10 22:04:56 +10:00
|
|
|
return RenderAPI::Metal;
|
2021-11-19 00:25:51 -06:00
|
|
|
#endif
|
2022-04-03 14:49:49 +10:00
|
|
|
|
2023-12-31 17:45:13 +10:00
|
|
|
// We could end up here if we ever removed a renderer.
|
2021-12-19 00:43:50 +10:00
|
|
|
default:
|
2023-04-01 15:47:02 +10:00
|
|
|
return GetAPIForRenderer(GSUtil::GetPreferredRenderer());
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool recreate_window,
|
|
|
|
|
GSVSyncMode vsync_mode, bool allow_present_throttle)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2023-04-08 13:20:41 +10:00
|
|
|
const RenderAPI new_api = GetAPIForRenderer(renderer);
|
|
|
|
|
switch (new_api)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2026-07-12 21:19:26 -07:00
|
|
|
case RenderAPI::None:
|
|
|
|
|
g_gs_device = std::make_unique<GSDeviceNone>();
|
|
|
|
|
break;
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
#ifdef _WIN32
|
2022-10-10 22:04:56 +10:00
|
|
|
case RenderAPI::D3D11:
|
2021-12-19 00:43:50 +10:00
|
|
|
g_gs_device = std::make_unique<GSDevice11>();
|
|
|
|
|
break;
|
2022-10-10 22:04:56 +10:00
|
|
|
case RenderAPI::D3D12:
|
2022-03-19 22:19:16 +10:00
|
|
|
g_gs_device = std::make_unique<GSDevice12>();
|
|
|
|
|
break;
|
2021-12-19 00:43:50 +10:00
|
|
|
#endif
|
2021-11-19 00:25:51 -06:00
|
|
|
#ifdef __APPLE__
|
2022-10-10 22:04:56 +10:00
|
|
|
case RenderAPI::Metal:
|
2021-11-19 00:25:51 -06:00
|
|
|
g_gs_device = std::unique_ptr<GSDevice>(MakeGSDeviceMTL());
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
2022-04-03 14:49:49 +10:00
|
|
|
#ifdef ENABLE_OPENGL
|
2022-10-10 22:04:56 +10:00
|
|
|
case RenderAPI::OpenGL:
|
2021-12-19 00:43:50 +10:00
|
|
|
g_gs_device = std::make_unique<GSDeviceOGL>();
|
|
|
|
|
break;
|
2022-04-03 14:49:49 +10:00
|
|
|
#endif
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2021-10-21 18:45:27 +10:00
|
|
|
#ifdef ENABLE_VULKAN
|
2022-10-10 22:04:56 +10:00
|
|
|
case RenderAPI::Vulkan:
|
2021-10-21 18:45:27 +10:00
|
|
|
g_gs_device = std::make_unique<GSDeviceVK>();
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
default:
|
2023-04-08 13:20:41 +10:00
|
|
|
Console.Error("Unsupported render API %s", GSDevice::RenderAPIToString(new_api));
|
2021-12-19 00:43:50 +10:00
|
|
|
return false;
|
2011-02-17 03:24:37 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
bool okay = g_gs_device->Create(vsync_mode, allow_present_throttle);
|
2023-04-01 15:54:04 +10:00
|
|
|
if (okay)
|
2009-09-17 07:40:38 +00:00
|
|
|
{
|
2023-04-01 15:54:04 +10:00
|
|
|
okay = ImGuiManager::Initialize();
|
|
|
|
|
if (!okay)
|
|
|
|
|
Console.Error("Failed to initialize ImGuiManager");
|
2009-09-17 07:40:38 +00:00
|
|
|
}
|
2023-02-11 16:21:36 +10:00
|
|
|
else
|
2009-09-18 18:45:37 +00:00
|
|
|
{
|
2023-04-01 15:54:04 +10:00
|
|
|
Console.Error("Failed to create GS device");
|
2009-09-18 18:45:37 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
if (!okay)
|
|
|
|
|
{
|
|
|
|
|
ImGuiManager::Shutdown(clear_state_on_fail);
|
2026-07-10 19:21:34 +02:00
|
|
|
g_gs_device->Destroy();
|
|
|
|
|
g_gs_device.reset();
|
2023-04-01 15:54:04 +10:00
|
|
|
Host::ReleaseRenderWindow();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:35:15 +07:00
|
|
|
if (!g_gs_device->SetGPUTimingEnabled(true))
|
|
|
|
|
GSConfig.OsdShowGPU = false;
|
2026-07-30 08:21:31 -04:00
|
|
|
if (GSConfig.OsdShowGPUStats && !g_gs_device->SetGPUPipelineStatisticsEnabled(true))
|
2026-04-30 23:48:39 -04:00
|
|
|
GSConfig.OsdShowGPUStats = false;
|
2023-04-01 15:54:04 +10:00
|
|
|
|
2023-04-08 13:20:41 +10:00
|
|
|
Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", GSDevice::RenderAPIToString(new_api));
|
2024-01-11 18:08:16 +10:00
|
|
|
Console.WriteLn(g_gs_device->GetDriverInfo());
|
2023-04-01 15:54:04 +10:00
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-25 22:52:41 +10:00
|
|
|
static void CloseGSDevice(bool clear_state)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2023-04-01 15:54:04 +10:00
|
|
|
if (!g_gs_device)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
ImGuiManager::Shutdown(clear_state);
|
|
|
|
|
g_gs_device->Destroy();
|
|
|
|
|
g_gs_device.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-01 00:21:03 +10:00
|
|
|
static void GSClampUpscaleMultiplier(Pcsx2Config::GSOptions& config)
|
|
|
|
|
{
|
|
|
|
|
const u32 max_upscale_multiplier = GSGetMaxUpscaleMultiplier(g_gs_device->GetMaxTextureSize());
|
|
|
|
|
if (config.UpscaleMultiplier <= static_cast<float>(max_upscale_multiplier))
|
|
|
|
|
{
|
|
|
|
|
// Shouldn't happen, but just in case.
|
2025-11-03 19:54:20 -06:00
|
|
|
if (config.UpscaleMultiplier < 0.0f)
|
|
|
|
|
config.UpscaleMultiplier = 0.0f;
|
2024-07-01 00:21:03 +10:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-04 13:46:37 +01:00
|
|
|
Host::AddIconOSDMessage("GSUpscaleMultiplierInvalid", ICON_FA_TRIANGLE_EXCLAMATION,
|
2024-07-01 00:21:03 +10:00
|
|
|
fmt::format(TRANSLATE_FS("GS", "Configured upscale multiplier {}x is above your GPU's supported multiplier of {}x."),
|
|
|
|
|
config.UpscaleMultiplier, max_upscale_multiplier),
|
|
|
|
|
Host::OSD_WARNING_DURATION);
|
|
|
|
|
config.UpscaleMultiplier = static_cast<float>(max_upscale_multiplier);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 02:33:44 -04:00
|
|
|
#ifdef __ANDROID__
|
|
|
|
|
// Some MediaTek Mali drivers render duplicated horizontal framebuffer regions in Tekken 5
|
|
|
|
|
// when the GameDB's Native half-pixel-offset mode (value 4) is active. Force the offset Off
|
|
|
|
|
// there — and ONLY there — preserving Native for every other GPU and game and respecting a
|
|
|
|
|
// user's manual hacks. Ported from sashkinbro/EmuCoreX. Reachable only while the Tekken 5
|
|
|
|
|
// GameDB entries keep halfPixelOffset: Native.
|
|
|
|
|
static bool IsTekken5Serial(const std::string_view serial)
|
|
|
|
|
{
|
|
|
|
|
static constexpr std::array<std::string_view, 11> k_tekken5_serials = {
|
|
|
|
|
"SCAJ-20125", "SCAJ-20126", "SCAJ-20199", "SCED-53538", "SCES-53202",
|
|
|
|
|
"SCKA-20049", "SCKA-20081", "SLPS-25510", "SLPS-73223", "SLUS-21059", "SLUS-21160"};
|
|
|
|
|
return std::find(k_tekken5_serials.begin(), k_tekken5_serials.end(), serial) != k_tekken5_serials.end();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ApplyAndroidGameDBOverrides()
|
|
|
|
|
{
|
|
|
|
|
if (!g_gs_device || !g_gs_device->IsMaliGPUProfile() || !g_gs_device->IsMediaTekSoC() ||
|
|
|
|
|
GSConfig.ManualUserHacks || GSConfig.UserHacks_HalfPixelOffset != GSHalfPixelOffset::Native)
|
|
|
|
|
return;
|
|
|
|
|
if (!IsTekken5Serial(VMManager::GetDiscSerial()))
|
|
|
|
|
return;
|
|
|
|
|
GSConfig.UserHacks_HalfPixelOffset = GSHalfPixelOffset::Off;
|
|
|
|
|
Console.WriteLn("Android: Tekken 5 on MediaTek Mali — forcing HalfPixelOffset Off (duplicated-framebuffer fix).");
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
// 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());
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
static bool OpenGSRenderer(GSRendererType renderer, u8* basemem)
|
|
|
|
|
{
|
2024-01-04 13:54:24 +10:00
|
|
|
// Must be done first, initialization routines in GSState use GSIsHardwareRenderer().
|
|
|
|
|
GSCurrentRenderer = renderer;
|
|
|
|
|
|
2024-03-28 14:41:31 +10:00
|
|
|
GSVertexSW::InitStatic();
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
if (renderer == GSRendererType::Null)
|
|
|
|
|
{
|
|
|
|
|
g_gs_renderer = std::make_unique<GSRendererNull>();
|
|
|
|
|
}
|
|
|
|
|
else if (renderer != GSRendererType::SW)
|
|
|
|
|
{
|
2024-07-01 00:21:03 +10:00
|
|
|
GSClampUpscaleMultiplier(GSConfig);
|
2023-04-01 15:54:04 +10:00
|
|
|
g_gs_renderer = std::make_unique<GSRendererHW>();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
g_gs_renderer = std::unique_ptr<GSRenderer>(MULTI_ISA_SELECT(makeGSRendererSW)(GSConfig.SWExtraThreads));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_gs_renderer->SetRegsMem(basemem);
|
|
|
|
|
g_gs_renderer->ResetPCRTC();
|
2023-07-30 01:55:06 +10:00
|
|
|
g_gs_renderer->UpdateRenderFixes();
|
2026-07-19 14:20:49 -07:00
|
|
|
|
|
|
|
|
// 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
|
2026-08-02 19:14:03 -07:00
|
|
|
// HW device). An EE-thread read of *live* local memory forces single-object
|
|
|
|
|
// (lockstep) — see below for why that is not every EE-thread read.
|
2026-07-19 14:20:49 -07:00
|
|
|
if (GSConfig.BackThreadMode == GSBackThreadMode::Pipelined && g_gs_renderer->IsBackThreadRunning())
|
|
|
|
|
{
|
2026-08-02 19:14:03 -07:00
|
|
|
// Which thread performs the readback is the wrong question here; what it reads is the
|
|
|
|
|
// right one. Unsynchronized takes GS local memory directly, with no lock and no drain,
|
|
|
|
|
// so a queued back thread leaves it arbitrarily far behind what the EE expects.
|
|
|
|
|
// Asynchronous instead takes the CPU shadow under m_async_readback_mutex, and that
|
|
|
|
|
// mutex is the synchronization point: the shadow only moves when the GS thread
|
|
|
|
|
// publishes a completed GPU download, never when a record is queued or executed. Queue
|
|
|
|
|
// depth therefore cannot change what the EE sees, and every shadow accessor already
|
|
|
|
|
// routes through m_mem_target, so a front object reaches the back's authoritative copy.
|
|
|
|
|
//
|
|
|
|
|
// The one exception is a shadow that never came up — ReadLocalMemoryUnsync then falls
|
|
|
|
|
// back to live local memory, which is exactly the Unsynchronized hazard, now against a
|
|
|
|
|
// concurrently drawing back thread. The renderer is already constructed at this point,
|
|
|
|
|
// so its shadow state is the thing to ask.
|
|
|
|
|
const bool ee_thread_reads_live_memory =
|
|
|
|
|
GSConfig.HWDownloadMode == GSHardwareDownloadMode::Unsynchronized ||
|
|
|
|
|
(GSConfig.HWDownloadMode == GSHardwareDownloadMode::Asynchronous &&
|
|
|
|
|
!g_gs_renderer->IsAsyncReadbackReady());
|
|
|
|
|
|
|
|
|
|
if (ee_thread_reads_live_memory && GSConfig.UseHardwareRenderer())
|
2026-07-19 14:33:20 -07:00
|
|
|
{
|
2026-08-02 19:14:03 -07:00
|
|
|
Console.Warning("GS: pipelined mode is unsupported with EE-thread reads of live GS memory — running lockstep.");
|
2026-07-19 14:33:20 -07:00
|
|
|
}
|
|
|
|
|
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).");
|
|
|
|
|
}
|
2026-07-19 14:20:49 -07:00
|
|
|
}
|
|
|
|
|
|
2023-04-08 12:28:46 +01:00
|
|
|
g_perfmon.Reset();
|
2023-04-01 15:54:04 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void CloseGSRenderer()
|
|
|
|
|
{
|
|
|
|
|
GSTextureReplacements::Shutdown();
|
|
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
// 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();
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
if (g_gs_renderer)
|
|
|
|
|
{
|
|
|
|
|
g_gs_renderer->Destroy();
|
|
|
|
|
g_gs_renderer.reset();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 14:46:20 -07:00
|
|
|
// GV7-2, same hazard GSUpdateConfig documents: the back thread executes draws
|
|
|
|
|
// against g_gs_device, so mutating that device from the MTGS thread — swapchain
|
|
|
|
|
// resize, window recreate, vsync change — races it. Drain first. The front only
|
|
|
|
|
// parses on this thread, so one drain up front quiesces the back thread for the
|
|
|
|
|
// whole call. No-op when the back thread is off (the default), and g_gs_renderer
|
|
|
|
|
// can legitimately be null while g_gs_device exists: the device is created first.
|
|
|
|
|
static void DrainBackQueueBeforeDeviceMutation()
|
|
|
|
|
{
|
|
|
|
|
if (g_gs_renderer)
|
|
|
|
|
g_gs_renderer->DrainBackQueue();
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-11 21:21:03 +10:00
|
|
|
bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_renderer,
|
|
|
|
|
std::optional<const Pcsx2Config::GSOptions*> old_config)
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2023-12-31 17:45:13 +10:00
|
|
|
Console.WriteLn("Reopening GS with %s device", recreate_device ? "new" : "existing");
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->Flush(GSState::GSFlushReason::GSREOPEN);
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2026-07-26 14:46:20 -07:00
|
|
|
// The Flush above only flushes FRONT parse state — it queues the resulting
|
|
|
|
|
// draw, it does not execute it. Everything below then hands the back thread's
|
|
|
|
|
// textures to the shredder: the device-loss arm purges the texture cache and
|
|
|
|
|
// the device pool outright, and the readback arm reads the texture cache. So
|
|
|
|
|
// drain between the two.
|
|
|
|
|
//
|
|
|
|
|
// This is safe on the device-loss path, which is the one that looks alarming.
|
|
|
|
|
// The back thread cannot be wedged in the driver here: BeginPresent only
|
|
|
|
|
// reports DeviceLost off m_last_submit_failed, i.e. the driver has ALREADY
|
|
|
|
|
// declared the loss, and post-loss calls return VK_ERROR_DEVICE_LOST rather
|
|
|
|
|
// than blocking. Nor is there a backlog to chew through — SubmitVsync drains
|
|
|
|
|
// before ExecVsyncRecord and present never queues, so the queue is empty on
|
|
|
|
|
// entry and the Flush above is the only producer.
|
|
|
|
|
DrainBackQueueBeforeDeviceMutation();
|
|
|
|
|
|
2024-05-11 21:21:03 +10:00
|
|
|
if (recreate_device && !recreate_renderer)
|
|
|
|
|
{
|
|
|
|
|
// Keeping the renderer around, this probably means we lost the device, so toss everything.
|
|
|
|
|
g_gs_renderer->PurgeTextureCache(true, true, true);
|
|
|
|
|
g_gs_device->ClearCurrent();
|
|
|
|
|
g_gs_device->PurgePool();
|
|
|
|
|
}
|
|
|
|
|
else if (GSConfig.UserHacks_ReadTCOnClose)
|
|
|
|
|
{
|
2023-02-23 21:00:48 +10:00
|
|
|
g_gs_renderer->ReadbackTextureCache();
|
2024-05-11 21:21:03 +10:00
|
|
|
}
|
2023-02-23 21:00:48 +10:00
|
|
|
|
2023-07-06 23:06:02 +10:00
|
|
|
std::string capture_filename;
|
|
|
|
|
GSVector2i capture_size;
|
2023-12-31 17:45:13 +10:00
|
|
|
if (GSCapture::IsCapturing())
|
2023-07-06 23:06:02 +10:00
|
|
|
{
|
|
|
|
|
capture_filename = GSCapture::GetNextCaptureFileName();
|
|
|
|
|
capture_size = GSCapture::GetSize();
|
|
|
|
|
Console.Warning(fmt::format("Restarting video capture to {}.", capture_filename));
|
|
|
|
|
g_gs_renderer->EndCapture();
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
u8* basemem = g_gs_renderer->GetRegsMem();
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
freezeData fd = {};
|
2024-05-11 21:21:03 +10:00
|
|
|
std::unique_ptr<u8[]> fd_data;
|
|
|
|
|
if (recreate_renderer)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
if (GSParseTarget()->Freeze(&fd, true) != 0)
|
2024-05-11 21:21:03 +10:00
|
|
|
{
|
|
|
|
|
Console.Error("(GSreopen) Failed to get GS freeze size");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-12-31 17:45:13 +10:00
|
|
|
|
2024-05-11 21:21:03 +10:00
|
|
|
fd_data = std::make_unique<u8[]>(fd.size);
|
|
|
|
|
fd.data = fd_data.get();
|
2026-07-19 14:20:49 -07:00
|
|
|
if (GSParseTarget()->Freeze(&fd, false) != 0)
|
2024-05-11 21:21:03 +10:00
|
|
|
{
|
|
|
|
|
Console.Error("(GSreopen) Failed to freeze GS");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2024-05-11 21:21:03 +10:00
|
|
|
CloseGSRenderer();
|
|
|
|
|
}
|
2023-12-31 17:45:13 +10:00
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
if (recreate_device)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2023-04-08 13:20:41 +10:00
|
|
|
// We need a new render window when changing APIs.
|
|
|
|
|
const bool recreate_window = (g_gs_device->GetRenderAPI() != GetAPIForRenderer(GSConfig.Renderer));
|
2024-05-23 23:35:34 +10:00
|
|
|
const GSVSyncMode vsync_mode = g_gs_device->GetVSyncMode();
|
|
|
|
|
const bool allow_present_throttle = g_gs_device->IsPresentThrottleAllowed();
|
2023-04-25 22:52:41 +10:00
|
|
|
CloseGSDevice(false);
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
if (!OpenGSDevice(new_renderer, false, recreate_window, vsync_mode, allow_present_throttle))
|
2020-09-20 03:16:55 -04:00
|
|
|
{
|
2023-06-19 20:27:34 +10:00
|
|
|
Host::AddKeyedOSDMessage("GSReopenFailed",
|
|
|
|
|
TRANSLATE_STR("GS", "Failed to reopen, restoring old configuration."),
|
|
|
|
|
Host::OSD_CRITICAL_ERROR_DURATION);
|
2022-05-30 20:15:16 +10:00
|
|
|
|
2023-04-25 22:52:41 +10:00
|
|
|
CloseGSDevice(false);
|
2022-05-30 20:15:16 +10:00
|
|
|
|
2023-12-31 17:45:13 +10:00
|
|
|
if (old_config.has_value())
|
|
|
|
|
GSConfig = *old_config.value();
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window, vsync_mode, allow_present_throttle))
|
2022-05-30 20:15:16 +10:00
|
|
|
{
|
2023-04-01 15:54:04 +10:00
|
|
|
pxFailRel("Failed to reopen GS on old config");
|
2023-04-08 13:20:41 +10:00
|
|
|
Host::ReleaseRenderWindow();
|
2022-05-30 20:15:16 +10:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
2023-12-31 17:45:13 +10:00
|
|
|
|
2024-05-11 21:21:03 +10:00
|
|
|
if (recreate_renderer)
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2026-07-14 02:33:44 -04:00
|
|
|
#ifdef __ANDROID__
|
|
|
|
|
ApplyAndroidGameDBOverrides();
|
|
|
|
|
#endif
|
2024-05-11 21:21:03 +10:00
|
|
|
if (!OpenGSRenderer(new_renderer, basemem))
|
|
|
|
|
{
|
|
|
|
|
Console.Error("(GSreopen) Failed to create new renderer");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2011-02-17 03:24:37 +00:00
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
if (GSParseTarget()->Defrost(&fd) != 0)
|
2024-05-11 21:21:03 +10:00
|
|
|
{
|
|
|
|
|
Console.Error("(GSreopen) Failed to defrost");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
2014-01-25 19:53:12 +00:00
|
|
|
|
2023-07-06 23:06:02 +10:00
|
|
|
if (!capture_filename.empty())
|
|
|
|
|
g_gs_renderer->BeginCapture(std::move(capture_filename), capture_size);
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem,
|
|
|
|
|
GSVSyncMode vsync_mode, bool allow_present_throttle)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2023-12-31 17:45:13 +10:00
|
|
|
GSConfig = config;
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
if (renderer == GSRendererType::Auto)
|
|
|
|
|
renderer = GSUtil::GetPreferredRenderer();
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
bool res = OpenGSDevice(renderer, true, false, vsync_mode, allow_present_throttle);
|
2023-04-01 15:54:04 +10:00
|
|
|
if (res)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2026-07-14 02:33:44 -04:00
|
|
|
#ifdef __ANDROID__
|
|
|
|
|
ApplyAndroidGameDBOverrides();
|
|
|
|
|
#endif
|
2023-04-01 15:54:04 +10:00
|
|
|
res = OpenGSRenderer(renderer, basemem);
|
|
|
|
|
if (!res)
|
2023-04-25 22:52:41 +10:00
|
|
|
CloseGSDevice(true);
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
if (!res)
|
2022-01-17 12:21:21 +10:00
|
|
|
{
|
2025-06-12 23:16:13 -05:00
|
|
|
Host::ReportErrorAsync("Error",
|
|
|
|
|
fmt::format(TRANSLATE_FS("GS", "Failed to create render device. This may be due to your GPU not supporting the "
|
|
|
|
|
"chosen renderer ({}), or because your graphics drivers need to be updated."),
|
|
|
|
|
Pcsx2Config::GSOptions::GetRendererName(GSConfig.Renderer)));
|
2022-01-17 12:21:21 +10:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
2009-09-17 07:40:38 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
void GSclose()
|
|
|
|
|
{
|
2024-01-28 20:49:35 +10:00
|
|
|
if (GSCapture::IsCapturing())
|
|
|
|
|
GSCapture::EndCapture();
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
CloseGSRenderer();
|
2023-04-25 22:52:41 +10:00
|
|
|
CloseGSDevice(true);
|
2023-04-27 16:06:25 +10:00
|
|
|
Host::ReleaseRenderWindow();
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
|
2022-05-27 21:20:32 +10:00
|
|
|
void GSreset(bool hardware_reset)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:33:20 -07:00
|
|
|
// 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.
|
2026-07-19 14:20:49 -07:00
|
|
|
if (g_gs_front)
|
|
|
|
|
g_gs_front->Reset(hardware_reset);
|
2023-06-28 23:05:53 +10:00
|
|
|
g_gs_renderer->Reset(hardware_reset);
|
2023-07-19 17:26:42 +10:00
|
|
|
|
|
|
|
|
// Restart video capture if it's been started.
|
|
|
|
|
// Otherwise we get a buildup of audio frames from the CPU thread.
|
|
|
|
|
if (hardware_reset && GSCapture::IsCapturing())
|
|
|
|
|
{
|
|
|
|
|
std::string next_filename = GSCapture::GetNextCaptureFileName();
|
|
|
|
|
const GSVector2i size = GSCapture::GetSize();
|
|
|
|
|
Console.Warning(fmt::format("Restarting video capture to {}.", next_filename));
|
|
|
|
|
g_gs_renderer->EndCapture();
|
|
|
|
|
g_gs_renderer->BeginCapture(std::move(next_filename), size);
|
|
|
|
|
}
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSgifSoftReset(u32 mask)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->SoftReset(mask);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSwriteCSR(u32 csr)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->WriteCSR(csr);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2022-04-24 20:09:55 +10:00
|
|
|
void GSInitAndReadFIFO(u8* mem, u32 size)
|
2014-05-02 23:03:02 +02:00
|
|
|
{
|
2022-04-24 20:09:55 +10:00
|
|
|
GL_PERF("Init and read FIFO %u qwc", size);
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->InitReadFIFO(mem, size);
|
|
|
|
|
GSParseTarget()->ReadFIFO(mem, size);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-27 18:53:14 +10:00
|
|
|
void GSReadLocalMemoryUnsync(u8* mem, u32 qwc, u64 BITBLITBUF, u64 TRXPOS, u64 TRXREG)
|
|
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->ReadLocalMemoryUnsync(mem, qwc, GIFRegBITBLTBUF{BITBLITBUF}, GIFRegTRXPOS{TRXPOS}, GIFRegTRXREG{TRXREG});
|
2022-05-27 18:53:14 +10:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSgifTransfer(const u8* mem, u32 size)
|
2010-07-11 04:38:33 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->Transfer<3>(mem, size);
|
2010-07-11 04:38:33 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSgifTransfer1(u8* mem, u32 addr)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->Transfer<0>(const_cast<u8*>(mem) + addr, (0x4000 - addr) / 16);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSgifTransfer2(u8* mem, u32 size)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->Transfer<1>(const_cast<u8*>(mem), size);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 21:45:52 -05:00
|
|
|
void GSgifTransfer3(u8* mem, u32 size)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
GSParseTarget()->Transfer<2>(const_cast<u8*>(mem), size);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 21:46:23 -04:00
|
|
|
// Manual frameskip target (Android). Set from the UI thread via the JNI
|
|
|
|
|
// setFrameSkip, read on the GS thread in GSRenderer::VSync. Relaxed atomic — a
|
|
|
|
|
// stale read at most mis-skips a single frame, which is harmless.
|
|
|
|
|
static std::atomic<u32> s_manual_frameskip{0};
|
|
|
|
|
void GSSetManualFrameSkip(u32 frames)
|
|
|
|
|
{
|
|
|
|
|
s_manual_frameskip.store(frames, std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
u32 GSGetManualFrameSkip()
|
|
|
|
|
{
|
|
|
|
|
return s_manual_frameskip.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 11:54:03 -06:00
|
|
|
// Caps display presentation without slowing emulation. The interval controls the
|
|
|
|
|
// exact cadence while milli-FPS preserves fractional targets for the OSD.
|
2026-07-08 21:46:23 -04:00
|
|
|
static std::atomic<u32> s_max_present_fps{0};
|
2026-08-04 11:54:03 -06:00
|
|
|
static std::atomic<u32> s_max_present_milli_fps{0};
|
2026-07-08 21:46:23 -04:00
|
|
|
static std::atomic<u64> s_max_present_interval{0};
|
2026-08-04 11:54:03 -06:00
|
|
|
static std::atomic<bool> s_present_cap_render_skip{false};
|
2026-07-08 21:46:23 -04:00
|
|
|
// Fast-forward (Turbo) bypasses the present cap so the speed-up is visible. Set
|
|
|
|
|
// from the limiter-mode JNI (Turbo → true, anything else → false) and read on
|
|
|
|
|
// the GS thread in GSRenderer::VSync. Unlimited (frame-limit-off steady state)
|
|
|
|
|
// deliberately does NOT set this — there the present cap is still wanted.
|
|
|
|
|
static std::atomic<bool> s_present_cap_suspended{false};
|
2026-08-04 11:54:03 -06:00
|
|
|
void GSSetMaxPresentFps(u32 fps, u64 present_interval, u32 milli_fps)
|
2026-07-08 21:46:23 -04:00
|
|
|
{
|
|
|
|
|
s_max_present_fps.store(fps, std::memory_order_relaxed);
|
2026-08-04 11:54:03 -06:00
|
|
|
s_max_present_milli_fps.store(present_interval == 0 ? 0 : (milli_fps != 0 ? milli_fps : fps * 1000),
|
|
|
|
|
std::memory_order_relaxed);
|
2026-07-08 21:46:23 -04:00
|
|
|
s_max_present_interval.store(present_interval, std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
u32 GSGetMaxPresentFps()
|
|
|
|
|
{
|
|
|
|
|
return s_max_present_fps.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
2026-08-04 11:54:03 -06:00
|
|
|
u32 GSGetMaxPresentMilliFps()
|
|
|
|
|
{
|
|
|
|
|
return s_max_present_milli_fps.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
2026-07-08 21:46:23 -04:00
|
|
|
u64 GSGetMaxPresentInterval()
|
|
|
|
|
{
|
|
|
|
|
return s_max_present_interval.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
2026-08-04 11:54:03 -06:00
|
|
|
void GSSetPresentCapRenderSkip(bool enabled)
|
|
|
|
|
{
|
|
|
|
|
s_present_cap_render_skip.store(enabled, std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
bool GSGetPresentCapRenderSkip()
|
|
|
|
|
{
|
|
|
|
|
return s_present_cap_render_skip.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
2026-07-08 21:46:23 -04:00
|
|
|
void GSSetPresentCapSuspended(bool suspended)
|
|
|
|
|
{
|
|
|
|
|
s_present_cap_suspended.store(suspended, std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
bool GSGetPresentCapSuspended()
|
|
|
|
|
{
|
|
|
|
|
return s_present_cap_suspended.load(std::memory_order_relaxed);
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-09 19:21:59 +10:00
|
|
|
void GSvsync(u32 field, bool registers_written)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2025-03-04 19:10:41 +00:00
|
|
|
// Update this here because we need to check if the pending draw affects the current frame, so our regs need to be updated.
|
2026-07-19 14:20:49 -07:00
|
|
|
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);
|
2025-03-04 19:10:41 +00:00
|
|
|
|
2026-07-19 11:53:04 -07:00
|
|
|
// 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.
|
2026-07-19 14:20:49 -07:00
|
|
|
front->SubmitPcrtcSync();
|
2025-03-04 19:10:41 +00:00
|
|
|
|
2023-07-04 18:28:20 +10:00
|
|
|
// 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).
|
2026-07-19 14:20:49 -07:00
|
|
|
front->Flush(GSState::VSYNC);
|
2026-07-19 11:53:04 -07:00
|
|
|
g_gs_renderer->SubmitVsync(field, registers_written);
|
2026-07-19 14:20:49 -07:00
|
|
|
if (g_gs_front)
|
|
|
|
|
g_gs_front->MirrorPostVsyncState();
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-07-06 08:08:18 +02:00
|
|
|
int GSfreeze(FreezeAction mode, freezeData* data)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2023-06-28 23:05:53 +10:00
|
|
|
if (mode == FreezeAction::Save)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
return GSParseTarget()->Freeze(data, false);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
2023-06-28 23:05:53 +10:00
|
|
|
else if (mode == FreezeAction::Size)
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
return GSParseTarget()->Freeze(data, true);
|
2023-06-28 23:05:53 +10:00
|
|
|
}
|
|
|
|
|
else // if (mode == FreezeAction::Load)
|
|
|
|
|
{
|
|
|
|
|
// Since Defrost doesn't do a hardware reset (since it would be clearing
|
|
|
|
|
// local memory just before it's overwritten), we have to manually wipe
|
|
|
|
|
// out the current textures.
|
|
|
|
|
g_gs_device->ClearCurrent();
|
2023-07-06 23:06:02 +10:00
|
|
|
|
|
|
|
|
// Dump audio frames in video capture if it's been started, otherwise we get
|
|
|
|
|
// a buildup of audio frames from the CPU thread.
|
|
|
|
|
if (GSCapture::IsCapturing())
|
|
|
|
|
GSCapture::Flush();
|
|
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
return GSParseTarget()->Defrost(data);
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 20:10:51 -07:00
|
|
|
bool GSQueueSnapshot(const std::string& path, u32 gsdump_frames)
|
2022-05-16 21:10:34 +10:00
|
|
|
{
|
2026-07-30 20:10:51 -07:00
|
|
|
return g_gs_renderer && g_gs_renderer->QueueSnapshot(path, gsdump_frames);
|
2022-05-16 21:10:34 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GSStopGSDump()
|
|
|
|
|
{
|
|
|
|
|
if (g_gs_renderer)
|
|
|
|
|
g_gs_renderer->StopGSDump();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 20:10:51 -07:00
|
|
|
bool GSIsDumpRecording()
|
|
|
|
|
{
|
|
|
|
|
return g_gs_renderer && g_gs_renderer->IsDumpRecording();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GSHasFrontParser()
|
|
|
|
|
{
|
|
|
|
|
return static_cast<bool>(g_gs_front);
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-18 23:05:00 +10:00
|
|
|
bool GSBeginCapture(std::string filename)
|
|
|
|
|
{
|
|
|
|
|
if (g_gs_renderer)
|
|
|
|
|
return g_gs_renderer->BeginCapture(std::move(filename));
|
|
|
|
|
else
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GSEndCapture()
|
|
|
|
|
{
|
|
|
|
|
if (g_gs_renderer)
|
|
|
|
|
g_gs_renderer->EndCapture();
|
2025-06-12 23:16:13 -05:00
|
|
|
}
|
2022-12-18 23:05:00 +10:00
|
|
|
|
2022-05-20 23:09:24 +10:00
|
|
|
void GSPresentCurrentFrame()
|
|
|
|
|
{
|
2026-07-27 18:43:51 -04:00
|
|
|
// Presenting records into the device's command buffer and begins a render pass, so it is a
|
|
|
|
|
// device mutation exactly like the four sites above -- this was the one that did not drain.
|
|
|
|
|
//
|
|
|
|
|
// It matters most while PAUSED. MTGS's idle loop re-presents the frame on a spin whenever the
|
|
|
|
|
// VM is not Running (MTGS.cpp, the s_run_idle_flag branch), so the moment the user pauses, this
|
|
|
|
|
// runs concurrently with a back thread that may still be executing queued draws. Both then call
|
|
|
|
|
// vkCmdBeginRenderPass on the same VkCommandBuffer, which Vulkan requires the caller to
|
|
|
|
|
// externally synchronize; Adreno's driver faults inside vkCmdBeginRenderPass rather than
|
|
|
|
|
// erroring, and both threads abort. Reproduced on an Adreno 740 by pausing with the GS back
|
|
|
|
|
// thread enabled.
|
|
|
|
|
DrainBackQueueBeforeDeviceMutation();
|
|
|
|
|
|
2022-05-20 23:09:24 +10:00
|
|
|
g_gs_renderer->PresentCurrentFrame();
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 15:09:22 +10:00
|
|
|
void GSThrottlePresentation()
|
|
|
|
|
{
|
2024-05-23 23:35:34 +10:00
|
|
|
if (g_gs_device->GetVSyncMode() == GSVSyncMode::FIFO)
|
2022-12-04 15:09:22 +10:00
|
|
|
{
|
|
|
|
|
// Let vsync take care of throttling.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
g_gs_device->ThrottlePresentation();
|
2022-12-04 15:09:22 +10:00
|
|
|
}
|
|
|
|
|
|
2023-07-30 01:55:06 +10:00
|
|
|
void GSGameChanged()
|
2009-02-09 21:15:56 +00:00
|
|
|
{
|
2023-12-31 17:45:13 +10:00
|
|
|
if (GSIsHardwareRenderer())
|
2026-07-21 20:42:35 -04:00
|
|
|
{
|
|
|
|
|
GSHwHack::ResetState();
|
2023-07-30 01:55:06 +10:00
|
|
|
GSTextureReplacements::GameChanged();
|
2026-07-21 20:42:35 -04:00
|
|
|
}
|
2024-01-28 20:49:35 +10:00
|
|
|
|
|
|
|
|
if (!VMManager::HasValidVM() && GSCapture::IsCapturing())
|
|
|
|
|
GSCapture::EndCapture();
|
2009-02-09 21:15:56 +00:00
|
|
|
}
|
|
|
|
|
|
2023-11-04 21:23:43 +10:00
|
|
|
bool GSHasDisplayWindow()
|
|
|
|
|
{
|
|
|
|
|
pxAssert(g_gs_device);
|
|
|
|
|
return (g_gs_device->GetWindowInfo().type != WindowInfo::Type::Surfaceless);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 22:02:34 +01:00
|
|
|
void GSResizeDisplayWindow(u32 width, u32 height, float scale)
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2026-07-26 13:20:09 -07:00
|
|
|
DrainBackQueueBeforeDeviceMutation();
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
g_gs_device->ResizeWindow(width, height, scale);
|
|
|
|
|
ImGuiManager::WindowResized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GSUpdateDisplayWindow()
|
|
|
|
|
{
|
2026-07-26 13:20:09 -07:00
|
|
|
DrainBackQueueBeforeDeviceMutation();
|
|
|
|
|
|
2023-04-25 22:52:41 +10:00
|
|
|
if (!g_gs_device->UpdateWindow())
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2024-09-15 19:24:05 +07:00
|
|
|
Host::ReportErrorAsync("Error", TRANSLATE_SV("GS", "Failed to change window after update. The log may contain more information."));
|
2023-04-01 15:54:04 +10:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGuiManager::WindowResized();
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
void GSSetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2024-05-23 23:35:34 +10:00
|
|
|
static constexpr std::array<const char*, static_cast<size_t>(GSVSyncMode::Count)> modes = {{
|
|
|
|
|
"Disabled",
|
|
|
|
|
"FIFO",
|
|
|
|
|
"Mailbox",
|
|
|
|
|
}};
|
|
|
|
|
Console.WriteLnFmt(Color_StrongCyan, "Setting vsync mode: {}{}", modes[static_cast<size_t>(mode)],
|
|
|
|
|
allow_present_throttle ? " (throttle allowed)" : "");
|
2026-07-26 13:20:09 -07:00
|
|
|
|
|
|
|
|
DrainBackQueueBeforeDeviceMutation();
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
g_gs_device->SetVSyncMode(mode, allow_present_throttle);
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
|
2023-04-25 22:52:41 +10:00
|
|
|
bool GSWantsExclusiveFullscreen()
|
|
|
|
|
{
|
|
|
|
|
if (!g_gs_device || !g_gs_device->SupportsExclusiveFullscreen())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
u32 width, height;
|
|
|
|
|
float refresh_rate;
|
|
|
|
|
return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
std::optional<float> GSGetHostRefreshRate()
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
|
|
|
|
if (!g_gs_device)
|
2024-05-23 23:35:34 +10:00
|
|
|
return std::nullopt;
|
2023-04-01 15:54:04 +10:00
|
|
|
|
2024-05-23 23:35:34 +10:00
|
|
|
const float surface_refresh_rate = g_gs_device->GetWindowInfo().surface_refresh_rate;
|
|
|
|
|
if (surface_refresh_rate == 0.0f)
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
else
|
|
|
|
|
return surface_refresh_rate;
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
|
2024-07-01 00:21:03 +10:00
|
|
|
std::vector<GSAdapterInfo> GSGetAdapterInfo(GSRendererType renderer)
|
2023-04-01 15:54:04 +10:00
|
|
|
{
|
2024-07-01 00:21:03 +10:00
|
|
|
std::vector<GSAdapterInfo> ret;
|
2023-04-01 15:54:04 +10:00
|
|
|
switch (renderer)
|
|
|
|
|
{
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
case GSRendererType::DX11:
|
|
|
|
|
case GSRendererType::DX12:
|
|
|
|
|
{
|
|
|
|
|
auto factory = D3D::CreateFactory(false);
|
|
|
|
|
if (factory)
|
2024-07-01 00:21:03 +10:00
|
|
|
ret = D3D::GetAdapterInfo(factory.get());
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-02-27 14:32:54 -05:00
|
|
|
#ifdef ENABLE_OPENGL
|
|
|
|
|
case GSRendererType::OGL:
|
|
|
|
|
{
|
|
|
|
|
ret = GSDeviceOGL::GetAdapterInfo();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
|
|
|
|
|
2023-04-01 15:54:04 +10:00
|
|
|
#ifdef ENABLE_VULKAN
|
|
|
|
|
case GSRendererType::VK:
|
|
|
|
|
{
|
2024-07-01 00:21:03 +10:00
|
|
|
ret = GSDeviceVK::GetAdapterInfo();
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifdef __APPLE__
|
|
|
|
|
case GSRendererType::Metal:
|
|
|
|
|
{
|
2024-07-01 00:21:03 +10:00
|
|
|
ret = GetMetalAdapterList();
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-07-01 00:21:03 +10:00
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u32 GSGetMaxUpscaleMultiplier(u32 max_texture_size)
|
|
|
|
|
{
|
|
|
|
|
// Maximum GS target size is 1280x1280. Assume we want to upscale the max size target.
|
|
|
|
|
return std::max(max_texture_size / 1280, 1u);
|
2023-04-01 15:54:04 +10:00
|
|
|
}
|
|
|
|
|
|
2022-04-11 14:00:18 +01:00
|
|
|
GSVideoMode GSgetDisplayMode()
|
|
|
|
|
{
|
2022-04-23 14:08:26 +10:00
|
|
|
GSRenderer* gs = g_gs_renderer.get();
|
2022-04-11 14:00:18 +01:00
|
|
|
|
|
|
|
|
return gs->GetVideoMode();
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
void GSgetInternalResolution(int* width, int* height)
|
2009-12-03 15:49:12 +00:00
|
|
|
{
|
2022-04-23 14:08:26 +10:00
|
|
|
GSRenderer* gs = g_gs_renderer.get();
|
2021-12-19 00:43:50 +10:00
|
|
|
if (!gs)
|
2011-02-17 03:24:37 +00:00
|
|
|
{
|
2021-12-19 00:43:50 +10:00
|
|
|
*width = 0;
|
|
|
|
|
*height = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GSVector2i res(gs->GetInternalResolution());
|
|
|
|
|
*width = res.x;
|
|
|
|
|
*height = res.y;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-11 01:28:55 +10:00
|
|
|
void GSgetStats(SmallStringBase& info)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2021-10-24 18:27:19 +10:00
|
|
|
GSPerfMon& pm = g_perfmon;
|
2023-04-08 13:20:41 +10:00
|
|
|
const char* api_name = GSDevice::RenderAPIToString(g_gs_device->GetRenderAPI());
|
2023-12-31 17:45:13 +10:00
|
|
|
if (GSCurrentRenderer == GSRendererType::SW)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2021-10-24 18:27:19 +10:00
|
|
|
const double fps = GetVerticalFrequency();
|
2021-12-19 00:43:50 +10:00
|
|
|
const double fillrate = pm.Get(GSPerfMon::Fillrate);
|
2024-10-09 21:25:30 +09:00
|
|
|
double pps = fps * fillrate;
|
|
|
|
|
char prefix = '\0';
|
2025-06-12 23:16:13 -05:00
|
|
|
|
2024-10-09 21:25:30 +09:00
|
|
|
if (pps >= 170000000)
|
|
|
|
|
{
|
2025-11-10 13:38:49 -06:00
|
|
|
pps /= _1gb; // Gpps
|
2024-10-09 21:25:30 +09:00
|
|
|
prefix = 'G';
|
|
|
|
|
}
|
|
|
|
|
else if (pps >= 35000000)
|
|
|
|
|
{
|
2025-11-10 13:38:49 -06:00
|
|
|
pps /= _1mb; // Mpps
|
2024-10-09 21:25:30 +09:00
|
|
|
prefix = 'M';
|
|
|
|
|
}
|
2025-11-10 13:38:49 -06:00
|
|
|
else if (pps >= _1kb)
|
2024-10-09 21:25:30 +09:00
|
|
|
{
|
2025-11-12 19:56:15 +00:00
|
|
|
pps /= _1kb; // kpps
|
|
|
|
|
prefix = 'k';
|
2024-10-09 21:25:30 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 19:56:15 +00:00
|
|
|
info.format("{} SW | {} SYNP | {} PRIM | {} DRW | {:.2f} SWIZ | {:.2f} UNSWIZ | {:.2f} {}pps",
|
2021-10-24 18:27:19 +10:00
|
|
|
api_name,
|
2021-12-19 00:43:50 +10:00
|
|
|
(int)pm.Get(GSPerfMon::SyncPoint),
|
|
|
|
|
(int)pm.Get(GSPerfMon::Prim),
|
|
|
|
|
(int)pm.Get(GSPerfMon::Draw),
|
2025-11-10 13:38:49 -06:00
|
|
|
pm.Get(GSPerfMon::Swizzle) / _1kb,
|
|
|
|
|
pm.Get(GSPerfMon::Unswizzle) / _1kb,
|
2025-06-12 23:16:13 -05:00
|
|
|
pps, prefix);
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
2023-12-31 17:45:13 +10:00
|
|
|
else if (GSCurrentRenderer == GSRendererType::Null)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2025-11-10 13:38:49 -06:00
|
|
|
info.format("{} Null", api_name);
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-13 20:13:19 -04:00
|
|
|
if (!GSConfig.HWROV)
|
|
|
|
|
{
|
|
|
|
|
info.format("{} HW | {} PRIM | {} DRW | {} DRWC | {} BAR | {} RP | {} RB | {} TC | {} TU",
|
|
|
|
|
api_name,
|
|
|
|
|
(int)pm.Get(GSPerfMon::Prim),
|
|
|
|
|
(int)pm.Get(GSPerfMon::Draw),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::DrawCalls)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::Barriers)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::RenderPasses)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::Readbacks)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::TextureCopies)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::TextureUploads)));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Add ROV stats along standard stats.
|
|
|
|
|
info.format("{} HW | {} PRIM | {} DRW | {}/{} DRWC | {}/{} BAR | {} RP | {} RB | {}/{} TC | {} TU",
|
|
|
|
|
api_name,
|
|
|
|
|
(int)pm.Get(GSPerfMon::Prim),
|
|
|
|
|
(int)pm.Get(GSPerfMon::Draw),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::DrawCalls)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::DrawCallsROV)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::Barriers)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::BarriersROV)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::RenderPasses)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::Readbacks)),
|
|
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::TextureCopies)),
|
2026-06-20 21:13:15 -04:00
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::TextureCopiesROV)),
|
2026-05-13 20:13:19 -04:00
|
|
|
(int)std::ceil(pm.Get(GSPerfMon::TextureUploads)));
|
|
|
|
|
}
|
2023-01-24 13:59:12 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-11 01:28:55 +10:00
|
|
|
void GSgetMemoryStats(SmallStringBase& info)
|
2023-01-24 13:59:12 +10:00
|
|
|
{
|
2023-03-17 23:20:06 +10:00
|
|
|
if (!g_texture_cache)
|
2025-11-10 13:38:49 -06:00
|
|
|
{
|
|
|
|
|
info.assign("");
|
2023-01-24 13:59:12 +10:00
|
|
|
return;
|
2025-11-10 13:38:49 -06:00
|
|
|
}
|
2023-01-24 13:59:12 +10:00
|
|
|
|
2025-11-10 13:38:49 -06:00
|
|
|
// Get megabyte values. Round negligible values to 0.1 MB to avoid swamping.
|
|
|
|
|
const auto get_MB = [](const double bytes) {
|
|
|
|
|
return (bytes <= 0.0 ? bytes : std::max(0.1, bytes / static_cast<double>(_1mb)));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const auto format_precision = [](const double megabytes) -> std::string {
|
|
|
|
|
return (megabytes < 10.0 ?
|
|
|
|
|
fmt::format("{:.1f}", megabytes) :
|
|
|
|
|
fmt::format("{:.0f}", std::round(megabytes)));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const double targets_MB = get_MB(static_cast<double>(g_texture_cache->GetTargetMemoryUsage()));
|
|
|
|
|
const double sources_MB = get_MB(static_cast<double>(g_texture_cache->GetSourceMemoryUsage()));
|
|
|
|
|
const double pool_MB = get_MB(static_cast<double>(g_gs_device->GetPoolMemoryUsage()));
|
2023-01-24 13:59:12 +10:00
|
|
|
|
|
|
|
|
if (GSConfig.TexturePreloading == TexturePreloadingLevel::Full)
|
|
|
|
|
{
|
2025-11-10 13:38:49 -06:00
|
|
|
const double hashcache_MB = get_MB(static_cast<double>(g_texture_cache->GetHashCacheMemoryUsage()));
|
|
|
|
|
const double total_MB = targets_MB + sources_MB + hashcache_MB + pool_MB;
|
2025-11-12 19:56:15 +00:00
|
|
|
info.format("VRAM: {} MB | TGT: {} MB | SRC: {} MB | HC: {} MB | PL: {} MB",
|
2025-11-10 13:38:49 -06:00
|
|
|
format_precision(total_MB),
|
|
|
|
|
format_precision(targets_MB),
|
|
|
|
|
format_precision(sources_MB),
|
|
|
|
|
format_precision(hashcache_MB),
|
|
|
|
|
format_precision(pool_MB));
|
2023-01-24 13:59:12 +10:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-11-10 13:38:49 -06:00
|
|
|
const double total_MB = targets_MB + sources_MB + pool_MB;
|
2025-11-12 19:56:15 +00:00
|
|
|
info.format("VRAM: {} MB | TGT: {} MB | SRC: {} MB | PL: {} MB",
|
2025-11-10 13:38:49 -06:00
|
|
|
format_precision(total_MB),
|
|
|
|
|
format_precision(targets_MB),
|
|
|
|
|
format_precision(sources_MB),
|
|
|
|
|
format_precision(pool_MB));
|
2011-02-17 03:24:37 +00:00
|
|
|
}
|
2009-12-03 15:49:12 +00:00
|
|
|
}
|
2009-10-31 01:06:23 +00:00
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
void GSgetTitleStats(std::string& info)
|
2009-12-04 03:24:59 +00:00
|
|
|
{
|
2022-12-24 13:46:11 +10:00
|
|
|
static constexpr const char* deinterlace_modes[] = {
|
|
|
|
|
"Automatic", "None", "Weave tff", "Weave bff", "Bob tff", "Bob bff", "Blend tff", "Blend bff", "Adaptive tff", "Adaptive bff"};
|
|
|
|
|
|
2023-04-08 13:20:41 +10:00
|
|
|
const char* api_name = GSDevice::RenderAPIToString(g_gs_device->GetRenderAPI());
|
2023-12-31 17:45:13 +10:00
|
|
|
const char* hw_sw_name = (GSCurrentRenderer == GSRendererType::Null) ? " Null" : (GSIsHardwareRenderer() ? " HW" : " SW");
|
2022-12-24 13:46:11 +10:00
|
|
|
const char* deinterlace_mode = deinterlace_modes[static_cast<int>(GSConfig.InterlaceMode)];
|
2022-04-03 23:46:05 +10:00
|
|
|
|
|
|
|
|
const char* interlace_mode = ReportInterlaceMode();
|
|
|
|
|
const char* video_mode = ReportVideoMode();
|
|
|
|
|
info = StringUtil::StdStringFromFormat("%s%s | %s | %s | %s", api_name, hw_sw_name, video_mode, interlace_mode, deinterlace_mode);
|
2021-12-19 00:43:50 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
|
|
|
|
|
{
|
|
|
|
|
Pcsx2Config::GSOptions old_config(std::move(GSConfig));
|
|
|
|
|
GSConfig = new_config;
|
2022-04-23 14:08:26 +10:00
|
|
|
if (!g_gs_renderer)
|
2021-12-19 00:43:50 +10:00
|
|
|
return;
|
|
|
|
|
|
2026-07-19 14:58:49 -07:00
|
|
|
// 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();
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
// Handle OSD scale changes by pushing a window resize through.
|
|
|
|
|
if (new_config.OsdScale != old_config.OsdScale)
|
2024-02-19 22:37:25 +09:00
|
|
|
ImGuiManager::RequestScaleUpdate();
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2026-03-08 08:30:10 -04:00
|
|
|
if (new_config.OsdFontPath != old_config.OsdFontPath)
|
|
|
|
|
ImGuiManager::ReloadFonts();
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
// Options which need a full teardown/recreate.
|
|
|
|
|
if (!GSConfig.RestartOptionsAreEqual(old_config))
|
2011-02-17 03:24:37 +00:00
|
|
|
{
|
2024-05-11 21:21:03 +10:00
|
|
|
if (!GSreopen(true, true, GSConfig.Renderer, &old_config))
|
2022-05-30 20:15:16 +10:00
|
|
|
pxFailRel("Failed to do full GS reopen");
|
2021-12-19 00:43:50 +10:00
|
|
|
return;
|
2011-02-17 03:24:37 +00:00
|
|
|
}
|
2021-12-19 00:43:50 +10:00
|
|
|
|
2024-07-01 00:21:03 +10:00
|
|
|
// Ensure upscale multiplier is in range.
|
|
|
|
|
GSClampUpscaleMultiplier(GSConfig);
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
// Options which aren't using the global struct yet, so we need to recreate all GS objects.
|
2023-05-11 20:41:26 +10:00
|
|
|
if (GSConfig.SWExtraThreads != old_config.SWExtraThreads ||
|
2022-12-24 13:46:11 +10:00
|
|
|
GSConfig.SWExtraThreadsHeight != old_config.SWExtraThreadsHeight)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2024-05-11 21:21:03 +10:00
|
|
|
if (!GSreopen(false, true, GSConfig.Renderer, &old_config))
|
2022-05-30 20:15:16 +10:00
|
|
|
pxFailRel("Failed to do quick GS reopen");
|
|
|
|
|
|
2021-12-19 00:43:50 +10:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-11 20:41:26 +10:00
|
|
|
if (GSConfig.UserHacks_DisableRenderFixes != old_config.UserHacks_DisableRenderFixes ||
|
2023-01-05 22:08:33 +10:00
|
|
|
GSConfig.UpscaleMultiplier != old_config.UpscaleMultiplier ||
|
|
|
|
|
GSConfig.GetSkipCountFunctionId != old_config.GetSkipCountFunctionId ||
|
2023-07-07 21:48:16 +10:00
|
|
|
GSConfig.BeforeDrawFunctionId != old_config.BeforeDrawFunctionId ||
|
|
|
|
|
GSConfig.MoveHandlerFunctionId != old_config.MoveHandlerFunctionId)
|
2022-01-01 20:20:16 +10:00
|
|
|
{
|
2023-07-30 01:55:06 +10:00
|
|
|
g_gs_renderer->UpdateRenderFixes();
|
2022-01-01 20:20:16 +10:00
|
|
|
}
|
|
|
|
|
|
2022-03-08 18:14:12 +10:00
|
|
|
// renderer-specific options (e.g. auto flush, TC offset)
|
2022-04-23 14:08:26 +10:00
|
|
|
g_gs_renderer->UpdateSettings(old_config);
|
2026-07-19 14:20:49 -07:00
|
|
|
if (g_gs_front)
|
|
|
|
|
g_gs_front->UpdateSettings(old_config);
|
2022-03-04 01:10:19 +10:00
|
|
|
|
|
|
|
|
// reload texture cache when trilinear filtering or TC options change
|
|
|
|
|
if (
|
2023-12-31 17:45:13 +10:00
|
|
|
(GSIsHardwareRenderer() && GSConfig.HWMipmap != old_config.HWMipmap) ||
|
2022-01-23 15:19:06 +10:00
|
|
|
GSConfig.TexturePreloading != old_config.TexturePreloading ||
|
2022-10-01 05:32:31 +02:00
|
|
|
GSConfig.TriFilter != old_config.TriFilter ||
|
2022-03-04 01:10:19 +10:00
|
|
|
GSConfig.GPUPaletteConversion != old_config.GPUPaletteConversion ||
|
|
|
|
|
GSConfig.PreloadFrameWithGSData != old_config.PreloadFrameWithGSData ||
|
|
|
|
|
GSConfig.UserHacks_CPUFBConversion != old_config.UserHacks_CPUFBConversion ||
|
|
|
|
|
GSConfig.UserHacks_DisableDepthSupport != old_config.UserHacks_DisableDepthSupport ||
|
|
|
|
|
GSConfig.UserHacks_DisablePartialInvalidation != old_config.UserHacks_DisablePartialInvalidation ||
|
2022-06-03 01:00:39 +10:00
|
|
|
GSConfig.UserHacks_TextureInsideRt != old_config.UserHacks_TextureInsideRt ||
|
2022-10-22 11:43:29 +01:00
|
|
|
GSConfig.UserHacks_CPUSpriteRenderBW != old_config.UserHacks_CPUSpriteRenderBW ||
|
2023-01-02 23:14:10 +10:00
|
|
|
GSConfig.UserHacks_CPUCLUTRender != old_config.UserHacks_CPUCLUTRender ||
|
2026-07-26 15:00:42 +02:00
|
|
|
GSConfig.UserHacks_GPUTargetCLUTMode != old_config.UserHacks_GPUTargetCLUTMode ||
|
2026-07-29 16:53:41 +02:00
|
|
|
// The geometry hacks below all outlive the draw, because what they move ends up
|
|
|
|
|
// baked into a cached target: native scaling swaps a target's texture for a
|
|
|
|
|
// downscaled one and pins m_scale to 1, the rest shift vertices or texture
|
|
|
|
|
// coordinates on the way in. Switch one off and a target the game doesn't redraw
|
|
|
|
|
// keeps the old pixels — that's the ghosting people report as a stuck setting.
|
|
|
|
|
GSConfig.UserHacks_NativeScaling != old_config.UserHacks_NativeScaling ||
|
|
|
|
|
GSConfig.UserHacks_AlignSpriteX != old_config.UserHacks_AlignSpriteX ||
|
|
|
|
|
GSConfig.UserHacks_MergePPSprite != old_config.UserHacks_MergePPSprite ||
|
|
|
|
|
GSConfig.UserHacks_RoundSprite != old_config.UserHacks_RoundSprite ||
|
|
|
|
|
GSConfig.UserHacks_HalfPixelOffset != old_config.UserHacks_HalfPixelOffset ||
|
|
|
|
|
GSConfig.UserHacks_ForceEvenSpritePosition != old_config.UserHacks_ForceEvenSpritePosition ||
|
|
|
|
|
GSConfig.UserHacks_NativePaletteDraw != old_config.UserHacks_NativePaletteDraw ||
|
|
|
|
|
GSConfig.UserHacks_BilinearHack != old_config.UserHacks_BilinearHack ||
|
|
|
|
|
GSConfig.UserHacks_TCOffsetX != old_config.UserHacks_TCOffsetX ||
|
|
|
|
|
GSConfig.UserHacks_TCOffsetY != old_config.UserHacks_TCOffsetY)
|
2022-01-01 20:20:16 +10:00
|
|
|
{
|
2023-02-23 21:00:48 +10:00
|
|
|
if (GSConfig.UserHacks_ReadTCOnClose)
|
|
|
|
|
g_gs_renderer->ReadbackTextureCache();
|
2023-11-04 21:32:27 +10:00
|
|
|
g_gs_renderer->PurgeTextureCache(true, true, true);
|
2024-05-11 21:21:03 +10:00
|
|
|
g_gs_device->ClearCurrent();
|
|
|
|
|
g_gs_device->PurgePool();
|
2022-01-01 20:20:16 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clear out the sampler cache when AF options change, since the anisotropy gets baked into them
|
|
|
|
|
if (GSConfig.MaxAnisotropy != old_config.MaxAnisotropy)
|
|
|
|
|
g_gs_device->ClearSamplerCache();
|
2022-02-20 00:20:18 +10:00
|
|
|
|
|
|
|
|
// texture dumping/replacement options
|
2023-12-31 17:45:13 +10:00
|
|
|
if (GSIsHardwareRenderer())
|
2023-04-01 13:26:06 +10:00
|
|
|
GSTextureReplacements::UpdateConfig(old_config);
|
2022-02-20 00:20:18 +10:00
|
|
|
|
|
|
|
|
// clear the hash texture cache since we might have replacements now
|
|
|
|
|
// also clear it when dumping changes, since we want to dump everything being used
|
|
|
|
|
if (GSConfig.LoadTextureReplacements != old_config.LoadTextureReplacements ||
|
|
|
|
|
GSConfig.DumpReplaceableTextures != old_config.DumpReplaceableTextures)
|
|
|
|
|
{
|
2023-11-04 21:32:27 +10:00
|
|
|
g_gs_renderer->PurgeTextureCache(true, false, true);
|
2022-02-20 00:20:18 +10:00
|
|
|
}
|
2022-03-11 22:05:38 +10:00
|
|
|
|
2026-07-22 09:56:36 -07:00
|
|
|
// Per-draw ledger. Writing on the true->false edge means a live capture is just
|
|
|
|
|
// "turn it on, play the slow bit, turn it off" -- both edges drivable over PINE.
|
|
|
|
|
if (GSConfig.DumpDrawLog != old_config.DumpDrawLog)
|
|
|
|
|
{
|
|
|
|
|
if (GSConfig.DumpDrawLog)
|
|
|
|
|
{
|
|
|
|
|
GSDrawLog::Reset();
|
|
|
|
|
GSDrawLog::Start();
|
|
|
|
|
Console.WriteLn("GSDrawLog: recording started.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
GSDrawLog::Stop();
|
|
|
|
|
if (GSDrawLog::GetRecordCount() > 0)
|
|
|
|
|
GSDrawLog::WriteCSV(Path::Combine(EmuFolders::Logs, "gs_drawlog.csv"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:35:15 +07:00
|
|
|
if (GSConfig.OsdShowGPU && !old_config.OsdShowGPU)
|
2022-03-11 22:05:38 +10:00
|
|
|
{
|
2026-06-06 17:35:15 +07:00
|
|
|
if (!g_gs_device->SetGPUTimingEnabled(true))
|
2022-09-07 20:19:40 +10:00
|
|
|
GSConfig.OsdShowGPU = false;
|
2022-03-11 22:05:38 +10:00
|
|
|
}
|
2026-04-30 23:48:39 -04:00
|
|
|
|
|
|
|
|
if (GSConfig.OsdShowGPUStats != old_config.OsdShowGPUStats)
|
|
|
|
|
{
|
|
|
|
|
if (!g_gs_device->SetGPUPipelineStatisticsEnabled(GSConfig.OsdShowGPUStats))
|
|
|
|
|
GSConfig.OsdShowGPUStats = false;
|
|
|
|
|
}
|
2009-12-04 03:24:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-31 17:45:13 +10:00
|
|
|
void GSSetSoftwareRendering(bool software_renderer, GSInterlaceMode new_interlace)
|
2021-07-09 00:33:44 +10:00
|
|
|
{
|
2023-12-31 17:45:13 +10:00
|
|
|
if (!g_gs_renderer)
|
2021-12-19 00:43:50 +10:00
|
|
|
return;
|
|
|
|
|
|
2023-10-29 02:37:23 +00:00
|
|
|
GSConfig.InterlaceMode = new_interlace;
|
|
|
|
|
|
2023-12-31 17:45:13 +10:00
|
|
|
if (!GSIsHardwareRenderer() != software_renderer)
|
|
|
|
|
{
|
2024-01-04 13:54:24 +10:00
|
|
|
// Config might be SW, and we're switching to HW -> use Auto.
|
|
|
|
|
const GSRendererType renderer = (software_renderer ? GSRendererType::SW :
|
|
|
|
|
(GSConfig.Renderer == GSRendererType::SW ? GSRendererType::Auto : GSConfig.Renderer));
|
2024-05-11 21:21:03 +10:00
|
|
|
if (!GSreopen(false, true, renderer, std::nullopt))
|
2023-12-31 17:45:13 +10:00
|
|
|
pxFailRel("Failed to reopen GS for renderer switch.");
|
|
|
|
|
}
|
2021-07-09 00:33:44 +10:00
|
|
|
}
|
|
|
|
|
|
2022-12-10 19:11:08 +10:00
|
|
|
bool GSSaveSnapshotToMemory(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders,
|
|
|
|
|
u32* width, u32* height, std::vector<u32>* pixels)
|
2021-12-19 00:43:50 +10:00
|
|
|
{
|
2022-04-23 14:08:26 +10:00
|
|
|
if (!g_gs_renderer)
|
2021-07-09 00:33:44 +10:00
|
|
|
return false;
|
|
|
|
|
|
2022-12-10 19:11:08 +10:00
|
|
|
return g_gs_renderer->SaveSnapshotToMemory(window_width, window_height, apply_aspect, crop_borders,
|
|
|
|
|
width, height, pixels);
|
2021-07-09 00:33:44 +10:00
|
|
|
}
|
|
|
|
|
|
2021-05-11 19:37:04 +02:00
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
2022-12-12 20:05:53 +10:00
|
|
|
void* GSAllocateWrappedMemory(size_t size, size_t repeat)
|
2022-06-28 17:37:52 +10:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
// 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)
|
2022-06-28 17:37:52 +10:00
|
|
|
{
|
|
|
|
|
Console.Error("Failed to create file mapping of size %zu. WIN API ERROR:%u", size, GetLastError());
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reserve the whole area with repeats.
|
|
|
|
|
u8* base = static_cast<u8*>(VirtualAlloc2(
|
|
|
|
|
GetCurrentProcess(), nullptr, repeat * size,
|
|
|
|
|
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS,
|
|
|
|
|
nullptr, 0));
|
|
|
|
|
if (base)
|
|
|
|
|
{
|
|
|
|
|
bool okay = true;
|
|
|
|
|
for (size_t i = 0; i < repeat; i++)
|
|
|
|
|
{
|
|
|
|
|
// 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)) ||
|
2026-07-19 14:20:49 -07:00
|
|
|
!MapViewOfFile3(fh, GetCurrentProcess(), addr, 0, size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0))
|
2022-06-28 17:37:52 +10:00
|
|
|
{
|
|
|
|
|
Console.Error("Failed to map repeat %zu of size %zu.", i, size);
|
|
|
|
|
okay = false;
|
|
|
|
|
|
|
|
|
|
for (size_t j = 0; j < i; j++)
|
|
|
|
|
UnmapViewOfFile2(GetCurrentProcess(), addr, MEM_PRESERVE_PLACEHOLDER);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (okay)
|
|
|
|
|
{
|
|
|
|
|
DbgCon.WriteLn("fifo_alloc(): Mapped %zu repeats of %zu bytes at %p.", repeat, size, base);
|
2026-07-19 14:20:49 -07:00
|
|
|
CloseHandle(fh);
|
2022-06-28 17:37:52 +10:00
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VirtualFreeEx(GetCurrentProcess(), base, 0, MEM_RELEASE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console.Error("Failed to reserve VA space of size %zu. WIN API ERROR:%u", size, GetLastError());
|
2026-07-19 14:20:49 -07:00
|
|
|
CloseHandle(fh);
|
2022-06-28 17:37:52 +10:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-12 20:05:53 +10:00
|
|
|
void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
|
2022-06-28 17:37:52 +10:00
|
|
|
{
|
|
|
|
|
for (size_t i = 0; i < repeat; i++)
|
|
|
|
|
{
|
|
|
|
|
u8* addr = (u8*)ptr + i * size;
|
|
|
|
|
UnmapViewOfFile2(GetCurrentProcess(), addr, MEM_PRESERVE_PLACEHOLDER);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VirtualFreeEx(GetCurrentProcess(), ptr, 0, MEM_RELEASE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
2021-05-11 19:37:04 +02:00
|
|
|
#include <sys/mman.h>
|
2022-05-20 00:46:33 +10:00
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <fcntl.h>
|
2021-05-11 19:37:04 +02:00
|
|
|
#include <unistd.h>
|
|
|
|
|
|
2022-12-12 20:05:53 +10:00
|
|
|
void* GSAllocateWrappedMemory(size_t size, size_t repeat)
|
2021-05-11 19:37:04 +02:00
|
|
|
{
|
2026-07-19 14:20:49 -07:00
|
|
|
// 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).
|
2026-07-19 16:19:06 -07:00
|
|
|
// 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.
|
2026-07-18 17:17:12 +02:00
|
|
|
const std::string file_name = HostSys::GetFileMappingName("GS.mem");
|
|
|
|
|
void* const handle = HostSys::CreateSharedMemory(file_name.c_str(), repeat * size);
|
|
|
|
|
if (!handle)
|
2026-07-08 21:46:23 -04:00
|
|
|
{
|
2026-07-18 17:17:12 +02:00
|
|
|
std::fprintf(stderr,
|
|
|
|
|
"GSAllocateWrappedMemory: HostSys::CreateSharedMemory failed "
|
|
|
|
|
"(size=%zu repeat=%zu total=%zu)\n",
|
|
|
|
|
size, repeat, repeat * size);
|
2026-07-08 21:46:23 -04:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-07-19 16:19:06 -07:00
|
|
|
const int fd = static_cast<int>(reinterpret_cast<intptr_t>(handle));
|
2021-05-11 19:37:04 +02:00
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
void* fifo = mmap(nullptr, size * repeat, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
2026-07-10 19:21:34 +02:00
|
|
|
|
2021-05-11 19:37:04 +02:00
|
|
|
for (size_t i = 1; i < repeat; i++)
|
|
|
|
|
{
|
2021-08-30 21:45:52 -05:00
|
|
|
void* base = (u8*)fifo + size * i;
|
2026-07-19 14:20:49 -07:00
|
|
|
u8* next = (u8*)mmap(base, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
|
2021-05-11 19:37:04 +02:00
|
|
|
if (next != base)
|
|
|
|
|
fprintf(stderr, "Fail to mmap contiguous segment\n");
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 14:20:49 -07:00
|
|
|
close(fd);
|
|
|
|
|
|
2021-05-11 19:37:04 +02:00
|
|
|
return fifo;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-12 20:05:53 +10:00
|
|
|
void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
|
2021-05-11 19:37:04 +02:00
|
|
|
{
|
|
|
|
|
munmap(ptr, size * repeat);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 19:21:34 +02:00
|
|
|
#endif
|
|
|
|
|
|
2023-07-09 04:39:30 +01:00
|
|
|
std::pair<u8, u8> GSGetRGBA8AlphaMinMax(const void* data, u32 width, u32 height, u32 stride)
|
|
|
|
|
{
|
|
|
|
|
GSVector4i minc = GSVector4i::xffffffff();
|
|
|
|
|
GSVector4i maxc = GSVector4i::zero();
|
|
|
|
|
|
|
|
|
|
const u8* ptr = static_cast<const u8*>(data);
|
|
|
|
|
if ((width % 4) == 0)
|
|
|
|
|
{
|
|
|
|
|
for (u32 r = 0; r < height; r++)
|
|
|
|
|
{
|
|
|
|
|
const u8* rptr = ptr;
|
|
|
|
|
for (u32 c = 0; c < width; c += 4)
|
|
|
|
|
{
|
2023-07-11 16:06:37 +01:00
|
|
|
const GSVector4i v = GSVector4i::load<false>(rptr);
|
2023-07-09 04:39:30 +01:00
|
|
|
rptr += sizeof(GSVector4i);
|
|
|
|
|
minc = minc.min_u32(v);
|
|
|
|
|
maxc = maxc.max_u32(v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ptr += stride;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
const u32 aligned_width = Common::AlignDownPow2(width, 4);
|
|
|
|
|
static constexpr const GSVector4i masks[3][2] = {
|
|
|
|
|
{GSVector4i::cxpr(0xFFFFFFFF, 0, 0, 0), GSVector4i::cxpr(0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF)},
|
2024-04-16 22:43:28 +10:00
|
|
|
{GSVector4i::cxpr(0xFFFFFFFF, 0xFFFFFFFF, 0, 0), GSVector4i::cxpr(0, 0, 0xFFFFFFFF, 0xFFFFFFFF)},
|
|
|
|
|
{GSVector4i::cxpr(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0), GSVector4i::cxpr(0, 0, 0, 0xFFFFFFFF)},
|
2023-07-09 04:39:30 +01:00
|
|
|
};
|
2024-04-16 22:43:28 +10:00
|
|
|
const u32 unaligned_pixels = width & 3;
|
|
|
|
|
const GSVector4i last_mask_and = masks[unaligned_pixels - 1][0];
|
|
|
|
|
const GSVector4i last_mask_or = masks[unaligned_pixels - 1][1];
|
2023-07-09 04:39:30 +01:00
|
|
|
|
|
|
|
|
for (u32 r = 0; r < height; r++)
|
|
|
|
|
{
|
|
|
|
|
const u8* rptr = ptr;
|
|
|
|
|
for (u32 c = 0; c < aligned_width; c += 4)
|
|
|
|
|
{
|
2023-07-11 16:06:37 +01:00
|
|
|
const GSVector4i v = GSVector4i::load<false>(rptr);
|
2023-07-09 04:39:30 +01:00
|
|
|
rptr += sizeof(GSVector4i);
|
|
|
|
|
minc = minc.min_u32(v);
|
|
|
|
|
maxc = maxc.max_u32(v);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-16 22:43:28 +10:00
|
|
|
GSVector4i v;
|
|
|
|
|
u32 vu;
|
|
|
|
|
if (unaligned_pixels == 3)
|
|
|
|
|
{
|
|
|
|
|
v = GSVector4i::loadl(rptr);
|
|
|
|
|
std::memcpy(&vu, rptr + sizeof(u32) * 2, sizeof(vu));
|
|
|
|
|
v = v.insert32<2>(vu);
|
|
|
|
|
}
|
|
|
|
|
else if (unaligned_pixels == 2)
|
|
|
|
|
{
|
|
|
|
|
v = GSVector4i::loadl(rptr);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
std::memcpy(&vu, rptr, sizeof(vu));
|
|
|
|
|
v = GSVector4i::load(vu);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-09 04:39:30 +01:00
|
|
|
minc = minc.min_u32(v | last_mask_or);
|
|
|
|
|
maxc = maxc.max_u32(v & last_mask_and);
|
|
|
|
|
|
|
|
|
|
ptr += stride;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::make_pair<u8, u8>(static_cast<u8>(minc.minv_u32() >> 24),
|
|
|
|
|
static_cast<u8>(maxc.maxv_u32() >> 24));
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 19:54:20 -06:00
|
|
|
static void HotkeyAdjustUpscaleMultiplier(const float delta)
|
2021-12-08 21:14:55 +10:00
|
|
|
{
|
2025-11-03 19:54:20 -06:00
|
|
|
if (!g_gs_renderer)
|
|
|
|
|
return;
|
2021-12-08 21:14:55 +10:00
|
|
|
|
2025-11-03 19:54:20 -06:00
|
|
|
if (GSCurrentRenderer == GSRendererType::SW || GSCurrentRenderer == GSRendererType::Null)
|
|
|
|
|
{
|
|
|
|
|
Host::AddIconOSDMessage("UpscaleMultiplierChanged", ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE,
|
|
|
|
|
TRANSLATE_STR("GS", "Upscaling can only be changed while using the Hardware Renderer."), Host::OSD_QUICK_DURATION);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clamp logic mirrors GraphicsSettingsWidget::populateUpscaleMultipliers().
|
|
|
|
|
float candidate_multiplier = EmuConfig.GS.UpscaleMultiplier + delta;
|
|
|
|
|
const float max_multiplier = static_cast<float>(std::clamp(GSGetMaxUpscaleMultiplier(g_gs_device->GetMaxTextureSize()),
|
|
|
|
|
10u, EmuConfig.GS.ExtendedUpscalingMultipliers ? 25u : 12u));
|
|
|
|
|
|
|
|
|
|
std::string osd_message;
|
|
|
|
|
if (candidate_multiplier <= 1)
|
|
|
|
|
{
|
|
|
|
|
candidate_multiplier = 1;
|
|
|
|
|
osd_message = TRANSLATE_STR("GS", "Upscale multiplier set to native resolution.");
|
|
|
|
|
}
|
|
|
|
|
else if (candidate_multiplier >= max_multiplier)
|
|
|
|
|
{
|
|
|
|
|
candidate_multiplier = max_multiplier;
|
|
|
|
|
osd_message = fmt::format(TRANSLATE_FS("GS", "Upscale multiplier maximized to {}x."), max_multiplier);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
osd_message = fmt::format(TRANSLATE_FS("GS", "Upscale multiplier {} to {}x."),
|
|
|
|
|
delta > 0 ? TRANSLATE_STR("GS", "increased") : TRANSLATE_STR("GS", "decreased"), candidate_multiplier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Need to calculate our own target resolution. Reading after applying settings is a race condition.
|
|
|
|
|
const GSVector2i base_resolution = g_gs_renderer ? g_gs_renderer->PCRTCDisplays.GetResolution() : GSVector2i(0, 0);
|
|
|
|
|
const int target_iwidth = static_cast<int>(std::round(static_cast<float>(base_resolution.x) * candidate_multiplier));
|
|
|
|
|
const int target_iheight = static_cast<int>(std::round(static_cast<float>(base_resolution.y) * candidate_multiplier));
|
|
|
|
|
|
|
|
|
|
//: Leftmost value is an OSD message about the upscale multiplier. Values in parentheses are a resolution width (left) and height (right).
|
|
|
|
|
Host::AddIconOSDMessage("UpscaleMultiplierChanged", ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE,
|
|
|
|
|
fmt::format(TRANSLATE_FS("GS", "{} ({} x {})"), osd_message, target_iwidth, target_iheight), Host::OSD_QUICK_DURATION);
|
|
|
|
|
|
|
|
|
|
// This is pretty slow. We only really need to flush the TC and recompile shaders.
|
2021-12-08 21:14:55 +10:00
|
|
|
// TODO(Stenzek): Make it faster at some point in the future.
|
2025-11-03 19:54:20 -06:00
|
|
|
EmuConfig.GS.UpscaleMultiplier = candidate_multiplier;
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::ApplySettings();
|
2021-12-08 21:14:55 +10:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 19:21:34 +02:00
|
|
|
static bool s_osd_hotkey_forced_simple = false;
|
|
|
|
|
|
|
|
|
|
static bool HasConfiguredOSD()
|
|
|
|
|
{
|
|
|
|
|
return EmuConfig.GS.OsdShowSpeed || EmuConfig.GS.OsdShowFPS || EmuConfig.GS.OsdShowVPS ||
|
|
|
|
|
EmuConfig.GS.OsdShowResolution || EmuConfig.GS.OsdShowGSStats || EmuConfig.GS.OsdShowCPU ||
|
|
|
|
|
EmuConfig.GS.OsdShowGPU || EmuConfig.GS.OsdShowGPUDebug || EmuConfig.GS.OsdShowIndicators ||
|
|
|
|
|
EmuConfig.GS.OsdShowFrameTimes || EmuConfig.GS.OsdShowHardwareInfo || EmuConfig.GS.OsdShowVersion ||
|
|
|
|
|
EmuConfig.GS.OsdShowSettings || EmuConfig.GS.OsdshowPatches || EmuConfig.GS.OsdShowInputs ||
|
|
|
|
|
EmuConfig.GS.OsdShowInputRec || EmuConfig.GS.OsdShowVideoCapture || EmuConfig.GS.OsdShowTextureReplacements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void SetForcedSimpleOSD(bool enabled)
|
|
|
|
|
{
|
|
|
|
|
s_osd_hotkey_forced_simple = enabled;
|
|
|
|
|
GSConfig.OsdShowFPS = enabled;
|
|
|
|
|
GSConfig.OsdShowVPS = enabled;
|
|
|
|
|
GSConfig.OsdShowSpeed = enabled;
|
|
|
|
|
GSConfig.OsdShowVersion = enabled;
|
|
|
|
|
GSConfig.OsdShowIndicators = enabled;
|
|
|
|
|
GSConfig.OsdMessagesPos = enabled ? OsdOverlayPos::TopLeft : OsdOverlayPos::None;
|
|
|
|
|
GSConfig.OsdPerformancePos = enabled ? OsdOverlayPos::TopRight : OsdOverlayPos::None;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-26 23:28:59 +10:00
|
|
|
static void HotkeyToggleOSD()
|
|
|
|
|
{
|
2026-07-10 19:21:34 +02:00
|
|
|
if (!HasConfiguredOSD())
|
|
|
|
|
{
|
|
|
|
|
SetForcedSimpleOSD(!s_osd_hotkey_forced_simple || GSConfig.OsdPerformancePos == OsdOverlayPos::None);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_osd_hotkey_forced_simple = false;
|
|
|
|
|
|
2023-08-26 23:28:59 +10:00
|
|
|
GSConfig.OsdShowSettings ^= EmuConfig.GS.OsdShowSettings;
|
2025-09-28 21:48:32 +07:00
|
|
|
GSConfig.OsdshowPatches ^= EmuConfig.GS.OsdshowPatches;
|
2023-08-26 23:28:59 +10:00
|
|
|
GSConfig.OsdShowInputs ^= EmuConfig.GS.OsdShowInputs;
|
2024-08-02 20:51:02 +07:00
|
|
|
GSConfig.OsdShowInputRec ^= EmuConfig.GS.OsdShowInputRec;
|
|
|
|
|
GSConfig.OsdShowVideoCapture ^= EmuConfig.GS.OsdShowVideoCapture;
|
2025-11-23 07:47:16 -05:00
|
|
|
GSConfig.OsdShowTextureReplacements ^= EmuConfig.GS.OsdShowTextureReplacements;
|
2024-07-29 00:43:28 -05:00
|
|
|
|
|
|
|
|
GSConfig.OsdMessagesPos =
|
|
|
|
|
GSConfig.OsdMessagesPos == OsdOverlayPos::None ? EmuConfig.GS.OsdMessagesPos : OsdOverlayPos::None;
|
|
|
|
|
GSConfig.OsdPerformancePos =
|
|
|
|
|
GSConfig.OsdPerformancePos == OsdOverlayPos::None ? EmuConfig.GS.OsdPerformancePos : OsdOverlayPos::None;
|
2023-08-26 23:28:59 +10:00
|
|
|
}
|
|
|
|
|
|
2023-06-19 20:27:34 +10:00
|
|
|
BEGIN_HOTKEY_LIST(g_gs_hotkeys){"Screenshot", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Save Screenshot"),
|
|
|
|
|
[](s32 pressed) {
|
2022-05-16 21:10:34 +10:00
|
|
|
if (!pressed)
|
|
|
|
|
{
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([]() { GSQueueSnapshot(std::string(), 0); });
|
2022-05-16 21:10:34 +10:00
|
|
|
}
|
|
|
|
|
}},
|
2023-06-19 20:27:34 +10:00
|
|
|
{"ToggleVideoCapture", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Video Capture"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
if (GSCapture::IsCapturing())
|
|
|
|
|
{
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([]() { g_gs_renderer->EndCapture(); });
|
|
|
|
|
MTGS::WaitGS(false, false, false);
|
2023-06-19 20:27:34 +10:00
|
|
|
return;
|
|
|
|
|
}
|
2022-12-18 23:05:00 +10:00
|
|
|
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([]() {
|
2023-06-19 20:27:34 +10:00
|
|
|
std::string filename(fmt::format("{}.{}", GSGetBaseVideoFilename(), GSConfig.CaptureContainer));
|
|
|
|
|
g_gs_renderer->BeginCapture(std::move(filename));
|
|
|
|
|
});
|
2023-01-20 00:01:54 +10:00
|
|
|
|
2023-06-19 20:27:34 +10:00
|
|
|
// Sync GS thread. We want to start adding audio at the same time as video.
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::WaitGS(false, false, false);
|
2023-06-19 20:27:34 +10:00
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{"GSDumpSingleFrame", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Save Single Frame GS Dump"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([]() { GSQueueSnapshot(std::string(), 1); });
|
2023-06-19 20:27:34 +10:00
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{"GSDumpMultiFrame", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Save Multi Frame GS Dump"),
|
|
|
|
|
[](s32 pressed) {
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([pressed]() {
|
2023-06-19 20:27:34 +10:00
|
|
|
if (pressed > 0)
|
|
|
|
|
GSQueueSnapshot(std::string(), std::numeric_limits<u32>::max());
|
|
|
|
|
else
|
|
|
|
|
GSStopGSDump();
|
2022-05-16 21:10:34 +10:00
|
|
|
});
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
|
|
|
|
{"ToggleSoftwareRendering", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Toggle Software Rendering"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::ToggleSoftwareRendering();
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
|
|
|
|
{"IncreaseUpscaleMultiplier", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Increase Upscale Multiplier"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
2025-11-03 19:54:20 -06:00
|
|
|
HotkeyAdjustUpscaleMultiplier(1.0f);
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
|
|
|
|
{"DecreaseUpscaleMultiplier", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Decrease Upscale Multiplier"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
2025-11-03 19:54:20 -06:00
|
|
|
HotkeyAdjustUpscaleMultiplier(-1.0f);
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
2023-08-26 23:28:59 +10:00
|
|
|
{"ToggleOSD", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle On-Screen Display"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
HotkeyToggleOSD();
|
|
|
|
|
}},
|
2023-06-19 20:27:34 +10:00
|
|
|
{"CycleAspectRatio", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle Aspect Ratio"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (pressed)
|
|
|
|
|
return;
|
2021-12-08 21:14:55 +10:00
|
|
|
|
2023-06-19 20:27:34 +10:00
|
|
|
// technically this races, but the worst that'll happen is one frame uses the old AR.
|
|
|
|
|
EmuConfig.CurrentAspectRatio = static_cast<AspectRatioType>(
|
|
|
|
|
(static_cast<int>(EmuConfig.CurrentAspectRatio) + 1) % static_cast<int>(AspectRatioType::MaxCount));
|
|
|
|
|
Host::AddKeyedOSDMessage("CycleAspectRatio",
|
2023-07-19 23:48:54 +10:00
|
|
|
fmt::format(TRANSLATE_FS("Hotkeys", "Aspect ratio set to '{}'."),
|
2023-06-19 20:27:34 +10:00
|
|
|
Pcsx2Config::GSOptions::AspectRatioNames[static_cast<int>(EmuConfig.CurrentAspectRatio)]),
|
|
|
|
|
Host::OSD_QUICK_DURATION);
|
|
|
|
|
}},
|
2024-05-16 15:58:04 +02:00
|
|
|
{"ToggleMipmapMode", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Hardware Mipmapping"),
|
2023-06-19 20:27:34 +10:00
|
|
|
[](s32 pressed) {
|
2024-05-16 15:58:04 +02:00
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
EmuConfig.GS.HWMipmap = !EmuConfig.GS.HWMipmap;
|
|
|
|
|
Host::AddKeyedOSDMessage("ToggleMipmapMode",
|
|
|
|
|
EmuConfig.GS.HWMipmap ?
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Hardware mipmapping is now enabled.") :
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Hardware mipmapping is now disabled."),
|
|
|
|
|
Host::OSD_INFO_DURATION);
|
|
|
|
|
MTGS::ApplySettings();
|
|
|
|
|
}
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
|
|
|
|
{"CycleInterlaceMode", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle Deinterlace Mode"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (pressed)
|
|
|
|
|
return;
|
2021-12-08 21:14:55 +10:00
|
|
|
|
2023-06-19 20:27:34 +10:00
|
|
|
static constexpr std::array<const char*, static_cast<int>(GSInterlaceMode::Count)> option_names = {{
|
2024-08-02 19:40:27 +07:00
|
|
|
TRANSLATE_NOOP("Hotkeys", "Automatic"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Off"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Weave (Top Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Weave (Bottom Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Bob (Top Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Bob (Bottom Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Blend (Top Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Blend (Bottom Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Adaptive (Top Field First)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Adaptive (Bottom Field First)"),
|
2023-06-19 20:27:34 +10:00
|
|
|
}};
|
2021-12-08 21:14:55 +10:00
|
|
|
|
2023-06-19 20:27:34 +10:00
|
|
|
const GSInterlaceMode new_mode = static_cast<GSInterlaceMode>(
|
|
|
|
|
(static_cast<s32>(EmuConfig.GS.InterlaceMode) + 1) % static_cast<s32>(GSInterlaceMode::Count));
|
|
|
|
|
Host::AddKeyedOSDMessage("CycleInterlaceMode",
|
|
|
|
|
fmt::format(
|
2023-07-19 23:48:54 +10:00
|
|
|
TRANSLATE_FS("Hotkeys", "Deinterlace mode set to '{}'."), option_names[static_cast<s32>(new_mode)]),
|
2023-06-19 20:27:34 +10:00
|
|
|
Host::OSD_QUICK_DURATION);
|
2021-12-08 21:14:55 +10:00
|
|
|
|
2026-03-08 00:22:57 +07:00
|
|
|
EmuConfig.GS.InterlaceMode = new_mode;
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::RunOnGSThread([new_mode]() { GSConfig.InterlaceMode = new_mode; });
|
2023-06-19 20:27:34 +10:00
|
|
|
}},
|
2025-07-02 15:45:31 -04:00
|
|
|
{"CycleTVShader", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle TV Shader"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (pressed)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
static constexpr std::array<const char*, 8> option_names = {{
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "None (Default)"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Scanline Filter"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Diagonal Filter"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Triangular Filter"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Wave Filter"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Lottes CRT"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "4xRGSS"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "NxAGSS"),
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
const u32 new_shader = (EmuConfig.GS.TVShader + 1) % 8;
|
|
|
|
|
Host::AddKeyedOSDMessage("CycleTVShader",
|
|
|
|
|
fmt::format(
|
|
|
|
|
TRANSLATE_FS("Hotkeys", "TV shader set to '{}'."), option_names[new_shader]),
|
|
|
|
|
Host::OSD_QUICK_DURATION);
|
|
|
|
|
|
2026-03-08 00:22:57 +07:00
|
|
|
EmuConfig.GS.TVShader = new_shader;
|
2025-07-02 15:45:31 -04:00
|
|
|
MTGS::RunOnGSThread([new_shader]() { GSConfig.TVShader = new_shader; });
|
|
|
|
|
}},
|
2026-03-08 00:22:57 +07:00
|
|
|
{"CycleBlendingAccuracy", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle Blending Accuracy"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (pressed)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
static constexpr std::array<const char*, static_cast<u8>(AccBlendLevel::MaxCount)> s_blending_option_names = {{
|
2026-05-16 23:49:45 +07:00
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "Minimum"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "Basic"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "Medium"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "High"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "Full"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys_BlendAcc", "Maximum"),
|
2026-03-08 00:22:57 +07:00
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
const AccBlendLevel new_blend_mode = static_cast<AccBlendLevel>(
|
|
|
|
|
(static_cast<u8>(EmuConfig.GS.AccurateBlendingUnit) + 1) % static_cast<u8>(AccBlendLevel::MaxCount));
|
|
|
|
|
Host::AddKeyedOSDMessage("CycleBlendingAccuracy",
|
|
|
|
|
fmt::format(
|
|
|
|
|
TRANSLATE_FS("Hotkeys", "Blending Accuracy set to {}."), s_blending_option_names[static_cast<u8>(new_blend_mode)]),
|
|
|
|
|
Host::OSD_QUICK_DURATION);
|
|
|
|
|
|
|
|
|
|
EmuConfig.GS.AccurateBlendingUnit = new_blend_mode;
|
|
|
|
|
MTGS::RunOnGSThread([new_blend_mode]() { GSConfig.AccurateBlendingUnit = new_blend_mode; });
|
|
|
|
|
}},
|
2023-06-19 20:27:34 +10:00
|
|
|
{"ToggleTextureDumping", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Texture Dumping"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
EmuConfig.GS.DumpReplaceableTextures = !EmuConfig.GS.DumpReplaceableTextures;
|
|
|
|
|
Host::AddKeyedOSDMessage("ToggleTextureReplacements",
|
|
|
|
|
EmuConfig.GS.DumpReplaceableTextures ? TRANSLATE_STR("Hotkeys", "Texture dumping is now enabled.") :
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Texture dumping is now disabled."),
|
|
|
|
|
Host::OSD_INFO_DURATION);
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::ApplySettings();
|
2023-06-19 20:27:34 +10:00
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{"ToggleTextureReplacements", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Toggle Texture Replacements"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
EmuConfig.GS.LoadTextureReplacements = !EmuConfig.GS.LoadTextureReplacements;
|
|
|
|
|
Host::AddKeyedOSDMessage("ToggleTextureReplacements",
|
|
|
|
|
EmuConfig.GS.LoadTextureReplacements ?
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Texture replacements are now enabled.") :
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Texture replacements are now disabled."),
|
|
|
|
|
Host::OSD_INFO_DURATION);
|
2023-06-24 17:46:36 +10:00
|
|
|
MTGS::ApplySettings();
|
2023-06-19 20:27:34 +10:00
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{"ReloadTextureReplacements", TRANSLATE_NOOP("Hotkeys", "Graphics"),
|
|
|
|
|
TRANSLATE_NOOP("Hotkeys", "Reload Texture Replacements"),
|
|
|
|
|
[](s32 pressed) {
|
|
|
|
|
if (!pressed)
|
|
|
|
|
{
|
|
|
|
|
if (!EmuConfig.GS.LoadTextureReplacements)
|
|
|
|
|
{
|
|
|
|
|
Host::AddKeyedOSDMessage("ReloadTextureReplacements",
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Texture replacements are not enabled."), Host::OSD_INFO_DURATION);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Host::AddKeyedOSDMessage("ReloadTextureReplacements",
|
|
|
|
|
TRANSLATE_STR("Hotkeys", "Reloading texture replacements..."), Host::OSD_INFO_DURATION);
|
2023-11-04 21:32:27 +10:00
|
|
|
MTGS::RunOnGSThread([]() {
|
|
|
|
|
if (!g_gs_renderer)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
GSTextureReplacements::ReloadReplacementMap();
|
|
|
|
|
g_gs_renderer->PurgeTextureCache(true, false, true);
|
|
|
|
|
});
|
2023-06-19 20:27:34 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
END_HOTKEY_LIST()
|