duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

game_database.cpp (50254B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "game_database.h"
      5 #include "controller.h"
      6 #include "host.h"
      7 #include "system.h"
      8 
      9 #include "util/cd_image.h"
     10 #include "util/imgui_manager.h"
     11 
     12 #include "common/assert.h"
     13 #include "common/binary_reader_writer.h"
     14 #include "common/error.h"
     15 #include "common/file_system.h"
     16 #include "common/heterogeneous_containers.h"
     17 #include "common/log.h"
     18 #include "common/path.h"
     19 #include "common/string_util.h"
     20 #include "common/timer.h"
     21 
     22 #include "ryml.hpp"
     23 
     24 #include <iomanip>
     25 #include <memory>
     26 #include <optional>
     27 #include <sstream>
     28 #include <type_traits>
     29 
     30 #include "IconsEmoji.h"
     31 #include "IconsFontAwesome5.h"
     32 
     33 Log_SetChannel(GameDatabase);
     34 
     35 namespace GameDatabase {
     36 
     37 enum : u32
     38 {
     39   GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48,
     40   GAME_DATABASE_CACHE_VERSION = 14,
     41 };
     42 
     43 static Entry* GetMutableEntry(std::string_view serial);
     44 static const Entry* GetEntryForId(std::string_view code);
     45 
     46 static bool LoadFromCache();
     47 static bool SaveToCache();
     48 
     49 static void SetRymlCallbacks();
     50 static bool LoadGameDBYaml();
     51 static bool ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value);
     52 static bool ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial);
     53 static bool LoadTrackHashes();
     54 
     55 static constexpr const std::array<const char*, static_cast<int>(CompatibilityRating::Count)>
     56   s_compatibility_rating_names = {
     57     {"Unknown", "DoesntBoot", "CrashesInIntro", "CrashesInGame", "GraphicalAudioIssues", "NoIssues"}};
     58 
     59 static constexpr const std::array<const char*, static_cast<size_t>(CompatibilityRating::Count)>
     60   s_compatibility_rating_display_names = {
     61     {TRANSLATE_NOOP("GameDatabase", "Unknown"), TRANSLATE_NOOP("GameDatabase", "Doesn't Boot"),
     62      TRANSLATE_NOOP("GameDatabase", "Crashes In Intro"), TRANSLATE_NOOP("GameDatabase", "Crashes In-Game"),
     63      TRANSLATE_NOOP("GameDatabase", "Graphical/Audio Issues"), TRANSLATE_NOOP("GameDatabase", "No Issues")}};
     64 
     65 static constexpr const std::array<const char*, static_cast<u32>(GameDatabase::Trait::Count)> s_trait_names = {{
     66   "ForceInterpreter",
     67   "ForceSoftwareRenderer",
     68   "ForceSoftwareRendererForReadbacks",
     69   "ForceRoundTextureCoordinates",
     70   "ForceAccurateBlending",
     71   "ForceInterlacing",
     72   "DisableAutoAnalogMode",
     73   "DisableTrueColor",
     74   "DisableUpscaling",
     75   "DisableTextureFiltering",
     76   "DisableSpriteTextureFiltering",
     77   "DisableScaledDithering",
     78   "DisableForceNTSCTimings",
     79   "DisableWidescreen",
     80   "DisablePGXP",
     81   "DisablePGXPCulling",
     82   "DisablePGXPTextureCorrection",
     83   "DisablePGXPColorCorrection",
     84   "DisablePGXPDepthBuffer",
     85   "DisablePGXPPreserveProjFP",
     86   "DisablePGXPOn2DPolygons",
     87   "ForcePGXPVertexCache",
     88   "ForcePGXPCPUMode",
     89   "ForceRecompilerMemoryExceptions",
     90   "ForceRecompilerICache",
     91   "ForceRecompilerLUTFastmem",
     92   "IsLibCryptProtected",
     93 }};
     94 
     95 static constexpr const std::array<const char*, static_cast<u32>(GameDatabase::Trait::Count)> s_trait_display_names = {{
     96   TRANSLATE_NOOP("GameDatabase", "Force Interpreter"),
     97   TRANSLATE_NOOP("GameDatabase", "Force Software Renderer"),
     98   TRANSLATE_NOOP("GameDatabase", "Force Software Renderer For Readbacks"),
     99   TRANSLATE_NOOP("GameDatabase", "Force Round Texture Coordinates"),
    100   TRANSLATE_NOOP("GameDatabase", "Force Accurate Blending"),
    101   TRANSLATE_NOOP("GameDatabase", "Force Interlacing"),
    102   TRANSLATE_NOOP("GameDatabase", "Disable Automatic Analog Mode"),
    103   TRANSLATE_NOOP("GameDatabase", "Disable True Color"),
    104   TRANSLATE_NOOP("GameDatabase", "Disable Upscaling"),
    105   TRANSLATE_NOOP("GameDatabase", "Disable Texture Filtering"),
    106   TRANSLATE_NOOP("GameDatabase", "Disable Sprite Texture Filtering"),
    107   TRANSLATE_NOOP("GameDatabase", "Disable Scaled Dithering"),
    108   TRANSLATE_NOOP("GameDatabase", "Disable Force NTSC Timings"),
    109   TRANSLATE_NOOP("GameDatabase", "Disable Widescreen"),
    110   TRANSLATE_NOOP("GameDatabase", "Disable PGXP"),
    111   TRANSLATE_NOOP("GameDatabase", "Disable PGXP Culling"),
    112   TRANSLATE_NOOP("GameDatabase", "Disable PGXP Texture Correction"),
    113   TRANSLATE_NOOP("GameDatabase", "Disable PGXP Color Correction"),
    114   TRANSLATE_NOOP("GameDatabase", "Disable PGXP Depth Buffer"),
    115   TRANSLATE_NOOP("GameDatabase", "Disable PGXP Preserve Projection Floating Point"),
    116   TRANSLATE_NOOP("GameDatabase", "Disable PGXP on 2D Polygons"),
    117   TRANSLATE_NOOP("GameDatabase", "Force PGXP Vertex Cache"),
    118   TRANSLATE_NOOP("GameDatabase", "Force PGXP CPU Mode"),
    119   TRANSLATE_NOOP("GameDatabase", "Force Recompiler Memory Exceptions"),
    120   TRANSLATE_NOOP("GameDatabase", "Force Recompiler ICache"),
    121   TRANSLATE_NOOP("GameDatabase", "Force Recompiler LUT Fastmem"),
    122   TRANSLATE_NOOP("GameDatabase", "Is LibCrypt Protected"),
    123 }};
    124 
    125 static constexpr const char* GAMEDB_YAML_FILENAME = "gamedb.yaml";
    126 static constexpr const char* DISCDB_YAML_FILENAME = "discdb.yaml";
    127 
    128 static bool s_loaded = false;
    129 static bool s_track_hashes_loaded = false;
    130 
    131 static std::vector<GameDatabase::Entry> s_entries;
    132 static PreferUnorderedStringMap<u32> s_code_lookup;
    133 
    134 static TrackHashesMap s_track_hashes_map;
    135 } // namespace GameDatabase
    136 
    137 // RapidYAML utility routines.
    138 
    139 ALWAYS_INLINE std::string_view to_stringview(const c4::csubstr& s)
    140 {
    141   return std::string_view(s.data(), s.size());
    142 }
    143 
    144 ALWAYS_INLINE std::string_view to_stringview(const c4::substr& s)
    145 {
    146   return std::string_view(s.data(), s.size());
    147 }
    148 
    149 ALWAYS_INLINE c4::csubstr to_csubstr(std::string_view sv)
    150 {
    151   return c4::csubstr(sv.data(), sv.length());
    152 }
    153 
    154 static bool GetStringFromObject(const ryml::ConstNodeRef& object, std::string_view key, std::string* dest)
    155 {
    156   dest->clear();
    157 
    158   const ryml::ConstNodeRef member = object.find_child(to_csubstr(key));
    159   if (!member.valid())
    160     return false;
    161 
    162   const c4::csubstr val = member.val();
    163   if (!val.empty())
    164     dest->assign(val.data(), val.size());
    165 
    166   return true;
    167 }
    168 
    169 template<typename T>
    170 static bool GetUIntFromObject(const ryml::ConstNodeRef& object, std::string_view key, T* dest)
    171 {
    172   *dest = 0;
    173 
    174   const ryml::ConstNodeRef member = object.find_child(to_csubstr(key));
    175   if (!member.valid())
    176     return false;
    177 
    178   const c4::csubstr val = member.val();
    179   if (val.empty())
    180   {
    181     ERROR_LOG("Unexpected empty value in {}", key);
    182     return false;
    183   }
    184 
    185   const std::optional<T> opt_value = StringUtil::FromChars<T>(to_stringview(val));
    186   if (!opt_value.has_value())
    187   {
    188     ERROR_LOG("Unexpected non-uint value in {}", key);
    189     return false;
    190   }
    191 
    192   *dest = opt_value.value();
    193   return true;
    194 }
    195 
    196 template<typename T>
    197 static std::optional<T> GetOptionalTFromObject(const ryml::ConstNodeRef& object, std::string_view key)
    198 {
    199   std::optional<T> ret;
    200 
    201   const ryml::ConstNodeRef member = object.find_child(to_csubstr(key));
    202   if (member.valid())
    203   {
    204     const c4::csubstr val = member.val();
    205     if (!val.empty())
    206     {
    207       ret = StringUtil::FromChars<T>(to_stringview(val));
    208       if (!ret.has_value())
    209       {
    210         if constexpr (std::is_floating_point_v<T>)
    211           ERROR_LOG("Unexpected non-float value in {}", key);
    212         else if constexpr (std::is_integral_v<T>)
    213           ERROR_LOG("Unexpected non-int value in {}", key);
    214       }
    215     }
    216     else
    217     {
    218       ERROR_LOG("Unexpected empty value in {}", key);
    219     }
    220   }
    221 
    222   return ret;
    223 }
    224 
    225 template<typename T>
    226 static std::optional<T> ParseOptionalTFromObject(const ryml::ConstNodeRef& object, std::string_view key,
    227                                                  std::optional<T> (*from_string_function)(const char* str))
    228 {
    229   std::optional<T> ret;
    230 
    231   const ryml::ConstNodeRef member = object.find_child(to_csubstr(key));
    232   if (member.valid())
    233   {
    234     const c4::csubstr val = member.val();
    235     if (!val.empty())
    236     {
    237       ret = from_string_function(TinyString(to_stringview(val)));
    238       if (!ret.has_value())
    239         ERROR_LOG("Unknown value for {}: {}", key, to_stringview(val));
    240     }
    241     else
    242     {
    243       ERROR_LOG("Unexpected empty value in {}", key);
    244     }
    245   }
    246 
    247   return ret;
    248 }
    249 
    250 void GameDatabase::EnsureLoaded()
    251 {
    252   if (s_loaded)
    253     return;
    254 
    255   Common::Timer timer;
    256 
    257   s_loaded = true;
    258 
    259   if (!LoadFromCache())
    260   {
    261     s_entries = {};
    262     s_code_lookup = {};
    263 
    264     LoadGameDBYaml();
    265     SaveToCache();
    266   }
    267 
    268   INFO_LOG("Database load of {} entries took {:.0f}ms.", s_entries.size(), timer.GetTimeMilliseconds());
    269 }
    270 
    271 void GameDatabase::Unload()
    272 {
    273   s_entries = {};
    274   s_code_lookup = {};
    275   s_loaded = false;
    276 }
    277 
    278 const GameDatabase::Entry* GameDatabase::GetEntryForId(std::string_view code)
    279 {
    280   if (code.empty())
    281     return nullptr;
    282 
    283   EnsureLoaded();
    284 
    285   auto iter = s_code_lookup.find(code);
    286   return (iter != s_code_lookup.end()) ? &s_entries[iter->second] : nullptr;
    287 }
    288 
    289 std::string GameDatabase::GetSerialForDisc(CDImage* image)
    290 {
    291   std::string ret;
    292 
    293   const GameDatabase::Entry* entry = GetEntryForDisc(image);
    294   if (entry)
    295     ret = entry->serial;
    296 
    297   return ret;
    298 }
    299 
    300 std::string GameDatabase::GetSerialForPath(const char* path)
    301 {
    302   std::string ret;
    303 
    304   if (System::IsLoadableFilename(path) && !System::IsExeFileName(path) && !System::IsPsfFileName(path))
    305   {
    306     std::unique_ptr<CDImage> image(CDImage::Open(path, false, nullptr));
    307     if (image)
    308       ret = GetSerialForDisc(image.get());
    309   }
    310 
    311   return ret;
    312 }
    313 
    314 const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image)
    315 {
    316   std::string id;
    317   System::GameHash hash;
    318   System::GetGameDetailsFromImage(image, &id, &hash);
    319   const Entry* entry = GetEntryForGameDetails(id, hash);
    320   if (entry)
    321     return entry;
    322 
    323   WARNING_LOG("No entry found for disc '{}'", id);
    324   return nullptr;
    325 }
    326 
    327 const GameDatabase::Entry* GameDatabase::GetEntryForGameDetails(const std::string& id, u64 hash)
    328 {
    329   const Entry* entry;
    330 
    331   if (!id.empty())
    332   {
    333     entry = GetEntryForId(id);
    334     if (entry)
    335       return entry;
    336   }
    337 
    338   // some games with invalid serials use the hash
    339   entry = GetEntryForId(System::GetGameHashId(hash));
    340   if (entry)
    341     return entry;
    342 
    343   return nullptr;
    344 }
    345 
    346 const GameDatabase::Entry* GameDatabase::GetEntryForSerial(std::string_view serial)
    347 {
    348   EnsureLoaded();
    349 
    350   return GetMutableEntry(serial);
    351 }
    352 
    353 GameDatabase::Entry* GameDatabase::GetMutableEntry(std::string_view serial)
    354 {
    355   for (Entry& entry : s_entries)
    356   {
    357     if (entry.serial == serial)
    358       return &entry;
    359   }
    360 
    361   return nullptr;
    362 }
    363 
    364 const char* GameDatabase::GetTraitName(Trait trait)
    365 {
    366   return s_trait_names[static_cast<size_t>(trait)];
    367 }
    368 
    369 const char* GameDatabase::GetTraitDisplayName(Trait trait)
    370 {
    371   return Host::TranslateToCString("GameDatabase", s_trait_display_names[static_cast<size_t>(trait)]);
    372 }
    373 
    374 const char* GameDatabase::GetCompatibilityRatingName(CompatibilityRating rating)
    375 {
    376   return s_compatibility_rating_names[static_cast<int>(rating)];
    377 }
    378 
    379 const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating rating)
    380 {
    381   return (rating >= CompatibilityRating::Unknown && rating < CompatibilityRating::Count) ?
    382            Host::TranslateToCString("GameDatabase", s_compatibility_rating_display_names[static_cast<size_t>(rating)]) :
    383            "";
    384 }
    385 
    386 void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_messages) const
    387 {
    388   if (display_active_start_offset.has_value())
    389   {
    390     settings.display_active_start_offset = display_active_start_offset.value();
    391     if (display_osd_messages)
    392       INFO_LOG("GameDB: Display active start offset set to {}.", settings.display_active_start_offset);
    393   }
    394   if (display_active_end_offset.has_value())
    395   {
    396     settings.display_active_end_offset = display_active_end_offset.value();
    397     if (display_osd_messages)
    398       INFO_LOG("GameDB: Display active end offset set to {}.", settings.display_active_end_offset);
    399   }
    400   if (display_line_start_offset.has_value())
    401   {
    402     settings.display_line_start_offset = display_line_start_offset.value();
    403     if (display_osd_messages)
    404       INFO_LOG("GameDB: Display line start offset set to {}.", settings.display_line_start_offset);
    405   }
    406   if (display_line_end_offset.has_value())
    407   {
    408     settings.display_line_end_offset = display_line_end_offset.value();
    409     if (display_osd_messages)
    410       INFO_LOG("GameDB: Display line end offset set to {}.", settings.display_line_start_offset);
    411   }
    412   if (dma_max_slice_ticks.has_value())
    413   {
    414     settings.dma_max_slice_ticks = dma_max_slice_ticks.value();
    415     if (display_osd_messages)
    416       INFO_LOG("GameDB: DMA max slice ticks set to {}.", settings.dma_max_slice_ticks);
    417   }
    418   if (dma_halt_ticks.has_value())
    419   {
    420     settings.dma_halt_ticks = dma_halt_ticks.value();
    421     if (display_osd_messages)
    422       INFO_LOG("GameDB: DMA halt ticks set to {}.", settings.dma_halt_ticks);
    423   }
    424   if (gpu_fifo_size.has_value())
    425   {
    426     settings.gpu_fifo_size = gpu_fifo_size.value();
    427     if (display_osd_messages)
    428       INFO_LOG("GameDB: GPU FIFO size set to {}.", settings.gpu_fifo_size);
    429   }
    430   if (gpu_max_run_ahead.has_value())
    431   {
    432     settings.gpu_max_run_ahead = gpu_max_run_ahead.value();
    433     if (display_osd_messages)
    434       INFO_LOG("GameDB: GPU max runahead set to {}.", settings.gpu_max_run_ahead);
    435   }
    436   if (gpu_pgxp_tolerance.has_value())
    437   {
    438     settings.gpu_pgxp_tolerance = gpu_pgxp_tolerance.value();
    439     if (display_osd_messages)
    440       INFO_LOG("GameDB: GPU PGXP tolerance set to {}.", settings.gpu_pgxp_tolerance);
    441   }
    442   if (gpu_pgxp_depth_threshold.has_value())
    443   {
    444     settings.SetPGXPDepthClearThreshold(gpu_pgxp_depth_threshold.value());
    445     if (display_osd_messages)
    446       INFO_LOG("GameDB: GPU depth clear threshold set to {}.", settings.GetPGXPDepthClearThreshold());
    447   }
    448   if (gpu_line_detect_mode.has_value())
    449   {
    450     settings.gpu_line_detect_mode = gpu_line_detect_mode.value();
    451     if (display_osd_messages)
    452     {
    453       INFO_LOG("GameDB: GPU line detect mode set to {}.",
    454                Settings::GetLineDetectModeName(settings.gpu_line_detect_mode));
    455     }
    456   }
    457 
    458   SmallStackString<512> messages;
    459 #define APPEND_MESSAGE(msg)                                                                                            \
    460   do                                                                                                                   \
    461   {                                                                                                                    \
    462     messages.append("\n        \u2022 ");                                                                              \
    463     messages.append(msg);                                                                                              \
    464   } while (0)
    465 #define APPEND_MESSAGE_FMT(...)                                                                                        \
    466   do                                                                                                                   \
    467   {                                                                                                                    \
    468     messages.append("\n        \u2022 ");                                                                              \
    469     messages.append_format(__VA_ARGS__);                                                                               \
    470   } while (0)
    471 
    472   if (display_crop_mode.has_value())
    473   {
    474     if (display_osd_messages && settings.display_crop_mode != display_crop_mode.value())
    475     {
    476       APPEND_MESSAGE_FMT(TRANSLATE_FS("GameDatabase", "Display cropping set to {}."),
    477                          Settings::GetDisplayCropModeDisplayName(display_crop_mode.value()));
    478     }
    479 
    480     settings.display_crop_mode = display_crop_mode.value();
    481   }
    482 
    483   if (display_deinterlacing_mode.has_value())
    484   {
    485     if (display_osd_messages && settings.display_deinterlacing_mode != display_deinterlacing_mode.value())
    486     {
    487       APPEND_MESSAGE_FMT(TRANSLATE_FS("GameDatabase", "Deinterlacing set to {}."),
    488                          Settings::GetDisplayDeinterlacingModeDisplayName(display_deinterlacing_mode.value()));
    489     }
    490 
    491     settings.display_deinterlacing_mode = display_deinterlacing_mode.value();
    492   }
    493 
    494   if (HasTrait(Trait::ForceInterpreter))
    495   {
    496     if (display_osd_messages && settings.cpu_execution_mode != CPUExecutionMode::Interpreter)
    497       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "CPU recompiler disabled."));
    498 
    499     settings.cpu_execution_mode = CPUExecutionMode::Interpreter;
    500   }
    501 
    502   if (HasTrait(Trait::ForceSoftwareRenderer))
    503   {
    504     if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software)
    505       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Hardware rendering disabled."));
    506 
    507     settings.gpu_renderer = GPURenderer::Software;
    508   }
    509 
    510   if (HasTrait(Trait::ForceSoftwareRendererForReadbacks))
    511   {
    512     if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software)
    513       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Software renderer readbacks enabled."));
    514 
    515     settings.gpu_use_software_renderer_for_readbacks = true;
    516   }
    517 
    518   if (HasTrait(Trait::ForceRoundUpscaledTextureCoordinates))
    519   {
    520     settings.gpu_force_round_texcoords = true;
    521   }
    522 
    523   if (HasTrait(Trait::ForceAccurateBlending))
    524   {
    525     if (display_osd_messages && !settings.IsUsingSoftwareRenderer() && !settings.gpu_accurate_blending)
    526       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Accurate blending enabled."));
    527 
    528     settings.gpu_accurate_blending = true;
    529   }
    530 
    531   if (HasTrait(Trait::ForceInterlacing))
    532   {
    533     if (display_osd_messages && settings.gpu_disable_interlacing)
    534       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Interlaced rendering enabled."));
    535 
    536     settings.gpu_disable_interlacing = false;
    537   }
    538 
    539   if (HasTrait(Trait::DisableTrueColor))
    540   {
    541     if (display_osd_messages && settings.gpu_true_color)
    542       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "True color disabled."));
    543 
    544     settings.gpu_true_color = false;
    545   }
    546 
    547   if (HasTrait(Trait::DisableUpscaling))
    548   {
    549     if (display_osd_messages && settings.gpu_resolution_scale > 1)
    550       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Upscaling disabled."));
    551 
    552     settings.gpu_resolution_scale = 1;
    553   }
    554 
    555   if (HasTrait(Trait::DisableTextureFiltering))
    556   {
    557     if (display_osd_messages && (settings.gpu_texture_filter != GPUTextureFilter::Nearest ||
    558                                  g_settings.gpu_sprite_texture_filter != GPUTextureFilter::Nearest))
    559     {
    560       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Texture filtering disabled."));
    561     }
    562 
    563     settings.gpu_texture_filter = GPUTextureFilter::Nearest;
    564     settings.gpu_sprite_texture_filter = GPUTextureFilter::Nearest;
    565   }
    566 
    567   if (HasTrait(Trait::DisableSpriteTextureFiltering))
    568   {
    569     if (display_osd_messages && g_settings.gpu_sprite_texture_filter != GPUTextureFilter::Nearest)
    570     {
    571       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Sprite texture filtering disabled."));
    572     }
    573 
    574     settings.gpu_sprite_texture_filter = GPUTextureFilter::Nearest;
    575   }
    576 
    577   if (HasTrait(Trait::DisableScaledDithering))
    578   {
    579     if (display_osd_messages && settings.gpu_scaled_dithering)
    580       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Scaled dithering."));
    581 
    582     settings.gpu_scaled_dithering = false;
    583   }
    584 
    585   if (HasTrait(Trait::DisableWidescreen))
    586   {
    587     if (display_osd_messages && settings.gpu_widescreen_hack)
    588       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Widescreen rendering disabled."));
    589 
    590     settings.gpu_widescreen_hack = false;
    591   }
    592 
    593   if (HasTrait(Trait::DisableForceNTSCTimings))
    594   {
    595     if (display_osd_messages && settings.gpu_force_ntsc_timings)
    596       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "Force NTSC timings disabled."));
    597 
    598     settings.gpu_force_ntsc_timings = false;
    599   }
    600 
    601   if (HasTrait(Trait::DisablePGXP))
    602   {
    603     if (display_osd_messages && settings.gpu_pgxp_enable)
    604       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP geometry correction disabled."));
    605 
    606     settings.gpu_pgxp_enable = false;
    607   }
    608 
    609   if (HasTrait(Trait::DisablePGXPCulling))
    610   {
    611     if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_culling)
    612       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP culling correction disabled."));
    613 
    614     settings.gpu_pgxp_culling = false;
    615   }
    616 
    617   if (HasTrait(Trait::DisablePGXPTextureCorrection))
    618   {
    619     if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction)
    620       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP perspective correct textures disabled."));
    621 
    622     settings.gpu_pgxp_texture_correction = false;
    623   }
    624 
    625   if (HasTrait(Trait::DisablePGXPColorCorrection))
    626   {
    627     if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction &&
    628         settings.gpu_pgxp_color_correction)
    629     {
    630       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP perspective correct colors disabled."));
    631     }
    632 
    633     settings.gpu_pgxp_color_correction = false;
    634   }
    635 
    636   if (HasTrait(Trait::DisablePGXPPreserveProjFP))
    637   {
    638     if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_preserve_proj_fp)
    639       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP preserve projection precision disabled."));
    640 
    641     settings.gpu_pgxp_preserve_proj_fp = false;
    642   }
    643 
    644   if (HasTrait(Trait::ForcePGXPVertexCache))
    645   {
    646     if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_vertex_cache)
    647       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP vertex cache enabled."));
    648 
    649     settings.gpu_pgxp_vertex_cache = settings.gpu_pgxp_enable;
    650   }
    651   else if (settings.gpu_pgxp_enable && settings.gpu_pgxp_vertex_cache)
    652   {
    653     Host::AddIconOSDMessage(
    654       "gamedb_force_pgxp_vertex_cache", ICON_EMOJI_WARNING,
    655       TRANSLATE_STR(
    656         "GameDatabase",
    657         "PGXP Vertex Cache is enabled, but it is not required for this game. This may cause rendering errors."),
    658       Host::OSD_WARNING_DURATION);
    659   }
    660 
    661   if (HasTrait(Trait::ForcePGXPCPUMode))
    662   {
    663     if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_cpu)
    664     {
    665 #ifndef __ANDROID__
    666       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP CPU mode enabled."));
    667 #else
    668       Host::AddIconOSDMessage("gamedb_force_pgxp_cpu", ICON_EMOJI_WARNING,
    669                               "This game requires PGXP CPU mode, which increases system requirements.\n"
    670                               "      If the game runs too slow, disable PGXP for this game.",
    671                               Host::OSD_WARNING_DURATION);
    672 #endif
    673     }
    674 
    675     settings.gpu_pgxp_cpu = settings.gpu_pgxp_enable;
    676   }
    677   else if (settings.UsingPGXPCPUMode())
    678   {
    679     Host::AddIconOSDMessage(
    680       "gamedb_force_pgxp_cpu", ICON_EMOJI_WARNING,
    681       TRANSLATE_STR("GameDatabase",
    682                     "PGXP CPU mode is enabled, but it is not required for this game. This may cause rendering errors."),
    683       Host::OSD_WARNING_DURATION);
    684   }
    685 
    686   if (HasTrait(Trait::DisablePGXPDepthBuffer))
    687   {
    688     if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_depth_buffer)
    689       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP depth buffer disabled."));
    690 
    691     settings.gpu_pgxp_depth_buffer = false;
    692   }
    693 
    694   if (HasTrait(Trait::DisablePGXPOn2DPolygons))
    695   {
    696     if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_disable_2d)
    697       APPEND_MESSAGE(TRANSLATE_SV("GameDatabase", "PGXP disabled on 2D polygons."));
    698 
    699     g_settings.gpu_pgxp_disable_2d = true;
    700   }
    701 
    702   if (HasTrait(Trait::ForceRecompilerMemoryExceptions))
    703   {
    704     WARNING_LOG("Memory exceptions for recompiler forced by compatibility settings.");
    705     settings.cpu_recompiler_memory_exceptions = true;
    706   }
    707 
    708   if (HasTrait(Trait::ForceRecompilerICache))
    709   {
    710     WARNING_LOG("ICache for recompiler forced by compatibility settings.");
    711     settings.cpu_recompiler_icache = true;
    712   }
    713 
    714   if (settings.cpu_fastmem_mode == CPUFastmemMode::MMap && HasTrait(Trait::ForceRecompilerLUTFastmem))
    715   {
    716     WARNING_LOG("LUT fastmem for recompiler forced by compatibility settings.");
    717     settings.cpu_fastmem_mode = CPUFastmemMode::LUT;
    718   }
    719 
    720   if (!messages.empty())
    721   {
    722     Host::AddIconOSDMessage(
    723       "GameDBCompatibility", ICON_EMOJI_INFORMATION,
    724       fmt::format("{}{}", TRANSLATE_SV("GameDatabase", "Compatibility settings for this game have been applied."),
    725                   messages.view()),
    726       Host::OSD_WARNING_DURATION);
    727   }
    728 
    729 #undef APPEND_MESSAGE_FMT
    730 #undef APPEND_MESSAGE
    731 
    732 #define BIT_FOR(ctype) (static_cast<u16>(1) << static_cast<u32>(ctype))
    733 
    734   if (supported_controllers != 0 && supported_controllers != static_cast<u16>(-1))
    735   {
    736     for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
    737     {
    738       const ControllerType ctype = settings.controller_types[i];
    739       if (ctype == ControllerType::None)
    740         continue;
    741 
    742       if (supported_controllers & BIT_FOR(ctype))
    743         continue;
    744 
    745       // Special case: Dualshock is permitted when not supported as long as it's in digital mode.
    746       if (ctype == ControllerType::AnalogController &&
    747           (supported_controllers & BIT_FOR(ControllerType::DigitalController)) != 0)
    748       {
    749         continue;
    750       }
    751 
    752       if (display_osd_messages)
    753       {
    754         SmallString supported_controller_string;
    755         for (u32 j = 0; j < static_cast<u32>(ControllerType::Count); j++)
    756         {
    757           const ControllerType supported_ctype = static_cast<ControllerType>(j);
    758           if ((supported_controllers & BIT_FOR(supported_ctype)) == 0)
    759             continue;
    760 
    761           if (!supported_controller_string.empty())
    762             supported_controller_string.append(", ");
    763 
    764           supported_controller_string.append(Controller::GetControllerInfo(supported_ctype)->GetDisplayName());
    765         }
    766 
    767         Host::AddKeyedOSDMessage(
    768           "gamedb_controller_unsupported",
    769           fmt::format(TRANSLATE_FS("GameDatabase",
    770                                    "Controller in port {0} ({1}) is not supported for {2}.\nSupported controllers: "
    771                                    "{3}\nPlease configure a supported controller from the list above."),
    772                       i + 1u, Controller::GetControllerInfo(ctype)->GetDisplayName(), System::GetGameTitle(),
    773                       supported_controller_string),
    774           Host::OSD_CRITICAL_ERROR_DURATION);
    775       }
    776     }
    777   }
    778 
    779 #undef BIT_FOR
    780 }
    781 
    782 template<typename T>
    783 static inline void AppendIntegerSetting(SmallStringBase& str, bool& heading, std::string_view title,
    784                                         const std::optional<T>& value)
    785 {
    786   if (!value.has_value())
    787     return;
    788 
    789   if (!heading)
    790   {
    791     heading = true;
    792     str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings"));
    793   }
    794 
    795   str.append_format(" - {}: {}\n", title, value.value());
    796 }
    797 
    798 static inline void AppendFloatSetting(SmallStringBase& str, bool& heading, std::string_view title,
    799                                       const std::optional<float>& value)
    800 {
    801   if (!value.has_value())
    802     return;
    803 
    804   if (!heading)
    805   {
    806     heading = true;
    807     str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings"));
    808   }
    809 
    810   str.append_format(" - {}: {:.2f}\n", title, value.value());
    811 }
    812 
    813 template<typename T>
    814 static inline void AppendEnumSetting(SmallStringBase& str, bool& heading, std::string_view title,
    815                                      const char* (*get_display_name_func)(T), const std::optional<T>& value)
    816 {
    817   if (!value.has_value())
    818     return;
    819 
    820   if (!heading)
    821   {
    822     heading = true;
    823     str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings"));
    824   }
    825 
    826   str.append_format(" - {}: {}\n", title, get_display_name_func(value.value()));
    827 }
    828 
    829 std::string GameDatabase::Entry::GenerateCompatibilityReport() const
    830 {
    831   LargeString ret;
    832   ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Title"), title);
    833   ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Serial"), serial);
    834   ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Rating"),
    835                     GetCompatibilityRatingDisplayName(compatibility));
    836 
    837   if (!compatibility_version_tested.empty())
    838     ret.append_format("**{}:**\n{}\n\n", TRANSLATE_SV("GameDatabase", "Version Tested"), compatibility_version_tested);
    839 
    840   if (!compatibility_comments.empty())
    841     ret.append_format("**{}**\n\n{}\n\n", TRANSLATE_SV("GameDatabase", "Comments"), compatibility_comments);
    842 
    843   if (supported_controllers != 0)
    844   {
    845     ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Supported Controllers"));
    846 
    847     for (u32 j = 0; j < static_cast<u32>(ControllerType::Count); j++)
    848     {
    849       if ((supported_controllers & (static_cast<u16>(1) << j)) == 0)
    850         continue;
    851 
    852       ret.append_format(" - {}\n", Controller::GetControllerInfo(static_cast<ControllerType>(j))->GetDisplayName());
    853     }
    854 
    855     ret.append("\n");
    856   }
    857 
    858   if (traits.any())
    859   {
    860     ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Traits"));
    861     for (u32 i = 0; i < static_cast<u32>(Trait::Count); i++)
    862     {
    863       if (traits.test(i))
    864         ret.append_format(" - {}\n", GetTraitDisplayName(static_cast<Trait>(i)));
    865     }
    866     ret.append("\n");
    867   }
    868 
    869   bool settings_heading = false;
    870   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active Start Offset"),
    871                        display_active_start_offset);
    872   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active End Offset"),
    873                        display_active_end_offset);
    874   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line Start Offset"),
    875                        display_line_start_offset);
    876   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line End Offset"),
    877                        display_line_end_offset);
    878   AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Crop Mode"),
    879                     &Settings::GetDisplayCropModeDisplayName, display_crop_mode);
    880   AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Deinterlacing Mode"),
    881                     &Settings::GetDisplayDeinterlacingModeDisplayName, display_deinterlacing_mode);
    882   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Max Slice Ticks"), dma_max_slice_ticks);
    883   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Halt Ticks"), dma_halt_ticks);
    884   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU FIFO Size"), gpu_fifo_size);
    885   AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Max Runahead"), gpu_max_run_ahead);
    886   AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU PGXP Tolerance"), gpu_pgxp_tolerance);
    887   AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU PGXP Depth Threshold"),
    888                      gpu_pgxp_depth_threshold);
    889   AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Line Detect Mode"),
    890                     &Settings::GetLineDetectModeDisplayName, gpu_line_detect_mode);
    891 
    892   if (!disc_set_name.empty())
    893   {
    894     ret.append_format("**{}:** {}\n", TRANSLATE_SV("GameDatabase", "Disc Set"), disc_set_name);
    895     for (const std::string& ds_serial : disc_set_serials)
    896       ret.append_format(" - {}\n", ds_serial);
    897   }
    898 
    899   return std::string(ret.view());
    900 }
    901 
    902 static std::string GetCacheFile()
    903 {
    904   return Path::Combine(EmuFolders::Cache, "gamedb.cache");
    905 }
    906 
    907 bool GameDatabase::LoadFromCache()
    908 {
    909   auto fp = FileSystem::OpenManagedCFile(GetCacheFile().c_str(), "rb");
    910   if (!fp)
    911   {
    912     DEV_LOG("Cache does not exist, loading full database.");
    913     return false;
    914   }
    915 
    916   BinaryFileReader reader(fp.get());
    917   const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0);
    918 
    919   u32 signature, version, num_entries, num_codes;
    920   u64 file_gamedb_ts;
    921   if (!reader.ReadU32(&signature) || !reader.ReadU32(&version) || !reader.ReadU64(&file_gamedb_ts) ||
    922       !reader.ReadU32(&num_entries) || !reader.ReadU32(&num_codes) || signature != GAME_DATABASE_CACHE_SIGNATURE ||
    923       version != GAME_DATABASE_CACHE_VERSION)
    924   {
    925     DEV_LOG("Cache header is corrupted or version mismatch.");
    926     return false;
    927   }
    928 
    929   if (gamedb_ts != file_gamedb_ts)
    930   {
    931     DEV_LOG("Cache is out of date, recreating.");
    932     return false;
    933   }
    934 
    935   s_entries.reserve(num_entries);
    936 
    937   for (u32 i = 0; i < num_entries; i++)
    938   {
    939     Entry& entry = s_entries.emplace_back();
    940 
    941     constexpr u32 num_bytes = (static_cast<u32>(Trait::Count) + 7) / 8;
    942     std::array<u8, num_bytes> bits;
    943     u8 compatibility;
    944     u32 num_disc_set_serials;
    945 
    946     if (!reader.ReadSizePrefixedString(&entry.serial) || !reader.ReadSizePrefixedString(&entry.title) ||
    947         !reader.ReadSizePrefixedString(&entry.genre) || !reader.ReadSizePrefixedString(&entry.developer) ||
    948         !reader.ReadSizePrefixedString(&entry.publisher) ||
    949         !reader.ReadSizePrefixedString(&entry.compatibility_version_tested) ||
    950         !reader.ReadSizePrefixedString(&entry.compatibility_comments) || !reader.ReadU64(&entry.release_date) ||
    951         !reader.ReadU8(&entry.min_players) || !reader.ReadU8(&entry.max_players) || !reader.ReadU8(&entry.min_blocks) ||
    952         !reader.ReadU8(&entry.max_blocks) || !reader.ReadU16(&entry.supported_controllers) ||
    953         !reader.ReadU8(&compatibility) || compatibility >= static_cast<u8>(GameDatabase::CompatibilityRating::Count) ||
    954         !reader.Read(bits.data(), num_bytes) || !reader.ReadOptionalT(&entry.display_active_start_offset) ||
    955         !reader.ReadOptionalT(&entry.display_active_end_offset) ||
    956         !reader.ReadOptionalT(&entry.display_line_start_offset) ||
    957         !reader.ReadOptionalT(&entry.display_line_end_offset) || !reader.ReadOptionalT(&entry.display_crop_mode) ||
    958         !reader.ReadOptionalT(&entry.display_deinterlacing_mode) || !reader.ReadOptionalT(&entry.dma_max_slice_ticks) ||
    959         !reader.ReadOptionalT(&entry.dma_halt_ticks) || !reader.ReadOptionalT(&entry.gpu_fifo_size) ||
    960         !reader.ReadOptionalT(&entry.gpu_max_run_ahead) || !reader.ReadOptionalT(&entry.gpu_pgxp_tolerance) ||
    961         !reader.ReadOptionalT(&entry.gpu_pgxp_depth_threshold) || !reader.ReadOptionalT(&entry.gpu_line_detect_mode) ||
    962         !reader.ReadSizePrefixedString(&entry.disc_set_name) || !reader.ReadU32(&num_disc_set_serials))
    963     {
    964       DEV_LOG("Cache entry is corrupted.");
    965       return false;
    966     }
    967 
    968     if (num_disc_set_serials > 0)
    969     {
    970       entry.disc_set_serials.reserve(num_disc_set_serials);
    971       for (u32 j = 0; j < num_disc_set_serials; j++)
    972       {
    973         if (!reader.ReadSizePrefixedString(&entry.disc_set_serials.emplace_back()))
    974         {
    975           DEV_LOG("Cache entry is corrupted.");
    976           return false;
    977         }
    978       }
    979     }
    980 
    981     entry.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility);
    982     entry.traits.reset();
    983     for (u32 j = 0; j < static_cast<int>(Trait::Count); j++)
    984     {
    985       if ((bits[j / 8] & (1u << (j % 8))) != 0)
    986         entry.traits[j] = true;
    987     }
    988   }
    989 
    990   for (u32 i = 0; i < num_codes; i++)
    991   {
    992     std::string code;
    993     u32 index;
    994     if (!reader.ReadSizePrefixedString(&code) || !reader.ReadU32(&index) || index >= static_cast<u32>(s_entries.size()))
    995     {
    996       DEV_LOG("Cache code entry is corrupted.");
    997       return false;
    998     }
    999 
   1000     s_code_lookup.emplace(std::move(code), index);
   1001   }
   1002 
   1003   return true;
   1004 }
   1005 
   1006 bool GameDatabase::SaveToCache()
   1007 {
   1008   const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0);
   1009 
   1010   Error error;
   1011   FileSystem::AtomicRenamedFile file = FileSystem::CreateAtomicRenamedFile(GetCacheFile(), "wb", &error);
   1012   if (!file)
   1013   {
   1014     ERROR_LOG("Failed to open cache file for writing: {}", error.GetDescription());
   1015     return false;
   1016   }
   1017 
   1018   BinaryFileWriter writer(file.get());
   1019   writer.WriteU32(GAME_DATABASE_CACHE_SIGNATURE);
   1020   writer.WriteU32(GAME_DATABASE_CACHE_VERSION);
   1021   writer.WriteU64(static_cast<u64>(gamedb_ts));
   1022 
   1023   writer.WriteU32(static_cast<u32>(s_entries.size()));
   1024   writer.WriteU32(static_cast<u32>(s_code_lookup.size()));
   1025 
   1026   for (const Entry& entry : s_entries)
   1027   {
   1028     writer.WriteSizePrefixedString(entry.serial);
   1029     writer.WriteSizePrefixedString(entry.title);
   1030     writer.WriteSizePrefixedString(entry.genre);
   1031     writer.WriteSizePrefixedString(entry.developer);
   1032     writer.WriteSizePrefixedString(entry.publisher);
   1033     writer.WriteSizePrefixedString(entry.compatibility_version_tested);
   1034     writer.WriteSizePrefixedString(entry.compatibility_comments);
   1035     writer.WriteU64(entry.release_date);
   1036     writer.WriteU8(entry.min_players);
   1037     writer.WriteU8(entry.max_players);
   1038     writer.WriteU8(entry.min_blocks);
   1039     writer.WriteU8(entry.max_blocks);
   1040     writer.WriteU16(entry.supported_controllers);
   1041     writer.WriteU8(static_cast<u8>(entry.compatibility));
   1042 
   1043     constexpr u32 num_bytes = (static_cast<u32>(Trait::Count) + 7) / 8;
   1044     std::array<u8, num_bytes> bits;
   1045     bits.fill(0);
   1046     for (u32 j = 0; j < static_cast<int>(Trait::Count); j++)
   1047     {
   1048       if (entry.traits[j])
   1049         bits[j / 8] |= (1u << (j % 8));
   1050     }
   1051 
   1052     writer.Write(bits.data(), num_bytes);
   1053 
   1054     writer.WriteOptionalT(entry.display_active_start_offset);
   1055     writer.WriteOptionalT(entry.display_active_end_offset);
   1056     writer.WriteOptionalT(entry.display_line_start_offset);
   1057     writer.WriteOptionalT(entry.display_line_end_offset);
   1058     writer.WriteOptionalT(entry.display_crop_mode);
   1059     writer.WriteOptionalT(entry.display_deinterlacing_mode);
   1060     writer.WriteOptionalT(entry.dma_max_slice_ticks);
   1061     writer.WriteOptionalT(entry.dma_halt_ticks);
   1062     writer.WriteOptionalT(entry.gpu_fifo_size);
   1063     writer.WriteOptionalT(entry.gpu_max_run_ahead);
   1064     writer.WriteOptionalT(entry.gpu_pgxp_tolerance);
   1065     writer.WriteOptionalT(entry.gpu_pgxp_depth_threshold);
   1066     writer.WriteOptionalT(entry.gpu_line_detect_mode);
   1067 
   1068     writer.WriteSizePrefixedString(entry.disc_set_name);
   1069     writer.WriteU32(static_cast<u32>(entry.disc_set_serials.size()));
   1070     for (const std::string& serial : entry.disc_set_serials)
   1071       writer.WriteSizePrefixedString(serial);
   1072   }
   1073 
   1074   for (const auto& it : s_code_lookup)
   1075   {
   1076     writer.WriteSizePrefixedString(it.first);
   1077     writer.WriteU32(it.second);
   1078   }
   1079 
   1080   return true;
   1081 }
   1082 
   1083 void GameDatabase::SetRymlCallbacks()
   1084 {
   1085   ryml::Callbacks callbacks = ryml::get_callbacks();
   1086   callbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void* userdata) {
   1087     ERROR_LOG("Parse error at {}:{} (bufpos={}): {}", loc.line, loc.col, loc.offset, std::string_view(msg, msg_len));
   1088   };
   1089   ryml::set_callbacks(callbacks);
   1090   c4::set_error_callback(
   1091     [](const char* msg, size_t msg_size) { ERROR_LOG("C4 error: {}", std::string_view(msg, msg_size)); });
   1092 }
   1093 
   1094 bool GameDatabase::LoadGameDBYaml()
   1095 {
   1096   const std::optional<std::string> gamedb_data = Host::ReadResourceFileToString(GAMEDB_YAML_FILENAME, false);
   1097   if (!gamedb_data.has_value())
   1098   {
   1099     ERROR_LOG("Failed to read game database");
   1100     return false;
   1101   }
   1102 
   1103   SetRymlCallbacks();
   1104 
   1105   const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(GAMEDB_YAML_FILENAME), to_csubstr(gamedb_data.value()));
   1106   const ryml::ConstNodeRef root = tree.rootref();
   1107   s_entries.reserve(root.num_children());
   1108 
   1109   for (const ryml::ConstNodeRef& current : root.children())
   1110   {
   1111     // TODO: binary sort
   1112     const u32 index = static_cast<u32>(s_entries.size());
   1113     Entry& entry = s_entries.emplace_back();
   1114     if (!ParseYamlEntry(&entry, current))
   1115     {
   1116       s_entries.pop_back();
   1117       continue;
   1118     }
   1119 
   1120     ParseYamlCodes(index, current, entry.serial);
   1121   }
   1122 
   1123   ryml::reset_callbacks();
   1124   return !s_entries.empty();
   1125 }
   1126 
   1127 bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value)
   1128 {
   1129   entry->serial = to_stringview(value.key());
   1130   if (entry->serial.empty())
   1131   {
   1132     ERROR_LOG("Missing serial for entry.");
   1133     return false;
   1134   }
   1135 
   1136   GetStringFromObject(value, "name", &entry->title);
   1137 
   1138   if (const ryml::ConstNodeRef metadata = value.find_child(to_csubstr("metadata")); metadata.valid())
   1139   {
   1140     GetStringFromObject(metadata, "genre", &entry->genre);
   1141     GetStringFromObject(metadata, "developer", &entry->developer);
   1142     GetStringFromObject(metadata, "publisher", &entry->publisher);
   1143 
   1144     GetUIntFromObject(metadata, "minPlayers", &entry->min_players);
   1145     GetUIntFromObject(metadata, "maxPlayers", &entry->max_players);
   1146     GetUIntFromObject(metadata, "minBlocks", &entry->min_blocks);
   1147     GetUIntFromObject(metadata, "maxBlocks", &entry->max_blocks);
   1148 
   1149     entry->release_date = 0;
   1150     {
   1151       std::string release_date;
   1152       if (GetStringFromObject(metadata, "releaseDate", &release_date))
   1153       {
   1154         std::istringstream iss(release_date);
   1155         struct tm parsed_time = {};
   1156         iss >> std::get_time(&parsed_time, "%Y-%m-%d");
   1157         if (!iss.fail())
   1158         {
   1159           parsed_time.tm_isdst = 0;
   1160 #ifdef _WIN32
   1161           entry->release_date = _mkgmtime(&parsed_time);
   1162 #else
   1163           entry->release_date = timegm(&parsed_time);
   1164 #endif
   1165         }
   1166       }
   1167     }
   1168   }
   1169 
   1170   entry->supported_controllers = static_cast<u16>(~0u);
   1171 
   1172   if (const ryml::ConstNodeRef controllers = value.find_child(to_csubstr("controllers"));
   1173       controllers.valid() && controllers.has_children())
   1174   {
   1175     bool first = true;
   1176     for (const ryml::ConstNodeRef& controller : controllers.children())
   1177     {
   1178       const std::string_view controller_str = to_stringview(controller.val());
   1179       if (controller_str.empty())
   1180       {
   1181         WARNING_LOG("controller is not a string in {}", entry->serial);
   1182         return false;
   1183       }
   1184 
   1185       const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(controller_str);
   1186       if (!cinfo)
   1187       {
   1188         WARNING_LOG("Invalid controller type {} in {}", controller_str, entry->serial);
   1189         continue;
   1190       }
   1191 
   1192       if (first)
   1193       {
   1194         entry->supported_controllers = 0;
   1195         first = false;
   1196       }
   1197 
   1198       entry->supported_controllers |= (1u << static_cast<u16>(cinfo->type));
   1199     }
   1200   }
   1201 
   1202   if (const ryml::ConstNodeRef compatibility = value.find_child(to_csubstr("compatibility"));
   1203       compatibility.valid() && compatibility.has_children())
   1204   {
   1205     const ryml::ConstNodeRef rating = compatibility.find_child(to_csubstr("rating"));
   1206     if (rating.valid())
   1207     {
   1208       const std::string_view rating_str = to_stringview(rating.val());
   1209 
   1210       const auto iter = std::find(s_compatibility_rating_names.begin(), s_compatibility_rating_names.end(), rating_str);
   1211       if (iter != s_compatibility_rating_names.end())
   1212       {
   1213         const size_t rating_idx = static_cast<size_t>(std::distance(s_compatibility_rating_names.begin(), iter));
   1214         DebugAssert(rating_idx < static_cast<size_t>(CompatibilityRating::Count));
   1215         entry->compatibility = static_cast<CompatibilityRating>(rating_idx);
   1216       }
   1217       else
   1218       {
   1219         WARNING_LOG("Unknown compatibility rating {} in {}", rating_str, entry->serial);
   1220       }
   1221     }
   1222 
   1223     GetStringFromObject(compatibility, "versionTested", &entry->compatibility_version_tested);
   1224     GetStringFromObject(compatibility, "comments", &entry->compatibility_comments);
   1225   }
   1226 
   1227   if (const ryml::ConstNodeRef traits = value.find_child(to_csubstr("traits")); traits.valid() && traits.has_children())
   1228   {
   1229     for (const ryml::ConstNodeRef& trait : traits.children())
   1230     {
   1231       const std::string_view trait_str = to_stringview(trait.val());
   1232       if (trait_str.empty())
   1233       {
   1234         WARNING_LOG("Empty trait in {}", entry->serial);
   1235         continue;
   1236       }
   1237 
   1238       const auto iter = std::find(s_trait_names.begin(), s_trait_names.end(), trait_str);
   1239       if (iter == s_trait_names.end())
   1240       {
   1241         WARNING_LOG("Unknown trait {} in {}", trait_str, entry->serial);
   1242         continue;
   1243       }
   1244 
   1245       const size_t trait_idx = static_cast<size_t>(std::distance(s_trait_names.begin(), iter));
   1246       DebugAssert(trait_idx < static_cast<size_t>(Trait::Count));
   1247       entry->traits[trait_idx] = true;
   1248     }
   1249   }
   1250 
   1251   if (const ryml::ConstNodeRef& libcrypt = value.find_child(to_csubstr("libcrypt")); libcrypt.valid())
   1252   {
   1253     if (const std::optional libcrypt_val = StringUtil::FromChars<bool>(to_stringview(libcrypt.val()));
   1254         libcrypt_val.has_value())
   1255     {
   1256       entry->traits[static_cast<size_t>(Trait::IsLibCryptProtected)] = true;
   1257     }
   1258     else
   1259     {
   1260       WARNING_LOG("Invalid libcrypt value in {}", entry->serial);
   1261     }
   1262   }
   1263 
   1264   if (const ryml::ConstNodeRef settings = value.find_child(to_csubstr("settings"));
   1265       settings.valid() && settings.has_children())
   1266   {
   1267     entry->display_active_start_offset = GetOptionalTFromObject<s16>(settings, "displayActiveStartOffset");
   1268     entry->display_active_end_offset = GetOptionalTFromObject<s16>(settings, "displayActiveEndOffset");
   1269     entry->display_line_start_offset = GetOptionalTFromObject<s8>(settings, "displayLineStartOffset");
   1270     entry->display_line_end_offset = GetOptionalTFromObject<s8>(settings, "displayLineEndOffset");
   1271     entry->display_crop_mode =
   1272       ParseOptionalTFromObject<DisplayCropMode>(settings, "displayCropMode", &Settings::ParseDisplayCropMode);
   1273     entry->display_deinterlacing_mode = ParseOptionalTFromObject<DisplayDeinterlacingMode>(
   1274       settings, "displayDeinterlacingMode", &Settings::ParseDisplayDeinterlacingMode);
   1275     entry->dma_max_slice_ticks = GetOptionalTFromObject<u32>(settings, "dmaMaxSliceTicks");
   1276     entry->dma_halt_ticks = GetOptionalTFromObject<u32>(settings, "dmaHaltTicks");
   1277     entry->gpu_fifo_size = GetOptionalTFromObject<u32>(settings, "gpuFIFOSize");
   1278     entry->gpu_max_run_ahead = GetOptionalTFromObject<u32>(settings, "gpuMaxRunAhead");
   1279     entry->gpu_pgxp_tolerance = GetOptionalTFromObject<float>(settings, "gpuPGXPTolerance");
   1280     entry->gpu_pgxp_depth_threshold = GetOptionalTFromObject<float>(settings, "gpuPGXPDepthThreshold");
   1281     entry->gpu_line_detect_mode =
   1282       ParseOptionalTFromObject<GPULineDetectMode>(settings, "gpuLineDetectMode", &Settings::ParseLineDetectModeName);
   1283   }
   1284 
   1285   if (const ryml::ConstNodeRef disc_set = value.find_child("discSet"); disc_set.valid() && disc_set.has_children())
   1286   {
   1287     GetStringFromObject(disc_set, "name", &entry->disc_set_name);
   1288 
   1289     if (const ryml::ConstNodeRef set_serials = disc_set.find_child("serials");
   1290         set_serials.valid() && set_serials.has_children())
   1291     {
   1292       entry->disc_set_serials.reserve(set_serials.num_children());
   1293       for (const ryml::ConstNodeRef& serial : set_serials)
   1294       {
   1295         const std::string_view serial_str = to_stringview(serial.val());
   1296         if (serial_str.empty())
   1297         {
   1298           WARNING_LOG("Empty disc set serial in {}", entry->serial);
   1299           continue;
   1300         }
   1301 
   1302         if (std::find(entry->disc_set_serials.begin(), entry->disc_set_serials.end(), serial_str) !=
   1303             entry->disc_set_serials.end())
   1304         {
   1305           WARNING_LOG("Duplicate serial {} in disc set serials for {}", serial_str, entry->serial);
   1306           continue;
   1307         }
   1308 
   1309         entry->disc_set_serials.emplace_back(serial_str);
   1310       }
   1311     }
   1312   }
   1313 
   1314   return true;
   1315 }
   1316 
   1317 bool GameDatabase::ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial)
   1318 {
   1319   const ryml::ConstNodeRef& codes = value.find_child(to_csubstr("codes"));
   1320   if (!codes.valid() || !codes.has_children())
   1321   {
   1322     // use serial instead
   1323     auto iter = s_code_lookup.find(serial);
   1324     if (iter != s_code_lookup.end())
   1325     {
   1326       WARNING_LOG("Duplicate code '{}'", serial);
   1327       return false;
   1328     }
   1329 
   1330     s_code_lookup.emplace(serial, index);
   1331     return true;
   1332   }
   1333 
   1334   u32 added = 0;
   1335   for (const ryml::ConstNodeRef& current_code : codes)
   1336   {
   1337     const std::string_view current_code_str = to_stringview(current_code.val());
   1338     if (current_code_str.empty())
   1339     {
   1340       WARNING_LOG("code is not a string in {}", serial);
   1341       continue;
   1342     }
   1343 
   1344     auto iter = s_code_lookup.find(current_code_str);
   1345     if (iter != s_code_lookup.end())
   1346     {
   1347       WARNING_LOG("Duplicate code '{}' in {}", current_code_str, serial);
   1348       continue;
   1349     }
   1350 
   1351     s_code_lookup.emplace(current_code_str, index);
   1352     added++;
   1353   }
   1354 
   1355   return (added > 0);
   1356 }
   1357 
   1358 void GameDatabase::EnsureTrackHashesMapLoaded()
   1359 {
   1360   if (s_track_hashes_loaded)
   1361     return;
   1362 
   1363   s_track_hashes_loaded = true;
   1364   LoadTrackHashes();
   1365 }
   1366 
   1367 bool GameDatabase::LoadTrackHashes()
   1368 {
   1369   Common::Timer load_timer;
   1370 
   1371   std::optional<std::string> gamedb_data(Host::ReadResourceFileToString(DISCDB_YAML_FILENAME, false));
   1372   if (!gamedb_data.has_value())
   1373   {
   1374     ERROR_LOG("Failed to read game database");
   1375     return false;
   1376   }
   1377 
   1378   SetRymlCallbacks();
   1379 
   1380   // TODO: Parse in-place, avoid string allocations.
   1381   const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(DISCDB_YAML_FILENAME), to_csubstr(gamedb_data.value()));
   1382   const ryml::ConstNodeRef root = tree.rootref();
   1383 
   1384   s_track_hashes_map = {};
   1385 
   1386   size_t serials = 0;
   1387   for (const ryml::ConstNodeRef& current : root.children())
   1388   {
   1389     const std::string_view serial = to_stringview(current.key());
   1390     if (serial.empty() || !current.has_children())
   1391     {
   1392       WARNING_LOG("entry is not an object");
   1393       continue;
   1394     }
   1395 
   1396     const ryml::ConstNodeRef track_data = current.find_child(to_csubstr("trackData"));
   1397     if (!track_data.valid() || !track_data.has_children())
   1398     {
   1399       WARNING_LOG("trackData is missing in {}", serial);
   1400       continue;
   1401     }
   1402 
   1403     u32 revision = 0;
   1404     for (const ryml::ConstNodeRef& track_revisions : track_data.children())
   1405     {
   1406       const ryml::ConstNodeRef tracks = track_revisions.find_child(to_csubstr("tracks"));
   1407       if (!tracks.valid() || !tracks.has_children())
   1408       {
   1409         WARNING_LOG("tracks member is missing in {}", serial);
   1410         continue;
   1411       }
   1412 
   1413       std::string revision_string;
   1414       GetStringFromObject(track_revisions, "version", &revision_string);
   1415 
   1416       for (const ryml::ConstNodeRef& track : tracks)
   1417       {
   1418         const ryml::ConstNodeRef md5 = track.find_child("md5");
   1419         std::string_view md5_str;
   1420         if (!md5.valid() || (md5_str = to_stringview(md5.val())).empty())
   1421         {
   1422           WARNING_LOG("md5 is missing in track in {}", serial);
   1423           continue;
   1424         }
   1425 
   1426         const std::optional<CDImageHasher::Hash> md5o = CDImageHasher::HashFromString(md5_str);
   1427         if (md5o.has_value())
   1428         {
   1429           s_track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5o.value()),
   1430                                      std::forward_as_tuple(std::string(serial), revision_string, revision));
   1431         }
   1432         else
   1433         {
   1434           WARNING_LOG("invalid md5 in {}", serial);
   1435         }
   1436       }
   1437       revision++;
   1438     }
   1439 
   1440     serials++;
   1441   }
   1442 
   1443   ryml::reset_callbacks();
   1444   INFO_LOG("Loaded {} track hashes from {} serials in {:.0f}ms.", s_track_hashes_map.size(), serials,
   1445            load_timer.GetTimeMilliseconds());
   1446   return !s_track_hashes_map.empty();
   1447 }
   1448 
   1449 const GameDatabase::TrackHashesMap& GameDatabase::GetTrackHashesMap()
   1450 {
   1451   EnsureTrackHashesMapLoaded();
   1452   return s_track_hashes_map;
   1453 }