mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-25 04:16:55 +01:00
shader: replace CPU selector chains with direct image-table lookup
This commit is contained in:
+214
-404
@@ -206,10 +206,7 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
|
||||
}
|
||||
|
||||
std::unique_ptr<Fixture>
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
bool memory_backed_material = false, uint32_t member_offset = 0,
|
||||
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
|
||||
uint32_t table_stride = 32, uint32_t table_offset = 0,
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
|
||||
uint32_t table_shader_offset = 0, bool nested = false) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> material_words;
|
||||
@@ -218,43 +215,14 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
material_words[dword] = fixture->UserData(dword);
|
||||
heap_words[dword] = fixture->UserData(dword + 4u);
|
||||
}
|
||||
if (memory_backed_material) {
|
||||
const auto pointer_address =
|
||||
fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0);
|
||||
MemoryInfo pointer_word;
|
||||
pointer_word.kind = ResourceKind::ScalarAddress;
|
||||
const auto pointer =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{pointer_address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(pointer_word, 0x10b0));
|
||||
const auto address = fixture->Address(pointer, Value(0u), 0x10c0);
|
||||
MemoryInfo descriptor_word;
|
||||
descriptor_word.kind = ResourceKind::ScalarAddress;
|
||||
material_words[0] =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(descriptor_word, 0x10c0));
|
||||
}
|
||||
const auto material = fixture->Buffer(material_words, 0x10d8);
|
||||
const auto heap = fixture->Buffer(heap_words, 0x10d8);
|
||||
if (memory_backed_material) {
|
||||
MemoryInfo shared_buffer;
|
||||
shared_buffer.kind = ResourceKind::Buffer;
|
||||
const auto load =
|
||||
fixture->Emit(ValueOpcode::LoadBufferU32,
|
||||
{material, Value(0u), Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(shared_buffer, 0x10d8));
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {load});
|
||||
}
|
||||
const auto invocation = fixture->Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
|
||||
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
|
||||
{invocation, Value(true)});
|
||||
const auto record =
|
||||
fixture->Emit(ValueOpcode::IMul32, {selector, Value(material_stride)});
|
||||
auto member = member_offset == 0 ? record :
|
||||
fixture->Emit(ValueOpcode::IAdd32, {record, Value(member_offset)});
|
||||
Value member;
|
||||
if (nested) {
|
||||
std::array<Value, 4> root_words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) root_words[word] = fixture->UserData(12u + word);
|
||||
@@ -266,16 +234,16 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
fixture->AddMemory(root_memory, 0x1000));
|
||||
const auto masked = fixture->Emit(ValueOpcode::BitwiseAnd32, {pointer, Value(0xfffffff1u)});
|
||||
member = fixture->Emit(ValueOpcode::IAdd32, {masked, Value(16u)});
|
||||
} else {
|
||||
member = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
|
||||
}
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {member});
|
||||
MemoryInfo material_scalar;
|
||||
material_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
material_scalar.offset = material_immediate;
|
||||
auto key =
|
||||
material_scalar.offset = 4u;
|
||||
const auto key =
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
|
||||
fixture->AddMemory(material_scalar, 0x10d8));
|
||||
if (selector_bits != UINT32_MAX)
|
||||
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
|
||||
auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
if (table_shader_offset != 0u)
|
||||
heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)});
|
||||
@@ -312,384 +280,225 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
}
|
||||
|
||||
void TestInvariantIndirectImageMaterialization() {
|
||||
auto fixture = MakeIndirectImageFixture(false);
|
||||
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto resource_plan = ExtractResourcePlan(fixture->program);
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
EliminateDeadCode(fixture->program.blocks);
|
||||
ValidateProgram(fixture->program, true);
|
||||
|
||||
Check(fixture->program.info.buffers.size() == 1 &&
|
||||
fixture->program.info.images.size() == 1 &&
|
||||
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
|
||||
}),
|
||||
"indirect image key was not retained as a scalar-buffer read");
|
||||
const auto source = fixture->program.info.images[0].source;
|
||||
Check(source < fixture->program.descriptor_sources.size() &&
|
||||
fixture->program.descriptor_sources[source]
|
||||
.indirect_descriptor.has_value(),
|
||||
"indirect image source was not retained for runtime proof");
|
||||
const auto image_handle =
|
||||
std::ranges::find_if(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
Check(image_handle != fixture->block->end() &&
|
||||
image_handle->Arg(0).ResolveInstruction() != nullptr &&
|
||||
image_handle->Arg(0).ResolveInstruction()->GetOpcode() ==
|
||||
ValueOpcode::ReadConstBuffer,
|
||||
"indirect image handle discarded the live material key");
|
||||
|
||||
std::array<uint32_t, 9> user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u,
|
||||
16u << 16u, 4u, 0u, 7u};
|
||||
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
||||
Check(!source.selector && fixture->program.info.buffers.size() == 3u,
|
||||
"bounded image table retained CPU selector traversal or lost live loads");
|
||||
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);
|
||||
std::array<uint32_t, 16> data{0x3000u, 16u << 16u, 0x200000u, 0u,
|
||||
0x2000u, 48u << 16u, 3u, 0u};
|
||||
std::copy_n(data.begin(), 4u, data.begin() + 12u);
|
||||
LinearTestMemory memory;
|
||||
std::array<uint32_t, 8> image_descriptor{};
|
||||
image_descriptor[0] = 0x20u;
|
||||
image_descriptor[1] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
|
||||
<< 20u;
|
||||
image_descriptor[2] = 3u | (3u << 14u);
|
||||
image_descriptor[3] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
|
||||
<< 28u);
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
|
||||
SrtRuntime runtime{.user_data = user_data,
|
||||
.userdata = &memory,
|
||||
memory.fail_address = 0x3000u; // GPU-only material backing must never be read on the CPU.
|
||||
memory.watched_address = 0x2000u;
|
||||
const auto put = [&](uint32_t offset, uint32_t identity) {
|
||||
auto value = descriptor;
|
||||
value[0] += identity;
|
||||
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1000u + offset) / 4u);
|
||||
};
|
||||
put(16u, 0u);
|
||||
put(64u, 1u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"invariant indirect image table did not materialize");
|
||||
|
||||
user_data[5] = 0u;
|
||||
user_data[6] = 19u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u && memory.watched_reads == 1u &&
|
||||
memory.watched_dwords == 36u && memory.reads == 1u,
|
||||
"image table was not read once independently of its GPU selector chain");
|
||||
const auto root = specialization.images[0];
|
||||
const auto mapping = root.indirect_mapping_offset;
|
||||
Check(root.indirect_search_iterations == 0u && snapshot.flattened_srt[mapping] == 17u &&
|
||||
snapshot.flattened_srt[mapping + 1u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 5u] == 1u &&
|
||||
snapshot.flattened_srt[mapping + 17u] == 2u &&
|
||||
snapshot.specialization_reads == std::vector<std::pair<uint64_t, uint64_t>>{{0x2000u, 144u}},
|
||||
"dense image mapping lost its null slot, trimmed length or coherent read dependency");
|
||||
const auto original_specialization = specialization;
|
||||
std::fill_n(memory.words.begin() + 0x1040u / 4u, 8u, 0u);
|
||||
put(112u, 1u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == original_specialization && memory.watched_reads == 2u &&
|
||||
snapshot.flattened_srt[mapping] == 29u,
|
||||
"moving a table entry changed shader specialization or reused stale mapping bytes");
|
||||
put(112u, 7u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images[2].dwords[0] == descriptor[0] + 7u,
|
||||
"changed table descriptor was not refreshed");
|
||||
// Scalar reads align each DWORD and return zero beyond the V# byte size.
|
||||
data[5] = 0u;
|
||||
data[6] = 19u;
|
||||
std::fill(memory.words.begin() + 0x1000u / 4u, memory.words.end(), 0u);
|
||||
put(0u, 0u);
|
||||
memory.fail_address = 0x2010u;
|
||||
memory.watched_address = 0x2000u;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u && snapshot.images.size() == 2u &&
|
||||
snapshot.images[1].dwords == descriptor,
|
||||
"partial descriptor prefix did not zero its out-of-bounds DWORDs");
|
||||
memory.fail_address = 0x2008u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization),
|
||||
"unreadable memory inside the descriptor prefix was accepted");
|
||||
user_data[5] = 16u << 16u;
|
||||
user_data[6] = 4u;
|
||||
memory.watched_address = UINT64_MAX;
|
||||
|
||||
memory.fail_address = 0x1004u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot,
|
||||
specialization),
|
||||
"unreadable material-table selector was accepted");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unreadable in-range table bytes were accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] = 0u;
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] = 0u;
|
||||
for (uint32_t size : {0u, 3u, 4u}) {
|
||||
data[6] = size;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage &&
|
||||
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"empty image table did not specialize as a safe null image");
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 3u] =
|
||||
image_descriptor[3] ^ (1u << 28u);
|
||||
ResourceSnapshot null_snapshot;
|
||||
ResourceSpecialization null_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, null_snapshot,
|
||||
null_specialization) &&
|
||||
std::ranges::all_of(null_snapshot.images[0].dwords,
|
||||
[](uint32_t dword) { return dword == 0u; }),
|
||||
"stale typed null image descriptors were not canonicalized");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
// The GPU computes these offsets; host materialization must not enumerate the key.
|
||||
for (const auto [shader, immediate, key, expected] :
|
||||
{std::array{16u, 0u, 0x55555555u, 0u},
|
||||
std::array{0u, 16u, 0x55555555u, UINT32_MAX},
|
||||
std::array{0xfffffff0u, 32u, 0u, UINT32_MAX}}) {
|
||||
auto wrap = MakeIndirectImageFixture(false, 48u, immediate, shader);
|
||||
wrap->PlanAndTrack();
|
||||
for (auto &inst : *wrap->block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
inst.ReplaceUsesWith(Value(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
SrtWalker walker(wrap->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
ResourceSnapshot dynamic_snapshot;
|
||||
ResourceSpecialization dynamic_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot,
|
||||
dynamic_specialization) &&
|
||||
dynamic_snapshot.images.size() == 2 &&
|
||||
dynamic_specialization.images.size() == 2,
|
||||
"dynamic indirect image table did not materialize");
|
||||
const auto second_image = (0x2020u - memory.base) / 4u;
|
||||
memory.words[second_image + 1u] |= 3u << 30u;
|
||||
memory.words[second_image + 2u] = 3u << 14u;
|
||||
memory.words[second_image + 3u] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
|
||||
memory.words[second_image + 4u] = 11u;
|
||||
ResourceSnapshot mixed_snapshot;
|
||||
ResourceSpecialization mixed_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization) &&
|
||||
mixed_snapshot.images.size() == 2 &&
|
||||
mixed_specialization.images.size() == 2 &&
|
||||
mixed_specialization.images[0].dimension ==
|
||||
Decoder::ImageDimension::Dim2D &&
|
||||
!mixed_specialization.images[0].cube &&
|
||||
mixed_specialization.images[1].dimension ==
|
||||
Decoder::ImageDimension::Dim2DArray &&
|
||||
mixed_specialization.images[1].cube &&
|
||||
std::equal(mixed_snapshot.images[1].dwords.begin(),
|
||||
mixed_snapshot.images[1].dwords.end(),
|
||||
memory.words.begin() + second_image),
|
||||
"mixed 2D and cube candidates were rejected or discarded");
|
||||
memory.words[second_image + 1u] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
|
||||
<< 20u |
|
||||
(3u << 30u);
|
||||
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization),
|
||||
"indirect images with different numeric classes were accepted");
|
||||
for (const auto dword : {1u, 2u, 3u, 4u}) {
|
||||
memory.words[second_image + dword] = image_descriptor[dword];
|
||||
}
|
||||
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
|
||||
Check(fixture->program.info.images.size() == 2 &&
|
||||
fixture->program.info.images[0].indirect_root == 0 &&
|
||||
fixture->program.info.images[0].indirect_search_iterations != 0 &&
|
||||
fixture->program.info.images[0].indirect_resources.size() == 2 &&
|
||||
dynamic_snapshot.images.size() == 2,
|
||||
"dynamic indirect image table was not specialized transactionally");
|
||||
const auto &mapping = dynamic_specialization.images[0];
|
||||
const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset];
|
||||
Check(mapping.indirect_search_iterations == std::bit_width(key_count) &&
|
||||
mapping.indirect_mapping_offset + 1u + key_count * 2u ==
|
||||
dynamic_snapshot.flattened_srt.size(),
|
||||
"indirect image mapping retained worst-case padding");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] += 0x101u;
|
||||
ResourceSnapshot rebound_snapshot;
|
||||
ResourceSpecialization rebound_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization == dynamic_specialization,
|
||||
"stable indirect key mapping did not accept changed image addresses");
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u];
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != dynamic_specialization,
|
||||
"collapsed indirect candidates did not select a new specialization");
|
||||
const auto collapsed_specialization = rebound_specialization;
|
||||
ResourceSnapshot capacity_snapshot;
|
||||
ResourceSpecialization capacity_specialization;
|
||||
for (const uint32_t records : {1u, 3u}) {
|
||||
user_data[2] = records;
|
||||
Check(MaterializeResources(resource_plan, runtime, capacity_snapshot,
|
||||
capacity_specialization),
|
||||
"runtime indirect key mapping rejected a valid material-table size");
|
||||
}
|
||||
user_data[2] = 2u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 1u;
|
||||
memory.words[(0x2040u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 2u;
|
||||
for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2040u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != collapsed_specialization,
|
||||
"larger indirect candidate topology reused the old specialization");
|
||||
|
||||
auto memory_backed = MakeIndirectImageFixture(false, 4u, true);
|
||||
memory_backed->PlanAndTrack();
|
||||
auto memory_backed_plan = ExtractResourcePlan(memory_backed->program);
|
||||
EliminateDeadCode(memory_backed->program.blocks);
|
||||
std::array<uint32_t, 11> memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u,
|
||||
0x2000u, 16u << 16u, 4u, 0u,
|
||||
7u, 0x3100u, 0u};
|
||||
memory.words[(0x3100u - memory.base) / 4u] = 0x3000u;
|
||||
memory.words[(0x3000u - memory.base) / 4u] = 0x1000u;
|
||||
memory.fail_address = 0x3100u;
|
||||
SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory =
|
||||
ReadLinearTestMemory};
|
||||
Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization),
|
||||
"unreadable indirect table descriptor was accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.watched_address = 0x3100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u,
|
||||
"descriptor, flattened SRT and indirect-table roots repeated a clean pointer read");
|
||||
const auto* buffer_storage = snapshot.buffers.data();
|
||||
const auto* image_storage = snapshot.images.data();
|
||||
const auto* flat_storage = snapshot.flattened_srt.data();
|
||||
const auto* user_data_storage = snapshot.user_data.data();
|
||||
const auto* buffer_specialization_storage = specialization.buffers.data();
|
||||
const auto* image_specialization_storage = specialization.images.data();
|
||||
const auto old_address = snapshot.images[0].dwords[0];
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u &&
|
||||
snapshot.images[0].dwords[0] == old_address + 0x100u,
|
||||
"cache refresh reused stale table contents or repeated its clean pointer read");
|
||||
Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage &&
|
||||
snapshot.flattened_srt.data() == flat_storage &&
|
||||
snapshot.user_data.data() == user_data_storage &&
|
||||
specialization.buffers.data() == buffer_specialization_storage &&
|
||||
specialization.images.data() == image_specialization_storage,
|
||||
"a same-shape refresh discarded the runtime output storage");
|
||||
|
||||
auto malformed = MakeIndirectImageFixture(true);
|
||||
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"malformed indirect image pattern was accepted");
|
||||
Check(!malformed->program.resource_tracking_complete &&
|
||||
malformed->program.info.images.empty() &&
|
||||
malformed->program.descriptor_sources.empty(),
|
||||
"malformed indirect image pattern was partially accepted");
|
||||
"noncontiguous descriptor DWORDs entered the table plan");
|
||||
Check(!malformed->program.resource_tracking_complete && malformed->program.descriptor_sources.empty(),
|
||||
"malformed image table tracking was not transactional");
|
||||
}
|
||||
|
||||
auto negative_immediate = MakeIndirectImageFixture(false, 0xfffffffcu);
|
||||
CheckFatal([&] { negative_immediate->PlanAndTrack(); },
|
||||
"not a valid runtime value",
|
||||
"negative scalar immediate entered the indirect image proof");
|
||||
|
||||
for (const auto [immediate, member, first, stride] :
|
||||
{std::array{0u, 4u, 4u, 224u}, std::array{4u, 4u, 8u, 224u},
|
||||
std::array{36u, 0u, 36u, 224u}, std::array{1u, 3u, 0u, 224u},
|
||||
std::array{1u, 3u, 0u, 1u}, std::array{1u, 3u, 0u, 2u}}) {
|
||||
auto split_offset = MakeIndirectImageFixture(false, immediate, false, member, stride);
|
||||
split_offset->PlanAndTrack();
|
||||
const auto split_plan = ExtractResourcePlan(split_offset->program);
|
||||
LinearTestMemory split_memory;
|
||||
std::copy(image_descriptor.begin(), image_descriptor.end(),
|
||||
split_memory.words.begin() + 0x1000u / 4u);
|
||||
split_memory.fail_address = first == 36u ? 0x1004u : UINT64_MAX;
|
||||
split_memory.watched_address = split_memory.base + first;
|
||||
user_data[1] = stride << 16u;
|
||||
user_data[2] = stride < 4u ? 8u / stride : 1u;
|
||||
SrtRuntime split_runtime{.user_data = user_data,
|
||||
.userdata = &split_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(split_plan, split_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
snapshot.images[0].dwords == image_descriptor &&
|
||||
split_memory.watched_reads == 1u,
|
||||
"indirect scalar offsets did not align and bound their components independently");
|
||||
void TestWaterfallImageTable() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
const auto make = [](bool equality, bool scalar) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
auto *entry = fixture->block;
|
||||
auto *body = fixture->AddBlock();
|
||||
auto *exit = fixture->AddBlock();
|
||||
entry->AddBranch(body);
|
||||
entry->AddBranch(exit);
|
||||
body->AddBranch(exit);
|
||||
std::array<Value, 4> words;
|
||||
for (uint32_t i = 0; i < 4u; ++i) words[i] = fixture->UserData(i);
|
||||
const auto table = fixture->Buffer(words);
|
||||
const auto lane_value = fixture->Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)});
|
||||
const auto active = fixture->Emit(ValueOpcode::INotEqual32, {lane_value, Value(0u)});
|
||||
const auto index = fixture->UserData(4u);
|
||||
std::array<Value, 8> image_words;
|
||||
auto guard = active;
|
||||
for (uint32_t group = 0; group < 2u; ++group) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Buffer;
|
||||
memory.idxen = true;
|
||||
memory.data_dwords = 4u;
|
||||
memory.offset = 16u + group * 16u;
|
||||
const auto loaded = fixture->Emit(ValueOpcode::LoadBufferU32x4,
|
||||
{table, index, Value(0u), Value(0u), active}, fixture->AddMemory(memory, 0x850u));
|
||||
for (uint32_t component = 0; component < 4u; ++component) {
|
||||
const auto word = group * 4u + component;
|
||||
const auto local = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{active, fixture->Emit(ValueOpcode::CompositeExtractU32x4,
|
||||
{loaded, Value(component)}), Value(0xdeadu)});
|
||||
image_words[word] = fixture->Emit(ValueOpcode::ReadLane, {local, Value(0u)});
|
||||
if (equality || word != 7u)
|
||||
guard = fixture->Emit(ValueOpcode::LogicalAnd,
|
||||
{guard, fixture->Emit(ValueOpcode::IEqual32, {image_words[word], local})});
|
||||
}
|
||||
}
|
||||
fixture->program.block_info[0].condition = guard;
|
||||
fixture->program.block_info[0].terminator = {
|
||||
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u};
|
||||
const auto sample = [&](Value image, Block *block) {
|
||||
const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
|
||||
fixture->Emit(ValueOpcode::ReferenceU32,
|
||||
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
|
||||
};
|
||||
if (scalar) {
|
||||
const auto key = fixture->Emit(ValueOpcode::ReadFirstLane, {lane_value, active});
|
||||
const auto offset = fixture->Emit(ValueOpcode::IAdd32,
|
||||
{fixture->Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(16u)});
|
||||
std::array<Value, 8> scalar_words;
|
||||
for (uint32_t word = 0; word < 8u; ++word) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarBuffer;
|
||||
memory.offset = word * 4u;
|
||||
scalar_words[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
|
||||
fixture->AddMemory(memory, 0x7bcu));
|
||||
}
|
||||
sample(fixture->Image(scalar_words), entry);
|
||||
}
|
||||
const auto image = fixture->Emit(ValueOpcode::GetImageResource,
|
||||
{image_words[0], image_words[1], image_words[2], image_words[3],
|
||||
image_words[4], image_words[5], image_words[6], image_words[7]}, 0, body);
|
||||
sample(image, body);
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make(true, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
Check(plan.info.images.size() == 1u &&
|
||||
plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor->table_record_bytes == 48u,
|
||||
"scalar and waterfall uses did not share their native image table");
|
||||
std::array<uint32_t, 5> data{0x2000u, 48u << 16u, 2u, 0x5204u, 1u};
|
||||
LinearTestMemory memory;
|
||||
const std::array<uint32_t, 8> descriptor{0x20u, 75u << 20u, 3u | (3u << 14u),
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1u &&
|
||||
snapshot.images.size() == 2u,
|
||||
"shared scalar/waterfall table repeated its coherent bulk read");
|
||||
const auto handle = std::ranges::find_if(*fixture->program.blocks[1], [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
for (const auto [index, format, expected] :
|
||||
{std::array{1u, 0x5204u, 64u}, std::array{2u, 0x5204u, UINT32_MAX},
|
||||
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
|
||||
data[4] = index;
|
||||
data[3] = format;
|
||||
SrtWalker walker(fixture->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"VMEM table projection lost record bounds, format disable or multiplication carry");
|
||||
}
|
||||
|
||||
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
|
||||
// each 64-byte material record; the image occupies bytes 16..47 of its record.
|
||||
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
|
||||
packed->PlanAndTrack();
|
||||
const auto packed_plan = ExtractResourcePlan(packed->program);
|
||||
LinearTestMemory packed_memory;
|
||||
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
|
||||
0x2000u, 48u << 16u, 2u, 0u};
|
||||
for (uint32_t i = 0; i < 3u; ++i)
|
||||
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
auto descriptor = image_descriptor;
|
||||
descriptor[0] += i;
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
|
||||
}
|
||||
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
|
||||
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
|
||||
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
|
||||
"packed material flags changed the table key or 48-byte descriptor addressing");
|
||||
packed_memory.words[60u / 4u] = 0xffff8002u;
|
||||
packed_memory.fail_address = 0x2070u;
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u &&
|
||||
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"masked out-of-range descriptor did not return zero without reading past table bounds");
|
||||
packed_memory.fail_address = 0x205cu;
|
||||
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
|
||||
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
|
||||
// 0x55555555 * 48 is 0xfffffff0: shader +16 wraps, scalar +16 does not.
|
||||
for (const auto [shader_offset, immediate, key, image_count] :
|
||||
{std::array{0u, 16u, 0x55555555u, 1u}, std::array{16u, 0u, 0x55555555u, 2u},
|
||||
std::array{0xfffffff0u, 32u, 0u, 1u}}) {
|
||||
auto wrapping = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
|
||||
UINT32_MAX, 48u, immediate, shader_offset);
|
||||
wrapping->PlanAndTrack();
|
||||
const auto wrapping_plan = ExtractResourcePlan(wrapping->program);
|
||||
LinearTestMemory wrapping_memory;
|
||||
wrapping_memory.words[60u / 4u] = key;
|
||||
std::copy(image_descriptor.begin(), image_descriptor.end(),
|
||||
wrapping_memory.words.begin() + 0x1000u / 4u);
|
||||
std::array<uint32_t, 8> data{0x1000u, 64u << 16u, 1u, 0u,
|
||||
0x2000u, 48u << 16u, 1u, 0u};
|
||||
SrtRuntime wrapped_runtime{.user_data = data, .userdata = &wrapping_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(wrapping_plan, wrapped_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == image_count &&
|
||||
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }) &&
|
||||
(image_count == 1u || snapshot.images[1].dwords == image_descriptor),
|
||||
"descriptor addressing conflated shader U32 wrap with scalar immediate addition");
|
||||
}
|
||||
|
||||
auto nested = MakeIndirectImageFixture(false, 0u, false, 0u, 1u,
|
||||
UINT32_MAX, 48u, 0u, 16u, true);
|
||||
nested->PlanAndTrack();
|
||||
const auto nested_plan = ExtractResourcePlan(nested->program);
|
||||
const auto &chain = *nested_plan.descriptor_sources[nested_plan.info.images[0].source].indirect_descriptor;
|
||||
Check(chain.selectors.size() == 2u && chain.selectors[0].stride == 32u &&
|
||||
chain.selectors[0].bits == 0xfffffff1u && chain.selectors[1].offset == 16u &&
|
||||
nested->program.info.buffers.size() == 3u,
|
||||
"nested material selectors lost their ordered reads or live descriptor field");
|
||||
LinearTestMemory nested_memory;
|
||||
nested_memory.words[0] = 0u;
|
||||
nested_memory.words[32u / 4u] = 1u;
|
||||
nested_memory.words[64u / 4u] = 0xffffffffu;
|
||||
nested_memory.words[0x2000u / 4u] = 0u;
|
||||
nested_memory.words[0x2010u / 4u] = 1u;
|
||||
nested_memory.fail_address = 0x3020u;
|
||||
nested_memory.watched_address = 0x3010u;
|
||||
for (uint32_t key = 0; key < 2u; ++key) {
|
||||
auto descriptor = image_descriptor;
|
||||
descriptor[0] += key;
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
nested_memory.words.begin() + (0x1010u + key * 48u) / 4u);
|
||||
}
|
||||
std::array<uint32_t, 16> nested_data{0x3000u, 16u << 16u, 0x200000u, 0u,
|
||||
0x2000u, 48u << 16u, 2u, 0u,
|
||||
0, 0, 0, 0, 0x1000u, 32u << 16u, 3u, 0u};
|
||||
SrtRuntime nested_runtime{.user_data = nested_data, .userdata = &nested_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(nested_plan, nested_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
|
||||
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
|
||||
nested_memory.watched_reads == 1u,
|
||||
"nested selector traversal scanned unrelated material fields or repeated aliased offsets");
|
||||
|
||||
auto disabled = make(true, false);
|
||||
disabled->PlanAndTrack();
|
||||
const auto disabled_plan = ExtractResourcePlan(disabled->program);
|
||||
data[3] = 0u;
|
||||
memory.fail_address = 0x2000u;
|
||||
Check(MaterializeResources(disabled_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
|
||||
"disabled VMEM descriptor table read its unavailable backing");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"VMEM format disable incorrectly disabled a shared scalar descriptor read");
|
||||
auto malformed = make(false, false);
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"waterfall without all descriptor equality witnesses was accepted");
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
@@ -780,7 +589,7 @@ void TestGuardedDirectImageTable() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->selectors.empty() &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 344u &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
indirect->key_count.Resolve().U32() == 32u &&
|
||||
@@ -1078,7 +887,7 @@ void TestBoundedComputeImageLoop() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->selectors.empty() &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 0x6b0u &&
|
||||
indirect->key_count.Resolve() == count.Resolve(),
|
||||
"bounded compute loop lost its runtime image count");
|
||||
@@ -1433,8 +1242,8 @@ void TestUniformizedMaterialImageKeys() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->selectors.front().stride == 0x90u &&
|
||||
indirect->selectors.front().offset == 0xc00u &&
|
||||
Check(indirect && indirect->selector->stride == 0x90u &&
|
||||
indirect->selector->offset == 0xc00u &&
|
||||
indirect->table_offset == 0x20e0u &&
|
||||
!indirect->selector_mask.IsEmpty() &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
@@ -3708,6 +3517,7 @@ int main() {
|
||||
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
|
||||
Run("dynamic storage mips", TestDynamicStorageMipTracking);
|
||||
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
|
||||
Run("waterfall image table", TestWaterfallImageTable);
|
||||
Run("guarded direct image table", TestGuardedDirectImageTable);
|
||||
Run("bounded compute image loop", TestBoundedComputeImageLoop);
|
||||
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
|
||||
|
||||
Reference in New Issue
Block a user