ailloli_ui_openxr/openxr_runtime/
composer.rs

1//! Composition-layer geometry and frame-loop option types.
2
3use ailloli_ui_core::math::Scale;
4use ailloli_ui_core::Color;
5use ailloli_ui_render_vulkan::VulkanRendererOptions;
6use openxr as xr;
7
8use super::input::OpenXrUiInputOptions;
9use super::swapchain::OpenXrQuadSwapchain;
10
11#[derive(Clone, Copy)]
12/// Options for a host-managed loop that clears and submits a quad layer.
13///
14/// Defaults to a 1024x576 image, opaque blue clear, and a 1.6x0.9 metre quad
15/// two metres in front of the reference-space origin.
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_openxr::OpenXrQuadFrameLoopOptions;
21/// let options = OpenXrQuadFrameLoopOptions::default();
22/// assert_eq!((options.pixel_width, options.pixel_height), (1024, 576));
23/// ```
24pub struct OpenXrQuadFrameLoopOptions {
25    /// Swapchain width in physical pixels; zero is rejected by swapchain creation.
26    pub pixel_width: u32,
27    /// Swapchain height in physical pixels; zero is rejected by swapchain creation.
28    pub pixel_height: u32,
29    /// Linear RGBA clear value forwarded to Vulkan.
30    pub clear_color: [f32; 4],
31    /// Quad pose, size, visibility, and flags.
32    pub layer: OpenXrQuadLayerOptions,
33}
34
35impl Default for OpenXrQuadFrameLoopOptions {
36    fn default() -> Self {
37        Self {
38            pixel_width: 1024,
39            pixel_height: 576,
40            clear_color: [0.05, 0.20, 0.80, 1.0],
41            layer: OpenXrQuadLayerOptions::default(),
42        }
43    }
44}
45
46#[derive(Clone, Copy)]
47/// Options for rendering Ailloli draw commands through Vulkan into a quad.
48///
49/// # Examples
50///
51/// ```
52/// use ailloli_ui_openxr::OpenXrRenderVulkanFrameLoopOptions;
53/// let options = OpenXrRenderVulkanFrameLoopOptions::default();
54/// assert_eq!(options.scale.dpr, 1.0);
55/// ```
56pub struct OpenXrRenderVulkanFrameLoopOptions {
57    /// Swapchain width in physical pixels.
58    pub pixel_width: u32,
59    /// Swapchain height in physical pixels.
60    pub pixel_height: u32,
61    /// Clear color applied before UI rendering.
62    pub clear: Color,
63    /// Logical-to-physical scale used by the renderer.
64    pub scale: Scale,
65    /// Quad composition-layer configuration.
66    pub layer: OpenXrQuadLayerOptions,
67    /// Vulkan renderer configuration.
68    pub renderer: VulkanRendererOptions,
69}
70
71impl Default for OpenXrRenderVulkanFrameLoopOptions {
72    fn default() -> Self {
73        Self {
74            pixel_width: 1024,
75            pixel_height: 576,
76            clear: Color::f32(0.05, 0.20, 0.80, 1.0),
77            scale: Scale::new(1.0),
78            layer: OpenXrQuadLayerOptions::default(),
79            renderer: VulkanRendererOptions::default(),
80        }
81    }
82}
83
84#[derive(Clone, Copy)]
85/// Complete options for the built-in Ailloli UI OpenXR frame loop.
86///
87/// # Examples
88///
89/// ```
90/// use ailloli_ui_openxr::OpenXrUiFrameLoopOptions;
91/// let options = OpenXrUiFrameLoopOptions::default();
92/// assert!(options.input.enabled);
93/// assert_eq!((options.pixel_width, options.pixel_height), (1024, 576));
94/// ```
95pub struct OpenXrUiFrameLoopOptions {
96    /// Swapchain width in physical pixels.
97    pub pixel_width: u32,
98    /// Swapchain height in physical pixels.
99    pub pixel_height: u32,
100    /// Clear color applied before UI rendering.
101    pub clear: Color,
102    /// Logical-to-physical scale; the default DPR is `1.0`.
103    pub scale: Scale,
104    /// Quad composition-layer configuration.
105    pub layer: OpenXrQuadLayerOptions,
106    /// Vulkan renderer configuration.
107    pub renderer: VulkanRendererOptions,
108    /// Controller and hand-input selection configuration.
109    pub input: OpenXrUiInputOptions,
110}
111
112impl Default for OpenXrUiFrameLoopOptions {
113    fn default() -> Self {
114        Self {
115            pixel_width: 1024,
116            pixel_height: 576,
117            clear: Color::f32(0.05, 0.20, 0.80, 1.0),
118            scale: Scale::new(1.0),
119            layer: OpenXrQuadLayerOptions::default(),
120            renderer: VulkanRendererOptions::default(),
121            input: OpenXrUiInputOptions::default(),
122        }
123    }
124}
125
126#[derive(Clone, Copy)]
127/// Pose and presentation attributes for one OpenXR quad layer.
128///
129/// Size and position use metres. The default faces both eyes at `(0, 0, -2)`
130/// with identity orientation and no composition flags.
131///
132/// # Examples
133///
134/// ```
135/// use ailloli_ui_openxr::OpenXrQuadLayerOptions;
136/// let layer = OpenXrQuadLayerOptions::default();
137/// assert_eq!((layer.size.width, layer.size.height), (1.6, 0.9));
138/// assert_eq!(layer.pose.position.z, -2.0);
139/// ```
140pub struct OpenXrQuadLayerOptions {
141    /// Layer pose relative to the submitted reference space, in metres.
142    pub pose: xr::Posef,
143    /// Physical layer width and height in metres.
144    pub size: xr::Extent2Df,
145    /// Eyes to which the layer is visible.
146    pub eye_visibility: xr::EyeVisibility,
147    /// OpenXR composition flags such as source-alpha blending.
148    pub layer_flags: xr::CompositionLayerFlags,
149}
150
151impl Default for OpenXrQuadLayerOptions {
152    fn default() -> Self {
153        Self {
154            pose: xr::Posef {
155                orientation: xr::Quaternionf {
156                    x: 0.0,
157                    y: 0.0,
158                    z: 0.0,
159                    w: 1.0,
160                },
161                position: xr::Vector3f {
162                    x: 0.0,
163                    y: 0.0,
164                    z: -2.0,
165                },
166            },
167            size: xr::Extent2Df {
168                width: 1.6,
169                height: 0.9,
170            },
171            eye_visibility: xr::EyeVisibility::BOTH,
172            layer_flags: xr::CompositionLayerFlags::EMPTY,
173        }
174    }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178/// Requested presentation surface geometry.
179///
180/// # Examples
181///
182/// ```
183/// use ailloli_ui_openxr::OpenXrScreenSurfaceMode;
184/// assert_ne!(OpenXrScreenSurfaceMode::Flat, OpenXrScreenSurfaceMode::Curved);
185/// ```
186pub enum OpenXrScreenSurfaceMode {
187    /// Submit a standard quad composition layer.
188    Flat,
189    /// Resolve to a cylinder extension or mesh fallback.
190    Curved,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194/// Backend preference used when [`OpenXrScreenSurfaceMode::Curved`] is selected.
195///
196/// # Examples
197///
198/// ```
199/// use ailloli_ui_openxr::OpenXrCurvedLayerBackend;
200/// let backend = OpenXrCurvedLayerBackend::Auto;
201/// assert!(matches!(backend, OpenXrCurvedLayerBackend::Auto));
202/// ```
203pub enum OpenXrCurvedLayerBackend {
204    /// Prefer a native cylinder when supported, otherwise select mesh fallback.
205    Auto,
206    /// Require native cylinder-layer composition.
207    Cylinder,
208    /// Request the quad-backed mesh fallback path.
209    Mesh,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
213/// Physical geometry for a curved screen.
214///
215/// All fields are metres. [`Self::sanitized`] enforces positive radius and
216/// dimensions and a non-negative gap.
217///
218/// # Examples
219///
220/// ```
221/// use ailloli_ui_openxr::OpenXrCurvedScreenOptions;
222/// let options = OpenXrCurvedScreenOptions::default();
223/// assert_eq!((options.radius_m, options.gap_m), (2.0, 0.12));
224/// ```
225pub struct OpenXrCurvedScreenOptions {
226    /// Cylinder radius in metres; sanitized to at least `0.05`.
227    pub radius_m: f32,
228    /// Arc width in metres; sanitized to at least `0.01`.
229    pub screen_width_m: f32,
230    /// Screen height in metres; sanitized to at least `0.01`.
231    pub screen_height_m: f32,
232    /// Separation between adjacent curved screens in metres; clamped to zero.
233    pub gap_m: f32,
234}
235
236impl Default for OpenXrCurvedScreenOptions {
237    fn default() -> Self {
238        Self {
239            radius_m: 2.0,
240            screen_width_m: 1.6,
241            screen_height_m: 0.9,
242            gap_m: 0.12,
243        }
244    }
245}
246
247impl OpenXrCurvedScreenOptions {
248    /// Returns a copy with geometry clamped to safe positive minima.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use ailloli_ui_openxr::OpenXrCurvedScreenOptions;
254    /// let sanitized = OpenXrCurvedScreenOptions { radius_m: 0.0, screen_width_m: -1.0, screen_height_m: 0.0, gap_m: -2.0 }.sanitized();
255    /// assert_eq!((sanitized.radius_m, sanitized.screen_width_m, sanitized.screen_height_m, sanitized.gap_m), (0.05, 0.01, 0.01, 0.0));
256    /// ```
257    pub fn sanitized(self) -> Self {
258        Self {
259            radius_m: self.radius_m.max(0.05),
260            screen_width_m: self.screen_width_m.max(0.01),
261            screen_height_m: self.screen_height_m.max(0.01),
262            gap_m: self.gap_m.max(0.0),
263        }
264    }
265
266    /// Returns the cylinder's central angle in radians (`width / radius`).
267    ///
268    /// Sanitization occurs before division.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use ailloli_ui_openxr::OpenXrCurvedScreenOptions;
274    /// assert!((OpenXrCurvedScreenOptions::default().central_angle() - 0.8).abs() < 1e-6);
275    /// ```
276    pub fn central_angle(self) -> f32 {
277        let options = self.sanitized();
278        options.screen_width_m / options.radius_m
279    }
280
281    /// Returns the sanitized physical width-to-height ratio.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use ailloli_ui_openxr::OpenXrCurvedScreenOptions;
287    /// assert!((OpenXrCurvedScreenOptions::default().aspect_ratio() - 16.0 / 9.0).abs() < 1e-6);
288    /// ```
289    pub fn aspect_ratio(self) -> f32 {
290        let options = self.sanitized();
291        options.screen_width_m / options.screen_height_m
292    }
293}
294
295#[derive(Clone, Copy)]
296/// Selects flat or curved composition together with each geometry configuration.
297///
298/// # Examples
299///
300/// ```
301/// use ailloli_ui_openxr::{OpenXrScreenLayerOptions, OpenXrScreenSurfaceMode};
302/// let options = OpenXrScreenLayerOptions::default();
303/// assert_eq!(options.surface_mode, OpenXrScreenSurfaceMode::Flat);
304/// ```
305pub struct OpenXrScreenLayerOptions {
306    /// Requested flat or curved surface.
307    pub surface_mode: OpenXrScreenSurfaceMode,
308    /// Preferred implementation for a curved surface.
309    pub curved_backend: OpenXrCurvedLayerBackend,
310    /// Pose, visibility, flags, and fallback quad dimensions.
311    pub flat: OpenXrQuadLayerOptions,
312    /// Physical cylinder or mesh geometry.
313    pub curved: OpenXrCurvedScreenOptions,
314}
315
316impl Default for OpenXrScreenLayerOptions {
317    fn default() -> Self {
318        Self {
319            surface_mode: OpenXrScreenSurfaceMode::Flat,
320            curved_backend: OpenXrCurvedLayerBackend::Auto,
321            flat: OpenXrQuadLayerOptions::default(),
322            curved: OpenXrCurvedScreenOptions::default(),
323        }
324    }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328/// Concrete composition backend selected for the current runtime capabilities.
329///
330/// # Examples
331///
332/// ```
333/// use ailloli_ui_openxr::OpenXrResolvedScreenBackend;
334/// let backend = OpenXrResolvedScreenBackend::Flat;
335/// assert_eq!(backend, OpenXrResolvedScreenBackend::Flat);
336/// ```
337pub enum OpenXrResolvedScreenBackend {
338    /// Standard OpenXR quad composition layer.
339    Flat,
340    /// `XR_KHR_composition_layer_cylinder` composition layer.
341    Cylinder,
342    /// Renderer-owned curved mesh submitted through a quad image.
343    Mesh,
344}
345
346/// Borrowed OpenXR composition layer produced by a screen composer.
347///
348/// # Examples
349///
350/// ```no_run
351/// use ailloli_ui_openxr::OpenXrScreenLayer;
352/// fn submit(layer: &OpenXrScreenLayer<'_>) {
353///     let _base: &openxr::CompositionLayerBase<'_, openxr::Vulkan> = layer.as_base();
354/// }
355/// ```
356pub enum OpenXrScreenLayer<'a> {
357    /// Flat quad layer, also used as the current mesh-fallback submission shell.
358    Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
359    /// Native cylinder-extension layer.
360    Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
361}
362
363impl<'a> OpenXrScreenLayer<'a> {
364    /// Erases the concrete layer type for `Session::end_frame` submission.
365    ///
366    /// # Examples
367    ///
368    /// ```no_run
369    /// use ailloli_ui_openxr::OpenXrScreenLayer;
370    /// fn base<'a>(layer: &'a OpenXrScreenLayer<'a>) -> &'a openxr::CompositionLayerBase<'a, openxr::Vulkan> {
371    ///     layer.as_base()
372    /// }
373    /// ```
374    pub fn as_base(&self) -> &xr::CompositionLayerBase<'_, xr::Vulkan> {
375        match self {
376            Self::Quad(layer) => layer,
377            Self::Cylinder(layer) => layer,
378        }
379    }
380}
381
382/// Builds a standard quad layer referencing one Ailloli swapchain.
383///
384/// # Examples
385///
386/// ```
387/// use ailloli_ui_openxr::{OpenXrQuadComposer, OpenXrQuadLayerOptions};
388/// let _composer = OpenXrQuadComposer::new(OpenXrQuadLayerOptions::default());
389/// ```
390pub struct OpenXrQuadComposer {
391    /// Immutable layer attributes copied into each submitted layer.
392    layer: OpenXrQuadLayerOptions,
393}
394
395impl OpenXrQuadComposer {
396    /// Creates a composer from fixed quad attributes.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use ailloli_ui_openxr::{OpenXrQuadComposer, OpenXrQuadLayerOptions};
402    /// let _: OpenXrQuadComposer = OpenXrQuadComposer::new(OpenXrQuadLayerOptions::default());
403    /// ```
404    pub fn new(layer: OpenXrQuadLayerOptions) -> Self {
405        Self { layer }
406    }
407
408    /// Builds a borrowed quad composition layer for one frame submission.
409    ///
410    /// The result references both `reference_space` and `swapchain`; keep both
411    /// alive through `end_frame`. Array image index is always zero.
412    ///
413    /// # Examples
414    ///
415    /// ```no_run
416    /// use ailloli_ui_openxr::{OpenXrQuadComposer, OpenXrQuadLayerOptions, OpenXrQuadSwapchain};
417    /// fn layer<'a>(composer: &OpenXrQuadComposer, space: &'a openxr::Space, swapchain: &'a OpenXrQuadSwapchain) -> openxr::CompositionLayerQuad<'a, openxr::Vulkan> {
418    ///     composer.build_layer(space, swapchain)
419    /// }
420    /// # let _ = OpenXrQuadLayerOptions::default();
421    /// ```
422    pub fn build_layer<'a>(
423        &self,
424        reference_space: &'a xr::Space,
425        swapchain: &'a OpenXrQuadSwapchain,
426    ) -> xr::CompositionLayerQuad<'a, xr::Vulkan> {
427        xr::CompositionLayerQuad::new()
428            .space(reference_space)
429            .eye_visibility(self.layer.eye_visibility)
430            .sub_image(
431                xr::SwapchainSubImage::new()
432                    .swapchain(swapchain.handle())
433                    .image_array_index(0)
434                    .image_rect(swapchain.image_rect()),
435            )
436            .pose(self.layer.pose)
437            .size(self.layer.size)
438            .layer_flags(self.layer.layer_flags)
439    }
440}
441
442/// Resolves screen geometry and builds its OpenXR composition layer.
443///
444/// # Examples
445///
446/// ```
447/// use ailloli_ui_openxr::{OpenXrResolvedScreenBackend, OpenXrScreenComposer, OpenXrScreenLayerOptions};
448/// let composer = OpenXrScreenComposer::new(OpenXrScreenLayerOptions::default());
449/// assert_eq!(composer.resolve_backend(false), OpenXrResolvedScreenBackend::Flat);
450/// ```
451pub struct OpenXrScreenComposer {
452    /// Requested surface and layer configuration.
453    options: OpenXrScreenLayerOptions,
454}
455
456impl OpenXrScreenComposer {
457    /// Creates a composer with immutable options.
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// use ailloli_ui_openxr::{OpenXrScreenComposer, OpenXrScreenLayerOptions};
463    /// let _: OpenXrScreenComposer = OpenXrScreenComposer::new(OpenXrScreenLayerOptions::default());
464    /// ```
465    pub fn new(options: OpenXrScreenLayerOptions) -> Self {
466        Self { options }
467    }
468
469    /// Resolves the requested surface against cylinder-extension availability.
470    ///
471    /// Explicit `Cylinder` remains cylinder even when the capability argument is
472    /// false; callers must validate that forced choice before submission.
473    ///
474    /// # Examples
475    ///
476    /// ```
477    /// use ailloli_ui_openxr::{OpenXrCurvedLayerBackend, OpenXrResolvedScreenBackend, OpenXrScreenComposer, OpenXrScreenLayerOptions, OpenXrScreenSurfaceMode};
478    /// let composer = OpenXrScreenComposer::new(OpenXrScreenLayerOptions { surface_mode: OpenXrScreenSurfaceMode::Curved, curved_backend: OpenXrCurvedLayerBackend::Auto, ..Default::default() });
479    /// assert_eq!(composer.resolve_backend(true), OpenXrResolvedScreenBackend::Cylinder);
480    /// assert_eq!(composer.resolve_backend(false), OpenXrResolvedScreenBackend::Mesh);
481    /// ```
482    pub fn resolve_backend(
483        &self,
484        composition_layer_cylinder_supported: bool,
485    ) -> OpenXrResolvedScreenBackend {
486        resolve_screen_backend(
487            self.options.surface_mode,
488            self.options.curved_backend,
489            composition_layer_cylinder_supported,
490        )
491    }
492
493    /// Builds the resolved borrowed layer for one frame.
494    ///
495    /// `Mesh` currently uses a quad submission shell; the renderer is responsible
496    /// for producing curved imagery in that swapchain.
497    ///
498    /// # Examples
499    ///
500    /// ```no_run
501    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrScreenComposer, OpenXrScreenLayer, OpenXrScreenLayerOptions};
502    /// fn build<'a>(composer: &OpenXrScreenComposer, space: &'a openxr::Space, swapchain: &'a OpenXrQuadSwapchain) -> OpenXrScreenLayer<'a> {
503    ///     composer.build_layer(space, swapchain, true)
504    /// }
505    /// # let _ = OpenXrScreenLayerOptions::default();
506    /// ```
507    pub fn build_layer<'a>(
508        &self,
509        reference_space: &'a xr::Space,
510        swapchain: &'a OpenXrQuadSwapchain,
511        composition_layer_cylinder_supported: bool,
512    ) -> OpenXrScreenLayer<'a> {
513        match self.resolve_backend(composition_layer_cylinder_supported) {
514            OpenXrResolvedScreenBackend::Flat | OpenXrResolvedScreenBackend::Mesh => {
515                OpenXrScreenLayer::Quad(
516                    OpenXrQuadComposer::new(self.options.flat)
517                        .build_layer(reference_space, swapchain),
518                )
519            }
520            OpenXrResolvedScreenBackend::Cylinder => {
521                let curved = self.options.curved.sanitized();
522                OpenXrScreenLayer::Cylinder(
523                    xr::CompositionLayerCylinderKHR::new()
524                        .space(reference_space)
525                        .eye_visibility(self.options.flat.eye_visibility)
526                        .sub_image(
527                            xr::SwapchainSubImage::new()
528                                .swapchain(swapchain.handle())
529                                .image_array_index(0)
530                                .image_rect(swapchain.image_rect()),
531                        )
532                        .pose(self.options.flat.pose)
533                        .radius(curved.radius_m)
534                        .central_angle(curved.central_angle())
535                        .aspect_ratio(curved.aspect_ratio())
536                        .layer_flags(self.options.flat.layer_flags),
537                )
538            }
539        }
540    }
541}
542
543/// Resolves a surface/backend request without accessing an OpenXR instance.
544///
545/// Flat always resolves to [`OpenXrResolvedScreenBackend::Flat`]. Auto-curved
546/// uses cylinder when available and mesh otherwise; explicit backends are kept.
547///
548/// # Examples
549///
550/// ```
551/// use ailloli_ui_openxr::{OpenXrCurvedLayerBackend, OpenXrResolvedScreenBackend, OpenXrScreenSurfaceMode};
552/// use ailloli_ui_openxr::openxr_runtime::composer::resolve_screen_backend;
553/// assert_eq!(resolve_screen_backend(OpenXrScreenSurfaceMode::Curved, OpenXrCurvedLayerBackend::Auto, false), OpenXrResolvedScreenBackend::Mesh);
554/// ```
555pub fn resolve_screen_backend(
556    surface_mode: OpenXrScreenSurfaceMode,
557    curved_backend: OpenXrCurvedLayerBackend,
558    composition_layer_cylinder_supported: bool,
559) -> OpenXrResolvedScreenBackend {
560    match surface_mode {
561        OpenXrScreenSurfaceMode::Flat => OpenXrResolvedScreenBackend::Flat,
562        OpenXrScreenSurfaceMode::Curved => match curved_backend {
563            OpenXrCurvedLayerBackend::Auto => {
564                if composition_layer_cylinder_supported {
565                    OpenXrResolvedScreenBackend::Cylinder
566                } else {
567                    OpenXrResolvedScreenBackend::Mesh
568                }
569            }
570            OpenXrCurvedLayerBackend::Cylinder => OpenXrResolvedScreenBackend::Cylinder,
571            OpenXrCurvedLayerBackend::Mesh => OpenXrResolvedScreenBackend::Mesh,
572        },
573    }
574}
575
576#[cfg(test)]
577/// Covers defaults, cylinder geometry, and deterministic backend resolution.
578mod tests {
579    use super::*;
580
581    #[test]
582    fn ui_frame_loop_options_default_matches_quad_defaults() {
583        let options = OpenXrUiFrameLoopOptions::default();
584        assert_eq!(options.pixel_width, 1024);
585        assert_eq!(options.pixel_height, 576);
586        assert_eq!(options.clear.to_array(), [0.05, 0.20, 0.80, 1.0]);
587        assert_eq!(options.scale.dpr, 1.0);
588        assert_eq!(options.layer.size.width, 1.6);
589        assert_eq!(options.layer.size.height, 0.9);
590        assert!(options.input.enabled);
591    }
592
593    #[test]
594    fn curved_options_compute_cylinder_geometry() {
595        let options = OpenXrCurvedScreenOptions::default();
596        assert!((options.central_angle() - 0.8).abs() < 1e-6);
597        assert!((options.aspect_ratio() - (16.0 / 9.0)).abs() < 1e-6);
598    }
599
600    #[test]
601    fn auto_curved_backend_prefers_cylinder_when_available() {
602        assert_eq!(
603            resolve_screen_backend(
604                OpenXrScreenSurfaceMode::Curved,
605                OpenXrCurvedLayerBackend::Auto,
606                true,
607            ),
608            OpenXrResolvedScreenBackend::Cylinder
609        );
610        assert_eq!(
611            resolve_screen_backend(
612                OpenXrScreenSurfaceMode::Curved,
613                OpenXrCurvedLayerBackend::Auto,
614                false,
615            ),
616            OpenXrResolvedScreenBackend::Mesh
617        );
618    }
619
620    #[test]
621    fn flat_mode_ignores_curved_backend() {
622        assert_eq!(
623            resolve_screen_backend(
624                OpenXrScreenSurfaceMode::Flat,
625                OpenXrCurvedLayerBackend::Cylinder,
626                true,
627            ),
628            OpenXrResolvedScreenBackend::Flat
629        );
630    }
631}