diff --git a/.gitignore b/.gitignore index e0ba890b..7cab1323 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ *~ target Cargo.lock + +.idea +.vscode +.gradle/ +build/ +*.iml +local.properties diff --git a/.travis.yml b/.travis.yml index d1074bbd..b43b0bbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,10 @@ before_script: script: - cargo fmt --all -- --check - cd webxr - - cargo build --features=glwindow,headless - - cargo build --features=ipc,glwindow,headless + - cargo build --features=glwindow,headless,googlevr + - cargo build --features=ipc,glwindow,headless,googlevr + - rustup target add arm-linux-androideabi + - cargo build --target arm-linux-androideabi --features=ipc,googlevr notifications: webhooks: http://build.servo.org:54856/travis diff --git a/webxr-api/error.rs b/webxr-api/error.rs index dee06b0e..ec0de677 100644 --- a/webxr-api/error.rs +++ b/webxr-api/error.rs @@ -14,4 +14,5 @@ use serde::{Deserialize, Serialize}; pub enum Error { NoMatchingDevice, CommunicationError, + BackendSpecific(String), } diff --git a/webxr/Cargo.toml b/webxr/Cargo.toml index 23c320bd..0291cdb7 100644 --- a/webxr/Cargo.toml +++ b/webxr/Cargo.toml @@ -22,10 +22,19 @@ path = "lib.rs" glwindow = ["glutin"] headless = [] ipc = ["webxr-api/ipc"] +googlevr = ["gvr-sys", "android_injected_glue"] + [dependencies] webxr-api = { path = "../webxr-api" } euclid = "0.20" gleam = "0.6" glutin = { version = "0.21", optional = true } -log = "0.4" +log = "0.4.6" +gvr-sys = { version = "0.7", optional = true } + +[target.'cfg(target_os = "android")'.dependencies] +android_injected_glue = { version = "0.2.2", optional = true } + +[build-dependencies] +gl_generator = "0.11" diff --git a/webxr/build.rs b/webxr/build.rs new file mode 100644 index 00000000..9cf2184f --- /dev/null +++ b/webxr/build.rs @@ -0,0 +1,28 @@ +use gl_generator::{Api, Fallbacks, Profile, Registry}; +use std::env; +use std::fs::{self, File}; +use std::path::Path; + +fn main() { + // Copy AARs + if let Ok(aar_out_dir) = env::var("AAR_OUT_DIR") { + fs::copy( + &Path::new("googlevr/aar/GVRService.aar"), + &Path::new(&aar_out_dir).join("GVRService.aar"), + ) + .unwrap(); + } + + if !cfg!(feature = "googlevr") { + return; + } + + let out_dir = env::var("OUT_DIR").unwrap(); + + // GLES 2.0 bindings + let mut file = File::create(&Path::new(&out_dir).join("gles_bindings.rs")).unwrap(); + let gles_reg = Registry::new(Api::Gles2, (3, 0), Profile::Core, Fallbacks::All, []); + gles_reg + .write_bindings(gl_generator::StaticGenerator, &mut file) + .unwrap(); +} diff --git a/webxr/googlevr/aar/GVRService.aar b/webxr/googlevr/aar/GVRService.aar new file mode 100644 index 00000000..b6c4fd8b Binary files /dev/null and b/webxr/googlevr/aar/GVRService.aar differ diff --git a/webxr/googlevr/device.rs b/webxr/googlevr/device.rs new file mode 100644 index 00000000..91eea43c --- /dev/null +++ b/webxr/googlevr/device.rs @@ -0,0 +1,668 @@ +use gleam::gl::GLsync; + +use webxr_api::Device; +use webxr_api::Error; +use webxr_api::Event; +use webxr_api::EventBuffer; +use webxr_api::Floor; +use webxr_api::Frame; +use webxr_api::InputFrame; +use webxr_api::InputId; +use webxr_api::InputSource; +use webxr_api::LeftEye; +use webxr_api::Native; +use webxr_api::Quitter; +use webxr_api::RightEye; +use webxr_api::Sender; +use webxr_api::TargetRayMode; +use webxr_api::View; +use webxr_api::Viewer; +use webxr_api::Views; + +use crate::gl; + +use euclid::default::Size2D as DefaultSize2D; +use euclid::Point2D; +use euclid::Rect; +use euclid::RigidTransform3D; +use euclid::Rotation3D; +use euclid::Size2D; +use euclid::Transform3D; +use euclid::Vector3D; + +use gvr_sys as gvr; +use gvr_sys::gvr_color_format_type::*; +use gvr_sys::gvr_depth_stencil_format_type::*; +use gvr_sys::gvr_feature::*; +use std::{mem, ptr}; + +use super::discovery::SendPtr; +use super::input::GoogleVRController; + +#[cfg(target_os = "android")] +use crate::jni_utils::JNIScope; +#[cfg(target_os = "android")] +use android_injected_glue::ffi as ndk; + +// 50ms is a good estimate recommended by the GVR Team. +// It takes in account the time between frame submission (without vsync) and +// when the rendered image is sent to the physical pixels on the display. +const PREDICTION_OFFSET_NANOS: i64 = 50000000; // 50ms + +pub(crate) struct GoogleVRDevice { + events: EventBuffer, + multiview: bool, + multisampling: bool, + depth: bool, + left_view: View, + right_view: View, + near: f32, + far: f32, + input: Option, + + #[cfg(target_os = "android")] + java_class: ndk::jclass, + #[cfg(target_os = "android")] + java_object: ndk::jobject, + ctx: *mut gvr::gvr_context, + controller_ctx: *mut gvr::gvr_controller_context, + viewport_list: *mut gvr::gvr_buffer_viewport_list, + left_eye_vp: *mut gvr::gvr_buffer_viewport, + right_eye_vp: *mut gvr::gvr_buffer_viewport, + render_size: gvr::gvr_sizei, + swap_chain: *mut gvr::gvr_swap_chain, + frame: *mut gvr::gvr_frame, + synced_head_matrix: gvr::gvr_mat4f, + fbo_id: u32, + fbo_texture: u32, + presenting: bool, + frame_bound: bool, +} + +fn empty_view() -> View { + View { + transform: RigidTransform3D::identity(), + projection: Transform3D::identity(), + viewport: Default::default(), + } +} + +impl GoogleVRDevice { + #[cfg(target_os = "android")] + pub fn new( + ctx: SendPtr<*mut gvr::gvr_context>, + controller_ctx: SendPtr<*mut gvr::gvr_controller_context>, + java_class: SendPtr, + java_object: SendPtr, + ) -> Result { + let mut device = GoogleVRDevice { + events: Default::default(), + multiview: false, + multisampling: false, + depth: false, + left_view: empty_view(), + right_view: empty_view(), + // https://github.com/servo/webxr/issues/32 + near: 0.1, + far: 1000.0, + input: None, + + ctx: ctx.get(), + controller_ctx: controller_ctx.get(), + java_class: java_class.get(), + java_object: java_object.get(), + viewport_list: ptr::null_mut(), + left_eye_vp: ptr::null_mut(), + right_eye_vp: ptr::null_mut(), + render_size: gvr::gvr_sizei { + width: 0, + height: 0, + }, + swap_chain: ptr::null_mut(), + frame: ptr::null_mut(), + synced_head_matrix: gvr_identity_matrix(), + fbo_id: 0, + fbo_texture: 0, + presenting: false, + frame_bound: false, + }; + unsafe { + device.init(); + } + // XXXManishearth figure out how to block until presentation + // starts + device.start_present(); + device.initialize_views(); + Ok(device) + } + + #[cfg(not(target_os = "android"))] + pub fn new( + ctx: SendPtr<*mut gvr::gvr_context>, + controller_ctx: SendPtr<*mut gvr::gvr_controller_context>, + ) -> Result { + let mut device = GoogleVRDevice { + events: Default::default(), + multiview: false, + multisampling: false, + depth: false, + left_view: empty_view(), + right_view: empty_view(), + // https://github.com/servo/webxr/issues/32 + near: 0.1, + far: 1000.0, + input: None, + + ctx: ctx.get(), + controller_ctx: controller_ctx.get(), + viewport_list: ptr::null_mut(), + left_eye_vp: ptr::null_mut(), + right_eye_vp: ptr::null_mut(), + render_size: gvr::gvr_sizei { + width: 0, + height: 0, + }, + swap_chain: ptr::null_mut(), + frame: ptr::null_mut(), + synced_head_matrix: gvr_identity_matrix(), + fbo_id: 0, + fbo_texture: 0, + presenting: false, + frame_bound: false, + }; + unsafe { + device.init(); + } + // XXXManishearth figure out how to block until presentation + // starts + device.start_present(); + device.initialize_views(); + Ok(device) + } + + unsafe fn init(&mut self) { + let list = gvr::gvr_buffer_viewport_list_create(self.ctx); + + // gvr_refresh_viewer_profile must be called before getting recommended bufer viewports. + gvr::gvr_refresh_viewer_profile(self.ctx); + + // Gets the recommended buffer viewport configuration, populating a previously + // allocated gvr_buffer_viewport_list object. The updated values include the + // per-eye recommended viewport and field of view for the target. + gvr::gvr_get_recommended_buffer_viewports(self.ctx, list); + + // Create viewport buffers for both eyes. + self.left_eye_vp = gvr::gvr_buffer_viewport_create(self.ctx); + gvr::gvr_buffer_viewport_list_get_item( + list, + gvr::gvr_eye::GVR_LEFT_EYE as usize, + self.left_eye_vp, + ); + self.right_eye_vp = gvr::gvr_buffer_viewport_create(self.ctx); + gvr::gvr_buffer_viewport_list_get_item( + list, + gvr::gvr_eye::GVR_RIGHT_EYE as usize, + self.right_eye_vp, + ); + + if let Ok(input) = GoogleVRController::new(self.ctx, self.controller_ctx) { + self.input = Some(input); + } + } + + unsafe fn initialize_gl(&mut self) { + // Note: In some scenarios gvr_initialize_gl crashes if gvr_refresh_viewer_profile call isn't called before. + gvr::gvr_refresh_viewer_profile(self.ctx); + // Initializes gvr necessary GL-related objects. + gvr::gvr_initialize_gl(self.ctx); + + // GVR_FEATURE_MULTIVIEW must be checked after gvr_initialize_gl is called or the function will crash. + if self.multiview && !gvr::gvr_is_feature_supported(self.ctx, GVR_FEATURE_MULTIVIEW as i32) + { + self.multiview = false; + warn!("Multiview not supported. Fallback to standar framebuffer.") + } + + // Create a framebuffer required to attach and + // blit the external texture into the main gvr pixel buffer. + gl::GenFramebuffers(1, &mut self.fbo_id); + + // Initialize gvr swap chain + let spec = gvr::gvr_buffer_spec_create(self.ctx); + self.render_size = self.recommended_render_size(); + + if self.multiview { + // Multiview requires half size because the buffer is a texture array with 2 half width layers. + gvr::gvr_buffer_spec_set_multiview_layers(spec, 2); + gvr::gvr_buffer_spec_set_size( + spec, + gvr::gvr_sizei { + width: self.render_size.width / 2, + height: self.render_size.height, + }, + ); + } else { + gvr::gvr_buffer_spec_set_size(spec, self.render_size); + } + + if self.multisampling { + gvr::gvr_buffer_spec_set_samples(spec, 2); + } else { + gvr::gvr_buffer_spec_set_samples(spec, 0); + } + gvr::gvr_buffer_spec_set_color_format(spec, GVR_COLOR_FORMAT_RGBA_8888 as i32); + + if self.depth { + gvr::gvr_buffer_spec_set_depth_stencil_format( + spec, + GVR_DEPTH_STENCIL_FORMAT_DEPTH_16 as i32, + ); + } else { + gvr::gvr_buffer_spec_set_depth_stencil_format( + spec, + GVR_DEPTH_STENCIL_FORMAT_NONE as i32, + ); + } + + self.swap_chain = gvr::gvr_swap_chain_create(self.ctx, mem::transmute(&spec), 1); + gvr::gvr_buffer_spec_destroy(mem::transmute(&spec)); + } + + fn recommended_render_size(&self) -> gvr::gvr_sizei { + // GVR SDK states that thee maximum effective render target size can be very large. + // Most applications need to scale down to compensate. + // Half pixel sizes are used by scaling each dimension by sqrt(2)/2 ~= 7/10ths. + let render_target_size = + unsafe { gvr::gvr_get_maximum_effective_render_target_size(self.ctx) }; + gvr::gvr_sizei { + width: (7 * render_target_size.width) / 10, + height: (7 * render_target_size.height) / 10, + } + } + + #[cfg(target_os = "android")] + fn start_present(&mut self) { + if self.presenting { + return; + } + self.presenting = true; + unsafe { + if let Ok(jni_scope) = JNIScope::attach() { + let jni = jni_scope.jni(); + let env = jni_scope.env; + let method = jni_scope.get_method(self.java_class, "startPresent", "()V", false); + (jni.CallVoidMethod)(env, self.java_object, method); + } + } + + if self.swap_chain.is_null() { + unsafe { + self.initialize_gl(); + debug_assert!(!self.swap_chain.is_null()); + } + } + } + + #[cfg(not(target_os = "android"))] + fn start_present(&mut self) { + if self.presenting { + return; + } + self.presenting = true; + if self.swap_chain.is_null() { + unsafe { + self.initialize_gl(); + debug_assert!(!self.swap_chain.is_null()); + } + } + } + + // Hint to indicate that we are going to stop sending frames to the device + #[cfg(target_os = "android")] + fn stop_present(&mut self) { + if !self.presenting { + return; + } + self.presenting = false; + unsafe { + if let Ok(jni_scope) = JNIScope::attach() { + let jni = jni_scope.jni(); + let env = jni_scope.env; + let method = jni_scope.get_method(self.java_class, "stopPresent", "()V", false); + (jni.CallVoidMethod)(env, self.java_object, method); + } + } + } + + #[cfg(not(target_os = "android"))] + fn stop_present(&mut self) { + self.presenting = false; + } + + fn initialize_views(&mut self) { + unsafe { + self.left_view = self.fetch_eye(gvr::gvr_eye::GVR_LEFT_EYE, self.left_eye_vp); + self.right_view = self.fetch_eye(gvr::gvr_eye::GVR_RIGHT_EYE, self.right_eye_vp); + } + } + + unsafe fn fetch_eye(&self, eye: gvr::gvr_eye, vp: *mut gvr::gvr_buffer_viewport) -> View { + let eye_fov = gvr::gvr_buffer_viewport_get_source_fov(vp); + let projection = fov_to_projection_matrix(&eye_fov, self.near, self.far); + + // this matrix converts from head space to eye space, + // i.e. it's the inverse of the offset + let eye_mat = gvr::gvr_get_eye_from_head_matrix(self.ctx, eye as i32); + // XXXManishearth we should decompose the matrix properly instead of assuming it's + // only translation + let transform = decompose_rigid(&eye_mat).inverse(); + + let size = Size2D::new(self.render_size.width / 2, self.render_size.height); + let origin = if eye == gvr::gvr_eye::GVR_LEFT_EYE { + Point2D::origin() + } else { + Point2D::new(self.render_size.width / 2, 0) + }; + let viewport = Rect::new(origin, size); + + View { + projection, + transform, + viewport, + } + } + + fn bind_framebuffer(&mut self) { + // No op + if self.frame.is_null() { + warn!("null frame with context"); + return; + } + + unsafe { + if self.frame_bound { + // Required to avoid some warnings from the GVR SDK. + // It doesn't like binding the same framebuffer multiple times. + gvr::gvr_frame_unbind(self.frame); + } + // gvr_frame_bind_buffer may make the current active texture unit dirty + let mut active_unit = 0; + gl::GetIntegerv(gl::ACTIVE_TEXTURE, &mut active_unit); + + // Bind daydream FBO + gvr::gvr_frame_bind_buffer(self.frame, 0); + self.frame_bound = true; + + // Restore texture unit + gl::ActiveTexture(active_unit as u32); + } + } + + fn update_recommended_buffer_viewports(&self) { + unsafe { + gvr::gvr_get_recommended_buffer_viewports(self.ctx, self.viewport_list); + if self.multiview { + // gvr_get_recommended_buffer_viewports function assumes that the client is not + // using multiview to render to multiple layers simultaneously. + // The uv and source layers need to be updated for multiview. + let fullscreen_uv = gvr_texture_bounds(&[0.0, 0.0, 1.0, 1.0]); + // Left eye + gvr::gvr_buffer_viewport_set_source_uv(self.left_eye_vp, fullscreen_uv); + gvr::gvr_buffer_viewport_set_source_layer(self.left_eye_vp, 0); + // Right eye + gvr::gvr_buffer_viewport_set_source_uv(self.right_eye_vp, fullscreen_uv); + gvr::gvr_buffer_viewport_set_source_layer(self.right_eye_vp, 1); + // Update viewport list + gvr::gvr_buffer_viewport_list_set_item(self.viewport_list, 0, self.left_eye_vp); + gvr::gvr_buffer_viewport_list_set_item(self.viewport_list, 1, self.right_eye_vp); + } + } + } + + fn fetch_head_matrix(&mut self) -> RigidTransform3D { + let mut next_vsync = unsafe { gvr::gvr_get_time_point_now() }; + next_vsync.monotonic_system_time_nanos += PREDICTION_OFFSET_NANOS; + unsafe { + let m = gvr::gvr_get_head_space_from_start_space_rotation(self.ctx, next_vsync); + self.synced_head_matrix = gvr::gvr_apply_neck_model(self.ctx, m, 1.0); + }; + decompose_rigid(&self.synced_head_matrix) + } + + unsafe fn acquire_frame(&mut self) { + if !self.frame.is_null() { + warn!("frame not submitted"); + // Release acquired frame if the user has not called submit_Frame() + gvr::gvr_frame_submit( + mem::transmute(&self.frame), + self.viewport_list, + self.synced_head_matrix, + ); + } + + self.update_recommended_buffer_viewports(); + // Handle resize + let size = self.recommended_render_size(); + if size.width != self.render_size.width || size.height != self.render_size.height { + gvr::gvr_swap_chain_resize_buffer(self.swap_chain, 0, size); + self.render_size = size; + } + + self.frame = gvr::gvr_swap_chain_acquire_frame(self.swap_chain); + } + + fn render_layer(&mut self, texture_id: u32, texture_size: DefaultSize2D) { + if self.frame.is_null() { + warn!("null frame when calling render_layer"); + return; + } + debug_assert!(self.fbo_id > 0); + + unsafe { + // Save current fbo to restore it when the frame is submitted. + let mut current_fbo = 0; + gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut current_fbo); + + if self.fbo_texture != texture_id { + // Attach external texture to the used later in BlitFramebuffer. + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + gl::FramebufferTexture2D( + gl::FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gl::TEXTURE_2D, + texture_id, + 0, + ); + self.fbo_texture = texture_id; + } + + // BlitFramebuffer: external texture to gvr pixel buffer + self.bind_framebuffer(); + gl::BindFramebuffer(gl::READ_FRAMEBUFFER, self.fbo_id); + gl::BlitFramebuffer( + 0, + 0, + texture_size.width, + texture_size.height, + 0, + 0, + self.render_size.width, + self.render_size.height, + gl::COLOR_BUFFER_BIT, + gl::LINEAR, + ); + gvr::gvr_frame_unbind(self.frame); + self.frame_bound = false; + // Restore bound fbo + gl::BindFramebuffer(gl::FRAMEBUFFER, current_fbo as u32); + + // set up uvs + // XXXManishearth do we need to negotiate size here? + // gvr::gvr_buffer_viewport_set_source_uv(self.left_eye_vp, gvr_texture_bounds(&layer.left_bounds)); + // gvr::gvr_buffer_viewport_set_source_uv(self.right_eye_vp, gvr_texture_bounds(&layer.right_bounds)); + } + } + + fn submit_frame(&mut self) { + if self.frame.is_null() { + warn!("null frame with context"); + return; + } + + unsafe { + if self.frame_bound { + gvr::gvr_frame_unbind(self.frame); + self.frame_bound = false; + } + // submit frame + gvr::gvr_frame_submit( + mem::transmute(&self.frame), + self.viewport_list, + self.synced_head_matrix, + ); + } + } + + fn input_state(&self) -> Vec { + if let Some(ref i) = self.input { + vec![InputFrame { + target_ray_origin: i.state(), + id: InputId(0), + }] + } else { + vec![] + } + } +} + +impl Device for GoogleVRDevice { + fn floor_transform(&self) -> RigidTransform3D { + // GoogleVR doesn't know about the floor + // XXXManishearth perhaps we should report a guesstimate value here + RigidTransform3D::identity() + } + + fn views(&self) -> Views { + Views::Stereo(self.left_view.clone(), self.right_view.clone()) + } + + fn wait_for_animation_frame(&mut self) -> Frame { + unsafe { + self.acquire_frame(); + } + // Predict head matrix + Frame { + transform: self.fetch_head_matrix(), + inputs: self.input_state(), + } + } + + fn render_animation_frame( + &mut self, + texture_id: u32, + texture_size: DefaultSize2D, + sync: Option, + ) { + self.render_layer(texture_id, texture_size); + self.submit_frame(); + if let Some(sync) = sync { + unsafe { + // XXXManishearth we really should figure out how + // to use gleam here + gl::WaitSync(sync as *const _, 0, gl::TIMEOUT_IGNORED); + } + } + } + + fn initial_inputs(&self) -> Vec { + if let Some(ref i) = self.input { + vec![InputSource { + handedness: i.handedness(), + id: InputId(0), + target_ray_mode: TargetRayMode::TrackedPointer, + }] + } else { + vec![] + } + } + + fn set_event_dest(&mut self, dest: Sender) { + self.events.upgrade(dest); + } + + fn quit(&mut self) { + self.stop_present(); + self.events.callback(Event::SessionEnd); + } + + fn set_quitter(&mut self, _: Quitter) { + // do nothing for now until we need the quitter + } +} + +#[inline] +fn fov_to_projection_matrix( + fov: &gvr::gvr_rectf, + near: f32, + far: f32, +) -> Transform3D { + let left = -fov.left.to_radians().tan() * near; + let right = fov.right.to_radians().tan() * near; + let top = fov.top.to_radians().tan() * near; + let bottom = -fov.bottom.to_radians().tan() * near; + Transform3D::ortho(left, right, bottom, top, near, far) +} + +#[inline] +fn gvr_texture_bounds(array: &[f32; 4]) -> gvr::gvr_rectf { + gvr::gvr_rectf { + left: array[0], + right: array[0] + array[2], + bottom: array[1], + top: array[1] + array[3], + } +} + +#[inline] +fn gvr_identity_matrix() -> gvr::gvr_mat4f { + gvr::gvr_mat4f { + m: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + } +} + +fn decompose_rotation(mat: &gvr::gvr_mat4f) -> Rotation3D { + // https://math.stackexchange.com/a/3183435/24293 + let m = &mat.m; + if m[2][2] < 0. { + if m[0][0] > m[1][1] { + let t = 1. + m[0][0] - m[1][1] - m[2][2]; + Rotation3D::unit_quaternion(t, m[0][1] + m[1][0], m[2][0] + m[0][2], m[1][2] - m[2][1]) + } else { + let t = 1. - m[0][0] + m[1][1] - m[2][2]; + Rotation3D::unit_quaternion(m[0][1] + m[1][0], t, m[1][2] + m[2][1], m[2][0] - m[0][2]) + } + } else { + if m[0][0] < -m[1][1] { + let t = 1. - m[0][0] - m[1][1] + m[2][2]; + Rotation3D::unit_quaternion(m[2][0] + m[0][2], m[1][2] + m[2][1], t, m[0][1] - m[1][0]) + } else { + let t = 1. + m[0][0] + m[1][1] + m[2][2]; + Rotation3D::unit_quaternion(m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0], t) + } + } +} + +fn decompose_translation(mat: &gvr::gvr_mat4f) -> Vector3D { + Vector3D::new(mat.m[0][3], mat.m[1][3], mat.m[2][3]) +} + +fn decompose_rigid(mat: &gvr::gvr_mat4f) -> RigidTransform3D { + // Rigid transform matrices formed by applying a rotation first and then a translation + // decompose cleanly based on their rotation and translation components. + RigidTransform3D::new(decompose_rotation(mat), decompose_translation(mat)) +} diff --git a/webxr/googlevr/discovery.rs b/webxr/googlevr/discovery.rs new file mode 100644 index 00000000..77f178af --- /dev/null +++ b/webxr/googlevr/discovery.rs @@ -0,0 +1,217 @@ +use webxr_api::Discovery; +use webxr_api::Error; +use webxr_api::Session; +use webxr_api::SessionBuilder; +use webxr_api::SessionMode; + +use super::device::GoogleVRDevice; + +#[cfg(target_os = "android")] +use crate::jni_utils::JNIScope; +#[cfg(target_os = "android")] +use android_injected_glue::ffi as ndk; +use gvr_sys as gvr; +use std::ptr; + +#[cfg(target_os = "android")] +const SERVICE_CLASS_NAME: &'static str = "com/rust/webvr/GVRService"; + +/// Quick way to make Sendable pointers +#[derive(Copy, Clone)] +pub(crate) struct SendPtr(T); + +unsafe impl Send for SendPtr {} + +impl SendPtr<*mut T> { + pub unsafe fn new(ptr: *mut T) -> Self { + SendPtr(ptr) + } + + pub fn get(self) -> *mut T { + self.0 + } +} + +pub struct GoogleVRDiscovery { + ctx: *mut gvr::gvr_context, + controller_ctx: *mut gvr::gvr_controller_context, + #[cfg(target_os = "android")] + java_object: ndk::jobject, + #[cfg(target_os = "android")] + java_class: ndk::jclass, +} + +impl GoogleVRDiscovery { + pub fn new() -> Result { + let mut this = Self::new_uninit(); + unsafe { + this.create_context().map_err(Error::BackendSpecific)?; + } + if this.ctx.is_null() { + return Err(Error::BackendSpecific( + "GoogleVR SDK failed to initialize".into(), + )); + } + unsafe { + this.create_controller_context(); + } + Ok(this) + } +} + +impl Discovery for GoogleVRDiscovery { + #[cfg(target_os = "android")] + fn request_session(&mut self, mode: SessionMode, xr: SessionBuilder) -> Result { + let (ctx, controller_ctx, java_class, java_object); + unsafe { + ctx = SendPtr::new(self.ctx); + controller_ctx = SendPtr::new(self.controller_ctx); + java_class = SendPtr::new(self.java_class); + java_object = SendPtr::new(self.java_object); + } + if self.supports_session(mode) { + xr.spawn(move || GoogleVRDevice::new(ctx, controller_ctx, java_class, java_object)) + } else { + Err(Error::NoMatchingDevice) + } + } + + #[cfg(not(target_os = "android"))] + fn request_session(&mut self, mode: SessionMode, xr: SessionBuilder) -> Result { + let (ctx, controller_ctx); + unsafe { + ctx = SendPtr::new(self.ctx); + controller_ctx = SendPtr::new(self.controller_ctx); + } + if self.supports_session(mode) { + xr.spawn(move || GoogleVRDevice::new(ctx, controller_ctx)) + } else { + Err(Error::NoMatchingDevice) + } + } + + fn supports_session(&self, mode: SessionMode) -> bool { + mode == SessionMode::ImmersiveVR + } +} + +impl GoogleVRDiscovery { + #[cfg(target_os = "android")] + pub fn new_uninit() -> Self { + Self { + ctx: ptr::null_mut(), + controller_ctx: ptr::null_mut(), + java_object: ptr::null_mut(), + java_class: ptr::null_mut(), + } + } + + #[cfg(not(target_os = "android"))] + pub fn new_uninit() -> Self { + Self { + ctx: ptr::null_mut(), + controller_ctx: ptr::null_mut(), + } + } + + // On Android, the gvr_context must be be obtained from + // the Java GvrLayout object via GvrLayout.getGvrApi().getNativeGvrContext() + // Java code is implemented in GVRService. It handles the life cycle of the GvrLayout. + // JNI code is used to comunicate with that Java code. + #[cfg(target_os = "android")] + unsafe fn create_context(&mut self) -> Result<(), String> { + use std::mem; + + let jni_scope = JNIScope::attach()?; + + let jni = jni_scope.jni(); + let env = jni_scope.env; + + // Use NativeActivity's classloader to find our class + self.java_class = jni_scope.find_class(SERVICE_CLASS_NAME)?; + if self.java_class.is_null() { + return Err("Didn't find GVRService class".into()); + }; + self.java_class = (jni.NewGlobalRef)(env, self.java_class); + + // Create GVRService instance and own it as a globalRef. + let method = jni_scope.get_method( + self.java_class, + "create", + "(Landroid/app/Activity;J)Ljava/lang/Object;", + true, + ); + let thiz: usize = mem::transmute(self as *mut GoogleVRDiscovery); + self.java_object = (jni.CallStaticObjectMethod)( + env, + self.java_class, + method, + jni_scope.activity, + thiz as ndk::jlong, + ); + if self.java_object.is_null() { + return Err("Failed to create GVRService instance".into()); + }; + self.java_object = (jni.NewGlobalRef)(env, self.java_object); + + // Finally we have everything required to get the gvr_context pointer from java :) + let method = jni_scope.get_method(self.java_class, "getNativeContext", "()J", false); + let pointer = (jni.CallLongMethod)(env, self.java_object, method); + self.ctx = pointer as *mut gvr::gvr_context; + if self.ctx.is_null() { + return Err("Failed to getNativeGvrContext from java GvrLayout".into()); + } + + Ok(()) + } + + #[cfg(not(target_os = "android"))] + unsafe fn create_context(&mut self) -> Result<(), String> { + self.ctx = gvr::gvr_create(); + Ok(()) + } + + unsafe fn create_controller_context(&mut self) { + let options = gvr::gvr_controller_get_default_options(); + self.controller_ctx = gvr::gvr_controller_create_and_init(options, self.ctx); + gvr::gvr_controller_resume(self.controller_ctx); + } + + pub fn on_pause(&self) { + warn!("focus/blur not yet supported") + } + + pub fn on_resume(&self) { + warn!("focus/blur not yet supported") + } +} + +#[cfg(target_os = "android")] +#[no_mangle] +#[allow(non_snake_case)] +#[allow(dead_code)] +pub extern "C" fn Java_com_rust_webvr_GVRService_nativeOnPause( + _: *mut ndk::JNIEnv, + service: ndk::jlong, +) { + use std::mem; + unsafe { + let service: *mut GoogleVRDiscovery = mem::transmute(service as usize); + (*service).on_pause(); + } +} + +#[cfg(target_os = "android")] +#[no_mangle] +#[allow(non_snake_case)] +#[allow(dead_code)] +pub extern "C" fn Java_com_rust_webvr_GVRService_nativeOnResume( + _: *mut ndk::JNIEnv, + service: ndk::jlong, +) { + use std::mem; + unsafe { + let service: *mut GoogleVRDiscovery = mem::transmute(service as usize); + (*service).on_resume(); + } +} diff --git a/webxr/googlevr/gradle/Makefile b/webxr/googlevr/gradle/Makefile new file mode 100644 index 00000000..04ede16f --- /dev/null +++ b/webxr/googlevr/gradle/Makefile @@ -0,0 +1,18 @@ +SOURCES = \ + src/main/java/com/rust/webvr/GVRService.java \ + src/main/AndroidManifest.xml \ + build.gradle \ + settings.gradle \ + gradle/wrapper/gradle-wrapper.properties \ + gradle/wrapper/gradle-wrapper.jar + +OUTPUT_AAR = build/outputs/aar/GVRService-release.aar +FINAL_AAR = ../aar/GVRService.aar + +all: $(FINAL_AAR) + +$(OUTPUT_AAR): $(SOURCES) + ./gradlew assembleRelease && touch $(OUTPUT_AAR) + +$(FINAL_AAR): $(OUTPUT_AAR) + cp $(OUTPUT_AAR) $(FINAL_AAR) diff --git a/webxr/googlevr/gradle/build.gradle b/webxr/googlevr/gradle/build.gradle new file mode 100644 index 00000000..379b66ba --- /dev/null +++ b/webxr/googlevr/gradle/build.gradle @@ -0,0 +1,48 @@ +buildscript { + repositories { + jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.1.3' + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "27.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 25 + versionCode 1 + versionName "1.0.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + repositories { + jcenter() + } + + dependencies { + implementation 'com.google.vr:sdk-base:1.120.0' + } +} + +repositories { + maven { + url 'https://maven.google.com/' + name 'Google' + } +} \ No newline at end of file diff --git a/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.jar b/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.properties b/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f156f27a --- /dev/null +++ b/webxr/googlevr/gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Nov 28 14:43:10 PST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/webxr/googlevr/gradle/gradlew b/webxr/googlevr/gradle/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/webxr/googlevr/gradle/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/webxr/googlevr/gradle/gradlew.bat b/webxr/googlevr/gradle/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/webxr/googlevr/gradle/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/webxr/googlevr/gradle/settings.gradle b/webxr/googlevr/gradle/settings.gradle new file mode 100644 index 00000000..332416ed --- /dev/null +++ b/webxr/googlevr/gradle/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'GVRService' \ No newline at end of file diff --git a/webxr/googlevr/gradle/src/main/AndroidManifest.xml b/webxr/googlevr/gradle/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4de55b82 --- /dev/null +++ b/webxr/googlevr/gradle/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/webxr/googlevr/gradle/src/main/java/com/rust/webvr/GVRService.java b/webxr/googlevr/gradle/src/main/java/com/rust/webvr/GVRService.java new file mode 100644 index 00000000..6f2b3d2e --- /dev/null +++ b/webxr/googlevr/gradle/src/main/java/com/rust/webvr/GVRService.java @@ -0,0 +1,188 @@ +package com.rust.webvr; + +import android.app.Activity; +import android.app.Application; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; + +import com.google.vr.ndk.base.AndroidCompat; +import com.google.vr.ndk.base.GvrLayout; + +class GVRService implements Application.ActivityLifecycleCallbacks { + private Activity mActivity; + private GvrLayout gvrLayout; + private long mPtr = 0; // Native Rustlang struct pointer + private boolean mPresenting = false; + private boolean mPaused = false; + private boolean mGvrResumed = false; + + private static native void nativeOnPause(long ptr); + private static native void nativeOnResume(long ptr); + + void init(final Activity activity, long ptr) { + mActivity = activity; + mPtr = ptr; + + Runnable initGvr = new Runnable() { + @Override + public void run() { + gvrLayout = new GvrLayout(activity); + // Decouple the app framerate from the display framerate + if (gvrLayout.setAsyncReprojectionEnabled(true)) { + // Android N hint to tune apps for a predictable, + // consistent level of device performance over long periods of time. + // The system automatically disables this mode when the window + // is no longer in focus. + AndroidCompat.setSustainedPerformanceMode(activity, true); + } + gvrLayout.setPresentationView(new View(activity)); + + activity.getApplication().registerActivityLifecycleCallbacks(GVRService.this); + + // Wait until completed + synchronized(this) { + this.notify(); + } + } + }; + + synchronized (initGvr) { + activity.runOnUiThread(initGvr); + try { + initGvr.wait(); + } + catch (Exception ex) { + Log.e("rust-webvr", Log.getStackTraceString(ex)); + } + } + } + + // Called from Native + public long getNativeContext() { + return gvrLayout.getGvrApi().getNativeGvrContext(); + } + + private void start() { + if (mPresenting) { + return; + } + + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + mActivity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (!AndroidCompat.setVrModeEnabled(mActivity, true)) { + Log.w("rust-webvr", "setVrModeEnabled failed"); + } + + // Show GvrLayout + FrameLayout rootLayout = (FrameLayout)mActivity.findViewById(android.R.id.content); + rootLayout.addView(gvrLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + if (!mGvrResumed) { + gvrLayout.onResume(); + mGvrResumed = true; + } + mPresenting = true; + } + + + // Called from Native + public void startPresent() { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + start(); + } + }); + } + + public void stopPresent() { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (!mPresenting) { + return; + } + mPresenting = false; + // Hide GvrLayout + FrameLayout rootLayout = (FrameLayout)mActivity.findViewById(android.R.id.content); + rootLayout.removeView(gvrLayout); + + AndroidCompat.setVrModeEnabled(mActivity, false); + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER); + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + }); + } + + // Called from JNI + public static Object create(Activity activity, long ptr) { + GVRService service = new GVRService(); + service.init(activity, ptr); + return service; + } + + // ActivityLifecycleCallbacks + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + + } + + @Override + public void onActivityStarted(Activity activity) { + if (activity != mActivity) { + return; + } + if (mPaused && gvrLayout != null && !mGvrResumed) { + gvrLayout.onResume(); + mGvrResumed = true; + mPaused = false; + nativeOnResume(mPtr); + } + } + + @Override + public void onActivityResumed(Activity activity) { + + } + + @Override + public void onActivityPaused(Activity activity) { + + } + + @Override + public void onActivityStopped(Activity activity) { + if (activity != mActivity) { + return; + } + + if (mPresenting && gvrLayout != null && mGvrResumed) { + gvrLayout.onPause(); + mGvrResumed = false; + mPaused = true; + nativeOnPause(mPtr); + } + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (mActivity == activity) { + if (gvrLayout != null) { + gvrLayout.shutdown(); + gvrLayout = null; + } + activity.getApplication().unregisterActivityLifecycleCallbacks(this); + mActivity = null; // Don't leak activity + } + } +} diff --git a/webxr/googlevr/input.rs b/webxr/googlevr/input.rs new file mode 100644 index 00000000..d1df5d94 --- /dev/null +++ b/webxr/googlevr/input.rs @@ -0,0 +1,66 @@ +use gvr_sys as gvr; +use gvr_sys::gvr_controller_api_status::*; +use gvr_sys::gvr_controller_handedness::*; + +use euclid::RigidTransform3D; +use euclid::Rotation3D; +use std::ffi::CStr; +use std::mem; +use webxr_api::Handedness; +use webxr_api::Input; +use webxr_api::Native; + +pub struct GoogleVRController { + ctx: *mut gvr::gvr_context, + controller_ctx: *mut gvr::gvr_controller_context, + state: *mut gvr::gvr_controller_state, +} + +impl GoogleVRController { + pub unsafe fn new( + ctx: *mut gvr::gvr_context, + controller_ctx: *mut gvr::gvr_controller_context, + ) -> Result { + let gamepad = Self { + ctx: ctx, + controller_ctx: controller_ctx, + state: gvr::gvr_controller_state_create(), + }; + gvr::gvr_controller_state_update(controller_ctx, 0, gamepad.state); + let api_status = gvr::gvr_controller_state_get_api_status(gamepad.state); + if api_status != GVR_CONTROLLER_API_OK as i32 { + let message = CStr::from_ptr(gvr::gvr_controller_api_status_to_string(api_status)); + return Err(message.to_string_lossy().into()); + } + + Ok(gamepad) + } + + pub fn handedness(&self) -> Handedness { + let handeness = unsafe { + let prefs = gvr::gvr_get_user_prefs(self.ctx); + gvr::gvr_user_prefs_get_controller_handedness(prefs) + }; + if handeness == GVR_CONTROLLER_LEFT_HANDED as i32 { + Handedness::Left + } else { + Handedness::Right + } + } + + pub fn state(&self) -> RigidTransform3D { + unsafe { + gvr::gvr_controller_state_update(self.controller_ctx, 0, self.state); + let quat = gvr::gvr_controller_state_get_orientation(self.state); + Rotation3D::unit_quaternion(quat.qx, quat.qy, quat.qz, quat.qw).into() + } + } +} + +impl Drop for GoogleVRController { + fn drop(&mut self) { + unsafe { + gvr::gvr_controller_state_destroy(mem::transmute(&self.state)); + } + } +} diff --git a/webxr/googlevr/mod.rs b/webxr/googlevr/mod.rs new file mode 100644 index 00000000..0257057a --- /dev/null +++ b/webxr/googlevr/mod.rs @@ -0,0 +1,16 @@ +/* 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/. */ + +pub use discovery::GoogleVRDiscovery; + +pub(crate) mod device; +pub(crate) mod discovery; +pub(crate) mod input; + +// Export functions called from Java +#[cfg(target_os = "android")] +pub mod jni { + pub use super::discovery::Java_com_rust_webvr_GVRService_nativeOnPause; + pub use super::discovery::Java_com_rust_webvr_GVRService_nativeOnResume; +} diff --git a/webxr/jni_utils.rs b/webxr/jni_utils.rs new file mode 100644 index 00000000..b984b237 --- /dev/null +++ b/webxr/jni_utils.rs @@ -0,0 +1,101 @@ +use android_injected_glue as android; +use android_injected_glue::ffi as ndk; +use std::ffi::CString; +use std::mem; +use std::ptr; + +pub struct JNIScope { + pub vm: *mut ndk::_JavaVM, + pub env: *mut ndk::JNIEnv, + pub activity: ndk::jobject, +} + +impl JNIScope { + pub unsafe fn attach() -> Result { + let mut env: *mut ndk::JNIEnv = mem::uninitialized(); + let activity: &ndk::ANativeActivity = mem::transmute(android::get_app().activity); + let vm: &mut ndk::_JavaVM = mem::transmute(activity.vm); + let vmf: &ndk::JNIInvokeInterface = mem::transmute(vm.functions); + + // Attach is required because native_glue is running in a separate thread + if (vmf.AttachCurrentThread)(vm as *mut _, &mut env as *mut _, ptr::null_mut()) != 0 { + return Err("JNI AttachCurrentThread failed".into()); + } + + Ok(JNIScope { + vm: vm, + env: env, + activity: activity.clazz, + }) + } + + pub unsafe fn find_class(&self, class_name: &str) -> Result { + // jni.FindClass doesn't find our classes because the attached thread has not our classloader. + // NativeActivity's classloader is used to fix this issue. + let env = self.env; + let jni = self.jni(); + + let activity_class = (jni.GetObjectClass)(env, self.activity); + if activity_class.is_null() { + return Err("Didn't find NativeActivity class".into()); + } + let method = self.get_method( + activity_class, + "getClassLoader", + "()Ljava/lang/ClassLoader;", + false, + ); + let classloader = (jni.CallObjectMethod)(env, self.activity, method); + if classloader.is_null() { + return Err("Didn't find NativeActivity's classloader".into()); + } + let classloader_class = (jni.GetObjectClass)(env, classloader); + let load_method = self.get_method( + classloader_class, + "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;", + false, + ); + + // Load our class using the classloader. + let class_name = CString::new(class_name).unwrap(); + let class_name = (jni.NewStringUTF)(env, class_name.as_ptr()); + let java_class = + (jni.CallObjectMethod)(env, classloader, load_method, class_name) as ndk::jclass; + (jni.DeleteLocalRef)(env, class_name); + + Ok(java_class) + } + + pub unsafe fn get_method( + &self, + class: ndk::jclass, + method: &str, + signature: &str, + is_static: bool, + ) -> ndk::jmethodID { + let method = CString::new(method).unwrap(); + let signature = CString::new(signature).unwrap(); + let jni = self.jni(); + + if is_static { + (jni.GetStaticMethodID)(self.env, class, method.as_ptr(), signature.as_ptr()) + } else { + (jni.GetMethodID)(self.env, class, method.as_ptr(), signature.as_ptr()) + } + } + + pub fn jni(&self) -> &mut ndk::JNINativeInterface { + unsafe { mem::transmute((*self.env).functions) } + } +} + +impl Drop for JNIScope { + // Autodetach JNI thread + fn drop(&mut self) { + unsafe { + let vmf: &ndk::JNIInvokeInterface = mem::transmute((*self.vm).functions); + (vmf.DetachCurrentThread)(self.vm); + } + } +} diff --git a/webxr/lib.rs b/webxr/lib.rs index 685398dc..ac23d4c7 100644 --- a/webxr/lib.rs +++ b/webxr/lib.rs @@ -4,8 +4,22 @@ //! This crate defines the Rust implementation of WebXR for various devices. +#[macro_use] +extern crate log; + #[cfg(feature = "glwindow")] pub mod glwindow; #[cfg(feature = "headless")] pub mod headless; + +#[cfg(feature = "googlevr")] +pub mod googlevr; + +#[cfg(feature = "googlevr")] +mod gl { + include!(concat!(env!("OUT_DIR"), "/gles_bindings.rs")); +} + +#[cfg(all(feature = "googlevr", target_os = "android"))] +pub(crate) mod jni_utils;