From ebd2be7ffe3d58014c85686a926ec056e834b0c0 Mon Sep 17 00:00:00 2001 From: Blackbird88 Date: Tue, 24 Sep 2024 14:34:42 +0200 Subject: [PATCH] GS/HW: Add all levels/unclamped mipmap modes (stenzek) --- pcsx2-qt/Settings/GraphicsSettingsWidget.cpp | 8 +- pcsx2-qt/Settings/GraphicsSettingsWidget.ui | 42 +- pcsx2/Config.h | 11 +- pcsx2/GS.cpp | 1306 ++++++++++++++--- pcsx2/GS/Renderers/HW/GSRendererHW.cpp | 71 +- pcsx2/GS/Renderers/HW/GSTextureCache.cpp | 9 +- .../GS/Renderers/HW/GSTextureReplacements.cpp | 2 +- pcsx2/GameDatabase.cpp | 9 +- pcsx2/ImGui/FullscreenUI.cpp | 23 +- pcsx2/ImGui/ImGuiOverlays.cpp | 4 +- pcsx2/Pcsx2Config.cpp | 4 +- pcsx2/VMManager.cpp | 4 +- 12 files changed, 1233 insertions(+), 260 deletions(-) diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp index 77244c713edd73..c235e7e8e8ad7e 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp @@ -146,7 +146,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.anisotropicFiltering, "EmuCore/GS", "MaxAnisotropy", s_anisotropic_filtering_entries, s_anisotropic_filtering_values, "0"); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.dithering, "EmuCore/GS", "dithering_ps2", 2); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mipmapping, "EmuCore/GS", "hw_mipmap", true); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.hwMipmapMode, "EmuCore/GS", "hw_mipmap_mode", + static_cast(GSHWMipmapMode::Enabled)); SettingWidgetBinder::BindWidgetToIntSetting( sif, m_ui.blending, "EmuCore/GS", "accurate_blending_unit", static_cast(AccBlendLevel::Basic)); SettingWidgetBinder::BindWidgetToIntSetting( @@ -494,8 +495,9 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* "FMV resolution will remain unchanged, as the video files are pre-rendered.")); dialog->registerWidgetHelp( - m_ui.mipmapping, tr("Mipmapping"), tr("Checked"), tr("Enables mipmapping, which some games require to render correctly.")); - + m_ui.hwMipmapMode, tr("Mipmapping"), tr("Enabled"), tr("Enables mipmapping, which many games require to render correctly. " + "Unclamped allows higher texture detail levels to be used, but may break certain graphical effects.")); + dialog->registerWidgetHelp( m_ui.textureFiltering, tr("Texture Filtering"), tr("Bilinear (PS2)"), tr("Control the texture filtering of the emulation.")); diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui index ed2641030956c7..880f8c15f290d7 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui @@ -643,14 +643,7 @@ Spin GPU During Readbacks - - - - - Mipmapping - - - + @@ -658,7 +651,7 @@ - + Manual Hardware Renderer Fixes @@ -666,6 +659,37 @@ + + + + + Mipmapping: + + + + + + + + Disabled + + + + + Enabled + + + + + All Levels + + + + + Unclamped + + + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index cd252a76faca6a..6001d6ccb88ca6 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -286,6 +286,15 @@ enum class BiFiltering : u8 Forced_But_Sprite, }; +enum class GSHWMipmapMode : u8 +{ + Disabled, + Enabled, + AllLevels, + Unclamped, + MaxCount +}; + enum class TriFiltering : s8 { Automatic = -1, @@ -627,7 +636,6 @@ struct Pcsx2Config AutoFlushSW : 1, PreloadFrameWithGSData : 1, Mipmap : 1, - HWMipmap : 1, ManualUserHacks : 1, UserHacks_AlignSpriteX : 1, UserHacks_CPUFBConversion : 1, @@ -682,6 +690,7 @@ struct Pcsx2Config float UpscaleMultiplier = 1.0f; AccBlendLevel AccurateBlendingUnit = AccBlendLevel::Basic; + GSHWMipmapMode HWMipmapMode = GSHWMipmapMode::Disabled; BiFiltering TextureFiltering = BiFiltering::PS2; TexturePreloadingLevel TexturePreloading = TexturePreloadingLevel::Full; GSDumpCompressionMethod GSDumpCompression = GSDumpCompressionMethod::Zstandard; diff --git a/pcsx2/GS.cpp b/pcsx2/GS.cpp index 1f7a6d5481a9a6..fb0268a8b6c356 100644 --- a/pcsx2/GS.cpp +++ b/pcsx2/GS.cpp @@ -1,343 +1,1231 @@ // SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "Counters.h" -#include "Common.h" #include "Config.h" -#include "Gif_Unit.h" +#include "Counters.h" +#include "ImGui/FullscreenUI.h" +#include "ImGui/ImGuiManager.h" +#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" +#include "Host.h" +#include "Input/InputManager.h" #include "MTGS.h" +#include "pcsx2/GS.h" +#include "GS/Renderers/Null/GSRendererNull.h" +#include "GS/Renderers/HW/GSRendererHW.h" +#include "GS/Renderers/HW/GSTextureReplacements.h" #include "VMManager.h" -#include +#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 + +#include "common/Console.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/SmallString.h" +#include "common/StringUtil.h" + +#include "IconsFontAwesome5.h" + +#include "fmt/format.h" -alignas(16) u8 g_RealGSMem[Ps2MemSize::GSregs]; -static bool s_GSRegistersWritten = false; +#include -void gsSetVideoMode(GS_VideoMode mode) +Pcsx2Config::GSOptions GSConfig; + +static GSRendererType GSCurrentRenderer; + +GSRendererType GSGetCurrentRenderer() { - gsVideoMode = mode; - UpdateVSyncRate(false); + return GSCurrentRenderer; } -// Make sure framelimiter options are in sync with GS capabilities. -void gsReset() +bool GSIsHardwareRenderer() { - MTGS::ResetGS(true); - gsVideoMode = GS_VideoMode::Uninitialized; - std::memset(g_RealGSMem, 0, sizeof(g_RealGSMem)); - UpdateVSyncRate(true); + // Null gets flagged as hw. + return (GSCurrentRenderer != GSRendererType::SW); } -static __fi void gsCSRwrite( const tGS_CSR& csr ) +static RenderAPI GetAPIForRenderer(GSRendererType renderer) { - if (csr.RESET) { - GUNIT_WARN("GUNIT_WARN: csr.RESET"); - //Console.Warning( "csr.RESET" ); - //gifUnit.Reset(true); // Don't think gif should be reset... - gifUnit.gsSIGNAL.queued = false; - gifUnit.gsFINISH.gsFINISHFired = true; - gifUnit.gsFINISH.gsFINISHPending = false; - // Privilage registers also reset. - std::memset(g_RealGSMem, 0, sizeof(g_RealGSMem)); - GSIMR.reset(); - CSRreg.Reset(); - MTGS::ResetGS(false); - } - - if(csr.FLUSH) + switch (renderer) { - // Our emulated GS has no FIFO, but if it did, it would flush it here... - //Console.WriteLn("GS_CSR FLUSH GS fifo: %x (CSRr=%x)", value, GSCSRr); + case GSRendererType::OGL: + return RenderAPI::OpenGL; + + case GSRendererType::VK: + return RenderAPI::Vulkan; + +#ifdef _WIN32 + case GSRendererType::DX11: + return RenderAPI::D3D11; + + case GSRendererType::DX12: + return RenderAPI::D3D12; +#endif + +#ifdef __APPLE__ + case GSRendererType::Metal: + return RenderAPI::Metal; +#endif + + // We could end up here if we ever removed a renderer. + default: + return GetAPIForRenderer(GSUtil::GetPreferredRenderer()); } +} - if(csr.SIGNAL) +static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool recreate_window, + GSVSyncMode vsync_mode, bool allow_present_throttle) +{ + const RenderAPI new_api = GetAPIForRenderer(renderer); + switch (new_api) { - const bool resume = CSRreg.SIGNAL; - // SIGNAL : What's not known here is whether or not the SIGID register should be updated - // here or when the IMR is cleared (below). - GUNIT_LOG("csr.SIGNAL"); - if (gifUnit.gsSIGNAL.queued) { - //DevCon.Warning("Firing pending signal"); - GSSIGLBLID.SIGID = (GSSIGLBLID.SIGID & ~gifUnit.gsSIGNAL.data[1]) - | (gifUnit.gsSIGNAL.data[0]&gifUnit.gsSIGNAL.data[1]); +#ifdef _WIN32 + case RenderAPI::D3D11: + g_gs_device = std::make_unique(); + break; + case RenderAPI::D3D12: + g_gs_device = std::make_unique(); + break; +#endif +#ifdef __APPLE__ + case RenderAPI::Metal: + g_gs_device = std::unique_ptr(MakeGSDeviceMTL()); + break; +#endif +#ifdef ENABLE_OPENGL + case RenderAPI::OpenGL: + g_gs_device = std::make_unique(); + break; +#endif - if (!GSIMR.SIGMSK) gsIrq(); - CSRreg.SIGNAL = true; // Just to be sure :p - } - else CSRreg.SIGNAL = false; - gifUnit.gsSIGNAL.queued = false; +#ifdef ENABLE_VULKAN + case RenderAPI::Vulkan: + g_gs_device = std::make_unique(); + break; +#endif - if (resume) - gifUnit.Execute(false, true); // Resume paused transfers + default: + Console.Error("Unsupported render API %s", GSDevice::RenderAPIToString(new_api)); + return false; } - if (csr.FINISH) { - CSRreg.FINISH = false; - gifUnit.gsFINISH.gsFINISHFired = false; //Clear the previously fired FINISH (YS, Indiecar 2005, MGS3) - gifUnit.gsFINISH.gsFINISHPending = false; + bool okay = g_gs_device->Create(vsync_mode, allow_present_throttle); + if (okay) + { + okay = ImGuiManager::Initialize(); + if (!okay) + Console.Error("Failed to initialize ImGuiManager"); + } + else + { + Console.Error("Failed to create GS device"); } - if(csr.HSINT) CSRreg.HSINT = false; - if(csr.VSINT) CSRreg.VSINT = false; - if(csr.EDWINT) CSRreg.EDWINT = false; + + if (!okay) + { + ImGuiManager::Shutdown(clear_state_on_fail); + g_gs_device->Destroy(); + g_gs_device.reset(); + Host::ReleaseRenderWindow(); + return false; + } + + GSConfig.OsdShowGPU = GSConfig.OsdShowGPU && g_gs_device->SetGPUTimingEnabled(true); + + Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", GSDevice::RenderAPIToString(new_api)); + Console.WriteLn(g_gs_device->GetDriverInfo()); + + return true; } -static __fi void IMRwrite(u32 value) +static void CloseGSDevice(bool clear_state) { - GUNIT_LOG("IMRwrite()"); + if (!g_gs_device) + return; - if (CSRreg.GetInterruptMask() & (~value & GSIMR._u32) >> 8) - gsIrq(); + ImGuiManager::Shutdown(clear_state); + g_gs_device->Destroy(); + g_gs_device.reset(); +} - GSIMR._u32 = (value & 0x1f00)|0x6000; +static void GSClampUpscaleMultiplier(Pcsx2Config::GSOptions& config) +{ + const u32 max_upscale_multiplier = GSGetMaxUpscaleMultiplier(g_gs_device->GetMaxTextureSize()); + if (config.UpscaleMultiplier <= static_cast(max_upscale_multiplier)) + { + // Shouldn't happen, but just in case. + if (config.UpscaleMultiplier < 1.0f) + config.UpscaleMultiplier = 1.0f; + return; + } + + Host::AddIconOSDMessage("GSUpscaleMultiplierInvalid", ICON_FA_EXCLAMATION_TRIANGLE, + 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(max_upscale_multiplier); } -__fi void gsWrite8(u32 mem, u8 value) +static bool OpenGSRenderer(GSRendererType renderer, u8* basemem) { - switch (mem) + // Must be done first, initialization routines in GSState use GSIsHardwareRenderer(). + GSCurrentRenderer = renderer; + + GSVertexSW::InitStatic(); + + if (renderer == GSRendererType::Null) + { + g_gs_renderer = std::make_unique(); + } + else if (renderer != GSRendererType::SW) + { + GSClampUpscaleMultiplier(GSConfig); + g_gs_renderer = std::make_unique(); + } + else { - // CSR 8-bit write handlers. - // I'm quite sure these would just write the CSR portion with the other - // bits set to 0 (no action). The previous implementation masked the 8-bit - // write value against the previous CSR write value, but that really doesn't - // make any sense, given that the real hardware's CSR circuit probably has no - // real "memory" where it saves anything. (for example, you can't write to - // and change the GS revision or ID portions -- they're all hard wired.) --air + g_gs_renderer = std::unique_ptr(MULTI_ISA_SELECT(makeGSRendererSW)(GSConfig.SWExtraThreads)); + } - case GS_CSR: // GS_CSR - gsCSRwrite( tGS_CSR((u32)value) ); break; - case GS_CSR + 1: // GS_CSR - gsCSRwrite( tGS_CSR(((u32)value) << 8) ); break; - case GS_CSR + 2: // GS_CSR - gsCSRwrite( tGS_CSR(((u32)value) << 16) ); break; - case GS_CSR + 3: // GS_CSR - gsCSRwrite( tGS_CSR(((u32)value) << 24) ); break; + g_gs_renderer->SetRegsMem(basemem); + g_gs_renderer->ResetPCRTC(); + g_gs_renderer->UpdateRenderFixes(); + g_perfmon.Reset(); + return true; +} - default: - *PS2GS_BASE(mem) = value; - break; +static void CloseGSRenderer() +{ + GSTextureReplacements::Shutdown(); + + if (g_gs_renderer) + { + g_gs_renderer->Destroy(); + g_gs_renderer.reset(); } - GIF_LOG("GS write 8 at %8.8lx with data %8.8lx", mem, value); } -////////////////////////////////////////////////////////////////////////// -// GS Write 16 bit +bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_renderer, + std::optional old_config) +{ + Console.WriteLn("Reopening GS with %s device", recreate_device ? "new" : "existing"); + + g_gs_renderer->Flush(GSState::GSFlushReason::GSREOPEN); + + 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) + { + g_gs_renderer->ReadbackTextureCache(); + } + + std::string capture_filename; + GSVector2i capture_size; + if (GSCapture::IsCapturing()) + { + capture_filename = GSCapture::GetNextCaptureFileName(); + capture_size = GSCapture::GetSize(); + Console.Warning(fmt::format("Restarting video capture to {}.", capture_filename)); + g_gs_renderer->EndCapture(); + } + + u8* basemem = g_gs_renderer->GetRegsMem(); + + freezeData fd = {}; + std::unique_ptr fd_data; + if (recreate_renderer) + { + if (g_gs_renderer->Freeze(&fd, true) != 0) + { + Console.Error("(GSreopen) Failed to get GS freeze size"); + return false; + } + + fd_data = std::make_unique(fd.size); + fd.data = fd_data.get(); + if (g_gs_renderer->Freeze(&fd, false) != 0) + { + Console.Error("(GSreopen) Failed to freeze GS"); + return false; + } + + CloseGSRenderer(); + } + + if (recreate_device) + { + // We need a new render window when changing APIs. + const bool recreate_window = (g_gs_device->GetRenderAPI() != GetAPIForRenderer(GSConfig.Renderer)); + const GSVSyncMode vsync_mode = g_gs_device->GetVSyncMode(); + const bool allow_present_throttle = g_gs_device->IsPresentThrottleAllowed(); + CloseGSDevice(false); + + if (!OpenGSDevice(new_renderer, false, recreate_window, vsync_mode, allow_present_throttle)) + { + Host::AddKeyedOSDMessage("GSReopenFailed", + TRANSLATE_STR("GS", "Failed to reopen, restoring old configuration."), + Host::OSD_CRITICAL_ERROR_DURATION); + + CloseGSDevice(false); -__fi void gsWrite16(u32 mem, u16 value) + if (old_config.has_value()) + GSConfig = *old_config.value(); + + if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window, vsync_mode, allow_present_throttle)) + { + pxFailRel("Failed to reopen GS on old config"); + Host::ReleaseRenderWindow(); + return false; + } + } + } + + if (recreate_renderer) + { + if (!OpenGSRenderer(new_renderer, basemem)) + { + Console.Error("(GSreopen) Failed to create new renderer"); + return false; + } + + if (g_gs_renderer->Defrost(&fd) != 0) + { + Console.Error("(GSreopen) Failed to defrost"); + return false; + } + } + + if (!capture_filename.empty()) + g_gs_renderer->BeginCapture(std::move(capture_filename), capture_size); + + return true; +} + +bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem, + GSVSyncMode vsync_mode, bool allow_present_throttle) { - GIF_LOG("GS write 16 at %8.8lx with data %8.8lx", mem, value); + GSConfig = config; + + if (renderer == GSRendererType::Auto) + renderer = GSUtil::GetPreferredRenderer(); - switch (mem) + bool res = OpenGSDevice(renderer, true, false, vsync_mode, allow_present_throttle); + if (res) { - // See note above about CSR 8 bit writes, and handling them as zero'd bits - // for all but the written parts. + res = OpenGSRenderer(renderer, basemem); + if (!res) + CloseGSDevice(true); + } - case GS_CSR: - gsCSRwrite( tGS_CSR((u32)value) ); - return; // do not write to MTGS memory + if (!res) + { + 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))); + return false; + } - case GS_CSR+2: - gsCSRwrite( tGS_CSR(((u32)value) << 16) ); - return; // do not write to MTGS memory + return true; +} - case GS_IMR: - IMRwrite(value); - return; // do not write to MTGS memory +void GSclose() +{ + if (GSCapture::IsCapturing()) + GSCapture::EndCapture(); + + CloseGSRenderer(); + CloseGSDevice(true); + Host::ReleaseRenderWindow(); +} + +void GSreset(bool hardware_reset) +{ + g_gs_renderer->Reset(hardware_reset); + + // 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); } +} + +void GSgifSoftReset(u32 mask) +{ + g_gs_renderer->SoftReset(mask); +} + +void GSwriteCSR(u32 csr) +{ + g_gs_renderer->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); +} + +void GSReadLocalMemoryUnsync(u8* mem, u32 qwc, u64 BITBLITBUF, u64 TRXPOS, u64 TRXREG) +{ + g_gs_renderer->ReadLocalMemoryUnsync(mem, qwc, GIFRegBITBLTBUF{BITBLITBUF}, GIFRegTRXPOS{TRXPOS}, GIFRegTRXREG{TRXREG}); +} + +void GSgifTransfer(const u8* mem, u32 size) +{ + g_gs_renderer->Transfer<3>(mem, size); +} - *(u16*)PS2GS_BASE(mem) = value; +void GSgifTransfer1(u8* mem, u32 addr) +{ + g_gs_renderer->Transfer<0>(const_cast(mem) + addr, (0x4000 - addr) / 16); } -////////////////////////////////////////////////////////////////////////// -// GS Write 32 bit +void GSgifTransfer2(u8* mem, u32 size) +{ + g_gs_renderer->Transfer<1>(const_cast(mem), size); +} -__fi void gsWrite32(u32 mem, u32 value) +void GSgifTransfer3(u8* mem, u32 size) { - pxAssume( (mem & 3) == 0 ); - GIF_LOG("GS write 32 at %8.8lx with data %8.8lx", mem, value); + g_gs_renderer->Transfer<2>(const_cast(mem), size); +} - switch (mem) +void GSvsync(u32 field, bool registers_written) +{ + // 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()); +} + +int GSfreeze(FreezeAction mode, freezeData* data) +{ + if (mode == FreezeAction::Save) { - case GS_CSR: - gsCSRwrite(tGS_CSR(value)); - return; + return g_gs_renderer->Freeze(data, false); + } + else if (mode == FreezeAction::Size) + { + return g_gs_renderer->Freeze(data, true); + } + 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(); - case GS_IMR: - IMRwrite(value); - return; + // 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(); + + return g_gs_renderer->Defrost(data); } +} - *(u32*)PS2GS_BASE(mem) = value; +void GSQueueSnapshot(const std::string& path, u32 gsdump_frames) +{ + if (g_gs_renderer) + g_gs_renderer->QueueSnapshot(path, gsdump_frames); } -////////////////////////////////////////////////////////////////////////// -// GS Write 64 bit +void GSStopGSDump() +{ + if (g_gs_renderer) + g_gs_renderer->StopGSDump(); +} -void gsWrite64_generic( u32 mem, u64 value ) +bool GSBeginCapture(std::string filename) { - GIF_LOG("GS Write64 at %8.8lx with data %8.8x_%8.8x", mem, (u32)(value >> 32), (u32)value); + 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(); +} - std::memcpy(PS2GS_BASE(mem), &value, sizeof(value)); +void GSPresentCurrentFrame() +{ + g_gs_renderer->PresentCurrentFrame(); } -void gsWrite64_page_00( u32 mem, u64 value ) +void GSThrottlePresentation() { - s_GSRegistersWritten |= (mem == GS_DISPFB1 || mem == GS_DISPFB2 || mem == GS_PMODE); - bool reqUpdate = false; - if (mem == GS_SMODE1 || mem == GS_SMODE2) + if (g_gs_device->GetVSyncMode() == GSVSyncMode::FIFO) { - if (value != *(u64*)PS2GS_BASE(mem)) - reqUpdate = true; + // Let vsync take care of throttling. + return; } - gsWrite64_generic( mem, value ); + g_gs_device->ThrottlePresentation(); +} - if (reqUpdate) - UpdateVSyncRate(false); +void GSGameChanged() +{ + if (GSIsHardwareRenderer()) + GSTextureReplacements::GameChanged(); + + if (!VMManager::HasValidVM() && GSCapture::IsCapturing()) + GSCapture::EndCapture(); } -void gsWrite64_page_01( u32 mem, u64 value ) +bool GSHasDisplayWindow() { - GIF_LOG("GS Write64 at %8.8lx with data %8.8x_%8.8x", mem, (u32)(value >> 32), (u32)value); + pxAssert(g_gs_device); + return (g_gs_device->GetWindowInfo().type != WindowInfo::Type::Surfaceless); +} - switch( mem ) +void GSResizeDisplayWindow(int width, int height, float scale) +{ + g_gs_device->ResizeWindow(width, height, scale); + ImGuiManager::WindowResized(); +} + +void GSUpdateDisplayWindow() +{ + if (!g_gs_device->UpdateWindow()) { - case GS_BUSDIR: + Host::ReportErrorAsync("Error", "Failed to change window after update. The log may contain more information."); + return; + } - gifUnit.stat.DIR = static_cast(value) & 1; - if (gifUnit.stat.DIR) { // Assume will do local->host transfer - gifUnit.stat.OPH = true; // Should we set OPH here? - gifUnit.FlushToMTGS(); // Send any pending GS Primitives to the GS - GUNIT_LOG("Busdir - GS->EE Download"); - } - else { - GUNIT_LOG("Busdir - EE->GS Upload"); - } + ImGuiManager::WindowResized(); +} - gsWrite64_generic( mem, value ); - return; +void GSSetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) +{ + static constexpr std::array(GSVSyncMode::Count)> modes = {{ + "Disabled", + "FIFO", + "Mailbox", + }}; + Console.WriteLnFmt(Color_StrongCyan, "Setting vsync mode: {}{}", modes[static_cast(mode)], + allow_present_throttle ? " (throttle allowed)" : ""); + g_gs_device->SetVSyncMode(mode, allow_present_throttle); +} - case GS_CSR: - gsCSRwrite(tGS_CSR(value)); - return; +bool GSWantsExclusiveFullscreen() +{ + if (!g_gs_device || !g_gs_device->SupportsExclusiveFullscreen()) + return false; - case GS_IMR: - IMRwrite(static_cast(value)); - return; + u32 width, height; + float refresh_rate; + return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate); +} + +std::optional GSGetHostRefreshRate() +{ + if (!g_gs_device) + return std::nullopt; + + 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; +} + +std::vector GSGetAdapterInfo(GSRendererType renderer) +{ + std::vector ret; + switch (renderer) + { +#ifdef _WIN32 + case GSRendererType::DX11: + case GSRendererType::DX12: + { + auto factory = D3D::CreateFactory(false); + if (factory) + ret = D3D::GetAdapterInfo(factory.get()); + } + break; +#endif + +#ifdef ENABLE_VULKAN + case GSRendererType::VK: + { + ret = GSDeviceVK::GetAdapterInfo(); + } + break; +#endif + +#ifdef __APPLE__ + case GSRendererType::Metal: + { + ret = GetMetalAdapterList(); + } + break; +#endif + + default: + break; } - gsWrite64_generic( mem, value ); + return ret; } -////////////////////////////////////////////////////////////////////////// -// GS Write 128 bit +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); +} -void TAKES_R128 gsWrite128_page_00( u32 mem, r128 value ) +GSVideoMode GSgetDisplayMode() { - gsWrite128_generic( mem, value ); + GSRenderer* gs = g_gs_renderer.get(); + + return gs->GetVideoMode(); } -void TAKES_R128 gsWrite128_page_01( u32 mem, r128 value ) +void GSgetInternalResolution(int* width, int* height) { - switch( mem ) + GSRenderer* gs = g_gs_renderer.get(); + if (!gs) { - case GS_CSR: - gsCSRwrite(r128_to_u32(value)); + *width = 0; + *height = 0; return; + } - case GS_IMR: - IMRwrite(r128_to_u32(value)); + const GSVector2i res(gs->GetInternalResolution()); + *width = res.x; + *height = res.y; +} + +void GSgetStats(SmallStringBase& info) +{ + GSPerfMon& pm = g_perfmon; + const char* api_name = GSDevice::RenderAPIToString(g_gs_device->GetRenderAPI()); + if (GSCurrentRenderer == GSRendererType::SW) + { + const double fps = GetVerticalFrequency(); + const double fillrate = pm.Get(GSPerfMon::Fillrate); + info.format("{} SW | {} S | {} P | {} D | {:.2f} U | {:.2f} D | {:.2f} mpps", + api_name, + (int)pm.Get(GSPerfMon::SyncPoint), + (int)pm.Get(GSPerfMon::Prim), + (int)pm.Get(GSPerfMon::Draw), + pm.Get(GSPerfMon::Swizzle) / 1024, + pm.Get(GSPerfMon::Unswizzle) / 1024, + fps * fillrate / (1024 * 1024)); + } + else if (GSCurrentRenderer == GSRendererType::Null) + { + fmt::format_to(std::back_inserter(info), "{} Null", api_name); + } + else + { + info.format("{} HW | {} P | {} D | {} DC | {} B | {} 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))); + } +} + +void GSgetMemoryStats(SmallStringBase& info) +{ + if (!g_texture_cache) return; + + const u64 targets = g_texture_cache->GetTargetMemoryUsage(); + const u64 sources = g_texture_cache->GetSourceMemoryUsage(); + const u64 hashcache = g_texture_cache->GetHashCacheMemoryUsage(); + const u64 pool = g_gs_device->GetPoolMemoryUsage(); + const u64 total = targets + sources + hashcache + pool; + + if (GSConfig.TexturePreloading == TexturePreloadingLevel::Full) + { + fmt::format_to(std::back_inserter(info), "VRAM: {} MB | T: {} MB | S: {} MB | H: {} MB | P: {} MB", + (int)std::ceil(total / 1048576.0f), + (int)std::ceil(targets / 1048576.0f), + (int)std::ceil(sources / 1048576.0f), + (int)std::ceil(hashcache / 1048576.0f), + (int)std::ceil(pool / 1048576.0f)); } + else + { + fmt::format_to(std::back_inserter(info), "VRAM: {} MB | T: {} MB | S: {} MB | P: {} MB", + (int)std::ceil(total / 1048576.0f), + (int)std::ceil(targets / 1048576.0f), + (int)std::ceil(sources / 1048576.0f), + (int)std::ceil(pool / 1048576.0f)); + } +} + +void GSgetTitleStats(std::string& info) +{ + static constexpr const char* deinterlace_modes[] = { + "Automatic", "None", "Weave tff", "Weave bff", "Bob tff", "Bob bff", "Blend tff", "Blend bff", "Adaptive tff", "Adaptive bff"}; - gsWrite128_generic( mem, value ); + const char* api_name = GSDevice::RenderAPIToString(g_gs_device->GetRenderAPI()); + const char* hw_sw_name = (GSCurrentRenderer == GSRendererType::Null) ? " Null" : (GSIsHardwareRenderer() ? " HW" : " SW"); + const char* deinterlace_mode = deinterlace_modes[static_cast(GSConfig.InterlaceMode)]; + + 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); } -void TAKES_R128 gsWrite128_generic( u32 mem, r128 value ) +void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config) { - alignas(16) const u128 uvalue = r128_to_u128(value); - GIF_LOG("GS Write128 at %8.8lx with data %8.8x_%8.8x_%8.8x_%8.8x", mem, - uvalue._u32[3], uvalue._u32[2], uvalue._u32[1], uvalue._u32[0]); + Pcsx2Config::GSOptions old_config(std::move(GSConfig)); + GSConfig = new_config; + if (!g_gs_renderer) + return; + + // Handle OSD scale changes by pushing a window resize through. + if (new_config.OsdScale != old_config.OsdScale) + ImGuiManager::RequestScaleUpdate(); + + // Options which need a full teardown/recreate. + if (!GSConfig.RestartOptionsAreEqual(old_config)) + { + if (!GSreopen(true, true, GSConfig.Renderer, &old_config)) + pxFailRel("Failed to do full GS reopen"); + return; + } + + // Ensure upscale multiplier is in range. + GSClampUpscaleMultiplier(GSConfig); + + // Options which aren't using the global struct yet, so we need to recreate all GS objects. + if (GSConfig.SWExtraThreads != old_config.SWExtraThreads || + GSConfig.SWExtraThreadsHeight != old_config.SWExtraThreadsHeight) + { + if (!GSreopen(false, true, GSConfig.Renderer, &old_config)) + pxFailRel("Failed to do quick GS reopen"); + + return; + } + + if (GSConfig.UserHacks_DisableRenderFixes != old_config.UserHacks_DisableRenderFixes || + GSConfig.UpscaleMultiplier != old_config.UpscaleMultiplier || + GSConfig.GetSkipCountFunctionId != old_config.GetSkipCountFunctionId || + GSConfig.BeforeDrawFunctionId != old_config.BeforeDrawFunctionId || + GSConfig.MoveHandlerFunctionId != old_config.MoveHandlerFunctionId) + { + g_gs_renderer->UpdateRenderFixes(); + } + + // renderer-specific options (e.g. auto flush, TC offset) + g_gs_renderer->UpdateSettings(old_config); + + // reload texture cache when trilinear filtering or TC options change + if ( + (GSIsHardwareRenderer() && GSConfig.HWMipmapMode != old_config.HWMipmapMode) || + GSConfig.TexturePreloading != old_config.TexturePreloading || + GSConfig.TriFilter != old_config.TriFilter || + 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 || + GSConfig.UserHacks_TextureInsideRt != old_config.UserHacks_TextureInsideRt || + GSConfig.UserHacks_CPUSpriteRenderBW != old_config.UserHacks_CPUSpriteRenderBW || + GSConfig.UserHacks_CPUCLUTRender != old_config.UserHacks_CPUCLUTRender || + GSConfig.UserHacks_GPUTargetCLUTMode != old_config.UserHacks_GPUTargetCLUTMode) + { + if (GSConfig.UserHacks_ReadTCOnClose) + g_gs_renderer->ReadbackTextureCache(); + g_gs_renderer->PurgeTextureCache(true, true, true); + g_gs_device->ClearCurrent(); + g_gs_device->PurgePool(); + } + + // 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(); + + // texture dumping/replacement options + if (GSIsHardwareRenderer()) + GSTextureReplacements::UpdateConfig(old_config); - r128_store(PS2GS_BASE(mem), value); + // 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) + { + g_gs_renderer->PurgeTextureCache(true, false, true); + } + + if (GSConfig.OsdShowGPU != old_config.OsdShowGPU) + { + if (!g_gs_device->SetGPUTimingEnabled(GSConfig.OsdShowGPU)) + GSConfig.OsdShowGPU = false; + } } -__fi u8 gsRead8(u32 mem) +void GSSetSoftwareRendering(bool software_renderer, GSInterlaceMode new_interlace) { - GIF_LOG("GS read 8 from %8.8lx value: %8.8lx", mem, *(u8*)PS2GS_BASE(mem)); + if (!g_gs_renderer) + return; + + GSConfig.InterlaceMode = new_interlace; - switch (mem & ~0xF) + if (!GSIsHardwareRenderer() != software_renderer) { - case GS_SIGLBLID: - return *(u8*)PS2GS_BASE(mem); - default: // Only SIGLBLID and CSR are readable, everything else mirrors CSR - return *(u8*)PS2GS_BASE(GS_CSR + (mem & 0xF)); + // 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)); + if (!GSreopen(false, true, renderer, std::nullopt)) + pxFailRel("Failed to reopen GS for renderer switch."); } } -__fi u16 gsRead16(u32 mem) +bool GSSaveSnapshotToMemory(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders, + u32* width, u32* height, std::vector* pixels) { - GIF_LOG("GS read 16 from %8.8lx value: %8.8lx", mem, *(u16*)PS2GS_BASE(mem)); - switch (mem & ~0xF) + if (!g_gs_renderer) + return false; + + return g_gs_renderer->SaveSnapshotToMemory(window_width, window_height, apply_aspect, crop_borders, + width, height, pixels); +} + +#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) { - case GS_SIGLBLID: - return *(u16*)PS2GS_BASE(mem); - default: // Only SIGLBLID and CSR are readable, everything else mirrors CSR - return *(u16*)PS2GS_BASE(GS_CSR + (mem & 0x7)); + 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(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)) || + !MapViewOfFile3(s_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; + + 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); + return base; + } + + VirtualFreeEx(GetCurrentProcess(), base, 0, MEM_RELEASE); + } + + Console.Error("Failed to reserve VA space of size %zu. WIN API ERROR:%u", size, GetLastError()); + CloseHandle(s_fh); + s_fh = NULL; + return nullptr; } -__fi u32 gsRead32(u32 mem) +void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat) { - GIF_LOG("GS read 32 from %8.8lx value: %8.8lx", mem, *(u32*)PS2GS_BASE(mem)); + pxAssertRel(s_fh, "Has a file mapping"); - switch (mem & ~0xF) + for (size_t i = 0; i < repeat; i++) { - case GS_SIGLBLID: - return *(u32*)PS2GS_BASE(mem); - default: // Only SIGLBLID and CSR are readable, everything else mirrors CSR - return *(u32*)PS2GS_BASE(GS_CSR + (mem & 0xC)); + u8* addr = (u8*)ptr + i * size; + UnmapViewOfFile2(GetCurrentProcess(), addr, MEM_PRESERVE_PLACEHOLDER); } + + VirtualFreeEx(GetCurrentProcess(), ptr, 0, MEM_RELEASE); + s_fh = NULL; } -__fi u64 gsRead64(u32 mem) +#else + +#include +#include +#include +#include + +static int s_shm_fd = -1; + +void* GSAllocateWrappedMemory(size_t size, size_t repeat) { - // fixme - PS2GS_BASE(mem+4) = (g_RealGSMem+(mem + 4 & 0x13ff)) - GIF_LOG("GS read 64 from %8.8lx value: %8.8lx_%8.8lx", mem, *(u32*)PS2GS_BASE(mem+4), *(u32*)PS2GS_BASE(mem) ); + pxAssert(s_shm_fd == -1); - switch (mem & ~0xF) + const char* file_name = "/GS.mem"; + s_shm_fd = shm_open(file_name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (s_shm_fd != -1) { - case GS_SIGLBLID: - return *(u64*)PS2GS_BASE(mem); - default: // Only SIGLBLID and CSR are readable, everything else mirrors CSR - return *(u64*)PS2GS_BASE(GS_CSR + (mem & 0x8)); + shm_unlink(file_name); // file is deleted but descriptor is still open } + else + { + fprintf(stderr, "Failed to open %s due to %s\n", file_name, strerror(errno)); + return nullptr; + } + + if (ftruncate(s_shm_fd, repeat * size) < 0) + fprintf(stderr, "Failed to reserve memory due to %s\n", strerror(errno)); + + void* fifo = mmap(nullptr, size * repeat, PROT_READ | PROT_WRITE, MAP_SHARED, s_shm_fd, 0); + + 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); + if (next != base) + fprintf(stderr, "Fail to mmap contiguous segment\n"); + } + + return fifo; } -__fi u128 gsNonMirroredRead(u32 mem) +void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat) { - return *(u128*)PS2GS_BASE(mem); + pxAssert(s_shm_fd >= 0); + + if (s_shm_fd < 0) + return; + + munmap(ptr, size * repeat); + + close(s_shm_fd); + s_shm_fd = -1; } -void gsIrq() { - hwIntcIrq(INTC_GS); +#endif + +std::pair GSGetRGBA8AlphaMinMax(const void* data, u32 width, u32 height, u32 stride) +{ + GSVector4i minc = GSVector4i::xffffffff(); + GSVector4i maxc = GSVector4i::zero(); + + const u8* ptr = static_cast(data); + if ((width % 4) == 0) + { + for (u32 r = 0; r < height; r++) + { + const u8* rptr = ptr; + for (u32 c = 0; c < width; c += 4) + { + const GSVector4i v = GSVector4i::load(rptr); + 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)}, + {GSVector4i::cxpr(0xFFFFFFFF, 0xFFFFFFFF, 0, 0), GSVector4i::cxpr(0, 0, 0xFFFFFFFF, 0xFFFFFFFF)}, + {GSVector4i::cxpr(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0), GSVector4i::cxpr(0, 0, 0, 0xFFFFFFFF)}, + }; + 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]; + + for (u32 r = 0; r < height; r++) + { + const u8* rptr = ptr; + for (u32 c = 0; c < aligned_width; c += 4) + { + const GSVector4i v = GSVector4i::load(rptr); + rptr += sizeof(GSVector4i); + minc = minc.min_u32(v); + maxc = maxc.max_u32(v); + } + + 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); + } + + minc = minc.min_u32(v | last_mask_or); + maxc = maxc.max_u32(v & last_mask_and); + + ptr += stride; + } + } + + return std::make_pair(static_cast(minc.minv_u32() >> 24), + static_cast(maxc.maxv_u32() >> 24)); } -//These are done at VSync Start. Drawing is done when VSync is off, then output the screen when Vsync is on -//The GS needs to be told at the start of a vsync else it loses half of its picture (could be responsible for some halfscreen issues) -//We got away with it before i think due to our awful GS timing, but now we have it right (ish) -void gsPostVsyncStart() +static void HotkeyAdjustUpscaleMultiplier(s32 delta) { - //gifUnit.FlushToMTGS(); // Needed for some (broken?) homebrew game loaders + const u32 new_multiplier = static_cast(std::clamp(static_cast(EmuConfig.GS.UpscaleMultiplier) + delta, 1, 8)); + Host::AddKeyedOSDMessage("UpscaleMultiplierChanged", + fmt::format(TRANSLATE_FS("GS", "Upscale multiplier set to {}x."), new_multiplier), Host::OSD_QUICK_DURATION); + EmuConfig.GS.UpscaleMultiplier = new_multiplier; - const bool registers_written = s_GSRegistersWritten; - s_GSRegistersWritten = false; - MTGS::PostVsyncStart(registers_written); + // this is pretty slow. we only really need to flush the TC and recompile shaders. + // TODO(Stenzek): Make it faster at some point in the future. + MTGS::ApplySettings(); } -bool SaveStateBase::gsFreeze() +static void HotkeyToggleOSD() { - FreezeMem(PS2MEM_GS, 0x2000); - Freeze(gsVideoMode); - return IsOkay(); + GSConfig.OsdShowMessages ^= EmuConfig.GS.OsdShowMessages; + GSConfig.OsdShowSpeed ^= EmuConfig.GS.OsdShowSpeed; + GSConfig.OsdShowFPS ^= EmuConfig.GS.OsdShowFPS; + GSConfig.OsdShowCPU ^= EmuConfig.GS.OsdShowCPU; + GSConfig.OsdShowGPU ^= EmuConfig.GS.OsdShowGPU; + GSConfig.OsdShowResolution ^= EmuConfig.GS.OsdShowResolution; + GSConfig.OsdShowGSStats ^= EmuConfig.GS.OsdShowGSStats; + GSConfig.OsdShowIndicators ^= EmuConfig.GS.OsdShowIndicators; + GSConfig.OsdShowSettings ^= EmuConfig.GS.OsdShowSettings; + GSConfig.OsdShowInputs ^= EmuConfig.GS.OsdShowInputs; + GSConfig.OsdShowFrameTimes ^= EmuConfig.GS.OsdShowFrameTimes; } +BEGIN_HOTKEY_LIST(g_gs_hotkeys){"Screenshot", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Save Screenshot"), + [](s32 pressed) { + if (!pressed) + { + MTGS::RunOnGSThread([]() { GSQueueSnapshot(std::string(), 0); }); + } + }}, + {"ToggleVideoCapture", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Video Capture"), + [](s32 pressed) { + if (!pressed) + { + if (GSCapture::IsCapturing()) + { + MTGS::RunOnGSThread([]() { g_gs_renderer->EndCapture(); }); + MTGS::WaitGS(false, false, false); + return; + } + + MTGS::RunOnGSThread([]() { + std::string filename(fmt::format("{}.{}", GSGetBaseVideoFilename(), GSConfig.CaptureContainer)); + g_gs_renderer->BeginCapture(std::move(filename)); + }); + + // Sync GS thread. We want to start adding audio at the same time as video. + MTGS::WaitGS(false, false, false); + } + }}, + {"GSDumpSingleFrame", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Save Single Frame GS Dump"), + [](s32 pressed) { + if (!pressed) + { + MTGS::RunOnGSThread([]() { GSQueueSnapshot(std::string(), 1); }); + } + }}, + {"GSDumpMultiFrame", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Save Multi Frame GS Dump"), + [](s32 pressed) { + MTGS::RunOnGSThread([pressed]() { + if (pressed > 0) + GSQueueSnapshot(std::string(), std::numeric_limits::max()); + else + GSStopGSDump(); + }); + }}, + {"ToggleSoftwareRendering", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Toggle Software Rendering"), + [](s32 pressed) { + if (!pressed) + MTGS::ToggleSoftwareRendering(); + }}, + {"IncreaseUpscaleMultiplier", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Increase Upscale Multiplier"), + [](s32 pressed) { + if (!pressed) + HotkeyAdjustUpscaleMultiplier(1); + }}, + {"DecreaseUpscaleMultiplier", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Decrease Upscale Multiplier"), + [](s32 pressed) { + if (!pressed) + HotkeyAdjustUpscaleMultiplier(-1); + }}, + {"ToggleOSD", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle On-Screen Display"), + [](s32 pressed) { + if (!pressed) + HotkeyToggleOSD(); + }}, + {"CycleAspectRatio", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle Aspect Ratio"), + [](s32 pressed) { + if (pressed) + return; + + // technically this races, but the worst that'll happen is one frame uses the old AR. + EmuConfig.CurrentAspectRatio = static_cast( + (static_cast(EmuConfig.CurrentAspectRatio) + 1) % static_cast(AspectRatioType::MaxCount)); + Host::AddKeyedOSDMessage("CycleAspectRatio", + fmt::format(TRANSLATE_FS("Hotkeys", "Aspect ratio set to '{}'."), + Pcsx2Config::GSOptions::AspectRatioNames[static_cast(EmuConfig.CurrentAspectRatio)]), + Host::OSD_QUICK_DURATION); + }}, + {"ToggleMipmapMode", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Hardware Mipmapping"), + [](s32 pressed) { + if (!pressed) + { + EmuConfig.GS.HWMipmapMode = + (EmuConfig.GS.HWMipmapMode >= GSHWMipmapMode::Enabled) ? GSHWMipmapMode::Disabled : GSHWMipmapMode::Enabled; + Host::AddKeyedOSDMessage("ToggleMipmapMode", + (EmuConfig.GS.HWMipmapMode >= GSHWMipmapMode::Enabled) ? + TRANSLATE_STR("Hotkeys", "Hardware mipmapping is now enabled.") : + TRANSLATE_STR("Hotkeys", "Hardware mipmapping is now disabled."), + Host::OSD_INFO_DURATION); + MTGS::ApplySettings(); + } + }}, + {"CycleInterlaceMode", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Cycle Deinterlace Mode"), + [](s32 pressed) { + if (pressed) + return; + + static constexpr std::array(GSInterlaceMode::Count)> option_names = {{ + "Automatic", + "Off", + "Weave (Top Field First)", + "Weave (Bottom Field First)", + "Bob (Top Field First)", + "Bob (Bottom Field First)", + "Blend (Top Field First)", + "Blend (Bottom Field First)", + "Adaptive (Top Field First)", + "Adaptive (Bottom Field First)", + }}; + + const GSInterlaceMode new_mode = static_cast( + (static_cast(EmuConfig.GS.InterlaceMode) + 1) % static_cast(GSInterlaceMode::Count)); + Host::AddKeyedOSDMessage("CycleInterlaceMode", + fmt::format( + TRANSLATE_FS("Hotkeys", "Deinterlace mode set to '{}'."), option_names[static_cast(new_mode)]), + Host::OSD_QUICK_DURATION); + EmuConfig.GS.InterlaceMode = new_mode; + + MTGS::RunOnGSThread([new_mode]() { GSConfig.InterlaceMode = new_mode; }); + }}, + {"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); + MTGS::ApplySettings(); + } + }}, + {"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); + MTGS::ApplySettings(); + } + }}, + {"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); + MTGS::RunOnGSThread([]() { + if (!g_gs_renderer) + return; + + GSTextureReplacements::ReloadReplacementMap(); + g_gs_renderer->PurgeTextureCache(true, false, true); + }); + } + } + }}, + END_HOTKEY_LIST() diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index e9f852b7dd24f0..453a1eb716d070 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -16,7 +16,7 @@ GSRendererHW::GSRendererHW() : GSRenderer() { MULTI_ISA_SELECT(GSRendererHWPopulateFunctions)(*this); - m_mipmap = GSConfig.HWMipmap; + m_mipmap = (GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled); SetTCOffset(); pxAssert(!g_texture_cache); @@ -88,7 +88,7 @@ void GSRendererHW::Reset(bool hardware_reset) void GSRendererHW::UpdateSettings(const Pcsx2Config::GSOptions& old_config) { GSRenderer::UpdateSettings(old_config); - m_mipmap = GSConfig.HWMipmap; + m_mipmap = (GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled); SetTCOffset(); } @@ -2398,6 +2398,7 @@ void GSRendererHW::Draw() GIFRegTEX0 TEX0 = {}; GSTextureCache::Source* src = nullptr; TextureMinMaxResult tmm; + bool mipmap_active = false; // Disable texture mapping if the blend is black and using alpha from vertex. if (m_process_texture) @@ -2407,7 +2408,8 @@ void GSRendererHW::Draw() m_lod = GSVector2i(0, 0); // Code from the SW renderer - if (IsMipMapActive()) + mipmap_active = IsMipMapActive(); + if (mipmap_active) { const int interpolation = (context->TEX1.MMIN & 1) + 1; // 1: round, 2: tri @@ -2464,24 +2466,42 @@ void GSRendererHW::Draw() m_lod.x = std::min(m_lod.x, mxl); m_lod.y = std::min(m_lod.y, mxl); - TEX0 = (m_lod.x == 0) ? m_cached_ctx.TEX0 : GetTex0Layer(m_lod.x); + if (GSConfig.HWMipmapMode < GSHWMipmapMode::AllLevels) + { + TEX0 = (m_lod.x == 0) ? m_cached_ctx.TEX0 : GetTex0Layer(m_lod.x); - // upload the full chain (with offset) for the hash cache, in case some other texture uses more levels - // for basic mipmapping, we can get away with just doing the base image, since all the mips get generated anyway. - hash_lod_range = GSVector2i(m_lod.x, GSConfig.HWMipmap ? mxl : m_lod.x); + // upload the full chain (with offset) for the hash cache, in case some other texture uses more levels + // for basic mipmapping, we can get away with just doing the base image, since all the mips get generated anyway. + hash_lod_range = GSVector2i(m_lod.x, mxl); - MIP_CLAMP.MINU >>= m_lod.x; - MIP_CLAMP.MINV >>= m_lod.x; - MIP_CLAMP.MAXU >>= m_lod.x; - MIP_CLAMP.MAXV >>= m_lod.x; + MIP_CLAMP.MINU >>= m_lod.x; + MIP_CLAMP.MINV >>= m_lod.x; + MIP_CLAMP.MAXU >>= m_lod.x; + MIP_CLAMP.MAXV >>= m_lod.x; - for (int i = 0; i < m_lod.x; i++) + for (int i = 0; i < m_lod.x; i++) + { + m_vt.m_min.t *= 0.5f; + m_vt.m_max.t *= 0.5f; + } + } + else { m_vt.m_min.t *= 0.5f; m_vt.m_max.t *= 0.5f; + hash_lod_range = GSVector2i(0, mxl); + TEX0 = m_cached_ctx.TEX0; } - GL_CACHE("Mipmap LOD %d %d (%f %f) new size %dx%d (K %d L %u)", m_lod.x, m_lod.y, m_vt.m_lod.x, m_vt.m_lod.y, 1 << TEX0.TW, 1 << TEX0.TH, m_context->TEX1.K, m_context->TEX1.L); + GL_CACHE("Mipmap LOD %d %d (%f %f) new size %dx%d (K %d L %u)", m_lod.x, m_lod.y, m_vt.m_lod.x, m_vt.m_lod.y, + (1 << m_cached_ctx.TEX0.TW) >> m_lod.x, (1 << m_cached_ctx.TEX0.TH) >> m_lod.x, m_context->TEX1.K, m_context->TEX1.L); + } + else if (GSConfig.HWMipmapMode >= GSHWMipmapMode::AllLevels && m_context->TEX1.MXL > 0 && !m_context->TEX1.LCM) + { + mipmap_active = true; + hash_lod_range = GSVector2i(0, std::min(static_cast(m_context->TEX1.MXL), 6)); + TEX0 = m_cached_ctx.TEX0; + GL_CACHE("Looking up all %d texture LODs", hash_lod_range.y); } else { @@ -2563,7 +2583,7 @@ void GSRendererHW::Draw() else { src = tex_psm.depth ? g_texture_cache->LookupDepthSource(true, TEX0, env.TEXA, MIP_CLAMP, tmm.coverage, possible_shuffle, m_vt.IsLinear(), m_cached_ctx.FRAME.Block(), req_color, req_alpha) : - g_texture_cache->LookupSource(true, TEX0, env.TEXA, MIP_CLAMP, tmm.coverage, (GSConfig.HWMipmap || GSConfig.TriFilter == TriFiltering::Forced) ? &hash_lod_range : nullptr, + g_texture_cache->LookupSource(true, TEX0, env.TEXA, MIP_CLAMP, tmm.coverage, (GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled || GSConfig.TriFilter == TriFiltering::Forced) ? &hash_lod_range : nullptr, possible_shuffle, m_vt.IsLinear(), m_cached_ctx.FRAME.Block(), req_color, req_alpha); if (!src) [[unlikely]] @@ -2958,7 +2978,7 @@ void GSRendererHW::Draw() } // Round 2 - if (IsMipMapActive() && GSConfig.HWMipmap && !tex_psm.depth && !src->m_from_hash_cache) + if (mipmap_active && !tex_psm.depth && !src->m_from_hash_cache) { // Upload remaining texture layers const GSVector4 tmin = m_vt.m_min.t; @@ -4845,7 +4865,7 @@ __ri void GSRendererHW::EmulateTextureSampler(const GSTextureCache::Target* rt, const bool shader_emulated_sampler = tex->m_palette || (tex->m_target && !m_conf.ps.shuffle && cpsm.fmt != 0) || complex_wms_wmt || psm.depth || target_region; const bool can_trilinear = !tex->m_palette && !tex->m_target && !m_conf.ps.shuffle; - const bool trilinear_manual = need_mipmap && GSConfig.HWMipmap; + bool trilinear_manual = need_mipmap && GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled; bool bilinear = m_vt.IsLinear(); int trilinear = 0; @@ -4860,7 +4880,7 @@ __ri void GSRendererHW::EmulateTextureSampler(const GSTextureCache::Target* rt, if (can_trilinear) { trilinear = static_cast(GS_MIN_FILTER::Linear_Mipmap_Linear); - trilinear_auto = !tex->m_target && (!need_mipmap || !GSConfig.HWMipmap); + trilinear_auto = !tex->m_target && (!need_mipmap || GSConfig.HWMipmapMode == GSHWMipmapMode::Disabled); } } break; @@ -4869,10 +4889,10 @@ __ri void GSRendererHW::EmulateTextureSampler(const GSTextureCache::Target* rt, case TriFiltering::Automatic: { // Can only use PS2 trilinear when mipmapping is enabled. - if (need_mipmap && GSConfig.HWMipmap && can_trilinear) + if (need_mipmap && GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled && can_trilinear) { trilinear = m_context->TEX1.MMIN; - trilinear_auto = !tex->m_target && !GSConfig.HWMipmap; + trilinear_auto = !tex->m_target && GSConfig.HWMipmapMode == GSHWMipmapMode::Disabled; } } break; @@ -4881,7 +4901,14 @@ __ri void GSRendererHW::EmulateTextureSampler(const GSTextureCache::Target* rt, default: break; } - + + if (GSConfig.HWMipmapMode >= GSHWMipmapMode::Unclamped && !shader_emulated_sampler && + m_context->TEX1.MXL > 0 && m_context->TEX1.MMIN >= 2 && m_context->TEX1.MMIN <= 5 && !m_context->TEX1.LCM) + { + trilinear = static_cast(m_vt.IsLinear() ? GS_MIN_FILTER::Linear_Mipmap_Linear : GS_MIN_FILTER::Nearest_Mipmap_Linear); + trilinear_manual = false; + } + // 1 and 0 are equivalent m_conf.ps.wms = (wms & 2 || target_region) ? wms : 0; m_conf.ps.wmt = (wmt & 2 || target_region) ? wmt : 0; @@ -5081,7 +5108,9 @@ __ri void GSRendererHW::EmulateTextureSampler(const GSTextureCache::Target* rt, { m_conf.cb_ps.LODParams.x = static_cast(m_context->TEX1.K) / 16.0f; m_conf.cb_ps.LODParams.y = static_cast(1 << m_context->TEX1.L); - m_conf.cb_ps.LODParams.z = static_cast(m_lod.x); // Offset because first layer is m_lod, dunno if we can do better + m_conf.cb_ps.LODParams.z = (GSConfig.HWMipmapMode >= GSHWMipmapMode::Unclamped) ? + std::log2(rt ? rt->GetScale() : ds->GetScale()) : + static_cast((GSConfig.HWMipmapMode == GSHWMipmapMode::AllLevels) ? 0.0f : m_lod.x); m_conf.cb_ps.LODParams.w = static_cast(m_lod.y); m_conf.ps.manual_lod = 1; } diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp index 216b41277d260d..0ea4be81f0b89d 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp @@ -4433,7 +4433,9 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con { // lod won't contain the full range when using basic mipmapping, only that // which is hashed, so we just allocate the full thing. - tlevels = GSConfig.HWMipmap ? std::min(lod->y - lod->x + 1, GSDevice::GetMipmapLevelsForSize(tw, th)) : -1; + tlevels = (GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled) ? + std::min(lod->y - lod->x + 1, GSDevice::GetMipmapLevelsForSize(tw, th)) : + -1; src->m_lod = *lod; } @@ -5376,7 +5378,10 @@ GSTextureCache::HashCacheEntry* GSTextureCache::LookupHashCache(const GIFRegTEX0 // expand/upload texture const int tw = region.HasX() ? region.GetWidth() : (1 << TEX0.TW); const int th = region.HasY() ? region.GetHeight() : (1 << TEX0.TH); - const int tlevels = lod ? (GSConfig.HWMipmap ? std::min(lod->y - lod->x + 1, GSDevice::GetMipmapLevelsForSize(tw, th)) : -1) : 1; + const int tlevels = lod ? ((GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled) ? + std::min(lod->y - lod->x + 1, GSDevice::GetMipmapLevelsForSize(tw, th)) : + -1) : + 1; GSTexture* tex = g_gs_device->CreateTexture(tw, th, tlevels, paltex ? GSTexture::Format::UNorm8 : GSTexture::Format::Color); if (!tex) { diff --git a/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp b/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp index 79616d6ffd87f5..8b86270e39bde0 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp @@ -661,7 +661,7 @@ void GSTextureReplacements::PrecacheReplacementTextures() // predict whether the requests will come with mipmaps // TODO: This will be wrong for hw mipmap games like Jak. - const bool mipmap = GSConfig.HWMipmap || GSConfig.TriFilter == TriFiltering::Forced; + const bool mipmap = (GSConfig.HWMipmapMode >= GSHWMipmapMode::Enabled || GSConfig.TriFilter == TriFiltering::Forced); // pretty simple, just go through the filenames and if any aren't cached, cache them for (const auto& it : s_replacement_texture_filenames) diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp index b1598593f87a1c..4688e4b58d60b3 100644 --- a/pcsx2/GameDatabase.cpp +++ b/pcsx2/GameDatabase.cpp @@ -622,7 +622,7 @@ bool GameDatabaseSchema::GameEntry::configMatchesHWFix(const Pcsx2Config::GSOpti return (static_cast(config.PCRTCOverscan) == value); case GSHWFixId::Mipmap: - return (static_cast(config.HWMipmap) == value); + return (static_cast(config.HWMipmapMode) == value); case GSHWFixId::TrilinearFiltering: return (config.TriFilter == TriFiltering::Automatic || static_cast(config.TriFilter) == value); @@ -780,8 +780,11 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& break; case GSHWFixId::Mipmap: - config.HWMipmap = (value > 0); - break; + { + if (value >= 0 && value < static_cast(GSHWMipmapMode::MaxCount)) + config.HWMipmapMode = static_cast(value > 0); + } + break; case GSHWFixId::TrilinearFiltering: { diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index a319d5b9d4625f..603a366dc20c08 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -3521,6 +3521,12 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad "11", "12", }; + static constexpr const char* s_mipmapping_options[] = { + FSUI_NSTR("Disabled"), + FSUI_NSTR("Enabled"), + FSUI_NSTR("All Levels"), + FSUI_NSTR("Unclamped"), + }; static constexpr const char* s_bilinear_options[] = { FSUI_NSTR("Nearest"), FSUI_NSTR("Bilinear (Forced)"), @@ -3654,6 +3660,9 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad DrawStringListSetting(bsi, FSUI_CSTR("Internal Resolution"), FSUI_CSTR("Multiplies the render resolution by the specified factor (upscaling)."), "EmuCore/GS", "upscale_multiplier", "1.000000", s_resolution_options, s_resolution_values, std::size(s_resolution_options), true); + DrawIntListSetting(bsi, FSUI_CSTR("Mipmapping"), + FSUI_CSTR("Enables emulation of the GS's texture mipmapping."), "EmuCore/GS", "hw_mipmap_mode", + static_cast(GSHWMipmapMode::Enabled), s_mipmapping_options, std::size(s_mipmapping_options), true); DrawIntListSetting(bsi, FSUI_CSTR("Bilinear Filtering"), FSUI_CSTR("Selects where bilinear filtering is utilized when rendering textures."), "EmuCore/GS", "filter", static_cast(BiFiltering::PS2), s_bilinear_options, std::size(s_bilinear_options), true); @@ -3673,8 +3682,6 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad "Uploads full textures to the GPU on use, rather than only the utilized regions. Can improve performance in some games."), "EmuCore/GS", "texture_preloading", static_cast(TexturePreloadingLevel::Off), s_preloading_options, std::size(s_preloading_options), true); - DrawToggleSetting( - bsi, FSUI_CSTR("Mipmapping"), FSUI_CSTR("Enables emulation of the GS's texture mipmapping."), "EmuCore/GS", "hw_mipmap", true); } else { @@ -7006,6 +7013,8 @@ TRANSLATE_NOOP("FullscreenUI", "Enables internal Anti-Blur hacks. Less accurate TRANSLATE_NOOP("FullscreenUI", "Rendering"); TRANSLATE_NOOP("FullscreenUI", "Internal Resolution"); TRANSLATE_NOOP("FullscreenUI", "Multiplies the render resolution by the specified factor (upscaling)."); +TRANSLATE_NOOP("FullscreenUI", "Mipmapping"); +TRANSLATE_NOOP("FullscreenUI", "Enables emulation of the GS's texture mipmapping."); TRANSLATE_NOOP("FullscreenUI", "Bilinear Filtering"); TRANSLATE_NOOP("FullscreenUI", "Selects where bilinear filtering is utilized when rendering textures."); TRANSLATE_NOOP("FullscreenUI", "Trilinear Filtering"); @@ -7018,8 +7027,6 @@ TRANSLATE_NOOP("FullscreenUI", "Blending Accuracy"); TRANSLATE_NOOP("FullscreenUI", "Determines the level of accuracy when emulating blend modes not supported by the host graphics API."); TRANSLATE_NOOP("FullscreenUI", "Texture Preloading"); TRANSLATE_NOOP("FullscreenUI", "Uploads full textures to the GPU on use, rather than only the utilized regions. Can improve performance in some games."); -TRANSLATE_NOOP("FullscreenUI", "Mipmapping"); -TRANSLATE_NOOP("FullscreenUI", "Enables emulation of the GS's texture mipmapping."); TRANSLATE_NOOP("FullscreenUI", "Software Rendering Threads"); TRANSLATE_NOOP("FullscreenUI", "Number of threads to use in addition to the main GS thread for rasterization."); TRANSLATE_NOOP("FullscreenUI", "Auto Flush (Software)"); @@ -7062,6 +7069,8 @@ TRANSLATE_NOOP("FullscreenUI", "When enabled GPU converts colormap-textures, oth TRANSLATE_NOOP("FullscreenUI", "Upscaling Fixes"); TRANSLATE_NOOP("FullscreenUI", "Half Pixel Offset"); TRANSLATE_NOOP("FullscreenUI", "Adjusts vertices relative to upscaling."); +TRANSLATE_NOOP("FullscreenUI", "Native Scaling"); +TRANSLATE_NOOP("FullscreenUI", "Attempt to do rescaling at native resolution."); TRANSLATE_NOOP("FullscreenUI", "Round Sprite"); TRANSLATE_NOOP("FullscreenUI", "Adjusts sprite coordinates."); TRANSLATE_NOOP("FullscreenUI", "Bilinear Upscale"); @@ -7421,6 +7430,10 @@ TRANSLATE_NOOP("FullscreenUI", "5x Native (~1620p)"); TRANSLATE_NOOP("FullscreenUI", "6x Native (~2160p/4K)"); TRANSLATE_NOOP("FullscreenUI", "7x Native (~2520p)"); TRANSLATE_NOOP("FullscreenUI", "8x Native (~2880p)"); +TRANSLATE_NOOP("FullscreenUI", "Disabled"); +TRANSLATE_NOOP("FullscreenUI", "Enabled"); +TRANSLATE_NOOP("FullscreenUI", "All Levels"); +TRANSLATE_NOOP("FullscreenUI", "Unclamped"); TRANSLATE_NOOP("FullscreenUI", "Nearest"); TRANSLATE_NOOP("FullscreenUI", "Bilinear (Forced)"); TRANSLATE_NOOP("FullscreenUI", "Bilinear (PS2)"); @@ -7471,13 +7484,13 @@ TRANSLATE_NOOP("FullscreenUI", "Sprites/Triangles"); TRANSLATE_NOOP("FullscreenUI", "Blended Sprites/Triangles"); TRANSLATE_NOOP("FullscreenUI", "1 (Normal)"); TRANSLATE_NOOP("FullscreenUI", "2 (Aggressive)"); -TRANSLATE_NOOP("FullscreenUI", "Disabled"); TRANSLATE_NOOP("FullscreenUI", "Inside Target"); TRANSLATE_NOOP("FullscreenUI", "Merge Targets"); TRANSLATE_NOOP("FullscreenUI", "Normal (Vertex)"); TRANSLATE_NOOP("FullscreenUI", "Special (Texture)"); TRANSLATE_NOOP("FullscreenUI", "Special (Texture - Aggressive)"); TRANSLATE_NOOP("FullscreenUI", "Align To Native"); +TRANSLATE_NOOP("FullscreenUI", "Aggressive"); TRANSLATE_NOOP("FullscreenUI", "Half"); TRANSLATE_NOOP("FullscreenUI", "Force Bilinear"); TRANSLATE_NOOP("FullscreenUI", "Force Nearest"); diff --git a/pcsx2/ImGui/ImGuiOverlays.cpp b/pcsx2/ImGui/ImGuiOverlays.cpp index b273edd219ae9a..9e4aa2668b1fd9 100644 --- a/pcsx2/ImGui/ImGuiOverlays.cpp +++ b/pcsx2/ImGui/ImGuiOverlays.cpp @@ -376,8 +376,8 @@ __ri void ImGuiManager::DrawSettingsOverlay(float scale, float margin, float spa if (GSConfig.HWDownloadMode != GSHardwareDownloadMode::Enabled) APPEND("DL={} ", static_cast(GSConfig.HWDownloadMode)); - if (GSConfig.HWMipmap) - APPEND("MM "); + if (GSConfig.HWMipmapMode != GSHWMipmapMode::Disabled) + APPEND("MM={} ", static_cast(GSConfig.HWMipmapMode)); // deliberately test global and print local here for auto values if (EmuConfig.GS.TextureFiltering != BiFiltering::PS2) diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 50630af795326e..a8ca44bd618b16 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -637,7 +637,6 @@ Pcsx2Config::GSOptions::GSOptions() AutoFlushSW = true; PreloadFrameWithGSData = false; Mipmap = true; - HWMipmap = true; ManualUserHacks = false; UserHacks_AlignSpriteX = false; @@ -703,6 +702,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const OpEqu(UpscaleMultiplier) && OpEqu(AccurateBlendingUnit) && + OpEqu(HWMipmapMode) && OpEqu(TextureFiltering) && OpEqu(TexturePreloading) && OpEqu(GSDumpCompression) && @@ -884,7 +884,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapIntEnumEx(Renderer, "Renderer"); SettingsWrapEntryEx(UpscaleMultiplier, "upscale_multiplier"); - SettingsWrapBitBoolEx(HWMipmap, "hw_mipmap"); + SettingsWrapIntEnumEx(HWMipmapMode, "hw_mipmap_mode"); SettingsWrapIntEnumEx(AccurateBlendingUnit, "accurate_blending_unit"); SettingsWrapIntEnumEx(TextureFiltering, "filter"); SettingsWrapIntEnumEx(TexturePreloading, "texture_preloading"); diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 162507f34bb022..0b0bc802953679 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -3140,10 +3140,10 @@ void VMManager::WarnAboutUnsafeSettings() append(ICON_FA_EXCLAMATION_CIRCLE, TRANSLATE_SV("VMManager", "Texture dumping is enabled, this will continually dump textures to disk.")); } - if (!EmuConfig.GS.HWMipmap) + if (EmuConfig.GS.HWMipmapMode != GSHWMipmapMode::Enabled && EmuConfig.GS.HWMipmapMode != GSHWMipmapMode::AllLevels) { append(ICON_FA_IMAGES, - TRANSLATE_SV("VMManager", "Mipmapping is disabled. This may break rendering in some games.")); + TRANSLATE_SV("VMManager", "Mipmapping is not set to Enabled/All Levels. This may break rendering in some games.")); } } if (EmuConfig.GS.TextureFiltering != BiFiltering::PS2)