ailloli_ui_openxr/openxr_runtime/session_loop.rs
1//! OpenXR/Vulkan initialization, session events, and built-in frame loops.
2
3use std::time::Duration;
4
5use ailloli_ui_render_vulkan::VulkanRenderer;
6use ailloli_ui_runtime::Scene;
7use ash::vk::Handle;
8use openxr as xr;
9
10use super::composer::{
11 OpenXrQuadComposer, OpenXrQuadFrameLoopOptions, OpenXrRenderVulkanFrameLoopOptions,
12};
13use super::error::OpenXrRuntimeError;
14use super::input::OpenXrInputCapabilities;
15use super::instance::{OpenXrInstance, OpenXrInstanceOptions};
16use super::swapchain::OpenXrQuadSwapchain;
17use super::ui_layer::OpenXrExternalVulkanContext;
18use super::vulkan::OpenXrVulkanContext;
19
20/// Owned OpenXR session and Vulkan objects for built-in frame loops.
21///
22/// Fields are declared in dependency-safe drop order: session-owned XR objects
23/// precede Vulkan, then the instance. A runtime must be driven from one thread in
24/// accordance with its OpenXR platform requirements.
25///
26/// # Examples
27///
28/// ```no_run
29/// use ailloli_ui_openxr::OpenXrRuntime;
30/// fn runtime_name(runtime: &OpenXrRuntime) -> &str { &runtime.xr.runtime_name }
31/// ```
32pub struct OpenXrRuntime {
33 // Fields drop in declaration order; keep XR session-owned objects before Vulkan.
34 /// Viewer reference space used to locate the HMD pose.
35 pub view_space: xr::Space,
36 /// Application reference space selected from runtime preference.
37 pub reference_space: xr::Space,
38 /// Frame submission stream paired with `frame_waiter`.
39 pub frame_stream: xr::FrameStream<xr::Vulkan>,
40 /// Wait handle that yields predicted frame timing.
41 pub frame_waiter: xr::FrameWaiter,
42 /// Vulkan-backed OpenXR session.
43 pub session: xr::Session<xr::Vulkan>,
44 /// Vulkan instance, device, queue, pool, and memory properties.
45 pub vk: OpenXrVulkanContext,
46 /// Loader instance, system identity, and negotiated extensions.
47 pub xr: OpenXrInstance,
48 /// Environment blend mode used by built-in frame submissions.
49 pub blend_mode: xr::EnvironmentBlendMode,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53/// Names and application reference-space preference for runtime creation.
54///
55/// Names must contain no NUL and fit OpenXR's byte-limited fields, including the
56/// trailing NUL. Defaults use `"ailloli_ui"` and local-then-stage fallback.
57///
58/// # Examples
59///
60/// ```
61/// use ailloli_ui_openxr::{OpenXrRuntimeOptions, ReferenceSpacePreference};
62/// let options = OpenXrRuntimeOptions::default();
63/// assert_eq!(options.reference_space, ReferenceSpacePreference::LocalThenStage);
64/// ```
65pub struct OpenXrRuntimeOptions {
66 /// OpenXR application name.
67 pub application_name: String,
68 /// OpenXR engine name.
69 pub engine_name: String,
70 /// Ordered choice of local and stage application space.
71 pub reference_space: ReferenceSpacePreference,
72}
73
74impl Default for OpenXrRuntimeOptions {
75 fn default() -> Self {
76 Self {
77 application_name: "ailloli_ui".to_string(),
78 engine_name: "ailloli_ui".to_string(),
79 reference_space: ReferenceSpacePreference::LocalThenStage,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85/// Application reference-space selection and fallback policy.
86///
87/// # Examples
88///
89/// ```
90/// use ailloli_ui_openxr::ReferenceSpacePreference;
91/// assert_ne!(ReferenceSpacePreference::LocalOnly, ReferenceSpacePreference::StageOnly);
92/// ```
93pub enum ReferenceSpacePreference {
94 /// Try local space first, then stage space if local creation fails.
95 LocalThenStage,
96 /// Require a local application space.
97 LocalOnly,
98 /// Require a stage/bounds-floor application space.
99 StageOnly,
100}
101
102#[derive(Debug, Clone, Copy, Default)]
103/// Internal running/focus state derived from session events.
104///
105/// # Examples
106///
107/// ```
108/// use ailloli_ui_openxr::OpenXrRuntimeOptions;
109/// let options = OpenXrRuntimeOptions::default();
110/// assert_eq!(options.application_name, "ailloli_ui");
111/// ```
112pub(crate) struct SessionLoopState {
113 /// Whether the session has begun and frames may be waited.
114 pub running: bool,
115 /// Whether the session currently accepts focused input.
116 pub focused: bool,
117}
118
119#[derive(Debug, Clone, Copy, Default)]
120/// Control signals produced while draining the OpenXR event queue.
121///
122/// # Examples
123///
124/// ```
125/// use ailloli_ui_openxr::OpenXrRuntimeOptions;
126/// assert_eq!(OpenXrRuntimeOptions::default().engine_name, "ailloli_ui");
127/// ```
128pub(crate) struct SessionEventOutcome {
129 /// Stop the host loop after exiting, loss-pending, or instance loss.
130 pub exit_requested: bool,
131 /// Clear retained input because focus/session validity changed.
132 pub reset_input: bool,
133}
134
135impl OpenXrRuntime {
136 /// Loads OpenXR/Vulkan and creates a stereo session and reference spaces.
137 ///
138 /// Vulkan 1.0 is requested through `XR_KHR_vulkan_enable2`; graphics queue
139 /// family zero within the selected family is used. No frame loop is started.
140 ///
141 /// # Errors
142 ///
143 /// Returns loader, extension, name, instance/system, Vulkan, session, or
144 /// reference-space initialization failures.
145 ///
146 /// # Examples
147 ///
148 /// ```no_run
149 /// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeOptions};
150 /// let runtime = OpenXrRuntime::new(OpenXrRuntimeOptions::default())?;
151 /// println!("{}", runtime.xr.runtime_name);
152 /// # Ok::<(), ailloli_ui_openxr::OpenXrRuntimeError>(())
153 /// ```
154 pub fn new(options: OpenXrRuntimeOptions) -> Result<Self, OpenXrRuntimeError> {
155 let xr = OpenXrInstance::new(OpenXrInstanceOptions {
156 application_name: &options.application_name,
157 engine_name: &options.engine_name,
158 })?;
159 let blend_mode = xr.blend_mode;
160 let vk = OpenXrVulkanContext::new(&xr, &options.application_name, &options.engine_name)?;
161
162 let (session, frame_waiter, frame_stream) = unsafe {
163 xr.instance.create_session::<xr::Vulkan>(
164 xr.system,
165 &xr::vulkan::SessionCreateInfo {
166 instance: vk.vk_instance.handle().as_raw() as _,
167 physical_device: vk.physical_device.as_raw() as _,
168 device: vk.vk_device.handle().as_raw() as _,
169 queue_family_index: vk.queue_family_index,
170 queue_index: 0,
171 },
172 )
173 }
174 .map_err(|result| OpenXrRuntimeError::CreateSession { result })?;
175
176 let reference_space = create_reference_space(&session, options.reference_space)?;
177 let view_space = session
178 .create_reference_space(xr::ReferenceSpaceType::VIEW, xr::Posef::IDENTITY)
179 .map_err(|result| OpenXrRuntimeError::CreateViewSpace { result })?;
180
181 Ok(Self {
182 view_space,
183 reference_space,
184 frame_stream,
185 frame_waiter,
186 session,
187 vk,
188 xr,
189 blend_mode,
190 })
191 }
192
193 /// Returns normalized hand-tracking and hand-aim capabilities.
194 ///
195 /// # Examples
196 ///
197 /// ```no_run
198 /// use ailloli_ui_openxr::{OpenXrInputCapabilities, OpenXrRuntime};
199 /// fn capabilities(runtime: &OpenXrRuntime) -> OpenXrInputCapabilities { runtime.input_capabilities() }
200 /// ```
201 pub fn input_capabilities(&self) -> OpenXrInputCapabilities {
202 OpenXrInputCapabilities::new(self.xr.hand_tracking_supported, self.xr.hand_aim_supported)
203 }
204
205 /// Borrows the runtime's Vulkan handles as an external render context.
206 ///
207 /// Physical-device memory properties are included, enabling staging uploads.
208 ///
209 /// # Examples
210 ///
211 /// ```no_run
212 /// use ailloli_ui_openxr::{OpenXrExternalVulkanContext, OpenXrRuntime};
213 /// fn context(runtime: &OpenXrRuntime) -> OpenXrExternalVulkanContext<'_> { runtime.external_vulkan_context() }
214 /// ```
215 pub fn external_vulkan_context(&self) -> OpenXrExternalVulkanContext<'_> {
216 OpenXrExternalVulkanContext::from(&self.vk)
217 }
218
219 /// Locates the HMD view space relative to the application reference space.
220 ///
221 /// Returns `Ok(None)` when position validity is absent. Orientation validity
222 /// is not separately required by this helper.
223 ///
224 /// # Errors
225 ///
226 /// Returns the native space-location failure.
227 ///
228 /// # Examples
229 ///
230 /// ```no_run
231 /// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeError};
232 /// fn pose(runtime: &OpenXrRuntime, time: openxr::Time) -> Result<Option<openxr::Posef>, OpenXrRuntimeError> { runtime.locate_view_pose(time) }
233 /// ```
234 pub fn locate_view_pose(
235 &self,
236 time: xr::Time,
237 ) -> Result<Option<xr::Posef>, OpenXrRuntimeError> {
238 let location = self
239 .view_space
240 .locate(&self.reference_space, time)
241 .map_err(|result| OpenXrRuntimeError::LocateViewSpace { result })?;
242 if !location
243 .location_flags
244 .contains(xr::SpaceLocationFlags::POSITION_VALID)
245 {
246 return Ok(None);
247 }
248 Ok(Some(location.pose))
249 }
250
251 /// Runs a session loop that submits zero composition layers.
252 ///
253 /// The shutdown callback is checked before each event/frame iteration.
254 /// Non-running sessions sleep for 16 ms. Runtime exit/loss and a true callback
255 /// return `Ok(())`.
256 ///
257 /// # Errors
258 ///
259 /// Returns event, session transition, frame wait/begin, or frame end failures.
260 ///
261 /// # Examples
262 ///
263 /// ```no_run
264 /// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeError};
265 /// fn run(runtime: &mut OpenXrRuntime) -> Result<(), OpenXrRuntimeError> { runtime.run_empty_frame_loop(|| false) }
266 /// ```
267 pub fn run_empty_frame_loop(
268 &mut self,
269 mut shutdown: impl FnMut() -> bool,
270 ) -> Result<(), OpenXrRuntimeError> {
271 let mut event_storage = xr::EventDataBuffer::new();
272 let mut session_state = SessionLoopState::default();
273
274 while !shutdown() {
275 if self
276 .poll_session_events(&mut event_storage, &mut session_state)?
277 .exit_requested
278 {
279 return Ok(());
280 }
281
282 if !session_state.running {
283 std::thread::sleep(Duration::from_millis(16));
284 continue;
285 }
286
287 let frame_state = self
288 .frame_waiter
289 .wait()
290 .map_err(|result| OpenXrRuntimeError::FrameWait { result })?;
291 self.frame_stream
292 .begin()
293 .map_err(|result| OpenXrRuntimeError::FrameBegin { result })?;
294 self.frame_stream
295 .end(frame_state.predicted_display_time, self.blend_mode, &[])
296 .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
297 }
298
299 Ok(())
300 }
301
302 /// Runs a clear-only quad-layer frame loop.
303 ///
304 /// Skipped-render frames submit no layers. Acquired images are synchronously
305 /// cleared and released before their quad is submitted.
306 ///
307 /// # Errors
308 ///
309 /// Returns session/frame, swapchain, clear, release, or submission failures.
310 ///
311 /// # Examples
312 ///
313 /// ```no_run
314 /// use ailloli_ui_openxr::{OpenXrQuadFrameLoopOptions, OpenXrRuntime, OpenXrRuntimeError};
315 /// fn run(runtime: &mut OpenXrRuntime) -> Result<(), OpenXrRuntimeError> { runtime.run_quad_frame_loop(OpenXrQuadFrameLoopOptions::default(), || false) }
316 /// ```
317 pub fn run_quad_frame_loop(
318 &mut self,
319 options: OpenXrQuadFrameLoopOptions,
320 mut shutdown: impl FnMut() -> bool,
321 ) -> Result<(), OpenXrRuntimeError> {
322 let mut swapchain =
323 OpenXrQuadSwapchain::new(&self.session, options.pixel_width, options.pixel_height)?;
324 let composer = OpenXrQuadComposer::new(options.layer);
325 let mut event_storage = xr::EventDataBuffer::new();
326 let mut session_state = SessionLoopState::default();
327
328 while !shutdown() {
329 if self
330 .poll_session_events(&mut event_storage, &mut session_state)?
331 .exit_requested
332 {
333 return Ok(());
334 }
335
336 if !session_state.running {
337 std::thread::sleep(Duration::from_millis(16));
338 continue;
339 }
340
341 let frame_state = self
342 .frame_waiter
343 .wait()
344 .map_err(|result| OpenXrRuntimeError::FrameWait { result })?;
345 self.frame_stream
346 .begin()
347 .map_err(|result| OpenXrRuntimeError::FrameBegin { result })?;
348
349 if !frame_state.should_render {
350 self.frame_stream
351 .end(frame_state.predicted_display_time, self.blend_mode, &[])
352 .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
353 continue;
354 }
355
356 let acquired = match swapchain.acquire_wait() {
357 Ok(acquired) => acquired,
358 Err(error) => {
359 let _ = self.frame_stream.end(
360 frame_state.predicted_display_time,
361 self.blend_mode,
362 &[],
363 );
364 return Err(error);
365 }
366 };
367
368 let clear_result =
369 swapchain.clear_acquired_image(&self.vk, &acquired, options.clear_color);
370 let release_result = swapchain.release();
371 if let Err(error) = clear_result.and(release_result) {
372 let _ =
373 self.frame_stream
374 .end(frame_state.predicted_display_time, self.blend_mode, &[]);
375 return Err(error);
376 }
377
378 let layer = composer.build_layer(&self.reference_space, &swapchain);
379 let layer_ref: &xr::CompositionLayerBase<'_, xr::Vulkan> = &layer;
380 self.frame_stream
381 .end(
382 frame_state.predicted_display_time,
383 self.blend_mode,
384 &[layer_ref],
385 )
386 .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
387 }
388
389 Ok(())
390 }
391
392 /// Runs a quad loop that asks `scene_provider` for Ailloli Vulkan content.
393 ///
394 /// The scene callback runs only on frames the runtime says should render.
395 /// Image release is attempted after every render attempt, and render errors
396 /// take precedence if release also fails.
397 ///
398 /// # Errors
399 ///
400 /// Returns session/frame, renderer, swapchain, render, release, or submission
401 /// failures.
402 ///
403 /// # Examples
404 ///
405 /// ```no_run
406 /// use ailloli_ui_openxr::{OpenXrRenderVulkanFrameLoopOptions, OpenXrRuntime, OpenXrRuntimeError};
407 /// use ailloli_ui_runtime::Scene;
408 /// fn run(runtime: &mut OpenXrRuntime) -> Result<(), OpenXrRuntimeError> { runtime.run_ailloli_ui_render_vulkan_frame_loop(OpenXrRenderVulkanFrameLoopOptions::default(), Scene::default, || false) }
409 /// ```
410 pub fn run_ailloli_ui_render_vulkan_frame_loop(
411 &mut self,
412 options: OpenXrRenderVulkanFrameLoopOptions,
413 mut scene_provider: impl FnMut() -> Scene,
414 mut shutdown: impl FnMut() -> bool,
415 ) -> Result<(), OpenXrRuntimeError> {
416 let mut swapchain = OpenXrQuadSwapchain::new_with_vulkan_context(
417 &self.session,
418 &self.vk,
419 options.pixel_width,
420 options.pixel_height,
421 )?;
422 let composer = OpenXrQuadComposer::new(options.layer);
423 let mut renderer = {
424 let context = self.vk.render_context();
425 VulkanRenderer::new(&context, options.renderer)
426 .map_err(|source| OpenXrRuntimeError::RenderVulkan { source })?
427 };
428 let mut event_storage = xr::EventDataBuffer::new();
429 let mut session_state = SessionLoopState::default();
430
431 while !shutdown() {
432 if self
433 .poll_session_events(&mut event_storage, &mut session_state)?
434 .exit_requested
435 {
436 return Ok(());
437 }
438
439 if !session_state.running {
440 std::thread::sleep(Duration::from_millis(16));
441 continue;
442 }
443
444 let frame_state = self
445 .frame_waiter
446 .wait()
447 .map_err(|result| OpenXrRuntimeError::FrameWait { result })?;
448 self.frame_stream
449 .begin()
450 .map_err(|result| OpenXrRuntimeError::FrameBegin { result })?;
451
452 if !frame_state.should_render {
453 self.frame_stream
454 .end(frame_state.predicted_display_time, self.blend_mode, &[])
455 .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
456 continue;
457 }
458
459 let acquired = match swapchain.acquire_wait() {
460 Ok(acquired) => acquired,
461 Err(error) => {
462 let _ = self.frame_stream.end(
463 frame_state.predicted_display_time,
464 self.blend_mode,
465 &[],
466 );
467 return Err(error);
468 }
469 };
470
471 let render_result = {
472 let target = swapchain.frame_target(&acquired);
473 let context = self.vk.render_context();
474 let scene = scene_provider();
475 renderer
476 .render_scene(&context, options.clear, &scene, options.scale, &target)
477 .map_err(|source| OpenXrRuntimeError::RenderVulkan { source })
478 };
479 let release_result = swapchain.release();
480 if let Err(error) = combine_render_release(render_result, release_result) {
481 let _ =
482 self.frame_stream
483 .end(frame_state.predicted_display_time, self.blend_mode, &[]);
484 return Err(error);
485 }
486
487 let layer = composer.build_layer(&self.reference_space, &swapchain);
488 let layer_ref: &xr::CompositionLayerBase<'_, xr::Vulkan> = &layer;
489 self.frame_stream
490 .end(
491 frame_state.predicted_display_time,
492 self.blend_mode,
493 &[layer_ref],
494 )
495 .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
496 }
497
498 Ok(())
499 }
500
501 /// Drains session events and updates running/focus state.
502 ///
503 /// READY begins the stereo session once, STOPPING ends it once, and exit/loss
504 /// signals stop immediately. Any loss of focus requests input reset.
505 ///
506 /// # Errors
507 ///
508 /// Returns [`OpenXrRuntimeError::PollEvent`] when event polling fails,
509 /// [`OpenXrRuntimeError::BeginSession`] when entering READY cannot start the
510 /// session, or [`OpenXrRuntimeError::EndSession`] when STOPPING cannot end it.
511 ///
512 /// # Examples
513 ///
514 /// ```no_run
515 /// use ailloli_ui_openxr::OpenXrRuntime;
516 /// fn drive(runtime: &mut OpenXrRuntime) { let _ = runtime; }
517 /// ```
518 pub(crate) fn poll_session_events(
519 &mut self,
520 event_storage: &mut xr::EventDataBuffer,
521 session_state: &mut SessionLoopState,
522 ) -> Result<SessionEventOutcome, OpenXrRuntimeError> {
523 let mut outcome = SessionEventOutcome::default();
524 while let Some(event) = self
525 .xr
526 .instance
527 .poll_event(event_storage)
528 .map_err(|result| OpenXrRuntimeError::PollEvent { result })?
529 {
530 use xr::Event::*;
531
532 match event {
533 SessionStateChanged(event) => match event.state() {
534 xr::SessionState::READY => {
535 if session_state.focused {
536 outcome.reset_input = true;
537 }
538 session_state.focused = false;
539 if !session_state.running {
540 self.session
541 .begin(xr::ViewConfigurationType::PRIMARY_STEREO)
542 .map_err(|result| OpenXrRuntimeError::BeginSession { result })?;
543 session_state.running = true;
544 }
545 }
546 xr::SessionState::FOCUSED => {
547 session_state.focused = true;
548 }
549 xr::SessionState::STOPPING => {
550 if session_state.running {
551 self.session
552 .end()
553 .map_err(|result| OpenXrRuntimeError::EndSession { result })?;
554 session_state.running = false;
555 }
556 if session_state.focused {
557 outcome.reset_input = true;
558 }
559 session_state.focused = false;
560 }
561 xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => {
562 outcome.exit_requested = true;
563 outcome.reset_input = true;
564 return Ok(outcome);
565 }
566 _ => {
567 if session_state.focused {
568 outcome.reset_input = true;
569 }
570 session_state.focused = false;
571 }
572 },
573 InstanceLossPending(_) => {
574 outcome.exit_requested = true;
575 outcome.reset_input = true;
576 return Ok(outcome);
577 }
578 EventsLost(_) => {}
579 _ => {}
580 }
581 }
582
583 Ok(outcome)
584 }
585}
586
587/// Combines render and release results while preserving render-error priority.
588///
589/// # Errors
590///
591/// Returns the render error when rendering failed, even if release also failed;
592/// otherwise returns the release error. It succeeds only when both inputs do.
593///
594/// # Examples
595///
596/// ```
597/// use ailloli_ui_openxr::OpenXrRuntimeError;
598/// let render: Result<(), OpenXrRuntimeError> = Ok(());
599/// let release: Result<(), OpenXrRuntimeError> = Ok(());
600/// assert!(render.and(release).is_ok());
601/// ```
602pub(crate) fn combine_render_release(
603 render_result: Result<(), OpenXrRuntimeError>,
604 release_result: Result<(), OpenXrRuntimeError>,
605) -> Result<(), OpenXrRuntimeError> {
606 match (render_result, release_result) {
607 (Err(render_error), _) => Err(render_error),
608 (Ok(()), Err(release_error)) => Err(release_error),
609 (Ok(()), Ok(())) => Ok(()),
610 }
611}
612
613/// Creates local/stage space according to the requested fallback policy.
614///
615/// # Errors
616///
617/// Returns [`OpenXrRuntimeError::CreateReferenceSpace`] with the attempted local
618/// and/or stage runtime results when the requested policy cannot create a space.
619fn create_reference_space(
620 session: &xr::Session<xr::Vulkan>,
621 preference: ReferenceSpacePreference,
622) -> Result<xr::Space, OpenXrRuntimeError> {
623 match preference {
624 ReferenceSpacePreference::LocalThenStage => {
625 let local_error = match session
626 .create_reference_space(xr::ReferenceSpaceType::LOCAL, xr::Posef::IDENTITY)
627 {
628 Ok(space) => return Ok(space),
629 Err(result) => result,
630 };
631 match session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)
632 {
633 Ok(space) => Ok(space),
634 Err(stage_error) => Err(OpenXrRuntimeError::CreateReferenceSpace {
635 preference: "LocalThenStage",
636 local_error: Some(local_error),
637 stage_error: Some(stage_error),
638 }),
639 }
640 }
641 ReferenceSpacePreference::LocalOnly => session
642 .create_reference_space(xr::ReferenceSpaceType::LOCAL, xr::Posef::IDENTITY)
643 .map_err(|result| OpenXrRuntimeError::CreateReferenceSpace {
644 preference: "LocalOnly",
645 local_error: Some(result),
646 stage_error: None,
647 }),
648 ReferenceSpacePreference::StageOnly => session
649 .create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)
650 .map_err(|result| OpenXrRuntimeError::CreateReferenceSpace {
651 preference: "StageOnly",
652 local_error: None,
653 stage_error: Some(result),
654 }),
655 }
656}