From 2356280a9ccd87e2624e06ced8d9ad31287681dc Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 08:54:45 -0700 Subject: [PATCH 01/12] Replacement: Add structure for delayed loading. --- Core/TextureReplacer.cpp | 4 ++++ Core/TextureReplacer.h | 2 ++ GPU/Common/TextureCacheCommon.cpp | 4 ++++ GPU/Common/TextureCacheCommon.h | 12 ++++++++---- GPU/D3D11/TextureCacheD3D11.cpp | 24 ++++++++++++++++++------ GPU/Directx9/TextureCacheDX9.cpp | 24 ++++++++++++++++++------ GPU/GLES/TextureCacheGLES.cpp | 24 ++++++++++++++++++------ GPU/Vulkan/TextureCacheVulkan.cpp | 24 ++++++++++++++++++------ 8 files changed, 90 insertions(+), 28 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index daa687c0b790..38c44a06225e 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -733,6 +733,10 @@ float TextureReplacer::LookupReduceHashRange(int& w, int& h) { } } +bool ReplacedTexture::IsReady(double budget) { + return true; +} + 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"); diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index 92fac7b746ae..110f9df57be5 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -153,6 +153,8 @@ struct ReplacedTexture { return (u8)alphaStatus_; } + bool IsReady(double budget); + bool Load(int level, void *out, int rowPitch); protected: diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 4b872306b06e..86610180076f 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -472,6 +472,10 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { reason = "scaling"; } } + if (match && (entry->status & TexCacheEntry::STATUS_TO_REPLACE) && replacementTimeThisFrame_ <= replacementFrameBudget_) { + match = false; + reason = "replacing"; + } if (match) { // got one! diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index e6a3c0b58f80..ceb2aa96ffb2 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -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. @@ -334,6 +335,9 @@ class TextureCacheCommon { int decimationCounter_; int texelsScaledThisFrame_ = 0; int timesInvalidatedAllThisFrame_ = 0; + double replacementTimeThisFrame_ = 0; + // TODO: Maybe vary by FPS... + double replacementFrameBudget_ = 0.75 / 60.0; TexCache cache_; u32 cacheSizeEstimate_ = 0; diff --git a/GPU/D3D11/TextureCacheD3D11.cpp b/GPU/D3D11/TextureCacheD3D11.cpp index be61a5ae48dc..1a8b34a54ca1 100644 --- a/GPU/D3D11/TextureCacheD3D11.cpp +++ b/GPU/D3D11/TextureCacheD3D11.cpp @@ -21,6 +21,7 @@ #include +#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_); @@ -486,13 +488,21 @@ void TextureCacheD3D11::BuildTexture(TexCacheEntry *const entry) { u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0; int w = gstate.getTextureWidth(0); int h = gstate.getTextureHeight(0); + double replaceStart = time_now_d(); ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.GetSize(0, w, h)) { - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; + if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { + if (replaced.GetSize(0, w, h)) { + replacementTimeThisFrame_ += time_now_d() - replaceStart; + + // We're replacing, so we won't scale. + scaleFactor = 1; + entry->status |= TexCacheEntry::STATUS_IS_SCALED; + entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; + } + } else if (replaced.MaxLevel() >= 0) { + entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } // Don't scale the PPGe texture. @@ -678,7 +688,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; diff --git a/GPU/Directx9/TextureCacheDX9.cpp b/GPU/Directx9/TextureCacheDX9.cpp index 0b1d536ed7f9..20f5c696ccfb 100644 --- a/GPU/Directx9/TextureCacheDX9.cpp +++ b/GPU/Directx9/TextureCacheDX9.cpp @@ -18,6 +18,7 @@ #include #include +#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_); @@ -441,13 +443,21 @@ void TextureCacheDX9::BuildTexture(TexCacheEntry *const entry) { u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0; int w = gstate.getTextureWidth(0); int h = gstate.getTextureHeight(0); + double replaceStart = time_now_d(); ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.GetSize(0, w, h)) { - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; + if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { + if (replaced.GetSize(0, w, h)) { + replacementTimeThisFrame_ += time_now_d() - replaceStart; + + // We're replacing, so we won't scale. + scaleFactor = 1; + entry->status |= TexCacheEntry::STATUS_IS_SCALED; + entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; + } + } else if (replaced.MaxLevel() >= 0) { + entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } // Don't scale the PPGe texture. @@ -615,7 +625,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; diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index 1dd35b39c0fc..9a972792c4e2 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -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()) { @@ -505,13 +507,21 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) { u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0; int w = gstate.getTextureWidth(0); int h = gstate.getTextureHeight(0); + double replaceStart = time_now_d(); ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.GetSize(0, w, h)) { - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; + if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { + if (replaced.GetSize(0, w, h)) { + replacementTimeThisFrame_ += time_now_d() - replaceStart; + + // We're replacing, so we won't scale. + scaleFactor = 1; + entry->status |= TexCacheEntry::STATUS_IS_SCALED; + entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; + } + } else if (replaced.MaxLevel() >= 0) { + entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } // Don't scale the PPGe texture. @@ -658,7 +668,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)); diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index ee26533f43c6..7df78771099f 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -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); @@ -776,13 +778,21 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) { u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0; int w = gstate.getTextureWidth(0); int h = gstate.getTextureHeight(0); + double replaceStart = time_now_d(); ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.GetSize(0, w, h)) { - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; + if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { + if (replaced.GetSize(0, w, h)) { + replacementTimeThisFrame_ += time_now_d() - replaceStart; + + // We're replacing, so we won't scale. + scaleFactor = 1; + entry->status |= TexCacheEntry::STATUS_IS_SCALED; + entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; + } + } else if (replaced.MaxLevel() >= 0) { + entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } bool hardwareScaling = g_Config.bTexHardwareScaling && (uploadCS_ != VK_NULL_HANDLE || copyCS_ != VK_NULL_HANDLE); @@ -934,7 +944,9 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) { if (replaced.Valid()) { // Directly load the replaced image. data = drawEngine_->GetPushBufferForTextureData()->PushAligned(size, &bufferOffset, &texBuf, pushAlignment); + 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) { From 36fc2c2628f33da40c8858bf06b94fffce907fdf Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 09:16:54 -0700 Subject: [PATCH 02/12] Replacement: Purge old cached decoded textures. Not actually decoding into the cache, just setup. --- Core/TextureReplacer.cpp | 38 ++++++++++++++++++++++++++++++- Core/TextureReplacer.h | 7 ++++++ GPU/Common/TextureCacheCommon.cpp | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 38c44a06225e..d92fb9e0377f 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -31,6 +31,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 +622,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 static typename std::unordered_map::const_iterator LookupWildcard(const std::unordered_map &map, Key &key, u64 cachekey, u32 hash, bool ignoreAddress) { auto alias = map.find(key); @@ -734,7 +744,33 @@ float TextureReplacer::LookupReduceHashRange(int& w, int& h) { } bool ReplacedTexture::IsReady(double budget) { - return true; + lastUsed_ = time_now_d(); + + if (levelData_.size() == levels_.size()) + return true; + if (budget <= 0.0) + return false; + + double deadline = lastUsed_ + budget; + for (int i = (int)levelData_.size(); i <= MaxLevel(); ++i) { + levelData_.resize(i + 1); + PrepareData(i); + if (time_now_d() >= deadline) { + break; + } + } + // If we finished all the levels, we're done. + return levelData_.size() == levels_.size(); +} + +void ReplacedTexture::PrepareData(int level) { + // TODO +} + +void ReplacedTexture::PurgeIfOlder(double t) { + if (lastUsed_ < t) { + levelData_.clear(); + } } bool ReplacedTexture::Load(int level, void *out, int rowPitch) { diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index 110f9df57be5..46ee4c58beef 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -158,8 +158,13 @@ struct ReplacedTexture { bool Load(int level, void *out, int rowPitch); protected: + void PrepareData(int level); + void PurgeIfOlder(double t); + std::vector levels_; + std::vector> levelData_; ReplacedTextureAlpha alphaStatus_; + double lastUsed_ = 0.0; friend TextureReplacer; }; @@ -193,6 +198,8 @@ class TextureReplacer { 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: diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 86610180076f..04c6bfae2ce6 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -724,6 +724,7 @@ void TextureCacheCommon::Decimate(bool forcePressure) { } DecimateVideos(); + replacer_.Decimate(forcePressure); } void TextureCacheCommon::DecimateVideos() { From 045d902525943b2fc6d31b44ad7c5e1895d14e27 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 09:35:11 -0700 Subject: [PATCH 03/12] Replacement: Delay load texture data. --- Core/TextureReplacer.cpp | 81 +++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index d92fb9e0377f..0aff1c77939a 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -764,24 +764,15 @@ bool ReplacedTexture::IsReady(double budget) { } void ReplacedTexture::PrepareData(int level) { - // TODO -} - -void ReplacedTexture::PurgeIfOlder(double t) { - if (lastUsed_ < t) { - levelData_.clear(); - } -} - -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"); const ReplacedTextureLevel &info = levels_[level]; + std::vector &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); @@ -791,29 +782,30 @@ 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); - } - }, 0, h, MIN_LINES_PER_THREAD); + 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(w * h * 4); + ParallelMemcpy(&g_threadManager, &out[0], image, info.w * 4 * info.h); 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], w, w, h); if (res == CHECKALPHA_ANY || level == 0) { alphaStatus_ = ReplacedTextureAlpha(res); } @@ -824,7 +816,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 != info.w || png.height != info.h) { + ERROR_LOG(G3D, "Texture replacement changed since header read: %s", info.file.c_str()); + fclose(fp); + return; } bool checkedAlpha = false; @@ -837,16 +834,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(png.width * png.height * 4); + if (!png_image_finish_read(&png, nullptr, &out[0], png.width * 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], png.width, png.width, png.height); if (res == CHECKALPHA_ANY || level == 0) { alphaStatus_ = ReplacedTextureAlpha(res); } @@ -854,6 +853,36 @@ bool ReplacedTexture::Load(int level, void *out, int rowPitch) { } fclose(fp); +} + +void ReplacedTexture::PurgeIfOlder(double t) { + if (lastUsed_ < t) { + levelData_.clear(); + } +} + +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"); + + const ReplacedTextureLevel &info = levels_[level]; + const std::vector &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; } From 83b7b33cfd565f50a344ecf4d34a45dc11288a1f Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 12:36:20 -0700 Subject: [PATCH 04/12] Replacement: Centralize lookup logic. And make sure we don't change our minds about using a replacement during a draw. --- Core/TextureReplacer.cpp | 2 +- Core/TextureReplacer.h | 3 +++ GPU/Common/TextureCacheCommon.cpp | 20 ++++++++++++++++++++ GPU/Common/TextureCacheCommon.h | 1 + GPU/D3D11/TextureCacheD3D11.cpp | 22 ++++++---------------- GPU/Directx9/TextureCacheDX9.cpp | 22 ++++++---------------- GPU/GLES/TextureCacheGLES.cpp | 22 ++++++---------------- GPU/Vulkan/TextureCacheVulkan.cpp | 26 ++++++++------------------ 8 files changed, 51 insertions(+), 67 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 0aff1c77939a..56de4ee66cd3 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -747,7 +747,7 @@ bool ReplacedTexture::IsReady(double budget) { lastUsed_ = time_now_d(); if (levelData_.size() == levels_.size()) - return true; + return Valid(); if (budget <= 0.0) return false; diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index 46ee4c58beef..a931b9e6453b 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -195,6 +195,9 @@ class TextureReplacer { ReplacedTexture &FindReplacement(u64 cachekey, u32 hash, int w, int h); bool FindFiltering(u64 cachekey, u32 hash, TextureFiltering *forceFiltering); + ReplacedTexture &None() { + return none_; + } void NotifyTextureDecoded(const ReplacedTextureDecodeInfo &replacedInfo, const void *data, int pitch, int level, int w, int h); diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 04c6bfae2ce6..0a931c32b1b3 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -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" @@ -1268,6 +1269,25 @@ u32 TextureCacheCommon::EstimateTexMemoryUsage(const TexCacheEntry *entry) { return pixelSize << (dimW + dimH); } +ReplacedTexture &TextureCacheCommon::FindReplacement(TexCacheEntry *entry, int &w, int &h) { + double replaceStart = time_now_d(); + u64 cachekey = replacer_.Enabled() ? entry->CacheKey() : 0; + ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); + if (replaced.IsReady(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; + } + return replacer_.None(); +} + static void ReverseColors(void *dstBuf, const void *srcBuf, GETextureFormat fmt, int numPixels, bool useBGRA) { switch (fmt) { case GE_TFMT_4444: diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index ceb2aa96ffb2..32ab227b062c 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -278,6 +278,7 @@ class TextureCacheCommon { 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 inline const T *GetCurrentClut() { diff --git a/GPU/D3D11/TextureCacheD3D11.cpp b/GPU/D3D11/TextureCacheD3D11.cpp index 1a8b34a54ca1..199f408e4ae5 100644 --- a/GPU/D3D11/TextureCacheD3D11.cpp +++ b/GPU/D3D11/TextureCacheD3D11.cpp @@ -485,24 +485,14 @@ 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); - double replaceStart = time_now_d(); - ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { - if (replaced.GetSize(0, w, h)) { - replacementTimeThisFrame_ += time_now_d() - replaceStart; - - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; - } - } else if (replaced.MaxLevel() >= 0) { - entry->status |= TexCacheEntry::STATUS_TO_REPLACE; + ReplacedTexture &replaced = FindReplacement(entry, w, h); + if (replaced.Valid()) { + // We're replacing, so we won't scale. + scaleFactor = 1; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; } // Don't scale the PPGe texture. diff --git a/GPU/Directx9/TextureCacheDX9.cpp b/GPU/Directx9/TextureCacheDX9.cpp index 20f5c696ccfb..26f15e6998b3 100644 --- a/GPU/Directx9/TextureCacheDX9.cpp +++ b/GPU/Directx9/TextureCacheDX9.cpp @@ -440,24 +440,14 @@ 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); - double replaceStart = time_now_d(); - ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { - if (replaced.GetSize(0, w, h)) { - replacementTimeThisFrame_ += time_now_d() - replaceStart; - - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; - } - } else if (replaced.MaxLevel() >= 0) { - entry->status |= TexCacheEntry::STATUS_TO_REPLACE; + ReplacedTexture &replaced = FindReplacement(entry, w, h); + if (replaced.Valid()) { + // We're replacing, so we won't scale. + scaleFactor = 1; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; } // Don't scale the PPGe texture. diff --git a/GPU/GLES/TextureCacheGLES.cpp b/GPU/GLES/TextureCacheGLES.cpp index 9a972792c4e2..b95f6881face 100644 --- a/GPU/GLES/TextureCacheGLES.cpp +++ b/GPU/GLES/TextureCacheGLES.cpp @@ -504,24 +504,14 @@ 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); - double replaceStart = time_now_d(); - ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { - if (replaced.GetSize(0, w, h)) { - replacementTimeThisFrame_ += time_now_d() - replaceStart; - - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; - } - } else if (replaced.MaxLevel() >= 0) { - entry->status |= TexCacheEntry::STATUS_TO_REPLACE; + ReplacedTexture &replaced = FindReplacement(entry, w, h); + if (replaced.Valid()) { + // We're replacing, so we won't scale. + scaleFactor = 1; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; } // Don't scale the PPGe texture. diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp index 7df78771099f..47b2fe892448 100644 --- a/GPU/Vulkan/TextureCacheVulkan.cpp +++ b/GPU/Vulkan/TextureCacheVulkan.cpp @@ -775,24 +775,14 @@ 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); - double replaceStart = time_now_d(); - ReplacedTexture &replaced = replacer_.FindReplacement(cachekey, entry->fullhash, w, h); - if (replaced.IsReady(replacementFrameBudget_ - replacementTimeThisFrame_)) { - if (replaced.GetSize(0, w, h)) { - replacementTimeThisFrame_ += time_now_d() - replaceStart; - - // We're replacing, so we won't scale. - scaleFactor = 1; - entry->status |= TexCacheEntry::STATUS_IS_SCALED; - entry->status &= ~TexCacheEntry::STATUS_TO_REPLACE; - maxLevel = replaced.MaxLevel(); - badMipSizes = false; - } - } else if (replaced.MaxLevel() >= 0) { - entry->status |= TexCacheEntry::STATUS_TO_REPLACE; + ReplacedTexture &replaced = FindReplacement(entry, w, h); + if (replaced.Valid()) { + // We're replacing, so we won't scale. + scaleFactor = 1; + maxLevel = replaced.MaxLevel(); + badMipSizes = false; } bool hardwareScaling = g_Config.bTexHardwareScaling && (uploadCS_ != VK_NULL_HANDLE || copyCS_ != VK_NULL_HANDLE); @@ -909,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); @@ -944,7 +934,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) { if (replaced.Valid()) { // Directly load the replaced image. data = drawEngine_->GetPushBufferForTextureData()->PushAligned(size, &bufferOffset, &texBuf, pushAlignment); - replaceStart = time_now_d(); + 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); From 0721405628dee69d131fe7dd9e93be1858abfcff Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 12:46:02 -0700 Subject: [PATCH 05/12] Replacement: Avoid clash with X define. --- Core/TextureReplacer.h | 2 +- GPU/Common/TextureCacheCommon.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index a931b9e6453b..e94a3143cfe3 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -195,7 +195,7 @@ class TextureReplacer { ReplacedTexture &FindReplacement(u64 cachekey, u32 hash, int w, int h); bool FindFiltering(u64 cachekey, u32 hash, TextureFiltering *forceFiltering); - ReplacedTexture &None() { + ReplacedTexture &FindNone() { return none_; } diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 0a931c32b1b3..f40b0152112d 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -1285,7 +1285,7 @@ ReplacedTexture &TextureCacheCommon::FindReplacement(TexCacheEntry *entry, int & } else if (replaced.Valid()) { entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } - return replacer_.None(); + return replacer_.FindNone(); } static void ReverseColors(void *dstBuf, const void *srcBuf, GETextureFormat fmt, int numPixels, bool useBGRA) { From ee882d18610454cda7d984b254d91e26b35c3b2e Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sun, 17 Oct 2021 13:26:02 -0700 Subject: [PATCH 06/12] Replacement: Avoid rebuild until ready. --- GPU/Common/TextureCacheCommon.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index f40b0152112d..83c317cada7b 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -473,9 +473,14 @@ TexCacheEntry *TextureCacheCommon::SetTexture() { reason = "scaling"; } } - if (match && (entry->status & TexCacheEntry::STATUS_TO_REPLACE) && replacementTimeThisFrame_ <= replacementFrameBudget_) { - match = false; - reason = "replacing"; + 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) { @@ -1285,6 +1290,7 @@ ReplacedTexture &TextureCacheCommon::FindReplacement(TexCacheEntry *entry, int & } else if (replaced.Valid()) { entry->status |= TexCacheEntry::STATUS_TO_REPLACE; } + replacementTimeThisFrame_ += time_now_d() - replaceStart; return replacer_.FindNone(); } From 09f0578a647087e203026508f7abde41b94adfb3 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Thu, 21 Oct 2021 13:21:23 -0700 Subject: [PATCH 07/12] Replacement: Use a thread to load tex replacements. --- Core/TextureReplacer.cpp | 106 +++++++++++++++++++++++++----- Core/TextureReplacer.h | 4 ++ GPU/Common/TextureCacheCommon.cpp | 5 +- GPU/Common/TextureCacheCommon.h | 2 +- 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 56de4ee66cd3..f79ae1373030 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -743,24 +743,86 @@ float TextureReplacer::LookupReduceHashRange(int& w, int& h) { } } +class LimitedWaitable : public Waitable { +public: + LimitedWaitable() {} + + void Wait() override { + if (!triggered_) { + std::unique_lock lock(mutex_); + cond_.wait(lock, [&] { return !triggered_; }); + } + } + + bool WaitFor(uint32_t us) { + if (!triggered_) { + std::unique_lock lock(mutex_); + cond_.wait_for(lock, std::chrono::microseconds(us), [&] { return !triggered_; }); + } + return triggered_; + } + + void Notify() { + std::unique_lock lock(mutex_); + triggered_ = true; + cond_.notify_all(); + } + +private: + std::condition_variable cond_; + std::mutex mutex_; + bool triggered_ = false; +}; + +class ReplacedTextureTask : public Task { +public: + ReplacedTextureTask(ReplacedTexture &tex, LimitedWaitable *w) : tex_(tex), waitable_(w) { + } + + void Run() override { + for (int i = (int)tex_.levelData_.size(); i <= tex_.MaxLevel(); ++i) { + tex_.levelData_.resize(i + 1); + tex_.PrepareData(i); + } + 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_.size() == levels_.size()) return Valid(); if (budget <= 0.0) return false; - double deadline = lastUsed_ + budget; - for (int i = (int)levelData_.size(); i <= MaxLevel(); ++i) { - levelData_.resize(i + 1); - PrepareData(i); - if (time_now_d() >= deadline) { - break; - } + threadWaitable_ = new LimitedWaitable(); + g_threadManager.EnqueueTask(new ReplacedTextureTask(*this, threadWaitable_), TaskType::IO_BLOCKING); + + uint32_t budget_us = (uint32_t)(budget * 1000000.0); + if (threadWaitable_->WaitFor(budget_us)) { + threadWaitable_->WaitAndRelease(); + threadWaitable_ = nullptr; + + // If we finished all the levels, we're done. + return levelData_.size() == levels_.size(); } - // If we finished all the levels, we're done. - return levelData_.size() == levels_.size(); + + // Still pending on thread. + return false; } void ReplacedTexture::PrepareData(int level) { @@ -794,18 +856,26 @@ void ReplacedTexture::PrepareData(int level) { int w, h, f; uint8_t *image; if (LoadZIMPtr(&zim[0], zimSize, &w, &h, &f, &image)) { - if (w != info.w || h != info.h) { + 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(w * h * 4); - ParallelMemcpy(&g_threadManager, &out[0], image, info.w * 4 * info.h); + out.resize(info.w * info.h * 4); + if (w == info.w) { + ParallelMemcpy(&g_threadManager, &out[0], image, info.w * 4 * info.h); + } else { + ParallelRangeLoop(&g_threadManager, [&](int l, int u) { + for (int y = l; y < u; ++y) { + memcpy(&out[info.w * 4 * y], image + w * 4 * y, w * 4); + } + }, 0, h, 4); + } free(image); } - CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)&out[0], w, w, h); + CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)&out[0], info.w, w, h); if (res == CHECKALPHA_ANY || level == 0) { alphaStatus_ = ReplacedTextureAlpha(res); } @@ -818,7 +888,7 @@ void ReplacedTexture::PrepareData(int level) { fclose(fp); return; } - if (png.width != info.w || png.height != info.h) { + 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; @@ -834,8 +904,8 @@ void ReplacedTexture::PrepareData(int level) { } png.format = PNG_FORMAT_RGBA; - out.resize(png.width * png.height * 4); - if (!png_image_finish_read(&png, nullptr, &out[0], png.width * 4, 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); out.resize(0); @@ -845,7 +915,7 @@ void ReplacedTexture::PrepareData(int level) { if (!checkedAlpha) { // This will only check the hashed bits. - CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)&out[0], png.width, 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); } @@ -856,7 +926,7 @@ void ReplacedTexture::PrepareData(int level) { } void ReplacedTexture::PurgeIfOlder(double t) { - if (lastUsed_ < t) { + if (lastUsed_ < t && !threadWaitable_) { levelData_.clear(); } } diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index e94a3143cfe3..11a0226389c6 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -32,6 +32,8 @@ class IniFile; class TextureCacheCommon; class TextureReplacer; +class ReplacedTextureTask; +class LimitedWaitable; enum class ReplacedTextureFormat { F_5650, @@ -165,8 +167,10 @@ struct ReplacedTexture { std::vector> levelData_; ReplacedTextureAlpha alphaStatus_; double lastUsed_ = 0.0; + LimitedWaitable * threadWaitable_ = nullptr; friend TextureReplacer; + friend ReplacedTextureTask; }; struct ReplacedTextureDecodeInfo { diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 83c317cada7b..b694730c4755 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -1275,10 +1275,13 @@ u32 TextureCacheCommon::EstimateTexMemoryUsage(const TexCacheEntry *entry) { } 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(replacementFrameBudget_ - replacementTimeThisFrame_)) { + if (replaced.IsReady(std::min(MAX_BUDGET_PER_TEX, replacementFrameBudget_ - replacementTimeThisFrame_))) { if (replaced.GetSize(0, w, h)) { replacementTimeThisFrame_ += time_now_d() - replaceStart; diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index 32ab227b062c..26d6d6d0e66b 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -338,7 +338,7 @@ class TextureCacheCommon { int timesInvalidatedAllThisFrame_ = 0; double replacementTimeThisFrame_ = 0; // TODO: Maybe vary by FPS... - double replacementFrameBudget_ = 0.75 / 60.0; + double replacementFrameBudget_ = 0.5 / 60.0; TexCache cache_; u32 cacheSizeEstimate_ = 0; From c0054dc6cf0e1fe7089fd2b3e28b3647a05ccd29 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 23 Oct 2021 08:06:55 -0700 Subject: [PATCH 08/12] Replacement: Ensurely orderly stop on reset. If the texture is being loaded and we stop or reset, make sure it stops to avoid any crash or hang. --- Core/TextureReplacer.cpp | 10 ++++++++++ Core/TextureReplacer.h | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index f79ae1373030..3c7e0c7ec627 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -781,6 +781,8 @@ class ReplacedTextureTask : public Task { void Run() override { for (int i = (int)tex_.levelData_.size(); i <= tex_.MaxLevel(); ++i) { + if (tex_.cancelPrepare_) + break; tex_.levelData_.resize(i + 1); tex_.PrepareData(i); } @@ -931,6 +933,14 @@ void ReplacedTexture::PurgeIfOlder(double t) { } } +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"); diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index 11a0226389c6..9b9551396c13 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -127,6 +127,8 @@ namespace std { } struct ReplacedTexture { + ~ReplacedTexture(); + inline bool Valid() { return !levels_.empty(); } @@ -167,7 +169,8 @@ struct ReplacedTexture { std::vector> levelData_; ReplacedTextureAlpha alphaStatus_; double lastUsed_ = 0.0; - LimitedWaitable * threadWaitable_ = nullptr; + LimitedWaitable *threadWaitable_ = nullptr; + bool cancelPrepare_ = false; friend TextureReplacer; friend ReplacedTextureTask; From 4c1b5564d2fdff07811478b6e6d7e1f63b65bb88 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 23 Oct 2021 19:58:23 -0700 Subject: [PATCH 09/12] Replacement: Tweak some thread safety. --- Core/TextureReplacer.cpp | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 3c7e0c7ec627..251f97841ee2 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -17,6 +17,7 @@ #include "ppsspp_config.h" #include +#include #include #include #include @@ -745,7 +746,9 @@ float TextureReplacer::LookupReduceHashRange(int& w, int& h) { class LimitedWaitable : public Waitable { public: - LimitedWaitable() {} + LimitedWaitable() { + triggered_ = false; + } void Wait() override { if (!triggered_) { @@ -771,7 +774,7 @@ class LimitedWaitable : public Waitable { private: std::condition_variable cond_; std::mutex mutex_; - bool triggered_ = false; + std::atomic triggered_; }; class ReplacedTextureTask : public Task { @@ -780,10 +783,10 @@ class ReplacedTextureTask : public Task { } void Run() override { - for (int i = (int)tex_.levelData_.size(); i <= tex_.MaxLevel(); ++i) { + tex_.levelData_.resize(tex_.MaxLevel() + 1); + for (int i = 0; i <= tex_.MaxLevel(); ++i) { if (tex_.cancelPrepare_) break; - tex_.levelData_.resize(i + 1); tex_.PrepareData(i); } waitable_->Notify(); @@ -806,8 +809,8 @@ bool ReplacedTexture::IsReady(double budget) { } // Loaded already, or not yet on a thread? - if (levelData_.size() == levels_.size()) - return Valid(); + if (!levelData_.empty()) + return true; if (budget <= 0.0) return false; @@ -820,7 +823,7 @@ bool ReplacedTexture::IsReady(double budget) { threadWaitable_ = nullptr; // If we finished all the levels, we're done. - return levelData_.size() == levels_.size(); + return !levelData_.empty(); } // Still pending on thread. @@ -866,13 +869,11 @@ void ReplacedTexture::PrepareData(int level) { out.resize(info.w * info.h * 4); if (w == info.w) { - ParallelMemcpy(&g_threadManager, &out[0], image, info.w * 4 * info.h); + memcpy(&out[0], image, info.w * 4 * info.h); } else { - ParallelRangeLoop(&g_threadManager, [&](int l, int u) { - for (int y = l; y < u; ++y) { - memcpy(&out[info.w * 4 * y], image + w * 4 * y, w * 4); - } - }, 0, h, 4); + for (int y = 0; y < h; ++y) { + memcpy(&out[info.w * 4 * y], image + w * 4 * y, w * 4); + } } free(image); } @@ -945,6 +946,9 @@ 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 &data = levelData_[level]; From fa0e19471c2092be3554467bf28029874b808542 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 23 Oct 2021 20:56:19 -0700 Subject: [PATCH 10/12] Replacement: Add ini setting to disable pop-in. --- Core/Config.cpp | 1 + Core/Config.h | 1 + Core/TextureReplacer.cpp | 37 +++++++++++++++++++++++-------------- Core/TextureReplacer.h | 1 + 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index 2afcf76cf584..8143123e8867 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -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), diff --git a/Core/Config.h b/Core/Config.h index b6bf7f062e06..7ded5c9aa012 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -197,6 +197,7 @@ struct Config { 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; diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 251f97841ee2..ab5c7697e478 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -783,12 +783,7 @@ class ReplacedTextureTask : public Task { } void Run() override { - tex_.levelData_.resize(tex_.MaxLevel() + 1); - for (int i = 0; i <= tex_.MaxLevel(); ++i) { - if (tex_.cancelPrepare_) - break; - tex_.PrepareData(i); - } + tex_.Prepare(); waitable_->Notify(); } @@ -814,22 +809,36 @@ bool ReplacedTexture::IsReady(double budget) { if (budget <= 0.0) return false; - threadWaitable_ = new LimitedWaitable(); - g_threadManager.EnqueueTask(new ReplacedTextureTask(*this, threadWaitable_), TaskType::IO_BLOCKING); + if (g_Config.bReplaceTexturesAllowLate) { + threadWaitable_ = new LimitedWaitable(); + g_threadManager.EnqueueTask(new ReplacedTextureTask(*this, threadWaitable_), TaskType::IO_BLOCKING); - uint32_t budget_us = (uint32_t)(budget * 1000000.0); - if (threadWaitable_->WaitFor(budget_us)) { - threadWaitable_->WaitAndRelease(); - threadWaitable_ = nullptr; + uint32_t budget_us = (uint32_t)(budget * 1000000.0); + if (threadWaitable_->WaitFor(budget_us)) { + threadWaitable_->WaitAndRelease(); + threadWaitable_ = nullptr; - // If we finished all the levels, we're done. - return !levelData_.empty(); + // 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"); diff --git a/Core/TextureReplacer.h b/Core/TextureReplacer.h index 9b9551396c13..6deefe8daf09 100644 --- a/Core/TextureReplacer.h +++ b/Core/TextureReplacer.h @@ -162,6 +162,7 @@ struct ReplacedTexture { bool Load(int level, void *out, int rowPitch); protected: + void Prepare(); void PrepareData(int level); void PurgeIfOlder(double t); From abc80f101549d2a71033031b7a112db4f59a2215 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 23 Oct 2021 22:00:51 -0700 Subject: [PATCH 11/12] Replacement: Correct budget on later frames. --- Core/TextureReplacer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index ab5c7697e478..27fcb4e42524 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -759,6 +759,8 @@ class LimitedWaitable : public Waitable { bool WaitFor(uint32_t us) { if (!triggered_) { + if (us == 0) + return false; std::unique_lock lock(mutex_); cond_.wait_for(lock, std::chrono::microseconds(us), [&] { return !triggered_; }); } @@ -795,7 +797,8 @@ class ReplacedTextureTask : public Task { bool ReplacedTexture::IsReady(double budget) { lastUsed_ = time_now_d(); if (threadWaitable_) { - if (!threadWaitable_->WaitFor(budget)) { + uint32_t budget_us = budget > 0 ? (uint32_t)(budget * 1000000.0) : 0; + if (!threadWaitable_->WaitFor(budget_us)) { return false; } else { threadWaitable_->WaitAndRelease(); From 76186d19191f4256b9d7dc36257db4ad8846fd38 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 23 Oct 2021 22:42:57 -0700 Subject: [PATCH 12/12] Replacement: Allow starting a texture at budget. --- Core/TextureReplacer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Core/TextureReplacer.cpp b/Core/TextureReplacer.cpp index 27fcb4e42524..3d40e302d0f0 100644 --- a/Core/TextureReplacer.cpp +++ b/Core/TextureReplacer.cpp @@ -757,7 +757,8 @@ class LimitedWaitable : public Waitable { } } - bool WaitFor(uint32_t us) { + bool WaitFor(double budget) { + uint32_t us = budget > 0 ? (uint32_t)(budget * 1000000.0) : 0; if (!triggered_) { if (us == 0) return false; @@ -797,8 +798,7 @@ class ReplacedTextureTask : public Task { bool ReplacedTexture::IsReady(double budget) { lastUsed_ = time_now_d(); if (threadWaitable_) { - uint32_t budget_us = budget > 0 ? (uint32_t)(budget * 1000000.0) : 0; - if (!threadWaitable_->WaitFor(budget_us)) { + if (!threadWaitable_->WaitFor(budget)) { return false; } else { threadWaitable_->WaitAndRelease(); @@ -809,15 +809,15 @@ bool ReplacedTexture::IsReady(double budget) { // Loaded already, or not yet on a thread? if (!levelData_.empty()) return true; - if (budget <= 0.0) + // 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); - uint32_t budget_us = (uint32_t)(budget * 1000000.0); - if (threadWaitable_->WaitFor(budget_us)) { + if (threadWaitable_->WaitFor(budget)) { threadWaitable_->WaitAndRelease(); threadWaitable_ = nullptr;