diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.cpp b/src/graphics/shader/recompiler/ShaderRecompiler.cpp index 89bf8b05f..a34bc9874 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.cpp +++ b/src/graphics/shader/recompiler/ShaderRecompiler.cpp @@ -548,30 +548,40 @@ TranslateResult TranslateProgram(std::span 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(cfg.blocks.size()), static_cast(cfg.natural_loops.size()), - static_cast(cfg.back_edges.size()), phase_ms()); - if (cfg.irreducible) { - LogDispatcherFallback(options, cfg, "build"); + static_cast(native_cfg.blocks.size()), + static_cast(native_cfg.natural_loops.size()), + static_cast(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(cfg.blocks.size())); + static_cast(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(cfg.blocks.size()), - static_cast(cfg.natural_loops.size()), phase_ms()); + static_cast(selected_cfg->blocks.size()), + static_cast(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 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); diff --git a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp index f2a2e5283..29151f0c9 100644 --- a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp +++ b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.cpp @@ -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) { diff --git a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.h b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.h index 04985f15a..035f557d6 100644 --- a/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.h +++ b/src/graphics/shader/recompiler/frontend/cfg/ShaderCFG.h @@ -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); diff --git a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp index a50e2cdda..cfdbbfca6 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp @@ -317,6 +317,7 @@ void Translator::WriteRawU32(const Decoder::Operand& operand, IR::U32 value) { case Decoder::OperandKind::Sgpr: { const auto reg = static_cast(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::RegIndex(reg) - 1u), @@ -699,11 +700,13 @@ std::array 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::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))); } diff --git a/src/graphics/shader/recompiler/ir/Program.cpp b/src/graphics/shader/recompiler/ir/Program.cpp index 1fe47e8f5..51ce341f0 100644 --- a/src/graphics/shader/recompiler/ir/Program.cpp +++ b/src/graphics/shader/recompiler/ir/Program.cpp @@ -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() != rhs->Flags()) { + } else if (lhs->GetOpcode() != ValueOpcode::ReadConst && + lhs->Flags() != rhs->Flags()) { return false; } for (size_t index = 0; index < lhs->NumArgs(); index++) { diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 6c45aa72e..88cd6d8e0 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -583,6 +583,8 @@ struct Program: ResourcePlan { CFG::FailureKind cfg_failure_kind = CFG::FailureKind::None; std::string fallback_reason; std::vector block_info; + struct ScalarWrite { uint32_t pc; ScalarReg reg; }; + std::vector 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 export_info; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 809b16050..5b2dee23d 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -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 candidates; + std::vector visited; + std::vector 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(); + 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(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 positions {{use->id, use_pc}}; + std::vector 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 m_scalar_writes; ShaderInfo m_info; std::vector m_sources; std::vector 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 diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.h b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.h index 78b00c3c7..a3fb5b4a7 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.h @@ -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 diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index 7cd170896..41d5c88ee 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -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())); const auto uses = patch.inst->Uses(); for (const auto& use: uses) { use.user->SetArg(use.operand, flat); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 4dd4d8af3..c37d47793 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -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(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 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(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 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); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 1a0a93b8f..5b766985c 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -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 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 table{ + 0x3000u, 0u, 4u, 3u << 28u, 0x5000u, 0u, 4u, 3u << 28u}; + std::array 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(table.data()); + user_data[12] = static_cast(address); + user_data[13] = static_cast(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 table{ + 0x2000u, 0u, 4u, 3u << 28u, 0x3000u, 0u, 4u, 3u << 28u, + 0x4000u, 0u, 4u, 3u << 28u, 0x5000u, 0u, 4u, 3u << 28u}; + std::array 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(table.data()); + user_data[12] = static_cast(address); + user_data[13] = static_cast(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 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{}; + 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 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 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 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(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(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(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(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();