Skip to content

fix(agc): stop and join the driver's thread pools at shutdown - #6641

Merged
oneandonlydean merged 12 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-shutdown-pool-threads
Oct 17, 2026
Merged

oneandonlydean merged 12 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-shutdown-pool-threads

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, a test process that uses the AGC driver can hang at exit, after its test has passed.

  • It shows in this repository's CI: Build 38742975960 on main failed only because agc_interpolant_mapping timed out on Windows (***Timeout 120.01 sec, 1 of 1,304 tests).
  • On GitHub's Windows runners on my fork, agc_interpolant_mapping hung in 11 and 15 of 60 rounds of eight runs, and agc_driver_readonly_resummarize hung too. Only ctest's timeout ends them.
  • Since main 82ddae7, guest_shader_link hangs at exit on GitHub's Windows runner too, within a few runs (run). Commits 7 and 8 fix its two causes.
  • No title is known to hang this way. A title's exit takes the same path, though: exit → LibcExit → LibcRunShutdown → Driver::stop, then ExitProcess.

Why it hangs.

  • ExitProcess terminates every other thread first, then runs each module's DLL_PROCESS_DETACH, where the static destructors run (as in Windows: guest _Exit hangs in FlipQueue's destructor because ExitProcess still runs module detach routines #2579).
  • Destroying a statically initialized condition variable that was never waited on takes winpthreads' one global spinlock for such variables (cond_locked). The same lock is held across a condition variable's first wait, while that wait creates its semaphores and critical sections.
  • A thread killed during a first wait leaves the lock held, and the next such destructor spins forever: ~DeviceUseGate in ~Driver, or one of libkernel's.
  • In the hung processes the spinning frame is pthread_spin_lock, called from pthread_cond_destroy, on that lock. Its word reads 0, meaning held, with only the main thread left.

Which threads. Both tests were run under gdb, stopped at RtlExitUserProcess, and every live thread was dumped. In 2 of the 6 hangs over 160 exits, the lock was already held at that point by a shader preparation worker that had just started:

  • one was inside RegistrationPreparePool::Main's first ready.wait;
  • the other was in PerformanceTrace::Configured's static-initialization guard, which waits on libstdc++'s guard condition variable.

Starting preparation workers were alive at 27 of the 160 exits. They came from the shutdown itself: CloseRegistrationPreparation took RegistrationPreparePool::Instance(), which creates the pool, with half the hardware threads and at least two, when nothing had been queued. Close then returned at once, and main returned while the workers were still starting.

guest_shader_link: two more causes. That test constructs the driver in its first sceAgcLinkShaders, a few milliseconds before it exits.

  • On main, the hung process's only thread waits in pthread_mutex_lock on the async log's lock, inside Driver::stop → FlushAsyncLog_nid_no_patch → AsyncLog::Flush. The driver's static is destroyed from its DLL's atexit table after ExitProcess has ended the log's worker, and Flush takes the log's lock and waits for every queued line. fix(agc): bound the async log's exit flush #6800's bounded flush runs too late there: the log registers it while the driver is being constructed, so it runs after the driver's destructor.
  • With that fixed, the same loop hung in ~Driver's destructor of a condition variable, spinning on winpthreads' lock as above. Stopped at RtlExitUserProcess 60 times, the process always had one thread of ours alive, the async log's worker, and in 38 of the 60 exits it was inside its first wait's setup of a condition variable (pthread_cond_init under pthread_cond_timedwait), which holds that lock.

The fix, in eight commits (the branch also merges main 82ddae7, for #6800's bounded flush):

  1. fix(agc): shutdown closes only the preparation pools that exist, as it already did for the prebuild pool. A pool first used after shutdown starts no workers, so later preparations run on the caller, as they did on a closed pool.
  2. fix(agc): the preparation workers are joinable. They leave once their pool is closed and idle, and Close joins them, so after shutdown no preparation worker is running or starting.
  3. fix(agc): the two other detached driver threads are stopped and joined at shutdown, the pipeline library's job thread before the device is reset and the parallel compare helpers after it:
    • the parallel compare helpers: a compare in progress finishes first, and later compares run on the caller;
    • the pipeline library's job thread: it finishes the job it is running, the jobs still queued are dropped and counted off their device, and later pipelines aren't optimized in the background.
  4. fix(agc): the pipeline library's job thread is stopped before the device is reset, not after (from review). Stopping it after the reset was already safe, since the device's teardown waits for the device's library jobs before it destroys the pipelines and the device, but now no library job runs once that teardown starts. The compare helpers stay after the reset: they compare guest memory only.
  5. fix(agc): the draw pipeline's committer (pipelined draws, on by default) is stopped and joined right after the queue workers, before the GPU lock is taken. It runs the commits still queued, so a deferred completion no longer runs after the device is reset, and a commit enqueued after shutdown runs on the caller.
  6. fix(agc): the shader disk cache writer is stopped and joined after the compare helpers. Entries still queued at exit are now written instead of lost, a store after shutdown is written on the caller, and a cache first used after shutdown starts no writer. The shader cache key check's reviewed names gain ShaderDiskCache::Shutdown, which changes nothing a recompile produces.
  7. fix(agc): Driver::stop flushes the async log through fix(agc): bound the async log's exit flush #6800's bounded flush instead of Flush. It reads the log's counters without its lock, gives up after 2 s and marks the log abandoned. Other callers keep the full flush. A title's exit stops the driver through libc's shutdown while the log's worker still runs, so there the bound matters only if the worker stalls. The profile output's Stop also takes a lock at that point; it exists only when APS5_PROFILE_* output is on, and is left as is.
  8. fix(agc): the async log's constructor waits once, with a zero timeout, on each of its three condition variables before it starts the worker, so they are set up on the constructing thread and the worker's waits never take winpthreads' lock.

Linux and macOS: the pools are stopped the same way, but the hang needs winpthreads' lock, so it is Windows-only.

Threads outside the driver: commits 5 and 6 come from an after-the-fact review of every thread alive at exit. Threads outside the driver can still meet the same lock: libc's exit waiter, where guest threads park after ProcessShutdown; libkernel's APR workers; the alarm thread; and AudioOut's SDL devices, which use Win32 primitives. They're left to separate changes.

Tested

  • New agc_driver_shutdown_threads (tests/execution/ShutdownThreads.cpp), no GPU:
    • closing the preparation in a process that prepared nothing starts no preparation thread, and preparations submitted afterwards run on the caller;
    • --joined: workers of both pools that ran a preparation are gone after closing;
    • --compare-helpers: a parallel compare's helpers are gone after stopping, and later compares give the same results on the caller.
    • --draw-committer: a committer runs queued commits off the caller; after the driver's shutdown none is left, the queued commit ran, and a later commit runs on the caller.
    • --shader-cache-writer: with the cache in a temporary directory, an entry stored before the driver's shutdown is on disk after it, no writer is left, and a later store is written at once.
  • agc_driver_pipeline_optimized_readback_*: after the draws, the library worker that built the optimized pipeline is running, and it is gone after stopping.
  • agc_driver_draw_cache_reuse, which draws through the driver with pipeline libraries: after the driver's own shutdown, no library worker is running.
  • GitHub's runners, on my fork:
    • the exit loop (both tests, four copies each, 60 rounds under one ctest --parallel 4, on main 5d9dcc4 with and without the fix): on main, 11 of 60 rounds hung (run), each an agc_interpolant_mapping exit whose main thread spins in winpthreads under ~DeviceUseGate in ~Driver, against 0 hangs in 60 rounds (480 exits) with the fix (run), Windows. test(agc): align the indirect arguments block at run time #6452's test fix was applied to both, because main doesn't build in full with MinGW since fix(agc): keep the gate run going past indirect dispatches whose arguments stay on the GPU #5747.
    • the new and touched tests, 20 runs each:
      • agc_driver_shutdown_threads and its _joined and _compare_helpers modes, agc_driver_prepared_shaders, agc_driver_prepared_publication and agc_driver_preparation_scope_settle: all pass on Linux and Windows (run).
      • The libc and libkernel suites pass apart from precise_wait on Linux and guest_kqueue on Windows, which fail on main on those runners too.
      • The shutdown tests and the readback tests, 20 runs each (run): on Linux the three shutdown tests and agc_driver_pipeline_optimized_readback_d16, _d16_fast, _d32 and _d32_fast pass 20 of 20, and the four colour modes skip on lavapipe; Windows skips the readback tests without a Vulkan device and passes the shutdown tests 20 of 20.
    • The library worker stopped before the device reset (af58b95), 10 runs each (run, run): on Linux, agc_driver_draw_cache_reuse, the three shutdown tests and the readback tests pass 10 of 10 (the four colour readback modes skip on lavapipe). On Windows the three shutdown tests pass 10 of 10, and agc_driver_draw_cache_reuse skips without a Vulkan device. The first run's Windows job stops in the full build, at main's GpuIndirectArgumentsMisalignedWriterAhead.cpp (alignas(65536) under MinGW), so the second runs the named tests only. The second is also marked failed for guest_kqueue on Windows, which fails in our other Windows runs today on unrelated changes.
    • The committer and the cache writer (966da79, 6522b92), 10 runs each (run): the five shutdown tests and agc_driver_draw_cache_reuse pass 10 of 10 on Linux and Windows, where agc_driver_draw_cache_reuse skips without a Vulkan device. The new tests use functions these commits add, so the before arm doesn't build. Their red is the branch with only the new call in Driver::stop removed: "the driver's shutdown left the draw committer running" and "... the shader cache writer running" (on a Linux cloud worker). The run is also marked failed for precise_wait on Linux and guest_kqueue on Windows, which these commits don't touch.
    • The exit loop again, with all six commits (run): 0 hangs in 60 rounds on the Windows runner.
    • The async log (commits 7 and 8), with yabanana's two test modes, on main 82ddae7 merged:
      • agc_driver_shutdown_threads_async_log_held: another thread holds the async log's lock, and the driver's shutdown must return within 10 s. With only stop()'s call put back to Flush, it fails 3 of 3 on Linux and Windows with "the driver's shutdown waited for the async log's lock" (run).
      • agc_driver_shutdown_threads_exit_after_start constructs the driver and returns at once. With the constructor's setup of the log's waits removed, it times out on Windows on the seventh run, after 6 passes, and passes on Linux, which has no winpthreads (run).
      • The eight shutdown modes pass 50 of 50 on Linux and Windows (run), and at yabanana's commit its six modes pass 10 of 10 (run). The in-flight preparation check in --joined and the late-cache mode (agc_driver_shutdown_threads_late_shader_cache) are yabanana's, from their review.
    • The guest_shader_link loop (rounds of up to 400 runs on the Windows runner; a run left for over 20 s has every thread dumped, and the loop stops after 2 hangs or 4 rounds): main 82ddae7 hung in 2 of 2 rounds, after 1 and 2 passes, on the async log's lock (run); with commit 7, 2 of 2 rounds, after 0 and 3 passes, in ~Driver's spin (run); with all eight commits, no hang in 4 rounds of 400 runs (run).
    • Threads at exit (guest_shader_link under cdb, stopped at RtlExitUserProcess, 60 exits): the async log's worker was inside its first wait's setup in 38 of 60 exits with commit 7 (run), and in none of 60 with commit 8 (run).
    • ^agc sweeps (run), with test(agc): align the indirect arguments block at run time #6452's test fix applied for that check only:
      • Linux: 609 of 609 pass, 43 skip. That includes agc_driver_pipeline_optimized_readback_d32 and _d16, whose library worker built the optimized pipeline and was gone after stopping.
      • Windows: 602 of 603 pass, 467 skip without a Vulkan device. The one failure, agc_driver_pm4_replay_tool (0xc0000139), also fails on main on that runner.
  • Merged with main 52798f7 (a6ff397, which brings fix(agc): drain library prebuilds before device replacement #6814), on GitHub's runners (run): the three shutdown tests, agc_driver_prepared_shaders, agc_driver_prepared_publication and the agc_driver_pipeline* tests pass 3 of 3 on Linux, where 4 skip, and on Windows, where the Vulkan ones skip (15). The only suite failure is guest_kqueue on Windows, which fails on that runner without this PR too.
  • python3 tools/check_conventions.py --base origin/main: passed.

Overlap: #6814 (drain library prebuilds before device replacement) is merged, and a6ff397 merges main into this branch. Their one conflict was PipelineLibrary.cpp's LibraryStore: #6814's activeBuilds stays beside this PR's worker and stopping, and started, which this PR replaces, is gone. Git merged the rest itself, ShaderRegistry among it, where #6814's AwaitLibraryPrebuilds sits beside this PR's RegistrationPreparationThreads and CloseAll. simonbronk built that composition on Windows with #6827 (comment), and yabanana's Linux integration with #6814 takes the same resolution.

Checklist

CloseRegistrationPreparation took RegistrationPreparePool::Instance(),
which creates the pool and starts its detached workers (half the
hardware threads, at least two) when nothing was queued for preparation
yet. In a process that prepared nothing in the background, the driver's
shutdown started the workers only to close the pool, and the process
exited while they were still starting. Their first waits take the
global lock of winpthreads' statically initialized condition variables;
ExitProcess killed them holding it, and the destructor of the next
static condition variable that was never waited on spun on it forever
at DLL detach (~DeviceUseGate in ~Driver, or one of libkernel's). On
GitHub's Windows runners agc_interpolant_mapping hung that way in 15 of
60 rounds of eight test runs.

Shutdown now closes only the pools that exist, and a pool first used
after it starts no workers, so later preparations run on the caller as
they did on a closed pool.
Closing a preparation pool waited until its queue was empty, but its
detached workers stayed parked on the queue until ExitProcess killed
them. A pool first used shortly before shutdown still had workers
starting at exit, inside the first waits that take winpthreads' global
condition-variable lock, which the next DLL-detach destructor of an
unwaited static condition variable then waits for forever.

The workers are now joinable and leave once their pool is closed and
idle, and Close joins them, so after the driver's shutdown no
preparation worker is running or starting.
…worker at shutdown

Two more detached driver threads were never stopped: the helpers of the
parallel compare pool, started at the first large compare, and the
pipeline library's job thread, started at the first link-time optimized
pipeline. A process that exited right after starting them killed them
inside the first waits that take winpthreads' global condition-variable
lock, as with the preparation workers.

The driver's shutdown now stops both and joins them, after the device
is reset. A compare in progress finishes first and later compares run
on the caller. The library worker finishes the job it is running, the
jobs still queued are dropped and counted off their device, and later
pipelines are not optimized in the background.
Comment thread core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp Outdated
@github-actions

github-actions Bot commented Oct 16, 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 5958 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1513
PR 6160 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp L10-11
PR 6856 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1513
PR 6946 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp L34-35, L62
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp L70-71, L85-87, L126-127, L156-157
PR 6947 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp L34-35, L62
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp L70-71, L85-87, L126-127, L156-157

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 1341 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/CMakeLists.txt
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4339 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4650 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4891 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
PR 5115 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5426 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5500 core/shader/recompiler/ShaderDiskCache.cpp
PR 5609 core/shader/recompiler/ShaderDiskCache.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
core/shader/recompiler/ShaderDiskCache.hpp
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6134 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6176 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp
PR 6222 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
PR 6259 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6320 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6373 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
core/shader/recompiler/ShaderDiskCache.hpp
PR 6503 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6535 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6538 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
PR 6539 core/shader/recompiler/ShaderDiskCache.cpp
PR 6718 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6924 core/shader/recompiler/ShaderDiskCache.cpp
core/shader/recompiler/ShaderDiskCache.hpp
PR 6937 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6973 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6987 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6989 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6995 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7002 core/libs/prx/libSceAgcDriver/CMakeLists.txt

Driver::stop reset the device and then stopped the worker. No job could
outlive the device, because ClearPipelineLibraries waits for the
device's jobs to finish, but that was only visible inside the wait.

The worker now stops first: a running job finishes, queued jobs are
dropped, and no job runs once the device's teardown starts.
agc_driver_draw_cache_reuse checks that the driver's own shutdown
leaves no library worker running.
The committer of pipelined draws, started at the first pipelined
submission, was never stopped. The driver's shutdown joined the queue
workers and left the committer parked on its queue, and a commit the
last submission left queued, such as its deferred completion, could
still run after the shutdown had reset the device. A process that
exited right after its first pipelined submissions could kill the
committer inside its first wait on the queue, which takes winpthreads'
global condition-variable lock, as with the preparation workers.

The driver's shutdown now stops the committer after joining the queue
workers and before taking the GPU lock. The committer runs the commits
still queued and leaves, and the shutdown joins it. A commit enqueued
after that runs on the caller.
The shader disk cache's writer, started with the cache at the first
shader lookup, was detached and never stopped. It stayed parked in a
timed wait on its queue until the process exit killed it, and entries
still queued at exit were never written. A process that exited right
after its first shaders could kill the writer inside its first wait,
which takes winpthreads' global condition-variable lock, as with the
driver's own threads.

The writer is now joinable. The driver's shutdown stops it after the
other driver threads: the writer writes the entries still queued and
leaves, and the shutdown joins it. A store after that is written on the
caller, and a cache first used after the shutdown starts no writer.
ShaderDiskCache::Shutdown joins the names the cache key check has
reviewed, since it changes nothing a recompile produces.
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 16, 2026
@simonbronk

Copy link
Copy Markdown
Contributor

Independent Windows native composition check of #6641 at 43830b93bdc8a58d436c7d0d3d8c9493eecdd8f1 with #6814 at 81e3db853c00453a04668e2c62e8b969f24d324b and its Windows CTest PATH follow-up #6827 (tree edbb564ebdb1bf30b8788d12c972292e5b0368bf). AI-assisted: OpenAI Codex.

There is one textual merge conflict in PipelineLibrary.cpp, in LibraryStore. I kept #6814's activeBuilds, kept this PR's worker and stopping, and dropped the replaced started flag:

    std::map<VkDevice, std::size_t> pending;
    std::map<VkDevice, std::size_t> activeBuilds;
    std::thread worker;
    bool stopping = false;

No other manual resolution or production alteration. The resolved local source tree is fc1137087816b3e8212e1f427f1621481145e4e0 (private validation commit 41a59800c169abf96ebd0a325a3396641d3a5ca6). Original authors' commits are retained; no duplicate runtime PR from me.

Windows x64 / Intel Core i7-8700, supported WinLibs GCC 15.2.0 r7 UCRT, CMake 4.4.4, Release, BUILD_TESTING enabled, SPIRV-Tools disabled. Built the real libSceAgcDriver.prx, its runtime dependencies and the three selected test executables, without a CPU-guard bypass or production instrumentation.

  • 9/9 CTests pass, no skips (12.09 s): agc_shader_helpers, agc_driver_graphics, agc_pipeline_journal, agc_pipeline_library_clear_waits_for_build, and all five agc_driver_shutdown_threads modes.
  • 60/60 repeated executions pass, no skips: those five shutdown modes plus agc_shader_helpers, ten each, -j 2. These are ordinary CTest processes with natural exits.
  • Original-DLL control: an external probe loads main 82ddae75b6761757f3d3ba9c0723647469270f76's previously built DLL or the newly composed DLL, looks up the existing CloseRegistrationPreparation export, and calls it without preparing anything. Four alternating executions per DLL: original 4 -> 10 process threads, composed 4 -> 4, in all runs. It counts threads 100 ms before/after the call using Toolhelp. The probe flushes its result and terminates its own isolated process to avoid entering the old exit race; it therefore demonstrates unnecessary worker creation, not a reproduced natural-exit deadlock. The normal CTests above supply natural-exit checks.
  • Conventions against current main 82ddae75b pass.

Limits: selected native targets/tests only, not a full all-target build or full CTest sweep. No local natural-exit hang was reproduced on main, and 60 successful exits do not prove absence of every shutdown race. No real Vulkan/GPU, title/FPS or Linux/macOS validation. I did not repeat the independent mocked-Vulkan part-error/link-error fixtures from #6827 on this composition; the official pipeline-clear regression does pass.

Attached: source probe, DLL hashes, exact source tree IDs, logs, raw repetition results and reproduction instructions. No binaries or game data.
shutdown-pipeline-composition.zip

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

What this change does: it stops the driver workers before their resources are torn down, and makes lazy initialization respect shutdown. I reviewed head 43830b93bdc8a58d436c7d0d3d8c9493eecdd8f1 and added focused checks for the in-flight and first-use cases.

Additional tests: patch based on this head.

  • --joined now holds both preparation tasks open while another thread closes the pools. It checks that closing waits, all workers are gone on return, repeated close is safe, and later work runs on the caller.
  • --late-shader-cache initializes the cache for the first time after driver shutdown. It checks that no writer starts and stores still reach disk. This reuses the existing cache-write test and its Windows PATH setup.

Verification:

  • Ubuntu 24.04 amd64, GCC 13.3 Release, QEMU 10.2.3 with -cpu max: all six shutdown CTests pass 10/10 fresh processes each (60 runs). No CPU guard, assertion, or CTest timeout was changed.
  • These Linux runs use integration dd8824b63629d68b7f205e66bcc578abb4a42e60: this PR, #6814, the two test patches, and the existing #6789 fixture-storage correction. The only production merge conflict is LibraryStore; keep #6814's activeBuilds alongside this PR's worker and stopping, and remove the replaced started field.
  • The combined pipeline lifetime regression, journal and address-space teardown tests pass. agc_driver_pipeline_optimized_readback_d32 executes on llvmpipe (LLVM 20.1.2, 256 bits): one fast draw, one optimized draw, one optimized creation, exact readback and the worker-shutdown check pass.
  • Separately, the unchanged ParallelCompare.cpp from this PR passes 20 native ARM64 processes under ThreadSanitizer on macOS 27.2 / Apple Clang 21. Four callers each make 100 comparisons while helpers stop; half the processes also race the pool's first use with shutdown. Every comparison is checked, no helper survives, and TSan reports nothing. Probe source and commands.
  • Conventions and whitespace checks pass.

The general graphics CTest exceeds 30 s in this emulated setup after #6789 fixes the earlier fixture error. The same binary completes its assertions in a separate, bounded 90 s diagnostic run; it reports write-watch-dependent cases unavailable. That diagnostic is not a full CTest pass, and the repository timeout remains unchanged.

Verdict: no new blocker found in the exercised shutdown paths; the additional regression tests are ready to reuse.
Not checked: Windows execution, physical GPUs, title exits, or full Linux/Windows CI. The combined result does not make either PR depend semantically on the other.

AI-assisted: yes.

EagleBearerYS and others added 4 commits October 16, 2026 07:51
…tops

The driver's static is destroyed from its DLL's atexit table at process exit, after ExitProcess has ended the async log's worker. Its stop() then flushed the log through Flush(), which takes the log's lock and waits for every queued line, so it never returned when the worker died holding that lock or with lines queued. boykopovar#6800's bounded flush runs after it, because the log registers it while the driver is being constructed. stop() now uses that bounded flush, which reads the counters without the lock and gives up after EndFlushLimit, marking the log abandoned. A new shutdown test mode holds the log's lock from another thread and checks that the driver's shutdown still returns.
…orker starts

A condition variable that winpthreads initializes statically is set up on its first wait, under one global spinlock for all such variables. The async log's worker made that first wait itself, right after the driver started it, so a process that exited soon after starting the driver, such as guest_shader_link, could end with ExitProcess killing the worker inside that setup. The spinlock then stayed held, and the next destructor of a never-used condition variable, in ~Driver, spun forever. The log's constructor now waits on each of its condition variables once with no timeout before it starts the worker, so the worker's waits never take that spinlock. A new shutdown test mode exits right after the driver starts.
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@simonbronk thank you for the Windows composition check with #6814 and #6827, and for the thread counts. Your LibraryStore resolution matches mine (#6814's activeBuilds, this PR's worker and stopping, started dropped); it's in the description's overlap section now.

@yabanana thank you for the review and the two test modes. They're on the branch as your commit, 8e671f4, and pass 10 of 10 on GitHub's Linux and Windows runners there, and 50 of 50 with the commits after it.

@MarcZX100 since your review, the branch merges main 82ddae7 and adds two fixes for the same exit on Windows, both in the async log. Driver::stop now uses #6800's bounded flush: on main the driver's destructor waited on the log's lock, held by a worker ExitProcess had killed. And the log sets up its condition variables before its worker starts: the worker was often killed inside that setup, holding winpthreads' lock. guest_shader_link, which hung within a few runs on main, ran 1,600 times without a hang. The runs are in the description.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@EagleBearerYS #6814 is merged now (7a2435e), so this conflicts in LibraryStore. Could you rebase onto main with the resolution you and @simonbronk agreed (keep #6814's activeBuilds plus this PR's worker/stopping)? We'll run full CI on the new head.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
main's 81e3db8 counts each device's active library builds
(activeBuilds), so ClearPipelineLibraries waits for them. Keep it
beside this branch's joinable worker and stopping flag, which replace
started.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@oneandonlydean done: a6ff397 merges main (with #6814) into this branch. LibraryStore keeps #6814's activeBuilds beside this PR's worker and stopping, and started is gone, as agreed with @simonbronk. I merged rather than rebased, so the reviewed commits keep their hashes. The shutdown and pipeline library tests are running on my fork: run.

@oneandonlydean
oneandonlydean merged commit af1e3ae into boykopovar:main Oct 17, 2026
9 checks passed
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 17, 2026
main 4827f36 adds the shader disk cache include to the driver
lifecycle (boykopovar#6641) where this branch adds the guest arena's. Keep both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants