Skip to content

Proposal: place GPU-oriented guest memory in VRAM via SAM / Resizable BAR (Linux, RX 9070 XT) #2

Open

Description

@DenizSAHIN570

Status (2026-10-09)

  • Research: source-level results are in this comment.
  • RADV support: memoryMapPlaced and memoryUnmapReserve are supported, alignment is the page size, and placed VRAM pays no per-submit cost.
  • memoryMapRangePlaced is 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 access tests atomics, read(), O_DIRECT, futexes and process_vm_readv on 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.


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:


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_VISIBLE but not HOST_CACHED on 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:

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:

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 mmap with MAP_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 -vv shows the GPU's BAR at full VRAM size.
  • vulkaninfo lists a DEVICE_LOCAL | HOST_VISIBLE heap of about 16 GB.
  • VkPhysicalDeviceMapMemoryPlacedFeaturesEXT reports memoryMapPlaced (and memoryMapRangePlaced). Record minPlacedMemoryMapAlignment.

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_NONE and count faults.
    • On x86-64 Linux, bit 1 of the page-fault error code (REG_ERR in the signal's ucontext) separates writes from reads.
  • 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:

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, vkQueueSubmit time ([recorder] lines under APS5_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_placed confirmed 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 (GuestArena in libc)? Can the arena's reserved pages be replaced in place, as MAP_FIXED does?
  • 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 memoryMapRangePlaced needed, 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 (mmap with MAP_FIXED) while staying "always valid" in the GPU VM?

Out of scope

References

Activity

  1. DenizSAHIN570 commented on Oct 9, 2026

    @DenizSAHIN570
    OwnerAuthor

    Research results: placed VRAM maps on RADV, and what can go wrong

    This is from reading source code (Mesa 59f580b7a7f1, Linux af32da41b032, BC5 181ebf8e58bb), not from runs on hardware. Mesa paths are relative to src/amd/; UNCONFIRMED marks anything the sources don't settle.

    RADV support (answers Step 0)

    Item Value Source
    VK_EXT_map_memory_placed always exposed vulkan/radv_physical_device.c:887
    memoryMapPlaced true :1284-1287
    memoryUnmapReserve true :1284-1287
    memoryMapRangePlaced false :1284-1287
    minPlacedMemoryMapAlignment page size :2319-2320
    Lavapipe not supported (CI needs ANV, NVK or turnip) docs/features.txt:670
    • How the map is placed: mmap(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.
    • Consequence: there is no range placement, so every placed map must cover a whole allocation. That means one Vulkan allocation per guest mapping, not a few big allocations sliced up. This answers the memoryMapRangePlaced open question.

    Memory types with full Resizable BAR

    • Heaps: two, GTT plus all of VRAM as CPU-visible (vulkan/radv_physical_device.c:448-656).
    • The type to use: type 3, DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT. No host-cached VRAM type exists.
    • Per-submit cost: ordinary allocations (not exported or imported) are created 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.
    • Compression: gfx12 compression is enabled only for no-CPU-access VRAM (vulkan/radv_device_memory.c:170-174), so placed guest memory is never compressed.
    • Eviction: VRAM buffers may spill to GTT under pressure (radv_amdgpu_bo.c:563-584). Whether the CPU mapping survives eviction transparently is UNCONFIRMED.
    • GPU address: a placed map's GPU address is RADV's high-range address. It sits at a constant offset from the guest address, not the same address. That's fine for upstream's page-table-based address path; the native path (Proposal: native RDNA4 path on Linux — PS5 shaders translated gfx1013 → gfx12, direct amdgpu submission (PoC: Dreaming Sarah) #1) maps through amdgpu at the same address instead.

    The risk: CPU-visible VRAM is a write-combined I/O mapping

    • What the kernel does: amdgpu maps VRAM for the CPU as write-combined (amdgpu_vram_mgr.c:610-613) through an I/O PFN mapping (amdgpu_ttm.c:661-671).
      • Uncached reads: every CPU load from it goes across PCIe.
      • Locked atomics: behaviour on it is UNCONFIRMED.
      • Kernel paths that pin pages (get_user_pages: O_DIRECT, shared futexes, process_vm_readv, userptr) are likely to fail (UNCONFIRMED).
    • Evidence: BC5 saw read() into GEM-mapped guest memory return EFAULT and moved away from that design (BC5 ADR 0006).
    • Why it matters: if titles read files straight into GPU-oriented memory, or use atomics or futexes on it, placement has to exclude those ranges or fall back.

    vram_bench access now tests each case on a real placed mapping:

    • locked atomics (fetch_add and CAS from 4 threads, checked);
    • read() into it and write() from it;
    • O_DIRECT reads;
    • private and shared futexes;
    • process_vm_readv;
    • mprotect to 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 internal Do* functions, so batch maps, AMM and the POSIX wrappers are covered.

    • tools/native-rdna/vram-bench, with five commands:

      It loads the system Vulkan loader at run time.

    • tools/native-rdna/host_check.py: Resizable BAR from mem_info_vis_vram_total against mem_info_vram_total, udmabuf limits, ttm pages_limit, and the Vulkan checks through vram_bench.

    Usage: NATIVE_RDNA.md. Remaining work and the order to do it in: NATIVE_RDNA_NEXT.md.

    Additions to the open questions

    • With memoryMapRangePlaced false, how many separate allocations does a title's direct-memory layout need, and does that collide with the guest arena's reservation granularity?
    • Which of the vram_bench access tests 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?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions