// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "texture_replacements.h" #include "host.h" #include "settings.h" #include "common/bitutils.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "common/timer.h" #include "fmt/format.h" #include "xxhash.h" #if defined(CPU_ARCH_X86) || defined(CPU_ARCH_X64) #include "xxh_x86dispatch.h" #endif #include Log_SetChannel(TextureReplacements); TextureReplacements g_texture_replacements; static constexpr u32 VRAMRGBA5551ToRGBA8888(u16 color) { u8 r = Truncate8(color & 31); u8 g = Truncate8((color >> 5) & 31); u8 b = Truncate8((color >> 10) & 31); u8 a = Truncate8((color >> 15) & 1); // 00012345 -> 1234545 b = (b << 3) | (b & 0b111); g = (g << 3) | (g & 0b111); r = (r << 3) | (r & 0b111); a = a ? 255 : 0; return ZeroExtend32(r) | (ZeroExtend32(g) << 8) | (ZeroExtend32(b) << 16) | (ZeroExtend32(a) << 24); } std::string TextureReplacementHash::ToString() const { return StringUtil::StdStringFromFormat("%" PRIx64 "%" PRIx64, high, low); } bool TextureReplacementHash::ParseString(const std::string_view& sv) { if (sv.length() != 32) return false; std::optional high_value = StringUtil::FromChars(sv.substr(0, 16), 16); std::optional low_value = StringUtil::FromChars(sv.substr(16), 16); if (!high_value.has_value() || !low_value.has_value()) return false; low = low_value.value(); high = high_value.value(); return true; } TextureReplacements::TextureReplacements() = default; TextureReplacements::~TextureReplacements() = default; void TextureReplacements::SetGameID(std::string game_id) { if (m_game_id == game_id) return; m_game_id = game_id; Reload(); } const TextureReplacementTexture* TextureReplacements::GetVRAMWriteReplacement(u32 width, u32 height, const void* pixels) { const TextureReplacementHash hash = GetVRAMWriteHash(width, height, pixels); const auto it = m_vram_write_replacements.find(hash); if (it == m_vram_write_replacements.end()) return nullptr; return LoadTexture(it->second); } void TextureReplacements::DumpVRAMWrite(u32 width, u32 height, const void* pixels) { std::string filename = GetVRAMWriteDumpFilename(width, height, pixels); if (filename.empty()) return; Common::RGBA8Image image; image.SetSize(width, height); const u16* src_pixels = reinterpret_cast(pixels); for (u32 y = 0; y < height; y++) { for (u32 x = 0; x < width; x++) { image.SetPixel(x, y, VRAMRGBA5551ToRGBA8888(*src_pixels)); src_pixels++; } } if (g_settings.texture_replacements.dump_vram_write_force_alpha_channel) { for (u32 y = 0; y < height; y++) { for (u32 x = 0; x < width; x++) image.SetPixel(x, y, image.GetPixel(x, y) | 0xFF000000u); } } Log_InfoPrintf("Dumping %ux%u VRAM write to '%s'", width, height, filename.c_str()); if (!image.SaveToFile(filename.c_str())) Log_ErrorPrintf("Failed to dump %ux%u VRAM write to '%s'", width, height, filename.c_str()); } void TextureReplacements::Shutdown() { m_texture_cache.clear(); m_vram_write_replacements.clear(); m_game_id.clear(); } std::string TextureReplacements::GetSourceDirectory() const { return Path::Combine(EmuFolders::Textures, m_game_id); } std::string TextureReplacements::GetDumpDirectory() const { return Path::Combine(EmuFolders::Dumps, Path::Combine("textures", m_game_id)); } TextureReplacementHash TextureReplacements::GetVRAMWriteHash(u32 width, u32 height, const void* pixels) const { XXH128_hash_t hash = XXH3_128bits(pixels, width * height * sizeof(u16)); return {hash.low64, hash.high64}; } std::string TextureReplacements::GetVRAMWriteDumpFilename(u32 width, u32 height, const void* pixels) const { if (m_game_id.empty()) return {}; const TextureReplacementHash hash = GetVRAMWriteHash(width, height, pixels); const std::string dump_directory(GetDumpDirectory()); std::string filename(Path::Combine(dump_directory, fmt::format("vram-write-{}.png", hash.ToString()))); if (FileSystem::FileExists(filename.c_str())) return {}; if (!FileSystem::EnsureDirectoryExists(dump_directory.c_str(), false)) return {}; return filename; } void TextureReplacements::Reload() { m_vram_write_replacements.clear(); if (g_settings.texture_replacements.AnyReplacementsEnabled()) FindTextures(GetSourceDirectory()); if (g_settings.texture_replacements.preload_textures) PreloadTextures(); PurgeUnreferencedTexturesFromCache(); } void TextureReplacements::PurgeUnreferencedTexturesFromCache() { TextureCache old_map = std::move(m_texture_cache); for (const auto& it : m_vram_write_replacements) { auto it2 = old_map.find(it.second); if (it2 != old_map.end()) { m_texture_cache[it.second] = std::move(it2->second); old_map.erase(it2); } } } bool TextureReplacements::ParseReplacementFilename(const std::string& filename, TextureReplacementHash* replacement_hash, ReplacmentType* replacement_type) { const char* extension = std::strrchr(filename.c_str(), '.'); const char* title = std::strrchr(filename.c_str(), '/'); #ifdef _WIN32 const char* title2 = std::strrchr(filename.c_str(), '\\'); if (title2 && (!title || title2 > title)) title = title2; #endif if (!title || !extension) return false; title++; const char* hashpart; if (StringUtil::Strncasecmp(title, "vram-write-", 11) == 0) { hashpart = title + 11; *replacement_type = ReplacmentType::VRAMWrite; } else { return false; } if (!replacement_hash->ParseString(std::string_view(hashpart, static_cast(extension - hashpart)))) return false; extension++; bool valid_extension = false; for (const char* test_extension : {"png", "jpg", "tga", "bmp"}) { if (StringUtil::Strcasecmp(extension, test_extension) == 0) { valid_extension = true; break; } } return valid_extension; } void TextureReplacements::FindTextures(const std::string& dir) { FileSystem::FindResultsArray files; FileSystem::FindFiles(dir.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE, &files); for (FILESYSTEM_FIND_DATA& fd : files) { if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) continue; TextureReplacementHash hash; ReplacmentType type; if (!ParseReplacementFilename(fd.FileName, &hash, &type)) continue; switch (type) { case ReplacmentType::VRAMWrite: { auto it = m_vram_write_replacements.find(hash); if (it != m_vram_write_replacements.end()) { Log_WarningPrintf("Duplicate VRAM write replacement: '%s' and '%s'", it->second.c_str(), fd.FileName.c_str()); continue; } m_vram_write_replacements.emplace(hash, std::move(fd.FileName)); } break; } } Log_InfoPrintf("Found %zu replacement VRAM writes for '%s'", m_vram_write_replacements.size(), m_game_id.c_str()); } const TextureReplacementTexture* TextureReplacements::LoadTexture(const std::string& filename) { auto it = m_texture_cache.find(filename); if (it != m_texture_cache.end()) return &it->second; Common::RGBA8Image image; if (!image.LoadFromFile(filename.c_str())) { Log_ErrorPrintf("Failed to load '%s'", filename.c_str()); return nullptr; } Log_InfoPrintf("Loaded '%s': %ux%u", filename.c_str(), image.GetWidth(), image.GetHeight()); it = m_texture_cache.emplace(filename, std::move(image)).first; return &it->second; } void TextureReplacements::PreloadTextures() { static constexpr float UPDATE_INTERVAL = 1.0f; Common::Timer last_update_time; u32 num_textures_loaded = 0; const u32 total_textures = static_cast(m_vram_write_replacements.size()); #define UPDATE_PROGRESS() \ if (last_update_time.GetTimeSeconds() >= UPDATE_INTERVAL) \ { \ Host::DisplayLoadingScreen("Preloading replacement textures...", 0, static_cast(total_textures), \ static_cast(num_textures_loaded)); \ last_update_time.Reset(); \ } for (const auto& it : m_vram_write_replacements) { UPDATE_PROGRESS(); LoadTexture(it.second); num_textures_loaded++; } #undef UPDATE_PROGRESS }