//! Illustrates rendering using Vulkan with multiview. Supports any Vulkan 1.1 capable environment. //! //! Renders a smooth gradient across the entire view, with different colors per eye. //! //! This example uses minimal abstraction for clarity. Real-world code should encapsulate and //! largely decouple its Vulkan and OpenXR components and handle errors gracefully. use std::{ io::Cursor, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, time::Duration, }; use ash::{ util::read_spv, vk::{self, Handle}, }; use openxr as xr; #[allow(clippy::field_reassign_with_default)] // False positive, might be fixed 1.51 #[cfg_attr(target_os = "android", ndk_glue::main)] pub fn main() { // Handle interrupts gracefully let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); ctrlc::set_handler(move || { r.store(false, Ordering::Relaxed); }) .expect("setting Ctrl-C handler"); #[cfg(feature = "static")] let entry = xr::Entry::linked(); #[cfg(not(feature = "static"))] let entry = unsafe { xr::Entry::load() .expect("couldn't find the OpenXR loader; try enabling the \"static\" feature") }; #[cfg(target_os = "android")] entry.initialize_android_loader().unwrap(); // OpenXR will fail to initialize if we ask for an extension that OpenXR can't provide! So we // need to check all our extensions before initializing OpenXR with them. Note that even if the // extension is present, it's still possible you may not be able to use it. For example: the // hand tracking extension may be present, but the hand sensor might not be plugged in or turned // on. There are often additional checks that should be made before using certain features! let available_extensions = entry.enumerate_extensions().unwrap(); // If a required extension isn't present, you want to ditch out here! It's possible something // like your rendering API might not be provided by the active runtime. APIs like OpenGL don't // have universal support. assert!(available_extensions.khr_vulkan_enable2); // Initialize OpenXR with the extensions we've found! 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 xr_instance = entry .create_instance( &xr::ApplicationInfo { application_name: "openxrs example", application_version: 0, engine_name: "openxrs example", engine_version: 0, api_version: xr::Version::new(1, 0, 0), }, &enabled_extensions, &[], ) .unwrap(); let instance_props = xr_instance.properties().unwrap(); println!( "loaded OpenXR runtime: {} {}", instance_props.runtime_name, instance_props.runtime_version ); // Request a form factor from the device (HMD, Handheld, etc.) let system = xr_instance .system(xr::FormFactor::HEAD_MOUNTED_DISPLAY) .unwrap(); // Check what blend mode is valid for this device (opaque vs transparent displays). We'll just // take the first one available! let environment_blend_mode = xr_instance .enumerate_environment_blend_modes(system, VIEW_TYPE) .unwrap()[0]; // OpenXR wants to ensure apps are using the correct graphics card and Vulkan features and // extensions, so the instance and device MUST be set up before Instance::create_session. let vk_target_version = vk::make_api_version(0, 1, 1, 0); // Vulkan 1.1 guarantees multiview support let vk_target_version_xr = xr::Version::new(1, 1, 0); let reqs = xr_instance .graphics_requirements::(system) .unwrap(); 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 ); } unsafe { let vk_entry = ash::Entry::load().unwrap(); let vk_app_info = vk::ApplicationInfo::default() .application_version(0) .engine_version(0) .api_version(vk_target_version); let vk_instance = { let vk_instance = xr_instance .create_vulkan_instance( system, std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), &vk::InstanceCreateInfo::default().application_info(&vk_app_info) as *const _ as *const _, ) .expect("XR error creating Vulkan instance") .map_err(vk::Result::from_raw) .expect("Vulkan error creating Vulkan instance"); ash::Instance::load( vk_entry.static_fn(), vk::Instance::from_raw(vk_instance as _), ) }; let vk_physical_device = vk::PhysicalDevice::from_raw( xr_instance .vulkan_graphics_device(system, vk_instance.handle().as_raw() as _) .unwrap() as _, ); let vk_device_properties = vk_instance.get_physical_device_properties(vk_physical_device); if vk_device_properties.api_version < vk_target_version { vk_instance.destroy_instance(None); panic!("Vulkan phyiscal device doesn't support version 1.1"); } let queue_family_index = vk_instance .get_physical_device_queue_family_properties(vk_physical_device) .into_iter() .enumerate() .find_map(|(queue_family_index, info)| { if info.queue_flags.contains(vk::QueueFlags::GRAPHICS) { Some(queue_family_index as u32) } else { None } }) .expect("Vulkan device has no graphics queue"); let vk_device = { let vk_device = xr_instance .create_vulkan_device( system, std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), vk_physical_device.as_raw() as _, &vk::DeviceCreateInfo::default() .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() .queue_family_index(queue_family_index) .queue_priorities(&[1.0])]) .push_next(&mut vk::PhysicalDeviceMultiviewFeatures { multiview: vk::TRUE, ..Default::default() }) as *const _ as *const _, ) .expect("XR error creating Vulkan device") .map_err(vk::Result::from_raw) .expect("Vulkan error creating Vulkan device"); ash::Device::load(vk_instance.fp_v1_0(), vk::Device::from_raw(vk_device as _)) }; let queue = vk_device.get_device_queue(queue_family_index, 0); let view_mask = !(!0 << VIEW_COUNT); let render_pass = vk_device .create_render_pass( &vk::RenderPassCreateInfo::default() .attachments(&[vk::AttachmentDescription { format: COLOR_FORMAT, samples: vk::SampleCountFlags::TYPE_1, load_op: vk::AttachmentLoadOp::CLEAR, store_op: vk::AttachmentStoreOp::STORE, initial_layout: vk::ImageLayout::UNDEFINED, final_layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, ..Default::default() }]) .subpasses(&[vk::SubpassDescription::default() .color_attachments(&[vk::AttachmentReference { attachment: 0, layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, }]) .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)]) .dependencies(&[vk::SubpassDependency { src_subpass: vk::SUBPASS_EXTERNAL, dst_subpass: 0, src_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, dst_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, dst_access_mask: vk::AccessFlags::COLOR_ATTACHMENT_WRITE, ..Default::default() }]) .push_next( &mut vk::RenderPassMultiviewCreateInfo::default() .view_masks(&[view_mask]) .correlation_masks(&[view_mask]), ), None, ) .unwrap(); let vert = read_spv(&mut Cursor::new(&include_bytes!("fullscreen.vert.spv")[..])).unwrap(); let frag = read_spv(&mut Cursor::new( &include_bytes!("debug_pattern.frag.spv")[..], )) .unwrap(); let vert = vk_device .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&vert), None) .unwrap(); let frag = vk_device .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&frag), None) .unwrap(); let pipeline_layout = vk_device .create_pipeline_layout( &vk::PipelineLayoutCreateInfo::default().set_layouts(&[]), None, ) .unwrap(); let noop_stencil_state = vk::StencilOpState { fail_op: vk::StencilOp::KEEP, pass_op: vk::StencilOp::KEEP, depth_fail_op: vk::StencilOp::KEEP, compare_op: vk::CompareOp::ALWAYS, compare_mask: 0, write_mask: 0, reference: 0, }; let pipeline = vk_device .create_graphics_pipelines( vk::PipelineCache::null(), &[vk::GraphicsPipelineCreateInfo::default() .stages(&[ vk::PipelineShaderStageCreateInfo { stage: vk::ShaderStageFlags::VERTEX, module: vert, p_name: b"main\0".as_ptr() as _, ..Default::default() }, vk::PipelineShaderStageCreateInfo { stage: vk::ShaderStageFlags::FRAGMENT, module: frag, p_name: b"main\0".as_ptr() as _, ..Default::default() }, ]) .vertex_input_state(&vk::PipelineVertexInputStateCreateInfo::default()) .input_assembly_state( &vk::PipelineInputAssemblyStateCreateInfo::default() .topology(vk::PrimitiveTopology::TRIANGLE_LIST), ) .viewport_state( &vk::PipelineViewportStateCreateInfo::default() .scissor_count(1) .viewport_count(1), ) .rasterization_state( &vk::PipelineRasterizationStateCreateInfo::default() .cull_mode(vk::CullModeFlags::NONE) .polygon_mode(vk::PolygonMode::FILL) .line_width(1.0), ) .multisample_state( &vk::PipelineMultisampleStateCreateInfo::default() .rasterization_samples(vk::SampleCountFlags::TYPE_1), ) .depth_stencil_state( &vk::PipelineDepthStencilStateCreateInfo::default() .depth_test_enable(false) .depth_write_enable(false) .front(noop_stencil_state) .back(noop_stencil_state), ) .color_blend_state( &vk::PipelineColorBlendStateCreateInfo::default().attachments(&[ vk::PipelineColorBlendAttachmentState { blend_enable: vk::TRUE, src_color_blend_factor: vk::BlendFactor::ONE, dst_color_blend_factor: vk::BlendFactor::ZERO, color_blend_op: vk::BlendOp::ADD, color_write_mask: vk::ColorComponentFlags::R | vk::ColorComponentFlags::G | vk::ColorComponentFlags::B, ..Default::default() }, ]), ) .dynamic_state( &vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&[ vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR, ]), ) .layout(pipeline_layout) .render_pass(render_pass) .subpass(0)], None, ) .unwrap()[0]; vk_device.destroy_shader_module(vert, None); vk_device.destroy_shader_module(frag, None); // A session represents this application's desire to display things! This is where we hook // up our graphics API. This does not start the session; for that, you'll need a call to // Session::begin, which we do in 'main_loop below. let (session, mut frame_wait, mut frame_stream) = xr_instance .create_session::( system, &xr::vulkan::SessionCreateInfo { instance: vk_instance.handle().as_raw() as _, physical_device: vk_physical_device.as_raw() as _, device: vk_device.handle().as_raw() as _, queue_family_index, queue_index: 0, }, ) .unwrap(); // Create an action set to encapsulate our actions let action_set = xr_instance .create_action_set("input", "input pose information", 0) .unwrap(); let right_action = action_set .create_action::("right_hand", "Right Hand Controller", &[]) .unwrap(); let left_action = action_set .create_action::("left_hand", "Left Hand Controller", &[]) .unwrap(); // Bind our actions to input devices using the given profile // If you want to access inputs specific to a particular device you may specify a different // interaction profile xr_instance .suggest_interaction_profile_bindings( xr_instance .string_to_path("/interaction_profiles/khr/simple_controller") .unwrap(), &[ xr::Binding::new( &right_action, xr_instance .string_to_path("/user/hand/right/input/grip/pose") .unwrap(), ), xr::Binding::new( &left_action, xr_instance .string_to_path("/user/hand/left/input/grip/pose") .unwrap(), ), ], ) .unwrap(); // Attach the action set to the session session.attach_action_sets(&[&action_set]).unwrap(); // Create an action space for each device we want to locate let right_space = right_action .create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY) .unwrap(); let left_space = left_action .create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY) .unwrap(); // OpenXR uses a couple different types of reference frames for positioning content; we need // to choose one for displaying our content! STAGE would be relative to the center of your // guardian system's bounds, and LOCAL would be relative to your device's starting location. let stage = session .create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY) .unwrap(); let cmd_pool = vk_device .create_command_pool( &vk::CommandPoolCreateInfo::default() .queue_family_index(queue_family_index) .flags( vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER | vk::CommandPoolCreateFlags::TRANSIENT, ), None, ) .unwrap(); let cmds = vk_device .allocate_command_buffers( &vk::CommandBufferAllocateInfo::default() .command_pool(cmd_pool) .command_buffer_count(PIPELINE_DEPTH), ) .unwrap(); let fences = (0..PIPELINE_DEPTH) .map(|_| { vk_device .create_fence( &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED), None, ) .unwrap() }) .collect::>(); // Main loop let mut swapchain = None; let mut event_storage = xr::EventDataBuffer::new(); let mut session_running = false; // Index of the current frame, wrapped by PIPELINE_DEPTH. Not to be confused with the // swapchain image index. let mut frame = 0; 'main_loop: loop { if !running.load(Ordering::Relaxed) { println!("requesting exit"); // The OpenXR runtime may want to perform a smooth transition between scenes, so we // can't necessarily exit instantly. Instead, we must notify the runtime of our // intent and wait for it to tell us when we're actually done. match session.request_exit() { Ok(()) => {} Err(xr::sys::Result::ERROR_SESSION_NOT_RUNNING) => break, Err(e) => panic!("{}", e), } } while let Some(event) = xr_instance.poll_event(&mut event_storage).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! println!("entered state {:?}", e.state()); match e.state() { xr::SessionState::READY => { session.begin(VIEW_TYPE).unwrap(); session_running = true; } xr::SessionState::STOPPING => { session.end().unwrap(); session_running = false; } xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => { break 'main_loop; } _ => {} } } InstanceLossPending(_) => { break 'main_loop; } EventsLost(e) => { println!("lost {} events", e.lost_event_count()); } _ => {} } } if !session_running { // Don't grind up the CPU std::thread::sleep(Duration::from_millis(100)); continue; } // Block until the previous frame is finished displaying, and is ready for another one. // Also returns a prediction of when the next frame will be displayed, for use with // predicting locations of controllers, viewpoints, etc. let xr_frame_state = frame_wait.wait().unwrap(); // Must be called before any rendering is done! frame_stream.begin().unwrap(); if !xr_frame_state.should_render { frame_stream .end( xr_frame_state.predicted_display_time, environment_blend_mode, &[], ) .unwrap(); continue; } let swapchain = swapchain.get_or_insert_with(|| { // Now we need to find all the viewpoints we need to take care of! This is a // property of the view configuration type; in this example we use PRIMARY_STEREO, // so we should have 2 viewpoints. // // Because we are using multiview in this example, we require that all view // dimensions are identical. let views = xr_instance .enumerate_view_configuration_views(system, VIEW_TYPE) .unwrap(); assert_eq!(views.len(), VIEW_COUNT as usize); assert_eq!(views[0], views[1]); // Create a swapchain for the viewpoints! A swapchain is a set of texture buffers // used for displaying to screen, typically this is a backbuffer and a front buffer, // one for rendering data to, and one for displaying on-screen. let resolution = vk::Extent2D { width: views[0].recommended_image_rect_width, height: 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: COLOR_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.width, height: resolution.height, face_count: 1, array_size: VIEW_COUNT, mip_count: 1, }) .unwrap(); // We'll want to track our own information about the swapchain, so we can draw stuff // onto it! We'll also create a buffer for each generated texture here as well. let images = handle.enumerate_images().unwrap(); Swapchain { handle, resolution, buffers: images .into_iter() .map(|color_image| { let color_image = vk::Image::from_raw(color_image); let color = vk_device .create_image_view( &vk::ImageViewCreateInfo::default() .image(color_image) .view_type(vk::ImageViewType::TYPE_2D_ARRAY) .format(COLOR_FORMAT) .subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: VIEW_COUNT, }), None, ) .unwrap(); let framebuffer = vk_device .create_framebuffer( &vk::FramebufferCreateInfo::default() .render_pass(render_pass) .width(resolution.width) .height(resolution.height) .attachments(&[color]) .layers(1), // Multiview handles addressing multiple layers None, ) .unwrap(); Framebuffer { framebuffer, color } }) .collect(), } }); // We need to ask which swapchain image to use for rendering! Which one will we get? // Who knows! It's up to the runtime to decide. let image_index = swapchain.handle.acquire_image().unwrap(); // Ensure the last use of this frame's resources is 100% done vk_device .wait_for_fences(&[fences[frame]], true, u64::MAX) .unwrap(); vk_device.reset_fences(&[fences[frame]]).unwrap(); let cmd = cmds[frame]; vk_device .begin_command_buffer( cmd, &vk::CommandBufferBeginInfo::default() .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), ) .unwrap(); vk_device.cmd_begin_render_pass( cmd, &vk::RenderPassBeginInfo::default() .render_pass(render_pass) .framebuffer(swapchain.buffers[image_index as usize].framebuffer) .render_area(vk::Rect2D { offset: vk::Offset2D::default(), extent: swapchain.resolution, }) .clear_values(&[vk::ClearValue { color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 1.0], }, }]), vk::SubpassContents::INLINE, ); let viewports = [vk::Viewport { x: 0.0, y: 0.0, width: swapchain.resolution.width as f32, height: swapchain.resolution.height as f32, min_depth: 0.0, max_depth: 1.0, }]; let scissors = [vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent: swapchain.resolution, }]; vk_device.cmd_set_viewport(cmd, 0, &viewports); vk_device.cmd_set_scissor(cmd, 0, &scissors); // Draw the scene. Multiview means we only need to do this once, and the GPU will // automatically broadcast operations to all views. Shaders can use `gl_ViewIndex` to // e.g. select the correct view matrix. vk_device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, pipeline); vk_device.cmd_draw(cmd, 3, 1, 0, 0); vk_device.cmd_end_render_pass(cmd); vk_device.end_command_buffer(cmd).unwrap(); session.sync_actions(&[(&action_set).into()]).unwrap(); // Find where our controllers are located in the Stage space let right_location = right_space .locate(&stage, xr_frame_state.predicted_display_time) .unwrap(); let left_location = left_space .locate(&stage, xr_frame_state.predicted_display_time) .unwrap(); let mut printed = false; if left_action.is_active(&session, xr::Path::NULL).unwrap() { print!( "Left Hand: ({:0<12},{:0<12},{:0<12}), ", left_location.pose.position.x, left_location.pose.position.y, left_location.pose.position.z ); printed = true; } if right_action.is_active(&session, xr::Path::NULL).unwrap() { print!( "Right Hand: ({:0<12},{:0<12},{:0<12})", right_location.pose.position.x, right_location.pose.position.y, right_location.pose.position.z ); printed = true; } if printed { println!(); } // Fetch the view transforms. To minimize latency, we intentionally do this *after* // recording commands to render the scene, i.e. at the last possible moment before // rendering begins in earnest on the GPU. Uniforms dependent on this data can be sent // to the GPU just-in-time by writing them to per-frame host-visible memory which the // GPU will only read once the command buffer is submitted. let (_, views) = session .locate_views(VIEW_TYPE, xr_frame_state.predicted_display_time, &stage) .unwrap(); // Wait until the image is available to render to before beginning work on the GPU. The // compositor could still be reading from it. swapchain.handle.wait_image(xr::Duration::INFINITE).unwrap(); // Submit commands to the GPU, then tell OpenXR we're done with our part. vk_device .queue_submit( queue, &[vk::SubmitInfo::default().command_buffers(&[cmd])], fences[frame], ) .unwrap(); swapchain.handle.release_image().unwrap(); // Tell OpenXR what to present for this frame let rect = xr::Rect2Di { offset: xr::Offset2Di { x: 0, y: 0 }, extent: xr::Extent2Di { width: swapchain.resolution.width as _, height: swapchain.resolution.height as _, }, }; frame_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(&swapchain.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(&swapchain.handle) .image_array_index(1) .image_rect(rect), ), ]), ], ) .unwrap(); frame = (frame + 1) % PIPELINE_DEPTH as usize; } // OpenXR MUST be allowed to clean up before we destroy Vulkan resources it could touch, so // first we must drop all its handles. drop(( session, frame_wait, frame_stream, stage, action_set, left_space, right_space, left_action, right_action, )); // Ensure all in-flight frames are finished before destroying resources they might use vk_device.wait_for_fences(&fences, true, !0).unwrap(); for fence in fences { vk_device.destroy_fence(fence, None); } if let Some(swapchain) = swapchain { for buffer in swapchain.buffers { vk_device.destroy_framebuffer(buffer.framebuffer, None); vk_device.destroy_image_view(buffer.color, None); } } vk_device.destroy_pipeline(pipeline, None); vk_device.destroy_pipeline_layout(pipeline_layout, None); vk_device.destroy_command_pool(cmd_pool, None); vk_device.destroy_render_pass(render_pass, None); vk_device.destroy_device(None); vk_instance.destroy_instance(None); } println!("exiting cleanly"); } pub const COLOR_FORMAT: vk::Format = vk::Format::R8G8B8A8_SRGB; pub const VIEW_COUNT: u32 = 2; const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO; struct Swapchain { handle: xr::Swapchain, buffers: Vec, resolution: vk::Extent2D, } struct Framebuffer { framebuffer: vk::Framebuffer, color: vk::ImageView, } /// Maximum number of frames in flight const PIPELINE_DEPTH: u32 = 2;