Skip to content

AMD Windows: draws whose pixel shader reads the packed ancillary VGPR leave pixels unwritten (agc_driver_relative_ancillary fails on main) #4800

Closed

Description

@LeandroLP23

On an AMD GPU on Windows, agc_driver_relative_ancillary (added by #4150) fails on main. The draw leaves its pixels unwritten. #4031's new agc_driver_pixel_ancillary_word fails the same way on this GPU. Both pass on lavapipe in CI.

Setup: Windows 11 (26300), AMD Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), WinLibs GCC 15.2.0, Release, BUILD_TESTING=ON, ANYPS5_NO_SHADER_CACHE=1.

main 584cc4c57:

wave 32 packed read
AGC graphics: relative ancillary draw produced an incorrect pixel: 64

The test fills the target with 0x40 (64) before drawing, so the first draw (wave 32 packed read, expected 255) wrote nothing to that pixel. It fails 3 runs of 3, and the same way with each of these set: APS5_NO_GPL=1, APS5_NO_PIPELINE_LTO=1, APS5_SPIRV_OPT=none.

The full ctest -R '^agc_|pixel_ancillary' -j 1 run is 445/446; this is the only failure. pixel_ancillary, which doesn't draw, passes.

#4031 head dd8403112 merged onto that main: its new agc_driver_pixel_ancillary_word fails 5 runs of 5 with ANCILLARY alone, triangle: pixel (0, 0) holds (nan, nan, nan, nan). That test fills its target with 0xff, so NaN is again an unwritten pixel. With APS5_NO_PIPELINE_LTO=1 the failing case moves between "ANCILLARY alone, triangle" and "ANCILLARY after the perspective center, rectangle", but each time the pixel is unwritten. Details: #4031 (comment)

In common: both tests draw with a pixel shader that reads the packed ancillary VGPR, and on this GPU those draws leave the target untouched where the test expects a write. Other AGC draw tests pass on the same machine.

Titles: this is the input behind #2548 (Stray, Kena, Lies of P and other Unreal titles), so ancillary-reading draws on AMD hardware may be affected beyond these tests. I have no such title to check that.

I'm looking into it next: the generated SPIR-V for these draws and what differs on AMD. I'll post what I find here.

AI-assisted: yes (Claude Code)

Activity

  1. LeandroLP23 commented on Oct 12, 2026

    @LeandroLP23
    ContributorAuthor

    First finding: the read of gl_Layer is what loses the pixels.

    I dumped the SPIR-V that agc_driver_relative_ancillary's first draw (wave 32 packed read) passes to vkCreateShaderModule on main 584cc4c57. The fragment shader composes the ancillary word from gl_SampleID (bits 8+) and gl_Layer (bits 16+), both Flat inputs, with SampleRateShading and Geometry declared. It writes out_mrt_0 only in non-helper lanes and kills nothing else. The vertex shader writes Position, ClipDistance, and reads VertexIndex and InstanceIndex. It does not write Layer.

    Then, with a local hook that swaps in an edited fragment module (one OpLoad replaced by an OpBitcast of the constant 0, nothing else changed):

    Fragment module First draw
    as emitted pixel unwritten (incorrect pixel: 64, the fill)
    gl_SampleID load replaced by 0 pixel unwritten (64)
    gl_Layer load replaced by 0 written; the test moves on and fails later only because the edited module is reused for its other cases
    both replaced by 0 written

    So on this AMD driver, a fragment shader that reads Layer, in a pipeline whose vertex stage doesn't write it, leaves the color target untouched. lavapipe returns 0 there. I haven't confirmed whether the Vulkan spec defines the value when no pre-rasterization stage writes Layer. Either way, the draw shouldn't lose its output on AMD.

    Next I'll check the other direction: the same pipeline with a vertex shader that writes Layer = 0.

    AI-assisted: yes (Claude Code)

  2. LeandroLP23 commented on Oct 12, 2026

    @LeandroLP23
    ContributorAuthor

    One more case narrows it down. With the gl_Layer load left in place but its result unused, the first draw writes its pixel. I changed only the OpBitcast that consumed the loaded value, so the value no longer reaches the ancillary word.

    In the emitted module, the layer lands only in bits 16+ of the word, and the output takes bits 8–11 (OpBitFieldUExtract … 8 4). So the layer value never reaches the colour, yet a live use of it loses the write. That looks like the AMD driver taking the fragment's render target layer from a vertex-stage export that is never written, when the fragment shader consumes Layer. The write then goes to a layer the one-layer target doesn't have. I haven't confirmed that mechanism on the hardware side. The SPIR-V results are what's established.

    Two ways the emulator could avoid it, for whoever picks this up:

    1. Fold Layer to 0 in the fragment shader when the pipeline's pre-rasterization stage doesn't write Layer, which is the common case. That is also the value the RT index has when the guest vertex shader doesn't export one. The pipeline knows both stages, so it could set it with a specialization constant, like the other per-pipeline values.
    2. Make the vertex stage write Layer = 0 whenever the fragment shader reads it and the guest vertex shader exports none. That needs shaderOutputLayer, and a vertex variant keyed on the fragment shader's needs.

    Option 1 keeps the vertex shaders as they are. I can test either one on this GPU.

    AI-assisted: yes (Claude Code)

  3. LeandroLP23 commented on Oct 12, 2026

    @LeandroLP23
    ContributorAuthor

    Fix proposed in #4842: the packed ancillary lowering uses 0 for the render target index field instead of reading the unwritten Layer. On the Radeon 860M agc_driver_relative_ancillary passes with it (445/445 agc_), and with #4031 merged on top (plus the same 0 at its whole-word read) #4031's agc_driver_pixel_ancillary_word passes too.

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