diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp index f8da272708..c8fc947064 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp @@ -141,8 +141,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* // HW Renderer Fixes ////////////////////////////////////////////////////////////////////////// SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.halfScreenFix, "EmuCore/GS", "UserHacks_Half_Bottom_Override", -1, -1); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.skipDrawRangeStart, "EmuCore/GS", "UserHacks_SkipDraw", 0); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.skipDrawRangeCount, "EmuCore/GS", "UserHacks_SkipDraw_Offset", 0); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.skipDrawStart, "EmuCore/GS", "UserHacks_SkipDraw_Offset", 0); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.skipDrawEnd, "EmuCore/GS", "UserHacks_SkipDraw", 0); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hwAutoFlush, "EmuCore/GS", "UserHacks_AutoFlush", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.frameBufferConversion, "EmuCore/GS", "UserHacks_CPU_FB_Conversion", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableDepthEmulation, "EmuCore/GS", "UserHacks_DisableDepthSupport", false); diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui index 784119deb8..7bc6122a15 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui @@ -586,7 +586,7 @@ - Enable Hardware Renderer Fixes + Manual Hardware Renderer Fixes @@ -661,10 +661,10 @@ - + - + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 0cb52ac849..9b96b663ba 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -442,7 +442,7 @@ struct Pcsx2Config WrapGSMem : 1, Mipmap : 1, AA1 : 1, - UserHacks : 1, + ManualUserHacks : 1, UserHacks_AlignSpriteX : 1, UserHacks_AutoFlush : 1, UserHacks_CPUFBConversion : 1, @@ -510,8 +510,8 @@ struct Pcsx2Config int SWExtraThreads{2}; int SWExtraThreadsHeight{4}; int TVShader{0}; - int SkipDraw{0}; - int SkipDrawOffset{0}; + int SkipDrawStart{0}; + int SkipDrawEnd{0}; int UserHacks_HalfBottomOverride{-1}; int UserHacks_HalfPixelOffset{0}; diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index 0978412c68..c29670a4ff 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -310,8 +310,6 @@ bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* b GSConfig = config; GSConfig.Renderer = renderer; - GSConfig.MaskUserHacks(); - GSConfig.MaskUpscalingHacks(); if (!Host::AcquireHostDisplay(GetAPIForRenderer(renderer))) { @@ -724,8 +722,6 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config) Pcsx2Config::GSOptions old_config(std::move(GSConfig)); GSConfig = new_config; GSConfig.Renderer = (GSConfig.Renderer == GSRendererType::Auto) ? GSUtil::GetPreferredRenderer() : GSConfig.Renderer; - GSConfig.MaskUserHacks(); - GSConfig.MaskUpscalingHacks(); if (!s_gs) return; diff --git a/pcsx2/GS/Renderers/HW/GSHwHack.cpp b/pcsx2/GS/Renderers/HW/GSHwHack.cpp index 3eedce61bc..1c0d300833 100644 --- a/pcsx2/GS/Renderers/HW/GSHwHack.cpp +++ b/pcsx2/GS/Renderers/HW/GSHwHack.cpp @@ -1053,7 +1053,7 @@ bool GSState::IsBadFrame() return false; } - if (m_skip == 0 && GSConfig.SkipDraw > 0) + if (m_skip == 0 && GSConfig.SkipDrawEnd > 0) { if (fi.TME) { @@ -1061,8 +1061,8 @@ bool GSState::IsBadFrame() // General, often problematic post processing if (GSLocalMemory::m_psm[fi.TPSM].depth || GSUtil::HasSharedBits(fi.FBP, fi.FPSM, fi.TBP0, fi.TPSM)) { - m_skip_offset = GSConfig.SkipDrawOffset; - m_skip = std::max(GSConfig.SkipDraw, m_skip_offset); + m_skip_offset = GSConfig.SkipDrawStart; + m_skip = GSConfig.SkipDrawEnd; } } } diff --git a/pcsx2/GS/Window/GSwxDialog.cpp b/pcsx2/GS/Window/GSwxDialog.cpp index 55287b0848..3f3a04a175 100644 --- a/pcsx2/GS/Window/GSwxDialog.cpp +++ b/pcsx2/GS/Window/GSwxDialog.cpp @@ -328,7 +328,7 @@ HacksTab::HacksTab(wxWindow* parent) PaddedBoxSizer tab_box(wxVERTICAL); auto hw_prereq = [this]{ return m_is_hardware; }; - auto* hacks_check_box = m_ui.addCheckBox(tab_box.inner, "Enable HW Hacks", "UserHacks", -1, hw_prereq); + auto* hacks_check_box = m_ui.addCheckBox(tab_box.inner, "Manual HW Hacks", "UserHacks", -1, hw_prereq); auto hacks_prereq = [this, hacks_check_box]{ return m_is_hardware && hacks_check_box->GetValue(); }; auto upscale_hacks_prereq = [this, hacks_check_box]{ return !m_is_native_res && hacks_check_box->GetValue(); }; diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp index 6559bd451b..8a7461d232 100644 --- a/pcsx2/GameDatabase.cpp +++ b/pcsx2/GameDatabase.cpp @@ -16,8 +16,8 @@ #include "PrecompiledHeader.h" #include "GameDatabase.h" -#include "Config.h" #include "Host.h" +#include "Patch.h" #include "common/FileSystem.h" #include "common/Path.h" @@ -31,6 +31,20 @@ #include "fmt/ranges.h" #include #include +#include + +namespace GameDatabaseSchema +{ + static const char* getHWFixName(GSHWFixId id); + static std::optional parseHWFixName(const std::string_view& name); + static bool isUserHackHWFix(GSHWFixId id); +} // namespace GameDatabaseSchema + +namespace GameDatabase +{ + static void parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node); + static void initDatabase(); +} // namespace GameDatabase static constexpr char GAMEDB_YAML_FILE_NAME[] = "GameIndex.yaml"; @@ -85,7 +99,7 @@ const char* GameDatabaseSchema::GameEntry::compatAsString() const } } -void parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node) +void GameDatabase::parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node) { GameDatabaseSchema::GameEntry gameEntry; if (node.has_child("name")) @@ -195,6 +209,25 @@ void parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node } } + if (node.has_child("gsHWFixes")) + { + for (const ryml::NodeRef& n : node["gsHWFixes"].children()) + { + const std::string_view id_name(n.key().data(), n.key().size()); + std::optional id = GameDatabaseSchema::parseHWFixName(id_name); + std::optional value = n.has_val() ? StringUtil::FromChars(std::string_view(n.val().data(), n.val().size())) : 1; + if (!id.has_value() || !value.has_value()) + { + Console.Error("[GameDB] Invalid GS HW Fix: '%*s' specified for serial '%*s'. Dropping!", + static_cast(id_name.size()), id_name.data(), + static_cast(serial.size()), serial.data()); + continue; + } + + gameEntry.gsHWFixes.emplace_back(id.value(), value.value()); + } + } + // Memory Card Filters - Store as a vector to allow flexibility in the future // - currently they are used as a '\n' delimited string in the app if (node.has_child("memcardFilters") && node["memcardFilters"].has_children()) @@ -231,16 +264,188 @@ void parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node s_game_db.emplace(std::move(serial), std::move(gameEntry)); } -static std::ifstream getFileStream(std::string path) +static const char* s_gs_hw_fix_names[] = { + "autoFlush", + "conservativeFramebuffer", + "cpuFramebufferConversion", + "disableDepthSupport", + "wrapGSMem", + "preloadFrameData", + "fastTextureInvalidation", + "textureInsideRT", + "alignSprite", + "mergeSprite", + "wildArmsHack", + "mipmap", + "trilinearFiltering", + "skipDrawStart", + "skipDrawEnd", + "halfBottomOverride", + "halfPixelOffset", + "roundSprite", + "texturePreloading", +}; +static_assert(std::size(s_gs_hw_fix_names) == static_cast(GameDatabaseSchema::GSHWFixId::Count), "HW fix name lookup is correct size"); + +const char* GameDatabaseSchema::getHWFixName(GSHWFixId id) { -#ifdef _WIN32 - return std::ifstream(StringUtil::UTF8StringToWideString(path)); -#else - return std::ifstream(path.c_str()); -#endif + return s_gs_hw_fix_names[static_cast(id)]; } -static void initDatabase() +static std::optional GameDatabaseSchema::parseHWFixName(const std::string_view& name) +{ + for (u32 i = 0; i < std::size(s_gs_hw_fix_names); i++) + { + if (name.compare(s_gs_hw_fix_names[i]) == 0) + return static_cast(i); + } + + return std::nullopt; +} + +bool GameDatabaseSchema::isUserHackHWFix(GSHWFixId id) +{ + switch (id) + { + case GSHWFixId::Mipmap: + case GSHWFixId::TexturePreloading: + case GSHWFixId::ConservativeFramebuffer: + return false; + +#ifdef PCSX2_CORE + // Trifiltering isn't a hack in Qt. + case GSHWFixId::TrilinearFiltering: + return false; +#endif + + default: + return true; + } +} + +u32 GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& config) const +{ + // Only apply GS HW fixes if the user hasn't manually enabled HW fixes. + const bool apply_auto_fixes = !config.ManualUserHacks; + if (!apply_auto_fixes) + Console.Warning("[GameDB] Hardware fixes are enabled, not using automatic fixes."); + + u32 num_applied_fixes = 0; + for (const auto& [id, value] : gsHWFixes) + { + if (isUserHackHWFix(id) && !apply_auto_fixes) + { + PatchesCon->Warning("[GameDB] Skipping GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value); + continue; + } + + switch (id) + { + case GSHWFixId::AutoFlush: + config.UserHacks_AutoFlush = (value > 0); + break; + + case GSHWFixId::ConservativeFramebuffer: + config.ConservativeFramebuffer = (value > 0); + break; + + case GSHWFixId::CPUFramebufferConversion: + config.UserHacks_CPUFBConversion = (value > 0); + break; + + case GSHWFixId::DisableDepthSupport: + config.UserHacks_DisableDepthSupport = (value > 0); + break; + + case GSHWFixId::WrapGSMem: + config.WrapGSMem = (value > 0); + break; + + case GSHWFixId::PreloadFrameData: + config.PreloadFrameWithGSData = (value > 0); + break; + + case GSHWFixId::FastTextureInvalidation: + config.UserHacks_DisablePartialInvalidation = (value > 0); + break; + + case GSHWFixId::TextureInsideRT: + config.UserHacks_TextureInsideRt = (value > 0); + break; + + case GSHWFixId::AlignSprite: + config.UserHacks_AlignSpriteX = (value > 0); + break; + + case GSHWFixId::MergeSprite: + config.UserHacks_MergePPSprite = (value > 0); + break; + + case GSHWFixId::WildArmsHack: + config.UserHacks_WildHack = (value > 0); + break; + + case GSHWFixId::Mipmap: + { + if (value >= 0 && value <= static_cast(HWMipmapLevel::Full)) + { + if (config.HWMipmap == HWMipmapLevel::Automatic) + config.HWMipmap = static_cast(value); + else if (config.HWMipmap == HWMipmapLevel::Off) + Console.Warning("[GameDB] Game requires mipmapping but it has been force disabled."); + } + } + break; + + case GSHWFixId::TrilinearFiltering: + { + if (value >= 0 && value <= static_cast(TriFiltering::Forced)) + config.UserHacks_TriFilter = static_cast(value); + } + break; + + case GSHWFixId::SkipDrawStart: + config.SkipDrawStart = value; + break; + + case GSHWFixId::SkipDrawEnd: + config.SkipDrawEnd = value; + break; + + case GSHWFixId::HalfBottomOverride: + config.UserHacks_HalfBottomOverride = value; + break; + + case GSHWFixId::HalfPixelOffset: + config.UserHacks_HalfPixelOffset = value; + break; + + case GSHWFixId::RoundSprite: + config.UserHacks_RoundSprite = value; + break; + + case GSHWFixId::TexturePreloading: + { + if (value >= 0 && value <= static_cast(TexturePreloadingLevel::Full)) + config.TexturePreloading = std::min(config.TexturePreloading, static_cast(value)); + } + break; + + default: + break; + } + + PatchesCon->WriteLn("[GameDB] Enabled GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value); + num_applied_fixes++; + } + + // fixup skipdraw range just in case the db has a bad range (but the linter should catch this) + config.SkipDrawEnd = std::max(config.SkipDrawStart, config.SkipDrawEnd); + + return num_applied_fixes; +} + +void GameDatabase::initDatabase() { ryml::Callbacks rymlCallbacks = ryml::get_callbacks(); rymlCallbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void*) { @@ -291,8 +496,6 @@ static void initDatabase() ryml::reset_callbacks(); } - - void GameDatabase::ensureLoaded() { std::call_once(s_load_once_flag, []() { diff --git a/pcsx2/GameDatabase.h b/pcsx2/GameDatabase.h index cf6264d1b5..7b6331c06e 100644 --- a/pcsx2/GameDatabase.h +++ b/pcsx2/GameDatabase.h @@ -15,16 +15,18 @@ #pragma once +#include "Config.h" +#include +#include +#include #include #include -#include enum GamefixId; enum SpeedhackId; -class GameDatabaseSchema +namespace GameDatabaseSchema { -public: enum class Compatibility { Unknown = 0, @@ -54,6 +56,34 @@ public: Full }; + enum class GSHWFixId : u32 + { + // boolean settings + AutoFlush, + ConservativeFramebuffer, + CPUFramebufferConversion, + DisableDepthSupport, + WrapGSMem, + PreloadFrameData, + FastTextureInvalidation, + TextureInsideRT, + AlignSprite, + MergeSprite, + WildArmsHack, + + // integer settings + Mipmap, + TrilinearFiltering, + SkipDrawStart, + SkipDrawEnd, + HalfBottomOverride, + HalfPixelOffset, + RoundSprite, + TexturePreloading, + + Count + }; + using Patch = std::vector; struct GameEntry @@ -67,6 +97,7 @@ public: ClampMode vuClampMode = ClampMode::Undefined; std::vector gameFixes; std::vector> speedHacks; + std::vector> gsHWFixes; std::vector memcardFilters; std::unordered_map patches; @@ -74,6 +105,9 @@ public: std::string memcardFiltersAsString() const; const Patch* findPatch(const std::string_view& crc) const; const char* compatAsString() const; + + /// Applies GS hardware fixes to an existing config. Returns the number of applied fixes. + u32 applyGSHardwareFixes(Pcsx2Config::GSOptions& config) const; }; }; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index e49a7d1557..6ce60d6224 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -314,7 +314,7 @@ Pcsx2Config::GSOptions::GSOptions() Mipmap = true; AA1 = true; - UserHacks = false; + ManualUserHacks = false; UserHacks_AlignSpriteX = false; UserHacks_AutoFlush = false; UserHacks_CPUFBConversion = false; @@ -385,8 +385,8 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const OpEqu(SWExtraThreads) && OpEqu(SWExtraThreadsHeight) && OpEqu(TVShader) && - OpEqu(SkipDraw) && - OpEqu(SkipDrawOffset) && + OpEqu(SkipDrawEnd) && + OpEqu(SkipDrawStart) && OpEqu(UserHacks_HalfBottomOverride) && OpEqu(UserHacks_HalfPixelOffset) && @@ -513,7 +513,7 @@ void Pcsx2Config::GSOptions::ReloadIniSettings() GSSettingBoolEx(WrapGSMem, "wrap_gs_mem"); GSSettingBoolEx(Mipmap, "mipmap"); GSSettingBoolEx(AA1, "aa1"); - GSSettingBoolEx(UserHacks, "UserHacks"); + GSSettingBoolEx(ManualUserHacks, "UserHacks"); GSSettingBoolEx(UserHacks_AlignSpriteX, "UserHacks_align_sprite_X"); GSSettingBoolEx(UserHacks_AutoFlush, "UserHacks_AutoFlush"); GSSettingBoolEx(UserHacks_CPUFBConversion, "UserHacks_CPU_FB_Conversion"); @@ -555,8 +555,9 @@ void Pcsx2Config::GSOptions::ReloadIniSettings() GSSettingIntEx(SWExtraThreads, "extrathreads"); GSSettingIntEx(SWExtraThreadsHeight, "extrathreads_height"); GSSettingIntEx(TVShader, "TVShader"); - GSSettingIntEx(SkipDraw, "UserHacks_SkipDraw"); - GSSettingIntEx(SkipDrawOffset, "UserHacks_SkipDraw_Offset"); + GSSettingIntEx(SkipDrawStart, "UserHacks_SkipDraw_Offset"); + GSSettingIntEx(SkipDrawEnd, "UserHacks_SkipDraw"); + SkipDrawEnd = std::max(SkipDrawStart, SkipDrawEnd); GSSettingIntEx(UserHacks_HalfBottomOverride, "UserHacks_Half_Bottom_Override"); GSSettingIntEx(UserHacks_HalfPixelOffset, "UserHacks_HalfPixelOffset"); @@ -588,7 +589,7 @@ void Pcsx2Config::GSOptions::ReloadIniSettings() void Pcsx2Config::GSOptions::MaskUserHacks() { - if (UserHacks) + if (ManualUserHacks) return; UserHacks_AlignSpriteX = false; @@ -605,8 +606,8 @@ void Pcsx2Config::GSOptions::MaskUserHacks() UserHacks_TextureInsideRt = false; UserHacks_TCOffsetX = 0; UserHacks_TCOffsetY = 0; - SkipDraw = 0; - SkipDrawOffset = 0; + SkipDrawStart = 0; + SkipDrawEnd = 0; // in wx, we put trilinear filtering behind user hacks, but not in qt. #ifndef PCSX2_CORE @@ -616,7 +617,7 @@ void Pcsx2Config::GSOptions::MaskUserHacks() void Pcsx2Config::GSOptions::MaskUpscalingHacks() { - if (UpscaleMultiplier == 1 || UserHacks) + if (UpscaleMultiplier == 1 || ManualUserHacks) return; UserHacks_AlignSpriteX = false; diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 40d2228dd2..a3059b68e2 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -218,6 +218,10 @@ void VMManager::LoadSettings() InputManager::ReloadSources(*si); InputManager::ReloadBindings(*si); + // Remove any user-specified hacks in the config (we don't want stale/conflicting values when it's globally disabled). + EmuConfig.GS.MaskUserHacks(); + EmuConfig.GS.MaskUpscalingHacks(); + if (HasValidVM()) ApplyGameFixes(); } @@ -295,6 +299,8 @@ void VMManager::ApplyGameFixes() if (id == Fix_GoemonTlbMiss && true) vtlb_Alloc_Ppmap(); } + + s_active_game_fixes += game->applyGSHardwareFixes(EmuConfig.GS); } std::string VMManager::GetGameSettingsPath(u32 game_crc) diff --git a/pcsx2/gui/AppCoreThread.cpp b/pcsx2/gui/AppCoreThread.cpp index 44177e01d1..b27d904e81 100644 --- a/pcsx2/gui/AppCoreThread.cpp +++ b/pcsx2/gui/AppCoreThread.cpp @@ -333,6 +333,8 @@ static int loadGameSettings(Pcsx2Config& dest, const GameDatabaseSchema::GameEnt vtlb_Alloc_Ppmap(); } + gf += game.applyGSHardwareFixes(dest.GS); + return gf; } @@ -404,6 +406,10 @@ static void _ApplySettings(const Pcsx2Config& src, Pcsx2Config& fixup) fixup.GS.VsyncEnable = VsyncMode::Off; } + // Remove any user-specified hacks in the config (we don't want stale/conflicting values when it's globally disabled). + fixup.GS.MaskUserHacks(); + fixup.GS.MaskUpscalingHacks(); + wxString gamePatch; wxString gameFixes; wxString gameCheats;