ailloli_ui_openxr/openxr_runtime/
ui_loop.rs

1//! Built-in OpenXR loop that owns Ailloli layout, input routing, paint, and render.
2
3use std::time::Duration;
4
5use ailloli_ui_core::Size;
6use ailloli_ui_runtime::app::RuntimeHandle;
7use ailloli_ui_runtime::component::IntoView;
8use openxr as xr;
9
10use super::composer::{OpenXrQuadComposer, OpenXrUiFrameLoopOptions};
11use super::error::OpenXrRuntimeError;
12use super::input::OpenXrActionInput;
13use super::session_loop::{combine_render_release, OpenXrRuntime, SessionLoopState};
14use super::swapchain::OpenXrQuadSwapchain;
15use super::ui_layer::{OpenXrExternalHostFrame, OpenXrExternalVulkanContext, OpenXrUiLayer};
16
17impl OpenXrRuntime {
18    /// Runs the full Ailloli UI lifecycle in a quad composition layer.
19    ///
20    /// Session focus controls input polling; focus/session transitions clear
21    /// retained input. The shutdown callback is checked each iteration. Frames
22    /// marked non-renderable submit no layer. Render release is always attempted,
23    /// with render failure taking precedence over simultaneous release failure.
24    ///
25    /// # Errors
26    ///
27    /// Returns UI initialization, actions, session/frame, swapchain, rendering,
28    /// release, or composition submission failures.
29    ///
30    /// # Examples
31    ///
32    /// ```no_run
33    /// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeError, OpenXrUiFrameLoopOptions};
34    /// use ailloli_ui_runtime::{app::RuntimeHandle, component::IntoView};
35    /// fn run<A: 'static>(runtime: &mut OpenXrRuntime, handle: RuntimeHandle<A>, root: impl IntoView<A>) -> Result<(), OpenXrRuntimeError> {
36    ///     runtime.run_ailloli_ui_frame_loop(OpenXrUiFrameLoopOptions::default(), handle, root, || false)
37    /// }
38    /// ```
39    pub fn run_ailloli_ui_frame_loop<A: 'static>(
40        &mut self,
41        options: OpenXrUiFrameLoopOptions,
42        runtime_handle: RuntimeHandle<A>,
43        root: impl IntoView<A>,
44        mut shutdown: impl FnMut() -> bool,
45    ) -> Result<(), OpenXrRuntimeError> {
46        let mut swapchain = OpenXrQuadSwapchain::new_with_vulkan_context(
47            &self.session,
48            &self.vk,
49            options.pixel_width,
50            options.pixel_height,
51        )?;
52        let composer = OpenXrQuadComposer::new(options.layer);
53        let mut ui_layer = OpenXrUiLayer::new(runtime_handle, root, options.into())?;
54        let mut input = OpenXrActionInput::new_for_runtime(&self.xr, options.input)?;
55        if let Some(input) = input.as_mut() {
56            input.attach_session(&self.session)?;
57        }
58        let mut event_storage = xr::EventDataBuffer::new();
59        let mut session_state = SessionLoopState::default();
60
61        while !shutdown() {
62            let session_outcome =
63                self.poll_session_events(&mut event_storage, &mut session_state)?;
64            if session_outcome.reset_input {
65                ui_layer.clear_input();
66                if let Some(input) = input.as_mut() {
67                    input.clear();
68                }
69            }
70            if session_outcome.exit_requested {
71                return Ok(());
72            }
73
74            if !session_state.running {
75                std::thread::sleep(Duration::from_millis(16));
76                continue;
77            }
78
79            let frame_state = self
80                .frame_waiter
81                .wait()
82                .map_err(|result| OpenXrRuntimeError::FrameWait { result })?;
83            self.frame_stream
84                .begin()
85                .map_err(|result| OpenXrRuntimeError::FrameBegin { result })?;
86
87            if !frame_state.should_render {
88                self.frame_stream
89                    .end(frame_state.predicted_display_time, self.blend_mode, &[])
90                    .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
91                continue;
92            }
93
94            let (logical_width, logical_height) = logical_size(options);
95            let logical_size = Size::new(logical_width, logical_height);
96            let input_frame = if session_state.focused {
97                match input.as_mut() {
98                    Some(input) => match input.poll_frame(
99                        &self.xr.instance,
100                        &self.session,
101                        &self.reference_space,
102                        options.layer,
103                        frame_state.predicted_display_time,
104                        logical_size,
105                    ) {
106                        Ok(frame) => Some(frame.pointer_frame),
107                        Err(error) => {
108                            let _ = self.frame_stream.end(
109                                frame_state.predicted_display_time,
110                                self.blend_mode,
111                                &[],
112                            );
113                            return Err(error);
114                        }
115                    },
116                    None => None,
117                }
118            } else {
119                None
120            };
121
122            let acquired = match swapchain.acquire_wait() {
123                Ok(acquired) => acquired,
124                Err(error) => {
125                    let _ = self.frame_stream.end(
126                        frame_state.predicted_display_time,
127                        self.blend_mode,
128                        &[],
129                    );
130                    return Err(error);
131                }
132            };
133
134            let render_result = {
135                let target = swapchain.frame_target(&acquired);
136                let frame = OpenXrExternalHostFrame::new(
137                    OpenXrExternalVulkanContext::from(&self.vk),
138                    target,
139                )
140                .with_pointer_frame(input_frame.as_ref());
141                ui_layer.layout_paint_render(frame).map(|_| ())
142            };
143            let release_result = swapchain.release();
144            if let Err(error) = combine_render_release(render_result, release_result) {
145                let _ =
146                    self.frame_stream
147                        .end(frame_state.predicted_display_time, self.blend_mode, &[]);
148                return Err(error);
149            }
150
151            let layer = composer.build_layer(&self.reference_space, &swapchain);
152            let layer_ref: &xr::CompositionLayerBase<'_, xr::Vulkan> = &layer;
153            self.frame_stream
154                .end(
155                    frame_state.predicted_display_time,
156                    self.blend_mode,
157                    &[layer_ref],
158                )
159                .map_err(|result| OpenXrRuntimeError::FrameEnd { result })?;
160        }
161
162        Ok(())
163    }
164}
165
166/// Converts physical pixels to logical dimensions with DPR clamped to `0.0001`.
167fn logical_size(options: OpenXrUiFrameLoopOptions) -> (f32, f32) {
168    let dpr = options.scale.dpr.max(0.0001);
169    (
170        options.pixel_width as f32 / dpr,
171        options.pixel_height as f32 / dpr,
172    )
173}