diff --git a/Common/File/VFS/DirectoryReader.cpp b/Common/File/VFS/DirectoryReader.cpp index b820a45196..386568d70b 100644 --- a/Common/File/VFS/DirectoryReader.cpp +++ b/Common/File/VFS/DirectoryReader.cpp @@ -1,16 +1,73 @@ +#include "ppsspp_config.h" + +#include #include #include "Common/Common.h" #include "Common/Log.h" +#include "Common/File/DirListing.h" #include "Common/File/VFS/DirectoryReader.h" -DirectoryReader::DirectoryReader(const Path &path) { +DirectoryReader::DirectoryReader(const Path &path, bool ignoreCase) { path_ = path; +#if !PPSSPP_PLATFORM(WINDOWS) + // Windows file systems already ignore case. + ignoreCase_ = ignoreCase; +#endif +} + +static std::string ToLowerASCII(std::string_view str) { + std::string lower(str); + for (char &c : lower) { + c = (char)tolower((unsigned char)c); + } + return lower; +} + +void DirectoryReader::IndexDirectory(const Path &dir, const std::string &prefix) { + std::vector files; + File::GetFilesInDir(dir, &files); + for (const File::FileInfo &file : files) { + const std::string relative = prefix + file.name; + if (file.isDirectory) { + IndexDirectory(file.fullName, relative + "/"); + } else { + lowerIndex_[ToLowerASCII(relative)] = relative; + } + } +} + +bool DirectoryReader::FindIgnoringCase(std::string_view path, std::string *actualPath) { + if (!ignoreCase_) { + return false; + } + std::lock_guard guard(indexLock_); + if (!indexBuilt_) { + // Only done on the first miss, so a directory where every name matches never pays for it. + IndexDirectory(path_, ""); + indexBuilt_ = true; + INFO_LOG(Log::IO, "Indexed %d files in %s to find names that differ in case", (int)lowerIndex_.size(), path_.c_str()); + } + auto it = lowerIndex_.find(ToLowerASCII(path)); + if (it == lowerIndex_.end()) { + return false; + } + *actualPath = it->second; + if (caseFixCount_++ < 5) { + WARN_LOG(Log::IO, "'%.*s' differs in case from the file '%s' in %s, which only works on some file systems", STR_VIEW(path), actualPath->c_str(), path_.c_str()); + } + return true; } uint8_t *DirectoryReader::ReadFile(std::string_view path, size_t *size) { - Path new_path = Path(path).StartsWith(path_) ? Path(path) : path_ / path; - return File::ReadLocalFile(new_path, size); + const bool absolute = Path(path).StartsWith(path_); + Path new_path = absolute ? Path(path) : path_ / path; + uint8_t *data = File::ReadLocalFile(new_path, size); + std::string actualPath; + if (!data && !absolute && FindIgnoringCase(path, &actualPath)) { + data = File::ReadLocalFile(path_ / actualPath, size); + } + return data; } bool DirectoryReader::GetFileListing(std::string_view path, std::vector *listing, const char *filter = nullptr) { @@ -53,7 +110,11 @@ public: VFSFileReference *DirectoryReader::GetFile(std::string_view path) { Path filePath = path_ / path; if (!File::Exists(filePath)) { - return nullptr; + std::string actualPath; + if (!FindIgnoringCase(path, &actualPath)) { + return nullptr; + } + filePath = path_ / actualPath; } DirectoryReaderFileReference *reference = new DirectoryReaderFileReference(); diff --git a/Common/File/VFS/DirectoryReader.h b/Common/File/VFS/DirectoryReader.h index 6bb078ae4a..b291e7610b 100644 --- a/Common/File/VFS/DirectoryReader.h +++ b/Common/File/VFS/DirectoryReader.h @@ -1,12 +1,19 @@ #pragma once +#include +#include +#include + #include "Common/File/VFS/VFS.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" class DirectoryReader : public VFSBackend { public: - explicit DirectoryReader(const Path &path); + // With ignoreCase, GetFile and ReadFile also find files whose names only differ in case, like the + // zip reader does. Meant for directories that people fill by hand on Windows, like texture packs, + // which then break on case sensitive file systems. + explicit DirectoryReader(const Path &path, bool ignoreCase = false); // use delete[] on the returned value. uint8_t *ReadFile(std::string_view path, size_t *size) override; @@ -27,5 +34,16 @@ public: } private: + // Returns the path (relative to path_) of a file whose name matches path except for case. + bool FindIgnoringCase(std::string_view path, std::string *actualPath); + void IndexDirectory(const Path &dir, const std::string &prefix); + Path path_; + bool ignoreCase_ = false; + + // Lowercase relative path -> actual relative path of every file, built on the first miss. + std::mutex indexLock_; + bool indexBuilt_ = false; + std::unordered_map lowerIndex_; + int caseFixCount_ = 0; }; diff --git a/Common/File/VFS/ZipFileReader.cpp b/Common/File/VFS/ZipFileReader.cpp index 83655bc989..595083f0f3 100644 --- a/Common/File/VFS/ZipFileReader.cpp +++ b/Common/File/VFS/ZipFileReader.cpp @@ -351,7 +351,14 @@ public: VFSFileReference *ZipFileReader::GetFile(std::string_view path) { std::string p(path); - int zi = zip_name_locate(zip_file_, p.c_str(), ZIP_FL_NOCASE); // this is EXPENSIVE + // Texture replacement looks up files from several threads. libzip isn't thread safe, and the lookup + // writes to the archive (it caches each name's encoding, and a converted copy for CP437 names). + std::lock_guard guard(lock_); + // An exact match uses libzip's hash table, the case insensitive one compares every name in the zip. + int zi = zip_name_locate(zip_file_, p.c_str(), 0); + if (zi < 0) { + zi = zip_name_locate(zip_file_, p.c_str(), ZIP_FL_NOCASE); + } if (zi < 0) { // Not found. return nullptr; diff --git a/GPU/Common/ReplacedTexture.cpp b/GPU/Common/ReplacedTexture.cpp index cf7d3187d2..7d4737496b 100644 --- a/GPU/Common/ReplacedTexture.cpp +++ b/GPU/Common/ReplacedTexture.cpp @@ -32,7 +32,6 @@ #include "Common/Data/Format/DDSLoad.h" #include "Common/Data/Format/ZIMLoad.h" #include "Common/Data/Format/PNGLoad.h" -#include "Common/Thread/ParallelLoop.h" #include "Common/Thread/Waitable.h" #include "Common/Thread/ThreadManager.h" #include "Common/Log.h" @@ -220,7 +219,6 @@ bool ReplacedTexture::Poll(double budget) { // Successfully waited! Can get rid of it. threadWaitable_->WaitAndRelease(); threadWaitable_ = nullptr; - lastUsed = now; } lastUsed_ = now; return true; @@ -289,10 +287,12 @@ void ReplacedTexture::Prepare(VFSBackend *vfs) { } std::string path(desc_.filenames[i]); - // Defense in depth: skip filenames that could escape the pack dir. + // Defense in depth: skip filenames that could escape the pack dir. Like a missing file, this ends + // the levels (without any, the texture is NOT_FOUND). if (HasParentDirComponent(path)) { - SetState(ReplacementState::CANCEL_INIT); - return; + ERROR_LOG(Log::TexReplacement, "Ignoring texture filename with parent dir component: %s", path.c_str()); + result = LoadLevelResult::DONE; + break; } VFSFileReference *fileRef = vfs_->GetFile(path.c_str()); if (!fileRef) { @@ -1007,9 +1007,6 @@ bool ReplacedTexture::CopyLevelTo(int level, uint8_t *out, size_t outDataSize, i return false; } - // Dubious if this is worth it, sometimes seems to help, sometimes not. -#define PARALLEL_COPY - int blockSize; if (!Draw::DataFormatIsBlockCompressed(fmt, &blockSize)) { if (fmt != Draw::DataFormat::R8G8B8A8_UNORM) { @@ -1024,46 +1021,28 @@ bool ReplacedTexture::CopyLevelTo(int level, uint8_t *out, size_t outDataSize, i _assert_msg_(data.size() == info.w * info.h * 4, "Data has wrong size"); + // Plain copies: one core already saturates memory bandwidth, and pool tasks can wait behind other + // work queued there (like pipeline compiles). if (rowPitch == info.w * 4) { -#ifdef PARALLEL_COPY - ParallelMemcpy(&g_threadManager, out, data.data(), info.w * 4 * info.h); -#else memcpy(out, data.data(), info.w * 4 * info.h); -#endif } else { -#ifdef PARALLEL_COPY - const int MIN_LINES_PER_THREAD = 4; - const int MAX_THREADS = 6; - ParallelRangeLoop(&g_threadManager, [&](int l, int h) { - int extraPixels = outW - info.w; - for (int y = l; y < h; ++y) { - memcpy((uint8_t *)out + rowPitch * y, data.data() + info.w * 4 * y, info.w * 4); - // Fill the rest of the line with black. - memset((uint8_t *)out + rowPitch * y + info.w * 4, 0, extraPixels * 4); - } - }, 0, info.h, MIN_LINES_PER_THREAD, MAX_THREADS); -#else int extraPixels = outW - info.w; for (int y = 0; y < info.h; ++y) { memcpy((uint8_t *)out + rowPitch * y, data.data() + info.w * 4 * y, info.w * 4); + // Fill the rest of the line with black. memset((uint8_t *)out + rowPitch * y + info.w * 4, 0, extraPixels * 4); } -#endif - // Memset the rest of the padding to avoid leaky edge pixels. Guess we could parallelize this too, but meh. + // Memset the rest of the padding to avoid leaky edge pixels. for (int y = info.h; y < outH; y++) { uint8_t *dest = (uint8_t *)out + rowPitch * y; memset(dest, 0, outW * 4); } } } else { -#ifdef PARALLEL_COPY - // Only parallel copy in the simple case for now. if (info.w == outW && info.h == outH) { - // TODO: Add sanity checks here for other formats? - ParallelMemcpy(&g_threadManager, out, data.data(), data.size()); + memcpy(out, data.data(), data.size()); return true; } -#endif // Alright, so careful copying of blocks it is, padding with zero-blocks as needed. int inBlocksW = (info.w + 3) / 4; int inBlocksH = (info.h + 3) / 4; diff --git a/GPU/Common/ReplacedTexture.h b/GPU/Common/ReplacedTexture.h index 11d545eb18..02bb7459e5 100644 --- a/GPU/Common/ReplacedTexture.h +++ b/GPU/Common/ReplacedTexture.h @@ -259,7 +259,6 @@ private: std::mutex lock_; Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED; TextureAlpha alphaStatus_ = TextureAlpha::Any; - double lastUsed = 0.0; std::atomic state_ = ReplacementState::UNLOADED; diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index fa59ee3e44..1241518dea 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -51,8 +51,6 @@ // If a texture hasn't been seen for this many frames, get rid of it. #define TEXTURE_KILL_AGE 200 -#define TEXTURE_KILL_AGE_LOWMEM 60 -// Not used in lowmem mode. #define TEXTURE_SECOND_KILL_AGE 100 // Used when there are multiple CLUT variants of a texture, to avoid blowing up memory. See #10418 // We should change this to do shader-based palette expansion. @@ -63,8 +61,10 @@ // Try to be prime to other decimation intervals. #define TEXCACHE_DECIMATION_INTERVAL 13 -#define TEXCACHE_MIN_PRESSURE 16 * 1024 * 1024 // Total in VRAM -#define TEXCACHE_SECOND_MIN_PRESSURE 4 * 1024 * 1024 +// Below these sizes (in host memory, see EstimateTexMemoryUsage) nothing is evicted. Above them, textures +// that haven't been used for the kill age are. A single 2048x2048 replacement is 16MB. +#define TEXCACHE_MIN_PRESSURE 128 * 1024 * 1024 +#define TEXCACHE_SECOND_MIN_PRESSURE 32 * 1024 * 1024 static bool MatchFramebuffer(const TextureDefinition &entry, VirtualFramebuffer *framebuffer, u32 texaddrOffset, RasterChannel channel, bool useBufferedRendering, FramebufferMatchInfo *matchInfo); static bool GetBestFramebufferCandidate(FramebufferManagerCommon *fbManager, const TextureDefinition &entry, u32 texAddrOffset, AttachCandidate *bestCandidate, const char *context); @@ -265,8 +265,9 @@ SamplerCacheKey TextureCacheCommon::GetSamplingParams(int maxLevel, const TexCac bool useReplacerFiltering = false; // (Not for indices that the shader colors, filtering them would mix up the palette entries.) if (entry && replacer_.Enabled() && entry->replacedTexture && !(entry->status & TexStatus::CLUT8_INDEXED)) { - // If replacement textures have multiple mip levels, enforce mip filtering. - if (entry->replacedTexture->State() == ReplacementState::ACTIVE && entry->replacedTexture->NumLevels() > 1) { + // If replacement textures have multiple mip levels, enforce mip filtering. Not if only one got + // uploaded, as with a fake mipmap change. + if (entry->replacedTexture->State() == ReplacementState::ACTIVE && entry->replacedTexture->NumLevels() > 1 && !(entry->status & TexStatus::NO_MIPS)) { key.mipEnable = true; key.mipFilt = 1; key.maxLevel = 9 * 256; @@ -646,11 +647,15 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { reason = "scaling"; } - if (match && (entry->status & TexStatus::TO_REPLACE) && replacementTimeThisFrame_ < replacementFrameBudgetSeconds_) { + if (match && (entry->status & TexStatus::TO_REPLACE)) { int w0 = gstate.getTextureWidth(0); int h0 = gstate.getTextureHeight(0); int d0 = 1; if (entry->replacedTexture) { + // Polling is cheap, so it's done every time. Only rebuilding the texture waits for the frame's + // budget, otherwise whatever used up the budget early in each frame could keep the textures + // drawn after it from ever getting their replacements. + const bool canRebuild = replacementTimeThisFrame_ < replacementFrameBudgetSeconds_; PollReplacement(entry, &w0, &h0, &d0); // This texture is pending a replacement load. // So check the replacer if it's reached a conclusion. @@ -660,16 +665,24 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) { DEBUG_LOG(Log::TexReplacement, "No replacement for texture %dx%d", w0, h0); entry->status &= ~TexStatus::TO_REPLACE; if (g_Config.bSaveNewTextures) { - // Load it once more to actually save it. Since we don't set STATUS_TO_REPLACE, we won't end up looping. - match = false; - reason = "replacing"; + if (canRebuild) { + // Load it once more to actually save it. Since we don't set STATUS_TO_REPLACE, we won't end up looping. + match = false; + reason = "replacing"; + } else { + entry->status |= TexStatus::TO_REPLACE; + } } break; case ReplacementState::ACTIVE: // There is now replacement data available! - // Just reload the texture to process the replacement. - match = false; - reason = "replacing"; + // Just reload the texture to process the replacement, or try again next time. + if (canRebuild) { + match = false; + reason = "replacing"; + } else { + entry->status |= TexStatus::TO_REPLACE; + } break; default: // We'll just wait for a result. @@ -1725,9 +1738,12 @@ bool TextureCacheCommon::GetCurrentClutBuffer(GPUDebugBuffer &buffer) { return true; } -// Host memory usage, not PSP memory usage. +// Host memory usage, not PSP memory usage. Level 0 only. u32 TexCacheEntry::EstimateTexMemoryUsage() const { - // TODO: This does not take into account the HD remaster's larger textures. + return hostMemorySize != 0 ? hostMemorySize : EstimatePSPSizeMemoryUsage(); +} + +u32 TexCacheEntry::EstimatePSPSizeMemoryUsage() const { const u8 dimW = ((dim >> 0) & 0xf); const u8 dimH = ((dim >> 8) & 0xf); @@ -3106,6 +3122,14 @@ bool TextureCacheCommon::PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEnt SaveIndexedTexture(entry, plan); } + // A replacement or upscaled texture takes many times the PSP size, and the decimation in + // Decimate() goes by this. + if (plan.doReplace) { + entry->hostMemorySize = plan.replaced->GetLevelDataSizeAfterCopy(0); + } else { + entry->hostMemorySize = entry->EstimatePSPSizeMemoryUsage() * plan.scaleFactor * plan.scaleFactor * plan.depth; + } + // Will be filled in again during decode. entry->SetAlphaStatus(TextureAlpha::Any); return true; @@ -3212,16 +3236,20 @@ void TextureCacheCommon::LoadTextureLevel(TexCacheEntry &entry, uint8_t *data, s PROFILE_THIS_SCOPE("decodetex"); + // Replacements and dumps go by the level of the texture we create. With a fake mipmap change, + // its level 0 comes from PSP level baseLevelSrc. + const int dstLevel = srcLevel - plan.baseLevelSrc; + if (plan.doReplace) { - plan.replaced->GetSize(srcLevel, &w, &h); + plan.replaced->GetSize(dstLevel, &w, &h); double replaceStart = time_now_d(); if (plan.decodeToClut8) { // Only an indexed replacement for the GPU gets here. The indices go up as they are, as an R8 image. - plan.replaced->CopyIndexLevelToPitched(srcLevel, data, dataSize, stride); + plan.replaced->CopyIndexLevelToPitched(dstLevel, data, dataSize, stride); } else if (plan.replaced->IsIndexed()) { - CopyIndexedReplacementLevel(plan.replaced, srcLevel, data, dataSize, stride); + CopyIndexedReplacementLevel(plan.replaced, dstLevel, data, dataSize, stride); } else { - plan.replaced->CopyLevelTo(srcLevel, data, dataSize, stride); + plan.replaced->CopyLevelTo(dstLevel, data, dataSize, stride); } replacementTimeThisFrame_ += time_now_d() - replaceStart; } else { @@ -3281,7 +3309,7 @@ void TextureCacheCommon::LoadTextureLevel(TexCacheEntry &entry, uint8_t *data, s replacedInfo.fmt = dstFmt; // NOTE: Reading the decoded texture here may be very slow, if we just wrote it to write-combined memory. - replacer_.NotifyTextureDecoded(plan.replaced, replacedInfo, pixelData, decPitch, srcLevel, w, h, scaledW, scaledH); + replacer_.NotifyTextureDecoded(plan.replaced, replacedInfo, pixelData, decPitch, dstLevel, w, h, scaledW, scaledH); } } } diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index d626d1744b..9724c4bb37 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -194,6 +194,8 @@ struct TexCacheEntry { #endif int lastFrame; int lastSyncDomain; + // Bytes of host memory the texture takes, set when it's built. See EstimateTexMemoryUsage. + u32 hostMemorySize = 0; TextureAlpha GetAlphaStatus() const { return (status & TexStatus::ALPHA_SOLID) ? TextureAlpha::Solid : TextureAlpha::Any; @@ -222,6 +224,8 @@ struct TexCacheEntry { u64 CacheKey() const; static u64 CacheKey(u32 addr, u8 format, u16 dim, u32 cluthash); u32 EstimateTexMemoryUsage() const; + // The size of the texture's level 0 at the PSP's size, in 32 bits for the CLUT formats. + u32 EstimatePSPSizeMemoryUsage() const; }; // TODO: Work on shrinking it further. @@ -286,7 +290,7 @@ struct BuildTexturePlan { int maxPossibleLevels; // Load the 0-mip from this PSP texture level instead of 0. - // If non-zero, we are only loading one level. + // If non-zero, we are only loading one level. Replacements and dumps still treat it as level 0. int baseLevelSrc; // The scale factor of the final texture. diff --git a/GPU/Common/TextureReplacer.cpp b/GPU/Common/TextureReplacer.cpp index a947410a06..650485285a 100644 --- a/GPU/Common/TextureReplacer.cpp +++ b/GPU/Common/TextureReplacer.cpp @@ -169,7 +169,7 @@ bool TextureReplacer::LoadIni(std::string *error, bool notify) { if (!dir) { INFO_LOG(Log::TexReplacement, "%s wasn't a zip file - opening the directory %s instead.", zipPath.c_str(), basePath_.c_str()); vfsIsZip_ = false; - dir = new DirectoryReader(basePath_); + dir = new DirectoryReader(basePath_, true); } else { if (!replaceEnabled_ && saveEnabled_) { WARN_LOG(Log::TexReplacement, "Found zip file even though only saving is enabled! This is weird."); @@ -904,9 +904,12 @@ void TextureReplacer::NotifyTextureDecoded(ReplacedTexture *texture, const Repla hashfile = HashName(replacementKey, level) + ".png"; } + if (level >= (int)ARRAY_SIZE(SavedTextureCacheData::levelSaved)) { + return; + } auto it = savedCache_.find(replacementKey); - if (it != savedCache_.end()) { - // We've already saved this texture. Ignore it. + if (it != savedCache_.end() && it->second.levelSaved[level]) { + // We've already saved this level. Ignore it. // We don't really care about changing the scale factor during runtime, only confusing. return; } diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index 457b24717f..3050cf55dd 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -220,7 +220,7 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) { Draw::DataFormat dstFmt = GetDestFormat(GETextureFormat(entry->format), gstate.getClutPaletteFormat()); if (plan.doReplace) { - plan.replaced->GetSize(plan.baseLevelSrc, &tw, &th); + plan.replaced->GetSize(0, &tw, &th); // Indices for the GPU are uploaded as the R8 format of the CLUT8 textures. dstFmt = plan.decodeToClut8 ? Draw::DataFormat::R8_UNORM : plan.replaced->Format(); } else if (plan.scaleFactor > 1 || plan.saveTexture) { diff --git a/GPU/Metal/TextureCacheMetal.mm b/GPU/Metal/TextureCacheMetal.mm index caacf4e4ab..923f05043a 100644 --- a/GPU/Metal/TextureCacheMetal.mm +++ b/GPU/Metal/TextureCacheMetal.mm @@ -434,7 +434,7 @@ void TextureCacheMetal::BuildTexture(TexCacheEntry *const entry) { } size_t size = (size_t)stride * rows; if (plan.doReplace && compressed) { - size = std::max(size, (size_t)plan.replaced->GetLevelDataSizeAfterCopy(plan.baseLevelSrc + i)); + size = std::max(size, (size_t)plan.replaced->GetLevelDataSizeAfterCopy(i)); } if (computeUpload) { const int srcWidth = gstate.getTextureWidth(i); diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 5e6b776a44..1b07e163ae 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -754,15 +754,15 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) { int rowLength = pixelStride; if (bcFormat) { // For block compressed formats, we just set the upload size to the data size.. - uploadSize = plan.replaced->GetLevelDataSizeAfterCopy(plan.baseLevelSrc + i); + uploadSize = plan.replaced->GetLevelDataSizeAfterCopy(i); rowLength = (mipWidth + 3) & ~3; } // Directly load the replaced image. data = pushBuffer->Allocate(uploadSize, pushAlignment, &texBuf, &bufferOffset); double replaceStart = time_now_d(); const bool copied = plan.decodeToClut8 ? - plan.replaced->CopyIndexLevelToPitched(plan.baseLevelSrc + i, (uint8_t *)data, uploadSize, byteStride) : - plan.replaced->CopyLevelTo(plan.baseLevelSrc + i, (uint8_t *)data, uploadSize, byteStride); + plan.replaced->CopyIndexLevelToPitched(i, (uint8_t *)data, uploadSize, byteStride) : + plan.replaced->CopyLevelTo(i, (uint8_t *)data, uploadSize, byteStride); if (!copied) { // If plan.doReplace, this shouldn't fail. WARN_LOG(Log::G3D, "Failed to copy replaced texture level"); // TODO: Fill with some pattern? @@ -815,7 +815,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) { replacedInfo.isVideo = IsVideo(entry->addr); replacedInfo.isFinal = (entry->status & TexStatus::TO_SCALE) == 0; replacedInfo.fmt = FromVulkanFormat(actualFmt); - replacer_.NotifyTextureDecoded(plan.replaced, replacedInfo, data, stride, plan.baseLevelSrc + i, mipUnscaledWidth, mipUnscaledHeight, w, h); + replacer_.NotifyTextureDecoded(plan.replaced, replacedInfo, data, stride, i, mipUnscaledWidth, mipUnscaledHeight, w, h); } } } diff --git a/docs/debugging.md b/docs/debugging.md index 67af0b05fe..c9c6f20fea 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -55,7 +55,9 @@ A working invocation, and the traps around it: - **The timeouts apply to the whole session**, not per test. `--timeout-wall=N` is real seconds and `--timeout-emulated=N` is emulated ones; both default to infinity, both can be set at once, and whichever - is reached first ends the run (`TIMEOUT` or `TIMEOUT (emulated)`). `--timeout` is the old name for + is reached first ends the run (`TIMEOUT` or `TIMEOUT (emulated)`). With `--state=`, emulated time counts + from when the state has loaded: until then the game boots for however long the load takes on the host, so + counting from boot made runs end on different frames. `--timeout` is the old name for `--timeout-wall`. As soon as you pass one it applies to your whole interactive debugging session too, so pass something huge (`--timeout-wall=100000`) or the process exits out from under you mid-session. The wall-clock check is skipped while `IsDebuggerPresent()`; the emulated one doesn't need that escape hatch, diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 85c028e454..46bee0dd8b 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -547,7 +547,9 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar const double emulatedNow = CoreTiming::GetGlobalTimeUs() / 1000000.0; const double emulatedStep = emulatedNow - lastEmulatedTime; lastEmulatedTime = emulatedNow; - if (emulatedStep > 0.0 && emulatedStep < emulatedStepLimit) { + // With --state, count from when the state lands. Until then the game boots, for however long + // reading and decompressing the state takes on the host, which made runs end on different frames. + if (emulatedStep > 0.0 && emulatedStep < emulatedStepLimit && !g_stateLoadPending) { emulatedElapsed += emulatedStep; } diff --git a/unittest/TestTextureReplacer.cpp b/unittest/TestTextureReplacer.cpp index 21e4c6d1d4..15c454c97e 100644 --- a/unittest/TestTextureReplacer.cpp +++ b/unittest/TestTextureReplacer.cpp @@ -6,6 +6,7 @@ #include "Common/Data/Format/PNGLoad.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" +#include "Common/File/VFS/DirectoryReader.h" #include "Common/Thread/ThreadManager.h" #include "GPU/Common/TextureReplacer.h" @@ -711,6 +712,30 @@ static bool TestSaveIndexedOption(const Path &packDir) { return true; } +// Packs made on Windows often name their folders differently in textures.ini (#18746). On a case +// insensitive file system this passes without the index, Linux CI is where it counts. +static bool TestDirectoryIgnoreCase(const Path &packDir) { + if (!File::CreateDir(packDir / "Portraits")) { + return false; + } + if (!CreateTestPNG(packDir / "Portraits" / "Face.png", 4, 4, 0xFF0000FF)) { + return false; + } + + DirectoryReader reader(packDir, true); + VFSFileReference *ref = reader.GetFile("portraits/face.png"); + EXPECT_TRUE(ref != nullptr); + if (ref) { + reader.ReleaseFile(ref); + } + size_t size = 0; + uint8_t *data = reader.ReadFile("PORTRAITS/FACE.PNG", &size); + EXPECT_TRUE(data != nullptr && size != 0); + delete[] data; + EXPECT_TRUE(reader.GetFile("portraits/missing.png") == nullptr); + return true; +} + bool TestTextureReplacer() { Path packDir = UnitTestTempPath("unittest_texture_pack"); if (!CreateTestPack(packDir)) { @@ -741,7 +766,7 @@ bool TestTextureReplacer() { } if (!TestIndexedPNG(packDir) || !TestIndexedReplacement(packDir) || !TestIndexedKTX2(packDir) || - !TestSaveIndexedPNG(packDir) || !TestSaveIndexedOption(packDir)) { + !TestSaveIndexedPNG(packDir) || !TestSaveIndexedOption(packDir) || !TestDirectoryIgnoreCase(packDir)) { File::DeleteDirRecursively(packDir); return false; }