feat(agc): import shared direct memory through udmabuf on Linux

On Linux, direct memory is a memfd mapped MAP_SHARED. amdgpu accepts only
private anonymous memory for a host pointer import (libdrm sets
AMDGPU_GEM_USERPTR_ANONONLY), so every import of direct memory failed with
VK_ERROR_INVALID_EXTERNAL_HANDLE and its ranges were copied.

When a host pointer import fails, the range's memfd slice is turned into a
dma-buf through /dev/udmabuf and imported with VK_EXT_external_memory_dma_buf.
The direct memory backing is sealed against shrinking and growing, as
udmabuf requires. libkernel reports the backing of a guest range through
GuestArenaSharedBacking. Without /dev/udmabuf, or past its size limit, the
range is copied as before. APS5_TRACE_HOST_IMPORT now prints the mappings
behind a failed import on Linux too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alfalfa-star 2026-10-06 20:43:07 +02:00 committed by Adrià Franch
parent 6c48acecb3
commit 815d274736
7 changed files with 199 additions and 12 deletions

View File

@ -208,6 +208,7 @@ struct VulkanDevice::State {
bool depthBiasClamp = false;
bool occlusionQueryPrecise = false;
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
bool depthRangeUnrestricted = false;
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
@ -832,6 +833,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
}
#endif
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
#ifndef _WIN32
state->dmaBufImport = hasExtension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) && hasExtension(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
if (state->dmaBufImport) {
deviceExtensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
}
#endif
}
VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT};
if (hasExtension(VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) {
@ -2445,6 +2453,7 @@ Graphics::Context VulkanDevice::buildContext() const {
state->depthClamp,
state->hostImportAlignment
};
context.dmaBufImport = state->dmaBufImport;
context.recorder = state->recorder.get();
context.descriptorCache = state->descriptorCache.get();
context.samplerCache = state->samplerCache.get();

View File

@ -106,6 +106,7 @@ struct Context {
bool depthClamp = false;
// Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment.
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;

View File

@ -13,7 +13,11 @@
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <linux/udmabuf.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
#include <atomic>
#include <bit>
@ -170,9 +174,7 @@ void retireImport(const Context& context, HostImports& state, std::map<std::uint
state.imports.erase(it);
}
const char* createImport(const Context& context, HostImport& entry, VkResult& failure) {
const auto bytes = entry.bytes;
void* const host = entry.alias != nullptr ? entry.alias : reinterpret_cast<void*>(entry.base);
const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) {
const auto failed = [&](const char* step, VkResult result) -> const char* {
if (entry.buffer) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, entry.buffer, nullptr);
if (entry.memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, entry.memory, nullptr);
@ -181,25 +183,24 @@ const char* createImport(const Context& context, HostImport& entry, VkResult& fa
failure = result;
return step;
};
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT};
allocated = false;
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast<VkExternalMemoryHandleTypeFlags>(handleType)};
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external};
info.size = bytes;
info.size = entry.bytes;
// INDIRECT_BUFFER: DISPATCH_INDIRECT group counts are read in place (VulkanDevice::DispatchIndirect).
info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (const auto result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result);
VkMemoryHostPointerPropertiesEXT pointer{VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &pointer); result != VK_SUCCESS) return failed("vkGetMemoryHostPointerPropertiesEXT", result);
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements);
const auto types = requirements.memoryTypeBits & pointer.memoryTypeBits;
const auto types = requirements.memoryTypeBits & importTypes;
if (types == 0) return failed("memory type selection", VK_ERROR_FORMAT_NOT_SUPPORTED);
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host};
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, &import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags};
allocation.allocationSize = bytes;
allocation.allocationSize = entry.bytes;
allocation.memoryTypeIndex = static_cast<std::uint32_t>(std::countr_zero(types));
if (const auto result = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &entry.memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result);
allocated = true;
if (const auto result = context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, entry.buffer, entry.memory, 0); result != VK_SUCCESS) return failed("vkBindBufferMemory", result);
const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, entry.buffer};
entry.address = context.Function<PFN_vkGetBufferDeviceAddressKHR>("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo);
@ -207,6 +208,61 @@ const char* createImport(const Context& context, HostImport& entry, VkResult& fa
return nullptr;
}
const char* createHostPointerImport(const Context& context, HostImport& entry, VkResult& failure) {
void* const host = entry.alias != nullptr ? entry.alias : reinterpret_cast<void*>(entry.base);
VkMemoryHostPointerPropertiesEXT pointer{VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &pointer); result != VK_SUCCESS) {
failure = result;
return "vkGetMemoryHostPointerPropertiesEXT";
}
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host};
bool allocated = false;
return bindImport(context, entry, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, &import, pointer.memoryTypeBits, allocated, failure);
}
#ifndef _WIN32
int udmabufDevice() {
static const int device = open("/dev/udmabuf", O_RDWR | O_CLOEXEC);
return device;
}
const char* createDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset, VkResult& failure) {
udmabuf_create request{};
request.memfd = static_cast<std::uint32_t>(file);
request.flags = UDMABUF_FLAGS_CLOEXEC;
request.offset = offset;
request.size = entry.bytes;
const int device = udmabufDevice();
const int buffer = device < 0 ? -1 : ioctl(device, UDMABUF_CREATE, &request);
close(file);
if (buffer < 0) {
failure = VK_ERROR_INVALID_EXTERNAL_HANDLE;
return device < 0 ? "open /dev/udmabuf" : "UDMABUF_CREATE";
}
VkMemoryFdPropertiesKHR properties{VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
if (const auto result = context.Function<PFN_vkGetMemoryFdPropertiesKHR>("vkGetMemoryFdPropertiesKHR")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer, &properties); result != VK_SUCCESS) {
close(buffer);
failure = result;
return "vkGetMemoryFdPropertiesKHR";
}
const VkImportMemoryFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer};
bool allocated = false;
const char* step = bindImport(context, entry, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, &import, properties.memoryTypeBits, allocated, failure);
if (!allocated) close(buffer);
return step;
}
#endif
const char* createImport(const Context& context, HostImport& entry, VkResult& failure) {
const char* step = createHostPointerImport(context, entry, failure);
#ifndef _WIN32
int file = -1;
std::uint64_t offset = 0;
if (step != nullptr && context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast<std::uintptr_t>(entry.base), static_cast<std::size_t>(entry.bytes), &file, &offset)) step = createDmaBufImport(context, entry, file, offset, failure);
#endif
return step;
}
#ifndef _WIN32
enum class ImportWatchRequest : std::uint8_t { Probe, Watch, Unwatch };
@ -337,6 +393,18 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
std::fprintf(stderr, "[gpu] 0x%llx+0x%llx state 0x%lx protect 0x%lx type 0x%lx allocation 0x%llx\n", static_cast<unsigned long long>(cursor), static_cast<unsigned long long>(std::min(regionEnd, base + bytes) - cursor), info.State, info.Protect, info.Type, reinterpret_cast<unsigned long long>(info.AllocationBase));
cursor = regionEnd;
}
#else
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
if (std::FILE* maps = trace ? std::fopen("/proc/self/maps", "r") : nullptr) {
char line[512];
while (std::fgets(line, sizeof(line), maps) != nullptr) {
unsigned long long first = 0;
unsigned long long last = 0;
if (std::sscanf(line, "%llx-%llx", &first, &last) != 2 || last <= base || first >= base + bytes) continue;
std::fprintf(stderr, "[gpu] %s", line);
}
std::fclose(maps);
}
#endif
return nullptr;
}

View File

@ -8,6 +8,10 @@
// absolute address and breaks on host addresses outside that range.
namespace GuestArena {
#ifndef _WIN32
using SharedBackingResolver = bool (*)(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset);
#endif
extern "C" {
bool GuestArenaAvailable_nid_postfix();
@ -32,6 +36,9 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes);
void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection);
void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes);
void GuestArenaUnmapAlias_nid_postfix(void* alias);
#else
void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver);
bool GuestArenaSharedBacking_nid_postfix(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset);
#endif
bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes);
void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes);

View File

@ -1,6 +1,7 @@
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/WindowsMappings.hpp"
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <iterator>
@ -267,4 +268,21 @@ void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes) {
#endif
}
#ifndef _WIN32
namespace {
std::atomic<SharedBackingResolver> sharedBackingResolver{nullptr};
}
void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver) {
sharedBackingResolver.store(resolver, std::memory_order_release);
}
bool GuestArenaSharedBacking_nid_postfix(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) {
const auto resolver = sharedBackingResolver.load(std::memory_order_acquire);
return resolver != nullptr && resolver(address, bytes, file, offset);
}
#endif
}

View File

@ -18,6 +18,7 @@
#include <vector>
#if defined(__linux__)
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fstream>
@ -233,18 +234,27 @@ public:
throw std::system_error(error, std::system_category(), message);
}
#else
file = memfd_create("direct memory", MFD_CLOEXEC);
file = memfd_create("direct memory", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (file < 0) throw std::system_error(errno, std::generic_category(), "create direct memory backing");
if (ftruncate(file, static_cast<off_t>(bytes)) != 0) {
const int error = errno;
::close(file);
throw std::system_error(error, std::generic_category(), "size direct memory backing");
}
if (fcntl(file, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW) != 0) {
const int error = errno;
::close(file);
throw std::system_error(error, std::generic_category(), "seal direct memory backing");
}
#endif
}
int MemoryType() const { return memoryType; }
#if defined(__linux__)
int File() const { return file; }
#endif
~PhysicalBacking() {
#ifdef _WIN32
CloseHandle(section);
@ -353,6 +363,32 @@ void WatchMapping(std::uintptr_t address, std::size_t len) {
}
#endif
#if defined(__linux__)
bool SharedBacking(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) {
std::lock_guard lock(g_directLock);
const auto next = g_directMappings.upper_bound(address);
if (bytes == 0 || bytes > std::numeric_limits<std::uintptr_t>::max() - address || next == g_directMappings.begin()) return false;
auto it = std::prev(next);
if (address >= it->second.end) return false;
const auto backing = it->second.backing;
const auto phys = it->second.phys + (address - it->first);
const auto page = g_physPages.find(phys - phys % PS5_PAGE_SIZE);
if (page == g_physPages.end() || page->second.backing != backing) return false;
const auto end = address + bytes;
for (auto covered = it->second.end; covered < end;) {
const auto following = std::next(it);
if (following == g_directMappings.end() || following->first != covered || following->second.backing != backing || following->second.phys != it->second.phys + (it->second.end - it->first)) return false;
it = following;
covered = it->second.end;
}
const int duplicate = fcntl(backing->File(), F_DUPFD_CLOEXEC, 0);
if (duplicate < 0) throw std::system_error(errno, std::generic_category(), "duplicate direct memory backing");
*file = duplicate;
*offset = page->second.offset + phys % PS5_PAGE_SIZE;
return true;
}
#endif
void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) {
ValidatePhysicalRange(phys, len);
EraseMappings(address, address + len);
@ -754,6 +790,10 @@ void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType) {
for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) {
if (g_physPages.contains(first + offset)) throw std::runtime_error("physical allocation overlaps live direct memory");
}
#if defined(__linux__)
static std::once_flag registered;
std::call_once(registered, [] { GuestArena::GuestArenaSetSharedBacking_nid_postfix(&SharedBacking); });
#endif
const auto backing = std::make_shared<PhysicalBacking>(len, memoryType);
std::map<std::uint64_t, PhysicalPage> pages;
for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) pages.emplace(first + offset, PhysicalPage{backing, offset});

