From 9781a75a22ba789124d7f34c6bdb4a85c78b2532 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Mon, 29 Jun 2026 08:41:47 +0100 Subject: [PATCH] [GPU] Avoid stale texture state after invalidation Check shared memory validity before marking textures up to date and installing watches. If the backing range was invalidated, leave the texture outdated so it can be reloaded later. --- src/xenia/gpu/shared_memory.cc | 28 ++++++++++++++++++++++++++++ src/xenia/gpu/shared_memory.h | 4 ++++ src/xenia/gpu/texture_cache.cc | 29 ++++++++++++++++++++++++----- src/xenia/gpu/texture_cache.h | 2 +- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/xenia/gpu/shared_memory.cc b/src/xenia/gpu/shared_memory.cc index 3f70a20c2..87bf4ca93 100644 --- a/src/xenia/gpu/shared_memory.cc +++ b/src/xenia/gpu/shared_memory.cc @@ -416,6 +416,34 @@ bool SharedMemory::RequestRange(uint32_t start, uint32_t length) { return UploadRanges(uploads, current_upload_range); } +bool SharedMemory::IsRangeValid(uint32_t start, uint32_t length) const { + if (!length) { + return true; + } + if (start > kBufferSize || (kBufferSize - start) < length) { + return false; + } + + const uint32_t page_first = start >> page_size_log2_; + const uint32_t page_last = (start + length - 1) >> page_size_log2_; + const uint32_t block_first = page_first >> 6; + const uint32_t block_last = page_last >> 6; + + for (uint32_t i = block_first; i <= block_last; ++i) { + uint64_t valid_mask = UINT64_MAX; + if (i == block_first) { + valid_mask &= ~((uint64_t(1) << (page_first & 63)) - 1); + } + if (i == block_last && (page_last & 63) != 63) { + valid_mask &= (uint64_t(1) << ((page_last & 63) + 1)) - 1; + } + if ((system_page_flags_valid_[i] & valid_mask) != valid_mask) { + return false; + } + } + return true; +} + template XE_FORCEINLINE XE_NOALIAS static T mod_shift_left(T value, uint32_t by) { #if XE_ARCH_AMD64 == 1 diff --git a/src/xenia/gpu/shared_memory.h b/src/xenia/gpu/shared_memory.h index 23f92011d..2b5675789 100644 --- a/src/xenia/gpu/shared_memory.h +++ b/src/xenia/gpu/shared_memory.h @@ -72,6 +72,10 @@ class SharedMemory { // ensures the host GPU memory backing the range are resident. Returns true if // the range has been fully updated and is usable. bool RequestRange(uint32_t start, uint32_t length); + // Returns whether every page in the range is currently valid in the host GPU + // memory copy. Hold the global critical region if relying on this for state + // transitions such as watch installation. + bool IsRangeValid(uint32_t start, uint32_t length) const; void TryFindUploadRange(const uint32_t& block_first, const uint32_t& block_last, diff --git a/src/xenia/gpu/texture_cache.cc b/src/xenia/gpu/texture_cache.cc index 8341764f1..5e012c9a6 100644 --- a/src/xenia/gpu/texture_cache.cc +++ b/src/xenia/gpu/texture_cache.cc @@ -517,23 +517,38 @@ TextureCache::Texture::~Texture() { texture_cache_.UpdateTexturesTotalHostMemoryUsage(0, host_memory_usage_); } -void TextureCache::Texture::MakeUpToDateAndWatch( +bool TextureCache::Texture::MakeUpToDateAndWatch( const global_unique_lock_type& global_lock) { SharedMemory& shared_memory = texture_cache().shared_memory(); - if (base_outdated_) { + const bool watch_base = base_outdated_; + const bool watch_mips = mips_outdated_; + assert_true(global_lock.owns_lock()); + if (watch_base && + !shared_memory.IsRangeValid( + key().base_page << 12, xe::align(GetGuestBaseSize(), UINT32_C(16)))) { + return false; + } + if (watch_mips && + !shared_memory.IsRangeValid( + key().mip_page << 12, xe::align(GetGuestMipsSize(), UINT32_C(16)))) { + return false; + } + + if (watch_base) { assert_not_zero(GetGuestBaseSize()); base_outdated_ = false; base_watch_handle_ = shared_memory.WatchMemoryRange( key().base_page << 12, GetGuestBaseSize(), TextureCache::WatchCallback, this, nullptr, 0); } - if (mips_outdated_) { + if (watch_mips) { assert_not_zero(GetGuestMipsSize()); mips_outdated_ = false; mips_watch_handle_ = shared_memory.WatchMemoryRange( key().mip_page << 12, GetGuestMipsSize(), TextureCache::WatchCallback, this, nullptr, 1); } + return true; } void TextureCache::Texture::MarkAsUsed() { @@ -781,7 +796,9 @@ void TextureCache::LoadTexturesData(Texture** textures, uint32_t n_textures) { // resolves as well to detect when the CPU wants to reuse the memory for a // regular texture or a vertex buffer, and thus the scaled resolve version // is not up to date anymore. - texture->MakeUpToDateAndWatch(crit); + if (!texture->MakeUpToDateAndWatch(crit)) { + continue; + } texture->LogAction("Loaded"); } @@ -861,7 +878,9 @@ bool TextureCache::LoadTextureData(Texture& texture) { // resolves as well to detect when the CPU wants to reuse the memory for a // regular texture or a vertex buffer, and thus the scaled resolve version is // not up to date anymore. - texture.MakeUpToDateAndWatch(global_critical_region_.Acquire()); + if (!texture.MakeUpToDateAndWatch(global_critical_region_.Acquire())) { + return false; + } texture.LogAction("Loaded"); diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index aa2f946e8..c1e6ca5c7 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -274,7 +274,7 @@ class TextureCache { // not). bool base_outdated_lockless() const { return base_outdated_; } bool mips_outdated_lockless() const { return mips_outdated_; } - void MakeUpToDateAndWatch(const global_unique_lock_type& global_lock); + bool MakeUpToDateAndWatch(const global_unique_lock_type& global_lock); void WatchCallback(const global_unique_lock_type& global_lock, bool is_mip);