From dbbe282c14ba2ddec9fa422afd7dbe461c7d05a2 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:57:28 -0400 Subject: [PATCH] fix(agc): require demote for masked subgroup reads ReadFirstLane, Ballot and PermuteU32 can consume uncovered helper lanes after a fragment explicitly enables EXEC. Classify those operations as neighbour reads so a partial quad mask refuses their kill fallback when demote is unavailable. ReadFirstLane also stays protected when a constant whole-quad instruction folds away. The CPU regression covers explicit EXEC, constant WQM, a comparison ballot and ds_permute. Unmasked programs still compile, supported masked programs retain demote and their subgroup operation, and constant colour keeps the kill fallback. The recovered source fails the readfirstlane and ballot checks; the corrected source passes both the focused check and shader-memory suite. Co-Authored-By: Codex --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 59 +++++++++++++++++++ .../SpirvBackend/src/SpirvAnalysis.cpp | 3 + 3 files changed, 67 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a1a9a41d74..25e74dc606 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1446,6 +1446,11 @@ if(BUILD_TESTING) add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) + add_test(NAME agc_shader_quad_mask_no_demote COMMAND agc_shader_memory_tests --quad-mask-no-demote) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index df0cf71b8e..462bbcd8ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4920,6 +4920,59 @@ void verifyGuardedNullPointers() { #endif } +void verifyQuadMaskNeighbourReads() { + using namespace ShaderRecompiler; + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + constexpr std::array demoteCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityDemoteToHelperInvocation}; + constexpr std::array demoteExtensions{"SPV_EXT_demote_to_helper_invocation"}; + constexpr std::array constant{0x7e0202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array explicitExec{0xbefe04c1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array constantWqm{0xbefe0ac1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array permute{0xbefe04c1u, 0x7e040284u, 0xdac80000u, 0x01000002u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array comparisonBallot{0xbe8003ffu, 0x3f000000u, 0xbefe04c1u, 0x7d840000u, 0xbe80046au, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + const auto compile = [&](std::span code, bool masked, bool demote) { + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x58000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->inputAddr = PixelInputBit(PixelInput::PositionX); + request.context.pixel->posX = true; + request.context.pixel->quadPixelMask = masked ? 0x2u : 0xfu; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.pixel->targetExportMapping.fill(0xe4u); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = demote ? std::span(demoteCapabilities) : std::span(capabilities); + if (demote) request.target.supportedExtensions = demoteExtensions; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + return Recompile(request); + }; + const auto declares = [](const RecompileResult& result, spv::Op opcode) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "quad mask: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == static_cast(opcode)) return true; + cursor += count; + } + return false; + }; + require(declares(compile(constant, true, false), spv::OpKill), "a quad mask without neighbour reads did not retain its kill fallback"); + for (const auto code : {std::span(explicitExec), std::span(constantWqm), std::span(permute)}) { + const auto unmasked = compile(code, false, false); + require(declares(unmasked, spv::OpGroupNonUniformShuffle), "the neighbour-read test did not retain its cross-lane shuffle"); + expectFailure([&] { static_cast(compile(code, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a cross-lane read with EXEC enabled for those lanes"); + const auto masked = compile(code, true, true); + require(declares(masked, spv::OpDemoteToHelperInvocation) && declares(masked, spv::OpGroupNonUniformShuffle), "a quad mask with demote did not preserve its cross-lane shuffle path"); + } + require(declares(compile(comparisonBallot, false, false), spv::OpGroupNonUniformBallot), "the comparison test did not retain its subgroup ballot"); + expectFailure([&] { static_cast(compile(comparisonBallot, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a ballot with EXEC enabled for those lanes"); + const auto maskedBallot = compile(comparisonBallot, true, true); + require(declares(maskedBallot, spv::OpDemoteToHelperInvocation) && declares(maskedBallot, spv::OpGroupNonUniformBallot), "a quad mask with demote did not preserve its ballot path"); +} + void verifyCubeDepthCompareMode() { using namespace ShaderRecompiler; ImageResource image{}; @@ -4975,6 +5028,11 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { + verifyQuadMaskNeighbourReads(); + std::cout << "Quad masks preserve neighbour-lane helpers or refuse without demote\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; @@ -5058,6 +5116,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifySrgbDepthCompareModes(); + verifyQuadMaskNeighbourReads(); verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 59f6348e2f..60295387c7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -17,8 +17,11 @@ bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { case IrOpcode::DppUpdateU32: case IrOpcode::SwizzleU32: case IrOpcode::BpermuteU32: + case IrOpcode::PermuteU32: case IrOpcode::Permlane16U32: case IrOpcode::ReadLane: + case IrOpcode::ReadFirstLane: + case IrOpcode::Ballot: case IrOpcode::WqmU64: return true; case IrOpcode::ImageSampleImplicitLod: