Skip to main content

konjure_web/
browser.rs

1//! DOM/browser API implementation, compiled only for `wasm32`.
2//!
3//! Capability discovery does not start media capture or request an XR session.
4//! Browser policy may still restrict support queries. GPU and sensor resources
5//! used by a renderer remain owned by that renderer's platform boundary.
6
7use crate::BrowserCapabilities;
8use js_sys::{Function, Promise, Reflect};
9use wasm_bindgen::{JsCast, JsValue};
10use wasm_bindgen_futures::JsFuture;
11use web_sys::{HtmlCanvasElement, WebGl2RenderingContext, WebglLoseContext};
12
13/// Probe the current window without requesting an XR session or sensor stream.
14///
15/// Both XR modes are queried independently. An unsupported mode is false; a
16/// rejected query is false with a labelled diagnostic in `xr_error`.
17///
18/// # Errors
19/// Returns a JavaScript error when no browser window/document exists or a
20/// required global cannot be read. Optional WebGL/XR failures remain in the
21/// returned capability snapshot so other observations are still usable.
22pub async fn discover_capabilities() -> Result<BrowserCapabilities, JsValue> {
23    let window =
24        web_sys::window().ok_or_else(|| JsValue::from_str("browser window unavailable"))?;
25    let document = window
26        .document()
27        .ok_or_else(|| JsValue::from_str("browser document unavailable"))?;
28    let navigator = window.navigator();
29    let mut capabilities = BrowserCapabilities {
30        secure_context: window.is_secure_context(),
31        wasm: Reflect::get(window.as_ref(), &JsValue::from_str("WebAssembly"))?.is_object(),
32        webgpu: !Reflect::get(navigator.as_ref(), &JsValue::from_str("gpu"))?
33            .is_null_or_undefined(),
34        ..BrowserCapabilities::default()
35    };
36    match probe_webgl(&document) {
37        Ok(supported) => capabilities.webgl2 = supported,
38        Err(error) => capabilities.webgl_error = Some(describe_error(&error)),
39    }
40    if capabilities.secure_context {
41        match Reflect::get(navigator.as_ref(), &JsValue::from_str("xr")) {
42            Ok(xr) if !xr.is_null_or_undefined() => {
43                // Attach rejection handlers to both promises before awaiting either.
44                let ar = query_support(&xr, "immersive-ar");
45                let vr = query_support(&xr, "immersive-vr");
46                let mut errors = Vec::new();
47                capabilities.immersive_ar = resolve_support(ar, "immersive-ar", &mut errors).await;
48                capabilities.immersive_vr = resolve_support(vr, "immersive-vr", &mut errors).await;
49                if !errors.is_empty() {
50                    capabilities.xr_error = Some(errors.join("; "));
51                }
52            }
53            Ok(_) => {}
54            Err(error) => capabilities.xr_error = Some(describe_error(&error)),
55        }
56    }
57    Ok(capabilities)
58}
59
60fn probe_webgl(document: &web_sys::Document) -> Result<bool, JsValue> {
61    let canvas: HtmlCanvasElement = document.create_element("canvas")?.dyn_into()?;
62    let Some(context) = canvas.get_context("webgl2")? else {
63        return Ok(false);
64    };
65    let context: WebGl2RenderingContext = context.dyn_into()?;
66    if let Some(extension) = context.get_extension("WEBGL_lose_context")? {
67        // getExtension defines this interface; extension objects have no global constructor.
68        extension
69            .unchecked_into::<WebglLoseContext>()
70            .lose_context();
71    }
72    Ok(true)
73}
74
75fn query_support(xr: &JsValue, mode: &str) -> Result<JsFuture, JsValue> {
76    // Reflect keeps this optional WebXR boundary independent of web-sys's
77    // unstable XR feature flags. No authored code or strings are evaluated.
78    let method: Function = Reflect::get(xr, &JsValue::from_str("isSessionSupported"))?
79        .dyn_into()
80        .map_err(|_| JsValue::from_str("XR support-query method unavailable"))?;
81    let promise: Promise = method
82        .call1(xr, &JsValue::from_str(mode))?
83        .dyn_into()
84        .map_err(|_| JsValue::from_str("XR support query did not return a Promise"))?;
85    Ok(JsFuture::from(promise))
86}
87
88async fn resolve_support(
89    query: Result<JsFuture, JsValue>,
90    mode: &str,
91    errors: &mut Vec<String>,
92) -> bool {
93    let result = match query {
94        Ok(pending) => pending.await,
95        Err(error) => Err(error),
96    };
97    match result {
98        Ok(value) => match value.as_bool() {
99            Some(supported) => supported,
100            None => {
101                errors.push(format!("{mode}: XR support query did not return a boolean"));
102                false
103            }
104        },
105        Err(error) => {
106            errors.push(format!("{mode}: {}", describe_error(&error)));
107            false
108        }
109    }
110}
111
112fn describe_error(error: &JsValue) -> String {
113    error
114        .as_string()
115        .or_else(|| {
116            Reflect::get(error, &JsValue::from_str("message"))
117                .ok()
118                .and_then(|value| value.as_string())
119        })
120        .unwrap_or_else(|| format!("{error:?}"))
121}