From 0364d3d6d736aaf91ee75a090474232ec51bc1df Mon Sep 17 00:00:00 2001 From: Dzmitry Malyshau Date: Thu, 23 Apr 2026 20:28:00 -0400 Subject: [PATCH] vk: support VK_EXT_external_memory_host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing `Memory::External(ExternalMemorySource::HostAllocation(ptr))` used to crash with `ERROR_INVALID_EXTERNAL_HANDLE` because: 1. The extension wasn't being enabled on the device — the `VkImportMemoryHostPointerInfoEXT` struct chained into `vkAllocateMemory` was unknown to the driver. 2. The memory type index was picked by `memory_type_bits.ilog2()`, which doesn't account for the host-pointer compatibility set the driver advertises via `vkGetMemoryHostPointerPropertiesEXT`. 3. `allocationSize` wasn't rounded up to `minImportedHostPointerAlignment`. Fix all three. When the extension is unsupported the capability stays off and attempts to import a host allocation now panic with a clear message pointing at `Capabilities::external_memory_host` rather than going into `vkAllocateMemory` and getting rejected. Verified end-to-end with meganeura's `external_buffer` example on AMD Radeon 780M / RADV PHOENIX (input bytes traverse producer-GPU → meganeura-GPU with no CPU roundtrip; GPU-vs-CPU max error 2.4e-7). Co-Authored-By: Claude Opus 4.7 (1M context) --- blade-graphics/src/vulkan/init.rs | 41 +++++++++++++ blade-graphics/src/vulkan/mod.rs | 8 +++ blade-graphics/src/vulkan/resource.rs | 86 ++++++++++++++++++++++++--- docs/CHANGELOG.md | 2 + 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/blade-graphics/src/vulkan/init.rs b/blade-graphics/src/vulkan/init.rs index 901229cb..cef3f076 100644 --- a/blade-graphics/src/vulkan/init.rs +++ b/blade-graphics/src/vulkan/init.rs @@ -103,6 +103,12 @@ struct AdapterCapabilities { pipeline_executable_properties: bool, full_screen_exclusive: bool, external_memory: bool, + external_memory_host: bool, + /// `VkPhysicalDeviceExternalMemoryHostPropertiesEXT::minImportedHostPointerAlignment`. + /// Allocation size and host-pointer alignment for an imported + /// `Memory::External(HostAllocation(..))` allocation must be a + /// multiple of this value (0 when the extension is unsupported). + min_imported_host_pointer_alignment: u64, timing: bool, dual_source_blending: bool, shader_float16: bool, @@ -374,6 +380,27 @@ fn inspect_adapter( &vk::KHR_EXTERNAL_MEMORY_FD_NAME }); + // `VK_EXT_external_memory_host` is what lets us import a plain host + // allocation (`ExternalMemorySource::HostAllocation(ptr)`) as a + // Vulkan buffer without a staging copy. It's orthogonal to the + // FD/Win32 variants above — a driver may support one without the + // other. RADV, NVIDIA, and modern Intel all expose it. + let external_memory_host = supported_extensions.contains(&vk::EXT_EXTERNAL_MEMORY_HOST_NAME); + let min_imported_host_pointer_alignment = if external_memory_host { + let mut host_props = vk::PhysicalDeviceExternalMemoryHostPropertiesEXT::default(); + let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut host_props); + unsafe { + instance + .get_physical_device_properties2 + .get_physical_device_properties2(phd, &mut props2); + } + // The spec forbids 0; guard anyway so downstream alignment + // math stays well-defined. + host_props.min_imported_host_pointer_alignment.max(1) + } else { + 0 + }; + let timing = if properties.limits.timestamp_compute_and_graphics == vk::FALSE { log::info!("No timing because of queue support"); false @@ -547,6 +574,8 @@ fn inspect_adapter( pipeline_executable_properties, full_screen_exclusive, external_memory, + external_memory_host, + min_imported_host_pointer_alignment, timing, dual_source_blending, shader_float16, @@ -951,6 +980,9 @@ impl super::Context { vk::KHR_EXTERNAL_MEMORY_FD_NAME }); } + if capabilities.external_memory_host { + device_extensions.push(vk::EXT_EXTERNAL_MEMORY_HOST_NAME); + } if capabilities.cooperative_matrix.is_supported() { device_extensions.push(vk::KHR_COOPERATIVE_MATRIX_NAME); if capabilities.api_version < vk::API_VERSION_1_2 { @@ -1184,6 +1216,15 @@ impl super::Context { } else { None }, + external_memory_host: if capabilities.external_memory_host { + Some(ash::ext::external_memory_host::Device::new( + &instance.core, + &device_core, + )) + } else { + None + }, + min_imported_host_pointer_alignment: capabilities.min_imported_host_pointer_alignment, core: device_core, device_information: capabilities.device_information, command_scope: if desc.capture { diff --git a/blade-graphics/src/vulkan/mod.rs b/blade-graphics/src/vulkan/mod.rs index 323e9cc4..036746e7 100644 --- a/blade-graphics/src/vulkan/mod.rs +++ b/blade-graphics/src/vulkan/mod.rs @@ -70,6 +70,14 @@ struct Device { external_memory: Option, #[cfg(not(target_os = "windows"))] external_memory: Option, + /// `VK_EXT_external_memory_host` device wrapper. Populated when + /// the extension is enabled; imports of + /// `Memory::External(HostAllocation(..))` fail clearly when this + /// is `None`. + external_memory_host: Option, + /// Driver-reported alignment for imported host pointers and + /// allocation sizes (0 when `external_memory_host` is `None`). + min_imported_host_pointer_alignment: u64, command_scope: Option, timing: Option, workarounds: Workarounds, diff --git a/blade-graphics/src/vulkan/resource.rs b/blade-graphics/src/vulkan/resource.rs index 896bc636..02dee255 100644 --- a/blade-graphics/src/vulkan/resource.rs +++ b/blade-graphics/src/vulkan/resource.rs @@ -47,11 +47,83 @@ impl super::Context { .core .get_physical_device_memory_properties(self.physical_device) }; - let memory_type_index = memory_types.ilog2(); - let memory_type: vk::MemoryType = - memory_properties.memory_types[memory_type_index as usize]; - let handle_type = external_source_handle_type(e); + + // For a `HostAllocation` import the driver constrains + // which memory types can back the pointer — query it + // via `vkGetMemoryHostPointerPropertiesEXT` and + // intersect with the buffer's requirements. FD / Win32 + // / Export paths keep the historical "any valid bit" + // heuristic. + let host_pointer_memory_type_bits = match e { + crate::ExternalMemorySource::HostAllocation(ptr) => { + let ext = self.device.external_memory_host.as_ref().expect( + "Memory::External(HostAllocation) requires \ + VK_EXT_external_memory_host — check \ + Capabilities::external_memory_host before calling", + ); + let mut host_props = vk::MemoryHostPointerPropertiesEXT::default(); + unsafe { + (ext.fp().get_memory_host_pointer_properties_ext)( + self.device.core.handle(), + handle_type, + ptr as *const std::ffi::c_void, + &mut host_props, + ) + .result() + .expect("vkGetMemoryHostPointerPropertiesEXT"); + } + Some(host_props.memory_type_bits) + } + _ => None, + }; + + let allowed = memory_types & host_pointer_memory_type_bits.unwrap_or(!0); + let (memory_type_index, memory_type) = if host_pointer_memory_type_bits.is_some() { + // Host-pointer imports need HOST_VISIBLE. Prefer + // HOST_COHERENT to skip explicit flushes. + (0..memory_properties.memory_type_count) + .filter(|&i| allowed & (1 << i) != 0) + .map(|i| (i, memory_properties.memory_types[i as usize])) + .filter(|item| { + item.1 + .property_flags + .contains(vk::MemoryPropertyFlags::HOST_VISIBLE) + }) + .min_by_key(|item| { + if item + .1 + .property_flags + .contains(vk::MemoryPropertyFlags::HOST_COHERENT) + { + 0u8 + } else { + 1u8 + } + }) + .expect( + "no host-visible memory type compatible with the imported \ + host pointer", + ) + } else { + assert!( + allowed != 0, + "no memory type satisfies the external-buffer requirements" + ); + let idx = allowed.ilog2(); + (idx, memory_properties.memory_types[idx as usize]) + }; + + // `VkPhysicalDeviceExternalMemoryHostPropertiesEXT::minImportedHostPointerAlignment` + // — allocationSize must be a multiple of this when + // importing a host pointer. + let allocation_size = if host_pointer_memory_type_bits.is_some() { + let align = self.device.min_imported_host_pointer_alignment.max(1); + requirements.size.next_multiple_of(align) + } else { + requirements.size + }; + let external_info: &mut dyn vk::ExtendsMemoryAllocateInfo = match e { #[cfg(target_os = "windows")] crate::ExternalMemorySource::Win32(Some(handle)) @@ -89,7 +161,7 @@ impl super::Context { }; let allocation_info = vk::MemoryAllocateInfo { - allocation_size: requirements.size, + allocation_size, memory_type_index, ..vk::MemoryAllocateInfo::default() } @@ -99,7 +171,7 @@ impl super::Context { self.device .core .allocate_memory(&allocation_info, None) - .unwrap() + .expect("vkAllocateMemory (external import)") }; unsafe { @@ -108,7 +180,7 @@ impl super::Context { memory_type_index, gpu_alloc_ash::memory_properties_from_ash(memory_type.property_flags), 0, - requirements.size, + allocation_size, ) } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6d7d714c..b7f3670e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,8 @@ Changelog for *Blade* project ## (TBD) +- vk: support `VK_EXT_external_memory_host` — enable the extension, query memory-type compatibility via `vkGetMemoryHostPointerPropertiesEXT`, and round allocation size to `minImportedHostPointerAlignment` so `Memory::External(HostAllocation)` imports succeed on drivers that expose the extension + ## blade-graphics-0.8.4 (17 Apr 2026) - vk: use driver API version for instance creation