Merge pull request #15025 from unknownbrackets/texreplace-pop

Allow delayed loading of texture replacements
This commit is contained in:
Henrik Rydgård 2021-10-25 23:31:30 +02:00 committed by GitHub
commit edc4e69c3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 271 additions and 37 deletions

View file

@ -887,6 +887,7 @@ static ConfigSetting graphicsSettings[] = {
ReportedConfigSetting("ReplaceTextures", &g_Config.bReplaceTextures, true, true, true),
ReportedConfigSetting("SaveNewTextures", &g_Config.bSaveNewTextures, false, true, true),
ConfigSetting("IgnoreTextureFilenames", &g_Config.bIgnoreTextureFilenames, false, true, true),
ConfigSetting("ReplaceTexturesAllowLate", &g_Config.bReplaceTexturesAllowLate, true, true, true),
ReportedConfigSetting("TexScalingLevel", &g_Config.iTexScalingLevel, 1, true, true),
ReportedConfigSetting("TexScalingType", &g_Config.iTexScalingType, 0, true, true),

View file

@ -197,6 +197,7 @@ public:
bool bReplaceTextures;
bool bSaveNewTextures;
bool bIgnoreTextureFilenames;
bool bReplaceTexturesAllowLate;
int iTexScalingLevel; // 0 = auto, 1 = off, 2 = 2x, ..., 5 = 5x
int iTexScalingType; // 0 = xBRZ, 1 = Hybrid
bool bTexDeposterize;

View file

@ -17,6 +17,7 @@
#include "ppsspp_config.h"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <png.h>
@ -31,6 +32,7 @@
#include "Common/File/FileUtil.h"
#include "Common/StringUtils.h"
#include "Common/Thread/ParallelLoop.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Host.h"
#include "Core/System.h"
@ -621,6 +623,15 @@ void TextureReplacer::NotifyTextureDecoded(const ReplacedTextureDecodeInfo &repl
savedCache_[replacementKey] = saved;
}
void TextureReplacer::Decimate(bool forcePressure) {
// Allow replacements to be cached for a long time, although they're large.
const double age = forcePressure ? 90.0 : 1800.0;
const double threshold = time_now_d() - age;
for (auto &item : cache_) {
item.second.PurgeIfOlder(threshold);
}
}
template <typename Key, typename Value>
static typename std::unordered_map<Key, Value>::const_iterator LookupWildcard(const std::unordered_map<Key, Value> &map, Key &key, u64 cachekey, u32 hash, bool ignoreAddress) {
auto alias = map.find(key);
@ -733,15 +744,114 @@ float TextureReplacer::LookupReduceHashRange(int& w, int& h) {
}
}
bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
class LimitedWaitable : public Waitable {
public:
LimitedWaitable() {
triggered_ = false;
}
void Wait() override {
if (!triggered_) {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [&] { return !triggered_; });
}
}
bool WaitFor(double budget) {
uint32_t us = budget > 0 ? (uint32_t)(budget * 1000000.0) : 0;
if (!triggered_) {
if (us == 0)
return false;
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait_for(lock, std::chrono::microseconds(us), [&] { return !triggered_; });
}
return triggered_;
}
void Notify() {
std::unique_lock<std::mutex> lock(mutex_);
triggered_ = true;
cond_.notify_all();
}
private:
std::condition_variable cond_;
std::mutex mutex_;
std::atomic<bool> triggered_;
};
class ReplacedTextureTask : public Task {
public:
ReplacedTextureTask(ReplacedTexture &tex, LimitedWaitable *w) : tex_(tex), waitable_(w) {
}
void Run() override {
tex_.Prepare();
waitable_->Notify();
}
private:
ReplacedTexture &tex_;
LimitedWaitable *waitable_;
};
bool ReplacedTexture::IsReady(double budget) {
lastUsed_ = time_now_d();
if (threadWaitable_) {
if (!threadWaitable_->WaitFor(budget)) {
return false;
} else {
threadWaitable_->WaitAndRelease();
threadWaitable_ = nullptr;
}
}
// Loaded already, or not yet on a thread?
if (!levelData_.empty())
return true;
// Let's not even start a new texture if we're already behind.
if (budget < 0.0)
return false;
if (g_Config.bReplaceTexturesAllowLate) {
threadWaitable_ = new LimitedWaitable();
g_threadManager.EnqueueTask(new ReplacedTextureTask(*this, threadWaitable_), TaskType::IO_BLOCKING);
if (threadWaitable_->WaitFor(budget)) {
threadWaitable_->WaitAndRelease();
threadWaitable_ = nullptr;
// If we finished all the levels, we're done.
return !levelData_.empty();
}
} else {
Prepare();
return true;
}
// Still pending on thread.
return false;
}
void ReplacedTexture::Prepare() {
levelData_.resize(MaxLevel() + 1);
for (int i = 0; i <= MaxLevel(); ++i) {
if (cancelPrepare_)
break;
PrepareData(i);
}
}
void ReplacedTexture::PrepareData(int level) {
_assert_msg_((size_t)level < levels_.size(), "Invalid miplevel");
_assert_msg_(out != nullptr && rowPitch > 0, "Invalid out/pitch");
const ReplacedTextureLevel &info = levels_[level];
std::vector<uint8_t> &out = levelData_[level];
FILE *fp = File::OpenCFile(info.file, "rb");
if (!fp) {
return false;
// Leaving the data sized at zero means failure.
return;
}
auto imageType = Identify(fp);
@ -751,29 +861,36 @@ bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
if (!zim) {
ERROR_LOG(G3D, "Failed to allocate memory for texture replacement");
fclose(fp);
return false;
return;
}
if (fread(&zim[0], 1, zimSize, fp) != zimSize) {
ERROR_LOG(G3D, "Could not load texture replacement: %s - failed to read ZIM", info.file.c_str());
fclose(fp);
return false;
return;
}
int w, h, f;
uint8_t *image;
const int MIN_LINES_PER_THREAD = 4;
if (LoadZIMPtr(&zim[0], zimSize, &w, &h, &f, &image)) {
ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
for (int y = l; y < h; ++y) {
memcpy((uint8_t *)out + rowPitch * y, image + w * 4 * y, w * 4);
if (w > info.w || h > info.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", info.file.c_str());
fclose(fp);
return;
}
out.resize(info.w * info.h * 4);
if (w == info.w) {
memcpy(&out[0], image, info.w * 4 * info.h);
} else {
for (int y = 0; y < h; ++y) {
memcpy(&out[info.w * 4 * y], image + w * 4 * y, w * 4);
}
}, 0, h, MIN_LINES_PER_THREAD);
}
free(image);
}
// This will only check the hashed bits.
CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)out, rowPitch / sizeof(u32), w, h);
CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)&out[0], info.w, w, h);
if (res == CHECKALPHA_ANY || level == 0) {
alphaStatus_ = ReplacedTextureAlpha(res);
}
@ -784,7 +901,12 @@ bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
if (!png_image_begin_read_from_stdio(&png, fp)) {
ERROR_LOG(G3D, "Could not load texture replacement info: %s - %s", info.file.c_str(), png.message);
fclose(fp);
return false;
return;
}
if (png.width > (uint32_t)info.w || png.height > (uint32_t)info.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", info.file.c_str());
fclose(fp);
return;
}
bool checkedAlpha = false;
@ -797,16 +919,18 @@ bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
}
png.format = PNG_FORMAT_RGBA;
if (!png_image_finish_read(&png, nullptr, out, rowPitch, nullptr)) {
out.resize(info.w * info.h * 4);
if (!png_image_finish_read(&png, nullptr, &out[0], info.w * 4, nullptr)) {
ERROR_LOG(G3D, "Could not load texture replacement: %s - %s", info.file.c_str(), png.message);
fclose(fp);
return false;
out.resize(0);
return;
}
png_image_free(&png);
if (!checkedAlpha) {
// This will only check the hashed bits.
CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)out, rowPitch / sizeof(u32), png.width, png.height);
CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)&out[0], info.w, png.width, png.height);
if (res == CHECKALPHA_ANY || level == 0) {
alphaStatus_ = ReplacedTextureAlpha(res);
}
@ -814,6 +938,47 @@ bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
}
fclose(fp);
}
void ReplacedTexture::PurgeIfOlder(double t) {
if (lastUsed_ < t && !threadWaitable_) {
levelData_.clear();
}
}
ReplacedTexture::~ReplacedTexture() {
if (threadWaitable_) {
cancelPrepare_ = true;
threadWaitable_->WaitAndRelease();
threadWaitable_ = nullptr;
}
}
bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
_assert_msg_((size_t)level < levels_.size(), "Invalid miplevel");
_assert_msg_(out != nullptr && rowPitch > 0, "Invalid out/pitch");
if (levelData_.empty())
return false;
const ReplacedTextureLevel &info = levels_[level];
const std::vector<uint8_t> &data = levelData_[level];
if (data.empty())
return false;
_assert_msg_(data.size() == info.w * info.h * 4, "Data has wrong size");
if (rowPitch == info.w * 4) {
ParallelMemcpy(&g_threadManager, out, &data[0], info.w * 4 * info.h);
} else {
const int MIN_LINES_PER_THREAD = 4;
ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
for (int y = l; y < h; ++y) {
memcpy((uint8_t *)out + rowPitch * y, &data[0] + info.w * 4 * y, info.w * 4);
}
}, 0, info.h, MIN_LINES_PER_THREAD);
}
return true;
}

View file

@ -32,6 +32,8 @@
class IniFile;
class TextureCacheCommon;
class TextureReplacer;
class ReplacedTextureTask;
class LimitedWaitable;
enum class ReplacedTextureFormat {
F_5650,
@ -125,6 +127,8 @@ namespace std {
}
struct ReplacedTexture {
~ReplacedTexture();
inline bool Valid() {
return !levels_.empty();
}
@ -153,13 +157,24 @@ struct ReplacedTexture {
return (u8)alphaStatus_;
}
bool IsReady(double budget);
bool Load(int level, void *out, int rowPitch);
protected:
void Prepare();
void PrepareData(int level);
void PurgeIfOlder(double t);
std::vector<ReplacedTextureLevel> levels_;
std::vector<std::vector<uint8_t>> levelData_;
ReplacedTextureAlpha alphaStatus_;
double lastUsed_ = 0.0;
LimitedWaitable *threadWaitable_ = nullptr;
bool cancelPrepare_ = false;
friend TextureReplacer;
friend ReplacedTextureTask;
};
struct ReplacedTextureDecodeInfo {
@ -188,9 +203,14 @@ public:
ReplacedTexture &FindReplacement(u64 cachekey, u32 hash, int w, int h);
bool FindFiltering(u64 cachekey, u32 hash, TextureFiltering *forceFiltering);
ReplacedTexture &FindNone() {
return none_;
}
void NotifyTextureDecoded(const ReplacedTextureDecodeInfo &replacedInfo, const void *data, int pitch, int level, int w, int h);
void Decimate(bool forcePressure);
static bool GenerateIni(const std::string &gameID, Path &generatedFilename);
protected:

View file

@ -22,6 +22,7 @@
#include "Common/Profiler/Profiler.h"
#include "Common/MemoryUtil.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/Reporting.h"
@ -472,6 +473,15 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
reason = "scaling";
}
}
if (match && (entry->status & TexCacheEntry::STATUS_TO_REPLACE) && replacementTimeThisFrame_ < replacementFrameBudget_) {
int w0 = gstate.getTextureWidth(0);
int h0 = gstate.getTextureHeight(0);
ReplacedTexture &replaced = FindReplacement(entry, w0, h0);
if (replaced.Valid()) {
match = false;
reason = "replacing";
}
}
if (match) {
// got one!
@ -720,6 +730,7 @@ void TextureCacheCommon::Decimate(bool forcePressure) {
}
DecimateVideos();
replacer_.Decimate(forcePressure);
}
void TextureCacheCommon::DecimateVideos() {
@ -1263,6 +1274,29 @@ u32 TextureCacheCommon::EstimateTexMemoryUsage(const TexCacheEntry *entry) {
return pixelSize << (dimW + dimH);
}
ReplacedTexture &TextureCacheCommon::FindReplacement(TexCacheEntry *entry, int &w, int &h) {
// Allow some delay to reduce pop-in.
constexpr double MAX_BUDGET_PER_TEX = 0.25 / 60.0;
double replaceStart = time_now_d();
u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0;
ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h);
if (replaced.IsReady(std::min(MAX_BUDGET_PER_TEX, replacementFrameBudget_ - replacementTimeThisFrame_))) {
if (replaced.GetSize(0, w, h)) {
replacementTimeThisFrame_ += time_now_d() - replaceStart;
// Consider it already "scaled" and remove any delayed replace flag.
entry->status |= TexCacheEntry::STATUS_IS_SCALED;
entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE;
return replaced;
}
} else if (replaced.Valid()) {
entry->status |= TexCacheEntry::STATUS_TO_REPLACE;
}
replacementTimeThisFrame_ += time_now_d() - replaceStart;
return replacer_.FindNone();
}
static void ReverseColors(void *dstBuf, const void *srcBuf, GETextureFormat fmt, int numPixels, bool useBGRA) {
switch (fmt) {
case GE_TFMT_4444:

View file

@ -123,16 +123,17 @@ struct TexCacheEntry {
STATUS_CLUT_RECHECK = 0x20, // Another texture with same addr had a hashfail.
STATUS_TO_SCALE = 0x80, // Pending texture scaling in a later frame.
STATUS_IS_SCALED = 0x100, // Has been scaled (can't be replaceImages'd.)
STATUS_TO_REPLACE = 0x0200, // Pending texture replacement.
// When hashing large textures, we optimize 512x512 down to 512x272 by default, since this
// is commonly the only part accessed. If access is made above 272, we hash the entire
// texture, and set this flag to allow scaling the texture just once for the new hash.
STATUS_FREE_CHANGE = 0x200, // Allow one change before marking "frequent".
STATUS_FREE_CHANGE = 0x0400, // Allow one change before marking "frequent".
STATUS_BAD_MIPS = 0x400, // Has bad or unusable mipmap levels.
STATUS_BAD_MIPS = 0x0800, // Has bad or unusable mipmap levels.
STATUS_FRAMEBUFFER_OVERLAP = 0x800,
STATUS_FRAMEBUFFER_OVERLAP = 0x1000,
STATUS_FORCE_REBUILD = 0x1000,
STATUS_FORCE_REBUILD = 0x2000,
};
// Status, but int so we can zero initialize.
@ -277,6 +278,7 @@ protected:
void DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, bool reverseColors, bool useBGRA, bool expandTo32Bit);
void UnswizzleFromMem(u32 *dest, u32 destPitch, const u8 *texptr, u32 bufw, u32 height, u32 bytesPerPixel);
void ReadIndexedTex(u8 *out, int outPitch, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool expandTo32Bit);
ReplacedTexture &FindReplacement(TexCacheEntry *entry, int &w, int &h);
template <typename T>
inline const T *GetCurrentClut() {
@ -334,6 +336,9 @@ protected:
int decimationCounter_;
int texelsScaledThisFrame_ = 0;
int timesInvalidatedAllThisFrame_ = 0;
double replacementTimeThisFrame_ = 0;
// TODO: Maybe vary by FPS...
double replacementFrameBudget_ = 0.5 / 60.0;
TexCache cache_;
u32 cacheSizeEstimate_ = 0;

View file

@ -21,6 +21,7 @@
#include <d3d11.h>
#include "Common/TimeUtil.h"
#include "Core/MemMap.h"
#include "Core/Reporting.h"
#include "GPU/ge_constants.h"
@ -169,6 +170,7 @@ void TextureCacheD3D11::InvalidateLastTexture() {
void TextureCacheD3D11::StartFrame() {
InvalidateLastTexture();
timesInvalidatedAllThisFrame_ = 0;
replacementTimeThisFrame_ = 0.0;
if (texelsScaledThisFrame_) {
// INFO_LOG(G3D, "Scaled %i texels", texelsScaledThisFrame_);
@ -483,14 +485,12 @@ void TextureCacheD3D11::BuildTexture(TexCacheEntry *const entry) {
scaleFactor = scaleFactor > 4 ? 4 : (scaleFactor > 2 ? 2 : 1);
}
u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0;
int w = gstate.getTextureWidth(0);
int h = gstate.getTextureHeight(0);
ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h);
if (replaced.GetSize(0, w, h)) {
ReplacedTexture &replaced = FindReplacement(entry, w, h);
if (replaced.Valid()) {
// We're replacing, so we won't scale.
scaleFactor = 1;
entry->status |= TexCacheEntry::STATUS_IS_SCALED;
maxLevel = replaced.MaxLevel();
badMipSizes = false;
}
@ -678,7 +678,9 @@ void TextureCacheD3D11::LoadTextureLevel(TexCacheEntry &entry, ReplacedTexture &
if (replaced.GetSize(level, w, h)) {
mapData = (u32 *)AllocateAlignedMemory(w * h * sizeof(u32), 16);
mapRowPitch = w * 4;
double replaceStart = time_now_d();
replaced.Load(level, mapData, mapRowPitch);
replacementTimeThisFrame_ += time_now_d() - replaceStart;
dstFmt = ToDXGIFormat(replaced.Format(level));
} else {
GETextureFormat tfmt = (GETextureFormat)entry.format;

View file

@ -18,6 +18,7 @@
#include <algorithm>
#include <cstring>
#include "Common/TimeUtil.h"
#include "Core/MemMap.h"
#include "Core/Reporting.h"
#include "GPU/ge_constants.h"
@ -134,6 +135,7 @@ void TextureCacheDX9::ApplySamplingParams(const SamplerCacheKey &key) {
void TextureCacheDX9::StartFrame() {
InvalidateLastTexture();
timesInvalidatedAllThisFrame_ = 0;
replacementTimeThisFrame_ = 0.0;
if (texelsScaledThisFrame_) {
VERBOSE_LOG(G3D, "Scaled %i texels", texelsScaledThisFrame_);
@ -438,14 +440,12 @@ void TextureCacheDX9::BuildTexture(TexCacheEntry *const entry) {
scaleFactor = scaleFactor > 4 ? 4 : (scaleFactor > 2 ? 2 : 1);
}
u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0;
int w = gstate.getTextureWidth(0);
int h = gstate.getTextureHeight(0);
ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h);
if (replaced.GetSize(0, w, h)) {
ReplacedTexture &replaced = FindReplacement(entry, w, h);
if (replaced.Valid()) {
// We're replacing, so we won't scale.
scaleFactor = 1;
entry->status |= TexCacheEntry::STATUS_IS_SCALED;
maxLevel = replaced.MaxLevel();
badMipSizes = false;
}
@ -615,7 +615,9 @@ void TextureCacheDX9::LoadTextureLevel(TexCacheEntry &entry, ReplacedTexture &re
gpuStats.numTexturesDecoded++;
if (replaced.GetSize(level, w, h)) {
double replaceStart = time_now_d();
replaced.Load(level, rect.pBits, rect.Pitch);
replacementTimeThisFrame_ += time_now_d() - replaceStart;
dstFmt = ToD3D9Format(replaced.Format(level));
} else {
GETextureFormat tfmt = (GETextureFormat)entry.format;

View file

@ -24,6 +24,7 @@
#include "Common/Math/math_util.h"
#include "Common/Profiler/Profiler.h"
#include "Common/GPU/OpenGL/GLRenderManager.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Host.h"
@ -152,6 +153,7 @@ static void ConvertColors(void *dstBuf, const void *srcBuf, Draw::DataFormat dst
void TextureCacheGLES::StartFrame() {
InvalidateLastTexture();
timesInvalidatedAllThisFrame_ = 0;
replacementTimeThisFrame_ = 0.0;
GLRenderManager *renderManager = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
if (!lowMemoryMode_ && renderManager->SawOutOfMemory()) {
@ -502,14 +504,12 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
scaleFactor = scaleFactor > 4 ? 4 : (scaleFactor > 2 ? 2 : 1);
}
u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0;
int w = gstate.getTextureWidth(0);
int h = gstate.getTextureHeight(0);
ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h);
if (replaced.GetSize(0, w, h)) {
ReplacedTexture &replaced = FindReplacement(entry, w, h);
if (replaced.Valid()) {
// We're replacing, so we won't scale.
scaleFactor = 1;
entry->status |= TexCacheEntry::STATUS_IS_SCALED;
maxLevel = replaced.MaxLevel();
badMipSizes = false;
}
@ -658,7 +658,9 @@ void TextureCacheGLES::LoadTextureLevel(TexCacheEntry &entry, ReplacedTexture &r
int bpp = replaced.Format(level) == ReplacedTextureFormat::F_8888 ? 4 : 2;
decPitch = w * bpp;
uint8_t *rearrange = (uint8_t *)AllocateAlignedMemory(decPitch * h, 16);
double replaceStart = time_now_d();
replaced.Load(level, rearrange, decPitch);
replacementTimeThisFrame_ += time_now_d() - replaceStart;
pixelData = rearrange;
dstFmt = ToDataFormat(replaced.Format(level));

View file

@ -28,6 +28,7 @@
#include "Common/Data/Convert/ColorConv.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Host.h"
#include "Core/MemMap.h"
@ -442,6 +443,7 @@ void TextureCacheVulkan::StartFrame() {
timesInvalidatedAllThisFrame_ = 0;
texelsScaledThisFrame_ = 0;
replacementTimeThisFrame_ = 0.0;
if (clearCacheNextFrame_) {
Clear(true);
@ -773,14 +775,12 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
scaleFactor = scaleFactor > 4 ? 4 : (scaleFactor > 2 ? 2 : 1);
}
u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0;
int w = gstate.getTextureWidth(0);
int h = gstate.getTextureHeight(0);
ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h);
if (replaced.GetSize(0, w, h)) {
ReplacedTexture &replaced = FindReplacement(entry, w, h);
if (replaced.Valid()) {
// We're replacing, so we won't scale.
scaleFactor = 1;
entry->status |= TexCacheEntry::STATUS_IS_SCALED;
maxLevel = replaced.MaxLevel();
badMipSizes = false;
}
@ -899,7 +899,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
ReplacedTextureDecodeInfo replacedInfo;
if (replacer_.Enabled() && !replaced.Valid()) {
replacedInfo.cachekey = cachekey;
replacedInfo.cachekey = entry->CacheKey();
replacedInfo.hash = entry->fullhash;
replacedInfo.addr = entry->addr;
replacedInfo.isVideo = IsVideo(entry->addr);
@ -934,7 +934,9 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
if (replaced.Valid()) {
// Directly load the replaced image.
data = drawEngine_->GetPushBufferForTextureData()->PushAligned(size, &bufferOffset, &texBuf, pushAlignment);
double replaceStart = time_now_d();
replaced.Load(i, data, stride); // if it fails, it'll just be garbage data... OK for now.
replacementTimeThisFrame_ += time_now_d() - replaceStart;
entry->vkTex->UploadMip(cmdInit, i, mipWidth, mipHeight, texBuf, bufferOffset, stride / bpp);
} else {
auto dispatchCompute = [&](VkDescriptorSet descSet) {