From 4e05e13bccd51d505e9c6d1477909237cae9cba1 Mon Sep 17 00:00:00 2001 From: Jasper Bekkers Date: Tue, 27 Oct 2026 12:06:33 +0100 Subject: [PATCH 1/2] Allocate memory that can be shared with another device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/vulkan/mod.rs | 142 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/src/vulkan/mod.rs b/src/vulkan/mod.rs index 93d22e2..3d8bc53 100644 --- a/src/vulkan/mod.rs +++ b/src/vulkan/mod.rs @@ -33,6 +33,54 @@ pub enum AllocationScheme { GpuAllocatorManaged, } +/// `GENERIC_ALL`, the access rights given to an exported `OPAQUE_WIN32` handle. +const GENERIC_ALL: vk::DWORD = 0x1000_0000; + +/// Ties an allocation to memory shared with another device or API, for +/// [`Allocator::allocate_external()`]. +/// +/// The resource bound to it has to agree: it must be created with a +/// [`vk::ExternalMemoryImageCreateInfo`] (or its buffer equivalent) naming the +/// same handle types. +#[derive(Debug)] +pub enum ExternalMemory { + /// Allocate memory another device can import as any of `handle_types`. Pull + /// the handle out afterwards with `vkGetMemory{Win32Handle,Fd}KHR` on + /// [`Allocation::memory()`], which needs the matching device extension. + /// + /// `OPAQUE_WIN32` handles are exported with a default security descriptor + /// and `GENERIC_ALL` access. + Export { + handle_types: vk::ExternalMemoryHandleTypeFlags, + }, + /// Import memory another device exported as a Win32 handle. + /// + /// Importing takes its own reference on an NT handle, so the caller keeps + /// owning `handle` and has to close it afterwards. + /// + /// For `OPAQUE_WIN32` the import must use the exporter's exact memory type + /// and allocation size (VUID-VkMemoryAllocateInfo-allocationSize-01742): + /// narrow `requirements.memory_type_bits` to that one type and pass the + /// exporter's size in `requirements.size`. + ImportWin32 { + handle_type: vk::ExternalMemoryHandleTypeFlags, + handle: vk::HANDLE, + }, + /// Import memory another device exported as a file descriptor. + /// + /// The allocator takes `fd`: a successful `vkAllocateMemory` hands it to the + /// Vulkan implementation, and on every other path it is closed here, so it + /// is never closed twice. + /// + /// For `OPAQUE_FD` the same memory type and size rule as for + /// [`Self::ImportWin32`] applies. + #[cfg(all(unix, feature = "std"))] + ImportFd { + handle_type: vk::ExternalMemoryHandleTypeFlags, + fd: std::os::fd::OwnedFd, + }, +} + #[derive(Clone, Debug)] pub struct AllocationCreateDesc<'a> { /// Name of the allocation, for tracking and debugging purposes @@ -351,6 +399,8 @@ pub(crate) struct MemoryBlock { } impl MemoryBlock { + // `external` is only consumed by the unix-only fd import. + #[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)] fn new( device: &ash::Device, size: u64, @@ -359,6 +409,7 @@ impl MemoryBlock { buffer_device_address: bool, allocation_scheme: AllocationScheme, requires_personal_block: bool, + external: Option, ) -> Result { let device_memory = { let alloc_info = vk::MemoryAllocateInfo::default() @@ -388,7 +439,56 @@ impl MemoryBlock { AllocationScheme::GpuAllocatorManaged => alloc_info, }; - unsafe { device.allocate_memory(&alloc_info, None) }.map_err(|e| match e { + // Shared memory: which handle types it can be exported as, or the handle + // it is imported from. + let mut export_info = vk::ExportMemoryAllocateInfo::default(); + let mut export_win32_info = vk::ExportMemoryWin32HandleInfoKHR::default(); + let mut import_win32_info = vk::ImportMemoryWin32HandleInfoKHR::default(); + #[cfg(all(unix, feature = "std"))] + let mut import_fd_info = vk::ImportMemoryFdInfoKHR::default(); + #[cfg(all(unix, feature = "std"))] + let mut imported_fd = None; + let alloc_info = match external { + Some(ExternalMemory::Export { handle_types }) => { + export_info = export_info.handle_types(handle_types); + let alloc_info = alloc_info.push_next(&mut export_info); + + // VUID-VkExportMemoryWin32HandleInfoKHR-handleTypes-00657 + if handle_types.contains(vk::ExternalMemoryHandleTypeFlags::OPAQUE_WIN32) { + export_win32_info = export_win32_info.dw_access(GENERIC_ALL); + alloc_info.push_next(&mut export_win32_info) + } else { + alloc_info + } + } + Some(ExternalMemory::ImportWin32 { + handle_type, + handle, + }) => { + import_win32_info = import_win32_info.handle_type(handle_type).handle(handle); + alloc_info.push_next(&mut import_win32_info) + } + #[cfg(all(unix, feature = "std"))] + Some(ExternalMemory::ImportFd { handle_type, fd }) => { + use std::os::fd::AsRawFd; + import_fd_info = import_fd_info.handle_type(handle_type).fd(fd.as_raw_fd()); + imported_fd = Some(fd); + alloc_info.push_next(&mut import_fd_info) + } + None => alloc_info, + }; + + let device_memory = unsafe { device.allocate_memory(&alloc_info, None) }; + // Only a successful import transfers ownership of the fd to the + // implementation; on failure it is dropped (closed) here instead. + #[cfg(all(unix, feature = "std"))] + if device_memory.is_ok() { + if let Some(fd) = imported_fd { + use std::os::fd::IntoRawFd; + let _ = fd.into_raw_fd(); + } + } + device_memory.map_err(|e| match e { vk::Result::ERROR_OUT_OF_DEVICE_MEMORY => AllocationError::OutOfMemory, e => AllocationError::Internal(format!( "Unexpected error in vkAllocateMemory: {e:?}" @@ -465,6 +565,7 @@ impl MemoryType { granularity: u64, #[cfg(feature = "std")] backtrace: Arc, allocation_sizes: &AllocationSizes, + external: Option, ) -> Result { let allocation_type = if desc.linear { AllocationType::Linear @@ -494,6 +595,7 @@ impl MemoryType { self.buffer_device_address, desc.allocation_scheme, requires_personal_block, + external, )?; let mut block_index = None; @@ -596,6 +698,9 @@ impl MemoryType { self.buffer_device_address, desc.allocation_scheme, false, + // General blocks are sub-allocated and so are never shared: an external + // allocation demands a dedicated scheme and takes the branch above. + None, )?; let new_block_index = if let Some(block_index) = empty_block_index { @@ -774,6 +879,33 @@ impl Allocator { } pub fn allocate(&mut self, desc: &AllocationCreateDesc<'_>) -> Result { + self.allocate_impl(desc, None) + } + + /// Allocates memory that is shared with another device or API, either exporting + /// an OS handle to it or importing one, as described by `external`. + /// + /// Shared memory is never sub-allocated — an importer would receive the whole + /// allocation, including whatever else was placed in it — so `desc` must ask for + /// a dedicated [`AllocationScheme`], and the resource bound to the result must be + /// created with an external-memory create info naming the same handle types. + pub fn allocate_external( + &mut self, + desc: &AllocationCreateDesc<'_>, + external: ExternalMemory, + ) -> Result { + if desc.allocation_scheme == AllocationScheme::GpuAllocatorManaged { + return Err(AllocationError::InvalidAllocationCreateDesc); + } + + self.allocate_impl(desc, Some(external)) + } + + fn allocate_impl( + &mut self, + desc: &AllocationCreateDesc<'_>, + external: Option, + ) -> Result { let size = desc.requirements.size; let alignment = desc.requirements.alignment; @@ -835,6 +967,10 @@ impl Allocator { None => return Err(AllocationError::NoCompatibleMemoryTypeFound), }; + // Retrying would import the same handle twice, and importing a file descriptor + // consumes it. + let may_retry = external.is_none(); + //Do not try to create a block if the heap is smaller than the required size (avoids validation warnings). let memory_type = &mut self.memory_types[memory_type_index]; let allocation = if size > self.memory_heaps[memory_type.heap_index].size { @@ -847,10 +983,11 @@ impl Allocator { #[cfg(feature = "std")] backtrace.clone(), &self.allocation_sizes, + external, ) }; - if desc.location == MemoryLocation::CpuToGpu { + if desc.location == MemoryLocation::CpuToGpu && may_retry { if allocation.is_err() { let mem_loc_preferred_bits = vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; @@ -870,6 +1007,7 @@ impl Allocator { #[cfg(feature = "std")] backtrace, &self.allocation_sizes, + None, ) } else { allocation From 486751478476a7acc1a9d1c26b4a65f0e324d04b Mon Sep 17 00:00:00 2001 From: Jasper Bekkers Date: Tue, 27 Oct 2026 12:06:33 +0100 Subject: [PATCH 2/2] d3d12: Add a resource type for memory another device can open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/d3d12/mod.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/d3d12/mod.rs b/src/d3d12/mod.rs index 6ead72c..bc9bfde 100644 --- a/src/d3d12/mod.rs +++ b/src/d3d12/mod.rs @@ -178,6 +178,20 @@ pub enum ResourceType<'a> { /// /// [`PlacedResource`]: https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createplacedresource Placed, + /// Create a resource whose memory another device or API can open, through + /// [`ID3D12Device::CreateSharedHandle`] on the returned [`Resource::resource()`]. + /// + /// This is [`Self::Committed`] with `D3D12_HEAP_FLAG_SHARED`, and committed is + /// not a detail the caller may vary: a placed resource would sit on a pool heap + /// created without that flag, and sharing that heap would hand the importer + /// every other resource on it as well. + /// + /// [`ID3D12Device::CreateSharedHandle`]: https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createsharedhandle + Shared { + heap_properties: &'a D3D12_HEAP_PROPERTIES, + /// Flags to share the resource with, on top of `D3D12_HEAP_FLAG_SHARED`. + heap_flags: D3D12_HEAP_FLAGS, + }, } #[derive(Debug)] @@ -786,7 +800,16 @@ impl Allocator { ResourceType::Committed { heap_properties, heap_flags, + } + | ResourceType::Shared { + heap_properties, + heap_flags, } => { + let heap_flags = if matches!(desc.resource_type, ResourceType::Shared { .. }) { + *heap_flags | D3D12_HEAP_FLAG_SHARED + } else { + *heap_flags + }; let mut result: Option = None; let clear_value: Option<*const D3D12_CLEAR_VALUE> = @@ -806,7 +829,7 @@ impl Allocator { let resource_desc1 = Self::d3d12_resource_desc_1(desc.resource_desc); device.CreateCommittedResource3( *heap_properties, - *heap_flags, + heap_flags, &resource_desc1, initial_layout, clear_value, @@ -828,7 +851,7 @@ impl Allocator { device.CreateCommittedResource3( *heap_properties, - *heap_flags, + heap_flags, &resource_desc1, initial_layout, clear_value, @@ -843,7 +866,7 @@ impl Allocator { (device, ResourceStateOrBarrierLayout::ResourceState(initial_state)) => { device.CreateCommittedResource( *heap_properties, - *heap_flags, + heap_flags, desc.resource_desc, initial_state, clear_value,