diff --git a/common/Buffer.h b/common/Buffer.h new file mode 100644 index 0000000000000..63dbab3bc13df --- /dev/null +++ b/common/Buffer.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once +#include "Pcsx2Types.h" +#include +#include + +namespace Common +{ + +/// Like `std::vector` but doesn't zero bytes on resize +class Buffer +{ + void* ptr_; + size_t size_; + size_t cap_; +public: + constexpr Buffer(): ptr_(nullptr), size_(0), cap_(0) {} + explicit Buffer(size_t size): ptr_(malloc(size)), size_(size), cap_(size) {} + ~Buffer() { if (ptr_) free(ptr_); } + Buffer(const Buffer& other): Buffer(other.size_) + { + memcpy(ptr_, other.ptr_, other.size_); + } + Buffer(Buffer&& other): ptr_(other.ptr_), size_(other.size_), cap_(other.cap_) + { + other.ptr_ = nullptr; + other.size_ = 0; + other.cap_ = 0; + } + Buffer& operator=(const Buffer& other) + { + resize(other.size_); + memcpy(ptr_, other.ptr_, other.size_); + return *this; + } + Buffer& operator=(Buffer&& other) + { + if (this != &other) + { + if (ptr_) free(ptr_); + ptr_ = other.ptr_; + size_ = other.size_; + cap_ = other.cap_; + other.ptr_ = nullptr; + other.size_ = 0; + other.cap_ = 0; + } + return *this; + } + + template const T* get() const { return static_cast(ptr_); } + template T* get() { return static_cast(ptr_); } + + size_t capacity() const { return cap_; } + size_t size() const { return size_; } + + void resize(size_t size) + { + size_ = size; + reserve(size); + } + + void reserve(size_t size) + { + if (cap_ >= size) + return; + cap_ = size; + ptr_ = realloc(ptr_, size); + } +}; + +} // namespace Common diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index d1bad4494f7b9..ca7125de56790 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -42,6 +42,7 @@ target_sources(common PRIVATE Assertions.h boost_spsc_queue.hpp BitUtils.h + Buffer.h ByteSwap.h Console.h CrashHandler.h @@ -64,6 +65,8 @@ target_sources(common PRIVATE MD5Digest.h MRCHelpers.h Path.h + Pcsx2Defs.h + Pcsx2Types.h PrecompiledHeader.h ProgressCallback.h ReadbackSpinManager.h @@ -83,6 +86,7 @@ target_sources(common PRIVATE WindowInfo.h WrappedMemCopy.h YAML.h + ZipHelpers.h ) if(ARCH_X86) diff --git a/common/Image.cpp b/common/Image.cpp index de6e51039f0fa..aa96c646b516c 100644 --- a/common/Image.cpp +++ b/common/Image.cpp @@ -55,7 +55,7 @@ static const FormatHandler* GetFormatHandler(const std::string_view extension) { for (const FormatHandler& handler : s_format_handlers) { - if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()) == 0) + if (StringUtil::compareNoCase(extension, handler.extension)) return &handler; } diff --git a/pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp b/pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp index cc94782d2a296..feb0bfee1224c 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp @@ -19,8 +19,8 @@ struct LoaderDefinition 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 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}, @@ -36,7 +36,7 @@ GSTextureReplacements::ReplacementTextureLoader GSTextureReplacements::GetLoader for (const LoaderDefinition& defn : s_loaders) { - if (StringUtil::Strncasecmp(extension.data(), defn.extension, extension.size()) == 0) + if (StringUtil::compareNoCase(extension, defn.extension)) return defn.loader; } @@ -146,7 +146,13 @@ static void ConvertTexture_R8G8B8(u32 width, u32 height, std::vector& data, // PNG Handlers //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -bool PNGLoader(const std::string& 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(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 std::string& filename, GSTextureReplacements::ReplacementTe 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_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& 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 std::string& filename, u32 mip_level, const DDSLoadInfo& info, u32 width, u32 height, std::vector& 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& data, u32& pitch, u32 size) { // D3D11 cannot handle block compressed textures where the first mip level is // not a multiple of the block size. @@ -578,12 +580,12 @@ static bool ReadDDSMipLevel(std::FILE* fp, const std::string& filename, u32 mip_ 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); + filename, info.block_size); return false; } 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 std::string& filename, u32 mip_ return true; } -bool DDSLoader(const std::string& 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.c_str(), "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 std::string& filename, GSTextureReplacements::ReplacementTe 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)); diff --git a/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp b/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp index 41c98cac8564a..77a28dfb6edfb 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureReplacements.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0+ #include "common/AlignedMalloc.h" +#include "common/Buffer.h" #include "common/Console.h" #include "common/HashCombine.h" #include "common/FileSystem.h" @@ -9,6 +10,7 @@ #include "common/StringUtil.h" #include "common/ScopedGuard.h" #include "common/TextureDecompress.h" +#include "common/ZipHelpers.h" #include "Config.h" #include "Host.h" @@ -100,16 +102,22 @@ 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 ParseReplacementName(const std::string& filename); + static std::optional ParseReplacementName(const char* filename); static std::string GetGameTextureDirectory(); static std::string GetDumpFilename(const TextureName& name, u32 level); template std::pair GetBCAlphaMinMax(ReplacementTexture& rtex); static void SetReplacementTextureAlphaMinMax(ReplacementTexture& rtex); - static std::optional 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 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 +135,7 @@ namespace GSTextureReplacements static std::mutex s_dumped_textures_mutex; /// Lookup map of texture names to replacements, if they exist. - static std::unordered_map s_replacement_texture_filenames; + static std::unordered_map s_replacement_texture_filenames; /// Lookup map of texture names without CLUT hash, to know when we need to disable paltex. static std::unordered_set s_replacement_textures_without_clut_hash; @@ -189,7 +197,7 @@ GSTextureCache::HashCacheKey GSTextureReplacements::HashCacheKeyFromTextureName( return key; } -std::optional GSTextureReplacements::ParseReplacementName(const std::string& filename) +std::optional GSTextureReplacements::ParseReplacementName(const char* filename) { TextureName ret; ret.miplevel = 0; @@ -197,7 +205,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std GSTextureCache::SourceRegion full_region; char extension_dot; - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_REGION_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, + if (std::sscanf(filename, TEXTURE_FILENAME_REGION_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, &ret.region_width, &ret.region_height, &ret.bits, &extension_dot) == 6 && extension_dot == '.') { @@ -205,7 +213,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std return ret; } - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_REGION_FORMAT_STRING "%c", &ret.TEX0Hash, + if (std::sscanf(filename, TEXTURE_FILENAME_REGION_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.region_width, &ret.region_height, &ret.bits, &extension_dot) == 5 && extension_dot == '.') { @@ -215,7 +223,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std } // Allow loading of dumped textures from older versions that included the full region bits. - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_OLD_REGION_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, + if (std::sscanf(filename, TEXTURE_FILENAME_OLD_REGION_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, &full_region.bits, &ret.bits, &extension_dot) == 5 && extension_dot == '.') { @@ -225,7 +233,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std return ret; } - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_OLD_REGION_FORMAT_STRING "%c", &ret.TEX0Hash, &full_region.bits, + if (std::sscanf(filename, TEXTURE_FILENAME_OLD_REGION_FORMAT_STRING "%c", &ret.TEX0Hash, &full_region.bits, &ret.bits, &extension_dot) == 4 && extension_dot == '.') { @@ -239,7 +247,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std ret.region_width = 0; ret.region_height = 0; - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, &ret.bits, + if (std::sscanf(filename, TEXTURE_FILENAME_CLUT_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.CLUTHash, &ret.bits, &extension_dot) == 4 && extension_dot == '.') { @@ -247,8 +255,7 @@ std::optional GSTextureReplacements::ParseReplacementName(const std return ret; } - if (std::sscanf(filename.c_str(), TEXTURE_FILENAME_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.bits, &extension_dot) == - 3 && + if (std::sscanf(filename, TEXTURE_FILENAME_FORMAT_STRING "%c", &ret.TEX0Hash, &ret.bits, &extension_dot) == 3 && extension_dot == '.') { ret.RemoveUnusedBits(); @@ -373,6 +380,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(); @@ -394,6 +413,8 @@ void GSTextureReplacements::ReloadReplacementMap() const std::string texture_dir = GetGameTextureDirectory(); const std::string replacement_dir(Path::Combine(texture_dir, TEXTURE_REPLACEMENT_SUBDIRECTORY_NAME)); + const std::string replacement_zip_name = s_current_serial + ".zip"; + const std::string replacement_zip = Path::Combine(EmuFolders::Textures, replacement_zip_name); FileSystem::FindResultsArray files; @@ -404,6 +425,8 @@ void GSTextureReplacements::ReloadReplacementMap() right_case_path = &texture_dir; else if (GetWrongCasePath(&wrong_case_path, texture_dir.c_str(), TEXTURE_REPLACEMENT_SUBDIRECTORY_NAME, &files)) right_case_path = &replacement_dir; + else if (GetWrongCasePath(&wrong_case_path, EmuFolders::Textures.c_str(), replacement_zip_name, &files)) + right_case_path = &replacement_zip; if (right_case_path) { Host::AddKeyedOSDMessage("TextureReplacementDirCaseMismatch", @@ -413,28 +436,47 @@ void GSTextureReplacements::ReloadReplacementMap() Host::OSD_WARNING_DURATION); } - if (!FileSystem::FindFiles(replacement_dir.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RECURSIVE, &files)) + files.clear(); + FileSystem::FindFiles(replacement_dir.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RECURSIVE, &files); + if (FileSystem::FileExists(replacement_zip.c_str())) + { + FILESYSTEM_FIND_DATA extra = {}; + extra.FileName = replacement_zip; + files.push_back(std::move(extra)); + } + if (files.empty()) return; - std::string filename; for (FILESYSTEM_FIND_DATA& fd : files) { // file format we can handle? - filename = Path::GetFileName(fd.FileName); + 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 (StringUtil::compareNoCase(ext, "zip")) + { + 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 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 name = ParseReplacementName(filename); - if (!name.has_value()) - continue; - - DbgCon.WriteLn("Found %ux%u replacement '%.*s'", name->Width(), name->Height(), static_cast(filename.size()), filename.data()); - 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 name = ParseReplacementName(filename)) + AddReplacementTexture(*name, filename, std::move(fd.FileName)); } if (!s_replacement_texture_filenames.empty()) @@ -538,7 +580,7 @@ GSTexture* GSTextureReplacements::LookupReplacementTexture(const GSTextureCache: else { // synchronous load - std::optional replacement(LoadReplacementTexture(name, fnit->second, !mipmap)); + std::optional replacement(LoadReplacementTexture(fnit->second, !mipmap)); if (!replacement.has_value()) return nullptr; @@ -629,17 +671,59 @@ void GSTextureReplacements::SetReplacementTextureAlphaMinMax(ReplacementTexture& } } -std::optional GSTextureReplacements::LoadReplacementTexture(const TextureName& name, const std::string& filename, bool only_base_image) +std::optional 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()) + { + 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; + Common::Buffer buf(stat.size); + auto zff = zip_fopen_index_managed(zf.get(), file.archive_idx, 0); + if (!zff || static_cast(zip_fread(zff.get(), buf.get(), 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.get(), 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 { - Console.Warning("Failed to load replacement texture %s", filename.c_str()); - return std::nullopt; + 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); @@ -647,7 +731,7 @@ std::optional 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); @@ -668,7 +752,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 replacement(LoadReplacementTexture(name, filename, !mipmap)); + std::optional 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 @@ -977,3 +1061,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(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; +} diff --git a/pcsx2/GS/Renderers/HW/GSTextureReplacements.h b/pcsx2/GS/Renderers/HW/GSTextureReplacements.h index ef176ed376715..079cbcf354cfc 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureReplacements.h +++ b/pcsx2/GS/Renderers/HW/GSTextureReplacements.h @@ -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 + 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(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 std::string& 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).