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}