shader: respect branch reachability when refreshing scalar resources

This commit is contained in:
nmzik
2026-10-01 18:42:53 +04:00
parent 0d0b215fab
commit 6409be2874
12 changed files with 342 additions and 122 deletions
@@ -974,6 +974,41 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
(pipeline_bind_point == vk::PipelineBindPoint::eCompute && (pipeline_bind_point == vk::PipelineBindPoint::eCompute &&
shader_stage != vk::ShaderStageFlagBits::eCompute)); shader_stage != vk::ShaderStageFlagBits::eCompute));
} }
for (const auto* reader: prepared_bindings) {
const auto& reads = reader->runtime->resources->specialization_reads;
if (reads.empty()) continue;
for (const auto* writer: prepared_bindings) {
if (writer->runtime->program->has_address_writes) {
EXIT("scalar resource reads cannot be proven disjoint from shader address writes\n");
}
}
for (const auto [address, size]: reads) {
for (const auto id: m_bound_images) {
const auto* image = m_context.GetTextureCache().m_slot_images.try_get(id);
if (image == nullptr ||
(!image->binding.shader_write && !image->binding.is_target)) continue;
for (const auto written: {image->info.data, image->info.stencil,
image->info.metadata.range}) {
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
EXIT("scalar resource reads overlap an image or attachment write\n");
}
}
}
for (const auto* writer: prepared_bindings) {
const auto& program = *writer->runtime->program;
for (uint32_t i = 0; i < writer->buffer_sources.size(); ++i) {
const auto resource = program.bindings.descriptors.front().resources[i];
if (!program.info.buffers[resource].written) continue;
const auto& written = writer->buffer_sources[i];
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
}
}
}
m_descriptor_buffers.clear(); m_descriptor_buffers.clear();
m_descriptor_images.clear(); m_descriptor_images.clear();
m_descriptor_writes.clear(); m_descriptor_writes.clear();
@@ -264,15 +264,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& program = *input_info.stage.program; const auto& program = *input_info.stage.program;
const auto& resources = *input_info.stage.resources; const auto& resources = *input_info.stage.resources;
if (TryConsumeComputeMetaClear(input_info, buffer)) { if (resources.specialization_reads.empty() &&
ResetBindings(); (TryConsumeComputeMetaClear(input_info, buffer) ||
return; TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
} thread_group_z, mode))) {
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode)) {
ResetBindings(); ResetBindings();
return; return;
} }
const bool large_workgroup = const bool large_workgroup =
(input_info.threads_num[0] * input_info.threads_num[1] * input_info.threads_num[2] >= 512); (input_info.threads_num[0] * input_info.threads_num[1] * input_info.threads_num[2] >= 512);
const bool has_sampler = !program.info.samplers.empty(); const bool has_sampler = !program.info.samplers.empty();
@@ -142,6 +142,7 @@ CompiledShaderInfo Program::TakeCompiledInfo() && {
.user_data_base = user_data_base, .user_data_base = user_data_base,
.user_data_count = user_data_count, .user_data_count = user_data_count,
.scratch_dwords = scratch_dwords, .scratch_dwords = scratch_dwords,
.has_address_writes = has_address_writes,
.info = std::move(info), .info = std::move(info),
.bindings = std::move(bindings), .bindings = std::move(bindings),
}; };
@@ -3,6 +3,7 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <utility>
#include <vector> #include <vector>
namespace Libs::Graphics::ShaderRecompiler::IR { namespace Libs::Graphics::ShaderRecompiler::IR {
@@ -34,6 +35,7 @@ struct ResourceSnapshot {
std::vector<DescriptorValue> samplers; std::vector<DescriptorValue> samplers;
std::vector<uint32_t> flattened_srt; std::vector<uint32_t> flattened_srt;
std::vector<uint32_t> user_data; std::vector<uint32_t> user_data;
std::vector<std::pair<uint64_t, uint64_t>> specialization_reads;
UniformFill uniform_fill; UniformFill uniform_fill;
}; };
+2 -1
View File
@@ -499,6 +499,7 @@ struct ResourceBlock {
Value condition; Value condition;
std::vector<uint32_t> successors; std::vector<uint32_t> successors;
std::vector<uint32_t> sources; std::vector<uint32_t> sources;
std::vector<uint32_t> srt_reads;
}; };
// Stable shader metadata consumed by the renderer after native IR has been discarded. // Stable shader metadata consumed by the renderer after native IR has been discarded.
@@ -510,6 +511,7 @@ struct CompiledShaderInfo {
uint32_t user_data_count = 64; uint32_t user_data_count = 64;
uint32_t scratch_dwords = 0; uint32_t scratch_dwords = 0;
uint32_t param_export_mask = 0; uint32_t param_export_mask = 0;
bool has_address_writes = false;
ShaderInfo info; ShaderInfo info;
BindingLayout bindings; BindingLayout bindings;
}; };
@@ -564,7 +566,6 @@ struct ResourcePlan {
mutable std::vector<uint8_t> visited_blocks; mutable std::vector<uint8_t> visited_blocks;
mutable std::vector<uint32_t> pending_blocks; mutable std::vector<uint32_t> pending_blocks;
mutable std::vector<uint32_t> material_keys; mutable std::vector<uint32_t> material_keys;
mutable std::vector<std::pair<uint64_t, uint64_t>> specialization_reads;
}; };
struct Program: ResourcePlan { struct Program: ResourcePlan {
@@ -157,34 +157,15 @@ bool CaptureStrictRead(void* userdata, uint64_t address, std::span<uint32_t> val
bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span<uint32_t> values) { bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<ReadCapture*>(userdata); auto& capture = *static_cast<ReadCapture*>(userdata);
if (!capture.source.read_memory(capture.source.userdata, address, values)) { if (capture.source.read_memory != nullptr) {
return false; if (!capture.source.read_memory(capture.source.userdata, address, values)) return false;
} else {
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
} }
capture.ranges.emplace_back(address, values.size_bytes()); capture.ranges.emplace_back(address, values.size_bytes());
return true; return true;
} }
bool WrittenBuffersDisjoint(const ResourcePlan& program, const ResourceSnapshot& snapshot,
std::span<const std::pair<uint64_t, uint64_t>> reads) {
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
if (!program.info.buffers[i].written) continue;
ShaderBufferResource buffer;
if (!DecodeBufferDescriptor(snapshot.buffers[i], buffer)) return false;
const auto base = buffer.Base48();
const auto size = buffer.GetSize();
if (size == 0u) continue;
if (size - 1u > AddressMask - base) return false;
for (const auto [address, bytes]: reads) {
if (bytes == 0u) continue;
if (address > AddressMask || bytes - 1u > AddressMask - address ||
(address <= base ? base - address < bytes : address - base < size)) {
return false;
}
}
}
return true;
}
const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) { const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
if (source >= program.descriptor_sources.size()) { if (source >= program.descriptor_sources.size()) {
return nullptr; return nullptr;
@@ -638,9 +619,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
return true; return true;
} }
static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) { template <typename Predicate>
// Any shader write may alias a scalar predicate read, including on a later loop visit. static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, const Predicate& predicate) {
if (program.blocks.size() != program.block_info.size() || HasShaderMemoryWrites(program)) { // The renderer checks captured scalar reads against final buffer and image write ranges.
if (program.blocks.size() != program.block_info.size() || program.has_address_writes) {
return {}; return {};
} }
std::unordered_map<uint32_t, uint32_t> indices; std::unordered_map<uint32_t, uint32_t> indices;
@@ -659,9 +641,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break; case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break;
case CFG::TerminatorKind::ConditionalBranch: case CFG::TerminatorKind::ConditionalBranch:
successors = {terminator.true_block, terminator.false_block}; successors = {terminator.true_block, terminator.false_block};
if (ValidateRuntimeValue(program, info.condition, RuntimeValueType::Integer)) { block.condition = info.condition;
block.condition = info.condition;
}
break; break;
case CFG::TerminatorKind::IndirectBranch: case CFG::TerminatorKind::IndirectBranch:
successors = terminator.indirect_targets; successors = terminator.indirect_targets;
@@ -678,13 +658,17 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
} }
for (const auto& inst: *program.blocks[i]) { for (const auto& inst: *program.blocks[i]) {
const auto op = inst.GetOpcode(); const auto op = inst.GetOpcode();
if (op == ValueOpcode::ReadConst) {
block.srt_reads.push_back(inst.Arg(1).Resolve().U32());
continue;
}
const auto buffer = BufferAccessOf(op); const auto buffer = BufferAccessOf(op);
const auto image = ImageOpcodeInfoOf(op); const auto image = ImageOpcodeInfoOf(op);
if (buffer == BufferAccess::None && image.access == ImageAccess::None) { if (buffer == BufferAccess::None && image.access == ImageAccess::None) {
continue; continue;
} }
const auto& memory = program.memory_info.at(inst.Flags<MemoryFlags>().index); const auto& memory = program.memory_info.at(inst.Flags<MemoryFlags>().index);
if (memory.planning_only) { if (memory.planning_only || memory.kind == ResourceKind::IndirectBuffer) {
continue; continue;
} }
if (buffer != BufferAccess::None) { if (buffer != BufferAccess::None) {
@@ -700,6 +684,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()), block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end()); block.sources.end());
} }
for (auto& block: blocks) block.condition = predicate(block.condition);
if (std::ranges::none_of( if (std::ranges::none_of(
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) { blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
return {}; return {};
@@ -914,10 +899,38 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (const auto& read: program.srt_reads) { for (const auto& read: program.srt_reads) {
plan.srt_reads.push_back({Clone(read.value), read.flat_offset}); plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
} }
plan.control_flow = ResourceControlFlow(program); // A proven uniform factor can decide a branch even when its other lanes are unknown.
for (auto& block: plan.control_flow) { // Keep only that Boolean structure, never the varying shader dependency graph.
block.condition = Clone(block.condition); Value unknown;
} std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value);
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
const auto operand = ClonePredicate(inst->Arg(0));
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, operand);
return Value(&node);
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
auto left = ClonePredicate(inst->Arg(0));
auto right = ClonePredicate(inst->Arg(1));
if (left.IsEmpty() && right.IsEmpty()) return {};
if (left.IsEmpty() || right.IsEmpty()) {
if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1));
if (left.IsEmpty()) left = unknown;
if (right.IsEmpty()) right = unknown;
}
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, left);
node.SetArg(1, right);
return Value(&node);
};
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
plan.uniform_fill = AnalyzeUniformFill(program); plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) { for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]); plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
@@ -936,12 +949,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source), MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
plan.clean_flat_slots); plan.clean_flat_slots);
} }
if (masked_image) { plan.capture_specialization_reads = masked_image || !plan.control_flow.empty();
plan.resource_tracking_complete &= !program.has_address_writes && // Writable descriptor addresses must come from clean backing for the alias proof.
!std::ranges::any_of(plan.info.images, &ImageResource::written); for (const auto& buffer: plan.info.buffers) {
plan.capture_specialization_reads = if (buffer.written) MarkCleanFlatSlots(plan, Source(plan, buffer.source), plan.clean_flat_slots);
std::ranges::any_of(plan.info.buffers, &BufferResource::written);
} }
for (const auto& image: plan.info.images) {
if (image.written) MarkCleanFlatSlots(plan, Source(plan, image.source), plan.clean_flat_slots);
}
if (masked_image) plan.resource_tracking_complete &= !program.has_address_writes;
return plan; return plan;
} }
@@ -952,21 +968,22 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return false; return false;
} }
const bool capture_reads = program.capture_specialization_reads; const bool capture_reads = program.capture_specialization_reads;
auto& reads = program.specialization_reads; auto& reads = snapshot.specialization_reads;
reads.clear();
ReadCapture capture {runtime, reads}; ReadCapture capture {runtime, reads};
SrtRuntime observed = runtime; SrtRuntime observed = runtime;
if (capture_reads) { if (capture_reads) {
reads.clear();
observed.userdata = &capture; observed.userdata = &capture;
observed.read_specialization_memory = CaptureStrictRead; observed.read_specialization_memory = runtime.read_specialization_memory != nullptr
if (observed.read_memory != nullptr) observed.read_memory = CaptureOrdinaryRead; ? CaptureStrictRead : nullptr;
observed.read_memory = CaptureOrdinaryRead;
} }
SrtWalker clean(program, CleanRuntime(observed)); SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed, program.clean_flat_slots, &clean); SrtWalker walker(program, observed, program.clean_flat_slots, &clean);
const auto active = clean.FindActiveSources();
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) { if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
return false; return false;
} }
const auto active = std::span<const uint8_t>(program.active_sources);
snapshot.uniform_fill = {}; snapshot.uniform_fill = {};
const auto& fill = program.uniform_fill; const auto& fill = program.uniform_fill;
const auto words = fill.fill.words; const auto words = fill.fill.words;
@@ -979,12 +996,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
snapshot.uniform_fill = fill.fill; snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0]; snapshot.uniform_fill.value = stored[0];
} }
const auto evaluate = [&](uint32_t source, DescriptorValue& value) { const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
if (source >= program.descriptor_sources.size()) { if (source >= program.descriptor_sources.size()) {
return false; return false;
} }
if (active.empty() || active[source]) { if (active.empty() || active[source]) {
return walker.EvaluateDescriptor(source, value); return (written ? clean : walker).EvaluateDescriptor(source, value);
} }
value = {}; value = {};
value.dword_count = program.descriptor_sources[source].dword_count; value.dword_count = program.descriptor_sources[source].dword_count;
@@ -992,19 +1009,11 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}; };
snapshot.buffers.resize(program.info.buffers.size()); snapshot.buffers.resize(program.info.buffers.size());
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) { for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
if (!evaluate(program.info.buffers[i].source, snapshot.buffers[i])) { if (!evaluate(program.info.buffers[i].source, snapshot.buffers[i],
program.info.buffers[i].written)) {
return false; return false;
} }
} }
if (capture_reads) {
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
const auto& buffer = program.info.buffers[i];
if (!buffer.written || (!active.empty() && !active[buffer.source])) continue;
DescriptorValue strict;
if (!clean.EvaluateDescriptor(buffer.source, strict) ||
strict != snapshot.buffers[i]) return false;
}
}
snapshot.images.resize(program.info.images.size()); snapshot.images.resize(program.info.images.size());
specialization.images.resize(program.info.images.size()); specialization.images.resize(program.info.images.size());
for (uint32_t i = 0; i < program.info.images.size(); ++i) { for (uint32_t i = 0; i < program.info.images.size(); ++i) {
@@ -1040,7 +1049,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return false; return false;
} }
} else { } else {
if (!evaluate(image.source, snapshot.images[i])) { if (!evaluate(image.source, snapshot.images[i], image.written)) {
return false; return false;
} }
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) { if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
@@ -1054,7 +1063,6 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return false; return false;
} }
} }
if (capture_reads && !WrittenBuffersDisjoint(program, snapshot, reads)) return false;
snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end()); snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end());
return BuildResourceSpecialization(program, snapshot, specialization); return BuildResourceSpecialization(program, snapshot, specialization);
} }
@@ -795,18 +795,26 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return true; return true;
} }
return false; return false;
case ValueOpcode::LogicalAnd: case ValueOpcode::LogicalAnd: {
if (binary()) { const bool left = Arg(inst, 0, a);
result = (a != 0u) && (b != 0u); if (left && a == 0u) {
result = 0u;
return true; return true;
} }
return false; if (!Arg(inst, 1, b) || (b != 0u && !left)) return false;
case ValueOpcode::LogicalOr: result = b != 0u;
if (binary()) { return true;
result = (a != 0u) || (b != 0u); }
case ValueOpcode::LogicalOr: {
const bool left = Arg(inst, 0, a);
if (left && a != 0u) {
result = 1u;
return true; return true;
} }
return false; if (!Arg(inst, 1, b) || (b == 0u && !left)) return false;
result = b != 0u;
return true;
}
case ValueOpcode::LogicalXor: case ValueOpcode::LogicalXor:
if (binary()) { if (binary()) {
result = (a != 0u) != (b != 0u); result = (a != 0u) != (b != 0u);
@@ -844,16 +852,31 @@ bool SrtWalker::EvaluateDescriptor(uint32_t source, DescriptorValue& result) {
return true; return true;
} }
std::span<const uint8_t> SrtWalker::FindActiveSources() { bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
if (m_program.control_flow.empty()) { if (!m_program.srt_plan_complete) return false;
return {}; const auto refresh = [&](uint32_t slot) {
} if (slot >= m_program.srt_reads.size()) return false;
const auto& read = m_program.srt_reads[slot];
const bool clean = read.flat_offset < m_clean_flat_slots.size() &&
m_clean_flat_slots[read.flat_offset] != 0u;
if (clean && (m_clean_evaluator == nullptr || m_runtime.read_specialization_memory == nullptr))
return false;
auto& evaluator = clean ? *m_clean_evaluator : *this;
return read.flat_offset < flat.size() && evaluator.Evaluate(read.value, flat[read.flat_offset]);
};
auto& active = m_program.active_sources; auto& active = m_program.active_sources;
if (m_program.control_flow.empty()) {
active.clear();
flat.resize(m_program.srt_reads.size());
for (uint32_t slot = 0; slot < m_program.srt_reads.size(); ++slot) {
if (!refresh(slot)) return false;
}
return true;
}
flat.assign(m_program.srt_reads.size(), 0u);
active.assign(m_program.descriptor_sources.size(), 1u); active.assign(m_program.descriptor_sources.size(), 1u);
for (const auto& block: m_program.control_flow) { for (const auto& block: m_program.control_flow) {
for (const auto source: block.sources) { for (const auto source: block.sources) active.at(source) = 0u;
active.at(source) = 0u;
}
} }
auto& visited = m_program.visited_blocks; auto& visited = m_program.visited_blocks;
auto& pending = m_program.pending_blocks; auto& pending = m_program.pending_blocks;
@@ -863,41 +886,22 @@ std::span<const uint8_t> SrtWalker::FindActiveSources() {
while (!pending.empty()) { while (!pending.empty()) {
const auto index = pending.back(); const auto index = pending.back();
pending.pop_back(); pending.pop_back();
if (visited.at(index)) { if (visited.at(index)) continue;
continue;
}
visited[index] = 1u; visited[index] = 1u;
const auto& block = m_program.control_flow[index]; const auto& block = m_program.control_flow[index];
for (const auto source: block.sources) { for (const auto source: block.sources) active[source] = 1u;
active[source] = 1u; for (const auto slot: block.srt_reads) {
if (!refresh(slot)) return false;
} }
uint32_t condition = 0; uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr && if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr &&
Evaluate(block.condition, condition)) { predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]); pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else { } else {
pending.insert(pending.end(), block.successors.begin(), block.successors.end()); pending.insert(pending.end(), block.successors.begin(), block.successors.end());
} }
} }
return active;
}
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
if (!m_program.srt_plan_complete) {
return false;
}
flat.resize(m_program.srt_reads.size());
for (const auto& read: m_program.srt_reads) {
const bool clean = read.flat_offset < m_clean_flat_slots.size() &&
m_clean_flat_slots[read.flat_offset] != 0u;
if (clean && (m_clean_evaluator == nullptr || m_runtime.read_specialization_memory == nullptr)) {
return false;
}
auto& evaluator = clean ? *m_clean_evaluator : *this;
if (read.flat_offset >= flat.size() || !evaluator.Evaluate(read.value, flat[read.flat_offset])) {
return false;
}
}
return true; return true;
} }
@@ -38,8 +38,7 @@ public:
bool Evaluate(Value value, uint32_t& result); bool Evaluate(Value value, uint32_t& result);
bool EvaluateDescriptor(uint32_t source, DescriptorValue& result); bool EvaluateDescriptor(uint32_t source, DescriptorValue& result);
// An empty span means that all sources are active. // Refreshes reachable scalar reads and active descriptor sources in one walk.
std::span<const uint8_t> FindActiveSources();
bool RefreshFlatBuffer(std::vector<uint32_t>& flat); bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
private: private:
+51
View File
@@ -219,6 +219,56 @@ void TestUnbasedFlatCacheHitMaterializes() {
"unbased FLAT plan produced unexpected descriptors"); "unbased FLAT plan produced unexpected descriptors");
} }
void TestWrittenDescriptorUsesStrictReaderOnce() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = true;
program.resource_tracking_complete = true;
auto &block = AddValueBlock(program);
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress});
auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource,
{Value(0x1000u), Value(0u)});
auto &offset = block.AppendNewInst(ValueOpcode::GetUserData,
{Value(static_cast<ScalarReg>(0))});
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32,
{Value(&handle), Value(&offset), Value(0u), Value(false)});
read.SetFlags(MemoryFlags{.index = 0});
DescriptorSource source;
source.dwords = {Value(&read), Value(0u), Value(4u), Value(0u)};
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{
.user_data = user_data,
.read_memory = +[](void *data, uint64_t, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
words[0] = 0x8000u;
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (!reads.clean || address != 0x1004u) return false;
words[0] = 0x8000u;
return true;
}};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1,
"GPU-dirty dynamic writable descriptor bypassed strict provenance");
reads.clean = true;
reads.strict = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u,
"writable descriptor was evaluated twice or scalar EXEC suppressed its read");
}
void TestFailedMaterializationRejectsStage() { void TestFailedMaterializationRejectsStage() {
using namespace Libs::Graphics::ShaderRecompiler::IR; using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = UserDataBufferPlan(); auto plan = UserDataBufferPlan();
@@ -262,6 +312,7 @@ int main() {
TestMappedSrtUsesDirectReaderByDefault(); TestMappedSrtUsesDirectReaderByDefault();
TestIntegerRuntimeValueFollowsSrtReads(); TestIntegerRuntimeValueFollowsSrtReads();
TestUnbasedFlatCacheHitMaterializes(); TestUnbasedFlatCacheHitMaterializes();
TestWrittenDescriptorUsesStrictReaderOnce();
TestFailedMaterializationRejectsStage(); TestFailedMaterializationRejectsStage();
TestMixedSamplerDuplicatesTheCorrectSnapshot(); TestMixedSamplerDuplicatesTheCorrectSnapshot();
std::puts("ResourceMaterializationTests: all cases passed"); std::puts("ResourceMaterializationTests: all cases passed");
+118 -6
View File
@@ -1386,11 +1386,17 @@ void TestUniformizedMaterialImageKeys() {
specialization.images[0].indirect_mapping_offset + 3u] == 4096u, specialization.images[0].indirect_mapping_offset + 3u] == 4096u,
"material mask did not limit sparse descriptor reads"); "material mask did not limit sparse descriptor reads");
user_data[8] = first_material; user_data[8] = first_material;
Check(!MaterializeResources(plan, runtime, snapshot, specialization), Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
"written buffer alias with a material key was accepted"); std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{first_material, 4}) !=
snapshot.specialization_reads.end(),
"material key read was omitted from the renderer write-overlap check");
user_data[8] = first_table; user_data[8] = first_table;
Check(!MaterializeResources(plan, runtime, snapshot, specialization), Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
"written buffer alias with an image record was accepted"); std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{first_table, 32}) !=
snapshot.specialization_reads.end(),
"image descriptor read was omitted from the renderer write-overlap check");
CheckFatal([&] { make_plan(Variant::WrongUpdate); }, "not a valid runtime value", CheckFatal([&] { make_plan(Variant::WrongUpdate); }, "not a valid runtime value",
"non-clearing material mask was accepted"); "non-clearing material mask was accepted");
CheckFatal([&] { make_plan(Variant::WrongEquality); }, "not a valid runtime value", CheckFatal([&] { make_plan(Variant::WrongEquality); }, "not a valid runtime value",
@@ -2785,6 +2791,108 @@ void TestConditionalBufferMaterialization() {
CheckActive(); CheckActive();
} }
void TestGuardedScalarDescriptorReads() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const bool shared : {false, true}) {
for (const bool exec : {false, true}) {
Fixture fixture;
auto *entry = fixture.block;
auto *optional = fixture.AddBlock();
auto *done = fixture.AddBlock();
entry->AddBranch(optional);
entry->AddBranch(done);
optional->AddBranch(done);
fixture.program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1, .false_block = 2};
fixture.program.block_info[1].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 2};
fixture.program.block_info[2].terminator.kind = CFG::TerminatorKind::Return;
const auto control = fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
fixture.UserData(2), fixture.UserData(3)});
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
scalar.offset = 28;
const auto flag = fixture.Emit(ValueOpcode::ReadConstBuffer, {control, Value(0u)},
fixture.AddMemory(scalar, 4));
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto varying = fixture.Emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
const auto enabled = fixture.Emit(ValueOpcode::INotEqual32, {flag, Value(0u)});
fixture.program.block_info[0].condition =
fixture.Emit(ValueOpcode::LogicalAnd, {varying, enabled});
const auto root = fixture.Address(fixture.UserData(4), Value(0u));
const auto Load = [&](Block *block) {
fixture.block = block;
MemoryInfo address;
address.kind = ResourceKind::ScalarAddress;
const auto pointer = fixture.Emit(ValueOpcode::LoadAddressU32,
{root, Value(0u), Value(0u), Value(exec)}, fixture.AddMemory(address, 8));
const auto table = fixture.Address(pointer, Value(0u));
std::array<Value, 4> words;
for (uint32_t word = 0; word < words.size(); ++word) {
address.offset = 40 + word * 4;
words[word] = fixture.Emit(ValueOpcode::LoadAddressU32,
{table, Value(0u), Value(0u), Value(exec)}, fixture.AddMemory(address, 12));
}
scalar.offset = 0;
fixture.Emit(ValueOpcode::ReadConstBuffer, {fixture.Buffer(words), Value(0u)},
fixture.AddMemory(scalar, 16));
};
Load(optional);
if (shared) Load(done);
fixture.block = done;
MemoryInfo store;
store.kind = ResourceKind::Buffer;
fixture.Emit(ValueOpcode::StoreBufferU32,
{fixture.Buffer({Value(0x9000u), Value(0u), Value(4u), Value(0u)}),
Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}, fixture.AddMemory(store, 20));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(!exec || (!plan.srt_reads.empty() && !plan.control_flow[1].srt_reads.empty() &&
(!shared || !plan.control_flow[2].srt_reads.empty())),
"guarded shared-read fixture lost its flattened execution sites");
struct Memory { LinearTestMemory data; uint32_t null_reads = 0; } memory;
const auto Read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &memory = *static_cast<Memory *>(data);
if (address == 40) ++memory.null_reads;
return ReadLinearTestMemory(&memory.data, address, words);
};
const std::array<uint32_t, 5> user_data{0x1000u, 0u, 64u, 0u, 0x1040u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = Read,
.userdata = &memory, .read_specialization_memory = Read};
memory.data.words[0xa8 / 4] = 0x2000u;
memory.data.words[0xb0 / 4] = 4u;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
if (!shared) {
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 0 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"disabled feature speculatively dereferenced its null BVH table");
memory.data.words[7] = 1;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1,
"active feature accepted an unreadable BVH table");
}
memory.data.words[0x40 / 4] = 0x1080u;
memory.data.watched_address = 0x10a8u;
memory.data.watched_reads = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
std::ranges::any_of(snapshot.buffers, [](const auto& descriptor) {
return descriptor.dwords[0] == 0x2000u;
}) &&
(!exec || (memory.data.watched_reads == 1 &&
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end())),
"active or shared descriptor read was omitted after refreshing the cached plan");
if (!shared) {
memory.data.words[7] = 0;
memory.data.words[0x40 / 4] = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"cached plan retained reads after the feature was disabled");
}
}
}
}
void TestConservativeBufferReachability() { void TestConservativeBufferReachability() {
std::array<uint32_t, 8> user_data{ std::array<uint32_t, 8> user_data{
0x1000, 16u << 16u, 1, 0x4dfac, 0x1000, 16u << 16u, 1, 0x4dfac,
@@ -2797,11 +2905,14 @@ void TestConservativeBufferReachability() {
auto plan = ConditionalBufferPlan(use); auto plan = ConditionalBufferPlan(use);
ResourceSnapshot snapshot; ResourceSnapshot snapshot;
ResourceSpecialization specialization; ResourceSpecialization specialization;
std::array<uint32_t, 4> expected{};
if (use != ConditionalBufferUse::Writable)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) && Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 && snapshot.buffers.size() == 2 &&
std::equal(user_data.begin() + 4, user_data.end(), std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()), snapshot.buffers[1].dwords.begin()),
"shared, loop-dependent, or writable-alias resource was pruned"); "shared or loop-dependent resource was pruned, or inactive scalar branch retained");
} }
} }
@@ -3187,6 +3298,7 @@ int main() {
Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma); Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma);
Run("buffer swizzle specialization", TestBufferSwizzleSpecialization); Run("buffer swizzle specialization", TestBufferSwizzleSpecialization);
Run("conditional buffer materialization", TestConditionalBufferMaterialization); Run("conditional buffer materialization", TestConditionalBufferMaterialization);
Run("guarded scalar descriptor reads", TestGuardedScalarDescriptorReads);
Run("conservative buffer reachability", TestConservativeBufferReachability); Run("conservative buffer reachability", TestConservativeBufferReachability);
Run("conditional indirect image", TestConditionalIndirectImageMaterialization); Run("conditional indirect image", TestConditionalIndirectImageMaterialization);
Run("shader info and bindings", TestShaderInfoAndBindingLayout); Run("shader info and bindings", TestShaderInfoAndBindingLayout);
+3 -1
View File
@@ -1497,6 +1497,7 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
.shader_base = reinterpret_cast<uint64_t>(test.code.data()), .shader_base = reinterpret_cast<uint64_t>(test.code.data()),
.read_memory = ReadTestMemory, .read_memory = ReadTestMemory,
.userdata = const_cast<std::vector<u32> *>(&test.initial), .userdata = const_cast<std::vector<u32> *>(&test.initial),
.read_specialization_memory = ReadTestMemory,
}; };
Require(test.name, "resource materialization", Require(test.name, "resource materialization",
ShaderRecompiler::IR::MaterializeResources( ShaderRecompiler::IR::MaterializeResources(
@@ -33928,7 +33929,8 @@ void CheckResourcePlanHandoff() {
ResourceSpecialization specialization; ResourceSpecialization specialization;
const SrtRuntime runtime{.user_data = user_data, const SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory, .read_memory = ReadTestMemory,
.userdata = &memory}; .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
Require(name, "initial descriptors", Require(name, "initial descriptors",
MaterializeResources(plan, runtime, snapshot, specialization) && MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 1 && snapshot.buffers[0].dwords[0] == 0x1000u, snapshot.buffers.size() == 1 && snapshot.buffers[0].dwords[0] == 0x1000u,
+14 -8
View File
@@ -8584,11 +8584,17 @@ void TestNestedSelectionPreservesDescriptorSources() {
"nested descriptor selection lost its structured resource bindings"); "nested descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) { for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
const auto pc = result.program.info.buffers[i].first_use_pc; const auto pc = result.program.info.buffers[i].first_use_pc;
const uint32_t expected = pc == 8u * 4u ? table[0] Check(pc == 8u * 4u || pc == 14u * 4u || pc == direct_arm * 4u || pc == join * 4u,
: pc == 14u * 4u ? table[4] "nested descriptor selection lost a native source location");
: pc == direct_arm * 4u ? user_data[8] const auto selected_pc = (mode == 0u ? 8u : mode == 1u ? 14u : direct_arm) * 4u;
: pc == join * 4u ? user_data[4] : 0u; const auto *expected = pc == 8u * 4u ? table.data()
Check(expected != 0u && result.resources.buffers[i].dwords[0] == expected, : pc == 14u * 4u ? table.data() + 4
: pc == direct_arm * 4u ? user_data.data() + 8 : user_data.data() + 4;
const auto &descriptor = result.resources.buffers[i];
const bool unused = pc != selected_pc && pc != join * 4u;
Check(descriptor.dword_count == 4 &&
(std::equal(expected, expected + 4, descriptor.dwords.begin()) ||
(unused && std::ranges::all_of(descriptor.dwords, [](auto word) { return word == 0; }))),
"nested descriptor selection chose another arm's descriptor"); "nested descriptor selection chose another arm's descriptor");
} }
CheckSpirvBinaryValidates(result.spirv); CheckSpirvBinaryValidates(result.spirv);
@@ -8696,10 +8702,10 @@ void TestNativeScalarReadDescriptorPlanning() {
auto options = MakeCompileOptions(ShaderType::Pixel); auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data; options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory); const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
std::ranges::equal(result.resources.flattened_srt, table),
"scalar descriptor planning retained an unrelated native execution mask");
const auto expected = mode == 2u ? table : std::array<uint32_t, 4>{}; const auto expected = mode == 2u ? table : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
std::ranges::equal(result.resources.flattened_srt, expected),
"scalar descriptor planning retained an unrelated native execution mask");
Check(std::equal(expected.begin(), expected.end(), result.resources.buffers[0].dwords.begin()), Check(std::equal(expected.begin(), expected.end(), result.resources.buffers[0].dwords.begin()),
"scalar descriptor planning selected the wrong native resource"); "scalar descriptor planning selected the wrong native resource");
CheckSpirvBinaryValidates(result.spirv); CheckSpirvBinaryValidates(result.spirv);