GS: Support loading replacement textures from zip files

This commit is contained in:
TellowKrinkle
2026-10-07 14:10:46 -04:00
committed by Ty
parent f00cb4cec3
commit bb78a1e250
3 changed files with 189 additions and 49 deletions
@@ -19,8 +19,8 @@ struct LoaderDefinition
GSTextureReplacements::ReplacementTextureLoader loader;
};
static bool PNGLoader(const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static bool DDSLoader(const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static bool PNGLoader(GSTextureReplacements::File& file, const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static bool DDSLoader(GSTextureReplacements::File& file, const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
static constexpr LoaderDefinition s_loaders[] = {
{"png", PNGLoader},
@@ -146,7 +146,13 @@ static void ConvertTexture_R8G8B8(u32 width, u32 height, std::vector<u8>& data,
// PNG Handlers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool PNGLoader(const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
static void PNGFileReader(png_structp png, png_bytep data, size_t amt)
{
GSTextureReplacements::File* file = static_cast<GSTextureReplacements::File*>(png_get_io_ptr(png));
file->Read(data, amt);
}
bool PNGLoader(GSTextureReplacements::File& file, const char* 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)
@@ -163,14 +169,10 @@ bool PNGLoader(const char* filename, GSTextureReplacements::ReplacementTexture*
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
});
auto fp = FileSystem::OpenManagedCFile(filename, "rb");
if (!fp)
return false;
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_init_io(png_ptr, fp.get());
png_set_read_fn(png_ptr, &file, PNGFileReader);
png_read_info(png_ptr, info_ptr);
png_uint_32 width = 0;
@@ -399,15 +401,15 @@ struct DDSLoadInfo
std::function<void(u32 width, u32 height, std::vector<u8>& data, u32& pitch)> conversion_function;
};
static bool ParseDDSHeader(std::FILE* fp, DDSLoadInfo* info)
static bool ParseDDSHeader(GSTextureReplacements::File& file, DDSLoadInfo* info)
{
u32 magic;
if (std::fread(&magic, sizeof(magic), 1, fp) != 1 || magic != DDS_MAGIC)
if (!file.ReadRawStruct(&magic) || 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)
if (!file.ReadRawStruct(&header) || header.dwSize < header_size)
return false;
// We should check for DDS_HEADER_FLAGS_TEXTURE here, but some tools don't seem
@@ -451,7 +453,7 @@ static bool ParseDDSHeader(std::FILE* fp, DDSLoadInfo* info)
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)
if (!file.ReadRawStruct(&dxt10_header))
return false;
// Can't handle array textures here. Doesn't make sense to use them, anyway.
@@ -562,13 +564,13 @@ static bool ParseDDSHeader(std::FILE* fp, DDSLoadInfo* info)
// Check for truncated or corrupted files.
info->base_image_offset = sizeof(magic) + header_size;
if (info->base_image_offset >= FileSystem::FSize64(fp))
if (info->base_image_offset >= file.Size())
return false;
return true;
}
static bool ReadDDSMipLevel(std::FILE* fp, const char* filename, u32 mip_level, const DDSLoadInfo& info, u32 width, u32 height, std::vector<u8>& data, u32& pitch, u32 size)
static bool ReadDDSMipLevel(GSTextureReplacements::File& file, const char* 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.
@@ -583,7 +585,7 @@ static bool ReadDDSMipLevel(std::FILE* fp, const char* filename, u32 mip_level,
}
data.resize(size);
if (std::fread(data.data(), size, 1, fp) != 1)
if (!file.Read(data.data(), size))
return false;
// Apply conversion function for uncompressed textures.
@@ -593,25 +595,21 @@ static bool ReadDDSMipLevel(std::FILE* fp, const char* filename, u32 mip_level,
return true;
}
bool DDSLoader(const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
bool DDSLoader(GSTextureReplacements::File& file, const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
{
auto fp = FileSystem::OpenManagedCFile(filename, "rb");
if (!fp)
return false;
DDSLoadInfo info;
if (!ParseDDSHeader(fp.get(), &info))
if (!ParseDDSHeader(file, &info))
return false;
// always load the base image
if (FileSystem::FSeek64(fp.get(), info.base_image_offset, SEEK_SET) != 0)
if (!file.Seek(info.base_image_offset))
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))
if (!ReadDDSMipLevel(file, 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.
@@ -622,7 +620,7 @@ bool DDSLoader(const char* filename, GSTextureReplacements::ReplacementTexture*
GSTextureReplacements::ReplacementTexture::MipData md;
u32 mip_size;
CalcBlockMipmapSize(info.block_size, info.bytes_per_block, info.width, info.height, level, md.width, md.height, md.pitch, mip_size);
if (!ReadDDSMipLevel(fp.get(), filename, level, info, md.width, md.height, md.data, md.pitch, mip_size))
if (!ReadDDSMipLevel(file, filename, level, info, md.width, md.height, md.data, md.pitch, mip_size))
break;
tex->mips.push_back(std::move(md));
+133 -24
View File
@@ -9,6 +9,7 @@
#include "common/StringUtil.h"
#include "common/ScopedGuard.h"
#include "common/TextureDecompress.h"
#include "common/ZipHelpers.h"
#include "Config.h"
#include "Host.h"
@@ -100,6 +101,12 @@ namespace std
namespace GSTextureReplacements
{
struct ReplacementFile
{
std::string path; ///< Path to archive or file
s64 archive_idx; ///< If archive, index into archive, otherwise -1
bool IsArchive() const { return archive_idx >= 0; }
};
static TextureName CreateTextureName(const GSTextureCache::HashCacheKey& hash, u32 miplevel);
static GSTextureCache::HashCacheKey HashCacheKeyFromTextureName(const TextureName& tn);
static std::optional<TextureName> ParseReplacementName(const char* filename);
@@ -108,8 +115,8 @@ namespace GSTextureReplacements
template <GSTexture::Format format>
std::pair<u8, u8> GetBCAlphaMinMax(ReplacementTexture& rtex);
static void SetReplacementTextureAlphaMinMax(ReplacementTexture& rtex);
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, bool cache_only);
static std::optional<ReplacementTexture> LoadReplacementTexture(const ReplacementFile& file, bool only_base_image);
static void QueueAsyncReplacementTextureLoad(const TextureName& name, const ReplacementFile& filename, bool mipmap, bool cache_only);
static void PrecacheReplacementTextures();
static void ClearReplacementTextures();
@@ -127,7 +134,7 @@ namespace GSTextureReplacements
static std::mutex s_dumped_textures_mutex;
/// Lookup map of texture names to replacements, if they exist.
static std::unordered_map<TextureName, std::string> s_replacement_texture_filenames;
static std::unordered_map<TextureName, ReplacementFile> s_replacement_texture_filenames;
/// Lookup map of texture names without CLUT hash, to know when we need to disable paltex.
static std::unordered_set<TextureName> s_replacement_textures_without_clut_hash;
@@ -372,6 +379,18 @@ static bool GetWrongCasePath(std::string* output, const char* dir, std::string_v
return false;
}
static void AddReplacementTexture(TextureName key, const char* filename, std::string path, s64 archive_idx = -1)
{
using namespace GSTextureReplacements;
DbgCon.WriteLn("Found %ux%u replacement '%s'", key.Width(), key.Height(), filename);
s_replacement_texture_filenames.emplace(key, ReplacementFile { std::move(path), archive_idx });
// zero out the CLUT hash, because we need this for checking if there's any replacements with this hash when using paltex
key.CLUTHash = 0;
s_replacement_textures_without_clut_hash.insert(key);
}
void GSTextureReplacements::ReloadReplacementMap()
{
SyncWorkerThread();
@@ -419,20 +438,32 @@ void GSTextureReplacements::ReloadReplacementMap()
{
// file format we can handle?
const char* filename = Path::GetFileName(fd.FileName).data(); // GetFileName takes a substring from the end, so the null terminator is preserved
std::string_view ext = Path::GetExtension(filename);
if (ext.size() == 3 && 0 == StringUtil::Strncasecmp(ext.data(), "zip", 3))
{
zip_error_t ze = {};
auto zf = zip_open_managed(fd.FileName.c_str(), ZIP_RDONLY, &ze);
zip_int64_t count = zip_get_num_entries(zf.get(), 0);
if (count < 0)
continue;
for (zip_int64_t i = 0; i < count; i++)
{
const char* entry = zip_get_name(zf.get(), i, 0);
std::string_view entry_sv;
if (!entry || !GetLoader((entry_sv = entry)))
continue;
const char* entry_filename = Path::GetFileName(entry_sv).data();
if (std::optional<TextureName> name = ParseReplacementName(entry_filename))
AddReplacementTexture(*name, entry_filename, fd.FileName, i);
}
continue;
}
if (!GetLoader(filename))
continue;
// parse the name if it's valid
std::optional<TextureName> name = ParseReplacementName(filename);
if (!name.has_value())
continue;
DbgCon.WriteLn("Found %ux%u replacement '%s'", name->Width(), name->Height(), filename);
s_replacement_texture_filenames.emplace(name.value(), std::move(fd.FileName));
// zero out the CLUT hash, because we need this for checking if there's any replacements with this hash when using paltex
name->CLUTHash = 0;
s_replacement_textures_without_clut_hash.insert(name.value());
if (std::optional<TextureName> name = ParseReplacementName(filename))
AddReplacementTexture(*name, filename, std::move(fd.FileName));
}
if (!s_replacement_texture_filenames.empty())
@@ -536,7 +567,7 @@ GSTexture* GSTextureReplacements::LookupReplacementTexture(const GSTextureCache:
else
{
// synchronous load
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(name, fnit->second, !mipmap));
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(fnit->second, !mipmap));
if (!replacement.has_value())
return nullptr;
@@ -627,17 +658,60 @@ void GSTextureReplacements::SetReplacementTextureAlphaMinMax(ReplacementTexture&
}
}
std::optional<GSTextureReplacements::ReplacementTexture> GSTextureReplacements::LoadReplacementTexture(const TextureName& name, const std::string& filename, bool only_base_image)
std::optional<GSTextureReplacements::ReplacementTexture> GSTextureReplacements::LoadReplacementTexture(const ReplacementFile& file, bool only_base_image)
{
ReplacementTextureLoader loader = GetLoader(filename);
if (!loader)
return std::nullopt;
ReplacementTexture rtex;
if (!loader(filename.c_str(), &rtex, only_base_image))
if (file.IsArchive())
{
Console.Warning("Failed to load replacement texture %s", filename.c_str());
return std::nullopt;
zip_error_t ze = {};
auto zf = zip_open_managed(file.path.c_str(), ZIP_RDONLY, &ze);
if (!zf)
{
Console.Warning("Failed to open replacement texture zip file %s: %s", file.path.c_str(), zip_error_strerror(&ze));
return std::nullopt;
}
zip_stat_t stat;
if (0 != zip_stat_index(zf.get(), file.archive_idx, 0, &stat))
{
Console.Warning("Replacement texture " PRId64 " missing from zip %s: %s", file.archive_idx, file.path.c_str(), zip_strerror(zf.get()));
return std::nullopt;
}
ReplacementTextureLoader loader = GetLoader(stat.name);
if (!loader)
return std::nullopt;
void* buf = malloc(stat.size);
ScopedGuard cleanup([buf]{ free(buf); });
auto zff = zip_fopen_index_managed(zf.get(), file.archive_idx, 0);
if (!zff || static_cast<u64>(zip_fread(zff.get(), buf, stat.size)) != stat.size)
{
if (!zff && zip_get_error(zf.get())->zip_err == ZIP_ER_COMPNOTSUPP)
Host::AddKeyedOSDMessage("ZipCompressionNotSupported",
fmt::format(TRANSLATE_FS("TextureReplacement", "Zip file {} contains replacement textures with an unsupported compression type.\n"
"Unzip the file to use its replacement textures."), file.path),
Host::OSD_WARNING_DURATION);
Console.Warning("Failed to load replacement texture %s from zip file %s: %s", stat.name, file.path.c_str(), zip_strerror(zf.get()));
return std::nullopt;
}
MemoryFile memfile(buf, stat.size);
if (!loader(memfile, stat.name, &rtex, only_base_image))
{
Console.Warning("Failed to load replacement texture %s", stat.name);
return std::nullopt;
}
}
else
{
ReplacementTextureLoader loader = GetLoader(file.path);
if (!loader)
return std::nullopt;
FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(file.path.c_str(), "rb");
CFile cfile(fp.get());
if (!loader(cfile, file.path.c_str(), &rtex, only_base_image))
{
Console.Warning("Failed to load replacement texture %s", file.path.c_str());
return std::nullopt;
}
}
SetReplacementTextureAlphaMinMax(rtex);
@@ -645,7 +719,7 @@ std::optional<GSTextureReplacements::ReplacementTexture> GSTextureReplacements::
return rtex;
}
void GSTextureReplacements::QueueAsyncReplacementTextureLoad(const TextureName& name, const std::string& filename, bool mipmap, bool cache_only)
void GSTextureReplacements::QueueAsyncReplacementTextureLoad(const TextureName& name, const ReplacementFile& filename, bool mipmap, bool cache_only)
{
// check the pending list, so we don't queue it up multiple times
auto it = s_pending_async_load_textures.find(name);
@@ -666,7 +740,7 @@ void GSTextureReplacements::QueueAsyncReplacementTextureLoad(const TextureName&
s_pending_async_load_textures.emplace(name, cache_only);
QueueWorkerThreadItem([name, filename, mipmap]() {
// actually load the file, this is what will take the time
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(name, filename, !mipmap));
std::optional<ReplacementTexture> replacement(LoadReplacementTexture(filename, !mipmap));
// check the pending set, there's a race here if we disable replacements while loading otherwise
// also check the full replacement list, if async loading is off, it might already be in there
@@ -975,3 +1049,38 @@ void GSTextureReplacements::CancelPendingLoadsAndDumps()
s_async_loaded_textures.clear();
s_pending_async_load_textures.clear();
}
bool GSTextureReplacements::CFile::Read(void* data, size_t amt)
{
return std::fread(data, amt, 1, file);
}
bool GSTextureReplacements::CFile::Seek(size_t offset)
{
return std::fseek(file, offset, SEEK_SET) == 0;
}
s64 GSTextureReplacements::CFile::Size()
{
return FileSystem::FSize64(file);
}
bool GSTextureReplacements::MemoryFile::Read(void* data, size_t amt)
{
if (amt > len - pos)
return false;
memcpy(data, static_cast<const u8*>(buffer) + pos, amt);
pos += amt;
return true;
}
bool GSTextureReplacements::MemoryFile::Seek(size_t offset)
{
pos = std::min(offset, len);
return true;
}
s64 GSTextureReplacements::MemoryFile::Size()
{
return len;
}
+34 -1
View File
@@ -53,8 +53,41 @@ namespace GSTextureReplacements
/// Get the number of replacement textures that have been loaded/cached.
u32 GetLoadedTextureCount();
class File
{
public:
virtual bool Read(void* data, size_t amt) = 0;
virtual bool Seek(size_t offset) = 0;
virtual s64 Size() = 0;
template <typename T>
bool ReadRawStruct(T* out) { return Read(out, sizeof(*out)); }
};
class CFile : public File
{
FILE* file;
public:
CFile(FILE* file_): file(file_) {};
bool Read(void* data, size_t amt) override;
bool Seek(size_t offset) override;
s64 Size() override;
};
class MemoryFile : public File
{
const u8* buffer;
size_t len;
size_t pos;
public:
MemoryFile(const void* buffer_, size_t len_): buffer(static_cast<const u8*>(buffer_)), len(len_), pos(0) {};
bool Read(void* data, size_t amt) override;
bool Seek(size_t offset) override;
s64 Size() override;
};
/// Loader will take a filename and interpret the format (e.g. DDS, PNG, etc).
using ReplacementTextureLoader = bool (*)(const char* filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image);
using ReplacementTextureLoader = bool (*)(File& file, const char* 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).