Files
ARMSX2/pcsx2/GameDatabase.cpp
Brian Degenhardt a6d1e35748 Settings: copying global settings into a game writes only the real decisions
"Copy Global Settings" does not copy the settings you can see. It runs the whole
configuration through a wrapper that writes every key unconditionally, so the file
it leaves behind holds roughly seven hundred of them — network adapters, the
debugger, trace logging, memory cards, sections no settings page ever shows.

That was untidy and no worse, until a key present in a per-game file started
meaning the player claimed it. Now one press of a button whose dialog promises only
that "the configuration for this game will be replaced by the current global
settings" turns off every automatic fix that game had, permanently and silently.

A value is worth writing down only if it decides something, and there are two ways
it can fail to. It can be the stock default, in which case the file carries it as
noise. Or it can be what the game database is going to set anyway, in which case
writing it can only become a claim that suppresses the fix it agrees with. So the
copy now excludes both, and what lands is what the player actually chose.

The comparison goes through the string form rather than the typed value, so a float
or an enum name compares the way it will be stored rather than the way it happens to
sit in memory. That is why the references are built with the same interface class:
same formatting on both sides, exact comparison, one path for every type.

The database reference is a default configuration with the entry applied, not this
one with the entry applied. The question is what the database wants, not where it
would leave the source. It matters for the handful of fixes that clamp rather than
assign, and it errs towards writing the player's value — never towards dropping a
fix, since a value is only skipped when it already equals what the fix would set.

Working the reference out means running the apply functions for an outcome nobody is
going to run with, so they take an apply mode. A hypothetical apply says nothing to
the log, raises none of the recommendation messages, and does not allocate the four
megabyte lookup table that the Goemon TLB fix asks for.

The tests cover the precedence rule and the filter, but the ones that matter are the
drift guards: they assert every gamefix, speedhack and clamp mode has a settings key,
and that the only graphics fixes without one are the six that genuinely have no
setting behind them — three renderer routine selectors and three that only raise a
recommendation. A knob nobody maps is a setting that goes quietly back to being
overridden, with no warning and no failure, and that is what these are here to catch.
2026-08-16 14:21:25 -07:00

1536 lines
50 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GameDatabase.h"
#include "GS/GS.h"
#include "Host.h"
#include "IconsFontAwesome.h"
#include "vtlb.h"
#include "common/Console.h"
#include "common/EnumOps.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "common/YAML.h"
#include <sstream>
#include "fmt/format.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 populateEntry(GameDatabaseSchema::GameEntry& gameEntry, const std::string_view serial,
const ryml::NodeRef& node, bool is_override);
static void parseAndInsert(const std::string_view serial, const ryml::NodeRef& node);
static void mergeOverride(const std::string_view serial, const ryml::NodeRef& node);
static void loadFile(const std::string& path, const std::string& name, bool is_override);
static void initDatabase();
} // namespace GameDatabase
static constexpr char GAMEDB_YAML_FILE_NAME[] = "GameIndex.yaml";
// ARMSX2 GameDB overrides — layered on top of upstream GameIndex.yaml after it
// loads. Entries either add NEW serials upstream doesn't cover, or PARTIAL-
// override existing upstream entries: scalar fields replace upstream when
// present; list fields (gameFixes / speedHacks / gsHWFixes / memcardFilters /
// patches / dynaPatches) get cleared when the override specifies them so we
// REPLACE rather than append. The file is optional — missing is a silent skip.
static constexpr char GAMEDB_OVERRIDE_YAML_FILE_NAME[] = "armsx2_overrides.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
{
if (crc == 0)
return nullptr;
auto it = patches.find(crc);
if (it != patches.end())
return &it->second;
it = patches.find(0);
if (it != patches.end())
return &it->second;
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::populateEntry(GameDatabaseSchema::GameEntry& gameEntry, const std::string_view serial,
const ryml::NodeRef& node, bool is_override)
{
if (node.has_child("name"))
{
node["name"] >> gameEntry.name;
}
if (node.has_child("name-sort"))
{
node["name-sort"] >> gameEntry.name_sort;
}
if (node.has_child("name-en"))
{
node["name-en"] >> gameEntry.name_en;
}
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;
if (eeVal >= 0 && eeVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.eeRoundMode = static_cast<FPRoundMode>(eeVal);
else
Console.Error(fmt::format("GameDB: Invalid EE round mode '{}', specified for serial: '{}'.", eeVal, serial));
}
if (node["roundModes"].has_child("eeDivRoundMode"))
{
int eeVal = -1;
node["roundModes"]["eeDivRoundMode"] >> eeVal;
if (eeVal >= 0 && eeVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.eeDivRoundMode = static_cast<FPRoundMode>(eeVal);
else
Console.Error(fmt::format("GameDB: Invalid EE division round mode '{}', specified for serial: '{}'.", eeVal, serial));
}
if (node["roundModes"].has_child("vuRoundMode"))
{
int vuVal = -1;
node["roundModes"]["vuRoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
{
gameEntry.vu0RoundMode = static_cast<FPRoundMode>(vuVal);
gameEntry.vu1RoundMode = static_cast<FPRoundMode>(vuVal);
}
else
{
Console.Error(fmt::format("GameDB: Invalid VU round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
}
if (node["roundModes"].has_child("vu0RoundMode"))
{
int vuVal = -1;
node["roundModes"]["vu0RoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.vu0RoundMode = static_cast<FPRoundMode>(vuVal);
else
Console.Error(fmt::format("GameDB: Invalid VU0 round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
if (node["roundModes"].has_child("vu1RoundMode"))
{
int vuVal = -1;
node["roundModes"]["vu1RoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.vu1RoundMode = static_cast<FPRoundMode>(vuVal);
else
Console.Error(fmt::format("GameDB: Invalid VU1 round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
}
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.vu0ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
gameEntry.vu1ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
if (node["clampModes"].has_child("vu0ClampMode"))
{
int vuVal = -1;
node["clampModes"]["vu0ClampMode"] >> vuVal;
gameEntry.vu0ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
if (node["clampModes"].has_child("vu1ClampMode"))
{
int vuVal = -1;
node["clampModes"]["vu1ClampMode"] >> vuVal;
gameEntry.vu1ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
}
// Validate game fixes, invalid ones will be dropped!
if (node.has_child("gameFixes") && node["gameFixes"].has_children())
{
// Override REPLACES upstream's list, it does not append.
if (is_override)
gameEntry.gameFixes.clear();
for (const auto& 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 (fix.ends_with("Hack"))
{
fix.erase(fix.size() - 4);
for (GamefixId id = GamefixId_FIRST; id < GamefixId_COUNT; id = static_cast<GamefixId>(enum_cast(id) + 1))
{
if (fix.compare(Pcsx2Config::GamefixOptions::GetGameFixName(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));
}
}
}
if (node.has_child("speedHacks") && node["speedHacks"].has_children())
{
if (is_override)
gameEntry.speedHacks.clear();
for (const auto& n : node["speedHacks"].children())
{
const std::string_view id_view = std::string_view(n.key().str, n.key().len);
const std::string_view value_view = std::string_view(n.val().str, n.val().len);
const std::optional<SpeedHack> id = Pcsx2Config::SpeedhackOptions::ParseSpeedHackName(id_view);
const std::optional<int> value = StringUtil::FromChars<int>(value_view);
if (id.has_value() && value.has_value() &&
std::none_of(gameEntry.speedHacks.begin(), gameEntry.speedHacks.end(),
[&id](const auto& it) { return it.first == id.value(); }))
{
gameEntry.speedHacks.emplace_back(id.value(), value.value());
}
else
{
Console.Error(fmt::format("GameDB: Invalid speedhack: '{}={}', specified for serial: '{}'. Dropping!",
id_view, value_view, serial));
}
}
}
if (node.has_child("gsHWFixes"))
{
if (is_override)
gameEntry.gsHWFixes.clear();
for (const auto& 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;
if (id.has_value() && (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount ||
id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw ||
id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler))
{
const std::string_view str_value(n.has_val() ? std::string_view(n.val().data(), n.val().size()) : std::string_view());
if (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount)
value = GSLookupGetSkipCountFunctionId(str_value);
else if (id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw)
value = GSLookupBeforeDrawFunctionId(str_value);
else if (id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler)
value = GSLookupMoveHandlerFunctionId(str_value);
if (value.value_or(-1) < 0)
{
Console.Error(fmt::format("GameDB: Invalid GS HW Fix Value for '{}' in '{}': '{}'", id_name, serial, str_value));
continue;
}
}
else
{
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(fmt::format("GameDB: Invalid GS HW Fix: '{}' specified for serial '{}'. Dropping!", id_name, serial));
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())
{
if (is_override)
gameEntry.memcardFilters.clear();
for (const auto& 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())
{
// Patches are keyed by CRC — clearing on override means upstream's
// CRC-keyed patches are dropped when the override ships its own block.
if (is_override)
gameEntry.patches.clear();
for (const auto& 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));
}
}
if (node.has_child("dynaPatches") && node["dynaPatches"].has_children())
{
if (is_override)
gameEntry.dynaPatches.clear();
for (const auto& n : node["dynaPatches"].children())
{
Patch::DynamicPatch patch;
if (n.has_child("pattern") && n["pattern"].has_children())
{
for (const auto& db_pattern : n["pattern"].children())
{
Patch::DynamicPatchEntry entry;
db_pattern["offset"] >> entry.offset;
db_pattern["value"] >> entry.value;
patch.pattern.push_back(entry);
}
for (const auto& db_replacement : n["replacement"].children())
{
Patch::DynamicPatchEntry entry;
db_replacement["offset"] >> entry.offset;
db_replacement["value"] >> entry.value;
patch.replacement.push_back(entry);
}
}
gameEntry.dynaPatches.push_back(patch);
}
}
}
void GameDatabase::parseAndInsert(const std::string_view serial, const ryml::NodeRef& node)
{
GameDatabaseSchema::GameEntry gameEntry;
populateEntry(gameEntry, serial, node, /*is_override=*/false);
s_game_db.emplace(std::string(serial), std::move(gameEntry));
}
void GameDatabase::mergeOverride(const std::string_view serial, const ryml::NodeRef& node)
{
// Fetch-or-create: when the serial already exists (a PARTIAL override of
// upstream), the override's scalars replace and its list fields clear-then-
// replace; when it's a NEW serial, this creates it from scratch, same shape
// as a normal entry.
auto& gameEntry = s_game_db[std::string(serial)];
populateEntry(gameEntry, serial, node, /*is_override=*/true);
}
static const char* s_round_modes[static_cast<u32>(FPRoundMode::MaxCount)] = {
"Nearest",
"NegativeInfinity",
"PositiveInfinity",
"Chop"};
static const char* s_gs_hw_fix_names[] = {
"autoFlush",
"cpuFramebufferConversion",
"readTCOnClose",
"disableDepthSupport",
"preloadFrameData",
"disablePartialInvalidation",
"textureInsideRT",
"limit24BitDepth",
"alignSprite",
"mergeSprite",
"mipmap",
"accurateAlphaTest",
"forceEvenSpritePosition",
"bilinearUpscale",
"nativePaletteDraw",
"estimateTextureRegion",
"drawBuffering",
"PCRTCOffsets",
"PCRTCOverscan",
"coalesceRenderPasses",
"trilinearFiltering",
"skipDrawStart",
"skipDrawEnd",
"halfPixelOffset",
"roundSprite",
"nativeScaling",
"texturePreloading",
"deinterlace",
"cpuSpriteRenderBW",
"cpuSpriteRenderLevel",
"cpuCLUTRender",
"gpuTargetCLUT",
"gpuPaletteConversion",
"minimumBlendingLevel",
"maximumBlendingLevel",
"recommendedBlendingLevel",
"recommendedAccurateAlphaTest",
"recommendedHWAA1",
"hwDownloadMode",
"getSkipCount",
"beforeDraw",
"moveHandler",
};
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::TrilinearFiltering:
case GSHWFixId::MinimumBlendingLevel:
case GSHWFixId::MaximumBlendingLevel:
case GSHWFixId::RecommendedBlendingLevel:
case GSHWFixId::PCRTCOffsets:
case GSHWFixId::PCRTCOverscan:
case GSHWFixId::GetSkipCount:
case GSHWFixId::BeforeDraw:
case GSHWFixId::MoveHandler:
return false;
default:
return true;
}
}
// Which fixes a player can claim for themselves. Anything not listed keeps the old
// behaviour, so the database still wins on the ones no UI exposes.
static std::optional<GSUserHackOverride> UserHackOverrideForHWFix(GameDatabaseSchema::GSHWFixId id)
{
switch (id)
{
case GameDatabaseSchema::GSHWFixId::AlignSprite:
return GSUserHackOverride::AlignSprite;
case GameDatabaseSchema::GSHWFixId::MergeSprite:
return GSUserHackOverride::MergeSprite;
case GameDatabaseSchema::GSHWFixId::RoundSprite:
return GSUserHackOverride::RoundSprite;
case GameDatabaseSchema::GSHWFixId::HalfPixelOffset:
return GSUserHackOverride::HalfPixelOffset;
case GameDatabaseSchema::GSHWFixId::ForceEvenSpritePosition:
return GSUserHackOverride::ForceEvenSpritePosition;
case GameDatabaseSchema::GSHWFixId::NativeScaling:
return GSUserHackOverride::NativeScaling;
case GameDatabaseSchema::GSHWFixId::NativePaletteDraw:
return GSUserHackOverride::NativePaletteDraw;
case GameDatabaseSchema::GSHWFixId::BilinearUpscale:
return GSUserHackOverride::BilinearHack;
case GameDatabaseSchema::GSHWFixId::AutoFlush:
return GSUserHackOverride::AutoFlush;
case GameDatabaseSchema::GSHWFixId::TextureInsideRT:
return GSUserHackOverride::TextureInsideRt;
case GameDatabaseSchema::GSHWFixId::PreloadFrameData:
return GSUserHackOverride::PreloadFrameData;
case GameDatabaseSchema::GSHWFixId::DisablePartialInvalidation:
return GSUserHackOverride::DisablePartialInvalidation;
case GameDatabaseSchema::GSHWFixId::GPUPaletteConversion:
return GSUserHackOverride::GPUPaletteConversion;
case GameDatabaseSchema::GSHWFixId::DisableDepthSupport:
return GSUserHackOverride::DisableDepthSupport;
case GameDatabaseSchema::GSHWFixId::CPUFramebufferConversion:
return GSUserHackOverride::CPUFBConversion;
case GameDatabaseSchema::GSHWFixId::Limit24BitDepth:
return GSUserHackOverride::Limit24BitDepth;
case GameDatabaseSchema::GSHWFixId::EstimateTextureRegion:
return GSUserHackOverride::EstimateTextureRegion;
case GameDatabaseSchema::GSHWFixId::DrawBuffering:
return GSUserHackOverride::DrawBuffering;
case GameDatabaseSchema::GSHWFixId::CPUSpriteRenderBW:
return GSUserHackOverride::CPUSpriteRenderBW;
case GameDatabaseSchema::GSHWFixId::CPUSpriteRenderLevel:
return GSUserHackOverride::CPUSpriteRenderLevel;
case GameDatabaseSchema::GSHWFixId::CPUCLUTRender:
return GSUserHackOverride::CPUCLUTRender;
case GameDatabaseSchema::GSHWFixId::GPUTargetCLUT:
return GSUserHackOverride::GPUTargetCLUT;
default:
return std::nullopt;
}
}
void GameDatabaseSchema::GameEntry::applyGameFixes(
Pcsx2Config& config, bool applyAuto, const PerGameOverrides& overrides, ApplyMode mode) const
{
std::string suppressed_fixes;
const bool quiet = (mode == ApplyMode::Hypothetical);
// Only apply core game fixes if the user has enabled them.
if (!applyAuto && !quiet)
Console.Warning("GameDB: Game Fixes are disabled");
// Decides whether one database value lands, and says why when it does not. Two
// reasons are possible and they are not the same thing: the global switch is off,
// or the player set this particular knob for this particular game. Only the second
// is worth putting on screen — the first they already know, they threw it.
//
// Names are the database's own YAML spelling so a log line and a GameIndex entry
// read together.
const auto wanted = [&](bool claimed, const char* name, std::optional<int> value = std::nullopt) {
if (quiet)
return applyAuto && !claimed;
const std::string label = value.has_value() ? fmt::format("{} = {}", name, value.value()) : std::string(name);
if (claimed)
{
Console.Warning("GameDB: Skipping %s, the per-game setting wins", label.c_str());
fmt::format_to(std::back_inserter(suppressed_fixes), "{}{}", suppressed_fixes.empty() ? " " : "\n ", label);
return false;
}
if (!applyAuto)
{
Console.Warning("GameDB: Skipping %s", label.c_str());
return false;
}
return true;
};
if (eeRoundMode < FPRoundMode::MaxCount &&
wanted(overrides.Has(CoreGameDBKnob::EERoundMode), "eeRoundMode", static_cast<int>(eeRoundMode)))
{
if (!quiet)
Console.WriteLn("GameDB: Changing EE/FPU roundmode to %d [%s]", eeRoundMode, s_round_modes[static_cast<u8>(eeRoundMode)]);
config.Cpu.FPUFPCR.SetRoundMode(eeRoundMode);
}
if (eeDivRoundMode < FPRoundMode::MaxCount &&
wanted(overrides.Has(CoreGameDBKnob::EEDivRoundMode), "eeDivRoundMode", static_cast<int>(eeDivRoundMode)))
{
if (!quiet)
{
Console.WriteLn("GameDB: Changing EE/FPU divison roundmode to %d [%s]", eeDivRoundMode,
s_round_modes[static_cast<u8>(eeDivRoundMode)]);
}
config.Cpu.FPUDivFPCR.SetRoundMode(eeDivRoundMode);
}
if (vu0RoundMode < FPRoundMode::MaxCount &&
wanted(overrides.Has(CoreGameDBKnob::VU0RoundMode), "vu0RoundMode", static_cast<int>(vu0RoundMode)))
{
if (!quiet)
Console.WriteLn("GameDB: Changing VU0 roundmode to %d [%s]", vu0RoundMode, s_round_modes[static_cast<u8>(vu0RoundMode)]);
config.Cpu.VU0FPCR.SetRoundMode(vu0RoundMode);
}
if (vu1RoundMode < FPRoundMode::MaxCount &&
wanted(overrides.Has(CoreGameDBKnob::VU1RoundMode), "vu1RoundMode", static_cast<int>(vu1RoundMode)))
{
if (!quiet)
Console.WriteLn("GameDB: Changing VU1 roundmode to %d [%s]", vu1RoundMode, s_round_modes[static_cast<u8>(vu1RoundMode)]);
config.Cpu.VU1FPCR.SetRoundMode(vu1RoundMode);
}
if (eeClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(eeClampMode);
if (wanted(overrides.Has(CoreGameDBKnob::EEClampMode), "eeClampMode", clampMode))
{
if (!quiet)
Console.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);
config.Cpu.Recompiler.fpuExactMode = (clampMode >= 4);
}
}
if (vu0ClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(vu0ClampMode);
if (wanted(overrides.Has(CoreGameDBKnob::VU0ClampMode), "vu0ClampMode", clampMode))
{
if (!quiet)
Console.WriteLn("GameDB: Changing VU0 clamp mode [mode=%d]", clampMode);
config.Cpu.Recompiler.vu0Overflow = (clampMode >= 1);
config.Cpu.Recompiler.vu0ExtraOverflow = (clampMode >= 2);
config.Cpu.Recompiler.vu0SignOverflow = (clampMode >= 3);
}
}
if (vu1ClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(vu1ClampMode);
if (wanted(overrides.Has(CoreGameDBKnob::VU1ClampMode), "vu1ClampMode", clampMode))
{
if (!quiet)
Console.WriteLn("GameDB: Changing VU1 clamp mode [mode=%d]", clampMode);
config.Cpu.Recompiler.vu1Overflow = (clampMode >= 1);
config.Cpu.Recompiler.vu1ExtraOverflow = (clampMode >= 2);
config.Cpu.Recompiler.vu1SignOverflow = (clampMode >= 3);
}
}
// TODO - config - this could be simplified with maps instead of bitfields and enums
for (const auto& it : speedHacks)
{
if (!wanted(overrides.Has(it.first), Pcsx2Config::SpeedhackOptions::GetSpeedHackName(it.first), it.second))
continue;
// Legacy note - speedhacks are setup in the GameDB as integer values, but
// are effectively booleans like the gamefixes
config.Speedhacks.Set(it.first, it.second);
if (!quiet)
{
Console.WriteLn("GameDB: Setting Speedhack '%s' to [mode=%d]",
Pcsx2Config::SpeedhackOptions::GetSpeedHackName(it.first), it.second);
}
}
// TODO - config - this could be simplified with maps instead of bitfields and enums
for (const GamefixId id : gameFixes)
{
// A database gamefix is only ever switched on, so a player who turned one on
// per-game already got their way. The claim matters for the other direction:
// turning one off, which until now the database silently undid.
if (!wanted(overrides.Has(id), Pcsx2Config::GamefixOptions::GetGameFixName(id)))
continue;
// if the fix is present, it is said to be enabled
config.Gamefixes.Set(id, true);
if (!quiet)
Console.WriteLn("GameDB: Enabled Gamefix: %s", Pcsx2Config::GamefixOptions::GetGameFixName(id));
// The LUT is only used for 1 game so we allocate it only when the gamefix is enabled (save 4MB).
// A hypothetical apply is not going to run anything, so it does not get the 4MB.
if (id == Fix_GoemonTlbMiss && !quiet)
vtlb_Alloc_Ppmap();
}
// Keyed so the next settings reload replaces or clears it, matching how the
// graphics half reports the same thing.
if (quiet)
return;
if (!suppressed_fixes.empty())
{
Host::AddKeyedOSDMessage("CoreFixesWarning",
fmt::format(ICON_FA_WAND_MAGIC_SPARKLES " {}\n{}",
TRANSLATE_SV("GameDatabase", "Your own choice was kept for these settings, so the automatic ones were not applied:"),
suppressed_fixes),
Host::OSD_ERROR_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("CoreFixesWarning");
}
}
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::FlushTCOnClose:
return (static_cast<int>(config.UserHacks_ReadTCOnClose) == value);
case GSHWFixId::DisableDepthSupport:
return (static_cast<int>(config.UserHacks_DisableDepthSupport) == 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::Limit24BitDepth:
return (static_cast<int>(config.UserHacks_Limit24BitDepth) == value);
case GSHWFixId::AlignSprite:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_AlignSpriteX) == value);
case GSHWFixId::MergeSprite:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_MergePPSprite) == value);
case GSHWFixId::ForceEvenSpritePosition:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_ForceEvenSpritePosition) == value);
case GSHWFixId::BilinearUpscale:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_BilinearHack) == value);
case GSHWFixId::NativePaletteDraw:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_NativePaletteDraw) == value);
case GSHWFixId::EstimateTextureRegion:
return (static_cast<int>(config.UserHacks_EstimateTextureRegion) == value);
case GSHWFixId::DrawBuffering:
return (static_cast<int>(config.UserHacks_DrawBuffering) == value);
case GSHWFixId::PCRTCOffsets:
return (static_cast<int>(config.PCRTCOffsets) == value);
case GSHWFixId::PCRTCOverscan:
return (static_cast<int>(config.PCRTCOverscan) == value);
case GSHWFixId::CoalesceRenderPasses:
return (static_cast<int>(config.CoalesceRenderPasses) == value);
case GSHWFixId::Mipmap:
return (static_cast<int>(config.HWMipmap) == value);
case GSHWFixId::AccurateAlphaTest:
return (static_cast<int>(config.HWAccurateAlphaTest) == value);
case GSHWFixId::TrilinearFiltering:
return (config.TriFilter == TriFiltering::Automatic || static_cast<int>(config.TriFilter) == value);
case GSHWFixId::SkipDrawStart:
return (config.SkipDrawStart == value);
case GSHWFixId::SkipDrawEnd:
return (config.SkipDrawEnd == value);
case GSHWFixId::HalfPixelOffset:
return (config.UpscaleMultiplier <= 1.0f || config.UserHacks_HalfPixelOffset == static_cast<GSHalfPixelOffset>(value));
case GSHWFixId::RoundSprite:
return (config.UpscaleMultiplier <= 1.0f || config.UserHacks_RoundSprite == value);
case GSHWFixId::NativeScaling:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_NativeScaling) == 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::HWDownloadMode:
// A non-default user choice already "matches" (we never override it — see the apply switch).
return (config.HWDownloadMode != GSHardwareDownloadMode::Enabled || static_cast<int>(config.HWDownloadMode) == value);
case GSHWFixId::CPUSpriteRenderBW:
return (config.UserHacks_CPUSpriteRenderBW == value);
case GSHWFixId::CPUSpriteRenderLevel:
return (config.UserHacks_CPUSpriteRenderLevel == value);
case GSHWFixId::CPUCLUTRender:
return (config.UserHacks_CPUCLUTRender == value);
case GSHWFixId::GPUTargetCLUT:
return (static_cast<int>(config.UserHacks_GPUTargetCLUTMode) == value);
case GSHWFixId::GPUPaletteConversion:
return (config.GPUPaletteConversion == ((value > 1) ? (config.TexturePreloading == TexturePreloadingLevel::Full) : (value != 0)));
case GSHWFixId::MinimumBlendingLevel:
return (static_cast<int>(config.AccurateBlendingUnit) >= value);
case GSHWFixId::MaximumBlendingLevel:
return (static_cast<int>(config.AccurateBlendingUnit) <= value);
case GSHWFixId::RecommendedBlendingLevel:
return true;
case GSHWFixId::GetSkipCount:
return (static_cast<int>(config.GetSkipCountFunctionId) == value);
case GSHWFixId::BeforeDraw:
return (static_cast<int>(config.BeforeDrawFunctionId) == value);
case GSHWFixId::MoveHandler:
return (static_cast<int>(config.MoveHandlerFunctionId) == value);
default:
return false;
}
}
void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(
Pcsx2Config::GSOptions& config, const PerGameOverrides& overrides, ApplyMode mode) const
{
std::string disabled_fixes;
const bool quiet = (mode == ApplyMode::Hypothetical);
// Only apply GS HW fixes if the user hasn't manually enabled HW fixes.
const bool apply_auto_fixes = !config.ManualUserHacks;
const bool is_sw_renderer = EmuConfig.GS.Renderer == GSRendererType::SW;
if (!apply_auto_fixes && !quiet)
Console.Warning("GameDB: Manual GS hardware renderer fixes are enabled, not using automatic hardware renderer fixes from GameDB.");
for (const auto& [id, value] : gsHWFixes)
{
// A pin is manual mode for one fix instead of all of them, so it takes the same
// road out: the player's value stays and this one gets named in the warning.
//
// Two vocabularies reach the same conclusion here. The stored UserHackOverrides
// mask only names the classic user hacks, and is what MaskUserHacks() and an
// already-written INI speak. The per-game overrides name every fix a settings
// key exists for, which is a wider set, and is how a setting like the mipmap or
// deinterlace picker gets claimed at all.
const std::optional<GSUserHackOverride> pin = UserHackOverrideForHWFix(id);
const bool pinned = (pin.has_value() && config.IsUserHackPinned(pin.value())) || overrides.Has(id);
// The pin is tested first and on its own. Nesting it under isUserHackHWFix()
// would leave the settings that were never user hacks — mipmapping, trilinear,
// deinterlacing, texture preloading, blend level, download mode — with no way
// to be claimed, which is most of what a player actually changes.
if (pinned || (isUserHackHWFix(id) && !apply_auto_fixes))
{
if (configMatchesHWFix(config, id, value) || quiet)
continue;
Console.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:
{
if (value >= 0 && value <= static_cast<int>(GSHWAutoFlushLevel::Enabled))
config.UserHacks_AutoFlush = static_cast<GSHWAutoFlushLevel>(value);
}
break;
case GSHWFixId::CPUFramebufferConversion:
config.UserHacks_CPUFBConversion = (value > 0);
break;
case GSHWFixId::FlushTCOnClose:
config.UserHacks_ReadTCOnClose = (value > 0);
break;
case GSHWFixId::DisableDepthSupport:
config.UserHacks_DisableDepthSupport = (value > 0);
break;
case GSHWFixId::PreloadFrameData:
config.PreloadFrameWithGSData = (value > 0);
break;
case GSHWFixId::HWDownloadMode:
// GameDB carries the recommended GS Hardware Download Mode as a DEFAULT only:
// it fills in for games the player hasn't touched, but must never override a
// deliberate choice (their per-game/global Hardware Download Mode wins, since
// LoadCoreSettings runs before this). So only apply when the current value is
// still the default (Enabled). Enable manual HW fixes to force Accurate back.
// The bound is a RANGE check over the raw wire value, not an ordering by
// accuracy (see the GSHardwareDownloadMode comment in Config.h) — so it has
// to name the LAST enumerator. Asynchronous was appended after Disabled, so
// bounding at Disabled silently discarded a GameDB entry asking for it: the
// parser accepted the value and this dropped it with no diagnostic.
if (config.HWDownloadMode == GSHardwareDownloadMode::Enabled &&
value > static_cast<int>(GSHardwareDownloadMode::Enabled) &&
value <= static_cast<int>(GSHardwareDownloadMode::Asynchronous))
{
config.HWDownloadMode = static_cast<GSHardwareDownloadMode>(value);
}
break;
case GSHWFixId::DisablePartialInvalidation:
config.UserHacks_DisablePartialInvalidation = (value > 0);
break;
case GSHWFixId::TextureInsideRT:
{
if (value >= 0 && value <= static_cast<int>(GSTextureInRtMode::MergeTargets))
config.UserHacks_TextureInsideRt = static_cast<GSTextureInRtMode>(value);
}
break;
case GSHWFixId::Limit24BitDepth:
{
if (value >= 0 && value <= static_cast<int>(GSLimit24BitDepth::PrioritizeLower))
config.UserHacks_Limit24BitDepth = static_cast<GSLimit24BitDepth>(value);
}
break;
case GSHWFixId::AlignSprite:
config.UserHacks_AlignSpriteX = (value > 0);
break;
case GSHWFixId::MergeSprite:
config.UserHacks_MergePPSprite = (value > 0);
break;
case GSHWFixId::ForceEvenSpritePosition:
config.UserHacks_ForceEvenSpritePosition = (value > 0);
break;
case GSHWFixId::BilinearUpscale:
{
if (value >= 0 && value < static_cast<int>(GSBilinearDirtyMode::MaxCount))
config.UserHacks_BilinearHack = static_cast<GSBilinearDirtyMode>(value);
}
break;
case GSHWFixId::NativePaletteDraw:
config.UserHacks_NativePaletteDraw = (value > 0);
break;
case GSHWFixId::EstimateTextureRegion:
config.UserHacks_EstimateTextureRegion = (value > 0);
break;
case GSHWFixId::DrawBuffering:
config.UserHacks_DrawBuffering = (value > 0);
break;
case GSHWFixId::PCRTCOffsets:
config.PCRTCOffsets = (value > 0);
break;
case GSHWFixId::PCRTCOverscan:
config.PCRTCOverscan = (value > 0);
break;
case GSHWFixId::CoalesceRenderPasses:
config.CoalesceRenderPasses = (value > 0);
break;
case GSHWFixId::Mipmap:
config.HWMipmap = (value > 0);
break;
case GSHWFixId::AccurateAlphaTest:
config.HWAccurateAlphaTest = (value > 0);
break;
case GSHWFixId::TrilinearFiltering:
{
if (value >= 0 && value <= static_cast<int>(TriFiltering::Forced))
{
if (config.TriFilter == TriFiltering::Automatic)
config.TriFilter = static_cast<TriFiltering>(value);
else if (config.TriFilter > TriFiltering::Off)
if (!quiet)
Console.Warning("GameDB: Game requires trilinear filtering to be disabled.");
}
}
break;
case GSHWFixId::SkipDrawStart:
config.SkipDrawStart = value;
break;
case GSHWFixId::SkipDrawEnd:
config.SkipDrawEnd = value;
break;
case GSHWFixId::HalfPixelOffset:
{
if (value >= 0 && value < static_cast<int>(GSHalfPixelOffset::MaxCount))
config.UserHacks_HalfPixelOffset = static_cast<GSHalfPixelOffset>(value);
}
break;
case GSHWFixId::RoundSprite:
config.UserHacks_RoundSprite = value;
break;
case GSHWFixId::NativeScaling:
config.UserHacks_NativeScaling = static_cast<GSNativeScaling>(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 >= static_cast<int>(GSInterlaceMode::Automatic) && value < static_cast<int>(GSInterlaceMode::Count))
{
if (config.InterlaceMode == GSInterlaceMode::Automatic)
config.InterlaceMode = static_cast<GSInterlaceMode>(value);
else
if (!quiet)
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::CPUSpriteRenderLevel:
config.UserHacks_CPUSpriteRenderLevel = value;
break;
case GSHWFixId::CPUCLUTRender:
config.UserHacks_CPUCLUTRender = value;
break;
case GSHWFixId::GPUTargetCLUT:
{
if (value >= 0 && value <= static_cast<int>(GSGPUTargetCLUTMode::InsideTarget))
config.UserHacks_GPUTargetCLUTMode = static_cast<GSGPUTargetCLUTMode>(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;
case GSHWFixId::MinimumBlendingLevel:
{
if (value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum))
config.AccurateBlendingUnit = std::max(config.AccurateBlendingUnit, static_cast<AccBlendLevel>(value));
}
break;
case GSHWFixId::MaximumBlendingLevel:
{
if (value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum))
config.AccurateBlendingUnit = std::min(config.AccurateBlendingUnit, static_cast<AccBlendLevel>(value));
}
break;
case GSHWFixId::RecommendedBlendingLevel:
{
if (quiet)
break;
if (!is_sw_renderer && value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum) && static_cast<int>(EmuConfig.GS.AccurateBlendingUnit) < value)
{
static constexpr std::array<const char*, static_cast<u8>(AccBlendLevel::MaxCount)> s_blending_option_names = {{
TRANSLATE_NOOP("GameDatabase", "Minimum"),
TRANSLATE_NOOP("GameDatabase", "Basic"),
TRANSLATE_NOOP("GameDatabase", "Medium"),
TRANSLATE_NOOP("GameDatabase", "High"),
TRANSLATE_NOOP("GameDatabase", "Full"),
TRANSLATE_NOOP("GameDatabase", "Maximum"),
}};
Host::AddKeyedOSDMessage("HWBlendingWarning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} Current Blending Accuracy is {1}.\n"
"Recommended Blending Accuracy for this game is {2}.\n"
"You can adjust the blending level in Game Properties to improve\n"
"graphical quality, but this will increase system requirements."),
ICON_FA_PAINTBRUSH,
s_blending_option_names[static_cast<u8>(EmuConfig.GS.AccurateBlendingUnit)],
s_blending_option_names[static_cast<u8>(value)]),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWBlendingWarning");
}
}
break;
case GSHWFixId::RecommendedAccurateAlphaTest:
{
if (quiet)
break;
if (!is_sw_renderer && value >= 0 && value <= 1 &&
static_cast<int>(config.HWAccurateAlphaTest) < value)
{
Host::AddKeyedOSDMessage("HWAlphaTestWarning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} Accurate Alpha Test is currently disabled.\n"
"This game recommends enabling Accurate Alpha Test.\n"
"You can enable it in Game Properties to improve graphical\n"
"accuracy, but this may increase system requirements."),
ICON_FA_PAINTBRUSH),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWAlphaTestWarning");
}
}
break;
case GSHWFixId::RecommendedHWAA1:
{
if (quiet)
break;
if (!is_sw_renderer && value >= 0 && value <= 1 &&
static_cast<int>(config.HWAA1) < value)
{
Host::AddKeyedOSDMessage("HWAA1Warning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} AA1 is currently disabled.\n"
"This game recommends enabling AA1.\n"
"You can enable it in Game Properties to improve graphical\n"
"accuracy, but this may increase system requirements."),
ICON_FA_PAINTBRUSH),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWAA1Warning");
}
}
break;
case GSHWFixId::GetSkipCount:
config.GetSkipCountFunctionId = static_cast<s16>(value);
break;
case GSHWFixId::BeforeDraw:
config.BeforeDrawFunctionId = static_cast<s16>(value);
break;
case GSHWFixId::MoveHandler:
config.MoveHandlerFunctionId = static_cast<s16>(value);
break;
default:
break;
}
if (!quiet)
Console.WriteLn("GameDB: Enabled GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value);
}
// 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);
if (quiet)
return;
if (!is_sw_renderer && !disabled_fixes.empty())
{
// Pinning a single hack lands here too, and then blaming manual mode would be a lie.
const std::string_view reason = apply_auto_fixes ?
TRANSLATE_SV("GameDatabase", "Your own choice was kept for these graphics fixes, so the automatic ones were not applied:") :
TRANSLATE_SV("GameDatabase", "Manual GS hardware renderer fixes are enabled, automatic fixes were not applied:");
Host::AddKeyedOSDMessage("HWFixesWarning",
fmt::format(ICON_FA_WAND_MAGIC_SPARKLES " {}\n{}", reason, disabled_fixes),
Host::OSD_ERROR_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWFixesWarning");
}
}
void GameDatabase::loadFile(const std::string& path, const std::string& name, bool is_override)
{
const std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
// Override file is optional — silent skip when not shipped.
// Upstream is mandatory — keep the error loud.
if (!is_override)
Console.Error("GameDB: Unable to open GameDB file, file does not exist.");
return;
}
const ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error, true);
if (!tree.has_value())
{
Console.ErrorFmt("GameDB: Failed to parse game database file {}:", path);
Console.Error(error.GetDescription());
return;
}
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.
// The override file legitimately reuses upstream serials by design — that's the whole point — so the duplicate
// guard only fires in the upstream load path.
if (!is_override && s_game_db.count(serial) == 1)
{
Console.ErrorFmt("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial);
continue;
}
if (n.is_map())
{
if (is_override)
mergeOverride(serial, n);
else
parseAndInsert(serial, n);
}
}
}
void GameDatabase::initDatabase()
{
loadFile(
Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME),
GAMEDB_YAML_FILE_NAME,
/*is_override=*/false);
loadFile(
Path::Combine(EmuFolders::Resources, GAMEDB_OVERRIDE_YAML_FILE_NAME),
GAMEDB_OVERRIDE_YAML_FILE_NAME,
/*is_override=*/true);
}
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();
auto iter = s_game_db.find(StringUtil::toLower(serial));
return (iter != s_game_db.end()) ? &iter->second : nullptr;
}
bool GameDatabase::TrackHash::parseHash(const std::string_view str)
{
constexpr u32 expected_length = SIZE * 2;
if (str.length() != expected_length)
return false;
std::memset(data, 0, sizeof(data));
for (u32 i = 0; i < SIZE * 2; i++)
{
const char ch = str[i];
u8 b;
if (ch >= '0' && ch <= '9')
b = static_cast<u8>(ch - '0');
else if (ch >= 'a' && ch <= 'f')
b = static_cast<u8>(ch - 'a') + 0xa;
else if (ch >= 'A' && ch <= 'F')
b = static_cast<u8>(ch - 'A') + 0xa;
else
return false;
data[i / 2] |= ((i % 2) == 0) ? (b << 4) : b;
}
return true;
}
std::string GameDatabase::TrackHash::toString() const
{
return fmt::format(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);
}
struct TrackHashHasher
{
size_t operator()(const GameDatabase::TrackHash& hash) const
{
return std::hash<std::string_view>()(std::string_view(reinterpret_cast<const char*>(hash.data),
GameDatabase::TrackHash::SIZE));
}
};
static constexpr char HASHDB_YAML_FILE_NAME[] = "RedumpDatabase.yaml";
std::unordered_map<GameDatabase::TrackHash, u32, TrackHashHasher> s_track_hash_to_entry_map;
std::vector<GameDatabase::HashDatabaseEntry> s_hash_database;
static bool parseHashDatabaseEntry(const ryml::NodeRef& node)
{
if (!node.has_child("name") || !node.has_child("hashes"))
{
Console.Warning("[HashDatabase] Incomplete entry found.");
return false;
}
GameDatabase::HashDatabaseEntry entry;
node["name"] >> entry.name;
if (node.has_child("version"))
node["version"] >> entry.version;
if (node.has_child("serial"))
node["serial"] >> entry.serial;
const u32 index = static_cast<u32>(s_hash_database.size());
for (const ryml::ConstNodeRef& n : node["hashes"].children())
{
if (!n.is_map() || !n.has_child("size") || !n.has_child("md5"))
{
Console.ErrorFmt("[HashDatabase] Incomplete hash definition in {}", entry.name);
return false;
}
GameDatabase::TrackHash th;
std::string md5;
n["md5"] >> md5;
n["size"] >> th.size;
if (!th.parseHash(md5))
{
Console.ErrorFmt("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5);
return false;
}
if (entry.tracks.empty() && s_track_hash_to_entry_map.find(th) != s_track_hash_to_entry_map.end())
Console.WarningFmt("[HashDatabase] Duplicate first track hash in {}", entry.name);
entry.tracks.push_back(th);
s_track_hash_to_entry_map.emplace(th, index);
}
s_hash_database.push_back(std::move(entry));
return true;
}
bool GameDatabase::loadHashDatabase()
{
if (!s_hash_database.empty())
return true;
Common::Timer load_timer;
const std::string path(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME));
const std::string name(HASHDB_YAML_FILE_NAME);
std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
Console.Error("[HashDatabase] Unable to open hash database file, file does not exist.");
return false;
}
ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error);
if (!tree.has_value())
{
Console.ErrorFmt("[HashDatabase] Failed to parse hash database file {}:", path);
Console.Error(error.GetDescription());
return false;
}
ryml::NodeRef root = tree->rootref();
bool okay = true;
for (const ryml::NodeRef& n : root.children())
{
if (!parseHashDatabaseEntry(n))
{
okay = false;
break;
}
}
if (!okay)
{
s_track_hash_to_entry_map.clear();
s_hash_database.clear();
return false;
}
Console.WriteLn(Color_StrongGreen, "[HashDatabase] Loaded YAML in %.0f ms", load_timer.GetTimeMilliseconds());
return true;
}
void GameDatabase::unloadHashDatabase()
{
s_track_hash_to_entry_map.clear();
s_hash_database.clear();
}
static size_t getTrackIndex(const GameDatabase::TrackHash* tracks, size_t num_tracks, const GameDatabase::TrackHash& track)
{
for (size_t i = 0; i < num_tracks; i++)
{
if (tracks[i] == track)
return i;
}
return num_tracks;
}
const GameDatabase::HashDatabaseEntry* GameDatabase::lookupHash(
const TrackHash* tracks, size_t num_tracks, bool* tracks_matched, std::string* match_error)
{
loadHashDatabase();
if (num_tracks == 0)
{
*match_error = TRANSLATE_STR("GameDatabase", "No tracks provided.");
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// match the first track, for DVDs this will be all there is anyway
const auto data_iter = s_track_hash_to_entry_map.find(tracks[0]);
if (data_iter == s_track_hash_to_entry_map.end())
{
*match_error = fmt::format(TRANSLATE_FS("GameDatabase", "Hash {} is not in database."), tracks[0].toString());
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// make sure they're not missing the data track
const GameDatabase::HashDatabaseEntry* candidate = &s_hash_database[data_iter->second];
if (getTrackIndex(candidate->tracks.data(), candidate->tracks.size(), tracks[0]) != 0)
{
*match_error = TRANSLATE_STR("GameDatabase", "Data track number does not match data track in database.");
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// first track is okay!
tracks_matched[0] = true;
match_error->clear();
// now check any audio tracks...
bool all_okay = true;
for (size_t track = 1; track < num_tracks; track++)
{
const auto audio_iter = s_track_hash_to_entry_map.find(tracks[track]);
if (audio_iter != s_track_hash_to_entry_map.end())
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} is not found in database.\n"), track + 1,
tracks[track].toString());
tracks_matched[track] = false;
all_okay = false;
continue;
}
// same game?
if (audio_iter->second != data_iter->second)
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} is for a different game ({2}).\n"), track + 1,
tracks[track].toString(), s_hash_database[audio_iter->second].name);
tracks_matched[track] = false;
all_okay = false;
continue;
}
// make sure it's the correct track number
if (getTrackIndex(candidate->tracks.data(), candidate->tracks.size(), tracks[track]) != track)
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} does not match database track.\n"), track + 1,
tracks[track].toString());
tracks_matched[track] = false;
all_okay = false;
continue;
}
tracks_matched[track] = true;
}
if (!match_error->empty() && match_error->back() == '\n')
match_error->pop_back();
return all_okay ? candidate : nullptr;
}