GPU: Add hardware texture cache

This commit is contained in:
Stenzek 2023-12-31 19:40:10 +10:00
parent 4132b5ef3d
commit e06f1f1002
No known key found for this signature in database
36 changed files with 5020 additions and 692 deletions

View File

@ -2998,6 +2998,10 @@ public:
return GSVector4i(vsetq_lane_s32(high, vsetq_lane_s32(low, vdupq_n_s32(0), 0), 1));
}
ALWAYS_INLINE GSVector2 xy() const { return GSVector2(vget_low_s32(v4s)); }
ALWAYS_INLINE GSVector2 zw() const { return GSVector2(vget_high_s32(v4s)); }
#define VECTOR4_SHUFFLE_4(xs, xn, ys, yn, zs, zn, ws, wn) \
ALWAYS_INLINE GSVector4 xs##ys##zs##ws() const \
{ \

View File

@ -2289,6 +2289,10 @@ public:
return GSVector4i(static_cast<s32>(F64[0]), static_cast<s32>(F64[1]), 0, 0);
}
ALWAYS_INLINE GSVector2 xy() const { return GSVector2(x, y); }
ALWAYS_INLINE GSVector2 zw() const { return GSVector2(z, w); }
#define VECTOR4_SHUFFLE_4(xs, xn, ys, yn, zs, zn, ws, wn) \
ALWAYS_INLINE GSVector4 xs##ys##zs##ws() const { return GSVector4(F32[xn], F32[yn], F32[zn], F32[wn]); }

View File

@ -2365,6 +2365,10 @@ public:
ALWAYS_INLINE GSVector4i f64toi32() const { return GSVector4i(_mm_cvttpd_epi32(_mm_castps_pd(m))); }
ALWAYS_INLINE GSVector2 xy() const { return GSVector2(m); }
ALWAYS_INLINE GSVector2 zw() const { return GSVector2(_mm_shuffle_ps(m, m, _MM_SHUFFLE(3, 2, 3, 2))); }
#define VECTOR4_SHUFFLE_4(xs, xn, ys, yn, zs, zn, ws, wn) \
ALWAYS_INLINE GSVector4 xs##ys##zs##ws() const \
{ \

View File

@ -51,6 +51,8 @@ add_library(core
gpu_hw.h
gpu_hw_shadergen.cpp
gpu_hw_shadergen.h
gpu_hw_texture_cache.cpp
gpu_hw_texture_cache.h
gpu_shadergen.cpp
gpu_shadergen.h
gpu_sw.cpp
@ -108,8 +110,6 @@ add_library(core
spu.h
system.cpp
system.h
texture_replacements.cpp
texture_replacements.h
timers.cpp
timers.h
timing_event.cpp

View File

@ -47,6 +47,7 @@
<ClCompile Include="gpu_backend.cpp" />
<ClCompile Include="gpu_commands.cpp" />
<ClCompile Include="gpu_hw_shadergen.cpp" />
<ClCompile Include="gpu_hw_texture_cache.cpp" />
<ClCompile Include="gpu_shadergen.cpp" />
<ClCompile Include="gpu_sw.cpp" />
<ClCompile Include="gpu_sw_backend.cpp" />
@ -81,7 +82,6 @@
<ClCompile Include="sio.cpp" />
<ClCompile Include="spu.cpp" />
<ClCompile Include="system.cpp" />
<ClCompile Include="texture_replacements.cpp" />
<ClCompile Include="timers.cpp" />
<ClCompile Include="timing_event.cpp" />
</ItemGroup>
@ -124,6 +124,7 @@
<ClInclude Include="gdb_server.h" />
<ClInclude Include="gpu_backend.h" />
<ClInclude Include="gpu_hw_shadergen.h" />
<ClInclude Include="gpu_hw_texture_cache.h" />
<ClInclude Include="gpu_shadergen.h" />
<ClInclude Include="gpu_sw.h" />
<ClInclude Include="gpu_sw_backend.h" />
@ -161,7 +162,6 @@
<ClInclude Include="sio.h" />
<ClInclude Include="spu.h" />
<ClInclude Include="system.h" />
<ClInclude Include="texture_replacements.h" />
<ClInclude Include="timers.h" />
<ClInclude Include="timing_event.h" />
<ClInclude Include="types.h" />

View File

@ -46,7 +46,6 @@
<ClCompile Include="cpu_recompiler_code_generator_aarch32.cpp" />
<ClCompile Include="gpu_backend.cpp" />
<ClCompile Include="gpu_sw_backend.cpp" />
<ClCompile Include="texture_replacements.cpp" />
<ClCompile Include="multitap.cpp" />
<ClCompile Include="host.cpp" />
<ClCompile Include="game_database.cpp" />
@ -67,6 +66,7 @@
<ClCompile Include="justifier.cpp" />
<ClCompile Include="gdb_server.cpp" />
<ClCompile Include="gpu_sw_rasterizer.cpp" />
<ClCompile Include="gpu_hw_texture_cache.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -116,7 +116,6 @@
<ClInclude Include="gpu_types.h" />
<ClInclude Include="gpu_backend.h" />
<ClInclude Include="gpu_sw_backend.h" />
<ClInclude Include="texture_replacements.h" />
<ClInclude Include="multitap.h" />
<ClInclude Include="host.h" />
<ClInclude Include="achievements.h" />
@ -140,6 +139,7 @@
<ClInclude Include="justifier.h" />
<ClInclude Include="gdb_server.h" />
<ClInclude Include="gpu_sw_rasterizer.h" />
<ClInclude Include="gpu_hw_texture_cache.h" />
</ItemGroup>
<ItemGroup>
<None Include="gpu_sw_rasterizer.inl" />

View File

@ -4502,23 +4502,45 @@ void FullscreenUI::DrawDisplaySettingsPage()
MenuHeading(FSUI_CSTR("Texture Replacements"));
DrawToggleSetting(bsi, FSUI_CSTR("Enable VRAM Write Texture Replacement"),
FSUI_CSTR("Enables the replacement of background textures in supported games."),
"TextureReplacements", "EnableVRAMWriteReplacements", false);
DrawToggleSetting(bsi, FSUI_CSTR("Preload Replacement Textures"),
FSUI_CSTR("Loads all replacement texture to RAM, reducing stuttering at runtime."),
"TextureReplacements", "PreloadTextures", false);
ActiveButton(FSUI_CSTR("The texture cache is currently experimental, and may cause rendering errors in some games."),
false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font);
DrawToggleSetting(bsi, FSUI_CSTR("Enable Texture Cache"),
FSUI_CSTR("Enables caching of guest textures, required for texture replacement."), "GPU",
"EnableTextureCache", false);
DrawToggleSetting(bsi, FSUI_CSTR("Use Old MDEC Routines"),
FSUI_CSTR("Enables the older, less accurate MDEC decoding routines. May be required for old "
"replacement backgrounds to match/load."),
"Hacks", "UseOldMDECRoutines", false);
DrawToggleSetting(bsi, FSUI_CSTR("Dump Replaceable VRAM Writes"),
FSUI_CSTR("Writes textures which can be replaced to the dump directory."), "TextureReplacements",
const bool texture_cache_enabled = GetEffectiveBoolSetting(bsi, "GPU", "EnableTextureCache", false);
DrawToggleSetting(bsi, FSUI_CSTR("Enable Texture Replacements"),
FSUI_CSTR("Enables loading of replacement textures. Not compatible with all games."),
"TextureReplacements", "EnableTextureReplacements", false, texture_cache_enabled);
DrawToggleSetting(
bsi, FSUI_CSTR("Enable Texture Dumping"),
FSUI_CSTR("Enables dumping of textures to image files, which can be replaced. Not compatible with all games."),
"TextureReplacements", "DumpTextures", false, texture_cache_enabled);
DrawToggleSetting(bsi, FSUI_CSTR("Dump Replaced Textures"),
FSUI_CSTR("Dumps textures that have replacements already loaded."), "TextureReplacements",
"DumpReplacedTextures", false, texture_cache_enabled);
DrawToggleSetting(bsi, FSUI_CSTR("Enable VRAM Write Texture Replacement"),
FSUI_CSTR("Enables the replacement of background textures in supported games."),
"TextureReplacements", "EnableVRAMWriteReplacements", false);
DrawToggleSetting(bsi, FSUI_CSTR("Enable VRAM Write Dumping"),
FSUI_CSTR("Writes backgrounds that can be replaced to the dump directory."), "TextureReplacements",
"DumpVRAMWrites", false);
DrawToggleSetting(bsi, FSUI_CSTR("Set VRAM Write Dump Alpha Channel"),
FSUI_CSTR("Clears the mask/transparency bit in VRAM write dumps."), "TextureReplacements",
"DumpVRAMWriteForceAlphaChannel", true);
DrawToggleSetting(bsi, FSUI_CSTR("Preload Replacement Textures"),
FSUI_CSTR("Loads all replacement texture to RAM, reducing stuttering at runtime."),
"TextureReplacements", "PreloadTextures", false,
((texture_cache_enabled &&
GetEffectiveBoolSetting(bsi, "TextureReplacements", "EnableTextureReplacements", false)) ||
GetEffectiveBoolSetting(bsi, "TextureReplacements", "EnableVRAMWriteReplacements", false)));
DrawFolderSetting(bsi, FSUI_CSTR("Textures Directory"), "Folders", "Textures", EmuFolders::Textures);
EndMenuButtons();
}
@ -7239,7 +7261,6 @@ TRANSLATE_NOOP("FullscreenUI", "Clear Settings");
TRANSLATE_NOOP("FullscreenUI", "Clear Shaders");
TRANSLATE_NOOP("FullscreenUI", "Clears a shader from the chain.");
TRANSLATE_NOOP("FullscreenUI", "Clears all settings set for this game.");
TRANSLATE_NOOP("FullscreenUI", "Clears the mask/transparency bit in VRAM write dumps.");
TRANSLATE_NOOP("FullscreenUI", "Close");
TRANSLATE_NOOP("FullscreenUI", "Close Game");
TRANSLATE_NOOP("FullscreenUI", "Close Menu");
@ -7319,7 +7340,8 @@ TRANSLATE_NOOP("FullscreenUI", "Downsampling");
TRANSLATE_NOOP("FullscreenUI", "Downsampling Display Scale");
TRANSLATE_NOOP("FullscreenUI", "Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)");
TRANSLATE_NOOP("FullscreenUI", "DuckStation is a free simulator/emulator of the Sony PlayStation(TM) console, focusing on playability, speed, and long-term maintainability.");
TRANSLATE_NOOP("FullscreenUI", "Dump Replaceable VRAM Writes");
TRANSLATE_NOOP("FullscreenUI", "Dump Replaced Textures");
TRANSLATE_NOOP("FullscreenUI", "Dumps textures that have replacements already loaded.");
TRANSLATE_NOOP("FullscreenUI", "Emulation Settings");
TRANSLATE_NOOP("FullscreenUI", "Emulation Speed");
TRANSLATE_NOOP("FullscreenUI", "Enable 8MB RAM");
@ -7338,6 +7360,10 @@ TRANSLATE_NOOP("FullscreenUI", "Enable Rewinding");
TRANSLATE_NOOP("FullscreenUI", "Enable SDL Input Source");
TRANSLATE_NOOP("FullscreenUI", "Enable Subdirectory Scanning");
TRANSLATE_NOOP("FullscreenUI", "Enable TTY Logging");
TRANSLATE_NOOP("FullscreenUI", "Enable Texture Cache");
TRANSLATE_NOOP("FullscreenUI", "Enable Texture Dumping");
TRANSLATE_NOOP("FullscreenUI", "Enable Texture Replacements");
TRANSLATE_NOOP("FullscreenUI", "Enable VRAM Write Dumping");
TRANSLATE_NOOP("FullscreenUI", "Enable VRAM Write Texture Replacement");
TRANSLATE_NOOP("FullscreenUI", "Enable XInput Input Source");
TRANSLATE_NOOP("FullscreenUI", "Enable debugging when supported by the host's renderer API. Only for developer use.");
@ -7345,6 +7371,9 @@ TRANSLATE_NOOP("FullscreenUI", "Enable/Disable the Player LED on DualSense contr
TRANSLATE_NOOP("FullscreenUI", "Enables alignment and bus exceptions. Not needed for any known games.");
TRANSLATE_NOOP("FullscreenUI", "Enables an additional 6MB of RAM to obtain a total of 2+6 = 8MB, usually present on dev consoles.");
TRANSLATE_NOOP("FullscreenUI", "Enables an additional three controller slots on each port. Not supported in all games.");
TRANSLATE_NOOP("FullscreenUI", "Enables caching of guest textures, required for texture replacement.");
TRANSLATE_NOOP("FullscreenUI", "Enables dumping of textures to image files, which can be replaced. Not compatible with all games.");
TRANSLATE_NOOP("FullscreenUI", "Enables loading of replacement textures. Not compatible with all games.");
TRANSLATE_NOOP("FullscreenUI", "Enables smooth scrolling of menus in Big Picture UI.");
TRANSLATE_NOOP("FullscreenUI", "Enables the older, less accurate MDEC decoding routines. May be required for old replacement backgrounds to match/load.");
TRANSLATE_NOOP("FullscreenUI", "Enables the replacement of background textures in supported games.");
@ -7626,7 +7655,6 @@ TRANSLATE_NOOP("FullscreenUI", "Selects the view that the game list will open to
TRANSLATE_NOOP("FullscreenUI", "Serial");
TRANSLATE_NOOP("FullscreenUI", "Session: {}");
TRANSLATE_NOOP("FullscreenUI", "Set Input Binding");
TRANSLATE_NOOP("FullscreenUI", "Set VRAM Write Dump Alpha Channel");
TRANSLATE_NOOP("FullscreenUI", "Sets a threshold for discarding precise values when exceeded. May help with glitches in some games.");
TRANSLATE_NOOP("FullscreenUI", "Sets a threshold for discarding the emulated depth buffer. May help in some games.");
TRANSLATE_NOOP("FullscreenUI", "Sets the fast forward speed. It is not guaranteed that this speed will be reached on all systems.");
@ -7705,10 +7733,12 @@ TRANSLATE_NOOP("FullscreenUI", "Temporarily disables all enhancements, useful wh
TRANSLATE_NOOP("FullscreenUI", "Test Unofficial Achievements");
TRANSLATE_NOOP("FullscreenUI", "Texture Filtering");
TRANSLATE_NOOP("FullscreenUI", "Texture Replacements");
TRANSLATE_NOOP("FullscreenUI", "Textures Directory");
TRANSLATE_NOOP("FullscreenUI", "The SDL input source supports most controllers.");
TRANSLATE_NOOP("FullscreenUI", "The XInput source provides support for XBox 360/XBox One/XBox Series controllers.");
TRANSLATE_NOOP("FullscreenUI", "The audio backend determines how frames produced by the emulator are submitted to the host.");
TRANSLATE_NOOP("FullscreenUI", "The selected memory card image will be used in shared mode for this slot.");
TRANSLATE_NOOP("FullscreenUI", "The texture cache is currently experimental, and may cause rendering errors in some games.");
TRANSLATE_NOOP("FullscreenUI", "This game has no achievements.");
TRANSLATE_NOOP("FullscreenUI", "This game has no leaderboards.");
TRANSLATE_NOOP("FullscreenUI", "Threaded Rendering");
@ -7763,7 +7793,7 @@ TRANSLATE_NOOP("FullscreenUI", "When playing a multi-disc game and using per-gam
TRANSLATE_NOOP("FullscreenUI", "When this option is chosen, the clock speed set below will be used.");
TRANSLATE_NOOP("FullscreenUI", "Widescreen Rendering");
TRANSLATE_NOOP("FullscreenUI", "Wireframe Rendering");
TRANSLATE_NOOP("FullscreenUI", "Writes textures which can be replaced to the dump directory.");
TRANSLATE_NOOP("FullscreenUI", "Writes backgrounds that can be replaced to the dump directory.");
TRANSLATE_NOOP("FullscreenUI", "Yes, {} now and risk memory card corruption.");
TRANSLATE_NOOP("FullscreenUI", "\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, cheats, and slowdown functions.");
TRANSLATE_NOOP("FullscreenUI", "\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe Limited. This software is not affiliated in any way with Sony Interactive Entertainment.");

View File

@ -1555,8 +1555,8 @@ void GPU::SetDrawMode(u16 value)
if (new_mode_reg.bits == m_draw_mode.mode_reg.bits)
return;
m_draw_mode.texture_page_changed |= ((new_mode_reg.bits & GPUDrawModeReg::TEXTURE_PAGE_MASK) !=
(m_draw_mode.mode_reg.bits & GPUDrawModeReg::TEXTURE_PAGE_MASK));
m_draw_mode.texture_page_changed |= ((new_mode_reg.bits & GPUDrawModeReg::TEXTURE_MODE_AND_PAGE_MASK) !=
(m_draw_mode.mode_reg.bits & GPUDrawModeReg::TEXTURE_MODE_AND_PAGE_MASK));
m_draw_mode.mode_reg.bits = new_mode_reg.bits;
if (m_GPUSTAT.draw_to_displayed_field != new_mode_reg.draw_to_displayed_field)

View File

@ -2,9 +2,9 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gpu.h"
#include "gpu_hw_texture_cache.h"
#include "interrupt_controller.h"
#include "system.h"
#include "texture_replacements.h"
#include "common/assert.h"
#include "common/log.h"
@ -532,9 +532,9 @@ void GPU::FinishVRAMWrite()
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))
if (GPUTextureCache::ShouldDumpVRAMWrite(m_vram_transfer.width, m_vram_transfer.height))
{
TextureReplacements::DumpVRAMWrite(m_vram_transfer.width, m_vram_transfer.height,
GPUTextureCache::DumpVRAMWrite(m_vram_transfer.width, m_vram_transfer.height,
reinterpret_cast<const u16*>(m_blit_buffer.data()));
}

View File

@ -194,6 +194,8 @@ GPU_HW::GPU_HW() : GPU()
GPU_HW::~GPU_HW()
{
GPUTextureCache::Shutdown();
if (m_sw_renderer)
{
m_sw_renderer->Shutdown();
@ -262,6 +264,8 @@ bool GPU_HW::Initialize()
m_clamp_uvs = ShouldClampUVs(m_texture_filtering) || ShouldClampUVs(m_sprite_texture_filtering);
m_compute_uv_range = m_clamp_uvs;
m_allow_sprite_mode = ShouldAllowSpriteMode(m_resolution_scale, m_texture_filtering, m_sprite_texture_filtering);
m_use_texture_cache = g_settings.gpu_texture_cache;
m_texture_dumping = m_use_texture_cache && g_settings.texture_replacements.dump_textures;
CheckSettings();
@ -282,13 +286,27 @@ bool GPU_HW::Initialize()
return false;
}
if (m_use_texture_cache)
{
if (!GPUTextureCache::Initialize())
{
ERROR_LOG("Failed to initialize texture cache, disabling.");
m_use_texture_cache = false;
}
}
UpdateDownsamplingLevels();
RestoreDeviceContext();
return true;
}
void GPU_HW::Reset(bool clear_vram)
{
// Texture cache needs to be invalidated before we load, otherwise we dump black.
if (m_use_texture_cache)
GPUTextureCache::Invalidate();
if (m_batch_vertex_ptr)
UnmapGPUBuffer(0, 0);
@ -364,6 +382,7 @@ bool GPU_HW::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di
else if (sw.IsReading())
{
// Need to update the VRAM copy on the GPU with the state data.
// Would invalidate the TC, but base DoState() calls Reset().
UpdateVRAMOnGPU(0, 0, VRAM_WIDTH, VRAM_HEIGHT, g_vram, VRAM_WIDTH * sizeof(u16), false, false, VRAM_SIZE_RECT);
}
@ -373,10 +392,12 @@ bool GPU_HW::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di
DebugAssert(!m_batch_vertex_ptr && !m_batch_index_ptr);
ClearVRAMDirtyRectangle();
SetFullVRAMDirtyRectangle();
UpdateVRAMReadTexture(true, false);
ClearVRAMDirtyRectangle();
ResetBatchVertexDepth();
}
return true;
return GPUTextureCache::DoState(sw, !m_use_texture_cache);
}
void GPU_HW::RestoreDeviceContext()
@ -401,7 +422,8 @@ void GPU_HW::UpdateSettings(const Settings& old_settings)
const bool clamp_uvs = ShouldClampUVs(m_texture_filtering) || ShouldClampUVs(m_sprite_texture_filtering);
const bool framebuffer_changed = (m_resolution_scale != resolution_scale || m_multisamples != multisamples ||
g_settings.IsUsingAccurateBlending() != old_settings.IsUsingAccurateBlending() ||
m_pgxp_depth_buffer != g_settings.UsingPGXPDepthBuffer());
m_pgxp_depth_buffer != g_settings.UsingPGXPDepthBuffer() ||
(!old_settings.gpu_texture_cache && g_settings.gpu_texture_cache));
const bool shaders_changed =
(m_resolution_scale != resolution_scale || m_multisamples != multisamples ||
m_true_color != g_settings.gpu_true_color || prev_force_progressive_scan != m_force_progressive_scan ||
@ -468,6 +490,8 @@ void GPU_HW::UpdateSettings(const Settings& old_settings)
m_clamp_uvs = clamp_uvs;
m_compute_uv_range = m_clamp_uvs;
m_allow_sprite_mode = ShouldAllowSpriteMode(resolution_scale, m_texture_filtering, m_sprite_texture_filtering);
m_use_texture_cache = g_settings.gpu_texture_cache;
m_texture_dumping = m_use_texture_cache && g_settings.texture_replacements.dump_textures;
m_batch.sprite_mode = (m_allow_sprite_mode && m_batch.sprite_mode);
const bool depth_buffer_changed = (m_pgxp_depth_buffer != g_settings.UsingPGXPDepthBuffer());
@ -521,6 +545,23 @@ void GPU_HW::UpdateSettings(const Settings& old_settings)
UpdateDepthBufferFromMaskBit();
}
if (m_use_texture_cache && !old_settings.gpu_texture_cache)
{
if (!GPUTextureCache::Initialize())
{
ERROR_LOG("Failed to initialize texture cache, disabling.");
m_use_texture_cache = false;
}
}
else if (!m_use_texture_cache && old_settings.gpu_texture_cache)
{
GPUTextureCache::Shutdown();
}
else if (m_use_texture_cache)
{
GPUTextureCache::UpdateSettings(old_settings);
}
if (g_settings.gpu_downsample_mode != old_settings.gpu_downsample_mode ||
(g_settings.gpu_downsample_mode == GPUDownsampleMode::Box &&
g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale))
@ -728,6 +769,9 @@ void GPU_HW::AddWrittenRectangle(const GSVector4i rect)
{
m_vram_dirty_write_rect = m_vram_dirty_write_rect.runion(rect);
SetTexPageChangedOnOverlap(m_vram_dirty_write_rect);
if (m_use_texture_cache)
GPUTextureCache::AddWrittenRectangle(rect);
}
void GPU_HW::AddDrawnRectangle(const GSVector4i rect)
@ -735,13 +779,22 @@ void GPU_HW::AddDrawnRectangle(const GSVector4i rect)
// Normally, we would check for overlap here. But the GPU's texture cache won't actually reload until the page
// changes, or it samples a larger region, so we can get away without doing so. This reduces copies considerably in
// games like Mega Man Legends 2.
m_vram_dirty_draw_rect = m_vram_dirty_draw_rect.runion(rect);
if (m_current_draw_rect.rcontains(rect))
return;
m_current_draw_rect = m_current_draw_rect.runion(rect);
m_vram_dirty_draw_rect = m_vram_dirty_draw_rect.runion(m_current_draw_rect);
if (m_use_texture_cache)
GPUTextureCache::AddDrawnRectangle(m_current_draw_rect, m_clamped_drawing_area);
}
void GPU_HW::AddUnclampedDrawnRectangle(const GSVector4i rect)
{
m_vram_dirty_draw_rect = m_vram_dirty_draw_rect.runion(rect);
SetTexPageChangedOnOverlap(m_vram_dirty_draw_rect);
if (m_use_texture_cache)
GPUTextureCache::AddDrawnRectangle(rect, rect);
}
void GPU_HW::SetTexPageChangedOnOverlap(const GSVector4i update_rect)
@ -749,9 +802,9 @@ void GPU_HW::SetTexPageChangedOnOverlap(const GSVector4i update_rect)
// the vram area can include the texture page, but the game can leave it as-is. in this case, set it as dirty so the
// shadow texture is updated
if (!m_draw_mode.IsTexturePageChanged() && m_batch.texture_mode != BatchTextureMode::Disabled &&
(m_draw_mode.mode_reg.GetTexturePageRectangle().rintersects(update_rect) ||
(GetTextureRect(m_draw_mode.mode_reg.texture_page, m_draw_mode.mode_reg.texture_mode).rintersects(update_rect) ||
(m_draw_mode.mode_reg.IsUsingPalette() &&
m_draw_mode.palette_reg.GetRectangle(m_draw_mode.mode_reg.texture_mode).rintersects(update_rect))))
GetPaletteRect(m_draw_mode.palette_reg, m_draw_mode.mode_reg.texture_mode).rintersects(update_rect))))
{
m_draw_mode.SetTexturePageChanged();
}
@ -882,6 +935,8 @@ void GPU_HW::ClearFramebuffer()
g_gpu_device->ClearDepth(m_vram_depth_texture.get(), m_pgxp_depth_buffer ? 1.0f : 0.0f);
}
ClearVRAMDirtyRectangle();
if (m_use_texture_cache)
GPUTextureCache::Invalidate();
m_last_depth_z = 1.0f;
}
@ -986,7 +1041,7 @@ bool GPU_HW::CompilePipelines(Error* error)
const u32 active_texture_modes =
m_allow_sprite_mode ? NUM_TEXTURE_MODES :
(NUM_TEXTURE_MODES - (NUM_TEXTURE_MODES - static_cast<u32>(BatchTextureMode::SpriteStart)));
const u32 total_vertex_shaders = (m_allow_sprite_mode ? 5 : 3);
const u32 total_vertex_shaders = (m_allow_sprite_mode ? 7 : 3);
const u32 total_fragment_shaders =
((needs_rov_depth ? 2 : 1) * 5 * 5 * active_texture_modes * 2 * (1 + BoolToUInt32(!true_color)) *
(1 + BoolToUInt32(!m_force_progressive_scan)) * (1 + BoolToUInt32(needs_rov_depth)));
@ -1013,7 +1068,7 @@ bool GPU_HW::CompilePipelines(Error* error)
// vertex shaders - [textured/palette/sprite]
// fragment shaders - [depth_test][render_mode][transparency_mode][texture_mode][check_mask][dithering][interlacing]
static constexpr auto destroy_shader = [](std::unique_ptr<GPUShader>& s) { s.reset(); };
DimensionalArray<std::unique_ptr<GPUShader>, 2, 2, 2> batch_vertex_shaders{};
DimensionalArray<std::unique_ptr<GPUShader>, 2, 3, 2> batch_vertex_shaders{};
DimensionalArray<std::unique_ptr<GPUShader>, 2, 2, 2, NUM_TEXTURE_MODES, 5, 5, 2> batch_fragment_shaders{};
ScopedGuard batch_shader_guard([&batch_vertex_shaders, &batch_fragment_shaders]() {
batch_vertex_shaders.enumerate(destroy_shader);
@ -1022,7 +1077,7 @@ bool GPU_HW::CompilePipelines(Error* error)
for (u8 textured = 0; textured < 2; textured++)
{
for (u8 palette = 0; palette < 2; palette++)
for (u8 palette = 0; palette < 3; palette++)
{
if (palette && !textured)
continue;
@ -1034,7 +1089,7 @@ bool GPU_HW::CompilePipelines(Error* error)
const bool uv_limits = ShouldClampUVs(sprite ? m_sprite_texture_filtering : m_texture_filtering);
const std::string vs = shadergen.GenerateBatchVertexShader(
textured != 0, palette != 0, uv_limits, !sprite && force_round_texcoords, m_pgxp_depth_buffer);
textured != 0, palette == 1, palette == 2, uv_limits, !sprite && force_round_texcoords, m_pgxp_depth_buffer);
if (!(batch_vertex_shaders[textured][palette][sprite] =
g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(), vs, error)))
{
@ -1203,6 +1258,8 @@ bool GPU_HW::CompilePipelines(Error* error)
static_cast<BatchTextureMode>(texture_mode) == BatchTextureMode::Palette8Bit ||
static_cast<BatchTextureMode>(texture_mode) == BatchTextureMode::SpritePalette4Bit ||
static_cast<BatchTextureMode>(texture_mode) == BatchTextureMode::SpritePalette8Bit);
const bool page_texture =
(static_cast<BatchTextureMode>(texture_mode) == BatchTextureMode::PageTexture);
const bool sprite = (static_cast<BatchTextureMode>(texture_mode) >= BatchTextureMode::SpriteStart);
const bool uv_limits = ShouldClampUVs(sprite ? m_sprite_texture_filtering : m_texture_filtering);
const bool use_shader_blending = (render_mode == static_cast<u8>(BatchRenderMode::ShaderBlend));
@ -1216,7 +1273,9 @@ bool GPU_HW::CompilePipelines(Error* error)
std::span<const GPUPipeline::VertexAttribute>(vertex_attributes, NUM_BATCH_VERTEX_ATTRIBUTES);
plconfig.vertex_shader =
batch_vertex_shaders[BoolToUInt8(textured)][BoolToUInt8(palette)][BoolToUInt8(sprite)].get();
batch_vertex_shaders[BoolToUInt8(textured)][page_texture ? 2 : BoolToUInt8(palette)]
[BoolToUInt8(sprite)]
.get();
plconfig.fragment_shader =
batch_fragment_shaders[BoolToUInt8(depth_test && needs_rov_depth)][render_mode]
[use_shader_blending ? transparency_mode :
@ -1836,19 +1895,26 @@ void GPU_HW::UnmapGPUBuffer(u32 used_vertices, u32 used_indices)
}
ALWAYS_INLINE_RELEASE void GPU_HW::DrawBatchVertices(BatchRenderMode render_mode, u32 num_indices, u32 base_index,
u32 base_vertex)
u32 base_vertex, const GPUTextureCache::Source* texture)
{
// [depth_test][transparency_mode][render_mode][texture_mode][dithering][interlacing][check_mask]
const u8 texture_mode = static_cast<u8>(m_batch.texture_mode) +
((m_batch.texture_mode != BatchTextureMode::Disabled && m_batch.sprite_mode) ?
const u8 texture_mode = texture ? static_cast<u8>(BatchTextureMode::PageTexture) :
(static_cast<u8>(m_batch.texture_mode) +
((m_batch.texture_mode < BatchTextureMode::PageTexture && m_batch.sprite_mode) ?
static_cast<u8>(BatchTextureMode::SpriteStart) :
0);
0));
const u8 depth_test = BoolToUInt8(m_batch.use_depth_buffer);
const u8 check_mask = BoolToUInt8(m_batch.check_mask_before_draw);
g_gpu_device->SetPipeline(m_batch_pipelines[depth_test][static_cast<u8>(m_batch.transparency_mode)][static_cast<u8>(
render_mode)][texture_mode][BoolToUInt8(m_batch.dithering)][BoolToUInt8(m_batch.interlacing)][check_mask]
.get());
if (m_use_texture_cache && texture_mode != static_cast<u8>(BatchTextureMode::Disabled))
{
g_gpu_device->SetTextureSampler(0, texture ? texture->texture : m_vram_read_texture.get(),
g_gpu_device->GetNearestSampler());
}
GL_INS_FMT("Texture mode: {}", s_batch_texture_modes[texture_mode]);
GL_INS_FMT("Transparency mode: {}", s_transparency_modes[static_cast<u8>(m_batch.transparency_mode)]);
GL_INS_FMT("Render mode: {}", s_batch_render_modes[static_cast<u8>(render_mode)]);
@ -2199,7 +2265,7 @@ void GPU_HW::ComputePolygonUVLimits(BatchVertex* vertices, u32 num_vertices)
for (u32 i = 0; i < num_vertices; i++)
vertices[i].SetUVLimits(min_u, max_u, min_v, max_v);
if (m_texpage_dirty != 0)
if (ShouldCheckForTexPageOverlap())
CheckForTexPageOverlap(GSVector4i(min).upl32(GSVector4i(max)).u16to32());
}
@ -2620,7 +2686,7 @@ void GPU_HW::LoadVertices()
const u32 tex_right = tex_left + quad_width;
const u32 uv_limits = BatchVertex::PackUVLimits(tex_left, tex_right - 1, tex_top, tex_bottom - 1);
if (rc.texture_enable && m_texpage_dirty != 0)
if (rc.texture_enable && ShouldCheckForTexPageOverlap())
{
CheckForTexPageOverlap(GSVector4i(static_cast<s32>(tex_left), static_cast<s32>(tex_top),
static_cast<s32>(tex_right), static_cast<s32>(tex_bottom)));
@ -2801,7 +2867,7 @@ void GPU_HW::LoadVertices()
}
}
bool GPU_HW::BlitVRAMReplacementTexture(const TextureReplacements::ReplacementImage* tex, u32 dst_x, u32 dst_y,
bool GPU_HW::BlitVRAMReplacementTexture(const GPUTextureCache::TextureReplacementImage* tex, u32 dst_x, u32 dst_y,
u32 width, u32 height)
{
if (!m_vram_replacement_texture || m_vram_replacement_texture->GetWidth() < tex->GetWidth() ||
@ -2844,7 +2910,7 @@ bool GPU_HW::BlitVRAMReplacementTexture(const TextureReplacements::ReplacementIm
ALWAYS_INLINE_RELEASE void GPU_HW::CheckForTexPageOverlap(GSVector4i uv_rect)
{
DebugAssert(m_texpage_dirty != 0 && m_batch.texture_mode != BatchTextureMode::Disabled);
DebugAssert((m_texpage_dirty != 0 || m_texture_dumping) && m_batch.texture_mode != BatchTextureMode::Disabled);
if (m_texture_window_active)
{
@ -2871,6 +2937,34 @@ ALWAYS_INLINE_RELEASE void GPU_HW::CheckForTexPageOverlap(GSVector4i uv_rect)
m_current_uv_rect = new_uv_rect;
bool update_drawn = false, update_written = false;
if (m_texpage_dirty & TEXPAGE_DIRTY_PAGE_RECT)
{
DebugAssert(!(m_texpage_dirty & (TEXPAGE_DIRTY_DRAWN_RECT | TEXPAGE_DIRTY_WRITTEN_RECT)));
DebugAssert(m_batch.texture_mode == BatchTextureMode::PageTexture &&
m_batch.texture_cache_key.page < NUM_VRAM_PAGES);
if (GPUTextureCache::AreSourcePagesDrawn(m_batch.texture_cache_key, m_current_uv_rect))
{
// UVs intersect with drawn area, can't use TC
if (m_batch_index_count > 0)
{
FlushRender();
EnsureVertexBufferSpaceForCurrentCommand();
}
// We need to swap the dirty tracking over to drawn/written.
const GSVector4i page_rect = GetTextureRect(m_batch.texture_cache_key.page, m_batch.texture_cache_key.mode);
m_texpage_dirty = (m_vram_dirty_draw_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_DRAWN_RECT : 0) |
(m_vram_dirty_write_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_WRITTEN_RECT : 0);
m_compute_uv_range = (ShouldCheckForTexPageOverlap() || m_clamp_uvs);
m_batch.texture_mode = static_cast<BatchTextureMode>(m_draw_mode.mode_reg.texture_mode.GetValue());
}
else
{
// Page isn't drawn, we're done.
return;
}
}
if (m_texpage_dirty & TEXPAGE_DIRTY_DRAWN_RECT)
{
DebugAssert(!m_vram_dirty_draw_rect.eq(INVALID_RECT));
@ -2905,6 +2999,11 @@ ALWAYS_INLINE_RELEASE void GPU_HW::CheckForTexPageOverlap(GSVector4i uv_rect)
}
}
bool GPU_HW::ShouldCheckForTexPageOverlap() const
{
return (m_texpage_dirty != 0);
}
ALWAYS_INLINE bool GPU_HW::IsFlushed() const
{
return (m_batch_index_count == 0);
@ -3003,8 +3102,13 @@ void GPU_HW::UpdateSoftwareRenderer(bool copy_vram_from_hw)
{
const bool current_enabled = (m_sw_renderer != nullptr);
const bool new_enabled = g_settings.gpu_use_software_renderer_for_readbacks;
const bool use_thread = !g_settings.gpu_texture_cache;
if (current_enabled == new_enabled)
{
if (m_sw_renderer)
m_sw_renderer->SetThreadEnabled(use_thread);
return;
}
if (!new_enabled)
{
@ -3015,7 +3119,7 @@ void GPU_HW::UpdateSoftwareRenderer(bool copy_vram_from_hw)
}
std::unique_ptr<GPU_SW_Backend> sw_renderer = std::make_unique<GPU_SW_Backend>();
if (!sw_renderer->Initialize(true))
if (!sw_renderer->Initialize(use_thread))
return;
// We need to fill in the SW renderer's VRAM with the current state for hot toggles.
@ -3080,7 +3184,17 @@ void GPU_HW::FillVRAM(u32 x, u32 y, u32 width, u32 height, u32 color)
GL_INS_FMT("Dirty draw area before: {}", m_vram_dirty_draw_rect);
const GSVector4i bounds = GetVRAMTransferBounds(x, y, width, height);
// If TC is enabled, we have to update local memory.
if (m_use_texture_cache && !IsInterlacedRenderingEnabled())
{
AddWrittenRectangle(bounds);
GPU_SW_Rasterizer::FillVRAM(x, y, width, height, color, false, 0);
}
else
{
AddUnclampedDrawnRectangle(bounds);
}
GL_INS_FMT("Dirty draw area after: {}", m_vram_dirty_draw_rect);
@ -3126,6 +3240,8 @@ void GPU_HW::ReadVRAM(u32 x, u32 y, u32 width, u32 height)
return;
}
// TODO: Only read if it's in the drawn area
// Get bounds with wrap-around handled.
GSVector4i copy_rect = GetVRAMTransferBounds(x, y, width, height);
@ -3177,7 +3293,17 @@ void GPU_HW::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* data, b
{
GL_SCOPE_FMT("UpdateVRAM({},{} => {},{} ({}x{})", x, y, x + width, y + height, width, height);
if (m_sw_renderer)
// TODO: Handle wrapped transfers... break them up or something
const GSVector4i bounds = GetVRAMTransferBounds(x, y, width, height);
DebugAssert(bounds.right <= static_cast<s32>(VRAM_WIDTH) && bounds.bottom <= static_cast<s32>(VRAM_HEIGHT));
AddWrittenRectangle(bounds);
// We want to dump *before* the write goes through, otherwise we dump bad data.
if (m_use_texture_cache)
{
GPUTextureCache::WriteVRAM(x, y, width, height, data, set_mask, check_mask, bounds);
}
else if (m_sw_renderer)
{
const u32 num_words = width * height;
GPUBackendUpdateVRAMCommand* cmd = m_sw_renderer->NewUpdateVRAMCommand(num_words);
@ -3192,10 +3318,6 @@ void GPU_HW::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* data, b
m_sw_renderer->PushCommand(cmd);
}
const GSVector4i bounds = GetVRAMTransferBounds(x, y, width, height);
DebugAssert(bounds.right <= static_cast<s32>(VRAM_WIDTH) && bounds.bottom <= static_cast<s32>(VRAM_HEIGHT));
AddWrittenRectangle(bounds);
if (check_mask)
{
// set new vertex counter since we want this to take into consideration previous masked pixels
@ -3203,7 +3325,7 @@ void GPU_HW::UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* data, b
}
else
{
const TextureReplacements::ReplacementImage* rtex = TextureReplacements::GetVRAMReplacement(width, height, data);
const GPUTextureCache::TextureReplacementImage* rtex = GPUTextureCache::GetVRAMReplacement(width, height, data);
if (rtex && BlitVRAMReplacementTexture(rtex, x * m_resolution_scale, y * m_resolution_scale,
width * m_resolution_scale, height * m_resolution_scale))
{
@ -3283,7 +3405,26 @@ void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32
{
GL_SCOPE_FMT("CopyVRAM({}x{} @ {},{} => {},{}", width, height, src_x, src_y, dst_x, dst_y);
if (m_sw_renderer)
// masking enabled, oversized, or overlapping
const bool use_shader =
(m_GPUSTAT.IsMaskingEnabled() || ((src_x % VRAM_WIDTH) + width) > VRAM_WIDTH ||
((src_y % VRAM_HEIGHT) + height) > VRAM_HEIGHT || ((dst_x % VRAM_WIDTH) + width) > VRAM_WIDTH ||
((dst_y % VRAM_HEIGHT) + height) > VRAM_HEIGHT);
const GSVector4i src_bounds = GetVRAMTransferBounds(src_x, src_y, width, height);
const GSVector4i dst_bounds = GetVRAMTransferBounds(dst_x, dst_y, width, height);
// If we're copying a region that hasn't been drawn to, and we're using the TC, we can do it in local memory.
if (m_use_texture_cache && !GPUTextureCache::IsRectDrawn(src_bounds))
{
GL_INS("Performed in local memory.");
GPUTextureCache::CopyVRAM(src_x, src_y, dst_x, dst_y, width, height, m_GPUSTAT.set_mask_while_drawing,
m_GPUSTAT.check_mask_before_draw, src_bounds, dst_bounds);
UpdateVRAMOnGPU(dst_bounds.left, dst_bounds.top, dst_bounds.width(), dst_bounds.height(),
&g_vram[dst_bounds.top * VRAM_WIDTH + dst_bounds.left], VRAM_WIDTH * sizeof(u16), false, false,
dst_bounds);
return;
}
else if (m_sw_renderer)
{
GPUBackendCopyVRAMCommand* cmd = m_sw_renderer->NewCopyVRAMCommand();
FillBackendCommandParameters(cmd);
@ -3296,16 +3437,8 @@ void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32
m_sw_renderer->PushCommand(cmd);
}
// masking enabled, oversized, or overlapping
const bool use_shader =
(m_GPUSTAT.IsMaskingEnabled() || ((src_x % VRAM_WIDTH) + width) > VRAM_WIDTH ||
((src_y % VRAM_HEIGHT) + height) > VRAM_HEIGHT || ((dst_x % VRAM_WIDTH) + width) > VRAM_WIDTH ||
((dst_y % VRAM_HEIGHT) + height) > VRAM_HEIGHT);
const GSVector4i src_bounds = GetVRAMTransferBounds(src_x, src_y, width, height);
const GSVector4i dst_bounds = GetVRAMTransferBounds(dst_x, dst_y, width, height);
const bool intersect_with_draw = m_vram_dirty_draw_rect.rintersects(src_bounds);
const bool intersect_with_write = m_vram_dirty_write_rect.rintersects(src_bounds);
if (use_shader || IsUsingMultisampling())
{
if (intersect_with_draw || intersect_with_write)
@ -3343,6 +3476,7 @@ void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32
g_gpu_device->SetViewportAndScissor(dst_bounds_scaled);
g_gpu_device->SetPipeline(
m_vram_copy_pipelines[BoolToUInt8(m_GPUSTAT.check_mask_before_draw && m_write_mask_as_depth)].get());
g_gpu_device->SetTextureSampler(0, m_vram_read_texture.get(), g_gpu_device->GetNearestSampler());
g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms));
g_gpu_device->Draw(3, 0);
RestoreDeviceContext();
@ -3362,7 +3496,8 @@ void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32
UpdateVRAMReadTexture(intersect_with_draw, intersect_with_write);
}
if (intersect_with_draw)
// We don't have it in local memory, so TC can't read it.
if (intersect_with_draw || m_use_texture_cache)
{
AddUnclampedDrawnRectangle(dst_bounds);
}
@ -3398,27 +3533,30 @@ void GPU_HW::DispatchRenderCommand()
{
const GPURenderCommand rc{m_render_command.bits};
BatchTextureMode texture_mode = BatchTextureMode::Disabled;
// TODO: avoid all this for vertex loading, only do when the type of draw changes
BatchTextureMode texture_mode = rc.IsTexturingEnabled() ? m_batch.texture_mode : BatchTextureMode::Disabled;
GPUTextureCache::SourceKey texture_cache_key = m_batch.texture_cache_key;
if (rc.IsTexturingEnabled())
{
// texture page changed - check that the new page doesn't intersect the drawing area
if (m_draw_mode.IsTexturePageChanged())
if (m_draw_mode.IsTexturePageChanged() || texture_mode == BatchTextureMode::Disabled)
{
m_draw_mode.ClearTexturePageChangedFlag();
#if 0
if (!m_vram_dirty_draw_rect.eq(INVALID_RECT) || !m_vram_dirty_write_rect.eq(INVALID_RECT))
{
GL_INS_FMT("VRAM DIRTY: {} {}", m_vram_dirty_draw_rect, m_vram_dirty_write_rect);
GL_INS_FMT("PAGE RECT: {}", m_draw_mode.mode_reg.GetTexturePageRectangle());
if (m_draw_mode.mode_reg.IsUsingPalette())
GL_INS_FMT("PALETTE RECT: {}", m_draw_mode.palette_reg.GetRectangle(m_draw_mode.mode_reg.texture_mode));
}
#endif
// start by assuming we can use the TC
bool use_texture_cache = m_use_texture_cache;
// check that the palette isn't in a drawn area
if (m_draw_mode.mode_reg.IsUsingPalette())
{
const GSVector4i palette_rect = m_draw_mode.palette_reg.GetRectangle(m_draw_mode.mode_reg.texture_mode);
const GSVector4i palette_rect =
GetPaletteRect(m_draw_mode.palette_reg, m_draw_mode.mode_reg.texture_mode, use_texture_cache);
if (!use_texture_cache || GPUTextureCache::IsRectDrawn(palette_rect))
{
if (use_texture_cache)
GL_INS_FMT("Palette at {} is in drawn area, can't use TC", palette_rect);
use_texture_cache = false;
const bool update_drawn = palette_rect.rintersects(m_vram_dirty_draw_rect);
const bool update_written = palette_rect.rintersects(m_vram_dirty_write_rect);
if (update_drawn || update_written)
@ -3430,33 +3568,61 @@ void GPU_HW::DispatchRenderCommand()
UpdateVRAMReadTexture(update_drawn, update_written);
}
}
}
const GSVector4i page_rect = m_draw_mode.mode_reg.GetTexturePageRectangle();
GSVector4i::storel(m_current_texture_page_offset, page_rect);
m_compute_uv_range = (m_clamp_uvs || m_texture_dumping);
u8 new_texpage_dirty = m_vram_dirty_draw_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_DRAWN_RECT : 0;
new_texpage_dirty |= m_vram_dirty_write_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_WRITTEN_RECT : 0;
const GPUTextureMode gpu_texture_mode =
(m_draw_mode.mode_reg.texture_mode == GPUTextureMode::Reserved_Direct16Bit) ? GPUTextureMode::Direct16Bit :
m_draw_mode.mode_reg.texture_mode;
const GSVector4i page_rect = GetTextureRect(m_draw_mode.mode_reg.texture_page, m_draw_mode.mode_reg.texture_mode);
if (new_texpage_dirty != 0)
// TODO: This will result in incorrect global-space UVs when the texture page wraps around.
// Need to deal with it if it becomes a problem.
m_current_texture_page_offset[0] = static_cast<s32>(m_draw_mode.mode_reg.GetTexturePageBaseX());
m_current_texture_page_offset[1] = static_cast<s32>(m_draw_mode.mode_reg.GetTexturePageBaseY());
if (use_texture_cache)
{
GL_INS("Texpage is in dirty area, checking UV ranges");
m_texpage_dirty = new_texpage_dirty;
m_compute_uv_range = true;
m_current_uv_rect = INVALID_RECT;
texture_mode = BatchTextureMode::PageTexture;
texture_cache_key =
GPUTextureCache::SourceKey(m_draw_mode.mode_reg.texture_page, m_draw_mode.palette_reg, gpu_texture_mode);
const bool is_drawn = GPUTextureCache::IsRectDrawn(page_rect);
if (is_drawn)
GL_INS_FMT("Texpage [{}] {} is drawn in TC, checking UV ranges", texture_cache_key.page, page_rect);
m_texpage_dirty =
(is_drawn ? TEXPAGE_DIRTY_PAGE_RECT : 0) | (m_texture_dumping ? TEXPAGE_DIRTY_ONLY_UV_RECT : 0);
m_compute_uv_range |= ShouldCheckForTexPageOverlap();
}
else
{
m_compute_uv_range = m_clamp_uvs;
if (m_texpage_dirty)
GL_INS("Texpage is no longer dirty");
m_texpage_dirty = 0;
texture_mode = static_cast<BatchTextureMode>(gpu_texture_mode);
m_texpage_dirty = (m_vram_dirty_draw_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_DRAWN_RECT : 0) |
(m_vram_dirty_write_rect.rintersects(page_rect) ? TEXPAGE_DIRTY_WRITTEN_RECT : 0);
if (m_texpage_dirty & TEXPAGE_DIRTY_DRAWN_RECT)
GL_INS_FMT("Texpage {} is in dirty DRAWN area {}", page_rect, m_vram_dirty_draw_rect);
if (m_texpage_dirty & TEXPAGE_DIRTY_WRITTEN_RECT)
GL_INS_FMT("Texpage {} is in dirty WRITTEN area {}", page_rect, m_vram_dirty_write_rect);
// Current UV rect _must_ be cleared here, because we're only check for texpage intersection when it grows in
// size, a switch from a non-contained page to a contained page would go undetected otherwise.
if (m_texpage_dirty != 0)
{
m_compute_uv_range = true;
m_current_uv_rect = INVALID_RECT;
}
}
}
}
texture_mode = (m_draw_mode.mode_reg.texture_mode == GPUTextureMode::Reserved_Direct16Bit) ?
BatchTextureMode::Direct16Bit :
static_cast<BatchTextureMode>(m_draw_mode.mode_reg.texture_mode.GetValue());
}
DebugAssert((rc.IsTexturingEnabled() && (texture_mode == BatchTextureMode::PageTexture &&
texture_cache_key.mode == m_draw_mode.mode_reg.texture_mode) ||
texture_mode == static_cast<BatchTextureMode>(m_draw_mode.mode_reg.texture_mode.GetValue())) ||
(!rc.IsTexturingEnabled() && texture_mode == BatchTextureMode::Disabled));
DebugAssert(!(m_texpage_dirty & TEXPAGE_DIRTY_PAGE_RECT) || texture_mode == BatchTextureMode::PageTexture ||
!rc.IsTexturingEnabled());
// has any state changed which requires a new batch?
// Reverse blending breaks with mixed transparent and opaque pixels, so we have to do one draw per polygon.
@ -3464,12 +3630,16 @@ void GPU_HW::DispatchRenderCommand()
const GPUTransparencyMode transparency_mode =
rc.transparency_enable ? m_draw_mode.mode_reg.transparency_mode : GPUTransparencyMode::Disabled;
const bool dithering_enable = (!m_true_color && rc.IsDitheringEnabled()) ? m_GPUSTAT.dither_enable : false;
if (!IsFlushed())
{
if (texture_mode != m_batch.texture_mode || transparency_mode != m_batch.transparency_mode ||
(transparency_mode == GPUTransparencyMode::BackgroundMinusForeground && !m_allow_shader_blend) ||
dithering_enable != m_batch.dithering)
dithering_enable != m_batch.dithering ||
(texture_mode == BatchTextureMode::PageTexture && m_batch.texture_cache_key != texture_cache_key))
{
FlushRender();
}
}
EnsureVertexBufferSpaceForCurrentCommand();
@ -3512,6 +3682,7 @@ void GPU_HW::DispatchRenderCommand()
m_batch.texture_mode = texture_mode;
m_batch.transparency_mode = transparency_mode;
m_batch.dithering = dithering_enable;
m_batch.texture_cache_key = texture_cache_key;
if (m_draw_mode.IsTextureWindowChanged())
{
@ -3577,10 +3748,21 @@ void GPU_HW::FlushRender()
return;
#ifdef _DEBUG
GL_SCOPE_FMT("Hardware Draw {}", ++s_draw_number);
GL_SCOPE_FMT("Hardware Draw {}: {}", ++s_draw_number, m_current_draw_rect);
#endif
GL_INS_FMT("Dirty draw area: {}", m_vram_dirty_draw_rect);
if (m_compute_uv_range)
GL_INS_FMT("UV rect: {}", m_current_uv_rect);
const GPUTextureCache::Source* texture = nullptr;
if (m_batch.texture_mode == BatchTextureMode::PageTexture)
{
texture = LookupSource(m_batch.texture_cache_key, m_current_uv_rect,
m_batch.transparency_mode != GPUTransparencyMode::Disabled ?
GPUTextureCache::PaletteRecordFlags::HasSemiTransparentDraws :
GPUTextureCache::PaletteRecordFlags::None);
}
if (m_batch_ubo_dirty)
{
@ -3589,21 +3771,24 @@ void GPU_HW::FlushRender()
m_batch_ubo_dirty = false;
}
m_current_draw_rect = INVALID_RECT;
m_current_uv_rect = INVALID_RECT;
if (m_wireframe_mode != GPUWireframeMode::OnlyWireframe)
{
if (NeedsShaderBlending(m_batch.transparency_mode, m_batch.texture_mode, m_batch.check_mask_before_draw) ||
m_rov_active || (m_use_rov_for_shader_blend && m_pgxp_depth_buffer))
{
DrawBatchVertices(BatchRenderMode::ShaderBlend, index_count, base_index, base_vertex);
DrawBatchVertices(BatchRenderMode::ShaderBlend, index_count, base_index, base_vertex, texture);
}
else if (NeedsTwoPassRendering())
{
DrawBatchVertices(BatchRenderMode::OnlyOpaque, index_count, base_index, base_vertex);
DrawBatchVertices(BatchRenderMode::OnlyTransparent, index_count, base_index, base_vertex);
DrawBatchVertices(BatchRenderMode::OnlyOpaque, index_count, base_index, base_vertex, texture);
DrawBatchVertices(BatchRenderMode::OnlyTransparent, index_count, base_index, base_vertex, texture);
}
else
{
DrawBatchVertices(m_batch.GetRenderMode(), index_count, base_index, base_vertex);
DrawBatchVertices(m_batch.GetRenderMode(), index_count, base_index, base_vertex, texture);
}
}
@ -3623,6 +3808,8 @@ void GPU_HW::UpdateDisplay()
GL_SCOPE("UpdateDisplay()");
GPUTextureCache::Compact();
if (g_settings.debugging.show_vram)
{
if (IsUsingMultisampling())

View File

@ -4,7 +4,7 @@
#pragma once
#include "gpu.h"
#include "texture_replacements.h"
#include "gpu_hw_texture_cache.h"
#include "util/gpu_device.h"
@ -38,6 +38,7 @@ public:
Palette4Bit,
Palette8Bit,
Direct16Bit,
PageTexture,
Disabled,
SpritePalette4Bit,
@ -52,6 +53,11 @@ public:
static_cast<u8>(BatchTextureMode::Palette8Bit) == static_cast<u8>(GPUTextureMode::Palette8Bit) &&
static_cast<u8>(BatchTextureMode::Direct16Bit) == static_cast<u8>(GPUTextureMode::Direct16Bit));
static constexpr GSVector4i VRAM_SIZE_RECT = GSVector4i::cxpr(0, 0, VRAM_WIDTH, VRAM_HEIGHT);
static constexpr GSVector4i INVALID_RECT =
GSVector4i::cxpr(std::numeric_limits<s32>::max(), std::numeric_limits<s32>::max(), std::numeric_limits<s32>::min(),
std::numeric_limits<s32>::min());
GPU_HW();
~GPU_HW() override;
@ -83,6 +89,8 @@ private:
{
TEXPAGE_DIRTY_DRAWN_RECT = (1 << 0),
TEXPAGE_DIRTY_WRITTEN_RECT = (1 << 1),
TEXPAGE_DIRTY_PAGE_RECT = (1 << 2),
TEXPAGE_DIRTY_ONLY_UV_RECT = (1 << 3),
};
static_assert(GPUDevice::MIN_TEXEL_BUFFER_ELEMENTS >= (VRAM_WIDTH * VRAM_HEIGHT));
@ -116,6 +124,8 @@ private:
bool use_depth_buffer = false;
bool sprite_mode = false;
GPUTextureCache::SourceKey texture_cache_key = {};
// Returns the render mode for this batch.
BatchRenderMode GetRenderMode() const;
};
@ -140,11 +150,6 @@ private:
u32 num_uniform_buffer_updates;
};
static constexpr GSVector4i VRAM_SIZE_RECT = GSVector4i::cxpr(0, 0, VRAM_WIDTH, VRAM_HEIGHT);
static constexpr GSVector4i INVALID_RECT =
GSVector4i::cxpr(std::numeric_limits<s32>::max(), std::numeric_limits<s32>::max(), std::numeric_limits<s32>::min(),
std::numeric_limits<s32>::min());
/// Returns true if a depth buffer should be created.
GPUTexture::Format GetDepthBufferFormat() const;
@ -169,7 +174,8 @@ private:
void DeactivateROV();
void MapGPUBuffer(u32 required_vertices, u32 required_indices);
void UnmapGPUBuffer(u32 used_vertices, u32 used_indices);
void DrawBatchVertices(BatchRenderMode render_mode, u32 num_indices, u32 base_index, u32 base_vertex);
void DrawBatchVertices(BatchRenderMode render_mode, u32 num_indices, u32 base_index, u32 base_vertex,
const GPUTextureCache::Source* texture);
u32 CalculateResolutionScale() const;
GPUDownsampleMode GetDownsampleMode(u32 resolution_scale) const;
@ -186,6 +192,7 @@ private:
void SetTexPageChangedOnOverlap(const GSVector4i update_rect);
void CheckForTexPageOverlap(GSVector4i uv_rect);
bool ShouldCheckForTexPageOverlap() const;
bool IsFlushed() const;
void EnsureVertexBufferSpace(u32 required_vertices, u32 required_indices);
@ -217,7 +224,7 @@ private:
void UpdateVRAMOnGPU(u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_pitch, bool set_mask,
bool check_mask, const GSVector4i bounds);
bool BlitVRAMReplacementTexture(const TextureReplacements::ReplacementImage* tex, u32 dst_x, u32 dst_y, u32 width,
bool BlitVRAMReplacementTexture(const GPUTextureCache::TextureReplacementImage* tex, u32 dst_x, u32 dst_y, u32 width,
u32 height);
/// Expands a line into two triangles.
@ -290,6 +297,9 @@ private:
bool m_texture_window_active : 1 = false;
bool m_rov_active : 1 = false;
bool m_use_texture_cache : 1 = false;
bool m_texture_dumping : 1 = false;
u8 m_texpage_dirty = 0;
BatchConfig m_batch;
@ -300,8 +310,9 @@ private:
// Bounding box of VRAM area that the GPU has drawn into.
GSVector4i m_vram_dirty_draw_rect = INVALID_RECT;
GSVector4i m_vram_dirty_write_rect = INVALID_RECT;
GSVector4i m_vram_dirty_write_rect = INVALID_RECT; // TODO: Don't use in TC mode, should be kept at zero.
GSVector4i m_current_uv_rect = INVALID_RECT;
GSVector4i m_current_draw_rect = INVALID_RECT;
s32 m_current_texture_page_offset[2] = {};
std::unique_ptr<GPUPipeline> m_wireframe_pipeline;

View File

@ -55,13 +55,14 @@ void GPU_HW_ShaderGen::WriteBatchUniformBuffer(std::stringstream& ss)
false);
}
std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool palette, bool uv_limits,
std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool palette, bool page_texture, bool uv_limits,
bool force_round_texcoords, bool pgxp_depth)
{
std::stringstream ss;
WriteHeader(ss);
DefineMacro(ss, "TEXTURED", textured);
DefineMacro(ss, "PALETTE", palette);
DefineMacro(ss, "PAGE_TEXTURE", page_texture);
DefineMacro(ss, "UV_LIMITS", uv_limits);
DefineMacro(ss, "FORCE_ROUND_TEXCOORDS", force_round_texcoords);
DefineMacro(ss, "PGXP_DEPTH", pgxp_depth);
@ -69,7 +70,22 @@ std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool pale
WriteBatchUniformBuffer(ss);
if (textured)
if (textured && page_texture)
{
if (uv_limits)
{
DeclareVertexEntryPoint(
ss, {"float4 a_pos", "float4 a_col0", "uint a_texcoord", "uint a_texpage", "float4 a_uv_limits"}, 1, 1,
{{"nointerpolation", "float4 v_uv_limits"}}, false, "", UsingMSAA(), UsingPerSampleShading(),
m_disable_color_perspective);
}
else
{
DeclareVertexEntryPoint(ss, {"float4 a_pos", "float4 a_col0", "uint a_texcoord", "uint a_texpage"}, 1, 1, {},
false, "", UsingMSAA(), UsingPerSampleShading(), m_disable_color_perspective);
}
}
else if (textured)
{
if (uv_limits)
{
@ -127,10 +143,11 @@ std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool pale
v_col0 = a_col0;
#if TEXTURED
v_tex0 = float2(uint2(a_texcoord & 0xFFFFu, a_texcoord >> 16));
#if !PALETTE
#if !PALETTE && !PAGE_TEXTURE
v_tex0 *= u_resolution_scale;
#endif
#if !PAGE_TEXTURE
// base_x,base_y,palette_x,palette_y
v_texpage.x = (a_texpage & 15u) * 64u;
v_texpage.y = ((a_texpage >> 4) & 1u) * 256u;
@ -138,6 +155,7 @@ std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool pale
v_texpage.z = ((a_texpage >> 16) & 63u) * 16u;
v_texpage.w = ((a_texpage >> 22) & 511u);
#endif
#endif
#if UV_LIMITS
v_uv_limits = a_uv_limits * 255.0;
@ -146,7 +164,7 @@ std::string GPU_HW_ShaderGen::GenerateBatchVertexShader(bool textured, bool pale
// Add 0.5 to the upper bounds when upscaling, to work around interpolation differences.
// Limited to force-round-texcoord hack, to avoid breaking other games.
v_uv_limits.zw += 0.5;
#elif !PALETTE
#elif !PAGE_TEXTURE && !PALETTE
// Treat coordinates as being in upscaled space, and extend the UV range to all "upscaled"
// pixels. This means 1-pixel-high polygon-based framebuffer effects won't be downsampled.
// (e.g. Mega Man Legends 2 haze effect)
@ -707,6 +725,7 @@ std::string GPU_HW_ShaderGen::GenerateBatchFragmentShader(
const bool textured = (texture_mode != GPU_HW::BatchTextureMode::Disabled);
const bool palette =
(texture_mode == GPU_HW::BatchTextureMode::Palette4Bit || texture_mode == GPU_HW::BatchTextureMode::Palette8Bit);
const bool page_texture = (texture_mode == GPU_HW::BatchTextureMode::PageTexture);
const bool shader_blending = (render_mode == GPU_HW::BatchRenderMode::ShaderBlend);
const bool use_dual_source = (!shader_blending && !use_rov && m_supports_dual_source_blend &&
((render_mode != GPU_HW::BatchRenderMode::TransparencyDisabled &&
@ -725,6 +744,7 @@ std::string GPU_HW_ShaderGen::GenerateBatchFragmentShader(
DefineMacro(ss, "PALETTE", palette);
DefineMacro(ss, "PALETTE_4_BIT", texture_mode == GPU_HW::BatchTextureMode::Palette4Bit);
DefineMacro(ss, "PALETTE_8_BIT", texture_mode == GPU_HW::BatchTextureMode::Palette8Bit);
DefineMacro(ss, "PAGE_TEXTURE", page_texture);
DefineMacro(ss, "DITHERING", dithering);
DefineMacro(ss, "DITHERING_SCALED", m_scaled_dithering);
DefineMacro(ss, "INTERLACING", interlacing);
@ -804,6 +824,8 @@ uint2 FloatToIntegerCoords(float2 coords)
return uint2((UPSCALED == 0 || FORCE_ROUND_TEXCOORDS != 0) ? roundEven(coords) : floor(coords));
}
#if !PAGE_TEXTURE
float4 SampleFromVRAM(TEXPAGE_VALUE texpage, float2 coords)
{
#if PALETTE
@ -855,11 +877,43 @@ float4 SampleFromVRAM(TEXPAGE_VALUE texpage, float2 coords)
#endif
}
#else
float4 SampleFromPageTexture(float2 coords)
{
// Cached textures.
#if UPSCALED == 0 || FORCE_ROUND_TEXCOORDS != 0
float2 fpart = coords - roundEven(coords);
#else
float2 fpart = frac(coords);
#endif
uint2 icoord = ApplyTextureWindow(FloatToIntegerCoords(coords));
coords = (float2(icoord) + fpart) * (1.0f / 256.0f);
return SAMPLE_TEXTURE(samp0, coords);
}
#endif
#endif // TEXTURED
)";
const u32 num_fragment_outputs = use_rov ? 0 : (use_dual_source ? 2 : 1);
if (textured)
if (textured && page_texture)
{
if (uv_limits)
{
DeclareFragmentEntryPoint(ss, 1, 1, {{"nointerpolation", "float4 v_uv_limits"}}, true, num_fragment_outputs,
use_dual_source, m_write_mask_as_depth, UsingMSAA(), UsingPerSampleShading(), false,
m_disable_color_perspective, shader_blending && !use_rov, use_rov);
}
else
{
DeclareFragmentEntryPoint(ss, 1, 1, {}, true, num_fragment_outputs, use_dual_source, m_write_mask_as_depth,
UsingMSAA(), UsingPerSampleShading(), false, m_disable_color_perspective,
shader_blending && !use_rov, use_rov);
}
}
else if (textured)
{
if (texture_filtering != GPUTextureFilter::Nearest)
WriteBatchTextureFilter(ss, texture_filtering);
@ -905,7 +959,17 @@ float4 SampleFromVRAM(TEXPAGE_VALUE texpage, float2 coords)
#if TEXTURED
float4 texcol;
#if TEXTURE_FILTERING
#if PAGE_TEXTURE
#if UV_LIMITS
texcol = SampleFromPageTexture(clamp(v_tex0, v_uv_limits.xy, v_uv_limits.zw));
#else
texcol = SampleFromPageTexture(v_tex0);
#endif
if (VECTOR_EQ(texcol, TRANSPARENT_PIXEL_COLOR))
discard;
ialpha = 1.0;
#elif TEXTURE_FILTERING
FilteredSampleFromVRAM(v_texpage, v_tex0, v_uv_limits, texcol, ialpha);
if (ialpha < 0.5)
discard;
@ -1687,3 +1751,33 @@ std::string GPU_HW_ShaderGen::GenerateBoxSampleDownsampleFragmentShader(u32 fact
return ss.str();
}
std::string GPU_HW_ShaderGen::GenerateReplacementMergeFragmentShader(bool semitransparent)
{
std::stringstream ss;
WriteHeader(ss);
DefineMacro(ss, "SEMITRANSPARENT", semitransparent);
DeclareUniformBuffer(ss, {"float4 u_src_rect"}, true);
DeclareTexture(ss, "samp0", 0);
DeclareFragmentEntryPoint(ss, 0, 1);
ss << R"(
{
float2 coords = u_src_rect.xy + v_tex0 * u_src_rect.zw;
float4 color = SAMPLE_TEXTURE(samp0, coords);
o_col0.rgb = color.rgb;
// Alpha processing.
#if SEMITRANSPARENT
// Map anything not 255 to 1 for semitransparent, otherwise zero for opaque.
o_col0.a = (color.a <= 0.95f) ? 1.0f : 0.0f;
o_col0.a = VECTOR_EQ(color, float4(0.0, 0.0, 0.0, 0.0)) ? 0.0f : o_col0.a;
#else
// Leave (0,0,0,0) as 0000 for opaque replacements for cutout alpha.
o_col0.a = color.a;
#endif
}
)";
return ss.str();
}

View File

@ -15,8 +15,8 @@ public:
bool supports_dual_source_blend, bool supports_framebuffer_fetch);
~GPU_HW_ShaderGen();
std::string GenerateBatchVertexShader(bool textured, bool palette, bool uv_limits, bool force_round_texcoords,
bool pgxp_depth);
std::string GenerateBatchVertexShader(bool textured, bool palette, bool page_texture, bool uv_limits,
bool force_round_texcoords, bool pgxp_depth);
std::string GenerateBatchFragmentShader(GPU_HW::BatchRenderMode render_mode, GPUTransparencyMode transparency,
GPU_HW::BatchTextureMode texture_mode, GPUTextureFilter texture_filtering,
bool uv_limits, bool force_round_texcoords, bool dithering, bool interlacing,
@ -36,6 +36,8 @@ public:
std::string GenerateAdaptiveDownsampleCompositeFragmentShader();
std::string GenerateBoxSampleDownsampleFragmentShader(u32 factor);
std::string GenerateReplacementMergeFragmentShader(bool semitransparent);
private:
ALWAYS_INLINE bool UsingMSAA() const { return m_multisamples > 1; }
ALWAYS_INLINE bool UsingPerSampleShading() const { return m_multisamples > 1 && m_per_sample_shading; }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "gpu_types.h"
class GPUTexture;
class RGBA8Image;
class StateWrapper;
struct Settings;
//////////////////////////////////////////////////////////////////////////
// Texture Cache
//////////////////////////////////////////////////////////////////////////
namespace GPUTextureCache {
/// 4 pages in C16 mode, 2+4 pages in P8 mode, 1+1 pages in P4 mode.
static constexpr u32 MAX_PAGE_REFS_PER_SOURCE = 6;
static constexpr u32 MAX_PAGE_REFS_PER_WRITE = 32;
enum class PaletteRecordFlags : u32
{
None = 0,
HasSemiTransparentDraws = (1 << 0),
};
IMPLEMENT_ENUM_CLASS_BITWISE_OPERATORS(PaletteRecordFlags);
using HashType = u64;
using TextureReplacementImage = RGBA8Image;
struct Source;
struct HashCacheEntry;
template<typename T>
struct TList;
template<typename T>
struct TListNode;
template<typename T>
struct TList
{
TListNode<T>* head;
TListNode<T>* tail;
};
template<typename T>
struct TListNode
{
// why inside itself? because we have 3 lists
T* ref;
TList<T>* list;
TListNode<T>* prev;
TListNode<T>* next;
};
struct SourceKey
{
u8 page;
GPUTextureMode mode;
GPUTexturePaletteReg palette;
SourceKey() = default;
ALWAYS_INLINE constexpr SourceKey(u8 page_, GPUTexturePaletteReg palette_, GPUTextureMode mode_)
: page(page_), mode(mode_), palette(palette_)
{
}
ALWAYS_INLINE constexpr SourceKey(const SourceKey& k) : page(k.page), mode(k.mode), palette(k.palette) {}
ALWAYS_INLINE bool HasPalette() const { return (mode < GPUTextureMode::Direct16Bit); }
ALWAYS_INLINE SourceKey& operator=(const SourceKey& k)
{
page = k.page;
mode = k.mode;
palette.bits = k.palette.bits;
return *this;
}
ALWAYS_INLINE bool operator==(const SourceKey& k) const { return (std::memcmp(&k, this, sizeof(SourceKey)) == 0); }
ALWAYS_INLINE bool operator!=(const SourceKey& k) const { return (std::memcmp(&k, this, sizeof(SourceKey)) != 0); }
};
static_assert(sizeof(SourceKey) == 4);
// TODO: Pool objects
struct Source
{
SourceKey key;
u32 num_page_refs;
GPUTexture* texture;
HashCacheEntry* from_hash_cache;
GSVector4i texture_rect;
GSVector4i palette_rect;
HashType texture_hash;
HashType palette_hash;
GSVector4i active_uv_rect;
PaletteRecordFlags palette_record_flags;
std::array<TListNode<Source>, MAX_PAGE_REFS_PER_SOURCE> page_refs;
TListNode<Source> hash_cache_ref;
};
bool Initialize();
void UpdateSettings(const Settings& old_settings);
bool DoState(StateWrapper& sw, bool skip);
void Shutdown();
void Invalidate();
void AddWrittenRectangle(const GSVector4i rect, bool update_vram_writes = false);
void AddDrawnRectangle(const GSVector4i rect, const GSVector4i clip_rect);
void CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32 height, bool set_mask, bool check_mask,
const GSVector4i src_bounds, const GSVector4i dst_bounds);
void WriteVRAM(u32 x, u32 y, u32 width, u32 height, const void* data, bool set_mask, bool check_mask,
const GSVector4i bounds);
void UpdateVRAMTrackingState();
const Source* LookupSource(SourceKey key, const GSVector4i uv_rect, PaletteRecordFlags flags);
bool IsPageDrawn(u32 page_index);
bool IsPageDrawn(u32 page_index, const GSVector4i rect);
bool IsRectDrawn(const GSVector4i rect);
bool AreSourcePagesDrawn(SourceKey key, const GSVector4i rect);
void InvalidatePageSources(u32 pn);
void InvalidatePageSources(u32 pn, const GSVector4i rc);
void DestroySource(Source* src);
void Compact();
void DecodeTexture(GPUTextureMode mode, const u16* page_ptr, const u16* palette, u32* dest, u32 dest_stride, u32 width,
u32 height);
HashType HashPartialPalette(GPUTexturePaletteReg palette, GPUTextureMode mode, u32 min, u32 max);
HashType HashRect(const GSVector4i rc);
void SetGameID(std::string game_id);
void ReloadTextureReplacements();
// VRAM Write Replacements
const TextureReplacementImage* GetVRAMReplacement(u32 width, u32 height, const void* pixels);
void DumpVRAMWrite(u32 width, u32 height, const void* pixels);
bool ShouldDumpVRAMWrite(u32 width, u32 height);
} // namespace GPUTextureCache

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gpu_sw.h"
#include "gpu_hw_texture_cache.h"
#include "settings.h"
#include "system.h"
@ -70,7 +71,11 @@ bool GPU_SW::DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_di
m_backend.Sync(true);
// ignore the host texture for software mode, since we want to save vram here
return GPU::DoState(sw, nullptr, update_display);
if (!GPU::DoState(sw, nullptr, update_display))
return false;
// need to still call the TC, to toss any data in the state
return GPUTextureCache::DoState(sw, true);
}
void GPU_SW::Reset(bool clear_vram)

View File

@ -26,7 +26,15 @@ enum : u32
GPU_MAX_DISPLAY_WIDTH = 720,
GPU_MAX_DISPLAY_HEIGHT = 576,
DITHER_MATRIX_SIZE = 4
DITHER_MATRIX_SIZE = 4,
VRAM_PAGE_WIDTH = 64,
VRAM_PAGE_HEIGHT = 256,
VRAM_PAGES_WIDE = VRAM_WIDTH / VRAM_PAGE_WIDTH,
VRAM_PAGES_HIGH = VRAM_HEIGHT / VRAM_PAGE_HEIGHT,
VRAM_PAGE_X_MASK = 0xf, // 16 pages wide
VRAM_PAGE_Y_MASK = 0x10, // 2 pages high
NUM_VRAM_PAGES = VRAM_PAGES_WIDE * VRAM_PAGES_HIGH,
};
enum : s32
@ -61,6 +69,11 @@ enum class GPUTextureMode : u8
IMPLEMENT_ENUM_CLASS_BITWISE_OPERATORS(GPUTextureMode);
ALWAYS_INLINE static constexpr bool TextureModeHasPalette(GPUTextureMode mode)
{
return (mode < GPUTextureMode::Direct16Bit);
}
enum class GPUTransparencyMode : u8
{
HalfBackgroundPlusHalfForeground = 0,
@ -169,7 +182,7 @@ static constexpr s32 TruncateGPUVertexPosition(s32 x)
union GPUDrawModeReg
{
static constexpr u16 MASK = 0b1111111111111;
static constexpr u16 TEXTURE_PAGE_MASK = UINT16_C(0b0000000000011111);
static constexpr u16 TEXTURE_MODE_AND_PAGE_MASK = UINT16_C(0b0000000110011111);
// Polygon texpage commands only affect bits 0-8, 11
static constexpr u16 POLYGON_TEXPAGE_MASK = 0b0000100111111111;
@ -177,11 +190,9 @@ union GPUDrawModeReg
// Bits 0..5 are returned in the GPU status register, latched at E1h/polygon draw time.
static constexpr u32 GPUSTAT_MASK = 0b11111111111;
static constexpr std::array<u32, 4> texture_page_widths = {
{TEXTURE_PAGE_WIDTH / 4, TEXTURE_PAGE_WIDTH / 2, TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_WIDTH}};
u16 bits;
BitField<u16, u8, 0, 5> texture_page;
BitField<u16, u8, 0, 4> texture_page_x_base;
BitField<u16, u8, 4, 1> texture_page_y_base;
BitField<u16, GPUTransparencyMode, 5, 2> transparency_mode;
@ -197,15 +208,6 @@ union GPUDrawModeReg
/// Returns true if the texture mode requires a palette.
ALWAYS_INLINE bool IsUsingPalette() const { return (bits & (2 << 7)) == 0; }
/// Returns a rectangle comprising the texture page area.
ALWAYS_INLINE_RELEASE GSVector4i GetTexturePageRectangle() const
{
const u32 base_x = GetTexturePageBaseX();
const u32 base_y = GetTexturePageBaseY();
return GSVector4i(base_x, base_y, base_x + texture_page_widths[static_cast<u8>(texture_mode.GetValue())],
base_y + TEXTURE_PAGE_HEIGHT);
}
};
union GPUTexturePaletteReg
@ -217,17 +219,8 @@ union GPUTexturePaletteReg
BitField<u16, u16, 0, 6> x;
BitField<u16, u16, 6, 9> y;
ALWAYS_INLINE u32 GetXBase() const { return static_cast<u32>(x) * 16u; }
ALWAYS_INLINE u32 GetYBase() const { return static_cast<u32>(y); }
/// Returns a rectangle comprising the texture palette area.
ALWAYS_INLINE_RELEASE GSVector4i GetRectangle(GPUTextureMode mode) const
{
static constexpr std::array<u32, 4> palette_widths = {{16, 256, 0, 0}};
const u32 base_x = GetXBase();
const u32 base_y = GetYBase();
return GSVector4i(base_x, base_y, base_x + palette_widths[static_cast<u8>(mode)], base_y + 1);
}
ALWAYS_INLINE constexpr u32 GetXBase() const { return static_cast<u32>(x) * 16u; }
ALWAYS_INLINE constexpr u32 GetYBase() const { return static_cast<u32>(y); }
};
struct GPUTextureWindow
@ -238,6 +231,119 @@ struct GPUTextureWindow
u8 or_y;
};
ALWAYS_INLINE static constexpr u32 VRAMPageIndex(u32 px, u32 py)
{
return ((py * VRAM_PAGES_WIDE) + px);
}
ALWAYS_INLINE static constexpr GSVector4i VRAMPageRect(u32 px, u32 py)
{
return GSVector4i::cxpr(px * VRAM_PAGE_WIDTH, py * VRAM_PAGE_HEIGHT, (px + 1) * VRAM_PAGE_WIDTH,
(py + 1) * VRAM_PAGE_HEIGHT);
}
ALWAYS_INLINE static constexpr GSVector4i VRAMPageRect(u32 pn)
{
// TODO: Put page rects in a LUT instead?
return VRAMPageRect(pn % VRAM_PAGES_WIDE, pn / VRAM_PAGES_WIDE);
}
ALWAYS_INLINE static constexpr u32 VRAMCoordinateToPage(u32 x, u32 y)
{
return VRAMPageIndex(x / VRAM_PAGE_WIDTH, y / VRAM_PAGE_HEIGHT);
}
ALWAYS_INLINE static constexpr u32 VRAMPageStartX(u32 pn)
{
return (pn % VRAM_PAGES_WIDE) * VRAM_PAGE_WIDTH;
}
ALWAYS_INLINE static constexpr u32 VRAMPageStartY(u32 pn)
{
return (pn / VRAM_PAGES_WIDE) * VRAM_PAGE_HEIGHT;
}
ALWAYS_INLINE static constexpr u8 GetTextureModeShift(GPUTextureMode mode)
{
return ((mode < GPUTextureMode::Direct16Bit) ? (2 - static_cast<u8>(mode)) : 0);
}
ALWAYS_INLINE static constexpr u32 ApplyTextureModeShift(GPUTextureMode mode, u32 vram_width)
{
return vram_width << GetTextureModeShift(mode);
}
ALWAYS_INLINE static GSVector4i ApplyTextureModeShift(GPUTextureMode mode, const GSVector4i rect)
{
return rect.sll32(GetTextureModeShift(mode));
}
ALWAYS_INLINE static constexpr u32 TexturePageCountForMode(GPUTextureMode mode)
{
return ((mode < GPUTextureMode::Direct16Bit) ? (1 + static_cast<u8>(mode)) : 4);
}
ALWAYS_INLINE static constexpr u32 TexturePageWidthForMode(GPUTextureMode mode)
{
return TEXTURE_PAGE_WIDTH >> GetTextureModeShift(mode);
}
ALWAYS_INLINE static constexpr bool TexturePageIsWrapping(GPUTextureMode mode, u32 pn)
{
return ((VRAMPageStartX(pn) + TexturePageWidthForMode(mode)) > VRAM_WIDTH);
}
ALWAYS_INLINE static constexpr u32 PalettePageCountForMode(GPUTextureMode mode)
{
return (mode == GPUTextureMode::Palette4Bit) ? 1 : 4;
}
ALWAYS_INLINE static constexpr u32 PalettePageNumber(GPUTexturePaletteReg reg)
{
return VRAMCoordinateToPage(reg.GetXBase(), reg.GetYBase());
}
ALWAYS_INLINE static constexpr GSVector4i GetTextureRect(u32 pn, GPUTextureMode mode)
{
u32 left = VRAMPageStartX(pn);
u32 top = VRAMPageStartY(pn);
u32 right = left + TexturePageWidthForMode(mode);
u32 bottom = top + VRAM_PAGE_HEIGHT;
if (right > VRAM_WIDTH) [[unlikely]]
{
left = 0;
right = VRAM_WIDTH;
}
if (bottom > VRAM_HEIGHT) [[unlikely]]
{
top = 0;
bottom = VRAM_HEIGHT;
}
return GSVector4i::cxpr(left, top, right, bottom);
}
/// Returns the maximum index for a paletted texture.
ALWAYS_INLINE static constexpr u32 GetPaletteWidth(GPUTextureMode mode)
{
return (mode == GPUTextureMode::Palette4Bit ? 16 : ((mode == GPUTextureMode::Palette8Bit) ? 256 : 0));
}
/// Returns a rectangle comprising the texture palette area.
ALWAYS_INLINE static constexpr GSVector4i GetPaletteRect(GPUTexturePaletteReg palette, GPUTextureMode mode,
bool clamp_instead_of_wrapping = false)
{
const u32 width = GetPaletteWidth(mode);
u32 left = palette.GetXBase();
u32 top = palette.GetYBase();
u32 right = left + width;
u32 bottom = top + 1;
if (right > VRAM_WIDTH) [[unlikely]]
{
right = VRAM_WIDTH;
left = clamp_instead_of_wrapping ? left : 0;
}
return GSVector4i::cxpr(left, top, right, bottom);
}
// 4x4 dither matrix.
static constexpr s32 DITHER_MATRIX[DITHER_MATRIX_SIZE][DITHER_MATRIX_SIZE] = {{-4, +0, -3, +1}, // row 0
{+2, -2, +3, -1}, // row 1

View File

@ -7,12 +7,12 @@
#include "cpu_pgxp.h"
#include "fullscreen_ui.h"
#include "gpu.h"
#include "gpu_hw_texture_cache.h"
#include "host.h"
#include "imgui_overlays.h"
#include "settings.h"
#include "spu.h"
#include "system.h"
#include "texture_replacements.h"
#include "util/gpu_device.h"
#include "util/input_manager.h"
@ -22,8 +22,8 @@
#include "common/file_system.h"
#include "common/timer.h"
#include "IconsFontAwesome5.h"
#include "IconsEmoji.h"
#include "IconsFontAwesome5.h"
#include "fmt/format.h"
#include <cmath>
@ -441,7 +441,7 @@ DEFINE_HOTKEY("ReloadTextureReplacements", TRANSLATE_NOOP("Hotkeys", "Graphics")
{
Host::AddKeyedOSDMessage("ReloadTextureReplacements",
TRANSLATE_STR("OSDMessage", "Texture replacements reloaded."), 10.0f);
TextureReplacements::Reload();
GPUTextureCache::ReloadTextureReplacements();
}
})
@ -554,7 +554,8 @@ DEFINE_HOTKEY("AudioCDAudioMute", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_
{
g_settings.cdrom_mute_cd_audio = !g_settings.cdrom_mute_cd_audio;
Host::AddIconOSDMessage(
"AudioControlHotkey", g_settings.cdrom_mute_cd_audio ? ICON_EMOJI_MUTED_SPEAKER : ICON_EMOJI_MEDIUM_VOLUME_SPEAKER,
"AudioControlHotkey",
g_settings.cdrom_mute_cd_audio ? ICON_EMOJI_MUTED_SPEAKER : ICON_EMOJI_MEDIUM_VOLUME_SPEAKER,
g_settings.cdrom_mute_cd_audio ? TRANSLATE_STR("OSDMessage", "CD Audio Muted.") :
TRANSLATE_STR("OSDMessage", "CD Audio Unmuted."),
2.0f);

View File

@ -6,7 +6,7 @@
#include "common/types.h"
static constexpr u32 SAVE_STATE_MAGIC = 0x43435544;
static constexpr u32 SAVE_STATE_VERSION = 72;
static constexpr u32 SAVE_STATE_VERSION = 73;
static constexpr u32 SAVE_STATE_MINIMUM_VERSION = 42;
static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION);

View File

@ -228,6 +228,7 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
si.GetStringValue("GPU", "ForceVideoTiming", GetForceVideoTimingName(DEFAULT_FORCE_VIDEO_TIMING_MODE)).c_str())
.value_or(DEFAULT_FORCE_VIDEO_TIMING_MODE);
gpu_widescreen_hack = si.GetBoolValue("GPU", "WidescreenHack", false);
gpu_texture_cache = si.GetBoolValue("GPU", "EnableTextureCache", false);
display_24bit_chroma_smoothing = si.GetBoolValue("GPU", "ChromaSmoothing24Bit", false);
gpu_pgxp_enable = si.GetBoolValue("GPU", "PGXPEnable", false);
gpu_pgxp_culling = si.GetBoolValue("GPU", "PGXPCulling", true);
@ -433,16 +434,43 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
debugging.show_mdec_state = si.GetBoolValue("Debug", "ShowMDECState");
debugging.show_dma_state = si.GetBoolValue("Debug", "ShowDMAState");
texture_replacements.enable_texture_replacements =
si.GetBoolValue("TextureReplacements", "EnableTextureReplacements", false);
texture_replacements.enable_vram_write_replacements =
si.GetBoolValue("TextureReplacements", "EnableVRAMWriteReplacements", false);
texture_replacements.preload_textures = si.GetBoolValue("TextureReplacements", "PreloadTextures", false);
texture_replacements.dump_textures = si.GetBoolValue("TextureReplacements", "DumpTextures", false);
texture_replacements.dump_replaced_textures = si.GetBoolValue("TextureReplacements", "DumpReplacedTextures", true);
texture_replacements.dump_vram_writes = si.GetBoolValue("TextureReplacements", "DumpVRAMWrites", false);
texture_replacements.dump_vram_write_force_alpha_channel =
texture_replacements.config.dump_texture_pages = si.GetBoolValue("TextureReplacements", "DumpTexturePages", false);
texture_replacements.config.dump_full_texture_pages =
si.GetBoolValue("TextureReplacements", "DumpFullTexturePages", false);
texture_replacements.config.dump_texture_force_alpha_channel =
si.GetBoolValue("TextureReplacements", "DumpTextureForceAlphaChannel", false);
texture_replacements.config.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);
texture_replacements.config.dump_c16_textures = si.GetBoolValue("TextureReplacements", "DumpC16Textures", false);
texture_replacements.config.reduce_palette_range = si.GetBoolValue("TextureReplacements", "ReducePaletteRange", true);
texture_replacements.config.convert_copies_to_writes =
si.GetBoolValue("TextureReplacements", "ConvertCopiesToWrites", false);
texture_replacements.config.replacement_scale_linear_filter =
si.GetBoolValue("TextureReplacements", "ReplacementScaleLinearFilter", true);
texture_replacements.config.max_vram_write_splits = si.GetUIntValue("TextureReplacements", "MaxVRAMWriteSplits", 0u);
texture_replacements.config.max_vram_write_coalesce_width =
si.GetUIntValue("TextureReplacements", "MaxVRAMWriteCoalesceWidth", 0u);
texture_replacements.config.max_vram_write_coalesce_height =
si.GetUIntValue("TextureReplacements", "MaxVRAMWriteCoalesceHeight", 0u);
texture_replacements.config.texture_dump_width_threshold =
si.GetUIntValue("TextureReplacements", "DumpTextureWidthThreshold", 16);
texture_replacements.config.texture_dump_height_threshold =
si.GetUIntValue("TextureReplacements", "DumpTextureHeightThreshold", 16);
texture_replacements.config.vram_write_dump_width_threshold =
si.GetUIntValue("TextureReplacements", "DumpVRAMWriteWidthThreshold", 128);
texture_replacements.config.vram_write_dump_height_threshold =
si.GetUIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold", 128);
#ifdef __ANDROID__
// Android users are incredibly silly and don't understand that stretch is in the aspect ratio list...
@ -526,6 +554,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetStringValue("GPU", "WireframeMode", GetGPUWireframeModeName(gpu_wireframe_mode));
si.SetStringValue("GPU", "ForceVideoTiming", GetForceVideoTimingName(gpu_force_video_timing));
si.SetBoolValue("GPU", "WidescreenHack", gpu_widescreen_hack);
si.SetBoolValue("GPU", "EnableTextureCache", gpu_texture_cache);
si.SetBoolValue("GPU", "ChromaSmoothing24Bit", display_24bit_chroma_smoothing);
si.SetBoolValue("GPU", "PGXPEnable", gpu_pgxp_enable);
si.SetBoolValue("GPU", "PGXPCulling", gpu_pgxp_culling);
@ -677,16 +706,41 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetBoolValue("Debug", "ShowDMAState", debugging.show_dma_state);
}
si.SetBoolValue("TextureReplacements", "EnableTextureReplacements", texture_replacements.enable_texture_replacements);
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", "DumpTextures", texture_replacements.dump_textures);
si.SetBoolValue("TextureReplacements", "DumpReplacedTextures", texture_replacements.dump_replaced_textures);
si.SetBoolValue("TextureReplacements", "DumpTexturePages", texture_replacements.config.dump_texture_pages);
si.SetBoolValue("TextureReplacements", "DumpFullTexturePages", texture_replacements.config.dump_full_texture_pages);
si.SetBoolValue("TextureReplacements", "DumpTextureForceAlphaChannel",
texture_replacements.config.dump_texture_force_alpha_channel);
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);
texture_replacements.config.dump_vram_write_force_alpha_channel);
si.SetBoolValue("TextureReplacements", "DumpC16Textures", texture_replacements.config.dump_c16_textures);
si.SetBoolValue("TextureReplacements", "ReducePaletteRange", texture_replacements.config.reduce_palette_range);
si.SetBoolValue("TextureReplacements", "ConvertCopiesToWrites", texture_replacements.config.convert_copies_to_writes);
si.SetBoolValue("TextureReplacements", "ReplacementScaleLinearFilter",
texture_replacements.config.replacement_scale_linear_filter);
si.SetUIntValue("TextureReplacements", "MaxVRAMWriteSplits", texture_replacements.config.max_vram_write_splits);
si.SetUIntValue("TextureReplacements", "MaxVRAMWriteCoalesceWidth",
texture_replacements.config.max_vram_write_coalesce_width);
si.GetUIntValue("TextureReplacements", "MaxVRAMWriteCoalesceHeight",
texture_replacements.config.max_vram_write_coalesce_height);
si.SetUIntValue("TextureReplacements", "DumpTextureWidthThreshold",
texture_replacements.config.texture_dump_width_threshold);
si.SetUIntValue("TextureReplacements", "DumpTextureHeightThreshold",
texture_replacements.config.texture_dump_height_threshold);
si.SetUIntValue("TextureReplacements", "DumpVRAMWriteWidthThreshold",
texture_replacements.config.vram_write_dump_width_threshold);
si.SetUIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold",
texture_replacements.config.vram_write_dump_height_threshold);
}
void Settings::Clear(SettingsInterface& si)
@ -716,6 +770,146 @@ void Settings::Clear(SettingsInterface& si)
si.ClearSection("TextureReplacements");
}
bool Settings::TextureReplacementSettings::Configuration::operator==(const Configuration& rhs) const
{
return (dump_texture_pages == rhs.dump_texture_pages && dump_full_texture_pages == rhs.dump_full_texture_pages &&
dump_texture_force_alpha_channel == rhs.dump_texture_force_alpha_channel &&
dump_vram_write_force_alpha_channel == rhs.dump_vram_write_force_alpha_channel &&
dump_c16_textures == rhs.dump_c16_textures && reduce_palette_range == rhs.reduce_palette_range &&
convert_copies_to_writes == rhs.convert_copies_to_writes &&
replacement_scale_linear_filter == rhs.replacement_scale_linear_filter &&
max_vram_write_splits == rhs.max_vram_write_splits &&
max_vram_write_coalesce_width == rhs.max_vram_write_coalesce_width &&
max_vram_write_coalesce_height == rhs.max_vram_write_coalesce_height &&
texture_dump_width_threshold == rhs.texture_dump_width_threshold &&
texture_dump_height_threshold == rhs.texture_dump_height_threshold &&
vram_write_dump_width_threshold == rhs.vram_write_dump_width_threshold &&
vram_write_dump_height_threshold == rhs.vram_write_dump_height_threshold);
}
bool Settings::TextureReplacementSettings::Configuration::operator!=(const Configuration& rhs) const
{
return !operator==(rhs);
}
bool Settings::TextureReplacementSettings::operator==(const TextureReplacementSettings& rhs) const
{
return (enable_texture_replacements == rhs.enable_texture_replacements &&
enable_vram_write_replacements == rhs.enable_vram_write_replacements &&
preload_textures == rhs.preload_textures && dump_textures == rhs.dump_textures &&
dump_replaced_textures == rhs.dump_replaced_textures && dump_vram_writes == rhs.dump_vram_writes &&
config == rhs.config);
}
bool Settings::TextureReplacementSettings::operator!=(const TextureReplacementSettings& rhs) const
{
return !operator==(rhs);
}
std::string Settings::TextureReplacementSettings::Configuration::ExportToYAML(bool comment) const
{
static constexpr const char CONFIG_TEMPLATE[] = R"(# DuckStation Texture Replacement Configuration
# This file allows you to set a per-game configuration for the dumping and
# replacement system, avoiding the need to use the normal per-game settings
# when moving files to a different computer. It also allows for the definition
# of texture aliases, for reducing duplicate files.
#
# All options are commented out by default. If an option is commented, the user's
# current setting will be used instead. If an option is defined in this file, it
# will always take precedence over the user's choice.
# Enables texture page dumping mode.
# Instead of tracking VRAM writes and attempting to identify the "real" size of
# textures, create sub-rectangles from pages based on how they are drawn. In
# most games, this will lead to significant duplication in dumps, and reduce
# replacement reliability. However, some games are incompatible with write
# tracking, and must use page mode.
{}DumpTexturePages: {}
# Dumps full texture pages instead of sub-rectangles.
# 256x256 pages will be dumped/replaced instead.
{}DumpFullTexturePages: {}
# Enables the dumping of direct textures (i.e. C16 format).
# Most games do not use direct textures, and when they do, it is usually for
# post-processing or FMVs. Ignoring C16 textures typically reduces garbage/false
# positive texture dumps, however, some games may require it.
{}DumpC16Textures: {}
# Reduces the size of palettes (i.e. CLUTs) to only those indices that are used.
# This can help reduce duplication and improve replacement reliability in games
# that use 8-bit textures, but do not reserve or use the full 1x256 region in
# video memory for storage of the palette. When replacing textures dumped with
# this option enabled, CPU usage on the GPU thread does increase trivially,
# however, generally it is worthwhile for the reliability improvement. Games
# that require this option include Metal Gear Solid.
{}ReducePaletteRange: {}
# Converts VRAM copies to VRAM writes, when a copy of performed into a previously
# tracked VRAM write. This is required for some games that construct animated
# textures by copying and replacing small portions of the texture with the parts
# that are animated. Generally this option will cause duplication when dumping,
# but it is required in some games, such as Final Fantasy VIII.
{}ConvertCopiesToWrites: {}
# Determines the maximum number of times a VRAM write/upload can be split, before
# it is discarded and no longer tracked. This is required for games that partially
# overwrite texture data, such as Gran Turismo.
{}MaxVRAMWriteSplits: {}
# Determines the maximum size of an incoming VRAM write that will be merged with
# another write to the left/above of the incoming write. Needed for games that
# upload textures one line at a time. These games will log "tracking VRAM write
# of Nx1" repeatedly during loading. If the upload size is 1, then you can set
# the corresponding maximum coalesce dimension to 1 to merge these uploads,
# which should enable these textures to be dumped/replaced.
{}MaxVRAMWriteCoalesceWidth: {}
{}MaxVRAMWriteCoalesceHeight: {}
# Determines the minimum size of a texture that will be dumped. Textures with a
# width/height smaller than this value will be ignored.
{}DumpTextureWidthThreshold: {}
{}DumpTextureHeightThreshold: {}
# Determines the minimum size of a VRAM write that will be dumped, in background
# dumping mode. Uploads smaller than this size will be ignored.
{}DumpVRAMWriteWidthThreshold: {}
{}DumpVRAMWriteHeightThreshold: {}
# Enables the use of a bilinear filter when scaling replacement textures.
# If more than one replacement texture in a 256x256 texture page has a different
# scaling over the native resolution, or the texture page is not covered, a
# bilinear filter will be used to resize/stretch the replacement texture, and/or
# the original native data.
{}ReplacementScaleLinearFilter: {}
# Use this section to define replacement aliases. One line per replacement
# texture, with the key set to the source ID, and the value set to the filename
# which should be loaded as a replacement. For example, without the newline,
# or keep the multi-line separator.
#Aliases:
# Alias-Texture-Name: Path-To-Texture
# texupload-P4-AAAAAAAAAAAAAAAA-BBBBBBBBBBBBBBBB-64x256-0-192-64x64-P0-14: |
# texupload-P4-BBBBBBBBBBBBBBBB-BBBBBBBBBBBBBBBB-64x256-0-64-64x64-P0-13.png
# texupload-P4-AAAAAAAAAAAAAAAA-BBBBBBBBBBBBBBBB-64x256-0-192-64x64-P0-14: mytexture.png
)";
const std::string_view comment_str = comment ? "#" : "";
return fmt::format(CONFIG_TEMPLATE, comment_str, dump_texture_pages, // DumpTexturePages
comment_str, dump_full_texture_pages, // DumpFullTexturePages
comment_str, dump_c16_textures, // DumpC16Textures
comment_str, reduce_palette_range, // ReducePaletteRange
comment_str, convert_copies_to_writes, // ConvertCopiesToWrites
comment_str, max_vram_write_splits, // MaxVRAMWriteSplits
comment_str, max_vram_write_coalesce_width, // MaxVRAMWriteCoalesceWidth
comment_str, max_vram_write_coalesce_height, // MaxVRAMWriteCoalesceHeight
comment_str, texture_dump_width_threshold, // DumpTextureWidthThreshold
comment_str, texture_dump_height_threshold, // DumpTextureHeightThreshold
comment_str, vram_write_dump_width_threshold, // DumpVRAMWriteWidthThreshold
comment_str, vram_write_dump_height_threshold, // DumpVRAMWriteHeightThreshold
comment_str, replacement_scale_linear_filter); // ReplacementScaleLinearFilter
}
void Settings::FixIncompatibleSettings(bool display_osd_messages)
{
if (g_settings.disable_all_enhancements)
@ -2043,7 +2237,6 @@ bool EmuFolders::EnsureFoldersExist()
result = FileSystem::EnsureDirectoryExists(Covers.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Dumps.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Path::Combine(Dumps, "audio").c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Path::Combine(Dumps, "textures").c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(GameIcons.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(GameSettings.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(InputProfiles.c_str(), false) && result;

View File

@ -117,6 +117,7 @@ struct Settings
bool gpu_force_round_texcoords : 1 = false;
bool gpu_accurate_blending : 1 = false;
bool gpu_widescreen_hack : 1 = false;
bool gpu_texture_cache : 1 = false;
bool gpu_pgxp_enable : 1 = false;
bool gpu_pgxp_culling : 1 = true;
bool gpu_pgxp_texture_correction : 1 = true;
@ -239,20 +240,46 @@ struct Settings
// texture replacements
struct TextureReplacementSettings
{
struct Configuration
{
constexpr Configuration() = default;
bool dump_texture_pages : 1 = false;
bool dump_full_texture_pages : 1 = false;
bool dump_texture_force_alpha_channel : 1 = false;
bool dump_vram_write_force_alpha_channel : 1 = true;
bool dump_c16_textures : 1 = false;
bool reduce_palette_range : 1 = true;
bool convert_copies_to_writes : 1 = false;
bool replacement_scale_linear_filter = true;
u32 max_vram_write_splits = 0;
u32 max_vram_write_coalesce_width = 0;
u32 max_vram_write_coalesce_height = 0;
u32 texture_dump_width_threshold = 16;
u32 texture_dump_height_threshold = 16;
u32 vram_write_dump_width_threshold = 128;
u32 vram_write_dump_height_threshold = 128;
bool operator==(const Configuration& rhs) const;
bool operator!=(const Configuration& rhs) const;
std::string ExportToYAML(bool comment) const;
};
bool enable_texture_replacements : 1 = false;
bool enable_vram_write_replacements : 1 = false;
bool preload_textures : 1 = false;
bool dump_textures : 1 = false;
bool dump_replaced_textures : 1 = true;
bool dump_vram_writes : 1 = false;
bool dump_vram_write_force_alpha_channel : 1 = 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; }
Configuration config;
ALWAYS_INLINE bool ShouldDumpVRAMWrite(u32 width, u32 height)
{
return dump_vram_writes && width >= dump_vram_write_width_threshold && height >= dump_vram_write_height_threshold;
}
bool operator==(const TextureReplacementSettings& rhs) const;
bool operator!=(const TextureReplacementSettings& rhs) const;
} texture_replacements;
bool bios_tty_logging : 1 = false;
@ -345,8 +372,6 @@ struct Settings
DEFAULT_DMA_HALT_TICKS = 100,
DEFAULT_GPU_FIFO_SIZE = 16,
DEFAULT_GPU_MAX_RUN_AHEAD = 128,
DEFAULT_VRAM_WRITE_DUMP_WIDTH_THRESHOLD = 128,
DEFAULT_VRAM_WRITE_DUMP_HEIGHT_THRESHOLD = 128,
};
void Load(SettingsInterface& si, SettingsInterface& controller_si);

View File

@ -16,6 +16,7 @@
#include "game_database.h"
#include "game_list.h"
#include "gpu.h"
#include "gpu_hw_texture_cache.h"
#include "gte.h"
#include "host.h"
#include "host_interface_progress_callback.h"
@ -30,7 +31,6 @@
#include "save_state_version.h"
#include "sio.h"
#include "spu.h"
#include "texture_replacements.h"
#include "timers.h"
#include "scmversion/scmversion.h"
@ -1794,7 +1794,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
// Texture replacement preloading.
// TODO: Move this and everything else below OnSystemStarted().
TextureReplacements::SetGameID(s_running_game_serial);
GPUTextureCache::SetGameID(s_running_game_serial);
// Good to go.
s_state = State::Running;
@ -1969,8 +1969,6 @@ void System::DestroySystem()
ClearMemorySaveStates();
TextureReplacements::Shutdown();
PCDrv::Shutdown();
SIO::Shutdown();
MDEC::Shutdown();
@ -1984,6 +1982,7 @@ void System::DestroySystem()
CPU::Shutdown();
Bus::Shutdown();
TimingEvents::Shutdown();
GPUTextureCache::Shutdown();
ClearRunningGame();
// Restore present-all-frames behavior.
@ -4078,7 +4077,7 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
}
if (!booting)
TextureReplacements::SetGameID(s_running_game_serial);
GPUTextureCache::SetGameID(s_running_game_serial);
if (booting)
Achievements::ResetHardcoreMode(true);
@ -4389,6 +4388,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_settings.gpu_downsample_mode != old_settings.gpu_downsample_mode ||
g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale ||
g_settings.gpu_wireframe_mode != old_settings.gpu_wireframe_mode ||
g_settings.gpu_texture_cache != old_settings.gpu_texture_cache ||
g_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode ||
g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing ||
g_settings.display_crop_mode != old_settings.display_crop_mode ||
@ -4404,7 +4404,8 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_settings.display_line_start_offset != old_settings.display_line_start_offset ||
g_settings.display_line_end_offset != old_settings.display_line_end_offset ||
g_settings.rewind_enable != old_settings.rewind_enable ||
g_settings.runahead_frames != old_settings.runahead_frames)
g_settings.runahead_frames != old_settings.runahead_frames ||
g_settings.texture_replacements.config != old_settings.texture_replacements.config)
{
g_gpu->UpdateSettings(old_settings);
if (IsPaused())
@ -4455,13 +4456,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
UpdateMemorySaveStateSettings();
}
if (g_settings.texture_replacements.enable_vram_write_replacements !=
old_settings.texture_replacements.enable_vram_write_replacements ||
g_settings.texture_replacements.preload_textures != old_settings.texture_replacements.preload_textures)
{
TextureReplacements::Reload();
}
if (g_settings.audio_backend != old_settings.audio_backend ||
g_settings.emulation_speed != old_settings.emulation_speed ||
g_settings.fast_forward_speed != old_settings.fast_forward_speed ||
@ -4576,7 +4570,9 @@ void System::WarnAboutUnsafeSettings()
LargeString messages;
auto append = [&messages](const char* icon, std::string_view msg) { messages.append_format("{} {}\n", icon, msg); };
if (!g_settings.disable_all_enhancements && ImGuiManager::IsShowingOSDMessages())
if (!g_settings.disable_all_enhancements)
{
if (ImGuiManager::IsShowingOSDMessages())
{
if (g_settings.cpu_overclock_active)
{
@ -4603,7 +4599,8 @@ void System::WarnAboutUnsafeSettings()
}
if (g_settings.gpu_force_video_timing != ForceVideoTimingMode::Disabled)
{
append(ICON_FA_TV, TRANSLATE_SV("System", "Force frame timings is enabled. Games may run at incorrect speeds."));
append(ICON_FA_TV,
TRANSLATE_SV("System", "Force frame timings is enabled. Games may run at incorrect speeds."));
}
if (!g_settings.IsUsingSoftwareRenderer())
{
@ -4626,6 +4623,16 @@ void System::WarnAboutUnsafeSettings()
}
}
// Always display TC warning.
if (g_settings.gpu_texture_cache)
{
append(
ICON_FA_PAINT_ROLLER,
TRANSLATE_SV("System",
"Texture cache is enabled. This feature is experimental, some games may not render correctly."));
}
}
if (g_settings.disable_all_enhancements)
{
append(ICON_EMOJI_WARNING, TRANSLATE_SV("System", "Safe mode is enabled."));

View File

@ -1,335 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "texture_replacements.h"
#include "gpu_types.h"
#include "host.h"
#include "settings.h"
#include "common/bitutils.h"
#include "common/file_system.h"
#include "common/hash_combine.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 <cinttypes>
#include <tuple>
#include <unordered_map>
#include <vector>
LOG_CHANNEL(TextureReplacements);
namespace TextureReplacements {
namespace {
struct VRAMReplacementHash
{
u64 low;
u64 high;
TinyString ToString() const;
bool ParseString(std::string_view sv);
bool operator<(const VRAMReplacementHash& rhs) const { return std::tie(low, high) < std::tie(rhs.low, rhs.high); }
bool operator==(const VRAMReplacementHash& rhs) const { return low == rhs.low && high == rhs.high; }
bool operator!=(const VRAMReplacementHash& rhs) const { return low != rhs.low || high != rhs.high; }
};
struct VRAMReplacementHashMapHash
{
size_t operator()(const VRAMReplacementHash& hash) const;
};
} // namespace
using VRAMWriteReplacementMap = std::unordered_map<VRAMReplacementHash, std::string, VRAMReplacementHashMapHash>;
using TextureCache = std::unordered_map<std::string, ReplacementImage>;
static bool ParseReplacementFilename(const std::string& filename, VRAMReplacementHash* replacement_hash,
ReplacmentType* replacement_type);
static std::string GetSourceDirectory();
static std::string GetDumpDirectory();
static VRAMReplacementHash GetVRAMWriteHash(u32 width, u32 height, const void* pixels);
static std::string GetVRAMWriteDumpFilename(u32 width, u32 height, const void* pixels);
static void FindTextures(const std::string& dir);
static const ReplacementImage* LoadTexture(const std::string& filename);
static void PreloadTextures();
static void PurgeUnreferencedTexturesFromCache();
static std::string s_game_id;
// TODO: Check the size, purge some when it gets too large.
static TextureCache s_texture_cache;
static VRAMWriteReplacementMap s_vram_write_replacements;
} // namespace TextureReplacements
size_t TextureReplacements::VRAMReplacementHashMapHash::operator()(const VRAMReplacementHash& hash) const
{
size_t hash_hash = std::hash<u64>{}(hash.low);
hash_combine(hash_hash, hash.high);
return hash_hash;
}
TinyString TextureReplacements::VRAMReplacementHash::ToString() const
{
return TinyString::from_format("{:08X}{:08X}", high, low);
}
bool TextureReplacements::VRAMReplacementHash::ParseString(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;
}
void TextureReplacements::SetGameID(std::string game_id)
{
if (s_game_id == game_id)
return;
s_game_id = game_id;
Reload();
}
const TextureReplacements::ReplacementImage* TextureReplacements::GetVRAMReplacement(u32 width, u32 height,
const void* pixels)
{
const VRAMReplacementHash hash = GetVRAMWriteHash(width, height, pixels);
const auto it = s_vram_write_replacements.find(hash);
if (it == s_vram_write_replacements.end())
return nullptr;
return LoadTexture(it->second);
}
void TextureReplacements::DumpVRAMWrite(u32 width, u32 height, const void* pixels)
{
const std::string filename = GetVRAMWriteDumpFilename(width, height, pixels);
if (filename.empty())
return;
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);
}
}
INFO_LOG("Dumping {}x{} VRAM write to '{}'", width, height, Path::GetFileName(filename));
if (!image.SaveToFile(filename.c_str())) [[unlikely]]
ERROR_LOG("Failed to dump {}x{} VRAM write to '{}'", width, height, filename);
}
void TextureReplacements::Shutdown()
{
s_texture_cache.clear();
s_vram_write_replacements.clear();
s_game_id.clear();
}
// TODO: Organize into PCSX2-style.
std::string TextureReplacements::GetSourceDirectory()
{
return Path::Combine(EmuFolders::Textures, s_game_id);
}
std::string TextureReplacements::GetDumpDirectory()
{
return Path::Combine(EmuFolders::Dumps, Path::Combine("textures", s_game_id));
}
TextureReplacements::VRAMReplacementHash TextureReplacements::GetVRAMWriteHash(u32 width, u32 height,
const void* pixels)
{
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)
{
if (s_game_id.empty())
return {};
const VRAMReplacementHash 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()
{
s_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(s_texture_cache);
s_texture_cache = {};
for (const auto& it : s_vram_write_replacements)
{
auto it2 = old_map.find(it.second);
if (it2 != old_map.end())
{
s_texture_cache[it.second] = std::move(it2->second);
old_map.erase(it2);
}
}
}
bool TextureReplacements::ParseReplacementFilename(const std::string& filename, VRAMReplacementHash* replacement_hash,
ReplacmentType* replacement_type)
{
const std::string_view file_title = Path::GetFileTitle(filename);
if (!file_title.starts_with("vram-write-"))
return false;
const std::string_view hashpart = file_title.substr(11);
if (!replacement_hash->ParseString(hashpart))
return false;
const std::string_view file_extension = Path::GetExtension(filename);
bool valid_extension = false;
for (const char* test_extension : {"png", "jpg", "webp"})
{
if (StringUtil::EqualNoCase(file_extension, test_extension))
{
valid_extension = true;
break;
}
}
*replacement_type = ReplacmentType::VRAMWrite;
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;
VRAMReplacementHash hash;
ReplacmentType type;
if (!ParseReplacementFilename(fd.FileName, &hash, &type))
continue;
switch (type)
{
case ReplacmentType::VRAMWrite:
{
auto it = s_vram_write_replacements.find(hash);
if (it != s_vram_write_replacements.end())
{
WARNING_LOG("Duplicate VRAM write replacement: '{}' and '{}'", it->second, fd.FileName);
continue;
}
s_vram_write_replacements.emplace(hash, std::move(fd.FileName));
}
break;
}
}
INFO_LOG("Found {} replacement VRAM writes for '{}'", s_vram_write_replacements.size(), s_game_id);
}
const TextureReplacements::ReplacementImage* TextureReplacements::LoadTexture(const std::string& filename)
{
auto it = s_texture_cache.find(filename);
if (it != s_texture_cache.end())
return &it->second;
RGBA8Image image;
if (!image.LoadFromFile(filename.c_str()))
{
ERROR_LOG("Failed to load '{}'", Path::GetFileName(filename));
return nullptr;
}
INFO_LOG("Loaded '{}': {}x{}", Path::GetFileName(filename), image.GetWidth(), image.GetHeight());
it = s_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>(s_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 : s_vram_write_replacements)
{
UPDATE_PROGRESS();
LoadTexture(it.second);
num_textures_loaded++;
}
#undef UPDATE_PROGRESS
}

View File

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "types.h"
#include "util/image.h"
#include <string>
namespace TextureReplacements {
using ReplacementImage = RGBA8Image;
enum class ReplacmentType
{
VRAMWrite,
};
void SetGameID(std::string game_id);
void Reload();
const ReplacementImage* GetVRAMReplacement(u32 width, u32 height, const void* pixels);
void DumpVRAMWrite(u32 width, u32 height, const void* pixels);
void Shutdown();
} // namespace TextureReplacements

View File

@ -143,6 +143,7 @@ set(SRCS
setupwizarddialog.cpp
setupwizarddialog.h
setupwizarddialog.ui
texturereplacementsettingsdialog.ui
)
set(TS_FILES

View File

@ -339,6 +339,9 @@
<QtUi Include="selectdiscdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="texturereplacementsettingsdialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View File

@ -284,6 +284,7 @@
<QtUi Include="audiostretchsettingsdialog.ui" />
<QtUi Include="controllerbindingwidget_justifier.ui" />
<QtUi Include="selectdiscdialog.ui" />
<QtUi Include="texturereplacementsettingsdialog.ui" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="duckstation-qt.rc" />

View File

@ -5,13 +5,21 @@
#include "qtutils.h"
#include "settingswindow.h"
#include "settingwidgetbinder.h"
#include "ui_texturereplacementsettingsdialog.h"
#include "core/game_database.h"
#include "core/gpu.h"
#include "core/settings.h"
#include "util/ini_settings_interface.h"
#include "util/media_capture.h"
#include "common/error.h"
#include <QtCore/QDir>
#include <QtWidgets/QDialog>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <algorithm>
static QVariant GetMSAAModeValue(uint multisamples, bool ssaa)
@ -232,26 +240,50 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
// Texture Replacements Tab
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vramWriteReplacement, "TextureReplacements",
"EnableVRAMWriteReplacements", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureCache, "GPU", "EnableTextureCache", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useOldMDECRoutines, "Hacks", "UseOldMDECRoutines", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureReplacements, "TextureReplacements",
"EnableTextureReplacements", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.preloadTextureReplacements, "TextureReplacements",
"PreloadTextures", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useOldMDECRoutines, "Hacks", "UseOldMDECRoutines", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTextureDumping, "TextureReplacements", "DumpTextures",
false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.dumpReplacedTextures, "TextureReplacements",
"DumpReplacedTextures", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vramWriteReplacement, "TextureReplacements",
"EnableVRAMWriteReplacements", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vramWriteDumping, "TextureReplacements", "DumpVRAMWrites",
false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.setVRAMWriteAlphaChannel, "TextureReplacements",
"DumpVRAMWriteForceAlphaChannel", true);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.minDumpedVRAMWriteWidth, "TextureReplacements",
"DumpVRAMWriteWidthThreshold",
Settings::DEFAULT_VRAM_WRITE_DUMP_WIDTH_THRESHOLD);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.minDumpedVRAMWriteHeight, "TextureReplacements",
"DumpVRAMWriteHeightThreshold",
Settings::DEFAULT_VRAM_WRITE_DUMP_HEIGHT_THRESHOLD);
if (!m_dialog->isPerGameSettings())
{
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.texturesDirectory, m_ui.texturesDirectoryBrowse,
tr("Select Textures Directory"), m_ui.texturesDirectoryOpen,
m_ui.texturesDirectoryReset, "Folders", "Textures",
Path::Combine(EmuFolders::DataRoot, "textures"));
}
else
{
m_ui.tabTextureReplacementsLayout->removeWidget(m_ui.texturesDirectoryGroup);
delete m_ui.texturesDirectoryGroup;
m_ui.texturesDirectoryGroup = nullptr;
m_ui.texturesDirectoryBrowse = nullptr;
m_ui.texturesDirectoryOpen = nullptr;
m_ui.texturesDirectoryReset = nullptr;
m_ui.texturesDirectoryLabel = nullptr;
m_ui.texturesDirectory = nullptr;
}
connect(m_ui.enableTextureCache, &QCheckBox::checkStateChanged, this,
&GraphicsSettingsWidget::onEnableTextureCacheChanged);
connect(m_ui.enableTextureReplacements, &QCheckBox::checkStateChanged, this,
&GraphicsSettingsWidget::onEnableAnyTextureReplacementsChanged);
connect(m_ui.vramWriteReplacement, &QCheckBox::checkStateChanged, this,
&GraphicsSettingsWidget::onEnableAnyTextureReplacementsChanged);
connect(m_ui.vramWriteDumping, &QCheckBox::checkStateChanged, this,
&GraphicsSettingsWidget::onEnableVRAMWriteDumpingChanged);
connect(m_ui.textureReplacementOptions, &QPushButton::clicked, this,
&GraphicsSettingsWidget::onTextureReplacementOptionsClicked);
// Debugging Tab
@ -274,8 +306,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
onMediaCaptureBackendChanged();
onMediaCaptureAudioEnabledChanged();
onMediaCaptureVideoEnabledChanged();
onEnableTextureCacheChanged();
onEnableAnyTextureReplacementsChanged();
onEnableVRAMWriteDumpingChanged();
onShowDebugSettingsChanged(QtHost::ShouldShowDebugOptions());
// Rendering Tab
@ -534,23 +566,26 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
"separate two pairs from each other.</b><br>For example: \"compression_level = 4 : joint_stereo = 1\""));
// Texture Replacements Tab
dialog->registerWidgetHelp(m_ui.vramWriteReplacement, tr("Enable VRAM Write Replacement"), tr("Unchecked"),
tr("Enables the replacement of background textures in supported games. <strong>This is "
"not general texture replacement.</strong>"));
dialog->registerWidgetHelp(m_ui.preloadTextureReplacements, tr("Preload Texture Replacements"), tr("Unchecked"),
tr("Loads all replacement texture to RAM, reducing stuttering at runtime."));
dialog->registerWidgetHelp(m_ui.enableTextureCache, tr("Enable Texture Cache"), tr("Unchecked"),
tr("Enables caching of guest textures, required for texture replacement."));
dialog->registerWidgetHelp(m_ui.useOldMDECRoutines, tr("Use Old MDEC Routines"), tr("Unchecked"),
tr("Enables the older, less accurate MDEC decoding routines. May be required for old "
"replacement backgrounds to match/load."));
dialog->registerWidgetHelp(m_ui.setVRAMWriteAlphaChannel, tr("Set Alpha Channel"), tr("Checked"),
tr("Clears the mask/transparency bit in VRAM write dumps."));
dialog->registerWidgetHelp(m_ui.enableTextureReplacements, tr("Enable Texture Replacements"), tr("Unchecked"),
tr("Enables loading of replacement textures. Not compatible with all games."));
dialog->registerWidgetHelp(m_ui.preloadTextureReplacements, tr("Preload Texture Replacements"), tr("Unchecked"),
tr("Loads all replacement texture to RAM, reducing stuttering at runtime."));
dialog->registerWidgetHelp(
m_ui.enableTextureDumping, tr("Enable Texture Dumping"), tr("Unchecked"),
tr("Enables dumping of textures to image files, which can be replaced. Not compatible with all games."));
dialog->registerWidgetHelp(m_ui.dumpReplacedTextures, tr("Dump Replaced Textures"), tr("Unchecked"),
tr("Dumps textures that have replacements already loaded."));
dialog->registerWidgetHelp(m_ui.vramWriteReplacement, tr("Enable VRAM Write Replacement"), tr("Unchecked"),
tr("Enables the replacement of background textures in supported games."));
dialog->registerWidgetHelp(m_ui.vramWriteDumping, tr("Enable VRAM Write Dumping"), tr("Unchecked"),
tr("Writes backgrounds that can be replaced to the dump directory."));
dialog->registerWidgetHelp(m_ui.minDumpedVRAMWriteWidth, tr("Dump Size Threshold"), tr("128px"),
tr("Determines the threshold that triggers a VRAM write to be dumped."));
dialog->registerWidgetHelp(m_ui.minDumpedVRAMWriteHeight, tr("Dump Size Threshold"), tr("128px"),
tr("Determines the threshold that triggers a VRAM write to be dumped."));
// Debugging Tab
@ -1097,19 +1132,120 @@ void GraphicsSettingsWidget::onMediaCaptureAudioEnabledChanged()
m_ui.audioCaptureArguments->setEnabled(enabled);
}
void GraphicsSettingsWidget::onEnableTextureCacheChanged()
{
const bool tc_enabled = m_dialog->getEffectiveBoolValue("GPU", "EnableTextureCache", false);
m_ui.enableTextureReplacements->setEnabled(tc_enabled);
m_ui.enableTextureDumping->setEnabled(tc_enabled);
}
void GraphicsSettingsWidget::onEnableAnyTextureReplacementsChanged()
{
const bool any_replacements_enabled =
m_dialog->getEffectiveBoolValue("TextureReplacements", "EnableVRAMWriteReplacements", false);
(m_dialog->getEffectiveBoolValue("TextureReplacements", "EnableVRAMWriteReplacements", false) ||
(m_dialog->getEffectiveBoolValue("GPU", "EnableTextureCache", false) &&
m_dialog->getEffectiveBoolValue("TextureReplacements", "EnableTextureReplacements", false)));
m_ui.preloadTextureReplacements->setEnabled(any_replacements_enabled);
}
void GraphicsSettingsWidget::onEnableVRAMWriteDumpingChanged()
void GraphicsSettingsWidget::onTextureReplacementOptionsClicked()
{
const bool enabled = m_dialog->getEffectiveBoolValue("TextureReplacements", "DumpVRAMWrites", false);
m_ui.setVRAMWriteAlphaChannel->setEnabled(enabled);
m_ui.minDumpedVRAMWriteWidth->setEnabled(enabled);
m_ui.minDumpedVRAMWriteHeight->setEnabled(enabled);
m_ui.vramWriteDumpThresholdLabel->setEnabled(enabled);
m_ui.vramWriteDumpThresholdSeparator->setEnabled(enabled);
QDialog dlg(QtUtils::GetRootWidget(this));
Ui::TextureReplacementSettingsDialog dlgui;
dlgui.setupUi(&dlg);
dlgui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("image-fill")).pixmap(32, 32));
constexpr Settings::TextureReplacementSettings::Configuration default_replacement_config;
SettingsInterface* const sif = m_dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.dumpTexturePages, "TextureReplacements", "DumpTexturePages",
default_replacement_config.dump_texture_pages);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.dumpFullTexturePages, "TextureReplacements",
"DumpFullTexturePages",
default_replacement_config.dump_full_texture_pages);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.dumpC16Textures, "TextureReplacements", "DumpC16Textures",
default_replacement_config.dump_c16_textures);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.reducePaletteRange, "TextureReplacements",
"ReducePaletteRange", default_replacement_config.reduce_palette_range);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.convertCopiesToWrites, "TextureReplacements",
"ConvertCopiesToWrites",
default_replacement_config.convert_copies_to_writes);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.maxVRAMWriteSplits, "TextureReplacements",
"MaxVRAMWriteSplits", default_replacement_config.max_vram_write_splits);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.maxVRAMWriteCoalesceWidth, "TextureReplacements",
"MaxVRAMWriteCoalesceWidth",
default_replacement_config.max_vram_write_coalesce_width);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.maxVRAMWriteCoalesceHeight, "TextureReplacements",
"MaxVRAMWriteCoalesceHeight",
default_replacement_config.max_vram_write_coalesce_height);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.minDumpedTextureWidth, "TextureReplacements",
"DumpTextureWidthThreshold",
default_replacement_config.texture_dump_width_threshold);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.minDumpedTextureHeight, "TextureReplacements",
"DumpTextureHeightThreshold",
default_replacement_config.texture_dump_height_threshold);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.setTextureDumpAlphaChannel, "TextureReplacements",
"DumpTextureForceAlphaChannel",
default_replacement_config.dump_texture_force_alpha_channel);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.minDumpedVRAMWriteWidth, "TextureReplacements",
"DumpVRAMWriteWidthThreshold",
default_replacement_config.vram_write_dump_width_threshold);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.minDumpedVRAMWriteHeight, "TextureReplacements",
"DumpVRAMWriteHeightThreshold",
default_replacement_config.vram_write_dump_height_threshold);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.setVRAMWriteAlphaChannel, "TextureReplacements",
"DumpVRAMWriteForceAlphaChannel",
default_replacement_config.dump_vram_write_force_alpha_channel);
dlgui.dumpFullTexturePages->setEnabled(
m_dialog->getEffectiveBoolValue("TextureReplacements", "DumpTexturePages", false));
connect(dlgui.dumpTexturePages, &QCheckBox::checkStateChanged, this, [this, full_cb = dlgui.dumpFullTexturePages]() {
full_cb->setEnabled(m_dialog->getEffectiveBoolValue("TextureReplacements", "DumpTexturePages", false));
});
connect(dlgui.closeButton, &QPushButton::clicked, &dlg, &QDialog::accept);
connect(dlgui.exportButton, &QPushButton::clicked, &dlg, [&dlg, &dlgui]() {
Settings::TextureReplacementSettings::Configuration config;
config.dump_texture_pages = dlgui.dumpTexturePages->isChecked();
config.dump_full_texture_pages = dlgui.dumpFullTexturePages->isChecked();
config.dump_c16_textures = dlgui.dumpC16Textures->isChecked();
config.reduce_palette_range = dlgui.reducePaletteRange->isChecked();
config.convert_copies_to_writes = dlgui.convertCopiesToWrites->isChecked();
config.max_vram_write_splits = dlgui.maxVRAMWriteSplits->value();
config.max_vram_write_coalesce_width = dlgui.maxVRAMWriteCoalesceWidth->value();
config.max_vram_write_coalesce_height = dlgui.maxVRAMWriteCoalesceHeight->value();
config.texture_dump_width_threshold = dlgui.minDumpedTextureWidth->value();
config.texture_dump_height_threshold = dlgui.minDumpedTextureHeight->value();
config.dump_texture_force_alpha_channel = dlgui.setTextureDumpAlphaChannel->isChecked();
config.vram_write_dump_width_threshold = dlgui.minDumpedVRAMWriteWidth->value();
config.vram_write_dump_height_threshold = dlgui.minDumpedVRAMWriteHeight->value();
config.dump_vram_write_force_alpha_channel = dlgui.setTextureDumpAlphaChannel->isChecked();
QInputDialog idlg(&dlg);
idlg.resize(600, 400);
idlg.setWindowTitle(tr("Texture Replacement Configuration"));
idlg.setInputMode(QInputDialog::TextInput);
idlg.setOption(QInputDialog::UsePlainTextEditForTextInput);
idlg.setLabelText(tr("Texture Replacement Configuration (config.yaml)"));
idlg.setTextValue(QString::fromStdString(config.ExportToYAML(false)));
idlg.setOkButtonText(tr("Save"));
if (idlg.exec())
{
const QString path = QFileDialog::getSaveFileName(&dlg, tr("Save Configuration"), QString(),
tr("Configuration Files (config.yaml)"));
if (path.isEmpty())
return;
Error error;
if (!FileSystem::WriteStringToFile(QDir::toNativeSeparators(path).toUtf8().constData(),
idlg.textValue().toStdString(), &error))
{
QMessageBox::critical(&dlg, tr("Write Failed"), QString::fromStdString(error.GetDescription()));
}
}
});
dlg.exec();
}

View File

@ -39,8 +39,9 @@ private Q_SLOTS:
void onMediaCaptureVideoAutoResolutionChanged();
void onMediaCaptureAudioEnabledChanged();
void onEnableTextureCacheChanged();
void onEnableAnyTextureReplacementsChanged();
void onEnableVRAMWriteDumpingChanged();
void onTextureReplacementOptionsClicked();
private:
static constexpr int TAB_INDEX_RENDERING = 0;

View File

@ -1054,9 +1054,9 @@
</widget>
<widget class="QWidget" name="tabTextureReplacements">
<attribute name="title">
<string>Texture Replacements</string>
<string>Texture Replacement</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="tabTextureReplacementsLayout">
<property name="leftMargin">
<number>0</number>
</property>
@ -1067,31 +1067,85 @@
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_5">
<widget class="QGroupBox" name="groupBox_12">
<property name="title">
<string>General Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_8">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QCheckBox" name="vramWriteReplacement">
<layout class="QGridLayout" name="gridLayout_9">
<item row="1" column="0">
<widget class="QCheckBox" name="enableTextureCache">
<property name="text">
<string>Enable VRAM Write Replacement</string>
<string>Enable Texture Cache</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>The texture cache is currently experimental, and may cause rendering errors in some games.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="useOldMDECRoutines">
<property name="text">
<string>Use Old MDEC Routines</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Texture Replacement</string>
</property>
<layout class="QFormLayout" name="formLayout_8">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_6" columnstretch="1,1">
<item row="0" column="0">
<widget class="QCheckBox" name="enableTextureReplacements">
<property name="text">
<string>Enable Texture Replacements</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="preloadTextureReplacements">
<property name="text">
<string>Preload Texture Replacements</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="useOldMDECRoutines">
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4"/>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QCheckBox" name="enableTextureDumping">
<property name="text">
<string>Use Old MDEC Routines</string>
<string>Enable Texture Dumping</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="textureReplacementOptions">
<property name="icon">
<iconset theme="settings-3-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="dumpReplacedTextures">
<property name="text">
<string>Dump Replaced Textures</string>
</property>
</widget>
</item>
@ -1103,72 +1157,68 @@
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>VRAM Write Dumping</string>
<string>VRAM Write (Background) Replacement</string>
</property>
<layout class="QFormLayout" name="formLayout_9">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QCheckBox" name="vramWriteReplacement">
<property name="text">
<string>Enable VRAM Write Replacement</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="vramWriteDumping">
<property name="text">
<string>Enable VRAM Write Dumping</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="setVRAMWriteAlphaChannel">
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="texturesDirectoryGroup">
<property name="title">
<string>Textures Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<item row="1" column="2">
<widget class="QPushButton" name="texturesDirectoryOpen">
<property name="text">
<string>Set Alpha Channel</string>
<string>Open...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="vramWriteDumpThresholdLabel">
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="texturesDirectoryLabel">
<property name="text">
<string>Dump Size Threshold:</string>
<string>Directory to load replacement textures from, and save dumps to.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,0,1">
<item>
<widget class="QSpinBox" name="minDumpedVRAMWriteWidth">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="vramWriteDumpThresholdSeparator">
<widget class="QPushButton" name="texturesDirectoryBrowse">
<property name="text">
<string>x</string>
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="minDumpedVRAMWriteHeight">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>512</number>
<item row="1" column="0">
<widget class="QLineEdit" name="texturesDirectory"/>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="texturesDirectoryReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>

View File

@ -1814,6 +1814,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo
m_ui.menuCheats->setDisabled(cheevos_challenge_mode);
m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode);
m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode);
m_ui.actionReloadTextureReplacements->setDisabled(starting || !running);
m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode);
@ -2095,6 +2096,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenTextureDirectoryTriggered);
connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread, &EmuThread::reloadTextureReplacements);
connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
@ -2815,6 +2818,15 @@ void MainWindow::onToolsOpenDataDirectoryTriggered()
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot)));
}
void MainWindow::onToolsOpenTextureDirectoryTriggered()
{
QString dir = QString::fromStdString(EmuFolders::Textures);
if (s_system_valid && !s_current_game_serial.isEmpty())
dir = QStringLiteral("%1" FS_OSPATH_SEPARATOR_STR "%2").arg(dir).arg(s_current_game_serial);
QtUtils::OpenURL(this, QUrl::fromLocalFile(dir));
}
void MainWindow::onSettingsTriggeredFromToolbar()
{
if (s_system_valid)

View File

@ -178,6 +178,7 @@ private Q_SLOTS:
void onToolsCoverDownloaderTriggered();
void onToolsMediaCaptureToggled(bool checked);
void onToolsOpenDataDirectoryTriggered();
void onToolsOpenTextureDirectoryTriggered();
void onSettingsTriggeredFromToolbar();
void onGameListRefreshComplete();

View File

@ -208,6 +208,8 @@
<property name="title">
<string>&amp;Tools</string>
</property>
<addaction name="actionOpenDataDirectory"/>
<addaction name="separator"/>
<addaction name="actionMemoryCardEditor"/>
<addaction name="actionCoverDownloader"/>
<addaction name="separator"/>
@ -215,7 +217,8 @@
<addaction name="separator"/>
<addaction name="actionMediaCapture"/>
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
<addaction name="actionOpenTextureDirectory"/>
<addaction name="actionReloadTextureReplacements"/>
</widget>
<addaction name="menuSystem"/>
<addaction name="menuSettings"/>
@ -917,6 +920,16 @@
<string>Media Ca&amp;pture</string>
</property>
</action>
<action name="actionOpenTextureDirectory">
<property name="text">
<string>Open Texture Directory...</string>
</property>
</action>
<action name="actionReloadTextureReplacements">
<property name="text">
<string>Reload Texture Replacements</string>
</property>
</action>
</widget>
<resources>
<include location="resources/duckstation-qt.qrc"/>

View File

@ -19,6 +19,7 @@
#include "core/game_list.h"
#include "core/gdb_server.h"
#include "core/gpu.h"
#include "core/gpu_hw_texture_cache.h"
#include "core/host.h"
#include "core/imgui_overlays.h"
#include "core/memory_card.h"
@ -1351,6 +1352,18 @@ void EmuThread::clearInputBindStateFromSource(InputBindingKey key)
InputManager::ClearBindStateFromSource(key);
}
void EmuThread::reloadTextureReplacements()
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "reloadTextureReplacements", Qt::QueuedConnection);
return;
}
if (System::IsValid())
GPUTextureCache::ReloadTextureReplacements();
}
void EmuThread::runOnEmuThread(std::function<void()> callback)
{
callback();

View File

@ -199,6 +199,7 @@ public Q_SLOTS:
void reloadPostProcessingShaders();
void updatePostProcessingSettings();
void clearInputBindStateFromSource(InputBindingKey key);
void reloadTextureReplacements();
private Q_SLOTS:
void stopInThread();

View File

@ -0,0 +1,380 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextureReplacementSettingsDialog</class>
<widget class="QDialog" name="TextureReplacementSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>646</width>
<height>587</height>
</rect>
</property>
<property name="windowTitle">
<string>Texture Replacement Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Texture Replacement Settings&lt;/span&gt;&lt;br/&gt;These settings fine-tune the behavior of the texture replacement system. You can also export a game-specific configuration file. Each of the options is explained in the configuration file, and at &lt;a href=&quot;https://github.com/stenzek/duckstation/wiki/Texture-Replacement&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0078d4;&quot;&gt;https://github.com/stenzek/duckstation/wiki/Texture-Replacement&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Texture Dumping Mode</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QCheckBox" name="reducePaletteRange">
<property name="text">
<string>Reduce Palette Range</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="dumpTexturePages">
<property name="text">
<string>Dump Texture Pages</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="dumpC16Textures">
<property name="text">
<string>Dump C16 Textures</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="dumpFullTexturePages">
<property name="text">
<string>Dump Full Texture Pages</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>The texture dumping system can either operate in page mode, or write-tracking mode. Replacements can be loaded from either dump method.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Write Tracking Options</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Maximum Write Coalesce Size:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_8" stretch="1,0,1">
<item>
<widget class="QSpinBox" name="maxVRAMWriteCoalesceWidth">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>1024</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="maxVRAMWriteCoalesceSeparator">
<property name="text">
<string>x</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="maxVRAMWriteCoalesceHeight">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>512</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Maximum Write Splits:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="maxVRAMWriteSplits">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>32</number>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="convertCopiesToWrites">
<property name="text">
<string>Convert Copies To Writes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Texture Dumping Options</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="vramWriteDumpThresholdLabel_2">
<property name="text">
<string>Dump Size Threshold:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7" stretch="1,0,1,0">
<item>
<widget class="QSpinBox" name="minDumpedTextureWidth">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="vramWriteDumpThresholdSeparator_2">
<property name="text">
<string>x</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="minDumpedTextureHeight">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>512</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="setTextureDumpAlphaChannel">
<property name="text">
<string>Set Alpha Channel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Determines the minimum size of a texture that will be dumped. Textures with a size smaller than this value will be ignored.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Background Dumping Options</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="vramWriteDumpThresholdLabel">
<property name="text">
<string>Dump Size Threshold:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,0,1,0">
<item>
<widget class="QSpinBox" name="minDumpedVRAMWriteWidth">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="vramWriteDumpThresholdSeparator">
<property name="text">
<string>x</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="minDumpedVRAMWriteHeight">
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>512</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="setVRAMWriteAlphaChannel">
<property name="text">
<string>Set Alpha Channel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Determines the minimum size of a VRAM write that will be dumped, in background dumping mode. Uploads smaller than this size will be ignored.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>198</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="exportButton">
<property name="text">
<string>Export...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>