Layer memory module: PS4 GPU addresses without sparse (plan, 2026-10-09)¶
Why¶
The new memory model keeps game memory in a single sparse arena and moves blocks between game
memory and VRAM by rebinding (vkQueueBindSparse). On NVIDIA (Linux, drivers ≥ 555), the bind
time grows with the number of pages already bound, and the call holds a global lock: each
move to VRAM cost seconds, and the game and the desktop froze (GTX 1660 Ti, 615.71). Without moves
(BB_FIXED_ARENA=1) it runs at 48.6 FPS versus ~205 on an RX 7800 XT. We need a model that works the
same on AMD, NVIDIA and Intel and is not tied to the shadPS4 buffer cache: part of a future layer
for running PS4 games.
Idea¶
The PS4 GPU has its own virtual memory (GPUVM) on top of shared memory; every V#, T# and pointer in user data is a virtual address. On PC we do the same with Vulkan 1.2 means (BDA):
- Memory sources: ordinary (non-sparse) Vulkan buffers:
- chunks of game memory: dma-buf (AMD) or host memory import (NVIDIA, Intel), one chunk per game direct memory allocation (allocations are mapped whole, so a range inside one mapping is always contiguous within one chunk);
- VRAM mirrors: segments aligned to game addresses (game page X sits in the segment at the same offset), allocated when the first VRAM page appears in the segment.
- Page table (a buffer on the GPU): page of a PS4 address → device address (BDA) of its current source. It is changed by a copy in the command stream, in order with draws, without semaphores.
- Direct path: the whole V# range is in one source (almost all bindings) → an ordinary descriptor (source buffer, offset, size). The shader translator does not change.
- Paged path: the range is mixed (≈85 huge bindings per frame: shaders bound to all of memory) → the descriptor points to a record {PS4 address, size}, and the shader translates each address through the table. A pipeline variant only for such bindings (specialization key).
- Vertices, indices, indirect arguments, copies, texture uploads: the direct path; otherwise, assembly into a contiguous temporary buffer (read-only) or per-page copies (the CPU knows the table).
- Moving to VRAM and back: the policy of the current new model (data whose writes we know about: loaders, the GPU; unknown writers stay in place), but the execution is a copy + a table write + choosing the source at bind time. Bindings recorded before a move read the old source, which is still valid; VRAM is freed once the GPU has passed those commands.
Order of work¶
- Module core (
gpu/layer/, no dependencies on shadPS4): sources, pages, table,Resolve(address, size)→ a direct source or "mixed", walking over sources for copies. The command recording interface is abstract (copy, table write), so it does not pull in the scheduler. - Hook-up without VRAM: the new model returns ranges in place through chunk buffers, not
through the arena; the new model does not create the sparse arena. Expected: ~48 FPS on AMD (like
BB_FIXED_ARENA), novkQueueBindSparseat all. Check the image, then make a build for NVIDIA. - VRAM mirrors + moves: the policy from
BufferCache, the page table, the paged path in shaders for mixed bindings. Goal: parity with the sparse arena on AMD (~205 FPS). - NVIDIA/Intel: a test build, a tester log; then remove sparse from the new model entirely.
Measurements: RX 7800 XT, standing still (tools/bench.sh), BB_PC_MODEL=1; diagnostics BB_RESIDENCY_MIX=1.
Status (2026-10-09, morning)¶
- Steps 1–3 are done. On the RX 7800 XT, standing still: module ~205 FPS, sparse arena ~205–238, old model
~220 (at the lamp in the Hunter's Nightmare); in another spot (1170 draw calls): 167.5 / 164.5 / 146.
vkQueueBindSparseis never called in module mode (counter in theLayer memory:line). - Mixed bindings are solved without the paged path: "volatile" blocks (
layer_volatile). A block with unknown bytes (the edge of a range, a gap between images) goes into the mirror together with the rest of the range, is refreshed from game memory before each binding, and whatever a dispatch wrote into it is copied back after the dispatch (LayerAfterBindings). The paged path remains for bindings of 64 MiB and larger.BB_LAYER_VOLATILE=0turns it off (for comparison). - Default: AMD uses the sparse arena, other GPUs use the module (
BB_LAYER_MEMORY=1/0to choose manually). Step 4 (NVIDIA): needs a tester log; the host memory import path cannot be checked on AMD (RADV does not import memfd).
Addendum: original copying and portable labels (2026-10-09)¶
In the new module, the paged path of the original copy shader is fixed: writes update game memory and the current mirror; the native shader is now selected by default. For GPU labels, a standard Vulkan path without AMD buffer marker was added. The implementation, switches and verification limits are described in TRANSLATION_GPU_COPY_LABELS.ru.md. This removes the dependency of label writes on the AMD extension, but does not replace hardware testing on NVIDIA.