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;