1use 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
13pub 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 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 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 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}