You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agc_driver_recorder_tests (full run): allocation stays pinned for the whole lease wait since cf5c3ebe, on NVIDIA and RADV #2833
Since cf5c3eb (fix(agc): retire host imports before guest unmap), the full agc_driver_recorder_tests run on Linux waits out the pin lease and stops with:
guest allocation 0x7f728a2b0000+0x1100000 is still pinned by GPU work or an import (waited 60000 ms for the lease (APS5_PIN_WAIT_MS))
It happens on an NVIDIA and on an AMD device. The full run is not a ctest (only the --…-only modes are registered), so CI does not see it.
Reproduce:cmake --build build --target agc_driver_recorder_tests, then ANYPS5_REQUIRE_VULKAN=1 build/tests/agc_driver_recorder_tests (no arguments). It exits 1 after about 120 s.
git bisect on the RADV device over 084aeeb..78aedb4 (1,462 commits, 11 steps), marking a commit bad when the output contains still pinned:
cf5c3ebe is the first bad commit
fix(agc): retire host imports before guest unmap
The last lines of the test's own output before the stop on main:
host imports stay watched
batch stamps off
late label tests ran with APS5_LABEL_TRUST_LATE=0
resident present format and conversion tests passed
denormal half clears are kept by this device
guest allocation 0x7f728a2b0000+0x1100000 is still pinned by GPU work or an import (waited 60000 ms for the lease (APS5_PIN_WAIT_MS))
Not established
Which lease or import keeps the 17 MiB allocation pinned; I have not traced it.
Follow-up with the cause. Runs are on main78aedb4, unmodified build, RTX 5080 and RADV.
Where:unitShadowTests in tests/Recorder.cpp, at the mutation.Remove(block) of the "retire through the registry" step (line 1816). The test thread holds GpuMutex there. A second full wait follows from the block's unregistration at scope exit, which is why the run takes two lease waits.
Measured
Run
Result
APS5_PIN_WAIT_MS=3000
same still pinned stop, after 6 s (two 3 s waits)
APS5_RELEASE_UNDER_LOCK=1
no wait; the run goes on and stops at once on the retire published a unit whose memory is no longer registered (tests/Recorder.cpp:1823)
Same on both devices.
What holds the pin (from reading the code, consistent with the second row):
cf5c3eb changed HostImport::range from weak_ptr to shared_ptr (Graphics/include/GuestBufferMemory.hpp), so a retired import now keeps the registered range alive.
retireImport (GuestBufferMemory.cpp:200) hands the RetiredImport holder to recorder->Keep when the recorder is not idle. Here it is not: the shadow units were just published.
The pin waiter, on a thread that holds GpuMutex, calls recorder->Sync(). But finish (Graphics/src/Recorder.cpp:3571) does not destroy a batch's kept objects on a thread that holds GpuMutex; it moves them to that thread's deferred list, released by the thread's own outermost unlock (ReleaseDeferredKeeps).
That unlock cannot happen while the thread waits for the lease, so the holder, and with it the range, stays alive until the wait gives up.
So the wait is made on the thread that must do the release. APS5_RELEASE_UNDER_LOCK=1 destroys kept objects in finish, which is why the wait disappears with it.
The next stop with that switch looks like a test expectation the same commit made stale, but I have only read this, not shown it: the comment above the step expects the import to retire at the next lookup, after the block was re-registered as five units. Since cf5c3eb it retires inside Remove, while all of the block is still registered, so unit 5 is published.
Not established: whether any product path removes a guest allocation on a thread that holds GpuMutex with the recorder busy (the waiter's own comment says the mutating thread "may be a driver thread holding it"); I did not trace the callers, and no title run shows it.
Windows data point, mostly so nobody reads a Windows pass as "not affected": the full run can't reach this there.
main57f96fd, Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, ANYPS5_REQUIRE_VULKAN=1 agc_driver_recorder_tests with no arguments. On an RTX 5060 Laptop GPU (32.0.16.1742) and an Arrow Lake iGPU (32.0.101.8991) it exits 0 in about 2 s, with no pin wait. But the run prints
host imports are compared, not watched: unit shadows not tested
host imports are compared, not watched: snapshot patching not tested
host imports are compared, not watched: draw snapshot reuse not tested
so unitShadowTests returns at Recorder.cpp:1891 on 57f96fd, before the mutation.Remove(block) at line 2021 where you see the stall (your line 1816 on 78aedb4). On Windows host imports go through comparisons rather than the write watch, so this only shows Windows hosts can't catch it, not that the change is fine there.
AI-assisted (Claude Code); the numbers above are from runs on this machine.
I opened PR #2864 with the fix and a focused agc_unit_shadow CTest for the retirement sequence you traced. It flushes the current thread's deferred recorder keeps after synchronization, before the GuestAllocations waiter can deadlock waiting for that same thread to unlock. The test also updates the expected bytes for units that are still covered by the old full registration when retirement occurs.
Validation so far:
Linux x86-64 GCC 12.2 Release build of agc_driver_recorder_tests: passed.
agc_unit_shadow: skipped on this host because asynchronous userfaultfd write watch is unavailable; this means I have not yet run the regression sequence on a capable Linux host.
PR conventions, intake, Linux/macOS register-context, both macOS write-watch jobs, and all three relinker jobs: passed. Full Linux and Windows PR builds were skipped under the repository workflow policy.
If you can run PR #2864's focused test on one of the affected Linux setups, the exact command is:
The CTest sets APS5_PIN_WAIT_MS=3000 and has a 30-second timeout. A pass or a failure log from that test would help verify the fix on the write-watch path this host cannot exercise.
Closing my open point above ("whether any product path removes a guest allocation on a thread that holds GpuMutex"): I found none, so this looks like a test-only stop, not a title hang.
Checked on main57f96fd with git grep over core, tests excluded:
Question
Result
Files outside core/libs/prx/libSceAgcDriver that mention GpuMutex
none
Non-test files that construct a GuestAllocations::Mutation
Non-test driver code that calls GuestHeapFree, GuestHeapRealloc, munmap_nid, mprotect_nid, DoMunmap or the GuestAllocationsRemove/Unmap/Protect exports
none
So every registry mutation that can reach the pin wait starts in libc or libkernel, on a thread that cannot hold GpuMutex through those files, and the waiter then takes its own lock_guard (GuestBufferMemory.cpp, the branch for a thread that does not hold the lock). That guard's unlock releases the deferred kept objects, which is the step the test thread cannot reach while it holds the lock itself at mutation.Remove(block).
Not checked: a guest signal or exception handler that runs on a thread while it is inside the driver and frees or unmaps memory there. Nothing above rules that out, and I have not tried to produce it.
That leaves the two test-side points from the earlier comment: the full agc_driver_recorder_tests run is not a registered ctest, and it has not passed at any commit I ran since 084aeeb.
The next stop of the full run, step (s), is a test expectation that no longer matches the driver, not lost results.
Setup:#2864 head b2a8590 merged onto main57f96fd (needed to get past the pin wait), full agc_driver_recorder_tests run with no arguments. RTX 5080 (615.78.08) and Radeon 610M (RADV, Mesa 26.1.4).
Test source
RTX 5080
RADV, Radeon 610M
As merged
stops at (s) results pending when their memory left the watch did not reach it
same
Temporary change: explicit flush before the (s) check
step (s) passes; stops later at (w) a cache over the pages the import reported sees no change
same
The temporary change, in staleGenerationTests before const auto pixels = readback.Bytes();, copies what the unwatched branch of storageRefreshTests in the same file already does:
Both Requires hold and the two (s) checks after them pass. So after the Refresh() the image is still registered as pending and an explicit flush puts FF 00 00 FF into guest memory; step (s) only fails because it expects Refresh() alone to store the results into the unwatched range.
From reading, not bisected:0e1d25a (fix(agc): avoid redundant transfers for untracked imported textures) added compareUntracked to Texture.cpp, after which a refresh over unchanged untracked memory keeps its results pending. git log -L on staleGenerationTests shows it was last touched on 2026-10-01 (d8c7c0c), before that commit. 0e1d25a is an ancestor of 2f655b1, where I first saw the (s) stop, and not of 084aeeb.
The stop after that, (w) a cache over the pages the import reported sees no change, is the assertion #1532 reports. I have not looked into it on this build.
Thanks for the follow-up. This confirms the full recorder executable still has separate stale-generation expectations after the pin-wait issue is bypassed: explicitly flushing gets past (s) but exposes the later (w) assertion on both GPUs. I kept #2864 scoped to the pin-release deadlock and noted that the full executable is not green; the focused regression passes with the fix and fails without it on both devices.
Activity
fma-rc commentedon Oct 10, 2026
Follow-up with the cause. Runs are on
main78aedb4, unmodified build, RTX 5080 and RADV.Where:
unitShadowTestsintests/Recorder.cpp, at themutation.Remove(block)of the "retire through the registry" step (line 1816). The test thread holdsGpuMutexthere. A second full wait follows from the block's unregistration at scope exit, which is why the run takes two lease waits.Measured
APS5_PIN_WAIT_MS=3000still pinnedstop, after 6 s (two 3 s waits)APS5_RELEASE_UNDER_LOCK=1the retire published a unit whose memory is no longer registered(tests/Recorder.cpp:1823)Same on both devices.
What holds the pin (from reading the code, consistent with the second row):
HostImport::rangefromweak_ptrtoshared_ptr(Graphics/include/GuestBufferMemory.hpp), so a retired import now keeps the registered range alive.retireImport(GuestBufferMemory.cpp:200) hands theRetiredImportholder torecorder->Keepwhen the recorder is not idle. Here it is not: the shadow units were just published.GpuMutex, callsrecorder->Sync(). Butfinish(Graphics/src/Recorder.cpp:3571) does not destroy a batch's kept objects on a thread that holdsGpuMutex; it moves them to that thread's deferred list, released by the thread's own outermost unlock (ReleaseDeferredKeeps).So the wait is made on the thread that must do the release.
APS5_RELEASE_UNDER_LOCK=1destroys kept objects infinish, which is why the wait disappears with it.The next stop with that switch looks like a test expectation the same commit made stale, but I have only read this, not shown it: the comment above the step expects the import to retire at the next lookup, after the block was re-registered as five units. Since cf5c3eb it retires inside
Remove, while all of the block is still registered, so unit 5 is published.Not established: whether any product path removes a guest allocation on a thread that holds
GpuMutexwith the recorder busy (the waiter's own comment says the mutating thread "may be a driver thread holding it"); I did not trace the callers, and no title run shows it.Silverash-i8 commentedon Oct 10, 2026
Windows data point, mostly so nobody reads a Windows pass as "not affected": the full run can't reach this there.
main57f96fd, Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release,ANYPS5_REQUIRE_VULKAN=1 agc_driver_recorder_testswith no arguments. On an RTX 5060 Laptop GPU (32.0.16.1742) and an Arrow Lake iGPU (32.0.101.8991) it exits 0 in about 2 s, with no pin wait. But the run printsso
unitShadowTestsreturns atRecorder.cpp:1891on 57f96fd, before themutation.Remove(block)at line 2021 where you see the stall (your line 1816 on 78aedb4). On Windows host imports go through comparisons rather than the write watch, so this only shows Windows hosts can't catch it, not that the change is fine there.AI-assisted (Claude Code); the numbers above are from runs on this machine.
MarcZX10 commentedon Oct 10, 2026
I opened PR #2864 with the fix and a focused
agc_unit_shadowCTest for the retirement sequence you traced. It flushes the current thread's deferred recorder keeps after synchronization, before the GuestAllocations waiter can deadlock waiting for that same thread to unlock. The test also updates the expected bytes for units that are still covered by the old full registration when retirement occurs.Validation so far:
agc_driver_recorder_tests: passed.agc_unit_shadow: skipped on this host because asynchronous userfaultfd write watch is unavailable; this means I have not yet run the regression sequence on a capable Linux host.If you can run PR #2864's focused test on one of the affected Linux setups, the exact command is:
ctest --test-dir build --output-on-failure -R '^agc_unit_shadow$'The CTest sets
APS5_PIN_WAIT_MS=3000and has a 30-second timeout. A pass or a failure log from that test would help verify the fix on the write-watch path this host cannot exercise.fma-rc commentedon Oct 10, 2026
Closing my open point above ("whether any product path removes a guest allocation on a thread that holds
GpuMutex"): I found none, so this looks like a test-only stop, not a title hang.Checked on
main57f96fd withgit grepovercore, tests excluded:core/libs/prx/libSceAgcDriverthat mentionGpuMutexGuestAllocations::Mutationlibc/src/GuestHeap.cpp,libkernel/DirectMemory/DirectMemory.cpp,libkernel/Module/src/DynamicLoader.cppGuestHeapFree,GuestHeapRealloc,munmap_nid,mprotect_nid,DoMunmapor theGuestAllocationsRemove/Unmap/ProtectexportsSo every registry mutation that can reach the pin wait starts in libc or libkernel, on a thread that cannot hold
GpuMutexthrough those files, and the waiter then takes its ownlock_guard(GuestBufferMemory.cpp, the branch for a thread that does not hold the lock). That guard's unlock releases the deferred kept objects, which is the step the test thread cannot reach while it holds the lock itself atmutation.Remove(block).Not checked: a guest signal or exception handler that runs on a thread while it is inside the driver and frees or unmaps memory there. Nothing above rules that out, and I have not tried to produce it.
That leaves the two test-side points from the earlier comment: the full
agc_driver_recorder_testsrun is not a registered ctest, and it has not passed at any commit I ran since 084aeeb.AI-assisted: yes
fma-rc commentedon Oct 10, 2026
The next stop of the full run, step (s), is a test expectation that no longer matches the driver, not lost results.
Setup: #2864 head b2a8590 merged onto
main57f96fd (needed to get past the pin wait), fullagc_driver_recorder_testsrun with no arguments. RTX 5080 (615.78.08) and Radeon 610M (RADV, Mesa 26.1.4).(s) results pending when their memory left the watch did not reach it(w) a cache over the pages the import reported sees no changeThe temporary change, in
staleGenerationTestsbeforeconst auto pixels = readback.Bytes();, copies what the unwatched branch ofstorageRefreshTestsin the same file already does:Both
Requires hold and the two (s) checks after them pass. So after theRefresh()the image is still registered as pending and an explicit flush putsFF 00 00 FFinto guest memory; step (s) only fails because it expectsRefresh()alone to store the results into the unwatched range.From reading, not bisected: 0e1d25a (
fix(agc): avoid redundant transfers for untracked imported textures) addedcompareUntrackedtoTexture.cpp, after which a refresh over unchanged untracked memory keeps its results pending.git log -LonstaleGenerationTestsshows it was last touched on 2026-10-01 (d8c7c0c), before that commit. 0e1d25a is an ancestor of 2f655b1, where I first saw the (s) stop, and not of 084aeeb.The stop after that,
(w) a cache over the pages the import reported sees no change, is the assertion #1532 reports. I have not looked into it on this build.AI-assisted: yes
MarcZX10 commentedon Oct 10, 2026
Thanks for the follow-up. This confirms the full recorder executable still has separate stale-generation expectations after the pin-wait issue is bypassed: explicitly flushing gets past (s) but exposes the later (w) assertion on both GPUs. I kept #2864 scoped to the pin-release deadlock and noted that the full executable is not green; the focused regression passes with the fix and fails without it on both devices.