From f4edc9ae929ae451ef87dfc5c954ec4ac17250a8 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 7 Oct 2026 16:07:43 -0500 Subject: [PATCH 1/2] fix(recompiler): split image reads whose descriptor is a web of SRT Phis SplitDescriptorPhi (#414) handles a T# or S# that is a Phi of SRT reads in one block. A descriptor picked inside a loop through a jump table reaches the read as a chain of Phis rooted at the loop header, so the plan threw "GetImageResource dword 0 is not a valid runtime value". When the one-block split does not apply, walk the descriptor dwords together through the Phi web, give each distinct SRT descriptor an arm, build a U32 selector Phi web carrying the arm index, and select between one copy of the read per arm. Paths whose dwords hold a program-counter, EXEC or ballot value fold onto the first arm (listed in TechnicalDebt); any other descriptor that cannot be recomputed still throws. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 14 ++ .../Optimization/src/ResourceTracker.cpp | 192 ++++++++++++++++-- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 192 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7a2e71bca..2c0d5ac08 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -509,6 +509,20 @@ void verifyDescriptorPhis() { auto dynamic = makeRequest(dynamicCode); expectFailure([&] { static_cast(GetResourcePlan(dynamic)); }, "GetSamplerResource dword 0 is not a valid runtime value", "descriptor Phi: an edge without an SRT slot was accepted"); + + const std::vector loopCode{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}; + auto programCounterCode = loopCode; + programCounterCode.insert(programCounterCode.begin() + 8, 0xbe8e1f00u); + for (const auto* code : std::array*, 2>{&loopCode, &programCounterCode}) { + const auto loopCapture = compile(*code, 2u, 1u); + const auto& loopImages = loopCapture->snapshot.images; + const auto holdsLoopImage = [&](const std::array& words) { + return std::ranges::any_of(loopImages, [&](const DescriptorValue& value) { + return std::equal(words.begin(), words.end(), value.dwords.begin()); + }); + }; + require(loopImages.size() == 2u && holdsLoopImage(first) && holdsLoopImage(second), "descriptor Phi: the loop's chained T# Phis were not split into the two SRT T#s"); + } } void verifyProgramCounterRelativeData() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index c6839c69f..1f407ecc7 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -598,7 +599,7 @@ private: void SplitDescriptorPhi(IrValue& inst) { const auto info = ImageOpcodeInfoOf(inst.Opcode()); const std::uint32_t handleCount = info.needsSampler ? 2u : 1u; - if (inst.ArgumentCount() < handleCount) { + if (inst.ArgumentCount() < handleCount || inst.Parent() == nullptr) { return; } std::array handles {}; @@ -625,21 +626,27 @@ private: if (phis.empty()) { return; } + if (!SplitDescriptorEdges(inst, handles, split, handleCount, phis)) { + SplitDescriptorWeb(inst, handles, split, handleCount); + } + } + + bool SplitDescriptorEdges(IrValue& inst, const std::array& handles, const std::array& split, std::uint32_t handleCount, const std::vector& phis) { IrBlock* block = phis.front()->Parent(); - if (block == nullptr || inst.Parent() == nullptr) { - return; + if (block == nullptr) { + return false; } const auto& predecessors = block->Predecessors(); if (predecessors.size() < 2u) { - return; + return false; } for (const IrValue* phi : phis) { if (phi->Parent() != block || phi->PhiBlockCount() != predecessors.size()) { - return; + return false; } for (const IrBlock* predecessor : predecessors) { if (PhiIncoming(*phi, predecessor) == nullptr) { - return; + return false; } } } @@ -665,24 +672,40 @@ private: for (std::uint32_t slot = 0; slot < handleCount; slot++) { for (std::size_t dword = 0; split[slot] && dword < handles[slot]->ArgumentCount(); dword++) { if (SubstituteEdge(handles[slot]->Argument(dword), arm, inst, false, phiSearchDepth) == nullptr) { - return; + return false; } } } } + std::vector conditions; + for (std::uint32_t arm = 0; arm + 1u < arms.size(); arm++) { + std::vector edges(edgeArm.size()); + for (std::size_t edge = 0; edge < edgeArm.size(); edge++) { + edges[edge] = edgeArm[edge] == arm; + } + conditions.push_back(&EdgeSelector(*block, edges)); + } + ReplaceWithArmCopies(inst, handles, split, handleCount, conditions, [&](std::uint32_t arm, std::uint32_t slot, std::size_t dword) { + return SubstituteEdge(handles[slot]->Argument(dword), arms[arm], inst, true, phiSearchDepth); + }); + return true; + } + + template + void ReplaceWithArmCopies(IrValue& inst, const std::array& handles, const std::array& split, std::uint32_t handleCount, std::span conditions, Dword&& dword) { std::vector copies; const auto flags = inst.Flags(); const auto memory = m_program.Resources().memoryInfo.at(flags.index); - for (std::uint32_t arm = 0; arm < arms.size(); arm++) { + for (std::uint32_t arm = 0; arm <= conditions.size(); arm++) { std::vector arguments(inst.Arguments().begin(), inst.Arguments().end()); for (std::uint32_t slot = 0; slot < handleCount; slot++) { if (!split[slot]) { continue; } std::vector dwords; - for (std::size_t dword = 0; dword < handles[slot]->ArgumentCount(); dword++) { - dwords.push_back(SubstituteEdge(handles[slot]->Argument(dword), arms[arm], inst, true, phiSearchDepth)); + for (std::size_t index = 0; index < handles[slot]->ArgumentCount(); index++) { + dwords.push_back(dword(arm, slot, index)); } arguments[slot] = EmitCopy(*handles[slot], dwords, inst); } @@ -706,11 +729,7 @@ private: }; IrValue* selected = extract(copies.back()); for (std::uint32_t arm = static_cast(copies.size()) - 1u; arm-- > 0u;) { - std::vector edges(edgeArm.size()); - for (std::size_t edge = 0; edge < edgeArm.size(); edge++) { - edges[edge] = edgeArm[edge] == arm; - } - const std::array arguments {&EdgeSelector(*block, edges), extract(copies[arm]), selected}; + const std::array arguments {conditions[arm], extract(copies[arm]), selected}; selected = EmitBefore(inst, IrOpcode::SelectU32, IrType::U32, arguments); } components[component] = selected; @@ -722,6 +741,149 @@ private: inst.Parent()->RemoveInstruction(&inst); } + static bool CarriesControlValue(IrValue* value, std::uint32_t depth) { + value = value->Resolve(); + if (value->HasImmediate() || value->IsPhi()) { + return false; + } + switch (value->Opcode()) { + case IrOpcode::GetShaderBase: + case IrOpcode::GetExec: + case IrOpcode::GetExecLo: + case IrOpcode::GetExecHi: + case IrOpcode::Ballot: + case IrOpcode::UndefU32: return true; + default: break; + } + if (depth == 0u || (!Detail::IsRuntimeUniformOp(value->Opcode()) && value->Opcode() != IrOpcode::CompositeExtractU32x4)) { + return false; + } + for (std::size_t index = 0; index < value->ArgumentCount(); index++) { + if (CarriesControlValue(value->Argument(index), depth - 1u)) { + return true; + } + } + return false; + } + + struct DescriptorWeb { + static constexpr std::size_t maxStates = 4096u; + static constexpr std::size_t maxArms = 64u; + std::vector> arms; + std::map, IrValue*> selectors; + std::vector> phis; + }; + + IrValue* WebSelector(std::vector tuple, DescriptorWeb& web, IrValue& position) { + for (IrValue*& value : tuple) { + value = value->Resolve(); + while (value->IsPhi()) { + IrValue* invariant = ResolveInvariantPhi(m_program.Resources(), value); + if (invariant == nullptr || invariant == value) { + break; + } + value = invariant->Resolve(); + } + } + if (const auto known = web.selectors.find(tuple); known != web.selectors.end()) { + return known->second; + } + const auto phi = std::ranges::find_if(tuple, [](const IrValue* value) { return value->IsPhi(); }); + if (phi == tuple.end()) { + if (std::ranges::any_of(tuple, [](IrValue* value) { return CarriesControlValue(value, phiSearchDepth); })) { + return &m_builder.Constant(0u); + } + for (IrValue* value : tuple) { + if (Rematerialize(value, position, false, phiSearchDepth) == nullptr) { + return nullptr; + } + } + const auto same = std::ranges::find_if(web.arms, [&](const std::vector& arm) { + return std::ranges::equal(tuple, arm, [&](IrValue* left, IrValue* right) { return EquivalentValue(m_program.Resources(), left, right); }); + }); + if (same != web.arms.end()) { + return &m_builder.Constant(static_cast(same - web.arms.begin())); + } + if (web.arms.size() == DescriptorWeb::maxArms) { + return nullptr; + } + web.arms.push_back(std::move(tuple)); + return &m_builder.Constant(static_cast(web.arms.size() - 1u)); + } + IrBlock* block = (*phi)->Parent(); + if (block == nullptr || web.selectors.size() == DescriptorWeb::maxStates) { + return nullptr; + } + const auto& predecessors = block->Predecessors(); + if (predecessors.size() < 2u) { + return nullptr; + } + for (const IrValue* value : tuple) { + if (value->IsPhi() && (value->Parent() != block || value->PhiBlockCount() != predecessors.size())) { + return nullptr; + } + } + IrValue& selector = m_program.CreateValue(IrOpcode::Phi, IrType::U32); + web.selectors.emplace(tuple, &selector); + web.phis.emplace_back(block, &selector); + for (IrBlock* predecessor : predecessors) { + std::vector incoming; + for (IrValue* value : tuple) { + IrValue* edge = value->IsPhi() ? PhiIncoming(*value, predecessor) : value; + if (edge == nullptr) { + return nullptr; + } + incoming.push_back(edge); + } + IrValue* edgeSelector = WebSelector(std::move(incoming), web, position); + if (edgeSelector == nullptr) { + return nullptr; + } + selector.AddPhiOperand(predecessor, edgeSelector); + } + return &selector; + } + + void SplitDescriptorWeb(IrValue& inst, const std::array& handles, const std::array& split, std::uint32_t handleCount) { + std::vector tuple; + std::array offsets {}; + for (std::uint32_t slot = 0; slot < handleCount; slot++) { + offsets[slot] = tuple.size(); + for (std::size_t dword = 0; split[slot] && dword < handles[slot]->ArgumentCount(); dword++) { + tuple.push_back(handles[slot]->Argument(dword)); + } + } + DescriptorWeb web; + IrValue* selector = WebSelector(tuple, web, inst); + const bool select = selector != nullptr && web.arms.size() > 1u; + if (select) { + for (const auto& [block, phi] : web.phis) { + block->InsertInstructionBefore(nullptr, phi); + } + } else { + for (const auto& [block, phi] : web.phis) { + for (std::size_t index = 0; index < phi->ArgumentCount(); index++) { + phi->ReplaceArgument(index, nullptr); + } + } + for (const auto& [block, phi] : web.phis) { + phi->Invalidate(); + } + } + if (selector == nullptr || web.arms.empty()) { + return; + } + + std::vector conditions; + for (std::uint32_t arm = 0; arm + 1u < web.arms.size(); arm++) { + const std::array compare {selector, &m_builder.Constant(arm)}; + conditions.push_back(EmitBefore(inst, IrOpcode::IEqual32, IrType::Bool, compare)); + } + ReplaceWithArmCopies(inst, handles, split, handleCount, conditions, [&](std::uint32_t arm, std::uint32_t slot, std::size_t dword) { + return Rematerialize(web.arms[arm][offsets[slot] + dword], inst, true, phiSearchDepth); + }); + } + void SplitDescriptorPhis() { std::vector candidates; for (auto& block : m_program.Blocks()) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 084451064..859ee88b0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -233,6 +233,7 @@ Throughout the project, every function at every stage either **does exactly what - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. +- [Image reads through a web of descriptor Phis](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) are split into one read per distinct SRT descriptor. A path whose T# or S# dwords hold a program-counter, EXEC or ballot value, or an undefined register, is folded onto the first descriptor: no real descriptor holds such a value, so the path is assumed not to reach the read. If a title does read through it, the hardware would fetch from a garbage descriptor, and the read here returns the first descriptor's result instead. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. From f5ba4e9c68dabe3be38fea19d484b02e7b9de9c6 Mon Sep 17 00:00:00 2001 From: Chad Date: Thu, 8 Oct 2026 14:02:49 -0500 Subject: [PATCH 2/2] fix(recompiler): throw for descriptor web leaves that are not SRT descriptors A path whose T# or S# dwords hold a program-counter, EXEC or compare-mask value is no longer folded onto the first descriptor: the web split gives up and the read throws as on main. Cache the invariant-Phi resolution per web, lower the state limit to 512 and test the program-counter, EXEC and compare-mask rows, unwritten registers, and the arm and state limits. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 66 +++++++++++++++---- .../Optimization/src/ResourceTracker.cpp | 42 +++++------- docs/dev/TechnicalDebt.md | 1 - 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2c0d5ac08..d3e266be7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -511,18 +511,62 @@ void verifyDescriptorPhis() { expectFailure([&] { static_cast(GetResourcePlan(dynamic)); }, "GetSamplerResource dword 0 is not a valid runtime value", "descriptor Phi: an edge without an SRT slot was accepted"); const std::vector loopCode{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}; - auto programCounterCode = loopCode; - programCounterCode.insert(programCounterCode.begin() + 8, 0xbe8e1f00u); - for (const auto* code : std::array*, 2>{&loopCode, &programCounterCode}) { - const auto loopCapture = compile(*code, 2u, 1u); - const auto& loopImages = loopCapture->snapshot.images; - const auto holdsLoopImage = [&](const std::array& words) { - return std::ranges::any_of(loopImages, [&](const DescriptorValue& value) { - return std::equal(words.begin(), words.end(), value.dwords.begin()); - }); - }; - require(loopImages.size() == 2u && holdsLoopImage(first) && holdsLoopImage(second), "descriptor Phi: the loop's chained T# Phis were not split into the two SRT T#s"); + const auto loopCapture = compile(loopCode, 2u, 1u); + const auto& loopImages = loopCapture->snapshot.images; + const auto holdsLoopImage = [&](const std::array& words) { + return std::ranges::any_of(loopImages, [&](const DescriptorValue& value) { + return std::equal(words.begin(), words.end(), value.dwords.begin()); + }); + }; + require(loopImages.size() == 2u && holdsLoopImage(first) && holdsLoopImage(second), "descriptor Phi: the loop's chained T# Phis were not split into the two SRT T#s"); + + const auto entryWrites = [&](std::initializer_list words) { + auto code = loopCode; + code.insert(code.begin() + 8, words); + return code; + }; + const std::array, const char*>, 3> invalidEntries{{ + {entryWrites({0xbe8e1f00u}), "descriptor Phi: a T# holding the program counter was accepted"}, + {entryWrites({0xbe8e037eu}), "descriptor Phi: a T# holding EXEC was accepted"}, + {entryWrites({0x7d840080u, 0xbe8e036au}), "descriptor Phi: a T# holding a compare mask was accepted"}, + }}; + for (const auto& [code, message] : invalidEntries) { + auto request = makeRequest(code); + expectFailure([&] { static_cast(GetResourcePlan(request)); }, "GetImageResource dword 0 is not a valid runtime value", message); } + auto unwritten = loopCode; + unwritten[4] = 0xf4080200u; + auto unwrittenRequest = makeRequest(unwritten); + require(GetResourcePlan(unwrittenRequest)->info.images.size() == 3u, "descriptor Phi: a T# whose unwritten registers read as 0 was not planned as its own image"); + + const std::array prologue{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu}; + const std::array epilogue{0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + const auto planImages = [&](const std::vector& body) { + std::vector code(prologue.begin(), prologue.end()); + code.insert(code.end(), body.begin(), body.end()); + code.insert(code.end(), epilogue.begin(), epilogue.end()); + auto request = makeRequest(code); + return GetResourcePlan(request)->info.images.size(); + }; + const auto distinctLoads = [](std::uint32_t loads) { + std::vector body; + for (std::uint32_t load = 0; load < loads; load++) { + body.insert(body.end(), {0xbf068014u, 0xbf850002u, 0xf40c0200u, 0xfa000060u + load * 0x20u}); + } + return body; + }; + require(planImages(distinctLoads(63u)) == 64u, "descriptor Phi: 64 distinct T#s were not given one image each"); + expectFailure([&] { static_cast(planImages(distinctLoads(64u))); }, "GetImageResource dword 0 is not a valid runtime value", "descriptor Phi: more than 64 distinct T#s were accepted"); + + const auto splitReloads = [](std::uint32_t joins) { + std::vector body; + for (std::uint32_t join = 0; join < joins; join++) { + body.insert(body.end(), {0xbf068014u, 0xbf850003u, 0xf40003c0u, join % 2u == 0u ? 0xfa000088u : 0xfa00008cu, 0xbf820006u, 0xf4080200u, 0xfa000060u, 0xf4040300u, 0xfa000070u, 0xf4000380u, 0xfa000078u}); + } + return body; + }; + require(planImages(splitReloads(8u)) == 4u, "descriptor Phi: separate reloads of T# dword 7 and dwords 0-6 were not split into their 4 T#s"); + expectFailure([&] { static_cast(planImages(splitReloads(260u))); }, "GetImageResource dword 0 is not a valid runtime value", "descriptor Phi: a web over more than 512 descriptor tuples was accepted"); } void verifyProgramCounterRelativeData() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 1f407ecc7..992df862a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -741,25 +742,16 @@ private: inst.Parent()->RemoveInstruction(&inst); } - static bool CarriesControlValue(IrValue* value, std::uint32_t depth) { + static bool HoldsProgramCounter(IrValue* value, std::uint32_t depth) { value = value->Resolve(); - if (value->HasImmediate() || value->IsPhi()) { - return false; + if (value->Opcode() == IrOpcode::GetShaderBase) { + return true; } - switch (value->Opcode()) { - case IrOpcode::GetShaderBase: - case IrOpcode::GetExec: - case IrOpcode::GetExecLo: - case IrOpcode::GetExecHi: - case IrOpcode::Ballot: - case IrOpcode::UndefU32: return true; - default: break; - } - if (depth == 0u || (!Detail::IsRuntimeUniformOp(value->Opcode()) && value->Opcode() != IrOpcode::CompositeExtractU32x4)) { + if (value->HasImmediate() || depth == 0u || !Detail::IsRuntimeUniformOp(value->Opcode())) { return false; } for (std::size_t index = 0; index < value->ArgumentCount(); index++) { - if (CarriesControlValue(value->Argument(index), depth - 1u)) { + if (HoldsProgramCounter(value->Argument(index), depth - 1u)) { return true; } } @@ -767,34 +759,34 @@ private: } struct DescriptorWeb { - static constexpr std::size_t maxStates = 4096u; + static constexpr std::size_t maxStates = 512u; static constexpr std::size_t maxArms = 64u; std::vector> arms; std::map, IrValue*> selectors; std::vector> phis; + std::unordered_map resolved; }; IrValue* WebSelector(std::vector tuple, DescriptorWeb& web, IrValue& position) { for (IrValue*& value : tuple) { value = value->Resolve(); - while (value->IsPhi()) { - IrValue* invariant = ResolveInvariantPhi(m_program.Resources(), value); - if (invariant == nullptr || invariant == value) { - break; - } - value = invariant->Resolve(); + if (!value->IsPhi()) { + continue; } + const auto [entry, inserted] = web.resolved.try_emplace(value, value); + if (inserted) { + IrValue* invariant = ResolveInvariantPhi(m_program.Resources(), value); + entry->second = invariant == nullptr ? value : invariant->Resolve(); + } + value = entry->second; } if (const auto known = web.selectors.find(tuple); known != web.selectors.end()) { return known->second; } const auto phi = std::ranges::find_if(tuple, [](const IrValue* value) { return value->IsPhi(); }); if (phi == tuple.end()) { - if (std::ranges::any_of(tuple, [](IrValue* value) { return CarriesControlValue(value, phiSearchDepth); })) { - return &m_builder.Constant(0u); - } for (IrValue* value : tuple) { - if (Rematerialize(value, position, false, phiSearchDepth) == nullptr) { + if (HoldsProgramCounter(value, phiSearchDepth) || Rematerialize(value, position, false, phiSearchDepth) == nullptr) { return nullptr; } } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 859ee88b0..084451064 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -233,7 +233,6 @@ Throughout the project, every function at every stage either **does exactly what - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. -- [Image reads through a web of descriptor Phis](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) are split into one read per distinct SRT descriptor. A path whose T# or S# dwords hold a program-counter, EXEC or ballot value, or an undefined register, is folded onto the first descriptor: no real descriptor holds such a value, so the path is assumed not to reach the read. If a title does read through it, the hardware would fetch from a garbage descriptor, and the read here returns the first descriptor's result instead. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice.