Merge pull request #22677 from hrydgard/texreplace-fake-mip

Texture replacement fixes: fake mipmap levels, case-insensitive packs, cache sizing
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-20 12:35:41 +02:00
commit 58f2eae3b4
14 files changed
+199 -71

No files matched your search

+65 -4
View File
@@ -1,16 +1,73 @@
#include "ppsspp_config.h"
#include <cctype>
#include <cstdio>
#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<File::FileInfo> 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<std::mutex> 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<File::FileInfo> *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();
+19 -1
View File
@@ -1,12 +1,19 @@
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
#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<std::string, std::string> lowerIndex_;
int caseFixCount_ = 0;
};
+8 -1
View File
@@ -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<std::mutex> 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;
+10 -31
View File
@@ -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;
-1
View File
@@ -259,7 +259,6 @@ private:
std::mutex lock_;
Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED;
TextureAlpha alphaStatus_ = TextureAlpha::Any;
double lastUsed = 0.0;
std::atomic<ReplacementState> state_ = ReplacementState::UNLOADED;
+48 -20
View File
@@ -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);
}
}
}
+5 -1
View File
@@ -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.
+6 -3
View File
@@ -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;
}
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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);
+4 -4
View File
@@ -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);
}
}
}
+3 -1
View File
@@ -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,
+3 -1
View File
@@ -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;
}
+26 -1
View File
@@ -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;
}