From cdb32ec2521409068be2f37deb43f122f582351b Mon Sep 17 00:00:00 2001 From: Alan Jeffrey Date: Fri, 15 Feb 2019 10:41:58 -0600 Subject: [PATCH 1/2] Add glwindow implementation, that renders to a GlWindow --- rust-webvr-api/src/lib.rs | 2 + .../src/vr_main_thread_heartbeat.rs | 10 + rust-webvr/Cargo.toml | 5 + rust-webvr/src/api/glwindow/display.rs | 261 ++++++++++++++++++ rust-webvr/src/api/glwindow/heartbeat.rs | 162 +++++++++++ rust-webvr/src/api/glwindow/mod.rs | 6 + rust-webvr/src/api/glwindow/service.rs | 80 ++++++ rust-webvr/src/api/mod.rs | 5 + rust-webvr/src/lib.rs | 13 + 9 files changed, 544 insertions(+) create mode 100644 rust-webvr-api/src/vr_main_thread_heartbeat.rs create mode 100644 rust-webvr/src/api/glwindow/display.rs create mode 100644 rust-webvr/src/api/glwindow/heartbeat.rs create mode 100644 rust-webvr/src/api/glwindow/mod.rs create mode 100644 rust-webvr/src/api/glwindow/service.rs diff --git a/rust-webvr-api/src/lib.rs b/rust-webvr-api/src/lib.rs index d01f36e..e3c4707 100644 --- a/rust-webvr-api/src/lib.rs +++ b/rust-webvr-api/src/lib.rs @@ -38,6 +38,7 @@ pub mod vr_stage_parameters; pub mod vr_event; pub mod vr_field_view; pub mod vr_gamepad; +pub mod vr_main_thread_heartbeat; pub use vr_display::{VRDisplay,VRDisplayPtr}; pub use vr_service::{VRService,VRServiceCreator}; @@ -56,3 +57,4 @@ pub use vr_event::{VREvent, VRDisplayEvent, VRDisplayEventReason, VRGamepadEvent pub use vr_field_view::VRFieldOfView; pub use vr_gamepad::{VRGamepad, VRGamepadPtr, VRGamepadHand, VRGamepadData, VRGamepadState, VRGamepadButton}; +pub use vr_main_thread_heartbeat::VRMainThreadHeartbeat; diff --git a/rust-webvr-api/src/vr_main_thread_heartbeat.rs b/rust-webvr-api/src/vr_main_thread_heartbeat.rs new file mode 100644 index 0000000..7bdd28d --- /dev/null +++ b/rust-webvr-api/src/vr_main_thread_heartbeat.rs @@ -0,0 +1,10 @@ +/// Each VR service may have some code which needs to run on the main thread, +/// for example window creation on MacOS is only supported on the main thread. +/// Implementations of this trait will usually be neither `Sync` nor `Send`. +pub trait VRMainThreadHeartbeat { + /// Run the heartbeat on the main thread. + fn heartbeat(&mut self); + + /// Is the heartbeat expecting to be called every frame? + fn heart_racing(&self) -> bool; +} diff --git a/rust-webvr/Cargo.toml b/rust-webvr/Cargo.toml index bda4cc9..b60db7e 100644 --- a/rust-webvr/Cargo.toml +++ b/rust-webvr/Cargo.toml @@ -21,6 +21,7 @@ build = "build.rs" [features] default = ["vrexternal", "openvr", "mock"] vrexternal = [] +glwindow = ["euclid", "gleam", "glutin", "winit"] openvr = ["libloading"] mock = [] googlevr = ["gvr-sys"] @@ -33,6 +34,10 @@ libloading = { version = "0.5", optional = true, default-features = false } gvr-sys = { version = "0.7", optional = true } ovr-mobile-sys = { version = "0.4", optional = true } libc = "0.2" +euclid = { version = "0.19", optional = true } +gleam = { version = "0.6", optional = true } +glutin = { version = "0.19", optional = true } +winit = { version = "0.18", optional = true } [build-dependencies] gl_generator = "0.10" diff --git a/rust-webvr/src/api/glwindow/display.rs b/rust-webvr/src/api/glwindow/display.rs new file mode 100644 index 0000000..3d6d571 --- /dev/null +++ b/rust-webvr/src/api/glwindow/display.rs @@ -0,0 +1,261 @@ +use euclid::Angle; +use euclid::Trig; +use gleam::gl; +use gleam::gl::Gl; +use rust_webvr_api::utils; +use rust_webvr_api::VRDisplay; +use rust_webvr_api::VRDisplayCapabilities; +use rust_webvr_api::VRDisplayData; +use rust_webvr_api::VREyeParameters; +use rust_webvr_api::VRFieldOfView; +use rust_webvr_api::VRFrameData; +use rust_webvr_api::VRFutureFrameData; +use rust_webvr_api::VRFramebuffer; +use rust_webvr_api::VRFramebufferAttributes; +use rust_webvr_api::VRLayer; +use rust_webvr_api::VRViewport; +use std::cell::RefCell; +use std::sync::Arc; +use std::sync::mpsc::Sender; +use super::heartbeat::GlWindowVRMessage; +use winit::dpi::PhysicalSize; + +// Fake a display with a distance between eyes of 5cm. +const EYE_DISTANCE: f32 = 0.05; + +pub type GlWindowVRDisplayPtr = Arc>; + +pub struct GlWindowVRDisplay { + id: u32, + name: String, + size: PhysicalSize, + sender: Sender, + pool: ArcPool>, +} + +unsafe impl Sync for GlWindowVRDisplay {} + +impl Drop for GlWindowVRDisplay { + fn drop(&mut self) { + self.stop_present(); + } +} + +impl VRDisplay for GlWindowVRDisplay { + fn id(&self) -> u32 { + self.id + } + + fn data(&self) -> VRDisplayData { + let capabilities = VRDisplayCapabilities { + has_position: false, + has_orientation: false, + has_external_display: true, + can_present: true, + presented_by_browser: false, + max_layers: 1, + }; + + let fov_right = GlWindowVRDisplay::fov_right(self.size).to_degrees(); + let fov_up = GlWindowVRDisplay::fov_up(self.size).to_degrees(); + + let field_of_view = VRFieldOfView { + down_degrees: fov_up, + left_degrees: fov_right, + right_degrees: fov_right, + up_degrees: fov_up, + }; + + let left_eye_parameters = VREyeParameters { + offset: [-EYE_DISTANCE / 2.0, 0.0, 0.0], + render_width: self.size.width as u32 / 2, + render_height: self.size.height as u32, + field_of_view: field_of_view, + }; + + let right_eye_parameters = VREyeParameters { + offset: [EYE_DISTANCE / 2.0, 0.0, 0.0], + ..left_eye_parameters.clone() + }; + + VRDisplayData { + display_id: self.id, + display_name: self.name.clone(), + connected: true, + capabilities: capabilities, + stage_parameters: None, + left_eye_parameters: left_eye_parameters, + right_eye_parameters: right_eye_parameters, + } + } + + fn immediate_frame_data(&self, near: f64, far: f64) -> VRFrameData { + GlWindowVRDisplay::frame_data(0.0, self.size, near, far) + } + + fn synced_frame_data(&self, near: f64, far: f64) -> VRFrameData { + self.immediate_frame_data(near, far) + } + + fn reset_pose(&mut self) {} + + fn sync_poses(&mut self) {} + + fn future_frame_data(&mut self, near: f64, far: f64) -> VRFutureFrameData { + let (resolver, result) = VRFutureFrameData::blocked(); + let _ = self.sender.send(GlWindowVRMessage::StartFrame(near, far, resolver)); + result + } + + fn bind_framebuffer(&mut self, _eye_index: u32) {} + + fn get_framebuffers(&self) -> Vec { + let left_viewport = VRViewport { + x: 0, + y: 0, + width: (self.size.width as i32) / 2, + height: self.size.height as i32, + }; + + let right_viewport = VRViewport { + x: self.size.width as i32 - left_viewport.width, + ..left_viewport + }; + + vec![ + VRFramebuffer { + eye_index: 0, + attributes: VRFramebufferAttributes::default(), + viewport: left_viewport, + }, + VRFramebuffer { + eye_index: 1, + attributes: VRFramebufferAttributes::default(), + viewport: right_viewport, + }, + ] + } + + fn render_layer(&mut self, _layer: &VRLayer) { + unreachable!() + } + + fn submit_frame(&mut self) { + unreachable!() + } + + fn submit_layer(&mut self, gl: &Gl, layer: &VRLayer) { + // TODO: this assumes that the current GL framebuffer contains the texture + // TODO: what to do if the layer has no texture_size? + if let Some((width, height)) = layer.texture_size { + let num_bytes = (width as usize) * (height as usize) * 4; + let mut buffer = self.pool.remove().unwrap_or_else(Vec::new); + buffer.resize(num_bytes, 0); + gl.read_pixels_into_buffer( + 0, + 0, + width as gl::GLsizei, + height as gl::GLsizei, + gl::RGBA, + gl::UNSIGNED_BYTE, + &mut buffer[..], + ); + let buffer = self.pool.add(buffer); + let _ = self.sender.send(GlWindowVRMessage::StopFrame(width, height, buffer)); + } + } + + fn start_present(&mut self, _attributes: Option) { + let _ = self.sender.send(GlWindowVRMessage::StartPresenting); + } + + fn stop_present(&mut self) { + let _ = self.sender.send(GlWindowVRMessage::StopPresenting); + } +} + +impl GlWindowVRDisplay { + pub(crate) fn new( + name: String, + size: PhysicalSize, + sender: Sender + ) -> GlWindowVRDisplay { + GlWindowVRDisplay { + id: utils::new_id(), + name: name, + size: size, + sender: sender, + pool: ArcPool::new(), + } + } + + fn fov_up(size: PhysicalSize) -> Angle { + Angle::radians(f64::fast_atan2( + 2.0 * size.height as f64, + size.width as f64, + )) + } + + fn fov_right(size: PhysicalSize) -> Angle { + Angle::radians(f64::fast_atan2( + 2.0 * size.width as f64, + size.height as f64, + )) + } + + fn perspective(size: PhysicalSize, near: f64, far: f64) -> [f32; 16] { + // https://github.com/toji/gl-matrix/blob/bd3307196563fbb331b40fc6ebecbbfcc2a4722c/src/mat4.js#L1271 + let near = near as f32; + let far = far as f32; + let f = 1.0 / GlWindowVRDisplay::fov_up(size).radians.tan() as f32; + let nf = 1.0 / (near - far); + let aspect = ((size.width / 2.0) as f32) / (size.height as f32); + + // Dear rustfmt, This is a 4x4 matrix, please leave it alone. Best, ajeffrey. + {#[rustfmt::skip] + return [ + f / aspect, 0.0, 0.0, 0.0, + 0.0, f, 0.0, 0.0, + 0.0, 0.0, (far + near) * nf, -1.0, + 0.0, 0.0, 2.0 * far * near * nf, 0.0, + ]; + } + } + + pub(crate) fn frame_data(timestamp: f64, size: PhysicalSize, near: f64, far: f64) -> VRFrameData { + let left_projection_matrix = GlWindowVRDisplay::perspective(size, near, far); + let right_projection_matrix = left_projection_matrix.clone(); + + VRFrameData { + timestamp: timestamp, + // TODO: adjust matrices for stereoscopic vision + left_projection_matrix: left_projection_matrix, + right_projection_matrix: right_projection_matrix, + ..VRFrameData::default() + } + } +} + +// A pool of Arc's. +// You can add a T into the pool, and get back an Arc. +// You can request a T from the pool, if there's an Arc with no other owners, +// it will be removed from the pool, unwrapped and returned. + +struct ArcPool(Vec>); + +impl ArcPool { + fn new() -> ArcPool { + ArcPool(Vec::new()) + } + + fn add(&mut self, val: T) -> Arc { + let result = Arc::new(val); + self.0.push(result.clone()); + result + } + + fn remove(&mut self) -> Option { + let i = self.0.iter().position(|arc| Arc::strong_count(arc) == 1); + i.and_then(|i| Arc::try_unwrap(self.0.swap_remove(i)).ok()) + } +} diff --git a/rust-webvr/src/api/glwindow/heartbeat.rs b/rust-webvr/src/api/glwindow/heartbeat.rs new file mode 100644 index 0000000..6096ef2 --- /dev/null +++ b/rust-webvr/src/api/glwindow/heartbeat.rs @@ -0,0 +1,162 @@ +use gleam::gl; +use gleam::gl::Gl; +use glutin::GlContext; +use glutin::GlWindow; +use rust_webvr_api::VRResolveFrameData; +use rust_webvr_api::VRMainThreadHeartbeat; +use std::rc::Rc; +use std::time::Duration; +use super::display::GlWindowVRDisplay; +use std::sync::Arc; +use std::sync::mpsc::Receiver; + +const TIMEOUT: Duration = Duration::from_millis(16); + +pub struct GlWindowVRMainThreadHeartbeat { + receiver: Receiver, + gl_window: GlWindow, + gl: Rc, + presenting: bool, + timestamp: f64, + texture_id: gl::GLuint, + framebuffer_id: gl::GLuint, +} + +impl VRMainThreadHeartbeat for GlWindowVRMainThreadHeartbeat { + fn heartbeat(&mut self) { + debug!("VR heartbeat start"); + loop { + // If we are presenting, we block the main thread on the VR thread. + let msg = if self.presenting { + self.receiver.recv_timeout(TIMEOUT).ok() + } else { + self.receiver.try_recv().ok() + }; + + match msg { + Some(msg) => if self.handle_msg(msg) { break; }, + None => break, + }; + } + debug!("VR heartbeat stop"); + } + + fn heart_racing(&self) -> bool { + self.presenting + } +} + +impl GlWindowVRMainThreadHeartbeat { + pub(crate) fn new( + receiver: Receiver, + gl_window: GlWindow, + gl: Rc, + ) -> GlWindowVRMainThreadHeartbeat { + debug!("Creating VR heartbeat"); + GlWindowVRMainThreadHeartbeat { + receiver: receiver, + gl_window: gl_window, + gl: gl, + presenting: false, + timestamp: 0.0, + texture_id: 0, + framebuffer_id: 0, + } + } + + fn handle_msg(&mut self, msg: GlWindowVRMessage) -> bool { + match msg { + GlWindowVRMessage::StartPresenting => { + debug!("VR starting"); + self.gl_window.show(); + self.presenting = true; + true + }, + GlWindowVRMessage::StartFrame(near, far, mut resolver) => { + debug!("VR start frame"); + let timestamp = self.timestamp; + let size = self.gl_window.get_inner_size().expect("No window size"); + let hidpi = self.gl_window.get_hidpi_factor(); + let size = size.to_physical(hidpi); + let data = GlWindowVRDisplay::frame_data(timestamp, size, near, far); + let _ = resolver.resolve(data); + self.timestamp = self.timestamp + 1.0; + false + }, + GlWindowVRMessage::StopFrame(width, height, buffer) => { + debug!("VR stop frame {}x{} ({})", width, height, buffer.len()); + // TODO: render the buffer contents + if let Err(err) = unsafe { self.gl_window.make_current() } { + error!("VR Display failed to make window current ({:?})", err); + return true; + } + if self.texture_id == 0 { + self.texture_id = self.gl.gen_textures(1)[0]; + debug!("Generated texture {}", self.texture_id); + } + if self.framebuffer_id == 0 { + self.framebuffer_id = self.gl.gen_framebuffers(1)[0]; + debug!("Generated framebuffer {}", self.framebuffer_id); + } + + self.gl.clear_color(0.2, 0.3, 0.3, 1.0); + self.gl.clear(gl::COLOR_BUFFER_BIT); + + self.gl.bind_texture(gl::TEXTURE_2D, self.texture_id); + self.gl.tex_image_2d( + gl::TEXTURE_2D, + 0, + gl::RGBA as gl::GLint, + width as gl::GLsizei, + height as gl::GLsizei, + 0, + gl::RGBA, + gl::UNSIGNED_BYTE, + Some(&buffer[..]), + ); + self.gl.bind_texture(gl::TEXTURE_2D, 0); + + self.gl.bind_framebuffer(gl::READ_FRAMEBUFFER, self.framebuffer_id); + self.gl.framebuffer_texture_2d( + gl::READ_FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gl::TEXTURE_2D, + self.texture_id, + 0 + ); + self.gl.viewport( + 0, 0, width as gl::GLsizei, height as gl::GLsizei, + ); + self.gl.blit_framebuffer( + 0, 0, width as gl::GLsizei, height as gl::GLsizei, + 0, 0, width as gl::GLsizei, height as gl::GLsizei, + gl::COLOR_BUFFER_BIT, + gl::NEAREST, + ); + self.gl.bind_framebuffer(gl::READ_FRAMEBUFFER, 0); + + let _ = self.gl_window.swap_buffers(); + + let err = self.gl.get_error(); + if err != 0 { + error!("Test VR Display GL error {}.", err); + } + + true + }, + GlWindowVRMessage::StopPresenting => { + debug!("VR stopping"); + self.gl_window.hide(); + self.presenting = false; + true + }, + } + } +} + +pub(crate) enum GlWindowVRMessage { + StartPresenting, + StartFrame(f64, f64, VRResolveFrameData), + StopFrame(u32, u32, Arc>), + StopPresenting, +} diff --git a/rust-webvr/src/api/glwindow/mod.rs b/rust-webvr/src/api/glwindow/mod.rs new file mode 100644 index 0000000..bb56481 --- /dev/null +++ b/rust-webvr/src/api/glwindow/mod.rs @@ -0,0 +1,6 @@ +mod display; +mod service; +mod heartbeat; + +pub use self::service::GlWindowVRService; +pub use self::heartbeat::GlWindowVRMainThreadHeartbeat; diff --git a/rust-webvr/src/api/glwindow/service.rs b/rust-webvr/src/api/glwindow/service.rs new file mode 100644 index 0000000..737db2b --- /dev/null +++ b/rust-webvr/src/api/glwindow/service.rs @@ -0,0 +1,80 @@ +use gleam::gl::Gl; +use glutin::GlWindow; +use rust_webvr_api::VRDisplayPtr; +use rust_webvr_api::VREvent; +use rust_webvr_api::VRGamepadPtr; +use rust_webvr_api::VRService; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::mpsc::channel; +use std::sync::mpsc::Sender; +use super::display::GlWindowVRDisplay; +use super::display::GlWindowVRDisplayPtr; +use super::heartbeat::GlWindowVRMainThreadHeartbeat; +use super::heartbeat::GlWindowVRMessage; +use winit::dpi::PhysicalSize; + +pub struct GlWindowVRService { + name: String, + size: PhysicalSize, + sender: Sender, + display: Option, +} + +// This is very very unsafe, but the API requires it. +unsafe impl Send for GlWindowVRService {} + +impl VRService for GlWindowVRService { + fn initialize(&mut self) -> Result<(), String> { + self.get_display(); + Ok(()) + } + + fn fetch_displays(&mut self) -> Result, String> { + Ok(vec![ self.get_display().clone() ]) + } + + fn fetch_gamepads(&mut self) -> Result, String> { + Ok(vec![]) + } + + fn is_available(&self) -> bool { + true + } + + fn poll_events(&self) -> Vec { + vec![] + } +} + +impl GlWindowVRService { + // This function should be called from the main thread. + pub fn new( + name: String, + gl_window: GlWindow, + gl: Rc, + ) -> (GlWindowVRService, GlWindowVRMainThreadHeartbeat) { + let (sender, receiver) = channel(); + let size = gl_window.get_inner_size().expect("No window size"); + let hidpi = gl_window.get_hidpi_factor(); + let heartbeat = GlWindowVRMainThreadHeartbeat::new(receiver, gl_window, gl); + let service = GlWindowVRService { + name: name, + size: size.to_physical(hidpi), + sender: sender, + display: None, + }; + (service, heartbeat) + } + + fn get_display(&mut self) -> &mut GlWindowVRDisplayPtr { + let name = &self.name; + let sender = &self.sender; + let size = self.size; + self.display.get_or_insert_with(|| { + let display = GlWindowVRDisplay::new(name.clone(), size, sender.clone()); + Arc::new(RefCell::new(display)) + }) + } +} diff --git a/rust-webvr/src/api/mod.rs b/rust-webvr/src/api/mod.rs index 340877e..7bcb001 100644 --- a/rust-webvr/src/api/mod.rs +++ b/rust-webvr/src/api/mod.rs @@ -10,6 +10,11 @@ mod mock; #[cfg(feature = "mock")] pub use self::mock::MockServiceCreator; +#[cfg(feature = "glwindow")] +mod glwindow; +#[cfg(feature = "glwindow")] +pub use self::glwindow::GlWindowVRService; + #[cfg(all(target_os="windows", feature = "openvr"))] mod openvr; #[cfg(all(target_os="windows", feature = "openvr"))] diff --git a/rust-webvr/src/lib.rs b/rust-webvr/src/lib.rs index b908c4b..3fa34e7 100644 --- a/rust-webvr/src/lib.rs +++ b/rust-webvr/src/lib.rs @@ -9,6 +9,16 @@ extern crate libloading; extern crate log; #[cfg(all(feature = "oculusvr", target_os= "android"))] extern crate ovr_mobile_sys; +#[cfg(feature = "glwindow")] +extern crate euclid; +#[cfg(feature = "glwindow")] +extern crate gleam; +#[cfg(feature = "glwindow")] +extern crate glutin; +#[cfg(feature = "glwindow")] +extern crate winit; +#[cfg(feature = "serde-serialization")] +#[macro_use] extern crate serde_derive; #[cfg(any(feature = "googlevr", feature= "oculusvr"))] mod gl { @@ -32,6 +42,9 @@ mod egl { include!(concat!(env!("OUT_DIR"), "/egl_bindings.rs")); } +#[cfg(feature= "glwindow")] +pub use api::GlWindowVRService; + pub mod api; mod vr_manager; From b61b1e533c2956e98af2b43dfa5def38311ea7ea Mon Sep 17 00:00:00 2001 From: Alan Jeffrey Date: Tue, 26 Feb 2019 12:10:04 -0600 Subject: [PATCH 2/2] Version bump --- rust-webvr-api/Cargo.toml | 2 +- rust-webvr/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust-webvr-api/Cargo.toml b/rust-webvr-api/Cargo.toml index 0de3f39..f2fc4dc 100644 --- a/rust-webvr-api/Cargo.toml +++ b/rust-webvr-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-webvr-api" -version = "0.10.3" +version = "0.10.4" authors = ["The Servo Project Developers"] homepage = "https://github.com/servo/rust-webvr" diff --git a/rust-webvr/Cargo.toml b/rust-webvr/Cargo.toml index b60db7e..9b03745 100644 --- a/rust-webvr/Cargo.toml +++ b/rust-webvr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-webvr" -version = "0.10.1" +version = "0.10.2" authors = ["The Servo Project Developers"] homepage = "https://github.com/servo/rust-webvr" @@ -28,7 +28,7 @@ googlevr = ["gvr-sys"] oculusvr = ["ovr-mobile-sys"] [dependencies] -rust-webvr-api = { path = "../rust-webvr-api", version = "0.10" } +rust-webvr-api = { path = "../rust-webvr-api", version = "0.10.4" } log = "0.4" libloading = { version = "0.5", optional = true, default-features = false } gvr-sys = { version = "0.7", optional = true }