Duckstation/src/core/texture_replacements.cpp

315 lines
8.7 KiB
C++
Raw Normal View History

2023-10-01 04:12:25 +00:00
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "texture_replacements.h"
2023-10-01 04:12:25 +00:00
#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"
2023-10-01 04:12:25 +00:00
#include "fmt/format.h"
#include "xxhash.h"
2023-10-01 03:57:25 +00:00
#if defined(CPU_ARCH_X86) || defined(CPU_ARCH_X64)
#include "xxh_x86dispatch.h"
#endif
2023-10-01 04:12:25 +00:00
#include <cinttypes>
2023-10-01 04:12:25 +00:00
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<u64> high_value = StringUtil::FromChars<u64>(sv.substr(0, 16), 16);
std::optional<u64> low_value = StringUtil::FromChars<u64>(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;
2024-03-06 06:08:10 +00:00
RGBA8Image image;
image.SetSize(width, height);
const u16* src_pixels = reinterpret_cast<const u16*>(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());
2022-07-11 09:45:31 +00:00
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(), '/');
2021-06-28 10:16:48 +00:00
#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<size_t>(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;
2024-03-06 06:08:10 +00:00
RGBA8Image image;
2022-07-11 09:45:31 +00:00
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<u32>(m_vram_write_replacements.size());
#define UPDATE_PROGRESS() \
if (last_update_time.GetTimeSeconds() >= UPDATE_INTERVAL) \
{ \
Host::DisplayLoadingScreen("Preloading replacement textures...", 0, static_cast<int>(total_textures), \
static_cast<int>(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
}