diff --git a/crates/bevy_openxr/Cargo.toml b/crates/bevy_openxr/Cargo.toml index 88541d4..c597dab 100644 --- a/crates/bevy_openxr/Cargo.toml +++ b/crates/bevy_openxr/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2021" [features] -default = ["vulkan", "passthrough"] +default = ["vulkan", "d3d12", "passthrough"] vulkan = ["dep:ash"] +d3d12 = ["wgpu/dx12", "wgpu-hal/dx12", "dep:winapi", "dep:d3d12"] passthrough = [] [dev-dependencies] @@ -30,3 +31,5 @@ openxr = { version = "0.18.0", features = ["mint"] } [target.'cfg(target_family = "windows")'.dependencies] openxr = { version = "0.18.0", features = ["mint", "static"] } +winapi = { version = "0.3.9", optional = true } +d3d12 = { version = "0.19", features = ["libloading"], optional = true } diff --git a/crates/bevy_openxr/src/openxr/error.rs b/crates/bevy_openxr/src/openxr/error.rs index 30c0893..e6a9747 100644 --- a/crates/bevy_openxr/src/openxr/error.rs +++ b/crates/bevy_openxr/src/openxr/error.rs @@ -61,6 +61,8 @@ mod init_error { VulkanError(ash::vk::Result), #[cfg(feature = "vulkan")] VulkanLoadingError(ash::LoadingError), + #[cfg(all(feature = "d3d12", windows))] + FailedToFindD3D12Adapter, } impl fmt::Display for InitError { @@ -72,6 +74,11 @@ mod init_error { InitError::VulkanLoadingError(error) => { write!(f, "Vulkan loading error: {}", error) } + #[cfg(all(feature = "d3d12", windows))] + InitError::FailedToFindD3D12Adapter => write!( + f, + "Failed to find D3D12 adapter matching LUID provided by the OpenXR runtime" + ), } } } diff --git a/crates/bevy_openxr/src/openxr/graphics.rs b/crates/bevy_openxr/src/openxr/graphics.rs index 3d152ce..aa93290 100644 --- a/crates/bevy_openxr/src/openxr/graphics.rs +++ b/crates/bevy_openxr/src/openxr/graphics.rs @@ -1,3 +1,5 @@ +#[cfg(all(feature = "d3d12", windows))] +mod d3d12; #[cfg(feature = "vulkan")] pub mod vulkan; @@ -62,7 +64,12 @@ impl GraphicsType for () { pub type GraphicsBackend = GraphicsWrap<()>; impl GraphicsBackend { - const ALL: &'static [Self] = &[Self::Vulkan(())]; + const ALL: &'static [Self] = &[ + #[cfg(feature = "vulkan")] + Self::Vulkan(()), + #[cfg(all(feature = "d3d12", windows))] + Self::D3D12(()), + ]; pub fn available_backends(exts: &OxrExtensions) -> Vec { Self::ALL @@ -89,6 +96,8 @@ impl GraphicsBackend { pub enum GraphicsWrap { #[cfg(feature = "vulkan")] Vulkan(T::Inner), + #[cfg(all(feature = "d3d12", windows))] + D3D12(T::Inner), } impl GraphicsWrap { @@ -157,6 +166,12 @@ macro_rules! graphics_match { type Api = openxr::Vulkan; graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?) }, + #[cfg(all(feature = "d3d12", windows))] + $crate::graphics::GraphicsWrap::D3D12($var) => { + #[allow(unused)] + type Api = openxr::D3D12; + graphics_match!(@arm_impl D3D12; $expr $(=> $($return)*)?) + }, } }; diff --git a/crates/bevy_openxr/src/openxr/graphics/d3d12.rs b/crates/bevy_openxr/src/openxr/graphics/d3d12.rs new file mode 100644 index 0000000..82dca9a --- /dev/null +++ b/crates/bevy_openxr/src/openxr/graphics/d3d12.rs @@ -0,0 +1,347 @@ +use bevy::log::error; +use wgpu_hal::{Adapter, Instance}; +use winapi::shared::dxgiformat::DXGI_FORMAT; +use winapi::um::d3d12 as winapi_d3d12; + +use super::{GraphicsExt, GraphicsType, GraphicsWrap}; +use crate::error::OxrError; +use crate::types::{AppInfo, OxrExtensions, Result, WgpuGraphics}; + +unsafe impl GraphicsExt for openxr::D3D12 { + fn wrap(item: T::Inner) -> GraphicsWrap { + GraphicsWrap::D3D12(item) + } + + fn required_exts() -> OxrExtensions { + let mut extensions = openxr::ExtensionSet::default(); + extensions.khr_d3d12_enable = true; + extensions.into() + } + + fn from_wgpu_format(format: wgpu::TextureFormat) -> Option { + wgpu_to_d3d12(format) + } + + fn into_wgpu_format(format: Self::Format) -> Option { + d3d12_to_wgpu(format) + } + + unsafe fn to_wgpu_img( + image: Self::SwapchainImage, + device: &wgpu::Device, + format: wgpu::TextureFormat, + resolution: bevy::prelude::UVec2, + ) -> Result { + let wgpu_hal_texture = ::Device::texture_from_raw( + d3d12::ComPtr::from_raw(image as *mut _), + format, + wgpu::TextureDimension::D2, + wgpu::Extent3d { + width: resolution.x, + height: resolution.y, + depth_or_array_layers: 2, + }, + 1, + 1, + ); + let texture = device.create_texture_from_hal::( + wgpu_hal_texture, + &wgpu::TextureDescriptor { + label: Some("VR Swapchain"), + size: wgpu::Extent3d { + width: resolution.x, + height: resolution.y, + depth_or_array_layers: 2, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }, + ); + Ok(texture) + } + + fn init_graphics( + app_info: &AppInfo, + instance: &openxr::Instance, + system_id: openxr::SystemId, + ) -> Result<(WgpuGraphics, Self::SessionCreateInfo)> { + let reqs = instance.graphics_requirements::(system_id)?; + + let instance_descriptor = &wgpu_hal::InstanceDescriptor { + name: &app_info.name, + dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env().unwrap_or( + wgpu::Dx12Compiler::Dxc { + dxil_path: None, + dxc_path: None, + }, + ), + flags: wgpu::InstanceFlags::from_build_config().with_env(), + gles_minor_version: Default::default(), + }; + let wgpu_raw_instance: wgpu_hal::dx12::Instance = + unsafe { wgpu_hal::dx12::Instance::init(instance_descriptor)? }; + let wgpu_adapters: Vec> = + unsafe { wgpu_raw_instance.enumerate_adapters() }; + + let wgpu_exposed_adapter = wgpu_adapters + .into_iter() + .find(|a| { + let mut desc = unsafe { std::mem::zeroed() }; + unsafe { a.adapter.raw_adapter().GetDesc1(&mut desc) }; + desc.AdapterLuid.HighPart == reqs.adapter_luid.HighPart + && desc.AdapterLuid.LowPart == reqs.adapter_luid.LowPart + }) + .ok_or(OxrError::InitError( + crate::error::InitError::FailedToFindD3D12Adapter, + ))?; + + let wgpu_instance = + unsafe { wgpu::Instance::from_hal::(wgpu_raw_instance) }; + + let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES + | wgpu::Features::MULTIVIEW + | wgpu::Features::MULTI_DRAW_INDIRECT_COUNT + | wgpu::Features::MULTI_DRAW_INDIRECT; + + let wgpu_limits = wgpu_exposed_adapter.capabilities.limits.clone(); + + let wgpu_open_device = unsafe { + wgpu_exposed_adapter + .adapter + .open(wgpu_features, &wgpu_limits)? + }; + + let device_supported_feature_level: d3d12::FeatureLevel = + get_device_feature_level(wgpu_open_device.device.raw_device()); + + if (device_supported_feature_level as u32) < (reqs.min_feature_level as u32) { + error!( + "OpenXR runtime requires D3D12 feature level >= {}", + reqs.min_feature_level + ); + return Err(OxrError::FailedGraphicsRequirements); + } + + let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) }; + let raw_device = wgpu_open_device.device.raw_device().as_mut_ptr(); + let raw_queue = wgpu_open_device.device.raw_queue().as_mut_ptr(); + let (wgpu_device, wgpu_queue) = unsafe { + wgpu_adapter.create_device_from_hal( + wgpu_open_device, + &wgpu::DeviceDescriptor { + label: Some("bevy_oxr device"), + required_features: wgpu_features, + required_limits: wgpu_limits, + }, + None, + )? + }; + + Ok(( + WgpuGraphics( + wgpu_device, + wgpu_queue, + wgpu_adapter.get_info(), + wgpu_adapter, + wgpu_instance, + ), + Self::SessionCreateInfo { + device: raw_device.cast(), + queue: raw_queue.cast(), + }, + )) + } +} + +// Extracted from https://github.com/gfx-rs/wgpu/blob/1161a22f4fbb4fc204eb06f2ac4243f83e0e980d/wgpu-hal/src/dx12/adapter.rs#L73-L94 +// license: MIT OR Apache-2.0 +fn get_device_feature_level( + device: &d3d12::ComPtr, +) -> d3d12::FeatureLevel { + // Detect the highest supported feature level. + let d3d_feature_level = [ + d3d12::FeatureLevel::L12_1, + d3d12::FeatureLevel::L12_0, + d3d12::FeatureLevel::L11_1, + d3d12::FeatureLevel::L11_0, + ]; + type FeatureLevelsInfo = winapi_d3d12::D3D12_FEATURE_DATA_FEATURE_LEVELS; + let mut device_levels: FeatureLevelsInfo = unsafe { std::mem::zeroed() }; + device_levels.NumFeatureLevels = d3d_feature_level.len() as u32; + device_levels.pFeatureLevelsRequested = d3d_feature_level.as_ptr().cast(); + unsafe { + device.CheckFeatureSupport( + winapi_d3d12::D3D12_FEATURE_FEATURE_LEVELS, + (&mut device_levels as *mut FeatureLevelsInfo).cast(), + std::mem::size_of::() as _, + ) + }; + // This cast should never fail because we only requested feature levels that are already in the enum. + let max_feature_level = d3d12::FeatureLevel::try_from(device_levels.MaxSupportedFeatureLevel) + .expect("Unexpected feature level"); + max_feature_level +} + +fn d3d12_to_wgpu(format: DXGI_FORMAT) -> Option { + use wgpu::TextureFormat as Tf; + use winapi::shared::dxgiformat::*; + + Some(match format { + DXGI_FORMAT_R8_UNORM => Tf::R8Unorm, + DXGI_FORMAT_R8_SNORM => Tf::R8Snorm, + DXGI_FORMAT_R8_UINT => Tf::R8Uint, + DXGI_FORMAT_R8_SINT => Tf::R8Sint, + DXGI_FORMAT_R16_UINT => Tf::R16Uint, + DXGI_FORMAT_R16_SINT => Tf::R16Sint, + DXGI_FORMAT_R16_UNORM => Tf::R16Unorm, + DXGI_FORMAT_R16_SNORM => Tf::R16Snorm, + DXGI_FORMAT_R16_FLOAT => Tf::R16Float, + DXGI_FORMAT_R8G8_UNORM => Tf::Rg8Unorm, + DXGI_FORMAT_R8G8_SNORM => Tf::Rg8Snorm, + DXGI_FORMAT_R8G8_UINT => Tf::Rg8Uint, + DXGI_FORMAT_R8G8_SINT => Tf::Rg8Sint, + DXGI_FORMAT_R16G16_UNORM => Tf::Rg16Unorm, + DXGI_FORMAT_R16G16_SNORM => Tf::Rg16Snorm, + DXGI_FORMAT_R32_UINT => Tf::R32Uint, + DXGI_FORMAT_R32_SINT => Tf::R32Sint, + DXGI_FORMAT_R32_FLOAT => Tf::R32Float, + DXGI_FORMAT_R16G16_UINT => Tf::Rg16Uint, + DXGI_FORMAT_R16G16_SINT => Tf::Rg16Sint, + DXGI_FORMAT_R16G16_FLOAT => Tf::Rg16Float, + DXGI_FORMAT_R8G8B8A8_UNORM => Tf::Rgba8Unorm, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => Tf::Rgba8UnormSrgb, + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => Tf::Bgra8UnormSrgb, + DXGI_FORMAT_R8G8B8A8_SNORM => Tf::Rgba8Snorm, + DXGI_FORMAT_B8G8R8A8_UNORM => Tf::Bgra8Unorm, + DXGI_FORMAT_R8G8B8A8_UINT => Tf::Rgba8Uint, + DXGI_FORMAT_R8G8B8A8_SINT => Tf::Rgba8Sint, + DXGI_FORMAT_R9G9B9E5_SHAREDEXP => Tf::Rgb9e5Ufloat, + DXGI_FORMAT_R10G10B10A2_UINT => Tf::Rgb10a2Uint, + DXGI_FORMAT_R10G10B10A2_UNORM => Tf::Rgb10a2Unorm, + DXGI_FORMAT_R11G11B10_FLOAT => Tf::Rg11b10Float, + DXGI_FORMAT_R32G32_UINT => Tf::Rg32Uint, + DXGI_FORMAT_R32G32_SINT => Tf::Rg32Sint, + DXGI_FORMAT_R32G32_FLOAT => Tf::Rg32Float, + DXGI_FORMAT_R16G16B16A16_UINT => Tf::Rgba16Uint, + DXGI_FORMAT_R16G16B16A16_SINT => Tf::Rgba16Sint, + DXGI_FORMAT_R16G16B16A16_UNORM => Tf::Rgba16Unorm, + DXGI_FORMAT_R16G16B16A16_SNORM => Tf::Rgba16Snorm, + DXGI_FORMAT_R16G16B16A16_FLOAT => Tf::Rgba16Float, + DXGI_FORMAT_R32G32B32A32_UINT => Tf::Rgba32Uint, + DXGI_FORMAT_R32G32B32A32_SINT => Tf::Rgba32Sint, + DXGI_FORMAT_R32G32B32A32_FLOAT => Tf::Rgba32Float, + DXGI_FORMAT_D24_UNORM_S8_UINT => Tf::Stencil8, + DXGI_FORMAT_D16_UNORM => Tf::Depth16Unorm, + DXGI_FORMAT_D32_FLOAT => Tf::Depth32Float, + DXGI_FORMAT_D32_FLOAT_S8X24_UINT => Tf::Depth32FloatStencil8, + DXGI_FORMAT_NV12 => Tf::NV12, + DXGI_FORMAT_BC1_UNORM => Tf::Bc1RgbaUnorm, + DXGI_FORMAT_BC1_UNORM_SRGB => Tf::Bc1RgbaUnormSrgb, + DXGI_FORMAT_BC2_UNORM => Tf::Bc2RgbaUnorm, + DXGI_FORMAT_BC2_UNORM_SRGB => Tf::Bc2RgbaUnormSrgb, + DXGI_FORMAT_BC3_UNORM => Tf::Bc3RgbaUnorm, + DXGI_FORMAT_BC3_UNORM_SRGB => Tf::Bc3RgbaUnormSrgb, + DXGI_FORMAT_BC4_UNORM => Tf::Bc4RUnorm, + DXGI_FORMAT_BC4_SNORM => Tf::Bc4RSnorm, + DXGI_FORMAT_BC5_UNORM => Tf::Bc5RgUnorm, + DXGI_FORMAT_BC5_SNORM => Tf::Bc5RgSnorm, + DXGI_FORMAT_BC6H_UF16 => Tf::Bc6hRgbUfloat, + DXGI_FORMAT_BC6H_SF16 => Tf::Bc6hRgbFloat, + DXGI_FORMAT_BC7_UNORM => Tf::Bc7RgbaUnorm, + DXGI_FORMAT_BC7_UNORM_SRGB => Tf::Bc7RgbaUnormSrgb, + _ => return None, + }) +} + +fn wgpu_to_d3d12(format: wgpu::TextureFormat) -> Option { + // Copied wholesale from: + // https://github.com/gfx-rs/wgpu/blob/v0.19/wgpu-hal/src/auxil/dxgi/conv.rs#L12-L94 + // license: MIT OR Apache-2.0 + use wgpu::TextureFormat as Tf; + use winapi::shared::dxgiformat::*; + + Some(match format { + Tf::R8Unorm => DXGI_FORMAT_R8_UNORM, + Tf::R8Snorm => DXGI_FORMAT_R8_SNORM, + Tf::R8Uint => DXGI_FORMAT_R8_UINT, + Tf::R8Sint => DXGI_FORMAT_R8_SINT, + Tf::R16Uint => DXGI_FORMAT_R16_UINT, + Tf::R16Sint => DXGI_FORMAT_R16_SINT, + Tf::R16Unorm => DXGI_FORMAT_R16_UNORM, + Tf::R16Snorm => DXGI_FORMAT_R16_SNORM, + Tf::R16Float => DXGI_FORMAT_R16_FLOAT, + Tf::Rg8Unorm => DXGI_FORMAT_R8G8_UNORM, + Tf::Rg8Snorm => DXGI_FORMAT_R8G8_SNORM, + Tf::Rg8Uint => DXGI_FORMAT_R8G8_UINT, + Tf::Rg8Sint => DXGI_FORMAT_R8G8_SINT, + Tf::Rg16Unorm => DXGI_FORMAT_R16G16_UNORM, + Tf::Rg16Snorm => DXGI_FORMAT_R16G16_SNORM, + Tf::R32Uint => DXGI_FORMAT_R32_UINT, + Tf::R32Sint => DXGI_FORMAT_R32_SINT, + Tf::R32Float => DXGI_FORMAT_R32_FLOAT, + Tf::Rg16Uint => DXGI_FORMAT_R16G16_UINT, + Tf::Rg16Sint => DXGI_FORMAT_R16G16_SINT, + Tf::Rg16Float => DXGI_FORMAT_R16G16_FLOAT, + Tf::Rgba8Unorm => DXGI_FORMAT_R8G8B8A8_UNORM, + Tf::Rgba8UnormSrgb => DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + Tf::Bgra8UnormSrgb => DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + Tf::Rgba8Snorm => DXGI_FORMAT_R8G8B8A8_SNORM, + Tf::Bgra8Unorm => DXGI_FORMAT_B8G8R8A8_UNORM, + Tf::Rgba8Uint => DXGI_FORMAT_R8G8B8A8_UINT, + Tf::Rgba8Sint => DXGI_FORMAT_R8G8B8A8_SINT, + Tf::Rgb9e5Ufloat => DXGI_FORMAT_R9G9B9E5_SHAREDEXP, + Tf::Rgb10a2Uint => DXGI_FORMAT_R10G10B10A2_UINT, + Tf::Rgb10a2Unorm => DXGI_FORMAT_R10G10B10A2_UNORM, + Tf::Rg11b10Float => DXGI_FORMAT_R11G11B10_FLOAT, + Tf::Rg32Uint => DXGI_FORMAT_R32G32_UINT, + Tf::Rg32Sint => DXGI_FORMAT_R32G32_SINT, + Tf::Rg32Float => DXGI_FORMAT_R32G32_FLOAT, + Tf::Rgba16Uint => DXGI_FORMAT_R16G16B16A16_UINT, + Tf::Rgba16Sint => DXGI_FORMAT_R16G16B16A16_SINT, + Tf::Rgba16Unorm => DXGI_FORMAT_R16G16B16A16_UNORM, + Tf::Rgba16Snorm => DXGI_FORMAT_R16G16B16A16_SNORM, + Tf::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT, + Tf::Rgba32Uint => DXGI_FORMAT_R32G32B32A32_UINT, + Tf::Rgba32Sint => DXGI_FORMAT_R32G32B32A32_SINT, + Tf::Rgba32Float => DXGI_FORMAT_R32G32B32A32_FLOAT, + Tf::Stencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT, + Tf::Depth16Unorm => DXGI_FORMAT_D16_UNORM, + Tf::Depth24Plus => DXGI_FORMAT_D24_UNORM_S8_UINT, + Tf::Depth24PlusStencil8 => DXGI_FORMAT_D24_UNORM_S8_UINT, + Tf::Depth32Float => DXGI_FORMAT_D32_FLOAT, + Tf::Depth32FloatStencil8 => DXGI_FORMAT_D32_FLOAT_S8X24_UINT, + Tf::NV12 => DXGI_FORMAT_NV12, + Tf::Bc1RgbaUnorm => DXGI_FORMAT_BC1_UNORM, + Tf::Bc1RgbaUnormSrgb => DXGI_FORMAT_BC1_UNORM_SRGB, + Tf::Bc2RgbaUnorm => DXGI_FORMAT_BC2_UNORM, + Tf::Bc2RgbaUnormSrgb => DXGI_FORMAT_BC2_UNORM_SRGB, + Tf::Bc3RgbaUnorm => DXGI_FORMAT_BC3_UNORM, + Tf::Bc3RgbaUnormSrgb => DXGI_FORMAT_BC3_UNORM_SRGB, + Tf::Bc4RUnorm => DXGI_FORMAT_BC4_UNORM, + Tf::Bc4RSnorm => DXGI_FORMAT_BC4_SNORM, + Tf::Bc5RgUnorm => DXGI_FORMAT_BC5_UNORM, + Tf::Bc5RgSnorm => DXGI_FORMAT_BC5_SNORM, + Tf::Bc6hRgbUfloat => DXGI_FORMAT_BC6H_UF16, + Tf::Bc6hRgbFloat => DXGI_FORMAT_BC6H_SF16, + Tf::Bc7RgbaUnorm => DXGI_FORMAT_BC7_UNORM, + Tf::Bc7RgbaUnormSrgb => DXGI_FORMAT_BC7_UNORM_SRGB, + Tf::Etc2Rgb8Unorm + | Tf::Etc2Rgb8UnormSrgb + | Tf::Etc2Rgb8A1Unorm + | Tf::Etc2Rgb8A1UnormSrgb + | Tf::Etc2Rgba8Unorm + | Tf::Etc2Rgba8UnormSrgb + | Tf::EacR11Unorm + | Tf::EacR11Snorm + | Tf::EacRg11Unorm + | Tf::EacRg11Snorm + | Tf::Astc { + block: _, + channel: _, + } => return None, + }) +} diff --git a/crates/bevy_openxr/src/openxr/mod.rs b/crates/bevy_openxr/src/openxr/mod.rs index 20dbaf4..34f62f0 100644 --- a/crates/bevy_openxr/src/openxr/mod.rs +++ b/crates/bevy_openxr/src/openxr/mod.rs @@ -11,7 +11,6 @@ use init::OxrInitPlugin; use render::OxrRenderPlugin; use self::{ - exts::OxrExtensions, features::{handtracking::HandTrackingPlugin, passthrough::OxrPassthroughPlugin}, reference_space::OxrReferenceSpacePlugin, }; diff --git a/flake.nix b/flake.nix index ddf372a..268197c 100644 --- a/flake.nix +++ b/flake.nix @@ -38,6 +38,7 @@ targets.wasm32-unknown-unknown.stable.rust-std targets.aarch64-linux-android.stable.rust-std + targets.x86_64-pc-windows-gnu.stable.rust-std ]; androidComposition = pkgs.androidenv.composeAndroidPackages { abiVersions = ["arm64-v8a"];