mirror of https://github.com/stenzek/duckstation
GPU: Support replacing VRAM writes with new textures
parent
a66f14b5c3
commit
8db961042a
@ -0,0 +1,301 @@
|
||||
#include "texture_replacements.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/timer.h"
|
||||
#include "host_interface.h"
|
||||
#include "settings.h"
|
||||
#include "xxhash.h"
|
||||
#include <cinttypes>
|
||||
Log_SetChannel(TextureReplacements);
|
||||
|
||||
TextureReplacements g_texture_replacements;
|
||||
|
||||
static constexpr u32 RGBA5551ToRGBA8888(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;
|
||||
|
||||
Common::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, RGBA5551ToRGBA8888(*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 (!Common::WriteImageToFile(image, 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 g_host_interface->GetUserDirectoryRelativePath("textures/%s", m_game_id.c_str());
|
||||
}
|
||||
|
||||
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);
|
||||
std::string filename = g_host_interface->GetUserDirectoryRelativePath("dump/textures/%s/vram-write-%s.png",
|
||||
m_game_id.c_str(), hash.ToString().c_str());
|
||||
|
||||
if (FileSystem::FileExists(filename.c_str()))
|
||||
return {};
|
||||
|
||||
const std::string dump_directory =
|
||||
g_host_interface->GetUserDirectoryRelativePath("dump/textures/%s", m_game_id.c_str());
|
||||
if (!FileSystem::DirectoryExists(dump_directory.c_str()) &&
|
||||
!FileSystem::CreateDirectory(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<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;
|
||||
|
||||
Common::RGBA8Image image;
|
||||
if (!Common::LoadImageFromFile(&image, 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) \
|
||||
{ \
|
||||
g_host_interface->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
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
#include "common/hash_combine.h"
|
||||
#include "common/image.h"
|
||||
#include "types.h"
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct TextureReplacementHash
|
||||
{
|
||||
u64 low;
|
||||
u64 high;
|
||||
|
||||
std::string ToString() const;
|
||||
bool ParseString(const std::string_view& sv);
|
||||
|
||||
bool operator<(const TextureReplacementHash& rhs) const { return std::tie(low, high) < std::tie(rhs.low, rhs.high); }
|
||||
bool operator==(const TextureReplacementHash& rhs) const { return low == rhs.low && high == rhs.high; }
|
||||
bool operator!=(const TextureReplacementHash& rhs) const { return low != rhs.low || high != rhs.high; }
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template<>
|
||||
struct hash<TextureReplacementHash>
|
||||
{
|
||||
size_t operator()(const TextureReplacementHash& h) const
|
||||
{
|
||||
size_t hash_hash = std::hash<u64>{}(h.low);
|
||||
hash_combine(hash_hash, h.high);
|
||||
return hash_hash;
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
using TextureReplacementTexture = Common::RGBA8Image;
|
||||
|
||||
class TextureReplacements
|
||||
{
|
||||
public:
|
||||
enum class ReplacmentType
|
||||
{
|
||||
VRAMWrite
|
||||
};
|
||||
|
||||
TextureReplacements();
|
||||
~TextureReplacements();
|
||||
|
||||
const std::string GetGameID() const { return m_game_id; }
|
||||
void SetGameID(std::string game_id);
|
||||
|
||||
void Reload();
|
||||
|
||||
const TextureReplacementTexture* GetVRAMWriteReplacement(u32 width, u32 height, const void* pixels);
|
||||
void DumpVRAMWrite(u32 width, u32 height, const void* pixels);
|
||||
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
struct ReplacementHashMapHash
|
||||
{
|
||||
size_t operator()(const TextureReplacementHash& hash);
|
||||
};
|
||||
|
||||
using VRAMWriteReplacementMap = std::unordered_map<TextureReplacementHash, std::string>;
|
||||
using TextureCache = std::unordered_map<std::string, TextureReplacementTexture>;
|
||||
|
||||
static bool ParseReplacementFilename(const std::string& filename, TextureReplacementHash* replacement_hash,
|
||||
ReplacmentType* replacement_type);
|
||||
|
||||
std::string GetSourceDirectory() const;
|
||||
|
||||
TextureReplacementHash GetVRAMWriteHash(u32 width, u32 height, const void* pixels) const;
|
||||
std::string GetVRAMWriteDumpFilename(u32 width, u32 height, const void* pixels) const;
|
||||
|
||||
void FindTextures(const std::string& dir);
|
||||
|
||||
const TextureReplacementTexture* LoadTexture(const std::string& filename);
|
||||
void PreloadTextures();
|
||||
void PurgeUnreferencedTexturesFromCache();
|
||||
|
||||
std::string m_game_id;
|
||||
|
||||
TextureCache m_texture_cache;
|
||||
|
||||
VRAMWriteReplacementMap m_vram_write_replacements;
|
||||
};
|
||||
|
||||
extern TextureReplacements g_texture_replacements;
|
Loading…
Reference in New Issue