Files
ARMSX2/pcsx2/GameDatabase.cpp
T
Connor McLaughlin 0f24048cd5 GameDatabase: Add gpuPaletteConversion = 2 value
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.
2022-09-15 12:24:43 +01:00

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;
}