Skip to content

fix(agc): accept any fragment export where the draw has no color target - #6708

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/agc-export-without-targets
Oct 20, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/agc-export-without-targets

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

Crimson Desert (PPSA25286) stops at its first draw:

[videoout] presentation failed: AGC graphics: fragment shader must export float4 colors to its attachments
terminate called after throwing an instance of 'std::runtime_error'

A throwaway print at the refusal in ValidateShaders (ShaderValidation.cpp) shows the draw:

[fsexport] location 0 signature vertex:u32x4 attachments 1 blends 0 outputs: 0:vertex:u32x4 colors:

The draw binds no color target (no blends, no colors), probably a depth-only pass. Its pixel shader still exports a uint4 at MRT0. The check goes over every output below max(blends, 1) and requires f32x4, or u32x4 for integer and packed targets. At location 0 it finds no target, so it falls back to f32x4 and refuses the draw.

A location without a color target has no attachment in the pipeline. With no blends, colorAttachmentCount is 0, and a gap between written slots is VK_ATTACHMENT_UNUSED. Vulkan discards an output that has no attachment, whatever its type. The check now applies only where a color target exists. The location still goes into the returned output set as before, so masks and pipeline keys of draws that already ran don't change.

Tested

Linux (Ubuntu 24.04, GCC 13.3, Release), RTX 4070 Ti, NVIDIA 595.99.

  • agc_driver_graphics: the packed-export shader there exports a uint4 at location 0. It's now validated against a state with no color targets and no blends, and against one with a target at slot 1 and slot 0 unused, the gap a draw can actually have. (@jotitadev pointed out that my first unused-slot state, no colors with one blend, is one the pipeline refuses before validation.) Both are accepted, and validating it against a float target still fails with float4 colors. With main's ShaderValidation.cpp the test fails with the title's line, fragment shader must export float4 colors to its attachments. It passes 3 of 3 with this change.
  • ctest (^agc_driver_|^agc_vertex_barrier|recompiler|shader_cache|request_serializer|spirv, 468 tests): 467 pass. agc_driver_store_over_untracked fails the same way on main 5d9dcc4 here; test(agc): keep waiting for the neighbour copy case's result #6385 addresses it.

Crimson Desert, scripted pad input, with #6612, #6613, #6706 and #6178 (for memory), which it needs to reach this draw:

Build Result
without this PR stops at its first draw, 597 s in, on the refusal above
with this PR gets past it with no stop: 22 draws by 591 s, while it is still building pipelines (1 to 3 s per draw), until my 16 GiB memory cap ends the run

Nothing is presented by then, so I haven't seen this draw's result. The test above covers the validation.

Validated on Windows 11 with an AMD Radeon 860M (RDNA 3.5, driver 32.0.13058.8001) by @LeandroLP23 (comment), in one build of main 6efb726 with this head and 13 other pull requests: agc_driver_graphics and agc_driver_store_over_untracked pass 3 runs of 3. 713 of 716 agc_* tests pass; the 3 that fail fail the same way on main there.

Not checked on Intel or on a console.

  • Khronos validation layer (LunarG SDK 1.4.363), NVIDIA RTX 4070 Ti (595.99.02), Ubuntu 24.04: VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 ctest -R '^agc_' -V on main e2a2914 and on main + this PR (3d5335a, a local merge). main: 815 of 815 pass; with this PR: 815 of 815 pass. Neither log reports the layer missing. VUIDs on main: VUID-VkImageViewCreateInfo-usage-02275 ×6, VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 ×18, VUID-vkCmdCopyBuffer-srcBuffer-00118 ×2, VUID-vkCmdDraw-multisampledRenderToSingleSampled-07285 ×2, VUID-vkCmdWaitEvents-srcStageMask-01158 ×2, VUID-vkSetEvent-event-09543 ×2; with this PR: VUID-VkImageViewCreateInfo-usage-02275 ×6, VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 ×18, VUID-vkCmdCopyBuffer-srcBuffer-00118 ×2, VUID-vkCmdDraw-multisampledRenderToSingleSampled-07285 ×2, VUID-vkCmdWaitEvents-srcStageMask-01158 ×2, VUID-vkSetEvent-event-09543 ×2. None is new with this PR.

Checklist

🤖 Generated with Claude Code

@MarcZX10

Copy link
Copy Markdown
Contributor

I also found a hole in the targetless case. ValidateShaders still loops only up to max(blends.size(), 1) and validates the export type when no ColorTarget exists at that location. So with colors.empty() but blends.size() == 1 (an unused attachment slot), a uint4 export still hits the fallback f32x4 check. The new test resizes blends but uses a packed export? It should exercise this exact state with a write-enabled blend entry; skipping type validation when find_if(state.colors, exportIndex) returns end fixes it while retaining the location in the returned set. I reproduced the path from the current function; the full graphics test build is currently blocked in unrelated GCC 12 parsing at SrtDescriptorEvaluation.cpp:183.

@MarcZX10

Copy link
Copy Markdown
Contributor

Correction: my previous comment was a false positive. I checked the PR diff again and its Require(color == state.colors.end() || ...) already skips type validation for a location with no ColorTarget, including when blends has an unused slot. The added tests cover the no-blend and unused-slot cases, so the extra write-mask case I suggested is redundant. Sorry for the noise.

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of head d5b49fd (no build, no GPU run).

The fix looks right to me: a location with no ColorTarget maps to VK_ATTACHMENT_UNUSED (or no attachment at all) in Pipeline.cpp, so skipping the type check there matches what Vulkan does with that output.

One note on the second new test case. targetless.colors.clear(); targetless.blends.resize(1); is a state the Pipeline constructor rejects:

Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && ...,
        "blend states do not match decoded color state");

With no colors, blends.size() has to be 0, so a draw never reaches ValidateShaders with that state's pipeline. The unused-slot shape a real draw can have is a target at exportIndex 1 with slot 0 empty (colors = {exportIndex 1}, blends.size() == 2), with the shader still exporting at location 0. The change covers it too (the find_if misses location 0), but neither case in the test builds it. Swapping the second case for that state would test the gap the description talks about.

Method: read the diff against main 5d9dcc4 and modelled both Requires in a small Python script; nothing was built.

Written with AI assistance (Claude Code). No builds or tests were run for this note; only the standalone snippet above.

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks @jotitadev, good catch: the pipeline would refuse my unused-slot state before validation. f663675 replaces it with your shape, a target at slot 1 with slot 0 unused and the shader exporting at location 0. It fails on main's ShaderValidation.cpp with the title's line, and passes 3 of 3 with the change.

AI-assisted: yes

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1395 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4040 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 5441 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5499 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5925 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6146 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6188 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6194 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6235 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6280 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6290 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 6481 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6548 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6636 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6683 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6808 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6972 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7085 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7256 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 7668 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7862 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7888 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 8099 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp

milescoviello and others added 2 commits October 17, 2026 23:22
The fragment output check demanded a float4 (or uint4 for integer and
packed targets) at every location below the draw's MRT count, at least
one. A location with no color target, a draw without color targets or a
gap between written slots, has no attachment there, and Vulkan discards
the output whatever its type. Crimson Desert's first draw binds no color
target and exports a uint4 at location 0, and stopped there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unused-slot case had no colors and one blend, which the pipeline
refuses before it validates shaders. A target at slot 1 with slot 0
unused is the gap a draw can have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LeandroLP23

Copy link
Copy Markdown
Contributor

AMD on Windows: the tests this PR names pass on an RDNA 3.5 GPU.

Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), Windows 11 26300, WinLibs GCC 15.2.0 r7, Release. main 6efb7261b with this PR (3d5335ab3) merged, together with #7296, #7446, #7377, #7445, #6748, #6785, #6706, #6613, #7449, #7391, #7489, #7484, #7514. They were built once, so the result is for the combination; each merge was clean.

  • agc_driver_graphics, agc_driver_store_over_untracked: pass, 3 runs of 3.
  • Every agc_* test on the combined build: 713 of 716 pass. The 3 that fail, agc_driver_host_import_lifetime, agc_driver_quad_pixel_mask and agc_driver_layered_volume_vertex_no_gpl, fail the same way on unchanged main here, so none comes from these PRs.

AI-assisted

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks @LeandroLP23 for the AMD run on Windows. I added it to the description.

AI-assisted: yes (Claude Code)

@adriadam10 adriadam10 self-assigned this Oct 19, 2026
@adriadam10
adriadam10 merged commit 38e04ad into boykopovar:main Oct 20, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Re-checked against current main: the merge is clean, ShaderValidation.cpp is untouched by main since the last review, and the two new test cases (no targets, and a gap below a target) are still in place after main's Graphics.cpp changes. The one-line change skips the export type check where no ColorTarget exists, which matches the pipeline (no attachment / VK_ATTACHMENT_UNUSED), and the location stays in the returned set. The new cases fail on main with the float4 colors message and a float target still fails. Merged.

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

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants