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 <noreply@openai.com>
This commit is contained in:
fdfawef324andCodex authored and Dean Galvin committed 2026-10-15 13:00:34 +13:00
1 parent 1ffc5f672b
commit dbbe282c14
3 files changed
+67

No files matched your search

@@ -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:$<TARGET_FILE_DIR:libc>")
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}")
@@ -4920,6 +4920,59 @@ void verifyGuardedNullPointers() {
#endif
}
void verifyQuadMaskNeighbourReads() {
using namespace ShaderRecompiler;
constexpr std::array<std::uint32_t, 6> capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic};
constexpr std::array<std::uint32_t, 7> demoteCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityDemoteToHelperInvocation};
constexpr std::array<std::string_view, 1> demoteExtensions{"SPV_EXT_demote_to_helper_invocation"};
constexpr std::array<std::uint32_t, 4> constant{0x7e0202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u};
constexpr std::array<std::uint32_t, 6> explicitExec{0xbefe04c1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u};
constexpr std::array<std::uint32_t, 6> constantWqm{0xbefe0ac1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u};
constexpr std::array<std::uint32_t, 7> permute{0xbefe04c1u, 0x7e040284u, 0xdac80000u, 0x01000002u, 0xf800180fu, 0x01010101u, 0xbf810000u};
constexpr std::array<std::uint32_t, 9> comparisonBallot{0xbe8003ffu, 0x3f000000u, 0xbefe04c1u, 0x7d840000u, 0xbe80046au, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u};
const auto compile = [&](std::span<const std::uint32_t> 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<const std::uint32_t>(demoteCapabilities) : std::span<const std::uint32_t>(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<std::uint32_t>(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<const std::uint32_t>(explicitExec), std::span<const std::uint32_t>(constantWqm), std::span<const std::uint32_t>(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<void>(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<void>(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<std::uint32_t> minimalSpirv{
@@ -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: