shader: retain runtime count bounds for finite descriptor choices

This commit is contained in:
nmzik
2026-10-15 16:37:56 +02:00
parent df92a08990
commit 2caf2f7e4e
4 changed files with 192 additions and 38 deletions
@@ -292,7 +292,7 @@ bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
indirect.key_count.IsEmpty() && indirect.workgroup_axis == UINT32_MAX &&
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
table != nullptr && table->dword_count == 4u;
}
@@ -609,11 +609,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
const auto child_count = pointer != nullptr ? pointer->count : 1u;
uint64_t domain = indirect.table_maximum ? uint64_t {*indirect.table_maximum} + 1u
: uint64_t {1} << 32u;
if (!indirect.key_count.IsEmpty()) {
uint32_t key_count = 0u;
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
domain = std::min(domain, uint64_t {key_count});
}
if (indexed_pointer &&
(!indirect.table_maximum || table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u || (table.Stride() & 3u) != 0u ||
uint64_t {indirect.table_immediate} + 8u > table.Stride() ||
*indirect.table_maximum >= table.NumRecords())) return failed("indexed pointer geometry");
domain > table.NumRecords()))
return failed("indexed pointer geometry", fmt::format("rows={}", domain));
const bool affine = indirect.table_stride != 0u || indexed_pointer;
const bool image_heap = dword_count == 8u && pointer == nullptr;
const uint64_t step = indexed_pointer ? table.Stride()
@@ -623,8 +631,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
const uint64_t first = ((indirect.table_maximum ? indirect.table_offset
: indirect.table_offset % step) & ~uint64_t {3}) +
(indirect.table_immediate & ~3u);
const uint64_t domain = indirect.table_maximum ? uint64_t {*indirect.table_maximum} + 1u
: (uint64_t {1} << 32u) / step;
if (!indirect.table_maximum) domain /= step;
const auto count = table_size >= first + 4u
? std::min((table_size - first - 4u) / step + 1u, domain)
: 0u;
@@ -2788,19 +2788,26 @@ private:
bool SetExactTableMaximum(Value key, DescriptorSource::IndirectDescriptor& indirect,
const Inst* handle = nullptr) {
uint32_t maximum = 0u;
const auto* lane = key.Resolve().TryInstruction();
if (lane != nullptr && lane->GetOpcode() != ValueOpcode::ReadFirstLane) lane = nullptr;
SelectorDomain result {0u};
if (handle != nullptr) {
if (handle->Uses().empty()) return false;
for (const auto& use: handle->Uses()) {
if (ImageOpcodeInfoOf(use.user->GetOpcode()).access == ImageAccess::None) return false;
maximum = std::max(maximum, SelectorMaximum(key, {}, 0u,
use.user->Arg(use.user->NumArgs() - 1u)));
const auto active = use.user->Arg(use.user->NumArgs() - 1u);
if (lane != nullptr) {
if (!Implies(active, lane->Arg(1))) return false;
} else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active));
}
} else maximum = SelectorMaximum(key);
if (maximum > UINT32_MAX / 4u ||
uint64_t {maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
}
if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
else if (handle == nullptr) result.maximum = SelectorMaximum(key);
if (result.maximum > UINT32_MAX / 4u ||
uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
return false;
indirect.table_maximum = maximum;
indirect.table_maximum = result.maximum;
indirect.key_count = result.count;
return true;
}
@@ -3272,7 +3279,12 @@ private:
return false;
}
uint32_t FiniteSelectorMaximum(Value value, Value guard) const {
struct SelectorDomain {
uint32_t maximum = UINT32_MAX;
Value count;
};
SelectorDomain FiniteSelectorDomain(Value value, Value guard) const {
value = value.Resolve();
bool nonnegative = false;
if (const auto* root = value.TryInstruction()) {
@@ -3286,31 +3298,55 @@ private:
Implies(guard, Value(use.user))) nonnegative = true;
}
}
std::vector<Value> pending {value};
struct Arm { Value value; const Inst* choice = nullptr; bool positive = true; };
std::vector<Arm> pending {{value}};
std::vector<const Inst*> visited;
uint32_t maximum = 0u;
bool found = false;
bool counted = true;
Value count;
for (size_t at = 0; at < pending.size(); ++at) {
uint32_t literal;
const auto current = pending[at].Resolve();
const auto current = ActiveValue(pending[at].value, guard);
if (ImmediateU32(current, literal)) {
if (nonnegative && std::bit_cast<int32_t>(literal) < 0) continue;
maximum = std::max(maximum, literal);
found = true;
if (counted) {
Value candidate;
if (const auto* choice = pending[at].choice; choice != nullptr) {
if (choice->GetOpcode() == ValueOpcode::SelectU32)
candidate = UnsignedBound(choice->Arg(0), current, pending[at].positive,
false, pending[at].positive ? choice->Arg(0) : Value {});
if (candidate.IsEmpty() && choice->Parent() != nullptr)
candidate = UnsignedBoundOnEntry(current, choice->Parent());
}
if (candidate.IsEmpty() || !ValidateRuntimeValue(m_program, candidate, RuntimeValueType::Integer) ||
(!count.IsEmpty() && !EquivalentValue(m_program, count, candidate))) counted = false;
else count = candidate;
}
continue;
}
const auto* choice = current.TryInstruction();
if (choice == nullptr) return UINT32_MAX;
if (choice == nullptr) return {};
const bool phi = choice->GetOpcode() == ValueOpcode::Phi;
if ((!phi && choice->GetOpcode() != ValueOpcode::SelectU32) ||
(phi && (choice->NumArgs() == 0u || choice->Parent() == nullptr ||
InCycle(choice->Parent())))) return UINT32_MAX;
InCycle(choice->Parent())))) return {};
// Each literal uses only its own choice and block-entry proof. A shared
// choice therefore cannot borrow a bound from a different parent path.
if (std::ranges::find(visited, choice) != visited.end()) continue;
visited.push_back(choice);
for (size_t arm = phi ? 0u : 1u; arm < choice->NumArgs(); ++arm)
pending.push_back(choice->Arg(arm));
pending.push_back({choice->Arg(arm), choice, arm == 1u});
}
return found ? maximum : UINT32_MAX;
return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {};
}
SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const {
if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent())
: !Implies(guard, lane.Arg(1))) return {};
return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
}
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
@@ -3327,9 +3363,7 @@ private:
};
switch (inst->GetOpcode()) {
case ValueOpcode::ReadFirstLane:
if (guard.IsEmpty() ? !HasActiveLane(inst->Arg(1), inst->Parent())
: !Implies(guard, inst->Arg(1))) return UINT32_MAX;
return FiniteSelectorMaximum(inst->Arg(0), inst->Arg(1));
return SelectedFiniteDomain(*inst, guard).maximum;
case ValueOpcode::ReadLane:
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
return PossibleU32Bits(inst->Arg(0));