script/dom/webxr/
xrsystem.rs1use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use js::context::JSContext;
9use js::realm::CurrentRealm;
10use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
11use script_bindings::cell::DomRefCell;
12use script_bindings::reflector::reflect_dom_object;
13use servo_base::id::PipelineId;
14use servo_config::pref;
15use webxr_api::{Error as XRError, Session, SessionInit, SessionMode};
16
17use crate::conversions::Convert;
18use crate::dom::bindings::codegen::Bindings::XRSystemBinding::{
19 XRSessionInit, XRSessionMode, XRSystemMethods,
20};
21use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible};
22use crate::dom::bindings::error::Error;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
25use crate::dom::bindings::reflector::DomGlobal;
26use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot, MutNullableDom};
27use crate::dom::eventtarget::EventTarget;
28use crate::dom::gamepad::Gamepad;
29use crate::dom::promise::{Promise, RootedPromise};
30use crate::dom::window::Window;
31use crate::dom::xrsession::XRSession;
32use crate::dom::xrtest::XRTest;
33use crate::event_loop::script_thread::ScriptThread;
34
35#[dom_struct]
36pub(crate) struct XRSystem {
37 eventtarget: EventTarget,
38 gamepads: DomRefCell<Vec<Dom<Gamepad>>>,
39 pending_immersive_session: Cell<bool>,
40 active_immersive_session: MutNullableDom<XRSession>,
41 active_inline_sessions: DomRefCell<Vec<Dom<XRSession>>>,
42 test: MutNullableDom<XRTest>,
43 #[no_trace]
44 pipeline: PipelineId,
45}
46
47impl XRSystem {
48 fn new_inherited(pipeline: PipelineId) -> XRSystem {
49 XRSystem {
50 eventtarget: EventTarget::new_inherited(),
51 gamepads: DomRefCell::new(Vec::new()),
52 pending_immersive_session: Cell::new(false),
53 active_immersive_session: Default::default(),
54 active_inline_sessions: DomRefCell::new(Vec::new()),
55 test: Default::default(),
56 pipeline,
57 }
58 }
59
60 pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<XRSystem> {
61 reflect_dom_object(
62 cx,
63 Box::new(XRSystem::new_inherited(window.pipeline_id())),
64 window,
65 )
66 }
67
68 pub(crate) fn pending_or_active_session(&self) -> bool {
69 self.pending_immersive_session.get() || self.active_immersive_session.get().is_some()
70 }
71
72 pub(crate) fn set_pending(&self) {
73 self.pending_immersive_session.set(true)
74 }
75
76 pub(crate) fn set_active_immersive_session(&self, session: &XRSession) {
77 self.pending_immersive_session.set(false);
80 self.active_immersive_session.set(Some(session))
81 }
82
83 pub(crate) fn end_session(&self, session: &XRSession) {
85 if let Some(active) = self.active_immersive_session.get() &&
87 Dom::from_ref(&*active) == Dom::from_ref(session)
88 {
89 self.active_immersive_session.set(None);
90 session.dirty_layers();
93 }
94 self.active_inline_sessions
95 .borrow_mut()
96 .retain(|sess| Dom::from_ref(&**sess) != Dom::from_ref(session));
97 }
98}
99
100impl Convert<SessionMode> for XRSessionMode {
101 fn convert(self) -> SessionMode {
102 match self {
103 XRSessionMode::Immersive_vr => SessionMode::ImmersiveVR,
104 XRSessionMode::Immersive_ar => SessionMode::ImmersiveAR,
105 XRSessionMode::Inline => SessionMode::Inline,
106 }
107 }
108}
109
110impl XRSystemMethods<crate::DomTypeHolder> for XRSystem {
111 fn IsSessionSupported(&self, cx: &mut CurrentRealm, mode: XRSessionMode) -> RootedPromise {
113 let promise = Promise::new_in_realm(cx);
115 let mut trusted = Some(TrustedPromise::from(&promise));
116 let global = self.global();
117 let task_source = global
118 .task_manager()
119 .dom_manipulation_task_source()
120 .to_sendable();
121
122 let callback = ProfileGenericCallback::new(move |message| {
123 let trusted = if let Some(trusted) = trusted.take() {
125 trusted
126 } else {
127 error!("supportsSession callback called twice!");
128 return;
129 };
130 let message: Result<(), webxr_api::Error> = if let Ok(message) = message {
131 message
132 } else {
133 error!("supportsSession callback given incorrect payload");
134 return;
135 };
136 if let Ok(()) = message {
137 task_source.queue(trusted.resolve_task(true));
138 } else {
139 task_source.queue(trusted.resolve_task(false));
140 };
141 })
142 .expect("Could not create callback");
143
144 if let Some(mut r) = global.as_window().webxr_registry() {
145 r.supports_session(mode.convert(), callback);
146 }
147
148 promise
149 }
150
151 fn RequestSession(
153 &self,
154 realm: &mut CurrentRealm,
155 mode: XRSessionMode,
156 init: &XRSessionInit,
157 ) -> RootedPromise {
158 let global = self.global();
159 let window = global.as_window();
160 let promise = Promise::new_in_realm(realm);
161
162 if mode != XRSessionMode::Inline {
163 if !ScriptThread::is_user_interacting() {
164 if pref!(dom_webxr_unsafe_assume_user_intent) {
165 warn!(
166 "The dom.webxr.unsafe-assume-user-intent preference assumes user intent to enter WebXR."
167 );
168 } else {
169 promise.reject_error(realm, Error::Security(None));
170 return promise;
171 }
172 }
173
174 if self.pending_or_active_session() {
175 promise.reject_error(realm, Error::InvalidState(None));
176 return promise;
177 }
178
179 self.set_pending();
180 }
181
182 let mut required_features = vec![];
183 let mut optional_features = vec![];
184
185 if let Some(ref r) = init.requiredFeatures {
186 for feature in r {
187 if let Ok(ConversionResult::Success(s)) =
188 String::from_jsval(realm, feature.as_handle_value(), ())
189 {
190 required_features.push(s)
191 } else {
192 warn!("Unable to convert required feature to string");
193 if mode != XRSessionMode::Inline {
194 self.pending_immersive_session.set(false);
195 }
196 promise.reject_error(realm, Error::NotSupported(None));
197 return promise;
198 }
199 }
200 }
201
202 if let Some(ref o) = init.optionalFeatures {
203 for feature in o {
204 if let Ok(ConversionResult::Success(s)) =
205 String::from_jsval(realm, feature.as_handle_value(), ())
206 {
207 optional_features.push(s)
208 } else {
209 warn!("Unable to convert optional feature to string");
210 }
211 }
212 }
213
214 if !required_features.contains(&"viewer".to_string()) {
215 required_features.push("viewer".to_string());
216 }
217
218 if !required_features.contains(&"local".to_string()) && mode != XRSessionMode::Inline {
219 required_features.push("local".to_string());
220 }
221
222 let init = SessionInit {
223 required_features,
224 optional_features,
225 first_person_observer_view: pref!(dom_webxr_first_person_observer_view),
226 };
227
228 let mut trusted = Some(TrustedPromise::from(&promise));
229 let this = Trusted::new(self);
230 let task_source = global
231 .task_manager()
232 .dom_manipulation_task_source()
233 .to_sendable();
234 let callback = ProfileGenericCallback::new(move |message| {
235 let Some(trusted) = trusted.take() else {
236 error!("requestSession callback called more than once");
237 return;
238 };
239 let this = this.clone();
240 let message: Result<Session, webxr_api::Error> = if let Ok(message) = message {
241 message
242 } else {
243 error!("requestSession callback given incorrect payload");
244 return;
245 };
246 task_source.queue(task!(request_session: move |cx| {
247 this.root().session_obtained(cx, message, &trusted.root(cx), mode);
248 }));
249 })
250 .expect("Could not create callback");
251 if let Some(mut r) = window.webxr_registry() {
252 r.request_session(mode.convert(), init, callback);
253 }
254 promise
255 }
256
257 fn Test(&self, cx: &mut JSContext) -> DomRoot<XRTest> {
259 self.test.or_init(|| XRTest::new(cx, &self.global()))
260 }
261}
262
263impl XRSystem {
264 fn session_obtained(
265 &self,
266 cx: &mut JSContext,
267 response: Result<Session, XRError>,
268 promise: &RootedPromise,
269 mode: XRSessionMode,
270 ) {
271 let session = match response {
272 Ok(session) => session,
273 Err(e) => {
274 warn!("Error requesting XR session: {:?}", e);
275 if mode != XRSessionMode::Inline {
276 self.pending_immersive_session.set(false);
277 }
278 promise.reject_error(cx, Error::NotSupported(None));
279 return;
280 },
281 };
282 let session = XRSession::new(cx, self.global().as_window(), session, mode);
283 if mode == XRSessionMode::Inline {
284 self.active_inline_sessions
285 .borrow_mut()
286 .push(Dom::from_ref(&*session));
287 } else {
288 self.set_active_immersive_session(&session);
289 }
290 promise.resolve_native(cx, &session);
291 session.setup_initial_inputs();
294 }
295
296 pub(crate) fn dispatch_sessionavailable(&self) {
298 let xr = Trusted::new(self);
299 self.global()
300 .task_manager()
301 .dom_manipulation_task_source()
302 .queue(task!(fire_sessionavailable_event: move |cx| {
303 let xr = xr.root();
305 let _guard = ScriptThread::user_interacting_guard();
306 xr.upcast::<EventTarget>().fire_bubbling_event(cx, atom!("sessionavailable"));
307 }));
308 }
309}