Skip to content

fix(agc): skip a periodic pipeline cache save while a pipeline creation holds the cache - #6958

Open
Pipodi wants to merge 1 commit into
boykopovar:mainfrom
Pipodi:fix/pipeline-cache-save-try-lock
Open

Pipodi wants to merge 1 commit into
boykopovar:mainfrom
Pipodi:fix/pipeline-cache-save-try-lock

Conversation

@Pipodi

@Pipodi Pipodi commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

What

PipelineCache's saver thread wakes every 10 s and snapshots the Vulkan pipeline cache with LockForSnapshot(), an exclusive lock on the shared_mutex that every pipeline creation holds shared (LockPipelineCacheForUse). If a creation is running, the save blocks until it ends, and because the lock prefers a waiting writer, every later creation on every queue blocks behind the save as well.

With a compute pipeline that takes minutes in the NVIDIA compiler, this stops all graphics work for the whole compile, though the compile runs on a compute queue. Ghost of Yōtei (PPSA26344) froze its loading at frame 797–799 for 8+ minutes this way on every warm run.

With this change, the periodic save takes the snapshot with TryLockForSnapshot() at each of its three snapshot points (live size, merge, data). When a creation holds the cache, it returns and tries again at the next interval. SaveNow() and the teardown save still wait for the lock. SaveUnlessBusy() exposes the periodic path for the test.

Thread dumps of the freeze (run193, five dumps between 382 and ~800 s, identical)

Symbolized against the build that ran; line numbers from addr2line:

Thread Where State
queue worker 0x0 (graphics) Draw → CachedPipeline → Pipeline::Pipeline (Pipeline.cpp:543) → LinkPipelineFromLibraries → LockPipelineCacheForUse (PipelineLibrary.cpp:214), lock_shared same stack pointer in all five dumps
PipelineCache::run → save(false) LockForSnapshot (PipelineCache.cpp:172), lock waiting in all five dumps
a second PipelineCache::run → save(false) pathMutex (PipelineCache.cpp:168) waiting on the first
queue worker 0x20 (compute) inside nvgpucomp64.dll instruction pointer changes between dumps: compiling

The driver's periodic stats stopped about when the frame count did ([gpu] worker ... draw 64.7 s then silence), while the title's CPU side kept running (2.35 cores, audio streaming).

Tests

pipelineCacheFileTests (agc_pipeline_cache_file) gains a case: one thread holds LockForPipelineUse() like a long creation. SaveUnlessBusy() must return within 5 s, and a second LockForPipelineUse() must then be granted within 5 s. If the save blocks, the test releases the holder after the timeout and fails, so it doesn't hang.

  • With SaveUnlessBusy() routed to the blocking save, the case fails with "a periodic pipeline cache save waited for a pipeline creation to finish".
  • With the change, it passes.

Tested

Windows 11 Pro 26200, RTX 4090, driver 617.42.

Run Change Frame at 241 s at 261 s at 321 s Then
run191 without 773 781 799 799 until 900 s
run192 without 780 797 797 797 until 900 s
run193 without 758 769 799 799 until 900 s (the dumps above)
run194 with 793 812 813 past the freeze; stopped at 813 for a separate reason (below)

In run194 two periodic saves wrote the file, and a thread dump at 442 s shows no save waiting and the graphics worker idle in its run loop rather than on the lock. The title then stops at frame 813 while a compute queue worker is inside the NVIDIA compiler again; that one is not a lock wait and is still being looked into.

Technical debt

New entry in docs/dev/debt/libSceAgcDriver.md, "Periodic pipeline cache saves skip busy intervals":

  • the saver skips any interval in which a creation holds the cache;
  • while creations overlap with no gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save;
  • how often that happens is not measured.

Related

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: PPSA26344 Ghost of Yōtei (PPSA26344) #675 (loading freeze at frame ~799)
  • GPUs tested: NVIDIA GeForce RTX 4090, driver 617.42, Windows 11
  • AI-assisted: yes

🤖 Generated with Claude Code

@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 6457 docs/dev/debt/libSceAgcDriver.md L192
PR 6479 docs/dev/debt/libSceAgcDriver.md L192
PR 6730 docs/dev/debt/libSceAgcDriver.md L192
PR 7844 docs/dev/debt/libSceAgcDriver.md L192

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 1368 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 4448 docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5499 docs/dev/debt/libSceAgcDriver.md
PR 5925 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6146 docs/dev/debt/libSceAgcDriver.md
PR 6185 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6186 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6187 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6209 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6252 docs/dev/debt/libSceAgcDriver.md
PR 6280 docs/dev/debt/libSceAgcDriver.md
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6588 docs/dev/debt/libSceAgcDriver.md
PR 6636 docs/dev/debt/libSceAgcDriver.md
PR 6683 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6741 docs/dev/debt/libSceAgcDriver.md
PR 6747 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6868 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6887 docs/dev/debt/libSceAgcDriver.md
PR 6946 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6965 docs/dev/debt/libSceAgcDriver.md
PR 6970 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6971 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6973 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7085 docs/dev/debt/libSceAgcDriver.md
PR 7254 docs/dev/debt/libSceAgcDriver.md
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7390 docs/dev/debt/libSceAgcDriver.md
PR 7426 docs/dev/debt/libSceAgcDriver.md
PR 7432 docs/dev/debt/libSceAgcDriver.md
PR 7446 docs/dev/debt/libSceAgcDriver.md
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7457 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7505 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7604 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7622 core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp
PR 7642 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7659 docs/dev/debt/libSceAgcDriver.md
PR 7683 docs/dev/debt/libSceAgcDriver.md
PR 7800 docs/dev/debt/libSceAgcDriver.md

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 16, 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.

Right fix for the right problem: a blocking exclusive LockForSnapshot on a writer-preferring shared_mutex stalls every later creation on every queue, and TryLockForSnapshot removes that without touching SaveNow or the teardown save. The thread dumps in the description make the cause clear, merged is destroyed by DestroyMerged on the three early returns, and the debt entry states the cost (no periodic save while creations overlap without a gap).

Minor:

  • When the first snapshot succeeds but the merge or the data snapshot loses the try, the function returns after readHeader()/readFile() and the merge-cache creation, and nothing is settled, so the next interval repeats all of it. During a long compile that is a read of the whole cache file every 10 s for no result. Check for a creation in flight before reading the file, or settle liveBytes so the next interval returns early.
  • The test only covers the first snapshot point. A second case where the creation starts between the first and the second snapshot would cover the other two owns_lock() returns.

@keravyn-ops

Copy link
Copy Markdown
Contributor

AMD / Windows run of this PR at eb47773: Radeon 780M (RDNA 3, 0x15bf), AMD driver 25.10.20.01 (32.0.21020.1007), Windows 11 Home 10.0.26100, WinLibs GCC 15.2.0 r7, Release.

Every ctest entry built from the test targets this PR changes passes (42):

agc_pipeline_cache, agc_pipeline_cache_file, agc_pipeline_library, agc_pipeline_library_off, agc_pipeline_library_fast_only, agc_import_watch, agc_import_watch_other_family, agc_unchanged_cpu_stamps, agc_storage_kept_unit_settle, agc_storage_baseline_settled, agc_storage_integer_clear, agc_storage_upload_lifetime, agc_storage_upload_failure, agc_storage_written_buffer_order, agc_storage_split_writeback, agc_copied_direct_ordering, agc_recorder_read_list, agc_space_pending_writes, agc_host_import_unmap, agc_parked_import_unmap, agc_host_import_retirement, agc_sparse_run_import, agc_sparse_run_import_copied, agc_unit_shadow, agc_storage_cpu_snapshot, agc_descriptor_pool_recycling, agc_draw_snapshot_cap, agc_draw_snapshot_epoch, agc_multisample_guest_access, agc_draw_inputs_in_place, agc_pending_write_snapshot, agc_resident_present, agc_queued_write_groups, agc_tiled_resolve, agc_slice_dirty, agc_metadata_pass, agc_host_import_second_mapping, agc_pending_key_stores, agc_shadowed_key_stores, agc_sample_dumps, agc_driver_vertex_target_range_copied, agc_driver_vertex_target_range_imported

AI-assisted: yes (runs made on the device above).

@ps54all ps54all mentioned this pull request Oct 18, 2026
11 tasks done
@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 18, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@adriadam10 adriadam10 self-assigned this Oct 19, 2026
…on holds the cache

The saver thread took the snapshot lock with a blocking exclusive lock. Behind
a pipeline creation that runs for minutes (an NVIDIA compile of a 1.5M-word
compute pipeline), the queued writer made the shared_mutex hold every later
pipeline creation on every queue until the compile ended. The periodic save now
tries the lock and waits for the next interval when it is busy; SaveNow and the
teardown save still wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/pipeline-cache-save-try-lock branch from eb47773 to 1d8acd1 Compare October 19, 2026 12:16
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants