Skip to content

AGC: on an AMD RDNA 2 iGPU (Windows) every pixel of a killed BDA fault still lands one read-modify-write (agc_driver_pixel_interlock) #2009

Closed

Description

@tezzgod74

agc_driver_pixel_interlock fails in its BDA fault draw on an AMD RDNA 2 iGPU, every run (5/5):

AGC graphics: pixel interlock wave64 BDA fault: pixel 0 counted 1 of 0 overlapping read-modify-writes

With the checks in DrawFault turned into a log of every mismatch:

Run Mismatches Pixels Counted Expected Stored into read range
wave64 BDA fault 256 every pixel with x % 8 == 0 (all 8 columns × 32 rows) 1 0 0
wave32 BDA fault 256 same 1 0 0

The draw does report the unmapped read (BDA access failed, reason=1), and every other pixel counts 64. The three normal passes and the pass after each fault draw pass in both wave sizes. On each faulting pixel, exactly one of the 64 overlapping fragments' increments lands.

The RX 9060 XT (RDNA 4) in the same machine passes the whole test.

ANYPS5_GPU Device Driver Vulkan Result
Radeon(TM) Graphics Ryzen 9 7950X iGPU (RDNA 2, 0x164e) 32.0.21045.5002 1.4.315 fails
RX 9060 AMD Radeon RX 9060 XT (RDNA 4, 0x7590) 32.0.31041.1004 1.4.349 passes

The test arrived with #1130 (merged in 3069680), and CI has no AMD device, so this may be its first run on RDNA 2. Not yet known whether the leaked increment comes from the recompiled shader or from the driver's handling of the killed invocation.

Environment: main at 79d23eb, Release, MinGW-w64 GCC 15.2.0 (WinLibs r7), Windows 11 Pro 10.0.26300.

Reproduce

$env:ANYPS5_GPU = "Radeon(TM) Graphics"
ctest --test-dir build -R '^agc_driver_pixel_interlock$' --output-on-failure

Activity

  1. LeandroLP23 commented on Oct 9, 2026

    @LeandroLP23
    Contributor

    Passes on RDNA 3.5: AMD Radeon 860M (0x1114), driver 32.0.13058.8001, Windows 11 10.0.26300, main f145139, 5/5 runs (10 draws of 64 overlapping triangles over 2048 pixels, 2 of them with a killed BDA fault in every 8th column). So far only the RDNA 2 device fails; its driver branch (32.0.21045) also differs from both passing devices.

  2. AcelezXYZ commented on Oct 9, 2026

    @AcelezXYZ

    Thanks, that is a useful data point. With it the picture is:

    Device Arch Driver Result
    Ryzen 9 7950X iGPU (0x164e) RDNA 2 32.0.21045.5002 fails 5/5
    Radeon 860M (0x1114) RDNA 3.5 32.0.13058.8001 passes 5/5
    RX 9060 XT (0x7590) RDNA 4 32.0.31041.1004 passes

    Two things stand out:

    • The failing device is the only RDNA 2 one and the only one on the 21045 driver branch, so architecture and driver are still confounded.
    • The driver build numbers do not line up with the architecture order (13058 passes, 21045 fails, 31041 passes), so I would not read this as "older driver = broken". I do not know how to order these build numbers by release date, so please check the Adrenalin version each one belongs to rather than relying on my reading of them.

    To separate the two, the most informative runs would be:

    1. The same RDNA 2 iGPU with a different driver branch (for example the one that ships with the 860M or the 9060 XT machine's package), if AMD's package supports the 7950X iGPU. The 7950X iGPU and the 9060 XT are in one machine, so installing one package for both would also remove the "two separate driver installs" variable.
    2. The Layers = 1 fault draw from my previous comment on the failing iGPU. If a faulting pixel still counts 1 of 0 with no overlap, the increment does not depend on the interlock ordering.

    If the RDNA 2 failure follows the driver or the architecture and cannot be fixed on the user side, one thing could be tried in the generator without touching other hardware: in StopBdaInvocationIf, set the pixel valid mask to 0 and branch to the common return so that OpEndInvocationInterlockEXT is emitted once at the end and OpKill happens after the critical section (that is what EmitKillIfPixelValidMaskInactive already does for the normal return). It is valid SPIR-V on every device, so it should not regress the two passing ones, but whether it changes behavior on RDNA 2 is unknown until someone runs it there.

  3. danielradosa commented on Oct 9, 2026

    @danielradosa
    Contributor

    Reproduced on a second machine with the same iGPU and driver, plus the Layers = 1 run.

    Ryzen 5 7600 iGPU (RDNA 2, 0x164e), AMD driver 32.0.21045.5002, Vulkan 1.4.315, Windows 11 Pro 10.0.26300; RTX 4060 (0x2882, driver 616.92) in the same machine. MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, BUILD_TESTING=ON, ANYPS5_ENABLE_SPIRV_TOOLS=ON, ANYPS5_REQUIRE_VULKAN=1. Tree: main 0fdda7b plus #1748, which only touches libkernel.

    ANYPS5_GPU Draw Result
    Radeon(TM) Graphics as on main (64 layers) fails 5/5: pixel interlock wave64 BDA fault: pixel 0 counted 1 of 0 overlapping read-modify-writes
    Radeon(TM) Graphics Layers = 1 fails 3/3, same line: the faulting pixel still counts 1 of 0
    RTX 4060 as on main passes 5/5

    With one triangle per pixel there is no overlap and nothing to order, and the faulting invocation's read-modify-write still lands once. So the increment does not come from the interlock ordering; the counter load, add and store after the stopped BDA read run once for the invocation that StopBdaInvocationIf kills. On this driver the architecture and the driver are still the same variable as before; this only rules out that the first machine was a one-off.

    I will try the StopBdaInvocationIf change you described on the same device and post the result.

    AI-assisted: yes (Claude Code).

  4. danielradosa commented on Oct 9, 2026

    @danielradosa
    Contributor

    Follow-up with the generator change, same machine (Radeon iGPU 0x164e, driver 32.0.21045.5002; RTX 4060 as control), same tree (main 0fdda7b + #1748).

    What the fault draw does on the Radeon, with temporary prints after the existing reason=1 check: every killed column pixel ends with guest = 1 and Counters = 0 in both wave64 and wave32, and every live pixel counts 64. So the killed lane's buffer_store is dropped, but its later global_atomic_umax (an OpAtomicUMax through the BDA pointer) still lands with 0 + 1 + 0 = 1, which is why it is 1 of 0 even with one layer. The RTX leaves those pixels at 0. That fits the lane running on past the OpKill that StopBdaInvocationIf puts in a selection whose merge is the rest of the program; the logs show the effect, not how the driver lowers OpKill.

    Two changes each pass the test 5/5 on the Radeon and 3/3 on the RTX, with the fault still reported as reason=1 and the read-range check passing:

    • (a) Your suggestion. Structured SPIR-V cannot branch from the nested selection to a shared return, so the stop branch repeats the return tail: store pixel_valid_mask_active = 0, OpEndInvocationInterlockEXT, the valid-mask kill, OpReturn. After the optimizer this is store 0; End; if (!helper) OpKill; OpReturn, so a lane that survives the kill only reaches OpReturn.
    • (b) OpTerminateInvocation in place of OpKill. The target is SPIR-V 1.3, so this also needs VK_KHR_shader_terminate_invocation enabled (both devices report it), SPV_KHR_terminate_invocation declared in the module and allowed in ShaderValidation.cpp.

    agc_driver_graphics, agc_driver_bda_device and agc_driver_pixel_{helper_lanes,interlock,lds,lds_lane_slots,valid_mask,wave_reduction} pass on both GPUs with either change. Only the interlock test and the ordered-BDA check in agc_driver_graphics reach the changed code, and that check's target does not list the terminate extension, so it does not exercise (b).

    Limits: (a) covers only ordered pixel shaders that have the valid-mask variable, and it relies on the kill staying in a selection whose merge is OpReturn; if the !helper test is folded away it is End + OpKill again. (b) also changes non-ordered pixel shaders, whose BDA stops still use a plain OpKill and may leak the same way on this driver; I did not test that case.

    (a), without the temporary prints:

    --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvFlowEmitter.hpp
    +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvFlowEmitter.hpp
    @@ -12,6 +12,8 @@
     void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& functionState, const IrProgram& program);
     
    +void EmitKillIfPixelValidMaskInactive(SpirvEmitterState& state);
    +
     }
    --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp
    +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp
    @@ -1,4 +1,5 @@
     #include "SpirvBackend/SpirvBda.hpp"
    +#include "SpirvBackend/SpirvFlowEmitter.hpp"
    @@ -91,6 +92,11 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) {
         if (state.bdaStopValue != 0) {
             state.module.AddFunction(spv::OpReturnValue, state.bdaStopValue);
    +    } else if (OrderedPixelShader(state) && state.pixelValidMaskVariable != 0) {
    +        state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 0u));
    +        state.module.AddFunction(spv::OpEndInvocationInterlockEXT);
    +        EmitKillIfPixelValidMaskInactive(state);
    +        state.module.AddFunction(spv::OpReturn);
         } else {
    --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
    +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp
    @@ -26,6 +26,8 @@ void EmitKillIfBoolFalse(SpirvEmitterState& state, std::uint32_t active) {
    +}
    +
     void EmitKillIfPixelValidMaskInactive(SpirvEmitterState& state) {
    @@ -43,6 +45,8 @@ void EmitKillIfPixelValidMaskInactive(SpirvEmitterState& state) {
    +namespace {
    +

    New pull requests from me are closed by the intake rule for now, so whoever picks this up is welcome to the patch; I can rerun anything on this device.

    AI-assisted: yes (Claude Code).

  5. tezzgod74 commented on Oct 10, 2026

    @tezzgod74
    ContributorAuthor

    On the RDNA 2 iGPU, the faulting fragments are killed with OpKill but still carry out their guest write. With OpTerminateInvocation the test passes. All runs below are on main d70b899 with local diagnostic changes only (no PR). Both GPUs are in the same machine. Results are wave64 and wave32 alike, 256 fault pixels and 1792 others per draw:

    Change iGPU (RDNA 2, 32.0.21045.5002): fault / other pixels RX 9060 XT (RDNA 4, 32.0.31041.1004)
    none 1 / 64 0 / 64
    no OpEndInvocationInterlockEXT before the fault's OpKill (critical section held through the kill) 1 / 64 0 / 64
    guest global_atomic_umax (dwords 22-23 of FaultPixelCode, opcode 0x38) replaced by s_nop 0 / 0 0 / 0
    OpKill in StopBdaInvocationIf replaced by OpTerminateInvocation (SPV_KHR_terminate_invocation, device extension and shaderTerminateInvocation enabled) 0 / 64 0 / 64
    • The 1 comes from the shader's own guest write (the global_atomic_umax): without that instruction, the fault pixels stay 0.
    • It doesn't depend on the interlock order. With the critical section held through the kill, the count is still 1, not 64.
    • A count of exactly 1 with the order held is consistent with the killed fragments carrying on as helpers whose loads return 0 while their atomic through the physical storage buffer still lands, but I haven't measured that directly. With OpTerminateInvocation the invocations end and nothing is stored.

    So on this driver, OpKill looks like it behaves as a demote, and the demoted invocation's BDA atomic isn't masked. The RDNA 3.5 and RDNA 4 drivers terminate. Which part is the driver's fault (helper stores landing) depends on how strictly OpKill is defined there. SPV_KHR_terminate_invocation was added because implementations differed on it.

    On the generator side, two options:

    1. Emit OpTerminateInvocation where the device supports VK_KHR_shader_terminate_invocation (core in Vulkan 1.3). Tested above: it passes on both devices. The other OpKill sites (SpirvFlowEmitter.cpp:25, SpirvModuleEmitter.cpp:983) would have the same exposure for any store a discarded pixel makes after the kill on this driver. I haven't tested those.
    2. The branch to the common return suggested above. It skips the remaining instructions instead of relying on OpKill. @danielradosa ran it above and it passes on the same iGPU model; I haven't run it here.

    AI-assisted: yes

    Edit: corrected the guest write to global_atomic_umax (it was mislabelled global_store). This agrees with @danielradosa's runs above, which were posted first and also cover both generator changes.

  6. poonasor commented on Oct 11, 2026

    @poonasor
    Contributor

    Third RDNA 2 device, a discrete one, and option 1/(b) from above as a commit.

    AMD Radeon RX 6700 XT (Navi 22, 0x73df), driver 32.0.21045.5002 (Adrenalin 25.10.45.05), Vulkan 1.4.315, Windows 11 Pro 23H2, WinLibs GCC 15.2.0 r7, Release. Same driver branch as the two iGPU reports. On main the test faults in amdvlk64.dll first (#2944, fixed by #3173); with #3173 it fails as reported here:

    AGC graphics: pixel interlock wave64 BDA fault: pixel 0 counted 1 of 0 overlapping read-modify-writes
    

    Commit 73969879 on poonasor:fix/shader-bda-stop-terminate-invocation (one commit on main 06da1176, 4 files, +31/-1):

    • VulkanDevice.cpp: when the device has VK_KHR_shader_terminate_invocation with shaderTerminateInvocation, enables both and offers SPV_KHR_terminate_invocation to the recompiler.
    • SpirvBdaFault.cpp: StopBdaInvocationIf in a pixel shader emits OpTerminateInvocation (and declares the extension) when it is offered, OpKill otherwise.
    • ShaderValidation.cpp: accepts SPV_KHR_terminate_invocation in a fragment shader.
    • SpirvSpecialization.cpp: lists OpTerminateInvocation next to OpKill.

    Only the BDA stop changes. The pixel kill and valid-mask OpKill sites (SpirvFlowEmitter.cpp, SpirvModuleEmitter.cpp) are left as they are: nobody has measured them on this driver, and terminating there would also end helper invocations that neighbouring derivatives may need.

    Measured on main 379d8b4e + #3173 + #1463, with and without the change:

    Build agc_driver_pixel_interlock Full ctest
    without fails, line above 726 of 732 passed, 5 skipped, 1 failed
    with 60609c45 (same change) passes 3/3 727 of 732 passed, 5 skipped (video_out), 0 failed

    Dreaming Sarah (PPSA02929) relinked against that build runs at about 60 FPS as before.

    Limits: the branch itself is a clean cherry-pick onto 06da1176 and passes tools/check_conventions.py, but I built and tested the change only on the 379d8b4e stack above. Not tested: a device without the extension (the OpKill fallback), NVIDIA, Intel, Linux.

    New pull requests from me are closed by the intake rule, so anyone with intake access is welcome to carry the commit; I can rerun anything on this GPU.

    AI-assisted (Claude Code).

  7. tezzgod74 commented on Oct 11, 2026

    @tezzgod74
    ContributorAuthor

    Tested @poonasor's 73969879 on the RDNA 2 iGPU, cherry-picked onto main 9ba3d0d (Windows 11 Pro 10.0.26300, MinGW-w64 GCC 15.2.0 r7, Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON):

    ANYPS5_GPU Device Driver main main + 73969879
    Radeon(TM) Graphics Ryzen 9 7950X iGPU (RDNA 2, 0x164e) 32.0.21045.5002 pixel_interlock fails passes 5/5
    RX 9060 RX 9060 XT (RDNA 4, 0x7590) 32.0.31041.1004 passes passes 5/5

    Together with the earlier reports, three RDNA 2 devices on the 21045 branch fail on main: two iGPUs (0x164e) and the RX 6700 XT. The full ctest shows no failure the commit introduces on either device; agc_driver_flat_apertures failed once on the RX 9060 XT in a parallel run and passed 5/5 alone.

    One review note on ShaderValidation.cpp: SPV_KHR_terminate_invocation is accepted in any fragment shader without checking that the device enabled shaderTerminateInvocation, while SPV_KHR_fragment_shader_barycentric next to it requires fragmentShaderBarycentric. The emitter only declares the extension when it is offered, so this doesn't change the result above.

    The 7950X iGPU on main 9ba3d0d also shows #2944: 21 agc_driver_* tests segfault on it in one full run (the set varies between runs), while the RX 9060 XT in the same machine has none. pixel_interlock isn't one of them, so it could be tested without #3173.

    AI-assisted: yes

  8. abnergtz commented on Oct 11, 2026

    @abnergtz
    Contributor

    Fourth RDNA 2 device, and @poonasor's 73969879 carried forward with @tezzgod74's validation note addressed, plus tests.

    AMD Radeon RX 6900 XT (Navi 21, 0x73af), driver 32.0.21045.5002, Vulkan 1.4.315, Windows 11 Pro 10.0.26220, WinLibs GCC 15.2.0, Release. Same driver branch as the other three reports; main fails as reported here:

    AGC graphics: pixel interlock wave64 BDA fault: pixel 0 counted 1 of 0 overlapping read-modify-writes
    

    Branch abnergtz:fix/agc-pixel-interlock-terminate-invocation (6 commits on main aac0f1b8, 10 files, +150/-13):

    • 0ca06cf2 fix(recompiler): StopBdaInvocationIf in a pixel shader emits OpTerminateInvocation (and declares SPV_KHR_terminate_invocation) when the target offers it, OpKill otherwise. Same as 73969879, without the SpirvSpecialization.cpp case: OpTerminateInvocation has no operands, so the VisitInputs default path already handles it identically.
    • 19486efa feat(agc): ShaderValidation accepts the extension only in a fragment shader and only when the device enabled shaderTerminateInvocation, like SPV_KHR_fragment_shader_barycentric next to it (the review note above). The flag is part of the validation memo key in Draw.cpp.
    • 81f711d8 feat(agc): VulkanDevice enables VK_KHR_shader_terminate_invocation and the feature when available; ShaderDeviceProfile rejects a target that advertises the SPIR-V extension without the enabled feature.
    • 4f16f09f fix(agc): validation also checks OpTerminateInvocation itself (fragment stage, enabled feature, extension declared), not only the OpExtension.
    • a1f407b4 test(agc): recompiler and validation tests for both paths (1 valid-mask OpKill + 5 terminations with the extension, 6 OpKill without; interlock end before every exit; no termination in non-pixel stages; non-interlocked pixel shader; specialization keeps the opcode and declaration; feature struct chained with VK_FALSE is rejected).
    • f23c94ca docs(technical-debt): records the RDNA 2 measurements, that the OpKill fallback is still affected on a driver without the extension, and the two OpKill sites left as they are.

    Measured with #3173 applied (1d7f8bd3, not part of the branch) to remove the #2944 segfaults, -DANYPS5_ENABLE_SPIRV_TOOLS=ON:

    Build agc_driver_pixel_interlock Full ctest
    main aac0f1b8 + #3173 fails 20/20, line above 760 of 768 passed, 8 failed
    branch + #3173 passes 20/20 761 of 768 passed, 7 failed
    branch alone, SPIRV-Tools off passes 40/40 —

    The 7 failures are the same on both builds and none is in the changed code: agc_driver_sdwa_float_selectors, agc_driver_sdwa_ldexp_f16 (#1463), agc_driver_lds_mixed_width_atomics, three tls_function_coverage timeouts that pass when run alone, and import_audit (fails only in the git worktree used for the A/B; passes in a normal checkout). No amdvlk64.dll segfault in any of these runs.

    Limits: one machine and driver. Not tested: a device without the extension (the OpKill fallback runs only in the recompiler tests), NVIDIA, Intel, Linux/lavapipe. The validation memo key change has no direct test: ValidationKey and CachedFragmentOutputs are internal to Draw.cpp and only reachable through a device-backed draw.

    I can't open pull requests under the intake rule either, so anyone with intake access is welcome to carry the branch; I can rerun anything on this GPU.

    AI-assisted (Claude Code).

  9. blawesom commented on Oct 11, 2026

    @blawesom
    Contributor

    agc_driver_pixel_interlock on RDNA 4: no mismatch.

    AMD Radeon RX 9070 XT (Navi 48, gfx1201, device 0x7550, subgroup 64), RADV Mesa 26.2.4, CachyOS, kernel 7.2.9, GCC 16, Release, main abdad2a. The wave64 and wave32 BDA-fault draws count 0 overlapping read-modify-writes (the expected value) and the test passes, standalone and inside a full ctest sweep (793 tests, 0 failures, 4 display-related skips).

    So the one-RMW-per-killed-pixel behaviour reported on the RDNA 2 iGPU does not reproduce on gfx1201 with Mesa 26.2.4.

  10. 11 remaining items

  11. oneandonlydean commented on Oct 16, 2026

    @oneandonlydean
    Collaborator

    Fixed by #5804 (65d56a2): pixel invocations stop with OpTerminateInvocation after a BDA fault when the feature is available. agc_driver_pixel_interlock passes on the RDNA 2 iGPU with the fix branch. Closing; reopen if it still fails on current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions