Bound uniformized image keys by guarded material mask

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>
This commit is contained in:
nmzik
2026-09-24 03:04:07 +04:00
parent 1617ee77e5
commit 963a0817dc
4 changed files with 637 additions and 10 deletions
+238
View File
@@ -847,6 +847,243 @@ void TestBoundedComputeImageLoop() {
"oversized compute loop bound was accepted for image enumeration");
}
void TestUniformizedMaterialImageKeys() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
const auto make_plan = [](bool wrong_update, bool wrong_equality) {
Fixture fixture;
auto *entry = fixture.block;
auto *header = fixture.AddBlock();
auto *inactive = fixture.AddBlock();
auto *sentinel = fixture.AddBlock();
auto *bit = fixture.AddBlock();
auto *merge = fixture.AddBlock();
auto *choose = fixture.AddBlock();
auto *sample = fixture.AddBlock();
auto *done = fixture.AddBlock();
entry->AddBranch(header);
header->AddBranch(inactive);
inactive->AddBranch(merge);
inactive->AddBranch(sentinel);
sentinel->AddBranch(merge);
sentinel->AddBranch(bit);
bit->AddBranch(header);
bit->AddBranch(merge);
merge->AddBranch(choose);
choose->AddBranch(sample);
choose->AddBranch(done);
sample->AddBranch(done);
fixture.program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 1u};
fixture.program.block_info[1].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 2u};
const auto active_on_entry = fixture.Emit(
ValueOpcode::INotEqual32, {fixture.UserData(5u), Value(0u)}, 0, entry);
auto &active_phi = header->AppendNewInst(
ValueOpcode::Phi, {}, static_cast<uint64_t>(Type::U1));
auto &mask_phi = header->AppendNewInst(
ValueOpcode::Phi, {}, static_cast<uint64_t>(Type::U32));
const auto mask = Value(&mask_phi);
const auto active = Value(&active_phi);
fixture.program.block_info[2].condition = fixture.Emit(
ValueOpcode::LogicalNot, {active}, 0, inactive);
fixture.program.block_info[2].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = 5u, .false_block = 3u};
const auto nonzero = fixture.Emit(
ValueOpcode::INotEqual32, {Value(0u), mask}, 0, sentinel);
const auto bit_guard = fixture.Emit(
ValueOpcode::LogicalAnd, {active, nonzero}, 0, sentinel);
fixture.program.block_info[3].condition = fixture.Emit(
ValueOpcode::LogicalNot, {bit_guard}, 0, sentinel);
fixture.program.block_info[3].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = 5u, .false_block = 4u};
const auto first = fixture.Emit(ValueOpcode::FindILsb32, {mask}, 0, bit);
const auto position = fixture.Emit(
ValueOpcode::BitwiseAnd32, {first, Value(31u)}, 0, bit);
const auto one_bit = fixture.Emit(
ValueOpcode::ShiftLeftLogical32, {Value(1u), position}, 0, bit);
const auto cleared = fixture.Emit(
wrong_update ? ValueOpcode::BitwiseOr32 : ValueOpcode::BitwiseXor32,
{mask, one_bit}, 0, bit);
const auto continuation = fixture.Emit(
ValueOpcode::LogicalAnd, {bit_guard, active_on_entry}, 0, bit);
fixture.program.block_info[4].condition = continuation;
fixture.program.block_info[4].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = 1u, .false_block = 5u};
active_phi.AddPhiOperand(entry, active_on_entry);
active_phi.AddPhiOperand(bit, continuation);
mask_phi.AddPhiOperand(entry, fixture.UserData(4u));
mask_phi.AddPhiOperand(bit, cleared);
const auto arbitrary = fixture.UserData(6u);
const auto sentinel_index = fixture.Emit(
ValueOpcode::SelectU32, {active, Value(32u), arbitrary}, 0, sentinel);
const auto bit_index = fixture.Emit(
ValueOpcode::SelectU32, {bit_guard, first, sentinel_index}, 0, bit);
auto &index_phi = merge->AppendNewInst(
ValueOpcode::Phi, {}, static_cast<uint64_t>(Type::U32));
index_phi.AddPhiOperand(inactive, arbitrary);
index_phi.AddPhiOperand(sentinel, sentinel_index);
index_phi.AddPhiOperand(bit, bit_index);
const auto index = Value(&index_phi);
const auto below = fixture.Emit(
ValueOpcode::SGreaterThan32, {Value(32u), index}, 0, merge);
const auto material_guard = fixture.Emit(
ValueOpcode::LogicalAnd, {active_on_entry, below}, 0, merge);
fixture.program.block_info[5].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 6u};
const auto scaled = fixture.Emit(
ValueOpcode::ShiftLeftLogical32, {index, Value(4u)}, 0, choose);
const auto selected_scale = fixture.Emit(
ValueOpcode::SelectU32, {material_guard, scaled, Value(0u)}, 0, choose);
const auto times_eight = fixture.Emit(
ValueOpcode::ShiftLeftLogical32, {selected_scale, Value(3u)}, 0, choose);
const auto times_nine = fixture.Emit(
ValueOpcode::IAdd32, {times_eight, selected_scale}, 0, choose);
const auto material_offset = fixture.Emit(
ValueOpcode::SelectU32,
{material_guard,
fixture.Emit(ValueOpcode::IAdd32,
{times_nine, Value(0xc00u)}, 0, choose),
times_nine}, 0, choose);
const auto base = fixture.Address(fixture.UserData(0u), fixture.UserData(1u));
MemoryInfo material_memory;
material_memory.kind = ResourceKind::Global;
const auto loaded = fixture.Emit(
ValueOpcode::LoadAddressU32,
{base, material_offset, Value(0u), material_guard},
fixture.AddMemory(material_memory, 0x1a88u), choose);
const auto local = fixture.Emit(
ValueOpcode::SelectU32, {material_guard, loaded, arbitrary}, 0, choose);
const auto key = fixture.Emit(
ValueOpcode::ReadLane, {local, Value(0u)}, 0, choose);
const auto compared = fixture.Emit(
ValueOpcode::IEqual32,
{key, wrong_equality ? arbitrary : local}, 0, choose);
const auto sample_guard = fixture.Emit(
ValueOpcode::LogicalAnd, {material_guard, compared}, 0, choose);
fixture.program.block_info[6].condition = fixture.Emit(
ValueOpcode::LogicalNot, {sample_guard}, 0, choose);
fixture.program.block_info[6].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = 8u, .false_block = 7u};
const auto table_offset = fixture.Emit(
ValueOpcode::IAdd32,
{fixture.Emit(ValueOpcode::ShiftLeftLogical32,
{key, Value(5u)}, 0, sample), Value(0x20e0u)}, 0, sample);
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
MemoryInfo memory;
memory.kind = ResourceKind::ScalarAddress;
memory.offset = word * 4u;
words[word] = fixture.Emit(
ValueOpcode::LoadAddressU32,
{base, table_offset, Value(0u), Value(true)},
fixture.AddMemory(memory, 0x1accu), sample);
}
const auto image = fixture.Emit(
ValueOpcode::GetImageResource,
{words[0], words[1], words[2], words[3],
words[4], words[5], words[6], words[7]}, 0, sample);
const auto sampler = fixture.Emit(
ValueOpcode::GetSamplerResource,
{Value(0u), Value(0u), Value(0u), Value(0u)}, 0, sample);
const auto address = fixture.Emit(
ValueOpcode::MakeImageAddress,
{Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u)}, 0, sample);
MemoryInfo image_memory;
image_memory.kind = ResourceKind::Image;
image_memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
fixture.AddMemory(image_memory, 0x1aecu), sample);
fixture.program.block_info[7].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 8u};
fixture.program.block_info[8].terminator.kind = CFG::TerminatorKind::Return;
const auto output = fixture.Emit(
ValueOpcode::GetBufferResource,
{fixture.UserData(8u), fixture.UserData(9u),
fixture.UserData(10u), fixture.UserData(11u)}, 0, done);
MemoryInfo output_memory;
output_memory.kind = ResourceKind::Buffer;
fixture.Emit(ValueOpcode::StoreBufferU32,
{output, Value(0u), Value(0u), Value(0u),
Value(1u), Value(true)},
fixture.AddMemory(output_memory, 0x1b00u), done);
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_image;
Check(indirect && indirect->selector_stride == 0x90u &&
indirect->selector_offset == 0xc00u &&
indirect->table_offset == 0x20e0u &&
!indirect->selector_mask.IsEmpty() &&
indirect->key_count.Resolve().IsImmediate() &&
indirect->key_count.Resolve().U32() == 32u,
"uniformized image key lost its finite material range");
return ExtractResourcePlan(fixture.program);
};
auto plan = make_plan(false, false);
Check(plan.requires_specialization_memory &&
plan.descriptor_sources[plan.info.images[0].source]
.indirect_image->selector_mask.Resolve().TryInstruction() != nullptr,
"uniformized image mask did not survive extraction");
LinearTestMemory memory;
memory.words.resize(0x23000u / 4u);
constexpr uint64_t base = 0x1000u;
constexpr uint64_t first_material = base + 0xc00u + 2u * 0x90u;
constexpr uint64_t second_material = base + 0xc00u + 29u * 0x90u;
constexpr uint64_t first_table = base + 0x20e0u + 7u * 32u;
constexpr uint64_t second_table = base + 0x20e0u + 4096u * 32u;
memory.words[(first_material - base) / 4u] = 7u;
memory.words[(second_material - base) / 4u] = 4096u;
const auto set_descriptor = [&](uint64_t address, uint32_t color) {
const auto word = (address - base) / 4u;
memory.words[word] = color;
memory.words[word + 1u] = static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
};
set_descriptor(first_table, 0x200u);
set_descriptor(second_table, 0x400u);
std::array<uint32_t, 12> user_data{};
user_data[0] = base;
user_data[4] = (1u << 2u) | (1u << 29u);
user_data[5] = 1u;
user_data[8] = 0x400000u;
user_data[9] = 16u << 16u;
user_data[10] = 1u;
memory.fail_address = base + 0xc00u + 3u * 0x90u;
const SrtRuntime runtime{
.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 2u &&
memory.reads == 4u && memory.descriptor_reads == 2u &&
snapshot.flattened_srt[
specialization.images[0].indirect_mapping_offset] == 2u &&
snapshot.flattened_srt[
specialization.images[0].indirect_mapping_offset + 1u] == 7u &&
snapshot.flattened_srt[
specialization.images[0].indirect_mapping_offset + 3u] == 4096u,
"material mask did not limit sparse descriptor reads");
user_data[8] = first_material;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"written buffer alias with a material key was accepted");
user_data[8] = first_table;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"written buffer alias with an image record was accepted");
CheckFatal([&] { make_plan(true, false); }, "not a valid runtime value",
"non-clearing material mask was accepted");
CheckFatal([&] { make_plan(false, true); }, "not a valid runtime value",
"unrelated ReadLane key was accepted");
}
void TestImageDescriptorFields() {
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> reserved{
{{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u},
@@ -2655,6 +2892,7 @@ int main() {
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
Run("guarded direct image table", TestGuardedDirectImageTable);
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
Run("image descriptor fields", TestImageDescriptorFields);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);