Skip to content

fix(agc): bind heap mirrors only where the guest holds bytes - #7647

Open
fma-rc wants to merge 3 commits into
boykopovar:mainfrom
fma-rc:fix/agc-heap-mirror-resident
Open

fma-rc wants to merge 3 commits into
boykopovar:mainfrom
fma-rc:fix/agc-heap-mirror-resident

Conversation

@fma-rc

@fma-rc fma-rc commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

A heap mirror copied its whole registered range into device memory, so a title with large GPU heaps paid for every page it had reserved, not just the ones it had written. Playroom (PPSA01325) on a Radeon 680M APU loses the device on current main at about 359 s:

radv/amdgpu: Not enough memory for command submission.
[videoout] presentation failed: AGC graphics: vkQueueSubmit recorder: Vulkan result -4

Its three heap mirrors hold 10126 MiB. Heap mirrors are made wherever a range's host import is refused, which happens once registered memory exceeds the import budget, so this is not specific to one GPU or OS. This is the heap mirror part of the device loss in #7047; that report is a Radeon 780M on Windows, which I could not run, and host imports are not changed here.

A heap mirror is now a sparse, aliased buffer of 64 KiB blocks. Each block that holds a nonzero guest byte is bound to device-local memory and keeps a host copy; every other block is bound to one shared zero block per device. Holes of the guest's shared memory are found with SEEK_DATA and never read. A block the guest or the GPU writes later is bound when the next build sees the change. Changed bytes reach the device by copies recorded before the build's work, and bytes the GPU wrote, including into the mirrors a cached address space holds, are copied back after it, so no block needs a host-visible memory type (the RTX 5080 has none that a sparse aliased buffer accepts). A device read of a mirror inside a GPU completion throws.

A device without sparseBinding, sparseResidencyBuffer and sparseResidencyAliased, or without enough sparse address space left for a mirror, keeps the whole-range copy main makes. The RTX 5080, Radeon 610M and 680M (RADV) and lavapipe report all three; lavapipe has 2 GiB of sparse address space, so a larger heap range is copied whole there. From the driver sources, Intel before Gen12 on i915, Mali (panvk), Raspberry Pi (v3dv) and MoltenVK lack them. Device creation no longer limits sparse binding to dma-buf import.

A sparse queue takes only a queue that is free. Main and settle queue selection is unchanged. When no free queue binds, binds go to the main queue under its lock. One change on Linux with dma-buf import: main dropped the settle queue when the sparse family was the settle family with a single queue. That settle queue is now kept.

A new joined host import bumps the import epoch, so the next build releases heap mirrors over its ranges. Without that, the next refresh throws a heap mirror's range is no longer write-watched. PR 6546 and PR 6646 handle that throw by refreshing or re-planning the mirror. This PR removes the case where a joined import leaves the mirrors behind. It does not change what they do in other cases.

Memory use is not lower overall: on the 680M the GTT peak drops and the process RSS peak is about the same (table). Texture snapshot memory (CachedTexture.bytes) is left to PR 6982.

Tested

Arch Linux, main c4c2b3a with and without this PR (the branch is rebased onto bf2c63f since, after #6646 and the import requester counts changed the same code).

Playroom 600 s, Radeon 680M (RADV, Mesa 26.2.4) main this PR
result device lost at 359 s no error to the end of the run, 590 s
heap mirror device memory 10126 MiB 5125 MiB
peak GTT / RSS 15409 / 15551 MiB 13907 / 14393 MiB

Over all runs on this machine, including earlier heads of both: main lost the device in 9 of 9 (342-372 s), this branch ran to the end in 12 of 12.

New test agc_driver_resident_heap_mirror:

  • It fails on main: the heap mirrors of two 8192 KiB heaps hold 16384 KiB where the guest and the GPU wrote 192 KiB.
  • With the readback fix for cached address spaces removed (only the completion check kept), the GPU's stores never reach the guest: the GPU store of thread 1 into a never-written heap block reached the guest as 0x0, expected 0x4.
  • Its joined-import part fails with the epoch bump removed (a heap mirror's range is no longer write-watched).
  • It also checks that a heap mapped at a freed heap's address reads its own bytes.
  • With no sparse address space (SetResidentAddressSpaceLimit), the two heaps are mirrored whole and the reads and GPU stores still match. With that path replaced by a throw it fails 3 of 3 on the 680M and on lavapipe, and passes 3 of 3 on each with it.

New test agc_driver_device_queue_plan covers queue selection for 9 queue family layouts.

On this head (rebased onto bf2c63f), all 96 tests that match the changed areas (agc_import_watch, agc_*host_import*, agc_driver_bda*, agc_driver_address*, agc_driver_image_*, agc_driver_graphics, the heap mirror unwatch tests from #6646, the two new tests and others) pass on the NVIDIA RTX 5080 and lavapipe. On the RADV 610M 94 pass; agc_driver_image_table_vouched_lookups and agc_driver_guest_memory_lifetime fail there and also fail on main 674c253 on the same GPU (3 of 3 runs each). agc_driver_native_depth_bda_clear is skipped on RADV and lavapipe as on main.

Driver suite (agc_driver|recompiler) with this PR: lavapipe 541/541; RADV 680M 539/541, the two failures also fail on main (agc_driver_tessellation, a Mesa 26.2 crash, and agc_driver_bda_device, see #7584); NVIDIA agc_driver|bda|mirror|import 555/555. On the rebased head the full suite passes on lavapipe (1654/1654) and on the RADV 680M all but the four that also fail on main there (agc_driver_tessellation, agc_driver_bda_device, agc_driver_image_table_vouched_lookups, agc_driver_guest_memory_lifetime); On 2d50c08 (the Windows test fixes from @Zenodeon) the Windows VM (WinLibs GCC 15.2, no GPU) builds the PR and passes 1760 of 1761 tests; the one failure, agc_driver_image_array_window_past_layer_limit, does not compile there and fails the same way on the merge base (#8310 fixes it). The VM has no GPU; @LeandroLP23 ran the heap mirror tests on this head on a Radeon 860M (Windows 11, AMD driver 32.0.13058.8001): 8 of 8 pass in 4 runs, and agc_driver_resident_heap_mirror runs there instead of skipping (comment).

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: PPSA01325
  • GPUs tested: NVIDIA RTX 5080 (driver 615.78.08, Linux), AMD Radeon 610M and 680M (RADV Mesa 26.1.4 / 26.2.4, Linux), lavapipe; AMD Radeon 860M (Windows 11, run by @LeandroLP23)
  • AI-assisted: yes

@github-actions github-actions Bot added unblocks title Names a title ID on the Unblocks line build agc libSceAgc* labels Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 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 8016 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L73, L2286-2288, L2544-2545, L2579-2583, L5246-5247, L5277, L5280-5282, L5284
PR 8380 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L2562-2563, L2571-2574, L2579-2580, L2606-2608, L2657-2664
PR 8874 core/libs/prx/libSceAgcDriver/CMakeLists.txt L339, L2517
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L7, L5237-5238, L5246-5247, L5334
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp L3050
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp L11238
PR 9289 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L4882-4890
PR 9551 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L2111-2112, L2659-2660
PR 9670 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp L1580-1584, L1596-1610

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 1191 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 1896 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 4040 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 4449 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 4472 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5441 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6146 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6169 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6194 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6235 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6292 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6501 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6548 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6615 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6741 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6804 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6868 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6959 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6960 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 7256 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 7369 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7410 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7446 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7618 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7622 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 7705 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7803 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8004 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 8014 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 8180 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 8246 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8365 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8372 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8395 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8527 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 8542 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8552 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8608 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8612 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 8672 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 8735 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 8963 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 8999 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9020 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9193 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9243 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9248 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9335 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9356 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9387 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9400 core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9412 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9431 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9432 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9489 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 9533 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9672 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9673 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9674 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9675 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9676 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9717 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9742 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9748 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9852 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 9891 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9892 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9893 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9895 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 9898 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 10026 core/libs/prx/libSceAgcDriver/CMakeLists.txt

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

Static review plus a small arm64 repro on macOS (Apple M2 Max, macOS 27.2, Apple clang 21), head e6a2bcbbf. I did not build the AGC driver: main does not build it on macOS. The macOS series in #7332 does build it, and there GuestMemory.cpp reads regions with mach_vm_region instead of /proc/self/maps.

The capability check now runs after a /proc read, so the refusal message changes on hosts without procfs.

  • GuestBufferMemory.cpp:2406 calls GuestNonzeroBlocks before make_shared<ResidentBuffer> at line 2410, which is where ResidentMirror.cpp:342-343 rejects a device without sparseBinding or sparseResidencyAliased.
  • GuestNonzeroBlocks → possiblyNonzero → mappings() opens /proc/self/maps under #ifndef _WIN32 (ResidentMirror.cpp:217-218). populatedPages does the same with /proc/self/pagemap (line 256).
  • On macOS, the first heap mirror therefore throws AGC graphics: cannot read /proc/self/maps for a heap mirror's pages. That error is raised outside the try that wraps it in heapMirrorError, so it carries neither heap mirror of 0x nor sparse binding.
  • heapMirrorsRefused (BdaResources.cpp:1218-1222) requires both strings, so on MoltenVK, which the PR body lists as lacking the sparse features, its Require fails instead of reporting "refused ... mirror behaviour not tested".

Repro: I extracted mappings(), Require, the ResidentBuffer capability throw and the heapMirrorsRefused predicate with sparseBinding = false:

PR order (GuestNonzeroBlocks first):
  error: AGC graphics: cannot read /proc/self/maps for a heap mirror's pages
  heapMirrorsRefused predicate: false -> Require fails
capability checked first:
  error: AGC graphics: heap mirror of 0x10000+0x10000: the device has no queue with sparse binding for a resident heap mirror; ...
  heapMirrorsRefused predicate: true

Suggestion:

  1. Check context.sparseBinding && context.sparseResidencyAliased before GuestNonzeroBlocks at line 2406, for example with a static ResidentBuffer::Supported(context) that throws through heapMirrorError. This keeps the refusal the same on every host.
  2. Gate the mappings()/populatedPages() path on __linux__ rather than !_WIN32, and give other hosts the conservative _WIN32 branch (maybe.push_back({begin, end, true})). A sparse-capable non-Linux device then still gets a working mirror instead of a procfs error.

AI-assisted: yes (Claude Code)

@LeandroLP23

Copy link
Copy Markdown
Contributor

AMD on Windows run. Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), Windows 11 26300, WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON. Build: main e8ad642fe with #7651, #7650, #7647, #7655 and #7648 merged together (each merge clean, built once).

agc_driver_bda_device, agc_driver_device_queue_plan, agc_driver_graphics, agc_driver_native_depth_bda_clear, agc_driver_tessellation and agc_import_watch pass 3 runs of 3. agc_driver_resident_heap_mirror skips itself here with skipped, no guest write watch: heap mirrors need one, so this host doesn't exercise the heap-mirror path this PR changes.

AI-assisted

@savvaniss

Copy link
Copy Markdown
Contributor

Windows data: with this PR the 15 tests built from tests/BdaResources.cpp, DeviceQueuePlan.cpp and ResidentHeapMirror.cpp pass on an Intel UHD 770 and an AMD RX 9070 XT, but the new agc_driver_resident_heap_mirror skips on both: skipped, no guest write watch: heap mirrors need one.

Windows 11 Pro 26300, WinLibs GCC 15.2.0 r7, Release, every execution test built, ANYPS5_NO_SHADER_CACHE=1, serial CTest. Intel UHD Graphics 770 (8086:a780), driver 32.0.101.7088. AMD Radeon RX 9070 XT (1002:7550), driver 32.0.31041.1004, with VkCache emptied first. This head e6a2bcbbf merged into main f3059421f.

Tests, ×3 UHD 770 RX 9070 XT
This head: 15 tests 14 pass, agc_driver_resident_heap_mirror skips 14 pass, agc_driver_resident_heap_mirror skips

The test files don't build on main without the rest of the PR, so there is no before-run. On Windows the heap-mirror path itself is therefore not exercised by this test.

I also ran full CTest with this PR and the 14 other open AGC PRs I tested in the same batch merged together (#7641, #7630, #7627, #7603, #7600, #7591, #7579, #7535, #7533, #7474, #7462, #7050, #7048, #7650). Neither GPU showed a failure beyond main's own, except #7650's new test.

AI-assisted: yes (Claude Code)

@fma-rc

fma-rc commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@jotitadev thanks, fixed in 71ca66c. A device without sparse binding, buffer residency or aliased residency now keeps the whole-range copy main makes instead of throwing, and the nonzero-block scan (the /proc/self/maps and pagemap reads) only runs when the device has all three, so a host without procfs takes the copy path without touching /proc. heapMirrorsRefused is gone with the refusal; the new test checks the copy path with SetResidentAddressSpaceLimit instead. On this head the 86 tests that match the changed areas pass on the RTX 5080, Radeon 610M and lavapipe, and the full ctest in a Windows VM passes 1537/1537.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@3mro777o

Copy link
Copy Markdown

This is another reproduction of the #7047 pattern, this time on a discrete AMD GPU with most of its VRAM free. On this title the mirrored ranges are almost all untouched guest memory, which is the case this PR targets.

Setup:

  • RX 6700 XT (gfx1031, 12 GiB VRAM), 16 GB RAM, so the GTT is 7934 MiB.
  • Linux 7.2, RADV Mesa 26.2.2.
  • Title: Bugsnax (PPSA01503) 02.000.000.
  • Build: main 8816fd7, plus a local fix for the earlier DB_ALPHA_TO_MASK = 0xe701 stop. A PR for that fix will follow.

Stop: about 10 s after the overworld starts loading, the run fails with radv/amdgpu: Not enough memory for command submission (the kernel logs the same *ERROR*), then AGC graphics: vkQueueSubmit recorder: Vulkan result -4. Three runs stopped the same way.

Memory at the stop (mem_info_*_used sampled once a second, APS5_PROFILE_DRAW=1):

value
GTT used 7850 of 7934 MiB (31 MiB before the start)
VRAM used 3004 of 12272 MiB
MemAvailable about 1 GiB
[buffers] image mirrors 388 ranges, 7320 MiB, all heap mirrors, 0 sparse heaps bound
host imports 2570 MiB live of 3967 MiB; 4 refused (9728 MiB), the last 0x2f8c40000+0x98000000

What is mirrored: the title makes four 2432 MiB (0x98000000) GPU pools and prints how much of each it uses:

###   MEMORY:  CPU: 130/1536 mb GPU1: 2430/2432 mb GPU2: 67/2432 mb GPU3: 0/2432 mb GPU4: 0/2432 mb

I have not run this PR on the title. It conflicts with current main in GuestBufferMemory.cpp, around the new heap mirror budget and sweep, and I did not want to resolve that conflict myself. Once it is rebased I can run Bugsnax on this machine and report the mirror totals and whether the stop goes away.

AI-assisted: yes (Claude Code)

@fma-rc
fma-rc force-pushed the fix/agc-heap-mirror-resident branch from 71ca66c to 0670cec Compare October 19, 2026 12:37
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@fma-rc
fma-rc force-pushed the fix/agc-heap-mirror-resident branch from 0670cec to 7472e23 Compare October 19, 2026 21:22
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@Zenodeon

Copy link
Copy Markdown
Contributor

I tested this head (7472e230e) on Windows and found a test-coverage gap in ResidentHeapMirror.cpp: GuestWriteWatchAvailable_nid_postfix() always returns false on Windows, where AGC instead uses the tracked guest arena. The resident-mirror test therefore skips before exercising the implementation. The fixture's separate VirtualAlloc heaps are also outside the arena that Windows AGC write tracking covers.

I prepared the small test-only correction below:

  • Check GuestArenaWriteWatched_nid_postfix() on Windows, keeping the existing check on other platforms.
  • Allocate and commit the Windows output/heap blocks through GuestArena.
  • Reset and release them through the arena so the address-reuse case actually creates a fresh mapping.

On your unmodified head, the resident-mirror test reports Skipped. With only this fixture correction, 4/4 focused checks pass without skips: agc_driver_resident_heap_mirror, agc_driver_device_queue_plan, agc_driver_bda_device, and agc_driver_bda_span_reads. The resident-mirror case takes 0.53 seconds and retains its reads/stores, later CPU updates, address-reuse, joined-import retirement and non-sparse fallback checks. The two shared BDA checks also pass on current main 3fd1cc393.

Machine: Windows 11 build 26200, Intel i7-13700, NVIDIA RTX 4070, driver 610.88; GCC 15.2, Release, SPIR-V Tools enabled. Both main and candidate checks used ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1, and VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, with verbose CTest and a 30-second timeout. Neither log contains a VUID error or missing requested validation layer.

This validates the focused test behavior on this machine, not a full Astro gameplay run or an FPS/RAM improvement. The runtime implementation is yours; this patch only enables the existing regression on Windows. Would you like to include it here?

Test-fixture correction
diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp
index 7657a227d..44cd382a4 100644
--- a/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp
+++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResidentHeapMirror.cpp
@@ -3,6 +3,7 @@
 #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
 #include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp"
 #include "prx/libc/include/GuestAllocations.hpp"
+#include "prx/libc/include/GuestArena.hpp"
 #include "prx/libc/include/GuestWriteWatch.hpp"
 #include "Recompiler.hpp"
 #include "VulkanTestDevice.hpp"
@@ -49,7 +50,8 @@ class OutputBlock {
 public:
     OutputBlock() {
 #ifdef _WIN32
-        block = static_cast<std::uint32_t*>(VirtualAlloc(nullptr, Block, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
+        block = static_cast<std::uint32_t*>(GuestArena::GuestArenaAllocate_nid_postfix(Block, Block));
+        if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, Block, PAGE_READWRITE, Block);
 #else
         block = static_cast<std::uint32_t*>(std::aligned_alloc(Block, Block));
 #endif
@@ -60,7 +62,8 @@ public:
     ~OutputBlock() {
         GuestAllocations::Mutation().Remove(block);
 #ifdef _WIN32
-        VirtualFree(block, 0, MEM_RELEASE);
+        GuestArena::GuestArenaReset_nid_postfix(block, Block);
+        GuestArena::GuestArenaRelease_nid_postfix(block, Block);
 #else
         std::free(block);
 #endif
@@ -80,8 +83,9 @@ public:
     explicit GuestHeap(bool writable, std::uint64_t ranges = 1, std::uint64_t rangeBytes = HeapBytes, void* at = nullptr) : ranges(ranges), rangeBytes(rangeBytes) {
         const auto bytes = rangeBytes * ranges;
 #ifdef _WIN32
-        raw = VirtualAlloc(at, bytes, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE);
+        raw = at != nullptr ? GuestArena::GuestArenaAllocateFromHint_nid_postfix(reinterpret_cast<std::uintptr_t>(at), bytes, Block) : GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
         Require(raw != nullptr && (at == nullptr || raw == at), "resident heap mirror: cannot allocate the guest heap");
+        GuestArena::GuestArenaCommit_nid_postfix(raw, bytes, PAGE_READWRITE, Block);
         base = static_cast<std::uint8_t*>(raw);
 #else
         raw = mmap(at, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | (at != nullptr ? MAP_FIXED_NOREPLACE : 0), -1, 0);
@@ -100,7 +104,8 @@ public:
         }
         GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(base, rangeBytes * ranges);
 #ifdef _WIN32
-        VirtualFree(raw, 0, MEM_RELEASE);
+        GuestArena::GuestArenaReset_nid_postfix(raw, rangeBytes * ranges);
+        GuestArena::GuestArenaRelease_nid_postfix(raw, rangeBytes * ranges);
 #else
         munmap(raw, rangeBytes * ranges + Block);
 #endif
@@ -325,7 +330,12 @@ int main() {
     try {
         const auto device = OpenVulkanTestDevice();
         if (!device) return VulkanTestSkipped;
-        if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
+#ifdef _WIN32
+        const bool watched = GuestArena::GuestArenaWriteWatched_nid_postfix();
+#else
+        const bool watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix();
+#endif
+        if (!watched) {
             std::puts("skipped, no guest write watch: heap mirrors need one");
             return VulkanTestSkipped;
         }

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 20, 2026
A heap mirror is now a sparse aliased buffer of 64 KiB blocks: blocks
that hold nonzero guest bytes are bound to device-local memory with a
host copy, and every other block aliases one zero block, bound as the
guest writes it. A device without sparse binding, buffer residency or
aliased residency, or without the sparse address space for a mirror,
keeps the whole-range copy of main.

Changed bytes reach the device by copies recorded before the build's
work, and bytes the GPU wrote, also into the mirrors a cached address
space holds, are copied back after it, so no mirror block needs a
host-visible memory type. A device read of a mirror inside a GPU
completion throws.

Sparse binds take a free queue only. Main and settle queue selection is
unchanged and the settle queue is kept: when no free queue binds, binds
go to the main queue under its lock. With dma-buf import on Linux, the
settle queue that main dropped when the sparse family was a one-queue
settle family is now kept.

A new joined host import bumps the import epoch, so the next build
releases heap mirrors over its ranges instead of refreshing a range that
is no longer write-watched.
@fma-rc
fma-rc force-pushed the fix/agc-heap-mirror-resident branch from 7472e23 to ed8f15b Compare October 20, 2026 16:01
@keravyn-ops

Copy link
Copy Markdown
Contributor

Windows / Radeon 780M (Ryzen Z1 Extreme) run of this PR with SILENT HILL: Townfall (PPSA33286), the title from #7047.

Setup

  • AMD Ryzen Z1 Extreme, Radeon 780M, 16 GiB shared (12.7 GiB visible to Windows), Windows 11 26100, AMD driver 32.0.23044.19 (Vulkan driverVersion 2.0.388)
  • vulkaninfo: sparseBinding, sparseResidencyBuffer and sparseResidencyAliased are all true; the graphics, a compute and a transfer family carry SPARSE_BINDING
  • A: v0.1.3 release (3fd1cc393b85); B: this PR at ed8f15bc1 (base 636aa09cb), built with WinLibs GCC 15.2.0, --target libs relinker, eboot relinked with the PR's relinker. A and B differ by the main commits between v0.1.3 and the PR base as well as by this PR.
  • Same eboot for both. Disclosure: its PS5.DefaultBackBufferWidth/Height defaults are patched from 3840x2160 to 1280x720 (two mov edx, imm32), since the title registers 4K display buffers otherwise. -nothreadtimeout in uecommandline.txt.
  • APS5_PROFILE_DRAW=1, APS5_ADDRESS_COPY_MAX_MIB=8192, warm shader cache, no input (title sequence), 360 s per run, process memory sampled every 5 s

Results (360 s each)

A: v0.1.3 B: #7647
presents / average FPS 360 / 1.01 360 / 1.01
[buffers] image mirrors at the end 3648 ranges, 1275.0 MiB, heap 1273.4 MiB 3601 ranges, 1280.9 MiB, heap 1073.9 MiB
heap mirrors released for imports 31565 (2440.4 MiB) 33183 (2111.4 MiB)
GPU shared usage, peak 3559 MiB 3104 MiB
app.exe private bytes, peak 9244 MiB 9461 MiB
system RAM free, minimum 1770 MiB 1212 MiB
[guestmem] read (cumulative) 10.2 M calls, 730970 MiB, 79.2 s 9.3 M calls, 652897 MiB, 73.5 s

Observations

Logs and the sampled CSV are available if useful.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 20, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

bindSparseMirror no longer catches a failed acquireMirror. On main (from b88ad02) a sparse descriptor heap whose mirror can't be made prints [gpu] ...; copying the sparse heap instead and returns false, so the caller copies the heap for that dispatch. On ed8f15b the same failure (past APS5_HEAP_MIRROR_MIB, a buffer or allocation failure, or a sparse bind error) is thrown out of the build instead. The description doesn't mention this, and no test covers it.

Every new throw path in acquireMirror returns its reserved bytes to heapBytes before throwing, so keeping main's try/catch looks safe. Could you restore it, or say why throwing is right now and add a test for it?

@fma-rc

fma-rc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

You're right, that catch shouldn't have gone. Restored in 0b9b519: a sparse heap whose mirror can't be made prints the same [gpu] ...; copying the sparse heap instead line and is copied for that dispatch, as on main.

New test agc_sparse_heap_mirror_budget: an 8 MiB sparse heap with 5 MiB committed, past the test's 4 MiB APS5_HEAP_MIRROR_MIB. It checks that the heap is copied up to its last committed byte, that a store is written back, and that no mirror or held bytes are left. Without the catch it throws past the budget; with it, it passes on NVIDIA, RADV and lavapipe, along with the other heap mirror tests.

@LeandroLP23

Copy link
Copy Markdown
Contributor

A Windows / AMD APU data point: Radeon 860M (1002:1114, driver 32.0.13058.8001, 512 MiB UMA carve-out on 32 GiB), Windows 11 26300, WinLibs GCC 15.2.0, Release. I tested this PR at 0b9b519a5e, merged onto main ec7eacc62, with ANYPS5_REQUIRE_VULKAN=1 and ANYPS5_NO_SHADER_CACHE=1.

Driver support. vulkaninfo reports sparseBinding, sparseResidencyBuffer and sparseResidencyAliased as true, with sparseAddressSpaceSize 0x7ffa00000000. The graphics, compute and transfer families all carry SPARSE_BINDING.

agc_sparse_heap_mirror_budget doesn't start on Windows. It exits with 0xc0000135 (DLL not found) because it isn't in the if(WIN32) set_tests_properties(... ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") list next to it, or in the TIMEOUT 30 list. With build/core/libs/libs/unpatched prepended to PATH by hand, it passes:

[gpu] AGC graphics: heap mirror of 0x200000000+0x800000: past the budget; heap mirrors hold 0 MiB of 4 MiB (APS5_HEAP_MIRROR_MIB); copying the sparse heap instead
a sparse heap whose mirror would pass the heap mirror budget is copied

agc_driver_resident_heap_mirror skips on Windows: skipped, no guest write watch: heap mirrors need one. So no test runs the resident path on Windows. Its fixture doesn't take its heaps from write-watched guest memory, which on Windows means the guest arena.

Other tests. The other 43 tests matching resident_heap_mirror|device_queue_plan|heap_mirror|host_import|import_watch|bda|address pass, agc_driver_device_queue_plan among them; agc_host_import_sparse skips.

At runtime it didn't run. I ran four alternating SuperTuxKart races (PPSA69421), two on main and two with this PR, with APS5_HOST_IMPORT_MIB=1056. That leaves 5,000–23,000 imports refused per run, but the title makes no heap mirrors at all: the refused ranges are served by copies. FPS is the same in both trees, about 2.1 in the race, and neither tree logs an error. So none of the titles I have here exercise the resident path, and I can't add an end-to-end measurement to keravyn-ops's 780M run.

@Zenodeon

Copy link
Copy Markdown
Contributor

@fma-rc I prepared the Windows test fixes in fma-rc#4, targeting your fix/agc-heap-mirror-resident branch so you can merge it directly (or cherry-pick 11e02f25438bcf809fcddad57f643ff73ca42f7e).

It adds the missing DLL-path setup and 30-second timeout for agc_sparse_heap_mirror_budget, and moves the Windows resident fixture to guest-arena allocations with the appropriate write-watch probe and cleanup. All existing assertions are retained; production code is unchanged.

Both reported issues reproduced before the fix. Afterward, 12 unique focused tests pass with zero skips on Windows 11 / RTX 4070, driver 610.88; the resident test actually executes and passes. Matched Vulkan controls on main and the fix show identical known older-layer diagnostics, with no new validation-error signatures or missing-layer messages. Full details are in the fix PR. AI-assisted: yes, OpenAI Codex.

@fma-rc

fma-rc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Zenodeon, cherry-picked as 2d50c08 with you as author. It adds agc_sparse_heap_mirror_budget to the Windows PATH and timeout lists and moves the resident fixture to guest-arena heaps, the write-watched memory whose absence made agc_driver_resident_heap_mirror skip for @LeandroLP23.

On this head the Windows VM build passes 1760 of 1761 tests; the failing one doesn't compile on the merge base either (#8310). The VM has no GPU, so @LeandroLP23, if you have time, a run of resident_heap_mirror|heap_mirror on the 860M would be the first Windows GPU run of the resident path.

@LeandroLP23

Copy link
Copy Markdown
Contributor

@fma-rc ran heap_mirror on head 2d50c08473 on the Radeon 860M (Windows 11, AMD driver 32.0.13058.8001, MinGW GCC 15.2, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1): 8/8 pass, and 3 more repeats pass too.

  • agc_driver_resident_heap_mirror now runs instead of skipping. It selected the 860M and printed resident heap mirror tests passed (0.89 s). Its log also shows the import write watch holding: host imports of write-watched memory keep the write watch: yes.
  • agc_sparse_heap_mirror_budget now finds libc.dll and passes (0.10 s).
  • agc_heap_mirror_unwatch, agc_sparse_heap_mirror_unwatch, agc_heap_mirror_run_unwatch and their _no_space_cache variants pass.

The build used -k 0; the only target that didn't build is ImageArrayWindowPastLayerLimit.cpp, which fails the same way on main. The test binaries were rebuilt from this head.

@3mro777o

3mro777o commented Oct 21, 2026 •

Copy link
Copy Markdown

@fma-rc here is the Bugsnax run I promised, on head 2d50c08473 merged onto main a6dcea927.

Setup: RX 6700 XT (gfx1031, 12 GiB VRAM), 16 GB RAM (GTT 7934 MiB), Linux 7.2, RADV Mesa 26.2.5, Bugsnax (PPSA01503) 02.000.000, APS5_PROFILE_DRAW=1. vulkaninfo reports sparseBinding, sparseResidencyBuffer and sparseResidencyAliased as true. Current main still stops this title at DB_ALPHA_TO_MASK = 0xe701, so both builds skip that check (a test-only local change, not part of either side). Each run lasts up to 120 s, and a monitor stops it if GTT passes 85 %.

main this PR
runs 1 3
GTT peak 7898 MiB at 15 s (stopped by the monitor) 3909, 5855, 5886 MiB
heap mirrors ([buffers] image mirrors) 3 of the 2432 MiB pools, ~7.3 GiB (as in my earlier comment) 100 / 2502 / 2526 MiB heap, 0 sparse heaps bound
title's own counter at the end GPU1: 2429 GPU2: 39 GPU1: 2430 GPU2: 2429 GPU3: 38–62 (2 runs)
end GTT exhausted 2 runs reach 120 s with no Not enough memory; 1 run hits a general protection fault in libc.prx.guest at 16.5 s
  • The stop goes away in 2 of 3 runs. The title loads past where main stopped, and fills its second pool, while GTT stays under 6 GiB. In those runs the overworld was playable: the character could be moved around normally.
  • The fault run had its mirror heap at 100 MiB when it crashed, so I don't think the mirrors caused it. I've seen the same kind of guest libc fault on another title once host imports start succeeding. I haven't tied it to this PR.
  • Import budget: in all three runs the host import budget still drops to ~2.5 GiB after 3 failed vkAllocateMemory calls. That's separate from this PR.

@3mro777o

3mro777o commented Oct 21, 2026 •

Copy link
Copy Markdown

Follow-up: a longer manual session on the same build, about 22 minutes of play in the overworld and cutscenes, with the window at 2560x1440 and the title rendering at 3840x2160.

  • Memory: GTT peaked at 5735 MiB and the heap mirrors stayed around 2.5 GiB. There was no Not enough memory and no device loss.
  • Rendering: I saw no missing or wrong textures.
  • Frame rate: 15–30 FPS, about 20 on average. It dropped to 9.5 FPS in one cutscene. The overlay showed DRIVER: 100 ms of a 105 ms frame there, so the time is spent on the CPU side of the driver, not in mirror memory.
  • End of the session: it ended at 23 min (1396 s), right after I chose New Game from the menu, with a general protection fault at the same place as the 16.5 s run above, libc.prx.guest.prx+0xb6ff. The last log line was a texture load (/app0/Content/Models/Environment/TerrainTextures/DesertSand01_D.gnf). main never runs this long on this machine, so I cannot tell whether the fault is new with this PR. I have not looked into it yet.

@EagleBearerYS

Copy link
Copy Markdown
Contributor

@fma-rc a data point for this PR from #9514's thread: parl0v ran Sword of the Sea (PPSA25344) 01.510.000 on Windows 10, with an RTX 4060 8 GB and 16 GB of RAM (comment). Once #9514 gets the New Game load past the copy cap, it stops on heap mirror of 0x32af9e0000+0x100000: vkAllocateMemory buffer ... heap 1 7704/7448 MiB; heap mirrors hold 2858 MiB, and main with APS5_ADDRESS_COPY_MAX_MIB=1024 stops on the same heap. This PR binds heap mirrors in device-local memory, only where the guest holds bytes, so with it the mirrors wouldn't come from that heap, provided the 4060 reports sparseBinding, sparseResidencyBuffer and sparseResidencyAliased. I've asked parl0v whether they'd try #9514 with this PR.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

@fma-rc a coordination note, since #9551 edits the same heap mirror code.

What #9551 does:

  • bindSparseMirror falls back to a joined range over a run of adjacent registered ranges, a new joinedHeapRange with a small cache.
  • sweepMirrors first releases joined ranges whose member ranges have expired.
  • releaseImportedMirrors releases a heap mirror only when the imported range covers the mirror's reserved span, so importing one member no longer drops a joined mirror.

How it meets this PR: git merge-tree of the two heads reports two textual conflicts in GuestBufferMemory.cpp, both mechanical:

  1. Both PRs insert code just before void sweepMirrors(); (the joined-range cache here, blockBytes and the related helpers there). Keep both.
  2. Both edit the release loop in releaseImportedMirrors. Keep perf(agc): mirror a sparse heap whose committed span crosses adjacent ranges #9551's found->second->reserved > bytes condition together with this PR's accounted / unlistMirror accounting.

Neither depends on the other: #9551 changes which range a mirror is built over and when it is released, and this PR changes how a mirror's memory is allocated and bound. Whichever merges second needs the two resolutions above, and I'm happy to rebase #9551 onto this one if it lands first.

@winstrex

Copy link
Copy Markdown
Contributor

@fma-rc #9670 adds a presentation queue in the device-creation block that this PR moves into PlanDeviceQueues. A merge of the two conflicts only there, in VulkanDevice.cpp. #9670 is small (+62/−7) and its open review items are process ones, so I'd propose that it lands first and that this PR adds the presentation queue to its plan when it rebases. If you'd rather this one went first, I'll rebase #9670 on top of it instead. Just say which you prefer.

Whichever lands second has to keep this:

  • With a window, the presentation queue is the next free index of the main family, after the main and settle queues: SelectPresentQueue(families[family].queueCount, mainCount). In the plan that is one more field (e.g. presentIndex), the main family's queueCount becomes presentIndex + 1, and priorities needs three entries.
  • Without a free index there is no presentation queue, and the present stays on the main queue under the GPU mutex (RADV and lavapipe).
  • The sparse queue never takes an index of the main family, so it doesn't compete with the presentation queue. A DeviceQueuePlan case such as "settle on the main family, three queues, with a window" would cover that.

@tibzejoker

Copy link
Copy Markdown
Contributor

Windows notes from GTA V (PPSA04264, RTX 4090 + Intel UHD 770). They come from my own mirror experiment on an older main (around 2026-10-15), not from this PR.

Device-local vs import. On a BDA bench with 64 MiB nodes, dependent reads took 210 ms from a host import and 0.78 ms from device-local memory on the 4090 (x271). On the UHD 770 (UMA) the ratio was 1.0. So on APUs a mirror only costs memory, and binding only written blocks is a pure win. On discrete GPUs mirrors are a big speed lever, and keeping them current costs the most.

What sank my version: GPU writes. I used budgeted, chunked mirrors. In GTA, GPU writes into mirrored blocks made about 2.4 GB of copies per 10 s, and FPS fell from 2.0 to about 1.5. This PR copies GPU-written bytes back after each build. A copy-back bytes per 10 s count in the [buffers] line would show that cost early.

Windows nonzero scan. On Windows possiblyNonzero returns the whole range, so allZero reads every candidate byte. If that includes committed pages the guest never touched, each read makes Windows allocate a zero page. On a large, mostly empty heap the working set could grow by the empty part, and the scan could take seconds. GTA has one 4.6 GB direct-memory range, and its import was refused here once. Worth checking.

I can run GTA on your head and main ([buffers] lines, working set, FPS).

AI-assisted: yes (Claude Code)

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* build unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.