Skip to content

fix(agc): keep queue 0's worker running when its wake reason is reaped - #12225

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
dantdmdarealguy:fix/agc-queue0-worker-keeps-running
Nov 2, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
dantdmdarealguy:fix/agc-queue0-worker-keeps-running

Conversation

@dantdmdarealguy

@dantdmdarealguy dantdmdarealguy commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

agc_submit_progress fails on CI with submission lost its real GPU completion store in builds of unrelated pull requests. Example: Linux job 122561999606 (2026-10-28 00:46Z, main 58dfa66 + #11507, a cube-descriptor change), failed after 2.14 s. The test normally takes about 0.2 s.

This isn't a slow GPU. In a reproduction the label never arrives, even after 12 s. A stack dump taken while the test waits shows no queue worker thread left in the process. Queue 0's worker had ended its own thread, and every later submission to queue 0 stayed queued.

Cause. In Driver::run (QueueWorker.cpp), queue 0's idle wait ends when there is work, a failure or a stop. It also ends when idleEpoch changes or completions are pending; #7134 added that last condition. After the wait, the worker checked those two reasons again:

changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch... != idleEpochAtWait || completionsPending())); });
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
if (id == 0 && !ready() && (idleEpoch... != idleEpochAtWait || completionsPending())) continue;
break;

completionsPending() reads the recorder's completion counters, which the driver mutex does not protect. A submitter's opportunistic reap (SubmitRecorded(true)) or WaitIdle can reap the batch between the wake and the second check. The worker then breaks out of the wait loop with no work. A few lines later, if (stopping || ... || pending.empty()) break; ends the thread. enqueue only starts a worker when worker.thread is not joinable, and an ended but unjoined thread still is, so no worker runs queue 0 again.

Before #7134 the only other wake reason was idleEpoch changing. That counter only grows, so the second check could not disagree with the first.

Change. After the wait, the worker goes back to the loop condition (continue). while (!ready()) leaves only when there is work, a failure or a stop. Otherwise the worker reaps or waits again.

Measurements (agc_driver_submit_progress_tests as ctest runs it: APS5_SUBMIT_PROGRESS=1, ANYPS5_REQUIRE_VULKAN=1). Arch Linux in WSL2 on the machine below, lavapipe (the CI runner uses llvmpipe), with 16 copies at once next to 32 busy-loop processes:

failed
main 3/4000
this change 0/4000

A diagnostic build (diff) prints when queue 0's worker leaves its loop:

  • Unchanged loop: 33 hangs in 6000 runs. In every one, worker 0x0 left its loop stopping=0 pending=0 came before the test's 2 s deadline.
  • With this change: 0/6000.

Targeted case: the same diagnostic build releases the mutex and sleeps between the wake and the second check, which widens the window. 4 copies at once, no load:

window main this change
20 µs 3/100 0/100
100 µs 3/100 0/100
400 µs 66/100 0/100

No test in the repository. The window is a few instructions between two reads made under the same mutex, so it is only hit when the worker is preempted right there. Pinning the test to one or two CPUs doesn't trigger it (0/200 on main either way). I found no way to force it from a test without adding a hook to the driver. The diagnostic diff (queue0-worker-exit-diagnostics.diff) and the stress script (stress.sh) are attached.

Tested

Windows 11 Pro, Intel Core i9-14900K, 128 GB RAM, NVIDIA GeForce RTX 5080 (driver 32.0.16.1742), WinLibs GCC 15.2.0, Release, BUILD_TESTING=ON, main 3850cc8. Linux: Arch Linux in WSL2 on the same machine, GCC, Release, lavapipe.

  • cmake --build build and cmake --build build --target libs: both succeed, with no warnings in QueueWorker.cpp.
  • ctest -R "agc_submit_progress|idle_results|same_queue_label_wait|pipelined_indirect_draw|patched_libraries": 8/8 passed. patched_libraries: 368 patched libraries loaded, 0 failed.
  • ctest -R "^agc_|^guest_agc": 1103/1104 passed. The one failure is agc_driver_mesh_barycentric_emulation, which fails on main 3850cc8 on this GPU too (since feat(recompiler): read I/J from barycentric built-ins on MoltenVK #9193; the fix is open as test(agc): disable barycentric built-ins in the mesh barycentric emulation test #12062).
  • Validation: the same 7 AGC tests on main and on this change, with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_NO_SHADER_CACHE=1, ANYPS5_REQUIRE_VULKAN=1 and ctest -V:
    • 7/7 pass on both.
    • Neither log contains was not found but was requested by env var VK_INSTANCE_LAYERS. VK_LOADER_DEBUG=layer shows Insert instance layer "VK_LAYER_KHRONOS_validation".
    • No VUID- in either log.
  • Linux stress numbers: above.
  • Windows, unmodified main, 1500 runs with 8 at once: 0 failures. I couldn't reproduce it on Windows on this machine.
  • python tools/check_conventions.py --base origin/main: passed.
  • python tools/check_test_registration.py build: 1480 test executable(s), 0 built and not run as recorded, none new.

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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 5080, driver 32.0.16.1742, Windows 11 Pro; lavapipe (Mesa llvmpipe) in Arch Linux WSL2
  • AI-assisted: yes, some AI assistance was used

Queue 0's idle wait also ends when completions are pending (boykopovar#7134).
After the wait it checked the reasons again and left the wait loop
unless one still held, assuming the submission queue was then
non-empty. completionsPending() is not under the driver mutex: a
submitter's opportunistic reap or WaitIdle can reap the batch between
the wake and that check. The worker then left the loop with nothing
pending, saw an empty queue, and ended its thread. enqueue starts a
worker only when the thread is not joinable, so every later
submission to queue 0 stayed queued and its labels were never written.

After the wait the worker now goes back to the loop condition, which
leaves the loop only when there is work, a failure or a stop.
dantdmdarealguy added a commit to dantdmdarealguy/AnyPS5 that referenced this pull request Oct 28, 2026
dantdmdarealguy added a commit to dantdmdarealguy/AnyPS5 that referenced this pull request Oct 28, 2026

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

The worker-loop change is correct: after the condition-variable wake, rechecking ready() at the loop condition handles the case where the non-mutex-protected completion reason is reaped before the second check. The worker remains alive and can process later queue-0 submissions instead of exiting with an unjoined thread.

The reported main-versus-change stress runs, targeted wake/reap-window runs, and same-machine Vulkan validation comparisons provide useful evidence for this narrow race. I independently reviewed the exact head, confirmed the current-main merge preview is clean, and ran git diff --check and python3 tools/check_conventions.py --base 68200c47ea5a5b205204e58269c6ed9cb6acfde6 successfully. The hosted Windows build is still pending; Linux and specialized builds were skipped by the workflow. I did not run the GPU stress tests locally.

@github-actions github-actions Bot added the agc libSceAgc* label Oct 28, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 28, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

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 11436 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp
PR 12567 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp
PR 13707 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp

@MarcZX10

Copy link
Copy Markdown
Contributor

I manually compared the shared QueueWorker.cpp changes flagged by the overlap check. #12225 changes the queue-0 wake loop to retry after a wake whose completion reason has already been reaped; #7978 adds flip-completion submission handling in deferCompletion/completeSubmission. They touch separate functions and hunks, and I found no direct behavior or landing-order dependency in the current patches. This comparison is limited to the source overlap; it does not validate #7978's stale-base integration or tests.

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head 10ecaba merged onto main 52c09d5 · tested 2026-10-29 09:47 UTC · 27 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,198 passed · 6 failed (0 new, 5 also fail on main, 1 unstable on main) · 39 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,243 of 2,873 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (855 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) none
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_submit_progress: failed in 2 of the last 4 main runs
Failing on main too (5, not counted against this PR)
agc_compute_pipeline_inspection
agc_submit_progress
agc_submit_progress_no_timeline
guest_mmap_executable
guest_socket_options_multicast
Passed on retry (1, failed in the parallel run, passed when re-run with a 360 s timeout; not counted against this PR)
agc_driver_job_manager_compute_capacity: Failed in the parallel run; also passed only on retry in 23 other runs in the last 7 days
Validation IDs already present on main (4 IDs in 405 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (405 tests): agc_compute_pipeline_replay, agc_driver, agc_driver_address_passes, agc_driver_address_passes_draw_transitions, agc_driver_address_passes_targets_split, agc_driver_alpha_to_coverage, agc_driver_array_layer_target_cpu, agc_driver_bc_block_copy, and 397 more
VUID-vkCmdCopyImage-srcImage-01548 (11 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_cpu_write_after_memo, agc_driver_depth_plane_cpu_write_after_page_unwatch, agc_driver_depth_plane_cpu_write_after_partial_unwatch, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_partial_store_unimported, agc_driver_depth_storage_dispatch, and 3 more
VUID-vkCmdCopyImage-srcImage-01551 (11 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_cpu_write_after_memo, agc_driver_depth_plane_cpu_write_after_page_unwatch, agc_driver_depth_plane_cpu_write_after_partial_unwatch, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_partial_store_unimported, agc_driver_depth_storage_dispatch, and 3 more
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (2 tests): agc_driver_prepared_exports, agc_driver_srgb8_color_target
Skipped (39)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_msaa_dcc_compared_import_2: skipped, the device reports host imports that keep write protection, so the write-watch probe compares none of them
agc_driver_msaa_dcc_compared_import_4: skipped, the device reports host imports that keep write protection, so the write-watch probe compares none of them
agc_driver_msaa_dcc_compared_import_8: skipped, the device reports host imports that keep write protection, so the write-watch probe compares none of them
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop_main: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_fresh: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_labels: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SD
Reproduce
git fetch origin 10ecaba5da731fc8d84511777b13202af143f4e0 52c09d567d2fa453494dd720fcf53a98149468e4
git checkout 52c09d567d2fa453494dd720fcf53a98149468e4 && git merge --no-ff 10ecaba5da731fc8d84511777b13202af143f4e0
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@cookie42073-cmd

Copy link
Copy Markdown
Contributor

Hey @dantdmdarealguy and @TechLevelZero, the newer #6416 report hits submission lost its real GPU completion store in all three modes: #6416 (comment) . The raw log gives 8.37 s for both explicit modes and 7.26 s for the default mode. Those are whole-test durations, not measured wait times; the logs don't identify the failed wait or worker state.

Could one failing Linux run capture the expected/observed label, phase (first submit, small submissions, REWIND, gate 77 or slow dispatch 78), actual wait duration, and worker backtrace or exit state? Keeping the exact build/environment and flushing timestamped stdout/stderr would help. Your existing worker-exit diagnostic may supply much of this; the normal two-second assertion should stay unchanged.

I tried 828ac0b2 + 8e6e7347 on Windows 11 Pro 26300, RTX 4070 SUPER (621.01 / 32.0.16.2101) and Intel UHD 770 (32.0.101.7088): all 34 bounded checks passed, covering three parallel repetitions of the three modes per GPU and one run of each observed neighboring-test group per GPU. Full Release build passed, Vulkan validation was loaded, and local logs had no VUID/SYNC errors. This approximates the co-runners, not the Linux scheduler, and doesn't clear the race.

The first two-second wait can overlap lazy driver-device creation, its float-controls probe and shader preparation. That is a source-supported alternative to distinguish from the stopping=0 pending=0 worker exit already demonstrated here, not a diagnosis from buffered log ordering. No deadline increase or competing queue-worker implementation proposed. AI-assisted investigation and independent review.

@oneandonlydean
oneandonlydean merged commit a9a448c into boykopovar:main Nov 2, 2026
12 of 13 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