Skip to content

feat(agc): capture submissions and replay their decode and wait releases offline - #4969

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
fdfawef324:feat/agc-pm4-capture-replay
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
fdfawef324:feat/agc-pm4-capture-replay

Conversation

@fdfawef324

@fdfawef324 fdfawef324 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

A hang with threads in Driver::waitMemory takes a ten-minute boot to reproduce, and the existing switches show only the first submissions of one queue (APS5_DUMP_QUEUE) or a rejected one (APS5_DUMP_REJECTED). This adds a capture of every submission and an offline tool that sorts the captured waits into three groups: those the capture proves released, those no captured store can release, and those it cannot decide.

Capture. APS5_DUMP_SUBMITS=<dir> writes every submission the driver accepts. <dir>/index.txt starts with aps5-submissions 1 and the column line ordinal queue type serial time_us dwords kind file held conditional, then one line per submission. held lists the dword offsets of the waits whose value passed their compare at submit (Submission::heldAtSubmit, which the driver then does not wait for). conditional lists the COND_EXEC and predicated indirect buffer ranges as start:end (from Submission::conditionalEnds). submission_<ordinal>.bin holds the copied dwords with indirect buffers inlined. REWIND tails and suspend points are listed too. SubmissionCapture::Prepare copies the commands before the driver lock. Under the lock, Take sets only the ordinal, the time and the held waits, after reserveOutputs, and does not allocate (it is noexcept). The file and then its index line are written after the lock is released. A failed write reports the error to the driver (Driver::ReportFailure) and throws: the call that made the submission throws after the submission was queued, and every later driver call throws the same error. When the switch is unset, Submit only tests a static pointer.

An index written by an earlier build, whose column line is ordinal queue type serial time_us dwords kind file, is still read. Its held waits and conditional ranges are unknown (CapturedSubmission::heldKnown, conditionalKnown), and the report says so.

Analysis. agc_pm4_replay <dir> [--full] decodes each submission and throws if the driver would reject a packet, since that means the capture is corrupt.

The tool does not model cross-queue timing. A store of another queue can run before, between or after the stores of the waiting queue, whatever order the submissions entered the driver in, so the tool never uses submit order across queues.

Each WAIT_REG_MEM, WAIT_REG_MEM_64 and REWIND gets one of three outcomes.

  • released, only by a route the capture proves on its own:
    • the compare always passes.
    • the wait was held at submit (recorded in the capture).
    • current value: the last store to the awaited bytes before the wait on its queue (in the same submission or an earlier one) covers them, is not conditional and leaves a value the compare accepts, and no other queue stores to any of those bytes anywhere in the capture.
    • label history: a RELEASE_MEM or WRITE_DATA label earlier in the waiting submission stores a value the compare accepts. The driver must keep that label (KeepsLabelStore: 4-byte aligned, at most 64 bytes; not conditional). Fewer than LabelStoreHistory (4) other stores to the awaited bytes may run between the label and the wait.
      • Why only the same submission: storedSince (LabelHistory.cpp:21) finds a store only if its stamp is above the waiting submission's received. The capture proves that only for a store in the same submission.
      • Why all queues count: noteLabelStore (LabelHistory.cpp:9-19) keeps one history per dword, shared by all queues. So the count includes the stores between the label and the wait on the waiting queue, plus every store of another queue to those bytes, whatever its submit order.
      • Stores later on the waiting queue run after the wait, so they are not counted.
    • a REWIND whose tail is in the capture (one that was valid at submit without a captured tail is unknown: the driver reads the valid bit again when the queue reaches the REWIND).
  • unreleased: the wait was not held at submit, and no store in the capture that can run before it may write a value the compare accepts.
    • A store of only part of the awaited bytes, or of a value known only at run time, counts as one that may.
    • A store later on the waiting queue runs after the wait and does not count.
    • The GPU does not release such a wait, but a CPU store could, and so could a shader store: the capture holds neither. The report says unreleased by the GPU; a CPU store could release it.
    • A REWIND without a tail that was not valid at submit is unreleased.
  • unknown, with the reason:
    • unknown: depends on cross-queue timing (stores on queue 0x20 at #377, #427, #477; ...) when another queue stores to the awaited bytes and no route above holds. For each queue, the list gives the 4 stores nearest the waiting submission in submit order and how many more there are.
    • the last store before the wait on its queue is conditional (COND_WRITE, inside a COND_EXEC range or a predicated packet), stores a value known only at run time, or stores only part of the awaited bytes.
    • the last store before the wait is rejected, and the capture does not prove that the label history still holds an earlier accepted store. That store is in an earlier submission, is not a label the history keeps, or has 4 or more stores after it.
    • the index has no held column and the wait would otherwise be unreleased, because its value may have passed the compare at submit. Without the conditional column, every packet after a COND_EXEC or predicated INDIRECT_BUFFER counts as conditional to the end of its submission.

The outcomes depend only on the order of each queue's own stores and on which stores the other queues make. They never depend on the submit order across queues.

This revision removes the fixed-point machinery of the previous one (two sets of waits grown and alternated, store run states). Each wait is now matched once, against the stores to its bytes.

The report counts waits per outcome and route, and the unknown waits that depend on cross-queue timing. It says when the index lacks the held and conditional columns. It lists up to 32 unreleased and then unknown waits, newest first, each with the packets around it and the stores to the awaited bytes. It ends with the last two submissions of each queue. The exit code is 1 when a wait is unreleased and 2 on errors.

Shared decode. The store and wait fields come from new Pm4 helpers: DecodeStores, DecodeWait, DecodeConditionalWrite, DecodeConstantRamDump, CopyDataSource/CopyDataImmediate/CopyDataBytes/CopyDataToRegister, DmaBytes and the DMA select constants, SampleDump/SampleDumpAddress and the sample-dump layout constants, and RewindValid. Pm4::Execute, ResolveStore, Validate, DecodeMemoryCopy, PacketMemory/PacketExecution (sample dump), VulkanDevice::DumpSamplesOnGpu, noteHeldAtSubmit and executeRewindTail now use them in place of their own copies. LabelStoreHistory and KeepsLabelStore moved to Driver/Synchronization/LabelStores.hpp, and noteLabelStore uses KeepsLabelStore. There is no EVENT_WRITE_EOP decoding: Validate rejects opcode 0x47.

Tests. agc_driver_pm4_replay builds command buffers with the libSceAgc builders and submits them through the driver with the switch set. It checks:

  • the index lines;
  • the held waits: a preset label is held, and a label the CPU stores after the submit is not;
  • the COND_EXEC range, the captured dwords, and the decoded packets and stores;
  • the outcome of each of the eight waits:
    • the wait inside the graphics DMA fill is released by current value (or held at submit);
    • WRITE_DATA 1, WRITE_DATA 0, WAIT == 1 in one submission is released by the label history. The driver releases that wait the same way and finishes the submission;
    • the CPU label is unreleased;
    • the wait after a COND_EXEC is unknown (conditional);
    • the REWIND is released by its tail;
    • the two waits on stores of another queue are held at submit or unknown by cross-queue timing, depending on how far the driver got before the submit.

It then checks hand-made captures:

  • every capture from the reviews, with its exact dwords: deadlock, stale, stale-held, evict3, evict4 and evict-own4. No wait may be released, except the compute wait of stale-held, which is held at submit. Every other wait must be unknown: depends on cross-queue timing (stores on queue ..., and the report must exit with 0.

  • each provable route:

    • compare always;
    • current value from the same submission, and from an earlier submission while another queue stores to an unrelated address;
    • the label history with 3 stores of another queue after the label, and with 3 stores of its own queue;
    • held at submit;
    • the rewind tail (driver capture).
  • the limits of those routes:

    • a store of another queue to the awaited bytes that entered after the store on the waiting queue, or before it: both unknown by cross-queue timing;
    • 4 stores after the label: unknown, not released;
    • a DMA_DATA fill the history does not keep, then a store of 0: unknown;
    • a store of 1 in an earlier submission overwritten by a store of 0: unknown, because the history holds the store of 1 only if it ran after the waiting submission entered the driver.
  • unreleased: a store later on the waiting queue, a value of another queue the compare rejects, and a REWIND without a tail, reported newest first.

  • permutations: five groups of waits, each on its own queues and addresses, written in eight submission orders: in sequence, reversed, round robin both ways and four pseudo-random interleavings. The groups are:

    • the stale pattern;
    • the deadlock;
    • current value, with two unreleased waits;
    • the label history, with a rejected store of another queue;
    • the label history with 3 and with 4 stores of another queue.

    All 13 waits must get the same verdict, route and cross-queue flag in every order.

  • an index without the held and conditional columns.

  • corrupt captures: EVENT_WRITE_EOP, a held offset that is no wait, and a range that starts at no COND_EXEC.

Last, a directory in place of the next submission file makes the capture write fail. The submit throws cannot write, the next submit throws the same error, the index gets no line for it, and shutdown throws the error too.

agc_driver_pm4_replay_tool (tests/Pm4ReplayTool.cmake) runs the tool on the driver capture. It checks:

  • exit code 1 and the counts;
  • the statement that cross-queue timing is not modelled;
  • the unreleased CPU wait listed first;
  • --full;
  • exit code 2 on a missing capture.

Tested

Windows Server 2025, RTX 3060, GCC 15.2 (winlibs), build-dev configuration, rebased on main at ea2bbef.

  • agc_driver_pm4_replay and agc_driver_pm4_replay_tool pass.
  • Red checks. Each is a one-line change to the new analysis that brings back a rule an earlier review found wrong, and each makes agc_driver_pm4_replay fail:
    • current value while other queues store to the bytes: the reviewer capture evict-own4 reports the wait at #0 +12 as 0 route 3;
    • label history counting only the waiting queue's stores: the same capture, route 4;
    • label history from an earlier submission: a store of 1 in an earlier submission, overwritten by a store of 0, does not leave the wait unknown.
  • The tool on the reviewers' own directories, as they are on disk:
    • deadlock, stale, evict3, evict4, evict-own4: released 0, unknown 2 (cross-queue timing 2), unreleased 0.
    • stale-held: released 1 (... held at submit 1 ...), unknown 1 (cross-queue timing 1).
    • Exit code 0 for all six.
  • ctest -R "^agc" -j 4, with every test executable built: 462 tests, 461 pass and agc_driver_stencil_ref_export skips. The tests listed as failing on main on this machine (agc_driver_flat_loads*, agc_driver_global_loads*, agc_driver_flat_apertures_2, _5, agc_driver_pixel_interlock) pass on this base.
  • The tool on copies of two PPSA01342 integration captures, byte-identical to the originals (diff -r). An earlier build wrote both without the held and conditional columns, so no wait can be unreleased: a wait that would be is unknown instead. I did not convert them, because writing - would claim that no wait was held.
  • python -I tools/check_test_registration.py build-dev and the conventions check pass.
  • Not run: a title with the switch set (no title was launched), Linux.

A final review found that a REWIND valid at submit was reported released although the driver reads its valid bit again when the queue reaches it; such a REWIND without a captured tail is now unknown, covered by a new case in agc_driver_pm4_replay (a REWIND that was valid at submit without its tail), which fails with the previous rule. agc_driver_pm4_replay and agc_driver_pm4_replay_tool pass on the final commit.

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: none
  • GPUs tested: NVIDIA GeForce RTX 3060 12 GB, driver 591.86 (32.0.15.9186), Vulkan 1.4.325, Windows Server 2025 10.0.26100
  • AI-assisted: yes

🤖 Generated with Claude Code

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Windows test (MinGW GCC 15.2, Release, Windows 10, RTX 4080 SUPER). Built on main 534451426 together with six other open GPU PRs (their own commits applied, no merge conflicts), so only the tests that belong to this PR say something about it:

  • tests/Pm4Replay.cpp, which ctest runs as agc_driver_pm4_replay: 3 of 3 runs passed.

Not tested: no game run. The batch was built once, not per PR; nothing failed, so no bisect was needed.

AI-assisted (Claude Code).

@github-actions

github-actions Bot commented Oct 13, 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
#5477 core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp L135-137, L152-154, L604-606, L989
#5539 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp L14

…ses offline

A hang with threads in Driver::waitMemory takes a ten-minute boot to
reproduce, and the existing switches show only the first submissions of
one queue (APS5_DUMP_QUEUE) or a rejected one (APS5_DUMP_REJECTED).

Capture. APS5_DUMP_SUBMITS=<dir> writes every submission the driver
accepts. <dir>/index.txt starts with "aps5-submissions 1" and "ordinal
queue type serial time_us dwords kind file held conditional", then one
line per submission: acceptance ordinal, queue id, graphics or compute,
serial, microseconds on the APS5_TRACE_GPU clock, dword count, kind,
file name, the dword offsets of the waits held at submit
(Submission::heldAtSubmit) and the COND_EXEC and predicated indirect
buffer ranges (Submission::conditionalEnds) as start:end, "-" for none.
<dir>/submission_<ordinal>.bin holds the copied dwords, indirect
buffers inlined. REWIND tails are listed as rewind with the serial of
their submission, suspend points as suspend without a file.
SubmissionCapture::Prepare copies the commands before the driver lock;
Take, under it after reserveOutputs, sets only the ordinal, time and
held waits and does not allocate; the file and then its index line are
written after the lock. A failed write reports the failure to the driver
(Driver::ReportFailure) and throws, so every later driver call throws
too. Unset, Submit tests a static pointer. An index written before the
held and conditional columns ("ordinal queue type serial time_us dwords
kind file") is read with both unknown.

Analysis. agc_pm4_replay <dir> [--full] decodes each submission and
throws on a packet the driver rejects (a corrupt capture). It does not
model cross-queue timing: a wait is released only when the capture
alone proves that the driver releases it (WaitMemory.cpp,
LabelHistory.cpp), and unknown, with the reason, when its outcome
depends on when another queue's stores run. A memory wait is released
when its compare always passes, when it was held at submit, when the
last store to the awaited bytes before it on its queue covers them with
an accepted value, is not conditional and no other queue stores to
those bytes anywhere in the capture, or when a RELEASE_MEM or
WRITE_DATA label earlier in the waiting submission that the label
history keeps (KeepsLabelStore, not conditional) stores an accepted
value and fewer than LabelStoreHistory other stores to the awaited
bytes can run between it and the wait: those between them on the
waiting queue and every store of another queue to those bytes, since
the history keeps the last stores per dword over all queues and finds
only a store that ran after the waiting submission entered the driver.
A REWIND is released when its tail is in the capture; one valid at
submit without a captured tail is unknown, because the driver reads the
valid bit again when the queue reaches it. A wait is unreleased when it was not held at submit and no
store that can run before it may write an accepted value (a store of
part of the awaited bytes or of a run-time value may; a store later on
its queue runs after it): the GPU does not release it, a CPU or shader
store could. A REWIND without a tail that was not valid at submit is unreleased. Every other wait is
unknown: "depends on cross-queue timing (stores on queue X at #...)"
when another queue stores to the awaited bytes, otherwise because the
last store before it on its queue is conditional, stores a run-time
value or part of the bytes, or overwrites an accepted store the capture
does not prove still in the label history. Without the held column a
wait that would be unreleased is unknown; without the conditional
column every packet after a COND_EXEC or predicated INDIRECT_BUFFER is
conditional to the end of its submission. The outcomes depend on the
order of each queue's stores and on which stores other queues make, not
on the submit order across queues. The report counts the outcomes,
routes and cross-queue unknowns, says when the index lacks the columns,
lists up to 32 unreleased and then unknown waits newest first with the
packets around them and their stores, then the last submissions of
each queue; exit code 1 when a wait is unreleased, 2 on errors.

Pm4 gains DecodeStores, DecodeWait, DecodeConditionalWrite,
DecodeConstantRamDump, the COPY_DATA and DMA_DATA field helpers, the
sample dump layout and RewindValid; Execute, ResolveStore, Validate,
DecodeMemoryCopy, the driver's sample dump, noteHeldAtSubmit and
executeRewindTail use them. LabelStoreHistory and KeepsLabelStore move
to LabelStores.hpp, used by noteLabelStore and the analysis.

Test. agc_driver_pm4_replay submits command buffers built with the
libSceAgc builders through the driver with the switch set and checks the
index, the held waits, the COND_EXEC range, the dwords, the decode and
the outcome of eight waits, including WRITE_DATA 1, WRITE_DATA 0,
WAIT == 1 in one submission, which the driver releases through its
label history. Hand-made captures check each release route, a store of
another queue to the awaited bytes that entered before or after
(unknown), the label history limit with later stores on the waiting
queue or on another queue, a fill the history does not keep, a store
in an earlier submission overwritten later, later stores, rejected
values and a missing rewind tail (unreleased), the six captures of the
reviews (none released but the wait held at submit), five groups of
waits in eight submission orders with identical outcomes, an index
without the held and conditional columns and corrupt captures. A
directory in place of the next submission file makes the write fail,
and the submit, the next submit and shutdown throw.
agc_driver_pm4_replay_tool runs the tool through
tests/Pm4ReplayTool.cmake and checks exit codes 1 and 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on the PR merged onto current main: scoped build and ctest of libSceAgcDriver (524 tests), including agc_driver_pm4_replay and agc_driver_pm4_replay_tool (pass); the shared Pm4 decode helpers keep Execute/ResolveStore/Validate behaviour; the capture does nothing but test a static pointer when APS5_DUMP_SUBMITS is unset. agc_driver_tessellation and agc_recorder_kept_bytes fail here, and both fail the same way on main without this PR. Windows is covered by the full Build before merging; merging once it is green.

@adriadam10
adriadam10 merged commit e9a985d into boykopovar:main Oct 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants