Skip to content

fix(agc): check the deferred destruction resolver only against live devices - #11705

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
jotitadev:fix/agc-deferred-resolver-after-last-device
Oct 28, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
jotitadev:fix/agc-deferred-resolver-after-last-device

Conversation

@jotitadev

@jotitadev jotitadev commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What

Deferred destruction (#10062, MoltenVK only) keeps the vkGetDeviceProcAddr of the first device in a process-wide registry and throws deferred destruction found two different device resolvers when a later device brings a different pointer. The registry never forgets it: it is still there after its device is gone.

VulkanDevice unloads the Vulkan loader with SDL_UnloadObject when the device goes, and on macOS the next SDL_LoadObject can map libvulkan.1.dylib at another address. In an x86_64 probe, three dlopen/dlsym("vkGetDeviceProcAddr")/dlclose rounds returned 0x10937b240, 0x1094ea240 and 0x10937b240. So every test that opens a second VulkanDevice in the same process fails at device setup. imellag saw the same message on 9 tests in #11551, changing from run to run.

The registry now keeps the resolver of every device it set up and passes each device's calls through its own one, so a wrapper returned for an earlier device keeps reaching that device's loader after a later device is set up. The most recent resolver is used only for devices the registry never set up. A device set up after the last deferred device went may bring a different resolver; two live devices with different resolvers still throw.

agc_deferred_destruction gets a case that sets up a device, destroys its deferred objects, and sets up the next one with a different resolver, as after a loader reload. It checks that the next device is accepted and passes through to its own resolver, that the earlier device's wrapper still passes through to the earlier resolver (MarcZX10's review), and that a second live device with the first resolver is still refused.

Tested

macOS 27.2, Apple M2 Max, x86_64 Release under Rosetta 2. main alone doesn't build on macOS yet, so the GPU runs use main 27ad7244d2 plus the open macOS build PRs (#10366, #7631, #10659, #10660, #11249, #11358, #10669, #10552, #7804, #10540, #10676, #11447, #11437) and #11554, which loads libvulkan.1.dylib and makes this reachable. ANYPS5_NO_SHADER_CACHE=1, ctest -R '^agc' -j1 --timeout 30, without the flip, display and lds_atomics64 tests (1027 tests).

  • MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0), same tree with and without this change:
    • without: 156 failures, 118 of them stop at deferred destruction found two different device resolvers (2 of those as 30 s timeouts);
    • with: 41 failures, none with that message. 115 of the 118 pass. The other 3 now get further and fail on something else: agc_driver_indirect_arguments_pending_producer loads libvulkan.so.1 on macOS, agc_driver_stencil_ref_export_unused hits a MoltenVK MSL compile error, agc_driver_rect_list_indirect passes on MoltenVK main with MSL: Preserve negative zero in precise multiplication KhronosGroup/SPIRV-Cross#2704 (signed zero). No test fails with the change that passes without it.
  • MoltenVK main + SPIRV-Cross#2704, with the change: 40 failures, none with that message.
  • agc_deferred_destruction (the test and DeferredDestruction.cpp built alone, macOS x86_64 Apple clang and Linux Ubuntu GCC 15.2, same results): on main's DeferredDestruction.cpp the new case fails with "a device set up after the last one went could not use a resolver from a reloaded Vulkan loader"; on the first head of this PR (be7db25) it fails with "an earlier device's wrapper passed through to a later device's resolver"; at the current head the whole test passes.
  • The GPU numbers above were measured with the first head of this PR (be7db25), not re-run at the current head. Both heads pass a live device's calls to that device's own resolver; they differ only for calls on a device after a later device was set up, which VulkanDevice doesn't make (it destroys the device right after DestroyDeferredObjects).
  • 6 of the 118 (agc_driver_address_passes, clamped_global_fill, compute_table_calls, depth_copy_draw, host_import_teardown, indexed_copy) with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_REQUIRE_VULKAN=1, ctest -V: all pass, the layer loads, no VUID- errors. Without the change they stop at device setup, so there is no run to compare against.
  • python3 tools/check_conventions.py --base origin/main passes.

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 (reachable on macOS once fix(agc): load the Vulkan loader and list MoltenVK on macOS #11554 is in)
  • Unblocks: none
  • GPUs tested: Apple M2 Max (MoltenVK 1.4.2, Vulkan SDK 1.4.363.0; and MoltenVK main + SPIRV-Cross#2704), macOS 27.2
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

…evices

The deferred destruction registry kept the first device's
vkGetDeviceProcAddr after that device was destroyed. VulkanDevice
unloads the Vulkan loader with the device, and on macOS the next load
can map it at another address, so every later device in the process
failed with "deferred destruction found two different device
resolvers". The check now applies only while another device is still
registered; the new device's resolver replaces the old one.
@github-actions

github-actions Bot commented Oct 27, 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 10194 core/libs/prx/libSceAgcDriver/Graphics/src/DeferredDestruction.cpp L1-2, L64
core/libs/prx/libSceAgcDriver/tests/DeferredDestruction.cpp L117

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

Reviewed exact head be7db251ad005c9ead50177d190b9f04a17743f9 against current main ddd72f488b6070c3864c8782cc5691ca3e30e680. Changes requested for one reproduced resolver-routing regression.

[P2] Preserve the resolver for a previously returned device-proc wrapper across reload. deferringDeviceProc routes passthrough calls through the single process-wide registry().resolver. This change replaces that resolver whenever the registry has no deferred devices. After DeferDestructionToIdle(first, mockProc) and DestroyDeferredObjects(first), setting up second with reloadedProc changes what the already-returned firstProc routes to: firstProc(first, "vkQueueWaitIdle") now returns reloadedQueueWaitIdle, not mockQueueWaitIdle. I reproduced this by compiling the exact PR implementation/test with that additional assertion; it fails. DestroyDeferredObjects only removes deferred tracking; production VulkanDevice calls it before vkDestroyDevice, so the old device may still be live during this interval. The existing test explicitly expects a previously returned wrapper to pass through after DestroyDeferredObjects, but does not cover a subsequent resolver replacement.

Please preserve resolver routing for that old wrapper/device across the reload (or establish that this transition is unsupported and remove the stale-wrapper behavior contract), and add a regression for the sequence above. The requested loader-reload case and the two-live-device rejection should both remain covered.

Current-main conventions and git diff --check pass. Hosted policy checks pass; product test jobs are skipped by path policy. This review's additional focused resolver regression was compiled and executed locally; it did not run the full AGC suite.

…rough

The previous commit replaced the process-wide resolver when a device
was set up after the last deferred device went, so a wrapper returned
for the earlier device, which may still be alive until vkDestroyDevice,
passed through to the later device's loader. The registry now keeps the
resolver of every device it set up and passes each device's calls
through its own one; the most recent resolver is used only for devices
it never set up. Two live devices with different resolvers are still
refused.
@jotitadev

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thanks, you're right. Fixed in 846e298.

  • The registry now keeps the resolver of every device it set up and passes each device's calls through its own one. The most recent resolver is only the fallback for devices it never set up. Two live devices with different resolvers still throw.
  • Your assertion is in the regression case: after DestroyDeferredObjects(first) and setting up second with reloadedProc, firstProc(first, "vkQueueWaitIdle") must still return mockQueueWaitIdle.

agc_deferred_destruction, with the test and DeferredDestruction.cpp built alone on macOS (x86_64, Apple clang) and Linux (GCC 15.2), same results on both:

DeferredDestruction.cpp your assertion (old wrapper keeps its resolver) mine (later device accepted with a reloaded resolver)
main not reached (the setup it needs throws) fails
be7db25 fails passes
846e298 passes passes, and the whole test passes

The GPU suite numbers in the description are from be7db25. I haven't re-run them on this head yet. For a device that is still live, both heads pick the same resolver.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

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

Re-reviewed exact head 846e2989232e60ebb4c1720c7bdb8d68b095b928.

The updated per-device resolver map fixes the earlier-wrapper regression: a wrapper returned for the first device retains that device's resolver after a later loader reload, while the new device uses its own resolver. The live-device resolver mismatch guard remains, and the regression test now checks both routing directions plus rejection of two live devices with different resolvers.

I applied the exact PR changes since the current-main merge base in a disposable review tree, built agc_deferred_destruction_tests, and ran ctest -R '^agc_deferred_destruction$': 1/1 passed. The normal host CPU guard was bypassed only in that scratch tree for this CPU-only test, then restored; tracked source is clean. The PR correctly distinguishes GPU-suite numbers from its previous head; its current-head focused tests pass on Linux and macOS per the author. Hosted eligibility, conventions and label checks pass; product jobs are skipped by path policy.

No blocking issue found in the updated diff.

@jotitadev

Copy link
Copy Markdown
Contributor Author

GPU re-check of 846e298923 on Apple silicon, with the per-device resolver.

Machine: Apple M2 Max, macOS 27.2, x86_64 Release build under Rosetta 2.
Tree: main b986c11a34 + the open macOS build PRs needed to compile and open a device (#10366, #7631, #10659, #10660, #11249, #11358, #10669, #10552, #7804, #10676, #11447, #11437, #11554) + this PR's DeferredDestruction.cpp and test change. ANYPS5_NO_SHADER_CACHE=1, -j1 --timeout 30.
Drivers: MoltenVK 1.4.2 (LunarG SDK 1.4.363.0) and MoltenVK main + SPIRV-Cross #2704.

  • agc_deferred_destruction with the new test: passes 3/3. With DeferredDestruction.cpp reverted to main (test kept) it fails with "a device set up after the last one went could not use a resolver from a reloaded Vulkan loader".
  • agc_driver_address_passes, clamped_global_fill, depth_copy_draw, direct_write_completion (each failed with "deferred destruction found two different device resolvers" in my earlier no-fix control on macOS): pass 3/3 on both drivers.
  • Full ^agc suite, 1037 tests: 41 failures (SDK) / 40 (MoltenVK main), none with "two different device resolvers". Same failure list as with the previous design (be7db251a), so the per-device version keeps all 118 tests that the resolver check broke on macOS. The 3 of those 118 that still fail stop on unrelated causes (loader name in IndirectArgumentsPendingProducer.cpp, which test(agc): load the Vulkan loader by its macOS name in the host import and producer gate helpers #11706 fixes; an MSL compile error in stencil_ref_export_unused; rect_list_indirect on the SDK driver only, a -0 sign issue that passes with Ratchet & Clank: Rift Apart: SIGSEGV since #861, an address-less 64 GiB reservation now takes the addresses the title hints next #2704).
  • Validation layer (VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, SDK 1.4.363.0, loader output shows it inserted) on the five tests above: all pass, no VUID-.

GPUs tested: Apple M2 Max (MoltenVK 1.4.2 and MoltenVK main + #2704)

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@maskym

maskym commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Windows + NVIDIA result for head 846e298923, merged into main 2ee8e9751a (the PR's GPUs tested line names no NVIDIA run on Windows).

This PR's own tests: agc_deferred_destruction (0.04 s), all passed.

Host: Windows 11 26300, Intel i7-12700K, NVIDIA GeForce RTX 2070 SUPER (vendor 0x10de, device 0x1e84; Vulkan loader 1.4.357 from the NVIDIA driver), Khronos Validation Layers nightly 1.4.366 (b2b44c0d37), WinLibs GCC 15.2.0 (x86_64-ucrt-posix-seh, r6), Release, BUILD_TESTING=ON, Ninja.

Same focused run on both: VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_NO_SHADER_CACHE=1, ANYPS5_REQUIRE_VULKAN=1, ctest --test-dir build -R '^(agc|guest_video_out|video_out)' --parallel 4 -V, run on main 2ee8e9751a and on this tree from the same machine and GPU.

  • main: 1125/1125 passed (9 skipped, as without the layer), 146 s.
  • This tree: the same 1125 passed (6 further tests from the other PRs in the tree were not run because their executables failed to build; see those PRs), 136 s.
  • Neither log contains was not found but was requested by env var VK_INSTANCE_LAYERS.
  • Validation: both logs report exactly one VUID, VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (18 occurrences each, from agc_driver_prepared_exports and agc_driver_srgb8_color_target), which is pre-existing on main; this PR adds no new VUID- error.

The tree also carried the open PRs #11873, #11943, #11721, #11708, #11703, #11702, #11701, #11948, all merged cleanly, so the run is a joint one. Not run on a PS5.

AI-assisted: yes (Claude Code), the runs and the numbers above are from this machine.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the per-device resolver map against the diff: a wrapper returned for an earlier device keeps its own resolver, a later device uses its own, and two live devices with different resolvers still throw. The new case in agc_deferred_destruction fails on the previous registry and covers both routing directions; MarcZX10's earlier request is met and approved on this head. Merged.

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.

4 participants