mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Up until now, Servo was using a very old version of time to get a cross-process monotonic timestamp (using `time::precise_time_ns()`). This change replaces the usage of old time with a new serializable monotonic time called `CrossProcessInstant` and uses it where `u64` timestamps were stored before. The standard library doesn't provide this functionality because it isn't something you can do reliably on all platforms. The idea is that we do our best and then fall back gracefully. This is a big change, because Servo was using `u64` timestamps all over the place some as raw values taken from `time::precise_time_ns()` and some as relative offsets from the "navigation start," which is a concept similar to DOM's `timeOrigin` (but not exactly the same). It's very difficult to fix this situation without fixing it everywhere as the `Instant` concept is supposed to be opaque. The good thing is that this change clears up all ambiguity when passing times as a `time::Duration` is unit agnostic and a `CrossProcessInstant` represents an absolute moment in time. The `time` version of `Duration` is used because it can both be negative and is also serializable. Good things: - No need too pass around `time` and `time_precise` any longer. `CrossProcessInstant` is also precise and monotonic. - The distinction between a time that is unset or at `0` (at some kind of timer epoch) is now gone. There still a lot of work to do to clean up timing, but this is the first step. In general, I've tried to preserve existing behavior, even when not spec compliant, as much as possible. I plan to submit followup PRs fixing some of the issues I've noticed. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
1053 lines
39 KiB
Rust
1053 lines
39 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::cell::Cell;
|
|
use std::collections::HashMap;
|
|
use std::f64::consts::{FRAC_PI_2, PI};
|
|
use std::rc::Rc;
|
|
use std::{mem, ptr};
|
|
|
|
use base::cross_process_instant::CrossProcessInstant;
|
|
use dom_struct::dom_struct;
|
|
use euclid::{RigidTransform3D, Transform3D, Vector3D};
|
|
use ipc_channel::ipc::IpcReceiver;
|
|
use ipc_channel::router::ROUTER;
|
|
use js::jsapi::JSObject;
|
|
use js::jsval::JSVal;
|
|
use js::typedarray::Float32Array;
|
|
use profile_traits::ipc;
|
|
use servo_atoms::Atom;
|
|
use webxr_api::{
|
|
self, util, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
|
|
Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray,
|
|
SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility,
|
|
};
|
|
|
|
use super::bindings::trace::HashMapTracedValues;
|
|
use crate::dom::bindings::buffer_source::create_buffer_source;
|
|
use crate::dom::bindings::callback::ExceptionHandling;
|
|
use crate::dom::bindings::cell::DomRefCell;
|
|
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
|
|
use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
|
|
use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{
|
|
XRHitTestOptionsInit, XRHitTestTrackableType,
|
|
};
|
|
use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
|
|
use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{
|
|
XRRenderStateInit, XRRenderStateMethods,
|
|
};
|
|
use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{
|
|
XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods,
|
|
XRVisibilityState,
|
|
};
|
|
use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
|
|
use crate::dom::bindings::error::{Error, ErrorResult};
|
|
use crate::dom::bindings::inheritance::Castable;
|
|
use crate::dom::bindings::num::Finite;
|
|
use crate::dom::bindings::refcounted::Trusted;
|
|
use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
|
|
use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom};
|
|
use crate::dom::bindings::utils::to_frozen_array;
|
|
use crate::dom::event::Event;
|
|
use crate::dom::eventtarget::EventTarget;
|
|
use crate::dom::globalscope::GlobalScope;
|
|
use crate::dom::promise::Promise;
|
|
use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace;
|
|
use crate::dom::xrframe::XRFrame;
|
|
use crate::dom::xrhittestsource::XRHitTestSource;
|
|
use crate::dom::xrinputsourcearray::XRInputSourceArray;
|
|
use crate::dom::xrinputsourceevent::XRInputSourceEvent;
|
|
use crate::dom::xrreferencespace::XRReferenceSpace;
|
|
use crate::dom::xrrenderstate::XRRenderState;
|
|
use crate::dom::xrsessionevent::XRSessionEvent;
|
|
use crate::dom::xrspace::XRSpace;
|
|
use crate::realms::InRealm;
|
|
use crate::script_runtime::JSContext;
|
|
use crate::task_source::TaskSource;
|
|
|
|
#[dom_struct]
|
|
pub struct XRSession {
|
|
eventtarget: EventTarget,
|
|
blend_mode: XREnvironmentBlendMode,
|
|
mode: XRSessionMode,
|
|
visibility_state: Cell<XRVisibilityState>,
|
|
viewer_space: MutNullableDom<XRSpace>,
|
|
#[ignore_malloc_size_of = "defined in webxr"]
|
|
#[no_trace]
|
|
session: DomRefCell<Session>,
|
|
frame_requested: Cell<bool>,
|
|
pending_render_state: MutNullableDom<XRRenderState>,
|
|
active_render_state: MutDom<XRRenderState>,
|
|
/// Cached projection matrix for inline sessions
|
|
#[no_trace]
|
|
inline_projection_matrix: DomRefCell<Transform3D<f32, Viewer, Display>>,
|
|
|
|
next_raf_id: Cell<i32>,
|
|
#[ignore_malloc_size_of = "closures are hard"]
|
|
raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
|
|
#[ignore_malloc_size_of = "closures are hard"]
|
|
current_raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
|
|
input_sources: Dom<XRInputSourceArray>,
|
|
// Any promises from calling end()
|
|
#[ignore_malloc_size_of = "promises are hard"]
|
|
end_promises: DomRefCell<Vec<Rc<Promise>>>,
|
|
/// <https://immersive-web.github.io/webxr/#ended>
|
|
ended: Cell<bool>,
|
|
#[ignore_malloc_size_of = "defined in webxr"]
|
|
#[no_trace]
|
|
next_hit_test_id: Cell<HitTestId>,
|
|
#[ignore_malloc_size_of = "defined in webxr"]
|
|
pending_hit_test_promises: DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>>>,
|
|
/// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame"
|
|
/// <https://immersive-web.github.io/webxr/#opaque-framebuffer>
|
|
outside_raf: Cell<bool>,
|
|
#[ignore_malloc_size_of = "defined in webxr"]
|
|
#[no_trace]
|
|
input_frames: DomRefCell<HashMap<InputId, InputFrame>>,
|
|
framerate: Cell<f32>,
|
|
#[ignore_malloc_size_of = "promises are hard"]
|
|
update_framerate_promise: DomRefCell<Option<Rc<Promise>>>,
|
|
}
|
|
|
|
impl XRSession {
|
|
fn new_inherited(
|
|
session: Session,
|
|
render_state: &XRRenderState,
|
|
input_sources: &XRInputSourceArray,
|
|
mode: XRSessionMode,
|
|
) -> XRSession {
|
|
XRSession {
|
|
eventtarget: EventTarget::new_inherited(),
|
|
blend_mode: session.environment_blend_mode().into(),
|
|
mode,
|
|
visibility_state: Cell::new(XRVisibilityState::Visible),
|
|
viewer_space: Default::default(),
|
|
session: DomRefCell::new(session),
|
|
frame_requested: Cell::new(false),
|
|
pending_render_state: MutNullableDom::new(None),
|
|
active_render_state: MutDom::new(render_state),
|
|
inline_projection_matrix: Default::default(),
|
|
|
|
next_raf_id: Cell::new(0),
|
|
raf_callback_list: DomRefCell::new(vec![]),
|
|
current_raf_callback_list: DomRefCell::new(vec![]),
|
|
input_sources: Dom::from_ref(input_sources),
|
|
end_promises: DomRefCell::new(vec![]),
|
|
ended: Cell::new(false),
|
|
next_hit_test_id: Cell::new(HitTestId(0)),
|
|
pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()),
|
|
outside_raf: Cell::new(true),
|
|
input_frames: DomRefCell::new(HashMap::new()),
|
|
framerate: Cell::new(0.0),
|
|
update_framerate_promise: DomRefCell::new(None),
|
|
}
|
|
}
|
|
|
|
pub fn new(
|
|
global: &GlobalScope,
|
|
session: Session,
|
|
mode: XRSessionMode,
|
|
frame_receiver: IpcReceiver<Frame>,
|
|
) -> DomRoot<XRSession> {
|
|
let ivfov = if mode == XRSessionMode::Inline {
|
|
Some(FRAC_PI_2)
|
|
} else {
|
|
None
|
|
};
|
|
let render_state = XRRenderState::new(global, 0.1, 1000.0, ivfov, None, Vec::new());
|
|
let input_sources = XRInputSourceArray::new(global);
|
|
let ret = reflect_dom_object(
|
|
Box::new(XRSession::new_inherited(
|
|
session,
|
|
&render_state,
|
|
&input_sources,
|
|
mode,
|
|
)),
|
|
global,
|
|
);
|
|
ret.attach_event_handler();
|
|
ret.setup_raf_loop(frame_receiver);
|
|
ret
|
|
}
|
|
|
|
pub fn with_session<R, F: FnOnce(&Session) -> R>(&self, with: F) -> R {
|
|
let session = self.session.borrow();
|
|
with(&session)
|
|
}
|
|
|
|
pub fn is_ended(&self) -> bool {
|
|
self.ended.get()
|
|
}
|
|
|
|
pub fn is_immersive(&self) -> bool {
|
|
self.mode != XRSessionMode::Inline
|
|
}
|
|
|
|
// https://immersive-web.github.io/layers/#feature-descriptor-layers
|
|
pub fn has_layers_feature(&self) -> bool {
|
|
// We do not support creating layers other than projection layers
|
|
// https://github.com/servo/servo/issues/27493
|
|
false
|
|
}
|
|
|
|
fn setup_raf_loop(&self, frame_receiver: IpcReceiver<Frame>) {
|
|
let this = Trusted::new(self);
|
|
let global = self.global();
|
|
let window = global.as_window();
|
|
let (task_source, canceller) = window
|
|
.task_manager()
|
|
.dom_manipulation_task_source_with_canceller();
|
|
ROUTER.add_route(
|
|
frame_receiver.to_opaque(),
|
|
Box::new(move |message| {
|
|
let frame: Frame = message.to().unwrap();
|
|
let time = CrossProcessInstant::now();
|
|
let this = this.clone();
|
|
let _ = task_source.queue_with_canceller(
|
|
task!(xr_raf_callback: move || {
|
|
this.root().raf_callback(frame, time);
|
|
}),
|
|
&canceller,
|
|
);
|
|
}),
|
|
);
|
|
|
|
self.session.borrow_mut().start_render_loop();
|
|
}
|
|
|
|
pub fn is_outside_raf(&self) -> bool {
|
|
self.outside_raf.get()
|
|
}
|
|
|
|
fn attach_event_handler(&self) {
|
|
let this = Trusted::new(self);
|
|
let global = self.global();
|
|
let window = global.as_window();
|
|
let (task_source, canceller) = window
|
|
.task_manager()
|
|
.dom_manipulation_task_source_with_canceller();
|
|
let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
|
|
|
|
ROUTER.add_route(
|
|
receiver.to_opaque(),
|
|
Box::new(move |message| {
|
|
let this = this.clone();
|
|
let _ = task_source.queue_with_canceller(
|
|
task!(xr_event_callback: move || {
|
|
this.root().event_callback(message.to().unwrap());
|
|
}),
|
|
&canceller,
|
|
);
|
|
}),
|
|
);
|
|
|
|
// request animation frame
|
|
self.session.borrow_mut().set_event_dest(sender);
|
|
}
|
|
|
|
// Must be called after the promise for session creation is resolved
|
|
// https://github.com/immersive-web/webxr/issues/961
|
|
//
|
|
// This enables content that assumes all input sources are accompanied
|
|
// by an inputsourceschange event to work properly. Without
|
|
pub fn setup_initial_inputs(&self) {
|
|
let initial_inputs = self.session.borrow().initial_inputs().to_owned();
|
|
|
|
if initial_inputs.is_empty() {
|
|
// do not fire an empty event
|
|
return;
|
|
}
|
|
|
|
let global = self.global();
|
|
let window = global.as_window();
|
|
let (task_source, canceller) = window
|
|
.task_manager()
|
|
.dom_manipulation_task_source_with_canceller();
|
|
let this = Trusted::new(self);
|
|
// Queue a task so that it runs after resolve()'s microtasks complete
|
|
// so that content has a chance to attach a listener for inputsourceschange
|
|
let _ = task_source.queue_with_canceller(
|
|
task!(session_initial_inputs: move || {
|
|
let this = this.root();
|
|
this.input_sources.add_input_sources(&this, &initial_inputs);
|
|
}),
|
|
&canceller,
|
|
);
|
|
}
|
|
|
|
fn event_callback(&self, event: XREvent) {
|
|
match event {
|
|
XREvent::SessionEnd => {
|
|
// https://immersive-web.github.io/webxr/#shut-down-the-session
|
|
// Step 2
|
|
self.ended.set(true);
|
|
// Step 3-4
|
|
self.global().as_window().Navigator().Xr().end_session(self);
|
|
// Step 5: We currently do not have any such promises
|
|
// Step 6 is happening n the XR session
|
|
// https://immersive-web.github.io/webxr/#dom-xrsession-end step 3
|
|
for promise in self.end_promises.borrow_mut().drain(..) {
|
|
promise.resolve_native(&());
|
|
}
|
|
// Step 7
|
|
let event = XRSessionEvent::new(&self.global(), atom!("end"), false, false, self);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
},
|
|
XREvent::Select(input, kind, ty, frame) => {
|
|
use servo_atoms::Atom;
|
|
const START_ATOMS: [Atom; 2] = [atom!("selectstart"), atom!("squeezestart")];
|
|
const EVENT_ATOMS: [Atom; 2] = [atom!("select"), atom!("squeeze")];
|
|
const END_ATOMS: [Atom; 2] = [atom!("selectend"), atom!("squeezeend")];
|
|
|
|
// https://immersive-web.github.io/webxr/#primary-action
|
|
let source = self.input_sources.find(input);
|
|
let atom_index = if kind == SelectKind::Squeeze { 1 } else { 0 };
|
|
if let Some(source) = source {
|
|
let frame = XRFrame::new(&self.global(), self, frame);
|
|
frame.set_active(true);
|
|
if ty == SelectEvent::Start {
|
|
let event = XRInputSourceEvent::new(
|
|
&self.global(),
|
|
START_ATOMS[atom_index].clone(),
|
|
false,
|
|
false,
|
|
&frame,
|
|
&source,
|
|
);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
} else {
|
|
if ty == SelectEvent::Select {
|
|
let event = XRInputSourceEvent::new(
|
|
&self.global(),
|
|
EVENT_ATOMS[atom_index].clone(),
|
|
false,
|
|
false,
|
|
&frame,
|
|
&source,
|
|
);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
}
|
|
let event = XRInputSourceEvent::new(
|
|
&self.global(),
|
|
END_ATOMS[atom_index].clone(),
|
|
false,
|
|
false,
|
|
&frame,
|
|
&source,
|
|
);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
}
|
|
frame.set_active(false);
|
|
}
|
|
},
|
|
XREvent::VisibilityChange(v) => {
|
|
let v = match v {
|
|
Visibility::Visible => XRVisibilityState::Visible,
|
|
Visibility::VisibleBlurred => XRVisibilityState::Visible_blurred,
|
|
Visibility::Hidden => XRVisibilityState::Hidden,
|
|
};
|
|
self.visibility_state.set(v);
|
|
let event = XRSessionEvent::new(
|
|
&self.global(),
|
|
atom!("visibilitychange"),
|
|
false,
|
|
false,
|
|
self,
|
|
);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
// The page may be visible again, dirty the layers
|
|
// This also wakes up the event loop if necessary
|
|
self.dirty_layers();
|
|
},
|
|
XREvent::AddInput(info) => {
|
|
self.input_sources.add_input_sources(self, &[info]);
|
|
},
|
|
XREvent::RemoveInput(id) => {
|
|
self.input_sources.remove_input_source(self, id);
|
|
},
|
|
XREvent::UpdateInput(id, source) => {
|
|
self.input_sources.add_remove_input_source(self, id, source);
|
|
},
|
|
XREvent::InputChanged(id, frame) => {
|
|
self.input_frames.borrow_mut().insert(id, frame);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#xr-animation-frame>
|
|
fn raf_callback(&self, mut frame: Frame, time: CrossProcessInstant) {
|
|
debug!("WebXR RAF callback {:?}", frame);
|
|
|
|
// Step 1-2 happen in the xebxr device thread
|
|
|
|
// Step 3
|
|
if let Some(pending) = self.pending_render_state.take() {
|
|
// https://immersive-web.github.io/webxr/#apply-the-pending-render-state
|
|
// (Steps 1-4 are implicit)
|
|
// Step 5
|
|
self.active_render_state.set(&pending);
|
|
// Step 6-7: XXXManishearth handle inlineVerticalFieldOfView
|
|
|
|
if !self.is_immersive() {
|
|
self.update_inline_projection_matrix()
|
|
}
|
|
}
|
|
|
|
// TODO: how does this fit the webxr spec?
|
|
for event in frame.events.drain(..) {
|
|
self.handle_frame_event(event);
|
|
}
|
|
|
|
// Step 4
|
|
// TODO: what should this check be?
|
|
// This is checking that the new render state has the same
|
|
// layers as the frame.
|
|
// Related to https://github.com/immersive-web/webxr/issues/1051
|
|
if !self
|
|
.active_render_state
|
|
.get()
|
|
.has_sub_images(&frame.sub_images[..])
|
|
{
|
|
// If the frame has different layers than the render state,
|
|
// we just return early, drawing a blank frame.
|
|
// This can result in flickering when the render state is changed.
|
|
// TODO: it would be better to not render anything until the next frame.
|
|
warn!("Rendering blank XR frame");
|
|
self.session.borrow_mut().render_animation_frame();
|
|
return;
|
|
}
|
|
|
|
// Step 5: XXXManishearth handle inline session
|
|
|
|
// Step 6-7
|
|
{
|
|
let mut current = self.current_raf_callback_list.borrow_mut();
|
|
assert!(current.is_empty());
|
|
mem::swap(&mut *self.raf_callback_list.borrow_mut(), &mut current);
|
|
}
|
|
|
|
let time = self.global().performance().to_dom_high_res_time_stamp(time);
|
|
let frame = XRFrame::new(&self.global(), self, frame);
|
|
|
|
// Step 8-9
|
|
frame.set_active(true);
|
|
frame.set_animation_frame(true);
|
|
|
|
// Step 10
|
|
self.apply_frame_updates(&frame);
|
|
|
|
// TODO: how does this fit with the webxr and xr layers specs?
|
|
self.layers_begin_frame(&frame);
|
|
|
|
// Step 11-12
|
|
self.outside_raf.set(false);
|
|
let len = self.current_raf_callback_list.borrow().len();
|
|
for i in 0..len {
|
|
let callback = self.current_raf_callback_list.borrow()[i].1.clone();
|
|
if let Some(callback) = callback {
|
|
let _ = callback.Call__(time, &frame, ExceptionHandling::Report);
|
|
}
|
|
}
|
|
self.outside_raf.set(true);
|
|
*self.current_raf_callback_list.borrow_mut() = vec![];
|
|
|
|
// TODO: how does this fit with the webxr and xr layers specs?
|
|
self.layers_end_frame(&frame);
|
|
|
|
// Step 13
|
|
frame.set_active(false);
|
|
|
|
// TODO: how does this fit the webxr spec?
|
|
self.session.borrow_mut().render_animation_frame();
|
|
}
|
|
|
|
fn update_inline_projection_matrix(&self) {
|
|
debug_assert!(!self.is_immersive());
|
|
let render_state = self.active_render_state.get();
|
|
let size = if let Some(base) = render_state.GetBaseLayer() {
|
|
base.size()
|
|
} else {
|
|
return;
|
|
};
|
|
let mut clip_planes = util::ClipPlanes::default();
|
|
let near = *render_state.DepthNear() as f32;
|
|
let far = *render_state.DepthFar() as f32;
|
|
clip_planes.update(near, far);
|
|
let top = *render_state
|
|
.GetInlineVerticalFieldOfView()
|
|
.expect("IVFOV should be non null for inline sessions") /
|
|
2.;
|
|
let top = near * top.tan() as f32;
|
|
let bottom = top;
|
|
let left = top * size.width as f32 / size.height as f32;
|
|
let right = left;
|
|
let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes);
|
|
*self.inline_projection_matrix.borrow_mut() = matrix;
|
|
}
|
|
|
|
/// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size
|
|
pub fn inline_view(&self) -> View<Viewer> {
|
|
debug_assert!(!self.is_immersive());
|
|
View {
|
|
// Inline views have no offset
|
|
transform: RigidTransform3D::identity(),
|
|
projection: *self.inline_projection_matrix.borrow(),
|
|
}
|
|
}
|
|
|
|
pub fn session_id(&self) -> SessionId {
|
|
self.session.borrow().id()
|
|
}
|
|
|
|
pub fn dirty_layers(&self) {
|
|
if let Some(layer) = self.RenderState().GetBaseLayer() {
|
|
layer.context().mark_as_dirty();
|
|
}
|
|
}
|
|
|
|
// TODO: how does this align with the layers spec?
|
|
fn layers_begin_frame(&self, frame: &XRFrame) {
|
|
if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
|
|
layer.begin_frame(frame);
|
|
}
|
|
self.active_render_state.get().with_layers(|layers| {
|
|
for layer in layers {
|
|
layer.begin_frame(frame);
|
|
}
|
|
});
|
|
}
|
|
|
|
// TODO: how does this align with the layers spec?
|
|
fn layers_end_frame(&self, frame: &XRFrame) {
|
|
if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
|
|
layer.end_frame(frame);
|
|
}
|
|
self.active_render_state.get().with_layers(|layers| {
|
|
for layer in layers {
|
|
layer.end_frame(frame);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#xrframe-apply-frame-updates>
|
|
fn apply_frame_updates(&self, _frame: &XRFrame) {
|
|
// <https://www.w3.org/TR/webxr-gamepads-module-1/#xrframe-apply-gamepad-frame-updates>
|
|
for (id, frame) in self.input_frames.borrow_mut().drain() {
|
|
let source = self.input_sources.find(id);
|
|
if let Some(source) = source {
|
|
source.update_gamepad_state(frame);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_frame_event(&self, event: FrameUpdateEvent) {
|
|
match event {
|
|
FrameUpdateEvent::HitTestSourceAdded(id) => {
|
|
if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) {
|
|
promise.resolve_native(&XRHitTestSource::new(&self.global(), id, self));
|
|
} else {
|
|
warn!(
|
|
"received hit test add request for unknown hit test {:?}",
|
|
id
|
|
)
|
|
}
|
|
},
|
|
_ => self.session.borrow_mut().apply_event(event),
|
|
}
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#apply-the-nominal-frame-rate>
|
|
fn apply_nominal_framerate(&self, rate: f32) {
|
|
if self.framerate.get() == rate || self.ended.get() {
|
|
return;
|
|
}
|
|
|
|
self.framerate.set(rate);
|
|
|
|
let event = XRSessionEvent::new(
|
|
&self.global(),
|
|
Atom::from("frameratechange"),
|
|
false,
|
|
false,
|
|
self,
|
|
);
|
|
event.upcast::<Event>().fire(self.upcast());
|
|
}
|
|
}
|
|
|
|
impl XRSessionMethods for XRSession {
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-end
|
|
event_handler!(end, GetOnend, SetOnend);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-select
|
|
event_handler!(select, GetOnselect, SetOnselect);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart
|
|
event_handler!(selectstart, GetOnselectstart, SetOnselectstart);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend
|
|
event_handler!(selectend, GetOnselectend, SetOnselectend);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze
|
|
event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart
|
|
event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend
|
|
event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange
|
|
event_handler!(
|
|
visibilitychange,
|
|
GetOnvisibilitychange,
|
|
SetOnvisibilitychange
|
|
);
|
|
|
|
// https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange
|
|
event_handler!(
|
|
inputsourceschange,
|
|
GetOninputsourceschange,
|
|
SetOninputsourceschange
|
|
);
|
|
|
|
// https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange
|
|
event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange);
|
|
|
|
// https://immersive-web.github.io/webxr/#dom-xrsession-renderstate
|
|
fn RenderState(&self) -> DomRoot<XRRenderState> {
|
|
self.active_render_state.get()
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate>
|
|
fn UpdateRenderState(&self, init: &XRRenderStateInit, _: InRealm) -> ErrorResult {
|
|
// Step 2
|
|
if self.ended.get() {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
// Step 3:
|
|
if let Some(Some(ref layer)) = init.baseLayer {
|
|
if Dom::from_ref(layer.session()) != Dom::from_ref(self) {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
}
|
|
|
|
// Step 4:
|
|
if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() {
|
|
return Err(Error::InvalidState);
|
|
}
|
|
|
|
// https://immersive-web.github.io/layers/#updaterenderstatechanges
|
|
// Step 1.
|
|
if init.baseLayer.is_some() {
|
|
if self.has_layers_feature() {
|
|
return Err(Error::NotSupported);
|
|
}
|
|
// https://github.com/immersive-web/layers/issues/189
|
|
if init.layers.is_some() {
|
|
return Err(Error::Type(String::from(
|
|
"Cannot set WebXR layers and baseLayer",
|
|
)));
|
|
}
|
|
}
|
|
|
|
if let Some(Some(ref layers)) = init.layers {
|
|
// Step 2
|
|
for layer in layers {
|
|
let count = layers
|
|
.iter()
|
|
.filter(|other| other.layer_id() == layer.layer_id())
|
|
.count();
|
|
if count > 1 {
|
|
return Err(Error::Type(String::from("Duplicate entry in WebXR layers")));
|
|
}
|
|
}
|
|
|
|
// Step 3
|
|
for layer in layers {
|
|
if layer.session() != self {
|
|
return Err(Error::Type(String::from(
|
|
"Layer from different session in WebXR layers",
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 4-5
|
|
let pending = self
|
|
.pending_render_state
|
|
.or_init(|| self.active_render_state.get().clone_object());
|
|
|
|
// Step 6
|
|
if let Some(ref layers) = init.layers {
|
|
let layers = layers.as_deref().unwrap_or_default();
|
|
pending.set_base_layer(None);
|
|
pending.set_layers(layers.iter().map(|x| &**x).collect());
|
|
let layers = layers
|
|
.iter()
|
|
.filter_map(|layer| {
|
|
let context_id = WebXRContextId::from(layer.context_id());
|
|
let layer_id = layer.layer_id()?;
|
|
Some((context_id, layer_id))
|
|
})
|
|
.collect();
|
|
self.session.borrow_mut().set_layers(layers);
|
|
}
|
|
|
|
// End of https://immersive-web.github.io/layers/#updaterenderstatechanges
|
|
|
|
if let Some(near) = init.depthNear {
|
|
let mut near = *near;
|
|
// Step 8 from #apply-the-pending-render-state
|
|
// this may need to be changed if backends wish to impose
|
|
// further constraints
|
|
if near < 0. {
|
|
near = 0.;
|
|
}
|
|
pending.set_depth_near(near);
|
|
}
|
|
if let Some(far) = init.depthFar {
|
|
let mut far = *far;
|
|
// Step 9 from #apply-the-pending-render-state
|
|
// this may need to be changed if backends wish to impose
|
|
// further constraints
|
|
// currently the maximum is infinity, so just check that
|
|
// the value is non-negative
|
|
if far < 0. {
|
|
far = 0.;
|
|
}
|
|
pending.set_depth_far(far);
|
|
}
|
|
if let Some(fov) = init.inlineVerticalFieldOfView {
|
|
let mut fov = *fov;
|
|
// Step 10 from #apply-the-pending-render-state
|
|
// this may need to be changed if backends wish to impose
|
|
// further constraints
|
|
if fov < 0. {
|
|
fov = 0.0001;
|
|
} else if fov > PI {
|
|
fov = PI - 0.0001;
|
|
}
|
|
pending.set_inline_vertical_fov(fov);
|
|
}
|
|
if let Some(ref layer) = init.baseLayer {
|
|
pending.set_base_layer(layer.as_deref());
|
|
pending.set_layers(Vec::new());
|
|
let layers = layer
|
|
.iter()
|
|
.filter_map(|layer| {
|
|
let context_id = WebXRContextId::from(layer.context_id());
|
|
let layer_id = layer.layer_id()?;
|
|
Some((context_id, layer_id))
|
|
})
|
|
.collect();
|
|
self.session.borrow_mut().set_layers(layers);
|
|
}
|
|
|
|
if init.depthFar.is_some() || init.depthNear.is_some() {
|
|
self.session
|
|
.borrow_mut()
|
|
.update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe>
|
|
fn RequestAnimationFrame(&self, callback: Rc<XRFrameRequestCallback>) -> i32 {
|
|
// queue up RAF callback, obtain ID
|
|
let raf_id = self.next_raf_id.get();
|
|
self.next_raf_id.set(raf_id + 1);
|
|
self.raf_callback_list
|
|
.borrow_mut()
|
|
.push((raf_id, Some(callback)));
|
|
|
|
raf_id
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe>
|
|
fn CancelAnimationFrame(&self, frame: i32) {
|
|
let mut list = self.raf_callback_list.borrow_mut();
|
|
if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
|
|
pair.1 = None;
|
|
}
|
|
|
|
let mut list = self.current_raf_callback_list.borrow_mut();
|
|
if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
|
|
pair.1 = None;
|
|
}
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-environmentblendmode>
|
|
fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode {
|
|
self.blend_mode
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-visibilitystate>
|
|
fn VisibilityState(&self) -> XRVisibilityState {
|
|
self.visibility_state.get()
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace>
|
|
fn RequestReferenceSpace(&self, ty: XRReferenceSpaceType, comp: InRealm) -> Rc<Promise> {
|
|
let p = Promise::new_in_current_realm(comp);
|
|
|
|
// https://immersive-web.github.io/webxr/#create-a-reference-space
|
|
|
|
// XXXManishearth reject based on session type
|
|
// https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines
|
|
|
|
if !self.is_immersive() &&
|
|
(ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded)
|
|
{
|
|
p.reject_error(Error::NotSupported);
|
|
return p;
|
|
}
|
|
|
|
match ty {
|
|
XRReferenceSpaceType::Unbounded => {
|
|
// XXXmsub2 figure out how to support this
|
|
p.reject_error(Error::NotSupported)
|
|
},
|
|
ty => {
|
|
if ty != XRReferenceSpaceType::Viewer &&
|
|
(!self.is_immersive() || ty != XRReferenceSpaceType::Local)
|
|
{
|
|
let s = ty.as_str();
|
|
if !self
|
|
.session
|
|
.borrow()
|
|
.granted_features()
|
|
.iter()
|
|
.any(|f| *f == s)
|
|
{
|
|
p.reject_error(Error::NotSupported);
|
|
return p;
|
|
}
|
|
}
|
|
if ty == XRReferenceSpaceType::Bounded_floor {
|
|
let space = XRBoundedReferenceSpace::new(&self.global(), self);
|
|
p.resolve_native(&space);
|
|
} else {
|
|
let space = XRReferenceSpace::new(&self.global(), self, ty);
|
|
p.resolve_native(&space);
|
|
}
|
|
},
|
|
}
|
|
p
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-inputsources>
|
|
fn InputSources(&self) -> DomRoot<XRInputSourceArray> {
|
|
DomRoot::from_ref(&*self.input_sources)
|
|
}
|
|
|
|
/// <https://immersive-web.github.io/webxr/#dom-xrsession-end>
|
|
fn End(&self) -> Rc<Promise> {
|
|
let global = self.global();
|
|
let p = Promise::new(&global);
|
|
if self.ended.get() && self.end_promises.borrow().is_empty() {
|
|
// If the session has completely ended and all end promises have been resolved,
|
|
// don't queue up more end promises
|
|
//
|
|
// We need to check for end_promises being empty because `ended` is set
|
|
// before everything has been completely shut down, and we do not want to
|
|
// prematurely resolve the promise then
|
|
//
|
|
// However, if end_promises is empty, then all end() promises have already resolved,
|
|
// so the session has completely shut down and we should not queue up more promises
|
|
p.resolve_native(&());
|
|
return p;
|
|
}
|
|
self.end_promises.borrow_mut().push(p.clone());
|
|
// This is duplicated in event_callback since this should
|
|
// happen ASAP for end() but can happen later if the device
|
|
// shuts itself down
|
|
self.ended.set(true);
|
|
global.as_window().Navigator().Xr().end_session(self);
|
|
self.session.borrow_mut().end_session();
|
|
p
|
|
}
|
|
|
|
// https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource
|
|
fn RequestHitTestSource(&self, options: &XRHitTestOptionsInit) -> Rc<Promise> {
|
|
let p = Promise::new(&self.global());
|
|
|
|
if !self
|
|
.session
|
|
.borrow()
|
|
.granted_features()
|
|
.iter()
|
|
.any(|f| f == "hit-test")
|
|
{
|
|
p.reject_error(Error::NotSupported);
|
|
return p;
|
|
}
|
|
|
|
let id = self.next_hit_test_id.get();
|
|
self.next_hit_test_id.set(HitTestId(id.0 + 1));
|
|
|
|
let space = options.space.space();
|
|
let ray = if let Some(ref ray) = options.offsetRay {
|
|
ray.ray()
|
|
} else {
|
|
Ray {
|
|
origin: Vector3D::new(0., 0., 0.),
|
|
direction: Vector3D::new(0., 0., -1.),
|
|
}
|
|
};
|
|
|
|
let mut types = EntityTypes::default();
|
|
|
|
if let Some(ref tys) = options.entityTypes {
|
|
for ty in tys {
|
|
match ty {
|
|
XRHitTestTrackableType::Point => types.point = true,
|
|
XRHitTestTrackableType::Plane => types.plane = true,
|
|
XRHitTestTrackableType::Mesh => types.mesh = true,
|
|
}
|
|
}
|
|
} else {
|
|
types.plane = true;
|
|
}
|
|
|
|
let source = HitTestSource {
|
|
id,
|
|
space,
|
|
ray,
|
|
types,
|
|
};
|
|
self.pending_hit_test_promises
|
|
.borrow_mut()
|
|
.insert(id, p.clone());
|
|
|
|
self.session.borrow().request_hit_test(source);
|
|
|
|
p
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrsession-interactionmode>
|
|
fn InteractionMode(&self) -> XRInteractionMode {
|
|
// Until Servo supports WebXR sessions on mobile phones or similar non-XR devices,
|
|
// this should always be world space
|
|
XRInteractionMode::World_space
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#dom-xrsession-framerate>
|
|
fn GetFrameRate(&self) -> Option<Finite<f32>> {
|
|
let session = self.session.borrow();
|
|
if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
|
|
None
|
|
} else {
|
|
Finite::new(self.framerate.get())
|
|
}
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#dom-xrsession-supportedframerates>
|
|
fn GetSupportedFrameRates(&self, cx: JSContext) -> Option<Float32Array> {
|
|
let session = self.session.borrow();
|
|
if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
|
|
None
|
|
} else {
|
|
let framerates = session.supported_frame_rates();
|
|
rooted!(in (*cx) let mut array = ptr::null_mut::<JSObject>());
|
|
Some(
|
|
create_buffer_source(cx, framerates, array.handle_mut())
|
|
.expect("Failed to construct supported frame rates array"),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures>
|
|
fn EnabledFeatures(&self, cx: JSContext) -> JSVal {
|
|
let session = self.session.borrow();
|
|
let features = session.granted_features();
|
|
to_frozen_array(features, cx)
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#dom-xrsession-issystemkeyboardsupported>
|
|
fn IsSystemKeyboardSupported(&self) -> bool {
|
|
// Support for this only exists on Meta headsets (no desktop support)
|
|
// so this will always be false until that changes
|
|
false
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webxr/#dom-xrsession-updatetargetframerate>
|
|
fn UpdateTargetFrameRate(&self, rate: Finite<f32>, comp: InRealm) -> Rc<Promise> {
|
|
let mut session = self.session.borrow_mut();
|
|
let supported_frame_rates = session.supported_frame_rates();
|
|
let promise = Promise::new_in_current_realm(comp);
|
|
|
|
if self.mode == XRSessionMode::Inline ||
|
|
supported_frame_rates.is_empty() ||
|
|
self.ended.get()
|
|
{
|
|
promise.reject_error(Error::InvalidState);
|
|
return promise;
|
|
}
|
|
|
|
if !supported_frame_rates.contains(&*rate) {
|
|
promise.reject_error(Error::Type("Provided framerate not supported".into()));
|
|
return promise;
|
|
}
|
|
|
|
*self.update_framerate_promise.borrow_mut() = Some(promise.clone());
|
|
|
|
let this = Trusted::new(self);
|
|
let global = self.global();
|
|
let window = global.as_window();
|
|
let (task_source, canceller) = window
|
|
.task_manager()
|
|
.dom_manipulation_task_source_with_canceller();
|
|
let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
|
|
|
|
ROUTER.add_route(
|
|
receiver.to_opaque(),
|
|
Box::new(move |message| {
|
|
let this = this.clone();
|
|
let _ = task_source.queue_with_canceller(
|
|
task!(update_session_framerate: move || {
|
|
let session = this.root();
|
|
session.apply_nominal_framerate(message.to().unwrap());
|
|
if let Some(promise) = session.update_framerate_promise.borrow_mut().take() {
|
|
promise.resolve_native(&());
|
|
};
|
|
}),
|
|
&canceller,
|
|
);
|
|
}),
|
|
);
|
|
|
|
session.update_frame_rate(*rate, sender);
|
|
|
|
promise
|
|
}
|
|
}
|
|
|
|
// The pose of an object in native-space. Should never be exposed.
|
|
pub type ApiPose = RigidTransform3D<f32, ApiSpace, webxr_api::Native>;
|
|
// A transform between objects in some API-space
|
|
pub type ApiRigidTransform = RigidTransform3D<f32, ApiSpace, ApiSpace>;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct BaseSpace;
|
|
|
|
pub type BaseTransform = RigidTransform3D<f32, webxr_api::Native, BaseSpace>;
|
|
|
|
#[allow(unsafe_code)]
|
|
pub fn cast_transform<T, U, V, W>(
|
|
transform: RigidTransform3D<f32, T, U>,
|
|
) -> RigidTransform3D<f32, V, W> {
|
|
unsafe { mem::transmute(transform) }
|
|
}
|
|
|
|
impl From<EnvironmentBlendMode> for XREnvironmentBlendMode {
|
|
fn from(x: EnvironmentBlendMode) -> Self {
|
|
match x {
|
|
EnvironmentBlendMode::Opaque => XREnvironmentBlendMode::Opaque,
|
|
EnvironmentBlendMode::AlphaBlend => XREnvironmentBlendMode::Alpha_blend,
|
|
EnvironmentBlendMode::Additive => XREnvironmentBlendMode::Additive,
|
|
}
|
|
}
|
|
}
|