Skip to content

fix(agc): run a wait-free submission of another queue after queue 0's earlier submissions - #465

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
oneandonlydean:fix/agc-cross-queue-submission-order
Oct 4, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
oneandonlydean:fix/agc-cross-queue-submission-order

Conversation

@oneandonlydean

Copy link
Copy Markdown
Collaborator

What

A submission with no waits on a compute queue now starts only after queue 0 has run every submission it received before it.

The problem in PPSA21564's intro video

  • The title submits the queue 0 work of each video frame F first.
  • 1 to 5 ms later it submits one 12 MiB DMA_DATA on queue 0x3a. That copy refills the video texture frame F sampled, for frame F + 2; the two textures are used in turn.
  • The draw that samples the texture is the first packet of the frame's first queue 0 submission.
  • No label, wait or flip lies between the copy and that draw. The title relies on submission order and time alone.

On a GPU that keeps up, each submission starts as it arrives. The draw reads its texture right after the frame is submitted, and the copy submitted after it lands later, every frame, so the video plays with a steady two-frame latency.

Here each queue's worker records on its own. The copy was recorded before or after the draw depending on how far queue 0's worker lagged behind the title. The video showed either the newest frame or the one before, and every switch between the two showed a frame backwards. That happened 24 to 36 times per 45 s of intro, counting every frame, and more often the faster the emulator runs.

The rule

  • A submission on a queue other than 0 is wait-free when it has no WAIT_REG_MEM/WAIT_REG_MEM_64, no flip, no rendering wait and no rewind.
  • Such a submission is held until queue 0 has finished every non-suspend submission received before it.
  • The held worker sleeps on the driver's condition variable. Queue 0's completions and blocking waits notify it, and it re-checks every millisecond.

Why this is the right model, and where it stops

The hardware gives no formal ordering between queues. Two queues run concurrently, and nothing but timing places queue 0x3a's copy after queue 0's draw. This PR does not claim an ordering the hardware guarantees. It reproduces the one a title can rely on in practice: the GPU drains each submission promptly, so work submitted earlier on queue 0 has started, and here finished, before an unrelated submission made later on another queue runs. A wait-free submission is the case where that timing is the only ordering; a submission with waits is already ordered by its waits and is never held.

The emulator's queue 0 can lag behind the title by much more than a GPU would. The hold makes the other queue wait for the work queue 0 would already have done. It stops where a GPU that keeps up would itself be stuck: while queue 0 is blocked in a wait, the held submission is released

  • at once when its queue stores the awaited label (the held submission or a later one on that queue), since a GPU would run it while queue 0 waits;
  • when nothing else could store it: no other queue worker is running, and no recorded GPU work or completion writes it. A GPU that keeps up would be blocked at that wait as well, so it would run the other queue's submission meanwhile. This also covers a label the CPU stores after it sees the held submission's results, or one stored by a shader or a copy rather than a label packet.

So the order holds only while queue 0 still has work a faster GPU would already have done. No wait, on the GPU or on the CPU, can deadlock on it. The rewind tail's wait on its control word counts as a blocking wait too.

Costs

  • A held submission waits at most until queue 0 has caught up with the point where it was submitted.
  • Wait-free compute submissions no longer record in parallel with queue 0's earlier work. In PPSA21564 only the video copies qualify: its other compute queues start with waits.

Tested

  • Linux (CachyOS, x86-64): full build with BUILD_TESTING=ON and ctest pass (145/145).

  • Windows (MinGW, CI on my fork): build and ctest pass.

  • New tests in agc_driver (tests/Submit.cpp):

    • testWaitFreeSubmissionAfterEarlierQueue0Work: queue 0 gets 20000 WRITE_DATA packets, then queue 0x20 gets one wait-free WRITE_DATA; when the second lands, the first must be complete. Without the hold it fails with "a wait-free submission ran before queue 0's earlier submission finished".
    • testWaitFreeSubmissionQueue0WaitsOn: queue 0 waits on a label that the held submission stores; it passes the wait at once.
    • testWaitFreeSubmissionBehindHeldOne: the label comes from a second submission of the held queue, behind the held one; queue 0 passes at once, and the held one ran first.
    • testWaitFreeSubmissionTheCpuWaitsFor: queue 0 waits on a flag the CPU sets only after the held submission has run; queue 0 passes well inside the 1 s wait timeout. With only the first release rule (the queue stores the awaited label) it fails with "a held submission the CPU waits for was not released while queue 0 waited on the CPU".
  • PPSA21564: dense checks of the intro video (45 s, every frame kept) on my integration branch:

    • 24 to 36 frames shown backwards per run before the change;
    • 0 in each of four runs with it, video at 57 to 60 fps;
    • two runs of my visual regression check pass, intro FPS unchanged.

    Those runs used the hold with the first release rule only (the held submission stores the awaited label or copies into it). The second rule, for anything else nothing could store, was added for this PR, after reviewing the deadlock cases above. With both rules, as in this branch, three more runs keeping every frame of the intro video had 0, 0 and 0 backwards steps and no torn or stale frames, two runs of my visual regression check passed, and the guest never hit the 1 s WAIT_REG_MEM give-up in a gate, a level or the world map.

  • Overlap: my fix(agc): record large memory-to-memory DMA_DATA copies on the GPU in queue order #439 records large DMA_DATA copies on the GPU in queue order. It touches the copy itself, not when a submission starts, so the two are independent and merge cleanly. Without fix(agc): record large memory-to-memory DMA_DATA copies on the GPU in queue order #439 the copy runs on the CPU at the 0x3a worker's turn, and the race is the same. No open PR touches the queue workers' start order. feat(agc): host import windows, least-recent eviction under the budget #290, perf(agc): draw cache keys on the user words a program reads, memoizes failures, serves data-only hits #291, perf(agc): memoize source failures of draw stages as for compute #422 and GTA V: the prologue plays (testing branch of the PRs it needs) #426 edit Driver.hpp or Submission.hpp and merge cleanly.

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

… earlier submissions

PPSA21564 submits the queue 0 work of each intro video frame F first, then 1
to 5 ms later a 12 MiB DMA_DATA on queue 0x3a that refills the video texture
F sampled, for frame F + 2 (two textures, used in turn). Nothing orders them
but time: no label, wait or flip lies between the copy and the draw, which
is the first packet of the frame's first submission. A GPU that keeps up
starts each submission as it arrives, so the draw reads its texture right
after the frame is submitted, and the copy submitted after it lands later,
every frame.

Here the queue workers record independently, so the copy was recorded either
before or after the draw depending on how far queue 0's worker lagged: the
video showed the new frame or the one before, and every switch between the
two was a frame shown backwards.

A submission with no waits on another queue now starts only after queue 0
has run every submission it received earlier, as on a GPU that keeps up.
While queue 0 is blocked in a wait, the held submission is released when its
queue stores the awaited label (it or a later submission of that queue), or
when nothing else could store it: no other worker is running and no recorded
GPU work or completion writes it. A GPU that keeps up would be blocked at
that wait too and run the other queue's submission meanwhile, so the order
holds only while queue 0 still has work it would already have done, and no
wait can deadlock on it.
@github-actions github-actions Bot added the agc libSceAgc* label Oct 3, 2026
@oneandonlydean
oneandonlydean merged commit dca710d into boykopovar:main Oct 4, 2026
3 checks passed
@oneandonlydean
oneandonlydean deleted the fix/agc-cross-queue-submission-order branch October 4, 2026 02:45
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.

1 participant