Skip to content

Commit fa9ddce

Browse files
committed
vulkan: add DXGI flip-model interop swapchain via D3D12 direct back-buffer sharing (Windows)
Vulkan renders directly into the DXGI flip-model back buffers, which are created on a LUID-matched interop D3D12 device and shared into Vulkan as external-memory images (no copy). A shared D3D12 fence orders Vulkan rendering ahead of a no-op present command list that binds each back buffer and transitions it to PRESENT. Opt-in via VulkanSwapchainKind / WGPU_VULKAN_SWAPCHAIN_KIND; the native VK_KHR_swapchain path stays the default.
1 parent 3eada12 commit fa9ddce

10 files changed

Lines changed: 1247 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@ A new standalone example, `examples/standalone/03_hdr_surface`, prints a surface
143143

144144
By @stuartparmenter in [#9658](https://github.com/gfx-rs/wgpu/pull/9658).
145145

146+
#### DXGI flip-model swapchain for Vulkan on Windows
147+
148+
The Vulkan backend can now present on Windows through a DXGI flip-model swapchain that wgpu drives directly, rather than the native `VK_KHR_swapchain`. The native swapchain is implemented by the driver on its own DXGI swapchain, which wgpu cannot configure and which often paces frames poorly. Driving the flip-model swapchain ourselves gives the same presentation control the DX12 backend has, so frame pacing is steadier and present latency is lower. This is opt-in; the native swapchain stays the default.
149+
150+
Turn it on at instance creation, or set `WGPU_VULKAN_SWAPCHAIN_KIND` to `native`, `dxgi-hwnd`, or `dxgi-visual`:
151+
152+
```diff
153+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
154+
backends: wgpu::Backends::VULKAN,
155+
+ backend_options: wgpu::BackendOptions {
156+
+ vulkan: wgpu::VulkanBackendOptions {
157+
+ swapchain_kind: wgpu::VulkanSwapchainKind::DxgiFromHwnd,
158+
+ },
159+
+ ..Default::default()
160+
+ },
161+
..Default::default()
162+
});
163+
```
164+
165+
A DXGI kind needs a device LUID, timeline semaphores, and `VK_KHR_external_memory_win32` / `VK_KHR_external_semaphore_win32`; adapters without them cannot present under it.
166+
167+
By @cwfitzgerald in [#XXXX](https://github.com/gfx-rs/wgpu/pull/XXXX).
168+
146169
### Added/New Features
147170

148171
#### General

wgpu-hal/src/auxil/dxgi/conv.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,8 @@ pub fn map_texture_format_nosrgb(format: wgt::TextureFormat) -> Dxgi::Common::DX
120120
/// The typeless DXGI format family for `format`, for the color formats wgpu may view as both their
121121
/// sRGB and non-sRGB form. Returns `None` for formats with no such castable typeless family.
122122
///
123-
/// Shared by the DX12 resource-creation path ([`map_texture_format_for_resource`]) and the Windows
124-
/// Vulkan DXGI interop swapchain, which creates its shared interop textures typeless so the imported
125-
/// Vulkan image can be viewed through either form.
123+
/// Used by the DX12 resource-creation path ([`map_texture_format_for_resource`]).
124+
#[cfg(dx12)]
126125
pub fn map_texture_format_typeless(
127126
format: wgt::TextureFormat,
128127
) -> Option<Dxgi::Common::DXGI_FORMAT> {

wgpu-hal/src/vulkan/adapter.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,10 @@ pub struct PhysicalDeviceProperties {
11851185
/// `VK_EXT_pci_bus_info` extension.
11861186
pci_bus_info: Option<vk::PhysicalDevicePCIBusInfoPropertiesEXT<'static>>,
11871187

1188+
/// Device identity properties from Vulkan 1.1, including the device LUID used to
1189+
/// match the Vulkan physical device against a DXGI adapter for interop.
1190+
id: Option<vk::PhysicalDeviceIDProperties<'static>>,
1191+
11881192
/// The device API version.
11891193
///
11901194
/// Which is the version of Vulkan supported for device-level functionality.
@@ -1340,6 +1344,11 @@ impl PhysicalDeviceProperties {
13401344
extensions.push(khr::external_memory_win32::NAME);
13411345
}
13421346

1347+
// Optional `VK_KHR_external_semaphore_win32`
1348+
if self.supports_extension(khr::external_semaphore_win32::NAME) {
1349+
extensions.push(khr::external_semaphore_win32::NAME);
1350+
}
1351+
13431352
// Optional `VK_KHR_external_memory_fd`
13441353
if self.supports_extension(khr::external_memory_fd::NAME) {
13451354
extensions.push(khr::external_memory_fd::NAME);
@@ -1973,6 +1982,13 @@ impl super::InstanceShared {
19731982
properties2 = properties2.push_next(next);
19741983
}
19751984

1985+
if capabilities.device_api_version >= vk::API_VERSION_1_1 {
1986+
let next = capabilities
1987+
.id
1988+
.insert(vk::PhysicalDeviceIDProperties::default());
1989+
properties2 = properties2.push_next(next);
1990+
}
1991+
19761992
unsafe {
19771993
get_device_properties.get_physical_device_properties2(phd, &mut properties2)
19781994
};
@@ -2422,6 +2438,9 @@ impl super::Instance {
24222438
max_draw_indirect_count: phd_capabilities.properties.limits.max_draw_indirect_count,
24232439
non_coherent_map_mask: phd_capabilities.properties.limits.non_coherent_atom_size - 1,
24242440
can_present: true,
2441+
device_luid: phd_capabilities
2442+
.id
2443+
.and_then(|id| (id.device_luid_valid == vk::TRUE).then_some(id.device_luid)),
24252444
//TODO: make configurable
24262445
robust_buffer_access: phd_features.core.robust_buffer_access != 0,
24272446
robust_image_access: match phd_features.robustness2 {
@@ -2662,6 +2681,15 @@ impl super::Adapter {
26622681
} else {
26632682
None
26642683
};
2684+
let external_semaphore_win32_fn =
2685+
if enabled_extensions.contains(&khr::external_semaphore_win32::NAME) {
2686+
Some(khr::external_semaphore_win32::Device::new(
2687+
&self.instance.raw,
2688+
&raw_device,
2689+
))
2690+
} else {
2691+
None
2692+
};
26652693

26662694
let naga_options = {
26672695
use naga::back::spv;
@@ -2917,6 +2945,7 @@ impl super::Adapter {
29172945
ray_tracing_pipelines: ray_tracing_pipeline_fns,
29182946
mesh_shading: mesh_shading_fns,
29192947
external_memory_fd: external_memory_fd_fn,
2948+
external_semaphore_win32: external_semaphore_win32_fn,
29202949
},
29212950
pipeline_cache_validation_key,
29222951
vendor_id: self.phd_capabilities.properties.vendor_id,
@@ -2933,6 +2962,8 @@ impl super::Adapter {
29332962
texture_identity_factory: super::ResourceIdentityFactory::new(),
29342963
texture_view_identity_factory: super::ResourceIdentityFactory::new(),
29352964
empty_descriptor_set_layout,
2965+
#[cfg(windows)]
2966+
dxgi_interop: once_cell::sync::OnceCell::new(),
29362967
});
29372968

29382969
let relay_semaphores = super::RelaySemaphores::new(&shared)?;

wgpu-hal/src/vulkan/command.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ use hashbrown::hash_map::Entry;
77
const ALLOCATION_GRANULARITY: u32 = 16;
88
const DST_IMAGE_LAYOUT: vk::ImageLayout = vk::ImageLayout::TRANSFER_DST_OPTIMAL;
99

10+
/// The layout for `usage`, honoring a texture's overridden present layout. Identical to
11+
/// [`conv::derive_image_layout`] except that `TextureUses::PRESENT` resolves to the texture's own
12+
/// `present_layout` (`GENERAL` for DXGI interop images rather than `PRESENT_SRC_KHR`).
13+
fn barrier_image_layout(texture: &super::Texture, usage: wgt::TextureUses) -> vk::ImageLayout {
14+
if usage == wgt::TextureUses::PRESENT {
15+
texture.present_layout
16+
} else {
17+
conv::derive_image_layout(usage, texture.format)
18+
}
19+
}
20+
1021
impl super::Texture {
1122
fn map_buffer_copies<T>(&self, regions: T) -> impl Iterator<Item = vk::BufferImageCopy>
1223
where
@@ -247,10 +258,10 @@ impl crate::CommandEncoder for super::CommandEncoder {
247258
&self.device.private_caps,
248259
);
249260
let (src_stage, src_access) = conv::map_texture_usage_to_barrier(bar.usage.from);
250-
let src_layout = conv::derive_image_layout(bar.usage.from, bar.texture.format);
261+
let src_layout = barrier_image_layout(bar.texture, bar.usage.from);
251262
src_stages |= src_stage;
252263
let (dst_stage, dst_access) = conv::map_texture_usage_to_barrier(bar.usage.to);
253-
let dst_layout = conv::derive_image_layout(bar.usage.to, bar.texture.format);
264+
let dst_layout = barrier_image_layout(bar.texture, bar.usage.to);
254265
dst_stages |= dst_stage;
255266

256267
vk_barriers.push(

wgpu-hal/src/vulkan/device.rs

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ impl super::Device {
314314
format: desc.format,
315315
copy_size: desc.copy_extent(),
316316
identity,
317+
present_layout: vk::ImageLayout::PRESENT_SRC_KHR,
317318
}
318319
}
319320

@@ -444,28 +445,35 @@ impl super::Device {
444445
})
445446
}
446447

448+
/// Create a [`super::Texture`] backed by a D3D11 texture or D3D12 resource shared via an NT
449+
/// handle.
450+
///
451+
/// `handle_type` selects how `handle` is interpreted: `D3D11_TEXTURE` for an `ID3D11Texture2D`
452+
/// shared handle, or `D3D12_RESOURCE` for an `ID3D12Resource` shared handle.
453+
///
447454
/// # Safety
448455
///
449-
/// - Vulkan (with VK_KHR_external_memory_win32)
450-
/// - The `d3d11_shared_handle` must be valid and respecting `desc`
451-
/// - `VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT` flag is used because we need to hold a reference to the handle
456+
/// - `VK_KHR_external_memory_win32` must be enabled.
457+
/// - The texture must be imported on the same physical device that owns the shared resource.
458+
/// - `handle` must be a valid shared NT handle whose resource matches `desc` and `handle_type`.
452459
#[cfg(windows)]
453-
pub unsafe fn texture_from_d3d11_shared_handle(
460+
pub unsafe fn texture_from_shared_handle(
454461
&self,
455-
d3d11_shared_handle: windows::Win32::Foundation::HANDLE,
462+
handle: windows::Win32::Foundation::HANDLE,
463+
handle_type: vk::ExternalMemoryHandleTypeFlags,
456464
desc: &crate::TextureDescriptor,
457465
) -> Result<super::Texture, crate::DeviceError> {
458466
if !self
459467
.shared
460-
.features
461-
.contains(wgt::Features::VULKAN_EXTERNAL_MEMORY_WIN32)
468+
.enabled_extensions
469+
.contains(&ash::khr::external_memory_win32::NAME)
462470
{
463471
log::error!("Vulkan driver does not support VK_KHR_external_memory_win32");
464472
return Err(crate::DeviceError::Unexpected);
465473
}
466474

467-
let mut external_memory_image_info = vk::ExternalMemoryImageCreateInfo::default()
468-
.handle_types(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE);
475+
let mut external_memory_image_info =
476+
vk::ExternalMemoryImageCreateInfo::default().handle_types(handle_type);
469477

470478
let image =
471479
self.create_image_without_memory(desc, Some(&mut external_memory_image_info))?;
@@ -476,8 +484,8 @@ impl super::Device {
476484
vk::MemoryDedicatedAllocateInfo::default().image(image.raw);
477485

478486
let mut import_memory_info = vk::ImportMemoryWin32HandleInfoKHR::default()
479-
.handle_type(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE)
480-
.handle(d3d11_shared_handle.0 as _);
487+
.handle_type(handle_type)
488+
.handle(handle.0 as _);
481489
// TODO: We should use `push_next` instead, but currently ash does not provide this method for the `ImportMemoryWin32HandleInfoKHR` type.
482490
#[allow(clippy::unnecessary_mut_passed)]
483491
{
@@ -3042,6 +3050,59 @@ impl super::DeviceShared {
30423050
}
30433051
}
30443052

3053+
/// Creates a timeline semaphore with the given initial value.
3054+
///
3055+
/// When the semaphore is to be backed by an imported D3D11/D3D12 fence, pass the value `0` and
3056+
/// feed the result to [`Self::import_timeline_semaphore_d3d12_fence`], which replaces the
3057+
/// payload with the shared fence.
3058+
#[cfg(windows)]
3059+
pub(super) fn new_timeline_semaphore(
3060+
&self,
3061+
initial_value: u64,
3062+
name: &str,
3063+
) -> Result<vk::Semaphore, crate::DeviceError> {
3064+
let mut type_info = vk::SemaphoreTypeCreateInfo::default()
3065+
.semaphore_type(vk::SemaphoreType::TIMELINE)
3066+
.initial_value(initial_value);
3067+
let info = vk::SemaphoreCreateInfo::default().push_next(&mut type_info);
3068+
let semaphore = unsafe { self.raw.create_semaphore(&info, None) }
3069+
.map_err(super::map_host_device_oom_err)?;
3070+
unsafe { self.set_object_name(semaphore, name) };
3071+
Ok(semaphore)
3072+
}
3073+
3074+
/// Imports a shared D3D11/D3D12 fence (an `ID3D11Fence`/`ID3D12Fence` NT handle) into an
3075+
/// existing timeline `semaphore`, so the same monotonic counter is visible to both APIs.
3076+
///
3077+
/// # Safety
3078+
///
3079+
/// - `handle` must be a valid shared NT handle to an `ID3D11Fence`/`ID3D12Fence`.
3080+
/// - `semaphore` must be a timeline semaphore with no pending operations.
3081+
#[cfg(windows)]
3082+
pub(super) unsafe fn import_timeline_semaphore_d3d12_fence(
3083+
&self,
3084+
semaphore: vk::Semaphore,
3085+
handle: windows::Win32::Foundation::HANDLE,
3086+
) -> Result<(), crate::DeviceError> {
3087+
let ext = self
3088+
.extension_fns
3089+
.external_semaphore_win32
3090+
.as_ref()
3091+
.ok_or_else(|| {
3092+
log::error!("Vulkan driver does not support VK_KHR_external_semaphore_win32");
3093+
crate::DeviceError::Unexpected
3094+
})?;
3095+
3096+
let import_info = vk::ImportSemaphoreWin32HandleInfoKHR::default()
3097+
.semaphore(semaphore)
3098+
.handle_type(vk::ExternalSemaphoreHandleTypeFlags::D3D12_FENCE)
3099+
.handle(handle.0 as _);
3100+
3101+
unsafe { ext.import_semaphore_win32_handle(&import_info) }
3102+
.map_err(super::map_host_device_oom_err)?;
3103+
Ok(())
3104+
}
3105+
30453106
pub(super) fn wait_for_fence(
30463107
&self,
30473108
fence: &super::Fence,

wgpu-hal/src/vulkan/instance.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,22 @@ impl super::Instance {
368368

369369
let drop_guard = crate::DropGuard::from_option(drop_callback);
370370

371+
// Resolve the DXGI swapchain backend. A requested DXGI kind degrades to `Native` when DXGI
372+
// is unavailable (instance-wide, terminal for the DXGI path).
373+
#[cfg(windows)]
374+
let (swapchain_kind, dxgi_instance) =
375+
super::swapchain::init_dxgi_instance(swapchain_kind, flags);
376+
#[cfg(not(windows))]
377+
let swapchain_kind = if swapchain_kind == wgt::VulkanSwapchainKind::Native {
378+
swapchain_kind
379+
} else {
380+
log::warn!(
381+
"VulkanSwapchainKind::{swapchain_kind:?} is only supported on Windows; \
382+
using the native Vulkan swapchain"
383+
);
384+
wgt::VulkanSwapchainKind::Native
385+
};
386+
371387
Ok(Self {
372388
shared: Arc::new(super::InstanceShared {
373389
raw: raw_instance,
@@ -382,6 +398,8 @@ impl super::Instance {
382398
instance_api_version,
383399
android_sdk_version,
384400
swapchain_kind,
401+
#[cfg(windows)]
402+
dxgi_instance,
385403
}),
386404
})
387405
}
@@ -490,13 +508,31 @@ impl super::Instance {
490508
hinstance: vk::HINSTANCE,
491509
hwnd: vk::HWND,
492510
) -> Result<super::Surface, crate::InstanceError> {
493-
// Scaffold: the DXGI swapchain surface does not exist yet, so a requested DXGI kind
494-
// falls back to the native surface. Removed once `DxgiSurface` lands.
495-
if self.shared.swapchain_kind != wgt::VulkanSwapchainKind::Native {
496-
log::warn!(
497-
"VulkanSwapchainKind::{:?} is not yet implemented; using the native swapchain",
498-
self.shared.swapchain_kind
499-
);
511+
// The swapchain backend is fixed here and immutable for the surface's lifetime.
512+
#[cfg(windows)]
513+
match self.shared.swapchain_kind {
514+
wgt::VulkanSwapchainKind::Native => {}
515+
kind @ (wgt::VulkanSwapchainKind::DxgiFromHwnd
516+
| wgt::VulkanSwapchainKind::DxgiFromVisual) => {
517+
use windows::Win32::Foundation::HWND;
518+
let hwnd = HWND(hwnd as _);
519+
let target = match kind {
520+
wgt::VulkanSwapchainKind::DxgiFromHwnd => {
521+
crate::auxil::dxgi::swapchain::SurfaceTarget::WndHandle(hwnd)
522+
}
523+
_ => crate::auxil::dxgi::swapchain::SurfaceTarget::VisualFromWndHandle {
524+
handle: hwnd,
525+
dcomp_state: parking_lot::Mutex::new(Default::default()),
526+
},
527+
};
528+
return Ok(super::Surface {
529+
swapchain: RwLock::new(None),
530+
inner: Box::new(super::swapchain::DxgiSurface::new(
531+
Arc::clone(&self.shared),
532+
target,
533+
)),
534+
});
535+
}
500536
}
501537

502538
if !self.shared.extensions.contains(&khr::win32_surface::NAME) {
@@ -1008,6 +1044,32 @@ impl crate::Instance for super::Instance {
10081044
}
10091045
}
10101046

1047+
// When a DXGI swapchain is selected, an adapter that lacks the interop prerequisites
1048+
// (a valid LUID, timeline semaphores, and both external-memory/semaphore win32 extensions)
1049+
// cannot present through the DXGI path, and there is no per-surface native fallback.
1050+
#[cfg(windows)]
1051+
if self.shared.swapchain_kind != wgt::VulkanSwapchainKind::Native {
1052+
for exposed in exposed_adapters.iter_mut() {
1053+
let interop_capable = exposed.adapter.private_caps.device_luid.is_some()
1054+
&& exposed.adapter.private_caps.timeline_semaphores
1055+
&& exposed
1056+
.adapter
1057+
.phd_capabilities
1058+
.supports_extension(khr::external_memory_win32::NAME)
1059+
&& exposed
1060+
.adapter
1061+
.phd_capabilities
1062+
.supports_extension(khr::external_semaphore_win32::NAME);
1063+
if !interop_capable {
1064+
log::warn!(
1065+
"Disabling presentation on '{}': missing DXGI interop prerequisites",
1066+
exposed.info.name
1067+
);
1068+
exposed.adapter.private_caps.can_present = false;
1069+
}
1070+
}
1071+
}
1072+
10111073
exposed_adapters
10121074
}
10131075
}

0 commit comments

Comments
 (0)