Files
duckstation/src/core/game_database.cpp
2025-11-20 14:19:42 +10:00

1841 lines
67 KiB
C++

// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "game_database.h"
#include "controller.h"
#include "host.h"
#include "system.h"
#include "util/cd_image.h"
#include "util/imgui_manager.h"
#include "common/assert.h"
#include "common/binary_reader_writer.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/heterogeneous_containers.h"
#include "common/log.h"
#include "common/path.h"
#include "common/ryml_helpers.h"
#include "common/string_util.h"
#include "common/timer.h"
#include "ryml.hpp"
#include <bit>
#include <iomanip>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <type_traits>
#include "IconsEmoji.h"
#include "IconsFontAwesome6.h"
#include "fmt/format.h"
LOG_CHANNEL(GameDatabase);
namespace GameDatabase {
enum : u32
{
GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48,
GAME_DATABASE_CACHE_VERSION = 33,
};
static const Entry* GetEntryForId(std::string_view code);
static void EnsureLoaded();
static void Load();
static bool LoadFromCache();
static bool SaveToCache();
static bool LoadGameDBYaml();
static bool ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value);
static bool ParseYamlDiscSetEntry(DiscSetEntry* entry, const ryml::ConstNodeRef& value);
static bool ParseYamlCodes(PreferUnorderedStringMap<std::string_view>& lookup, const ryml::ConstNodeRef& value,
std::string_view serial);
static void BindDiscSetsToEntries();
static bool LoadTrackHashes();
static constexpr const std::array<const char*, static_cast<int>(CompatibilityRating::Count)>
s_compatibility_rating_names = {{
"Unknown",
"DoesntBoot",
"CrashesInIntro",
"CrashesInGame",
"GraphicalAudioIssues",
"NoIssues",
}};
static constexpr const std::array<const char*, static_cast<size_t>(CompatibilityRating::Count)>
s_compatibility_rating_display_names = {{
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Unknown", "CompatibilityRating"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Doesn't Boot", "CompatibilityRating"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Crashes In Intro", "CompatibilityRating"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Crashes In-Game", "CompatibilityRating"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Graphical/Audio Issues", "CompatibilityRating"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "No Issues", "CompatibilityRating"),
}};
static constexpr const std::array s_trait_names = {
"ForceInterpreter",
"ForceSoftwareRenderer",
"ForceSoftwareRendererForReadbacks",
"ForceRoundTextureCoordinates",
"ForceShaderBlending",
"ForceFullTrueColor",
"ForceDeinterlacing",
"ForceFullBoot",
"DisableAutoAnalogMode",
"DisableMultitap",
"DisableFastForwardMemoryCardAccess",
"DisableCDROMReadSpeedup",
"DisableCDROMSeekSpeedup",
"DisableCDROMSpeedupOnMDEC",
"DisableTrueColor",
"DisableFullTrueColor",
"DisableUpscaling",
"DisableTextureFiltering",
"DisableSpriteTextureFiltering",
"DisableScaledDithering",
"DisableScaledInterlacing",
"DisableAllBordersCrop",
"DisableWidescreen",
"DisablePGXP",
"DisablePGXPCulling",
"DisablePGXPTextureCorrection",
"DisablePGXPColorCorrection",
"DisablePGXPDepthBuffer",
"DisablePGXPOn2DPolygons",
"ForcePGXPVertexCache",
"ForcePGXPCPUMode",
"ForceRecompilerICache",
"ForceCDROMSubQSkew",
"IsLibCryptProtected",
};
static_assert(s_trait_names.size() == static_cast<size_t>(Trait::MaxCount));
static constexpr const std::array s_trait_display_names = {
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Interpreter", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Software Renderer", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Software Renderer For Readbacks", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Round Texture Coordinates", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Shader Blending", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Full True Color", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Deinterlacing", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Full Boot", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Automatic Analog Mode", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Multitap", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Fast Forward Memory Card Access", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable CD-ROM Read Speedup", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable CD-ROM Seek Speedup", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable CD-ROM Speedup on MDEC", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable True Color", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Full True Color", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Upscaling", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Texture Filtering", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Sprite Texture Filtering", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Scaled Dithering", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Scaled Interlacing", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable All Borders Crop", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable Widescreen", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP Culling", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP Texture Correction", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP Color Correction", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP Depth Buffer", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Disable PGXP on 2D Polygons", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force PGXP Vertex Cache", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force PGXP CPU Mode", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Recompiler ICache", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force CD-ROM SubQ Skew", "GameDatabase::Trait"),
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Is LibCrypt Protected", "GameDatabase::Trait"),
};
static_assert(s_trait_display_names.size() == static_cast<size_t>(Trait::MaxCount));
static constexpr std::array s_language_names = {
"Catalan", "Chinese", "Czech", "Danish", "Dutch", "English", "Finnish", "French",
"German", "Greek", "Hebrew", "Iranian", "Italian", "Japanese", "Korean", "Norwegian",
"Polish", "Portuguese", "Russian", "Spanish", "Swedish", "Turkish",
};
static_assert(s_language_names.size() == static_cast<size_t>(Language::MaxCount));
static constexpr const char* GAMEDB_YAML_FILENAME = "gamedb.yaml";
static constexpr const char* DISCSETS_YAML_FILENAME = "discsets.yaml";
static constexpr const char* DISCDB_YAML_FILENAME = "discdb.yaml";
static DisplayDeinterlacingMode DEFAULT_DEINTERLACING_MODE = DisplayDeinterlacingMode::Adaptive;
namespace {
struct State
{
bool loaded;
bool track_hashes_loaded;
DynamicHeapArray<u8> db_data; // we take strings from the data, so store a copy
DynamicHeapArray<u8> disc_set_db_data; // if loaded from binary cache, this will be empty
std::vector<GameDatabase::Entry> entries;
std::vector<GameDatabase::DiscSetEntry> disc_sets;
PreferUnorderedStringMap<u32> code_lookup;
TrackHashesMap track_hashes_map;
std::once_flag load_once_flag;
};
} // namespace
ALIGN_TO_CACHE_LINE static State s_state;
} // namespace GameDatabase
void GameDatabase::EnsureLoaded()
{
if (s_state.loaded)
return;
std::call_once(s_state.load_once_flag, &GameDatabase::Load);
}
void GameDatabase::Load()
{
Timer timer;
if (!LoadFromCache())
{
s_state.entries = {};
s_state.code_lookup = {};
s_state.db_data.deallocate();
s_state.disc_set_db_data.deallocate();
if (LoadGameDBYaml())
{
SaveToCache();
}
else
{
s_state.entries = {};
s_state.code_lookup = {};
s_state.db_data.deallocate();
s_state.disc_set_db_data.deallocate();
}
}
s_state.loaded = true;
INFO_LOG("Database load of {} entries took {:.0f}ms.", s_state.entries.size(), timer.GetTimeMilliseconds());
}
const GameDatabase::Entry* GameDatabase::GetEntryForId(std::string_view code)
{
if (code.empty())
return nullptr;
EnsureLoaded();
auto iter = s_state.code_lookup.find(code);
return (iter != s_state.code_lookup.end()) ? &s_state.entries[iter->second] : nullptr;
}
std::string GameDatabase::GetSerialForDisc(CDImage* image)
{
std::string ret;
const GameDatabase::Entry* entry = GetEntryForDisc(image);
if (entry)
ret = entry->serial;
return ret;
}
std::string GameDatabase::GetSerialForPath(const char* path)
{
std::string ret;
if (System::IsLoadablePath(path) && !System::IsExePath(path) && !System::IsPsfPath(path))
{
std::unique_ptr<CDImage> image(CDImage::Open(path, false, nullptr));
if (image)
ret = GetSerialForDisc(image.get());
}
return ret;
}
const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image)
{
std::string id;
GameHash hash;
System::GetGameDetailsFromImage(image, &id, &hash);
const Entry* entry = GetEntryForGameDetails(id, hash);
if (entry)
return entry;
WARNING_LOG("No entry found for disc '{}'", id);
return nullptr;
}
const GameDatabase::Entry* GameDatabase::GetEntryForGameDetails(const std::string& id, u64 hash)
{
const Entry* entry;
if (!id.empty())
{
entry = GetEntryForId(id);
if (entry)
return entry;
}
// some games with invalid serials use the hash
entry = GetEntryForId(System::GetGameHashId(hash));
if (entry)
return entry;
return nullptr;
}
const GameDatabase::Entry* GameDatabase::GetEntryForSerial(std::string_view serial)
{
if (serial.empty())
return nullptr;
EnsureLoaded();
const auto it =
std::lower_bound(s_state.entries.cbegin(), s_state.entries.cend(), serial,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
return (it != s_state.entries.end() && it->serial == serial) ? &(*it) : nullptr;
}
const char* GameDatabase::GetTraitName(Trait trait)
{
return s_trait_names[static_cast<size_t>(trait)];
}
const char* GameDatabase::GetTraitDisplayName(Trait trait)
{
return Host::TranslateToCString("GameDatabase", s_trait_display_names[static_cast<size_t>(trait)]);
}
const char* GameDatabase::GetCompatibilityRatingName(CompatibilityRating rating)
{
return s_compatibility_rating_names[static_cast<int>(rating)];
}
const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating rating)
{
return (rating >= CompatibilityRating::Unknown && rating < CompatibilityRating::Count) ?
Host::TranslateToCString("GameDatabase", s_compatibility_rating_display_names[static_cast<size_t>(rating)],
"CompatibilityRating") :
"";
}
const char* GameDatabase::GetLanguageName(Language language)
{
return s_language_names[static_cast<size_t>(language)];
}
std::optional<GameDatabase::Language> GameDatabase::ParseLanguageName(std::string_view str)
{
for (size_t i = 0; i < static_cast<size_t>(Language::MaxCount); i++)
{
if (str == s_language_names[i])
return static_cast<Language>(i);
}
return std::nullopt;
}
TinyString GameDatabase::GetLanguageFlagResourceName(std::string_view language_name)
{
return TinyString::from_format("images/flags/{}.svg", language_name);
}
std::string_view GameDatabase::Entry::GetLanguageFlagName(DiscRegion region) const
{
// If there's only one language, this is the flag we want to use.
// Except if it's English, then we want to use the disc region's flag.
std::string_view ret;
if (languages.count() == 1 && !languages.test(static_cast<size_t>(GameDatabase::Language::English)))
ret = GameDatabase::GetLanguageName(static_cast<GameDatabase::Language>(std::countr_zero(languages.to_ulong())));
else
ret = Settings::GetDiscRegionName(region);
return ret;
}
SmallString GameDatabase::Entry::GetLanguagesString() const
{
SmallString ret;
bool first = true;
for (u32 i = 0; i < static_cast<u32>(Language::MaxCount); i++)
{
if (languages.test(i))
{
ret.append_format("{}{}", first ? "" : ", ", GetLanguageName(static_cast<Language>(i)));
first = false;
}
}
if (ret.empty())
ret.append(TRANSLATE_SV("GameDatabase", "Unknown"));
return ret;
}
std::string_view GameDatabase::Entry::GetDisplayTitle(bool localized) const
{
return (localized && !localized_title.empty()) ? localized_title : title;
}
std::string_view GameDatabase::Entry::GetSortTitle() const
{
return !sort_title.empty() ? sort_title : title;
}
std::string_view GameDatabase::Entry::GetSaveTitle() const
{
return !save_title.empty() ? save_title : title;
}
bool GameDatabase::Entry::IsFirstDiscInSet() const
{
return (disc_set && disc_set->GetFirstSerial() == serial);
}
std::string_view GameDatabase::DiscSetEntry::GetDisplayTitle(bool localized) const
{
return (localized && !localized_title.empty()) ? localized_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetSortTitle() const
{
return !sort_title.empty() ? sort_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetSaveTitle() const
{
return !save_title.empty() ? save_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetFirstSerial() const
{
return serials[0];
}
void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_messages) const
{
if (display_active_start_offset.has_value())
{
settings.display_active_start_offset = display_active_start_offset.value();
if (display_osd_messages)
INFO_LOG("GameDB: Display active start offset set to {}.", settings.display_active_start_offset);
}
if (display_active_end_offset.has_value())
{
settings.display_active_end_offset = display_active_end_offset.value();
if (display_osd_messages)
INFO_LOG("GameDB: Display active end offset set to {}.", settings.display_active_end_offset);
}
if (display_line_start_offset.has_value())
{
settings.display_line_start_offset = display_line_start_offset.value();
if (display_osd_messages)
INFO_LOG("GameDB: Display line start offset set to {}.", settings.display_line_start_offset);
}
if (display_line_end_offset.has_value())
{
settings.display_line_end_offset = display_line_end_offset.value();
if (display_osd_messages)
INFO_LOG("GameDB: Display line end offset set to {}.", settings.display_line_start_offset);
}
if (dma_max_slice_ticks.has_value())
{
settings.dma_max_slice_ticks = dma_max_slice_ticks.value();
if (display_osd_messages)
INFO_LOG("GameDB: DMA max slice ticks set to {}.", settings.dma_max_slice_ticks);
}
if (dma_halt_ticks.has_value())
{
settings.dma_halt_ticks = dma_halt_ticks.value();
if (display_osd_messages)
INFO_LOG("GameDB: DMA halt ticks set to {}.", settings.dma_halt_ticks);
}
if (cdrom_max_seek_speedup_cycles.has_value() && g_settings.cdrom_seek_speedup == 0)
{
settings.cdrom_max_seek_speedup_cycles = cdrom_max_seek_speedup_cycles.value();
if (display_osd_messages)
INFO_LOG("GameDB: CDROM maximum seek speedup cycles set to {}.", settings.cdrom_max_seek_speedup_cycles);
}
if (cdrom_max_read_speedup_cycles.has_value() && g_settings.cdrom_read_speedup == 0)
{
settings.cdrom_max_read_speedup_cycles = cdrom_max_read_speedup_cycles.value();
if (display_osd_messages)
INFO_LOG("GameDB: CDROM maximum read speedup cycles set to {}.", settings.cdrom_max_read_speedup_cycles);
}
if (gpu_fifo_size.has_value())
{
settings.gpu_fifo_size = gpu_fifo_size.value();
if (display_osd_messages)
INFO_LOG("GameDB: GPU FIFO size set to {}.", settings.gpu_fifo_size);
}
if (gpu_max_run_ahead.has_value())
{
settings.gpu_max_run_ahead = gpu_max_run_ahead.value();
if (display_osd_messages)
INFO_LOG("GameDB: GPU max runahead set to {}.", settings.gpu_max_run_ahead);
}
if (gpu_pgxp_tolerance.has_value())
{
settings.gpu_pgxp_tolerance = gpu_pgxp_tolerance.value();
if (display_osd_messages)
INFO_LOG("GameDB: PGXP tolerance set to {}.", settings.gpu_pgxp_tolerance);
}
if (gpu_pgxp_depth_threshold.has_value())
{
settings.SetPGXPDepthClearThreshold(gpu_pgxp_depth_threshold.value());
if (display_osd_messages)
INFO_LOG("GameDB: PGXP depth clear threshold set to {}.", settings.GetPGXPDepthClearThreshold());
}
if (gpu_line_detect_mode.has_value())
{
settings.gpu_line_detect_mode = gpu_line_detect_mode.value();
if (display_osd_messages)
{
INFO_LOG("GameDB: GPU line detect mode set to {}.",
Settings::GetLineDetectModeName(settings.gpu_line_detect_mode));
}
}
if (cpu_overclock.has_value() && (!settings.cpu_overclock_enable || settings.disable_all_enhancements))
{
settings.SetCPUOverclockPercent(cpu_overclock.value());
settings.cpu_overclock_enable = settings.cpu_overclock_active = true;
if (display_osd_messages)
INFO_LOG("GameDB: CPU overclock set to {}.", cpu_overclock.value());
}
LargeString messages;
const auto append_message = [&messages](std::string_view msg) {
messages.append(" \u2022 ");
messages.append(msg);
messages.append('\n');
};
const auto append_message_fmt = [&messages]<typename... T>(fmt::format_string<T...> fmt, T&&... args) {
messages.append(" \u2022 ");
messages.append_vformat(fmt, fmt::make_format_args(args...));
messages.append('\n');
};
if (HasTrait(Trait::ForceInterpreter))
{
if (display_osd_messages && settings.cpu_execution_mode != CPUExecutionMode::Interpreter)
append_message(TRANSLATE_SV("GameDatabase", "CPU recompiler disabled."));
settings.cpu_execution_mode = CPUExecutionMode::Interpreter;
}
if (HasTrait(Trait::ForceFullBoot))
{
if (display_osd_messages && settings.bios_patch_fast_boot)
append_message(TRANSLATE_SV("GameDatabase", "Fast boot disabled."));
settings.bios_patch_fast_boot = false;
}
if (HasTrait(Trait::DisableMultitap))
{
if (display_osd_messages && settings.multitap_mode != MultitapMode::Disabled)
append_message(TRANSLATE_SV("GameDatabase", "Multitap disabled."));
settings.multitap_mode = MultitapMode::Disabled;
}
if (HasTrait(Trait::DisableFastForwardMemoryCardAccess) && g_settings.memory_card_fast_forward_access)
{
if (display_osd_messages)
append_message(TRANSLATE_SV("GameDatabase", "Fast forward memory card access disabled."));
settings.memory_card_fast_forward_access = false;
}
if (HasTrait(Trait::DisableCDROMReadSpeedup))
{
if (settings.cdrom_read_speedup != 1)
append_message(TRANSLATE_SV("GameDatabase", "CD-ROM read speedup disabled."));
settings.cdrom_read_speedup = 1;
}
if (HasTrait(Trait::DisableCDROMSeekSpeedup))
{
if (settings.cdrom_seek_speedup != 1)
append_message(TRANSLATE_SV("GameDatabase", "CD-ROM seek speedup disabled."));
settings.cdrom_seek_speedup = 1;
}
if (HasTrait(Trait::DisableCDROMSpeedupOnMDEC))
{
WARNING_LOG("Disabling CD-ROM speedup on MDEC.");
settings.mdec_disable_cdrom_speedup = true;
}
else if (settings.mdec_disable_cdrom_speedup && settings.cdrom_read_speedup != 1)
{
Host::AddIconOSDWarning(
"GameDBDisableCDROMSpeedupUnnecessary", ICON_EMOJI_WARNING,
TRANSLATE_STR("GameDatabase", "Disable CD-ROM speedup on MDEC is enabled, but it is not required for this game."),
Host::OSD_WARNING_DURATION);
}
if (display_crop_mode.has_value())
{
if (display_osd_messages && settings.display_crop_mode != display_crop_mode.value())
{
append_message_fmt(TRANSLATE_FS("GameDatabase", "Display cropping set to {}."),
Settings::GetDisplayCropModeDisplayName(display_crop_mode.value()));
}
settings.display_crop_mode = display_crop_mode.value();
}
else if (HasTrait(Trait::DisableAllBordersCrop) && settings.display_crop_mode >= DisplayCropMode::Borders &&
settings.display_crop_mode <= DisplayCropMode::BordersUncorrected)
{
constexpr DisplayCropMode new_mode = DisplayCropMode::Overscan;
if (display_osd_messages)
{
append_message_fmt(TRANSLATE_FS("GameDatabase", "Display cropping set to {}."),
Settings::GetDisplayCropModeDisplayName(new_mode));
}
settings.display_crop_mode = new_mode;
}
if (HasTrait(Trait::ForceSoftwareRenderer))
{
if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software)
append_message(TRANSLATE_SV("GameDatabase", "Hardware rendering disabled."));
settings.gpu_renderer = GPURenderer::Software;
}
if (HasTrait(Trait::ForceSoftwareRendererForReadbacks))
{
if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software)
append_message(TRANSLATE_SV("GameDatabase", "Software renderer readbacks enabled."));
settings.gpu_use_software_renderer_for_readbacks = true;
}
if (HasTrait(Trait::ForceRoundUpscaledTextureCoordinates))
{
settings.gpu_force_round_texcoords = true;
}
if (HasTrait(Trait::ForceDeinterlacing))
{
const DisplayDeinterlacingMode new_mode = display_deinterlacing_mode.value_or(
(settings.display_deinterlacing_mode != DisplayDeinterlacingMode::Disabled &&
settings.display_deinterlacing_mode != DisplayDeinterlacingMode::Progressive) ?
settings.display_deinterlacing_mode :
DEFAULT_DEINTERLACING_MODE);
if (display_osd_messages && settings.display_deinterlacing_mode != new_mode)
{
append_message_fmt(TRANSLATE_FS("GameDatabase", "Deinterlacing set to {}."),
Settings::GetDisplayDeinterlacingModeDisplayName(new_mode));
}
settings.display_deinterlacing_mode = new_mode;
}
else if (display_deinterlacing_mode.has_value())
{
// If the user has it set to progressive, then preserve that.
if (settings.display_deinterlacing_mode != DisplayDeinterlacingMode::Progressive)
{
if (display_osd_messages && settings.display_deinterlacing_mode != display_deinterlacing_mode.value())
{
append_message_fmt(TRANSLATE_FS("GameDatabase", "Deinterlacing set to {}."),
Settings::GetDisplayDeinterlacingModeDisplayName(display_deinterlacing_mode.value()));
}
settings.display_deinterlacing_mode = display_deinterlacing_mode.value();
}
}
if (HasTrait(Trait::DisableTrueColor) || HasTrait(Trait::DisableFullTrueColor) ||
HasTrait(Trait::DisableScaledDithering) || HasTrait(Trait::ForceShaderBlending) ||
HasTrait(Trait::ForceFullTrueColor))
{
// Note: The order these are applied matters.
const GPUDitheringMode old_mode = settings.gpu_dithering_mode;
if (HasTrait(Trait::DisableTrueColor) && settings.IsUsingTrueColor())
{
settings.gpu_dithering_mode = GPUDitheringMode::Scaled;
}
if (HasTrait(Trait::DisableScaledDithering) && settings.IsUsingDithering())
{
settings.gpu_dithering_mode =
(settings.IsUsingShaderBlending() ? GPUDitheringMode::UnscaledShaderBlend : GPUDitheringMode::Unscaled);
}
if (HasTrait(Trait::ForceShaderBlending) && settings.IsUsingDithering() && !settings.IsUsingShaderBlending())
{
settings.gpu_dithering_mode = (settings.gpu_dithering_mode == GPUDitheringMode::Scaled) ?
GPUDitheringMode::ScaledShaderBlend :
GPUDitheringMode::UnscaledShaderBlend;
}
if (HasTrait(Trait::ForceFullTrueColor) && settings.gpu_dithering_mode == GPUDitheringMode::TrueColor)
{
settings.gpu_dithering_mode = GPUDitheringMode::TrueColorFull;
}
if (HasTrait(Trait::DisableFullTrueColor) && settings.gpu_dithering_mode == GPUDitheringMode::TrueColorFull)
{
settings.gpu_dithering_mode = GPUDitheringMode::TrueColor;
}
if (display_osd_messages && settings.gpu_dithering_mode != old_mode)
{
append_message_fmt(TRANSLATE_FS("GameDatabase", "Dithering set to {}."),
Settings::GetGPUDitheringModeDisplayName(settings.gpu_dithering_mode));
}
}
if (HasTrait(Trait::DisableUpscaling))
{
if (display_osd_messages)
{
if (settings.gpu_resolution_scale != 1)
append_message(TRANSLATE_SV("GameDatabase", "Upscaling disabled."));
if (settings.gpu_multisamples != 1)
append_message(TRANSLATE_SV("GameDatabase", "MSAA disabled."));
}
settings.gpu_resolution_scale = 1;
settings.gpu_automatic_resolution_scale = false;
settings.gpu_multisamples = 1;
}
if (HasTrait(Trait::DisableTextureFiltering))
{
if (display_osd_messages && (settings.gpu_texture_filter != GPUTextureFilter::Nearest ||
g_settings.gpu_sprite_texture_filter != GPUTextureFilter::Nearest))
{
append_message(TRANSLATE_SV("GameDatabase", "Texture filtering disabled."));
}
settings.gpu_texture_filter = GPUTextureFilter::Nearest;
settings.gpu_sprite_texture_filter = GPUTextureFilter::Nearest;
}
if (HasTrait(Trait::DisableSpriteTextureFiltering))
{
if (display_osd_messages && g_settings.gpu_sprite_texture_filter != GPUTextureFilter::Nearest)
{
append_message(TRANSLATE_SV("GameDatabase", "Sprite texture filtering disabled."));
}
settings.gpu_sprite_texture_filter = GPUTextureFilter::Nearest;
}
if (HasTrait(Trait::DisableScaledInterlacing))
{
if (display_osd_messages && settings.gpu_scaled_interlacing &&
settings.display_deinterlacing_mode != DisplayDeinterlacingMode::Progressive)
{
append_message(TRANSLATE_SV("GameDatabase", "Scaled interlacing disabled."));
}
settings.gpu_scaled_interlacing = false;
}
if (HasTrait(Trait::DisableWidescreen))
{
if (display_osd_messages && settings.gpu_widescreen_rendering)
append_message(TRANSLATE_SV("GameDatabase", "Widescreen rendering disabled."));
settings.gpu_widescreen_rendering = false;
settings.gpu_widescreen_hack = false;
}
if (HasTrait(Trait::DisablePGXP))
{
if (display_osd_messages && settings.gpu_pgxp_enable)
append_message(TRANSLATE_SV("GameDatabase", "PGXP geometry correction disabled."));
settings.gpu_pgxp_enable = false;
}
if (HasTrait(Trait::DisablePGXPCulling))
{
if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_culling)
append_message(TRANSLATE_SV("GameDatabase", "PGXP culling correction disabled."));
settings.gpu_pgxp_culling = false;
}
if (HasTrait(Trait::DisablePGXPTextureCorrection))
{
if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction)
append_message(TRANSLATE_SV("GameDatabase", "PGXP perspective correct textures disabled."));
settings.gpu_pgxp_texture_correction = false;
}
if (HasTrait(Trait::DisablePGXPColorCorrection))
{
if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction &&
settings.gpu_pgxp_color_correction)
{
append_message(TRANSLATE_SV("GameDatabase", "PGXP perspective correct colors disabled."));
}
settings.gpu_pgxp_color_correction = false;
}
if (HasTrait(Trait::ForcePGXPVertexCache))
{
if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_vertex_cache)
append_message(TRANSLATE_SV("GameDatabase", "PGXP vertex cache enabled."));
settings.gpu_pgxp_vertex_cache = settings.gpu_pgxp_enable;
}
else if (settings.gpu_pgxp_enable && settings.gpu_pgxp_vertex_cache)
{
Host::AddIconOSDWarning(
"gamedb_force_pgxp_vertex_cache", ICON_EMOJI_WARNING,
TRANSLATE_STR(
"GameDatabase",
"PGXP Vertex Cache is enabled, but it is not required for this game. This may cause rendering errors."),
Host::OSD_WARNING_DURATION);
}
if (HasTrait(Trait::ForcePGXPCPUMode))
{
if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_cpu)
{
#ifndef __ANDROID__
append_message(TRANSLATE_SV("GameDatabase", "PGXP CPU mode enabled."));
#else
Host::AddIconOSDWarning("gamedb_force_pgxp_cpu", ICON_EMOJI_WARNING,
"This game requires PGXP CPU mode, which increases system requirements.\n"
" If the game runs too slow, disable PGXP for this game.",
Host::OSD_WARNING_DURATION);
#endif
}
settings.gpu_pgxp_cpu = settings.gpu_pgxp_enable;
}
else if (settings.UsingPGXPCPUMode())
{
Host::AddIconOSDWarning(
"gamedb_force_pgxp_cpu", ICON_EMOJI_WARNING,
TRANSLATE_STR("GameDatabase",
"PGXP CPU mode is enabled, but it is not required for this game. This may cause rendering errors."),
Host::OSD_WARNING_DURATION);
}
if (HasTrait(Trait::DisablePGXPDepthBuffer))
{
if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_depth_buffer)
append_message(TRANSLATE_SV("GameDatabase", "PGXP depth buffer disabled."));
settings.gpu_pgxp_depth_buffer = false;
}
if (HasTrait(Trait::DisablePGXPOn2DPolygons))
{
if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_disable_2d)
append_message(TRANSLATE_SV("GameDatabase", "PGXP disabled on 2D polygons."));
g_settings.gpu_pgxp_disable_2d = true;
}
if (gpu_pgxp_preserve_proj_fp.has_value())
{
if (display_osd_messages)
{
INFO_LOG("GameDB: GPU preserve projection precision set to {}.",
gpu_pgxp_preserve_proj_fp.value() ? "true" : "false");
if (settings.gpu_pgxp_enable && settings.gpu_pgxp_preserve_proj_fp && !gpu_pgxp_preserve_proj_fp.value())
append_message(TRANSLATE_SV("GameDatabase", "PGXP preserve projection precision disabled."));
}
settings.gpu_pgxp_preserve_proj_fp = gpu_pgxp_preserve_proj_fp.value();
}
if (HasTrait(Trait::ForceRecompilerICache))
{
WARNING_LOG("ICache for recompiler forced by compatibility settings.");
settings.cpu_recompiler_icache = true;
}
if (HasTrait(Trait::ForceCDROMSubQSkew))
{
WARNING_LOG("CD-ROM SubQ Skew forced by compatibility settings.");
settings.cdrom_subq_skew = true;
}
if (!messages.empty())
{
if (messages.back() == '\n')
messages.pop_back();
Host::AddIconOSDMessage("GameDBCompatibility", ICON_EMOJI_INFORMATION,
TRANSLATE_STR("GameDatabase", "Compatibility settings for this game have been applied."),
std::string(messages.view()), Host::OSD_INFO_DURATION);
}
#define BIT_FOR(ctype) (static_cast<u16>(1) << static_cast<u32>(ctype))
if (supported_controllers != 0 && supported_controllers != static_cast<u16>(-1))
{
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{
const ControllerType ctype = settings.controller_types[i];
if (ctype == ControllerType::None)
continue;
if (supported_controllers & BIT_FOR(ctype))
continue;
// Special case: Dualshock is permitted when not supported as long as it's in digital mode.
if (ctype == ControllerType::AnalogController &&
(supported_controllers & BIT_FOR(ControllerType::DigitalController)) != 0)
{
continue;
}
if (display_osd_messages)
{
SmallString supported_controller_string;
for (u32 j = 0; j < static_cast<u32>(ControllerType::Count); j++)
{
const ControllerType supported_ctype = static_cast<ControllerType>(j);
if ((supported_controllers & BIT_FOR(supported_ctype)) == 0)
continue;
if (!supported_controller_string.empty())
supported_controller_string.append(", ");
supported_controller_string.append(Controller::GetControllerInfo(supported_ctype).GetDisplayName());
}
Host::AddIconOSDWarning(
fmt::format("GameDBController{}Unsupported", i), ICON_EMOJI_WARNING,
fmt::format(
TRANSLATE_FS("GameDatabase",
"Controller in Port {0} ({1}) is not supported for this game.\nSupported controllers: "
"{2}\nPlease configure a supported controller from the list above."),
i + 1u, Controller::GetControllerInfo(ctype).GetDisplayName(), supported_controller_string),
Host::OSD_CRITICAL_ERROR_DURATION);
}
}
if (g_settings.multitap_mode != MultitapMode::Disabled && !(supported_controllers & SUPPORTS_MULTITAP_BIT))
{
Host::AddIconOSDMessage("GameDBMultitapUnsupported", ICON_EMOJI_WARNING,
TRANSLATE_STR("GameDatabase",
"This game does not support multitap, but multitap is enabled.\n"
" This may result in dropped controller inputs."),
Host::OSD_CRITICAL_ERROR_DURATION);
}
}
#undef BIT_FOR
}
static inline void AppendSettingsHeading(SmallStringBase& str, bool& heading)
{
if (!heading)
{
heading = true;
str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings"));
}
}
static inline void AppendBoolSetting(SmallStringBase& str, bool& heading, std::string_view title,
const std::optional<bool>& value)
{
if (!value.has_value())
return;
AppendSettingsHeading(str, heading);
str.append_format(" - {}: {}\n", title, value.value() ? "Enabled" : "Disabled");
}
template<typename T>
static inline void AppendIntegerSetting(SmallStringBase& str, bool& heading, std::string_view title,
const std::optional<T>& value)
{
if (!value.has_value())
return;
AppendSettingsHeading(str, heading);
str.append_format(" - {}: {}\n", title, value.value());
}
static inline void AppendFloatSetting(SmallStringBase& str, bool& heading, std::string_view title,
const std::optional<float>& value)
{
if (!value.has_value())
return;
AppendSettingsHeading(str, heading);
str.append_format(" - {}: {:.2f}\n", title, value.value());
}
template<typename T>
static inline void AppendEnumSetting(SmallStringBase& str, bool& heading, std::string_view title,
const char* (*get_display_name_func)(T), const std::optional<T>& value)
{
if (!value.has_value())
return;
AppendSettingsHeading(str, heading);
str.append_format(" - {}: {}\n", title, get_display_name_func(value.value()));
}
std::string GameDatabase::Entry::GenerateCompatibilityReport() const
{
LargeString ret;
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Serial"), serial);
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Title"), title);
if (!sort_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Sort Title"), sort_title);
if (!localized_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Localized Title"), localized_title);
if (!save_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Save Title"), save_title);
if (languages.any())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Languages"), GetLanguagesString());
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Rating"),
GetCompatibilityRatingDisplayName(compatibility));
if (!compatibility_version_tested.empty())
ret.append_format("**{}:**\n{}\n\n", TRANSLATE_SV("GameDatabase", "Version Tested"), compatibility_version_tested);
if (!compatibility_comments.empty())
ret.append_format("**{}**\n\n{}\n\n", TRANSLATE_SV("GameDatabase", "Comments"), compatibility_comments);
if (supported_controllers != 0)
{
ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Supported Controllers"));
for (u32 j = 0; j < static_cast<u32>(ControllerType::Count); j++)
{
if ((supported_controllers & (static_cast<u16>(1) << j)) == 0)
continue;
ret.append_format(" - {}\n", Controller::GetControllerInfo(static_cast<ControllerType>(j)).GetDisplayName());
}
if (supported_controllers & SUPPORTS_MULTITAP_BIT)
ret.append(" - Multitap\n");
ret.append("\n");
}
if (traits.any())
{
ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Traits"));
for (u32 i = 0; i < static_cast<u32>(Trait::MaxCount); i++)
{
if (traits.test(i))
ret.append_format(" - {}\n", GetTraitDisplayName(static_cast<Trait>(i)));
}
ret.append("\n");
}
bool settings_heading = false;
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active Start Offset"),
display_active_start_offset);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active End Offset"),
display_active_end_offset);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line Start Offset"),
display_line_start_offset);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line End Offset"),
display_line_end_offset);
AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Crop Mode"),
&Settings::GetDisplayCropModeDisplayName, display_crop_mode);
AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Deinterlacing Mode"),
&Settings::GetDisplayDeinterlacingModeDisplayName, display_deinterlacing_mode);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "CPU Overclock Percent"), cpu_overclock);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Max Slice Ticks"), dma_max_slice_ticks);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Halt Ticks"), dma_halt_ticks);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "CD-ROM Max Seek Speedup Cycles"),
cdrom_max_seek_speedup_cycles);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "CD-ROM Max Read Speedup Cycles"),
cdrom_max_read_speedup_cycles);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU FIFO Size"), gpu_fifo_size);
AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Max Runahead"), gpu_max_run_ahead);
AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Line Detect Mode"),
&Settings::GetLineDetectModeDisplayName, gpu_line_detect_mode);
AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "PGXP Tolerance"), gpu_pgxp_tolerance);
AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "PGXP Depth Clear Threshold"),
gpu_pgxp_depth_threshold);
AppendBoolSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "PGXP Preserve Projection Precision"),
gpu_pgxp_preserve_proj_fp);
if (settings_heading)
ret.append("\n");
if (disc_set)
{
ret.append_format("**{}:** {}\n", TRANSLATE_SV("GameDatabase", "Disc Set"), disc_set->GetDisplayTitle(true));
for (const std::string_view& ds_serial : disc_set->serials)
ret.append_format(" - {}\n", ds_serial);
}
return std::string(ret.view());
}
static std::string GetCacheFile()
{
return Path::Combine(EmuFolders::Cache, "gamedb.cache");
}
bool GameDatabase::LoadFromCache()
{
Error error;
std::optional<DynamicHeapArray<u8>> db_data = FileSystem::ReadBinaryFile(GetCacheFile().c_str(), &error);
if (!db_data.has_value())
{
DEV_LOG("Failed to read cache, loading full database: {}", error.GetDescription());
return false;
}
BinarySpanReader reader(db_data->cspan());
const u64 gamedb_ts = static_cast<u64>(Host::GetResourceFileTimestamp(GAMEDB_YAML_FILENAME, false).value_or(0));
const u64 discsets_ts = static_cast<u64>(Host::GetResourceFileTimestamp(DISCSETS_YAML_FILENAME, false).value_or(0));
u32 signature, version, num_disc_set_entries, num_entries, num_codes;
u64 file_gamedb_ts, file_discsets_ts;
if (!reader.ReadU32(&signature) || !reader.ReadU32(&version) || !reader.ReadU64(&file_gamedb_ts) ||
!reader.ReadU64(&file_discsets_ts) || !reader.ReadU32(&num_disc_set_entries) || !reader.ReadU32(&num_entries) ||
!reader.ReadU32(&num_codes) || signature != GAME_DATABASE_CACHE_SIGNATURE ||
version != GAME_DATABASE_CACHE_VERSION)
{
DEV_LOG("Cache header is corrupted or version mismatch.");
return false;
}
if (gamedb_ts != file_gamedb_ts || discsets_ts != file_discsets_ts)
{
DEV_LOG("Cache is out of date, recreating.");
return false;
}
s_state.entries.reserve(num_entries);
s_state.disc_sets.reserve(num_disc_set_entries);
for (u32 i = 0; i < num_disc_set_entries; i++)
{
DiscSetEntry& disc_set = s_state.disc_sets.emplace_back();
u32 num_serials;
if (!reader.ReadSizePrefixedString(&disc_set.title) || !reader.ReadSizePrefixedString(&disc_set.sort_title) ||
!reader.ReadSizePrefixedString(&disc_set.localized_title) ||
!reader.ReadSizePrefixedString(&disc_set.save_title) || !reader.ReadU32(&num_serials))
{
DEV_LOG("Cache disc set entry is corrupted.");
return false;
}
if (num_serials > 0)
{
disc_set.serials.reserve(num_serials);
for (u32 j = 0; j < num_serials; j++)
{
if (!reader.ReadSizePrefixedString(&disc_set.serials.emplace_back()))
{
DEV_LOG("Cache disc set entry is corrupted.");
return false;
}
}
}
}
for (u32 i = 0; i < num_entries; i++)
{
Entry& entry = s_state.entries.emplace_back();
constexpr u32 trait_num_bytes = (static_cast<u32>(Trait::MaxCount) + 7) / 8;
constexpr u32 language_num_bytes = (static_cast<u32>(Language::MaxCount) + 7) / 8;
std::array<u8, trait_num_bytes> trait_bits;
std::array<u8, language_num_bytes> language_bits;
u8 compatibility;
s32 disc_set_index;
if (!reader.ReadSizePrefixedString(&entry.serial) || !reader.ReadSizePrefixedString(&entry.title) ||
!reader.ReadSizePrefixedString(&entry.sort_title) || !reader.ReadSizePrefixedString(&entry.localized_title) ||
!reader.ReadSizePrefixedString(&entry.save_title) || !reader.ReadSizePrefixedString(&entry.genre) ||
!reader.ReadSizePrefixedString(&entry.developer) || !reader.ReadSizePrefixedString(&entry.publisher) ||
!reader.ReadSizePrefixedString(&entry.compatibility_version_tested) ||
!reader.ReadSizePrefixedString(&entry.compatibility_comments) || !reader.ReadS32(&disc_set_index) ||
(disc_set_index >= 0 && static_cast<u32>(disc_set_index) >= num_disc_set_entries) ||
!reader.ReadU64(&entry.release_date) || !reader.ReadU8(&entry.min_players) ||
!reader.ReadU8(&entry.max_players) || !reader.ReadU8(&entry.min_blocks) || !reader.ReadU8(&entry.max_blocks) ||
!reader.ReadU16(&entry.supported_controllers) || !reader.ReadU8(&compatibility) ||
compatibility >= static_cast<u8>(GameDatabase::CompatibilityRating::Count) ||
!reader.Read(trait_bits.data(), trait_num_bytes) || !reader.Read(language_bits.data(), language_num_bytes) ||
!reader.ReadOptionalT(&entry.display_active_start_offset) ||
!reader.ReadOptionalT(&entry.display_active_end_offset) ||
!reader.ReadOptionalT(&entry.display_line_start_offset) ||
!reader.ReadOptionalT(&entry.display_line_end_offset) || !reader.ReadOptionalT(&entry.display_crop_mode) ||
!reader.ReadOptionalT(&entry.display_deinterlacing_mode) || !reader.ReadOptionalT(&entry.dma_max_slice_ticks) ||
!reader.ReadOptionalT(&entry.dma_halt_ticks) || !reader.ReadOptionalT(&entry.cdrom_max_seek_speedup_cycles) ||
!reader.ReadOptionalT(&entry.cdrom_max_read_speedup_cycles) || !reader.ReadOptionalT(&entry.gpu_fifo_size) ||
!reader.ReadOptionalT(&entry.gpu_max_run_ahead) || !reader.ReadOptionalT(&entry.gpu_pgxp_tolerance) ||
!reader.ReadOptionalT(&entry.gpu_pgxp_depth_threshold) ||
!reader.ReadOptionalT(&entry.gpu_pgxp_preserve_proj_fp) || !reader.ReadOptionalT(&entry.gpu_line_detect_mode) ||
!reader.ReadOptionalT(&entry.cpu_overclock))
{
DEV_LOG("Cache entry is corrupted.");
return false;
}
entry.disc_set = (disc_set_index >= 0) ? &s_state.disc_sets[static_cast<u32>(disc_set_index)] : nullptr;
entry.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility);
entry.traits.reset();
for (size_t j = 0; j < static_cast<size_t>(Trait::MaxCount); j++)
{
if ((trait_bits[j / 8] & (1u << (j % 8))) != 0)
entry.traits[j] = true;
}
for (size_t j = 0; j < static_cast<size_t>(Language::MaxCount); j++)
{
if ((language_bits[j / 8] & (1u << (j % 8))) != 0)
entry.languages[j] = true;
}
}
for (u32 i = 0; i < num_codes; i++)
{
std::string code;
u32 index;
if (!reader.ReadSizePrefixedString(&code) || !reader.ReadU32(&index) ||
index >= static_cast<u32>(s_state.entries.size()))
{
DEV_LOG("Cache code entry is corrupted.");
return false;
}
s_state.code_lookup.emplace(std::move(code), index);
}
s_state.db_data = std::move(db_data.value());
return true;
}
bool GameDatabase::SaveToCache()
{
const u64 gamedb_ts = static_cast<u64>(Host::GetResourceFileTimestamp(GAMEDB_YAML_FILENAME, false).value_or(0));
const u64 discsets_ts = static_cast<u64>(Host::GetResourceFileTimestamp(DISCSETS_YAML_FILENAME, false).value_or(0));
Error error;
FileSystem::AtomicRenamedFile file = FileSystem::CreateAtomicRenamedFile(GetCacheFile(), &error);
if (!file)
{
ERROR_LOG("Failed to open cache file for writing: {}", error.GetDescription());
return false;
}
BinaryFileWriter writer(file.get());
writer.WriteU32(GAME_DATABASE_CACHE_SIGNATURE);
writer.WriteU32(GAME_DATABASE_CACHE_VERSION);
writer.WriteU64(gamedb_ts);
writer.WriteU64(discsets_ts);
writer.WriteU32(static_cast<u32>(s_state.disc_sets.size()));
writer.WriteU32(static_cast<u32>(s_state.entries.size()));
writer.WriteU32(static_cast<u32>(s_state.code_lookup.size()));
for (const DiscSetEntry& disc_set : s_state.disc_sets)
{
writer.WriteSizePrefixedString(disc_set.title);
writer.WriteSizePrefixedString(disc_set.sort_title);
writer.WriteSizePrefixedString(disc_set.localized_title);
writer.WriteSizePrefixedString(disc_set.save_title);
writer.WriteU32(static_cast<u32>(disc_set.serials.size()));
for (const std::string_view& ds_serial : disc_set.serials)
writer.WriteSizePrefixedString(ds_serial);
}
for (const Entry& entry : s_state.entries)
{
writer.WriteSizePrefixedString(entry.serial);
writer.WriteSizePrefixedString(entry.title);
writer.WriteSizePrefixedString(entry.sort_title);
writer.WriteSizePrefixedString(entry.localized_title);
writer.WriteSizePrefixedString(entry.save_title);
writer.WriteSizePrefixedString(entry.genre);
writer.WriteSizePrefixedString(entry.developer);
writer.WriteSizePrefixedString(entry.publisher);
writer.WriteSizePrefixedString(entry.compatibility_version_tested);
writer.WriteSizePrefixedString(entry.compatibility_comments);
writer.WriteS32(entry.disc_set ? static_cast<s32>(entry.disc_set - &s_state.disc_sets[0]) : -1);
writer.WriteU64(entry.release_date);
writer.WriteU8(entry.min_players);
writer.WriteU8(entry.max_players);
writer.WriteU8(entry.min_blocks);
writer.WriteU8(entry.max_blocks);
writer.WriteU16(entry.supported_controllers);
writer.WriteU8(static_cast<u8>(entry.compatibility));
constexpr u32 trait_num_bytes = (static_cast<u32>(Trait::MaxCount) + 7) / 8;
std::array<u8, trait_num_bytes> trait_bits = {};
for (size_t j = 0; j < static_cast<size_t>(Trait::MaxCount); j++)
{
if (entry.traits[j])
trait_bits[j / 8] |= (1u << (j % 8));
}
writer.Write(trait_bits.data(), trait_num_bytes);
constexpr u32 language_num_bytes = (static_cast<u32>(Language::MaxCount) + 7) / 8;
std::array<u8, language_num_bytes> language_bits = {};
for (size_t j = 0; j < static_cast<size_t>(Language::MaxCount); j++)
{
if (entry.languages[j])
language_bits[j / 8] |= (1u << (j % 8));
}
writer.Write(language_bits.data(), language_num_bytes);
writer.WriteOptionalT(entry.display_active_start_offset);
writer.WriteOptionalT(entry.display_active_end_offset);
writer.WriteOptionalT(entry.display_line_start_offset);
writer.WriteOptionalT(entry.display_line_end_offset);
writer.WriteOptionalT(entry.display_crop_mode);
writer.WriteOptionalT(entry.display_deinterlacing_mode);
writer.WriteOptionalT(entry.dma_max_slice_ticks);
writer.WriteOptionalT(entry.dma_halt_ticks);
writer.WriteOptionalT(entry.cdrom_max_seek_speedup_cycles);
writer.WriteOptionalT(entry.cdrom_max_read_speedup_cycles);
writer.WriteOptionalT(entry.gpu_fifo_size);
writer.WriteOptionalT(entry.gpu_max_run_ahead);
writer.WriteOptionalT(entry.gpu_pgxp_tolerance);
writer.WriteOptionalT(entry.gpu_pgxp_depth_threshold);
writer.WriteOptionalT(entry.gpu_pgxp_preserve_proj_fp);
writer.WriteOptionalT(entry.gpu_line_detect_mode);
writer.WriteOptionalT(entry.cpu_overclock);
}
for (const auto& it : s_state.code_lookup)
{
writer.WriteSizePrefixedString(it.first);
writer.WriteU32(it.second);
}
return true;
}
bool GameDatabase::LoadGameDBYaml()
{
Error error;
std::optional<DynamicHeapArray<u8>> gamedb_data = Host::ReadResourceFile(GAMEDB_YAML_FILENAME, false, &error);
if (!gamedb_data.has_value())
{
ERROR_LOG("Failed to read game database: {}", error.GetDescription());
return false;
}
std::optional<DynamicHeapArray<u8>> disc_set_data = Host::ReadResourceFile(DISCSETS_YAML_FILENAME, false, &error);
if (!disc_set_data.has_value())
{
ERROR_LOG("Failed to read disc set database: {}", error.GetDescription());
return false;
}
PreferUnorderedStringMap<std::string_view> code_lookup;
{
const ryml::Tree tree = ryml::parse_in_place(
to_csubstr(GAMEDB_YAML_FILENAME), c4::substr(reinterpret_cast<char*>(gamedb_data->data()), gamedb_data->size()));
const ryml::ConstNodeRef root = tree.rootref();
s_state.entries.reserve(root.num_children());
for (const ryml::ConstNodeRef& current : root.cchildren())
{
const std::string_view serial = to_stringview(current.key());
if (current.empty())
{
ERROR_LOG("Missing serial for entry.");
return false;
}
Entry& entry = s_state.entries.emplace_back();
entry.serial = serial;
if (!ParseYamlEntry(&entry, current))
{
s_state.entries.pop_back();
continue;
}
ParseYamlCodes(code_lookup, current, serial);
}
}
// Sorting must be done before generating code lookup, because otherwise the indices won't match.
s_state.entries.shrink_to_fit();
std::sort(s_state.entries.begin(), s_state.entries.end(),
[](const Entry& lhs, const Entry& rhs) { return (lhs.serial < rhs.serial); });
for (const auto& [code, serial] : code_lookup)
{
const auto it =
std::lower_bound(s_state.entries.cbegin(), s_state.entries.cend(), serial,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
if (it == s_state.entries.end() || it->serial != serial)
{
ERROR_LOG("Somehow we messed up our code lookup for {} and {}?!", code, serial);
continue;
}
if (!s_state.code_lookup.emplace(code, static_cast<u32>(std::distance(s_state.entries.cbegin(), it))).second)
ERROR_LOG("Failed to insert code {}", code);
}
if (s_state.entries.empty())
{
ERROR_LOG("Game database is empty.");
return false;
}
// parse disc sets
{
const ryml::Tree tree =
ryml::parse_in_place(to_csubstr(DISCSETS_YAML_FILENAME),
c4::substr(reinterpret_cast<char*>(disc_set_data->data()), disc_set_data->size()));
const ryml::ConstNodeRef root = tree.rootref();
s_state.disc_sets.reserve(root.num_children());
for (const ryml::ConstNodeRef& current : root.cchildren())
{
DiscSetEntry& disc_set = s_state.disc_sets.emplace_back();
if (!ParseYamlDiscSetEntry(&disc_set, current))
{
s_state.disc_sets.pop_back();
continue;
}
}
s_state.disc_sets.shrink_to_fit();
BindDiscSetsToEntries();
}
s_state.db_data = std::move(gamedb_data.value());
s_state.disc_set_db_data = std::move(disc_set_data.value());
return true;
}
bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value)
{
GetStringFromObject(value, "name", &entry->title);
GetStringFromObject(value, "sortName", &entry->sort_title);
GetStringFromObject(value, "localizedName", &entry->localized_title);
GetStringFromObject(value, "saveName", &entry->save_title);
entry->supported_controllers = static_cast<u16>(~0u);
if (const ryml::ConstNodeRef controllers = value.find_child(to_csubstr("controllers"));
controllers.valid() && controllers.has_children())
{
bool first = true;
for (const ryml::ConstNodeRef& controller : controllers.cchildren())
{
const std::string_view controller_str = to_stringview(controller.val());
if (controller_str.empty())
{
WARNING_LOG("controller is not a string in {}", entry->serial);
return false;
}
const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(controller_str);
if (!cinfo)
{
WARNING_LOG("Invalid controller type {} in {}", controller_str, entry->serial);
continue;
}
if (first)
{
entry->supported_controllers = 0;
first = false;
}
entry->supported_controllers |= (1u << static_cast<u16>(cinfo->type));
}
}
if (const ryml::ConstNodeRef metadata = value.find_child(to_csubstr("metadata")); metadata.valid())
{
GetStringFromObject(metadata, "genre", &entry->genre);
GetStringFromObject(metadata, "developer", &entry->developer);
GetStringFromObject(metadata, "publisher", &entry->publisher);
GetUIntFromObject(metadata, "minPlayers", &entry->min_players);
GetUIntFromObject(metadata, "maxPlayers", &entry->max_players);
GetUIntFromObject(metadata, "minBlocks", &entry->min_blocks);
GetUIntFromObject(metadata, "maxBlocks", &entry->max_blocks);
if (const ryml::ConstNodeRef languages = metadata.find_child(to_csubstr("languages")); languages.valid())
{
for (const ryml::ConstNodeRef language : languages.cchildren())
{
const std::string_view vlanguage = to_stringview(language.val());
if (const std::optional<Language> planguage = ParseLanguageName(vlanguage); planguage.has_value())
entry->languages[static_cast<size_t>(planguage.value())] = true;
else
WARNING_LOG("Unknown language {} in {}.", vlanguage, entry->serial);
}
}
entry->release_date = 0;
{
std::string release_date;
if (GetStringFromObject(metadata, "releaseDate", &release_date))
{
std::istringstream iss(release_date);
struct tm parsed_time = {};
iss >> std::get_time(&parsed_time, "%Y-%m-%d");
if (!iss.fail())
{
parsed_time.tm_isdst = 0;
#ifdef _WIN32
entry->release_date = _mkgmtime(&parsed_time);
#else
entry->release_date = timegm(&parsed_time);
#endif
}
}
}
if (const ryml::ConstNodeRef& multitap = metadata.find_child(to_csubstr("multitap")); multitap.valid())
{
if (const std::optional multitap_val = StringUtil::FromChars<bool>(to_stringview(multitap.val()));
multitap_val.has_value())
{
if (multitap_val.value())
entry->supported_controllers |= Entry::SUPPORTS_MULTITAP_BIT;
}
else
{
WARNING_LOG("Invalid multitap value in {}", entry->serial);
}
}
}
if (const ryml::ConstNodeRef compatibility = value.find_child(to_csubstr("compatibility"));
compatibility.valid() && compatibility.has_children())
{
const ryml::ConstNodeRef rating = compatibility.find_child(to_csubstr("rating"));
if (rating.valid())
{
const std::string_view rating_str = to_stringview(rating.val());
const auto iter = std::find(s_compatibility_rating_names.begin(), s_compatibility_rating_names.end(), rating_str);
if (iter != s_compatibility_rating_names.end())
{
const size_t rating_idx = static_cast<size_t>(std::distance(s_compatibility_rating_names.begin(), iter));
DebugAssert(rating_idx < static_cast<size_t>(CompatibilityRating::Count));
entry->compatibility = static_cast<CompatibilityRating>(rating_idx);
}
else
{
WARNING_LOG("Unknown compatibility rating {} in {}", rating_str, entry->serial);
}
}
GetStringFromObject(compatibility, "versionTested", &entry->compatibility_version_tested);
GetStringFromObject(compatibility, "comments", &entry->compatibility_comments);
}
if (const ryml::ConstNodeRef traits = value.find_child(to_csubstr("traits")); traits.valid() && traits.has_children())
{
for (const ryml::ConstNodeRef& trait : traits.cchildren())
{
const std::string_view trait_str = to_stringview(trait.val());
if (trait_str.empty())
{
WARNING_LOG("Empty trait in {}", entry->serial);
continue;
}
const auto iter = std::find(s_trait_names.begin(), s_trait_names.end(), trait_str);
if (iter == s_trait_names.end())
{
WARNING_LOG("Unknown trait {} in {}", trait_str, entry->serial);
continue;
}
const size_t trait_idx = static_cast<size_t>(std::distance(s_trait_names.begin(), iter));
DebugAssert(trait_idx < static_cast<size_t>(Trait::MaxCount));
entry->traits[trait_idx] = true;
}
}
if (const ryml::ConstNodeRef& libcrypt = value.find_child(to_csubstr("libcrypt")); libcrypt.valid())
{
if (const std::optional libcrypt_val = StringUtil::FromChars<bool>(to_stringview(libcrypt.val()));
libcrypt_val.has_value())
{
entry->traits[static_cast<size_t>(Trait::IsLibCryptProtected)] = libcrypt_val.value();
}
else
{
WARNING_LOG("Invalid libcrypt value in {}", entry->serial);
}
}
if (const ryml::ConstNodeRef settings = value.find_child(to_csubstr("settings"));
settings.valid() && settings.has_children())
{
entry->display_active_start_offset = GetOptionalTFromObject<s16>(settings, "displayActiveStartOffset");
entry->display_active_end_offset = GetOptionalTFromObject<s16>(settings, "displayActiveEndOffset");
entry->display_line_start_offset = GetOptionalTFromObject<s8>(settings, "displayLineStartOffset");
entry->display_line_end_offset = GetOptionalTFromObject<s8>(settings, "displayLineEndOffset");
entry->display_crop_mode =
ParseOptionalTFromObject<DisplayCropMode>(settings, "displayCropMode", &Settings::ParseDisplayCropMode);
entry->display_deinterlacing_mode = ParseOptionalTFromObject<DisplayDeinterlacingMode>(
settings, "displayDeinterlacingMode", &Settings::ParseDisplayDeinterlacingMode);
entry->dma_max_slice_ticks = GetOptionalTFromObject<u32>(settings, "dmaMaxSliceTicks");
entry->dma_halt_ticks = GetOptionalTFromObject<u32>(settings, "dmaHaltTicks");
entry->cdrom_max_seek_speedup_cycles = GetOptionalTFromObject<u32>(settings, "cdromMaxSeekSpeedupCycles");
entry->cdrom_max_read_speedup_cycles = GetOptionalTFromObject<u32>(settings, "cdromMaxReadSpeedupCycles");
entry->gpu_fifo_size = GetOptionalTFromObject<u32>(settings, "gpuFIFOSize");
entry->gpu_max_run_ahead = GetOptionalTFromObject<u32>(settings, "gpuMaxRunAhead");
entry->gpu_pgxp_tolerance = GetOptionalTFromObject<float>(settings, "gpuPGXPTolerance");
entry->gpu_pgxp_depth_threshold = GetOptionalTFromObject<float>(settings, "gpuPGXPDepthThreshold");
entry->gpu_pgxp_preserve_proj_fp = GetOptionalTFromObject<bool>(settings, "gpuPGXPPreserveProjFP");
entry->gpu_line_detect_mode =
ParseOptionalTFromObject<GPULineDetectMode>(settings, "gpuLineDetectMode", &Settings::ParseLineDetectModeName);
entry->cpu_overclock = GetOptionalTFromObject<u8>(settings, "cpuOverclockPercent");
}
return true;
}
bool GameDatabase::ParseYamlDiscSetEntry(DiscSetEntry* entry, const ryml::ConstNodeRef& value)
{
if (!GetStringFromObject(value, "name", &entry->title) || entry->title.empty())
{
WARNING_LOG("Disc set entry is missing a title.");
return false;
}
GetStringFromObject(value, "sortName", &entry->sort_title);
GetStringFromObject(value, "localizedName", &entry->localized_title);
GetStringFromObject(value, "saveName", &entry->save_title);
// ensure there are no duplicates, linear search is okay here because there aren't that many disc sets
const std::string_view save_title = entry->GetSaveTitle();
const size_t search_count = s_state.disc_sets.size() - 1;
for (size_t i = 0; i < search_count; i++)
{
if (s_state.disc_sets[i].GetSaveTitle() == save_title)
{
WARNING_LOG("Duplicate disc set title {}.", save_title);
return false;
}
}
if (const ryml::ConstNodeRef set_serials = value.find_child("serials");
set_serials.valid() && set_serials.has_children())
{
entry->serials.reserve(set_serials.num_children());
for (const ryml::ConstNodeRef& serial : set_serials)
{
const std::string_view serial_str = to_stringview(serial.val());
if (serial_str.empty())
{
WARNING_LOG("Empty disc set serial in {}", entry->title);
continue;
}
if (std::find(entry->serials.begin(), entry->serials.end(), serial_str) != entry->serials.end())
{
WARNING_LOG("Duplicate serial {} in disc set serials for {}", serial_str, entry->title);
continue;
}
const auto entry_it =
std::lower_bound(s_state.entries.begin(), s_state.entries.end(), serial_str,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
if (entry_it == s_state.entries.end() || entry_it->serial != serial_str)
{
WARNING_LOG("Serial {} in disc set {} does not exist in game database.", serial_str, entry->title);
continue;
}
else if (entry_it->disc_set)
{
WARNING_LOG("Serial {} in disc set {} is already part of another disc set.", serial_str, entry->title);
continue;
}
entry->serials.emplace_back(serial_str);
}
}
if (entry->serials.empty())
{
WARNING_LOG("Disc set {} has {} serials, dropping.", entry->title, entry->serials.size());
return false;
}
return true;
}
void GameDatabase::BindDiscSetsToEntries()
{
for (DiscSetEntry& dsentry : s_state.disc_sets)
{
for (const std::string_view& serial : dsentry.serials)
{
const auto entry_it =
std::lower_bound(s_state.entries.begin(), s_state.entries.end(), serial,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
Assert(entry_it != s_state.entries.end() && entry_it->serial == serial);
// link the entry to this disc set
entry_it->disc_set = &dsentry;
}
}
}
bool GameDatabase::ParseYamlCodes(PreferUnorderedStringMap<std::string_view>& lookup, const ryml::ConstNodeRef& value,
std::string_view serial)
{
const ryml::ConstNodeRef& codes = value.find_child(to_csubstr("codes"));
if (!codes.valid() || !codes.has_children())
{
// use serial instead
auto iter = lookup.find(serial);
if (iter != lookup.end())
{
WARNING_LOG("Duplicate code '{}'", serial);
return false;
}
lookup.emplace(serial, serial);
return true;
}
u32 added = 0;
for (const ryml::ConstNodeRef& current_code : codes)
{
const std::string_view current_code_str = to_stringview(current_code.val());
if (current_code_str.empty())
{
WARNING_LOG("code is not a string in {}", serial);
continue;
}
auto iter = lookup.find(current_code_str);
if (iter != lookup.end())
{
WARNING_LOG("Duplicate code '{}' in {}", current_code_str, serial);
continue;
}
lookup.emplace(current_code_str, serial);
added++;
}
return (added > 0);
}
void GameDatabase::EnsureTrackHashesMapLoaded()
{
if (s_state.track_hashes_loaded)
return;
s_state.track_hashes_loaded = true;
LoadTrackHashes();
}
bool GameDatabase::LoadTrackHashes()
{
Timer load_timer;
Error error;
std::optional<std::string> gamedb_data(Host::ReadResourceFileToString(DISCDB_YAML_FILENAME, false, &error));
if (!gamedb_data.has_value())
{
ERROR_LOG("Failed to read disc database: {}", error.GetDescription());
return false;
}
// TODO: Parse in-place, avoid string allocations.
const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(DISCDB_YAML_FILENAME), to_csubstr(gamedb_data.value()));
const ryml::ConstNodeRef root = tree.rootref();
s_state.track_hashes_map = {};
size_t serials = 0;
for (const ryml::ConstNodeRef& current : root.cchildren())
{
const std::string_view serial = to_stringview(current.key());
if (serial.empty() || !current.has_children())
{
WARNING_LOG("entry is not an object");
continue;
}
const ryml::ConstNodeRef track_data = current.find_child(to_csubstr("trackData"));
if (!track_data.valid() || !track_data.has_children())
{
WARNING_LOG("trackData is missing in {}", serial);
continue;
}
u32 revision = 0;
for (const ryml::ConstNodeRef& track_revisions : track_data.cchildren())
{
const ryml::ConstNodeRef tracks = track_revisions.find_child(to_csubstr("tracks"));
if (!tracks.valid() || !tracks.has_children())
{
WARNING_LOG("tracks member is missing in {}", serial);
continue;
}
std::string revision_string;
GetStringFromObject(track_revisions, "version", &revision_string);
for (const ryml::ConstNodeRef& track : tracks)
{
const ryml::ConstNodeRef md5 = track.find_child("md5");
std::string_view md5_str;
if (!md5.valid() || (md5_str = to_stringview(md5.val())).empty())
{
WARNING_LOG("md5 is missing in track in {}", serial);
continue;
}
const std::optional<CDImageHasher::Hash> md5o = CDImageHasher::HashFromString(md5_str);
if (md5o.has_value())
{
s_state.track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5o.value()),
std::forward_as_tuple(std::string(serial), revision_string, revision));
}
else
{
WARNING_LOG("invalid md5 in {}", serial);
}
}
revision++;
}
serials++;
}
ryml::reset_callbacks();
INFO_LOG("Loaded {} track hashes from {} serials in {:.0f}ms.", s_state.track_hashes_map.size(), serials,
load_timer.GetTimeMilliseconds());
return !s_state.track_hashes_map.empty();
}
const GameDatabase::TrackHashesMap& GameDatabase::GetTrackHashesMap()
{
EnsureTrackHashesMapLoaded();
return s_state.track_hashes_map;
}