From c2a48416b1c103d4c129baaa2659cf071e38d9ed Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:04:48 +0200 Subject: [PATCH 1/6] fix(shader): follow exec phis and long kill chains when proving masked lanes unread Taxi Chaos 2 (PPSA30279) pixel shader 0x18c07bf000 is refused when it is registered, and the title stops: packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?) The shader reads the ancillary VGPR (v2) once, with v_bfe_u32 v3, v2, 16, 11, which #1904 lowers to Layer. The next write to v2, v_rcp_f32 v2, s33, runs before any kill under the initial exec and becomes SelectU32(IEqual32(HelperInvocation, 0), rcp, ancillary). The raw word therefore survives only in helper lanes. The shader has no s_wqm, no DPP, readlane or swizzle, no stores, and only *_lz samples and gathers. Every helper-lane value that comes from v2 is dropped by a later exec select, a compare that is ANDed with exec before its ballot, or an export whose exec excludes helper lanes. MaskedSelectEliminator exists to remove selects like this one, but it could not prove the case. The kill paths save and restore exec, so the later execs are phis at block merges, and the AND chains the kills build go deeper than its eight-level limit. When it checks whether a later exec implies the write's exec, it now: - accepts a phi when the phi and each incoming value lie outside loops and every incoming value implies that exec; - pairs low and high exec words merged by phis in one block, predecessor by predecessor; - memoises results for each select and follows up to 64 levels. A cycle counts as not implied. Once the select is gone, nothing reads the ancillary word, so no bit of it is produced. A select whose false lanes can still be observed is kept and the shader is still refused. That covers a data test that leaves the word in non-helper lanes that later export, an export under the full wave, and an unmasked ballot. Related: #2548 Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 110 +++++++++++++++ core/libs/tests/PixelAncillary.cpp | 89 ++++++++++++ .../src/MaskedSelectEliminator.cpp | 127 ++++++++++++++---- 3 files changed, 297 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 119f8792511b..ea19428f1ed3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -275,6 +275,116 @@ int main() { return count == 0u && !gone; }); + const auto mergedExec = [](bool wideBranch, bool loopMerge) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& killed = b.logicalAnd(wideBranch ? wide : exec, b.mask(16u)); + auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::Bool); + merge.AppendInstruction(&phi); + phi.AddPhiOperand(&head, &exec); + phi.AddPhiOperand(&body, &killed); + if (loopMerge) { + IrBlock& exit = b.newBlock(); + merge.AddBranch(&merge); + merge.AddBranch(&exit); + phi.AddPhiOperand(&merge, &phi); + b.block = &exit; + } else { + b.block = &merge; + } + b.keep(b.select(phi, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec merged by a phi whose every incoming exec implies the write's exec drops the select", [&] { + const auto [count, gone] = mergedExec(false, false); + return count == 1u && gone; + }); + + passed &= run("an exec merged by a phi with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, false); + return count == 0u && !gone; + }); + + passed &= run("an exec merged by a phi inside a loop keeps the select", [&] { + const auto [count, gone] = mergedExec(false, true); + return count == 0u && !gone; + }); + + const auto mergedWords = [](bool wideBranch) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& saved = b.emit(IrOpcode::Ballot, IrType::U32x4, {&exec}); + auto& savedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(0u)}); + auto& savedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(1u)}); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& survived = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.logicalAnd(wideBranch ? wide : exec, b.mask(24u))}); + auto& survivedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(0u)}); + auto& survivedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(1u)}); + auto& low = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + auto& high = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&low); + merge.AppendInstruction(&high); + low.AddPhiOperand(&head, &savedLow); + low.AddPhiOperand(&body, &survivedLow); + high.AddPhiOperand(&head, &savedHigh); + high.AddPhiOperand(&body, &survivedHigh); + b.block = &merge; + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from mask words merged by phis implies the write's exec", [&] { + const auto [count, gone] = mergedWords(false); + return count == 1u && gone; + }); + + passed &= run("an exec rebuilt from merged mask words with a wider incoming ballot keeps the select", [&] { + const auto [count, gone] = mergedWords(true); + return count == 0u && !gone; + }); + + const auto chained = [](bool wideRoot) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* later = wideRoot ? &wide : &exec; + for (std::uint32_t kill = 0; kill < 24u; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill)); + b.keep(b.select(*later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false); + return count == 1u && gone; + }); + + passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] { + const auto [count, gone] = chained(true); + return count == 0u && !gone; + }); + passed &= run("a write in a loop must keep its select", [] { Builder b; auto& exec = b.mask(16u); diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index b7be6fc810c8..2100713ec5ee 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -1,6 +1,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/ShaderInfoCollector.hpp" #include #include @@ -253,6 +254,90 @@ static void MergedRefused(IrOpcode opcode, std::uint32_t offset, std::uint32_t c } Require(false); } +enum class HelperRead { Export, FullWaveExport, DataTestExport, Ballot }; + +struct HelperProgram { + IrValue* written = nullptr; + IrValue* layer = nullptr; + IrValue* exported = nullptr; +}; + +static HelperProgram HelperOnly(IrProgram& program, HelperRead read) { + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& tested = builder.LogicalAnd(live, FrontFacingBuiltin(builder)); + IrValue& written = builder.Select(read == HelperRead::DataTestExport ? tested : live, builder.Constant(SelectOther), ancillary); + builder.SetInsertionPoint(body); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(written), &builder.BitCastF32(written)}); + IrValue& killed = builder.Select(tested, builder.BitCastU32(squared), written); + IrValue& exec = program.CreateValue(IrOpcode::Phi, IrType::Bool); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&exec); + merge.AppendInstruction(&value); + exec.AddPhiOperand(&entry, &live); + exec.AddPhiOperand(&body, &tested); + value.AddPhiOperand(&entry, &written); + value.AddPhiOperand(&body, &killed); + builder.SetInsertionPoint(merge); + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&value, &target, &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + IrValue& guard = read == HelperRead::FullWaveExport ? builder.ConstantBool(true) : exec; + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + if (read == HelperRead::Ballot) { + IrValue& ballot = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&builder.INotEqual(value, builder.Constant(0u))}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&builder.CompositeExtract(ballot, 0u)})); + } + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void LowerMasked(IrProgram& program) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + static_cast(MaskedSelectEliminator().Eliminate(program)); + DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void HelperLanesOnly() { + IrProgram program; + const HelperProgram built = HelperOnly(program, HelperRead::Export); + LowerMasked(program); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + const IrValue* field = built.layer->Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLanesRefused(HelperRead read) { + IrProgram program; + static_cast(HelperOnly(program, read)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -292,4 +377,8 @@ int main() { MergedRefused(IrOpcode::BitwiseOr32, 1u, 0u, false); MergedRefused(IrOpcode::BitFieldUExtract, 12u, 4u, false); MergedRefused(IrOpcode::BitFieldUExtract, 0u, 2u, true); + HelperLanesOnly(); + HelperLanesRefused(HelperRead::FullWaveExport); + HelperLanesRefused(HelperRead::DataTestExport); + HelperLanesRefused(HelperRead::Ballot); } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 74f4cda6f718..1966437f11c0 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -266,36 +266,105 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth = 0); +struct ImplicationKey { + const IrValue* first = nullptr; + const IrValue* second = nullptr; -bool wordsImply(const IrValue* low, const IrValue* high, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { - if (depth > DepthLimit) return false; - if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; - if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { - const IrValue* ballot = low->Argument(0)->Resolve(); - const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); - if (ballot->Opcode() == IrOpcode::Ballot && sameBallot && implies(ballot->Argument(0), mask, waveSize, depth + 1u)) return true; + bool operator==(const ImplicationKey&) const = default; +}; + +struct ImplicationKeyHash { + std::size_t operator()(const ImplicationKey& key) const { + return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); } - if (low->Opcode() != IrOpcode::BitwiseAnd32) return false; - if (waveSize == 32u) return wordsImply(low->Argument(0)->Resolve(), high, mask, waveSize, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, mask, waveSize, depth + 1u); - if (high->Opcode() != IrOpcode::BitwiseAnd32) return false; - for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { - for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) { - if (wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), mask, waveSize, depth + 1u)) return true; +}; + +class Implications { +public: + Implications(const std::unordered_set& once, const IrValue* mask, std::uint32_t waveSize) : once(once), mask(mask), waveSize(waveSize), always(alwaysTrue(mask, waveSize)) {} + + bool Implies(const IrValue* condition, std::uint32_t depth = 0) { + condition = condition->Resolve(); + if (condition == mask || always) return true; + if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); + const ImplicationKey key{condition, nullptr}; + if (const auto known = results.find(key); known != results.end()) return known->second; + if (depth > ImplicationDepthLimit) return false; + results.emplace(key, false); + bool result = false; + if (condition->Opcode() == IrOpcode::LogicalAnd) { + result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); + } else if (condition->IsPhi()) { + result = mergesOutsideLoops(*condition); + for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { + result = Implies(condition->Argument(index), depth + 1u); + } + } else if (const auto bit = threadBit(condition, waveSize)) { + result = wordsImply(bit->low, bit->high, depth + 1u); } + results[key] = result; + return result; } - return false; -} -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { - condition = condition->Resolve(); - if (condition == mask || alwaysTrue(mask, waveSize)) return true; - if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); - if (depth > DepthLimit) return false; - if (condition->Opcode() == IrOpcode::LogicalAnd) return implies(condition->Argument(0), mask, waveSize, depth + 1u) || implies(condition->Argument(1), mask, waveSize, depth + 1u); - const auto bit = threadBit(condition, waveSize); - return bit && wordsImply(bit->low, bit->high, mask, waveSize, depth + 1u); -} +private: + static constexpr std::uint32_t ImplicationDepthLimit = 64; + + bool mergesOutsideLoops(const IrValue& phi) const { + if (phi.Parent() == nullptr || !once.contains(phi.Parent()) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { + const IrValue* incoming = phi.Argument(index)->Resolve(); + if (!incoming->HasImmediate() && (incoming->Parent() == nullptr || !once.contains(incoming->Parent()))) return false; + } + return true; + } + + static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { + for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { + if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); + } + return nullptr; + } + + bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { + if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; + const ImplicationKey key{low, high}; + if (const auto known = results.find(key); known != results.end()) return known->second; + if (depth > ImplicationDepthLimit) return false; + results.emplace(key, false); + bool result = false; + if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { + const IrValue* ballot = low->Argument(0)->Resolve(); + const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); + result = ballot->Opcode() == IrOpcode::Ballot && sameBallot && Implies(ballot->Argument(0), depth + 1u); + } + if (!result && low->Opcode() == IrOpcode::BitwiseAnd32) { + if (waveSize == 32u) { + result = wordsImply(low->Argument(0)->Resolve(), high, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, depth + 1u); + } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t lowIndex = 0; !result && lowIndex < 2u; ++lowIndex) { + for (std::size_t highIndex = 0; !result && highIndex < 2u; ++highIndex) { + result = wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), depth + 1u); + } + } + } + } + if (!result && low->IsPhi() && mergesOutsideLoops(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && mergesOutsideLoops(*high)))) { + result = true; + for (std::size_t index = 0; result && index < low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); + result = highIncoming != nullptr && wordsImply(low->Argument(index)->Resolve(), highIncoming, depth + 1u); + } + } + results[key] = result; + return result; + } + + const std::unordered_set& once; + const IrValue* mask; + std::uint32_t waveSize; + bool always; + std::unordered_map results; +}; std::unordered_set blocksOutsideLoops(const IrProgram& program) { const auto& order = program.BlockOrder(); @@ -338,7 +407,7 @@ std::unordered_set blocksOutsideLoops(const IrProgram& program) } bool unobservedWhereMasked(const IrProgram& program, const std::unordered_set& once, const IrValue& select, const IrValue* mask) { - const auto waveSize = program.WaveSize(); + Implications implications(once, mask, program.WaveSize()); std::vector pending{&select}; std::unordered_set visited{&select}; while (!pending.empty()) { @@ -347,9 +416,9 @@ bool unobservedWhereMasked(const IrProgram& program, const std::unordered_setOperandUses()) { const IrValue* user = use.user; if (user->Parent() == nullptr || !once.contains(user->Parent())) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implies(user->Argument(0), mask, waveSize)) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implies(user->Argument(1u - use.operand), mask, waveSize)) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implies(user->Argument(user->ArgumentCount() - 1u), mask, waveSize)) continue; + if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; + if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; + if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; if (visited.insert(user).second) { if (visited.size() > VisitLimit) return false; From cd4eaacf7c7300fee08a30c4c1188a86177e85db Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:43:36 +0200 Subject: [PATCH 2/6] perf(shader): scale the masked-lane walk limit with the program LEGO Horizon Adventures (PPSA14632) still refuses a pixel shader with packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?) although it has the form the previous commit handles: the only whole-word reader is SelectU32(IEqual32(HelperInvocation, 0), BitFieldUExtract(x, 0, 11), ancillary), the exec words are Ballot(not helper) & Ballot(true), and there are no image ops, stores or loop phis. About 10,250 IR values derive from the word, so the walk from that select stops at its fixed limit of 4096 visited values and the select is kept. With the limit raised to 8192 the shader prepares (reported on Linux with an RX 9070 XT). A walk may now take max(4096, 2 * instructions) steps. A step is a value visited for the first time or an implication evaluated for the first time. The walk only visits values derived from the select, at most one per instruction, so a walk is only cut short when the implications it evaluates cost as much as the program itself. On the 198 Taxi Chaos 2 requests no walk needs more than 0.79 * instructions steps. One walk stays linear in the program, O(instructions + operands): each value is expanded once, and with the per-walk memo each implication is evaluated once. The walk is also cheaper, so that the larger limit costs nothing: - visited values are marked in a table indexed by value id instead of a hash set, and the table, the work list and the memo are reused across walks; - blocks outside loops are looked up in a table indexed by block id instead of a hash set. This lookup ran once per use and was most of the pass's time. Time in MaskedSelectEliminator per prepare (median of 3, the pass runs twice per prepare), 54 Taxi Chaos 2 requests: shader before 40a8a5a7 this commit main 5edae353 0x1ac0413000 689 ms 285 ms 401 ms 0x19002bd200 102 ms 34 ms 100 ms 0x18c07bf000 191 ms 52 ms (refused) all 54 1294 ms 484 ms 919 ms 0x1ac0413000 and its three copies in the NVIDIA dump now remove 659 selects instead of 625: their walks no longer stop at 4096. No other request changes what it removes. 197 of the 198 requests prepare before and after; the other one fails the same way in both (v_readlane_b32 of lane 63 on a 32-lane host subgroup). Related: #2548 Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 23 ++++ core/libs/tests/PixelAncillary.cpp | 16 ++- .../src/MaskedSelectEliminator.cpp | 127 +++++++++++++----- 3 files changed, 126 insertions(+), 40 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ea19428f1ed3..839a465a6ad6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -162,6 +162,29 @@ int main() { return b.eliminate() == 2u && removed(first) && removed(second) && !removed(sink); }); + passed &= run("a write read through thousands of lane-local values still loses its select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(exec, *value, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("thousands of lane-local values ending in a wider read keep the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(wide, *value, old)); + return b.eliminate() == 0u && !removed(written); + }); + passed &= run("cross-lane reads must keep the select", [] { bool ok = true; for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 2100713ec5ee..9832c0ebe578 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -262,7 +262,7 @@ struct HelperProgram { IrValue* exported = nullptr; }; -static HelperProgram HelperOnly(IrProgram& program, HelperRead read) { +static HelperProgram HelperOnly(IrProgram& program, HelperRead read, std::uint32_t chain = 0u) { auto& entry = Begin(program); auto& body = program.CreateBlock(); auto& merge = program.CreateBlock(); @@ -292,7 +292,9 @@ static HelperProgram HelperOnly(IrProgram& program, HelperRead read) { value.AddPhiOperand(&entry, &written); value.AddPhiOperand(&body, &killed); builder.SetInsertionPoint(merge); - IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&value, &target, &builder.Constant(0u), &builder.Constant(0u)}); + IrValue* shaded = &value; + for (std::uint32_t step = 0; step < chain; ++step) shaded = &builder.IAdd(*shaded, helper); + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {shaded, &target, &builder.Constant(0u), &builder.Constant(0u)}); program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); IrValue& guard = read == HelperRead::FullWaveExport ? builder.ConstantBool(true) : exec; static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); @@ -312,9 +314,9 @@ static void LowerMasked(IrProgram& program) { const ShaderPixelInputInfo pixel {}; ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); } -static void HelperLanesOnly() { +static void HelperLanesOnly(std::uint32_t chain = 0u) { IrProgram program; - const HelperProgram built = HelperOnly(program, HelperRead::Export); + const HelperProgram built = HelperOnly(program, HelperRead::Export, chain); LowerMasked(program); for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { @@ -327,9 +329,9 @@ static void HelperLanesOnly() { const IrValue* entryValue = built.exported->Argument(0)->Resolve(); Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); } -static void HelperLanesRefused(HelperRead read) { +static void HelperLanesRefused(HelperRead read, std::uint32_t chain = 0u) { IrProgram program; - static_cast(HelperOnly(program, read)); + static_cast(HelperOnly(program, read, chain)); try { LowerMasked(program); } catch (const std::runtime_error& error) { @@ -378,7 +380,9 @@ int main() { MergedRefused(IrOpcode::BitFieldUExtract, 12u, 4u, false); MergedRefused(IrOpcode::BitFieldUExtract, 0u, 2u, true); HelperLanesOnly(); + HelperLanesOnly(6000u); HelperLanesRefused(HelperRead::FullWaveExport); HelperLanesRefused(HelperRead::DataTestExport); HelperLanesRefused(HelperRead::Ballot); + HelperLanesRefused(HelperRead::FullWaveExport, 6000u); } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 1966437f11c0..e9d218b806ac 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -1,6 +1,7 @@ #include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/DeadCodeEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" +#include #include #include #include @@ -10,7 +11,8 @@ namespace ShaderRecompiler { namespace { -constexpr std::size_t VisitLimit = 4096; +constexpr std::size_t MinimumWalkLimit = 4096; +constexpr std::size_t WalkLimitPerInstruction = 2; constexpr std::uint32_t DepthLimit = 8; bool isSelect(IrOpcode opcode) { @@ -266,6 +268,12 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } +enum class Region : std::uint8_t { Unreached, Once, Loop }; + +Region regionOf(const std::vector& regions, const IrBlock* block) { + return block != nullptr && block->Id() < regions.size() ? regions[block->Id()] : Region::Unreached; +} + struct ImplicationKey { const IrValue* first = nullptr; const IrValue* second = nullptr; @@ -281,7 +289,13 @@ struct ImplicationKeyHash { class Implications { public: - Implications(const std::unordered_set& once, const IrValue* mask, std::uint32_t waveSize) : once(once), mask(mask), waveSize(waveSize), always(alwaysTrue(mask, waveSize)) {} + Implications(const std::vector& regions, std::uint32_t waveSize, std::size_t& work) : regions(regions), waveSize(waveSize), work(work) {} + + void Reset(const IrValue* writeMask) { + mask = writeMask; + always = alwaysTrue(writeMask, waveSize); + results.clear(); + } bool Implies(const IrValue* condition, std::uint32_t depth = 0) { condition = condition->Resolve(); @@ -289,7 +303,8 @@ class Implications { if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); const ImplicationKey key{condition, nullptr}; if (const auto known = results.find(key); known != results.end()) return known->second; - if (depth > ImplicationDepthLimit) return false; + if (depth > ImplicationDepthLimit || work == 0) return false; + --work; results.emplace(key, false); bool result = false; if (condition->Opcode() == IrOpcode::LogicalAnd) { @@ -310,10 +325,10 @@ class Implications { static constexpr std::uint32_t ImplicationDepthLimit = 64; bool mergesOutsideLoops(const IrValue& phi) const { - if (phi.Parent() == nullptr || !once.contains(phi.Parent()) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + if (regionOf(regions, phi.Parent()) != Region::Once || phi.ArgumentCount() != phi.PhiBlockCount()) return false; for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { const IrValue* incoming = phi.Argument(index)->Resolve(); - if (!incoming->HasImmediate() && (incoming->Parent() == nullptr || !once.contains(incoming->Parent()))) return false; + if (!incoming->HasImmediate() && regionOf(regions, incoming->Parent()) != Region::Once) return false; } return true; } @@ -329,7 +344,8 @@ class Implications { if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; const ImplicationKey key{low, high}; if (const auto known = results.find(key); known != results.end()) return known->second; - if (depth > ImplicationDepthLimit) return false; + if (depth > ImplicationDepthLimit || work == 0) return false; + --work; results.emplace(key, false); bool result = false; if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { @@ -359,14 +375,15 @@ class Implications { return result; } - const std::unordered_set& once; - const IrValue* mask; + const std::vector& regions; std::uint32_t waveSize; - bool always; + std::size_t& work; + const IrValue* mask = nullptr; + bool always = false; std::unordered_map results; }; -std::unordered_set blocksOutsideLoops(const IrProgram& program) { +std::vector blockRegions(const IrProgram& program) { const auto& order = program.BlockOrder(); if (order.empty()) return {}; std::unordered_map state; @@ -399,46 +416,88 @@ std::unordered_set blocksOutsideLoops(const IrProgram& program) looping.insert(body.begin(), body.end()); } } - std::unordered_set result; + std::vector result; for (const auto& [block, visited] : state) { - if (visited == 2 && !looping.contains(block)) result.insert(block); + if (visited != 2) continue; + if (block->Id() >= result.size()) result.resize(block->Id() + 1u, Region::Unreached); + result[block->Id()] = looping.contains(block) ? Region::Loop : Region::Once; } return result; } -bool unobservedWhereMasked(const IrProgram& program, const std::unordered_set& once, const IrValue& select, const IrValue* mask) { - Implications implications(once, mask, program.WaveSize()); - std::vector pending{&select}; - std::unordered_set visited{&select}; - while (!pending.empty()) { - const IrValue* value = pending.back(); - pending.pop_back(); - for (const IrUse& use : value->OperandUses()) { - const IrValue* user = use.user; - if (user->Parent() == nullptr || !once.contains(user->Parent())) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; - if (visited.insert(user).second) { - if (visited.size() > VisitLimit) return false; - pending.push_back(user); +std::size_t walkLimit(const IrProgram& program) { + std::size_t instructions = 0; + for (const IrBlock* block : program.BlockOrder()) instructions += block->Instructions().size(); + return std::max(MinimumWalkLimit, WalkLimitPerInstruction * instructions); +} + +class MaskedLaneWalk { +public: + MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), limit(walkLimit(program)), implications(regions, program.WaveSize(), work) {} + + bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { + work = limit; + implications.Reset(mask); + startVisits(); + pending.clear(); + pending.push_back(&select); + static_cast(firstVisit(select)); + while (!pending.empty()) { + const IrValue* value = pending.back(); + pending.pop_back(); + for (const IrUse& use : value->OperandUses()) { + const IrValue* user = use.user; + if (regionOf(regions, user->Parent()) != Region::Once) return false; + if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; + if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; + if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; + if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; + if (firstVisit(*user)) { + if (work == 0) return false; + --work; + pending.push_back(user); + } } } + return true; } - return true; -} + +private: + void startVisits() { + if (++epoch != 0u) return; + std::fill(visits.begin(), visits.end(), 0u); + epoch = 1u; + } + + bool firstVisit(const IrValue& value) { + const std::uint32_t id = value.Id(); + if (id >= visits.size()) visits.resize(id + 1u, 0u); + if (visits[id] == epoch) return false; + visits[id] = epoch; + return true; + } + + const IrProgram& program; + const std::vector& regions; + std::size_t limit; + std::size_t work = 0; + Implications implications; + std::vector pending; + std::vector visits; + std::uint32_t epoch = 0; +}; } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; - const auto once = blocksOutsideLoops(program); + const auto regions = blockRegions(program); + MaskedLaneWalk walk(program, regions); bool changed = true; while (changed) { changed = false; for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { - const bool single = once.contains(*blockIt); + const bool single = regionOf(regions, *blockIt) == Region::Once; auto& instructions = (*blockIt)->Instructions(); auto it = instructions.end(); while (it != instructions.begin()) { @@ -446,7 +505,7 @@ MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& progra IrValue* inst = *it; if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; const IrValue* mask = inst->Argument(0)->Resolve(); - if (!alwaysTrue(mask, program.WaveSize()) && (!single || !unobservedWhereMasked(program, once, *inst, mask))) continue; + if (!alwaysTrue(mask, program.WaveSize()) && (!single || !walk.UnobservedWhereMasked(*inst, mask))) continue; inst->ReplaceAllUsesWith(inst->Argument(1)); inst->Invalidate(); inst->SetParent(nullptr); From 98f99d38836c8626db22dfe50f6ed4115dc3d03d Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:06:44 +0200 Subject: [PATCH 3/6] fix(shader): prove masked lanes unread across loops Kena: Bridge of Spirits (PPSA01802) refuses ten pixel shaders with packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?) at any visit limit. All ten have 30 to 33 phis on loop back edges (reported on Linux with an RX 9070 XT). Two rules stopped the proof at a loop, and the first one fires before the second is ever asked: - The walk from the select gave up at the first use inside a loop. A value derived from the select reaches a loop phi as soon as its register is written inside the loop, because the masked write becomes Select(loopExec, new, phi). A loop that leaves exec alone needs nothing more than this. - An exec that a loop narrows and carries around its back edge is a phi inside the loop, and such phis counted as not implying the write's exec. The walk now follows uses into loops. A phi inside a loop implies the write's exec when all of its incoming values do, where the phi itself counts as implying it while its back-edge values are checked. This is sound because each rule of the proof takes a lane bit of one value from a value computed earlier: a phi's value on each iteration is the incoming value computed before the edge was taken, a set lane of an And is set in both operands, a set lane of a ballot is a lane whose predicate held. If any value in the proof had a lane set where the write's exec is clear, the earliest such value would take that lane from an earlier value of the proof that is set there too, which contradicts it being the earliest. Two conditions keep this true: - The write's exec must be computed once, outside every loop. Otherwise a phi can carry a ballot of it from an earlier iteration, which is not the value the select used. Masks computed inside a loop keep the old rules. - A result may only be kept if the assumptions it used held. When a value that was assumed turns out not to be implied, every result recorded since it was assumed is dropped. Results that say "not implied" are kept; they only make the proof refuse more. A lane that is turned back on inside the loop makes the back-edge exec wider than the write's exec, so the phi is not implied and a read under it keeps the select. A lane that is turned back on but never reads the derived value does not. New tests cover a loop that carries the write with exec saved, narrowed and restored (read inside and after the loop), a loop that leaves exec alone, and the helper-only ancillary select feeding such loops. The refusing tests each fail when the matching check is removed: a revived lane that reads the write inside the loop (ignoring back-edge values), a result proven under an assumption that later fails (keeping it), and a write mask computed inside an earlier loop (allowing such masks). Two existing tests expected a loop to keep the select although every read is under the write's own exec; they now expect it to go, next to new counterparts with a wider exec. On the Taxi Chaos 2 requests, compute shaders with a loop inside a branch now lose the branch's masked writes: 0x194000b400 removes 207 selects instead of 36, 0x19002bd200 111 instead of 28, and seven more gain 5 to 43. In 0x16400cf700 the 26 new removals are writes under the branch condition tid < count; the loop's exec starts as a ballot of that condition, only loses lanes at the latch, and every store and atomic inside the loop runs under it. No proof on these requests had to drop a result. Walks now go on into loops, so the pass takes longer on loop shaders, but it stays below main. Time in MaskedSelectEliminator per prepare (median of 3, the pass runs twice per prepare), 54 Taxi Chaos 2 requests, one run: shader previous commit this commit main 5edae353 0x1ac0413000 334 ms 349 ms 424 ms 0x19002bd200 38 ms 103 ms 112 ms 0x19c016aa00 9 ms 74 ms 16 ms 0x194000b400 13 ms 31 ms 32 ms all 54 547 ms 805 ms 977 ms 0x19c016aa00 removes nothing more; its walks only end later inside its loops. The 144 requests of the NVIDIA dump go from 1283 to 1589 ms and gain removals in the copies of the same shaders. 197 of the 198 requests prepare, as before. Related: #2548 Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 161 +++++++++++++++++- core/libs/tests/PixelAncillary.cpp | 91 ++++++++++ .../src/MaskedSelectEliminator.cpp | 73 ++++++-- 3 files changed, 304 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 839a465a6ad6..56dc21e87b97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -77,6 +77,22 @@ struct Builder { return threadBit(low, high); } + std::pair ballotWords(IrValue& predicate) { + auto& ballot = emit(IrOpcode::Ballot, IrType::U32x4, {&predicate}); + return {&emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(0u)}), &emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(1u)})}; + } + + IrValue& wordAnd(IrValue& left, IrValue& right) { + return emit(IrOpcode::BitwiseAnd32, IrType::U32, {&left, &right}); + } + + IrValue& phi(IrType type, IrBlock& predecessor, IrValue& incoming) { + auto& created = program.CreateValue(IrOpcode::Phi, type); + block->AppendInstruction(&created); + created.AddPhiOperand(&predecessor, &incoming); + return created; + } + void keep(IrValue& value) { (void)emit(IrOpcode::ReferenceU32, IrType::Void, {&value}); } @@ -339,8 +355,13 @@ int main() { return count == 0u && !gone; }); - passed &= run("an exec merged by a phi inside a loop keeps the select", [&] { + passed &= run("an exec carried around a loop whose every incoming exec implies the write's exec drops the select", [&] { const auto [count, gone] = mergedExec(false, true); + return count == 1u && gone; + }); + + passed &= run("an exec carried around a loop with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, true); return count == 0u && !gone; }); @@ -425,9 +446,10 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - passed &= run("a write read inside a later loop must keep its select", [] { + const auto laterLoop = [](bool wideRead) { Builder b; - auto& exec = b.mask(16u); + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); IrBlock& head = *b.block; @@ -435,7 +457,138 @@ int main() { head.AddBranch(&loop); loop.AddBranch(&loop); b.block = &loop; - b.keep(b.select(exec, b.add(written, 2u), old)); + b.keep(b.select(wideRead ? wide : exec, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write read inside a later loop under its own exec loses its select", [&] { + const auto [count, gone] = laterLoop(false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a later loop under a wider exec keeps its select", [&] { + const auto [count, gone] = laterLoop(true); + return count == 0u && !gone; + }); + + enum class Latch { Narrowed, Revived }; + const auto carried = [](Latch latch, bool readInside) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + auto& value = b.phi(IrType::U32, entry, written); + auto& loopExec = b.threadBit(low, high); + auto& next = b.select(loopExec, b.add(value, 2u), value); + if (readInside) b.keep(b.select(loopExec, b.add(value, 3u), old)); + if (latch == Latch::Narrowed) { + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + low.AddPhiOperand(&header, &b.wordAnd(low, *stayLow)); + high.AddPhiOperand(&header, &b.wordAnd(high, *stayHigh)); + } else { + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + low.AddPhiOperand(&header, revivedLow); + high.AddPhiOperand(&header, revivedHigh); + } + value.AddPhiOperand(&header, &next); + b.block = &exit; + b.keep(b.select(b.threadBit(*savedLow, *savedHigh), b.add(value, 4u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write carried through a loop whose exec is saved, narrowed and restored loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a loop under the loop's narrowed exec loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, true); + return count == 1u && gone; + }); + + passed &= run("a lane that becomes active again inside a loop and reads the write keeps the select", [&] { + const auto [count, gone] = carried(Latch::Revived, true); + return count == 0u && !gone; + }); + + passed &= run("a lane that becomes active again inside a loop but never reads the write lets the select go", [&] { + const auto [count, gone] = carried(Latch::Revived, false); + return count == 1u && gone; + }); + + passed &= run("an exec proven only by assuming a loop phi is forgotten once that phi turns out wider", [] { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& kill = b.newBlock(); + IrBlock& revive = b.newBlock(); + IrBlock& latch = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&kill); + header.AddBranch(&revive); + kill.AddBranch(&latch); + revive.AddBranch(&latch); + latch.AddBranch(&header); + latch.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + (void)b.select(b.threadBit(low, high), written, old); + b.block = &kill; + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + auto& killedLow = b.wordAnd(low, *stayLow); + auto& killedHigh = b.wordAnd(high, *stayHigh); + b.keep(b.select(b.threadBit(killedLow, killedHigh), written, old)); + b.block = &revive; + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + b.block = &latch; + auto& mergedLow = b.phi(IrType::U32, kill, killedLow); + auto& mergedHigh = b.phi(IrType::U32, kill, killedHigh); + mergedLow.AddPhiOperand(&revive, revivedLow); + mergedHigh.AddPhiOperand(&revive, revivedHigh); + low.AddPhiOperand(&latch, &mergedLow); + high.AddPhiOperand(&latch, &mergedHigh); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a write mask computed inside an earlier loop does not let that loop's phis stand for it", [] { + Builder b; + auto& old = b.lane(); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, b.constant(0u)); + auto& high = b.phi(IrType::U32, entry, b.constant(0u)); + auto& bound = b.phi(IrType::U32, entry, b.constant(64u)); + auto& exec = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.lane(), &bound}); + const auto [stayLow, stayHigh] = b.ballotWords(exec); + low.AddPhiOperand(&header, stayLow); + high.AddPhiOperand(&header, stayHigh); + bound.AddPhiOperand(&header, &b.emit(IrOpcode::ISub32, IrType::U32, {&bound, &b.constant(8u)})); + b.block = &exit; + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); return b.eliminate() == 0u && !removed(written); }); diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 9832c0ebe578..15740c514a88 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -340,6 +340,93 @@ static void HelperLanesRefused(HelperRead read, std::uint32_t chain = 0u) { } Require(false); } +enum class LoopRead { ExportAfter, ExportInside, UniformLoop, RevivedExportInside }; + +static IrValue& LaneBit(IrBuilder& builder, IrValue& low, IrValue& high) { + IrValue& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + IrValue& word = builder.Select(builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}), low, high); + IrValue& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &builder.BitwiseAnd(lane, builder.Constant(31u))}); + return builder.INotEqual(builder.BitwiseAnd(shifted, builder.Constant(1u)), builder.Constant(0u)); +} +static HelperProgram HelperLoop(IrProgram& program, LoopRead read) { + auto& entry = Begin(program); + auto& header = program.CreateBlock(); + auto& exit = program.CreateBlock(); + program.BlockOrder().push_back(&header); + program.BlockOrder().push_back(&exit); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& frontFacing = FrontFacingBuiltin(builder); + IrValue& saved = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + IrValue& savedLow = builder.CompositeExtract(saved, 0u); + IrValue& savedHigh = builder.CompositeExtract(saved, 1u); + IrValue& low = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& high = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + header.AppendInstruction(&low); + header.AppendInstruction(&high); + header.AppendInstruction(&value); + low.AddPhiOperand(&entry, &savedLow); + high.AddPhiOperand(&entry, &savedHigh); + value.AddPhiOperand(&entry, &written); + builder.SetInsertionPoint(header); + IrValue& loopExec = read == LoopRead::UniformLoop ? live : LaneBit(builder, low, high); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(value), &builder.BitCastF32(value)}); + IrValue& next = builder.Select(loopExec, builder.BitCastU32(squared), value); + IrValue& stay = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&frontFacing}); + IrValue& stayLow = builder.CompositeExtract(stay, 0u); + IrValue& stayHigh = builder.CompositeExtract(stay, 1u); + const bool revived = read == LoopRead::RevivedExportInside; + low.AddPhiOperand(&header, revived ? &stayLow : &builder.BitwiseAnd(low, stayLow)); + high.AddPhiOperand(&header, revived ? &stayHigh : &builder.BitwiseAnd(high, stayHigh)); + value.AddPhiOperand(&header, &next); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + const auto exportValue = [&](IrValue& guard) { + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&value, &target, &builder.Constant(0u), &builder.Constant(0u)}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + }; + if (read == LoopRead::ExportInside || revived) exportValue(loopExec); + builder.SetInsertionPoint(exit); + if (read == LoopRead::ExportAfter) exportValue(LaneBit(builder, savedLow, savedHigh)); + if (read == LoopRead::UniformLoop) exportValue(live); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void HelperLoopOnly(LoopRead read) { + IrProgram program; + const HelperProgram built = HelperLoop(program, read); + LowerMasked(program); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + const IrValue* field = built.layer->Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLoopRefused(LoopRead read) { + IrProgram program; + static_cast(HelperLoop(program, read)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -385,4 +472,8 @@ int main() { HelperLanesRefused(HelperRead::DataTestExport); HelperLanesRefused(HelperRead::Ballot); HelperLanesRefused(HelperRead::FullWaveExport, 6000u); + HelperLoopOnly(LoopRead::ExportAfter); + HelperLoopOnly(LoopRead::ExportInside); + HelperLoopOnly(LoopRead::UniformLoop); + HelperLoopRefused(LoopRead::RevivedExportInside); } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index e9d218b806ac..be902084dd85 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -274,6 +274,11 @@ Region regionOf(const std::vector& regions, const IrBlock* block) { return block != nullptr && block->Id() < regions.size() ? regions[block->Id()] : Region::Unreached; } +bool followable(const std::vector& regions, const IrBlock* block, bool acrossLoops) { + const Region region = regionOf(regions, block); + return region == Region::Once || (acrossLoops && region == Region::Loop); +} + struct ImplicationKey { const IrValue* first = nullptr; const IrValue* second = nullptr; @@ -291,10 +296,13 @@ class Implications { public: Implications(const std::vector& regions, std::uint32_t waveSize, std::size_t& work) : regions(regions), waveSize(waveSize), work(work) {} - void Reset(const IrValue* writeMask) { + void Reset(const IrValue* writeMask, bool acrossLoops) { mask = writeMask; + loops = acrossLoops; always = alwaysTrue(writeMask, waveSize); results.clear(); + proven.clear(); + assumptionReads = 0; } bool Implies(const IrValue* condition, std::uint32_t depth = 0) { @@ -302,33 +310,61 @@ class Implications { if (condition == mask || always) return true; if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); const ImplicationKey key{condition, nullptr}; - if (const auto known = results.find(key); known != results.end()) return known->second; + if (const auto known = lookup(key)) return *known; if (depth > ImplicationDepthLimit || work == 0) return false; --work; - results.emplace(key, false); + const Attempt attempt = assume(key); bool result = false; if (condition->Opcode() == IrOpcode::LogicalAnd) { result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); } else if (condition->IsPhi()) { - result = mergesOutsideLoops(*condition); + result = merges(*condition); for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { result = Implies(condition->Argument(index), depth + 1u); } } else if (const auto bit = threadBit(condition, waveSize)) { result = wordsImply(bit->low, bit->high, depth + 1u); } - results[key] = result; - return result; + return settle(key, attempt, result); } private: static constexpr std::uint32_t ImplicationDepthLimit = 64; - bool mergesOutsideLoops(const IrValue& phi) const { - if (regionOf(regions, phi.Parent()) != Region::Once || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + enum class Proof : std::uint8_t { Assumed, Implied, NotImplied }; + + struct Attempt { + std::size_t proven = 0; + std::size_t assumptionReads = 0; + }; + + std::optional lookup(const ImplicationKey& key) { + const auto known = results.find(key); + if (known == results.end()) return std::nullopt; + if (known->second == Proof::Assumed) ++assumptionReads; + return known->second != Proof::NotImplied; + } + + Attempt assume(const ImplicationKey& key) { + results.emplace(key, Proof::Assumed); + return Attempt{proven.size(), assumptionReads}; + } + + bool settle(const ImplicationKey& key, const Attempt& attempt, bool implied) { + if (!implied && assumptionReads != attempt.assumptionReads) { + for (std::size_t index = attempt.proven; index < proven.size(); ++index) results.erase(proven[index]); + proven.resize(attempt.proven); + } + results[key] = implied ? Proof::Implied : Proof::NotImplied; + if (implied) proven.push_back(key); + return implied; + } + + bool merges(const IrValue& phi) const { + if (!followable(regions, phi.Parent(), loops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { const IrValue* incoming = phi.Argument(index)->Resolve(); - if (!incoming->HasImmediate() && regionOf(regions, incoming->Parent()) != Region::Once) return false; + if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), loops)) return false; } return true; } @@ -343,10 +379,10 @@ class Implications { bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; const ImplicationKey key{low, high}; - if (const auto known = results.find(key); known != results.end()) return known->second; + if (const auto known = lookup(key)) return *known; if (depth > ImplicationDepthLimit || work == 0) return false; --work; - results.emplace(key, false); + const Attempt attempt = assume(key); bool result = false; if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { const IrValue* ballot = low->Argument(0)->Resolve(); @@ -364,23 +400,25 @@ class Implications { } } } - if (!result && low->IsPhi() && mergesOutsideLoops(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && mergesOutsideLoops(*high)))) { + if (!result && low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { result = true; for (std::size_t index = 0; result && index < low->PhiBlockCount(); ++index) { const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); result = highIncoming != nullptr && wordsImply(low->Argument(index)->Resolve(), highIncoming, depth + 1u); } } - results[key] = result; - return result; + return settle(key, attempt, result); } const std::vector& regions; std::uint32_t waveSize; std::size_t& work; const IrValue* mask = nullptr; + bool loops = false; bool always = false; - std::unordered_map results; + std::unordered_map results; + std::vector proven; + std::size_t assumptionReads = 0; }; std::vector blockRegions(const IrProgram& program) { @@ -436,8 +474,9 @@ class MaskedLaneWalk { MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), limit(walkLimit(program)), implications(regions, program.WaveSize(), work) {} bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { + const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; work = limit; - implications.Reset(mask); + implications.Reset(mask, acrossLoops); startVisits(); pending.clear(); pending.push_back(&select); @@ -447,7 +486,7 @@ class MaskedLaneWalk { pending.pop_back(); for (const IrUse& use : value->OperandUses()) { const IrValue* user = use.user; - if (regionOf(regions, user->Parent()) != Region::Once) return false; + if (!followable(regions, user->Parent(), acrossLoops)) return false; if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; From 83f8c193c980de696f2b593ee7802be75e190003 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:26:51 +0200 Subject: [PATCH 4/6] fix(shader): treat an image read as a use that only reads where its exec is active An ImageRead carries a trailing exec argument like the buffer loads, but it was in neither the read-only-where-active list nor the lane-local list, so a masked select feeding its address was kept. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp | 11 +++++++++-- .../Optimization/src/MaskedSelectEliminator.cpp | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 56dc21e87b97..ecf3a581e49b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -636,6 +636,13 @@ int main() { case IrOpcode::LoadBufferU32: b.keep(b.select(exec, b.emit(opcode, IrType::U32, {&b.emit(IrOpcode::GetBufferResource, IrType::BufferResource, {}), &written, &b.constant(0u), &b.constant(0u), &active}), old)); break; + case IrOpcode::ImageRead: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& read = b.emit(opcode, IrType::U32x4, {&image, &address, &active}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old)); + break; + } default: b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); break; @@ -645,11 +652,11 @@ int main() { }; passed &= run("a store, load or export whose exec implies the write's exec drops the select", [&] { - return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::SetAttribute, true, false); + return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::ImageRead, true, false) && guarded(IrOpcode::SetAttribute, true, false); }); passed &= run("a store, load or export under a wider exec keeps the select", [&] { - return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false); }); passed &= run("a value used as a store's exec keeps the select", [&] { diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index be902084dd85..30331f633b85 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -204,6 +204,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { case IrOpcode::WriteSharedU32x3: case IrOpcode::WriteSharedU32x4: case IrOpcode::SetAttribute: + case IrOpcode::ImageRead: return true; default: return false; From fda4aa90c9f81fa3b5160150b3bee926b6923955 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:53:22 +0200 Subject: [PATCH 5/6] fix(shader): let image writes, explicit-LOD gathers and size queries pass the masked-lane walk Following the ImageRead case, every image operation was checked against the two lists the masked-lane walk uses: accesses guarded by a trailing exec operand, and lane-local values. - ImageWrite carries its exec last like StoreBuffer, and EmitWriteOp stores only inside an if on it (bindless tables and FMASK refuse the store). It joins the exec-guarded list. - A gather with an LOD of its own (gather4_l, gather4_lz) or outside a pixel shader reads only its lane's address, like the explicit-LOD samples the walk already follows. A pixel gather without one still keeps the select, since on the GPU it takes its LOD from the quad. - ImageQueryDimensions returns the size of its lane's level of the image and is lane-local. Left out on purpose: - ImageQueryLod and implicit-LOD samples read their quad. - ImageBvhIntersectRay has a trailing exec operand, but its emitter ignores it and records faults in every lane. - Image, buffer, shared and address atomics are guarded by their exec in the emitter too, but the test from 60c65521 pins them as keeping the select, and no refused shader has needed them so far. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 34 +++++++++++++++++-- .../src/MaskedSelectEliminator.cpp | 4 ++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ecf3a581e49b..17c06cd64a40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -592,7 +592,7 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags) { + const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; @@ -608,7 +608,7 @@ int main() { MemoryFlags flags{0u, 0u}; std::uint64_t bits = 0; std::memcpy(&bits, &flags, sizeof(flags)); - auto& sampled = b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}, bits); + auto& sampled = b.emit(opcode, IrType::U32x4, {&image, &sampler, &address}, bits); b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&sampled, &b.constant(0u)}), old)); b.eliminate(); return removed(written); @@ -622,6 +622,26 @@ int main() { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); + passed &= run("a pixel gather without an LOD of its own takes it from the quad and keeps the select", [&] { + return !sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageGatherRaw) && !sample(IrShaderStage::Pixel, RdnaImageSampleFlagBias, IrOpcode::ImageGatherRaw); + }); + + passed &= run("a gather with an explicit LOD reads only its lane and drops the select", [&] { + return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero, IrOpcode::ImageGatherRaw) && sample(IrShaderStage::Pixel, RdnaImageSampleFlagLod, IrOpcode::ImageGatherRaw) && + sample(IrShaderStage::Mesh, 0u, IrOpcode::ImageGatherRaw); + }); + + passed &= run("an image size query reads only its lane's level and drops the select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written}); + auto& size = b.emit(IrOpcode::ImageQueryDimensions, IrType::U32x4, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&size, &b.constant(0u)}), old)); + return b.eliminate() == 1u && removed(written); + }); + const auto guarded = [](IrOpcode opcode, bool sameExec, bool asActive) { Builder b; auto& wide = b.mask(48u); @@ -643,6 +663,12 @@ int main() { b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old)); break; } + case IrOpcode::ImageWrite: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&old, &old}); + auto& texel = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}); + b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address, &texel, &active}); + break; + } default: b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); break; @@ -659,6 +685,10 @@ int main() { return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false); }); + passed &= run("an image write under the write's exec drops the select, under a wider exec or as its exec keeps it", [&] { + return guarded(IrOpcode::ImageWrite, true, false) && !guarded(IrOpcode::ImageWrite, false, false) && !guarded(IrOpcode::ImageWrite, true, true); + }); + passed &= run("a value used as a store's exec keeps the select", [&] { return !guarded(IrOpcode::WriteSharedU32, true, true); }); diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 30331f633b85..e500ead2ae57 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -171,6 +171,7 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::FPFrexpMant64: case IrOpcode::FPFrexpExp64: case IrOpcode::MakeImageAddress: + case IrOpcode::ImageQueryDimensions: return true; default: return false; @@ -205,6 +206,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { case IrOpcode::WriteSharedU32x4: case IrOpcode::SetAttribute: case IrOpcode::ImageRead: + case IrOpcode::ImageWrite: return true; default: return false; @@ -212,7 +214,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { } bool isExplicitLodSample(const IrProgram& program, const IrValue& value) { - if (value.Opcode() != IrOpcode::ImageSampleRaw) return false; + if (value.Opcode() != IrOpcode::ImageSampleRaw && value.Opcode() != IrOpcode::ImageGatherRaw) return false; if (program.Resources().stage != IrShaderStage::Pixel) return true; const auto index = value.Flags().index; if (index >= program.Resources().memoryInfo.size()) return false; From 9870a3ff7970667ddad87c1aec6cc8a54def0fc6 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:39:50 +0200 Subject: [PATCH 6/6] feat(shader): give reads the masked-select pass keeps the whole pixel ancillary word A packed ancillary word that still reaches an export, or any other read the masked-select pass cannot drop, no longer stops the recompile. After that pass every remaining use gets the composed word: the primitive class at bits 0-1, SampleId at bits 8-11 and Layer at bits 16-28, with every other bit zero. Those lanes do export the ancillary VGPR on the console, so the word has to be right, not just present. The primitive class (0 point, 1 line, 2 triangle, 3 rectangle) comes from the draw. RasterPrimitiveOf maps rect lists to 3, line lists on the vertex path to 1 and everything else the driver accepts to 2, because its geometry and tessellation paths only output triangles. It reaches the shader as pipeline specialization constant 8233, the way dual-source blending does: a request field that is folded into the specialized module and is part of the result memo, the draw key and the serialized request (version 16). Only shaders that read the class pay for it. ShaderInfo records the read (disk cache format 25), and only those shaders put the class in their module key, so a shader that reads just the sample or layer field keeps one module for every primitive class. The draw sets the class only when the pixel shader loads the ancillary VGPR, so other draws keep the draw cache entries they have today. The VRS rates (bits 2-5) and the eye id (bit 7) are 0 because the driver supports neither VRS nor multiview. SampleId is 0 because the driver rejects MSAA and per-sample shading, and Layer is 0 while it ignores vertex layer exports. TechnicalDebt.md records the layout sources and these assumptions. Tests: - pixel_ancillary builds an export to target 0 whose two channels select between a packed value and a phi carrying the word. It checks the word for each primitive class, sample and layer, checks that the three shapes the masked-select pass keeps now compile with the composed word, and that a program whose select the pass drops reads no class. - agc_driver_pixel_ancillary_word exports the word from a draw and reads back 2, 3 and 1 for triangle, rectangle and line. A shader that reads only the sample field shares one module across the classes, and the whole-word shader gets one per class. It also checks RasterPrimitiveOf. - agc_shader_memory covers the version 16 round trip, and agc_shader_disk_cache the new flag. Both ancillary tests fail with the old refusal when the composition is disabled. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 10 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 11 +- .../tests/execution/PixelAncillaryWord.cpp | 306 ++++++++++++++++++ core/libs/tests/PixelAncillary.cpp | 69 +++- .../ControlFlow/src/RequestSerializer.cpp | 14 +- .../IrMetadata/ShaderInfo.hpp | 1 + .../IrMetadata/StageIO.hpp | 1 + .../include/Optimization/ConstantFolder.hpp | 2 + .../Optimization/src/ConstantFolder.cpp | 53 ++- .../Optimization/src/ShaderInfoCollector.cpp | 4 + .../recompiler/PipelineSpecialization.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 6 + core/shader/recompiler/Recompiler.hpp | 8 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 3 + docs/dev/TechnicalDebt.md | 2 +- 22 files changed, 477 insertions(+), 24 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f4444a8083bd..2a455c681e3e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -114,6 +114,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; + if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); return product; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 090b9b10dbea..c352dc495cc6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -68,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); + mix(static_cast(pixel.rasterPrimitive)); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 1759f417ebd6..1cfdcf606a84 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -105,7 +105,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.rasterPrimitive != q.rasterPrimitive) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 05cb27663e88..80a7febe0791 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -113,6 +113,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f29320922a1a..4ed9e64851e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -782,6 +782,16 @@ std::array ExportPackings(const State& return packings; } +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { + if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; + switch (state.topology) { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return ShaderRecompiler::RasterPrimitive::Point; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: return ShaderRecompiler::RasterPrimitive::Line; + default: return ShaderRecompiler::RasterPrimitive::Triangle; + } +} + std::array ExportMappings(const State& state) { std::array mappings{}; mappings.fill(0xe4u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 0aeb9425e42e..374e895e43ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -230,6 +230,7 @@ CompiledVariant sampleVariant() { info.info.hasBitwiseXor = true; info.info.usesDma = true; info.info.usesFaultBuffer = true; + info.info.rasterPrimitive = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c1c8977ac01b..9aa0a135ee02 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1372,13 +1372,15 @@ void verifyPixelRequestSerialization() { minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; + minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQ8AAAA=", "new requests did not use serialization version 15"); + require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 17u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing or dual-source flag was accepted"); + for (std::size_t bytes = 0; bytes < 18u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source flag or primitive class was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRAAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1467,6 +1469,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); + require(pixel.rasterPrimitive == RasterPrimitive::Triangle, "a legacy request drew something other than triangles"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp new file mode 100644 index 000000000000..559a514ed707 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp @@ -0,0 +1,306 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 5> Programs{}; +std::size_t programCount = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::uint32_t ancillaryVgpr, bool wholeWord) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + if (wholeWord) { + code[at++] = 0x36000000u | (20u << 17u) | (ancillaryVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (ancillaryVgpr << 9u) | 0x90u; + } else { + code[at++] = 0xd5480000u | 20u; + code[at++] = (0x84u << 18u) | (0x88u << 9u) | (256u + ancillaryVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + } + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, ShaderRecompiler::RasterPrimitive primitive, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t ancillaryVgpr, ShaderRecompiler::RasterPrimitive primitive, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, primitive, Program(ancillaryVgpr, true)); + const auto stored = ReadBack(device, block.Address(), bytes); + const auto word = static_cast(primitive); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(word & 0xffffu), static_cast(word >> 16u), 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +std::uint64_t PixelModule(const ShaderRecompiler::SpirvTarget& target, std::span code, ShaderRecompiler::RasterPrimitive primitive) { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, pixel, std::nullopt, memory}, + target, + PixelPushLayout(0, target) + }; + return ShaderRecompiler::Recompile(request).PipelineVariantId(); +} + +void ExpectModules(const ShaderRecompiler::SpirvTarget& target, bool wholeWord, const std::string& what) { + using ShaderRecompiler::RasterPrimitive; + const auto code = Program(0u, wholeWord); + const auto triangle = PixelModule(target, code, RasterPrimitive::Triangle); + Require(PixelModule(target, code, RasterPrimitive::Triangle) == triangle, what + ": the same primitive class built a second module"); + for (const auto primitive : {RasterPrimitive::Line, RasterPrimitive::Rectangle}) { + Require((PixelModule(target, code, primitive) != triangle) == wholeWord, what + (wholeWord ? ": a line or rectangle draw reused the triangle module" : ": a line or rectangle draw built its own module")); + } +} + +void ExpectPrimitive(VkPrimitiveTopology topology, bool rectList, bool tessellated, ShaderRecompiler::RasterPrimitive expected, const std::string& what) { + AgcDriver::Graphics::State state{}; + state.topology = topology; + state.rectList = rectList; + if (tessellated) state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{}; + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == expected, what + ": wrong primitive class"); +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + using ShaderRecompiler::RasterPrimitive; + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, false, RasterPrimitive::Triangle, "triangle strip"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_LINE_LIST, false, false, RasterPrimitive::Line, "line list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, false, false, RasterPrimitive::Point, "point list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, true, false, RasterPrimitive::Rectangle, "rect list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, false, true, RasterPrimitive::Triangle, "tessellated patches"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Triangle, "ANCILLARY alone, triangle"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::Ancillary), 2u, RasterPrimitive::Rectangle, "ANCILLARY after the perspective center, rectangle"); + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Line, "ANCILLARY alone, line"); + ExpectModules(device->Target(), true, "whole word"); + ExpectModules(device->Target(), false, "sample field only"); + std::puts("pixel ancillary word tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 15740c514a88..c6fa84df1466 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -3,6 +3,7 @@ #include "Optimization/DeadCodeEliminator.hpp" #include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/ShaderInfoCollector.hpp" +#include "Recompiler.hpp" #include #include #include @@ -18,6 +19,7 @@ struct Row { std::uint32_t sample; std::uint32_t layer; bool frontFacing; + RasterPrimitive primitive = RasterPrimitive::Triangle; }; constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}}; @@ -153,6 +155,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { case StageInputKind::SampleId: return row.sample; case StageInputKind::Layer: return row.layer; case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u; + case StageInputKind::RasterPrimitive: return static_cast(row.primitive); default: break; } break; @@ -305,19 +308,21 @@ static HelperProgram HelperOnly(IrProgram& program, HelperRead read, std::uint32 static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); return HelperProgram {&written, &layer, &value}; } -static void LowerMasked(IrProgram& program) { +static void LowerMasked(IrProgram& program, bool compose = false) { ConstantFolder().Fold(program); DeadCodeEliminator().RemoveIdentities(program); DeadCodeEliminator().Eliminate(program); static_cast(MaskedSelectEliminator().Eliminate(program)); DeadCodeEliminator().Eliminate(program); + if (compose && ConstantFolder().ComposePackedAncillary(program)) DeadCodeEliminator().Eliminate(program); const ShaderPixelInputInfo pixel {}; ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); } static void HelperLanesOnly(std::uint32_t chain = 0u) { IrProgram program; const HelperProgram built = HelperOnly(program, HelperRead::Export, chain); - LowerMasked(program); + LowerMasked(program, true); + Require(!program.Info().rasterPrimitive); for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); @@ -427,6 +432,62 @@ static void HelperLoopRefused(LoopRead read) { } Require(false); } +static bool AncillaryRemains(const IrProgram& program) { + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::PackedAncillary) return true; + } + } + return false; +} +static void ExportedWholeWord() { + IrProgram program; + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& tested = builder.INotEqual(builder.Select(FrontFacingBuiltin(builder), builder.Constant(7u), builder.Constant(0u)), builder.Constant(0u)); + builder.SetInsertionPoint(body); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& carried = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&carried); + carried.AddPhiOperand(&entry, &ancillary); + carried.AddPhiOperand(&body, &written); + builder.SetInsertionPoint(merge); + const auto channel = [&](float value) -> IrValue& { + IrValue& half = builder.ConstantF32(value); + return builder.Select(tested, builder.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&half, &half}), carried); + }; + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&channel(0.25f), &channel(0.75f), &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &live}, ExportFlags {0u, 0u})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + const IrValue* word = carried.Argument(0)->Resolve(); + constexpr std::array words {{{0u, 0u, false, RasterPrimitive::Triangle}, {0u, 0u, false, RasterPrimitive::Rectangle}, {0u, 0u, false, RasterPrimitive::Line}, {15u, 0x1FFFu, false, RasterPrimitive::Point}, {3u, 0x7FFu, false, RasterPrimitive::Triangle}}}; + for (const Row& row : words) { + Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (row.layer << 16u))); + } +} +static void WholeWordReplacesRefusal(HelperRead read) { + IrProgram program; + const HelperProgram built = HelperOnly(program, read); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(built.written->Parent() != nullptr); + const Row row {9u, 0x123u, false, RasterPrimitive::Rectangle}; + Require(Evaluate(built.written->Argument(2), row) == (3u | (9u << 8u) | (0x123u << 16u))); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -476,4 +537,8 @@ int main() { HelperLoopOnly(LoopRead::ExportInside); HelperLoopOnly(LoopRead::UniformLoop); HelperLoopRefused(LoopRead::RevivedExportInside); + ExportedWholeWord(); + WholeWordReplacesRefusal(HelperRead::FullWaveExport); + WholeWordReplacesRefusal(HelperRead::DataTestExport); + WholeWordReplacesRefusal(HelperRead::Ballot); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index e5b8a2f7c649..18b580a3adb8 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -243,6 +243,14 @@ ColorExportPacking readColorExportPacking(Reader& reader) { return static_cast(value); } +RasterPrimitive readRasterPrimitive(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(RasterPrimitive::Rectangle)) { + throw std::runtime_error("RequestSerializer: invalid RasterPrimitive value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -324,6 +332,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(value)); } writer.WriteBool(info.dualSourceBlend); + writer.WriteU8(static_cast(info.rasterPrimitive)); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -371,6 +380,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } } if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); + if (version >= 16u) info.rasterPrimitive = readRasterPrimitive(reader); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -732,7 +742,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(15u); + writer.WriteU32(16u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -759,7 +769,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 15u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index aa1f183eea97..089d299cf7b6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -35,6 +35,7 @@ struct ShaderInfo { bool bdaWrites = false; bool usesFaultBuffer = false; bool dispatchThreadLimit = false; + bool rasterPrimitive = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index ca9e8fd91e7b..66f8516c9c29 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -36,6 +36,7 @@ enum class StageInputKind { DispatchThreadLimit, Parameter, HelperInvocation, + RasterPrimitive, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index 6be55fb2319a..d2df4ed3cca0 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,6 +14,8 @@ class ConstantFolder { void Fold(IrProgram& program, std::span blocks) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; + private: [[nodiscard]] bool tryFoldValue(IrProgram& program, IrValue& value) const; }; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index a37056fc7749..78d9c71511ab 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -243,9 +243,25 @@ std::optional packedField(std::uint32_t offset, std::uint32_t count return static_cast(field - packedFields.begin()); } +bool isPackedAncillary(const IrValue& builtin) { + auto& kind = resolveArg(builtin, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::PackedAncillary; +} + +IrValue& insertBefore(IrProgram& program, IrValue& next, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + IrValue& created = program.CreateValue(opcode, IrType::U32); + created.AddArgument(&lhs); + created.AddArgument(&rhs); + next.Parent()->InsertInstructionBefore(&next, &created); + return created; +} + +IrValue& insertBuiltin(IrProgram& program, IrBuilder& builder, IrValue& next, StageInputKind kind) { + return insertBefore(program, next, IrOpcode::GetBuiltin, builder.Constant(static_cast(kind)), builder.Constant(0u)); +} + void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { - auto& kind = resolveArg(ancillary, 0); - if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + if (!isPackedAncillary(ancillary)) { return; } const std::vector uses = ancillary.OperandUses(); @@ -291,21 +307,12 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { if (fields[index] == nullptr) { - const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); - created.AddArgument(&builder.Constant(static_cast(builtin))); - created.AddArgument(&builder.Constant(0u)); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - fields[index] = &created; + fields[index] = &insertBuiltin(program, builder, ancillary, index == 0u ? StageInputKind::SampleId : StageInputKind::Layer); } return *fields[index]; }; const auto emit = [&](IrOpcode opcode, IrValue& lhs, IrValue& rhs) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - created.AddArgument(&lhs); - created.AddArgument(&rhs); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - return created; + return insertBefore(program, ancillary, opcode, lhs, rhs); }; if (!forwarded.empty()) { IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, field(0u), builder.Constant(8u)); @@ -755,4 +762,24 @@ void ConstantFolder::Fold(IrProgram& program) const { Fold(program, program.BlockOrder()); } +bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { + std::vector words; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && inst->HasUses() && isPackedAncillary(*inst)) { + words.push_back(inst); + } + } + } + IrBuilder builder(program); + for (IrValue* word : words) { + IrValue& primitive = insertBuiltin(program, builder, *word, StageInputKind::RasterPrimitive); + IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::SampleId), builder.Constant(8u)); + IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::Layer), builder.Constant(16u)); + IrValue& low = insertBefore(program, *word, IrOpcode::BitwiseOr32, primitive, sample); + word->ReplaceAllUsesWith(&insertBefore(program, *word, IrOpcode::BitwiseOr32, low, layer)); + } + return !words.empty(); +} + } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 672f01ef4356..a999c04a4350 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -143,6 +143,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } case StageInputKind::Layer: case StageInputKind::SampleId: + case StageInputKind::RasterPrimitive: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { return Fail("typed pixel scalar input is invalid"); } @@ -386,6 +387,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::DispatchThreadLimit: info.dispatchThreadLimit = true; break; + case StageInputKind::RasterPrimitive: + info.rasterPrimitive = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index f6ae066765ac..8ed8a0af98de 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -26,6 +26,7 @@ inline constexpr std::uint32_t PushDataOffset = 8188u; inline constexpr std::uint32_t ExportBase = 8192u; inline constexpr std::uint32_t ExportPackingBase = 8224u; inline constexpr std::uint32_t DualSourceBlend = 8232u; +inline constexpr std::uint32_t RasterPrimitive = 8233u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index bbaece6136bb..a1c6c1785d1f 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -212,6 +212,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { deadCodeEliminator.Eliminate(program); } + if (constantFolder.ComposePackedAncillary(program)) { + deadCodeEliminator.Eliminate(program); + } }; simplify(); if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) { @@ -689,6 +692,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast(packing)); moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); + if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; @@ -732,6 +736,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]}); constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); + if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { @@ -919,6 +924,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto mapping : request.context.pixel->targetExportMapping) mix(mapping); for (const auto packing : request.context.pixel->targetExportPacking) mix(static_cast(packing)); mix(request.context.pixel->dualSourceBlend); + mix(static_cast(request.context.pixel->rasterPrimitive)); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5688af8e8019..b3575279183d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -113,6 +113,13 @@ enum class ColorExportPacking : std::uint8_t { Unorm10_11_11 }; +enum class RasterPrimitive : std::uint8_t { + Point, + Line, + Triangle, + Rectangle +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -140,6 +147,7 @@ struct ShaderPixelStageInfo { std::array targetExportMapping; std::array targetExportPacking; bool dualSourceBlend; + RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index fe8b0889611d..834b5cba39c1 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -534,6 +534,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.bdaWrites); writer.Value(info.usesFaultBuffer); writer.Value(info.dispatchThreadLimit); + writer.Value(info.rasterPrimitive); encodeLayout(writer, compiled.bindings); } @@ -641,6 +642,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.bdaWrites); reader.Value(info.usesFaultBuffer); reader.Value(info.dispatchThreadLimit); + reader.Value(info.rasterPrimitive); decodeLayout(reader, compiled.bindings); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 6111e47c17b5..08b1d6c8d74e 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 24; +inline constexpr std::uint32_t FormatVersion = 25; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 07f3ca94857d..36ccef0f7575 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -45,6 +45,9 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::DispatchThreadLimit) { return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); } + if (kind == StageInputKind::RasterPrimitive) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::RasterPrimitive, static_cast(RasterPrimitive::Triangle)); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3d64959512cb..74d50e410292 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -48,7 +48,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [getuid, geteuid, getgid, getegid and issetugid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet -- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select, move or phi is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 +- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample and layer fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. The VRS rates and the eye id are zero because the driver supports neither variable-rate shading nor multiview; a title that enables either would read other bits on the console. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`), and the layer field reads Layer, which is 0 while the driver ignores vertex layer exports - [sceAudioPropagationSourceSetAudioPath](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. - [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. sRGB formats, anisotropic filters and bilinear cube comparisons without `DISABLE_CUBE_WRAP` are refused; cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified