mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-21 02:09:05 +02:00
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:
14 files changed
+199
-71
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user