shader: prove descriptor bounds from simplified image EXEC predicates

This commit is contained in:
nmzik
2026-10-13 00:22:23 +02:00
parent 25e1cc7ec9
commit ed1195d1e7
4 changed files with 231 additions and 84 deletions
@@ -248,6 +248,40 @@ bool FoldCompositeExtract(Inst& inst, ValueOpcode construct, size_t components)
return false;
}
void FoldZeroComparison(Block& block, Block::iterator instruction, FoldWorklist& worklist) {
auto& inst = *instruction;
Value value;
for (uint32_t arg = 0; arg < 2u; ++arg)
if (Arg(inst, arg) == Value(0u)) value = Arg(inst, arg ^ 1u);
const auto* source = value.TryInstruction();
if (source == nullptr) return;
const bool equal = inst.GetOpcode() == ValueOpcode::IEqual32;
const auto boolean_select = [](const Inst* select) {
return select != nullptr && select->GetOpcode() == ValueOpcode::SelectU32 &&
IsImmediate(Arg(*select, 1), Type::U32) &&
IsImmediate(Arg(*select, 2), Type::U32);
};
if (boolean_select(source)) {
const bool when_true = (Arg(*source, 1).U32() == 0u) == equal;
const bool when_false = (Arg(*source, 2).U32() == 0u) == equal;
if (when_true == when_false) Replace(inst, Value(when_true));
else if (when_true) Replace(inst, Arg(*source, 0));
else Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot,
{Arg(*source, 0)})));
} else if (source->GetOpcode() == ValueOpcode::BitwiseOr32 &&
(boolean_select(Arg(*source, 0).TryInstruction()) ||
boolean_select(Arg(*source, 1).TryInstruction()))) {
// SCC encoded as a DWORD needs no select or bitwise operation when only
// its zero predicate is consumed. Keep arbitrary DWORDs as zero comparisons.
const auto lhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
{Arg(*source, 0), Value(0u)});
const auto rhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
{Arg(*source, 1), Value(0u)});
Replace(inst, Value(&*worklist.Prepend(block, instruction,
equal ? ValueOpcode::LogicalAnd : ValueOpcode::LogicalOr, {Value(&*lhs), Value(&*rhs)})));
}
}
void FoldInstruction(Block& block, Block::iterator instruction,
FoldWorklist& worklist) {
auto& inst = *instruction;
@@ -630,10 +664,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
FoldU32(inst, [](uint32_t a, uint32_t b) { return std::max(a, b); });
return;
case ValueOpcode::IEqual32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; });
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; }))
FoldZeroComparison(block, instruction, worklist);
return;
case ValueOpcode::INotEqual32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; });
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; }))
FoldZeroComparison(block, instruction, worklist);
return;
case ValueOpcode::ULessThan32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a < b; });
@@ -695,19 +731,43 @@ void FoldInstruction(Block& block, Block::iterator instruction,
const auto lhs = Arg(inst, 0);
const auto rhs = Arg(inst, 1);
const auto simplify = [&](Value assumption, Value expression) {
const auto* disjunction = expression.TryInstruction();
if (disjunction == nullptr || disjunction->GetOpcode() != ValueOpcode::LogicalOr) {
return false;
const auto* test = expression.TryInstruction();
if (test == nullptr) return false;
const auto replace = [&](Value value) {
inst.SetArg(assumption == lhs ? 1u : 0u, value);
return true;
};
const auto negate = [&](Value value) {
return Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot, {value}));
};
const bool inverse = test->GetOpcode() == ValueOpcode::LogicalNot;
if (inverse) {
test = Arg(*test, 0).TryInstruction();
if (test == nullptr) return false;
if (test->GetOpcode() == ValueOpcode::LogicalAnd)
for (uint32_t arg = 0; arg < 2u; ++arg)
if (Arg(*test, arg) == assumption)
return replace(negate(Arg(*test, arg ^ 1u)));
}
for (uint32_t i = 0; i < 2u; ++i) {
const auto* inverse = disjunction->Arg(i).Resolve().TryInstruction();
if (inverse != nullptr && inverse->GetOpcode() == ValueOpcode::LogicalNot &&
inverse->Arg(0).Resolve() == assumption) {
inst.SetArg(assumption == lhs ? 1u : 0u,
disjunction->Arg(i ^ 1u));
return true;
if (test->GetOpcode() == ValueOpcode::IEqual32 ||
test->GetOpcode() == ValueOpcode::INotEqual32) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
if (Arg(*test, arg) != Value(0u)) continue;
const auto original = Arg(*test, arg ^ 1u);
const auto active = ResolveActiveU32(original, assumption);
if (!active.IsEmpty() && active != original) {
const auto comparison = Value(&*worklist.Prepend(block, instruction,
test->GetOpcode(), {active, Value(0u)}));
return replace(inverse ? negate(comparison) : comparison);
}
}
}
if (inverse || test->GetOpcode() != ValueOpcode::LogicalOr) return false;
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* negation = Arg(*test, arg).TryInstruction();
if (negation != nullptr && negation->GetOpcode() == ValueOpcode::LogicalNot &&
Arg(*negation, 0) == assumption) return replace(Arg(*test, arg ^ 1u));
}
return false;
};
if (simplify(lhs, rhs) || simplify(rhs, lhs)) return;
@@ -1656,9 +1656,10 @@ private:
const auto op = test->GetOpcode();
if (op == ValueOpcode::LogicalNot)
return UnsignedBound(test->Arg(0), value, !positive);
if (op == ValueOpcode::LogicalAnd && positive) {
const auto left = UnsignedBound(test->Arg(0), value);
return left.IsEmpty() ? UnsignedBound(test->Arg(1), value) : left;
if ((op == ValueOpcode::LogicalAnd && positive) ||
(op == ValueOpcode::LogicalOr && !positive)) {
const auto left = UnsignedBound(test->Arg(0), value, positive);
return left.IsEmpty() ? UnsignedBound(test->Arg(1), value, positive) : left;
}
if ((positive ? op == ValueOpcode::ULessThan32 : op == ValueOpcode::UGreaterThanEqual32) &&
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
@@ -1684,6 +1685,24 @@ private:
return guarded ? bound : Value {};
}
Value UnsignedImageBound(Value key, const Inst& handle) const {
Value bound;
for (const auto& use: handle.Uses()) {
const auto& image = *use.user;
const auto op = image.GetOpcode();
if (use.operand != 0u || (op != ValueOpcode::ImageSampleRaw &&
op != ValueOpcode::ImageGatherRaw && op != ValueOpcode::ImageRead)) return {};
// The image's own EXEC predicate proves the range for every accessing lane;
// an EXEC-nonzero branch alone only proves that some lane is active.
const auto candidate = UnsignedBound(image.Arg(image.NumArgs() - 1u), key);
if (candidate.IsEmpty() ||
!ValidateRuntimeValue(m_program, candidate, RuntimeValueType::Integer)) return {};
if (bound.IsEmpty()) bound = candidate;
else if (!EquivalentValue(m_program, candidate, bound)) return {};
}
return bound;
}
Value SharedLoadBound(Value value, Value active) {
const auto* read = ActiveValue(value, active).TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
@@ -2417,7 +2436,8 @@ private:
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
} else if (table_source.dword_count == 2u) {
const auto guarded_count = UnsignedBoundOnEntry(key, handle.Parent());
auto guarded_count = UnsignedBoundOnEntry(key, handle.Parent());
if (guarded_count.IsEmpty()) guarded_count = UnsignedImageBound(key, handle);
const auto* selector = key.Resolve().TryInstruction();
const bool bitscan = guarded_count.IsEmpty() && selector != nullptr &&
selector->GetOpcode() == ValueOpcode::FindILsb32 &&