shader: resolve descriptors from control flow

This commit is contained in:
nmzik
2026-09-28 03:44:37 +04:00
parent 5fddd12b78
commit 06f73f07d8
11 changed files with 447 additions and 227 deletions
@@ -548,30 +548,40 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
StageName(options.stage), options.shader_hash);
auto cfg = CFG::BuildGraph(decoded);
auto native_cfg = CFG::BuildGraph(decoded);
CFG::Graph structured_cfg;
auto* selected_cfg = &native_cfg;
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
static_cast<uint64_t>(cfg.back_edges.size()), phase_ms());
if (cfg.irreducible) {
LogDispatcherFallback(options, cfg, "build");
static_cast<uint64_t>(native_cfg.blocks.size()),
static_cast<uint64_t>(native_cfg.natural_loops.size()),
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
if (native_cfg.irreducible) {
LogDispatcherFallback(options, native_cfg, "build");
} else {
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
if (!CFG::Structurize(cfg)) {
LogDispatcherFallback(options, cfg, "structurize");
structured_cfg = CFG::Structurize(native_cfg);
if (structured_cfg.unsupported) {
native_cfg.unsupported = true;
native_cfg.failure_kind = structured_cfg.failure_kind;
native_cfg.failure_block = structured_cfg.failure_block;
native_cfg.unsupported_reason = structured_cfg.unsupported_reason;
LogDispatcherFallback(options, native_cfg, "structurize");
} else {
selected_cfg = &structured_cfg;
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
static_cast<uint64_t>(cfg.blocks.size()));
static_cast<uint64_t>(selected_cfg->blocks.size()));
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(cfg.blocks.size()),
static_cast<uint64_t>(cfg.natural_loops.size()), phase_ms());
static_cast<uint64_t>(selected_cfg->blocks.size()),
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
}
const auto& cfg = *selected_cfg;
Frontend::EmbeddedFetchPlan embedded_fetch;
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
@@ -616,7 +626,7 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
LowerTessellationMemory(ir, options);
IR::BuildSrtPlan(ir);
IR::EliminateDeadCode(ir.blocks);
IR::TrackResources(ir);
IR::TrackResources(ir, decoded, native_cfg);
IR::EliminateDeadCode(ir.blocks);
TranslateResult result;
result.program = std::move(ir);
@@ -1758,16 +1758,13 @@ Graph BuildGraph(const Decoder::Program& program) {
return graph;
}
bool Structurize(Graph& graph) {
if (graph.unsupported || graph.irreducible) return false;
auto structured = GotoStructurizer(graph).Run();
if (structured.unsupported) {
SetFailure(graph, structured.failure_kind, structured.failure_block,
structured.unsupported_reason);
return false;
Graph Structurize(const Graph& graph) {
if (graph.unsupported || graph.irreducible) {
Graph failed;
SetFailure(failed, graph.failure_kind, graph.failure_block, graph.unsupported_reason);
return failed;
}
graph = std::move(structured);
return true;
return GotoStructurizer(graph).Run();
}
std::string BranchConditionToString(BranchCondition condition) {
@@ -119,9 +119,9 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
// Commits structured control flow on success; preserves the original graph with
// failure diagnostics on failure. failure_block is an original block ID or UINT32_MAX.
bool Structurize(Graph& graph);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
std::string BranchConditionToString(BranchCondition condition);
std::string FailureKindToString(FailureKind kind);
std::string GraphToString(const Graph& graph);
@@ -317,6 +317,7 @@ void Translator::WriteRawU32(const Decoder::Operand& operand, IR::U32 value) {
case Decoder::OperandKind::Sgpr: {
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
ir.SetScalarReg(reg, value);
program.scalar_writes.push_back({current_pc, reg});
ir.SetScalarMaskTag(reg, IR::U1(IR::Value(false)));
if (IR::RegIndex(reg) > 0u) {
ir.SetScalarMaskTag(static_cast<IR::ScalarReg>(IR::RegIndex(reg) - 1u),
@@ -699,11 +700,13 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
IR::U1(IR::Value(false)));
}
ir.SetScalarReg(reg, mask[0]);
program.scalar_writes.push_back({current_pc, reg});
// A wave32 VALU mask destination must not overwrite the neighboring SGPR.
if ((write_64 || program.wave_size == 64u) &&
IR::RegIndex(reg) + 1u < IR::NumScalarRegs) {
const auto high = static_cast<IR::ScalarReg>(IR::RegIndex(reg) + 1u);
ir.SetScalarReg(high, mask[1]);
program.scalar_writes.push_back({current_pc, high});
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
}
@@ -81,7 +81,8 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
program.memory_info[li] != program.memory_info[ri]) {
return false;
}
} else if (lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
} else if (lhs->GetOpcode() != ValueOpcode::ReadConst &&
lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
return false;
}
for (size_t index = 0; index < lhs->NumArgs(); index++) {
@@ -583,6 +583,8 @@ struct Program: ResourcePlan {
CFG::FailureKind cfg_failure_kind = CFG::FailureKind::None;
std::string fallback_reason;
std::vector<BlockInfo> block_info;
struct ScalarWrite { uint32_t pc; ScalarReg reg; };
std::vector<ScalarWrite> scalar_writes;
// Typed memory and export instructions reference shader-local metadata by dense index.
// Decoder-only details (such as NSA register numbers) have already become IR operands.
std::vector<ExportInfo> export_info;
@@ -82,7 +82,10 @@ uint32_t ByteExtent(const MemoryInfo& memory) {
class Tracker {
public:
explicit Tracker(Program& program): m_program(program), m_info(program.info) {
Tracker(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg)
: m_program(program), m_decoded(decoded), m_native_cfg(native_cfg),
m_scalar_writes(std::move(program.scalar_writes)), m_info(program.info) {
std::ranges::sort(m_scalar_writes, {}, &Program::ScalarWrite::pc);
m_info.buffers.clear();
m_info.images.clear();
m_info.samplers.clear();
@@ -169,7 +172,98 @@ private:
std::abort();
}
Value LowerDescriptorPhi(Value value, const Block* use) {
Value NativeDescriptorSource(Value value, uint32_t reg, uint32_t use_pc) const {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi || m_native_cfg.blocks.empty())
return value;
std::vector<const Inst*> candidates;
std::vector<const Inst*> visited;
std::vector<const Inst*> pending {phi};
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
if (std::ranges::find(visited, inst) != visited.end()) continue;
visited.push_back(inst);
if (inst->GetOpcode() == ValueOpcode::Phi) {
for (size_t i = 0; i < inst->NumArgs(); ++i) {
const auto* arg = inst->Arg(i).Resolve().TryInstruction();
if (arg != nullptr) pending.push_back(arg);
}
} else if (inst->GetOpcode() == ValueOpcode::ReadConst ||
inst->GetOpcode() == ValueOpcode::GetUserData) {
candidates.push_back(inst);
}
}
if (candidates.empty()) return value;
const auto source_at = [&](uint32_t pc) {
Value source;
const auto native = std::ranges::lower_bound(m_decoded.instructions, pc, {},
&Decoder::Instruction::pc);
for (const auto* candidate: candidates) {
if (pc == UINT32_MAX) {
if (candidate->GetOpcode() != ValueOpcode::GetUserData ||
RegIndex(candidate->Arg(0).ScalarRegister()) != reg)
continue;
} else {
if (candidate->GetOpcode() != ValueOpcode::ReadConst) continue;
const auto flags = candidate->Flags<MemoryFlags>();
if (flags.pc != pc || flags.index >= m_program.memory_info.size()) continue;
if (native == m_decoded.instructions.end() || native->pc != pc ||
native->dst.kind != Decoder::OperandKind::Sgpr ||
native->dst.reg + m_program.memory_info[flags.index].component_index != reg)
continue;
}
const Value current(const_cast<Inst*>(candidate));
if (!source.IsEmpty() && !EquivalentValue(m_program, source, current)) return Value {};
source = current;
}
return source;
};
const auto use = std::ranges::find_if(m_native_cfg.blocks, [&](const auto& block) {
return block.start_pc <= use_pc && use_pc < block.end_pc;
});
if (use == m_native_cfg.blocks.end()) return value;
struct Position { uint32_t block; uint32_t before; };
std::vector<Position> positions {{use->id, use_pc}};
std::vector<bool> reached(m_native_cfg.blocks.size());
Value selected;
const auto select = [&](uint32_t pc) {
const auto source = source_at(pc);
if (source.IsEmpty() || (!selected.IsEmpty() && !EquivalentValue(m_program, selected, source)))
return false;
selected = source;
return true;
};
while (!positions.empty()) {
const auto position = positions.back();
positions.pop_back();
const auto& block = m_native_cfg.blocks[position.block];
// A backedge may revisit the use block after a write later than the original use.
if (position.before == block.end_pc) {
if (reached[block.id]) continue;
reached[block.id] = true;
}
auto write = std::ranges::lower_bound(m_scalar_writes, position.before, {},
&Program::ScalarWrite::pc);
bool found = false;
while (write != m_scalar_writes.begin()) {
--write;
if (write->pc < block.start_pc) break;
if (RegIndex(write->reg) != reg) continue;
if (!select(write->pc)) return value;
found = true;
break;
}
if (found) continue;
if (block.id == m_native_cfg.entry_block && !select(UINT32_MAX)) return value;
for (const auto pred: block.predecessors)
positions.push_back({pred, m_native_cfg.blocks[pred].end_pc});
}
return selected.IsEmpty() ? value : selected;
}
Value LowerDescriptorPhi(Value value) {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi ||
@@ -183,42 +277,6 @@ private:
branch == phi->PhiBlock(1)) {
return value;
}
// Structurization can merge the descriptor and its use predicate in parallel Phis.
// Match their incoming blocks to exclude only edges that cannot reach this use.
if (use != nullptr && use->ImmPredecessors().size() == 1u &&
use->ImmPredecessors()[0] == merge) {
const auto merge_it = std::ranges::find(m_program.blocks, merge);
const auto use_it = std::ranges::find(m_program.blocks, use);
if (merge_it != m_program.blocks.end() && use_it != m_program.blocks.end()) {
const auto& info = m_program.block_info[merge_it - m_program.blocks.begin()];
const auto& term = info.terminator;
const auto id = m_program.block_info[use_it - m_program.blocks.begin()].id;
const auto* condition = info.condition.Resolve().TryInstruction();
if (term.kind == CFG::TerminatorKind::ConditionalBranch &&
term.true_block != term.false_block &&
(id == term.true_block || id == term.false_block) && condition != nullptr &&
condition->GetOpcode() == ValueOpcode::Phi && condition->GetType() == Type::U1 &&
condition->Parent() == merge && condition->NumArgs() == 2u &&
condition->NumPhiBlocks() == 2u) {
const bool taken = id == term.true_block;
for (uint32_t skipped = 0; skipped < 2u; skipped++) {
const auto excluded = condition->Arg(skipped).Resolve();
const auto included = condition->Arg(skipped ^ 1u).Resolve();
if (!excluded.IsImmediate() || excluded.GetType() != Type::U1 ||
excluded.U1() == taken ||
(included.IsImmediate() && included.U1() != taken)) {
continue;
}
for (uint32_t selected = 0; selected < 2u; selected++) {
if (phi->PhiBlock(selected) == condition->PhiBlock(skipped ^ 1u) &&
phi->PhiBlock(selected ^ 1u) == condition->PhiBlock(skipped)) {
return phi->Arg(selected);
}
}
}
}
}
}
for (const auto& [original, selected]: m_descriptor_selections) {
if (original == phi) {
return selected;
@@ -276,14 +334,16 @@ private:
}
void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust,
DescriptorSource& descriptor, uint32_t pc) {
uint32_t base_reg, DescriptorSource& descriptor, uint32_t pc) {
if (handle.NumArgs() != width) {
Fail(pc, fmt::format("{} has {} descriptor dwords, expected {}",
ValueOpcodeName(handle.GetOpcode()), handle.NumArgs(), width));
}
descriptor.dword_count = width;
for (uint32_t i = 0; i < width; i++) {
descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i), handle.Parent());
const auto value = base_reg != UINT32_MAX
? NativeDescriptorSource(handle.Arg(i), base_reg + i, pc) : handle.Arg(i);
descriptor.dwords[i] = LowerDescriptorPhi(value);
}
if (sample_adjust) {
descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]);
@@ -404,7 +464,7 @@ private:
if (width == 0u) {
return false;
}
MakeSource(handle, width, false, false, descriptor, pc);
MakeSource(handle, width, false, false, UINT32_MAX, descriptor, pc);
uint32_t bad_dword = 0;
return ValidateSource(descriptor, bad_dword);
}
@@ -1014,14 +1074,15 @@ private:
}
}
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
uint32_t& source, bool sampler = false, bool sample_adjust = false) {
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc,
uint32_t base_reg, Inst*& handle, uint32_t& source, bool sampler = false,
bool sample_adjust = false) {
handle = value.Resolve().TryInstruction();
if (handle == nullptr || handle->GetOpcode() != expected) {
Fail(pc, fmt::format("memory operation requires {}", ValueOpcodeName(expected)));
}
DescriptorSource descriptor;
MakeSource(*handle, width, sampler, sample_adjust, descriptor, pc);
MakeSource(*handle, width, sampler, sample_adjust, base_reg, descriptor, pc);
uint32_t bad_dword = 0;
if (!ValidateSource(descriptor, bad_dword)) {
if (expected == ValueOpcode::GetBufferResource &&
@@ -1204,8 +1265,8 @@ private:
uint32_t resource = 0;
if (buffer != BufferAccess::None) {
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle,
source)) {
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc,
memory.resource * 4u, handle, source)) {
if (memory.kind != ResourceKind::Buffer || !memory.SupportsIndirectBufferLoad(op)) {
Fail(flags.pc,
"buffer descriptor is not a valid runtime value; GPU-selected access "
@@ -1255,7 +1316,8 @@ private:
if (indirect != nullptr) {
source = indirect->source;
} else {
GetHandle(inst.Arg(0), ValueOpcode::GetImageResource, 8, flags.pc, handle, source);
GetHandle(inst.Arg(0), ValueOpcode::GetImageResource, 8, flags.pc,
memory.resource * 4u, handle, source);
}
resource = AddImage(source, memory, op, flags.pc);
if (resource == UINT32_MAX) {
@@ -1271,8 +1333,8 @@ private:
uint32_t sampler_source = 0;
const bool sample_adjust =
(memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0;
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc, sampler_handle,
sampler_source, true, sample_adjust);
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc,
memory.sampler * 4u, sampler_handle, sampler_source, true, sample_adjust);
sampler = AddSampler(sampler_source, flags.pc);
if (sampler == UINT32_MAX) {
Fail(flags.pc, "sampler resource limit exceeded");
@@ -1313,6 +1375,9 @@ private:
}
Program& m_program;
const Decoder::Program& m_decoded;
const CFG::Graph& m_native_cfg;
std::vector<Program::ScalarWrite> m_scalar_writes;
ShaderInfo m_info;
std::vector<DescriptorSource> m_sources;
std::vector<HandlePatch> m_handle_patches;
@@ -1324,8 +1389,8 @@ private:
} // namespace
void TrackResources(Program& program) {
Tracker(program).Run();
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg) {
Tracker(program, decoded, native_cfg).Run();
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -7,7 +7,7 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
// Collects immutable resource topology from typed SSA handles, interns their resolved dwords in
// descriptor_sources, then writes dense indices to handle flags and MemoryInfo.
void TrackResources(Program& program);
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg);
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -430,7 +430,8 @@ private:
const auto resource =
Value(&*block->PrependNewInst(where, ValueOpcode::GetSrtResource));
const auto flat = Value(&*block->PrependNewInst(where, ValueOpcode::ReadConst,
{resource, Value(patch.slot)}));
{resource, Value(patch.slot)},
patch.inst->Flags<uint64_t>()));
const auto uses = patch.inst->Uses();
for (const auto& use: uses) {
use.user->SetArg(use.operand, flat);
+9 -94
View File
@@ -127,7 +127,7 @@ struct Fixture {
void PlanAndTrack() {
BuildSrtPlan(program);
TrackResources(program);
TrackResources(program, {}, {});
}
};
@@ -520,7 +520,7 @@ void TestInvariantIndirectImageMaterialization() {
auto malformed = MakeIndirectImageFixture(true);
BuildSrtPlan(malformed->program);
CheckFatal([&] { TrackResources(malformed->program); }, "not a valid runtime value",
CheckFatal([&] { TrackResources(malformed->program, {}, {}); }, "not a valid runtime value",
"malformed indirect image pattern was accepted");
Check(!malformed->program.resource_tracking_complete &&
malformed->program.info.images.empty() &&
@@ -529,7 +529,7 @@ void TestInvariantIndirectImageMaterialization() {
auto wrapped_immediate = MakeIndirectImageFixture(false, 4u);
BuildSrtPlan(wrapped_immediate->program);
CheckFatal([&] { TrackResources(wrapped_immediate->program); },
CheckFatal([&] { TrackResources(wrapped_immediate->program, {}, {}); },
"not a valid runtime value",
"wrapped scalar immediate entered the invariant image proof");
Check(!wrapped_immediate->program.resource_tracking_complete,
@@ -1405,7 +1405,7 @@ void TestDenseBufferTracking() {
fixture.program.memory_info[other_flags.index].resource == 1,
"typed memory metadata was not patched to dense indices");
CheckFatal([&] { TrackResources(fixture.program); }, "already tracked",
CheckFatal([&] { TrackResources(fixture.program, {}, {}); }, "already tracked",
"resource tracking allowed a second mutation pass");
}
@@ -1602,7 +1602,7 @@ void TestSampleAdjustSamplerScratch() {
{rejected_image, rejected_sampler, rejected.ImageAddress()},
rejected.AddMemory(rejected_memory, 0x200));
BuildSrtPlan(rejected.program);
CheckFatal([&] { TrackResources(rejected.program); },
CheckFatal([&] { TrackResources(rejected.program, {}, {}); },
"not a valid runtime value", message);
};
CheckRejected(0u, 12u,
@@ -1991,7 +1991,7 @@ void TestPhiValidation() {
fixture.AddMemory(memory, 20), merge);
BuildSrtPlan(fixture.program);
CheckFatal([&] { TrackResources(fixture.program); }, "not a valid runtime value",
CheckFatal([&] { TrackResources(fixture.program, {}, {}); }, "not a valid runtime value",
"control-dependent descriptor phi was accepted");
Check(!fixture.program.resource_tracking_complete &&
fixture.program.info.buffers.empty() &&
@@ -2155,90 +2155,6 @@ void TestConditionalSamplerPhi() {
}
}
void TestGuardedSamplerPhi() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
const auto make_plan = [](bool take_true, bool reverse_phi, bool bypass,
bool mismatched, bool ambiguous) {
Fixture fixture(ShaderType::Pixel);
auto *entry = fixture.block;
auto *loaded = fixture.AddBlock();
auto *merge = fixture.AddBlock();
auto *sample = fixture.AddBlock();
auto *exit = fixture.AddBlock();
entry->AddBranch(loaded);
entry->AddBranch(merge);
loaded->AddBranch(merge);
merge->AddBranch(sample);
merge->AddBranch(exit);
sample->AddBranch(exit);
if (bypass) exit->AddBranch(sample);
const auto lane = fixture.Emit(ValueOpcode::LaneId);
const auto predicate = fixture.Emit(ValueOpcode::IEqual32, {lane, Value(0u)});
fixture.program.block_info[0].condition = predicate;
fixture.program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = 1u, .false_block = 2u};
fixture.program.block_info[1].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 2u};
auto &guard = merge->AppendNewInst(ValueOpcode::Phi, {},
static_cast<uint64_t>(Type::U1));
guard.AddPhiOperand(entry, ambiguous ? predicate : Value(!take_true));
guard.AddPhiOperand(mismatched ? sample : loaded, predicate);
fixture.program.block_info[2].condition = Value(&guard);
fixture.program.block_info[2].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = take_true ? 3u : 4u,
.false_block = take_true ? 4u : 3u};
fixture.program.block_info[3].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 4u};
fixture.program.block_info[4].terminator = {
.kind = bypass ? CFG::TerminatorKind::Branch : CFG::TerminatorKind::Return,
.true_block = 3u};
std::array<Value, 4> words;
for (uint32_t word = 0; word < words.size(); ++word) {
// An arbitrary excluded value proves this is control-flow reasoning, not null filtering.
const auto first = Value(0xbad000u + word);
const auto second = fixture.UserData(word);
auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {},
static_cast<uint64_t>(Type::U32));
phi.AddPhiOperand(reverse_phi ? loaded : entry, reverse_phi ? second : first);
phi.AddPhiOperand(reverse_phi ? entry : loaded, reverse_phi ? first : second);
words[word] = Value(&phi);
}
fixture.block = sample;
const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u), Value(0u)});
const auto sampler = fixture.Sampler(words);
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x2a0));
fixture.PlanAndTrack();
Check(words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi,
"guarded host descriptor selection rewrote the GPU Phi");
return ExtractResourcePlan(fixture.program);
};
for (const bool take_true : {false, true}) {
for (const bool reverse_phi : {false, true}) {
auto plan = make_plan(take_true, reverse_phi, false, false, false);
const std::array<uint32_t, 4> user_data{0x444u, 0x555u, 0x666u, 0x777u};
DescriptorValue selected;
Check(SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor(
plan.info.samplers[0].source, selected) &&
std::equal(user_data.begin(), user_data.end(), selected.dwords.begin()),
"guarded sampler did not match descriptor and predicate predecessors");
}
}
CheckFatal([&] { make_plan(true, false, true, false, false); },
"not a valid runtime value", "sampler guard accepted an unguarded path");
CheckFatal([&] { make_plan(true, false, false, true, false); },
"not a valid runtime value", "sampler guard ignored predecessor identity");
CheckFatal([&] { make_plan(true, false, false, false, true); },
"not a valid runtime value", "sampler guard discarded a reachable alternative");
}
void TestLoopCycleEnteredThroughRuntimeValue() {
Fixture fixture;
auto *entry = fixture.block;
@@ -2852,7 +2768,7 @@ void TestResourceLimitIsTransactional() {
fixture.AddMemory(memory, index * 4u));
}
BuildSrtPlan(fixture.program);
CheckFatal([&] { TrackResources(fixture.program); },
CheckFatal([&] { TrackResources(fixture.program, {}, {}); },
"buffer resource limit exceeded",
"resource-limit failure was not reported");
Check(!fixture.program.resource_tracking_complete &&
@@ -2872,7 +2788,7 @@ void TestMalformedMemoryKindsRejected() {
fixture.AddMemory(memory, 4));
BuildSrtPlan(fixture.program);
CheckFatal(
[&] { TrackResources(fixture.program); },
[&] { TrackResources(fixture.program, {}, {}); },
"address operation has invalid resource kind",
"resource tracking accepted an address opcode with buffer metadata");
}
@@ -2889,7 +2805,7 @@ void TestMalformedMemoryKindsRejected() {
fixture.AddMemory(memory, 8));
BuildSrtPlan(fixture.program);
CheckFatal(
[&] { TrackResources(fixture.program); },
[&] { TrackResources(fixture.program, {}, {}); },
"image operation has invalid resource kind",
"resource tracking accepted an image opcode with address metadata");
}
@@ -2924,7 +2840,6 @@ int main() {
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
Run("phi validation", TestPhiValidation);
Run("conditional sampler phi", TestConditionalSamplerPhi);
Run("guarded sampler phi", TestGuardedSamplerPhi);
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
Run("invariant loop phi", TestInvariantLoopPhi);
Run("DMA address materialization", TestDmaAddressMaterialization);
+282 -56
View File
@@ -7778,8 +7778,8 @@ void TestNewShaderRecompilerCfgLoopHeaderDsAppendConsumeStructured() {
const auto original_block_count = graph.blocks.size();
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(graph.natural_loops.size() == 1u, "DS loop was not preserved");
Check(graph.blocks.size() == original_block_count + 1u,
"DS loop structurization did not add exactly one empty header");
@@ -7914,7 +7914,8 @@ void TestNewShaderRecompilerCfgLoopExitSharedWithSelection() {
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(block_count == 8u && graph.natural_loops.size() == 1u,
"shared loop-exit fixture has the wrong native CFG");
Check(ShaderRecompiler::CFG::Structurize(graph), graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared loop-exit routing changed semantic instruction coverage");
@@ -8029,8 +8030,8 @@ void TestNewShaderRecompilerCfgNestedLoopExitTailMergeSplit() {
ShaderRecompiler::CFG::Graph graph;
graph = ShaderRecompiler::CFG::BuildGraph(program);
const auto original_block_count = graph.blocks.size();
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(graph.blocks.size() > original_block_count,
"nested loop exit tails did not create a private inner merge");
@@ -8177,8 +8178,8 @@ void TestNewShaderRecompilerCfgLoopGatewaySelection() {
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"loop gateway structurization duplicated semantic instructions");
@@ -8216,8 +8217,8 @@ void TestNewShaderRecompilerCfgConditionalLoopHeaderSelection() {
ShaderRecompiler::CFG::Graph graph;
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_block_count = graph.blocks.size();
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(graph.blocks.size() > original_block_count,
"conditional guest loop header did not create a synthetic header");
@@ -8269,8 +8270,8 @@ void TestNewShaderRecompilerCfgMultipleLoopLatches() {
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.back_edges.size() == 2u,
"multiple-latch fixture lacks two native backedges");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared backedge construction duplicated guest instructions");
Check(graph.back_edges.size() == 1u && graph.natural_loops.size() == 1u,
@@ -8344,8 +8345,8 @@ void TestNewShaderRecompilerCfgNestedEarlyExitLoopForwarders() {
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(original_block_count == 7u && graph.natural_loops.size() == 1u,
"nested early-exit fixture has the wrong native CFG");
const bool structured = ShaderRecompiler::CFG::Structurize(graph);
Check(structured, graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == original_coverage,
"nested early-exit structurization changed semantic coverage");
std::unordered_set<uint32_t> merges;
@@ -8391,8 +8392,8 @@ void TestNewShaderRecompilerCfgExecSccSharedArm() {
Check(std::ranges::all_of(original_coverage,
[](uint32_t uses) { return uses == 1u; }),
"shared-arm fixture already duplicated a semantic instruction");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"EXEC/SCC shared-arm structurization changed semantic coverage");
@@ -8431,8 +8432,8 @@ void TestSharedReturnPreservesDescriptorDominance() {
auto graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
const auto *overwrite = graph.FindBlockByPc(0x10u);
const auto *body = graph.FindBlockByPc(0x20u);
Check(overwrite != nullptr && body != nullptr &&
@@ -8464,6 +8465,224 @@ void TestSharedReturnPreservesDescriptorDominance() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNestedSelectionPreservesDescriptorSources() {
constexpr uint32_t nested_arm = 12, direct_arm = 18, join = 21, end = 23;
const uint32_t shader[] = {
EncodeSopc(0x07, 9, 128),
EncodeSopp(0x04, end - 1 - 1),
EncodeSopc(0x07, 8, 130),
EncodeSopp(0x04, direct_arm - 3 - 1),
EncodeSmem0(0x02, 16, 10), 125u << 25u,
EncodeSopc(0x07, 8, 129),
EncodeSopp(0x04, nested_arm - 7 - 1),
EncodeSmem0(0x08, 24, 8), 125u << 25u,
EncodeVop1(0x01, 0, 24),
EncodeSopp(0x02, join - 11 - 1),
EncodeSmem0(0x02, 16, 10), (125u << 25u) | 16u,
EncodeSmem0(0x08, 24, 8), 125u << 25u,
EncodeVop1(0x01, 0, 24),
EncodeSopp(0x02, join - 17 - 1),
EncodeSmem0(0x08, 24, 8), 125u << 25u,
EncodeVop1(0x01, 0, 24),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(0, 3, 0),
EncodeSopp(0x01),
};
ShaderRecompiler::Decoder::Program decoded;
ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded);
auto graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.blocks.size() == 8u &&
std::ranges::all_of(coverage, [](uint32_t uses) { return uses == 1u; }),
"nested descriptor fixture has the wrong native topology");
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"nested descriptor selection changed semantic instruction coverage");
const std::array<uint32_t, 8> table{
0x3000u, 0u, 4u, 3u << 28u, 0x5000u, 0u, 4u, 3u << 28u};
std::array<uint32_t, 14> user_data{};
user_data[1] = 1u;
user_data[4] = 0x4000u;
user_data[6] = 4u;
user_data[7] = 3u << 28u;
user_data[10] = 4u;
user_data[11] = 3u << 28u;
const auto address = reinterpret_cast<uint64_t>(table.data());
user_data[12] = static_cast<uint32_t>(address);
user_data[13] = static_cast<uint32_t>(address >> 32u);
for (uint32_t mode = 0; mode != 3; ++mode) {
user_data[0] = mode;
user_data[8] = 0x2000u + mode * 0x10000u;
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 4u,
"nested descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
const auto pc = result.program.info.buffers[i].first_use_pc;
const uint32_t expected = pc == 8u * 4u ? table[0]
: pc == 14u * 4u ? table[4]
: pc == direct_arm * 4u ? user_data[8]
: pc == join * 4u ? user_data[4] : 0u;
Check(expected != 0u && result.resources.buffers[i].dwords[0] == expected,
"nested descriptor selection chose another arm's descriptor");
}
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestSharedExitPreservesNativeDescriptorSources() {
constexpr uint32_t selected = 8, later = 18, normal = 26, killed = 29;
const uint32_t shader[] = {
EncodeSopc(0x07, 0, 128),
EncodeSopp(0x04, normal - 1 - 1),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 32u,
EncodeSopc(0x07, 1, 128),
EncodeSopp(0x04, selected - 5 - 1),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 16u,
EncodeSopc(0x07, 2, 128),
EncodeSopp(0x04, later - 9 - 1),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 32u,
EncodeSopc(0x07, 3, 128),
EncodeSopp(0x04, killed - 13 - 1),
EncodeSmem0(0x08, 16, 4), 125u << 25u,
EncodeVop1(0x01, 0, 16),
EncodeSopp(0x02, normal - 17 - 1),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 48u,
EncodeSopc(0x07, 4, 128),
EncodeSopp(0x04, killed - 21 - 1),
EncodeSmem0(0x08, 16, 4), 125u << 25u,
EncodeVop1(0x01, 0, 16),
EncodeSopp(0x02, normal - 25 - 1),
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
EncodeSopp(0x01),
};
ShaderRecompiler::Decoder::Program decoded;
ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded);
auto graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.blocks.size() == 10u &&
std::ranges::all_of(coverage, [](uint32_t uses) { return uses == 1u; }),
"shared-exit descriptor fixture has the wrong native topology");
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"shared-exit descriptor selection changed semantic instruction coverage");
const std::array<uint32_t, 16> table{
0x2000u, 0u, 4u, 3u << 28u, 0x3000u, 0u, 4u, 3u << 28u,
0x4000u, 0u, 4u, 3u << 28u, 0x5000u, 0u, 4u, 3u << 28u};
std::array<uint32_t, 14> user_data{};
std::fill_n(user_data.begin(), 5, 1u);
user_data[8] = 0x1000u;
user_data[10] = 4u;
user_data[11] = 3u << 28u;
const auto address = reinterpret_cast<uint64_t>(table.data());
user_data[12] = static_cast<uint32_t>(address);
user_data[13] = static_cast<uint32_t>(address >> 32u);
for (uint32_t arm = 0; arm != 2; ++arm) {
user_data[2] = arm;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 2u,
"shared-exit descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
const auto pc = result.program.info.buffers[i].first_use_pc;
const uint32_t expected = pc == 14u * 4u ? table[8]
: pc == 22u * 4u ? table[12] : 0u;
Check(expected != 0u && result.resources.buffers[i].dwords[0] == expected,
"shared-exit descriptor selection retained an overwritten native source");
}
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestNativeGuardedSamplerSource() {
constexpr uint32_t nested = 12, sample = 18, join = 20, end = 22;
const uint32_t shader[] = {
EncodeSopc(0x07, 21, 128),
EncodeSopp(0x04, end - 1 - 1),
EncodeSopc(0x07, 20, 130),
EncodeSopp(0x04, sample - 3 - 1),
EncodeSMovB32(16, 133), EncodeSMovB32(17, 134),
EncodeSMovB32(18, 135), EncodeSMovB32(19, 136),
EncodeSopc(0x07, 20, 129),
EncodeSopp(0x04, nested - 9 - 1),
EncodeVop1(0x01, 0, 128),
EncodeSopp(0x02, join - 11 - 1),
EncodeSMovB32(16, 137), EncodeSMovB32(17, 138),
EncodeSMovB32(18, 139), EncodeSMovB32(19, 140),
EncodeVop1(0x01, 0, 129),
EncodeSopp(0x02, join - 17 - 1),
EncodeMimg0(0x20, 0x1), EncodeMimg1(0, 2, 4, 2),
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
};
auto user_data = ImageTestUserData();
const std::array<uint32_t, 4> sampler{0u, 0u, 0x09500000u, 0u};
std::ranges::copy(sampler, user_data.begin() + 8);
user_data[13] = 1u;
for (uint32_t mode = 0; mode != 3; ++mode) {
user_data[12] = mode;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(shader, options);
const auto expected = mode == 2u ? sampler : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.samplers.size() == 1u &&
result.resources.samplers[0].dword_count == expected.size() &&
std::equal(expected.begin(), expected.end(), result.resources.samplers[0].dwords.begin()),
"guarded native sample selected an unrelated arm's sampler constants");
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestNativeDescriptorProvenanceRejectsGpuSelection() {
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
const std::array<uint32_t, 13> selected{
EncodeSmem0(0x02, 8, 6), 125u << 25u,
EncodeVop1(0x01, 1, 128),
EncodeVopc(0xc4, 256, 1),
EncodeSopp(0x06, 2),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 16u,
EncodeSmem0(0x08, 16, 4), 125u << 25u,
EncodeVop1(0x01, 2, 16),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(2, 0, 0),
EncodeSopp(0x01),
};
auto partial = selected;
partial[5] = EncodeVop1(0x02, 11, 256);
partial[6] = EncodeSopp(0x00);
const std::array<uint32_t, 13> loop{
EncodeSmem0(0x02, 8, 6), 125u << 25u,
EncodeSmem0(0x08, 16, 4), 125u << 25u,
EncodeVop1(0x01, 2, 16),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(2, 0, 0),
EncodeSmem0(0x02, 8, 6), (125u << 25u) | 16u,
EncodeVop1(0x01, 1, 128),
EncodeVopc(0xc4, 256, 1),
EncodeSopp(0x07, 0xfff6u),
EncodeSopp(0x01),
};
std::array<uint32_t, 14> user_data{};
ShaderComputeInputInfo input_info{};
input_info.thread_ids_num = 1;
input_info.threads_num[0] = 64;
input_info.threads_num[1] = input_info.threads_num[2] = 1;
auto options = MakeCompileOptions(ShaderType::Compute);
options.input_info.compute = &input_info;
options.user_data = user_data;
for (const auto &shader : {selected, partial, loop}) {
ExpectFatal([&] { ShaderRecompiler::TranslateProgram(shader, options); },
"native descriptor provenance accepted a GPU-selected or loop-carried value");
}
#endif
}
void TestCfgSiblingSharedExit() {
// f7030726b9470dd8: a nested exit on one side and two exits on the
// other side share an epilogue, separate from the normal return.
@@ -8481,8 +8700,8 @@ void TestCfgSiblingSharedExit() {
Decoder::Program decoded;
Decoder::DecodeProgram(std::span{shader}, decoded);
const auto original = CFG::BuildGraph(decoded);
auto structured = original;
Check(CFG::Structurize(structured), structured.unsupported_reason.c_str());
auto structured = CFG::Structurize(original);
Check(!structured.unsupported, structured.unsupported_reason.c_str());
Check(CfgInstructionCoverage(structured, decoded.instructions.size()) ==
CfgInstructionCoverage(original, decoded.instructions.size()),
"shared sibling exit duplicated or dropped guest instructions");
@@ -8564,8 +8783,8 @@ void TestNewShaderRecompilerCfgNestedTailEarlyExit() {
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"nested-tail routing changed semantic instruction coverage");
@@ -8603,8 +8822,8 @@ void TestNewShaderRecompilerCfgSharedReturnAfterNestedSelections() {
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"shared-exit route ordering changed semantic instruction coverage");
@@ -8641,7 +8860,8 @@ void TestNewShaderRecompilerCfgAlternatingSharedReturns() {
auto graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto coverage = CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.blocks.size() == 5u, "alternating returns fixture has the wrong CFG");
Check(ShaderRecompiler::CFG::Structurize(graph), graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) == coverage,
"alternating returns duplicated a terminal epilogue");
@@ -8680,8 +8900,8 @@ void TestNewShaderRecompilerCfgLoopSharedRegion() {
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.natural_loops.size() == 1u,
"loop shared-region fixture has the wrong native CFG");
const bool structured = ShaderRecompiler::CFG::Structurize(graph);
Check(structured, graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
const auto *loop_header = graph.FindBlockByPc(0x00u);
Check(graph.natural_loops.size() == 1u && graph.back_edges.size() == 1u &&
loop_header != nullptr && loop_header->terminator.loop_header &&
@@ -8734,8 +8954,8 @@ void TestNewShaderRecompilerCfgSharedRegionBeforeEarlyBreakLoop() {
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(graph.blocks.size() == 9u && graph.natural_loops.size() == 1u,
"shared-region/early-break fixture has the wrong native CFG");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(graph.natural_loops.size() == 1u && graph.back_edges.size() == 1u,
"selection routing introduced a cycle around a structured loop exit");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
@@ -8795,8 +9015,8 @@ void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() {
const auto original_block_count = graph.blocks.size();
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
const bool structured = ShaderRecompiler::CFG::Structurize(graph);
Check(structured, graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"early-exit ladder routing changed semantic instruction coverage");
@@ -8834,8 +9054,8 @@ void TestNewShaderRecompilerCfgNestedEarlyExitSharedTerminal() {
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
const bool structured = ShaderRecompiler::CFG::Structurize(graph);
Check(structured, graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"nested early-exit structurization changed semantic coverage");
@@ -8877,8 +9097,8 @@ void TestNewShaderRecompilerCfgEarlyReturnSharedLoopContinuation() {
auto graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage &&
graph.natural_loops.size() == 2u,
@@ -8914,8 +9134,8 @@ void TestNewShaderRecompilerCfgSharedTerminalEarlyExit() {
graph = ShaderRecompiler::CFG::BuildGraph(decoded);
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(
CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
@@ -8950,8 +9170,8 @@ void TestNewShaderRecompilerCfgPrunesUnreachableSelectionEntry() {
CfgInstructionCoverage(graph, decoded.instructions.size());
Check(original_coverage[1] == 0u,
"CFG retained an unreachable external selection entry");
Check(ShaderRecompiler::CFG::Structurize(graph),
graph.unsupported_reason.c_str());
graph = ShaderRecompiler::CFG::Structurize(graph);
Check(!graph.unsupported, graph.unsupported_reason.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"selection structurization changed reachable semantic code");
@@ -8979,28 +9199,24 @@ void TestNewShaderRecompilerCfgFailedStructurizationPreservesGraph() {
const auto original = graph;
Check(graph.natural_loops.size() == 1u && !graph.irreducible,
"failed structurization fixture must have one reducible loop");
Check(!CFG::Structurize(graph) && graph.unsupported &&
graph.failure_kind == CFG::FailureKind::StructuredControlFlow &&
!graph.unsupported_reason.empty(),
const auto failed = CFG::Structurize(graph);
Check(failed.unsupported &&
failed.failure_kind == CFG::FailureKind::StructuredControlFlow &&
!failed.unsupported_reason.empty(),
"loop without a merge did not retain its structurization failure");
Check(graph.failure_block == UINT32_MAX,
Check(failed.failure_block == UINT32_MAX,
"failed structurization exposed a discarded synthetic block ID");
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
CfgInstructionCoverage(original, decoded.instructions.size()),
"failed structurization changed instruction coverage");
auto topology = graph;
topology.unsupported = original.unsupported;
topology.failure_kind = original.failure_kind;
topology.failure_block = original.failure_block;
topology.unsupported_reason = original.unsupported_reason;
Check(CFG::GraphToString(topology) == CFG::GraphToString(original),
Check(CFG::GraphToString(graph) == CFG::GraphToString(original),
"failed structurization changed CFG topology or analyses");
auto options = MakeCompileOptions(ShaderType::Compute);
auto result = RecompileForTest(shader, options);
Check(result.program.dispatcher_fallback &&
result.program.cfg_failure_kind == graph.failure_kind &&
result.program.fallback_reason == graph.unsupported_reason,
result.program.cfg_failure_kind == failed.failure_kind &&
result.program.fallback_reason == failed.unsupported_reason,
"compiler did not consume CFG failure diagnostics directly");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -12231,7 +12447,13 @@ void TestTypedDescriptorRealWideMoveTranslation() {
};
ShaderRecompiler::IR::Program ir;
BuildTypedPlan(shader, static_cast<uint32_t>(std::size(shader)), ir);
ShaderRecompiler::IR::TrackResources(ir);
for (uint32_t reg = 0; reg < 4; ++reg) {
Check(std::ranges::any_of(ir.scalar_writes, [&](const auto &write) {
return write.pc == (reg / 2u) * 4u &&
ShaderRecompiler::IR::RegIndex(write.reg) == reg;
}), "wide scalar move lost a native descriptor component write");
}
ShaderRecompiler::IR::TrackResources(ir, {}, {});
Check(ir.info.buffers.size() == 1,
"real wide-move shader did not track one buffer use");
const auto *source = TypedDescriptorSource(ir, ir.info.buffers[0].source);
@@ -12421,7 +12643,7 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
}
Check(address_reads == 4 && buffer_reads == 4,
"real scalar loads used the wrong raw typed operations");
ShaderRecompiler::IR::TrackResources(load_ir);
ShaderRecompiler::IR::TrackResources(load_ir, {}, {});
Check(load_ir.info.buffers.size() == 2,
"real scalar-load descriptor sources were not attached");
for (const auto &buffer : load_ir.info.buffers) {
@@ -12452,7 +12674,7 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
BuildTypedPlan(inline_sampler_shader,
static_cast<uint32_t>(std::size(inline_sampler_shader)),
inline_sampler_ir);
ShaderRecompiler::IR::TrackResources(inline_sampler_ir);
ShaderRecompiler::IR::TrackResources(inline_sampler_ir, {}, {});
ShaderRecompiler::IR::DescriptorValue sampler;
ShaderRecompiler::IR::SrtRuntime runtime;
Check(inline_sampler_ir.info.samplers.size() == 1 &&
@@ -12643,7 +12865,7 @@ void TestScalarMemoryLoadCrossesIntoVcc() {
"wide SMEM destination crossing into VCC lost scalar provenance");
CheckFlattenedReadSlots(
ir, 4, "wide SMEM destination crossing into VCC used wrong flat offsets");
ShaderRecompiler::IR::TrackResources(ir);
ShaderRecompiler::IR::TrackResources(ir, {}, {});
Check(ir.info.buffers.size() == 1,
"wide SMEM destination crossing into VCC lost its buffer use");
const auto *source =
@@ -12705,7 +12927,7 @@ void TestResourceTrackingRealDensePatching() {
};
ShaderRecompiler::IR::Program ir;
BuildTypedPlan(shader, static_cast<uint32_t>(std::size(shader)), ir);
ShaderRecompiler::IR::TrackResources(ir);
ShaderRecompiler::IR::TrackResources(ir, {}, {});
Check(ir.info.buffers.size() == 2 && ir.info.images.size() == 2 &&
ir.info.samplers.size() == 1,
"real resource tracking produced the wrong dense list sizes");
@@ -12772,7 +12994,7 @@ void TestDirectTranslationResetsAnalysisState() {
ShaderRecompiler::IR::Program ir;
BuildTypedPlan(first_shader, static_cast<uint32_t>(std::size(first_shader)),
ir);
ShaderRecompiler::IR::TrackResources(ir);
ShaderRecompiler::IR::TrackResources(ir, {}, {});
ShaderComputeInputInfo compute;
ShaderRecompiler::IR::CollectShaderInfo(ir, {.compute = &compute});
Check(ir.resource_tracking_complete && ir.shader_info_complete &&
@@ -13648,6 +13870,10 @@ int main() {
TestNewShaderRecompilerCfgNestedEarlyExitLoopForwarders();
TestNewShaderRecompilerCfgExecSccSharedArm();
TestSharedReturnPreservesDescriptorDominance();
TestSharedExitPreservesNativeDescriptorSources();
TestNestedSelectionPreservesDescriptorSources();
TestNativeGuardedSamplerSource();
TestNativeDescriptorProvenanceRejectsGpuSelection();
TestCfgSiblingSharedExit();
TestNewShaderRecompilerCfgNestedTailEarlyExit();
TestNewShaderRecompilerCfgSharedReturnAfterNestedSelections();