diff --git a/bin/resources/GameIndex.yaml b/bin/resources/GameIndex.yaml
index d0c3c0c5c6..1694ffcfa2 100644
--- a/bin/resources/GameIndex.yaml
+++ b/bin/resources/GameIndex.yaml
@@ -12860,6 +12860,8 @@ SLES-51504:
SLES-51507:
name: "Futurama"
region: "PAL-M5"
+ gsHWFixes:
+ readTCOnClose: 1 # Fixes render to target getting lost on state/switch.
SLES-51508:
name: "Hulk, The"
region: "PAL-M5"
@@ -42646,6 +42648,8 @@ SLUS-20439:
name: "Futurama"
region: "NTSC-U"
compat: 5
+ gsHWFixes:
+ readTCOnClose: 1 # Fixes render to target getting lost on state/switch.
SLUS-20440:
name: "Kya - Dark Lineage"
region: "NTSC-U"
diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
index dacf3a408f..db1ec41003 100644
--- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
+++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
@@ -198,6 +198,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.disablePartialInvalidation, "EmuCore/GS", "UserHacks_DisablePartialInvalidation", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.textureInsideRt, "EmuCore/GS", "UserHacks_TextureInsideRt", false);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.readTCOnClose, "EmuCore/GS", "UserHacks_ReadTCOnClose", false);
//////////////////////////////////////////////////////////////////////////
// HW Upscaling Fixes
@@ -537,6 +538,10 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
dialog->registerWidgetHelp(m_ui.textureInsideRt, tr("Texture Inside RT"), tr("Unchecked"),
tr("Allows the texture cache to reuse as an input texture the inner portion of a previous framebuffer. "
"In some selected games this is enabled by default regardless of this setting."));
+
+ dialog->registerWidgetHelp(m_ui.readTCOnClose, tr("Read Targets When Closing"), tr("Unchecked"),
+ tr("Flushes all targets in the texture cache back to local memory when shutting down. Can prevent lost visuals when saving "
+ "state or switching renderers, but can also cause graphical corruption."));
}
// Upscaling Fixes tab
diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
index d8249cb8bb..ead2e10fbf 100644
--- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
+++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui
@@ -947,13 +947,6 @@
-
-
-
-
-
- Auto Flush
-
-
-
-
@@ -961,6 +954,13 @@
+ -
+
+
+ Auto Flush
+
+
+
-
@@ -968,10 +968,10 @@
- -
-
+
-
+
- Disable Safe Features
+ Preload Frame Data
@@ -982,10 +982,10 @@
- -
-
+
-
+
- Preload Frame Data
+ Disable Safe Features
@@ -996,6 +996,13 @@
+ -
+
+
+ Read Targets When Closing
+
+
+
-
diff --git a/pcsx2/Config.h b/pcsx2/Config.h
index 9d010785f6..9d7a3eb314 100644
--- a/pcsx2/Config.h
+++ b/pcsx2/Config.h
@@ -664,6 +664,7 @@ struct Pcsx2Config
UserHacks_AlignSpriteX : 1,
UserHacks_AutoFlush : 1,
UserHacks_CPUFBConversion : 1,
+ UserHacks_ReadTCOnClose : 1,
UserHacks_DisableDepthSupport : 1,
UserHacks_DisablePartialInvalidation : 1,
UserHacks_DisableSafeFeatures : 1,
diff --git a/pcsx2/Docs/gamedb-schema.json b/pcsx2/Docs/gamedb-schema.json
index 9946bdef0d..18c8a1357c 100644
--- a/pcsx2/Docs/gamedb-schema.json
+++ b/pcsx2/Docs/gamedb-schema.json
@@ -126,6 +126,11 @@
"minimum": 0,
"maximum": 1
},
+ "readTCOnClose": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1
+ },
"disableDepthSupport": {
"type": "integer",
"minimum": 0,
diff --git a/pcsx2/Frontend/FullscreenUI.cpp b/pcsx2/Frontend/FullscreenUI.cpp
index d24647d1ef..881ea28be1 100644
--- a/pcsx2/Frontend/FullscreenUI.cpp
+++ b/pcsx2/Frontend/FullscreenUI.cpp
@@ -3196,6 +3196,9 @@ void FullscreenUI::DrawGraphicsSettingsPage()
DrawToggleSetting(bsi, "Texture Inside Render Target",
"Allows the texture cache to reuse as an input texture the inner portion of a previous framebuffer.", "EmuCore/GS",
"UserHacks_TextureInsideRt", false, manual_hw_fixes);
+ DrawToggleSetting(bsi, "Read Targets When Closing",
+ "Flushes all targets in the texture cache back to local memory when shutting down.", "EmuCore/GS",
+ "UserHacks_ReadTCOnClose", false, manual_hw_fixes);
MenuHeading("Upscaling Fixes");
DrawIntListSetting(bsi, "Half-Pixel Offset", "Adjusts vertices relative to upscaling.", "EmuCore/GS",
diff --git a/pcsx2/Frontend/ImGuiOverlays.cpp b/pcsx2/Frontend/ImGuiOverlays.cpp
index b710ba6314..2c077bdd0e 100644
--- a/pcsx2/Frontend/ImGuiOverlays.cpp
+++ b/pcsx2/Frontend/ImGuiOverlays.cpp
@@ -421,6 +421,8 @@ void ImGuiManager::DrawSettingsOverlay()
APPEND("AF ");
if (GSConfig.UserHacks_CPUFBConversion)
APPEND("FBC ");
+ if (GSConfig.UserHacks_ReadTCOnClose)
+ APPEND("FTC ");
if(GSConfig.UserHacks_DisableDepthSupport)
APPEND("DDE ");
if (GSConfig.UserHacks_DisablePartialInvalidation)
diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp
index 059b2dfa25..cdf4ed4d9b 100644
--- a/pcsx2/GS/GS.cpp
+++ b/pcsx2/GS/GS.cpp
@@ -252,6 +252,9 @@ bool GSreopen(bool recreate_display, bool recreate_renderer, const Pcsx2Config::
if (recreate_renderer)
g_gs_renderer->Flush(GSState::GSFlushReason::GSREOPEN);
+ if (GSConfig.UserHacks_ReadTCOnClose)
+ g_gs_renderer->ReadbackTextureCache();
+
freezeData fd = {};
std::unique_ptr fd_data;
if (recreate_renderer)
@@ -273,8 +276,8 @@ bool GSreopen(bool recreate_display, bool recreate_renderer, const Pcsx2Config::
else
{
// Make sure nothing is left over.
- g_gs_renderer->PurgePool();
g_gs_renderer->PurgeTextureCache();
+ g_gs_renderer->PurgePool();
}
if (recreate_display)
@@ -762,6 +765,8 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
GSConfig.UserHacks_CPUCLUTRender != old_config.UserHacks_CPUCLUTRender ||
GSConfig.UserHacks_GPUTargetCLUTMode != old_config.UserHacks_GPUTargetCLUTMode)
{
+ if (GSConfig.UserHacks_ReadTCOnClose)
+ g_gs_renderer->ReadbackTextureCache();
g_gs_renderer->PurgeTextureCache();
g_gs_renderer->PurgePool();
}
@@ -800,8 +805,6 @@ void GSSwitchRenderer(GSRendererType new_renderer)
if (existing_api == RenderAPI::OpenGLES)
existing_api = RenderAPI::OpenGL;
- g_gs_renderer->PurgeTextureCache();
-
const bool is_software_switch = (new_renderer == GSRendererType::SW || GSConfig.Renderer == GSRendererType::SW);
const bool recreate_display = (!is_software_switch && existing_api != GetAPIForRenderer(new_renderer));
const Pcsx2Config::GSOptions old_config(GSConfig);
diff --git a/pcsx2/GS/GSState.cpp b/pcsx2/GS/GSState.cpp
index d82ae80cd2..b243b41d6b 100644
--- a/pcsx2/GS/GSState.cpp
+++ b/pcsx2/GS/GSState.cpp
@@ -2126,6 +2126,19 @@ void GSState::ReadLocalMemoryUnsync(u8* mem, int qwc, GIFRegBITBLTBUF BITBLTBUF,
m_mem.ReadImageX(tb.x, tb.y, mem, len, BITBLTBUF, TRXPOS, TRXREG);
}
+void GSState::PurgePool()
+{
+ g_gs_device->PurgePool();
+}
+
+void GSState::PurgeTextureCache()
+{
+}
+
+void GSState::ReadbackTextureCache()
+{
+}
+
template void GSState::Transfer<0>(const u8* mem, u32 size);
template void GSState::Transfer<1>(const u8* mem, u32 size);
template void GSState::Transfer<2>(const u8* mem, u32 size);
@@ -2345,6 +2358,9 @@ int GSState::Freeze(freezeData* fd, bool sizeonly)
Flush(GSFlushReason::SAVESTATE);
+ if (GSConfig.UserHacks_ReadTCOnClose)
+ ReadbackTextureCache();
+
u8* data = fd->data;
const u32 version = STATE_VERSION;
diff --git a/pcsx2/GS/GSState.h b/pcsx2/GS/GSState.h
index 3b7605bf8f..5ae2756da4 100644
--- a/pcsx2/GS/GSState.h
+++ b/pcsx2/GS/GSState.h
@@ -886,7 +886,9 @@ public:
bool TestDrawChanged();
void FlushWrite();
virtual void Draw() = 0;
- virtual void PurgePool() = 0;
+ virtual void PurgePool();
+ virtual void PurgeTextureCache();
+ virtual void ReadbackTextureCache();
virtual void InvalidateVideoMem(const GIFRegBITBLTBUF& BITBLTBUF, const GSVector4i& r, bool eewrite = false) {}
virtual void InvalidateLocalMem(const GIFRegBITBLTBUF& BITBLTBUF, const GSVector4i& r, bool clut = false) {}
virtual void ExpandTarget(const GIFRegBITBLTBUF& BITBLTBUF, const GSVector4i& r) {}
diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
index 3d3647e899..c7a5c5128f 100644
--- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp
+++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
@@ -829,15 +829,6 @@ void GSRenderer::EndCapture()
GSCapture::EndCapture();
}
-void GSRenderer::PurgePool()
-{
- g_gs_device->PurgePool();
-}
-
-void GSRenderer::PurgeTextureCache()
-{
-}
-
GSTexture* GSRenderer::LookupPaletteSource(u32 CBP, u32 CPSM, u32 CBW, GSVector2i& offset, const GSVector2i& size)
{
return nullptr;
diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.h b/pcsx2/GS/Renderers/Common/GSRenderer.h
index fc504d2777..479843740c 100644
--- a/pcsx2/GS/Renderers/Common/GSRenderer.h
+++ b/pcsx2/GS/Renderers/Common/GSRenderer.h
@@ -53,9 +53,6 @@ public:
virtual GSVector2 GetTextureScaleFactor() { return { 1.0f, 1.0f }; }
GSVector2i GetInternalResolution();
- virtual void PurgePool() override;
- virtual void PurgeTextureCache();
-
virtual GSTexture* LookupPaletteSource(u32 CBP, u32 CPSM, u32 CBW, GSVector2i& offset, const GSVector2i& size);
bool SaveSnapshotToMemory(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders,
diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp
index 3b6b33c81a..f1001a3d16 100644
--- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp
+++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp
@@ -71,8 +71,12 @@ void GSRendererHW::Destroy()
void GSRendererHW::PurgeTextureCache()
{
- GSRenderer::PurgeTextureCache();
- m_tc->RemoveAll(true);
+ m_tc->RemoveAll();
+}
+
+void GSRendererHW::ReadbackTextureCache()
+{
+ m_tc->ReadbackAll();
}
GSTexture* GSRendererHW::LookupPaletteSource(u32 CBP, u32 CPSM, u32 CBW, GSVector2i& offset, const GSVector2i& size)
diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.h b/pcsx2/GS/Renderers/HW/GSRendererHW.h
index 4c6c8cf79e..07a5bc5087 100644
--- a/pcsx2/GS/Renderers/HW/GSRendererHW.h
+++ b/pcsx2/GS/Renderers/HW/GSRendererHW.h
@@ -166,6 +166,7 @@ public:
void Draw() override;
void PurgeTextureCache() override;
+ void ReadbackTextureCache() override;
GSTexture* LookupPaletteSource(u32 CBP, u32 CPSM, u32 CBW, GSVector2i& offset, const GSVector2i& size) override;
// Called by the texture cache to know if current texture is useful
diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp
index dc6ebe9939..b375d97499 100644
--- a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp
+++ b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp
@@ -46,18 +46,23 @@ GSTextureCache::~GSTextureCache()
_aligned_free(s_unswizzle_buffer);
}
-void GSTextureCache::RemoveAll(bool readback_targets)
+void GSTextureCache::ReadbackAll()
+{
+ for (int type = 0; type < 2; type++)
+ {
+ for (auto t : m_dst[type])
+ Read(t, t->m_drawn_since_read);
+ }
+}
+
+void GSTextureCache::RemoveAll()
{
m_src.RemoveAll();
for (int type = 0; type < 2; type++)
{
for (auto t : m_dst[type])
- {
- if (readback_targets)
- Read(t, t->m_drawn_since_read);
delete t;
- }
m_dst[type].clear();
}
diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.h b/pcsx2/GS/Renderers/HW/GSTextureCache.h
index 19e89c7d7e..f3877bfde8 100644
--- a/pcsx2/GS/Renderers/HW/GSTextureCache.h
+++ b/pcsx2/GS/Renderers/HW/GSTextureCache.h
@@ -406,7 +406,8 @@ public:
void Read(Target* t, const GSVector4i& r);
void Read(Source* t, const GSVector4i& r);
- void RemoveAll(bool readback_targets = false);
+ void RemoveAll();
+ void ReadbackAll();
void AddDirtyRectTarget(Target* target, GSVector4i rect, u32 psm, u32 bw);
GSTexture* LookupPaletteSource(u32 CBP, u32 CPSM, u32 CBW, GSVector2i& offset, const GSVector2i& size);
diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp
index 0fcc0d8786..15388622f9 100644
--- a/pcsx2/GameDatabase.cpp
+++ b/pcsx2/GameDatabase.cpp
@@ -342,6 +342,7 @@ void GameDatabase::parseAndInsert(const std::string_view& serial, const c4::yml:
static const char* s_gs_hw_fix_names[] = {
"autoFlush",
"cpuFramebufferConversion",
+ "readTCOnClose",
"disableDepthSupport",
"wrapGSMem",
"preloadFrameData",
@@ -548,6 +549,9 @@ bool GameDatabaseSchema::GameEntry::configMatchesHWFix(const Pcsx2Config::GSOpti
case GSHWFixId::CPUFramebufferConversion:
return (static_cast(config.UserHacks_CPUFBConversion) == value);
+ case GSHWFixId::FlushTCOnClose:
+ return (static_cast(config.UserHacks_ReadTCOnClose) == value);
+
case GSHWFixId::DisableDepthSupport:
return (static_cast(config.UserHacks_DisableDepthSupport) == value);
@@ -654,6 +658,10 @@ u32 GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions&
config.UserHacks_CPUFBConversion = (value > 0);
break;
+ case GSHWFixId::FlushTCOnClose:
+ config.UserHacks_ReadTCOnClose = (value > 0);
+ break;
+
case GSHWFixId::DisableDepthSupport:
config.UserHacks_DisableDepthSupport = (value > 0);
break;
diff --git a/pcsx2/GameDatabase.h b/pcsx2/GameDatabase.h
index 2250609d54..340a82054b 100644
--- a/pcsx2/GameDatabase.h
+++ b/pcsx2/GameDatabase.h
@@ -62,6 +62,7 @@ namespace GameDatabaseSchema
// boolean settings
AutoFlush,
CPUFramebufferConversion,
+ FlushTCOnClose,
DisableDepthSupport,
WrapGSMem,
PreloadFrameData,
diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp
index 0b3794e076..62db4165cd 100644
--- a/pcsx2/Pcsx2Config.cpp
+++ b/pcsx2/Pcsx2Config.cpp
@@ -429,6 +429,7 @@ Pcsx2Config::GSOptions::GSOptions()
UserHacks_AlignSpriteX = false;
UserHacks_AutoFlush = false;
UserHacks_CPUFBConversion = false;
+ UserHacks_ReadTCOnClose = false;
UserHacks_DisableDepthSupport = false;
UserHacks_DisablePartialInvalidation = false;
UserHacks_DisableSafeFeatures = false;
@@ -640,6 +641,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap)
GSSettingBoolEx(UserHacks_AlignSpriteX, "UserHacks_align_sprite_X");
GSSettingBoolEx(UserHacks_AutoFlush, "UserHacks_AutoFlush");
GSSettingBoolEx(UserHacks_CPUFBConversion, "UserHacks_CPU_FB_Conversion");
+ GSSettingBoolEx(UserHacks_ReadTCOnClose, "UserHacks_ReadTCOnClose");
GSSettingBoolEx(UserHacks_DisableDepthSupport, "UserHacks_DisableDepthSupport");
GSSettingBoolEx(UserHacks_DisablePartialInvalidation, "UserHacks_DisablePartialInvalidation");
GSSettingBoolEx(UserHacks_DisableSafeFeatures, "UserHacks_Disable_Safe_Features");
@@ -770,6 +772,7 @@ void Pcsx2Config::GSOptions::MaskUserHacks()
UserHacks_DisablePartialInvalidation = false;
UserHacks_DisableDepthSupport = false;
UserHacks_CPUFBConversion = false;
+ UserHacks_ReadTCOnClose = false;
UserHacks_TextureInsideRt = false;
UserHacks_TCOffsetX = 0;
UserHacks_TCOffsetY = 0;