shader: materialize image keys from proven GPU bitmap subsets

This commit is contained in:
nmzik
2026-10-12 20:44:12 +02:00
parent f01a3750d4
commit 145578ac03
4 changed files with 443 additions and 15 deletions
+3 -1
View File
@@ -507,6 +507,8 @@ struct DescriptorSource {
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
bool bitset = false;
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
bool operator==(const SelectorRead& other) const = default;
@@ -521,7 +523,7 @@ struct DescriptorSource {
bool indexed_store = false;
uint32_t workgroup_axis = UINT32_MAX;
Value key_count;
// First selector index, or first descriptor key when no selector is present.
// First selector index, or first descriptor key for a direct or bitmap range.
Value selector_first;
Value selector_mask;
std::vector<uint32_t> sources;
@@ -338,6 +338,33 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::iota(keys.begin(), keys.end(), first_key);
if (!keys.empty() && keys.back() < keys.front())
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
} else if (selector->bitset) {
ShaderBufferResource material;
std::array<uint32_t, 4> masks {};
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count != masks.size() * 32u ||
selector->stride != sizeof(masks) || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
(material.Base48() & 3u) != 0u ||
material.GetSize() / sizeof(uint32_t) > MaxIndirectDescriptorProbes) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid) {
keys.resize(material.GetSize() / sizeof(uint32_t));
if (!keys.empty() && !ReadScalarTable(material.Base48(), material.GetSize(), 0u,
runtime, keys)) return false;
for (size_t word = 0; word < keys.size(); ++word) masks[word % masks.size()] |= keys[word];
keys.clear();
for (uint32_t word = 0; word < masks.size(); ++word) {
auto mask = masks[word];
while (mask != 0u) {
const auto rank = word * 32u + std::countr_zero(mask);
keys.push_back(first + count - 1u - rank);
mask &= mask - 1u;
}
}
}
} else if (!selector->byte_ranges.empty()) {
if (material_value.dword_count != 2u) return false;
const auto base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
@@ -1202,20 +1202,24 @@ private:
(Implies(inst->Arg(0), required) || Implies(inst->Arg(1), required));
}
Value EqualLocalKey(Value guard, Value key) const {
Value EqualLocalValue(Value guard, const auto& matches) const {
guard = SimplifyGuard(guard);
const auto* inst = guard.TryInstruction();
if (inst == nullptr) return {};
if (inst->GetOpcode() == ValueOpcode::LogicalAnd) {
const auto left = EqualLocalKey(inst->Arg(0), key);
return left.IsEmpty() ? EqualLocalKey(inst->Arg(1), key) : left;
const auto left = EqualLocalValue(inst->Arg(0), matches);
return left.IsEmpty() ? EqualLocalValue(inst->Arg(1), matches) : left;
}
if (inst->GetOpcode() != ValueOpcode::IEqual32 || inst->NumArgs() != 2u) return {};
if (EquivalentValue(m_program, inst->Arg(0), key)) return inst->Arg(1).Resolve();
if (EquivalentValue(m_program, inst->Arg(1), key)) return inst->Arg(0).Resolve();
if (matches(inst->Arg(0))) return inst->Arg(1).Resolve();
if (matches(inst->Arg(1))) return inst->Arg(0).Resolve();
return {};
}
Value EqualLocalKey(Value guard, Value key) const {
return EqualLocalValue(guard, [&](Value at) { return EquivalentValue(m_program, at, key); });
}
struct AffineOffset {
Value index;
uint64_t stride = 0;
@@ -1977,6 +1981,216 @@ private:
return false;
}
using SelectorBounds = std::span<const std::pair<const Inst*, uint32_t>>;
// Each equation is an inductive subset obligation. Phis require every incoming
// value; AND needs either operand. Grounding rejects cycles without an initial
// value, then the greatest fixed point checks every initial and carried edge.
bool BitsetMaskSubset(Value mask, Value active, Value word, const Inst& read) const {
struct State {
Value value;
Value guard;
uint32_t word = UINT32_MAX;
uint32_t component = UINT32_MAX; // U1 implication of the load guard.
bool operator==(const State&) const = default;
};
struct Equation {
bool all = true;
std::vector<size_t> terms;
};
const auto load_guard = read.Arg(4).Resolve();
std::vector<State> states(2);
std::vector<Equation> equations(2);
const auto add = [&](State state) {
state.value = state.value.Resolve();
const auto found = std::find(states.begin() + 2u, states.end(), state);
if (found != states.end()) return static_cast<size_t>(found - states.begin());
states.push_back(state);
equations.emplace_back();
return states.size() - 1u;
};
std::array<size_t, 5> roots {add({.value = active})};
for (uint32_t component = 0; component < 4u; ++component)
roots[component + 1u] = add({mask, active, component, component});
for (size_t index = 2; index < states.size(); ++index) {
const auto state = states[index];
const auto* inst = state.value.TryInstruction();
Equation equation;
const auto term = [&](Value value, Value guard, uint32_t selected) {
return add({value, guard, selected, state.component});
};
if (state.component == UINT32_MAX) {
if (Implies(state.value, load_guard)) equation.terms = {1u};
else if (inst != nullptr && (inst->GetOpcode() == ValueOpcode::LogicalAnd ||
inst->GetOpcode() == ValueOpcode::Phi)) {
equation.all = inst->GetOpcode() == ValueOpcode::Phi;
for (size_t arg = 0; arg < inst->NumArgs(); ++arg)
equation.terms.push_back(add({.value = inst->Arg(arg)}));
}
} else if (state.value == Value(0u)) {
equation.terms = {1u};
} else if (inst != nullptr) {
const auto constant = [&](const auto& self, Value value) -> std::optional<uint32_t> {
uint32_t literal;
if (ImmediateU32(value, literal)) return literal;
if (state.word != UINT32_MAX && EquivalentValue(m_program, value, word)) return state.word;
const auto* operation = value.Resolve().TryInstruction();
if (operation == nullptr || operation->GetOpcode() != ValueOpcode::BitwiseAnd32) return {};
const auto left = self(self, operation->Arg(0));
const auto right = self(self, operation->Arg(1));
return left && right ? std::optional {*left & *right} : std::nullopt;
};
const auto condition = [&](const auto& self, Value value) -> std::optional<bool> {
value = value.Resolve();
if (value.IsImmediate() && value.GetType() == Type::U1) return value.U1();
if (Implies(state.guard, value) || Implies(load_guard, value)) return true;
const auto* test = value.TryInstruction();
if (test == nullptr) return {};
if (test->GetOpcode() == ValueOpcode::LogicalAnd) {
const auto left = self(self, test->Arg(0));
const auto right = self(self, test->Arg(1));
if ((left && !*left) || (right && !*right)) return false;
if (left && right) return *left && *right;
} else if (test->GetOpcode() == ValueOpcode::IEqual32) {
const auto left = constant(constant, test->Arg(0));
const auto right = constant(constant, test->Arg(1));
if (left && right) return *left == *right;
}
return {};
};
switch (inst->GetOpcode()) {
case ValueOpcode::CompositeExtractU32x4: {
uint32_t component;
if (inst->Arg(0).Resolve().TryInstruction() == &read &&
ImmediateU32(inst->Arg(1), component) && component == state.component)
equation.terms = {1u};
break;
}
case ValueOpcode::Phi:
// The predecessor may be an earlier outer iteration. Establish
// this register's invariant without the current word or EXEC.
for (size_t arg = 0; arg < inst->NumArgs(); ++arg)
equation.terms.push_back(term(inst->Arg(arg), load_guard, UINT32_MAX));
break;
case ValueOpcode::BitwiseAnd32:
equation.all = false;
equation.terms = {term(inst->Arg(0), state.guard, state.word),
term(inst->Arg(1), state.guard, state.word)};
break;
case ValueOpcode::SelectU32: {
const auto selected = condition(condition, inst->Arg(0));
if (!selected || *selected) {
auto selected_word = state.word;
const auto equality = EqualLocalValue(inst->Arg(0), [&](Value key) {
key = key.Resolve();
const auto* masked = key.TryInstruction();
uint32_t bound = 0;
if (masked != nullptr && masked->GetOpcode() == ValueOpcode::BitwiseAnd32 &&
DescendingLoop(word, masked->Parent(), bound) && bound == 3u) {
for (uint32_t side = 0; side < 2u; ++side) {
uint32_t bits;
if (ImmediateU32(masked->Arg(side), bits) && (bits & 3u) == 3u)
key = masked->Arg(side ^ 1u).Resolve();
}
}
return EquivalentValue(m_program, key, word);
});
uint32_t literal;
if (ImmediateU32(equality, literal) && literal < 4u) selected_word = literal;
equation.terms.push_back(term(inst->Arg(1), inst->Arg(0), selected_word));
}
if (!selected || !*selected)
equation.terms.push_back(term(inst->Arg(2), state.guard, state.word));
break;
}
default: break;
}
}
if (equation.terms.empty()) equation.terms = {0u};
equations[index] = std::move(equation);
}
std::vector<bool> valid(states.size(), false);
valid[1] = true;
bool changed;
do {
changed = false;
for (size_t index = 2; index < equations.size(); ++index) {
if (!valid[index] && std::ranges::any_of(equations[index].terms,
[&](size_t term) { return valid[term]; })) valid[index] = changed = true;
}
} while (changed);
do {
changed = false;
for (size_t index = 2; index < equations.size(); ++index) {
const auto& equation = equations[index];
const auto proven = [&](size_t term) { return valid[term]; };
if (valid[index] && !(equation.all ? std::ranges::all_of(equation.terms, proven)
: std::ranges::any_of(equation.terms, proven))) {
valid[index] = false;
changed = true;
}
}
} while (changed);
return std::ranges::all_of(roots, [&](size_t root) { return valid[root]; });
}
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds,
DescriptorSource::IndirectDescriptor& indirect) {
const auto selected_value = [&](Value value) {
for (;;) {
value = ActiveValue(value, active);
const auto* select = value.TryInstruction();
if (select == nullptr || select->GetOpcode() != ValueOpcode::SelectU32 ||
!SelectorPredicateFalse(select->Arg(0), bounds, 0u, active)) return value;
value = select->Arg(2);
}
};
const auto* sum = selected_value(rank).TryInstruction();
if (sum == nullptr || sum->GetOpcode() != ValueOpcode::IAdd32) return false;
Value word;
Value bit;
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* shift = selected_value(sum->Arg(arg)).TryInstruction();
if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftLeftLogical32 ||
shift->Arg(1).Resolve() != Value(5u)) continue;
word = selected_value(shift->Arg(0));
bit = selected_value(sum->Arg(arg ^ 1u));
}
const auto* word_inst = word.TryInstruction();
if (word_inst == nullptr || std::ranges::none_of(bounds, [&](const auto& bound) {
return bound.first == word_inst && bound.second == 3u;
})) return false;
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
const auto* subtract = bit.TryInstruction();
if (subtract == nullptr || subtract->GetOpcode() != ValueOpcode::ISub32 ||
subtract->Arg(0).Resolve() != Value(31u)) return false;
bit = selected_value(subtract->Arg(1));
}
const auto* scan = bit.TryInstruction();
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
!ImpliesNonzero(active, scan->Arg(0))) return false;
for (const auto* block: m_program.blocks) {
for (const auto& read: *block) {
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(block)) continue;
const auto index = read.Flags<MemoryFlags>().index;
if (index >= m_program.memory_info.size()) continue;
const auto& memory = m_program.memory_info[index];
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset != 0u || memory.data_bits != 32u ||
memory.data_dwords != 4u || read.Arg(2).Resolve() != Value(0u) ||
read.Arg(3).Resolve() != Value(0u) ||
!BitsetMaskSubset(scan->Arg(0), active, word, read)) continue;
DescriptorSource source;
if (!MakeRuntimeTableSource(read, source)) continue;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
.source = InternSource(source), .stride = 16u, .offset = 0u, .bitset = true});
return true;
}
}
return false;
}
bool MatchReversedImageRange(Value key, DescriptorSource::IndirectDescriptor& indirect) {
const auto* subtract = key.Resolve().TryInstruction();
if (subtract == nullptr || subtract->GetOpcode() != ValueOpcode::ISub32 ||
@@ -2000,6 +2214,7 @@ private:
first.SetArg(1, Value(maximum));
indirect.selector_first = Value(&first);
indirect.key_count = Value(maximum + 1u);
if (maximum == 127u) MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, indirect);
return true;
}
@@ -2300,7 +2515,6 @@ private:
return finish();
}
using SelectorBounds = std::span<const std::pair<const Inst*, uint32_t>>;
bool SelectorPredicateFalse(Value value, SelectorBounds bounds, uint32_t depth = 0,
Value guard = {}) const {
+193 -8
View File
@@ -1096,9 +1096,14 @@ void TestBoundedComputeImageLoop() {
void TestDescendingImageRange() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
enum class Variant { Valid, WrongStep, IncrementBypass, MissingNonzero, NegativeInitial };
enum class Variant {
Valid, WrongStep, IncrementBypass, MissingNonzero, NegativeInitial,
Bitmap, BitmapAddsBit, BitmapWrongWord, BitmapUnknownInitial,
BitmapByteOffset, BitmapScalarOffset, BitmapOuterClobber, BitmapWrongLoadMask
};
const auto make_plan = [](Variant variant) {
Fixture fixture;
const bool bitmap = variant >= Variant::Bitmap;
auto *entry = fixture.block;
auto *header = fixture.AddBlock();
auto *guard = fixture.AddBlock();
@@ -1117,21 +1122,69 @@ void TestDescendingImageRange() {
.true_block = yes, .false_block = no};
};
branch(entry, header);
branch(body, latch);
branch(body, bitmap ? guard : latch);
branch(latch, header);
auto &induction = header->AppendNewInst(ValueOpcode::Phi);
induction.SetFlags(Type::U32);
std::array<Inst *, 4> outer_words {}, remaining_words {};
std::array<Value, 4> loaded_words {};
if (bitmap)
for (auto& word : outer_words)
word = &header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
const auto bound = fixture.Emit(ValueOpcode::SGreaterThanEqual32,
{Value(&induction), Value(0u)}, 0, header);
choose(header, guard, variant == Variant::IncrementBypass ? latch : exit, bound);
MemoryInfo lds;
lds.kind = ResourceKind::Lds;
const auto mask = fixture.Emit(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)}, fixture.AddMemory(lds, 0x10), guard);
Value mask;
Value load_active(true);
if (bitmap) {
fixture.block = entry;
const auto lane = fixture.Emit(ValueOpcode::LaneId);
load_active = fixture.Emit(ValueOpcode::ULessThan32, {lane, Value(32u)});
const auto buffer = fixture.Buffer({fixture.UserData(4), fixture.UserData(5),
fixture.UserData(6), fixture.UserData(7)});
MemoryInfo read;
read.kind = ResourceKind::Buffer;
read.idxen = true;
read.offen = variant == Variant::BitmapByteOffset;
read.data_dwords = 4u;
const auto packed = fixture.Emit(ValueOpcode::LoadBufferU32x4,
{buffer, lane,
Value(variant == Variant::BitmapByteOffset ? 4u : 0u),
Value(variant == Variant::BitmapScalarOffset ? 4u : 0u), load_active},
fixture.AddMemory(read, 0x10));
for (uint32_t word = 0; word < 4u; ++word) {
auto component = fixture.Emit(ValueOpcode::CompositeExtractU32x4,
{packed, Value(variant == Variant::BitmapWrongWord ? (word + 1u) % 4u : word)}, 0, entry);
component = fixture.Emit(ValueOpcode::SelectU32,
{load_active, component, fixture.Emit(ValueOpcode::UndefU32)});
loaded_words[word] = component;
if (variant == Variant::BitmapUnknownInitial && word == 0u)
component = fixture.Emit(ValueOpcode::SelectU32,
{fixture.Emit(ValueOpcode::IEqual32, {fixture.Emit(ValueOpcode::LaneId), Value(0u)}),
component, fixture.UserData(3)});
outer_words[word]->AddPhiOperand(entry, component);
remaining_words[word] = &guard->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
remaining_words[word]->AddPhiOperand(header, Value(outer_words[word]));
}
auto selected = fixture.Emit(ValueOpcode::UndefU32, {}, 0, guard);
for (uint32_t word = 0; word < 4u; ++word) {
selected = fixture.Emit(ValueOpcode::SelectU32,
{fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)}, 0, guard),
Value(remaining_words[word]), selected}, 0, guard);
}
mask = selected;
} else {
MemoryInfo lds;
lds.kind = ResourceKind::Lds;
mask = fixture.Emit(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)}, fixture.AddMemory(lds, 0x10), guard);
}
auto active = bound;
if (variant != Variant::MissingNonzero)
active = fixture.Emit(ValueOpcode::LogicalAnd,
{bound, fixture.Emit(ValueOpcode::ULessThan32, {Value(0u), mask}, 0, guard)}, 0, guard);
if (bitmap && variant != Variant::BitmapWrongLoadMask)
active = fixture.Emit(ValueOpcode::LogicalAnd, {load_active, active}, 0, guard);
choose(guard, body, latch, active);
fixture.block = body;
const auto stale = fixture.UserData(3);
@@ -1171,6 +1224,26 @@ void TestDescendingImageRange() {
fixture.Emit(ValueOpcode::ImageSampleRaw,
{fixture.Image(words), fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}),
fixture.ImageAddress(), active}, fixture.AddMemory(sample, 0x30));
if (bitmap) {
auto next = fixture.Emit(ValueOpcode::BitwiseAnd32,
{mask, fixture.Emit(ValueOpcode::BitwiseNot32,
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(1u), bit})})});
if (variant == Variant::BitmapAddsBit)
next = fixture.Emit(ValueOpcode::BitwiseOr32, {next, Value(1u << 21u)});
for (uint32_t word = 0; word < 4u; ++word) {
const auto selected_word = fixture.Emit(ValueOpcode::LogicalAnd,
{active, fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)})});
remaining_words[word]->AddPhiOperand(body, fixture.Emit(ValueOpcode::SelectU32,
{selected_word, next, Value(remaining_words[word])}));
auto carried = Value(remaining_words[word]);
if (variant == Variant::BitmapOuterClobber)
carried = fixture.Emit(ValueOpcode::SelectU32,
{fixture.Emit(ValueOpcode::IEqual32,
{Value(&induction), Value(word + 1u)}, 0, latch),
loaded_words[(word + 1u) % 4u], carried}, 0, latch);
outer_words[word]->AddPhiOperand(latch, carried);
}
}
const auto step = fixture.Emit(ValueOpcode::IAdd32,
{Value(&induction), Value(variant == Variant::WrongStep ? UINT32_MAX - 1u : UINT32_MAX)}, 0, latch);
induction.AddPhiOperand(entry, Value(variant == Variant::NegativeInitial ? UINT32_MAX : 3u));
@@ -1180,9 +1253,11 @@ void TestDescendingImageRange() {
"descending range retained impossible relative-register stores");
const auto &indirect = fixture.program.descriptor_sources[
fixture.program.info.images[0].source].indirect_descriptor;
Check(indirect && !indirect->selector && indirect->key_count == Value(128u) &&
Check(indirect && indirect->selector.has_value() == (variant == Variant::Bitmap) &&
(!indirect->selector || indirect->selector->bitset) &&
indirect->key_count == Value(128u) &&
!indirect->selector_first.IsEmpty(),
"descending bitscan range lost its runtime first key or 128-key bound");
"descending bitscan range lost its bound or accepted an unproved bitmap subset");
return ExtractResourcePlan(fixture.program);
};
auto plan = make_plan(Variant::Valid);
@@ -1218,6 +1293,116 @@ void TestDescendingImageRange() {
previous = key;
}
}
auto bitmap_plan = make_plan(Variant::Bitmap);
for (const auto variant : {Variant::BitmapAddsBit, Variant::BitmapWrongWord,
Variant::BitmapUnknownInitial, Variant::BitmapByteOffset,
Variant::BitmapScalarOffset, Variant::BitmapOuterClobber,
Variant::BitmapWrongLoadMask})
make_plan(variant);
// Every descriptor is distinct, so retained keys are visible in the snapshot as well as
// its lookup table. An unrelated table slot must never be read, even when it is malformed.
for (uint32_t record = 1u; record <= 130u; ++record)
memory.words[record * 8u] = 0x100u + record;
constexpr uint32_t bitmap_address = 0x4000u;
const auto bitmap_word = (bitmap_address - memory.base) / 4u;
memory.words.resize(bitmap_word + MaxIndirectDescriptorProbes + 4u, 0u);
std::array<uint32_t, 8> bitmap_data {0x1000u, 0u, 0u, UINT32_MAX,
bitmap_address, 16u << 16u, 3u, 0x5204u};
const SrtRuntime bitmap_runtime {.user_data = bitmap_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
memory.watched_address = bitmap_address;
const auto check_bitmap = [&](std::initializer_list<uint32_t> ranks, uint32_t count) {
bitmap_data[2] = count;
memory.watched_reads = memory.descriptor_reads = 0u;
std::vector<uint32_t> keys;
for (const auto rank : ranks) keys.push_back(count - rank);
std::ranges::sort(keys);
Check(MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization) &&
memory.watched_reads == uint32_t(bitmap_data[6] != 0u) &&
(bitmap_data[6] == 0u || memory.watched_dwords == bitmap_data[6] * 4u) &&
memory.descriptor_reads == keys.size() &&
snapshot.images.size() == std::max<size_t>(keys.size(), 1u),
"bitmap selector did not read one bounded snapshot or retained unreachable descriptors");
Check(bitmap_data[6] == 0u || std::ranges::find(snapshot.specialization_reads,
std::pair{uint64_t{bitmap_address}, uint64_t{bitmap_data[6]} * 16u}) !=
snapshot.specialization_reads.end(),
"bitmap snapshot was omitted from same-dispatch write-alias protection");
if (keys.empty()) {
Check(std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t value) { return value == 0u; }) &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
"empty bitmap retained a previously materialized image");
return;
}
if (keys.size() == 1u) {
Check(snapshot.images[0].dwords[0] == 0x100u + uint32_t(0x1000u + keys[0] * 32u) / 32u &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
"a single bitmap rank became empty or required an indirect image mapping");
return;
}
const auto offset = specialization.images[0].indirect_mapping_offset;
Check(snapshot.flattened_srt[offset] == keys.size(), "bitmap mapping lost a selected key");
for (uint32_t index = 0; index < keys.size(); ++index) {
const auto descriptor_record = uint32_t(0x1000u + keys[index] * 32u) / 32u;
Check(snapshot.flattened_srt[offset + 1u + index * 2u] == keys[index] &&
snapshot.images[index].dwords[0] == 0x100u + descriptor_record,
"bitmap word association or U32-wrapped key selected a different descriptor");
}
};
memory.fail_address = 0x2000u - 21u * 32u;
check_bitmap({}, 0u);
memory.words[bitmap_word] = 1u;
check_bitmap({0u}, 0u);
memory.words[bitmap_word + 1u] = 1u << 3u;
memory.words[bitmap_word + 4u] = 1u; // Duplicate rank in another record.
memory.words[bitmap_word + 6u] = 1u << 31u;
memory.words[bitmap_word + 11u] = 1u << 1u;
check_bitmap({0u, 35u, 95u, 97u}, 0u);
check_bitmap({0u, 35u, 95u, 97u}, 2u);
std::fill_n(memory.words.begin() + bitmap_word, 12u, 0u);
for (uint32_t word = 0; word < 4u; ++word) {
memory.words[bitmap_word + word] = 1u << 31u;
memory.words[bitmap_word + 4u + word] = 1u;
}
check_bitmap({0u, 31u, 32u, 63u, 64u, 95u, 96u, 127u}, 0u);
std::fill_n(memory.words.begin() + bitmap_word, 12u, 0u);
memory.words[bitmap_word + 3u] = 1u << 31u;
memory.words[bitmap_word + 8u] = 1u << 2u;
check_bitmap({2u, 127u}, 0u);
std::fill_n(memory.words.begin() + bitmap_word, 12u, 0u);
check_bitmap({}, 0u);
memory.fail_address = bitmap_address + 16u;
Check(!MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization),
"bitmap selector used a stale or partial snapshot after a failed coherent read");
memory.fail_address = UINT64_MAX;
const auto valid_data = bitmap_data;
for (const auto [word, value] : std::array<std::pair<uint32_t, uint32_t>, 7>{{
{4u, bitmap_address + 1u}, {5u, 8u << 16u}, {5u, (16u << 16u) | (1u << 31u)},
{7u, 0x5204u | (1u << 23u)}, {7u, 0x5204u | (1u << 28u)},
{7u, 0x5204u | (1u << 30u)},
{6u, MaxIndirectDescriptorProbes / 4u + 1u}}}) {
bitmap_data[word] = value;
Check(!MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization),
"bitmap selector accepted an unproved buffer layout or exceeded its read bound");
bitmap_data = valid_data;
}
bitmap_data[7] &= ~(0x7fu << 12u); // FORMAT=INVALID makes even an unformatted load unbound.
memory.words[bitmap_word] = 1u;
memory.watched_reads = memory.descriptor_reads = 0u;
Check(MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization) &&
memory.watched_reads == 0u && memory.descriptor_reads == 0u &&
snapshot.images.size() == 1u &&
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t value) { return value == 0u; }),
"unbound bitmap V# read backing bytes or manufactured an image key");
bitmap_data = valid_data;
memory.words[bitmap_word] = 0u;
bitmap_data[6] = 0u;
check_bitmap({}, 0u);
bitmap_data[6] = MaxIndirectDescriptorProbes / 4u;
memory.words[bitmap_word + MaxIndirectDescriptorProbes - 1u] = 1u << 31u;
memory.words[bitmap_word] = 1u;
check_bitmap({0u, 127u}, 0u);
}
void TestMixedIndirectImageSamplers() {