Skip to content

fix(agc): keep the gate run going past indirect dispatches whose arguments stay on the GPU - #5747

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
fisherjoey:fix/gate-capture-gpu-indirect
Oct 15, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
fisherjoey:fix/gate-capture-gpu-indirect

Conversation

@fisherjoey

@fisherjoey fisherjoey commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

The dispatch look-ahead added in #1864 resolves a gated compute group at the gate, before the title can rewrite the tables the group reads. #2990 made the look-ahead stop before a DISPATCH_INDIRECT whose 12 argument bytes an earlier dispatch of the run may write (or when an earlier dispatch has unknown writes). The stop is needed when the driver reads the arguments on the CPU at resolve time. It also fires when the arguments stay on the GPU, where the CPU never reads them, so in that case it protects nothing.

With this change the look-ahead stops only for the CPU-read paths. Driver::indirectArgumentsPath() now holds the path choice that dispatchIndirect already made (thread dimensions, misaligned, APS5_NO_GPU_INDIRECT), and resolveGroupAhead (DispatchAhead.cpp) asks it before stopping. The dynamic CPU-read reasons (pending image results, workgroup memory, a pending label or copied writer over the argument dwords, no host import) are decided in VulkanDevice::decideIndirect. That runs only on the queue-order pass, after the look-ahead's parked result is adopted (Driver::dispatch returns before any device call while resolving), so it sees the earlier dispatches of the run already recorded. A new entry point, AgcDriverDispatchesAdoptedAhead_nid_postfix, counts the dispatches adopted from the look-ahead so a test can tell a gate-time capture from a late one.

Why

When the stop fires on a GPU-read indirect dispatch, that dispatch and everything behind it in the group are captured when the worker reaches them, after the GPU-mutex waits between dispatches. That is the late read from #1787: by then the title has re-armed the gate and started reusing its table slots, so the capture can read a descriptor the title has not written yet.

Demon's Souls (PPSA01341 v1.07) hits this while loading the tutorial level, on compute shader 0x248a05b00, with the same messages #1787 listed: buffer descriptor uses an unsupported type (ShaderResources.cpp) and runtime image descriptor is invalid. Every indirect dispatch in that title is GPU-read. The [groupcap] ... read an earlier dispatch's output counter was 0 on the #1864 build and has been 150 to 760 per 10 s on every build since 10-11.

@CnRJay, this narrows your stop from #2990; every path that reads the arguments on the CPU still stops. #2990's test agc_driver_indirect_arguments_written_ahead still passes.

#5058 (@Kikoarl) changes the same lines and adds two more CPU-read reasons: arguments not covered by a registered readable range, and pending image writes to flush. Whichever of the two lands second should put the new checks in indirectArgumentsPath(). The look-ahead then keeps stopping for them, because both places use the same function. I'll rebase this PR if #5058 goes in first.

Tested

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 under Silent stubs in their library's technical debt file
  • 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
  • Unblocks: PPSA01341 Demon's Souls (PPSA01341) #1283
  • GPUs tested: lavapipe (Mesa llvmpipe, LLVM 22.1.8), Linux, for the tests; NVIDIA GeForce RTX 5070 Ti, driver 595.91, Linux (Nobara, kernel 7.2), for the game run
  • AI-assisted: yes

@github-actions

github-actions Bot commented Oct 14, 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 5058 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp L30-33

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 1191 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 1747 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 4040 core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp
PR 4650 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 4891 core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp
PR 4943 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 5497 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 5609 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 5656 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 5867 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 6208 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026

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

Thanks for the ping. Narrowing the stop to the paths that read the arguments on the CPU is the right idea. On a GPU-read dispatch it protects nothing, and stopping there brings back the late capture from #1787. Demon's Souls going from 3 of 5 stops to a full 1500 s run with 0 read an earlier dispatch's output is good evidence.

One gap: indirectArgumentsPath() covers only the three static reasons (gpuIndirect, thread dimensions, misalignment). VulkanDevice on main has four more CPU-read reasons, decided while the dispatch is being recorded:

  • cpuReason = 1: StorageTexture::FlushPending over the 12 argument bytes;
  • cpuReason = 8: the program uses workgroup memory;
  • cpuReason = 2: a pending label or CopiedWriterOverlaps over the argument dwords;
  • cpuReason = 3: no host import holds the arguments.

In each of these, GuestMemory::Read(arguments, …) reads the counts. My question is when that runs for a dispatch the look-ahead adopted. If an adopted dispatch is recorded in queue order, after the earlier dispatches of its run, those reasons are evaluated with the earlier writes in place and the narrowing is safe. If adopting it at the gate also settles its recipe, including cpuReason, then a dispatch that indirectArgumentsPath() calls GPU-read can still read its arguments on the CPU at gate time, before the earlier dispatch that writes them has run. That is exactly the case #2990 stops for. Reason 2 is the likely one: a writer from the same run whose staged region isn't registered yet in copiedWriters when the look-ahead runs.

Either answer is fine. If it's the first, a sentence in the description saying where the dynamic reasons are decided would let a maintainer check it quickly. If it's the second, the look-ahead could also stop when the arguments are covered by a write of the run and no import holds them yet. #5058's two extra checks would then go into indirectArgumentsPath() as you suggest. A test variant where the earlier dispatch writes the arguments through a misaligned or staged binding would pin it either way.

@fisherjoey

Copy link
Copy Markdown
Contributor Author

@CnRJay It's the first case. The look-ahead never reaches the device. While resolving, Driver::dispatch captures and recompiles, parks the result in resolvedAhead() and returns (the if (resolving) block in Dispatch.cpp, before any VulkanDevice call). The parked ResolvedDispatch holds the compiled result, the captured regions, the cache variant and the unresolved packet with its argument address. It holds no recipe, no cpuReason and no group counts.

When the worker reaches the packet in queue order, it adopts those fields and only then calls PrepareDispatch, takes the GPU mutex, records the queued labels and calls DispatchIndirect or DispatchRecipe. Both go through decideIndirect, which decides reasons 1, 8, 2 and 3 and does the GuestMemory::Read of the counts. By then the earlier dispatches of the run are recorded and listed in copiedWriters. A Recipe carries no cpuReason either, so a recipe hit decides again. The existing test already takes your reason 2: with a trace in decideIndirect, its indirect dispatch records with cpuReason 2 and one copied writer while it is adopted from the gate, and it runs the 24 groups the writer stored.

I found one CPU read of indirect arguments in the Driver itself. The fill kernel path in Driver::dispatch read the counts while resolving and then threw them away (the partial-group bit that would have parked them is rejected for DISPATCH_INDIRECT in Pm4.cpp). d70fd22 returns before that read during the look-ahead, so nothing reads indirect arguments on the CPU at the gate now.

Two new tests pin it:

  • agc_driver_gpu_indirect_arguments_misaligned_writer_ahead: the arguments block is registered, so a host import holds it, and the earlier dispatch writes the arguments through a V# that is dword aligned but not aligned to minStorageBufferOffsetAlignment. The driver copies that region on the GPU and back, so the indirect dispatch stays GPU-read, and it has to run the written counts while still adopted at the gate.
  • agc_driver_indirect_fill_kernel_arguments_written_ahead: the same with an indirect fill kernel dispatch, the Driver-side CPU-read path.

Both pass on an RTX 5070 Ti and on llvmpipe, along with the ten gate and indirect tests from before. I also added a sentence to the description on where the dynamic reasons are decided.

AI-assisted: yes

@fisherjoey
fisherjoey force-pushed the fix/gate-capture-gpu-indirect branch from d70fd22 to c097f9a Compare October 14, 2026 18:24
@CnRJay

CnRJay commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Thanks, that settles it. The dynamic reasons are decided in decideIndirect when the worker records the adopted dispatch in queue order, and the fill-kernel read was the one CPU read at the gate. d70fd22 and the two new tests cover what I asked about. Nothing more from me.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked on the PR merged onto current main: libSceAgcDriver build + ctest green (only agc_driver_tessellation fails, it also fails on main without this PR). The first two tests fail without the change (they need AgcDriverDispatchesAdoptedAhead_nid_postfix) and pass with it.

One point:

  • IndirectFillKernelArgumentsWrittenAhead.cpp passes with the second commit's change removed. I replaced if (resolving) return; at Dispatch.cpp:63 with nothing, rebuilt, and agc_driver_indirect_fill_kernel_arguments_written_ahead still passed (128 records filled). So the test doesn't pin that behaviour. Please make the test reach the fill-kernel branch with the look-ahead active (for example, assert through AgcDriverDispatchesAdoptedAhead_nid_postfix that the fill kernel dispatch was not read at the gate, or change the setup so the argument read at the gate gives a different count) so that it fails without the line. If the line turns out to be unreachable in that setup, drop it from the PR.

Happy to look again once the test fails without that line.

…un writes its arguments

The dispatch look-ahead stopped before every DISPATCH_INDIRECT whose
arguments an earlier dispatch of the run may write, or whose earlier
dispatches have unknown writes. That stop protects a CPU read of the
arguments at the resolve, which only the thread-dimensions, misaligned and
APS5_NO_GPU_INDIRECT paths make. A GPU-read indirect dispatch never reads
its arguments on the CPU, so the stop left it and every dispatch behind it
to be captured when the worker reached them, after the GPU mutex waits
between dispatches. That is the late read the group capture exists to
avoid: by then the title had re-armed the gate and reused its table slots,
and the capture read descriptors the title had not written yet. The
look-ahead now stops only before an indirect dispatch whose arguments it
would read on the CPU; the path choice is shared with dispatchIndirect.

AgcDriverDispatchesAdoptedAhead_nid_postfix counts the dispatches adopted
from the look-ahead so a test can tell a gate-time capture from a late one.
Two tests check through it that both dispatches of a gated group are
captured at the gate: one whose writer stores the indirect arguments
through a registered binding, and one whose writer stores them through a
binding misaligned in its host import.
@fisherjoey
fisherjoey force-pushed the fix/gate-capture-gpu-indirect branch from 03f5443 to 6b14169 Compare October 15, 2026 12:52
@fisherjoey

Copy link
Copy Markdown
Contributor Author

You're right, the test couldn't pin that line. With the look-ahead active, dispatch does enter the fill-kernel branch, and without the line it reads the arguments at the gate (groups 1 1 1, before the writer ran). The return at Dispatch.cpp:76 then throws that read away and leaves the dispatch unresolved, so both builds end in the same state. The only case where a gate-time read would be kept is partialLastGroup, and Pm4.cpp:360 rejects PARTIAL_TG_EN on DISPATCH_INDIRECT (mask 0xa06c), so no indirect dispatch gets there.

So I dropped the line and IndirectFillKernelArgumentsWrittenAhead.cpp, and squashed the branch to one commit, 6b14169 (not rebased, still on 922abe6). agc_driver_gpu_indirect_*, agc_driver_indirect_*, agc_driver_gated_dispatch_capture*, agc_driver_write_data_written_ahead and agc_driver_prepared_shaders_indirect pass, 8/8 on an RTX 5070 Ti and 8/8 on lavapipe. The description no longer mentions the fill-kernel change.

AI-assisted: yes

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-checked on the PR merged onto current main: libSceAgcDriver build + ctest green (only agc_driver_tessellation fails, as on main). The fill-kernel commit is gone, which settles my earlier point.

With AgcDriverDispatchesAdoptedAhead_nid_postfix kept but the indirectArgumentsPath(...) != IndirectGpu condition removed from DispatchAhead.cpp:153, both agc_driver_gpu_indirect_arguments_written_ahead and agc_driver_gpu_indirect_arguments_misaligned_writer_ahead fail; with the change they pass. Merging once the full Build is green.

@adriadam10
adriadam10 merged commit ce998f9 into boykopovar:main Oct 15, 2026
9 checks passed
@Amoenus

Amoenus commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

@fisherjoey Independent Windows build finding after this merge: unchanged main 0c4816f6119e5dc1208b16b25a171d1c88aa18f4 cannot compile GpuIndirectArgumentsMisalignedWriterAhead.cpp:25 with the repository-required WinLibs GCC 15.2.0 r7 (Windows 11 Enterprise 26200, Release/bundled dependencies):

error: requested alignment '65536' exceeds object file maximum 8192

I reproduced it by building the fixture's object target from an otherwise clean main checkout. The file is also byte-identical on newer main 0029e62f9. The same error independently stopped our full integration build; this is a compiler limit, before GPU execution.

The four-line fixture-only correction is now proposed in #6452, fork commit 4fe92c9c2, based on main0029. Full Windows app/libs/checkers pass. Eight related groups pass five repetitions per NVIDIA/AMD selector; this fixture also passes twenty extra fresh processes per GPU, with verified device identity. It retains the 64 KiB registered block, four-byte argument offset, and original 24-group/two-adopted-dispatch assertions. The PR records the exact tests and the unchanged Low() Cppcheck advisory. No production-driver change or title result is claimed. AI-assisted: Codex.

@fisherjoey

fisherjoey commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

@Amoenus thanks for catching it. I only built and ran it on Linux (GCC 16), on an RTX 5070 Ti and lavapipe. #6452 looks right to me: a 128 KiB minus one buffer always contains a 64 KiB-aligned 64 KiB block, the new Require fails loudly if it doesn't, and the offset-4 arguments and the adopted-count assertions stay as they were.

AI-assisted: yes

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.

4 participants