Skip to content

fix(shader): read other control points' off-chip outputs in hull programs - #9743

Open
frankea wants to merge 4 commits into
boykopovar:mainfrom
frankea:fix/hull-reads-control-point-outputs
Open

frankea wants to merge 4 commits into
boykopovar:mainfrom
frankea:fix/hull-reads-control-point-outputs

Conversation

@frankea

@frankea frankea commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

A hull program that reads control-point outputs of its own patch back from the off-chip ring threw tessellation lowering: unsupported tessellation ring access. Only domain programs could read that ring.

  • A hull ring read inside its patch's control points now lowers to a gl_out read. A read at a fixed offset reads control point offset / stride, and a read at the program's own control point's offset reads its own record. The stride comes from the program's ring writes, as before.
  • These reads still throw: a read where no control point writes, so the stride is unknown; a read past the patch's control points, such as its patch constants; a hull read of the factor ring.
  • On the console the read needs no barrier. Gran Turismo 7's two hull programs have their only s_barrier at pc 0x28, before their ring stores, and read the stores back after an s_waitcnt. A Vulkan tessellation control shader that reads another invocation's output needs an OpControlBarrier between the write and the read. In that execution model the barrier also makes the Output writes before it visible to the patch's other invocations (SPIR-V, OpControlBarrier). The modules use the GLSL450 memory model, where this implicit synchronization applies.
  • So before a read of another control point's record, the lowering inserts the barrier that s_barrier already becomes in a hull program (OpControlBarrier Workgroup, Invocation, None).
    • Where: in the last block that every invocation reaches, outside any loop, before every such read. Within that block it goes before the first read, or at the end.
    • A conditional branch counts only along the edge it takes when its condition has the same value for every control point index. The lowering evaluates the condition for each index, where a hull program's lane ID is its invocation ID. This is how the hull-lane guard both of GT7's hull programs start with is seen to skip nothing: v_mbcnt compared against the hull thread count in s3, then s_cbranch_execz.
    • The read throws if no block qualifies, if a ring write comes after the barrier, or if no control point reaches the read (the hull shader reads another control point's off-chip output ...).
  • fix(shader): index tessellation attributes at a constant address with constants #8884, on main since, added an immediate-address path to TessellationPointer that indexed every control-point output access by the reader's own control point. A hull read of another control point's output at an immediate address now takes that path too: it reads control point address / stride, clamped to the last output control point, as the evaluated path does.
  • The tessellation ring debt entry says a hull program reads its own patch's control points from gl_out after the inserted barrier. It also says a field that no control point writes reads an undefined value instead of the ring's earlier contents.

Why: Gran Turismo 7 (PPSA01317) stops on this refusal while loading the World Map, past the main-pass depth refusal (#9721): shader 0x1280659800 stage 2: ShaderRecompiler::Recompile: tessellation lowering: unsupported tessellation ring access at pc 0x28c.

  • Every control point stores 4 dwords at a time to its record at (cp << 6) + (patch << 8) (4 output control points, 64-byte stride).
  • Then v_cmpx_gt_u32 1, v20 and s_cbranch_execz keep control point 0, which loads fields of control points 0, 1 and 2 at fixed offsets 0x00, 0x40, 0x80 and 0x90 from the same ring.
  • A second hull program, 0x1280dcce00, does the same with a 192-byte stride (loads at 0x2c, 0xec, 0x1ac and 0x26c).

Tested

Windows 11 (build 26200), NVIDIA GeForce RTX 4090 (driver 616.92), MinGW GCC Release build with BUILD_TESTING=ON, all on one machine.

New tests:

  • agc_tessellation_lowering has twelve new IR cases.
    • Lowered: control point 0 reading another control point's record behind a branch gets the barrier after the writes and before the branch. A read in the block that writes gets the barrier between them. A read of the program's own record gets no barrier and is indexed by the invocation ID.
    • A branch around the program that no control point takes, !(LaneId < 4), does not keep the barrier from the writes. One that some control points take, !(LaneId < 3), is refused. Both cases number their block metadata apart from the IR's block ids, as translated programs do.
    • Refused: a read behind a branch that every control point takes around it, a write after the read, a read in a loop through the entry, a read where no control point writes, and a read past the patch's control points.
    • fix(shader): index tessellation attributes at a constant address with constants #8884's immediate-address path: a hull read of control point 2's output at a fixed address reads control point 2, and a fixed address past the last control point reads the last one.
  • agc_driver_tessellation (GPU) adds a merged hull program like the existing one, except that control point 0 reads control point 2's level for its tessellation factors from the off-chip ring instead of LDS, behind GT7's two s_cbranch_execz.
    • Its patches give control points 0 and 1 other levels than control point 2. A read of the wrong record, or of one not yet written, changes which patches are drawn flat, culled or subdivided. The test draws them and checks each patch's coverage.
    • It also checks that the hull's SPIR-V has a barrier after the control-point stores and before the read.

Runs:

  • Focused set: the 52 ctest entries of 8 targets: agc_tessellation_lowering_tests, agc_tessellation_factor_ring_tests, agc_driver_tessellation_tests, agc_driver_graphics_tests, agc_vector_window_tests, agc_vertex_input_validation_tests, agc_driver_bda_device_tests and agc_shader_memory_tests.
  • This PR (7caf508, which merges main 7e69be6): 52/52 passed, each test run three times (ctest --repeat until-fail:3).
  • Control for the immediate-address path: this head with only SpirvModuleEmitter.cpp from before 7caf508. agc_tessellation_lowering fails on the two new cases: the read is indexed by the reader's own control point, which the test program never declares (SPIR-V module emission failed: stage input InvocationId was not declared).
  • Validation layer: VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (Vulkan SDK 1.4.363.0), ANYPS5_NO_SHADER_CACHE=1 and ANYPS5_REQUIRE_VULKAN=1 with ctest -V, on main 83462cc (7e69be6 plus eight commits to tools/ only) (52/52) and on this PR (52/52). Neither log has was not found but was requested by env var VK_INSTANCE_LAYERS, and neither has a VUID- line.
  • Before the main merge (9c03379 on main 4777cac): 52/52 three times, and 52/52 under the layer with no VUID- line. main control there: main 4777cac with only this PR's two test files. Both tests fail. The ten new lowering cases are refused with unsupported tessellation ring access, and the GPU test stops on the new hull program with the same refusal at pc 0x114, its first ring load.
  • Linux (lavapipe), by @Dextroid17 on this PR merged with main: the same results, including the control's stop at pc 0x114.
  • tools/check_conventions.py --base origin/main reports no findings.

Checklist

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6612 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp L1031

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 5021 docs/dev/debt/shader.md
PR 5371 docs/dev/debt/shader.md
PR 6235 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
docs/dev/debt/shader.md
PR 6280 docs/dev/debt/shader.md
PR 6412 docs/dev/debt/shader.md
PR 6488 docs/dev/debt/shader.md
PR 6924 docs/dev/debt/shader.md
PR 7201 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 7223 docs/dev/debt/shader.md
PR 7395 docs/dev/debt/shader.md
PR 7410 docs/dev/debt/shader.md
PR 7484 docs/dev/debt/shader.md
PR 7642 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 7677 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 7684 docs/dev/debt/shader.md
PR 7922 docs/dev/debt/shader.md
PR 7942 docs/dev/debt/shader.md
PR 8004 docs/dev/debt/shader.md
PR 8077 docs/dev/debt/shader.md
PR 8939 docs/dev/debt/shader.md
PR 9356 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 9387 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 9457 docs/dev/debt/shader.md
PR 9508 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 9633 docs/dev/debt/shader.md
PR 9748 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
docs/dev/debt/shader.md
PR 9749 docs/dev/debt/shader.md
PR 9790 docs/dev/debt/shader.md
PR 9889 docs/dev/debt/shader.md
PR 9969 docs/dev/debt/shader.md
PR 10193 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp
PR 10348 core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp

@MarcZX10 MarcZX10 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.

The implementation and test report are thorough. The ten new lowering cases exercise the barrier placement and refusal boundaries, the GPU test checks output behavior, and the report includes negative controls plus a same-machine validation-layer run. I did not find a substantiated implementation defect in the reviewed diff.

Before merge, please refresh this branch from current main and rerun the focused tessellation set on the refreshed base and change. The reported runs use main 4777cac, while this PR is currently 54 commits behind the upstream tip. CONTRIBUTING.md requires shader recompiler changes to run the same focused tests on main and the change with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_NO_SHADER_CACHE=1, and ANYPS5_REQUIRE_VULKAN=1 via ctest -V, and compare the logs for new validation errors. The described runs satisfy this for their tested revision; current main still needs coverage.

The overlap bot identifies two still-open PRs that overlap this change: #6612 in tests/execution/Tessellation.cpp, and #8884 in tests/TessellationLowering.cpp / tests/execution/Tessellation.cpp. The body explains how the #8884 interaction can be integrated but the checklist says Depends on: none. Please coordinate the merge order with the affected authors and record the agreed dependency/order in the checklist, as the overlap rule requires.

Local checks: python tools/check_conventions.py --base upstream/main --head upstream/pr/9743 passed and git diff --check upstream/main...upstream/pr/9743 passed. I could not independently run the C++ or Vulkan tests: this checkout has no configured build and only about 649 MB free. Hosted product build jobs are skipped; hosted conventions, eligibility, intake, labels, and report checks pass.

@Dextroid17

Copy link
Copy Markdown
Contributor

Linux run, since the body says "Not run on Linux": llvmpipe (LLVM 23.1.1, 256 bits) via lavapipe, Mesa 26.2.4, GALLIUM_OVERRIDE_CPU_CAPS=avx, i9-11900K, CachyOS, GCC 16.2.1, CMake 4.4.3, Release, ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1, -j1; 9c033796c merged with main b7d1d47a5 (your merge base 4777caca3 is 54 commits behind it; local merge b60c1a691, clean, the 5 files of the diff only).

Head. The three tessellation tests (agc_tessellation_lowering, agc_tessellation_factor_ring, agc_driver_tessellation) pass 6 of 6 runs (3 plain, 3 under VK_LAYER_KHRONOS_validation 1.4.363, the layer inserted at both of agc_driver_tessellation_tests' instance creations per run, the other two tests load no Vulkan, 0 VUID-). Your focused set as the ctest entries those 8 targets register here (53, not 52: agc_driver_graphics_maps_fallback is Linux-only) passes 53 of 53 plain and 53 of 53 under the layer, 0 VUID-. agc_driver_tessellation on this device skips the two BDA subtests ("llvmpipe runs tessellation-control loads that the check of the unmapped address skips"; the message is already in Tessellation.cpp at b7d1d47a5) and runs the new merged hull program: nothing in the diff prints on success, so the evidence that it ran is the negative control.

Negative control. core/shader/recompiler/Optimization/src/TessellationLowering.cpp and core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp from b7d1d47a5, the PR's two test files kept, rebuilt: agc_tessellation_lowering fails 6 of 6 with all ten new cases refused (unsupported tessellation ring access at pc 0xc0, 10 per run), and agc_driver_tessellation fails 6 of 6 at the new hull program with ShaderRecompiler::Recompile: tessellation lowering: unsupported tessellation ring access at pc 0x114, which you name as its first ring load; that abort comes before the BDA subtests, so the control arm never reaches them. Both match your Windows control; agc_tessellation_factor_ring and the other 50 entries still pass. 0 VUID- on either arm.

AI-assisted: yes

@frankea

frankea commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

@Dextroid17 thank you for the Linux run, and for merging with current main first. Both arms on lavapipe match the Windows results, including the control's stop at pc 0x114, so the GPU case has now run on a second driver.

I corrected one number in the body. 0x1280659800 has 40 read-back loads after the barrier, not 48: I had counted that module by hand, and recounting it with the script I used for the others gives 40.

The nine GT7 hull programs that this run met next are in #9806. They fail on a separate limit, an address that masks the control point out of v1 with 0x1f00.

@MarcZX10 MarcZX10 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.

The existing evidence is strong: the PR reports paired Windows RTX 4090 and Linux lavapipe controls, repeated focused tests, spirv-val, and GT7 progress. The exact head 9c033796cb6938a453161b89d1e91f445f71c89e is based on 4777caca34, 75 commits behind current main 11f976b27c16ed91633f00039dba5dfd97dd7cc1; merge-tree/diff-check are clean. The PR body describes interaction with open #8884 and #6612, but the checklist still says Depends on: none, and I found no author-to-author order confirmation in this thread. Please coordinate the interaction with those authors and record the resulting order/dependency in the checklist, then rebase and rerun the focused tessellation tests plus required paired validation-layer comparison on current main and the final head. I did not independently run GPU tests locally.

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed shader core/shader docs agc libSceAgc* labels Oct 22, 2026
…l-point-outputs

# Conflicts:
#	core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp
#	core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@frankea

frankea commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thank you for the review. Both items are addressed, and the body is updated.

AI-assisted: yes (Claude Code)

@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.

Apple M2 Max data point for head 7caf5085b.

Setup: Apple M2 Max, macOS 27.2, x86-64 under Rosetta 2, Release. Not main (it doesn't link on macOS): #8658 @ 91adbf7 (which includes #8884) + this PR's diff. Two drivers: MoltenVK 1.4.2 from the Vulkan SDK 1.4.363.0, and MoltenVK main with KhronosGroup/SPIRV-Cross#2704.

main() of agc_driver_tessellation stops at the first failing case, and on Apple the existing merged hull program case already fails before this PR's case runs. To see every case, I ran a local copy of tests/execution/Tessellation.cpp with each Require*/ExpectRefusal call wrapped in a try/catch.

Control (this PR's two test files only, without the lowering changes):

  • agc_tessellation_lowering fails; the new cases are refused with unsupported tessellation ring access at pc 0xc0.
  • With both drivers, RequireBarrierBeforeRingReadBack and the new merged hull program reading control point 2's level back from the off-chip ring case both stop on unsupported tessellation ring access at pc 0x114, as in your control.

This PR, both drivers:

  • RequireBarrierBeforeRingReadBack passes.
  • The new draw case fails: merged hull program reading control point 2's level back from the off-chip ring, unculled: the level-1 patch was not drawn as one flat triangle, centre (16, 24, 40, 255).
  • That's the same failure as the existing merged hull program and merged hull program storing its local part's registers cases, which fail the same way on this base without this PR, with #8884 included. So on Apple the new draw case can't confirm the read-back, but the failure isn't caused by this PR.
  • Full ^agc suite (-j1 --timeout 30, flip/display/lds_atomics64 excluded): the same failing tests, with each driver, as an earlier base run without this PR (#8658 @ c5fa396, also with #8884).

GPUs tested: Apple M2 Max (MoltenVK 1.4.2; MoltenVK main + SPIRV-Cross#2704)

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@frankea

frankea commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 this branch now merges main 0db47b2 (head 5da72b8, no conflicts), and the focused tessellation set and the paired validation-layer comparison are rerun on it: the first bullet under Tested. #8884 has merged since your review, and I've asked @milescoviello on #6612 to confirm the order there (whichever lands second keeps both sets of GPU cases).

AI-assisted: yes (Claude Code)

@adriadam10 adriadam10 self-assigned this Oct 23, 2026

This branch has not been deployed

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

Labels

agc libSceAgc* docs shader core/shader unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants