Repository navigation
Proposal: place GPU-oriented guest memory in VRAM via SAM / Resizable BAR (Linux, RX 9070 XT) #2
Description
Status (2026-10-09)
- Research: source-level results are in this comment.
- RADV support:
memoryMapPlacedandmemoryUnmapReserveare supported, alignment is the page size, and placed VRAM pays no per-submit cost.memoryMapRangePlacedis false: each guest mapping needs its own allocation, which answers that open question.- Risk: CPU-visible VRAM is a write-combined I/O mapping.
vram_bench accesstests atomics,read(), O_DIRECT, futexes andprocess_vm_readvon it.- Tools: for Steps 0–2 on branch
groundwork.- Next: remaining work and the order to do it in: NATIVE_RDNA_NEXT.md.
Proposal
Place the guest memory a PS5 title uses for GPU data in VRAM, mapped into the CPU through SAM (Resizable BAR) at the guest address the title expects. CPU-oriented memory stays in system RAM. The GPU then reads that data at VRAM bandwidth instead of over PCIe, with no imports, no pinning and no copies for it.
- Hardware: Radeon RX 9070 XT (16 GB) with SAM enabled, on Linux with RADV.
- Independence: this is separate from Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1, the native RDNA4 path, and neither depends on the other.
- First target: upstream's existing SPIR-V/Vulkan path, which runs every title upstream runs today.
- Later: the same placement serves Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1's direct amdgpu backend.
Why: what guest memory costs today
A PS5 title assumes its CPU and GPU share one memory. On a PC with a discrete GPU they don't, and upstream's driver spends a lot of effort on that gap:
| Evidence | Measured cost | Source |
|---|---|---|
| Read-only guest buffers copied for every draw | ~4 ms of every draw copying ~30 MiB, ~250 GB per 10 s, before snapshots were reused | #276 |
| Host imports of direct memory on AMD/Linux | every import failed (the kernel's userptr refuses memfd mappings), so every range was copied, until udmabuf imports | #943 |
| Many imported buffer objects, on an RX 9070 XT | each imported BO is validated at every submission: ~7000 imports made each vkQueueSubmit ~2.3 ms (vs 3.5 µs with 1000 ordinary allocations) |
#1152 |
| Pinning guest memory for import | ~0.3 s per GiB, under the device lock | #1961 |
| Write-back of GPU results (Silksong) | correct at 0.9 fps with eager write-back vs garbled at ~5 fps with lazy write-back, before later fixes | #1970 |
| Moving from synchronous round trips to host-imported memory with deferred write-back (Demon's Souls) | 13 s → ~0.1 s per frame | #5 |
How upstream handles guest memory on Linux today:
- Direct memory is backed by memfd (
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp). The driver imports the ranges the GPU uses as udmabuf dma-bufs (feat(agc): import shared direct memory through udmabuf on Linux boykopovar/AnyPS5#943), or as host pointers. - CPU writes are tracked (
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) so the driver knows what to copy or re-upload. - Read-only buffers are snapshotted and copied (perf(agc): reuse unchanged draw input snapshots across draws boykopovar/AnyPS5#276), and GPU results are written back (
Graphics/src/GuestBufferMemory.cpp). - No allocation uses host-visible VRAM. Every allocation is either GPU-only VRAM or host-visible system memory (
Graphics/src/GuestBufferMemory.cpp:1823,Graphics/src/BufferPool.cpp:58).
What SAM can and can't do
SAM lets the CPU map all of VRAM instead of a 256 MB window. The CPU still reaches it over PCIe, so:
- CPU reads from VRAM are very slow. The mapping is uncached or write-combined, so each load is a PCIe round trip of about a microsecond, against about a nanosecond for a cache hit. Vulkan exposes this memory as
DEVICE_LOCAL | HOST_VISIBLEbut notHOST_CACHEDon discrete GPUs. Data the CPU reads must stay in system RAM. - CPU writes into VRAM are fine. Write-combining merges them into full bursts.
- The GPU reads VRAM at full bandwidth, instead of reading system memory over PCIe.
So SAM gives a CPU window into GPU memory, not shared memory. The design depends on putting only the right memory there.
Design: placement by the title's own hints
Signals the title already gives:
- The memory type passed when it maps direct memory, or changes with
sceKernelMtypeprotect. Upstream records it for each mapping (DirectMemory.cpp:memoryType,DoMtypeprotect,QueryDirectMapping). - GPU access on the mapping. Upstream's allocation registry already knows which ranges were mapped with GPU access (fix(agc): import GPU-mapped guest memory when it is mapped boykopovar/AnyPS5#1961).
Policy:
- Mappings with GPU access and a GPU-oriented, write-combined memory type go into VRAM.
- Everything else stays in system RAM on the current path.
- PS5 titles already avoid CPU reads of GPU-oriented memory, because those reads are slow on the console too.
Mechanism on the SPIR-V/Vulkan path:
- Allocate
VkDeviceMemoryfrom aDEVICE_LOCAL | HOST_VISIBLEmemory type. - Map it at the guest address with
VK_EXT_map_memory_placed:vkMapMemory2KHRwithVkMemoryMapPlacedInfoEXT. This works likemmapwithMAP_FIXED. RADV has supported it since early 2024, and Wine uses it for WOW64. - The memory is the driver's own allocation, not an import. It needs no userptr or udmabuf and no pinning. Per perf(agc): share one dma-buf import per 64 MiB of Linux direct memory boykopovar/AnyPS5#1152's measurements, ordinary allocations don't carry the per-submit validation cost imports do.
- Buffers inside these ranges are bound directly. No snapshot copies, no write-back.
- Textures that the driver detiles into separate images still need upstream's dirty tracking, at least until Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1's surface design applies.
Mechanism on the native path (#1):
- A VRAM buffer object allocated through
libdrm_amdgpu. - Mapped into the CPU at the guest address (DRM render node
mmapwithMAP_FIXED), and into the GPU at the same virtual address.
Fallback and budget:
- Anything that cannot be placed stays on the current path.
- VRAM is 16 GB, and titles can reserve most of their 13.5 GiB of direct memory (BC5 reports 12.4 GiB of GPU-visible memory for Astro Bot). Render targets, detiled copies and the desktop also need VRAM.
- So placement runs under a budget. When VRAM is full, new GPU-oriented mappings fall back to system RAM, and the choice is logged.
Steps
Step 0: Check the hardware and driver
lspci -vvshows the GPU's BAR at full VRAM size.vulkaninfolists aDEVICE_LOCAL | HOST_VISIBLEheap of about 16 GB.VkPhysicalDeviceMapMemoryPlacedFeaturesEXTreportsmemoryMapPlaced(andmemoryMapRangePlaced). RecordminPlacedMemoryMapAlignment.
Step 1: Memory census
- Log every direct-memory mapping for Dreaming Sarah, and for a heavier title that runs on upstream Linux: memory type, size, GPU access, later type changes.
- Measure how often the CPU reads each GPU-oriented range, using a debug-only run:
- Map the range
PROT_NONEand count faults. - On x86-64 Linux, bit 1 of the page-fault error code (
REG_ERRin the signal'sucontext) separates writes from reads.
- Map the range
- Output: which memory types hold what, how much would go to VRAM, and whether any GPU-oriented range is read often enough to hurt in VRAM.
Step 2: Microbenchmark, standalone
Allocate DEVICE_LOCAL | HOST_VISIBLE memory and map it at a fixed address with VK_EXT_map_memory_placed. Measure:
- CPU streaming-write bandwidth (
memcpyinto it); - CPU read latency and bandwidth;
- GPU read bandwidth from it, against a udmabuf import of system memory;
vkQueueSubmitcost with N such allocations, against N imports (repeating perf(agc): share one dma-buf import per 64 MiB of Linux direct memory boykopovar/AnyPS5#1152's table).
Step 3: Integration
- libkernel: a VRAM backing kind for direct memory, chosen by the policy above. It is created through a hook the AGC driver provides, because it needs the Vulkan device.
- Guest arena: placed maps go into the arena's reserved range at the guest address, honouring
minPlacedMemoryMapAlignment. - Driver: binds these ranges directly, skips imports, snapshots and write-back for them, and keeps dirty tracking where derived copies (detiled textures) need it.
- Debug switches: one to turn placement off, and one to log every decision.
Step 4: Measurement
- Run Dreaming Sarah and the heavier title with placement off and on.
- Compare:
[guestmem]copy and compare bytes, import counts,vkQueueSubmittime ([recorder]lines underAPS5_PROFILE_DRAW=1), frame time and 1% lows (APS5_ENABLE_TIMING_LOG=ON). - Check correctness with frame dumps (
APS5_DUMP_FRAMES).
Milestones
- M0: ReBAR and
VK_EXT_map_memory_placedconfirmed on the RX 9070 XT (Step 0) - M1: memory census for two titles (Step 1)
- M2: microbenchmark results (Step 2)
- M3: placement integrated behind a switch (Step 3)
- M4: before/after measurements (Step 4)
Success criteria
- Copy, compare and write-back traffic for GPU-oriented ranges drops to near zero.
- Per-submit cost no longer grows with the number of GPU-used allocations.
- Frame time improves, or at least does not regress, on both titles.
- Frames match placement-off runs.
- Unsupported states throw, as upstream requires. No silent fallbacks beyond the logged budget fallback.
Open questions
- How does a placed map interact with upstream's guest arena reservation (
GuestArenain libc)? Can the arena's reserved pages be replaced in place, asMAP_FIXEDdoes? - Does upstream's Linux write tracking (
GuestWriteWatch, page queries) work on a placed device-memory mapping? - How do titles actually use memory types? Is any GPU-oriented memory read by the CPU often enough to hurt in VRAM? Step 1 answers this.
- What happens under VRAM pressure? The kernel can evict buffer objects to system memory. Is that transparent through the CPU mapping, and how slow is it?
- Is
memoryMapRangePlacedneeded, for a guest mapping that covers part of an allocation? - Can a title change a placed range's memory type to a CPU-oriented one, and does that need migrating the range back to system RAM?
- Native path (Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1): can a VRAM buffer object be mapped at a fixed CPU address through the DRM render node (
mmapwithMAP_FIXED) while staying "always valid" in the GPU VM?
Out of scope
- Windows: we don't know whether AMD's Windows driver supports placed maps, and there's no hardware here to test it.
- The native shader path. That is Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1.
- Changes to the relinker.
References
VK_EXT_map_memory_placed: proposal, reference page, Wine's use of it- Upstream PRs on guest memory: #5, #276, #943, #1152, #1961, #1970
- BC5 (same-address dma-buf mapping on the BC-250): https://github.com/RedMadKnight/BC5
- Upstream: https://github.com/boykopovar/AnyPS5
Activity
DenizSAHIN570 commentedon Oct 9, 2026
Research results: placed VRAM maps on RADV, and what can go wrong
This is from reading source code (Mesa
59f580b7a7f1, Linuxaf32da41b032, BC5181ebf8e58bb), not from runs on hardware. Mesa paths are relative tosrc/amd/; UNCONFIRMED marks anything the sources don't settle.RADV support (answers Step 0)
VK_EXT_map_memory_placedvulkan/radv_physical_device.c:887memoryMapPlaced:1284-1287memoryUnmapReserve:1284-1287memoryMapRangePlaced:1284-1287minPlacedMemoryMapAlignment:2319-2320docs/features.txt:670mmap(MAP_FIXED)on the DRM fd, which replaces the reservation (vulkan/radv_device_memory.c:286-314). An unmap with RESERVE puts a PROT_NONE mapping back.memoryMapRangePlacedopen question.Memory types with full Resizable BAR
vulkan/radv_physical_device.c:448-656).DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT. No host-cached VRAM type exists.VM_ALWAYS_VALID(vulkan/winsys/amdgpu/radv_amdgpu_bo.c:604-608) and cost nothing per submit. Imported, exported and host-pointer memory joins the global BO list and is validated on every submit (radv_amdgpu_bo.c:1199-1224), which matches upstream perf(agc): share one dma-buf import per 64 MiB of Linux direct memory boykopovar/AnyPS5#1152's measurement on this card. Placed VRAM avoids the per-submit validation cost that imports pay.vulkan/radv_device_memory.c:170-174), so placed guest memory is never compressed.radv_amdgpu_bo.c:563-584). Whether the CPU mapping survives eviction transparently is UNCONFIRMED.The risk: CPU-visible VRAM is a write-combined I/O mapping
amdgpu_vram_mgr.c:610-613) through an I/O PFN mapping (amdgpu_ttm.c:661-671).get_user_pages: O_DIRECT, shared futexes,process_vm_readv, userptr) are likely to fail (UNCONFIRMED).read()into GEM-mapped guest memory return EFAULT and moved away from that design (BC5 ADR 0006).vram_bench accessnow tests each case on a real placed mapping:read()into it andwrite()from it;process_vm_readv;mprotectto read-only and back.On lavapipe, using its ordinary-RAM memory, every test passes, which confirms the harness itself works. The results that matter come from the RX 9070 XT.
Built on branch
groundwork(nothing has run on the RX 9070 XT yet)APS5_MEMORY_CENSUS=<file>(libkernel): one CSV row per direct-memory allocation, release, mapping, protection change, type change and unmap, with memory type, protection and result. This is the Step 1 input. It logs from the internalDo*functions, so batch maps, AMM and the POSIX wrappers are covered.tools/native-rdna/vram-bench, with five commands:info: heaps and types, the placed-map features and alignment, udmabuf;placed: CPU write/read/latency, plus GPU read bandwidth from placed VRAM against device-only VRAM, GTT, host-pointer and udmabuf imports;access: the tests above;submits: a repeat of perf(agc): share one dma-buf import per 64 MiB of Linux direct memory boykopovar/AnyPS5#1152's comparison, ordinary allocations against imports;all: everything.It loads the system Vulkan loader at run time.
tools/native-rdna/host_check.py: Resizable BAR frommem_info_vis_vram_totalagainstmem_info_vram_total, udmabuf limits, ttmpages_limit, and the Vulkan checks throughvram_bench.Usage: NATIVE_RDNA.md. Remaining work and the order to do it in: NATIVE_RDNA_NEXT.md.
Additions to the open questions
memoryMapRangePlacedfalse, how many separate allocations does a title's direct-memory layout need, and does that collide with the guest arena's reservation granularity?vram_bench accesstests fail on the RX 9070 XT, and which guest operations (file reads into GPU memory, atomics, futexes) does the memory census show on GPU-oriented ranges?