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 }