diff --git a/Cargo.toml b/Cargo.toml index 4c935e7..094811e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0.75" +ash = "0.37.3" bevy = { git = "https://github.com/awtterpip/bevy", default-features = false, features = ["bevy_render"] } openxr = "0.17.1" wgpu = "0.16.0" @@ -18,4 +19,4 @@ bevy = { git = "https://github.com/awtterpip/bevy" } [[example]] name = "xr" -path = "examples/xr.rs" \ No newline at end of file +path = "examples/xr.rs" diff --git a/examples/xr.rs b/examples/xr.rs index 6fd6993..57868bf 100644 --- a/examples/xr.rs +++ b/examples/xr.rs @@ -1,6 +1,6 @@ //! A simple 3D scene with light shining over a cube sitting on a plane. -use bevy_openxr::DefaultXrPlugins; -use bevy::prelude::*; +use bevy_openxr::{DefaultXrPlugins, LEFT_XR_TEXTURE_HANDLE, RIGHT_XR_TEXTURE_HANDLE}; +use bevy::{prelude::*, render::camera::RenderTarget}; fn main() { App::new() @@ -43,4 +43,23 @@ fn setup( transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }); + + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + order: -1, + target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE), + ..default() + }, + ..default() + }); + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + order: -1, + target: RenderTarget::TextureView(RIGHT_XR_TEXTURE_HANDLE), + ..default() + }, + ..default() + }); } diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs new file mode 100644 index 0000000..f2a527b --- /dev/null +++ b/src/graphics/mod.rs @@ -0,0 +1,12 @@ +mod vulkan; + +use bevy::render::renderer::{RenderDevice, RenderQueue, RenderAdapterInfo, RenderAdapter}; +use bevy::window::RawHandleWrapper; +use wgpu::Instance; + +use crate::input::XrInput; +use crate::resources::{XrInstance, XrSession, XrEnvironmentBlendMode, XrSessionRunning, XrFrameWaiter, XrSwapchain, XrViews}; + +pub fn initialize_xr_graphics(window: Option) -> anyhow::Result<(RenderDevice, RenderQueue, RenderAdapterInfo, RenderAdapter, Instance, XrInstance, XrSession, XrEnvironmentBlendMode, XrSessionRunning, XrFrameWaiter, XrSwapchain, XrInput, XrViews)>{ + vulkan::initialize_xr_graphics(window) +} \ No newline at end of file diff --git a/src/graphics/vulkan.rs b/src/graphics/vulkan.rs new file mode 100644 index 0000000..2f9c3e7 --- /dev/null +++ b/src/graphics/vulkan.rs @@ -0,0 +1,441 @@ +use std::ffi::{c_void, CString}; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::AtomicBool; + +use anyhow::Context; +use ash::vk::{self, Handle}; +use bevy::math::uvec2; +use bevy::prelude::*; +use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use bevy::window::RawHandleWrapper; +use openxr as xr; +use wgpu::Instance; + +use crate::input::XrInput; +use crate::resources::{ + XrEnvironmentBlendMode, XrFrameWaiter, XrInstance, XrSession, XrSessionRunning, XrSwapchain, Swapchain, SwapchainInner, XrViews, +}; +use crate::VIEW_TYPE; + +pub fn initialize_xr_graphics(window: Option) -> anyhow::Result<( + RenderDevice, + RenderQueue, + RenderAdapterInfo, + RenderAdapter, + Instance, + XrInstance, + XrSession, + XrEnvironmentBlendMode, + XrSessionRunning, + XrFrameWaiter, + XrSwapchain, + XrInput, + XrViews, +)> { + use wgpu_hal::{api::Vulkan as V, Api}; + + let xr_entry = unsafe { xr::Entry::load() }?; + + let available_extensions = xr_entry.enumerate_extensions()?; + assert!(available_extensions.khr_vulkan_enable2); + info!("available xr exts: {:#?}", available_extensions); + + let mut enabled_extensions = xr::ExtensionSet::default(); + enabled_extensions.khr_vulkan_enable2 = true; + #[cfg(target_os = "android")] + { + enabled_extensions.khr_android_create_instance = true; + } + + let available_layers = xr_entry.enumerate_layers()?; + info!("available xr layers: {:#?}", available_layers); + + let xr_instance = xr_entry.create_instance( + &xr::ApplicationInfo { + application_name: "Ambient", + ..Default::default() + }, + &enabled_extensions, + &[], + )?; + info!("created instance"); + let instance_props = xr_instance.properties()?; + let xr_system_id = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?; + info!("created system"); + let system_props = xr_instance.system_properties(xr_system_id).unwrap(); + info!( + "loaded OpenXR runtime: {} {} {}", + instance_props.runtime_name, + instance_props.runtime_version, + if system_props.system_name.is_empty() { + "" + } else { + &system_props.system_name + } + ); + + let blend_mode = xr_instance.enumerate_environment_blend_modes(xr_system_id, VIEW_TYPE)?[0]; + + let vk_target_version = vk::make_api_version(0, 1, 1, 0); + let vk_target_version_xr = xr::Version::new(1, 1, 0); + let reqs = xr_instance.graphics_requirements::(xr_system_id)?; + if vk_target_version_xr < reqs.min_api_version_supported + || vk_target_version_xr.major() > reqs.max_api_version_supported.major() + { + panic!( + "OpenXR runtime requires Vulkan version > {}, < {}.0.0", + reqs.min_api_version_supported, + reqs.max_api_version_supported.major() + 1 + ); + } + + let vk_entry = unsafe { ash::Entry::load() }?; + let flags = wgpu_hal::InstanceFlags::empty(); + let extensions = + ::Instance::required_extensions(&vk_entry, vk_target_version, flags)?; + let device_extensions = vec![ + ash::extensions::khr::Swapchain::name(), + ash::extensions::khr::DrawIndirectCount::name(), + ]; + info!( + "creating vulkan instance with these extensions: {:#?}", + extensions + ); + + let vk_instance = unsafe { + let extensions_cchar: Vec<_> = extensions.iter().map(|s| s.as_ptr()).collect(); + + let app_name = CString::new("Ambient")?; + let vk_app_info = vk::ApplicationInfo::builder() + .application_name(&app_name) + .application_version(1) + .engine_name(&app_name) + .engine_version(1) + .api_version(vk_target_version); + + let vk_instance = xr_instance + .create_vulkan_instance( + xr_system_id, + std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), + &vk::InstanceCreateInfo::builder() + .application_info(&vk_app_info) + .enabled_extension_names(&extensions_cchar) as *const _ + as *const _, + ) + .context("XR error creating Vulkan instance") + .unwrap() + .map_err(vk::Result::from_raw) + .context("Vulkan error creating Vulkan instance") + .unwrap(); + + ash::Instance::load( + vk_entry.static_fn(), + vk::Instance::from_raw(vk_instance as _), + ) + }; + info!("created vulkan instance"); + + let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void; + + let vk_physical_device = vk::PhysicalDevice::from_raw(unsafe { + xr_instance.vulkan_graphics_device(xr_system_id, vk_instance.handle().as_raw() as _)? as _ + }); + let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void; + + let vk_device_properties = + unsafe { vk_instance.get_physical_device_properties(vk_physical_device) }; + if vk_device_properties.api_version < vk_target_version { + unsafe { vk_instance.destroy_instance(None) } + panic!("Vulkan physical device doesn't support version 1.1"); + } + + let wgpu_vk_instance = unsafe { + ::Instance::from_raw( + vk_entry.clone(), + vk_instance.clone(), + vk_target_version, + 0, + extensions, + flags, + false, + Some(Box::new(())), + )? + }; + + 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_exposed_adapter = wgpu_vk_instance + .expose_adapter(vk_physical_device) + .context("failed to expose adapter")?; + + let enabled_extensions = wgpu_exposed_adapter + .adapter + .required_device_extensions(wgpu_features); + + let (wgpu_open_device, vk_device_ptr, queue_family_index) = { + let extensions_cchar: Vec<_> = device_extensions.iter().map(|s| s.as_ptr()).collect(); + let mut enabled_phd_features = wgpu_exposed_adapter + .adapter + .physical_device_features(&enabled_extensions, wgpu_features); + let family_index = 0; + let family_info = vk::DeviceQueueCreateInfo::builder() + .queue_family_index(family_index) + .queue_priorities(&[1.0]) + .build(); + let family_infos = [family_info]; + let info = enabled_phd_features + .add_to_device_create_builder( + vk::DeviceCreateInfo::builder() + .queue_create_infos(&family_infos) + .push_next(&mut vk::PhysicalDeviceMultiviewFeatures { + multiview: vk::TRUE, + ..Default::default() + }), + ) + .enabled_extension_names(&extensions_cchar) + .build(); + let vk_device = unsafe { + let vk_device = xr_instance + .create_vulkan_device( + xr_system_id, + std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), + vk_physical_device.as_raw() as _, + &info as *const _ as *const _, + ) + .context("XR error creating Vulkan device")? + .map_err(vk::Result::from_raw) + .context("Vulkan error creating Vulkan device")?; + + ash::Device::load(vk_instance.fp_v1_0(), vk::Device::from_raw(vk_device as _)) + }; + let vk_device_ptr = vk_device.handle().as_raw() as *const c_void; + + let wgpu_open_device = unsafe { + wgpu_exposed_adapter.adapter.device_from_raw( + vk_device, + true, + &enabled_extensions, + wgpu_features, + family_info.queue_family_index, + 0, + ) + }?; + + ( + wgpu_open_device, + vk_device_ptr, + family_info.queue_family_index, + ) + }; + + let wgpu_instance = + unsafe { wgpu::Instance::from_hal::(wgpu_vk_instance) }; + let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) }; + let (wgpu_device, wgpu_queue) = unsafe { + wgpu_adapter.create_device_from_hal( + wgpu_open_device, + &wgpu::DeviceDescriptor { + label: None, + features: wgpu_features, + limits: wgpu::Limits { + max_bind_groups: 8, + max_storage_buffer_binding_size: wgpu_adapter + .limits() + .max_storage_buffer_binding_size, + max_push_constant_size: 4, + ..Default::default() + }, + }, + None, + ) + }?; + + let (session, frame_wait, frame_stream) = unsafe { + xr_instance.create_session::( + xr_system_id, + &xr::vulkan::SessionCreateInfo { + instance: vk_instance_ptr, + physical_device: vk_physical_device_ptr, + device: vk_device_ptr, + queue_family_index, + queue_index: 0, + }, + ) + }?; + + let views = xr_instance.enumerate_view_configuration_views(xr_system_id, VIEW_TYPE)?; + + let surface = window.map(|wrapper| unsafe { + // SAFETY: Plugins should be set up on the main thread. + let handle = wrapper.get_handle(); + wgpu_instance + .create_surface(&handle) + .expect("Failed to create wgpu surface") + }); + let swapchain_format = surface + .as_ref() + .map(|surface| surface.get_capabilities(&wgpu_adapter).formats[0]) + .unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb); + + let resolution = uvec2(views[0].recommended_image_rect_width, views[0].recommended_image_rect_height); + + let handle = session + .create_swapchain(&xr::SwapchainCreateInfo { + create_flags: xr::SwapchainCreateFlags::EMPTY, + usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT + | xr::SwapchainUsageFlags::SAMPLED, + format: wgpu_to_vulkan(swapchain_format).as_raw() as _, + // The Vulkan graphics pipeline we create is not set up for multisampling, + // so we hardcode this to 1. If we used a proper multisampling setup, we + // could set this to `views[0].recommended_swapchain_sample_count`. + sample_count: 1, + width: resolution.x, + height: resolution.y, + face_count: 1, + array_size: 2, + mip_count: 1, + }) + .unwrap(); + let images = handle.enumerate_images().unwrap(); + + let buffers = images + .into_iter() + .map(|color_image| { + let color_image = vk::Image::from_raw(color_image); + let wgpu_hal_texture = unsafe { + ::Device::texture_from_raw( + color_image, + &wgpu_hal::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: swapchain_format, + usage: wgpu_hal::TextureUses::COLOR_TARGET + | wgpu_hal::TextureUses::COPY_DST, + memory_flags: wgpu_hal::MemoryFlags::empty(), + view_formats: vec![], + }, + None, + ) + }; + let texture = unsafe { + wgpu_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: swapchain_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }, + ) + }; + texture + }) + .collect(); + + + Ok(( + wgpu_device.into(), + RenderQueue(Arc::new(wgpu_queue)), + RenderAdapterInfo(wgpu_adapter.get_info()), + RenderAdapter(Arc::new(wgpu_adapter)), + wgpu_instance, + xr_instance.clone().into(), + session.clone().into_any_graphics().into(), + blend_mode.into(), + AtomicBool::new(false).into(), + Mutex::new(frame_wait).into(), + Mutex::new(Swapchain::Vulkan(SwapchainInner { + stream: frame_stream, + handle, + resolution, + buffers, + })).into(), + XrInput::new(xr_instance, session.into_any_graphics())?, + Mutex::default().into(), + )) +} + +fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> vk::Format { + use vk::Format; + match format { + wgpu::TextureFormat::R8Unorm => Format::R8_UNORM, + wgpu::TextureFormat::R8Snorm => Format::R8_SNORM, + wgpu::TextureFormat::R8Uint => Format::R8_UINT, + wgpu::TextureFormat::R8Sint => Format::R8_SINT, + wgpu::TextureFormat::R16Uint => Format::R16_UINT, + wgpu::TextureFormat::R16Sint => Format::R16_SINT, + wgpu::TextureFormat::R16Unorm => Format::R16_UNORM, + wgpu::TextureFormat::R16Snorm => Format::R16_SNORM, + wgpu::TextureFormat::R16Float => Format::R16_SFLOAT, + wgpu::TextureFormat::Rg8Unorm => Format::R8G8_UNORM, + wgpu::TextureFormat::Rg8Snorm => Format::R8G8_SNORM, + wgpu::TextureFormat::Rg8Uint => Format::R8G8_UINT, + wgpu::TextureFormat::Rg8Sint => Format::R8G8_SINT, + wgpu::TextureFormat::R32Uint => Format::R32_UINT, + wgpu::TextureFormat::R32Sint => Format::R32_SINT, + wgpu::TextureFormat::R32Float => Format::R32_SFLOAT, + wgpu::TextureFormat::Rg16Uint => Format::R16G16_UINT, + wgpu::TextureFormat::Rg16Sint => Format::R16G16_SINT, + wgpu::TextureFormat::Rg16Unorm => Format::R16G16_UNORM, + wgpu::TextureFormat::Rg16Snorm => Format::R16G16_SNORM, + wgpu::TextureFormat::Rg16Float => Format::R16G16_SFLOAT, + wgpu::TextureFormat::Rgba8Unorm => Format::R8G8B8A8_UNORM, + wgpu::TextureFormat::Rgba8UnormSrgb => Format::R8G8B8A8_SRGB, + wgpu::TextureFormat::Rgba8Snorm => Format::R8G8B8A8_SNORM, + wgpu::TextureFormat::Rgba8Uint => Format::R8G8B8A8_UINT, + wgpu::TextureFormat::Rgba8Sint => Format::R8G8B8A8_SINT, + wgpu::TextureFormat::Bgra8Unorm => Format::B8G8R8A8_UNORM, + wgpu::TextureFormat::Bgra8UnormSrgb => Format::B8G8R8A8_SRGB, + wgpu::TextureFormat::Rgb9e5Ufloat => Format::E5B9G9R9_UFLOAT_PACK32, // this might be the wrong type??? i can't tell + wgpu::TextureFormat::Rgb10a2Unorm => Format::A2R10G10B10_UNORM_PACK32, + wgpu::TextureFormat::Rg11b10Float => panic!("this texture type invokes nothing but fear within my soul and i don't think vulkan has a proper type for this"), + wgpu::TextureFormat::Rg32Uint => Format::R32G32_UINT, + wgpu::TextureFormat::Rg32Sint => Format::R32G32_SINT, + wgpu::TextureFormat::Rg32Float => Format::R32G32_SFLOAT, + wgpu::TextureFormat::Rgba16Uint => Format::R16G16B16A16_UINT, + wgpu::TextureFormat::Rgba16Sint => Format::R16G16B16A16_SINT, + wgpu::TextureFormat::Rgba16Unorm => Format::R16G16B16A16_UNORM, + wgpu::TextureFormat::Rgba16Snorm => Format::R16G16B16A16_SNORM, + wgpu::TextureFormat::Rgba16Float => Format::R16G16B16A16_SFLOAT, + wgpu::TextureFormat::Rgba32Uint => Format::R32G32B32A32_UINT, + wgpu::TextureFormat::Rgba32Sint => Format::R32G32B32A32_SINT, + wgpu::TextureFormat::Rgba32Float => Format::R32G32B32A32_SFLOAT, + wgpu::TextureFormat::Stencil8 => Format::S8_UINT, + wgpu::TextureFormat::Depth16Unorm => Format::D16_UNORM, + wgpu::TextureFormat::Depth24Plus => Format::X8_D24_UNORM_PACK32, + wgpu::TextureFormat::Depth24PlusStencil8 => Format::D24_UNORM_S8_UINT, + wgpu::TextureFormat::Depth32Float => Format::D32_SFLOAT, + wgpu::TextureFormat::Depth32FloatStencil8 => Format::D32_SFLOAT_S8_UINT, + wgpu::TextureFormat::Etc2Rgb8Unorm => Format::ETC2_R8G8B8_UNORM_BLOCK, + wgpu::TextureFormat::Etc2Rgb8UnormSrgb => Format::ETC2_R8G8B8_SRGB_BLOCK, + wgpu::TextureFormat::Etc2Rgb8A1Unorm => Format::ETC2_R8G8B8A1_UNORM_BLOCK, + wgpu::TextureFormat::Etc2Rgb8A1UnormSrgb => Format::ETC2_R8G8B8A1_SRGB_BLOCK, + wgpu::TextureFormat::Etc2Rgba8Unorm => Format::ETC2_R8G8B8A8_UNORM_BLOCK, + wgpu::TextureFormat::Etc2Rgba8UnormSrgb => Format::ETC2_R8G8B8A8_SRGB_BLOCK, + wgpu::TextureFormat::EacR11Unorm => Format::EAC_R11_UNORM_BLOCK, + wgpu::TextureFormat::EacR11Snorm => Format::EAC_R11_SNORM_BLOCK, + wgpu::TextureFormat::EacRg11Unorm => Format::EAC_R11G11_UNORM_BLOCK, + wgpu::TextureFormat::EacRg11Snorm => Format::EAC_R11G11_SNORM_BLOCK, + wgpu::TextureFormat::Astc { .. } => panic!("please god kill me now"), + _ => panic!("fuck no") + } +} \ No newline at end of file diff --git a/src/input.rs b/src/input.rs index b35cb38..6ae2959 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,16 +1,18 @@ +use std::sync::Arc; + use bevy::prelude::*; use openxr as xr; type XrPose = (Vec3, Quat); -#[derive(Resource)] +#[derive(Clone, Resource)] pub struct XrInput { action_set: xr::ActionSet, right_action: xr::Action, left_action: xr::Action, - right_space: xr::Space, - left_space: xr::Space, - stage: xr::Space, + right_space: Arc, + left_space: Arc, + pub stage: Arc, } impl XrInput { @@ -47,9 +49,9 @@ impl XrInput { action_set, right_action, left_action, - right_space, - left_space, - stage, + right_space: Arc::new(right_space), + left_space: Arc::new(left_space), + stage: Arc::new(stage), }) } } diff --git a/src/lib.rs b/src/lib.rs index 41aa0c2..b88c5d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,23 @@ pub mod input; pub mod resource_macros; pub mod resources; +mod graphics; use std::sync::{Arc, Mutex}; use bevy::ecs::system::SystemState; use bevy::prelude::*; -use bevy::render::settings::WgpuSettings; -use bevy::render::{FutureRendererResources, RenderPlugin, renderer}; +use bevy::render::camera::{ManualTextureViews, ManualTextureView, ManualTextureViewHandle}; +use bevy::render::{FutureRendererResources, RenderPlugin, RenderApp, Render, RenderSet}; use bevy::window::{PrimaryWindow, RawHandleWrapper}; use input::XrInput; use resources::*; +use openxr as xr; + +const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO; + +pub const LEFT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(1208214591); +pub const RIGHT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(3383858418); /// Adds OpenXR support to an App #[derive(Default)] @@ -28,6 +35,7 @@ pub struct FutureXrResources( XrFrameWaiter, XrSwapchain, XrInput, + XrViews, )>, >, >, @@ -49,38 +57,25 @@ impl Plugin for OpenXrPlugin { SystemState::new(&mut app.world); let primary_window = system_state.get(&app.world).get_single().ok().cloned(); - let settings = WgpuSettings::default(); bevy::tasks::IoTaskPool::get() .spawn_local(async move { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: settings.backends.unwrap(), - dx12_shader_compiler: settings.dx12_shader_compiler.clone(), - }); - let surface = primary_window.map(|wrapper| unsafe { - // SAFETY: Plugins should be set up on the main thread. - let handle = wrapper.get_handle(); - instance - .create_surface(&handle) - .expect("Failed to create wgpu surface") - }); - - let request_adapter_options = wgpu::RequestAdapterOptions { - power_preference: settings.power_preference, - compatible_surface: surface.as_ref(), - ..Default::default() - }; - - let (device, queue, adapter_info, render_adapter) = - renderer::initialize_renderer(&instance, &settings, &request_adapter_options) - .await; + let (device, queue, adapter_info, render_adapter, instance, xr_instance, session, blend_mode, session_running, frame_waiter, swapchain, input, views) = graphics::initialize_xr_graphics(primary_window).unwrap(); debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); debug!("Configured wgpu adapter Features: {:#?}", device.features()); let mut future_renderer_resources_inner = future_renderer_resources_wrapper.lock().unwrap(); *future_renderer_resources_inner = Some((device, queue, adapter_info, render_adapter, instance)); + let mut future_xr_resources_inner = future_xr_resources_wrapper.lock().unwrap(); + *future_xr_resources_inner = + Some((xr_instance, session, blend_mode, session_running, frame_waiter, swapchain, input, views)); }) .detach(); + + app.add_systems(Last, pre_frame); + + let render_app = app.sub_app_mut(RenderApp); + render_app.add_systems(Render, (post_frame.in_set(RenderSet::Prepare), post_queue_submit.in_set(RenderSet::Cleanup))); } fn ready(&self, app: &App) -> bool { @@ -94,7 +89,7 @@ impl Plugin for OpenXrPlugin { if let Some(future_renderer_resources) = app.world.remove_resource::() { - let (instance, session, blend_mode, session_running, frame_waiter, swapchain, input) = + let (instance, session, blend_mode, session_running, frame_waiter, swapchain, input, views) = future_renderer_resources.0.lock().unwrap().take().unwrap(); app.insert_resource(instance.clone()) @@ -102,7 +97,20 @@ impl Plugin for OpenXrPlugin { .insert_resource(blend_mode.clone()) .insert_resource(session_running.clone()) .insert_resource(frame_waiter.clone()) - .insert_resource(swapchain.clone()); + .insert_resource(swapchain.clone()) + .insert_resource(input.clone()) + .insert_resource(views.clone()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app.insert_resource(instance) + .insert_resource(session) + .insert_resource(blend_mode) + .insert_resource(session_running) + .insert_resource(frame_waiter) + .insert_resource(swapchain) + .insert_resource(input) + .insert_resource(views); } } } @@ -116,3 +124,87 @@ impl PluginGroup for DefaultXrPlugins { .add_before::(OpenXrPlugin) } } + +pub fn pre_frame( + instance: Res, + session: Res, + session_running: Res, + frame_state: Res, + frame_waiter: Res, + swapchain: Res, + mut manual_texture_views: ResMut, +){ + while let Some(event) = instance.poll_event(&mut Default::default()).unwrap() { + use xr::Event::*; + match event { + SessionStateChanged(e) => { + // Session state change is where we can begin and end sessions, as well as + // find quit messages! + info!("entered XR state {:?}", e.state()); + match e.state() { + xr::SessionState::READY => { + session.begin(VIEW_TYPE).unwrap(); + session_running.store(true, std::sync::atomic::Ordering::Relaxed); + } + xr::SessionState::STOPPING => { + session.end().unwrap(); + session_running.store(false, std::sync::atomic::Ordering::Relaxed); + } + xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => { + return + } + _ => {} + } + } + InstanceLossPending(_) => { + return + } + EventsLost(e) => { + warn!("lost {} XR events", e.lost_event_count()); + } + _ => {} + } + } + if !session_running.load(std::sync::atomic::Ordering::Relaxed) { + // Don't grind up the CPU + std::thread::sleep(std::time::Duration::from_millis(10)); + return + } + + *frame_state.lock().unwrap() = Some(frame_waiter.lock().unwrap().wait().unwrap()); + + let mut swapchain = swapchain.lock().unwrap(); + + swapchain.begin().unwrap(); + let (left, right) = swapchain.get_render_views(); + let left = ManualTextureView::with_default_format(left.into(), swapchain.resolution()); + let right = ManualTextureView::with_default_format(right.into(), swapchain.resolution()); + manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); + manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); +} + +pub fn post_frame( + views: Res, + input: Res, + session: Res, + xr_frame_state: Res, +) { + *views.lock().unwrap() = session.locate_views( + VIEW_TYPE, + xr_frame_state.lock().unwrap().unwrap().predicted_display_time, + &input.stage, + ).unwrap().1; +} + +pub fn post_queue_submit( + xr_frame_state: Res, + views: Res, + input: Res, + swapchain: Res, + environment_blend_mode: Res, +) { + let xr_frame_state = xr_frame_state.lock().unwrap().unwrap(); + let views = &*views.lock().unwrap(); + let stage = &input.stage; + swapchain.lock().unwrap().post_queue_submit(xr_frame_state, views, stage, **environment_blend_mode).unwrap(); +} \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index 80cb575..d3a4c41 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,23 +1,127 @@ -use std::sync::Mutex; use std::sync::atomic::AtomicBool; +use std::sync::Mutex; use crate::resource_macros::*; +use bevy::prelude::*; use openxr as xr; xr_resource_wrapper!(XrInstance, xr::Instance); xr_resource_wrapper!(XrSession, xr::Session); xr_resource_wrapper!(XrEnvironmentBlendMode, xr::EnvironmentBlendMode); +xr_resource_wrapper!(XrViewConfigurationViews, Vec); xr_arc_resource_wrapper!(XrSessionRunning, AtomicBool); -xr_arc_resource_wrapper!(XrFrameWaiter, Mutex); +xr_arc_resource_wrapper!(XrFrameWaiter, Mutex); xr_arc_resource_wrapper!(XrSwapchain, Mutex); +xr_arc_resource_wrapper!(XrFrameState, Mutex>); +xr_arc_resource_wrapper!(XrViews, Mutex>); pub enum Swapchain { - Vulkan(SwapchainInner) + Vulkan(SwapchainInner), +} + +impl Swapchain { + pub(crate) fn begin(&mut self) -> xr::Result<()> { + match self { + Swapchain::Vulkan(swap) => swap.begin(), + } + } + + pub(crate) fn get_render_views(&mut self) -> (wgpu::TextureView, wgpu::TextureView) { + match self { + Swapchain::Vulkan(swap) => swap.get_render_views(), + } + } + + pub(crate) fn resolution(&self) -> UVec2 { + match self { + Swapchain::Vulkan(swap) => swap.resolution, + } + } + + pub(crate) fn post_queue_submit( + &mut self, + xr_frame_state: xr::FrameState, + views: &[openxr::View], + stage: &xr::Space, + environment_blend_mode: xr::EnvironmentBlendMode, + ) -> xr::Result<()> { + match self { + Swapchain::Vulkan(swap) => swap.post_queue_submit(xr_frame_state, views, stage, environment_blend_mode), + } + } } pub struct SwapchainInner { - stream: xr::FrameStream, - handle: xr::Swapchain, - resolution: (u32, u32), - buffers: Vec, -} \ No newline at end of file + pub(crate) stream: xr::FrameStream, + pub(crate) handle: xr::Swapchain, + pub(crate) resolution: UVec2, + pub(crate) buffers: Vec, +} + +impl SwapchainInner { + fn begin(&mut self) -> xr::Result<()> { + self.stream.begin() + } + + fn get_render_views(&mut self) -> (wgpu::TextureView, wgpu::TextureView) { + let image_index = self.handle.acquire_image().unwrap(); + self.handle.wait_image(xr::Duration::INFINITE).unwrap(); + + let texture = &self.buffers[image_index as usize]; + + ( + texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + array_layer_count: Some(1), + ..Default::default() + }), + texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + array_layer_count: Some(1), + base_array_layer: 1, + ..Default::default() + }), + ) + } + + fn post_queue_submit( + &mut self, + xr_frame_state: xr::FrameState, + views: &[openxr::View], + stage: &xr::Space, + environment_blend_mode: xr::EnvironmentBlendMode, + ) -> xr::Result<()> { + self.handle.release_image().unwrap(); + let rect = xr::Rect2Di { + offset: xr::Offset2Di { x: 0, y: 0 }, + extent: xr::Extent2Di { + width: self.resolution.x as _, + height: self.resolution.y as _, + }, + }; + self.stream.end( + xr_frame_state.predicted_display_time, + environment_blend_mode, + &[&xr::CompositionLayerProjection::new().space(stage).views(&[ + xr::CompositionLayerProjectionView::new() + .pose(views[0].pose) + .fov(views[0].fov) + .sub_image( + xr::SwapchainSubImage::new() + .swapchain(&self.handle) + .image_array_index(0) + .image_rect(rect), + ), + xr::CompositionLayerProjectionView::new() + .pose(views[1].pose) + .fov(views[1].fov) + .sub_image( + xr::SwapchainSubImage::new() + .swapchain(&self.handle) + .image_array_index(1) + .image_rect(rect), + ), + ])], + ) + } +}