Browse Source

GS: Add texture dumping and replacement system

pull/1/head
Connor McLaughlin 4 months ago committed by refractionpcsx2
parent
commit
5a25cc171d
  1. 12
      pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
  2. 105
      pcsx2-qt/Settings/GraphicsSettingsWidget.ui
  3. 3
      pcsx2/CMakeLists.txt
  4. 9
      pcsx2/Config.h
  5. 18
      pcsx2/GS/GS.cpp
  6. 8
      pcsx2/GS/Renderers/HW/GSRendererHW.cpp
  7. 198
      pcsx2/GS/Renderers/HW/GSTextureCache.cpp
  8. 8
      pcsx2/GS/Renderers/HW/GSTextureCache.h
  9. 642
      pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp
  10. 670
      pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp
  11. 60
      pcsx2/GS/Renderers/HW/GSTextureReplacements.h
  12. 12
      pcsx2/GS/Window/GSwxDialog.cpp
  13. 1
      pcsx2/PathDefs.h
  14. 19
      pcsx2/Pcsx2Config.cpp
  15. 25
      pcsx2/gui/AppConfig.cpp
  16. 6
      pcsx2/gui/AppConfig.h
  17. 1
      pcsx2/gui/AppMain.cpp
  18. 3
      pcsx2/pcsx2.vcxproj
  19. 9
      pcsx2/pcsx2.vcxproj.filters
  20. 3
      pcsx2/pcsx2core.vcxproj
  21. 9
      pcsx2/pcsx2core.vcxproj.filters
  22. 163
      tools/texture_dump_alpha_scaler.py

12
pcsx2-qt/Settings/GraphicsSettingsWidget.cpp

@ -163,6 +163,16 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* @@ -163,6 +163,16 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mergeSprite, "EmuCore/GS", "UserHacks_merge_pp_sprite", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.wildHack, "EmuCore/GS", "UserHacks_WildHack", false);
//////////////////////////////////////////////////////////////////////////
// Texture Replacements
//////////////////////////////////////////////////////////////////////////
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.dumpReplaceableTextures, "EmuCore/GS", "DumpReplaceableTextures", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.dumpReplaceableMipmaps, "EmuCore/GS", "DumpReplaceableMipmaps", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.dumpTexturesWithFMVActive, "EmuCore/GS", "DumpTexturesWithFMVActive", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.loadTextureReplacements, "EmuCore/GS", "LoadTextureReplacements", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.loadTextureReplacementsAsync, "EmuCore/GS", "LoadTextureReplacementsAsync", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.precacheTextureReplacements, "EmuCore/GS", "PrecacheTextureReplacements", false);
//////////////////////////////////////////////////////////////////////////
// Advanced Settings
//////////////////////////////////////////////////////////////////////////
@ -339,7 +349,7 @@ void GraphicsSettingsWidget::updateRendererDependentOptions() @@ -339,7 +349,7 @@ void GraphicsSettingsWidget::updateRendererDependentOptions()
{
// software has no hacks tabs
m_ui.verticalLayout->insertWidget(1, m_ui.softwareRendererGroup);
m_ui.softwareRendererGroup->setCurrentIndex((current_tab >= 4) ? (current_tab - 2) : (current_tab >= 2 ? 1 : current_tab));
m_ui.softwareRendererGroup->setCurrentIndex((current_tab >= 5) ? (current_tab - 3) : (current_tab >= 2 ? 1 : current_tab));
}
m_software_renderer_visible = is_software;

105
pcsx2-qt/Settings/GraphicsSettingsWidget.ui

@ -934,24 +934,93 @@ @@ -934,24 +934,93 @@
<attribute name="title">
<string>Advanced</string>
</attribute>
<layout class="QFormLayout" name="formLayout_6">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="useBlitSwapChain">
<property name="text">
<string>Use Blit Swap Chain</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="useDebugDevice">
<property name="text">
<string>Use Debug Device</string>
</property>
</widget>
</item>
</layout>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Debug Options</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QCheckBox" name="useBlitSwapChain">
<property name="text">
<string>Use Blit Swap Chain</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="useDebugDevice">
<property name="text">
<string>Use Debug Device</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Texture Replacement</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QCheckBox" name="dumpReplaceableTextures">
<property name="text">
<string>Dump Textures</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="dumpReplaceableMipmaps">
<property name="text">
<string>Dump Mipmaps</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="loadTextureReplacementsAsync">
<property name="text">
<string>Async Texture Loading</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="precacheTextureReplacements">
<property name="text">
<string>Precache Textures</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="loadTextureReplacements">
<property name="text">
<string>Load Textures</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="dumpTexturesWithFMVActive">
<property name="text">
<string>Dump FMV Textures</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>

3
pcsx2/CMakeLists.txt

@ -657,6 +657,8 @@ set(pcsx2GSSources @@ -657,6 +657,8 @@ set(pcsx2GSSources
GS/Renderers/HW/GSRendererHW.cpp
GS/Renderers/HW/GSRendererNew.cpp
GS/Renderers/HW/GSTextureCache.cpp
GS/Renderers/HW/GSTextureReplacementLoaders.cpp
GS/Renderers/HW/GSTextureReplacements.cpp
GS/Renderers/SW/GSDrawScanline.cpp
GS/Renderers/SW/GSDrawScanlineCodeGenerator.cpp
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.cpp
@ -721,6 +723,7 @@ set(pcsx2GSHeaders @@ -721,6 +723,7 @@ set(pcsx2GSHeaders
GS/Renderers/HW/GSRendererHW.h
GS/Renderers/HW/GSRendererNew.h
GS/Renderers/HW/GSTextureCache.h
GS/Renderers/HW/GSTextureReplacements.h
GS/Renderers/HW/GSVertexHW.h
GS/Renderers/SW/GSDrawScanlineCodeGenerator.h
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.h

9
pcsx2/Config.h

@ -458,7 +458,13 @@ struct Pcsx2Config @@ -458,7 +458,13 @@ struct Pcsx2Config
SaveRT : 1,
SaveFrame : 1,
SaveTexture : 1,
SaveDepth : 1;
SaveDepth : 1,
DumpReplaceableTextures : 1,
DumpReplaceableMipmaps : 1,
DumpTexturesWithFMVActive : 1,
LoadTextureReplacements : 1,
LoadTextureReplacementsAsync : 1,
PrecacheTextureReplacements : 1;
};
};
@ -886,6 +892,7 @@ namespace EmuFolders @@ -886,6 +892,7 @@ namespace EmuFolders
extern wxDirName Cache;
extern wxDirName Covers;
extern wxDirName GameSettings;
extern wxDirName Textures;
// Assumes that AppRoot and DataRoot have been initialized.
void SetDefaults();

18
pcsx2/GS/GS.cpp

@ -27,6 +27,7 @@ @@ -27,6 +27,7 @@
#include "Renderers/Null/GSDeviceNull.h"
#include "Renderers/OpenGL/GSDeviceOGL.h"
#include "Renderers/HW/GSRendererNew.h"
#include "Renderers/HW/GSTextureReplacements.h"
#include "GSLzma.h"
#include "common/pxStreams.h"
@ -824,6 +825,17 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config) @@ -824,6 +825,17 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
// clear out the sampler cache when AF options change, since the anisotropy gets baked into them
if (GSConfig.MaxAnisotropy != old_config.MaxAnisotropy)
g_gs_device->ClearSamplerCache();
// texture dumping/replacement options
GSTextureReplacements::UpdateConfig(old_config);
// clear the hash texture cache since we might have replacements now
// also clear it when dumping changes, since we want to dump everything being used
if (GSConfig.LoadTextureReplacements != old_config.LoadTextureReplacements ||
GSConfig.DumpReplaceableTextures != old_config.DumpReplaceableTextures)
{
s_gs->PurgeTextureCache();
}
}
void GSSwitchRenderer(GSRendererType new_renderer)
@ -1304,6 +1316,9 @@ void GSApp::Init() @@ -1304,6 +1316,9 @@ void GSApp::Init()
m_default_configuration["disable_shader_cache"] = "0";
m_default_configuration["dithering_ps2"] = "2";
m_default_configuration["dump"] = "0";
m_default_configuration["DumpReplaceableTextures"] = "0";
m_default_configuration["DumpReplaceableMipmaps"] = "0";
m_default_configuration["DumpTexturesWithFMVActive"] = "0";
m_default_configuration["extrathreads"] = "2";
m_default_configuration["extrathreads_height"] = "4";
m_default_configuration["filter"] = std::to_string(static_cast<s8>(BiFiltering::PS2));
@ -1314,6 +1329,8 @@ void GSApp::Init() @@ -1314,6 +1329,8 @@ void GSApp::Init()
m_default_configuration["interlace"] = "7";
m_default_configuration["conservative_framebuffer"] = "1";
m_default_configuration["linear_present"] = "1";
m_default_configuration["LoadTextureReplacements"] = "0";
m_default_configuration["LoadTextureReplacementsAsync"] = "1";
m_default_configuration["MaxAnisotropy"] = "0";
m_default_configuration["mipmap"] = "1";
m_default_configuration["mipmap_hw"] = std::to_string(static_cast<int>(HWMipmapLevel::Automatic));
@ -1341,6 +1358,7 @@ void GSApp::Init() @@ -1341,6 +1358,7 @@ void GSApp::Init()
m_default_configuration["override_GL_ARB_texture_barrier"] = "-1";
m_default_configuration["paltex"] = "0";
m_default_configuration["png_compression_level"] = std::to_string(Z_BEST_SPEED);
m_default_configuration["PrecacheTextureReplacements"] = "0";
m_default_configuration["preload_frame_with_gs_data"] = "0";
m_default_configuration["Renderer"] = std::to_string(static_cast<int>(GSRendererType::Auto));
m_default_configuration["resx"] = "1024";

8
pcsx2/GS/Renderers/HW/GSRendererHW.cpp

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
#include "PrecompiledHeader.h"
#include "GSRendererHW.h"
#include "GSTextureReplacements.h"
#include "GS/GSGL.h"
#include "Host.h"
@ -74,6 +75,7 @@ GSRendererHW::GSRendererHW() @@ -74,6 +75,7 @@ GSRendererHW::GSRendererHW()
}
m_dump_root = root_hw;
GSTextureReplacements::Initialize(m_tc);
}
void GSRendererHW::SetScaling()
@ -189,6 +191,7 @@ GSRendererHW::~GSRendererHW() @@ -189,6 +191,7 @@ GSRendererHW::~GSRendererHW()
void GSRendererHW::Destroy()
{
m_tc->RemoveAll();
GSTextureReplacements::Shutdown();
GSRenderer::Destroy();
}
@ -260,6 +263,8 @@ void GSRendererHW::SetGameCRC(u32 crc, int options) @@ -260,6 +263,8 @@ void GSRendererHW::SetGameCRC(u32 crc, int options)
break;
}
}
GSTextureReplacements::GameChanged();
}
bool GSRendererHW::CanUpscale()
@ -306,6 +311,9 @@ void GSRendererHW::VSync(u32 field, bool registers_written) @@ -306,6 +311,9 @@ void GSRendererHW::VSync(u32 field, bool registers_written)
m_reset = false;
}
if (GSConfig.LoadTextureReplacements)
GSTextureReplacements::ProcessAsyncLoadedTextures();
//Check if the frame buffer width or display width has changed
SetScaling();

198
pcsx2/GS/Renderers/HW/GSTextureCache.cpp

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
#include "PrecompiledHeader.h"
#include "GSTextureCache.h"
#include "GSTextureReplacements.h"
#include "GSRendererHW.h"
#include "GS/GSGL.h"
#include "GS/GSIntrin.h"
@ -65,6 +66,8 @@ GSTextureCache::GSTextureCache(GSRenderer* r) @@ -65,6 +66,8 @@ GSTextureCache::GSTextureCache(GSRenderer* r)
GSTextureCache::~GSTextureCache()
{
GSTextureReplacements::Shutdown();
RemoveAll();
m_surface_offset_cache.clear();
@ -1452,50 +1455,18 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con @@ -1452,50 +1455,18 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con
}
else
{
// maintain the clut even when paltex is on for the dump/replacement texture lookup
bool paltex = (GSConfig.GPUPaletteConversion && psm.pal > 0);
const u32* clut = (psm.pal > 0) ? static_cast<const u32*>(m_renderer->m_mem.m_clut) : nullptr;
// try the hash cache
if (CanCacheTextureSize(TEX0.TW, TEX0.TH))
if ((src->m_from_hash_cache = LookupHashCache(TEX0, TEXA, paltex, clut, lod)) != nullptr)
{
const bool paltex = (GSConfig.GPUPaletteConversion && psm.pal > 0);
const u32* clut = (!paltex && psm.pal > 0) ? static_cast<const u32*>(m_renderer->m_mem.m_clut) : nullptr;
const HashCacheKey key{ HashCacheKey::Create(TEX0, TEXA, m_renderer, clut, lod) };
auto it = m_hash_cache.find(key);
if (it == m_hash_cache.end())
{
// hash and upload texture
src->m_texture = g_gs_device->CreateTexture(tw, th, paltex ? false : (lod != nullptr), paltex ? GSTexture::Format::UNorm8 : GSTexture::Format::Color);
PreloadTexture(TEX0, TEXA, m_renderer->m_mem, paltex, src->m_texture, 0);
// upload mips if present
if (lod)
{
const int basemip = lod->x;
const int nmips = lod->y - lod->x + 1;
for (int mip = 1; mip < nmips; mip++)
{
const GIFRegTEX0 MIP_TEX0{m_renderer->GetTex0Layer(basemip + mip)};
PreloadTexture(MIP_TEX0, TEXA, m_renderer->m_mem, paltex, src->m_texture, mip);
}
}
// insert it into the hash cache
HashCacheEntry entry{ src->m_texture, 1, 0 };
it = m_hash_cache.emplace(key, entry).first;
m_hash_cache_memory_usage += src->m_texture->GetMemUsage();
}
else
{
// use existing texture
src->m_texture = it->second.texture;
it->second.refcount++;
}
src->m_from_hash_cache = &it->second;
src->m_texture = src->m_from_hash_cache->texture;
if (psm.pal > 0)
AttachPaletteToSource(src, psm.pal, paltex);
}
else if (GSConfig.GPUPaletteConversion && psm.pal > 0)
else if (paltex)
{
src->m_texture = g_gs_device->CreateTexture(tw, th, false, GSTexture::Format::UNorm8);
AttachPaletteToSource(src, psm.pal, true);
@ -1517,6 +1488,120 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con @@ -1517,6 +1488,120 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con
return src;
}
// This really needs a better home...
extern bool FMVstarted;
GSTextureCache::HashCacheEntry* GSTextureCache::LookupHashCache(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, bool& paltex, const u32* clut, const GSVector2i* lod)
{
// don't bother hashing if we're not dumping or replacing.
const bool dump = GSConfig.DumpReplaceableTextures && (!FMVstarted || GSConfig.DumpTexturesWithFMVActive);
const bool replace = GSConfig.LoadTextureReplacements;
const bool can_cache = CanCacheTextureSize(TEX0.TW, TEX0.TH);
if (!dump && !replace && !can_cache)
return nullptr;
// need the hash either for replacing, dumping or caching.
// if dumping/replacing is on, we compute the clut hash regardless, since replacements aren't indexed
HashCacheKey key{HashCacheKey::Create(TEX0, TEXA, m_renderer, (dump || replace || !paltex) ? clut : nullptr, lod)};
// handle dumping first, this is mostly isolated.
if (dump)
{
// dump base level
GSTextureReplacements::DumpTexture(key, TEX0, TEXA, m_renderer->m_mem, 0);
// and the mips
if (lod && GSConfig.DumpReplaceableMipmaps)
{
const int basemip = lod->x;
const int nmips = lod->y - lod->x + 1;
for (int mip = 1; mip < nmips; mip++)
{
const GIFRegTEX0 MIP_TEX0{m_renderer->GetTex0Layer(basemip + mip)};
GSTextureReplacements::DumpTexture(key, MIP_TEX0, TEXA, m_renderer->m_mem, mip);
}
}
}
// check with the full key
auto it = m_hash_cache.find(key);
// if this fails, and paltex is on, try indexed texture
const bool needs_second_lookup = paltex && (dump || replace);
if (needs_second_lookup && it == m_hash_cache.end())
it = m_hash_cache.find(key.WithRemovedCLUTHash());
// did we find either a replacement, cached/indexed texture?
if (it != m_hash_cache.end())
{
// super easy, cache hit. remove paltex if it's a replacement texture.
HashCacheEntry* entry = &it->second;
paltex &= (entry->texture->GetFormat() == GSTexture::Format::UNorm8);
entry->refcount++;
return entry;
}
// cache miss.
// check for a replacement texture with the full clut key
if (replace)
{
bool replacement_texture_pending = false;
GSTexture* replacement_tex = GSTextureReplacements::LookupReplacementTexture(key, lod != nullptr, &replacement_texture_pending);
if (replacement_tex)
{
// found a replacement texture! insert it into the hash cache, and clear paltex (since it's not indexed)
const HashCacheEntry entry{replacement_tex, 1u, 0u};
m_hash_cache_memory_usage += replacement_tex->GetMemUsage();
paltex = false;
return &m_hash_cache.emplace(key, entry).first->second;
}
else if (replacement_texture_pending)
{
// we didn't have a texture immediately, but there is a replacement available (and being loaded).
// so clear paltex, since when it gets injected back, it's not going to be indexed
paltex = false;
}
}
// if this texture isn't cacheable, bail out now since we don't want to waste time preloading it
if (!can_cache)
return nullptr;
// expand/upload texture
const int tw = 1 << TEX0.TW;
const int th = 1 << TEX0.TH;
GSTexture* tex = g_gs_device->CreateTexture(tw, th, paltex ? false : (lod != nullptr), paltex ? GSTexture::Format::UNorm8 : GSTexture::Format::Color);
if (!tex)
{
// out of video memory if we hit here
return nullptr;
}
// upload base level
PreloadTexture(TEX0, TEXA, m_renderer->m_mem, paltex, tex, 0);
// upload mips if present
if (lod)
{
const int basemip = lod->x;
const int nmips = lod->y - lod->x + 1;
for (int mip = 1; mip < nmips; mip++)
{
const GIFRegTEX0 MIP_TEX0{m_renderer->GetTex0Layer(basemip + mip)};
PreloadTexture(MIP_TEX0, TEXA, m_renderer->m_mem, paltex, tex, mip);
}
}
// remove the palette hash when using paltex/indexed
if (paltex)
key.RemoveCLUTHash();
// insert into the cache cache, and we're done
const HashCacheEntry entry{tex, 1u, 0u};
m_hash_cache_memory_usage += tex->GetMemUsage();
return &m_hash_cache.emplace(key, entry).first->second;
}
GSTextureCache::Target* GSTextureCache::CreateTarget(const GIFRegTEX0& TEX0, int w, int h, int type, const bool clear)
{
ASSERT(type == RenderTarget || type == DepthStencil);
@ -2388,6 +2473,29 @@ GSTextureCache::SurfaceOffset GSTextureCache::ComputeSurfaceOffset(const Surface @@ -2388,6 +2473,29 @@ GSTextureCache::SurfaceOffset GSTextureCache::ComputeSurfaceOffset(const Surface
return so;
}
void GSTextureCache::InjectHashCacheTexture(const HashCacheKey& key, GSTexture* tex)
{
auto it = m_hash_cache.find(key);
if (it == m_hash_cache.end())
{
// We must've got evicted before we finished loading. No matter, add it in there anyway;
// if it's not used again, it'll get tossed out later.
const HashCacheEntry entry{ tex, 1u, 0u };
m_hash_cache_memory_usage += tex->GetMemUsage();
m_hash_cache.emplace(key, entry).first->second;
return;
}
// Reset age so we don't get thrown out too early.
it->second.age = 0;
// Update memory usage, swap the textures, and recycle the old one for reuse.
m_hash_cache_memory_usage -= it->second.texture->GetMemUsage();
m_hash_cache_memory_usage += tex->GetMemUsage();
it->second.texture->Swap(tex);
g_gs_device->Recycle(tex);
}
// GSTextureCache::Palette
GSTextureCache::Palette::Palette(const GSRenderer* renderer, u16 pal, bool need_gs_texture)
@ -2752,6 +2860,18 @@ GSTextureCache::HashCacheKey GSTextureCache::HashCacheKey::Create(const GIFRegTE @@ -2752,6 +2860,18 @@ GSTextureCache::HashCacheKey GSTextureCache::HashCacheKey::Create(const GIFRegTE
return ret;
}
GSTextureCache::HashCacheKey GSTextureCache::HashCacheKey::WithRemovedCLUTHash() const
{
HashCacheKey ret{*this};
ret.CLUTHash = 0;
return ret;
}
void GSTextureCache::HashCacheKey::RemoveCLUTHash()
{
CLUTHash = 0;
}
u64 GSTextureCache::HashCacheKeyHash::operator()(const HashCacheKey& key) const
{
std::size_t h = 0;

8
pcsx2/GS/Renderers/HW/GSTextureCache.h

@ -53,6 +53,9 @@ public: @@ -53,6 +53,9 @@ public:
static HashCacheKey Create(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSRenderer* renderer, const u32* clut,
const GSVector2i* lod);
HashCacheKey WithRemovedCLUTHash() const;
void RemoveCLUTHash();
__fi bool operator==(const HashCacheKey& e) const { return std::memcmp(this, &e, sizeof(*this)) == 0; }
__fi bool operator!=(const HashCacheKey& e) const { return std::memcmp(this, &e, sizeof(*this)) != 0; }
__fi bool operator<(const HashCacheKey& e) const { return std::memcmp(this, &e, sizeof(*this)) < 0; }
@ -291,6 +294,8 @@ protected: @@ -291,6 +294,8 @@ protected:
Source* CreateSource(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, Target* t = NULL, bool half_right = false, int x_offset = 0, int y_offset = 0, const GSVector2i* lod = nullptr);
Target* CreateTarget(const GIFRegTEX0& TEX0, int w, int h, int type, const bool clear);
HashCacheEntry* LookupHashCache(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, bool& paltex, const u32* clut, const GSVector2i* lod);
static void PreloadTexture(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSLocalMemory& mem, bool paltex, GSTexture* tex, u32 level);
static HashType HashTexture(GSRenderer* renderer, const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA);
@ -335,4 +340,7 @@ public: @@ -335,4 +340,7 @@ public:
SurfaceOffset ComputeSurfaceOffset(const GSOffset& off, const GSVector4i& r, const Target* t);
SurfaceOffset ComputeSurfaceOffset(const uint32_t bp, const uint32_t bw, const uint32_t psm, const GSVector4i& r, const Target* t);
SurfaceOffset ComputeSurfaceOffset(const SurfaceOffsetKey& sok);
/// Injects a texture into the hash cache, by using GSTexture::Swap(), transitively applying to all sources. Ownership of tex is transferred.
void InjectHashCacheTexture(const HashCacheKey& key, GSTexture* tex);
};

642
pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp

@ -0,0 +1,642 @@ @@ -0,0 +1,642 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/Align.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "common/ScopedGuard.h"
#include "GS/Renderers/HW/GSTextureReplacements.h"
#include <csetjmp>
#include <png.h>
struct LoaderDefinition
{
const char* extension;
GSTextureReplacements::ReplacementTextureLoader loader;
};
static bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static constexpr LoaderDefinition s_loaders[] = {
{"png", PNGLoader},
{"dds", DDSLoader},
};
GSTextureReplacements::ReplacementTextureLoader GSTextureReplacements::GetLoader(const std::string_view& filename)
{
const std::string_view extension(FileSystem::GetExtension(filename));
if (extension.empty())
return nullptr;
for (const LoaderDefinition& defn : s_loaders)
{
if (StringUtil::Strncasecmp(extension.data(), defn.extension, extension.size()) == 0)
return defn.loader;
}
return nullptr;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helper routines
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static u32 GetBlockCount(u32 extent, u32 block_size)
{
return std::max(Common::AlignUp(extent, block_size) / block_size, 1u);
}
static void CalcBlockMipmapSize(u32 block_size, u32 bytes_per_block, u32 base_width, u32 base_height, u32 mip, u32& width, u32& height, u32& pitch, u32& size)
{
width = std::max<u32>(base_width >> mip, 1u);
height = std::max<u32>(base_width >> mip, 1u);
const u32 blocks_wide = GetBlockCount(width, block_size);
const u32 blocks_high = GetBlockCount(height, block_size);
// Pitch can't be specified with each mip level, so we have to calculate it ourselves.
pitch = blocks_wide * bytes_per_block;
size = blocks_high * pitch;
}
static void ConvertTexture_X8B8G8R8(u32 width, u32 height, std::vector<u8>& data, u32& pitch)
{
for (u32 row = 0; row < height; row++)
{
u8* data_ptr = data.data() + row * pitch;
for (u32 x = 0; x < width; x++)
{
// Set alpha channel to full intensity.
data_ptr[3] = 0x80;
data_ptr += sizeof(u32);
}
}
}
static void ConvertTexture_A8R8G8B8(u32 width, u32 height, std::vector<u8>& data, u32& pitch)
{
for (u32 row = 0; row < height; row++)
{
u8* data_ptr = data.data() + row * pitch;
for (u32 x = 0; x < width; x++)
{
// Byte swap ABGR -> RGBA
u32 val;
std::memcpy(&val, data_ptr, sizeof(val));
val = ((val & 0xFF00FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000));
std::memcpy(data_ptr, &val, sizeof(u32));
data_ptr += sizeof(u32);
}
}
}
static void ConvertTexture_X8R8G8B8(u32 width, u32 height, std::vector<u8>& data, u32& pitch)
{
for (u32 row = 0; row < height; row++)
{
u8* data_ptr = data.data() + row * pitch;
for (u32 x = 0; x < width; x++)
{
// Byte swap XBGR -> RGBX, and set alpha to full intensity.
u32 val;
std::memcpy(&val, data_ptr, sizeof(val));
val = ((val & 0x0000FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000)) | 0xFF000000;
std::memcpy(data_ptr, &val, sizeof(u32));
data_ptr += sizeof(u32);
}
}
}
static void ConvertTexture_R8G8B8(u32 width, u32 height, std::vector<u8>& data, u32& pitch)
{
const u32 new_pitch = width * sizeof(u32);
std::vector<u8> new_data(new_pitch * height);
for (u32 row = 0; row < height; row++)
{
const u8* rgb_data_ptr = data.data() + row * pitch;
u8* data_ptr = new_data.data() + row * new_pitch;
for (u32 x = 0; x < width; x++)
{
// This is BGR in memory.
u32 val;
std::memcpy(&val, rgb_data_ptr, sizeof(val));
val = ((val & 0x0000FF00) | ((val >> 16) & 0xFF) | ((val << 16) & 0xFF0000)) | 0xFF000000;
std::memcpy(data_ptr, &val, sizeof(u32));
data_ptr += sizeof(u32);
rgb_data_ptr += 3;
}
}
data = std::move(new_data);
pitch = new_pitch;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PNG Handlers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
return false;
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
});
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "rb");
if (!fp)
return false;
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_init_io(png_ptr, fp.get());
png_read_info(png_ptr, info_ptr);
png_uint_32 width = 0;
png_uint_32 height = 0;
int bitDepth = 0;
int colorType = -1;
if (png_get_IHDR(png_ptr, info_ptr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr) != 1 ||
width == 0 || height == 0)
{
return false;
}
const u32 pitch = width * sizeof(u32);
tex->width = width;
tex->height = height;
tex->format = GSTexture::Format::Color;
tex->pitch = pitch;
tex->data.resize(pitch * height);
const png_uint_32 row_bytes = png_get_rowbytes(png_ptr, info_ptr);
std::vector<u8> row_data(row_bytes);
for (u32 y = 0; y < height; y++)
{
png_read_row(png_ptr, static_cast<png_bytep>(row_data.data()), nullptr);
const u8* row_ptr = row_data.data();
u8* out_ptr = tex->data.data() + y * pitch;
if (colorType == PNG_COLOR_TYPE_RGB)
{
for (u32 x = 0; x < width; x++)
{
u32 pixel = static_cast<u32>(*(row_ptr)++);
pixel |= static_cast<u32>(*(row_ptr)++) << 8;
pixel |= static_cast<u32>(*(row_ptr)++) << 16;
pixel |= 0x80000000u; // make opaque
std::memcpy(out_ptr, &pixel, sizeof(pixel));
out_ptr += sizeof(pixel);
}
}
else if (colorType == PNG_COLOR_TYPE_RGBA)
{
std::memcpy(out_ptr, row_ptr, pitch);
}
}
return true;
}
bool GSTextureReplacements::SavePNGImage(const std::string& filename, u32 width, u32 height, const u8* buffer, u32 pitch)
{
const int compression = theApp.GetConfigI("png_compression_level");
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
return false;
png_infop info_ptr = png_create_info_struct(png_ptr);
if (info_ptr == nullptr)
{
png_destroy_write_struct(&png_ptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
png_destroy_write_struct(&png_ptr, &info_ptr);
});
if (setjmp(png_jmpbuf(png_ptr)))
return false;
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "wb");
if (!fp)
return false;
png_init_io(png_ptr, fp.get());
png_set_compression_level(png_ptr, compression);
png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
png_set_swap(png_ptr);
for (u32 y = 0; y < height; ++y)
{
// cast is needed here for mac builder
png_write_row(png_ptr, (png_bytep)(buffer + y * pitch));
}
png_write_end(png_ptr, nullptr);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// DDS Handler
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// From https://raw.githubusercontent.com/Microsoft/DirectXTex/master/DirectXTex/DDS.h
//
// This header defines constants and structures that are useful when parsing
// DDS files. DDS files were originally designed to use several structures
// and constants that are native to DirectDraw and are defined in ddraw.h,
// such as DDSURFACEDESC2 and DDSCAPS2. This file defines similar
// (compatible) constants and structures so that one can use DDS files
// without needing to include ddraw.h.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// http://go.microsoft.com/fwlink/?LinkId=248926
#pragma pack(push, 1)
static constexpr uint32_t DDS_MAGIC = 0x20534444; // "DDS "
struct DDS_PIXELFORMAT
{
uint32_t dwSize;
uint32_t dwFlags;
uint32_t dwFourCC;
uint32_t dwRGBBitCount;
uint32_t dwRBitMask;
uint32_t dwGBitMask;
uint32_t dwBBitMask;
uint32_t dwABitMask;
};
#define DDS_FOURCC 0x00000004 // DDPF_FOURCC
#define DDS_RGB 0x00000040 // DDPF_RGB
#define DDS_RGBA 0x00000041 // DDPF_RGB | DDPF_ALPHAPIXELS
#define DDS_LUMINANCE 0x00020000 // DDPF_LUMINANCE
#define DDS_LUMINANCEA 0x00020001 // DDPF_LUMINANCE | DDPF_ALPHAPIXELS
#define DDS_ALPHA 0x00000002 // DDPF_ALPHA
#define DDS_PAL8 0x00000020 // DDPF_PALETTEINDEXED8
#define DDS_PAL8A 0x00000021 // DDPF_PALETTEINDEXED8 | DDPF_ALPHAPIXELS
#define DDS_BUMPDUDV 0x00080000 // DDPF_BUMPDUDV
#ifndef MAKEFOURCC
#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) | ((uint32_t)(uint8_t)(ch2) << 16) | \
((uint32_t)(uint8_t)(ch3) << 24))
#endif /* defined(MAKEFOURCC) */
#define DDS_HEADER_FLAGS_TEXTURE \
0x00001007 // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
#define DDS_HEADER_FLAGS_MIPMAP 0x00020000 // DDSD_MIPMAPCOUNT
#define DDS_HEADER_FLAGS_VOLUME 0x00800000 // DDSD_DEPTH
#define DDS_HEADER_FLAGS_PITCH 0x00000008 // DDSD_PITCH
#define DDS_HEADER_FLAGS_LINEARSIZE 0x00080000 // DDSD_LINEARSIZE
// Subset here matches D3D10_RESOURCE_DIMENSION and D3D11_RESOURCE_DIMENSION
enum DDS_RESOURCE_DIMENSION
{
DDS_DIMENSION_TEXTURE1D = 2,
DDS_DIMENSION_TEXTURE2D = 3,
DDS_DIMENSION_TEXTURE3D = 4,
};
struct DDS_HEADER
{
uint32_t dwSize;
uint32_t dwFlags;
uint32_t dwHeight;
uint32_t dwWidth;
uint32_t dwPitchOrLinearSize;
uint32_t dwDepth; // only if DDS_HEADER_FLAGS_VOLUME is set in dwFlags
uint32_t dwMipMapCount;
uint32_t dwReserved1[11];
DDS_PIXELFORMAT ddspf;
uint32_t dwCaps;
uint32_t dwCaps2;
uint32_t dwCaps3;
uint32_t dwCaps4;
uint32_t dwReserved2;
};
struct DDS_HEADER_DXT10
{
uint32_t dxgiFormat;
uint32_t resourceDimension;
uint32_t miscFlag; // see DDS_RESOURCE_MISC_FLAG
uint32_t arraySize;
uint32_t miscFlags2; // see DDS_MISC_FLAGS2
};
#pragma pack(pop)
static_assert(sizeof(DDS_HEADER) == 124, "DDS Header size mismatch");
static_assert(sizeof(DDS_HEADER_DXT10) == 20, "DDS DX10 Extended Header size mismatch");
constexpr DDS_PIXELFORMAT DDSPF_A8R8G8B8 = {
sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000};
constexpr DDS_PIXELFORMAT DDSPF_X8R8G8B8 = {
sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000};
constexpr DDS_PIXELFORMAT DDSPF_A8B8G8R8 = {
sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000};
constexpr DDS_PIXELFORMAT DDSPF_X8B8G8R8 = {
sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0x00000000};
constexpr DDS_PIXELFORMAT DDSPF_R8G8B8 = {
sizeof(DDS_PIXELFORMAT), DDS_RGB, 0, 24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000};
// End of Microsoft code from DDS.h.
static bool DDSPixelFormatMatches(const DDS_PIXELFORMAT& pf1, const DDS_PIXELFORMAT& pf2)
{
return std::tie(pf1.dwSize, pf1.dwFlags, pf1.dwFourCC, pf1.dwRGBBitCount, pf1.dwRBitMask,
pf1.dwGBitMask, pf1.dwGBitMask, pf1.dwBBitMask, pf1.dwABitMask) ==
std::tie(pf2.dwSize, pf2.dwFlags, pf2.dwFourCC, pf2.dwRGBBitCount, pf2.dwRBitMask,
pf2.dwGBitMask, pf2.dwGBitMask, pf2.dwBBitMask, pf2.dwABitMask);
}
struct DDSLoadInfo
{
u32 block_size = 1;
u32 bytes_per_block = 4;
u32 width = 0;
u32 height = 0;
u32 mip_count = 0;
GSTexture::Format format = GSTexture::Format::Color;
s64 base_image_offset = 0;
u32 base_image_size = 0;
u32 base_image_pitch = 0;
std::function<void(u32 width, u32 height, std::vector<u8>& data, u32& pitch)> conversion_function;
};
static bool ParseDDSHeader(std::FILE* fp, DDSLoadInfo* info)
{
u32 magic;
if (std::fread(&magic, sizeof(magic), 1, fp) != 1 || magic != DDS_MAGIC)
return false;
DDS_HEADER header;
u32 header_size = sizeof(header);
if (std::fread(&header, header_size, 1, fp) != 1 || header.dwSize < header_size)
return false;
// Required fields.
if ((header.dwFlags & DDS_HEADER_FLAGS_TEXTURE) != DDS_HEADER_FLAGS_TEXTURE)
return false;
// Image should be 2D.
if (header.dwFlags & DDS_HEADER_FLAGS_VOLUME)
return false;
// Presence of width/height fields is already tested by DDS_HEADER_FLAGS_TEXTURE.
info->width = header.dwWidth;
info->height = header.dwHeight;
if (info->width == 0 || info->height == 0)
return false;
// Check for mip levels.
if (header.dwFlags & DDS_HEADER_FLAGS_MIPMAP)
{
info->mip_count = header.dwMipMapCount;
if (header.dwMipMapCount != 0)
info->mip_count = header.dwMipMapCount;
else
info->mip_count = GSTextureReplacements::CalcMipmapLevelsForReplacement(info->width, info->height);
}
else
{
info->mip_count = 1;
}
// Handle fourcc formats vs uncompressed formats.
const bool has_fourcc = (header.ddspf.dwFlags & DDS_FOURCC) != 0;
if (has_fourcc)
{
// Handle DX10 extension header.
u32 dxt10_format = 0;
if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', '1', '0'))
{
DDS_HEADER_DXT10 dxt10_header;
if (std::fread(&dxt10_header, sizeof(dxt10_header), 1, fp) != 1)
return false;
// Can't handle array textures here. Doesn't make sense to use them, anyway.
if (dxt10_header.resourceDimension != DDS_DIMENSION_TEXTURE2D || dxt10_header.arraySize != 1)
return false;
header_size += sizeof(dxt10_header);
dxt10_format = dxt10_header.dxgiFormat;
}
const GSDevice::FeatureSupport features(g_gs_device->Features());
if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '1') || dxt10_format == 71)
{
info->format = GSTexture::Format::BC1;
info->block_size = 4;
info->bytes_per_block = 8;
if (!features.dxt_textures)
return false;
}
else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '2') || header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '3') || dxt10_format == 74)
{
info->format = GSTexture::Format::BC2;
info->block_size = 4;
info->bytes_per_block = 16;
if (!features.dxt_textures)
return false;
}
else if (header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '4') || header.ddspf.dwFourCC == MAKEFOURCC('D', 'X', 'T', '5') || dxt10_format == 77)
{
info->format = GSTexture::Format::BC3;
info->block_size = 4;
info->bytes_per_block = 16;
if (!features.dxt_textures)
return false;
}
else if (dxt10_format == 98)
{
info->format = GSTexture::Format::BC7;
info->block_size = 4;
info->bytes_per_block = 16;
if (!features.bptc_textures)
return false;
}
else
{
// Leave all remaining formats to SOIL.
return false;
}
}
else
{
if (DDSPixelFormatMatches(header.ddspf, DDSPF_A8R8G8B8))
{
info->conversion_function = ConvertTexture_A8R8G8B8;
}
else if (DDSPixelFormatMatches(header.ddspf, DDSPF_X8R8G8B8))
{
info->conversion_function = ConvertTexture_X8R8G8B8;
}
else if (DDSPixelFormatMatches(header.ddspf, DDSPF_X8B8G8R8))
{
info->conversion_function = ConvertTexture_X8B8G8R8;
}
else if (DDSPixelFormatMatches(header.ddspf, DDSPF_R8G8B8))
{
info->conversion_function = ConvertTexture_R8G8B8;
}
else if (DDSPixelFormatMatches(header.ddspf, DDSPF_A8B8G8R8))
{
// This format is already in RGBA order, so no conversion necessary.
}
else
{
return false;
}
// All these formats are RGBA, just with byte swapping.
info->format = GSTexture::Format::Color;
info->block_size = 1;
info->bytes_per_block = header.ddspf.dwRGBBitCount / 8;
}
// Mip levels smaller than the block size are padded to multiples of the block size.
const u32 blocks_wide = GetBlockCount(info->width, info->block_size);
const u32 blocks_high = GetBlockCount(info->height, info->block_size);
// Pitch can be specified in the header, otherwise we can derive it from the dimensions. For
// compressed formats, both DDS_HEADER_FLAGS_LINEARSIZE and DDS_HEADER_FLAGS_PITCH should be
// set. See https://msdn.microsoft.com/en-us/library/windows/desktop/bb943982(v=vs.85).aspx
if (header.dwFlags & DDS_HEADER_FLAGS_PITCH && header.dwFlags & DDS_HEADER_FLAGS_LINEARSIZE)
{
// Convert pitch (in bytes) to texels/row length.
if (header.dwPitchOrLinearSize < info->bytes_per_block)
{
// Likely a corrupted or invalid file.
return false;
}
info->base_image_pitch = header.dwPitchOrLinearSize;
info->base_image_size = info->base_image_pitch * blocks_high;
}
else
{
// Assume no padding between rows of blocks.
info->base_image_pitch = blocks_wide * info->bytes_per_block;
info->base_image_size = info->base_image_pitch * blocks_high;
}
// Check for truncated or corrupted files.
info->base_image_offset = sizeof(magic) + header_size;
if (info->base_image_offset >= FileSystem::FSize64(fp))
return false;
return true;
}
static bool ReadDDSMipLevel(std::FILE* fp, const std::string& filename, u32 mip_level, const DDSLoadInfo& info, u32 width, u32 height, std::vector<u8>& data, u32& pitch, u32 size)
{
// D3D11 cannot handle block compressed textures where the first mip level is
// not a multiple of the block size.
if (mip_level == 0 && info.block_size > 1 &&
((width % info.block_size) != 0 || (height % info.block_size) != 0))
{
Console.Error(
"Invalid dimensions for DDS texture %s. For compressed textures of this format, "
"the width/height of the first mip level must be a multiple of %u.",
filename.c_str(), info.block_size);
return false;
}
data.resize(size);
if (std::fread(data.data(), size, 1, fp) != 1)
return false;
// Apply conversion function for uncompressed textures.
if (info.conversion_function)
info.conversion_function(width, height, data, pitch);
return true;
}
bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
{
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "rb");
if (!fp)
return false;
DDSLoadInfo info;
if (!ParseDDSHeader(fp.get(), &info))
return false;
// always load the base image
if (FileSystem::FSeek64(fp.get(), info.base_image_offset, SEEK_SET) != 0)
return false;
tex->format = info.format;
tex->width = info.width;
tex->height = info.height;
tex->pitch = info.base_image_pitch;
if (!ReadDDSMipLevel(fp.get(), filename, 0, info, tex->width, tex->height, tex->data, tex->pitch, info.base_image_size))
return false;
// Read in any remaining mip levels in the file.
if (!only_base_image)
{
for (u32 level = 1; level <= info.mip_count; level++)
{
GSTextureReplacements::ReplacementTexture::MipData md;
u32 mip_width, mip_height, mip_size;
CalcBlockMipmapSize(info.block_size, info.bytes_per_block, info.width, info.height, level, mip_width, mip_height, md.pitch, mip_size);
if (!ReadDDSMipLevel(fp.get(), filename, level, info, mip_width, mip_height, md.data, md.pitch, mip_size))
break;
tex->mips.push_back(std::move(md));
}
}
return true;
}

670
pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp

@ -0,0 +1,670 @@ @@ -0,0 +1,670 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/HashCombine.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "common/ScopedGuard.h"
#include "Config.h"
#include "GS/GSLocalMemory.h"
#include "GS/Renderers/HW/GSTextureReplacements.h"
#ifndef PCSX2_CORE
#include "gui/AppCoreThread.h"
#else
#include "VMManager.h"
#endif
#include <cinttypes>
#include <cstring>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <tuple>
#include <thread>
// this is a #define instead of a variable to avoid warnings from non-literal format strings
#define TEXTURE_FILENAME_FORMAT_STRING "%" PRIx64 "-%08x"
#define TEXTURE_FILENAME_CLUT_FORMAT_STRING "%" PRIx64 "-%" PRIx64 "-%08x"
#define TEXTURE_REPLACEMENT_SUBDIRECTORY_NAME "replacements"
#define TEXTURE_DUMP_SUBDIRECTORY_NAME "dumps"
namespace
{
struct TextureName // 24 bytes
{
u64 TEX0Hash;
u64 CLUTHash;
union
{
struct
{
u32 TEX0_PSM : 6;
u32 TEX0_TW : 4;
u32 TEX0_TH : 4;
u32 TEX0_TCC : 1;
u32 TEXA_TA0 : 8;
u32 TEXA_AEM : 1;
u32 TEXA_TA1 : 8;
};
u32 bits;
};
u32 miplevel;
__fi u32 Width() const { return (1u << TEX0_TW); }
__fi u32 Height() const { return (1u << TEX0_TH); }
__fi bool HasPalette() const { return (GSLocalMemory::m_psm[TEX0_PSM].pal > 0); }
__fi GSVector2 ReplacementScale(const GSTextureReplacements::ReplacementTexture& rtex) const
{
return ReplacementScale(rtex.width, rtex.height);
}
__fi GSVector2 ReplacementScale(u32 rwidth, u32 rheight) const
{
return GSVector2(static_cast<float>(rwidth) / static_cast<float>(Width()), static_cast<float>(rheight) / static_cast<float>(Height()));
}
__fi bool operator==(const TextureName& rhs) const { return std::tie(TEX0Hash, CLUTHash, bits) == std::tie(rhs.TEX0Hash, rhs.CLUTHash, rhs.bits); }
__fi bool operator!=(const TextureName& rhs) const { return std::tie(TEX0Hash, CLUTHash, bits) != std::tie(rhs.TEX0Hash, rhs.CLUTHash, rhs.bits); }
__fi bool operator<(const TextureName& rhs) const { return std::tie(TEX0Hash, CLUTHash, bits) < std::tie(rhs.TEX0Hash, rhs.CLUTHash, rhs.bits); }
};
static_assert(sizeof(TextureName) == 24, "ReplacementTextureName is expected size");
} // namespace
namespace std
{
template <>
struct hash<TextureName>
{
std::size_t operator()(const TextureName& val) const
{
std::size_t h = 0;
HashCombine(h, val.TEX0Hash, val.CLUTHash, val.bits, val.miplevel);
return h;
}
};
} // namespace std
namespace GSTextureReplacements
{
static TextureName CreateTextureName(const GSTextureCache::HashCacheKey& hash, u32 miplevel);
static GSTextureCache::HashCacheKey HashCacheKeyFromTextureName(const TextureName& tn);
static std::optional<TextureName> ParseReplacementName(const std::string& filename);
static std::string GetGameTextureDirectory();
static std::string GetDumpFilename(const TextureName& name, u32 level);
static std::string GetGameSerial();
static std::optional<ReplacementTexture> LoadReplacementTexture(const TextureName& name, const std::string& filename, bool only_base_image);
static void QueueAsyncReplacementTextureLoad(const TextureName& name, const std::string& filename, bool mipmap);
static void PrecacheReplacementTextures();
static void ClearReplacementTextures();
static void StartWorkerThread();
static void StopWorkerThread();
static void QueueWorkerThreadItem(std::function<void()> fn);
static void WorkerThreadEntryPoint();
static void SyncWorkerThread();
static void CancelPendingLoadsAndDumps();
static std::string s_current_serial;
/// Backreference to the texture cache so we can inject replacements.
static GSTextureCache* s_tc;
/// Textures that have been dumped, to save stat() calls.
static std::unordered_set<TextureName> s_dumped_textures;
/// Lookup map of texture names to replacements, if they exist.
static std::unordered_map<TextureName, std::string> s_replacement_texture_filenames;
/// Lookup map of texture names to replacement data which has been cached.
static std::unordered_map<TextureName, ReplacementTexture> s_replacement_texture_cache;
static std::mutex s_replacement_texture_cache_mutex;
/// List of textures that are pending asynchronous load.
static std::unordered_set<TextureName> s_pending_async_load_textures;
/// List of textures that we have asynchronously loaded and can now be injected back into the TC.
/// Second element is whether the texture should be created with mipmaps.
static std::vector<std::pair<TextureName, bool>> s_async_loaded_textures;
/// Loader/dumper thread.
static std::thread s_worker_thread;
static std::mutex s_worker_thread_mutex;
static std::condition_variable s_worker_thread_cv;
static std::queue<std::function<void()>> s_worker_thread_queue;
static bool s_worker_thread_running = false;
}; // namespace GSTextureReplacements
TextureName GSTextureReplacements::CreateTextureName(