Skip to content

feat(shader): read hull patch constants in domain programs - #4175

Closed
tibzejoker wants to merge 3 commits into
boykopovar:mainfrom
tibzejoker:feat/domain-patch-constants
Closed

tibzejoker wants to merge 3 commits into
boykopovar:mainfrom
tibzejoker:feat/domain-patch-constants

Conversation

@tibzejoker

Copy link
Copy Markdown
Contributor

What

Stacked on #4114 (which is stacked on #4082). Only the last commit, feat(shader): read hull patch constants in domain programs, is new here; the two commits below it are #4082 and #4114 as they are.

With #4082 and #4114, GTA V (PPSA04264) gets past its hull program and stops on its domain program:

tessellation lowering: domain shader reads of patch constants or other patches are unsupported at pc 0x9c

The domain program reads the patch constants that its hull wrote at 0x8000 - stride * (rel + 1). TessellationLowering refused every domain read of the off-chip ring whose relative patch coefficient is not a whole patch of control points.

What changes:

  • A domain read that stays inside its own patch's control points is still a control point read.
  • A read at the control point record stride that lands inside the group's control point region (a neighbouring patch's control points) still fails with the existing "outside its patch's control points" error.
  • Any other read becomes a per-patch input (TessellationAttribute::PatchInput). This requires that the domain program also reads a control point, because the patch interface location comes from the control point record size. It also requires that the address lies past the patch's control points and that the records do not overlap the neighbouring patches' records. Unproven shapes still throw.
  • Hull patch outputs and domain patch inputs use one layout: the offset inside the record is the ring address modulo the patch record stride, and the per-patch interface is one record stride wide. Each stage gets the stride from its own accesses, so a hull and a domain program compiled separately use the same locations, whether the hull lays its records out ascending or descending (as in fix(agc): bound tessellation patch records by the draw's patch count #4114). The hull no longer packs its patch outputs from its lowest written offset, because the domain program cannot know that offset.

The limit is listed in TechnicalDebt.md. A domain read of another patch's constants that is a whole number of strides away from its own record reads its own record, because neither stage knows where the other's records begin.

Tested

Windows 11, RTX 4090, WinLibs GCC 15.2.

  • agc_driver_tessellation: new case domain program reading its patch constants. The existing triangle hull (patch constants at 0x8000 - 16 * (rel + 1)) is drawn with a domain program that takes two control point x coordinates from those patch constants instead of the control points. The case also checks each drawn patch's shape at mid-height. This shape check was added to every RequirePatches case.
    • On fix(agc): bound tessellation patch records by the draw's patch count #4114 (recompiler unchanged, new test): fails with tessellation lowering: domain shader reads of patch constants or other patches are unsupported at pc 0x38.
    • On this branch: Tessellation tests passed.
    • With the two patch constant components swapped in the SPIR-V emitter on purpose, the test fails with patch 0 is not the triangle of its control points at mid-height, so the check sees wrong values.
  • The old DomainPatchRead refusal case is replaced by DomainNeighbourRead (a read of the next patch's control point), which is still refused on both fix(agc): bound tessellation patch records by the draw's patch count #4114 and this branch.
  • ctest -R "agc_driver|agc_masked|agc_shader|agc_vector|tess" -j8: 336/337 pass on fix(agc): bound tessellation patch records by the draw's patch count #4114 and the same set passes on this branch. agc_driver_prepared_shaders_registered_workgroup_sync fails on both. agc_driver sometimes exits with 0xc0000374 under -j8 on both and passes when run alone.
  • python tools/check_conventions.py --base upstream/main: passed.

GTA V result: on main 325e0a8 with #4082, #4114 and this commit (plus the other PRs GTA needs that are unrelated to tessellation: #425, #290, #637, #448, #423, #4021), the domain program that stopped at pc 0x9c compiles and Story mode runs past it; the log of a 150 s prologue run has no tessellation error.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV

Celegans12 and others added 3 commits October 12, 2026 00:42
…and level clamps

DecodeShaderStages accepted only triangle patches with clockwise output
and HOS level clamps that leave [1, 63] untouched, so tessellated
terrain drawn as quad patches with TRI_CCW output and
VGT_HOS_MAX_TESS_LEVEL 9 aborted. Quad domains and TRI_CCW now decode
and emit Quads and VertexOrderCcw; isolines, other partitionings and
point or line output still throw.

VGT_HOS_MIN/MAX_TESS_LEVEL travel in the tessellation configuration
(draw keys, hull cache key, recompile request version 17) and clamp the
factors the hull writes; zero, negative and NaN outer factors still
cull. Clamps outside 0 <= min <= 63, max >= 1, min <= max throw.

Quad hulls write four outer and two inner factors per patch at a
24-byte stride, the layout Mesa's TF ring store uses; quad hulls that
use the 16-byte triangle stride are refused.
TessellationLowering proved ring and LDS addresses affine over a
relative patch index of up to 256. A hull that lays its patch-constant
records out downwards, at 0x8000 - 208 * (rel + 1) like the quad
particle hull in Ghost of Yotei, then reached a negative address for
the last slot and aborted as "not affine". The relative patch index of
hull and domain programs stays below VGT_LS_HS_CONFIG.NUM_PATCHES
(bits 7:0, patches per threadgroup), which is 63 for that draw.

DecodeShaderStages now carries NUM_PATCHES as patchesPerGroup in the
tessellation configuration (draw keys, hull and domain cache keys,
recompile request version 18) and refuses zero, and the lowering bounds
the relative patch index by it. Configurations without it keep 256.
TessellationLowering refused every domain read of the off-chip ring
whose relative patch coefficient is not a whole patch of control points,
so a domain program that reads the patch constants its hull wrote
stopped with "domain shader reads of patch constants or other patches
are unsupported" (GTA V PPSA04264 at pc 0x9c, on top of the descending
records of boykopovar#4114).

A domain read that stays inside its own patch's control points is still
a control point read. A read at the control point record stride inside
the group's control point region is left to the existing "outside its
patch's control points" refusal. Any other read becomes a per-patch
input, provided the domain also reads a control point (the patch
interface location follows the control point record size), its address
lies past the patch's control points and its records do not overlap
those of the neighbouring patches.

Hull patch outputs and domain patch inputs share one layout: the offset
inside the record is the ring address modulo the patch record stride,
and the interface is one record stride wide. Both stages derive the
stride from their own accesses, so separately compiled hull and domain
programs agree on locations without knowing where the hull placed its
records (ascending, or descending from 0x8000 as in boykopovar#4114).

The tessellation execution test draws the triangle patches with a
domain program that takes two control point x coordinates from the
hull's patch constants and checks each patch's shape at mid-height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
@Celegans12

Copy link
Copy Markdown
Contributor

Thanks for stacking this on #4082 and #4114, good to see GTA V hitting the same stop. Heads-up so we don't make double work for the reviewers: #4163 (opened about 20 minutes before this one, on the same base) reads the hull's patch constants in domain programs too, from Yōtei's terrain domain, which reads its hull's records at 0x8000 - 208 * (rel + 1).

The two take slightly different routes to agreeing on the record layout between the stages, so it would be useful to know whether either one covers both games. If you get a chance, could you try GTA V's domain at pc 0x9c on #4163's head? I'll check Yōtei's on this one in return, and then we can suggest one of them to the maintainers.

@tibzejoker

Copy link
Copy Markdown
Contributor Author

Thanks, I missed #4163 when I checked for duplicates. I will run GTA V's domain on #4163's head and report here. If it covers GTA, I will close this one in favour of yours.

AI-assisted (Claude Code).

@tibzejoker

Copy link
Copy Markdown
Contributor Author

Closing in favour of #4163, which covers GTA V's domain program as well (tested on its head, details there). Thanks @Celegans12.

AI-assisted (Claude Code).

@github-actions

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
#3763 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1628, L1633
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L737, L765
#3988 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1628, L1633
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L737, L765
#4011 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1628, L1633
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L737, L765
#4031 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1628, L1633
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L737, L765
#4082 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp L3, L29
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp L87
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp L22, L703
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp L727
core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp L549-550, L552-553
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp L533-534, L538-539, L542-543
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1493, L1628, L1633
core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp L109, L114, L126, L134, L152, L166, L169, L196, L213, L227, L264, L328, L331
core/shader/recompiler/CacheKey.hpp L6, L108
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L1, L508, L511, L517, L720, L737, L765
core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp L114
core/shader/recompiler/Optimization/src/TessellationLowering.cpp L21, L486-487, L489
core/shader/recompiler/Recompiler.hpp L275
core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp L4, L181, L187
#4152 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1628, L1633
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L737, L765
#4163 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp L66, L95-100, L109, L114, L126, L134, L152, L166, L169, L196, L213, L227, L244, L247-250, L254, L261, L264, L326, L328, L331
core/shader/recompiler/Optimization/src/TessellationLowering.cpp L19, L21, L354, L385, L451-454, L486-487, L489, L494, L610
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp L478, L486, L494-495, L498, L501-504, L516, L519, L531-533, L539-547, L562, L567-569, L586, L588, L596, L603, L605, L1181, L1184, L1186, L1188, L1190
docs/dev/TechnicalDebt.md L580
#4172 core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp L66, L95-100, L109, L114, L126, L134, L152, L166, L169, L196, L213, L227, L244, L247-250, L254, L261, L264, L325-326, L328, L331
core/shader/recompiler/Optimization/src/TessellationLowering.cpp L19, L21, L354, L385, L451-454, L486-487, L489, L494, L610
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp L478, L486, L494-495, L498, L501-504, L516, L519, L531-533, L539-547, L562, L567-569, L586, L588, L596, L603, L605, L1181, L1184, L1186, L1188, L1190
docs/dev/TechnicalDebt.md L580
#4201 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp L3, L29
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp L87
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp L22, L703
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp L727
core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp L549-550, L552-553
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp L533-534, L538-539, L542-543, L550
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1493, L1628, L1633
core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp L66, L95-100, L109, L114, L126, L134, L152, L166, L169, L196, L213, L227, L244, L247-250, L254, L261, L264, L325-326, L328, L331
core/shader/recompiler/CacheKey.hpp L6, L108
core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp L1, L508, L511, L517, L720, L737, L765
core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp L114
core/shader/recompiler/Optimization/src/TessellationLowering.cpp L19, L21, L354, L385, L451-454, L486-487, L489, L494, L610
core/shader/recompiler/Recompiler.hpp L275
core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp L478, L486, L494-495, L498, L501-504, L516, L519, L531-533, L539-547, L562, L567-569, L586, L588, L596, L603, L605, L1181, L1184, L1186, L1188, L1190
core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp L4, L181, L187
docs/dev/TechnicalDebt.md L580

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants