diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 285012e73..1180558dd 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -92,6 +92,8 @@ add_library(core
spu.h
system.cpp
system.h
+ texture_replacements.cpp
+ texture_replacements.h
timers.cpp
timers.h
timing_event.cpp
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 0ca308ce4..374f81ffb 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -149,6 +149,7 @@
+
@@ -226,6 +227,7 @@
+
@@ -246,6 +248,9 @@
{9c8ddeb0-2b8f-4f5f-ba86-127cdf27f035}
+
+ {09553c96-9f39-49bf-8ae6-7acbd07c410c}
+
{7ff9fdb9-d504-47db-a16a-b08071999620}
@@ -464,7 +469,7 @@
WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -490,7 +495,7 @@
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -516,7 +521,7 @@
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -542,7 +547,7 @@
WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
Default
true
false
@@ -571,7 +576,7 @@
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
Default
true
false
@@ -600,7 +605,7 @@
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
Default
true
false
@@ -628,7 +633,7 @@
MaxSpeed
true
WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -655,7 +660,7 @@
MaxSpeed
true
WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
true
stdcpp17
@@ -683,7 +688,7 @@
MaxSpeed
true
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -710,7 +715,7 @@
MaxSpeed
true
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -737,7 +742,7 @@
MaxSpeed
true
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
true
stdcpp17
@@ -765,7 +770,7 @@
MaxSpeed
true
WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\vixl\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
true
stdcpp17
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index adf4f86e5..b50bf245a 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -55,6 +55,7 @@
+
@@ -113,5 +114,6 @@
+
\ No newline at end of file
diff --git a/src/core/gpu_commands.cpp b/src/core/gpu_commands.cpp
index e027d0775..1cfc402bd 100644
--- a/src/core/gpu_commands.cpp
+++ b/src/core/gpu_commands.cpp
@@ -4,6 +4,7 @@
#include "gpu.h"
#include "interrupt_controller.h"
#include "system.h"
+#include "texture_replacements.h"
Log_SetChannel(GPU);
#define CHECK_COMMAND_SIZE(num_words) \
@@ -497,13 +498,6 @@ bool GPU::HandleCopyRectangleCPUToVRAMCommand()
void GPU::FinishVRAMWrite()
{
- if (g_settings.debugging.dump_cpu_to_vram_copies && m_blit_remaining_words == 0)
- {
- DumpVRAMToFile(StringUtil::StdStringFromFormat("cpu_to_vram_copy_%u.png", s_cpu_to_vram_dump_id++).c_str(),
- m_vram_transfer.width, m_vram_transfer.height, sizeof(u16) * m_vram_transfer.width,
- m_blit_buffer.data(), true);
- }
-
if (IsInterlacedRenderingEnabled() && IsCRTCScanlinePending())
SynchronizeCRTC();
@@ -511,6 +505,19 @@ void GPU::FinishVRAMWrite()
if (m_blit_remaining_words == 0)
{
+ if (g_settings.debugging.dump_cpu_to_vram_copies)
+ {
+ DumpVRAMToFile(StringUtil::StdStringFromFormat("cpu_to_vram_copy_%u.png", s_cpu_to_vram_dump_id++).c_str(),
+ m_vram_transfer.width, m_vram_transfer.height, sizeof(u16) * m_vram_transfer.width,
+ m_blit_buffer.data(), true);
+ }
+
+ if (g_settings.texture_replacements.ShouldDumpVRAMWrite(m_vram_transfer.width, m_vram_transfer.height))
+ {
+ g_texture_replacements.DumpVRAMWrite(m_vram_transfer.width, m_vram_transfer.height,
+ reinterpret_cast(m_blit_buffer.data()));
+ }
+
UpdateVRAM(m_vram_transfer.x, m_vram_transfer.y, m_vram_transfer.width, m_vram_transfer.height,
m_blit_buffer.data(), m_GPUSTAT.set_mask_while_drawing, m_GPUSTAT.check_mask_before_draw);
}
diff --git a/src/core/gpu_hw_d3d11.cpp b/src/core/gpu_hw_d3d11.cpp
index 90ccc5eb4..9317c0cee 100644
--- a/src/core/gpu_hw_d3d11.cpp
+++ b/src/core/gpu_hw_d3d11.cpp
@@ -616,6 +616,52 @@ void GPU_HW_D3D11::DrawUtilityShader(ID3D11PixelShader* shader, const void* unif
m_context->Draw(3, 0);
}
+bool GPU_HW_D3D11::BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width,
+ u32 height)
+{
+ if (m_vram_replacement_texture.GetWidth() < tex->GetWidth() ||
+ m_vram_replacement_texture.GetHeight() < tex->GetHeight())
+ {
+ if (!m_vram_replacement_texture.Create(m_device.Get(), tex->GetWidth(), tex->GetHeight(), 1,
+ DXGI_FORMAT_R8G8B8A8_UNORM, D3D11_BIND_SHADER_RESOURCE, tex->GetPixels(),
+ tex->GetByteStride(), true))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ D3D11_MAPPED_SUBRESOURCE sr;
+ HRESULT hr = m_context->Map(m_vram_replacement_texture, 0, D3D11_MAP_WRITE_DISCARD, 0, &sr);
+ if (FAILED(hr))
+ {
+ Log_ErrorPrintf("Texture map failed: %08X", hr);
+ return false;
+ }
+
+ const u32 copy_size = std::min(tex->GetByteStride(), sr.RowPitch);
+ const u8* src_ptr = reinterpret_cast(tex->GetPixels());
+ u8* dst_ptr = static_cast(sr.pData);
+ for (u32 i = 0; i < tex->GetHeight(); i++)
+ {
+ std::memcpy(dst_ptr, src_ptr, copy_size);
+ src_ptr += tex->GetByteStride();
+ dst_ptr += sr.RowPitch;
+ }
+
+ m_context->Unmap(m_vram_replacement_texture, 0);
+ }
+
+ m_context->OMSetDepthStencilState(m_depth_disabled_state.Get(), 0);
+ m_context->PSSetShaderResources(0, 1, m_vram_replacement_texture.GetD3DSRVArray());
+ SetViewportAndScissor(dst_x, dst_y, width, height);
+
+ const float uniforms[] = {0.0f, 0.0f, 1.0f, 1.0f};
+ DrawUtilityShader(m_copy_pixel_shader.Get(), uniforms, sizeof(uniforms));
+ RestoreGraphicsAPIState();
+ return true;
+}
+
void GPU_HW_D3D11::DrawBatchVertices(BatchRenderMode render_mode, u32 base_vertex, u32 num_vertices)
{
const bool textured = (m_batch.texture_mode != GPUTextureMode::Disabled);
@@ -803,6 +849,16 @@ void GPU_HW_D3D11::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* d
const Common::Rectangle bounds = GetVRAMTransferBounds(x, y, width, height);
GPU_HW::UpdateVRAM(bounds.left, bounds.top, bounds.GetWidth(), bounds.GetHeight(), data, set_mask, check_mask);
+ if (!check_mask)
+ {
+ const TextureReplacementTexture* rtex = g_texture_replacements.GetVRAMWriteReplacement(width, height, data);
+ if (rtex && BlitVRAMReplacementTexture(rtex, x * m_resolution_scale, y * m_resolution_scale,
+ width * m_resolution_scale, height * m_resolution_scale))
+ {
+ return;
+ }
+ }
+
const u32 num_pixels = width * height;
const auto map_result = m_texture_stream_buffer.Map(m_context.Get(), sizeof(u16), num_pixels * sizeof(u16));
std::memcpy(map_result.pointer, data, num_pixels * sizeof(u16));
diff --git a/src/core/gpu_hw_d3d11.h b/src/core/gpu_hw_d3d11.h
index 4c9dd3401..bab3afd9a 100644
--- a/src/core/gpu_hw_d3d11.h
+++ b/src/core/gpu_hw_d3d11.h
@@ -4,6 +4,7 @@
#include "common/d3d11/stream_buffer.h"
#include "common/d3d11/texture.h"
#include "gpu_hw.h"
+#include "texture_replacements.h"
#include
#include
#include
@@ -68,6 +69,8 @@ private:
void DrawUtilityShader(ID3D11PixelShader* shader, const void* uniforms, u32 uniforms_size);
+ bool BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width, u32 height);
+
ComPtr m_device;
ComPtr m_context;
@@ -118,4 +121,6 @@ private:
ComPtr m_vram_copy_pixel_shader;
ComPtr m_vram_update_depth_pixel_shader;
std::array, 3>, 2> m_display_pixel_shaders; // [depth_24][interlaced]
+
+ D3D11::Texture m_vram_replacement_texture;
};
diff --git a/src/core/gpu_hw_opengl.cpp b/src/core/gpu_hw_opengl.cpp
index fb092a936..a253a9474 100644
--- a/src/core/gpu_hw_opengl.cpp
+++ b/src/core/gpu_hw_opengl.cpp
@@ -5,6 +5,7 @@
#include "gpu_hw_shadergen.h"
#include "host_display.h"
#include "system.h"
+#include "texture_replacements.h"
Log_SetChannel(GPU_HW_OpenGL);
GPU_HW_OpenGL::GPU_HW_OpenGL() : GPU_HW() {}
@@ -618,6 +619,37 @@ void GPU_HW_OpenGL::SetBlendMode()
}
}
+bool GPU_HW_OpenGL::BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width,
+ u32 height)
+{
+ if (!m_vram_write_replacement_texture.IsValid())
+ {
+ if (!m_vram_write_replacement_texture.Create(tex->GetWidth(), tex->GetHeight(), 1, GL_RGBA, GL_RGBA,
+ GL_UNSIGNED_BYTE, tex->GetPixels()) ||
+ !m_vram_write_replacement_texture.CreateFramebuffer())
+ {
+ m_vram_write_replacement_texture.Destroy();
+ return false;
+ }
+ }
+ else
+ {
+ m_vram_write_replacement_texture.Replace(tex->GetWidth(), tex->GetHeight(), GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE,
+ tex->GetPixels());
+ }
+
+ glDisable(GL_SCISSOR_TEST);
+ m_vram_write_replacement_texture.BindFramebuffer(GL_READ_FRAMEBUFFER);
+
+ dst_y = m_vram_texture.GetHeight() - dst_y - height;
+ glBlitFramebuffer(0, tex->GetHeight(), tex->GetWidth(), 0, dst_x, dst_y, dst_x + width, dst_y + height,
+ GL_COLOR_BUFFER_BIT, GL_LINEAR);
+
+ m_vram_read_texture.Bind();
+ glEnable(GL_SCISSOR_TEST);
+ return true;
+}
+
void GPU_HW_OpenGL::SetDepthFunc()
{
SetDepthFunc(m_batch.use_depth_buffer ? GL_LEQUAL : (m_batch.check_mask_before_draw ? GL_GEQUAL : GL_ALWAYS));
@@ -849,12 +881,22 @@ void GPU_HW_OpenGL::FillVRAM(u32 x, u32 y, u32 width, u32 height, u32 color)
void GPU_HW_OpenGL::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* data, bool set_mask, bool check_mask)
{
+ const Common::Rectangle bounds = GetVRAMTransferBounds(x, y, width, height);
+ GPU_HW::UpdateVRAM(bounds.left, bounds.top, bounds.GetWidth(), bounds.GetHeight(), data, set_mask, check_mask);
+
+ if (!check_mask)
+ {
+ const TextureReplacementTexture* rtex = g_texture_replacements.GetVRAMWriteReplacement(width, height, data);
+ if (rtex && BlitVRAMReplacementTexture(rtex, x * m_resolution_scale, y * m_resolution_scale,
+ width * m_resolution_scale, height * m_resolution_scale))
+ {
+ return;
+ }
+ }
+
const u32 num_pixels = width * height;
if (num_pixels < m_max_texture_buffer_size || m_use_ssbo_for_vram_writes)
{
- const Common::Rectangle bounds = GetVRAMTransferBounds(x, y, width, height);
- GPU_HW::UpdateVRAM(bounds.left, bounds.top, bounds.GetWidth(), bounds.GetHeight(), data, set_mask, check_mask);
-
const auto map_result = m_texture_stream_buffer->Map(sizeof(u16), num_pixels * sizeof(u16));
std::memcpy(map_result.pointer, data, num_pixels * sizeof(u16));
m_texture_stream_buffer->Unmap(num_pixels * sizeof(u16));
diff --git a/src/core/gpu_hw_opengl.h b/src/core/gpu_hw_opengl.h
index 74ecd1638..9c3255cbf 100644
--- a/src/core/gpu_hw_opengl.h
+++ b/src/core/gpu_hw_opengl.h
@@ -5,6 +5,7 @@
#include "common/gl/texture.h"
#include "glad.h"
#include "gpu_hw.h"
+#include "texture_replacements.h"
#include
#include
#include
@@ -67,12 +68,15 @@ private:
void SetDepthFunc(GLenum func);
void SetBlendMode();
+ bool BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width, u32 height);
+
// downsample texture - used for readbacks at >1xIR.
GL::Texture m_vram_texture;
GL::Texture m_vram_depth_texture;
GL::Texture m_vram_read_texture;
GL::Texture m_vram_encoding_texture;
GL::Texture m_display_texture;
+ GL::Texture m_vram_write_replacement_texture;
std::unique_ptr m_vertex_stream_buffer;
GLuint m_vram_fbo_id = 0;
diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp
index be760d14e..32e5e475e 100644
--- a/src/core/gpu_hw_vulkan.cpp
+++ b/src/core/gpu_hw_vulkan.cpp
@@ -1148,6 +1148,16 @@ void GPU_HW_Vulkan::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void*
const Common::Rectangle bounds = GetVRAMTransferBounds(x, y, width, height);
GPU_HW::UpdateVRAM(bounds.left, bounds.top, bounds.GetWidth(), bounds.GetHeight(), data, set_mask, check_mask);
+ if (!check_mask)
+ {
+ const TextureReplacementTexture* rtex = g_texture_replacements.GetVRAMWriteReplacement(width, height, data);
+ if (rtex && BlitVRAMReplacementTexture(rtex, x * m_resolution_scale, y * m_resolution_scale,
+ width * m_resolution_scale, height * m_resolution_scale))
+ {
+ return;
+ }
+ }
+
const u32 data_size = width * height * sizeof(u16);
const u32 alignment = std::max(sizeof(u16), static_cast(g_vulkan_context->GetTexelBufferAlignment()));
if (!m_texture_stream_buffer.ReserveMemory(data_size, alignment))
@@ -1326,6 +1336,80 @@ void GPU_HW_Vulkan::ClearDepthBuffer()
m_last_depth_z = 1.0f;
}
+bool GPU_HW_Vulkan::CreateTextureReplacementStreamBuffer()
+{
+ if (m_texture_replacment_stream_buffer.IsValid())
+ return true;
+
+ if (!m_texture_replacment_stream_buffer.Create(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, TEXTURE_REPLACEMENT_BUFFER_SIZE))
+ {
+ Log_ErrorPrint("Failed to allocate texture replacement streaming buffer");
+ return false;
+ }
+
+ return true;
+}
+
+bool GPU_HW_Vulkan::BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width,
+ u32 height)
+{
+ if (!CreateTextureReplacementStreamBuffer())
+ return false;
+
+ if (m_vram_write_replacement_texture.GetWidth() < tex->GetWidth() ||
+ m_vram_write_replacement_texture.GetHeight() < tex->GetHeight())
+ {
+ if (!m_vram_write_replacement_texture.Create(tex->GetWidth(), tex->GetHeight(), 1, 1, VK_FORMAT_R8G8B8A8_UNORM,
+ VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_TILING_OPTIMAL,
+ VK_IMAGE_USAGE_TRANSFER_DST_BIT))
+ {
+ Log_ErrorPrint("Failed to create VRAM write replacement texture");
+ return false;
+ }
+ }
+
+ const u32 required_size = tex->GetWidth() * tex->GetHeight() * sizeof(u32);
+ const u32 alignment = static_cast(g_vulkan_context->GetBufferImageGranularity());
+ if (!m_texture_replacment_stream_buffer.ReserveMemory(required_size, alignment))
+ {
+ Log_PerfPrint("Executing command buffer while waiting for texture replacement buffer space");
+ g_vulkan_context->ExecuteCommandBuffer(false);
+ if (!m_texture_replacment_stream_buffer.ReserveMemory(required_size, alignment))
+ {
+ Log_ErrorPrintf("Failed to allocate %u bytes from texture replacement streaming buffer", required_size);
+ return false;
+ }
+ }
+
+ // upload to buffer
+ const u32 buffer_offset = m_texture_replacment_stream_buffer.GetCurrentOffset();
+ std::memcpy(m_texture_replacment_stream_buffer.GetCurrentHostPointer(), tex->GetPixels(), required_size);
+ m_texture_replacment_stream_buffer.CommitMemory(required_size);
+
+ // buffer -> texture
+ VkCommandBuffer cmdbuf = g_vulkan_context->GetCurrentCommandBuffer();
+ m_vram_write_replacement_texture.UpdateFromBuffer(cmdbuf, 0, 0, 0, 0, tex->GetWidth(), tex->GetHeight(),
+ m_texture_replacment_stream_buffer.GetBuffer(), buffer_offset);
+
+ // texture -> vram
+ const VkImageBlit blit = {
+ {VK_IMAGE_ASPECT_COLOR_BIT, 0u, 0u, 1u},
+ {
+ {0, 0, 0},
+ {static_cast(tex->GetWidth()), static_cast(tex->GetHeight()), 1},
+ },
+ {VK_IMAGE_ASPECT_COLOR_BIT, 0u, 0u, 1u},
+ {{static_cast(dst_x), static_cast(dst_y), 0},
+ {static_cast(dst_x + width), static_cast(dst_y + height), 1}},
+ };
+ m_vram_write_replacement_texture.TransitionToLayout(cmdbuf, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
+ m_vram_texture.TransitionToLayout(cmdbuf, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
+ vkCmdBlitImage(cmdbuf, m_vram_write_replacement_texture.GetImage(), m_vram_write_replacement_texture.GetLayout(),
+ m_vram_texture.GetImage(), m_vram_texture.GetLayout(), 1, &blit, VK_FILTER_LINEAR);
+ m_vram_texture.TransitionToLayout(cmdbuf, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
+ return true;
+}
+
std::unique_ptr GPU::CreateHardwareVulkanRenderer()
{
return std::make_unique();
diff --git a/src/core/gpu_hw_vulkan.h b/src/core/gpu_hw_vulkan.h
index 131703105..50afcfbef 100644
--- a/src/core/gpu_hw_vulkan.h
+++ b/src/core/gpu_hw_vulkan.h
@@ -4,6 +4,7 @@
#include "common/vulkan/stream_buffer.h"
#include "common/vulkan/texture.h"
#include "gpu_hw.h"
+#include "texture_replacements.h"
#include
#include
#include
@@ -41,6 +42,7 @@ private:
enum : u32
{
MAX_PUSH_CONSTANTS_SIZE = 64,
+ TEXTURE_REPLACEMENT_BUFFER_SIZE = 64 * 1024 * 1024
};
void SetCapabilities();
void DestroyResources();
@@ -64,6 +66,10 @@ private:
bool CompilePipelines();
void DestroyPipelines();
+ bool CreateTextureReplacementStreamBuffer();
+
+ bool BlitVRAMReplacementTexture(const TextureReplacementTexture* tex, u32 dst_x, u32 dst_y, u32 width, u32 height);
+
VkRenderPass m_current_render_pass = VK_NULL_HANDLE;
VkRenderPass m_vram_render_pass = VK_NULL_HANDLE;
@@ -86,6 +92,7 @@ private:
Vulkan::Texture m_vram_readback_texture;
Vulkan::StagingTexture m_vram_readback_staging_texture;
Vulkan::Texture m_display_texture;
+ bool m_use_ssbos_for_vram_writes = false;
VkFramebuffer m_vram_framebuffer = VK_NULL_HANDLE;
VkFramebuffer m_vram_update_depth_framebuffer = VK_NULL_HANDLE;
@@ -123,5 +130,7 @@ private:
// [depth_24][interlace_mode]
DimensionalArray m_display_pipelines{};
- bool m_use_ssbos_for_vram_writes = false;
+ // texture replacements
+ Vulkan::Texture m_vram_write_replacement_texture;
+ Vulkan::StreamBuffer m_texture_replacment_stream_buffer;
};
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 21943a9cc..178160791 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -255,6 +255,17 @@ void Settings::Load(SettingsInterface& si)
debugging.show_timers_state = si.GetBoolValue("Debug", "ShowTimersState");
debugging.show_mdec_state = si.GetBoolValue("Debug", "ShowMDECState");
debugging.show_dma_state = si.GetBoolValue("Debug", "ShowDMAState");
+
+ texture_replacements.enable_vram_write_replacements =
+ si.GetBoolValue("TextureReplacements", "EnableVRAMWriteReplacements", false);
+ texture_replacements.preload_textures = si.GetBoolValue("TextureReplacements", "PreloadTextures", false);
+ texture_replacements.dump_vram_writes = si.GetBoolValue("TextureReplacements", "DumpVRAMWrites", false);
+ texture_replacements.dump_vram_write_force_alpha_channel =
+ si.GetBoolValue("TextureReplacements", "DumpVRAMWriteForceAlphaChannel", true);
+ texture_replacements.dump_vram_write_width_threshold =
+ si.GetIntValue("TextureReplacements", "DumpVRAMWriteWidthThreshold", 128);
+ texture_replacements.dump_vram_write_height_threshold =
+ si.GetIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold", 128);
}
void Settings::Save(SettingsInterface& si) const
@@ -381,6 +392,17 @@ void Settings::Save(SettingsInterface& si) const
si.SetBoolValue("Debug", "ShowTimersState", debugging.show_timers_state);
si.SetBoolValue("Debug", "ShowMDECState", debugging.show_mdec_state);
si.SetBoolValue("Debug", "ShowDMAState", debugging.show_dma_state);
+
+ si.SetBoolValue("TextureReplacements", "EnableVRAMWriteReplacements",
+ texture_replacements.enable_vram_write_replacements);
+ si.SetBoolValue("TextureReplacements", "PreloadTextures", texture_replacements.preload_textures);
+ si.SetBoolValue("TextureReplacements", "DumpVRAMWrites", texture_replacements.dump_vram_writes);
+ si.SetBoolValue("TextureReplacements", "DumpVRAMWriteForceAlphaChannel",
+ texture_replacements.dump_vram_write_force_alpha_channel);
+ si.SetIntValue("TextureReplacements", "DumpVRAMWriteWidthThreshold",
+ texture_replacements.dump_vram_write_width_threshold);
+ si.SetIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold",
+ texture_replacements.dump_vram_write_height_threshold);
}
static std::array s_log_level_names = {
@@ -635,8 +657,8 @@ static std::array s_display_aspect_ratio_names = {{"Auto (Game
"19:9", "21:9", "32:9", "8:7", "5:4", "3:2",
"2:1 (VRAM 1:1)", "1:1", "PAR 1:1"}};
static constexpr std::array s_display_aspect_ratio_values = {
- {-1.0f, 4.0f / 3.0f, 16.0f / 9.0f, 16.0f / 10.0f, 19.0f / 9.0f, 64.0f / 27.0f, 32.0f / 9.0f, 8.0f / 7.0f, 5.0f / 4.0f, 3.0f / 2.0f,
- 2.0f / 1.0f, 1.0f, -1.0f}};
+ {-1.0f, 4.0f / 3.0f, 16.0f / 9.0f, 16.0f / 10.0f, 19.0f / 9.0f, 64.0f / 27.0f, 32.0f / 9.0f, 8.0f / 7.0f, 5.0f / 4.0f,
+ 3.0f / 2.0f, 2.0f / 1.0f, 1.0f, -1.0f}};
std::optional Settings::ParseDisplayAspectRatio(const char* str)
{
diff --git a/src/core/settings.h b/src/core/settings.h
index 4ccf1cc89..eec5b40e2 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -171,6 +171,25 @@ struct Settings
mutable bool show_dma_state = false;
} debugging;
+ // texture replacements
+ struct TextureReplacementSettings
+ {
+ bool enable_vram_write_replacements = false;
+ bool preload_textures = false;
+
+ bool dump_vram_writes = false;
+ bool dump_vram_write_force_alpha_channel = true;
+ u32 dump_vram_write_width_threshold = 128;
+ u32 dump_vram_write_height_threshold = 128;
+
+ ALWAYS_INLINE bool AnyReplacementsEnabled() const { return enable_vram_write_replacements; }
+
+ ALWAYS_INLINE bool ShouldDumpVRAMWrite(u32 width, u32 height)
+ {
+ return dump_vram_writes && width >= dump_vram_write_width_threshold && height >= dump_vram_write_height_threshold;
+ }
+ } texture_replacements;
+
// TODO: Controllers, memory cards, etc.
bool bios_patch_tty_enable = false;
@@ -228,7 +247,9 @@ struct Settings
DEFAULT_DMA_MAX_SLICE_TICKS = 1000,
DEFAULT_DMA_HALT_TICKS = 100,
DEFAULT_GPU_FIFO_SIZE = 16,
- DEFAULT_GPU_MAX_RUN_AHEAD = 128
+ DEFAULT_GPU_MAX_RUN_AHEAD = 128,
+ DEFAULT_VRAM_WRITE_DUMP_WIDTH_THRESHOLD = 128,
+ DEFAULT_VRAM_WRITE_DUMP_HEIGHT_THRESHOLD = 128,
};
void Load(SettingsInterface& si);
diff --git a/src/core/system.cpp b/src/core/system.cpp
index b17c3e3a6..21542ff1e 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -27,6 +27,7 @@
#include "save_state_version.h"
#include "sio.h"
#include "spu.h"
+#include "texture_replacements.h"
#include "timers.h"
#include
#include
@@ -770,6 +771,8 @@ void Shutdown()
if (s_state == State::Shutdown)
return;
+ g_texture_replacements.Shutdown();
+
g_sio.Shutdown();
g_mdec.Shutdown();
g_spu.Shutdown();
@@ -1691,6 +1694,8 @@ void UpdateRunningGame(const char* path, CDImage* image)
s_running_game_code.c_str(), s_running_game_title.c_str());
}
+ g_texture_replacements.SetGameID(s_running_game_code);
+
g_host_interface->OnRunningGameChanged();
}
diff --git a/src/core/texture_replacements.cpp b/src/core/texture_replacements.cpp
new file mode 100644
index 000000000..12b46da03
--- /dev/null
+++ b/src/core/texture_replacements.cpp
@@ -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
+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 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, 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(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(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(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
+}
diff --git a/src/core/texture_replacements.h b/src/core/texture_replacements.h
new file mode 100644
index 000000000..4c19ab7b0
--- /dev/null
+++ b/src/core/texture_replacements.h
@@ -0,0 +1,89 @@
+#pragma once
+#include "common/hash_combine.h"
+#include "common/image.h"
+#include "types.h"
+#include
+#include
+#include
+#include
+
+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
+{
+ size_t operator()(const TextureReplacementHash& h) const
+ {
+ size_t hash_hash = std::hash{}(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;
+ using TextureCache = std::unordered_map;
+
+ 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;
\ No newline at end of file