From 38c884e8c16a2ae6fab6c950a8a248a225a66f7b Mon Sep 17 00:00:00 2001 From: nmzik Date: Fri, 23 Oct 2026 06:40:04 +0200 Subject: [PATCH] graphics: keep image heap descriptors of unmapped textures null --- .../renderer/pipeline/descriptors.cpp | 4 ++ .../host_gpu/renderer/pipeline/imageHeap.cpp | 44 +++++++++++++-- .../host_gpu/renderer/pipeline/imageHeap.h | 8 ++- tests/ShaderRecompilerComputeTests.cpp | 55 +++++++++++++++++++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index cde93c6ef..3a3f0abc0 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -585,6 +585,10 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR const auto alignment = desc.info.DataAlignment(); EXIT_NOT_IMPLEMENTED(desc.info.data.size == 0 || alignment == 0 || (address & (static_cast(alignment) - 1u)) != 0); + // Image heaps keep the descriptors of unmapped textures, which PS5 never samples. + if (resource.heap && !m_context.IsMapped(address, desc.info.data.size)) { + return {.layout = vk::ImageLayout::eGeneral}; + } if (storage) { ValidateStorageTexture(resource, descriptor, desc.info.data.size); } diff --git a/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp b/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp index b89c09b17..926691df0 100644 --- a/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp @@ -131,6 +131,7 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& created->base = request.base; created->size = size; created->words.resize(size / 4u); + created->dead.resize(size / 4u); created->dirty_pages.assign((Common::AlignUp(request.base + size, TRACKER_PAGE_SIZE) - Common::AlignDown(request.base, TRACKER_PAGE_SIZE)) / TRACKER_PAGE_SIZE, @@ -278,7 +279,10 @@ void ImageHeap::UpdateProbe(View& view, uint64_t probe) { std::copy_n(words.begin() + offset, std::min(view.profile.r128 ? 4u : 8u, words.size() - offset), value.dwords.begin()); - const auto kind = IR::ImageHeapKind(view.profile, value); + // A buried word stays null in every view until its descriptor changes. + auto& dead = view.memory.dead[offset]; + if (dead != 0u && dead != DescriptorHash {}(value)) dead = 0u; + const auto kind = dead != 0u ? 0u : IR::ImageHeapKind(view.profile, value); Assign(view, offset, kind == 0u ? 0u : Intern(view, kind, value)); } @@ -288,6 +292,32 @@ void ImageHeap::Assign(View& view, size_t slot, uint32_t entry) { if (previous != entry) Widen(view.dirty_begin, view.dirty_end, slot, slot + 1u); } +void ImageHeap::Bury() { + if (m_unmapped.empty()) return; + for (const auto& memory: m_memories) { + for (const auto& view: memory->views) { + bool buried = false; + for (const auto& [value, index]: view->elements) { + const auto& binding = m_elements[index].binding; + if (binding.image_id && std::ranges::any_of(m_unmapped, [&](GuestRange range) { + return ImageRangeOverlaps(binding.desc.info.data, range); + })) { + m_elements[index].stale = buried = true; + } + } + if (!buried) continue; + for (uint64_t probe = 0; probe < view->count; ++probe) { + const auto slot = (view->first + probe * view->step) / 4u; + const auto& element = m_elements[view->slots[slot] & ElementMask]; + if (!element.stale) continue; + memory->dead[slot] = DescriptorHash {}(element.value); + Assign(*view, slot, 0u); + } + } + } + m_unmapped.clear(); +} + uint32_t ImageHeap::Intern(View& view, uint32_t kind, const DescriptorValue& value) { const auto arm = static_cast(std::ranges::find(view.kind_keys, kind) - view.kind_keys.begin()); @@ -398,9 +428,9 @@ void ImageHeap::Rebind(RenderExecutor& executor) { } for (const auto index: view->pending) { if (m_elements[index].refs == 0u) continue; - auto binding = executor.ResolveTexture( - IR::ImageHeapResource(view->profile, m_elements[index].kind), - m_elements[index].value); + auto resource = IR::ImageHeapResource(view->profile, m_elements[index].kind); + resource.heap = true; // Resolves only mapped textures. + auto binding = executor.ResolveTexture(resource, m_elements[index].value); if (binding.image_id) { auto& image = textures.GetImage(binding.image_id); image.heap_refs.fetch_add(1u, std::memory_order_relaxed); @@ -483,6 +513,12 @@ void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point, } void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) { + // Descriptors of textures in the range stay null even once it is mapped again. + if (!m_unmapped.empty() && m_unmapped.back().End() == address) { + m_unmapped.back().size += size; + } else { + m_unmapped.push_back({address, size}); + } auto& scheduler = m_context.GetCommandScheduler(); std::scoped_lock lock {m_mutex}; std::erase_if(m_memories, [&](const std::unique_ptr& memory) { diff --git a/src/graphics/host_gpu/renderer/pipeline/imageHeap.h b/src/graphics/host_gpu/renderer/pipeline/imageHeap.h index aa5b3fb63..2c80fa1e6 100644 --- a/src/graphics/host_gpu/renderer/pipeline/imageHeap.h +++ b/src/graphics/host_gpu/renderer/pipeline/imageHeap.h @@ -33,7 +33,10 @@ public: KYTY_CLASS_NO_COPY(ImageHeap); // Starts a draw or dispatch, before its programs acquire views. - void BeginDraw() { ++m_draw; } + void BeginDraw() { + ++m_draw; + Bury(); + } bool Acquire(const ShaderRecompiler::IR::ImageHeapRequest& request, ShaderRecompiler::IR::ImageHeapView& result) override; // Image discovery: resolves new elements and rediscovers changed images. @@ -89,6 +92,7 @@ private: uint64_t size = 0; std::vector words; std::vector dirty_pages; // Unwatched, so unread since a write. + std::vector dead; // Per word, a buried descriptor's hash. uint64_t draw = 0; // Last draw that refreshed it. std::vector> views; }; @@ -101,6 +105,7 @@ private: void UpdateProbes(View& view, uint64_t begin, uint64_t end); void UpdateProbe(View& view, uint64_t probe); void Assign(View& view, size_t slot, uint32_t entry); + void Bury(); uint32_t Intern(View& view, uint32_t kind, const DescriptorValue& value); void Release(View& view, uint32_t entry); void Unreference(Element& element); @@ -120,6 +125,7 @@ private: std::vector> m_runs; std::vector m_scratch; std::vector m_writes; // Elements whose descriptors changed. + std::vector m_unmapped; // Since the last Bury. vk::DescriptorSetLayout m_layout = nullptr; vk::DescriptorPool m_pool = nullptr; vk::DescriptorSet m_set = nullptr; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1de673799..351ea6e62 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -5365,6 +5365,61 @@ public: descriptor(3, Format::k8_8_8_8UNorm), "a stale element's rescan took a kind its view's programs lack"); + // Tables keep the descriptors of unmapped textures, which PS5 never samples. Their words + // stay null in every view, even when the memory is mapped again before the next draw, + // until one of their own words changes. + const auto texture = [](uint32_t index) { return base + 0x10000u + index * 0x1000u; }; + write(20, descriptor(20, Format::k8_8_8_8UNorm)); + Require(name, "live texture", acquire(sampled), "the view was declined"); + draw(); + Require(name, "live image", + static_cast(ImageHeapTestAccess::ImageOf(heap, filtered.slots[160])), + "a mapped texture was not resolved"); + context.UnmapMemory(texture(20), 0x1000u); + context.MapMemory(texture(20), 0x1000u); + heap.BeginDraw(); + Require(name, "dead in a new view", + acquire(sampled, table, 64u) && views.back()->slots[160] == 0u, + "a view created after the remap resolved a dead descriptor"); + Require(name, "remapped texture", acquire(sampled), "the view was declined"); + draw(); + Require(name, "dead descriptor", + filtered.slots[160] == 0u && !textures.IsRegionRegistered(texture(20), 0x1000u), + "the descriptor of an unmapped texture was resolved again after a remap"); + write(19, descriptor(19, Format::k8_8_8_8UNorm)); + write(21, descriptor(21, Format::k8_8_8_8UNorm)); + Require(name, "neighbour writes", acquire(sampled), "the view was declined"); + draw(); + Require(name, "dead reprobe", filtered.slots[160] == 0u, + "rescanning changed neighbours revived a dead descriptor"); + write(24, descriptor(20, Format::k8_8_8_8UNorm)); + Require(name, "republished texture", acquire(sampled), "the view was declined"); + draw(); + Require(name, "republished descriptor", + filtered.slots[160] == 0u && ImageHeapTestAccess::ImageOf(heap, filtered.slots[192]), + "the same descriptor in another word was not resolved"); + write(20, descriptor(22, Format::k8_8_8_8UNorm)); + Require(name, "rewritten word", acquire(sampled), "the view was declined"); + draw(); + Require(name, "rewritten descriptor", + static_cast(ImageHeapTestAccess::ImageOf(heap, filtered.slots[160])), + "a new descriptor in a dead word was not resolved"); + // A view that a skipped draw left acquired buries before its rescan. + Require(name, "skipped draw view", acquire(sampled), "the view was declined"); + context.UnmapMemory(texture(22), 0x1000u); + context.MapMemory(texture(22), 0x1000u); + heap.BeginDraw(); + draw(); + Require(name, "dead after a skipped draw", filtered.slots[160] == 0u, + "the rescan of a view left acquired resolved a dead descriptor"); + // A new descriptor over unmapped memory is never resolved. + context.UnmapMemory(texture(23), 0x1000u); + write(23, descriptor(23, Format::k8_8_8_8UNorm)); + Require(name, "unmapped texture", acquire(sampled), "the view was declined"); + draw(); + Require(name, "unmapped descriptor", !textures.IsRegionRegistered(texture(23), 0x1000u), + "a descriptor over unmapped memory was resolved"); + heap.UnmapMemory(table, table_size); Require(name, "unmapped table", ImageHeapTestAccess::MemoryCount(heap) == 0u &&