GS: Add texture dumping and replacement system

This commit is contained in:
Connor McLaughlin 2022-02-20 00:20:18 +10:00 committed by refractionpcsx2
parent 32dc68f103
commit 5a25cc171d
22 changed files with 1923 additions and 61 deletions

View File

@ -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()
{
// 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;

View File

@ -934,9 +934,13 @@
<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">
<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">
@ -952,6 +956,71 @@
</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>

View File

@ -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
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

View File

@ -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
extern wxDirName Cache;
extern wxDirName Covers;
extern wxDirName GameSettings;
extern wxDirName Textures;
// Assumes that AppRoot and DataRoot have been initialized.
void SetDefaults();

View File

@ -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)
// 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()
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()
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()
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";

View File

@ -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()
}
m_dump_root = root_hw;
GSTextureReplacements::Initialize(m_tc);
}
void GSRendererHW::SetScaling()
@ -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)
break;
}
}
GSTextureReplacements::GameChanged();
}
bool GSRendererHW::CanUpscale()
@ -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();

View File

@ -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)
GSTextureCache::~GSTextureCache()
{
GSTextureReplacements::Shutdown();
RemoveAll();
m_surface_offset_cache.clear();
@ -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
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
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
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;

View File

@ -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:
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:
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);
};

View File

@ -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;
}

View File

@ -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(const GSTextureCache::HashCacheKey& hash, u32 miplevel)
{
TextureName name;
name.bits = 0;
name.TEX0_PSM = hash.TEX0.PSM;
name.TEX0_TW = hash.TEX0.TW;
name.TEX0_TH = hash.TEX0.TH;
name.TEX0_TCC = hash.TEX0.TCC;
name.TEXA_TA0 = hash.TEXA.TA0;
name.TEXA_AEM = hash.TEXA.AEM;
name.TEXA_TA1 = hash.TEXA.TA1;
name.TEX0Hash = hash.TEX0Hash;
name.CLUTHash = name.HasPalette() ? hash.CLUTHash : 0;
name.miplevel = miplevel;
return name;
}
GSTextureCache::HashCacheKey GSTextureReplacements::HashCacheKeyFromTextureName(const TextureName& tn)
{
GSTextureCache::HashCacheKey key = {};
key.TEX0.PSM = tn.TEX0_PSM;
key.TEX0.TW = tn.TEX0_TW;
key.TEX0.TH = tn.TEX0_TH;
key.TEX0.TCC = tn.TEX0_TCC;
key.TEXA.TA0 = tn.TEXA_TA0;
key.TEXA.AEM = tn.TEXA_AEM;
key.TEXA.TA1 = tn.TEXA_TA1;
key.TEX0Hash = tn.TEX0Hash;
key.CLUTHash = tn.HasPalette() ? tn.CLUTHash : 0;
return key;
}
std::optional<TextureName> GSTextureReplacements::ParseReplacementName(const std::string& filename)
{
TextureName ret;
ret.miplevel = 0;
// TODO(Stenzek): Make this better.
char extension_dot;
if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, &ret.bits, &extension_dot) != 4 || extension_dot != '.')
{
if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.bits, &extension_dot) != 3 || extension_dot != '.')
return std::nullopt;
}
return ret;
}
std::string GSTextureReplacements::GetGameTextureDirectory()
{
return Path::CombineStdString(EmuFolders::Textures, s_current_serial);
}
std::string GSTextureReplacements::GetDumpFilename(const TextureName& name, u32 level)
{
std::string ret;
if (s_current_serial.empty())
return ret;
const std::string game_dir(GetGameTextureDirectory());
if (!FileSystem::DirectoryExists(game_dir.c_str()))
{
// create both dumps and replacements
if (!FileSystem::CreateDirectoryPath(game_dir.c_str(), false) ||
!FileSystem::EnsureDirectoryExists(Path::CombineStdString(game_dir, "dumps").c_str(), false) ||
!FileSystem::EnsureDirectoryExists(Path::CombineStdString(game_dir, "replacements").c_str(), false))
{
// if it fails to create, we're not going to be able to use it anyway
return ret;
}
}
const std::string game_subdir(Path::CombineStdString(game_dir, TEXTURE_DUMP_SUBDIRECTORY_NAME));
if (name.HasPalette())
{
const std::string filename(
(level > 0) ?
StringUtil::StdStringFromFormat(TEXTURE_FILENAME_CLUT_FORMAT_STRING "-mip%u.png", name.TEX0Hash, name.CLUTHash, name.bits, level) :
StringUtil::StdStringFromFormat(TEXTURE_FILENAME_CLUT_FORMAT_STRING ".png", name.TEX0Hash, name.CLUTHash, name.bits));
ret = Path::CombineStdString(game_subdir, filename);
}
else
{
const std::string filename(
(level > 0) ?
StringUtil::StdStringFromFormat(TEXTURE_FILENAME_FORMAT_STRING "-mip%u.png", name.TEX0Hash, name.bits, level) :
StringUtil::StdStringFromFormat(TEXTURE_FILENAME_FORMAT_STRING ".png", name.TEX0Hash, name.bits));
ret = Path::CombineStdString(game_subdir, filename);
}
return ret;
}
std::string GSTextureReplacements::GetGameSerial()
{
#ifndef PCSX2_CORE
return StringUtil::wxStringToUTF8String(GameInfo::gameSerial);
#else
return VMManager::GetGameSerial();
#endif
}
void GSTextureReplacements::Initialize(GSTextureCache* tc)
{
s_tc = tc;
s_current_serial = GetGameSerial();
if (GSConfig.DumpReplaceableTextures || GSConfig.LoadTextureReplacements)
StartWorkerThread();
ReloadReplacementMap();
}
void GSTextureReplacements::GameChanged()
{
if (!s_tc)
return;
std::string new_serial(GetGameSerial());
if (s_current_serial == new_serial)
return;
s_current_serial = std::move(new_serial);
ReloadReplacementMap();
ClearDumpedTextureList();
}
void GSTextureReplacements::ReloadReplacementMap()
{
SyncWorkerThread();
// clear out the caches
{
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
s_replacement_texture_cache.clear();
s_replacement_texture_filenames.clear();
s_pending_async_load_textures.clear();
s_async_loaded_textures.clear();
}
// can't replace bios textures.
if (s_current_serial.empty() || !GSConfig.LoadTextureReplacements)
return;
const std::string replacement_dir(Path::CombineStdString(GetGameTextureDirectory(), TEXTURE_REPLACEMENT_SUBDIRECTORY_NAME));
FileSystem::FindResultsArray files;
if (!FileSystem::FindFiles(replacement_dir.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RECURSIVE, &files))
return;
std::string filename;
for (FILESYSTEM_FIND_DATA& fd : files)
{
// file format we can handle?
filename = FileSystem::GetFileNameFromPath(fd.FileName);
if (!GetLoader(filename))
continue;
// parse the name if it's valid
std::optional<TextureName> name = ParseReplacementName(filename);
if (!name.has_value())
continue;
DevCon.WriteLn("Found %ux%u replacement '%*s'", name->Width(), name->Height(), static_cast<int>(filename.size()), filename.data());
s_replacement_texture_filenames.emplace(std::move(name.value()), std::move(fd.FileName));
}
if (GSConfig.PrecacheTextureReplacements)
PrecacheReplacementTextures();
}
void GSTextureReplacements::UpdateConfig(Pcsx2Config::GSOptions& old_config)
{
// get rid of worker thread if it's no longer needed
if (s_worker_thread_running && !GSConfig.DumpReplaceableTextures && !GSConfig.LoadTextureReplacements)
StopWorkerThread();
if (!s_worker_thread_running && (GSConfig.DumpReplaceableTextures || GSConfig.LoadTextureReplacements))
StartWorkerThread();
if ((!GSConfig.DumpReplaceableTextures && old_config.DumpReplaceableTextures) ||
(!GSConfig.LoadTextureReplacements && old_config.LoadTextureReplacements))
{
CancelPendingLoadsAndDumps();
}
if (GSConfig.LoadTextureReplacements && !old_config.LoadTextureReplacements)
ReloadReplacementMap();
else if (!GSConfig.LoadTextureReplacements && old_config.LoadTextureReplacements)
ClearReplacementTextures();
if (!GSConfig.DumpReplaceableTextures && old_config.DumpReplaceableTextures)
ClearDumpedTextureList();
if (GSConfig.LoadTextureReplacements && GSConfig.PrecacheTextureReplacements && !old_config.PrecacheTextureReplacements)
PrecacheReplacementTextures();
}
void GSTextureReplacements::Shutdown()
{
StopWorkerThread();
std::string().swap(s_current_serial);
ClearReplacementTextures();
ClearDumpedTextureList();
s_tc = nullptr;
}
u32 GSTextureReplacements::CalcMipmapLevelsForReplacement(u32 width, u32 height)
{
return static_cast<u32>(std::log2(std::max(width, height))) + 1u;
}
GSTexture* GSTextureReplacements::LookupReplacementTexture(const GSTextureCache::HashCacheKey& hash, bool mipmap, bool* pending)
{
const TextureName name(CreateTextureName(hash, 0));
*pending = false;
// replacement for this name exists?
auto fnit = s_replacement_texture_filenames.find(name);
if (fnit == s_replacement_texture_filenames.end())
return nullptr;
// try the full cache first, to avoid reloading from disk
{
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
auto it = s_replacement_texture_cache.find(name);
if (it != s_replacement_texture_cache.end())
{
// replacement is cached, can immediately upload to host GPU
return CreateReplacementTexture(it->second, name.ReplacementScale(it->second), mipmap);
}
}
// load asynchronously?
if (GSConfig.LoadTextureReplacementsAsync)
{
// replacement will be injected into the TC later on
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
QueueAsyncReplacementTextureLoad(name, fnit->second, mipmap);
*pending = true;
return nullptr;
}
else
{
// synchronous load
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(name, fnit->second, !mipmap));
if (!replacement.has_value())
return nullptr;
// insert into cache
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
const ReplacementTexture& rtex = s_replacement_texture_cache.emplace(name, std::move(replacement.value())).first->second;
// and upload to gpu
return CreateReplacementTexture(rtex, name.ReplacementScale(rtex), mipmap);
}
}
std::optional<GSTextureReplacements::ReplacementTexture> GSTextureReplacements::LoadReplacementTexture(const TextureName& name, const std::string& filename, bool only_base_image)
{
ReplacementTextureLoader loader = GetLoader(filename);
if (!loader)
return std::nullopt;
ReplacementTexture rtex;
if (!loader(filename.c_str(), &rtex, only_base_image))
return std::nullopt;
return rtex;
}
void GSTextureReplacements::QueueAsyncReplacementTextureLoad(const TextureName& name, const std::string& filename, bool mipmap)
{
// check the pending list, so we don't queue it up multiple times
if (s_pending_async_load_textures.find(name) != s_pending_async_load_textures.end())
return;
s_pending_async_load_textures.insert(name);
QueueWorkerThreadItem([name, filename, mipmap]() {
// actually load the file, this is what will take the time
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(name, filename, !mipmap));
// check the pending set, there's a race here if we disable replacements while loading otherwise
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
if (s_pending_async_load_textures.find(name) == s_pending_async_load_textures.end())
return;
// insert into the cache and queue for later injection
if (replacement.has_value())
{
s_replacement_texture_cache.emplace(name, std::move(replacement.value()));
s_async_loaded_textures.emplace_back(name, mipmap);
}
else
{
// loading failed, so clear it from the pending list
s_pending_async_load_textures.erase(name);
}
});
}
void GSTextureReplacements::PrecacheReplacementTextures()
{
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
// predict whether the requests will come with mipmaps
// TODO: This will be wrong for hw mipmap games like Jak.
const bool mipmap = GSConfig.HWMipmap >= HWMipmapLevel::Basic ||
GSConfig.UserHacks_TriFilter == TriFiltering::Forced;
// pretty simple, just go through the filenames and if any aren't cached, cache them
for (const auto& it : s_replacement_texture_filenames)
{
if (s_replacement_texture_cache.find(it.first) != s_replacement_texture_cache.end())
continue;
// precaching always goes async.. for now
QueueAsyncReplacementTextureLoad(it.first, it.second, mipmap);
}
}
void GSTextureReplacements::ClearReplacementTextures()
{
s_replacement_texture_filenames.clear();
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
s_replacement_texture_cache.clear();
s_pending_async_load_textures.clear();
s_async_loaded_textures.clear();
}
GSTexture* GSTextureReplacements::CreateReplacementTexture(const ReplacementTexture& rtex, const GSVector2& scale, bool mipmap)
{
GSTexture* tex = g_gs_device->CreateTexture(rtex.width, rtex.height, mipmap, rtex.format);
if (!tex)
return nullptr;
// upload base level
tex->Update(GSVector4i(0, 0, rtex.width, rtex.height), rtex.data.data(), rtex.pitch);
// and the mips if they're present in the replacement texture
if (!rtex.mips.empty())
{
for (u32 i = 0; i < static_cast<u32>(rtex.mips.size()); i++)
{
const u32 mip = i + 1;
const u32 mipw = std::max<u32>(rtex.width >> mip, 1u);
const u32 miph = std::max<u32>(rtex.height >> mip, 1u);
tex->Update(GSVector4i(0, 0, mipw, miph), rtex.mips[i].data.data(), rtex.mips[i].pitch, mip);
}
}
tex->SetScale(scale);
return tex;
}
void GSTextureReplacements::ProcessAsyncLoadedTextures()
{
// this holds the lock while doing the upload, but it should be reasonably quick
std::unique_lock<std::mutex> lock(s_replacement_texture_cache_mutex);
for (const auto& [name, mipmap] : s_async_loaded_textures)
{
// no longer pending!
s_pending_async_load_textures.erase(name);
// we should be in the cache now, lock and loaded
auto it = s_replacement_texture_cache.find(name);
if (it == s_replacement_texture_cache.end())
continue;
// upload and inject into TC
GSTexture* tex = CreateReplacementTexture(it->second, name.ReplacementScale(it->second), mipmap);
if (tex)
s_tc->InjectHashCacheTexture(HashCacheKeyFromTextureName(name), tex);
}
s_async_loaded_textures.clear();
}
void GSTextureReplacements::DumpTexture(const GSTextureCache::HashCacheKey& hash, const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSLocalMemory& mem, u32 level)
{
// check if it's been dumped already
const TextureName name(CreateTextureName(hash, level));
if (s_dumped_textures.find(name) != s_dumped_textures.end())
return;
s_dumped_textures.insert(name);
// already exists on disk?
std::string filename(GetDumpFilename(name, level));
if (filename.empty() || FileSystem::FileExists(filename.c_str()))
return;
const std::string_view title(FileSystem::GetFileTitleFromPath(filename));
DevCon.WriteLn("Dumping %ux%u texture '%*s'.", name.Width(), name.Height(), static_cast<int>(title.size()), title.data());
// compute width/height
const GSLocalMemory::psm_t& psm = GSLocalMemory::m_psm[TEX0.PSM];
const GSVector2i& bs = psm.bs;
const int tw = 1 << TEX0.TW;
const int th = 1 << TEX0.TH;
const GSVector4i rect(0, 0, tw, th);
const GSVector4i block_rect(rect.ralign<Align_Outside>(bs));
const int read_width = std::max(tw, psm.bs.x);
const int read_height = std::max(th, psm.bs.y);
const u32 pitch = static_cast<u32>(read_width) * sizeof(u32);
// use per-texture buffer so we can compress the texture asynchronously and not block the GS thread
std::vector<u8> buffer(pitch * static_cast<u32>(read_height));
(mem.*psm.rtx)(mem.GetOffset(TEX0.TBP0, TEX0.TBW, TEX0.PSM), block_rect, buffer.data(), pitch, TEXA);
// okay, now we can actually dump it
QueueWorkerThreadItem([filename = std::move(filename), tw, th, pitch, buffer = std::move(buffer)]() {
if (!SavePNGImage(filename.c_str(), tw, th, buffer.data(), pitch))
Console.Error("Failed to dump texture to '%s'.", filename.c_str());
});
}
void GSTextureReplacements::ClearDumpedTextureList()
{
s_dumped_textures.clear();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Worker Thread
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void GSTextureReplacements::StartWorkerThread()
{
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
if (s_worker_thread.joinable())
return;
s_worker_thread_running = true;
s_worker_thread = std::thread(WorkerThreadEntryPoint);
}
void GSTextureReplacements::StopWorkerThread()
{
{
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
if (!s_worker_thread.joinable())
return;
s_worker_thread_running = false;
s_worker_thread_cv.notify_one();
}
s_worker_thread.join();
// clear out workery-things too
CancelPendingLoadsAndDumps();
}
void GSTextureReplacements::QueueWorkerThreadItem(std::function<void()> fn)
{
pxAssert(s_worker_thread.joinable());
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
s_worker_thread_queue.push(std::move(fn));
s_worker_thread_cv.notify_one();
}
void GSTextureReplacements::WorkerThreadEntryPoint()
{
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
while (s_worker_thread_running)
{
if (s_worker_thread_queue.empty())
{
s_worker_thread_cv.wait(lock);
continue;
}
std::function<void()> fn = std::move(s_worker_thread_queue.front());
s_worker_thread_queue.pop();
lock.unlock();
fn();
lock.lock();
}
}
void GSTextureReplacements::SyncWorkerThread()
{
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
if (!s_worker_thread.joinable())
return;
// not the most efficient by far, but it only gets called on config changes, so whatever
for (;;)
{
if (s_worker_thread_queue.empty())
break;
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
lock.lock();
}
}
void GSTextureReplacements::CancelPendingLoadsAndDumps()
{
std::unique_lock<std::mutex> lock(s_worker_thread_mutex);
if (!s_worker_thread.joinable())
return;
while (!s_worker_thread_queue.empty())
s_worker_thread_queue.pop();
s_async_loaded_textures.clear();
s_pending_async_load_textures.clear();
}

View File

@ -0,0 +1,60 @@
/* 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/>.
*/
#pragma once
#include "GS/Renderers/HW/GSTextureCache.h"
namespace GSTextureReplacements
{
struct ReplacementTexture
{
u32 width;
u32 height;
GSTexture::Format format;
u32 pitch;
std::vector<u8> data;
struct MipData
{
u32 pitch;
std::vector<u8> data;
};
std::vector<MipData> mips;
};
void Initialize(GSTextureCache* tc);
void GameChanged();
void ReloadReplacementMap();
void UpdateConfig(Pcsx2Config::GSOptions& old_config);
void Shutdown();
u32 CalcMipmapLevelsForReplacement(u32 width, u32 height);
GSTexture* LookupReplacementTexture(const GSTextureCache::HashCacheKey& hash, bool mipmap, bool* pending);
GSTexture* CreateReplacementTexture(const ReplacementTexture& rtex, const GSVector2& scale, bool mipmap);
void ProcessAsyncLoadedTextures();
void DumpTexture(const GSTextureCache::HashCacheKey& hash, const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSLocalMemory& mem, u32 level);
void ClearDumpedTextureList();
/// Loader will take a filename and interpret the format (e.g. DDS, PNG, etc).
using ReplacementTextureLoader = bool (*)(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
ReplacementTextureLoader GetLoader(const std::string_view& filename);
/// Saves an image buffer to a PNG file (for dumping).
bool SavePNGImage(const std::string& filename, u32 width, u32 height, const u8* buffer, u32 pitch);
} // namespace GSTextureReplacements

View File

@ -574,6 +574,18 @@ DebugTab::DebugTab(wxWindow* parent)
tab_box->Add(ogl_box.outer, wxSizerFlags().Expand());
PaddedBoxSizer<wxStaticBoxSizer> tex_box(wxVERTICAL, this, "Texture Replacements");
auto* tex_grid = new wxFlexGridSizer(2, space, space);
m_ui.addCheckBox(tex_grid, "Dump Textures", "DumpReplaceableTextures", -1);
m_ui.addCheckBox(tex_grid, "Dump Mipmaps", "DumpReplaceableMipmaps", -1);
m_ui.addCheckBox(tex_grid, "Dump FMV Textures", "DumpTexturesWithFMVActive", -1);
m_ui.addCheckBox(tex_grid, "Async Texture Loading", "LoadTextureReplacementsAsync", -1);
m_ui.addCheckBox(tex_grid, "Load Textures", "LoadTextureReplacements", -1);
m_ui.addCheckBox(tex_grid, "Precache Textures", "PrecacheTextureReplacements", -1);
tex_box->Add(tex_grid);
tab_box->Add(tex_box.outer, wxSizerFlags().Expand());
SetSizerAndFit(tab_box.outer);
}

View File

@ -36,6 +36,7 @@ enum FoldersEnum_t
FolderId_Cheats,
FolderId_CheatsWS,
FolderId_Cache,
FolderId_Textures,
FolderId_COUNT
};

