graphics: keep bounded image heaps resident in a bindless descriptor set

This commit is contained in:
nmzik
2026-10-22 22:15:56 +02:00
parent 79837b423d
commit 27dc7c281f
31 changed files with 1377 additions and 75 deletions
+124
View File
@@ -436,6 +436,128 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
"malformed image table tracking was not transactional");
}
void TestResidentImageHeap() {
struct FakeImageHeap final : ImageHeapSource {
bool accept = true;
std::vector<DescriptorValue> kinds;
std::vector<ImageHeapRequest> requests;
bool Acquire(const ImageHeapRequest &request, ImageHeapView &view) override {
requests.push_back(request);
if (!accept) return false;
view = {.slots = 0x123456789abcull, .slot_count = 36u, .kinds = kinds};
return true;
}
};
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
fixture->PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture->program);
std::array<uint32_t, 8> descriptor{};
descriptor[0] = 0x20u;
descriptor[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
descriptor[2] = 3u | (3u << 14u);
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
auto cube = descriptor;
cube[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
std::array<uint32_t, 16> data{0x3000u, 4u << 16u, 0x100000u, 0u,
0x2000u, 48u << 16u, 3u, 0u};
std::copy_n(data.begin(), 4u, data.begin() + 12u);
LinearTestMemory memory;
memory.fail_address = 0x3000u;
memory.watched_address = 0x2000u;
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
FakeImageHeap heap;
heap.kinds = {{descriptor, 8u}, {cube, 8u}};
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory, .image_heap = &heap};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
heap.requests.size() == 1u && heap.requests[0].base == 0x2000u &&
heap.requests[0].size == 144u && heap.requests[0].step == 16u &&
heap.requests[0].first == 0u && heap.requests[0].count == 9u &&
heap.requests[0].root == &plan.info.images[0] &&
memory.watched_reads == 0u && snapshot.images.size() == 3u,
"resident image heap was materialized entry by entry");
const auto &root = specialization.images[0];
const auto mapping = root.indirect_mapping_offset;
Check(root.heap && root.indirect_root == 0u && root.indirect_search_iterations == 0u &&
snapshot.flattened_srt.size() == mapping + 3u &&
snapshot.flattened_srt[mapping] == 36u &&
snapshot.flattened_srt[mapping + 1u] == 0x56789abcu &&
snapshot.flattened_srt[mapping + 2u] == 0x1234u &&
specialization.images[1].heap && specialization.images[1].indirect_root == 0u &&
!specialization.images[1].cube && specialization.images[2].heap &&
specialization.images[2].cube && snapshot.images[2].dwords == cube &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{0x2000u, 144u}) !=
snapshot.specialization_reads.end(),
"resident image heap lost its slot mapping, typed kinds or read dependency");
auto &program = fixture->program;
ApplyResourceSpecialization(program, specialization);
ShaderComputeInputInfo compute{};
CollectShaderInfo(program, {.compute = &compute});
Check(program.info.uses_image_heap && program.info.images.size() == 3u &&
program.info.images[0].indirect_resources == std::vector<uint32_t>({0u, 1u, 2u}) &&
std::ranges::all_of(program.info.images, &ImageResource::heap),
"applied heap kinds lost their root or heap flag");
heap.kinds.clear();
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1u && !specialization.images[0].heap &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
"a resident heap without kinds did not read as one null image");
heap.accept = false;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
heap.requests.size() == 3u && memory.watched_reads == 1u &&
snapshot.images.size() == 2u && !specialization.images[0].heap &&
snapshot.images[1].dwords == descriptor,
"a declined heap was not materialized entry by entry");
}
void TestImageHeapKinds() {
using Format = Libs::Graphics::Prospero::BufferFormat;
using Type = Libs::Graphics::Prospero::ImageType;
ImageResource root;
root.resource_class = ImageResourceClass::Sampled;
root.dimension = Decoder::ImageDimension::Dim2DArray;
root.source = 3u;
const auto descriptor = [](Format format, Type type) {
DescriptorValue value{.dword_count = 8u};
value.dwords[0] = 0x20u;
value.dwords[1] = static_cast<uint32_t>(format) << 20u;
value.dwords[2] = 3u | (3u << 14u);
value.dwords[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast<uint32_t>(type) << 28u);
return value;
};
const auto kind_of = [&](Format format, Type type) {
auto value = descriptor(format, type);
return ImageHeapKind(root, value);
};
const auto float_kind = kind_of(Format::k32_32_32_32Float, Type::kColor2DArray);
const auto uint_kind = kind_of(Format::k32UInt, Type::kColor2DArray);
const auto cube_kind = kind_of(Format::k32_32_32_32Float, Type::kCube);
auto volume = descriptor(Format::k32_32_32_32Float, Type::kColor3D);
Check(float_kind != 0u && uint_kind != 0u && cube_kind != 0u && float_kind != uint_kind &&
float_kind != cube_kind && kind_of(Format::kInvalid, Type::kColor2DArray) == 0u &&
ImageHeapKind(root, volume) == 0u &&
std::ranges::all_of(volume.dwords, [](uint32_t word) { return word == 0u; }),
"heap kinds did not separate typed entries or null mismatched ones");
const auto resource = ImageHeapResource(root, uint_kind);
const auto cube_resource = ImageHeapResource(root, cube_kind);
auto other = root;
other.source = 9u;
Check(resource.numeric_class == Libs::Graphics::Prospero::TextureNumericClass::Uint &&
!resource.cube && cube_resource.cube &&
cube_resource.numeric_class == Libs::Graphics::Prospero::TextureNumericClass::Float &&
resource.resource_class == ImageResourceClass::Sampled &&
ImageHeapResource(root, 0u) == ImageHeapResource(other, 0u),
"heap kind resources lost their type or kept root identity");
root.depth_compare = true;
Check(kind_of(Format::k32UInt, Type::kColor2DArray) == 0u,
"depth comparison accepted an integer heap entry");
}
void TestBoundedImageViewEligibility() {
using Type = Libs::Graphics::Prospero::ImageType;
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
@@ -8796,6 +8918,8 @@ int main() {
Run("dynamic storage mips", TestDynamicStorageMipTracking);
Run("invariant indirect images", [] { TestInvariantIndirectImageMaterialization(true); });
Run("unbounded material image keys", [] { TestInvariantIndirectImageMaterialization(false); });
Run("resident image heap", TestResidentImageHeap);
Run("image heap kinds", TestImageHeapKinds);
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
Run("exact unfiltered image candidates", TestExactUnfilteredImageCandidates);
Run("waterfall image table", TestWaterfallImageTable);
+279 -2
View File
@@ -228,6 +228,18 @@ struct TileManagerTestAccess {
}
};
struct ImageHeapTestAccess {
static size_t ElementCount(const ImageHeap &heap) { return heap.m_elements.size(); }
static size_t MemoryCount(const ImageHeap &heap) { return heap.m_memories.size(); }
static const auto &Views(const ImageHeap &heap) { return heap.m_memories.at(0)->views; }
static const auto &Element(const ImageHeap &heap, uint32_t entry) {
return heap.m_elements[entry & ((1u << ShaderRecompiler::IR::ImageHeapKindShift) - 1u)];
}
static ImageId ImageOf(const ImageHeap &heap, uint32_t entry) {
return Element(heap, entry).binding.image_id;
}
};
struct TextureCacheTestAccess {
static_assert(std::same_as<decltype(TextureCache::m_slot_images),
Common::SlotVector<Image>>);
@@ -5150,6 +5162,234 @@ public:
std::printf("[host] %-32s ok\n", name);
}
void CheckResidentImageHeap() {
constexpr const char *name = "ResidentImageHeap";
constexpr uintptr_t base = 0x0000000206800000ull;
constexpr uint64_t allocation_size = 0x200000;
constexpr uint64_t table = base + 0x100000;
constexpr uint64_t idle_table = base + 0x180000;
constexpr uint64_t table_size = 0x10000;
namespace IR = ShaderRecompiler::IR;
using Format = Prospero::BufferFormat;
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_size, 0, &direct_offset) == 0,
"image heap allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_size) == 0 && mapped == reinterpret_cast<void *>(base),
"image heap mapping failed");
std::memset(mapped, 0, allocation_size);
// A 4x4 linear 2D texture per index.
const auto descriptor = [](uint32_t texture, Format format) {
const uint64_t address = base + 0x10000u + texture * 0x1000u;
std::array<uint32_t, 8> words{};
words[0] = static_cast<uint32_t>(address >> 8u);
words[1] = static_cast<uint32_t>(address >> 40u) | (static_cast<uint32_t>(format) << 20u);
words[2] = 3u | (3u << 14u);
words[3] = DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Prospero::ImageType::kColor2D) << 28u);
return words;
};
const auto put = [&](uint64_t probe, const std::array<uint32_t, 8> &words) {
std::memcpy(reinterpret_cast<void *>(table + probe * 32u), words.data(), sizeof(words));
};
put(0, descriptor(0, Format::k8_8_8_8UNorm));
put(1, descriptor(0, Format::k8_8_8_8UNorm));
put(5, descriptor(1, Format::k8_8_8_8UNorm));
{
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &heap = context.GetImageHeap();
auto &executor = context.GetRenderExecutor();
auto &textures = context.GetTextureCache();
auto &device = context.GetGraphics().device;
const vk::DescriptorSetLayoutCreateInfo empty_info{};
vk::DescriptorSetLayout empty = nullptr;
vk::PipelineLayout layout = nullptr;
Require(name, "set 0 layout",
device.createDescriptorSetLayout(&empty_info, nullptr, &empty) ==
vk::Result::eSuccess,
"could not create the set 0 layout");
const std::array set_layouts{empty, context.GetGraphics().image_heap_layout};
vk::PipelineLayoutCreateInfo layout_info{};
layout_info.setLayoutCount = 2;
layout_info.pSetLayouts = set_layouts.data();
Require(name, "pipeline layout",
device.createPipelineLayout(&layout_info, nullptr, &layout) == vk::Result::eSuccess,
"could not create a pipeline layout with the heap set");
IR::ImageResource sampled{};
sampled.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
sampled.unfiltered_access_only = false;
auto unfiltered = sampled;
unfiltered.unfiltered_access_only = true;
IR::ImageHeapView view{};
const auto acquire = [&](const IR::ImageResource &root, uint64_t address = table,
uint64_t count = table_size / 32u) {
return heap.Acquire({address, table_size, 32u, 0u, count, &root}, view);
};
const auto draw = [&] {
heap.Rebind(executor);
heap.Commit(scheduler.Current(), vk::PipelineBindPoint::eCompute, layout);
scheduler.Finish();
heap.BeginDraw();
};
const auto write = [&](uint64_t probe, const std::array<uint32_t, 8> &words) {
Libs::LibKernel::Memory::WriteBacking(table + probe * 32u, words.data(), sizeof(words));
Require(name, "table write", context.InvalidateMemory(table + probe * 32u, sizeof(words)),
"the table was not mapped");
};
Require(name, "first view",
acquire(sampled) && view.kinds.size() == 1u && view.slot_count == table_size / 4u,
"a 64-KiB table was not kept resident");
const auto &views = ImageHeapTestAccess::Views(heap);
const auto &filtered = *views[0];
Require(name, "slot words",
filtered.slots[0] != 0u && filtered.slots[8] == filtered.slots[0] &&
filtered.slots[40] != 0u && filtered.slots[40] != filtered.slots[0] &&
filtered.slots[1] == 0u && filtered.slots.back() == 0u &&
ImageHeapTestAccess::ElementCount(heap) == 3u &&
ImageHeapTestAccess::Element(heap, 0u).binding.layout ==
vk::ImageLayout::eShaderReadOnlyOptimal,
"probes did not share equal descriptors, left a non-null gap or wrote the null "
"element without a sampled layout");
Require(name, "second view",
acquire(unfiltered) && views.size() == 2u &&
ImageHeapTestAccess::ElementCount(heap) == 5u,
"a second root profile did not get its own view");
const auto &idle = *views[1];
draw();
const auto resident = ImageHeapTestAccess::ImageOf(heap, filtered.slots[0]);
Require(name, "resident images",
resident && textures.GetImage(resident).heap_refs.load() == 2u,
"resolved elements did not pin their image");
// Only acquired views take table changes; the idle one must not allocate.
const auto idle_slot = idle.slots[40];
for (uint32_t texture = 2; texture < 4u; ++texture) {
write(5, descriptor(texture, Format::k8_8_8_8UNorm));
Require(name, "changed table", acquire(sampled), "a changed table was declined");
draw();
}
Require(name, "idle view",
idle.slots[40] == idle_slot && ImageHeapTestAccess::ElementCount(heap) <= 6u,
"an idle view took table changes or leaked released elements");
Require(name, "reactivated view", acquire(unfiltered), "the idle view was declined");
draw();
Require(name, "deferred probes",
ImageHeapTestAccess::ImageOf(heap, idle.slots[40]) ==
ImageHeapTestAccess::ImageOf(heap, filtered.slots[40]) &&
ImageHeapTestAccess::ElementCount(heap) <= 7u,
"a reactivated view missed the changes made while it was idle");
// All programs of one draw see the table and kinds of its first acquire.
Require(name, "draw kinds", acquire(sampled), "the view was declined");
write(7, descriptor(12, Format::k8_8_8_8UInt));
Require(name, "stable kinds",
acquire(sampled) && view.kinds.size() == 1u && filtered.slots[56] == 0u,
"a later program of a draw saw a kind the first one lacked");
draw();
Require(name, "next draw kinds",
acquire(sampled) && view.kinds.size() == 2u &&
filtered.slots[56] >> IR::ImageHeapKindShift == 1u,
"the next draw did not take the new kind");
draw();
Require(name, "bounded view",
acquire(sampled, table, 4u) && views.size() == 3u && views[2]->slots[0] != 0u &&
views[2]->slots[40] == 0u,
"a view probed descriptors past the guest table bound");
draw();
// Only leaving the read state invalidates resident images.
auto &image = textures.GetImage(resident);
const auto epoch = Libs::Graphics::Image::heap_epoch.load();
for (uint32_t pass = 0; pass < 2u; ++pass) {
image.Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderWrite, {},
scheduler.Current().Handle());
}
Require(name, "write epoch", Libs::Graphics::Image::heap_epoch.load() == epoch + 1u,
"repeated writes after leaving the read state bumped the heap epoch again");
Require(name, "read again", acquire(sampled), "the view was declined");
draw();
Require(name, "read layout",
image.backing.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal &&
Libs::Graphics::Image::heap_epoch.load() == epoch + 1u,
"commit did not return a resident image to its read layout");
// An image freed during a draw's discovery lives until the tick ends, and the draw's
// slots still select it.
write(9, descriptor(13, Format::k8_8_8_8UNorm));
Require(name, "freed image", acquire(sampled), "the view was declined");
heap.Rebind(executor);
const auto freed = ImageHeapTestAccess::ImageOf(heap, filtered.slots[72]);
textures.UnmapMemory(base + 0x10000u + 13u * 0x1000u, 0x1000u);
heap.Commit(scheduler.Current(), vk::PipelineBindPoint::eCompute, layout);
Require(name, "freed image layout",
!textures.GetImage(freed).registered &&
textures.GetImage(freed).backing.state.layout ==
vk::ImageLayout::eShaderReadOnlyOptimal,
"commit skipped the read transition of a freed but sampled image");
scheduler.Finish();
heap.BeginDraw();
// A draw skipped after its programs acquired views still leaves the next draw a fresh
// table.
Require(name, "skipped draw", acquire(sampled), "the view was declined");
heap.BeginDraw();
write(11, descriptor(16, Format::k8_8_8_8UNorm));
Require(name, "draw after a skipped one", acquire(sampled) && filtered.slots[88] != 0u,
"a draw sampled the table of a skipped draw");
// The skipped draw's view stays acquired. Another view refreshing the table must not let
// a stale element take a newer kind from it.
heap.BeginDraw();
write(5, descriptor(15, Format::k8_8_8_8SInt));
Require(name, "other view", acquire(unfiltered), "the view was declined");
textures.UnmapMemory(base + 0x10000u + 3u * 0x1000u, 0x1000u);
draw();
Require(name, "stale rescan",
filtered.kinds.size() == 2u &&
ImageHeapTestAccess::Element(heap, filtered.slots[40]).value.dwords ==
descriptor(3, Format::k8_8_8_8UNorm),
"a stale element's rescan took a kind its view's programs lack");
heap.UnmapMemory(table, table_size);
Require(name, "unmapped table",
ImageHeapTestAccess::MemoryCount(heap) == 0u &&
textures.GetImage(resident).heap_refs.load() == 0u,
"unmapping the table kept its images pinned");
// The idle table stays watched; destroying the heap must unwatch it before the page
// manager fails on live watches.
Require(name, "idle table",
acquire(sampled, idle_table) && view.kinds.empty() &&
ImageHeapTestAccess::MemoryCount(heap) == 1u,
"an all-null table was not kept resident");
draw();
context.UnmapMemory(base, idle_table - base);
scheduler.Finish();
device.destroyPipelineLayout(layout, nullptr);
device.destroyDescriptorSetLayout(empty, nullptr);
}
Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"image heap unmap failed");
Require(name, "release", Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"image heap release failed");
std::printf("[host] %-32s ok\n", name);
}
void CheckComputeMetaClearClassification() {
constexpr const char *name = "ComputeMetaClearClassification";
constexpr uint64_t read_only_meta = 0x0000000204201f00ull;
@@ -41289,6 +41529,41 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
Require(name, "alternating image emitted size", spirv.size() <= 8192u,
"alternating descriptors exceeded the 32 KiB SPIR-V regression budget");
// A resident heap selects its typed kind and element from a device slot word.
auto heap_root = root;
heap_root.heap = true;
heap_root.indirect_search_iterations = 0;
heap_root.indirect_resources = {0u, 1u, 2u};
auto heap_kind = candidate;
heap_kind.heap = true;
auto heap_uint = heap_kind;
heap_uint.numeric_class = Prospero::TextureNumericClass::Uint;
for (const bool mixed : {true, false}) {
program.info.images = {heap_root, heap_kind};
if (mixed) program.info.images.push_back(heap_uint);
program.info.images[0].indirect_resources.resize(program.info.images.size());
// ApplyResourceSpecialization adds the integer sampler variant of a Uint kind.
program.info.samplers.resize(1);
if (mixed) {
program.info.samplers.push_back(program.info.samplers[0]);
program.info.samplers[1].integer_border = true;
}
program.shader_info_complete = false;
CollectShaderInfo(program, {.compute = &compute});
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
ValidateSpirv("ResidentImageHeap", spirv);
Require(name, "image heap disassembly", tools.Disassemble(spirv, &text),
"failed to disassemble the image heap shader");
Require(name, "image heap selection",
program.info.uses_image_heap && CountText(text, "OpConvertUToPtr") == 1 &&
CountText(text, "UMin") == 1 &&
CountText(text, "OpSwitch") == (mixed ? 1 : 0) &&
CountText(text, "OpImageSampleExplicitLod") == (mixed ? 2 : 1) &&
CountText(text, "DescriptorSet 1") == (mixed ? 2 : 1) &&
text.find("PhysicalStorageBuffer64") != std::string::npos,
"image heap kinds did not sample the slot-selected heap element");
}
program.memory_info[0].image_dimension =
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
program.memory_info[0].image_address_components = 4;
@@ -44054,7 +44329,7 @@ void CheckComputeLdsLimit(VulkanHarness &vulkan) {
regs.cs_regs.num_thread_y = 1;
regs.cs_regs.num_thread_z = 1;
regs.cs_regs.wave_size = 32;
PipelineCache cache(graphics);
PipelineCache cache(graphics, nullptr);
ShaderProgram previous;
uint16_t previous_units = 0;
for (const auto units : {static_cast<uint16_t>(limit_units - 1u), limit_units,
@@ -49350,7 +49625,7 @@ void CheckPm4IndirectControlFlow(RenderContext &renderer) {
std::printf("[host] %-32s ok\n", name);
}
// waitUntilSafeForRendering stalls only its own command processor (SDK), and a display buffer
// waitUntilSafeForRendering stalls only its own command processor, and a display buffer
// stays busy while its flip is pending or while it is on screen.
void CheckWaitUntilSafeForRendering() {
constexpr const char *name = "WaitUntilSafeForRendering";
@@ -50653,6 +50928,7 @@ int main(int argc, char **argv) {
CheckNestedPointerImageTable(vulkan);
CheckGuardedRecordImageTable(vulkan);
CheckIndirectImageKeySwitch(vulkan);
vulkan.CheckResidentImageHeap();
CheckFiniteFirstLaneImageChoice(vulkan);
CheckFiniteInlineSamplerPhi(vulkan);
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
@@ -50831,6 +51107,7 @@ int main(int argc, char **argv) {
CheckNestedPointerImageTable(vulkan);
CheckGuardedRecordImageTable(vulkan);
CheckIndirectImageKeySwitch(vulkan);
vulkan.CheckResidentImageHeap();
CheckDynamicSampleOffsets(vulkan, 32u);
CheckDynamicSampleOffsets(vulkan, 64u);
CheckFiniteFirstLaneImageChoice(vulkan);