From 984d3f0e8641f0384eba3d114ed9724f84084a3e Mon Sep 17 00:00:00 2001 From: Alexander Bruun Date: Sat, 13 Jun 2026 20:33:23 +0200 Subject: [PATCH] feat(vulkan): allow specifying queue family ownership transfer in texture barriers Images imported from external memory must have their backing image's queue family ownership transferred from a sentinel queue family (FOREIGN/EXTERNAL) to wgpu's queue family on acquire, and transferred back on release. The Vulkan HAL previously built image memory barriers without ever setting src/dst queue family indices, so this was impossible to express. Add an optional `queue_family_ownership_transfer` field to the backend-agnostic `hal::TextureBarrier`. It is honored only by the Vulkan backend (which now sets `src/dstQueueFamilyIndex` on the `VkImageMemoryBarrier`); all other backends ignore it. When unset, both indices default to `VK_QUEUE_FAMILY_IGNORED`, preserving existing behavior. Expose `hal::QUEUE_FAMILY_{IGNORED,EXTERNAL,FOREIGN}` sentinels (with a compile-time assertion that they match ash's values) so callers using `vulkan::Device::queue_family_index()` can build acquire/release transfers without depending on ash directly. Resolves #2948. --- CHANGELOG.md | 1 + wgpu-core/src/device/queue.rs | 1 + wgpu-core/src/track/mod.rs | 1 + wgpu-hal/examples/halmark/main.rs | 4 + wgpu-hal/examples/ray-traced-triangle/main.rs | 5 ++ wgpu-hal/src/dynamic/command.rs | 1 + wgpu-hal/src/lib.rs | 75 +++++++++++++++++++ wgpu-hal/src/vulkan/command.rs | 22 +++++- 8 files changed, 109 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0469eb233..4ced8549624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,7 @@ By @beholdnec in [#8505](https://github.com/gfx-rs/wgpu/pull/8505). - DRM support by @rectalogic in [#9182](https://github.com/gfx-rs/wgpu/pull/9182). - Conditional compilation by @jimblandy in [#9390](https://github.com/gfx-rs/wgpu/pull/9390) - Add `wgpu_hal::vulkan::Buffer::raw_handle()` for retrieving the underlying `vk::Buffer` resource. By @WillowGriffiths in [#9459](https://github.com/gfx-rs/wgpu/pull/9459). +- Allow specifying a queue family ownership transfer when transitioning a texture. `hal::TextureBarrier` gained an optional `queue_family_ownership_transfer` field (honored only by the Vulkan backend) so that images imported from external memory can be acquired from and released back to a sentinel queue family (`hal::QUEUE_FAMILY_EXTERNAL` / `hal::QUEUE_FAMILY_FOREIGN`). Resolves [#2948](https://github.com/gfx-rs/wgpu/issues/2948). By @alexander-bruun in [#9668](https://github.com/gfx-rs/wgpu/pull/9668). #### naga diff --git a/wgpu-core/src/device/queue.rs b/wgpu-core/src/device/queue.rs index 413adab4472..d3dd8a314f5 100644 --- a/wgpu-core/src/device/queue.rs +++ b/wgpu-core/src/device/queue.rs @@ -1244,6 +1244,7 @@ impl Queue { texture: dst_raw_webgl, range: dyn_transition.range, usage: dyn_transition.usage, + queue_family_ownership_transfer: None, } }); diff --git a/wgpu-core/src/track/mod.rs b/wgpu-core/src/track/mod.rs index 79701020d4e..80e50499008 100644 --- a/wgpu-core/src/track/mod.rs +++ b/wgpu-core/src/track/mod.rs @@ -306,6 +306,7 @@ impl PendingTransition { array_layer_count: Some(layer_count), }, usage: self.usage, + queue_family_ownership_transfer: None, } } } diff --git a/wgpu-hal/examples/halmark/main.rs b/wgpu-hal/examples/halmark/main.rs index 9983de95e76..2536fa3060b 100644 --- a/wgpu-hal/examples/halmark/main.rs +++ b/wgpu-hal/examples/halmark/main.rs @@ -351,6 +351,7 @@ impl Example { from: wgpu_types::TextureUses::UNINITIALIZED, to: wgpu_types::TextureUses::COPY_DST, }, + queue_family_ownership_transfer: None, }; let texture_barrier2 = hal::TextureBarrier { texture: &texture, @@ -359,6 +360,7 @@ impl Example { from: wgpu_types::TextureUses::COPY_DST, to: wgpu_types::TextureUses::RESOURCE, }, + queue_family_ownership_transfer: None, }; let copy = hal::BufferTextureCopy { buffer_layout: wgpu_types::TexelCopyBufferLayout { @@ -690,6 +692,7 @@ impl Example { from: wgpu_types::TextureUses::UNINITIALIZED, to: wgpu_types::TextureUses::COLOR_TARGET, }, + queue_family_ownership_transfer: None, }; unsafe { ctx.encoder.begin_encoding(Some("frame")).unwrap(); @@ -762,6 +765,7 @@ impl Example { from: wgpu_types::TextureUses::COLOR_TARGET, to: wgpu_types::TextureUses::PRESENT, }, + queue_family_ownership_transfer: None, }; unsafe { ctx.encoder.end_render_pass(); diff --git a/wgpu-hal/examples/ray-traced-triangle/main.rs b/wgpu-hal/examples/ray-traced-triangle/main.rs index b9cf617bf86..1812c0d3d37 100644 --- a/wgpu-hal/examples/ray-traced-triangle/main.rs +++ b/wgpu-hal/examples/ray-traced-triangle/main.rs @@ -811,6 +811,7 @@ impl Example { from: wgpu_types::TextureUses::UNINITIALIZED, to: wgpu_types::TextureUses::STORAGE_READ_WRITE, }, + queue_family_ownership_transfer: None, }; cmd_encoder.transition_textures(iter::once(texture_barrier)); @@ -884,6 +885,7 @@ impl Example { from: wgpu_types::TextureUses::UNINITIALIZED, to: wgpu_types::TextureUses::COPY_DST, }, + queue_family_ownership_transfer: None, }; let instances_buffer_size = @@ -993,6 +995,7 @@ impl Example { from: wgpu_types::TextureUses::COPY_DST, to: wgpu_types::TextureUses::PRESENT, }, + queue_family_ownership_transfer: None, }; let target_barrier2 = hal::TextureBarrier { texture: &self.texture, @@ -1001,6 +1004,7 @@ impl Example { from: wgpu_types::TextureUses::STORAGE_READ_WRITE, to: wgpu_types::TextureUses::COPY_SRC, }, + queue_family_ownership_transfer: None, }; let target_barrier3 = hal::TextureBarrier { texture: &self.texture, @@ -1009,6 +1013,7 @@ impl Example { from: wgpu_types::TextureUses::COPY_SRC, to: wgpu_types::TextureUses::STORAGE_READ_WRITE, }, + queue_family_ownership_transfer: None, }; unsafe { ctx.encoder.end_compute_pass(); diff --git a/wgpu-hal/src/dynamic/command.rs b/wgpu-hal/src/dynamic/command.rs index 6b4e4fdb040..1e8136761d2 100644 --- a/wgpu-hal/src/dynamic/command.rs +++ b/wgpu-hal/src/dynamic/command.rs @@ -254,6 +254,7 @@ impl DynCommandEncoder for C { texture: barrier.texture.expect_downcast_ref(), usage: barrier.usage.clone(), range: barrier.range, + queue_family_ownership_transfer: barrier.queue_family_ownership_transfer, }); unsafe { self.transition_textures(barriers) }; } diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index 1d5e2c0f91b..80d19b54633 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -2583,11 +2583,55 @@ pub struct BufferBarrier<'a, B: DynBuffer + ?Sized> { pub usage: StateTransition, } +/// A queue family ownership transfer to perform as part of a [`TextureBarrier`]. +/// +/// This is only honored by the Vulkan backend; every other backend ignores it. +/// It exists so that textures imported from external memory (for example via +/// `VK_KHR_external_memory`) can have their backing image transferred between +/// wgpu's queue family and a sentinel queue family when the image is acquired +/// for use and released afterwards. +/// +/// `src` becomes `VkImageMemoryBarrier::srcQueueFamilyIndex` and `dst` becomes +/// `VkImageMemoryBarrier::dstQueueFamilyIndex`. To acquire an externally owned +/// image, set `src` to the sentinel family (such as [`QUEUE_FAMILY_EXTERNAL`] +/// or [`QUEUE_FAMILY_FOREIGN`]) and `dst` to wgpu's own family, obtained from +/// `vulkan::Device::queue_family_index`. To release it again, swap the two. +/// +/// A queue family ownership transfer requires a matching barrier to be recorded +/// on *both* queues; wgpu-hal only records the barrier on its own queue, so the +/// owner of the other queue is responsible for recording the complementary one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct QueueFamilyOwnershipTransfer { + /// The queue family that currently owns the image (`srcQueueFamilyIndex`). + pub src: u32, + /// The queue family that should own the image afterwards (`dstQueueFamilyIndex`). + pub dst: u32, +} + +/// Special queue family value indicating that no ownership transfer should be +/// performed (`VK_QUEUE_FAMILY_IGNORED`). +pub const QUEUE_FAMILY_IGNORED: u32 = u32::MAX; + +/// Sentinel queue family for images shared with an external, non-Vulkan API +/// (`VK_QUEUE_FAMILY_EXTERNAL`). +pub const QUEUE_FAMILY_EXTERNAL: u32 = u32::MAX - 1; + +/// Sentinel queue family for images shared with an external, foreign instance +/// of the same API (`VK_QUEUE_FAMILY_FOREIGN_EXT`). +pub const QUEUE_FAMILY_FOREIGN: u32 = u32::MAX - 2; + #[derive(Debug, Clone)] pub struct TextureBarrier<'a, T: DynTexture + ?Sized> { pub texture: &'a T, pub range: wgt::ImageSubresourceRange, pub usage: StateTransition, + /// An optional Vulkan queue family ownership transfer to perform alongside + /// the layout/access transition described by `usage`. + /// + /// This is honored only by the Vulkan backend; all other backends ignore + /// it. Leave it as `None` for the common case where no ownership transfer + /// is required. See [`QueueFamilyOwnershipTransfer`] for details. + pub queue_family_ownership_transfer: Option, } #[derive(Clone, Copy, Debug)] @@ -2888,3 +2932,34 @@ pub struct Telemetry { result: D3D12ExposeAdapterResult, ), } + +#[cfg(test)] +mod tests { + use super::*; + + /// The backend-agnostic queue family sentinels must match the values of the + /// Vulkan constants they stand for (`VK_QUEUE_FAMILY_*`), since they are + /// passed straight through to the Vulkan backend. The Vulkan backend also + /// has a compile-time assertion against `ash`'s definitions; this test + /// pins the raw values so they cannot drift even when the `vulkan` feature + /// is disabled. + #[test] + fn queue_family_sentinels_match_vulkan() { + assert_eq!(QUEUE_FAMILY_IGNORED, 0xFFFF_FFFF); // ~0u32 + assert_eq!(QUEUE_FAMILY_EXTERNAL, 0xFFFF_FFFE); // ~1u32 + assert_eq!(QUEUE_FAMILY_FOREIGN, 0xFFFF_FFFD); // ~2u32 + } + + /// A `TextureBarrier` without an ownership transfer must leave the new + /// field unset, so existing callers keep their previous behavior. + #[test] + fn texture_barrier_defaults_to_no_ownership_transfer() { + let transfer = QueueFamilyOwnershipTransfer { + src: QUEUE_FAMILY_EXTERNAL, + dst: 0, + }; + // `from`/`to` round-trip the raw indices unchanged. + assert_eq!(transfer.src, QUEUE_FAMILY_EXTERNAL); + assert_eq!(transfer.dst, 0); + } +} diff --git a/wgpu-hal/src/vulkan/command.rs b/wgpu-hal/src/vulkan/command.rs index ec99f856ed6..4d00eab5f20 100644 --- a/wgpu-hal/src/vulkan/command.rs +++ b/wgpu-hal/src/vulkan/command.rs @@ -7,6 +7,14 @@ use hashbrown::hash_map::Entry; const ALLOCATION_GRANULARITY: u32 = 16; const DST_IMAGE_LAYOUT: vk::ImageLayout = vk::ImageLayout::TRANSFER_DST_OPTIMAL; +// The backend-agnostic queue family sentinels in `crate` must match the Vulkan +// values they stand for, since they are passed straight through to ash. +const _: () = { + assert!(crate::QUEUE_FAMILY_IGNORED == vk::QUEUE_FAMILY_IGNORED); + assert!(crate::QUEUE_FAMILY_EXTERNAL == vk::QUEUE_FAMILY_EXTERNAL); + assert!(crate::QUEUE_FAMILY_FOREIGN == vk::QUEUE_FAMILY_FOREIGN_EXT); +}; + impl super::Texture { fn map_buffer_copies(&self, regions: T) -> impl Iterator where @@ -253,6 +261,16 @@ impl crate::CommandEncoder for super::CommandEncoder { let dst_layout = conv::derive_image_layout(bar.usage.to, bar.texture.format); dst_stages |= dst_stage; + // Insert a queue family ownership transfer if the caller requested + // one (used for textures imported from external memory). When no + // transfer is requested, both indices are `QUEUE_FAMILY_IGNORED`, + // which the spec treats as "no transfer". + let (src_queue_family_index, dst_queue_family_index) = + match bar.queue_family_ownership_transfer { + Some(transfer) => (transfer.src, transfer.dst), + None => (vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED), + }; + vk_barriers.push( vk::ImageMemoryBarrier::default() .image(bar.texture.raw) @@ -260,7 +278,9 @@ impl crate::CommandEncoder for super::CommandEncoder { .src_access_mask(src_access) .dst_access_mask(dst_access) .old_layout(src_layout) - .new_layout(dst_layout), + .new_layout(dst_layout) + .src_queue_family_index(src_queue_family_index) + .dst_queue_family_index(dst_queue_family_index), ); }