ailloli_ui_openxr/openxr_runtime/
instance.rs

1//! OpenXR loader, extension negotiation, system selection, and runtime identity.
2
3use openxr as xr;
4
5use super::error::OpenXrRuntimeError;
6
7/// Internal names used to create the OpenXR instance.
8///
9/// # Examples
10///
11/// ```no_run
12/// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeOptions};
13/// let options = OpenXrRuntimeOptions { application_name: "My XR app".to_string(), engine_name: "My engine".to_string(), ..Default::default() };
14/// let _runtime = OpenXrRuntime::new(options)?;
15/// # Ok::<(), ailloli_ui_openxr::OpenXrRuntimeError>(())
16/// ```
17pub(crate) struct OpenXrInstanceOptions<'a> {
18    /// Application name, shorter than OpenXR's byte limit and without NULs.
19    pub application_name: &'a str,
20    /// Engine name, shorter than OpenXR's byte limit and without NULs.
21    pub engine_name: &'a str,
22}
23
24/// Loaded OpenXR instance plus negotiated system capabilities.
25///
26/// The runtime and system handles remain valid for the lifetime of this value.
27/// Capability booleans reflect extensions enabled during construction, not a
28/// prediction about every individual input device.
29///
30/// # Examples
31///
32/// ```no_run
33/// use ailloli_ui_openxr::OpenXrInstance;
34/// fn report(instance: &OpenXrInstance) -> (&str, bool) {
35///     (&instance.runtime_name, instance.hand_tracking_supported)
36/// }
37/// ```
38pub struct OpenXrInstance {
39    /// Dynamically loaded OpenXR entry table.
40    pub entry: xr::Entry,
41    /// Created OpenXR instance handle.
42    pub instance: xr::Instance,
43    /// Selected head-mounted-display system identifier.
44    pub system: xr::SystemId,
45    /// First advertised stereo environment blend mode, or opaque fallback.
46    pub blend_mode: xr::EnvironmentBlendMode,
47    /// Runtime-reported implementation name.
48    pub runtime_name: String,
49    /// Runtime-reported implementation version.
50    pub runtime_version: xr::Version,
51    /// Whether system hand tracking and its extension are available.
52    pub hand_tracking_supported: bool,
53    /// Whether the FB hand-aim extension is enabled with hand tracking.
54    pub hand_aim_supported: bool,
55    /// Whether native cylinder composition layers are enabled.
56    pub composition_layer_cylinder_supported: bool,
57}
58
59impl OpenXrInstance {
60    /// Loads OpenXR and creates the instance used by the high-level runtime.
61    ///
62    /// The function enables Vulkan 2 integration, optional hand extensions, and
63    /// optional cylinder layers. Android also initializes and enables its loader
64    /// extension. The first advertised stereo blend mode is selected.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`OpenXrRuntimeError`] for invalid byte-limited names, loader or
69    /// extension failures, instance creation, property lookup, or HMD selection.
70    ///
71    /// # Examples
72    ///
73    /// ```no_run
74    /// use ailloli_ui_openxr::{OpenXrRuntime, OpenXrRuntimeOptions};
75    /// let runtime = OpenXrRuntime::new(OpenXrRuntimeOptions::default())?;
76    /// println!("{}", runtime.xr.runtime_name);
77    /// # Ok::<(), ailloli_ui_openxr::OpenXrRuntimeError>(())
78    /// ```
79    pub(crate) fn new(options: OpenXrInstanceOptions<'_>) -> Result<Self, OpenXrRuntimeError> {
80        validate_openxr_name(
81            "application_name",
82            options.application_name,
83            xr::sys::MAX_APPLICATION_NAME_SIZE,
84        )?;
85        validate_openxr_name(
86            "engine_name",
87            options.engine_name,
88            xr::sys::MAX_ENGINE_NAME_SIZE,
89        )?;
90
91        let entry = unsafe { xr::Entry::load() }.map_err(|err| OpenXrRuntimeError::LoadEntry {
92            message: err.to_string(),
93        })?;
94
95        #[cfg(target_os = "android")]
96        entry
97            .initialize_android_loader()
98            .map_err(|result| OpenXrRuntimeError::InitializeAndroidLoader { result })?;
99
100        let available = entry
101            .enumerate_extensions()
102            .map_err(|result| OpenXrRuntimeError::EnumerateExtensions { result })?;
103        if !available.khr_vulkan_enable2 {
104            return Err(OpenXrRuntimeError::MissingExtension(
105                "XR_KHR_vulkan_enable2",
106            ));
107        }
108        #[cfg(target_os = "android")]
109        if !available.khr_android_create_instance {
110            return Err(OpenXrRuntimeError::MissingExtension(
111                "XR_KHR_android_create_instance",
112            ));
113        }
114
115        let hand_tracking_ext_available = available.ext_hand_tracking;
116        let hand_aim_ext_available = hand_tracking_ext_available && available.fb_hand_tracking_aim;
117        let composition_layer_cylinder_supported = available.khr_composition_layer_cylinder;
118        if composition_layer_cylinder_supported {
119            log::info!("OpenXR extension XR_KHR_composition_layer_cylinder available/enabled");
120        } else {
121            log::warn!("OpenXR extension XR_KHR_composition_layer_cylinder unavailable");
122        }
123
124        let mut enabled = xr::ExtensionSet::default();
125        enabled.khr_vulkan_enable2 = true;
126        #[cfg(target_os = "android")]
127        {
128            enabled.khr_android_create_instance = true;
129        }
130        if hand_tracking_ext_available {
131            enabled.ext_hand_tracking = true;
132            if hand_aim_ext_available {
133                enabled.fb_hand_tracking_aim = true;
134            }
135        }
136        if composition_layer_cylinder_supported {
137            enabled.khr_composition_layer_cylinder = true;
138        }
139
140        let instance = entry
141            .create_instance(
142                &xr::ApplicationInfo {
143                    application_name: options.application_name,
144                    application_version: 0,
145                    engine_name: options.engine_name,
146                    engine_version: 0,
147                    api_version: xr::Version::new(1, 0, 0),
148                },
149                &enabled,
150                &[],
151            )
152            .map_err(|result| OpenXrRuntimeError::CreateInstance { result })?;
153        let properties = instance
154            .properties()
155            .map_err(|result| OpenXrRuntimeError::InstanceProperties { result })?;
156
157        let system = instance
158            .system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)
159            .map_err(|result| OpenXrRuntimeError::System { result })?;
160
161        let blend_modes = instance
162            .enumerate_environment_blend_modes(system, xr::ViewConfigurationType::PRIMARY_STEREO)
163            .map_err(|result| OpenXrRuntimeError::BlendModes { result })?;
164        let blend_mode = blend_modes
165            .first()
166            .copied()
167            .unwrap_or(xr::EnvironmentBlendMode::OPAQUE);
168
169        let hand_tracking_supported = if hand_tracking_ext_available {
170            instance.supports_hand_tracking(system).unwrap_or(false)
171        } else {
172            false
173        };
174        let hand_aim_supported = hand_tracking_supported && hand_aim_ext_available;
175
176        Ok(Self {
177            entry,
178            instance,
179            system,
180            blend_mode,
181            runtime_name: properties.runtime_name,
182            runtime_version: properties.runtime_version,
183            hand_tracking_supported,
184            hand_aim_supported,
185            composition_layer_cylinder_supported,
186        })
187    }
188}
189
190/// Validates an OpenXR fixed-size, NUL-terminated UTF-8 name field.
191///
192/// Embedded NUL bytes are rejected. `max_bytes_with_nul` includes the trailing
193/// NUL, so the string itself must be strictly shorter than that limit.
194///
195/// # Errors
196///
197/// Returns [`OpenXrRuntimeError::InvalidOpenXrName`] when `value` contains NUL or
198/// its UTF-8 byte length plus the terminator exceeds `max_bytes_with_nul`.
199fn validate_openxr_name(
200    field: &'static str,
201    value: &str,
202    max_bytes_with_nul: usize,
203) -> Result<(), OpenXrRuntimeError> {
204    if value.as_bytes().contains(&0) {
205        return Err(OpenXrRuntimeError::InvalidOpenXrName {
206            field,
207            reason: "must not contain NUL bytes".to_string(),
208        });
209    }
210    if value.len() >= max_bytes_with_nul {
211        return Err(OpenXrRuntimeError::InvalidOpenXrName {
212            field,
213            reason: format!(
214                "must be shorter than {max_bytes_with_nul} bytes including trailing NUL, got {}",
215                value.len() + 1
216            ),
217        });
218    }
219    Ok(())
220}