Skip to content

Allocate memory that can be shared with another device or API - #318

Open
Jasper-Bekkers wants to merge 2 commits into
mainfrom
external-memory-allocation
Open

Jasper-Bekkers wants to merge 2 commits into
mainfrom
external-memory-allocation

Conversation

@Jasper-Bekkers

Copy link
Copy Markdown
Member

What

Two allocator entry points for memory that another device or API opens. Breda uses them to share textures between D3D12, Vulkan and an external host (breda#10674).

  • Vulkan: Allocator::allocate_external(desc, ExternalMemory) chains VkExportMemoryAllocateInfo or VkImportMemory{Win32Handle,Fd}InfoKHR onto the one vkAllocateMemory this crate owns. Shared memory comes back as an ordinary Allocation that frees like any other, instead of every caller writing its own vkAllocateMemory, memory-type search and second ownership path.
  • D3D12: ResourceType::Shared { heap_properties, heap_flags } is Committed plus D3D12_HEAP_FLAG_SHARED. A shared placed resource would sit on a pool heap created without the flag, and sharing that heap would hand the importer every other resource on it. So "sharing implies committed" lives in the allocator, not in each caller.

Rules this enforces

  • Shared memory is never sub-allocated. allocate_external rejects AllocationScheme::GpuAllocatorManaged with InvalidAllocationCreateDesc.
  • The CpuToGpu fallback retry is skipped for external allocations. A retry would import the same handle twice.
  • VkExportMemoryWin32HandleInfoKHR (default security descriptor, GENERIC_ALL) is only chained when the export types contain OPAQUE_WIN32 (VUID-VkExportMemoryWin32HandleInfoKHR-handleTypes-00657).
  • ExternalMemory::ImportFd takes an OwnedFd (cfg(all(unix, feature = "std"))). Per spec only a successful import transfers ownership, so the fd is released to the driver right after vkAllocateMemory succeeds and closed on every other path. The caller can't double-close it. That includes a failure after vkAllocateMemory, such as mapping, where the freed memory already owned the fd.
  • ImportWin32 leaves the NT handle with the caller, because the import takes its own reference.

Caller's responsibility (documented, not enforced)

  • Opaque imports (OPAQUE_FD / OPAQUE_WIN32) must use the exporter's exact memoryTypeIndex and allocationSize (VUID-VkMemoryAllocateInfo-allocationSize-01742). Pass them through requirements.memory_type_bits (narrowed to one bit) and requirements.size.
  • The resource bound to the allocation has to be created with a matching VkExternalMemory{Image,Buffer}CreateInfo.
  • Pulling the handle out (vkGetMemory{Win32Handle,Fd}KHR, ID3D12Device::CreateSharedHandle) stays with the caller. Those Vulkan entry points live in device extensions this crate doesn't load.

Not here

No fence/semaphore export, no queue-family ownership transfer, no dma-buf / DRM modifiers.

Testing

cargo clippy --workspace --all-targets -D warnings is clean for vulkan,std, vulkan,hashbrown, d3d12,std and vulkan,d3d12,std,visualizer on Windows. --lib with vulkan,std and vulkan,hashbrown is clean for x86_64-unknown-linux-gnu, which covers the ImportFd path. No runtime test: nothing in this repo creates a second device to import into.

This was previously the external-memory branch, which sat on ash-next (6a68a5b). It is rebased onto main and uses push_next for released ash 0.38.

🤖 Generated with Claude Code

Vulkan can hand memory to another device — or take it from one — through
`VkExportMemoryAllocateInfo` and `VkImportMemory{Win32Handle,Fd}InfoKHR`,
which chain onto `vkAllocateMemory`. That is the one call this crate owns and
does not expose, so callers who need a shared texture have had to allocate
around it: their own `vkAllocateMemory`, their own memory-type search, their
own `VkDeviceMemory` to bind and free, and a second ownership path through
every resource that might hold one.

`Allocator::allocate_external` chains those structs instead, so shared memory
comes back as an ordinary `Allocation` that frees like any other.

Sharing implies a dedicated allocation, since an importer receives the whole
allocation and must not find another resource's data in it — so the entry
point rejects `AllocationScheme::GpuAllocatorManaged` rather than silently
sub-allocating, and the `CpuToGpu` fallback is skipped for it: retrying would
import the same handle twice, and importing a file descriptor consumes it.

`OPAQUE_WIN32` exports get a default security descriptor and `GENERIC_ALL`,
which is what sharing within one process needs. The Win32 export info is
only chained when the handle types include `OPAQUE_WIN32`, as
VUID-VkExportMemoryWin32HandleInfoKHR-handleTypes-00657 requires.

`ImportFd` takes an `OwnedFd`. Only a successful `vkAllocateMemory` hands
the fd to the implementation, so the allocator releases it to the driver
there and closes it on every other path. The caller never has to guess
whether a failed allocation consumed it: a failure after `vkAllocateMemory`
frees memory that already owns the fd, and closing it again would close
whatever reused that fd number.

Opaque imports must use the exporter's exact memory type and allocation
size (VUID-VkMemoryAllocateInfo-allocationSize-01742). The caller provides
both through `requirements`, by narrowing `memory_type_bits` to that one
type.

Pulling the OS handle back out stays with the caller, because
`vkGetMemory{Win32Handle,Fd}KHR` live in device extensions this crate does
not load. `Allocation::memory()` is all they need for it.
Sharing a D3D12 resource only takes `D3D12_HEAP_FLAG_SHARED`, which callers
can already pass through `ResourceType::Committed` — but nothing stops them
asking for a shared *placed* resource, and that quietly produces something
unusable: a placed resource lands on a pool heap created without the flag,
and sharing that heap would hand the importer every other resource sitting
on it.

`ResourceType::Shared` states the intent instead of the mechanism, so the
allocator adds the heap flag and commits the resource because it has to, not
because the caller remembered to. That mirrors the Vulkan side, where
`Allocator::allocate_external` refuses to sub-allocate for the same reason.

Turning the handle into something another device can open stays with the
caller: `ID3D12Device::CreateSharedHandle` takes the resource, which
`Resource::resource()` already hands out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant