diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.cpp b/src/graphics/host_gpu/renderer/cache/textureCache.cpp index ece2c7c0f..9d4edd437 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/textureCache.cpp @@ -1302,6 +1302,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) { view_layer = overlap.layer; } } + // PS5: image heaps hold a descriptor per dynamic-resolution extent of a render target, + // and only one extent at an address holds data, so heap lookups create no second one. + if (!result && desc.heap && + std::any_of(candidates.begin(), candidates.end(), [&](ImageId id) { + const auto* image = m_slot_images.try_get(id); + return image != nullptr && image->registered && + image->heap_refs.load(std::memory_order_relaxed) != 0u && + image->info.data.address == desc.info.data.address && + image->info.type == desc.info.type && + image->info.extent != desc.info.extent; + })) { + return {}; + } } else if (block_alias) { for (const auto candidate: candidates) { const auto& owner = m_slot_images[candidate]; diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.h b/src/graphics/host_gpu/renderer/cache/textureCache.h index 0b5fe8538..cee930a23 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.h +++ b/src/graphics/host_gpu/renderer/cache/textureCache.h @@ -43,6 +43,7 @@ public: ImageInfo info; ImageViewInfo view_info; BindingType type = BindingType::Texture; + bool heap = false; // PS5: resolved for a resident image heap. }; TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager, diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index 709828073..35940134a 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -604,8 +604,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR view_levels, desc.info.resources.layers); desc.view_info.base_level = view_base; desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture; + desc.heap = resource.heap; - auto id = texture_cache.FindImage(desc, shader_conversion); + auto id = texture_cache.FindImage(desc, shader_conversion); + if (!id) { + // Another extent at the address holds it; the heap retries once an image is created. + return {.desc = std::move(desc), .layout = vk::ImageLayout::eGeneral}; + } auto* image = &texture_cache.GetImage(id); const bool stencil_association = static_cast(image->depth_id); if (stencil_association) { diff --git a/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp b/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp index b91143fce..dc8a65c9f 100644 --- a/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/imageHeap.cpp @@ -394,6 +394,9 @@ uint32_t ImageHeap::Allocate() { } void ImageHeap::Retire(View& view) { + if (view.released.empty()) return; + // A retired index is reused once its tick ends, so no held entry may keep it. + std::erase_if(view.held, [&](uint32_t index) { return m_elements[index].refs == 0u; }); const auto tick = m_context.GetCommandScheduler().CurrentTick(); for (const auto index: view.released) m_retired.emplace_back(tick, index); view.released.clear(); @@ -440,6 +443,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) { KYTY_PROFILER_FUNCTION(); auto& textures = m_context.GetTextureCache(); const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed); + const auto serial = Image::serials.load(std::memory_order_relaxed); for (auto* view: m_active) { bool stale = false; const auto mark = [&](uint32_t index) { m_elements[index].stale = stale = true; }; @@ -468,6 +472,19 @@ void ImageHeap::Rebind(RenderExecutor& executor) { mark(index); return true; }); + // Held elements retry once an image is created, such as a render target of their extent. + if (view->held_serial != serial) { + view->held_serial = serial; + std::erase_if(view->held, [&](uint32_t index) { + const auto& element = m_elements[index]; + if (element.refs == 0u || element.stale) return true; + const auto binding = executor.ResolveTexture( + IR::ImageHeapResource(view->profile, element.kind), element.value); + if (binding.image_id) mark(index); + // An unmapped texture stays null. + return binding.image_id || binding.desc.info.data.Empty(); + }); + } if (stale) { // A changed image needs a new element; pending draws still read the old one. The new // element keeps the old value, since a view takes table changes only when acquired. @@ -496,6 +513,8 @@ void ImageHeap::Rebind(RenderExecutor& executor) { view->dcc.push_back(index); } view->away.push_back(index); + } else if (!binding.desc.info.data.Empty()) { + view->held.push_back(index); } m_elements[index].binding = std::move(binding); m_writes.push_back(index); diff --git a/src/graphics/host_gpu/renderer/pipeline/imageHeap.h b/src/graphics/host_gpu/renderer/pipeline/imageHeap.h index e0056ed91..44720178b 100644 --- a/src/graphics/host_gpu/renderer/pipeline/imageHeap.h +++ b/src/graphics/host_gpu/renderer/pipeline/imageHeap.h @@ -81,6 +81,7 @@ private: std::vector released; // No slot selects them after the next upload. std::vector away; // Not yet moved to the read layout. std::vector dcc; // Color metadata clears apply on each discovery. + std::vector held; // Another extent holds their address. std::unique_ptr buffer; size_t dirty_begin = 0; // Slots to upload. size_t dirty_end = 0; @@ -88,6 +89,7 @@ private: size_t changed_end = 0; uint64_t resolve_epoch = 0; uint64_t layout_epoch = 0; + uint64_t held_serial = 0; // Image::serials when held was last retried. }; struct Memory { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 3942a0892..b3066ac95 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -5511,6 +5511,65 @@ public: ImageHeapTestAccess::ImageOf(heap, filtered.slots[240]), "a reactivated view kept an element whose image slot holds a new image"); + // Dynamic-resolution tables hold a descriptor per extent of one render target, each with + // its own pitch. Only the first gets an image; the others wait until an image of their + // extent exists. + const auto extent = [&](uint32_t width) { + auto words = descriptor(50, Format::k8_8_8_8UNorm); + words[2] = (width - 1u) | (3u << 14u); + return words; + }; + const auto resolve = [&](const auto &root, const auto &words) { + return executor.ResolveTexture(root, {.dwords = words, .dword_count = 8u}).image_id; + }; + write(50, extent(64)); + write(51, extent(128)); + Require(name, "extents", acquire(sampled), "the view was declined"); + draw(); + Require(name, "held extent", + ImageHeapTestAccess::ImageOf(heap, filtered.slots[400]) && + !ImageHeapTestAccess::ImageOf(heap, filtered.slots[408]), + "a second extent of one address got its own image"); + // A non-heap lookup, as for a render target, creates the second extent. + const auto rendered = resolve(sampled, extent(128)); + Require(name, "rendered extent", acquire(sampled), "the view was declined"); + draw(); + Require(name, "held descriptor", + rendered && ImageHeapTestAccess::ImageOf(heap, filtered.slots[408]) == rendered, + "a held descriptor missed the image made for its extent"); + // A released held element's index is reused once its tick ends; it is no longer held. + write(52, extent(256)); + Require(name, "third extent", acquire(sampled), "the view was declined"); + draw(); + const auto held = filtered.slots[416]; + write(52, {}); + Require(name, "released extent", acquire(sampled), "the view was declined"); + draw(); + write(53, descriptor(53, Format::k8_8_8_8UNorm)); + Require(name, "reused element", acquire(sampled), "the view was declined"); + draw(); + Require(name, "retry", acquire(sampled), "the view was declined"); + draw(); + Require(name, "reused held element", held != 0u && filtered.slots[424] == held, + "a reused element was retried as the held element it replaced"); + // A 2D view of a heap-pinned volume is another image type, not another extent. + auto plane_root = sampled; + plane_root.heap = true; + auto volume_root = plane_root; + volume_root.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim3D; + auto volume_words = descriptor(54, Format::k8_8_8_8UNorm); + volume_words[3] = DstSel(4, 5, 6, 7) | + (static_cast(Prospero::ImageType::kColor3D) << 28u); + volume_words[4] = 3u; + const auto volume = resolve(volume_root, volume_words); + Require(name, "volume", static_cast(volume), "a volume texture was not resolved"); + auto &volume_refs = textures.GetImage(volume).heap_refs; + volume_refs.fetch_add(1u); + const auto plane = resolve(plane_root, descriptor(54, Format::k8_8_8_8UNorm)); + volume_refs.fetch_sub(1u); + Require(name, "volume plane", static_cast(plane), + "a 2D view of a pinned volume was held as another extent"); + // A table follows its descriptor's size and keeps its elements. Growing probes the new // words, shrinking releases them, and a draw sees one size. write(table_size / 32u, descriptor(40, Format::k8_8_8_8UNorm));