From 8ed0bc395dffcdff884d1d028985d12d23b77eee Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 14:37:56 -0400 Subject: [PATCH 1/3] perf(shader): avoid redundancy traversal map copies --- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 73 ++++++++++++++ core/shader/recompiler/PatchSpirvTools.cmake | 94 ++++++++++++++++--- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b9c39114062..52350df247d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -378,6 +378,77 @@ OpFunctionEnd } } +void testLongControlFlow(bool optimizationEnabled, bool large) { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool full = mode != nullptr && std::string(mode) == "full"; + const std::size_t blocks = large && optimizationEnabled && !full ? 512 : 8; + std::string source = R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %array ArrayStride 4 +OpMemberDecorate %buffer 0 Offset 0 +OpDecorate %buffer Block +OpDecorate %data DescriptorSet 0 +OpDecorate %data Binding 0 +OpDecorate %byteArray ArrayStride 1 +OpMemberDecorate %byteBuffer 0 Offset 0 +OpDecorate %byteBuffer Block +OpDecorate %bytes DescriptorSet 0 +OpDecorate %bytes Binding 1 +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%boolean = OpTypeBool +%byte = OpTypeInt 8 0 +%byteArray = OpTypeRuntimeArray %byte +%byteBuffer = OpTypeStruct %byteArray +%byteBufferPointer = OpTypePointer StorageBuffer %byteBuffer +%bytePointer = OpTypePointer StorageBuffer %byte +%array = OpTypeRuntimeArray %uint +%buffer = OpTypeStruct %array +%bufferPointer = OpTypePointer StorageBuffer %buffer +%valuePointer = OpTypePointer StorageBuffer %uint +%function = OpTypeFunction %void +%data = OpVariable %bufferPointer StorageBuffer +%bytes = OpVariable %byteBufferPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + for (std::size_t i = 0; i < blocks * 4; ++i) source += "%index" + std::to_string(i) + " = OpConstant %uint " + std::to_string(i + 1) + "\n"; + source += "%main = OpFunction %void None %function\n%entry = OpLabel\n%input = OpAccessChain %valuePointer %data %zero %zero\n%seed = OpLoad %uint %input Volatile\n%condition = OpINotEqual %boolean %seed %zero\nOpBranch %block0\n"; + for (std::size_t i = 0; i < blocks; ++i) { + const auto number = std::to_string(i); + const auto previous = i == 0 ? "%seed" : "%next" + std::to_string(i - 1); + source += "%block" + number + " = OpLabel\n%sum" + number + " = OpIAdd %uint " + previous + " %one\n"; + source += "%duplicate" + number + " = OpIAdd %uint " + previous + " %one\n%shared" + number + " = OpIAdd %uint %seed %one\n"; + for (std::size_t j = 0; j < 3; ++j) { + const auto index = std::to_string(i * 4 + j); + source += "%slot" + index + " = OpAccessChain %valuePointer %data %zero %index" + index + "\n"; + source += "OpStore %slot" + index + " %" + (j == 0 ? "sum" : j == 1 ? "duplicate" : "shared") + number + " Volatile\n"; + } + const auto branchIndex = std::to_string(i * 4 + 3); + source += "%branchSlot" + number + " = OpAccessChain %valuePointer %data %zero %index" + branchIndex + "\n"; + source += "OpSelectionMerge %join" + number + " None\nOpBranchConditional %condition %left" + number + " %right" + number + "\n"; + source += "%left" + number + " = OpLabel\n%leftValue" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %leftValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%right" + number + " = OpLabel\n%rightCopy" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %rightCopy" + number + " Volatile\n"; + source += "%rightValue" + number + " = OpIAdd %uint %sum" + number + " %two\nOpStore %branchSlot" + number + " %rightValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%join" + number + " = OpLabel\n%next" + number + " = OpPhi %uint %leftValue" + number + " %left" + number + " %rightValue" + number + " %right" + number + "\n"; + if (i + 1 == blocks) source += "%byteSlot = OpAccessChain %bytePointer %bytes %zero %zero\n%truncated = OpUConvert %byte %next" + number + "\nOpStore %byteSlot %truncated Volatile\nOpReturn\n"; + else source += "OpBranch %block" + std::to_string(i + 1) + "\n"; + } + source += "OpFunctionEnd\n"; + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + const auto stores = opcodeCount(output, spv::OpStore); + check(stores >= blocks * 4 + 1 && stores <= blocks * 6 + 1, "long control-flow optimization discarded observable stores"); + const auto adds = opcodeCount(output, spv::OpIAdd); + if (!optimizationEnabled) check(output == input, "disabled optimization changed the long control-flow module"); + else check(adds >= blocks && adds <= blocks * 4, "control-flow optimization lost arithmetic or retained redundant values"); +} + } int main() { @@ -388,6 +459,8 @@ int main() { testInvalidInputIsRejected(); testSpecializedSelectionExit(); testSpecializedDeadContinue(); + testLongControlFlow(optimizationEnabled, false); + testLongControlFlow(optimizationEnabled, true); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake index 2a6e17df0e8..69cb4bcfc79 100644 --- a/core/shader/recompiler/PatchSpirvTools.cmake +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -3,31 +3,101 @@ set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cp set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) +set(spirvToolsLocalRedundancySource "${spirvToolsSourceDir}/local_redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsLocalRedundancySource}") +file(READ "${spirvToolsLocalRedundancySource}" spirvToolsLocalRedundancy) +set(spirvToolsExpectedElimination [=[ + auto func = [this, &vnTable, &modified, value_to_ids](Instruction* inst) { + if (inst->result_id() == 0) { + return; + } + + uint32_t value = vnTable.GetValueNumber(inst); + + if (value == 0) { + return; + } + + auto candidate = value_to_ids->insert({value, inst->result_id()}); + if (!candidate.second) { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }; + block->ForEachInst(func); +]=]) +string(FIND "${spirvToolsLocalRedundancy}" "${spirvToolsExpectedElimination}" spirvToolsEliminationPosition) +if(spirvToolsEliminationPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools value elimination has changed; review the local traversal fix") +endif() + set(spirvToolsOldTraversal [=[ + struct State { + DominatorTreeNode* node; + std::map value_to_id_map; + }; + std::vector todo; + todo.push_back({bb, std::map()}); + bool modified = false; + for (size_t next_node = 0; next_node < todo.size(); next_node++) { + modified |= EliminateRedundanciesInBB(todo[next_node].node->bb_, vnTable, + &todo[next_node].value_to_id_map); for (DominatorTreeNode* child : todo[next_node].node->children_) { todo.push_back({child, todo[next_node].value_to_id_map}); } + } ]=]) set(spirvToolsNewTraversal [=[ - const auto& children = todo[next_node].node->children_; - for (size_t child_index = 0; child_index < children.size(); ++child_index) { - if (child_index + 1 == children.size()) { - todo.push_back({children[child_index], - std::move(todo[next_node].value_to_id_map)}); - } else { - todo.push_back({children[child_index], todo[next_node].value_to_id_map}); - } + struct State { + DominatorTreeNode* node; + size_t next_child; + size_t added_begin; + bool entered; + }; + std::map value_to_id_map; + std::vector added_values; + std::vector todo; + todo.push_back({bb, 0, 0, false}); + bool modified = false; + while (!todo.empty()) { + auto& state = todo.back(); + if (!state.entered) { + state.entered = true; + state.node->bb_->ForEachInst([&](Instruction* inst) { + if (inst->result_id() == 0) return; + const uint32_t value = vnTable.GetValueNumber(inst); + if (value == 0) return; + auto candidate = value_to_id_map.insert({value, inst->result_id()}); + if (candidate.second) { + added_values.push_back(value); + } else { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }); + } + const auto& children = state.node->children_; + if (state.next_child != children.size()) { + auto* child = children[state.next_child++]; + todo.push_back({child, 0, added_values.size(), false}); + continue; + } + while (added_values.size() != state.added_begin) { + value_to_id_map.erase(added_values.back()); + added_values.pop_back(); } - todo[next_node].value_to_id_map.clear(); + todo.pop_back(); + } ]=]) string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) if(spirvToolsTraversalPosition EQUAL -1) message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") endif() string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") -string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" - "#include \"source/opt/redundancy_elimination.h\"\n\n#include \n" - spirvToolsRedundancy "${spirvToolsRedundancy}") set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9119b5f1141..b6c92fcc94e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,7 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. +- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll From fd192b20cc7169f057179da4820b6b5e694e6674 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 14:45:13 -0400 Subject: [PATCH 2/3] docs(shader): place traversal debt with the component --- docs/dev/TechnicalDebt.md | 1 - docs/dev/debt/shader.md | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b6c92fcc94e..4eddc9623dd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,6 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1aca9b11e98..40467bd2508 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -117,3 +117,7 @@ - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests + +### Build + +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. From b00931842321f466b8ce1e7a48ac952d4f06f1ec Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 19:36:52 -0400 Subject: [PATCH 3/3] docs(shader): place traversal debt in existing Build section --- docs/dev/debt/shader.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 40467bd2508..731cd62f0df 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -4,6 +4,8 @@ - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. + ### Unknown function info - [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. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. 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 field, 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. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. 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`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places @@ -117,7 +119,3 @@ - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - -### Build - -- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed.