shader: detect full buffer fills from uniform stores

Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
This commit is contained in:
nmzik
2026-09-05 10:34:30 +04:00
parent 994271e2f6
commit dea80f34ca
8 changed files with 422 additions and 43 deletions
@@ -25,6 +25,7 @@
#include "libs/errno.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
@@ -82,46 +83,54 @@ bool ResolveComputeImageClear(const ShaderComputeInputInfo& input, uint32_t grou
uint64_t& resolved_size) {
const auto& program = *input.stage.program;
const auto& resources = input.stage.resources;
if (program.info.buffers.size() != 1 || resources.buffers.size() != 1 ||
!program.info.images.empty() || !program.info.samplers.empty() || program.info.uses_dma ||
!resources.images.empty() || !resources.samplers.empty()) {
const auto& fill = resources.buffer_fill;
if (fill.element_size == 0 || fill.element_size > 16 || fill.element_size % 4 != 0 ||
fill.buffer >= resources.buffers.size() ||
resources.buffers.size() != program.info.buffers.size()) {
return false;
}
const auto& resource = program.info.buffers.front();
const auto& raw = resources.buffers.front();
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(raw);
const auto& resource = program.info.buffers[fill.buffer];
const auto& raw = resources.buffers[fill.buffer];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(raw);
constexpr std::array formats {
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
if (!resource.formatted || !resource.written || resource.read || resource.atomic ||
resource.scalar || resource.max_byte_extent != 16 || descriptor.Stride() != 16 ||
descriptor.Format() != Prospero::BufferFormat::k32_32_32_32UInt ||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
resource.scalar || descriptor.Stride() != fill.element_size ||
descriptor.Format() != formats[fill.element_size / 4 - 1] || descriptor.SwizzleEnabled() ||
descriptor.IndexStride() != 0 || descriptor.AddTid() ||
resource.packed_stride != descriptor.PackedStride() || raw.dword_count != 4 ||
program.user_data_base != 0 || resources.user_data.size() != 8) {
descriptor.Base48() == 0) {
return false;
}
for (uint32_t i = 0; i < raw.dword_count; i++) {
if (raw.dwords[i] != resources.user_data[i]) {
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride ||
input.threads_num[1] != 1 || input.threads_num[2] != 1 || group_x == 0 || group_y != 1 ||
group_z != 1 || mode != (input.dispatch_thread_dimensions ? 0x61u : 0x41u)) {
return false;
}
const uint64_t invocations = input.dispatch_thread_dimensions
? group_x
: static_cast<uint64_t>(group_x) * input.threads_num[0];
const auto size = BufferDescriptorSize(descriptor);
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
descriptor.Base48() > UINT64_MAX - size ||
(input.dispatch_thread_dimensions &&
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
return false;
}
for (uint32_t i = 0; i < resources.buffers.size(); ++i) {
if (i == fill.buffer) continue;
const auto source = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
const auto bytes = BufferDescriptorSize(source);
if (source.Base48() > UINT64_MAX - bytes ||
(source.Base48() < descriptor.Base48() + size &&
descriptor.Base48() < source.Base48() + bytes)) {
return false;
}
}
const uint32_t clear = resources.user_data[4];
if (resources.user_data[5] != clear || resources.user_data[6] != clear ||
resources.user_data[7] != clear) {
return false;
}
const bool full_dispatch =
input.dispatch_thread_dimensions && input.threads_num[0] == 64 &&
input.threads_num[1] == 1 && input.threads_num[2] == 1 && group_x != 0 && group_y == 1 &&
group_z == 1 && input.dispatch_threads_num[0] == group_x &&
input.dispatch_threads_num[1] == 1 && input.dispatch_threads_num[2] == 1 &&
input.group_id[0] && !input.group_id[1] && !input.group_id[2] &&
input.thread_ids_num == 1 && input.wave_size == 64 && !input.tg_size_en && mode == 0x61u &&
group_x % input.threads_num[0] == 0 && descriptor.NumRecords() == group_x;
const auto size = BufferDescriptorSize(descriptor);
if (!full_dispatch || size == 0) {
return false;
}
resolved_descriptor = descriptor;
resolved_clear = clear;
resolved_clear = fill.value;
resolved_size = size;
return true;
}
@@ -16,12 +16,22 @@ struct DescriptorValue {
}
};
struct BufferFill {
uint32_t buffer = 0;
uint32_t group_stride = 0;
uint32_t element_size = 0;
uint32_t value = 0;
bool operator==(const BufferFill&) const = default;
};
struct ResourceSnapshot {
std::vector<DescriptorValue> buffers;
std::vector<DescriptorValue> images;
std::vector<DescriptorValue> samplers;
std::vector<uint32_t> flattened_srt;
std::vector<uint32_t> user_data;
BufferFill buffer_fill;
};
} // namespace Libs::Graphics::ShaderRecompiler::IR
+8 -2
View File
@@ -490,8 +490,13 @@ struct CompiledShaderInfo {
BindingLayout bindings;
};
// Immutable runtime resource analysis retained by the shader cache. It owns the descriptor/SRT
// and uniform condition values, plus resource-use edges, without retaining translated blocks.
struct BufferFillPlan {
BufferFill fill;
std::array<Value, 4> values;
};
// Immutable runtime resource analysis retained by the shader cache. It owns descriptor/SRT,
// uniform condition and fill values without retaining translated blocks.
struct ResourcePlan {
ResourcePlan() = default;
~ResourcePlan();
@@ -516,6 +521,7 @@ struct ResourcePlan {
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
ShaderInfo info;
BufferFillPlan buffer_fill;
};
struct Program: ResourcePlan {
@@ -299,8 +299,19 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r
return false;
}
auto& next = snapshot.resources;
auto cursor = values.begin();
auto& next = snapshot.resources;
const auto& fill = program.buffer_fill;
const auto words = fill.fill.element_size / sizeof(uint32_t);
std::array<uint32_t, 4> stored {};
if (words != 0 &&
EvaluateUniformValues(program, std::span(fill.values).first(words), runtime,
std::span(stored).first(words)) &&
std::all_of(stored.begin(), stored.begin() + words,
[&](uint32_t value) { return value == stored[0]; })) {
next.buffer_fill = fill.fill;
next.buffer_fill.value = stored[0];
}
auto cursor = values.begin();
next.buffers.assign(cursor, cursor + program.info.buffers.size());
cursor += program.info.buffers.size();
next.flattened_srt = std::move(flattened_srt);
@@ -609,11 +620,11 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
return true;
}
static bool IntegerCondition(const ResourcePlan& program, Value condition) {
if (!ValidateRuntimeValue(program, condition)) {
static bool UniformIntegerValue(const ResourcePlan& program, Value root) {
if (!ValidateRuntimeValue(program, root)) {
return false;
}
std::vector<Value> pending {condition};
std::vector<Value> pending {root};
std::unordered_set<const Inst*> visited;
while (!pending.empty()) {
const auto value = pending.back().Resolve();
@@ -659,7 +670,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break;
case CFG::TerminatorKind::ConditionalBranch:
successors = {terminator.true_block, terminator.false_block};
if (IntegerCondition(program, info.condition)) {
if (UniformIntegerValue(program, info.condition)) {
block.condition = info.condition;
}
break;
@@ -713,6 +724,118 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
return blocks;
}
// Nonnegative affine coefficients for constant, local X and workgroup X. Reject modular
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t depth = 0) {
value = value.Resolve();
if (depth > 32 || value.GetType() != Type::U32) {
return {};
}
if (value.IsImmediate()) {
return std::array<uint64_t, 3> {value.U32(), 0, 0};
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) {
return {};
}
const auto op = inst->GetOpcode();
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(0u)) {
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
return std::array<uint64_t, 3> {0, 1, 0};
}
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) {
return std::array<uint64_t, 3> {0, 0, 1};
}
}
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
op != ValueOpcode::ShiftLeftLogical32) {
return {};
}
auto left = FillIndex(inst->Arg(0), depth + 1);
auto right = FillIndex(inst->Arg(1), depth + 1);
if (!left || !right) {
return {};
}
if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
std::swap(left, right);
}
if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
return {};
}
if (op == ValueOpcode::ShiftLeftLogical32) {
if ((*right)[0] >= 32) return {};
(*right)[0] = uint64_t {1} << (*right)[0];
}
for (uint32_t i = 0; i < left->size(); ++i) {
(*left)[i] =
op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0];
if ((*left)[i] > UINT32_MAX) return {};
}
return left;
}
static BufferFillPlan AnalyzeBufferFill(const Program& program) {
if (program.stage != ShaderType::Compute || program.blocks.empty() ||
program.blocks.size() != program.block_info.size() || program.info.uses_dma ||
!program.info.images.empty() || !program.info.samplers.empty()) {
return {};
}
std::unordered_set<uint32_t> visited;
uint32_t index = 0;
const Inst* store = nullptr;
for (;;) {
if (!visited.insert(index).second) return {};
for (const auto& inst: *program.blocks[index]) {
if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {};
if (!inst.MayHaveSideEffects()) continue;
if (store != nullptr || BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write)
return {};
store = &inst;
}
const auto& term = program.block_info[index].terminator;
if (term.kind == CFG::TerminatorKind::Return) break;
if (term.kind != CFG::TerminatorKind::Branch) return {};
const auto next = std::ranges::find(program.block_info, term.true_block, &BlockInfo::id);
if (next == program.block_info.end()) return {};
index = static_cast<uint32_t>(next - program.block_info.begin());
}
if (store == nullptr || visited.size() != program.blocks.size()) return {};
const auto op = store->GetOpcode();
constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2,
ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4};
const auto store_op = std::ranges::find(stores, op);
if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) ||
store->Arg(3).Resolve() != Value(0u) || store->Arg(5).Resolve() != Value(true))
return {};
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 ||
memory.data_bits != 32 ||
memory.data_dwords != static_cast<uint32_t>(store_op - stores.begin() + 1))
return {};
const auto address = FillIndex(store->Arg(1));
if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {};
for (const auto& buffer: program.info.buffers) {
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
}
BufferFillPlan result;
result.fill = {memory.resource, static_cast<uint32_t>((*address)[2]), memory.data_dwords * 4,
0};
const auto data = store->Arg(4).Resolve();
const auto* vector = data.TryInstruction();
constexpr std::array composites {ValueOpcode::CompositeConstructU32x2,
ValueOpcode::CompositeConstructU32x3,
ValueOpcode::CompositeConstructU32x4};
for (uint32_t i = 0; i < memory.data_dwords; ++i) {
if (memory.data_dwords > 1 &&
(vector == nullptr || vector->GetOpcode() != composites[memory.data_dwords - 2]))
return {};
const auto word = memory.data_dwords == 1 ? data : vector->Arg(i);
if (word.GetType() != Type::U32 || !UniformIntegerValue(program, word)) return {};
result.values[i] = word;
}
return result;
}
ResourcePlan ExtractResourcePlan(const Program& program) {
ResourcePlan plan;
plan.stage = program.stage;
@@ -772,6 +895,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (auto& block: plan.control_flow) {
block.condition = Clone(block.condition);
}
plan.buffer_fill = AnalyzeBufferFill(program);
for (uint32_t i = 0; i < plan.buffer_fill.fill.element_size / sizeof(uint32_t); ++i) {
plan.buffer_fill.values[i] = Clone(plan.buffer_fill.values[i]);
}
plan.materialization_sources.reserve(plan.info.buffers.size() + plan.info.images.size() +
plan.info.samplers.size());
for (const auto& buffer: plan.info.buffers) {
@@ -1034,6 +1034,24 @@ void BuildSrtPlan(Program& program) {
program.srt_plan_complete = true;
}
bool EvaluateUniformValues(const ResourcePlan& program, std::span<const Value> values,
const SrtRuntime& runtime, std::span<uint32_t> results) {
if (values.size() != results.size()) {
return false;
}
auto clean = runtime;
clean.read_memory = runtime.read_specialization_memory != nullptr
? runtime.read_specialization_memory
: +[](void*, uint64_t, uint32_t*) { return false; };
Evaluator evaluator(program, clean);
for (size_t i = 0; i < values.size(); ++i) {
if (!evaluator.Evaluate(values[i], results[i])) {
return false;
}
}
return true;
}
bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source,
const SrtRuntime& runtime, DescriptorValue& result) {
std::vector<DescriptorValue> results;
@@ -23,6 +23,8 @@ struct SrtRuntime {
// dynamic offsets remain explicit and are never assigned a fake slot.
void BuildSrtPlan(Program& program);
bool ValidateRuntimeValue(const ResourcePlan& program, Value value);
bool EvaluateUniformValues(const ResourcePlan& program, std::span<const Value> values,
const SrtRuntime& runtime, std::span<uint32_t> results);
bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source,
const SrtRuntime& runtime, DescriptorValue& result);