View File

@ -22,6 +22,7 @@
#include <utility>
#include <vector>
#if defined(__linux__)
#include <fcntl.h>
#include <fstream>
#include <string>
#include <sys/mman.h>
@ -936,6 +937,48 @@ static void CheckDirectMemoryBackingNeedsNoFilesystem() {
Require(sceKernelReleaseDirectMemory(phys, page) == 0);
}
static void CheckDirectMemorySharedBacking() {
constexpr std::size_t page = 0x4000;
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0);
void* first = nullptr;
Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys + page, 0) == 0);
const auto address = reinterpret_cast<std::uintptr_t>(first);
static_cast<volatile unsigned char*>(first)[page + 5] = 0x5a;
int file = -1;
std::uint64_t offset = 0;
Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address + page, page, &file, &offset));
Require(offset == page * 2);
unsigned char byte = 0;
Require(pread(file, &byte, 1, static_cast<off_t>(offset + 5)) == 1 && byte == 0x5a);
const int seals = fcntl(file, F_GET_SEALS);
Require(seals >= 0 && (seals & F_SEAL_SHRINK) != 0 && (seals & F_SEAL_GROW) != 0);
Require((fcntl(file, F_GETFD) & FD_CLOEXEC) != 0);
close(file);
Require(GuestArena::GuestArenaSharedBacking_nid_postfix(address, page * 2, &file, &offset) && offset == page);
close(file);
Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(address, page * 3, &file, &offset));
Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(address, 0, &file, &offset));
void* flexible = nullptr;
Require(sceKernelMapFlexibleMemory(&flexible, page * 2, 3, 0) == 0);
const auto flexibleAddress = reinterpret_cast<std::uintptr_t>(flexible);
Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page, &file, &offset));
void* low = flexible;
Require(sceKernelMapDirectMemory(&low, page, 3, 0x10, phys, 0) == 0);
void* high = static_cast<unsigned char*>(flexible) + page;
Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys + page, 0) == 0);
Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page * 2, &file, &offset) && offset == 0);
close(file);
Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys + page * 2, 0) == 0);
Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress, page * 2, &file, &offset));
Require(GuestArena::GuestArenaSharedBacking_nid_postfix(flexibleAddress + page, page, &file, &offset) && offset == page * 2);
close(file);
Require(sceKernelMunmap(flexible, page * 2) == 0);
Require(sceKernelMunmap(first, page * 2) == 0);
Require(!GuestArena::GuestArenaSharedBacking_nid_postfix(address, page, &file, &offset));
Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0);
}
static void CheckDirectMemoryWriteWatch() {
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
std::puts("write watch unavailable: direct memory not tested");
@ -1016,6 +1059,7 @@ int main() {
CheckWriteWatch();
CheckDirectMemoryWriteWatch();
CheckDirectMemoryBackingNeedsNoFilesystem();
CheckDirectMemorySharedBacking();
#endif
constexpr std::size_t page = 0x4000;
const auto failed = reinterpret_cast<void*>(static_cast<std::uintptr_t>(-1));