View File

@ -48,6 +48,7 @@ namespace EmuFolders
wxDirName Cache;
wxDirName Covers;
wxDirName GameSettings;
wxDirName Textures;
} // namespace EmuFolders
void TraceLogFilters::LoadSave(SettingsWrapper& wrap)
@ -311,6 +312,13 @@ Pcsx2Config::GSOptions::GSOptions()
UserHacks_MergePPSprite = false;
UserHacks_WildHack = false;
DumpReplaceableTextures = false;
DumpReplaceableMipmaps = false;
DumpTexturesWithFMVActive = false;
LoadTextureReplacements = false;
LoadTextureReplacementsAsync = true;
PrecacheTextureReplacements = false;
ShaderFX_Conf = "shaders/GS_FX_Settings.ini";
ShaderFX_GLSL = "shaders/GS.fx";
}
@ -513,6 +521,12 @@ void Pcsx2Config::GSOptions::ReloadIniSettings()
GSSettingBoolEx(SaveFrame, "savef");
GSSettingBoolEx(SaveTexture, "savet");
GSSettingBoolEx(SaveDepth, "savez");
GSSettingBoolEx(DumpReplaceableTextures, "DumpReplaceableTextures");
GSSettingBoolEx(DumpReplaceableMipmaps, "DumpReplaceableMipmaps");
GSSettingBoolEx(DumpTexturesWithFMVActive, "DumpTexturesWithFMVActive");
GSSettingBoolEx(LoadTextureReplacements, "LoadTextureReplacements");
GSSettingBoolEx(LoadTextureReplacementsAsync, "LoadTextureReplacementsAsync");
GSSettingBoolEx(PrecacheTextureReplacements, "PrecacheTextureReplacements");
GSSettingIntEnumEx(InterlaceMode, "interlace");
@ -1083,6 +1097,7 @@ void EmuFolders::SetDefaults()
GameSettings = DataRoot.Combine(wxDirName("gamesettings"));
Cache = DataRoot.Combine(wxDirName("cache"));
Resources = AppRoot.Combine(wxDirName("resources"));
Textures = AppRoot.Combine(wxDirName("textures"));
}
static wxDirName LoadPathFromSettings(SettingsInterface& si, const wxDirName& root, const char* name, const char* def)
@ -1106,6 +1121,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
Covers = LoadPathFromSettings(si, DataRoot, "Covers", "covers");
GameSettings = LoadPathFromSettings(si, DataRoot, "GameSettings", "gamesettings");
Cache = LoadPathFromSettings(si, DataRoot, "Cache", "cache");
Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures");
Console.WriteLn("BIOS Directory: %s", Bios.ToString().c_str().AsChar());
Console.WriteLn("Snapshots Directory: %s", Snapshots.ToString().c_str().AsChar());
@ -1117,6 +1133,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
Console.WriteLn("Covers Directory: %s", Covers.ToString().c_str().AsChar());
Console.WriteLn("Game Settings Directory: %s", GameSettings.ToString().c_str().AsChar());
Console.WriteLn("Cache Directory: %s", Cache.ToString().c_str().AsChar());
Console.WriteLn("Textures Directory: %s", Textures.ToString().c_str().AsChar());
}
void EmuFolders::Save(SettingsInterface& si)
@ -1131,6 +1148,7 @@ void EmuFolders::Save(SettingsInterface& si)
si.SetStringValue("Folders", "Cheats", wxDirName::MakeAutoRelativeTo(Cheats, datarel).c_str());
si.SetStringValue("Folders", "CheatsWS", wxDirName::MakeAutoRelativeTo(CheatsWS, datarel).c_str());
si.SetStringValue("Folders", "Cache", wxDirName::MakeAutoRelativeTo(Cache, datarel).c_str());
si.SetStringValue("Folders", "Textures", wxDirName::MakeAutoRelativeTo(Textures, datarel).c_str());
}
bool EmuFolders::EnsureFoldersExist()
@ -1146,5 +1164,6 @@ bool EmuFolders::EnsureFoldersExist()
result = Covers.Mkdir() && result;
result = GameSettings.Mkdir() && result;
result = Cache.Mkdir() && result;
result = Textures.Mkdir() && result;
return result;
}

View File

@ -114,6 +114,12 @@ namespace PathDefs
static const wxDirName retval(L"cache");
return retval;
}
const wxDirName& Textures()
{
static const wxDirName retval(L"textures");
return retval;
}
};
// Specifies the root folder for the application install.
@ -263,6 +269,11 @@ namespace PathDefs
return GetDocuments() + Base::Cache();
}
wxDirName GetTextures()
{
return GetDocuments() + Base::Textures();
}
wxDirName Get(FoldersEnum_t folderidx)
{
switch (folderidx)
@ -287,6 +298,8 @@ namespace PathDefs
return GetCheatsWS();
case FolderId_Cache:
return GetCache();
case FolderId_Textures:
return GetTextures();
case FolderId_Documents:
return CustomDocumentsFolder;
@ -402,6 +415,8 @@ wxDirName& AppConfig::FolderOptions::operator[](FoldersEnum_t folderidx)
return CheatsWS;
case FolderId_Cache:
return Cache;
case FolderId_Textures:
return Textures;
case FolderId_Documents:
return CustomDocumentsFolder;
@ -440,6 +455,8 @@ bool AppConfig::FolderOptions::IsDefault(FoldersEnum_t folderidx) const
return UseDefaultCheatsWS;
case FolderId_Cache:
return UseDefaultCache;
case FolderId_Textures:
return UseDefaultTextures;
case FolderId_Documents:
return false;
@ -518,6 +535,13 @@ void AppConfig::FolderOptions::Set(FoldersEnum_t folderidx, const wxString& src,
EmuFolders::Cache.Mkdir();
break;
case FolderId_Textures:
Cache = src;
UseDefaultCache = useDefault;
EmuFolders::Textures = GetResolvedFolder(FolderId_Textures);
EmuFolders::Textures.Mkdir();
break;
jNO_DEFAULT
}
}
@ -794,6 +818,7 @@ void AppSetEmuFolders()
EmuFolders::CheatsWS = GetResolvedFolder(FolderId_CheatsWS);
EmuFolders::Resources = g_Conf->Folders.Resources;
EmuFolders::Cache = GetResolvedFolder(FolderId_Cache);
EmuFolders::Textures = GetResolvedFolder(FolderId_Textures);
// Ensure cache directory exists, since we're going to write to it (e.g. game database)
EmuFolders::Cache.Mkdir();

View File

@ -128,7 +128,8 @@ public:
UseDefaultLangs:1,
UseDefaultCheats:1,
UseDefaultCheatsWS:1,
UseDefaultCache:1;
UseDefaultCache:1,
UseDefaultTextures:1;
BITFIELD_END
wxDirName
@ -141,7 +142,8 @@ public:
Cheats,
CheatsWS,
Resources,
Cache;
Cache,
Textures;
wxDirName RunIso; // last used location for Iso loading.
wxDirName RunELF; // last used location for ELF loading.

View File

@ -600,6 +600,7 @@ void AppApplySettings( const AppConfig* oldconf )
g_Conf->Folders.Snapshots.Mkdir();
g_Conf->Folders.Cheats.Mkdir();
g_Conf->Folders.CheatsWS.Mkdir();
g_Conf->Folders.Textures.Mkdir();
RelocateLogfile();

View File

@ -316,6 +316,8 @@
<ClCompile Include="Gif_Logger.cpp" />
<ClCompile Include="Gif_Unit.cpp" />
<ClCompile Include="GS\Renderers\DX11\D3D.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacementLoaders.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacements.cpp" />
<ClCompile Include="GS\Window\GSwxDialog.cpp" />
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
@ -760,6 +762,7 @@
<ClInclude Include="GameDatabase.h" />
<ClInclude Include="Gif_Unit.h" />
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h" />
<ClInclude Include="GS\Window\GSwxDialog.h" />
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />

View File

@ -1703,6 +1703,12 @@
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp">
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
</ClCompile>
<ClCompile Include="GS\Renderers\HW\GSTextureReplacements.cpp">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClCompile>
<ClCompile Include="GS\Renderers\HW\GSTextureReplacementLoaders.cpp">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Patch.h">
@ -2828,6 +2834,9 @@
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
</ClInclude>
<ClInclude Include="GS.h" />
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="windows\wxResources.rc">

View File

@ -188,6 +188,8 @@
<ClCompile Include="Gif_Logger.cpp" />
<ClCompile Include="Gif_Unit.cpp" />
<ClCompile Include="GS\Renderers\DX11\D3D.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacementLoaders.cpp" />
<ClCompile Include="GS\Renderers\HW\GSTextureReplacements.cpp" />
<ClCompile Include="GS\Window\GSwxDialog.cpp" />
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
@ -487,6 +489,7 @@
<ClInclude Include="GameDatabase.h" />
<ClInclude Include="Gif_Unit.h" />
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h" />
<ClInclude Include="GS\Window\GSwxDialog.h" />
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />

View File

@ -1196,6 +1196,12 @@
<ClCompile Include="windows\WinCompressNTFS.cpp">
<Filter>Host</Filter>
</ClCompile>
<ClCompile Include="GS\Renderers\HW\GSTextureReplacements.cpp">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClCompile>
<ClCompile Include="GS\Renderers\HW\GSTextureReplacementLoaders.cpp">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Patch.h">
@ -1973,6 +1979,9 @@
<ClInclude Include="PAD\Host\Global.h">
<Filter>System\Ps2\PAD</Filter>
</ClInclude>
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h">
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="GS\GS.rc">

View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
import sys
import glob
import os
import argparse
from PIL import Image
# 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/>.
DESCRIPTION = """Quick script to scale alpha values commonly seen in PCSX2 texture dumps.
This script will scale textures with a maximum alpha intensity of 128 to 255, and then back
again with the unscale command, suitable for use as replacements. Not unscaling after editing
may result in broken rendering!
Example usage:
python3 texture_dump_alpha_scaler.py scale path/to/serial/dumps
<edit your images, move to replacements, including the index txt>
python3 texture_dump_alpha_scaler.py unscale path/to/serial/replacements
"""
# pylint: disable=bare-except, disable=missing-function-docstring
def get_index_path(idir):
return os.path.join(idir, "__scaled_images__.txt")
def scale_image(path, relpath):
try:
img = Image.open(path, "r")
except:
return False
print("Processing '%s'" % relpath)
if img.mode != "RGBA":
print(" Skipping because it's not RGBA (%s)" % img.mode)
return False
data = img.getdata()
max_alpha = max(map(lambda p: p[3], data))
print(" max alpha %u" % max_alpha)
if max_alpha > 128:
print(" skipping because of large alpha value")
return False
new_pixels = list(map(lambda p: (p[0], p[1], p[2], min(p[3] * 2 - 1, 255)), data))
img.putdata(new_pixels)
img.save(path)
print(" scaled!")
return True
def unscale_image(path, relpath):
try:
img = Image.open(path, "r")
except:
return False
print("Processing '%s'" % relpath)
if img.mode != "RGBA":
print(" Skipping because it's not RGBA (%s)" % img.mode)
return False
data = img.getdata()
new_pixels = list(map(lambda p: (p[0], p[1], p[2], max((p[3] + 1) // 2, 0)), data))
img.putdata(new_pixels)
img.save(path)
print(" unscaled!")
return True
def get_scaled_images(idir):
try:
scaled_images = set()
with open(get_index_path(idir), "r") as ifile:
for line in ifile.readlines():
line = line.strip()
if len(line) == 0:
continue
scaled_images.add(line)
return scaled_images
except:
return set()
def put_scaled_images(idir, scaled_images):
if len(scaled_images) > 0:
with open(get_index_path(idir), "w") as ifile:
ifile.writelines(map(lambda s: s + "\n", scaled_images))
elif os.path.exists(get_index_path(idir)):
os.remove(get_index_path(idir))
def scale_images(idir, force):
scaled_images = get_scaled_images(idir)
for path in glob.glob(idir + "/**", recursive=True):
relpath = os.path.relpath(path, idir)
if not path.endswith(".png"):
continue
if relpath in scaled_images and not force:
continue
if not scale_image(path, relpath):
continue
scaled_images.add(relpath)
put_scaled_images(idir, scaled_images)
def unscale_images(idir, force):
scaled_images = get_scaled_images(idir)
if force:
for path in glob.glob(idir + "/**", recursive=True):
relpath = os.path.relpath(path, idir)
if not path.endswith(".png"):
continue
scaled_images.add(relpath)
for relpath in list(scaled_images):
if unscale_image(os.path.join(idir, relpath), relpath):
scaled_images.remove(relpath)
put_scaled_images(idir, scaled_images)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=DESCRIPTION,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("command", type=str,
help="Command, should be scale or unscale")
parser.add_argument("directory", type=str,
help="Directory containing images, searched recursively")
parser.add_argument("--force",
help="Scale images regardless of whether it's in the index",
action="store_true", required=False)
args = parser.parse_args()
if args.command == "scale":
scale_images(args.directory, args.force)
sys.exit(0)
elif args.command == "unscale":
unscale_images(args.directory, args.force)
sys.exit(0)
else:
print("Unknown command, should be scale or unscale")
sys.exit(1)