mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Some games (e.g. Metal Gear Solid 2) use large-ish textures, with a bunch of different CLUTs/palettes, depending on the draw. Kind-of like a texture atlas. This causes issues when texture preloading is enabled, as both VRAM and GS CPU thread usage increase proporiately to the number of texture:clut pairs (since it has to be hashed). An alternative to disabling preloading, which is what we currently do, is enabling GPU palette conversion in these games. Even though we ever-so-slightly increase the GPU load due to having to do shader sampling, the CPU load on the GS thread is considerably reduced, and overall performance is greater. In theory it'll also achieve higher cache hit rates on the GPU, since we're not duplicating a bunch of textures. However, as a general rule of thumb, we don't want to encourage people to enable paltex, as most games run slower with it on. So, what this PR does, is add a GameDB option for these types of games, to enable paltex when texture preloading is set to full/hash cache, but otherwise leave the setting alone. The best of both worlds. NOTE: I've also forced paltex=0 for Spider-Man 2, as it uses a massive number of palettes which can cause descriptor issues in DX12/Vulkan. A perfect example of where you *don't* want to use paltex.
766 lines
23 KiB
C++
766 lines
23 KiB
C++
/* PCSX2 - PS2 Emulator for PCs
|
|
* Copyright (C) 2002-2020 PCSX2 Dev Team
|
|
*
|
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "PrecompiledHeader.h"
|
|
|
|
#include "GameDatabase.h"
|
|
#include "Host.h"
|
|
#include "Patch.h"
|
|
#include "vtlb.h"
|
|
|
|
#include "common/FileSystem.h"
|
|
#include "common/Path.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/Timer.h"
|
|
|
|
#include <sstream>
|
|
#include "ryml_std.hpp"
|
|
#include "ryml.hpp"
|
|
#include "fmt/core.h"
|
|
#include "fmt/ranges.h"
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <optional>
|
|
|
|
namespace GameDatabaseSchema
|
|
{
|
|
static const char* getHWFixName(GSHWFixId id);
|
|
static std::optional<GSHWFixId> 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";
|
|
|
|
static std::unordered_map<std::string, GameDatabaseSchema::GameEntry> s_game_db;
|
|
static std::once_flag s_load_once_flag;
|
|
|
|
std::string GameDatabaseSchema::GameEntry::memcardFiltersAsString() const
|
|
{
|
|
return fmt::to_string(fmt::join(memcardFilters, "/"));
|
|
}
|
|
|
|
const std::string* GameDatabaseSchema::GameEntry::findPatch(u32 crc) const
|
|
{
|
|
Console.WriteLn(fmt::format("[GameDB] Searching for patch with CRC '{:08X}'", crc));
|
|
|
|
auto it = patches.find(crc);
|
|
if (it != patches.end())
|
|
{
|
|
Console.WriteLn(fmt::format("[GameDB] Found patch with CRC '{:08X}'", crc));
|
|
return &it->second;
|
|
}
|
|
|
|
it = patches.find(0);
|
|
if (it != patches.end())
|
|
{
|
|
Console.WriteLn("[GameDB] Found and falling back to default patch");
|
|
return &it->second;
|
|
}
|
|
Console.WriteLn("[GameDB] No CRC-specific patch or default patch found");
|
|
return nullptr;
|
|
}
|
|
|
|
const char* GameDatabaseSchema::GameEntry::compatAsString() const
|
|
{
|
|
switch (compat)
|
|
{
|
|
case GameDatabaseSchema::Compatibility::Perfect:
|
|
return "Perfect";
|
|
case GameDatabaseSchema::Compatibility::Playable:
|
|
return "Playable";
|
|
case GameDatabaseSchema::Compatibility::InGame:
|
|
return "In-Game";
|
|
case GameDatabaseSchema::Compatibility::Menu:
|
|
return "Menu";
|
|
case GameDatabaseSchema::Compatibility::Intro:
|
|
return "Intro";
|
|
case GameDatabaseSchema::Compatibility::Nothing:
|
|
return "Nothing";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
void GameDatabase::parseAndInsert(const std::string_view& serial, const c4::yml::NodeRef& node)
|
|
{
|
|
GameDatabaseSchema::GameEntry gameEntry;
|
|
if (node.has_child("name"))
|
|
{
|
|
node["name"] >> gameEntry.name;
|
|
}
|
|
if (node.has_child("region"))
|
|
{
|
|
node["region"] >> gameEntry.region;
|
|
}
|
|
if (node.has_child("compat"))
|
|
{
|
|
int val = 0;
|
|
node["compat"] >> val;
|
|
gameEntry.compat = static_cast<GameDatabaseSchema::Compatibility>(val);
|
|
}
|
|
if (node.has_child("roundModes"))
|
|
{
|
|
if (node["roundModes"].has_child("eeRoundMode"))
|
|
{
|
|
int eeVal = -1;
|
|
node["roundModes"]["eeRoundMode"] >> eeVal;
|
|
gameEntry.eeRoundMode = static_cast<GameDatabaseSchema::RoundMode>(eeVal);
|
|
}
|
|
if (node["roundModes"].has_child("vuRoundMode"))
|
|
{
|
|
int vuVal = -1;
|
|
node["roundModes"]["vuRoundMode"] >> vuVal;
|
|
gameEntry.vuRoundMode = static_cast<GameDatabaseSchema::RoundMode>(vuVal);
|
|
}
|
|
}
|
|
if (node.has_child("clampModes"))
|
|
{
|
|
if (node["clampModes"].has_child("eeClampMode"))
|
|
{
|
|
int eeVal = -1;
|
|
node["clampModes"]["eeClampMode"] >> eeVal;
|
|
gameEntry.eeClampMode = static_cast<GameDatabaseSchema::ClampMode>(eeVal);
|
|
}
|
|
if (node["clampModes"].has_child("vuClampMode"))
|
|
{
|
|
int vuVal = -1;
|
|
node["clampModes"]["vuClampMode"] >> vuVal;
|
|
gameEntry.vuClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
|
|
}
|
|
}
|
|
|
|
// Validate game fixes, invalid ones will be dropped!
|
|
if (node.has_child("gameFixes") && node["gameFixes"].has_children())
|
|
{
|
|
for (const ryml::NodeRef& n : node["gameFixes"].children())
|
|
{
|
|
bool fixValidated = false;
|
|
auto fix = std::string(n.val().str, n.val().len);
|
|
|
|
// Enum values don't end with Hack, but gamedb does, so remove it before comparing.
|
|
if (StringUtil::EndsWith(fix, "Hack"))
|
|
{
|
|
fix.erase(fix.size() - 4);
|
|
for (GamefixId id = GamefixId_FIRST; id < pxEnumEnd; ++id)
|
|
{
|
|
if (fix.compare(EnumToString(id)) == 0 &&
|
|
std::find(gameEntry.gameFixes.begin(), gameEntry.gameFixes.end(), id) == gameEntry.gameFixes.end())
|
|
{
|
|
gameEntry.gameFixes.push_back(id);
|
|
fixValidated = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fixValidated)
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Invalid gamefix: '{}', specified for serial: '{}'. Dropping!", fix, serial));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate speed hacks, invalid ones will be dropped!
|
|
if (node.has_child("speedHacks") && node["speedHacks"].has_children())
|
|
{
|
|
for (const ryml::NodeRef& n : node["speedHacks"].children())
|
|
{
|
|
bool speedHackValidated = false;
|
|
auto speedHack = std::string(n.key().str, n.key().len);
|
|
|
|
// Same deal with SpeedHacks
|
|
if (StringUtil::EndsWith(speedHack, "SpeedHack"))
|
|
{
|
|
speedHack.erase(speedHack.size() - 9);
|
|
for (SpeedhackId id = SpeedhackId_FIRST; id < pxEnumEnd; ++id)
|
|
{
|
|
if (speedHack.compare(EnumToString(id)) == 0 &&
|
|
std::none_of(gameEntry.speedHacks.begin(), gameEntry.speedHacks.end(), [id](const auto& it) { return it.first == id; }))
|
|
{
|
|
gameEntry.speedHacks.emplace_back(id, std::atoi(n.val().str));
|
|
speedHackValidated = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!speedHackValidated)
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Invalid speedhack: '{}', specified for serial: '{}'. Dropping!", speedHack.c_str(), serial));
|
|
}
|
|
}
|
|
}
|
|
|
|
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<GameDatabaseSchema::GSHWFixId> id = GameDatabaseSchema::parseHWFixName(id_name);
|
|
std::optional<s32> value = n.has_val() ? StringUtil::FromChars<s32>(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<int>(id_name.size()), id_name.data(),
|
|
static_cast<int>(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())
|
|
{
|
|
for (const ryml::NodeRef& n : node["memcardFilters"].children())
|
|
{
|
|
auto memcardFilter = std::string(n.val().str, n.val().len);
|
|
gameEntry.memcardFilters.emplace_back(std::move(memcardFilter));
|
|
}
|
|
}
|
|
|
|
// Game Patches
|
|
if (node.has_child("patches") && node["patches"].has_children())
|
|
{
|
|
for (const ryml::NodeRef& n : node["patches"].children())
|
|
{
|
|
// use a crc of 0 for default patches
|
|
const std::string_view crc_str(n.key().str, n.key().len);
|
|
const std::optional<u32> crc = (StringUtil::compareNoCase(crc_str, "default")) ? std::optional<u32>(0) : StringUtil::FromChars<u32>(crc_str, 16);
|
|
if (!crc.has_value())
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Invalid CRC '{}' found for serial: '{}'. Skipping!", crc_str, serial));
|
|
continue;
|
|
}
|
|
if (gameEntry.patches.find(crc.value()) != gameEntry.patches.end())
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Duplicate CRC '{}' found for serial: '{}'. Skipping, CRCs are case-insensitive!", crc_str, serial));
|
|
continue;
|
|
}
|
|
|
|
std::string patch;
|
|
if (n.has_child("content"))
|
|
n["content"] >> patch;
|
|
gameEntry.patches.emplace(crc.value(), std::move(patch));
|
|
}
|
|
}
|
|
|
|
s_game_db.emplace(std::move(serial), std::move(gameEntry));
|
|
}
|
|
|
|
static const char* s_gs_hw_fix_names[] = {
|
|
"autoFlush",
|
|
"cpuFramebufferConversion",
|
|
"disableDepthSupport",
|
|
"wrapGSMem",
|
|
"preloadFrameData",
|
|
"disablePartialInvalidation",
|
|
"textureInsideRT",
|
|
"alignSprite",
|
|
"mergeSprite",
|
|
"wildArmsHack",
|
|
"pointListPalette",
|
|
"mipmap",
|
|
"trilinearFiltering",
|
|
"skipDrawStart",
|
|
"skipDrawEnd",
|
|
"halfBottomOverride",
|
|
"halfPixelOffset",
|
|
"roundSprite",
|
|
"texturePreloading",
|
|
"deinterlace",
|
|
"cpuSpriteRenderBW",
|
|
"gpuPaletteConversion",
|
|
};
|
|
static_assert(std::size(s_gs_hw_fix_names) == static_cast<u32>(GameDatabaseSchema::GSHWFixId::Count), "HW fix name lookup is correct size");
|
|
|
|
const char* GameDatabaseSchema::getHWFixName(GSHWFixId id)
|
|
{
|
|
return s_gs_hw_fix_names[static_cast<u32>(id)];
|
|
}
|
|
|
|
static std::optional<GameDatabaseSchema::GSHWFixId> 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<GameDatabaseSchema::GSHWFixId>(i);
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool GameDatabaseSchema::isUserHackHWFix(GSHWFixId id)
|
|
{
|
|
switch (id)
|
|
{
|
|
case GSHWFixId::Deinterlace:
|
|
case GSHWFixId::Mipmap:
|
|
case GSHWFixId::TexturePreloading:
|
|
case GSHWFixId::PointListPalette:
|
|
return false;
|
|
|
|
#ifdef PCSX2_CORE
|
|
// Trifiltering isn't a hack in Qt.
|
|
case GSHWFixId::TrilinearFiltering:
|
|
return false;
|
|
#endif
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
u32 GameDatabaseSchema::GameEntry::applyGameFixes(Pcsx2Config& config, bool applyAuto) const
|
|
{
|
|
// Only apply core game fixes if the user has enabled them.
|
|
if (!applyAuto)
|
|
Console.Warning("[GameDB] Game Fixes are disabled");
|
|
|
|
u32 num_applied_fixes = 0;
|
|
|
|
if (eeRoundMode != GameDatabaseSchema::RoundMode::Undefined)
|
|
{
|
|
const SSE_RoundMode eeRM = (SSE_RoundMode)enum_cast(eeRoundMode);
|
|
if (EnumIsValid(eeRM))
|
|
{
|
|
if (applyAuto)
|
|
{
|
|
PatchesCon->WriteLn("(GameDB) Changing EE/FPU roundmode to %d [%s]", eeRM, EnumToString(eeRM));
|
|
config.Cpu.sseMXCSR.SetRoundMode(eeRM);
|
|
num_applied_fixes++;
|
|
}
|
|
else
|
|
PatchesCon->Warning("[GameDB] Skipping changing EE/FPU roundmode to %d [%s]", eeRM, EnumToString(eeRM));
|
|
}
|
|
}
|
|
|
|
if (vuRoundMode != GameDatabaseSchema::RoundMode::Undefined)
|
|
{
|
|
const SSE_RoundMode vuRM = (SSE_RoundMode)enum_cast(vuRoundMode);
|
|
if (EnumIsValid(vuRM))
|
|
{
|
|
if (applyAuto)
|
|
{
|
|
PatchesCon->WriteLn("(GameDB) Changing VU0/VU1 roundmode to %d [%s]", vuRM, EnumToString(vuRM));
|
|
config.Cpu.sseVUMXCSR.SetRoundMode(vuRM);
|
|
num_applied_fixes++;
|
|
}
|
|
else
|
|
PatchesCon->Warning("[GameDB] Skipping changing VU0/VU1 roundmode to %d [%s]", vuRM, EnumToString(vuRM));
|
|
}
|
|
}
|
|
|
|
if (eeClampMode != GameDatabaseSchema::ClampMode::Undefined)
|
|
{
|
|
const int clampMode = enum_cast(eeClampMode);
|
|
if (applyAuto)
|
|
{
|
|
PatchesCon->WriteLn("(GameDB) Changing EE/FPU clamp mode [mode=%d]", clampMode);
|
|
config.Cpu.Recompiler.fpuOverflow = (clampMode >= 1);
|
|
config.Cpu.Recompiler.fpuExtraOverflow = (clampMode >= 2);
|
|
config.Cpu.Recompiler.fpuFullMode = (clampMode >= 3);
|
|
num_applied_fixes++;
|
|
}
|
|
else
|
|
PatchesCon->Warning("[GameDB] Skipping changing EE/FPU clamp mode [mode=%d]", clampMode);
|
|
}
|
|
|
|
if (vuClampMode != GameDatabaseSchema::ClampMode::Undefined)
|
|
{
|
|
const int clampMode = enum_cast(vuClampMode);
|
|
if (applyAuto)
|
|
{
|
|
PatchesCon->WriteLn("(GameDB) Changing VU0/VU1 clamp mode [mode=%d]", clampMode);
|
|
config.Cpu.Recompiler.vuOverflow = (clampMode >= 1);
|
|
config.Cpu.Recompiler.vuExtraOverflow = (clampMode >= 2);
|
|
config.Cpu.Recompiler.vuSignOverflow = (clampMode >= 3);
|
|
num_applied_fixes++;
|
|
}
|
|
else
|
|
PatchesCon->Warning("[GameDB] Skipping changing VU0/VU1 clamp mode [mode=%d]", clampMode);
|
|
}
|
|
|
|
// TODO - config - this could be simplified with maps instead of bitfields and enums
|
|
for (const auto& it : speedHacks)
|
|
{
|
|
const bool mode = it.second != 0;
|
|
if (!applyAuto)
|
|
{
|
|
PatchesCon->Warning("[GameDB] Skipping setting Speedhack '%s' to [mode=%d]", EnumToString(it.first), mode);
|
|
continue;
|
|
}
|
|
// Legacy note - speedhacks are setup in the GameDB as integer values, but
|
|
// are effectively booleans like the gamefixes
|
|
config.Speedhacks.Set(it.first, mode);
|
|
PatchesCon->WriteLn("(GameDB) Setting Speedhack '%s' to [mode=%d]", EnumToString(it.first), mode);
|
|
num_applied_fixes++;
|
|
}
|
|
|
|
// TODO - config - this could be simplified with maps instead of bitfields and enums
|
|
for (const GamefixId id : gameFixes)
|
|
{
|
|
if (!applyAuto)
|
|
{
|
|
PatchesCon->Warning("[GameDB] Skipping Gamefix: %s", EnumToString(id));
|
|
continue;
|
|
}
|
|
// if the fix is present, it is said to be enabled
|
|
config.Gamefixes.Set(id, true);
|
|
PatchesCon->WriteLn("(GameDB) Enabled Gamefix: %s", EnumToString(id));
|
|
num_applied_fixes++;
|
|
|
|
// The LUT is only used for 1 game so we allocate it only when the gamefix is enabled (save 4MB)
|
|
if (id == Fix_GoemonTlbMiss && true)
|
|
vtlb_Alloc_Ppmap();
|
|
}
|
|
|
|
return num_applied_fixes;
|
|
}
|
|
|
|
bool GameDatabaseSchema::GameEntry::configMatchesHWFix(const Pcsx2Config::GSOptions& config, GSHWFixId id, int value)
|
|
{
|
|
switch (id)
|
|
{
|
|
case GSHWFixId::AutoFlush:
|
|
return (static_cast<int>(config.UserHacks_AutoFlush) == value);
|
|
|
|
case GSHWFixId::CPUFramebufferConversion:
|
|
return (static_cast<int>(config.UserHacks_CPUFBConversion) == value);
|
|
|
|
case GSHWFixId::DisableDepthSupport:
|
|
return (static_cast<int>(config.UserHacks_DisableDepthSupport) == value);
|
|
|
|
case GSHWFixId::WrapGSMem:
|
|
return (static_cast<int>(config.WrapGSMem) == value);
|
|
|
|
case GSHWFixId::PreloadFrameData:
|
|
return (static_cast<int>(config.PreloadFrameWithGSData) == value);
|
|
|
|
case GSHWFixId::DisablePartialInvalidation:
|
|
return (static_cast<int>(config.UserHacks_DisablePartialInvalidation) == value);
|
|
|
|
case GSHWFixId::TextureInsideRT:
|
|
return (static_cast<int>(config.UserHacks_TextureInsideRt) == value);
|
|
|
|
case GSHWFixId::AlignSprite:
|
|
return (config.UpscaleMultiplier == 1 || static_cast<int>(config.UserHacks_AlignSpriteX) == value);
|
|
|
|
case GSHWFixId::MergeSprite:
|
|
return (config.UpscaleMultiplier == 1 || static_cast<int>(config.UserHacks_MergePPSprite) == value);
|
|
|
|
case GSHWFixId::WildArmsHack:
|
|
return (config.UpscaleMultiplier == 1 || static_cast<int>(config.UserHacks_WildHack) == value);
|
|
|
|
case GSHWFixId::PointListPalette:
|
|
return (static_cast<int>(config.PointListPalette) == value);
|
|
|
|
case GSHWFixId::Mipmap:
|
|
return (config.HWMipmap == HWMipmapLevel::Automatic || static_cast<int>(config.HWMipmap) == value);
|
|
|
|
case GSHWFixId::TrilinearFiltering:
|
|
return (config.UserHacks_TriFilter == TriFiltering::Automatic || static_cast<int>(config.UserHacks_TriFilter) == value);
|
|
|
|
case GSHWFixId::SkipDrawStart:
|
|
return (config.SkipDrawStart == value);
|
|
|
|
case GSHWFixId::SkipDrawEnd:
|
|
return (config.SkipDrawEnd == value);
|
|
|
|
case GSHWFixId::HalfBottomOverride:
|
|
return (config.UserHacks_HalfBottomOverride == value);
|
|
|
|
case GSHWFixId::HalfPixelOffset:
|
|
return (config.UpscaleMultiplier == 1 || config.UserHacks_HalfPixelOffset == value);
|
|
|
|
case GSHWFixId::RoundSprite:
|
|
return (config.UpscaleMultiplier == 1 || config.UserHacks_RoundSprite == value);
|
|
|
|
case GSHWFixId::TexturePreloading:
|
|
return (static_cast<int>(config.TexturePreloading) <= value);
|
|
|
|
case GSHWFixId::Deinterlace:
|
|
return (config.InterlaceMode == GSInterlaceMode::Automatic || static_cast<int>(config.InterlaceMode) == value);
|
|
|
|
case GSHWFixId::CPUSpriteRenderBW:
|
|
return (config.UserHacks_CPUSpriteRenderBW == value);
|
|
|
|
case GSHWFixId::GPUPaletteConversion:
|
|
return (config.GPUPaletteConversion == ((value > 1) ? (config.TexturePreloading == TexturePreloadingLevel::Full) : (value != 0)));
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
u32 GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& config) const
|
|
{
|
|
std::string disabled_fixes;
|
|
|
|
// 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] Manual GS hardware renderer fixes are enabled, not using automatic hardware renderer fixes from GameDB.");
|
|
|
|
u32 num_applied_fixes = 0;
|
|
for (const auto& [id, value] : gsHWFixes)
|
|
{
|
|
if (isUserHackHWFix(id) && !apply_auto_fixes)
|
|
{
|
|
if (configMatchesHWFix(config, id, value))
|
|
continue;
|
|
|
|
PatchesCon->Warning("[GameDB] Skipping GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value);
|
|
fmt::format_to(std::back_inserter(disabled_fixes), "{} {} = {}", disabled_fixes.empty() ? " " : "\n ", getHWFixName(id), value);
|
|
continue;
|
|
}
|
|
|
|
switch (id)
|
|
{
|
|
case GSHWFixId::AutoFlush:
|
|
config.UserHacks_AutoFlush = (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::DisablePartialInvalidation:
|
|
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::PointListPalette:
|
|
config.PointListPalette = (value > 0);
|
|
break;
|
|
|
|
case GSHWFixId::Mipmap:
|
|
{
|
|
if (value >= 0 && value <= static_cast<int>(HWMipmapLevel::Full))
|
|
{
|
|
if (config.HWMipmap == HWMipmapLevel::Automatic)
|
|
config.HWMipmap = static_cast<HWMipmapLevel>(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<int>(TriFiltering::Forced))
|
|
{
|
|
if (config.UserHacks_TriFilter == TriFiltering::Automatic)
|
|
config.UserHacks_TriFilter = static_cast<TriFiltering>(value);
|
|
else if (config.UserHacks_TriFilter == TriFiltering::Off)
|
|
Console.Warning("[GameDB] Game requires trilinear filtering but it has been force disabled.");
|
|
}
|
|
}
|
|
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<int>(TexturePreloadingLevel::Full))
|
|
config.TexturePreloading = std::min(config.TexturePreloading, static_cast<TexturePreloadingLevel>(value));
|
|
}
|
|
break;
|
|
|
|
case GSHWFixId::Deinterlace:
|
|
{
|
|
if (value >= 0 && value <= static_cast<int>(GSInterlaceMode::Automatic))
|
|
{
|
|
if (config.InterlaceMode == GSInterlaceMode::Automatic)
|
|
config.InterlaceMode = static_cast<GSInterlaceMode>(value);
|
|
else
|
|
Console.Warning("[GameDB] Game requires different deinterlace mode but it has been overridden by user setting.");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GSHWFixId::CPUSpriteRenderBW:
|
|
config.UserHacks_CPUSpriteRenderBW = value;
|
|
break;
|
|
|
|
|
|
case GSHWFixId::GPUPaletteConversion:
|
|
{
|
|
// if 2, enable paltex when preloading is full, otherwise leave as-is
|
|
if (value > 1)
|
|
config.GPUPaletteConversion = (config.TexturePreloading == TexturePreloadingLevel::Full) ? true : config.GPUPaletteConversion;
|
|
else
|
|
config.GPUPaletteConversion = (value != 0);
|
|
}
|
|
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);
|
|
|
|
#ifdef PCSX2_CORE
|
|
if (!disabled_fixes.empty())
|
|
{
|
|
Host::AddKeyedOSDMessage("HWFixesWarning",
|
|
fmt::format("Manual GS hardware renderer fixes are enabled, automatic fixes were not applied:\n{}",
|
|
disabled_fixes),
|
|
10.0f);
|
|
}
|
|
else
|
|
{
|
|
Host::RemoveKeyedOSDMessage("HWFixesWarning");
|
|
}
|
|
#endif
|
|
|
|
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*) {
|
|
throw std::runtime_error(fmt::format("[YAML] Parsing error at {}:{} (bufpos={}): {}",
|
|
loc.line, loc.col, loc.offset, msg));
|
|
};
|
|
ryml::set_callbacks(rymlCallbacks);
|
|
c4::set_error_callback([](const char* msg, size_t msg_size) {
|
|
throw std::runtime_error(fmt::format("[YAML] Internal Parsing error: {}",
|
|
msg));
|
|
});
|
|
try
|
|
{
|
|
auto buf = Host::ReadResourceFileToString(GAMEDB_YAML_FILE_NAME);
|
|
if (!buf.has_value())
|
|
{
|
|
Console.Error("[GameDB] Unable to open GameDB file, file does not exist.");
|
|
return;
|
|
}
|
|
|
|
ryml::Tree tree = ryml::parse_in_arena(c4::to_csubstr(buf.value()));
|
|
ryml::NodeRef root = tree.rootref();
|
|
|
|
for (const ryml::NodeRef& n : root.children())
|
|
{
|
|
auto serial = StringUtil::toLower(std::string(n.key().str, n.key().len));
|
|
|
|
// Serials and CRCs must be inserted as lower-case, as that is how they are retrieved
|
|
// this is because the application may pass a lowercase CRC or serial along
|
|
//
|
|
// However, YAML's keys are as expected case-sensitive, so we have to explicitly do our own duplicate checking
|
|
if (s_game_db.count(serial) == 1)
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial));
|
|
continue;
|
|
}
|
|
if (n.is_map())
|
|
{
|
|
parseAndInsert(serial, n);
|
|
}
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Console.Error(fmt::format("[GameDB] Error occured when initializing GameDB: {}", e.what()));
|
|
}
|
|
ryml::reset_callbacks();
|
|
}
|
|
|
|
void GameDatabase::ensureLoaded()
|
|
{
|
|
std::call_once(s_load_once_flag, []() {
|
|
Common::Timer timer;
|
|
Console.WriteLn(fmt::format("[GameDB] Has not been initialized yet, initializing..."));
|
|
initDatabase();
|
|
Console.WriteLn("[GameDB] %zu games on record (loaded in %.2fms)", s_game_db.size(), timer.GetTimeMilliseconds());
|
|
});
|
|
}
|
|
|
|
const GameDatabaseSchema::GameEntry* GameDatabase::findGame(const std::string_view& serial)
|
|
{
|
|
GameDatabase::ensureLoaded();
|
|
|
|
std::string serialLower = StringUtil::toLower(serial);
|
|
Console.WriteLn(fmt::format("[GameDB] Searching for '{}' in GameDB", serialLower));
|
|
const auto gameEntry = s_game_db.find(serialLower);
|
|
if (gameEntry != s_game_db.end())
|
|
{
|
|
Console.WriteLn(fmt::format("[GameDB] Found '{}' in GameDB", serialLower));
|
|
return &gameEntry->second;
|
|
}
|
|
|
|
Console.Error(fmt::format("[GameDB] Could not find '{}' in GameDB", serialLower));
|
|
return nullptr;
|
|
}
|