Skip to content

fix(shader): count folded draw offsets once in the vertex and instance indices - #2285

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Celegans12:fix/fetch-offset-double-add
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Celegans12:fix/fetch-offset-double-add

Conversation

@Celegans12

@Celegans12 Celegans12 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

An embedded vertex fetch adds the base vertex or start instance user SGPR to the index VGPR (v_add_co_u32 v8, vcc, s19, v8), and the driver folds that SGPR into the Vulkan draw's first vertex and first instance so the replaced attribute fetch starts there. v5 and v8 were gl_VertexIndex and gl_InstanceIndex, which already hold that offset, so the translated add counted it twice: index + offset before the add, index + 2 × offset after it. This hits direct draws and indirect records drawn on the CPU path alike; PPSA26344 indexes a per-instance buffer with v8 + s19 at start instances up to 59,455.

  • A vertex or local program's entry now writes the index VGPRs as the builtin minus the folded SGPR's user data word. Programs without a folded SGPR are unchanged.
  • The fold moves into Driver::FoldDrawOffsets with the same rules. APS5_INDX_OFFSET_SKIP_FOLD and APS5_NO_INDEXED_OFFSET_FOLD go, as skipping the fold would now leave the indices short.
  • Hardware: the VertexID/InstanceID VGPRs are zero-based; the offsets reach the shader only through the user SGPRs the draw names, which the CP fills from the record for DRAW_INDIRECT and DRAW_INDEX_INDIRECT (Mesa radeonsi si_emit_draw_packets, RADV radv_cs_emit_indirect_draw_packet).
  • A record drawn in place keeps the CPU's SGPR word, so EmbeddedVertexFetchAnalyzer now counts the register as shared unless the fetch add is the only reader of the index VGPR and the SGPR (no earlier read, carry, clamp or second add); such records take the CPU path, where both values are exact.
  • No request or cache layout change.
  • perf(agc): pipeline indirect draws behind the packet worker (APS5_PIPELINE_INDIRECT) #1896 calls the same fold lambda, which keeps its signature.

Tested

  • Windows 11, MinGW-w64 GCC 15.2.0, based on main d70b899: configure -DBUILD_TESTING=ON, full build, --target libs, ctest:
  • 508/508 tests pass (ctest --output-on-failure)
  • Linux (WSL Ubuntu 24.04, GCC 13.3, Mesa llvmpipe, upstream's job configuration), merged with main d70b899: 517/517 pass
  • New agc_driver_draw_offset_indices: a vertex program stores v5/v8 before and after its fetch adds for direct draws, records the now public ClassifyIndirectDraw sends to the CPU path, one drawn in place and one whose packet names no register; it also checks which programs may stay in place. With main's translator it fails (at start instance 40, v8 reads 40, not 0, and 80 after the add; the 59,455 record gives 118,910), and without the analyzer change a record whose program copies v5/v8 first stays in place.
  • Not tested: console behaviour.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@MarcZX10

MarcZX10 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

One remaining semantics question from the full diff: DrawOffsetIndices passes checkRaw=false for the in-place GPU indirect case, so it verifies only the values after the embedded add. drawIndex subtracts the SGPR in the shader prologue for every draw using that embedded-fetch program, but this path keeps the indirect record in place and does not fold that SGPR into the Vulkan draw parameters. That makes the initial v5/v8 observably different from the previous translation; the new TechnicalDebt entry records this. Could you provide a source or hardware basis that guest code cannot rely on those pre-add values in this path, or preserve them and test both values? The current test establishes the post-add result only.

…e indices

When the embedded vertex fetch adds the base vertex or start instance
user SGPR to the index VGPR, the driver folds that SGPR into the draw's
first vertex and first instance. v5/v8 were gl_VertexIndex and
gl_InstanceIndex, which already include it, so the translated add
counted it twice, on direct draws and CPU-path indirect records alike.
The entry of a vertex or local program now subtracts the folded SGPR's
user data word from the builtin, which gives the zero-based indices the
hardware has. A record drawn in place on the GPU keeps the CPU's word,
so the fetch analyzer counts the SGPR as shared unless the fetch add is
the only reader of v5/v8 and the SGPR (no earlier access or EXEC write,
no carry, clamp, second add or other write of the SGPR); such records
take the CPU path. The fold moves into Driver::FoldDrawOffsets,
ClassifyIndirectDraw becomes public for the test, and the env switches
that skipped the fold go.
@Celegans12
Celegans12 force-pushed the fix/fetch-offset-double-add branch from 6fb6049 to fcc5105 Compare October 9, 2026 16:46
@Celegans12

Copy link
Copy Markdown
Contributor Author

You're right that the in-place case only proved the value after the add, so I went back to what the hardware puts in these registers. Neither the old pre-add values nor my first version's were the hardware's: on GFX10 the VertexID/InstanceID VGPRs are zero-based, and the base vertex and start instance reach the shader only through the user SGPRs the draw names, which the CP fills from the record for DRAW_INDIRECT and DRAW_INDEX_INDIRECT (radeonsi and RADV both rely on this, sources below). For the test's in-place record (vertex offset 700, start instance 59,455, stale user data 0x51/0x62) the hardware has v5/v8 = index/instance before the adds; main gives index + 700 / instance + 59,455 there, since gl_VertexIndex/gl_InstanceIndex already contain the record, and my first version gave index + 619 / instance + 59,357.

So I made everything a program can observe match the hardware instead. In an in-place draw the shader's s16 still holds the CPU's word rather than the record's, so the analyzer now keeps a record in place only when the fetch add is the only thing that sees v5/v8 and the SGPR. A program that touches v5/v8 or writes EXEC before the add, adds the SGPR into another VGPR, reads the add's carry, clamps it, adds the SGPR twice, writes the SGPR through a carry or lane mask, or addresses SGPRs relatively counts the register as shared, and its records take the CPU path, where both values are exact. Programs that pass have no pre-add value anything can read, and their post-add value is gl_VertexIndex itself, so the TechnicalDebt entry is gone. There's no request or cache layout change, and nothing moves in Yotei: all 28 of its programs that fold an SGPR also read or rewrite it elsewhere, so their records already took the CPU path.

The test now checks both values on every path that has a pre-add value, including the in-place-shaped record whose program copies v5/v8 (ClassifyIndirectDraw now sends it to the CPU path, and the test draws it the way that path does) and a GPU draw whose packet names no register. Does that answer it, or would you rather see the gl_BaseVertex route below?

Details

Sources (Mesa main ed458ffa):

Values per path (U = the CPU's word in s16, R = the record's offset, i = index; instance analogous; before / after the add):

path hardware main first version now
direct draw i / i+U i+U / i+2U exact exact
record on the CPU path (s16 patched to R) i / i+R i+R / i+2R exact exact
record in place, s16 named by the packet i / i+R i+R / i+R+U i+R−U / i+R CPU path if anything but the add can see v5, s16 or the carry; otherwise only i+R exists, and it's exact
record whose packet names no register i / i+U i+U / i+2U exact exact

(i includes GE_INDX_OFFSET wherever the driver already applied it.)

Why not gl_BaseVertex/gl_BaseInstance for the SGPR: that makes the in-place value exact too, but on draws with a GE_INDX_OFFSET firstVertex also carries that offset, so BaseVertex isn't the SGPR's value there. It needs a per-draw value or a shader variant per path, plus shaderDrawParameters and a request change. Sending the rare program that looks at the raw index to the CPU path costs nothing measurable here and keeps the layout as it is.

Test cases (NVIDIA RTX 3080 Laptop and Windows llvmpipe): eight programs check the analyzer (copies v5/v8 first, adds only, reads the instance add's carry, clamped v_sad_u32, s16 added twice, EXEC written first, s17 overwritten by another add's carry, s16 added into v0); direct auto and indexed draws; an indexed and an auto record of the copying program, classified to the CPU path and drawn with both values checked; an indexed record of the adds-only program, classified in place and checked after the add; and an indexed record whose packet names no register, drawn on the GPU with both values checked. With main's translator the test fails as before (start instance 40 gives v8 = 40 and 80); with the analyzer change reverted, the copying program's records are classified to the GPU. The full suite on fcc5105 passes: 508/508 on Windows, 517/517 in the Linux job merged with main d70b899.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux (build + ctest): agc_driver_draw_offset_indices, agc_driver_graphics, agc_driver_vertex_fetch, guest_indexed_draw pass. The new test fails with the InstructionTranslator.cpp change reverted and also with the EmbeddedVertexFetch.cpp analyzer change reverted. The Mesa vertex_id_zero_based claim checks out against the linked sources. Merging.

Heads-up: this has adjacency conflicts with #2313 (Driver.hpp) and #1896 (CMakeLists.txt, and it calls the fold lambda); whichever lands later needs a rebase.

@adriadam10
adriadam10 merged commit 189990d into boykopovar:main Oct 10, 2026
8 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…ouble-add

fix(shader): count folded draw offsets once in the vertex and instance indices
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants