shader: materialize compact image and paired sampler tables

Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
nmzik
2026-10-15 03:02:55 +02:00
parent 7d24972b8f
commit 81169d77ea
4 changed files with 250 additions and 55 deletions
@@ -299,13 +299,13 @@ bool IsBoundedDescriptorTable(const ResourcePlan& program,
template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect,
uint32_t resource_index, uint32_t dword_count,
uint32_t resource_index, uint32_t dword_count, uint32_t read_dwords,
const SrtRuntime& runtime, SrtWalker& clean,
ResourceSnapshot& snapshot,
std::vector<DescriptorValue>& descriptors,
std::vector<Specialization>& specializations,
uint32_t maximum_resources, Normalize&& normalize) {
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
auto& keys = program.material_keys;
@@ -445,12 +445,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, address) ||
!ReadScalarTable((uint64_t {address[1]} << 32u) | address[0], UINT64_MAX,
*indirect.pointer_offset, runtime,
std::span(candidate.dwords).first(dword_count))) return false;
std::span(candidate.dwords).first(read_dwords))) return false;
} else if (affine) {
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(dword_count))) return false;
std::span(candidate.dwords).first(read_dwords))) return false;
} else {
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
std::copy_n(keys.begin() + word, std::min<size_t>(read_dwords, keys.size() - word),
candidate.dwords.begin());
}
if (!normalize(candidate))
@@ -672,7 +672,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(dword_count))) {
std::span(candidate.dwords).first(read_dwords))) {
return false;
}
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
@@ -1441,7 +1441,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
snapshot.buffers[i] = {.dword_count = 4u};
if (active.empty() || active[base.source]) {
if (!MaterializeIndirectDescriptor(
program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot,
program, *source->indirect_descriptor, i, 4u, 4u, observed, clean, snapshot,
snapshot.buffers, specialization.buffers, UINT32_MAX,
[&](DescriptorValue& value) {
return NormalizeIndirectBuffer(
@@ -1505,7 +1505,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
program, indirect, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
@@ -1538,7 +1538,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (indirect != nullptr && indirect->sources.empty()) {
snapshot.samplers[i] = {.dword_count = 4u};
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
program, *indirect, i, 4u, observed, clean, snapshot, snapshot.samplers,
program, *indirect, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
specialization.samplers, ShaderInfo::MaxSamplers,
[](DescriptorValue&) { return true; })) return false;
} else {
@@ -513,7 +513,7 @@ public:
}
}
if (plan.reads[0] != nullptr && !plan.retain_reads) {
for (uint32_t word = 0; word < plan.handle->NumArgs(); ++word)
for (uint32_t word = 0; word < plan.read_dwords; ++word)
m_program.memory_info[plan.memory[word]].planning_only = true;
}
}
@@ -522,7 +522,7 @@ public:
for (const auto& plan: m_indirect_descriptors) {
if (plan.handle->GetOpcode() != ValueOpcode::GetImageResource || plan.reads[0] == nullptr)
continue;
for (const auto* read: plan.reads) {
for (const auto* read: std::span(plan.reads).first(plan.read_dwords)) {
const auto first = std::ranges::find_if(m_indirect_descriptors, [&](const auto& candidate) {
return candidate.reads[0] == read;
});
@@ -559,6 +559,7 @@ private:
struct IndirectDescriptorPlan {
Inst* handle = nullptr;
uint32_t source = 0;
uint32_t read_dwords = 0;
Value key;
std::array<Value, 8> roots {};
std::array<uint32_t, 8> memory {};
@@ -2196,7 +2197,8 @@ private:
}
bool MatchScalarMaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source, bool require_bound) {
DescriptorSource& material_source, bool require_bound,
bool allow_scalar_domain = false) {
DescriptorSource::IndirectDescriptor::SelectorRead selector;
const auto* read = SelectorReadValue(key, Value(true), selector);
if (read == nullptr) return false;
@@ -2211,9 +2213,10 @@ private:
uint32_t offset = 0, stride = 0;
if (memory == nullptr || memory->offset > INT32_MAX ||
!MemoryIndexBelongsTo(memory_index, *read)) return false;
if (MatchTableOffset(read->Arg(1), index, offset, stride) &&
const bool affine = MatchTableOffset(read->Arg(1), index, offset, stride) &&
((offset | stride | memory->offset) & 3u) == 0u &&
uint64_t {offset} + memory->offset <= UINT32_MAX) {
uint64_t {offset} + memory->offset <= UINT32_MAX;
if (affine) {
workgroup_axis = WorkgroupAxis(index);
if (workgroup_axis == UINT32_MAX) {
const auto* sum = index.Resolve().TryInstruction();
@@ -2242,6 +2245,12 @@ private:
if (rows) {
stride = 4u;
offset = 0u;
} else if (count.IsEmpty() && workgroup_axis == UINT32_MAX &&
!require_bound && allow_scalar_domain && affine && stride != 0u) {
// The bounded selector V# covers this complete U32 address congruence
// class, including the zero value of out-of-bounds scalar reads.
stride = 1u << std::countr_zero(stride);
offset %= stride;
} else if (count.IsEmpty() && workgroup_axis == UINT32_MAX) {
auto address = read->Arg(1);
auto active = Value(true);
@@ -2666,7 +2675,7 @@ private:
uint32_t& table_stride, uint32_t& table_immediate) {
Inst* table_handle = nullptr;
Value table_guard;
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
for (uint32_t dword = 0; dword < plan.read_dwords; ++dword) {
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
if (read == nullptr) {
return false;
@@ -2734,9 +2743,9 @@ private:
}
plan.split_offsets |= !indexed && offset != table_offset;
table_handle = current_handle;
if (handle.NumArgs() == 8u) {
if (memory->kind == ResourceKind::ScalarAddress)
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
if (handle.GetOpcode() != ValueOpcode::GetBufferResource) {
plan.retain_reads |= handle.GetOpcode() == ValueOpcode::GetSamplerResource ||
(memory->kind == ResourceKind::ScalarAddress && !UsesOnlyImageDescriptors(*read));
} else if (std::ranges::any_of(read->Uses(), [&](const Use& use) {
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
std::ranges::any_of(use.user->Uses(), [&](const Use& consumer) {
@@ -2770,7 +2779,7 @@ private:
handle.GetOpcode() != ValueOpcode::GetSamplerResource) return false;
Inst* pointer = nullptr;
uint32_t descriptor_offset = 0;
for (uint32_t word = 0; word < handle.NumArgs(); ++word) {
for (uint32_t word = 0; word < plan.read_dwords; ++word) {
const auto* read = descriptor.dwords[word].Resolve().TryInstruction();
uint32_t index, offset;
const auto* memory = read != nullptr ? ScalarReadMemory(*read, index) : nullptr;
@@ -2789,7 +2798,7 @@ private:
}
// An out-of-bounds pointer pair is zero. Guest mappings exclude the first
// 16 KiB page, so these pointed reads have the existing null-image semantics.
if (pointer == nullptr || uint64_t {descriptor_offset} + handle.NumArgs() * 4u > 0x4000u)
if (pointer == nullptr || uint64_t {descriptor_offset} + plan.read_dwords * 4u > 0x4000u)
return false;
const auto* low = pointer->Arg(0).Resolve().TryInstruction();
const auto* high = pointer->Arg(1).Resolve().TryInstruction();
@@ -2830,9 +2839,11 @@ private:
return true;
}
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
if ((!sampler && handle.GetOpcode() != ValueOpcode::GetImageResource) ||
handle.NumArgs() != (sampler ? 4u : 8u)) return false;
DescriptorSource table_source;
Value key;
uint32_t table_offset = 0;
@@ -2852,7 +2863,7 @@ private:
plan.retain_reads = true;
} else if (!plan.table_indexed &&
MatchScalarMaterialKey(key, indirect, material_source,
table_source.dword_count == 4u)) {
!sampler && table_source.dword_count == 4u, sampler)) {
// Keep live descriptor payloads, but project descriptor-only SMEM onto
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
@@ -2862,11 +2873,11 @@ private:
// A runtime count can describe fewer rows than the mask's static bit range.
if (guarded_count.IsEmpty()) guarded_count = UnsignedBoundOnEntry(key, handle.Parent(), true);
if (!guarded_count.IsEmpty()) {
if (table_stride < 32u || (table_stride & 3u) != 0u) return false;
if (table_stride < plan.read_dwords * 4u || (table_stride & 3u) != 0u) return false;
// Materialization captures the count and descriptor reads; renderer alias
// checks reject writes to those ranges while unrelated stores remain valid.
indirect.key_count = guarded_count;
} else if (table_stride != 32u) {
} else if (table_stride != plan.read_dwords * 4u) {
return false;
} else {
uint32_t first = 0;
@@ -2911,31 +2922,31 @@ private:
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
if (plan.split_offsets) return false;
if (sampler || plan.split_offsets) return false;
indirect.table_stride = 0u;
indirect.table_offset = 0u;
indirect.table_immediate = 0u;
indirect.workgroup_axis = UINT32_MAX;
plan.table_read = plan.reads[0];
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + plan.read_dwords * 4u;
else indirect.table_scalar = true;
plan.retain_reads = true;
}
if (plan.retain_reads) plan.reads.fill(nullptr);
indirect.table_source = InternSource(table_source);
DescriptorSource image_source;
image_source.dword_count = 8u;
image_source.dwords.fill(Value(0u));
DescriptorSource source;
source.dword_count = handle.NumArgs();
source.dwords.fill(Value(0u));
std::copy_n(material_source.dwords.begin(), material_source.dword_count,
image_source.dwords.begin());
std::copy_n(table_source.dwords.begin(), table_source.dword_count,
image_source.dwords.begin() + 4u);
image_source.indirect_descriptor = indirect;
source.dwords.begin());
if (!sampler) std::copy_n(table_source.dwords.begin(), table_source.dword_count,
source.dwords.begin() + 4u);
source.indirect_descriptor = indirect;
plan.handle = &handle;
plan.source = InternSource(image_source);
plan.source = InternSource(source);
plan.key = key;
plan.roots = image_source.dwords;
plan.roots = source.dwords;
return true;
}
@@ -3375,8 +3386,8 @@ private:
return std::any_of(m_indirect_descriptors.begin(), m_indirect_descriptors.end(),
[&](const IndirectDescriptorPlan& plan) {
return plan.reads[0] != nullptr && !plan.retain_reads &&
std::find(plan.memory.begin(), plan.memory.begin() + plan.handle->NumArgs(), index) !=
plan.memory.begin() + plan.handle->NumArgs();
std::find(plan.memory.begin(), plan.memory.begin() + plan.read_dwords, index) !=
plan.memory.begin() + plan.read_dwords;
});
}
@@ -3398,10 +3409,22 @@ private:
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) return;
IndirectDescriptorPlan plan;
plan.read_dwords = width;
if (!sampler && width == 8u && memory.image_r128) {
// R128 handles are normalized to eight words; only their low four
// words come from memory. Every use must agree on that encoding.
if (!std::ranges::all_of(std::span(descriptor.dwords).subspan(4u),
[](Value word) { return word.Resolve() == Value(0u); }) ||
!std::ranges::all_of(handle->Uses(), [&](const Use& use) {
return ImageOpcodeInfoOf(use.user->GetOpcode()).access != ImageAccess::None &&
m_program.memory_info[use.user->Flags<MemoryFlags>().index].image_r128;
})) return;
plan.read_dwords = 4u;
}
if (TryMakeNestedDescriptor(*handle, descriptor, plan) ||
(sampler ? TryMakeFiniteDescriptor(*handle, plan)
: (TryMakeIndirectImage(*handle, descriptor, plan) ||
TryMakeFiniteDescriptor(*handle, plan) ||
(sampler && TryMakeFiniteDescriptor(*handle, plan)) ||
TryMakeTableDescriptor(*handle, descriptor, plan) ||
(!sampler && (TryMakeFiniteDescriptor(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan)))) {
for (const auto& previous: m_indirect_descriptors) {
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
+138
View File
@@ -2933,6 +2933,143 @@ void TestUniformScalarBufferImage() {
"unavailable nested image table was accepted");
}
void TestCompactScalarImageTable() {
using F = Libs::Graphics::Prospero::BufferFormat;
constexpr uint32_t stride = 0x330u, image_offset = 0x98u, sampler_offset = 0x88u;
const auto make_plan = [&](bool compact, uint32_t invalid = 0u) {
Fixture fixture;
const auto list = invalid == 1u
? fixture.Address(fixture.UserData(0), fixture.UserData(1))
: fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
fixture.UserData(2), fixture.UserData(3)});
const auto table = fixture.Buffer({fixture.UserData(4), fixture.UserData(5),
fixture.UserData(6), fixture.UserData(7)});
// Uniform on the GPU, but no CPU-evaluable row or workgroup-prefix proof.
const auto group = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)), Value(0u)});
const auto index = fixture.Emit(ValueOpcode::BitwiseXor32, {group, Value(1u)});
const auto offset = fixture.Emit(invalid == 2u ? ValueOpcode::UDiv32
: ValueOpcode::ShiftLeftLogical32,
{index, Value(2u)});
MemoryInfo scalar;
scalar.kind = invalid == 1u ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer;
const auto key = invalid == 1u
? fixture.Emit(ValueOpcode::LoadAddressU32, {list, offset, Value(0u), Value(true)},
fixture.AddMemory(scalar, 0x570u))
: fixture.Emit(ValueOpcode::ReadConstBuffer, {list, offset},
fixture.AddMemory(scalar, 0x570u));
const auto record = fixture.Emit(ValueOpcode::IMul32, {key, Value(stride)});
scalar.kind = ResourceKind::ScalarBuffer;
std::array<Value, 8> image_words;
std::array<Value, 4> sampler_words;
for (uint32_t word = 0; word < 8u; ++word) {
scalar.offset = image_offset + word * 4u;
image_words[word] = compact && word >= 4u ? Value(0u)
: fixture.Emit(ValueOpcode::ReadConstBuffer, {table, record},
fixture.AddMemory(scalar, 0x590u));
if (word < 4u) {
scalar.offset = sampler_offset + word * 4u;
sampler_words[word] = fixture.Emit(ValueOpcode::ReadConstBuffer, {table, record},
fixture.AddMemory(scalar, 0x590u));
}
}
if (invalid == 3u) image_words[7] = Value(1u);
const auto image = fixture.Image(image_words, 0x59cu);
MemoryInfo sample;
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
sample.image_r128 = compact;
const auto sampler = fixture.Sampler(sampler_words, 0x59cu);
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x59cu));
if (invalid == 4u) {
sample.image_r128 = false;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x5a0u));
}
fixture.PlanAndTrack();
const auto &source = fixture.program.descriptor_sources[fixture.program.info.samplers[0].source];
Check(source.indirect_descriptor && source.indirect_descriptor->selector &&
source.indirect_descriptor->selector->stride == 4u &&
source.indirect_descriptor->selector_first.IsEmpty() &&
source.indirect_descriptor->key_count.IsEmpty() &&
source.indirect_descriptor->table_stride == stride &&
source.indirect_descriptor->table_immediate == sampler_offset,
"compact image sampler lost its complete bounded index-list domain");
return ExtractResourcePlan(fixture.program);
};
for (const bool compact : {false, true}) {
auto plan = make_plan(compact);
const uint32_t width = compact ? 4u : 8u;
const auto bytes = 2u * stride + image_offset + width * 4u;
std::array<uint32_t, 8> data{0x1000u, 0u, 8u, 0u, 0x2000u, 0u, bytes, 0u};
struct Memory : LinearTestMemory { uint32_t selector_attempts = 0; } memory;
memory.words.resize((0x1000u + bytes) / 4u, 0xdeadbeefu);
memory.words[0] = 2u;
memory.words[1] = 1u;
for (uint32_t row = 0; row < 3u; ++row) {
auto at = memory.words.begin() + (0x1000u + row * stride + sampler_offset) / 4u;
std::fill_n(at, 4u, 0u);
at[0] = row;
at += 4;
std::fill_n(at, width, 0u);
at[0] = 0x100u + row;
at[1] = static_cast<uint32_t>(F::k32_32_32_32Float) << 20u;
at[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
if (!compact) at[5] = 0x00700000u;
}
memory.fail_address = 0x2000u + bytes; // Last descriptor ends at the readable boundary.
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = [](void *userdata, uint64_t address,
std::span<uint32_t> words) {
auto &memory = *static_cast<Memory *>(userdata);
if (address == memory.base) ++memory.selector_attempts;
return ReadLinearTestMemory(&memory, address, words);
}};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] {
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"paired compact image/sampler table did not materialize");
Check(snapshot.images.size() == 4u && snapshot.samplers.size() == 3u,
"paired table omitted an image, index-list value or OOB zero key");
for (const auto &image : snapshot.images) {
Check(image.dword_count == 8u, "compact image escaped normalized eight-DWORD ABI");
if (compact) Check(std::ranges::all_of(std::span(image.dwords).subspan(4u),
[](uint32_t word) { return word == 0u; }),
"R128 image copied poisoned adjacent record fields");
}
};
materialize();
const auto original = specialization;
memory.words[(0x1000u + stride + image_offset) / 4u] = 0x201u;
materialize();
Check(specialization == original && std::ranges::any_of(snapshot.images,
[](const auto &image) { return image.dwords[0] == 0x201u; }),
"compact descriptor payload refresh changed specialization or stayed stale");
data[2] = 4u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.samplers.size() == 2u,
"index-list count refresh retained a stale sampler candidate");
data[2] = 0u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.samplers.size() == 1u &&
snapshot.samplers[0].dwords[0] == 0u,
"empty index list lost the scalar OOB zero key");
data[2] = 8u;
memory.fail_address = 0x1004u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable selected index-list bytes were accepted");
memory.fail_address = UINT64_MAX;
data[2] = (MaxIndirectDescriptorProbes + 1u) * 4u;
memory.selector_attempts = 0u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.selector_attempts == 0u,
"oversized index list was read before the existing probe cap");
}
for (uint32_t invalid : {1u, 2u, 3u, 4u})
CheckFatal([&] { make_plan(true, invalid); }, "not a valid runtime value",
"compact table accepted an unproved selector or inconsistent descriptor width");
}
void TestScalarBufferSelectorDomain() {
const auto make_plan = [](bool active = true, bool sparse = false, bool uniformize_first = false) {
Fixture fixture;
@@ -7313,6 +7450,7 @@ int main() {
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
Run("image descriptor fields", TestImageDescriptorFields);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("compact scalar image table", TestCompactScalarImageTable);
Run("scalar buffer selector domain", TestScalarBufferSelectorDomain);
Run("private LDS forwarding", TestPrivateLdsForwarding);
Run("counted row image selector", TestCountedRowImageSelector);
+46 -12
View File
@@ -38696,33 +38696,46 @@ void CheckPointerImageTable(VulkanHarness &vulkan) {
void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
using O = ShaderOpcode;
constexpr u32 keys_address = 128, table = 256, stride = 592, objects = 4096;
constexpr u32 keys_address = 128, list_address = 160, table = 256, objects = 4096;
constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u};
constexpr std::array list{2u, 1u, 4u, 0u, 5u, 3u};
for (const bool compact : {false, true})
for (const u32 wave : {32u, 64u}) {
const u32 stride = compact ? 0x330u : 592u;
const u32 table_bytes = (keys.size() - 1u) * stride + 0x98u + 16u;
TestCase test;
test.name = "NestedPointerImageSamplerTable";
test.initial.resize((objects + keys.size() * 0x430u) / 4u, 0);
test.name = compact ? "CompactRecordImageSamplerTable" : "NestedPointerImageSamplerTable";
test.initial.resize((compact ? table + table_bytes : objects + keys.size() * 0x430u) / 4u,
compact ? 0xdeadbeefu : 0u);
std::fill_n(test.initial.begin(), keys.size(), 0xdeadbeefu);
std::fill_n(test.initial.begin() + keys_address / 4u, keys.size(), UINT32_MAX);
if (compact) std::copy(list.begin(), list.end(), test.initial.begin() + list_address / 4u);
for (u32 row = 0; row < keys.size(); ++row) {
const u32 object = objects + row * 0x430u;
test.initial[(table + row * stride) / 4u] = object;
const u32 object = compact ? table + row * stride : objects + row * 0x430u;
if (!compact) test.initial[(table + row * stride) / 4u] = object;
auto image = MakeSampledTextureData(Prospero::BufferFormat::k32Float);
image[0] = 10u + row;
image[1] |= 1u << 30u; // Two texels: zero, then the row's value.
image[3] = DstSel(4, 0, 0, 1) |
(static_cast<u32>(Prospero::ImageType::kColor2D) << 28u);
std::copy_n(image.begin(), 8, test.initial.begin() + (object + 0x3d0u) / 4u);
std::copy_n(image.begin(), compact ? 4u : 8u,
test.initial.begin() + (object + (compact ? 0x98u : 0x3d0u)) / 4u);
const std::array sampler{0u, 0x00fff000u, (row & 1u) ? 0x0a500000u : 0u, 0u};
std::copy(sampler.begin(), sampler.end(),
test.initial.begin() + (object + 0x3f8u) / 4u);
test.initial.begin() + (object + (compact ? 0x88u : 0x3f8u)) / 4u);
}
auto records = MakeStructuredStorageBufferData(stride, keys.size());
auto records = MakeStructuredStorageBufferData(compact ? 0u : stride,
compact ? table_bytes : keys.size());
records[0] = table;
std::copy_n(records.begin(), 4, test.user_data.begin() + 32);
auto key_buffer = MakeStructuredStorageBufferData(0, keys.size() * 4u);
key_buffer[0] = keys_address;
std::copy_n(key_buffer.begin(), 4, test.user_data.begin() + 4);
if (compact) {
auto indices = MakeStructuredStorageBufferData(0, list.size() * 4u);
indices[0] = list_address;
std::copy_n(indices.begin(), 4, test.user_data.begin() + 36);
}
const auto output = MakeStructuredStorageBufferData(0, keys.size() * 4u);
std::copy_n(output.begin(), 4, test.user_data.begin() + 48);
test.bda_mappings = {{0u, 0u}};
@@ -38739,20 +38752,39 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
code.insert(code.end(), {EncodeMubuf0(0x0c), EncodeMubuf1(4, 1, 30),
EncodeVop2(0x16, 6, InlineU32(16), 4)});
AppendVop3(&code, 0x360, 22, Vgpr(6), InlineU32(0));
code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride,
if (compact) {
code.insert(code.end(), {EncodeSop2(0x1e, 23, 22, InlineU32(2)),
EncodeSmem0(0x08, 22, 18), EncodeSmem1(0, 23),
EncodeSop2(0x26, 23, 22, 255), stride,
EncodeSmem0(0x0b, 8, 16), EncodeSmem1(0x88u, 23)});
for (u32 reg = 16u; reg < 20u; ++reg) AppendSMovLiteral(&code, reg, 0xdeadbeefu);
} else code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride,
EncodeSmem0(0x09, 20, 16), EncodeSmem1(0, 23),
EncodeSmem0(0x03, 8, 10), EncodeSmem1(0x3d0, 125),
EncodeSmem0(0x02, 16, 10), EncodeSmem1(0x3f8, 125)});
AppendVMovLiteral(&code, 20, std::bit_cast<u32>(0.5f));
AppendVMovLiteral(&code, 21, std::bit_cast<u32>(0.5f));
code.insert(code.end(), {EncodeMimg0(0x27, 1), EncodeMimg1(5, 20, 2, 4)});
code.insert(code.end(), {EncodeMimg0(0x27, 1, 0, false, 1, compact),
EncodeMimg1(5, 20, compact ? 3u : 2u, compact ? 2u : 4u)});
AppendBufferStoreDword(&code, 5, 30);
AppendEnd(&code);
test.opcodes = {O::S_LSHL_B32, O::V_MOV_B32, O::BUFFER_LOAD_DWORD,
O::V_LSHRREV_B32, O::V_READLANE_B32, O::S_MUL_I32,
O::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4,
O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
if (compact) {
test.opcodes.insert(test.opcodes.end(), {O::S_BUFFER_LOAD_DWORD,
O::S_BUFFER_LOAD_DWORDX8, O::S_MOV_B32});
test.decoded_counts = {{"r128=1", 1}};
} else test.opcodes.insert(test.opcodes.end(),
{O::S_BUFFER_LOAD_DWORDX2, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX4});
const auto compiled = CompileCase(test, vulkan.SubgroupSize());
if (compact) Require(test.name, "physical R128 descriptor boundary",
compiled.resources.images.size() == keys.size() + 1u &&
std::ranges::all_of(compiled.resources.images, [](const auto &image) {
return image.dword_count == 8u &&
std::ranges::all_of(std::span(image.dwords).subspan(4u),
[](u32 word) { return word == 0u; });
}), "compact descriptor copied adjacent poison or omitted the trailing 16-byte image");
for (const auto &value : compiled.resources.buffers) {
ShaderBufferResource descriptor{};
std::copy_n(value.dwords.begin(), 4, descriptor.fields);
@@ -38792,8 +38824,10 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
nullptr, images, samplers);
std::vector<u32> expected;
for (const auto key : keys)
for (auto key : keys) {
if (compact) key = list[key];
expected.push_back(std::bit_cast<u32>(float(10u + key) * ((key & 1u) ? 0.5f : 1.0f)));
}
CompareWords(test, "GPU-selected nested image and sampler", expected,
vulkan.ReadBuffer(test.name, buffer, expected.size()));
vulkan.DestroyBuffer(&buffer);