Skip to content

feat(agc): accept the dispatch and indirect packet forms vkQuake's RADV emits - #2756

Merged
adriadam10 merged 8 commits into
boykopovar:mainfrom
TheAggie:feat/agc-radv-vkquake-dispatch
Oct 11, 2026
Merged

adriadam10 merged 8 commits into
boykopovar:mainfrom
TheAggie:feat/agc-radv-vkquake-dispatch

Conversation

@TheAggie

Copy link
Copy Markdown
Contributor

What

Split out of #1917 at review's request. These six commits came from running vkQuake (PPSA99010, the homebrew Vulkan port) through RADV on the PS5 (mihawk-99/PS5_Mesa). They change how many threads a dispatch runs and how indirect draws and dispatches are decoded, so they get their own review here. #1917, with the ccleste packet forms, is merged.

  • Partial last thread group. For an unaligned direct dispatch RADV sets PARTIAL_TG_EN in the initiator and puts each axis's last-group size in the upper half of COMPUTE_NUM_THREAD_X/Y/Z. The dispatch now runs (groups - 1) * full + last threads per axis through the dispatch-size limit USE_THREAD_DIMENSIONS already uses, or plain groups when every last group is complete. A zero or oversized last group throws (Pm4::PartialGroupThreads). DecodeComputeStageInfo reads only NUM_THREAD_FULL (bits 15:0); it read the whole register, so a partial size would have widened the workgroup.
  • COPY_DATA from memory to context registers. Where it doesn't use LOAD_CONTEXT_REG_INDEX, RADV loads clear values into DB_STENCIL_CLEAR/DB_DEPTH_CLEAR and CB_COLORn_CLEAR_WORD0/1 with COPY_DATA to a register (DST_SEL 0, one or two dwords). It isn't a memory store, so the packet drains like other COPY_DATA the driver can't record on the GPU, and executing it reads the source then and writes the registers. Register, GDS, immediate and clock sources, and destinations outside the context registers, are still rejected.
  • COPY_DATA to memory through DST_SEL 5 (COPY_DATA_DST_MEM in Mesa's sid.h), the current form of the deprecated GRBM-synchronized DST_SEL 1 already accepted. RADV uses it for every memory destination; vkQuake's timestamp writes hit it.
  • In-order dispatches. RADV clears ORDER_MODE for a dispatch that needs in-order wave launch. That is the stricter guarantee and the driver doesn't reorder waves, so a direct or indirect dispatch now needs only COMPUTE_SHADER_EN.
  • Indirect draw register location 0. RADV writes 0 as the start-instance location of DRAW_INDIRECT and DRAW_INDEX_INDIRECT when the vertex shader doesn't read the base instance, and as the draw-index location of the multi forms when draw IDs are off. The validator accepted only 0x280 as "no register"; 0 is now accepted and decoded as 0x280.
  • 4-byte-aligned indirect argument bases. RADV sets the draw and dispatch indirect base with SET_BASE to the argument buffer address plus the command's offset, which Vulkan only requires to be a multiple of 4 (vkQuake's ends in 0x14). The validator required 8-byte alignment; the records are read as 32-bit words, so a 4-byte-aligned base is now accepted and a 2-byte-aligned one still rejected.

Tests (agc_driver_pm4, agc_driver_graphics): PartialGroupThreads for complete, partial, empty and invalid last groups; DecodeComputeStageInfo ignoring the partial half; COPY_DATA writing one and two context registers, not resolving as a store, and rejecting an immediate source, a non-context register, an overflowing range and a misaligned source; DST_SEL 5 with an immediate and a 64-bit timestamp; initiators 0x2047, 0x2007 and indirect 0x2005 accepted and 0x40 rejected. Each new check fails with its change reverted.

Tests for these two (agc_driver_pm4): location 0 in DRAW_INDIRECT and in the multi form resolves to 0x280, and an out-of-range location (0x200) is still rejected; a SET_BASE ending in 0x14 is accepted and one ending in 0x12 rejected. Each fails with its change reverted.

Tested

Windows 11 Pro (26200), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, NVIDIA GeForce RTX 5070 Ti.

  • Rebased onto main cf8b129. Full suite: 551 of 554 pass, 1 skipped. The two failures, agc_driver_prepared_shaders_debug_failure and guest_memory, fail the same way on clean main cf8b129 on this machine. It was run from the generated CTestTestfile.cmake commands because antivirus blocks ctest.exe on this machine.
  • tools/check_conventions.py passes.
  • vkQuake got through startup and played its demo with these commits (and feat(agc): accept the GFX10 packet forms RADV emits #1917's) in earlier testing, on a local branch that also carried my other RADV changes. It can't run on main alone yet: it stops at FAIL: unresolved ELF import 1nTKA7pN1jw from libSceLibcInternal.prx (utime, feat(libSceLibcInternal): implement guest utime #2024), and 13 more imports are missing after that. I have not rerun it since this rebase.
  • @johnmaia's earlier Linux run (RX 7600, RADV) covered the eight-commit head of feat(agc): accept the GFX10 packet forms RADV emits #1917, which included these: 494/494.

Not tested on AMD, Intel, Linux or macOS on this head.

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

🤖 Generated with Claude Code

@github-actions github-actions Bot added the agc libSceAgc* label Oct 10, 2026
@fma-rc

fma-rc commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Tested merged onto main 78aedb4 with a temporary test (Linux; NVIDIA RTX 5080 and RADV on a Radeon 610M, same results on both).

1. The fill fast path ignores PARTIAL_TG_EN. fillBuffer/copyBuffer return in Dispatch.cpp before PartialGroupThreads is called, and DecodePatternFillRange only knows initiator bit 0x20. With PatternFillKernel, 0x207..0x209 = {0x00100040, 0x00010001, 0x00010001}, DISPATCH_DIRECT {2, 1, 1, 0x43}, control count and records 256:

dwords filled
2 full groups, initiator 0x41 128
last group 16 of 64, initiator 0x43 128 ((groups - 1) * full + last gives 80)
last group size 0 (0x207 = 0x00000040), initiator 0x43 128, no throw

main rejects initiator 0x43, so this path is new with this PR.

2. PartialGroupThreads returns instead of throwing in two cases.

  • ({0x10002, 1, 1}, {0x0001ffff, 0x00010001, 0x00010001}) returns {0, 1, 1}: the 32-bit product wraps. Only reachable with absurd sizes, but it is a silent wrong limit.
  • ({1, 0, 1}, {0x00100040, 0x00010001, 0x00000001}) returns {0, 0, 0}, while ({1, 1, 1}, …) with the same registers throws: the early return on a zero group count skips validating the remaining axes.

3. Question: Validate now accepts initiator 0x63 (PARTIAL_TG_EN with USE_THREAD_DIMENSIONS). Dispatch.cpp then sets partialThreads from the packet words as thread counts and overwrites it treating the same words as group counts. Is that combination meant to be accepted?

The new tests in tests/Pm4.cpp call Validate and PartialGroupThreads directly; none of them drives a dispatch with bit 0x2, which is why 1 isn't caught.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Points 1 and 2 from fma-rc need fixing first: the fill/copy fast path in Dispatch.cpp returns before PartialGroupThreads, so initiator 0x43 fills whole groups and a zero last group doesn't throw; and PartialGroupThreads wraps its product and stops validating after a zero group count, where both should throw. For 3, either reject 0x63 in Validate or decode it once. A test that runs a dispatch with bit 0x2 through the driver would have caught 1.

@TheAggie

Copy link
Copy Markdown
Contributor Author

Thanks @fma-rc and @oneandonlydean. Fixed in 7c72f04 (a new commit on top, so the change is easy to review):

  1. Fill/copy fast path. Dispatch.cpp now decodes the partial sizes before fillBuffer/copyBuffer. When the last group is partial, it skips both fast paths and runs the shader with partialThreads set. A zero or oversized last group now throws there, before either fast path.
  2. PartialGroupThreads.
    • It validates all three axes before an empty dispatch returns, so ({1, 0, 1}, {0x00100040, 0x00010001, 0x00000001}) now throws.
    • It computes (groups - 1) * full + last in 64 bits and throws when that doesn't fit in 32, so ({0x10002, 1, 1}, {0x0001ffff, …}) throws instead of wrapping.
  3. Initiator 0x63. Validate now rejects PARTIAL_TG_EN together with USE_THREAD_DIMENSIONS.

Tests:

  • Driver test: agc_driver_pattern_buffer_fill now runs the fill kernel through the driver with 0x207..0x209 = {0x00100040, 0x00010001, 0x00010001} and DISPATCH_DIRECT {2, 1, 1, 0x43} (control count and records 256), in wave32 and wave64. It checks that exactly 80 dwords are written. With the Dispatch.cpp change reverted, it fails at index 80.
  • Unit tests: tests/Pm4.cpp gains the two throwing PartialGroupThreads cases and the 0x63 rejection.
  • Suite: agc|shader|recompiler|pm4|dispatch passes: 322 of 322 on Windows 11, RTX 5070 Ti, MinGW-w64 GCC 15.2.0.

AI-assisted (Claude Code).

@fma-rc

fma-rc commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Re-tested at 7c72f04 merged onto main 9206c3e (Linux; RTX 5080 and RADV on a Radeon 610M, same results on both). The three points are fixed here, and I have two new ones.

Fixed, by run through the driver (PatternFillKernel, 64-thread groups):

Dispatch dwords filled
2 groups, last 16 of 64, initiator 0x43 80
3 groups, last 1 of 64, 0x43 129
1 group, last 63 of 64, 0x43 63
2 groups, last group complete (0x00400040), 0x43 128
2 groups, last 16 of 64, 0x2007 80
last group size 0, or 65 of 64, 0x43 throws COMPUTE_NUM_THREAD partial group size is zero or exceeds the full group

agc_driver_pm4, agc_driver_pm4_failure, agc_driver_graphics and agc_driver_pattern_buffer_fill pass on both devices, and agc_driver_pattern_buffer_fill fails with Dispatch.cpp reverted to main.

1. The ResolveStore assertion does not test its guard. tests/Pm4.cpp:387 uses register 0xa00a. With || copyDataToRegister(packet) removed from Pm4.cpp:597 the test still passes, because 0xa00a % 4 != 0 and fits() already returns nullopt:

Pm4.cpp:597 register in the assertion agc_driver_pm4
as in the PR 0xa00a passes
guard removed 0xa00a passes
guard removed 0xa00c fails: COPY_DATA to a register resolved as a memory store
as in the PR 0xa00c passes

A register number that is a multiple of 4 makes the assertion cover the guard.

2. A dispatch with a start offset is run from 0, and the partial arithmetic now applies to it. With COMPUTE_START_X (0x204) = 1 and FORCE_START_AT_000 clear:

Dispatch dwords filled
dim 2, initiator 0x41 [0,128)
dim 2, 0x45 (FORCE_START_AT_000 set) [0,128)
dim 3, last 16 of 64, 0x43 [0,144)

RADV emits this form when a dispatch has a base offset, and then the packet words are end values, not counts (radv_cmd_buffer.c:15212):

if (params->start[0] || params->start[1] || params->start[2]) {
   /* The blocks in the packet are not counts but end values. */
   for (unsigned i = 0; i < 3; ++i)
      params->dim[i] += params->start[i];
} else {
   params->dispatch_initiator |= S_00B800_FORCE_START_AT_000(1);
}

Read that way, the first row is group 1 only and the third is groups 1 and 2 with a 16-thread last group; Dispatch.cpp:75 passes the packet words to PartialGroupThreads as counts. Accepting 0x41 with a start offset is probably older than this PR (I did not run the probe on main); the 0x43 row is new with it. Throwing when bit 0x4 is clear and a start register is non-zero would keep it from running wrong.

Not checked: vkQuake itself, the copy kernel with a partial last group, partial last groups on Y and Z through a shader, COPY_DATA to a register through a full submission.

@Amoenus

Amoenus commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Independent local validation of 7c72f04

Tested your current head 7c72f0487cf838feabb180c8fedd4357fe008d81 as seven cherry-picked commits onto main 43d2a7c4c00dd532076af30d2b75243ac10ad6c7. Windows 11 Enterprise 26200, RTX 5080, WinLibs r7 GCC 15.2.0, Release, BUILD_TESTING=ON. Full system libraries built. agc_driver_pm4, agc_driver_pm4_failure, agc_driver_graphics and agc_driver_pattern_buffer_fill all passed five repetitions, no skips, each below 30 seconds. Full suite, Linux and console hardware were not tested.

I reproduced the two points in fma-rc's latest comment independently:

  • A driver-level PatternFillKernel probe with COMPUTE_START_X=1 writes [0,128) for both initiators 0x41 and 0x45, and [0,144) for both 0x43 and 0x47 with dim X=3, full=64/last=16. The bit-clear cases still run from zero; I am not claiming this was introduced by this PR.
  • Changed only the ResolveStore assertion's register from 0xa00a to 0xa00c. Removing the copyDataToRegister guard then fails agc_driver_pm4 with COPY_DATA to a register resolved as a memory store. This confirms the alignment issue with the original assertion.

For local diagnostic use I added an explicit refusal of known nonzero COMPUTE_START_X/Y/Z when FORCE_START_AT_000 is clear, before fast paths/preparation. Separate driver cases cover X/Y/Z for direct, partial and indirect dispatches, verifying refusal and unchanged output. FORCE_START_AT_000 controls execute all three axes and verify the written/untouched range. The 12 new groups plus the four above passed five repetitions. Removing the start guard makes its direct-X case fail. Both mutation sources were restored byte-for-byte, full libraries rebuilt, and all 16 groups passed five repetitions again. The guard and stronger COPY_DATA assertion are separate unpublished local topic commits (6b8f9a4d, 9e7fb76f), not new PRs.

PPSA99902 (Sonic Unleashed Recompiled) also reaches your exact dispatch form on a conditional local startup stack: captured DISPATCH_DIRECT {0x50,1,1,0x2047}, with COMPUTE_START_X/Y/Z all zero and NUM_THREAD_X/Y/Z {0x00400040,0x00010001,0x00010001}. The last groups are complete and FORCE_START_AT_000 is set. Five prior-stack runs refuse that initiator. Title comparison is now complete on the same pinned main 43d2a7c4: before stack d25142c, proposal/adapters stack 60d016a7. Profiles differ only in libSceAgcDriver.prx. Five controlled strict-filter runs pass this initiator and stop later at DescriptorBindingBuilder::Populate multisampled image resources have no descriptor image shape (29.048, 29.090, 29.520, 28.871, 28.632s), with the BDA write-back warning more than 4096 pages stored to through the BDA table in one use are not implemented beforehand in all five. Before/after restored controls reproduce the 0x2047 refusal (28.566/28.805s). No run timed out; baseline libraries/guest libc/original executable were restored/hash checked. The refreshed integrated selection passed 46 groups five times, no skips, maximum 4.21 s. An initial full-profile pilot also changed two dependent wrapper binaries and was excluded from the single-driver comparison. Default filtering still misses pathconf. The actual multisampled resource and BDA page count remain under investigation. This is not a gameplay or console-semantics claim.

One source question remains outside these tests: the PR accepts ORDER_MODE=0 because the driver does not reorder waves. AMD's register reference describes ordered wave launch, while Vulkan shader execution leaves relative invocation execution order undefined. These are different guarantees; the tests above do not establish that the host honors ordered wave launch. Is there a source or measurement establishing that this bit can be ignored without changing observable behavior? This title's captured 0x2047 has ORDER_MODE=1.

Logs, captured private title data, exact patches and hashes retained outside the repository. No branch push or new PR. AI-assisted validation/local changes: Codex.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

The ResolveStore assertion at tests/Pm4.cpp:387 uses register 0xa00a, which fits() already rejects, so it passes with the copyDataToRegister guard removed; please use 0xa00c as fma-rc showed. The nonzero COMPUTE_START_* case is older than this PR and can go in a separate one.

TheAggie and others added 8 commits October 11, 2026 05:05
RADV sets PARTIAL_TG_EN in the dispatch initiator for an unaligned direct
dispatch, with each axis's last-group size in the upper half of
COMPUTE_NUM_THREAD_X/Y/Z, and TUNNEL_ENABLE on every GFX10 dispatch. The
validator rejected PARTIAL_TG_EN, and DecodeComputeStageInfo read the
whole register, so a partial size would have widened the workgroup.

A direct dispatch with PARTIAL_TG_EN now runs (groups - 1) * full + last
threads on each axis through the dispatch-size limit USE_THREAD_DIMENSIONS
already uses, or plain groups when every last group is complete; a zero
or oversized last group throws. The workgroup size is NUM_THREAD_FULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where the GPU has no LOAD_CONTEXT_REG_INDEX, RADV loads an image's clear
values into DB_STENCIL_CLEAR/DB_DEPTH_CLEAR and CB_COLORn_CLEAR_WORD0/1
with COPY_DATA from memory to a register (DST_SEL 0), one or two dwords.
The validator rejected every register destination.

A COPY_DATA from memory to one or two context registers is now accepted.
It is not a memory store, so the packet drains like other COPY_DATA the
driver cannot record on the GPU, and executing it reads the source then and
writes the registers. Register, GDS, immediate and clock sources, and
destinations outside the context registers, are still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RADV writes every COPY_DATA memory destination with DST_SEL 5
(COPY_DATA_DST_MEM in Mesa's sid.h), the current form of the deprecated
GRBM-synchronized DST_SEL 1 that was already accepted. vkQuake's timestamp
writes use it. It is now validated, resolved and executed as memory like 1
and 2; GDS and performance-counter destinations are still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RADV clears ORDER_MODE in the dispatch initiator for a dispatch that
needs its waves launched in order, and the validator required it set.
In-order launch is the stricter guarantee and the driver does not reorder
waves either way, so a direct or indirect dispatch now needs only
COMPUTE_SHADER_EN; vkQuake's map load issues one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RADV emits 0 as the start-instance location of DRAW_INDIRECT and
DRAW_INDEX_INDIRECT when the vertex shader does not read the base instance,
and as the draw-index location of the multi forms when draw IDs are off.
The validator accepted only 0x280 as "no register". Location 0 is now
accepted and decoded as 0x280; vkQuake's draws use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RADV sets the draw and dispatch indirect base with SET_BASE to the
argument buffer address plus the command's offset, which Vulkan requires
only to be a multiple of 4; vkQuake's base ends in 0x14. The validator
required 8-byte alignment. The records are read as 32-bit words, so a
4-byte-aligned base is now accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…date every axis

The buffer fill and copy fast paths returned before the partial last group
was applied, so a PARTIAL_TG_EN dispatch of the fill kernel filled whole
groups, and a zero last group did not throw. The partial sizes are now
decoded before the fast paths, which are skipped when the last group is
partial; the dispatch then runs the shader with the partial thread counts.

PartialGroupThreads validates all three axes before an empty dispatch
returns, and throws when (groups - 1) * full + last does not fit in 32 bits
instead of wrapping. Validate rejects PARTIAL_TG_EN combined with
USE_THREAD_DIMENSIONS (initiator 0x63).

agc_driver_pattern_buffer_fill now runs the fill kernel with a 16-of-64 last
group through the driver and checks that 80 dwords are written.

AI-assisted (Claude Code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0xa00a is not 4-byte aligned, so ResolveStore rejected it before the register guard.
@adriadam10
adriadam10 force-pushed the feat/agc-radv-vkquake-dispatch branch from 7c72f04 to a585ec1 Compare October 11, 2026 07:15
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked at 7c72f04 merged onto main: build and the libSceAgcDriver ctest (354 tests) are green, and the new agc_driver_pm4 checks fail without their change. ORDER_MODE matches Mesa (radv_device.c sets it to allow out-of-order launch, radv_cmd_buffer.c clears it for ordered dispatches).

Added one commit on top for @oneandonlydean's last point: the ResolveStore assertion in tests/Pm4.cpp now uses 0xa00c, so it fails if the copyDataToRegister guard is removed (verified). The nonzero COMPUTE_START_* case stays out of this PR.

Merging once CI is green.

@adriadam10
adriadam10 merged commit ccb299c into boykopovar:main Oct 11, 2026
14 checks passed
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