mirror of https://github.com/PCSX2/pcsx2.git
GS: Add texture dumping and replacement system
This commit is contained in:
parent
32dc68f103
commit
5a25cc171d
|
@ -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.mergeSprite, "EmuCore/GS", "UserHacks_merge_pp_sprite", false);
|
||||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.wildHack, "EmuCore/GS", "UserHacks_WildHack", 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
|
// Advanced Settings
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -339,7 +349,7 @@ void GraphicsSettingsWidget::updateRendererDependentOptions()
|
||||||
{
|
{
|
||||||
// software has no hacks tabs
|
// software has no hacks tabs
|
||||||
m_ui.verticalLayout->insertWidget(1, m_ui.softwareRendererGroup);
|
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;
|
m_software_renderer_visible = is_software;
|
||||||
|
|
|
@ -934,9 +934,13 @@
|
||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
<string>Advanced</string>
|
<string>Advanced</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
<layout class="QFormLayout" name="formLayout_6">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item row="0" column="0" colspan="2">
|
<item>
|
||||||
<layout class="QGridLayout" name="gridLayout_4">
|
<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">
|
<item row="0" column="0">
|
||||||
<widget class="QCheckBox" name="useBlitSwapChain">
|
<widget class="QCheckBox" name="useBlitSwapChain">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -952,6 +956,71 @@
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</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>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
|
@ -657,6 +657,8 @@ set(pcsx2GSSources
|
||||||
GS/Renderers/HW/GSRendererHW.cpp
|
GS/Renderers/HW/GSRendererHW.cpp
|
||||||
GS/Renderers/HW/GSRendererNew.cpp
|
GS/Renderers/HW/GSRendererNew.cpp
|
||||||
GS/Renderers/HW/GSTextureCache.cpp
|
GS/Renderers/HW/GSTextureCache.cpp
|
||||||
|
GS/Renderers/HW/GSTextureReplacementLoaders.cpp
|
||||||
|
GS/Renderers/HW/GSTextureReplacements.cpp
|
||||||
GS/Renderers/SW/GSDrawScanline.cpp
|
GS/Renderers/SW/GSDrawScanline.cpp
|
||||||
GS/Renderers/SW/GSDrawScanlineCodeGenerator.cpp
|
GS/Renderers/SW/GSDrawScanlineCodeGenerator.cpp
|
||||||
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.cpp
|
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.cpp
|
||||||
|
@ -721,6 +723,7 @@ set(pcsx2GSHeaders
|
||||||
GS/Renderers/HW/GSRendererHW.h
|
GS/Renderers/HW/GSRendererHW.h
|
||||||
GS/Renderers/HW/GSRendererNew.h
|
GS/Renderers/HW/GSRendererNew.h
|
||||||
GS/Renderers/HW/GSTextureCache.h
|
GS/Renderers/HW/GSTextureCache.h
|
||||||
|
GS/Renderers/HW/GSTextureReplacements.h
|
||||||
GS/Renderers/HW/GSVertexHW.h
|
GS/Renderers/HW/GSVertexHW.h
|
||||||
GS/Renderers/SW/GSDrawScanlineCodeGenerator.h
|
GS/Renderers/SW/GSDrawScanlineCodeGenerator.h
|
||||||
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.h
|
GS/Renderers/SW/GSDrawScanlineCodeGenerator.all.h
|
||||||
|
|
|
@ -458,7 +458,13 @@ struct Pcsx2Config
|
||||||
SaveRT : 1,
|
SaveRT : 1,
|
||||||
SaveFrame : 1,
|
SaveFrame : 1,
|
||||||
SaveTexture : 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 Cache;
|
||||||
extern wxDirName Covers;
|
extern wxDirName Covers;
|
||||||
extern wxDirName GameSettings;
|
extern wxDirName GameSettings;
|
||||||
|
extern wxDirName Textures;
|
||||||
|
|
||||||
// Assumes that AppRoot and DataRoot have been initialized.
|
// Assumes that AppRoot and DataRoot have been initialized.
|
||||||
void SetDefaults();
|
void SetDefaults();
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
#include "Renderers/Null/GSDeviceNull.h"
|
#include "Renderers/Null/GSDeviceNull.h"
|
||||||
#include "Renderers/OpenGL/GSDeviceOGL.h"
|
#include "Renderers/OpenGL/GSDeviceOGL.h"
|
||||||
#include "Renderers/HW/GSRendererNew.h"
|
#include "Renderers/HW/GSRendererNew.h"
|
||||||
|
#include "Renderers/HW/GSTextureReplacements.h"
|
||||||
#include "GSLzma.h"
|
#include "GSLzma.h"
|
||||||
|
|
||||||
#include "common/pxStreams.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
|
// clear out the sampler cache when AF options change, since the anisotropy gets baked into them
|
||||||
if (GSConfig.MaxAnisotropy != old_config.MaxAnisotropy)
|
if (GSConfig.MaxAnisotropy != old_config.MaxAnisotropy)
|
||||||
g_gs_device->ClearSamplerCache();
|
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)
|
void GSSwitchRenderer(GSRendererType new_renderer)
|
||||||
|
@ -1304,6 +1316,9 @@ void GSApp::Init()
|
||||||
m_default_configuration["disable_shader_cache"] = "0";
|
m_default_configuration["disable_shader_cache"] = "0";
|
||||||
m_default_configuration["dithering_ps2"] = "2";
|
m_default_configuration["dithering_ps2"] = "2";
|
||||||
m_default_configuration["dump"] = "0";
|
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"] = "2";
|
||||||
m_default_configuration["extrathreads_height"] = "4";
|
m_default_configuration["extrathreads_height"] = "4";
|
||||||
m_default_configuration["filter"] = std::to_string(static_cast<s8>(BiFiltering::PS2));
|
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["interlace"] = "7";
|
||||||
m_default_configuration["conservative_framebuffer"] = "1";
|
m_default_configuration["conservative_framebuffer"] = "1";
|
||||||
m_default_configuration["linear_present"] = "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["MaxAnisotropy"] = "0";
|
||||||
m_default_configuration["mipmap"] = "1";
|
m_default_configuration["mipmap"] = "1";
|
||||||
m_default_configuration["mipmap_hw"] = std::to_string(static_cast<int>(HWMipmapLevel::Automatic));
|
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["override_GL_ARB_texture_barrier"] = "-1";
|
||||||
m_default_configuration["paltex"] = "0";
|
m_default_configuration["paltex"] = "0";
|
||||||
m_default_configuration["png_compression_level"] = std::to_string(Z_BEST_SPEED);
|
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["preload_frame_with_gs_data"] = "0";
|
||||||
m_default_configuration["Renderer"] = std::to_string(static_cast<int>(GSRendererType::Auto));
|
m_default_configuration["Renderer"] = std::to_string(static_cast<int>(GSRendererType::Auto));
|
||||||
m_default_configuration["resx"] = "1024";
|
m_default_configuration["resx"] = "1024";
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include "PrecompiledHeader.h"
|
#include "PrecompiledHeader.h"
|
||||||
#include "GSRendererHW.h"
|
#include "GSRendererHW.h"
|
||||||
|
#include "GSTextureReplacements.h"
|
||||||
#include "GS/GSGL.h"
|
#include "GS/GSGL.h"
|
||||||
#include "Host.h"
|
#include "Host.h"
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ GSRendererHW::GSRendererHW()
|
||||||
}
|
}
|
||||||
|
|
||||||
m_dump_root = root_hw;
|
m_dump_root = root_hw;
|
||||||
|
GSTextureReplacements::Initialize(m_tc);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GSRendererHW::SetScaling()
|
void GSRendererHW::SetScaling()
|
||||||
|
@ -189,6 +191,7 @@ GSRendererHW::~GSRendererHW()
|
||||||
void GSRendererHW::Destroy()
|
void GSRendererHW::Destroy()
|
||||||
{
|
{
|
||||||
m_tc->RemoveAll();
|
m_tc->RemoveAll();
|
||||||
|
GSTextureReplacements::Shutdown();
|
||||||
GSRenderer::Destroy();
|
GSRenderer::Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,6 +263,8 @@ void GSRendererHW::SetGameCRC(u32 crc, int options)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GSTextureReplacements::GameChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GSRendererHW::CanUpscale()
|
bool GSRendererHW::CanUpscale()
|
||||||
|
@ -306,6 +311,9 @@ void GSRendererHW::VSync(u32 field, bool registers_written)
|
||||||
m_reset = false;
|
m_reset = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GSConfig.LoadTextureReplacements)
|
||||||
|
GSTextureReplacements::ProcessAsyncLoadedTextures();
|
||||||
|
|
||||||
//Check if the frame buffer width or display width has changed
|
//Check if the frame buffer width or display width has changed
|
||||||
SetScaling();
|
SetScaling();
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include "PrecompiledHeader.h"
|
#include "PrecompiledHeader.h"
|
||||||
#include "GSTextureCache.h"
|
#include "GSTextureCache.h"
|
||||||
|
#include "GSTextureReplacements.h"
|
||||||
#include "GSRendererHW.h"
|
#include "GSRendererHW.h"
|
||||||
#include "GS/GSGL.h"
|
#include "GS/GSGL.h"
|
||||||
#include "GS/GSIntrin.h"
|
#include "GS/GSIntrin.h"
|
||||||
|
@ -65,6 +66,8 @@ GSTextureCache::GSTextureCache(GSRenderer* r)
|
||||||
|
|
||||||
GSTextureCache::~GSTextureCache()
|
GSTextureCache::~GSTextureCache()
|
||||||
{
|
{
|
||||||
|
GSTextureReplacements::Shutdown();
|
||||||
|
|
||||||
RemoveAll();
|
RemoveAll();
|
||||||
|
|
||||||
m_surface_offset_cache.clear();
|
m_surface_offset_cache.clear();
|
||||||
|
@ -1452,50 +1455,18 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con
|
||||||
}
|
}
|
||||||
else
|
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
|
// 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);
|
src->m_texture = src->m_from_hash_cache->texture;
|
||||||
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;
|
|
||||||
|
|
||||||
if (psm.pal > 0)
|
if (psm.pal > 0)
|
||||||
AttachPaletteToSource(src, psm.pal, paltex);
|
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);
|
src->m_texture = g_gs_device->CreateTexture(tw, th, false, GSTexture::Format::UNorm8);
|
||||||
AttachPaletteToSource(src, psm.pal, true);
|
AttachPaletteToSource(src, psm.pal, true);
|
||||||
|
@ -1517,6 +1488,120 @@ GSTextureCache::Source* GSTextureCache::CreateSource(const GIFRegTEX0& TEX0, con
|
||||||
return src;
|
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)
|
GSTextureCache::Target* GSTextureCache::CreateTarget(const GIFRegTEX0& TEX0, int w, int h, int type, const bool clear)
|
||||||
{
|
{
|
||||||
ASSERT(type == RenderTarget || type == DepthStencil);
|
ASSERT(type == RenderTarget || type == DepthStencil);
|
||||||
|
@ -2388,6 +2473,29 @@ GSTextureCache::SurfaceOffset GSTextureCache::ComputeSurfaceOffset(const Surface
|
||||||
return so;
|
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
|
||||||
|
|
||||||
GSTextureCache::Palette::Palette(const GSRenderer* renderer, u16 pal, bool need_gs_texture)
|
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;
|
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
|
u64 GSTextureCache::HashCacheKeyHash::operator()(const HashCacheKey& key) const
|
||||||
{
|
{
|
||||||
std::size_t h = 0;
|
std::size_t h = 0;
|
||||||
|
|
|
@ -53,6 +53,9 @@ public:
|
||||||
static HashCacheKey Create(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSRenderer* renderer, const u32* clut,
|
static HashCacheKey Create(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, GSRenderer* renderer, const u32* clut,
|
||||||
const GSVector2i* lod);
|
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; }
|
__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);
|
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);
|
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 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);
|
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 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 uint32_t bp, const uint32_t bw, const uint32_t psm, const GSVector4i& r, const Target* t);
|
||||||
SurfaceOffset ComputeSurfaceOffset(const SurfaceOffsetKey& sok);
|
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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
|
@ -574,6 +574,18 @@ DebugTab::DebugTab(wxWindow* parent)
|
||||||
|
|
||||||
tab_box->Add(ogl_box.outer, wxSizerFlags().Expand());
|
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);
|
SetSizerAndFit(tab_box.outer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ enum FoldersEnum_t
|
||||||
FolderId_Cheats,
|
FolderId_Cheats,
|
||||||
FolderId_CheatsWS,
|
FolderId_CheatsWS,
|
||||||
FolderId_Cache,
|
FolderId_Cache,
|
||||||
|
FolderId_Textures,
|
||||||
|
|
||||||
FolderId_COUNT
|
FolderId_COUNT
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,6 +48,7 @@ namespace EmuFolders
|
||||||
wxDirName Cache;
|
wxDirName Cache;
|
||||||
wxDirName Covers;
|
wxDirName Covers;
|
||||||
wxDirName GameSettings;
|
wxDirName GameSettings;
|
||||||
|
wxDirName Textures;
|
||||||
} // namespace EmuFolders
|
} // namespace EmuFolders
|
||||||
|
|
||||||
void TraceLogFilters::LoadSave(SettingsWrapper& wrap)
|
void TraceLogFilters::LoadSave(SettingsWrapper& wrap)
|
||||||
|
@ -311,6 +312,13 @@ Pcsx2Config::GSOptions::GSOptions()
|
||||||
UserHacks_MergePPSprite = false;
|
UserHacks_MergePPSprite = false;
|
||||||
UserHacks_WildHack = false;
|
UserHacks_WildHack = false;
|
||||||
|
|
||||||
|
DumpReplaceableTextures = false;
|
||||||
|
DumpReplaceableMipmaps = false;
|
||||||
|
DumpTexturesWithFMVActive = false;
|
||||||
|
LoadTextureReplacements = false;
|
||||||
|
LoadTextureReplacementsAsync = true;
|
||||||
|
PrecacheTextureReplacements = false;
|
||||||
|
|
||||||
ShaderFX_Conf = "shaders/GS_FX_Settings.ini";
|
ShaderFX_Conf = "shaders/GS_FX_Settings.ini";
|
||||||
ShaderFX_GLSL = "shaders/GS.fx";
|
ShaderFX_GLSL = "shaders/GS.fx";
|
||||||
}
|
}
|
||||||
|
@ -513,6 +521,12 @@ void Pcsx2Config::GSOptions::ReloadIniSettings()
|
||||||
GSSettingBoolEx(SaveFrame, "savef");
|
GSSettingBoolEx(SaveFrame, "savef");
|
||||||
GSSettingBoolEx(SaveTexture, "savet");
|
GSSettingBoolEx(SaveTexture, "savet");
|
||||||
GSSettingBoolEx(SaveDepth, "savez");
|
GSSettingBoolEx(SaveDepth, "savez");
|
||||||
|
GSSettingBoolEx(DumpReplaceableTextures, "DumpReplaceableTextures");
|
||||||
|
GSSettingBoolEx(DumpReplaceableMipmaps, "DumpReplaceableMipmaps");
|
||||||
|
GSSettingBoolEx(DumpTexturesWithFMVActive, "DumpTexturesWithFMVActive");
|
||||||
|
GSSettingBoolEx(LoadTextureReplacements, "LoadTextureReplacements");
|
||||||
|
GSSettingBoolEx(LoadTextureReplacementsAsync, "LoadTextureReplacementsAsync");
|
||||||
|
GSSettingBoolEx(PrecacheTextureReplacements, "PrecacheTextureReplacements");
|
||||||
|
|
||||||
GSSettingIntEnumEx(InterlaceMode, "interlace");
|
GSSettingIntEnumEx(InterlaceMode, "interlace");
|
||||||
|
|
||||||
|
@ -1083,6 +1097,7 @@ void EmuFolders::SetDefaults()
|
||||||
GameSettings = DataRoot.Combine(wxDirName("gamesettings"));
|
GameSettings = DataRoot.Combine(wxDirName("gamesettings"));
|
||||||
Cache = DataRoot.Combine(wxDirName("cache"));
|
Cache = DataRoot.Combine(wxDirName("cache"));
|
||||||
Resources = AppRoot.Combine(wxDirName("resources"));
|
Resources = AppRoot.Combine(wxDirName("resources"));
|
||||||
|
Textures = AppRoot.Combine(wxDirName("textures"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static wxDirName LoadPathFromSettings(SettingsInterface& si, const wxDirName& root, const char* name, const char* def)
|
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");
|
Covers = LoadPathFromSettings(si, DataRoot, "Covers", "covers");
|
||||||
GameSettings = LoadPathFromSettings(si, DataRoot, "GameSettings", "gamesettings");
|
GameSettings = LoadPathFromSettings(si, DataRoot, "GameSettings", "gamesettings");
|
||||||
Cache = LoadPathFromSettings(si, DataRoot, "Cache", "cache");
|
Cache = LoadPathFromSettings(si, DataRoot, "Cache", "cache");
|
||||||
|
Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures");
|
||||||
|
|
||||||
Console.WriteLn("BIOS Directory: %s", Bios.ToString().c_str().AsChar());
|
Console.WriteLn("BIOS Directory: %s", Bios.ToString().c_str().AsChar());
|
||||||
Console.WriteLn("Snapshots Directory: %s", Snapshots.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("Covers Directory: %s", Covers.ToString().c_str().AsChar());
|
||||||
Console.WriteLn("Game Settings Directory: %s", GameSettings.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("Cache Directory: %s", Cache.ToString().c_str().AsChar());
|
||||||
|
Console.WriteLn("Textures Directory: %s", Textures.ToString().c_str().AsChar());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmuFolders::Save(SettingsInterface& si)
|
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", "Cheats", wxDirName::MakeAutoRelativeTo(Cheats, datarel).c_str());
|
||||||
si.SetStringValue("Folders", "CheatsWS", wxDirName::MakeAutoRelativeTo(CheatsWS, 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", "Cache", wxDirName::MakeAutoRelativeTo(Cache, datarel).c_str());
|
||||||
|
si.SetStringValue("Folders", "Textures", wxDirName::MakeAutoRelativeTo(Textures, datarel).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EmuFolders::EnsureFoldersExist()
|
bool EmuFolders::EnsureFoldersExist()
|
||||||
|
@ -1146,5 +1164,6 @@ bool EmuFolders::EnsureFoldersExist()
|
||||||
result = Covers.Mkdir() && result;
|
result = Covers.Mkdir() && result;
|
||||||
result = GameSettings.Mkdir() && result;
|
result = GameSettings.Mkdir() && result;
|
||||||
result = Cache.Mkdir() && result;
|
result = Cache.Mkdir() && result;
|
||||||
|
result = Textures.Mkdir() && result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,12 @@ namespace PathDefs
|
||||||
static const wxDirName retval(L"cache");
|
static const wxDirName retval(L"cache");
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wxDirName& Textures()
|
||||||
|
{
|
||||||
|
static const wxDirName retval(L"textures");
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Specifies the root folder for the application install.
|
// Specifies the root folder for the application install.
|
||||||
|
@ -263,6 +269,11 @@ namespace PathDefs
|
||||||
return GetDocuments() + Base::Cache();
|
return GetDocuments() + Base::Cache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wxDirName GetTextures()
|
||||||
|
{
|
||||||
|
return GetDocuments() + Base::Textures();
|
||||||
|
}
|
||||||
|
|
||||||
wxDirName Get(FoldersEnum_t folderidx)
|
wxDirName Get(FoldersEnum_t folderidx)
|
||||||
{
|
{
|
||||||
switch (folderidx)
|
switch (folderidx)
|
||||||
|
@ -287,6 +298,8 @@ namespace PathDefs
|
||||||
return GetCheatsWS();
|
return GetCheatsWS();
|
||||||
case FolderId_Cache:
|
case FolderId_Cache:
|
||||||
return GetCache();
|
return GetCache();
|
||||||
|
case FolderId_Textures:
|
||||||
|
return GetTextures();
|
||||||
|
|
||||||
case FolderId_Documents:
|
case FolderId_Documents:
|
||||||
return CustomDocumentsFolder;
|
return CustomDocumentsFolder;
|
||||||
|
@ -402,6 +415,8 @@ wxDirName& AppConfig::FolderOptions::operator[](FoldersEnum_t folderidx)
|
||||||
return CheatsWS;
|
return CheatsWS;
|
||||||
case FolderId_Cache:
|
case FolderId_Cache:
|
||||||
return Cache;
|
return Cache;
|
||||||
|
case FolderId_Textures:
|
||||||
|
return Textures;
|
||||||
|
|
||||||
case FolderId_Documents:
|
case FolderId_Documents:
|
||||||
return CustomDocumentsFolder;
|
return CustomDocumentsFolder;
|
||||||
|
@ -440,6 +455,8 @@ bool AppConfig::FolderOptions::IsDefault(FoldersEnum_t folderidx) const
|
||||||
return UseDefaultCheatsWS;
|
return UseDefaultCheatsWS;
|
||||||
case FolderId_Cache:
|
case FolderId_Cache:
|
||||||
return UseDefaultCache;
|
return UseDefaultCache;
|
||||||
|
case FolderId_Textures:
|
||||||
|
return UseDefaultTextures;
|
||||||
|
|
||||||
case FolderId_Documents:
|
case FolderId_Documents:
|
||||||
return false;
|
return false;
|
||||||
|
@ -518,6 +535,13 @@ void AppConfig::FolderOptions::Set(FoldersEnum_t folderidx, const wxString& src,
|
||||||
EmuFolders::Cache.Mkdir();
|
EmuFolders::Cache.Mkdir();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case FolderId_Textures:
|
||||||
|
Cache = src;
|
||||||
|
UseDefaultCache = useDefault;
|
||||||
|
EmuFolders::Textures = GetResolvedFolder(FolderId_Textures);
|
||||||
|
EmuFolders::Textures.Mkdir();
|
||||||
|
break;
|
||||||
|
|
||||||
jNO_DEFAULT
|
jNO_DEFAULT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -794,6 +818,7 @@ void AppSetEmuFolders()
|
||||||
EmuFolders::CheatsWS = GetResolvedFolder(FolderId_CheatsWS);
|
EmuFolders::CheatsWS = GetResolvedFolder(FolderId_CheatsWS);
|
||||||
EmuFolders::Resources = g_Conf->Folders.Resources;
|
EmuFolders::Resources = g_Conf->Folders.Resources;
|
||||||
EmuFolders::Cache = GetResolvedFolder(FolderId_Cache);
|
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)
|
// Ensure cache directory exists, since we're going to write to it (e.g. game database)
|
||||||
EmuFolders::Cache.Mkdir();
|
EmuFolders::Cache.Mkdir();
|
||||||
|
|
|
@ -128,7 +128,8 @@ public:
|
||||||
UseDefaultLangs:1,
|
UseDefaultLangs:1,
|
||||||
UseDefaultCheats:1,
|
UseDefaultCheats:1,
|
||||||
UseDefaultCheatsWS:1,
|
UseDefaultCheatsWS:1,
|
||||||
UseDefaultCache:1;
|
UseDefaultCache:1,
|
||||||
|
UseDefaultTextures:1;
|
||||||
BITFIELD_END
|
BITFIELD_END
|
||||||
|
|
||||||
wxDirName
|
wxDirName
|
||||||
|
@ -141,7 +142,8 @@ public:
|
||||||
Cheats,
|
Cheats,
|
||||||
CheatsWS,
|
CheatsWS,
|
||||||
Resources,
|
Resources,
|
||||||
Cache;
|
Cache,
|
||||||
|
Textures;
|
||||||
|
|
||||||
wxDirName RunIso; // last used location for Iso loading.
|
wxDirName RunIso; // last used location for Iso loading.
|
||||||
wxDirName RunELF; // last used location for ELF loading.
|
wxDirName RunELF; // last used location for ELF loading.
|
||||||
|
|
|
@ -600,6 +600,7 @@ void AppApplySettings( const AppConfig* oldconf )
|
||||||
g_Conf->Folders.Snapshots.Mkdir();
|
g_Conf->Folders.Snapshots.Mkdir();
|
||||||
g_Conf->Folders.Cheats.Mkdir();
|
g_Conf->Folders.Cheats.Mkdir();
|
||||||
g_Conf->Folders.CheatsWS.Mkdir();
|
g_Conf->Folders.CheatsWS.Mkdir();
|
||||||
|
g_Conf->Folders.Textures.Mkdir();
|
||||||
|
|
||||||
RelocateLogfile();
|
RelocateLogfile();
|
||||||
|
|
||||||
|
|
|
@ -316,6 +316,8 @@
|
||||||
<ClCompile Include="Gif_Logger.cpp" />
|
<ClCompile Include="Gif_Logger.cpp" />
|
||||||
<ClCompile Include="Gif_Unit.cpp" />
|
<ClCompile Include="Gif_Unit.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\DX11\D3D.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\Window\GSwxDialog.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
|
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
|
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
|
||||||
|
@ -760,6 +762,7 @@
|
||||||
<ClInclude Include="GameDatabase.h" />
|
<ClInclude Include="GameDatabase.h" />
|
||||||
<ClInclude Include="Gif_Unit.h" />
|
<ClInclude Include="Gif_Unit.h" />
|
||||||
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
|
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
|
||||||
|
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h" />
|
||||||
<ClInclude Include="GS\Window\GSwxDialog.h" />
|
<ClInclude Include="GS\Window\GSwxDialog.h" />
|
||||||
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
|
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
|
||||||
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />
|
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />
|
||||||
|
|
|
@ -1703,6 +1703,12 @@
|
||||||
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp">
|
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp">
|
||||||
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
|
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
|
||||||
</ClCompile>
|
</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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="Patch.h">
|
<ClInclude Include="Patch.h">
|
||||||
|
@ -2828,6 +2834,9 @@
|
||||||
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
|
<Filter>System\Ps2\GS\Renderers\Vulkan</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="GS.h" />
|
<ClInclude Include="GS.h" />
|
||||||
|
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h">
|
||||||
|
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
|
||||||
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ResourceCompile Include="windows\wxResources.rc">
|
<ResourceCompile Include="windows\wxResources.rc">
|
||||||
|
|
|
@ -188,6 +188,8 @@
|
||||||
<ClCompile Include="Gif_Logger.cpp" />
|
<ClCompile Include="Gif_Logger.cpp" />
|
||||||
<ClCompile Include="Gif_Unit.cpp" />
|
<ClCompile Include="Gif_Unit.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\DX11\D3D.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\Window\GSwxDialog.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
|
<ClCompile Include="GS\Renderers\Vulkan\GSDeviceVK.cpp" />
|
||||||
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
|
<ClCompile Include="GS\Renderers\Vulkan\GSTextureVK.cpp" />
|
||||||
|
@ -487,6 +489,7 @@
|
||||||
<ClInclude Include="GameDatabase.h" />
|
<ClInclude Include="GameDatabase.h" />
|
||||||
<ClInclude Include="Gif_Unit.h" />
|
<ClInclude Include="Gif_Unit.h" />
|
||||||
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
|
<ClInclude Include="GS\Renderers\DX11\D3D.h" />
|
||||||
|
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h" />
|
||||||
<ClInclude Include="GS\Window\GSwxDialog.h" />
|
<ClInclude Include="GS\Window\GSwxDialog.h" />
|
||||||
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
|
<ClInclude Include="GS\Renderers\Vulkan\GSDeviceVK.h" />
|
||||||
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />
|
<ClInclude Include="GS\Renderers\Vulkan\GSTextureVK.h" />
|
||||||
|
|
|
@ -1196,6 +1196,12 @@
|
||||||
<ClCompile Include="windows\WinCompressNTFS.cpp">
|
<ClCompile Include="windows\WinCompressNTFS.cpp">
|
||||||
<Filter>Host</Filter>
|
<Filter>Host</Filter>
|
||||||
</ClCompile>
|
</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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="Patch.h">
|
<ClInclude Include="Patch.h">
|
||||||
|
@ -1973,6 +1979,9 @@
|
||||||
<ClInclude Include="PAD\Host\Global.h">
|
<ClInclude Include="PAD\Host\Global.h">
|
||||||
<Filter>System\Ps2\PAD</Filter>
|
<Filter>System\Ps2\PAD</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="GS\Renderers\HW\GSTextureReplacements.h">
|
||||||
|
<Filter>System\Ps2\GS\Renderers\Hardware</Filter>
|
||||||
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ResourceCompile Include="GS\GS.rc">
|
<ResourceCompile Include="GS\GS.rc">
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue