// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // 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 "util/translation.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/progress_callback.h" #include "common/ryml_helpers.h" #include "common/string_util.h" #include "common/timer.h" #include "ryml.hpp" #include #include #include #include #include #include #include #include "IconsEmoji.h" #include "IconsFontAwesome.h" #include "fmt/format.h" LOG_CHANNEL(GameDatabase); namespace GameDatabase { namespace { enum : u32 { GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48, GAME_DATABASE_CACHE_VERSION = 33, }; /// Map of track hashes for image verification struct TrackData { TrackData(std::string serial_, std::string revision_str_, uint32_t revision_) : serial(std::move(serial_)), revision_str(std::move(revision_str_)), revision(revision_) { } friend bool operator==(const TrackData& left, const TrackData& right) { // 'revisionString' is deliberately ignored in comparisons as it's redundant with comparing 'revision'! Do not // change! return left.serial == right.serial && left.revision == right.revision; } std::string serial; std::string revision_str; u32 revision; }; struct TrackHashMatchResult { std::vector track_matches; std::string revision; std::string serial; }; using TrackHashesMap = std::multimap; } // namespace 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(UnorderedStringMap& lookup, const ryml::ConstNodeRef& value, std::string_view serial); static void BindDiscSetsToEntries(); static bool EnsureTrackHashesLoaded(Error* error); static bool LoadTrackHashes(Error* error); static TrackHashMatchResult MatchTrackHashes(const CDImageHasher::TrackHashes& track_hashes); static constexpr const std::array(CompatibilityRating::Count)> s_compatibility_rating_names = {{ "Unknown", "DoesntBoot", "CrashesInIntro", "CrashesInGame", "GraphicalAudioIssues", "NoIssues", }}; static constexpr const std::array(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(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(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(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 db_data; // we take strings from the data, so store a copy DynamicHeapArray disc_set_db_data; // if loaded from binary cache, this will be empty std::vector entries; std::vector disc_sets; UnorderedStringMap 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 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, nullptr); 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(trait)]; } const char* GameDatabase::GetTraitDisplayName(Trait trait) { return Host::TranslateToCString("GameDatabase", s_trait_display_names[static_cast(trait)]); } const char* GameDatabase::GetCompatibilityRatingName(CompatibilityRating rating) { return s_compatibility_rating_names[static_cast(rating)]; } const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating rating) { return (rating >= CompatibilityRating::Unknown && rating < CompatibilityRating::Count) ? Host::TranslateToCString("GameDatabase", s_compatibility_rating_display_names[static_cast(rating)], "CompatibilityRating") : ""; } const char* GameDatabase::GetLanguageName(Language language) { return s_language_names[static_cast(language)]; } std::optional GameDatabase::ParseLanguageName(std::string_view str) { for (size_t i = 0; i < static_cast(Language::MaxCount); i++) { if (str == s_language_names[i]) return static_cast(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(GameDatabase::Language::English))) ret = GameDatabase::GetLanguageName(static_cast(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(Language::MaxCount); i++) { if (languages.test(i)) { ret.append_format("{}{}", first ? "" : ", ", GetLanguageName(static_cast(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]; } std::optional GameDatabase::DiscSetEntry::GetDiscIndex(std::string_view serial) const { for (size_t i = 0; i < serials.size(); i++) { if (serials[i] == serial) return i; } return std::nullopt; } 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_end_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](fmt::format_string 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 (display_osd_messages && 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 (display_osd_messages && 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::AddIconOSDMessage( OSDMessageType::Warning, "GameDBDisableCDROMSpeedupUnnecessary", ICON_EMOJI_WARNING, TRANSLATE_STR("GameDatabase", "Disable CD-ROM speedup on MDEC is enabled, but it is not required for this game.")); } 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::AddIconOSDMessage( OSDMessageType::Warning, "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.")); } 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::AddIconOSDMessage(OSDMessageType::Warning, "gamedb_force_pgxp_cpu", ICON_EMOJI_WARNING, "This game requires PGXP CPU mode, which increases system requirements." "If the game runs too slow, disable PGXP for this game."); #endif } settings.gpu_pgxp_cpu = settings.gpu_pgxp_enable; } else if (settings.UsingPGXPCPUMode()) { Host::AddIconOSDMessage( OSDMessageType::Warning, "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.")); } 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.")); 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(OSDMessageType::Info, "GameDBCompatibility", ICON_EMOJI_INFORMATION, TRANSLATE_STR("GameDatabase", "Compatibility settings for this game have been applied."), std::string(messages.view())); } #define BIT_FOR(ctype) (static_cast(1) << static_cast(ctype)) if (supported_controllers != 0 && supported_controllers != static_cast(-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( TRANSLATE_SV("GameDatabase", "Please configure a supported controller from the following list:")); for (u32 j = 0; j < static_cast(ControllerType::Count); j++) { const ControllerType supported_ctype = static_cast(j); if ((supported_controllers & BIT_FOR(supported_ctype)) == 0) continue; supported_controller_string.append_format("\n \u2022 {}", Controller::GetControllerInfo(supported_ctype).GetDisplayName()); } Host::AddIconOSDMessage( OSDMessageType::Error, fmt::format("GameDBController{}Unsupported", i), ICON_EMOJI_WARNING, fmt::format(TRANSLATE_FS("GameDatabase", "Controller in Port {0} ({1}) is not supported for this game."), i + 1u, Controller::GetControllerInfo(ctype).GetDisplayName()), std::string(supported_controller_string)); } } if (g_settings.multitap_mode != MultitapMode::Disabled && !(supported_controllers & SUPPORTS_MULTITAP_BIT)) { Host::AddIconOSDMessage( OSDMessageType::Error, "GameDBMultitapUnsupported", ICON_EMOJI_WARNING, TRANSLATE_STR("GameDatabase", "This game does not support multitap, but multitap is enabled."), TRANSLATE_STR("GameDatabase", "This may result in dropped controller inputs.")); } } #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& value) { if (!value.has_value()) return; AppendSettingsHeading(str, heading); str.append_format(" - {}: {}\n", title, value.value() ? "Enabled" : "Disabled"); } template static inline void AppendIntegerSetting(SmallStringBase& str, bool& heading, std::string_view title, const std::optional& 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& value) { if (!value.has_value()) return; AppendSettingsHeading(str, heading); str.append_format(" - {}: {:.2f}\n", title, value.value()); } template static inline void AppendEnumSetting(SmallStringBase& str, bool& heading, std::string_view title, const char* (*get_display_name_func)(T), const std::optional& 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(ControllerType::Count); j++) { if ((supported_controllers & (static_cast(1) << j)) == 0) continue; ret.append_format(" - {}\n", Controller::GetControllerInfo(static_cast(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(Trait::MaxCount); i++) { if (traits.test(i)) ret.append_format(" - {}\n", GetTraitDisplayName(static_cast(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> 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(Host::GetResourceFileTimestamp(GAMEDB_YAML_FILENAME, false).value_or(0)); const u64 discsets_ts = static_cast(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(Trait::MaxCount) + 7) / 8; constexpr u32 language_num_bytes = (static_cast(Language::MaxCount) + 7) / 8; std::array trait_bits; std::array 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(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(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(disc_set_index)] : nullptr; entry.compatibility = static_cast(compatibility); entry.traits.reset(); for (size_t j = 0; j < static_cast(Trait::MaxCount); j++) { if ((trait_bits[j / 8] & (1u << (j % 8))) != 0) entry.traits[j] = true; } for (size_t j = 0; j < static_cast(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(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(Host::GetResourceFileTimestamp(GAMEDB_YAML_FILENAME, false).value_or(0)); const u64 discsets_ts = static_cast(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(s_state.disc_sets.size())); writer.WriteU32(static_cast(s_state.entries.size())); writer.WriteU32(static_cast(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(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(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(entry.compatibility)); constexpr u32 trait_num_bytes = (static_cast(Trait::MaxCount) + 7) / 8; std::array trait_bits = {}; for (size_t j = 0; j < static_cast(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(Language::MaxCount) + 7) / 8; std::array language_bits = {}; for (size_t j = 0; j < static_cast(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); } if (!writer.Flush(&error)) { ERROR_LOG("Failed to write cache file: {}", error.GetDescription()); FileSystem::DiscardAtomicRenamedFile(file); return false; } return true; } bool GameDatabase::LoadGameDBYaml() { Error error; std::optional> 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> 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; } UnorderedStringMap code_lookup; { const ryml::Tree tree = ryml::parse_in_place( to_csubstr(GAMEDB_YAML_FILENAME), c4::substr(reinterpret_cast(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(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(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(~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(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); GetIntFromObject(metadata, "minPlayers", &entry->min_players); GetIntFromObject(metadata, "maxPlayers", &entry->max_players); GetIntFromObject(metadata, "minBlocks", &entry->min_blocks); GetIntFromObject(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 planguage = ParseLanguageName(vlanguage); planguage.has_value()) entry->languages[static_cast(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(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(std::distance(s_compatibility_rating_names.begin(), iter)); DebugAssert(rating_idx < static_cast(CompatibilityRating::Count)); entry->compatibility = static_cast(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(std::distance(s_trait_names.begin(), iter)); DebugAssert(trait_idx < static_cast(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(to_stringview(libcrypt.val())); libcrypt_val.has_value()) { entry->traits[static_cast(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(settings, "displayActiveStartOffset"); entry->display_active_end_offset = GetOptionalTFromObject(settings, "displayActiveEndOffset"); entry->display_line_start_offset = GetOptionalTFromObject(settings, "displayLineStartOffset"); entry->display_line_end_offset = GetOptionalTFromObject(settings, "displayLineEndOffset"); entry->display_crop_mode = ParseOptionalTFromObject(settings, "displayCropMode", &Settings::ParseDisplayCropMode); entry->display_deinterlacing_mode = ParseOptionalTFromObject( settings, "displayDeinterlacingMode", &Settings::ParseDisplayDeinterlacingMode); entry->dma_max_slice_ticks = GetOptionalTFromObject(settings, "dmaMaxSliceTicks"); entry->dma_halt_ticks = GetOptionalTFromObject(settings, "dmaHaltTicks"); entry->cdrom_max_seek_speedup_cycles = GetOptionalTFromObject(settings, "cdromMaxSeekSpeedupCycles"); entry->cdrom_max_read_speedup_cycles = GetOptionalTFromObject(settings, "cdromMaxReadSpeedupCycles"); entry->gpu_fifo_size = GetOptionalTFromObject(settings, "gpuFIFOSize"); entry->gpu_max_run_ahead = GetOptionalTFromObject(settings, "gpuMaxRunAhead"); entry->gpu_pgxp_tolerance = GetOptionalTFromObject(settings, "gpuPGXPTolerance"); entry->gpu_pgxp_depth_threshold = GetOptionalTFromObject(settings, "gpuPGXPDepthThreshold"); entry->gpu_pgxp_preserve_proj_fp = GetOptionalTFromObject(settings, "gpuPGXPPreserveProjFP"); entry->gpu_line_detect_mode = ParseOptionalTFromObject(settings, "gpuLineDetectMode", &Settings::ParseLineDetectModeName); entry->cpu_overclock = GetOptionalTFromObject(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(UnorderedStringMap& 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); } bool GameDatabase::EnsureTrackHashesLoaded(Error* error) { if (s_state.track_hashes_loaded) return true; return (s_state.track_hashes_loaded = LoadTrackHashes(error)); } bool GameDatabase::LoadTrackHashes(Error* error) { Timer load_timer; std::optional gamedb_data(Host::ReadResourceFileToString(DISCDB_YAML_FILENAME, false, error)); if (!gamedb_data.has_value()) { Error::AddPrefix(error, "Failed to read disc database: "); 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 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(); } GameDatabase::TrackHashMatchResult GameDatabase::MatchTrackHashes(const CDImageHasher::TrackHashes& track_hashes) { TrackHashMatchResult result = {std::vector(track_hashes.size(), false), {}, {}}; DebugAssert(!track_hashes.empty()); const auto data_track_matches = s_state.track_hashes_map.equal_range(track_hashes[0]); if (data_track_matches.first != data_track_matches.second) { auto best_data_match = data_track_matches.second; for (auto iter = data_track_matches.first; iter != data_track_matches.second; ++iter) { std::vector current_verification_results(track_hashes.size(), false); const auto& data_track_attribs = iter->second; current_verification_results[0] = true; // Data track already matched for (auto audio_tracks_iter = std::next(track_hashes.begin()); audio_tracks_iter != track_hashes.end(); ++audio_tracks_iter) { auto audio_track_matches = s_state.track_hashes_map.equal_range(*audio_tracks_iter); for (auto audio_iter = audio_track_matches.first; audio_iter != audio_track_matches.second; ++audio_iter) { // If audio track comes from the same revision and code as the data track, "pass" it if (audio_iter->second == data_track_attribs) { current_verification_results[std::distance(track_hashes.begin(), audio_tracks_iter)] = true; break; } } } const auto old_matches_count = std::count(result.track_matches.begin(), result.track_matches.end(), true); const auto new_matches_count = std::count(current_verification_results.begin(), current_verification_results.end(), true); if (new_matches_count > old_matches_count) { best_data_match = iter; result.track_matches = std::move(current_verification_results); // If all elements got matched, early out { } } } result.revision = best_data_match->second.revision_str; result.serial = best_data_match->second.serial; } return result; } bool GameDatabase::VerifyImage(CDImage* image, std::string_view expected_serial, TrackVerificationResult* result, ProgressCallback* progress, Error* error) { CDImageHasher::TrackHashes track_hashes; const u32 num_tracks = image->GetTrackCount(); track_hashes.reserve(num_tracks); progress->SetState(TRANSLATE_SV("GameDatabase", "Verifying Image"), 0, num_tracks, true); for (u32 track = 0; track < num_tracks; track++) { progress->SetProgressValue(track); progress->PushState(); CDImageHasher::Hash hash; if (!CDImageHasher::GetTrackHash(image, static_cast(track + 1), &hash, progress, error)) { progress->PopState(); return false; } track_hashes.emplace_back(hash); progress->PopState(); } if (track_hashes.empty()) { Error::SetStringView(error, "Image contains no tracks."); return false; } if (!EnsureTrackHashesLoaded(error)) return false; TrackHashMatchResult match = MatchTrackHashes(track_hashes); std::string summary; if (!match.revision.empty()) summary = fmt::format(TRANSLATE_FS("GameDatabase", "Revision: {}"), match.revision); else summary = {}; if (match.serial != expected_serial) { if (match.serial.empty()) { summary = TRANSLATE_STR("GameDatabase", "No known dump found that matches this hash."); } else { const std::string mismatch = fmt::format(TRANSLATE_FS("GameDatabase", "Serial Mismatch: Disc {} vs Hash {}"), expected_serial, match.serial); summary = summary.empty() ? mismatch : fmt::format("{} | {}", mismatch, summary); } } result->track_hashes = std::move(track_hashes); result->track_matches = std::move(match.track_matches); result->summary = std::move(summary); return true; } std::string GameDatabase::GetRedumpSearchURL(std::string_view hash) { return fmt::format("https://redump.info/discs?q={}", hash); }