mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
shader: retain runtime count bounds for finite descriptor choices
This commit is contained in:
+102
-11
@@ -986,19 +986,35 @@ void TestIndexedPointerImageTable() {
|
||||
enum class Case { Valid, ReverseSigned, MissingParent, WrongParent, MissingChild,
|
||||
InclusiveChild, MismatchedPair, DifferentLaneMask, ExtraUse,
|
||||
UnboundedChoice, MissingOuterWitness, WrongOuterWitness, CyclicChoice,
|
||||
ExcessiveDomain };
|
||||
ExcessiveDomain, Counted, MissingCount, DifferentCount, MaskedCount, UnprovenMask };
|
||||
const auto make = [](Case variant = Case::Valid) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> table_words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) table_words[word] = fixture->UserData(word);
|
||||
const auto table = fixture->Buffer(table_words);
|
||||
const auto lane = fixture->Emit(ValueOpcode::LaneId);
|
||||
const auto choice = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(8u)});
|
||||
const auto valid = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(16u)});
|
||||
auto row = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{valid, fixture->Emit(ValueOpcode::SelectU32,
|
||||
{choice, Value(0u), variant == Case::UnboundedChoice ? lane : Value(1u)}),
|
||||
Value(UINT32_MAX)});
|
||||
const bool counted = variant >= Case::Counted;
|
||||
Value choice, row, count, other_count;
|
||||
if (counted) {
|
||||
const auto address = fixture->Address(fixture->UserData(4), fixture->UserData(5));
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarAddress;
|
||||
count = fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(memory, 0x8u));
|
||||
if (variant == Case::DifferentCount) {
|
||||
memory.offset = 4u;
|
||||
other_count = fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(memory, 0x10u));
|
||||
}
|
||||
choice = fixture->Emit(ValueOpcode::ULessThan32, {Value(0u), count});
|
||||
} else {
|
||||
choice = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(8u)});
|
||||
const auto valid = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(16u)});
|
||||
row = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{valid, fixture->Emit(ValueOpcode::SelectU32,
|
||||
{choice, Value(0u), variant == Case::UnboundedChoice ? lane : Value(1u)}),
|
||||
Value(UINT32_MAX)});
|
||||
}
|
||||
auto* entry = fixture->block;
|
||||
auto* left = fixture->AddBlock();
|
||||
auto* right = fixture->AddBlock();
|
||||
@@ -1012,22 +1028,44 @@ void TestIndexedPointerImageTable() {
|
||||
from->AddBranch(merge);
|
||||
from->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge};
|
||||
}
|
||||
if (counted) {
|
||||
fixture->block = left;
|
||||
const auto selector = fixture->Emit(ValueOpcode::BitwiseAnd32, {lane, Value(3u)});
|
||||
row = Value(UINT32_MAX);
|
||||
for (uint32_t key = 4u; key-- > 0u;) {
|
||||
auto active = fixture->Emit(ValueOpcode::IEqual32, {selector, Value(key)});
|
||||
// Row zero uses the uniform CFG-entry bound, like the captured shader.
|
||||
if (key != 0u && !(variant == Case::MissingCount && key == 2u)) {
|
||||
const auto bound = variant == Case::DifferentCount && key == 2u ? other_count : count;
|
||||
active = fixture->Emit(ValueOpcode::LogicalAnd,
|
||||
{active, fixture->Emit(ValueOpcode::ULessThan32, {Value(key), bound})});
|
||||
}
|
||||
row = fixture->Emit(ValueOpcode::SelectU32, {active, Value(key), row});
|
||||
}
|
||||
}
|
||||
fixture->block = merge;
|
||||
auto& phi = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
phi.AddPhiOperand(left, row);
|
||||
phi.AddPhiOperand(right, row);
|
||||
phi.AddPhiOperand(right, counted ? Value(UINT32_MAX) : row);
|
||||
row = Value(&phi);
|
||||
if (variant == Case::CyclicChoice) {
|
||||
merge->AddBranch(merge);
|
||||
merge->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge};
|
||||
phi.AddPhiOperand(merge, row);
|
||||
} else merge->terminator.kind = CFG::TerminatorKind::Return;
|
||||
const auto parent = variant == Case::MissingParent ? Value(true)
|
||||
Value selected_mask;
|
||||
if (variant == Case::MaskedCount || variant == Case::UnprovenMask) {
|
||||
selected_mask = fixture->Emit(ValueOpcode::ULessThan32, {lane, Value(2u)});
|
||||
row = fixture->Emit(ValueOpcode::SelectU32, {selected_mask, row, Value(0u)});
|
||||
}
|
||||
auto parent = variant == Case::MissingParent ? Value(true)
|
||||
: variant == Case::ReverseSigned
|
||||
? fixture->Emit(ValueOpcode::SGreaterThanEqual32, {row, Value(0u)})
|
||||
: fixture->Emit(variant == Case::WrongParent ? ValueOpcode::ULessThanEqual32
|
||||
: ValueOpcode::SLessThanEqual32,
|
||||
{Value(0u), row});
|
||||
if (variant == Case::MaskedCount)
|
||||
parent = fixture->Emit(ValueOpcode::LogicalAnd, {selected_mask, parent});
|
||||
MemoryInfo pointer_memory;
|
||||
pointer_memory.kind = ResourceKind::Buffer;
|
||||
pointer_memory.idxen = true;
|
||||
@@ -1096,7 +1134,7 @@ void TestIndexedPointerImageTable() {
|
||||
constexpr uint32_t table = 0x2000u, objects = 0x10000u, row_bytes = 1536u;
|
||||
LinearTestMemory memory;
|
||||
memory.base += high;
|
||||
memory.words.resize((objects + 2u * row_bytes - 0x1000u) / 4u, 0xdeadbeefu);
|
||||
memory.words.resize((objects + 4u * row_bytes - 0x1000u) / 4u, 0xdeadbeefu);
|
||||
const auto at = [&](uint32_t address) { return memory.words.begin() + (address - 0x1000u) / 4u; };
|
||||
const auto image_words = [](uint32_t id) {
|
||||
return std::array<uint32_t, 8>{id,
|
||||
@@ -1105,7 +1143,7 @@ void TestIndexedPointerImageTable() {
|
||||
};
|
||||
// Runtime stride differs from the native sampler's 136-byte table arithmetic.
|
||||
constexpr uint32_t stride = 160u;
|
||||
for (uint32_t row = 0; row < 2u; ++row) {
|
||||
for (uint32_t row = 0; row < 4u; ++row) {
|
||||
*at(table + row * stride) = objects + row * row_bytes;
|
||||
*at(table + row * stride + 4u) = 0xbeef0001u;
|
||||
for (uint32_t child = 0; child < 3u; ++child) {
|
||||
@@ -1197,6 +1235,59 @@ void TestIndexedPointerImageTable() {
|
||||
CheckFatal([&] { invalid->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"nested table accepted an unproved domain, mismatched pointer pair or unguarded use");
|
||||
}
|
||||
|
||||
auto counted = make(Case::Counted);
|
||||
counted->PlanAndTrack();
|
||||
const auto counted_plan = ExtractResourcePlan(counted->program);
|
||||
constexpr uint32_t count_address = 0x1800u;
|
||||
std::array<uint32_t, 6> counted_data{table, 1u | (stride << 16u), 2u, 0x5204u, count_address, 1u};
|
||||
const SrtRuntime counted_runtime{.user_data = counted_data, .read_memory = ReadLinearTestMemory, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
*at(table) = objects;
|
||||
*at(table + 4u) = 0xbeef0001u;
|
||||
*at(count_address) = *at(count_address + 4u) = 2u;
|
||||
Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && snapshot.images.size() == 7u,
|
||||
"finite parent choices lost their runtime count and enumerated unavailable rows");
|
||||
const auto& counted_indirect = *counted_plan.descriptor_sources[counted_plan.info.images[0].source].indirect_descriptor;
|
||||
Check(counted_indirect.table_maximum == 3u && !counted_indirect.key_count.IsEmpty() &&
|
||||
captured(high + count_address, 4u),
|
||||
"finite parent domain lost its static maximum or captured runtime count");
|
||||
auto masked = make(Case::MaskedCount);
|
||||
masked->PlanAndTrack();
|
||||
const auto masked_plan = ExtractResourcePlan(masked->program);
|
||||
Check(!masked_plan.descriptor_sources[masked_plan.info.images[0].source].indirect_descriptor->key_count.IsEmpty() &&
|
||||
MaterializeResources(masked_plan, counted_runtime, snapshot, specialization) && snapshot.images.size() == 7u,
|
||||
"inactive preserved selector value discarded the selected lane's runtime count");
|
||||
memory.watched_address = high + table;
|
||||
for (const uint32_t count : {0u, 1u, 2u, 4u, 5u}) {
|
||||
*at(count_address) = count;
|
||||
counted_data[2] = std::min(count, 4u);
|
||||
memory.watched_reads = 0u;
|
||||
Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u + 3u * std::min(count, 4u) &&
|
||||
(count != 0u || memory.watched_reads == 0u),
|
||||
"finite parent count refresh lost candidates, ignored its static cap or read an empty domain");
|
||||
}
|
||||
counted_data[2] = 2u;
|
||||
*at(count_address) = 3u;
|
||||
memory.watched_reads = 0u;
|
||||
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
|
||||
"runtime parent count exceeding V# records was clamped or read candidates");
|
||||
*at(count_address) = 2u;
|
||||
memory.fail_address = high + count_address;
|
||||
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
|
||||
"unreadable parent count was ignored before candidate reads");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
for (const auto variant : {Case::MissingCount, Case::DifferentCount, Case::UnprovenMask}) {
|
||||
auto unproved = make(variant);
|
||||
unproved->PlanAndTrack();
|
||||
const auto unproved_plan = ExtractResourcePlan(unproved->program);
|
||||
const auto& source = unproved_plan.descriptor_sources[unproved_plan.info.images[0].source];
|
||||
memory.watched_reads = 0u;
|
||||
Check(source.indirect_descriptor->key_count.IsEmpty() &&
|
||||
!MaterializeResources(unproved_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
|
||||
"finite choice borrowed a missing or different leaf's count or pruned an unproven inactive arm");
|
||||
}
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
|
||||
@@ -39014,19 +39014,22 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
constexpr u32 list_address = 160, objects = 4096;
|
||||
constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u};
|
||||
constexpr std::array list{2u, 1u, 4u, 0u, 5u, 3u};
|
||||
for (const u32 member : {0u, 0x98u, 0x16cu, 64u})
|
||||
for (const auto [member, counted] : {std::pair{0u, false}, std::pair{0x98u, false},
|
||||
std::pair{0x16cu, false}, std::pair{64u, false}, std::pair{64u, true}})
|
||||
for (const auto [wave, upper_only] : {std::pair{32u, false}, std::pair{64u, false},
|
||||
std::pair{64u, true}}) {
|
||||
const bool child_table = member == 64u, compact = member != 0u && !child_table;
|
||||
if (upper_only && !child_table) continue;
|
||||
const u32 keys_address = child_table ? 1024u : 128u, table = child_table ? 2048u : 256u;
|
||||
const u32 rows = child_table ? 4u : keys.size(), children = child_table ? 3u : 1u;
|
||||
const u32 records_count = counted ? 2u : rows, count_address = 768u;
|
||||
const u32 stride = child_table ? 136u : compact ? 0x330u : 592u;
|
||||
const u32 object_stride = child_table ? 1536u : 0x430u;
|
||||
const u32 table_bytes = (rows - 1u) * stride + member + 16u;
|
||||
const u32 outputs = child_table ? wave * 2u : keys.size();
|
||||
TestCase test;
|
||||
test.name = child_table ? (upper_only ? "IndexedPointerChildTableUpper64" : "IndexedPointerChildTable")
|
||||
test.name = counted ? (upper_only ? "CountedPointerChildTableUpper64" : "CountedPointerChildTable")
|
||||
: child_table ? (upper_only ? "IndexedPointerChildTableUpper64" : "IndexedPointerChildTable")
|
||||
: member == 0x16cu ? "CompactPairedImageSamplerTable"
|
||||
: compact ? "CompactRecordImageSamplerTable" : "NestedPointerImageSamplerTable";
|
||||
test.initial.resize((compact ? table + table_bytes : objects + rows * object_stride) / 4u,
|
||||
@@ -39034,7 +39037,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
std::fill_n(test.initial.begin(), outputs, 0xdeadbeefu);
|
||||
std::fill_n(test.initial.begin() + keys_address / 4u, child_table ? wave * 2u : keys.size(), UINT32_MAX);
|
||||
if (compact) std::copy(list.begin(), list.end(), test.initial.begin() + list_address / 4u);
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
for (u32 row = 0; row < records_count; ++row) {
|
||||
const u32 object = compact ? table + row * stride : objects + row * object_stride;
|
||||
if (!compact) {
|
||||
test.initial[(table + row * stride) / 4u] = object;
|
||||
@@ -39062,7 +39065,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
test.initial.begin() + (table + (member == 0x16cu ? 0x17cu : 0xbcu)) / 4u);
|
||||
}
|
||||
auto records = MakeStructuredStorageBufferData(compact ? 0u : stride,
|
||||
compact ? table_bytes : rows);
|
||||
compact ? table_bytes : records_count);
|
||||
records[0] = table;
|
||||
std::copy_n(records.begin(), 4, test.user_data.begin() + 32);
|
||||
auto key_buffer = MakeStructuredStorageBufferData(child_table ? 8u : 0u,
|
||||
@@ -39076,6 +39079,10 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
}
|
||||
const auto output = MakeStructuredStorageBufferData(0, outputs * 4u);
|
||||
std::copy_n(output.begin(), 4, test.user_data.begin() + 48);
|
||||
if (counted) {
|
||||
test.user_data[52] = count_address;
|
||||
test.initial[count_address / 4u] = records_count;
|
||||
}
|
||||
test.bda_mappings = {{0u, 0u}};
|
||||
test.has_user_data = test.has_compute_info = true;
|
||||
test.compute_info.wave_size = wave;
|
||||
@@ -39089,6 +39096,9 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
size_t loop = 0, empty = 0, skip_child = 0;
|
||||
if (child_table) {
|
||||
code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0));
|
||||
if (counted)
|
||||
code.insert(code.end(), {EncodeSmem0(0x00, 54, 26), EncodeSmem1(0, 125),
|
||||
EncodeSopp(0x0c, 0)});
|
||||
if (upper_only) {
|
||||
AppendSMovLiteral(&code, 126, 0u);
|
||||
AppendSMovLiteral(&code, 127, UINT32_MAX);
|
||||
@@ -39097,7 +39107,11 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
EncodeMubuf0(0x0d, 0, true, false), EncodeMubuf1(4, 1, 0), EncodeSopp(0x0c, 0)});
|
||||
AppendVMovLiteral(&code, 6, UINT32_MAX);
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
if (counted)
|
||||
code.insert(code.end(), {EncodeSopc(0x08, 54, InlineU32(row)),
|
||||
EncodeSop2(0x0b, 62, 126, InlineU32(0))});
|
||||
code.push_back(EncodeVopc(0xc2, InlineU32(row), 4));
|
||||
if (counted) code.push_back(EncodeSop2(0x0f, 106, 106, 62));
|
||||
AppendVop3(&code, 0x101, 6, Vgpr(6), InlineU32(row), 106);
|
||||
}
|
||||
code.push_back(EncodeVopc(0x93, InlineU32(0), 6)); // Only finite nonnegative parent rows.
|
||||
@@ -39156,6 +39170,8 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
test.decoded_counts = {{"BUFFER_LOAD_DWORDX2", 2u}, {"V_READFIRSTLANE_B32", 4u},
|
||||
{"V_BFE_I32", 1u}, {"V_CMPX_LE_I32", 1u}, {"S_CBRANCH_EXECNZ", 1u}};
|
||||
test.required_spirv = {"OpLoopMerge", "OpGroupNonUniformShuffle", "OpBitFieldSExtract"};
|
||||
if (counted) test.decoded_counts.insert(test.decoded_counts.end(),
|
||||
{{"S_LOAD_DWORD ", 1u}, {"S_CMP_GT_U32", rows}, {"S_CSELECT_B64", rows}});
|
||||
} else 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::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
@@ -39171,7 +39187,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
for (const auto &image : compiled.resources.images)
|
||||
if (image.dwords[0] != 0u) candidates.push_back(image.dwords[0]);
|
||||
std::ranges::sort(candidates);
|
||||
std::vector<u32> expected_candidates(rows * children);
|
||||
std::vector<u32> expected_candidates(records_count * children);
|
||||
std::iota(expected_candidates.begin(), expected_candidates.end(), 10u);
|
||||
Require(test.name, "complete child domain", candidates == expected_candidates,
|
||||
"parent/child planning omitted a record or admitted poisoned padding");
|
||||
@@ -39179,6 +39195,12 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
std::ranges::none_of(compiled.resources.specialization_reads, [&](const auto &span) {
|
||||
return span.first < keys_address + wave * 8u && keys_address < span.first + span.second;
|
||||
}), "host planning captured GPU-only selector bytes");
|
||||
if (counted)
|
||||
Require(test.name, "runtime parent count is captured",
|
||||
std::ranges::any_of(compiled.resources.specialization_reads, [&](const auto &span) {
|
||||
return span.first <= count_address && count_address - span.first < span.second &&
|
||||
sizeof(u32) <= span.second - (count_address - span.first);
|
||||
}), "the proven scalar count was absent from the captured alias ranges");
|
||||
}
|
||||
if (compact) {
|
||||
std::vector<u32> candidates;
|
||||
@@ -39235,7 +39257,7 @@ void CheckNestedPointerImageTable(VulkanHarness &vulkan) {
|
||||
test.expected[address / 4u + 1u] = child;
|
||||
if (upper_only && group < 32u) continue;
|
||||
test.expected[wave + group] = 0x76543210u;
|
||||
if (outer >= rows || child >= children) continue;
|
||||
if (outer >= records_count || child >= children) continue;
|
||||
}
|
||||
const u32 row = compact ? list[outer] : outer;
|
||||
test.expected[group] = std::bit_cast<u32>(float(10u + row * children + child) *
|
||||
|
||||
Reference in New Issue
Block a user