graphics: hold image heap descriptors whose address another extent pins

This commit is contained in:
nmzik committed 2026-10-26 05:32:02 +01:00
1 parent c5f7644751
commit 95bbc9c65d
6 files changed
+99

No files matched your search

+13
View File
@@ -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];
+1
View File
@@ -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,
@@ -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);
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<bool>(image->depth_id);
if (stencil_association) {
@@ -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);
@@ -81,6 +81,7 @@ private:
std::vector<uint32_t> released; // No slot selects them after the next upload.
std::vector<uint32_t> away; // Not yet moved to the read layout.
std::vector<uint32_t> dcc; // Color metadata clears apply on each discovery.
std::vector<uint32_t> held; // Another extent holds their address.
std::unique_ptr<Buffer> 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 {
+59
View File
@@ -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<uint32_t>(Prospero::ImageType::kColor3D) << 28u);
volume_words[4] = 3u;
const auto volume = resolve(volume_root, volume_words);
Require(name, "volume", static_cast<bool>(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<bool>(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));