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

texture_replacements.cpp (10196B)


      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 "texture_replacements.h"
      5 #include "gpu_types.h"
      6 #include "host.h"
      7 #include "settings.h"
      8 
      9 #include "common/bitutils.h"
     10 #include "common/file_system.h"
     11 #include "common/hash_combine.h"
     12 #include "common/log.h"
     13 #include "common/path.h"
     14 #include "common/string_util.h"
     15 #include "common/timer.h"
     16 
     17 #include "fmt/format.h"
     18 #include "xxhash.h"
     19 #if defined(CPU_ARCH_X86) || defined(CPU_ARCH_X64)
     20 #include "xxh_x86dispatch.h"
     21 #endif
     22 
     23 #include <cinttypes>
     24 #include <tuple>
     25 #include <unordered_map>
     26 #include <vector>
     27 
     28 Log_SetChannel(TextureReplacements);
     29 
     30 namespace TextureReplacements {
     31 namespace {
     32 struct VRAMReplacementHash
     33 {
     34   u64 low;
     35   u64 high;
     36 
     37   TinyString ToString() const;
     38   bool ParseString(std::string_view sv);
     39 
     40   bool operator<(const VRAMReplacementHash& rhs) const { return std::tie(low, high) < std::tie(rhs.low, rhs.high); }
     41   bool operator==(const VRAMReplacementHash& rhs) const { return low == rhs.low && high == rhs.high; }
     42   bool operator!=(const VRAMReplacementHash& rhs) const { return low != rhs.low || high != rhs.high; }
     43 };
     44 
     45 struct VRAMReplacementHashMapHash
     46 {
     47   size_t operator()(const VRAMReplacementHash& hash) const;
     48 };
     49 } // namespace
     50 
     51 using VRAMWriteReplacementMap = std::unordered_map<VRAMReplacementHash, std::string, VRAMReplacementHashMapHash>;
     52 using TextureCache = std::unordered_map<std::string, ReplacementImage>;
     53 
     54 static bool ParseReplacementFilename(const std::string& filename, VRAMReplacementHash* replacement_hash,
     55                                      ReplacmentType* replacement_type);
     56 
     57 static std::string GetSourceDirectory();
     58 static std::string GetDumpDirectory();
     59 
     60 static VRAMReplacementHash GetVRAMWriteHash(u32 width, u32 height, const void* pixels);
     61 static std::string GetVRAMWriteDumpFilename(u32 width, u32 height, const void* pixels);
     62 
     63 static void FindTextures(const std::string& dir);
     64 
     65 static const ReplacementImage* LoadTexture(const std::string& filename);
     66 static void PreloadTextures();
     67 static void PurgeUnreferencedTexturesFromCache();
     68 
     69 static std::string s_game_id;
     70 
     71 // TODO: Check the size, purge some when it gets too large.
     72 static TextureCache s_texture_cache;
     73 
     74 static VRAMWriteReplacementMap s_vram_write_replacements;
     75 } // namespace TextureReplacements
     76 
     77 size_t TextureReplacements::VRAMReplacementHashMapHash::operator()(const VRAMReplacementHash& hash) const
     78 {
     79   size_t hash_hash = std::hash<u64>{}(hash.low);
     80   hash_combine(hash_hash, hash.high);
     81   return hash_hash;
     82 }
     83 
     84 TinyString TextureReplacements::VRAMReplacementHash::ToString() const
     85 {
     86   return TinyString::from_format("{:08X}{:08X}", high, low);
     87 }
     88 
     89 bool TextureReplacements::VRAMReplacementHash::ParseString(std::string_view sv)
     90 {
     91   if (sv.length() != 32)
     92     return false;
     93 
     94   std::optional<u64> high_value = StringUtil::FromChars<u64>(sv.substr(0, 16), 16);
     95   std::optional<u64> low_value = StringUtil::FromChars<u64>(sv.substr(16), 16);
     96   if (!high_value.has_value() || !low_value.has_value())
     97     return false;
     98 
     99   low = low_value.value();
    100   high = high_value.value();
    101   return true;
    102 }
    103 
    104 void TextureReplacements::SetGameID(std::string game_id)
    105 {
    106   if (s_game_id == game_id)
    107     return;
    108 
    109   s_game_id = game_id;
    110   Reload();
    111 }
    112 
    113 const TextureReplacements::ReplacementImage* TextureReplacements::GetVRAMReplacement(u32 width, u32 height,
    114                                                                                      const void* pixels)
    115 {
    116   const VRAMReplacementHash hash = GetVRAMWriteHash(width, height, pixels);
    117 
    118   const auto it = s_vram_write_replacements.find(hash);
    119   if (it == s_vram_write_replacements.end())
    120     return nullptr;
    121 
    122   return LoadTexture(it->second);
    123 }
    124 
    125 void TextureReplacements::DumpVRAMWrite(u32 width, u32 height, const void* pixels)
    126 {
    127   const std::string filename = GetVRAMWriteDumpFilename(width, height, pixels);
    128   if (filename.empty())
    129     return;
    130 
    131   RGBA8Image image;
    132   image.SetSize(width, height);
    133 
    134   const u16* src_pixels = reinterpret_cast<const u16*>(pixels);
    135 
    136   for (u32 y = 0; y < height; y++)
    137   {
    138     for (u32 x = 0; x < width; x++)
    139     {
    140       image.SetPixel(x, y, VRAMRGBA5551ToRGBA8888(*src_pixels));
    141       src_pixels++;
    142     }
    143   }
    144 
    145   if (g_settings.texture_replacements.dump_vram_write_force_alpha_channel)
    146   {
    147     for (u32 y = 0; y < height; y++)
    148     {
    149       for (u32 x = 0; x < width; x++)
    150         image.SetPixel(x, y, image.GetPixel(x, y) | 0xFF000000u);
    151     }
    152   }
    153 
    154   INFO_LOG("Dumping {}x{} VRAM write to '{}'", width, height, Path::GetFileName(filename));
    155   if (!image.SaveToFile(filename.c_str())) [[unlikely]]
    156     ERROR_LOG("Failed to dump {}x{} VRAM write to '{}'", width, height, filename);
    157 }
    158 
    159 void TextureReplacements::Shutdown()
    160 {
    161   s_texture_cache.clear();
    162   s_vram_write_replacements.clear();
    163   s_game_id.clear();
    164 }
    165 
    166 // TODO: Organize into PCSX2-style.
    167 std::string TextureReplacements::GetSourceDirectory()
    168 {
    169   return Path::Combine(EmuFolders::Textures, s_game_id);
    170 }
    171 
    172 std::string TextureReplacements::GetDumpDirectory()
    173 {
    174   return Path::Combine(EmuFolders::Dumps, Path::Combine("textures", s_game_id));
    175 }
    176 
    177 TextureReplacements::VRAMReplacementHash TextureReplacements::GetVRAMWriteHash(u32 width, u32 height,
    178                                                                                const void* pixels)
    179 {
    180   XXH128_hash_t hash = XXH3_128bits(pixels, width * height * sizeof(u16));
    181   return {hash.low64, hash.high64};
    182 }
    183 
    184 std::string TextureReplacements::GetVRAMWriteDumpFilename(u32 width, u32 height, const void* pixels)
    185 {
    186   if (s_game_id.empty())
    187     return {};
    188 
    189   const VRAMReplacementHash hash = GetVRAMWriteHash(width, height, pixels);
    190   const std::string dump_directory(GetDumpDirectory());
    191   std::string filename(Path::Combine(dump_directory, fmt::format("vram-write-{}.png", hash.ToString())));
    192 
    193   if (FileSystem::FileExists(filename.c_str()))
    194     return {};
    195 
    196   if (!FileSystem::EnsureDirectoryExists(dump_directory.c_str(), false))
    197     return {};
    198 
    199   return filename;
    200 }
    201 
    202 void TextureReplacements::Reload()
    203 {
    204   s_vram_write_replacements.clear();
    205 
    206   if (g_settings.texture_replacements.AnyReplacementsEnabled())
    207     FindTextures(GetSourceDirectory());
    208 
    209   if (g_settings.texture_replacements.preload_textures)
    210     PreloadTextures();
    211 
    212   PurgeUnreferencedTexturesFromCache();
    213 }
    214 
    215 void TextureReplacements::PurgeUnreferencedTexturesFromCache()
    216 {
    217   TextureCache old_map = std::move(s_texture_cache);
    218   s_texture_cache = {};
    219 
    220   for (const auto& it : s_vram_write_replacements)
    221   {
    222     auto it2 = old_map.find(it.second);
    223     if (it2 != old_map.end())
    224     {
    225       s_texture_cache[it.second] = std::move(it2->second);
    226       old_map.erase(it2);
    227     }
    228   }
    229 }
    230 
    231 bool TextureReplacements::ParseReplacementFilename(const std::string& filename, VRAMReplacementHash* replacement_hash,
    232                                                    ReplacmentType* replacement_type)
    233 {
    234   const std::string_view file_title = Path::GetFileTitle(filename);
    235   if (!file_title.starts_with("vram-write-"))
    236     return false;
    237 
    238   const std::string_view hashpart = file_title.substr(11);
    239   if (!replacement_hash->ParseString(hashpart))
    240     return false;
    241 
    242   const std::string_view file_extension = Path::GetExtension(filename);
    243   bool valid_extension = false;
    244   for (const char* test_extension : {"png", "jpg", "webp"})
    245   {
    246     if (StringUtil::EqualNoCase(file_extension, test_extension))
    247     {
    248       valid_extension = true;
    249       break;
    250     }
    251   }
    252 
    253   *replacement_type = ReplacmentType::VRAMWrite;
    254   return valid_extension;
    255 }
    256 
    257 void TextureReplacements::FindTextures(const std::string& dir)
    258 {
    259   FileSystem::FindResultsArray files;
    260   FileSystem::FindFiles(dir.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE, &files);
    261 
    262   for (FILESYSTEM_FIND_DATA& fd : files)
    263   {
    264     if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
    265       continue;
    266 
    267     VRAMReplacementHash hash;
    268     ReplacmentType type;
    269     if (!ParseReplacementFilename(fd.FileName, &hash, &type))
    270       continue;
    271 
    272     switch (type)
    273     {
    274       case ReplacmentType::VRAMWrite:
    275       {
    276         auto it = s_vram_write_replacements.find(hash);
    277         if (it != s_vram_write_replacements.end())
    278         {
    279           WARNING_LOG("Duplicate VRAM write replacement: '{}' and '{}'", it->second, fd.FileName);
    280           continue;
    281         }
    282 
    283         s_vram_write_replacements.emplace(hash, std::move(fd.FileName));
    284       }
    285       break;
    286     }
    287   }
    288 
    289   INFO_LOG("Found {} replacement VRAM writes for '{}'", s_vram_write_replacements.size(), s_game_id);
    290 }
    291 
    292 const TextureReplacements::ReplacementImage* TextureReplacements::LoadTexture(const std::string& filename)
    293 {
    294   auto it = s_texture_cache.find(filename);
    295   if (it != s_texture_cache.end())
    296     return &it->second;
    297 
    298   RGBA8Image image;
    299   if (!image.LoadFromFile(filename.c_str()))
    300   {
    301     ERROR_LOG("Failed to load '{}'", Path::GetFileName(filename));
    302     return nullptr;
    303   }
    304 
    305   INFO_LOG("Loaded '{}': {}x{}", Path::GetFileName(filename), image.GetWidth(), image.GetHeight());
    306   it = s_texture_cache.emplace(filename, std::move(image)).first;
    307   return &it->second;
    308 }
    309 
    310 void TextureReplacements::PreloadTextures()
    311 {
    312   static constexpr float UPDATE_INTERVAL = 1.0f;
    313 
    314   Common::Timer last_update_time;
    315   u32 num_textures_loaded = 0;
    316   const u32 total_textures = static_cast<u32>(s_vram_write_replacements.size());
    317 
    318 #define UPDATE_PROGRESS()                                                                                              \
    319   if (last_update_time.GetTimeSeconds() >= UPDATE_INTERVAL)                                                            \
    320   {                                                                                                                    \
    321     Host::DisplayLoadingScreen("Preloading replacement textures...", 0, static_cast<int>(total_textures),              \
    322                                static_cast<int>(num_textures_loaded));                                                 \
    323     last_update_time.Reset();                                                                                          \
    324   }
    325 
    326   for (const auto& it : s_vram_write_replacements)
    327   {
    328     UPDATE_PROGRESS();
    329 
    330     LoadTexture(it.second);
    331     num_textures_loaded++;
    332   }
    333 
    334 #undef UPDATE_PROGRESS
    335 }