mirror of
https://github.com/PCSX2/pcsx2.git
synced 2026-10-11 22:49:56 +00:00
GS: Support loading replacement textures from zip files
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user