From 8b8620dcdaa7372da7c03e4ee4825792fd109af0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 23:28:43 +0000 Subject: [PATCH 1/7] fix(shader): plan a table T# that several image operations share TryMakeTableImage planned a T# read from a bindless image table only when its eight scalar reads had no user other than that one GetImageResource. Descriptor-phi splitting gives each image operation its own copy of the T# over the same reads, so a table entry that is both queried and sampled was refused, and GetHandle then rejected the scalar reads. Accept reads whose every user is a GetImageResource built from exactly those eight reads; each copy is planned as the same table. A shipped title stops on such a shader at start-up: image_get_resinfo and image_sample_lz on one s_buffer_load_dwordx8 T#. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit e015772307b216aa8b8f40072051f2d0a0ff2301) --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 17 +++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 15 +++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index aa2ad94219a..b3fff35c285 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -949,6 +949,23 @@ void verifyBindlessTable() { latch.context.memory = latchRegions; require(!Recompile(latch, *latchCapture)->spirv.empty(), "bindless: a sample in a loop latch did not compile"); + std::vector queryCode(wholeCode.begin(), wholeCode.begin() + 8); + queryCode.insert(queryCode.end(), {0xbea80380u, 0x7e040280u, 0x7e200500u, 0x80102810u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0380f08u, 0x00050402u, 0xf09c0f08u, 0x00450800u, 0x80288128u, 0xbf0a8228u, 0xbf85fff4u, 0xe0700000u, 0x80070800u, 0xe0700004u, 0x80070400u, 0xbf810000u}); + auto query = makeRequest(queryCode); + const auto queryPlan = GetResourcePlan(query); + require(std::ranges::count_if(queryPlan->descriptorSources, [](const DescriptorSource& source) { return source.indirectImage.has_value(); }) == 1, "bindless: a T# both queried and sampled was not planned as one table"); + AgcDriver::ShaderMemory queryMemory({}); + const auto queryCapture = queryMemory.Capture(query); + const auto queryDirect = static_cast(queryPlan->info.images.size()); + const auto queryRoot = tableRoot(*queryCapture, queryDirect); + require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); + query.context.memory = queryMemory.Regions(); + const auto queried = Recompile(query, *queryCapture); + require(!queried->spirv.empty(), "bindless: a T# both queried and sampled did not compile"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(queried->spirv, query.target.vulkanVersion, query.target.spirvVersion)); +#endif + // A table wider than the slots without a material pattern is rejected. fillSrt(100u); AgcDriver::ShaderMemory wideMemory({}); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 70fb382466a..26a0c1a7b5d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -358,9 +358,17 @@ class Tracker { return true; } - static bool usesOnly(const IrValue& value, std::span users) { + static bool usedOnlyAsImage(const IrValue& value, const std::array& reads) { return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const IrValue* user) { - return std::ranges::find(users, user) != users.end(); + if (user->Opcode() != IrOpcode::GetImageResource || user->ArgumentCount() != reads.size()) { + return false; + } + for (std::uint32_t dword = 0; dword < reads.size(); dword++) { + if (user->Argument(dword)->Resolve() != reads[dword]) { + return false; + } + } + return true; }); } @@ -535,9 +543,8 @@ class Tracker { } entryOffset += immediateOffset; - const std::array imageUsers {&handle}; for (const auto* read : heapReads) { - if (!usesOnly(*read, imageUsers)) { + if (!usedOnlyAsImage(*read, heapReads)) { return false; } } From fdbda8c3c25d64d6292686f9bbde83788b3bdf35 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 23:42:15 +0000 Subject: [PATCH 2/7] test(shader): check that a queried and sampled table T# is one image Co-Authored-By: Claude Opus 5.5 (cherry picked from commit cd1757e03659f4a363e1b73a2d8a3711808e084b) --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index b3fff35c285..1850ae66053 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -957,6 +957,7 @@ void verifyBindlessTable() { AgcDriver::ShaderMemory queryMemory({}); const auto queryCapture = queryMemory.Capture(query); const auto queryDirect = static_cast(queryPlan->info.images.size()); + require(queryDirect == 1u, "bindless: the queried and sampled T# became more than one image"); const auto queryRoot = tableRoot(*queryCapture, queryDirect); require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); query.context.memory = queryMemory.Regions(); From ba8d6d5edcd49f800574d1e4a1c8cc95417ff6dd Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:37:49 -0700 Subject: [PATCH 3/7] test(shader): check uniform table images after the descriptor phis #757 and #758 add their image table checks where this check was, after verifyBindlessTable, so the pull requests conflicted. (cherry picked from commit e092d2a3fc3ada8386d33e23d094cf5872f9b2ae) --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1850ae66053..ce673888e00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -991,63 +991,6 @@ void verifyBindlessTable() { fillSrt(4u); } -void verifyUniformTableImage() { - using namespace ShaderRecompiler; - constexpr std::uint32_t Format8888UNorm = 56; - constexpr std::uint32_t Type2D = 9; - struct alignas(256) Texture { std::array bytes{}; }; - static Texture textures[2]; - struct alignas(4096) GuestTables { - std::array, 3> table{}; - std::array output{}; - std::array srt{}; - }; - static GuestTables guest; - const auto texture = [](const Texture& image) { - const auto base = static_cast(reinterpret_cast(image.bytes.data())); - return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; - }; - const auto first = texture(textures[0]); - const auto second = texture(textures[1]); - std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); - std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); - const auto bufferDescriptor = [](const void* base, std::uint32_t records) { - const auto address = static_cast(reinterpret_cast(base)); - return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; - }; - const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); - const auto outputV = bufferDescriptor(guest.output.data(), 16u); - std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); - std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); - const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); - std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; - const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; - RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; - request.context.waveSize = 64; - request.context.userDataBaseRegister = 0; - request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; - request.target.vulkanVersion = 0x00401000u; - request.target.spirvVersion = 0x00010300u; - request.target.subgroupSize = 32; - request.target.supportedCapabilities = capabilities; - request.target.supportedExtensions = extensions; - request.target.bdaAbiVersion = BdaAbi::Version; - request.target.fragmentShaderBarycentricEnabled = false; - request.layout.pushConstantSizeBytes = 128; - const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); - for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { - userData[2] = key; - AgcDriver::ShaderMemory memory({}); - const auto capture = memory.Capture(request); - require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); - } -} - void verifyDescriptorPhis() { using namespace ShaderRecompiler; @@ -1221,6 +1164,63 @@ void verifyDescriptorPhis() { 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 verifyUniformTableImage() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + struct alignas(4096) GuestTables { + std::array, 3> table{}; + std::array output{}; + std::array srt{}; + }; + static GuestTables guest; + const auto texture = [](const Texture& image) { + const auto base = static_cast(reinterpret_cast(image.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto first = texture(textures[0]); + const auto second = texture(textures[1]); + std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); + std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); + const auto bufferDescriptor = [](const void* base, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; + }; + const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); + const auto outputV = bufferDescriptor(guest.output.data(), 16u); + std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); + std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); + for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { + userData[2] = key; + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + } +} + void verifyLaneSelectedDescriptors() { std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf40c0800u, 0xfa000060u, 0xbf8cc07fu, 0x7d880090u, 0xbf068010u, 0xbf850011u}; for (std::uint32_t dword = 0; dword < 8u; dword++) { @@ -3857,8 +3857,8 @@ int main(int argc, char** argv) { verifyPureFlatSlots(); verifyGuardedDescriptorSources(); verifyBindlessTable(); - verifyUniformTableImage(); verifyDescriptorPhis(); + verifyUniformTableImage(); verifyLaneSelectedDescriptors(); verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); From 8ad64412c7a85870ead9599c2165733f0f3aabdf Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:57:12 -0400 Subject: [PATCH 4/7] feat(recompiler): reuse pure IR values recomputed in dominated blocks The front end rebuilds operand flushes, NaN checks and bitcasts for every use of a register, and nothing merged them when SPIRV-Tools is off. A dominator-scoped value numbering pass before SPIR-V emission replaces a pure value with an earlier identical one in the same or a dominating block. Only operations that are functions of their operands are merged; loads, stores, atomics, barriers, phis and cross-lane reads are left alone. A block the entry does not reach throws. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit f0f920a66ecce2ab94af89f1539fa980fd656da7) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 + .../tests/RedundantValueElimination.cpp | 447 ++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../Optimization/RedundantValueEliminator.hpp | 20 + .../src/RedundantValueEliminator.cpp | 276 +++++++++++ core/shader/recompiler/Recompiler.cpp | 3 + 6 files changed, 760 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp create mode 100644 core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp create mode 100644 core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index cf2a3cca2eb..65e26a7d9cf 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -572,6 +572,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_redundant_value_elimination_tests EXCLUDE_FROM_ALL tests/RedundantValueElimination.cpp) +target_link_libraries(agc_redundant_value_elimination_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_redundant_value_elimination_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_redundant_value_elimination_tests) +if(BUILD_TESTING) + set_target_properties(agc_redundant_value_elimination_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_redundant_value_elimination COMMAND agc_redundant_value_elimination_tests) + if(WIN32) + set_tests_properties(agc_redundant_value_elimination PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp) target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp new file mode 100644 index 00000000000..55fb0a52978 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -0,0 +1,447 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/RedundantValueEliminator.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + + Builder() { + block = &newBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& floatConstant(std::uint32_t bits) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::F32); + created.SetImmediateU32(bits); + return created; + } + + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0u) { + auto& created = program.CreateValue(opcode, type, flags); + for (auto* argument : arguments) created.AddArgument(argument); + block->AppendInstruction(&created); + return created; + } + + IrValue& unknown() { + return emit(IrOpcode::LaneId, IrType::U32, {}); + } + + IrValue& flush(IrValue& bits) { + auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)}); + auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)}); + auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)}); + return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits}); + } + + void keep(IrValue& value) { + static_cast(emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + } + + std::uint32_t eliminate() { + return RedundantValueEliminator{}.Eliminate(program).replacedValues; + } +}; + +bool expect(const char* name, bool condition) { + if (!condition) std::fprintf(stderr, "%s\n", name); + return condition; +} + +bool run(const char* name, const std::function& test) { + try { + return expect(name, test()); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + return false; + } +} + +bool AllUsed(const std::vector& values) { + for (const auto* value : values) { + if (!value->HasUses()) return false; + } + return true; +} + +bool PureSpirv(std::uint32_t opcode) { + switch (opcode) { + case spv::OpBitcast: + case spv::OpUConvert: + case spv::OpSConvert: + case spv::OpFConvert: + case spv::OpConvertFToU: + case spv::OpConvertFToS: + case spv::OpConvertUToF: + case spv::OpConvertSToF: + case spv::OpCompositeConstruct: + case spv::OpCompositeExtract: + case spv::OpSNegate: + case spv::OpFNegate: + case spv::OpIAdd: + case spv::OpISub: + case spv::OpIMul: + case spv::OpFAdd: + case spv::OpFSub: + case spv::OpFMul: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpShiftRightArithmetic: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpBitwiseXor: + case spv::OpNot: + case spv::OpBitFieldInsert: + case spv::OpBitFieldUExtract: + case spv::OpBitFieldSExtract: + case spv::OpBitReverse: + case spv::OpBitCount: + case spv::OpSelect: + case spv::OpIEqual: + case spv::OpINotEqual: + case spv::OpULessThan: + case spv::OpULessThanEqual: + case spv::OpUGreaterThan: + case spv::OpUGreaterThanEqual: + case spv::OpSLessThan: + case spv::OpSLessThanEqual: + case spv::OpSGreaterThan: + case spv::OpSGreaterThanEqual: + case spv::OpFOrdEqual: + case spv::OpFOrdNotEqual: + case spv::OpFOrdLessThan: + case spv::OpFOrdGreaterThan: + case spv::OpFOrdLessThanEqual: + case spv::OpFOrdGreaterThanEqual: + case spv::OpFUnordEqual: + case spv::OpFUnordNotEqual: + case spv::OpFUnordLessThan: + case spv::OpFUnordGreaterThan: + case spv::OpFUnordLessThanEqual: + case spv::OpFUnordGreaterThanEqual: + case spv::OpIsNan: + case spv::OpLogicalAnd: + case spv::OpLogicalOr: + case spv::OpLogicalNot: + case spv::OpLogicalEqual: + case spv::OpLogicalNotEqual: + return true; + default: + return false; + } +} + +std::size_t RepeatedInBlock(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::size_t repeated = 0; + std::set> seen; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpLabel) seen.clear(); + if (PureSpirv(opcode) && count >= 3) { + std::vector key{opcode, words[offset + 1]}; + key.insert(key.end(), words.begin() + static_cast(offset + 3), words.begin() + static_cast(offset + count)); + if (!seen.insert(std::move(key)).second) ++repeated; + } + offset += count; + } + return repeated; +} + +std::vector Recompiled(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array capabilities{1u, 61u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +} + +int main() { + bool passed = true; + + passed &= run("a value recomputed from the same operands in one block is replaced by the first", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.flush(source); + auto& second = b.flush(source); + b.keep(first); + b.keep(second); + return b.eliminate() == 4u && !second.HasUses() && first.UseCount() == 2u; + }); + + passed &= run("values that differ in an immediate, an operand or the opcode stay", [] { + Builder b; + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& masked = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + auto& otherMask = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x80000000u)}); + auto& otherSource = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&other, &b.constant(0x7f800000u)}); + auto& otherOpcode = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + b.keep(masked); + b.keep(otherMask); + b.keep(otherSource); + b.keep(otherOpcode); + return b.eliminate() == 0u && AllUsed({&masked, &otherMask, &otherSource, &otherOpcode}); + }); + + passed &= run("values that differ only in flags, the result type or the type of an immediate stay", [] { + Builder b; + auto& source = b.unknown(); + auto& plain = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}); + auto& flagged = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}, 1u); + auto& otherType = b.emit(IrOpcode::BitwiseAnd32, IrType::F32, {&source, &b.constant(0x3f800000u)}); + auto& floatImmediate = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.floatConstant(0x3f800000u)}); + b.keep(plain); + b.keep(flagged); + b.keep(otherType); + b.keep(floatImmediate); + return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); + }); + + passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] { + Builder b; + auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& plusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x00000000u)}); + auto& minusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x80000000u)}); + auto& quiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& sameQuiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& otherPayload = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00001u)}); + for (auto* value : {&plusZero, &minusZero, &quiet, &sameQuiet, &otherPayload}) b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {value})); + return b.eliminate() == 2u && !sameQuiet.HasUses() && AllUsed({&quiet, &plusZero, &minusZero, &otherPayload}); + }); + + passed &= run("a value recomputed in a dominated block is replaced, one in a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + b.keep(first); + b.block = &then; + auto& dominated = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + auto& thenOnly = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(dominated); + b.keep(thenOnly); + b.block = &otherwise; + auto& sibling = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(sibling); + b.block = &merge; + auto& merged = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(merged); + return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); + }); + + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& source = b.unknown(); + auto& other = b.unknown(); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + b.keep(inHeader); + b.block = &body; + auto& inBody = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + auto& bodyOnly = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(inBody); + b.keep(bodyOnly); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(afterLoop); + return b.eliminate() == 1u && !inBody.HasUses() && inHeader.UseCount() == 2u && AllUsed({&bodyOnly, &afterLoop}); + }); + + passed &= run("a branch condition that is replaced names the value that stays", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + auto& second = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&first, &source, &b.constant(1u)})); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&second, &b.constant(1u), &source})); + BlockInfo info; + info.condition = &second; + b.program.Metadata().blockInfo.push_back(info); + return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first; + }); + + passed &= run("lane reads are not merged", [] { + Builder b; + auto& first = b.unknown(); + auto& second = b.unknown(); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && second.HasUses(); + }); + + passed &= run("cross-lane reads with the same operands are not merged", [] { + Builder b; + auto& source = b.unknown(); + std::vector values; + for (int copy = 0; copy < 2; ++copy) { + values.push_back(&b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::ReadLane, IrType::U32, {&source, &b.constant(3u)})); + values.push_back(&b.emit(IrOpcode::DppMoveU32, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::SwizzleU32, IrType::U32, {&source, &b.constant(0x1fu), &b.truth()})); + } + for (auto* value : values) b.keep(*value); + return b.eliminate() == 0u && AllUsed(values); + }); + + passed &= run("loads of one address are not merged, with no store, a store or a barrier between them", [] { + Builder b; + auto& address = b.unknown(); + std::vector loads; + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::StoreShared, IrType::Void, {&address, &b.constant(7u)})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::Barrier, IrType::Void, {})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + for (auto* load : loads) b.keep(*load); + return b.eliminate() == 0u && AllUsed(loads); + }); + + passed &= run("atomics with the same operands are not merged", [] { + Builder b; + auto& address = b.unknown(); + auto& first = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + auto& second = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("phis with the same incoming values are not merged", [] { + Builder b; + IrBlock& left = b.newBlock(); + IrBlock& right = b.newBlock(); + IrBlock& merge = b.newBlock(); + b.block->AddBranch(&left); + b.block->AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& one = b.unknown(); + auto& two = b.unknown(); + b.block = &merge; + auto& first = b.emit(IrOpcode::Phi, IrType::U32, {}); + auto& second = b.emit(IrOpcode::Phi, IrType::U32, {}); + for (auto* phi : {&first, &second}) { + phi->AddPhiOperand(&left, &one); + phi->AddPhiOperand(&right, &two); + } + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("float operations with the same operands in the same order are merged", [] { + Builder b; + auto& lhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& rhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& first = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& second = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& swapped = b.emit(IrOpcode::FPMul32, IrType::F32, {&rhs, &lhs}); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&first})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&second})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&swapped})); + return b.eliminate() == 2u && !second.HasUses() && swapped.HasUses(); + }); + + passed &= run("a block that the entry does not reach throws", [] { + Builder b; + static_cast(b.newBlock()); + try { + static_cast(b.eliminate()); + } catch (const std::runtime_error&) { + return true; + } + return false; + }); + + passed &= run("a wave64 shader that reads one register in several float operations computes nothing twice in a block", [] { + const std::array code{ + 0x10020100u, + 0x10040300u, + 0x10060500u, + 0x06020503u, + 0xe0700000u, 0x80000100u, + 0xbf810000u, + }; + const auto module = Recompiled(code); + const auto repeated = RepeatedInBlock(module); + if (repeated != 0u) std::fprintf(stderr, " %zu pure instructions repeat an earlier one in their block of the %zu-word module\n", repeated, module.size()); + return repeated == 0u; + }); + + return passed ? 0 : 1; +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index fd344ff5cb9..a9059679609 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -109,6 +109,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ShaderInfoCollector.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SharedMemoryBarrierInserter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DenormalFlushEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/RedundantValueEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtAddressArithmetic.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDiagnostics.cpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp new file mode 100644 index 00000000000..910c3499db3 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct RedundantValueEliminationStats { + std::uint32_t replacedValues = 0; +}; + +class RedundantValueEliminator { +public: + [[nodiscard]] RedundantValueEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp new file mode 100644 index 00000000000..a580617abdb --- /dev/null +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -0,0 +1,276 @@ +#include "Optimization/RedundantValueEliminator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Recomputable(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU16F16: + case IrOpcode::BitCastF16U16: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertU16U32: + case IrOpcode::ConvertU32U16: + case IrOpcode::ConvertU8U32: + case IrOpcode::ConvertU32U8: + case IrOpcode::ConvertF32F16: + case IrOpcode::ConvertF16F32: + case IrOpcode::ConvertS32F32: + case IrOpcode::ConvertU32F32: + case IrOpcode::ConvertF32S32: + case IrOpcode::ConvertF32U32: + case IrOpcode::CompositeConstructU64: + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::CompositeConstructU32x3: + case IrOpcode::CompositeConstructF32x2: + case IrOpcode::CompositeConstructU32x4: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::CompositeExtractU32x3: + case IrOpcode::CompositeExtractU32x4: + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::FPSaturate32: + case IrOpcode::BitFieldInsert: + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: + case IrOpcode::SelectU32: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + case IrOpcode::IMul32: + case IrOpcode::IAdd64: + case IrOpcode::ISub64: + case IrOpcode::IMul64: + case IrOpcode::SMulHi: + case IrOpcode::UMulHi: + case IrOpcode::IAbs32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::BitwiseNot32: + case IrOpcode::BitReverse32: + case IrOpcode::BitCount32: + case IrOpcode::BitCount64: + case IrOpcode::FindUMsb32: + case IrOpcode::FindUMsb64: + case IrOpcode::FindILsb32: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::SMax32: + case IrOpcode::UMax32: + case IrOpcode::SLessThan32: + case IrOpcode::SLessThan64: + case IrOpcode::ULessThan32: + case IrOpcode::ULessThan64: + case IrOpcode::IEqual32: + case IrOpcode::IEqual64: + case IrOpcode::SLessThanEqual32: + case IrOpcode::ULessThanEqual32: + case IrOpcode::SGreaterThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::UGreaterThan64: + case IrOpcode::INotEqual32: + case IrOpcode::INotEqual64: + case IrOpcode::SGreaterThanEqual32: + case IrOpcode::UGreaterThanEqual32: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalXor: + case IrOpcode::LogicalNot: + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: + case IrOpcode::FPOrdLessThan32: + case IrOpcode::FPUnordLessThan32: + case IrOpcode::FPOrdGreaterThan32: + case IrOpcode::FPUnordGreaterThan32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: + case IrOpcode::FPIsNan32: + case IrOpcode::FPAdd32: + case IrOpcode::FPSub32: + case IrOpcode::FPFma32: + case IrOpcode::FPMad32: + case IrOpcode::FPMul32: + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPFract32: + case IrOpcode::FPRoundEven32: + return true; + default: + return false; + } +} + +struct Operand { + const IrValue* value; + IrType type; + std::uint64_t immediate; + + bool operator==(const Operand& other) const = default; +}; + +struct ValueKey { + IrOpcode opcode; + IrType type; + std::uint64_t flags; + std::vector operands; + + bool operator==(const ValueKey& other) const = default; +}; + +struct ValueKeyHash { + std::size_t operator()(const ValueKey& key) const { + std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; + for (const auto& operand : key.operands) { + hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); + } + return hash; + } +}; + +Operand OperandOf(const IrValue* argument) { + const IrValue* resolved = argument->Resolve(); + if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; + return {resolved, resolved->Type(), 0u}; +} + +std::vector ReversePostOrder(IrBlock& entry) { + std::vector order; + std::unordered_map visited; + std::vector> stack{{&entry, 0u}}; + visited[&entry] = true; + while (!stack.empty()) { + auto& [block, next] = stack.back(); + if (next < block->Successors().size()) { + IrBlock* successor = block->Successors()[next++]; + if (visited.try_emplace(successor, true).second) stack.emplace_back(successor, 0u); + continue; + } + order.push_back(block); + stack.pop_back(); + } + return {order.rbegin(), order.rend()}; +} + +std::vector> DominatorChildren(const std::vector& order, std::unordered_map& index) { + for (std::size_t position = 0; position < order.size(); ++position) index[order[position]] = position; + constexpr std::size_t none = static_cast(-1); + std::vector dominator(order.size(), none); + dominator[0] = 0; + const auto intersect = [&](std::size_t left, std::size_t right) { + while (left != right) { + while (left > right) left = dominator[left]; + while (right > left) right = dominator[right]; + } + return left; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 1; position < order.size(); ++position) { + std::size_t candidate = none; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end() || dominator[found->second] == none) continue; + candidate = candidate == none ? found->second : intersect(candidate, found->second); + } + if (candidate != dominator[position]) { + dominator[position] = candidate; + changed = true; + } + } + } + std::vector> children(order.size()); + for (std::size_t position = 1; position < order.size(); ++position) children[dominator[position]].push_back(order[position]); + return children; +} + +} + +RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { + RedundantValueEliminationStats stats; + std::unordered_map available; + std::vector added; + std::unordered_map replacements; + ValueKey key; + const auto visit = [&](IrBlock& block) { + for (auto* inst : block.Instructions()) { + if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; + key.opcode = inst->Opcode(); + key.type = inst->Type(); + key.flags = inst->Flags(); + key.operands.clear(); + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) key.operands.push_back(OperandOf(inst->Argument(index))); + const auto [found, inserted] = available.try_emplace(key, inst); + if (inserted) { + added.push_back(&found->first); + continue; + } + inst->ReplaceAllUsesWith(found->second); + replacements.emplace(inst, found->second); + ++stats.replacedValues; + } + }; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::vector> stack{{0u, 0u}}; + std::vector scopes{added.size()}; + visit(*order[0]); + while (!stack.empty()) { + auto& [position, next] = stack.back(); + if (next < children[position].size()) { + const auto child = index.at(children[position][next++]); + scopes.push_back(added.size()); + visit(*order[child]); + stack.emplace_back(child, 0u); + continue; + } + while (added.size() > scopes.back()) { + available.erase(available.find(*added.back())); + added.pop_back(); + } + scopes.pop_back(); + stack.pop_back(); + } + const auto replaced = [&](IrValue*& value) { + if (value == nullptr) return; + if (const auto found = replacements.find(value->Resolve()); found != replacements.end()) value = found->second; + }; + for (auto& info : program.Metadata().blockInfo) { + replaced(info.condition); + replaced(info.indirectTarget); + } + for (auto*& read : program.Metadata().dynamicReads) replaced(read); + return stats; +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 3fac66aab4e..dcbaa36945e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -32,6 +32,7 @@ #include "Optimization/include/Optimization/HostInterpolationChecker.hpp" #include "Optimization/include/Optimization/MaskedSelectEliminator.hpp" #include "Optimization/include/Optimization/ReadLaneEliminator.hpp" +#include "Optimization/include/Optimization/RedundantValueEliminator.hpp" #include "Optimization/include/Optimization/RequestMemoryView.hpp" #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" @@ -533,6 +534,8 @@ void prepareInterface(const RecompileRequest& request, IrProgram& program) { resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets); deadCodeEliminator.RemoveIdentities(program); + constexpr RedundantValueEliminator redundantValueEliminator; + static_cast(redundantValueEliminator.Eliminate(program)); deadCodeEliminator.Eliminate(program); constexpr ShaderInfoCollector shaderInfoCollector; From 0b6dd0b328f06b5d90f91996b41891e4d2f3eef0 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 22:37:42 +1300 Subject: [PATCH 5/7] fix(shader): size per-invocation LDS by the append counter's offset FunctionLdsDwords bounds every LDS access by its address argument and gives up on opcodes without a dword count. DataAppend and DataConsume have neither: their first argument is M0, which only places GDS counters, and the emitter addresses the LDS counter by the instruction offset alone. Any pixel or vertex-path program with ds_append or ds_consume therefore declared the full 8192-dword Function array per invocation. LLVM 20 llvmpipe needs about 6 s to compile that array in the new pixel LDS read-modify-write test, which ran past the 5 s graphics fence on CI. The counter now takes the dword at its offset. (cherry picked from commit 13cc3a9ea685a23b77b9bb76a5f4248af42d6fc3) --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 7 +++++++ core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index ce673888e00..411c8ebf6d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3465,6 +3465,13 @@ void verifyFunctionLdsBound() { }); require(unbounded == FunctionLdsDwordLimit && unboundedRequirements.functionLdsDwords == FunctionLdsDwordLimit, "function LDS: an address without a bound must keep the full array"); + const auto [counter, counterRequirements] = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { + auto& m0 = emit(IrOpcode::GetUserData, IrType::U32, {&constant(0u)}); + emit(IrOpcode::DataAppend, IrType::U32, {&m0, &active}, 2u); + emit(IrOpcode::DataConsume, IrType::U32, {&m0, &active}, 1u); + }); + require(counter == 192u && counterRequirements.functionLdsDwords == 192u, "function LDS: an LDS append or consume must size the array by its offset, not by M0"); + const auto wrapping = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { emit(IrOpcode::LoadSharedU32, IrType::U32, {&constant(0x20u), &active}, 3u); }).first; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 3db47cb96cc..a749c41ae4f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -224,11 +224,17 @@ std::uint32_t FunctionLdsDwords(const IrProgram& program) { std::uint64_t needed = 0; for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { - if (SharedAccessOf(inst->Opcode()) == SharedAccess::None) continue; + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None) continue; const auto index = inst->Flags().index; if (index >= program.Resources().memoryInfo.size()) return FunctionLdsDwordLimit; const auto& memory = program.Resources().memoryInfo[index]; if (memory.kind != ResourceKind::Lds) continue; + if (access == SharedAccess::Append || access == SharedAccess::Consume) { + needed = std::max(needed, (std::uint64_t{memory.offset} >> 2u) + 1u); + if (needed >= FunctionLdsDwordLimit) return FunctionLdsDwordLimit; + continue; + } const auto dwords = SharedAccessDwords(inst->Opcode()); if (dwords == 0u || inst->ArgumentCount() == 0) return FunctionLdsDwordLimit; const auto address = bound.Of(inst->Argument(0)); From 5ef2999fa448382b2d0cdaa6f8f330441b65c0b3 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:20:40 +0200 Subject: [PATCH 6/7] fix(shader): record BDA faults in hull programs instead of refusing them Every hull program with a BDA access was refused with "BDA fault termination requires a barrier-safe tessellation-control execution protocol". The refusal predates BdaInvocationsMayStop: a program with s_barrier no longer stops a faulting invocation; the fault is recorded, the read returns zero and every invocation reaches the barrier. In a hull, s_barrier is the only control barrier the backend emits after the guest code starts, so that rule already covers it, and a hull without one can stop an invocation without stranding a barrier. The tessellation test draws hulls that scale their levels by a BDA read, with and without s_barrier, and requires an unmapped read to report a tessellation-control fault (not on llvmpipe, which runs hull loads that the unmapped-address check skips). The contract test now accepts a hull with s_barrier. (cherry picked from commit c1e4a0ba8b8998fa6ed12d3536b23c570b70bde4) --- core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp | 2 +- core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 3af9d345d98..cf54e315dd7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -41,7 +41,7 @@ void RunBdaContractTests() { ValidateBdaTarget(program, target); AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); program.Resources().stage = IrShaderStage::TessellationControl; - reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + ValidateBdaTarget(program, target); program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index eec86297ab6..067d57d1840 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -406,7 +406,6 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe for (const auto extension : {"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}) { if (std::find(target.supportedExtensions.begin(), target.supportedExtensions.end(), extension) == target.supportedExtensions.end()) throw std::runtime_error(std::string("BDA requires unsupported extension ") + extension); } - if (program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe tessellation-control execution protocol"); } bool BdaInvocationsMayStop(const IrProgram& program) { From c5683a9bd84ae5b06448619e676ca2f3ef7b052c Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Wed, 14 Oct 2026 10:32:28 +0400 Subject: [PATCH 7/7] fix(shader): normalize reserved buffer bits before scalar binding (cherry picked from commit bfab566fef533f62b76b278fac831a1eb5c877df) --- .../recompiler/Optimization/src/ResourceMaterializer.cpp | 5 ++++- docs/dev/TechnicalDebt.md | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index cec2fb27356..93087215330 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -616,7 +616,10 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, c const auto& buffer = plan.info.buffers[i]; const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); - if (scalarOnly) snapshot.buffers.at(i).dwords[3] = 0u; + if (scalarOnly) { + snapshot.buffers.at(i).dwords[1] &= ~0x40000000u; + snapshot.buffers.at(i).dwords[3] = 0u; + } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9f84e8dee25..484efbbee84 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -643,6 +643,7 @@ Throughout the project, every function at every stage either **does exactly what - [VALU lane masks written to an SGPR or VCC](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (shader recompiler) - the carry and borrow masks of `v_add_co_u32`, `v_sub_co_u32`, `v_subrev_co_u32` and their `_ci` forms, the carry of `v_mad_u64_u32` and `v_mad_i64_i32`, and the flag of `v_div_scale_f32` and `v_div_scale_f64` have 0 in the bits of lanes that EXEC disables, as `v_cmp` results do. The [RDNA 4 ISA guide](https://gpuopen.com/download/rdna4-instruction-set-architecture.pdf) states this for carry-outs (section 7.2.3, page 72). The RDNA 2 guide does not say it in words, and no guide says it for `v_mad_*64` or `v_div_scale`. Not measured on gfx1013 or another RDNA 2 GPU, in particular whether a disabled lane's bit that was 1 before the instruction is cleared or kept; [ValuMaskInactiveLanes.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/ValuMaskInactiveLanes.cpp) expects it cleared - [32_AR color exports](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) - [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. +- [Scalar buffer descriptors](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero and bit 30 of the second word (reserved) is cleared, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation.