servo: Merge #16260 - Gamepad API implementation (from MortimerGoro:gamepad); r=nox,larsbergstrom
authorImanol Fernandez <mortimergoro@gmail.com>
Thu, 13 Apr 2017 16:10:42 -0500
changeset 401190 0150a0000fc0cb0570ae495976d844849f2e9a7f
parent 401189 e8e2593692a51149373b2645e69a4d22b99fc466
child 401191 bec1fdf2a886c400062adb77e0bbbb807e6d2b27
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnox, larsbergstrom
bugs16260, 10977
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #16260 - Gamepad API implementation (from MortimerGoro:gamepad); r=nox,larsbergstrom <!-- Please describe your changes on the following line: --> Gamepad API implementation. Tested with HTC Vive and Daydream controllers ;) --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors - [x] These changes fix #10977 <!-- Either: --> - [ ] There are tests for these changes OR - [X] These changes do not require tests because : current gamepad tests are manual (https://github.com/w3c/web-platform-tests/tree/master/gamepad). There is a open issue about the best way to test WebVR/Gamepad without real devices https://github.com/w3c/webvr/issues/187. We'll work on the testing suite & mock devices/data on a separate issue. <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> Source-Repo: https://github.com/servo/servo Source-Revision: 26c45275ffaccf746e47606a74b3aee519673e54
servo/Cargo.lock
servo/components/constellation/constellation.rs
servo/components/script/dom/bindings/trace.rs
servo/components/script/dom/gamepad.rs
servo/components/script/dom/gamepadbutton.rs
servo/components/script/dom/gamepadbuttonlist.rs
servo/components/script/dom/gamepadevent.rs
servo/components/script/dom/gamepadlist.rs
servo/components/script/dom/mod.rs
servo/components/script/dom/navigator.rs
servo/components/script/dom/vr.rs
servo/components/script/dom/vrdisplay.rs
servo/components/script/dom/vrpose.rs
servo/components/script/dom/webidls/Gamepad.webidl
servo/components/script/dom/webidls/GamepadButton.webidl
servo/components/script/dom/webidls/GamepadButtonList.webidl
servo/components/script/dom/webidls/GamepadEvent.webidl
servo/components/script/dom/webidls/GamepadList.webidl
servo/components/script/dom/webidls/Navigator.webidl
servo/components/script/script_thread.rs
servo/components/script_traits/lib.rs
servo/components/webvr/webvr_thread.rs
servo/components/webvr_traits/Cargo.toml
servo/components/webvr_traits/lib.rs
servo/components/webvr_traits/webvr_traits.rs
servo/resources/prefs.json
servo/tests/html/webvr/vr-controllers.html
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -1,15 +1,15 @@
 [root]
 name = "webvr_traits"
 version = "0.0.1"
 dependencies = [
  "ipc-channel 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "msg 0.0.1",
- "rust-webvr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-webvr 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "adler32"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2188,19 +2188,20 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "rust-webvr"
-version = "0.2.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
+ "gl_generator 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "libloading 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -3490,17 +3491,17 @@ dependencies = [
 "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
 "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
 "checksum rayon 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50c575b58c2b109e2fbc181820cbe177474f35610ff9e357dc75f6bac854ffbf"
 "checksum redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b"
 "checksum ref_filter_map 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2b5ceb840e4009da4841ed22a15eb49f64fdd00a2138945c5beacf506b2fb5ed"
 "checksum ref_slice 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "825740057197b7d43025e7faf6477eaabc03434e153233da02d1f44602f71527"
 "checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
 "checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
-"checksum rust-webvr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ae0560bf176cd49f08d3df2784f9bfe74df6f6346b71b98ca3358160316e271"
+"checksum rust-webvr 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "454fc4c3a786029ab82c5528c14f01bf965f60f61b3f9b1ed51b4646223eab59"
 "checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95"
 "checksum rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "684ce48436d6465300c9ea783b6b14c4361d6b8dcbb1375b486a69cc19e2dfb0"
 "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
 "checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7"
 "checksum scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef399c8893e8cb7aa9696e895427fab3a6bf265977bb96e126f24ddd2cda85a"
 "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac"
 "checksum serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a702319c807c016e51f672e5c77d6f0b46afddd744b5e437d6b8436b888b458f"
 "checksum serde_codegen_internals 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d52006899f910528a10631e5b727973fe668f3228109d1707ccf5bad5490b6e"
--- a/servo/components/constellation/constellation.rs
+++ b/servo/components/constellation/constellation.rs
@@ -97,17 +97,16 @@ use profile_traits::time;
 use script_traits::{AnimationState, AnimationTickType, CompositorEvent};
 use script_traits::{ConstellationControlMsg, ConstellationMsg as FromCompositorMsg, DiscardBrowsingContext};
 use script_traits::{DocumentActivity, DocumentState, LayoutControlMsg, LoadData};
 use script_traits::{IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, TimerSchedulerMsg};
 use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, ScriptThreadFactory};
 use script_traits::{LogEntry, ServiceWorkerMsg, webdriver_msg};
 use script_traits::{MozBrowserErrorType, MozBrowserEvent, WebDriverCommandMsg, WindowSizeData};
 use script_traits::{SWManagerMsg, ScopeThings, WindowSizeType};
-use script_traits::WebVREventMsg;
 use serde::{Deserialize, Serialize};
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use servo_rand::{Rng, SeedableRng, ServoRng, random};
 use servo_remutex::ReentrantMutex;
 use servo_url::{Host, ImmutableOrigin, ServoUrl};
 use std::borrow::ToOwned;
 use std::cmp::Ordering;
@@ -119,17 +118,17 @@ use std::rc::{Rc, Weak};
 use std::sync::Arc;
 use std::sync::mpsc::{Receiver, Sender, channel};
 use std::thread;
 use style_traits::CSSPixel;
 use style_traits::cursor::Cursor;
 use style_traits::viewport::ViewportConstraints;
 use timer_scheduler::TimerScheduler;
 use webrender_traits;
-use webvr_traits::WebVRMsg;
+use webvr_traits::{WebVREvent, WebVRMsg};
 
 /// The `Constellation` itself. In the servo browser, there is one
 /// constellation, which maintains all of the browser global data.
 /// In embedded applications, there may be more than one constellation,
 /// which are independent of each other.
 ///
 /// The constellation may be in a different process from the pipelines,
 /// and communicates using IPC.
@@ -892,19 +891,19 @@ impl<Message, LTF, STF> Constellation<Me
             }
             FromCompositorMsg::LogEntry(top_level_frame_id, thread_name, entry) => {
                 self.handle_log_entry(top_level_frame_id, thread_name, entry);
             }
             FromCompositorMsg::SetWebVRThread(webvr_thread) => {
                 assert!(self.webvr_thread.is_none());
                 self.webvr_thread = Some(webvr_thread)
             }
-            FromCompositorMsg::WebVREvent(pipeline_ids, event) => {
-                debug!("constellation got WebVR event");
-                self.handle_webvr_event(pipeline_ids, event);
+            FromCompositorMsg::WebVREvents(pipeline_ids, events) => {
+                debug!("constellation got {:?} WebVR events", events.len());
+                self.handle_webvr_events(pipeline_ids, events);
             }
         }
     }
 
     fn handle_request_from_script(&mut self, message: FromScriptMsg) {
         match message {
             FromScriptMsg::PipelineExited(pipeline_id) => {
                 self.handle_pipeline_exited(pipeline_id);
@@ -1321,22 +1320,22 @@ impl<Message, LTF, STF> Constellation<Me
                 if WARNINGS_BUFFER_SIZE <= self.handled_warnings.len() {
                     self.handled_warnings.pop_front();
                 }
                 self.handled_warnings.push_back((thread_name, reason));
             },
         }
     }
 
-    fn handle_webvr_event(&mut self, ids: Vec<PipelineId>, event: WebVREventMsg) {
+    fn handle_webvr_events(&mut self, ids: Vec<PipelineId>, events: Vec<WebVREvent>) {
         for id in ids {
             match self.pipelines.get_mut(&id) {
                 Some(ref pipeline) => {
                     // Notify script thread
-                    let _ = pipeline.event_loop.send(ConstellationControlMsg::WebVREvent(id, event.clone()));
+                    let _ = pipeline.event_loop.send(ConstellationControlMsg::WebVREvents(id, events.clone()));
                 },
                 None => warn!("constellation got webvr event for dead pipeline")
             }
         }
     }
 
     fn handle_init_load(&mut self, url: ServoUrl) {
         let window_size = self.window_size.initial_viewport;
--- a/servo/components/script/dom/bindings/trace.rs
+++ b/servo/components/script/dom/bindings/trace.rs
@@ -102,16 +102,17 @@ use style::shared_lock::{SharedRwLock as
 use style::stylesheets::{CssRules, FontFaceRule, KeyframesRule, MediaRule};
 use style::stylesheets::{NamespaceRule, StyleRule, ImportRule, SupportsRule};
 use style::values::specified::Length;
 use style::viewport::ViewportRule;
 use time::Duration;
 use uuid::Uuid;
 use webrender_traits::{WebGLBufferId, WebGLError, WebGLFramebufferId, WebGLProgramId};
 use webrender_traits::{WebGLRenderbufferId, WebGLShaderId, WebGLTextureId};
+use webvr_traits::WebVRGamepadHand;
 
 /// A trait to allow tracing (only) DOM objects.
 pub unsafe trait JSTraceable {
     /// Trace `self`.
     unsafe fn trace(&self, trc: *mut JSTracer);
 }
 
 unsafe_no_jsmanaged_fields!(CSSError);
@@ -376,16 +377,17 @@ unsafe_no_jsmanaged_fields!(PathBuf);
 unsafe_no_jsmanaged_fields!(CSSErrorReporter);
 unsafe_no_jsmanaged_fields!(WebGLBufferId);
 unsafe_no_jsmanaged_fields!(WebGLFramebufferId);
 unsafe_no_jsmanaged_fields!(WebGLProgramId);
 unsafe_no_jsmanaged_fields!(WebGLRenderbufferId);
 unsafe_no_jsmanaged_fields!(WebGLShaderId);
 unsafe_no_jsmanaged_fields!(WebGLTextureId);
 unsafe_no_jsmanaged_fields!(MediaList);
+unsafe_no_jsmanaged_fields!(WebVRGamepadHand);
 
 unsafe impl<'a> JSTraceable for &'a str {
     #[inline]
     unsafe fn trace(&self, _: *mut JSTracer) {
         // Do nothing
     }
 }
 
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/gamepad.rs
@@ -0,0 +1,206 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+use core::nonzero::NonZero;
+use dom::bindings::codegen::Bindings::GamepadBinding;
+use dom::bindings::codegen::Bindings::GamepadBinding::GamepadMethods;
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::num::Finite;
+use dom::bindings::reflector::{DomObject, Reflector, reflect_dom_object};
+use dom::bindings::str::DOMString;
+use dom::event::Event;
+use dom::eventtarget::EventTarget;
+use dom::gamepadbuttonlist::GamepadButtonList;
+use dom::gamepadevent::{GamepadEvent, GamepadEventType};
+use dom::globalscope::GlobalScope;
+use dom::vrpose::VRPose;
+use dom_struct::dom_struct;
+use js::jsapi::{Heap, JSContext, JSObject};
+use js::typedarray::{Float64Array, CreateWith};
+use std::cell::Cell;
+use std::ptr;
+use webvr_traits::{WebVRGamepadData, WebVRGamepadHand, WebVRGamepadState};
+
+#[dom_struct]
+pub struct Gamepad {
+    reflector_: Reflector,
+    gamepad_id: u32,
+    id: String,
+    index: Cell<i32>,
+    connected: Cell<bool>,
+    timestamp: Cell<f64>,
+    mapping_type: String,
+    axes: Heap<*mut JSObject>,
+    buttons: JS<GamepadButtonList>,
+    pose: Option<JS<VRPose>>,
+    #[ignore_heap_size_of = "Defined in rust-webvr"]
+    hand: WebVRGamepadHand,
+    display_id: u32
+}
+
+impl Gamepad {
+    fn new_inherited(gamepad_id: u32,
+                     id: String,
+                     index: i32,
+                     connected: bool,
+                     timestamp: f64,
+                     mapping_type: String,
+                     axes: *mut JSObject,
+                     buttons: &GamepadButtonList,
+                     pose: Option<&VRPose>,
+                     hand: WebVRGamepadHand,
+                     display_id: u32) -> Gamepad {
+        Self {
+            reflector_: Reflector::new(),
+            gamepad_id: gamepad_id,
+            id: id,
+            index: Cell::new(index),
+            connected: Cell::new(connected),
+            timestamp: Cell::new(timestamp),
+            mapping_type: mapping_type,
+            axes: Heap::new(axes),
+            buttons: JS::from_ref(buttons),
+            pose: pose.map(JS::from_ref),
+            hand: hand,
+            display_id: display_id
+        }
+    }
+
+    #[allow(unsafe_code)]
+    pub fn new_from_vr(global: &GlobalScope,
+                       index: i32,
+                       data: &WebVRGamepadData,
+                       state: &WebVRGamepadState) -> Root<Gamepad> {
+        let buttons = GamepadButtonList::new_from_vr(&global, &state.buttons);
+        let pose = VRPose::new(&global, &state.pose);
+        let cx = global.get_cx();
+        rooted!(in (cx) let mut axes = ptr::null_mut());
+        unsafe {
+            let _ = Float64Array::create(cx,
+                                         CreateWith::Slice(&state.axes),
+                                         axes.handle_mut());
+        }
+
+        reflect_dom_object(box Gamepad::new_inherited(state.gamepad_id,
+                                                      data.name.clone(),
+                                                      index,
+                                                      state.connected,
+                                                      state.timestamp,
+                                                      "".into(),
+                                                      axes.get(),
+                                                      &buttons,
+                                                      Some(&pose),
+                                                      data.hand.clone(),
+                                                      data.display_id),
+                           global,
+                           GamepadBinding::Wrap)
+
+    }
+}
+
+impl GamepadMethods for Gamepad {
+    // https://w3c.github.io/gamepad/#dom-gamepad-id
+    fn Id(&self) -> DOMString {
+        DOMString::from(self.id.clone())
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-index
+    fn Index(&self) -> i32 {
+        self.index.get()
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-connected
+    fn Connected(&self) -> bool {
+        self.connected.get()
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-timestamp
+    fn Timestamp(&self) -> Finite<f64> {
+        Finite::wrap(self.timestamp.get())
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-mapping
+    fn Mapping(&self) -> DOMString {
+        DOMString::from(self.mapping_type.clone())
+    }
+
+    #[allow(unsafe_code)]
+    // https://w3c.github.io/gamepad/#dom-gamepad-axes
+    unsafe fn Axes(&self, _cx: *mut JSContext) -> NonZero<*mut JSObject> {
+        NonZero::new(self.axes.get())
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-buttons
+    fn Buttons(&self) -> Root<GamepadButtonList> {
+        Root::from_ref(&*self.buttons)
+    }
+
+    // https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum
+    fn Hand(&self) -> DOMString {
+        let value = match self.hand {
+            WebVRGamepadHand::Unknown => "",
+            WebVRGamepadHand::Left => "left",
+            WebVRGamepadHand::Right => "right"
+        };
+        value.into()
+    }
+
+    // https://w3c.github.io/gamepad/extensions.html#dom-gamepad-pose
+    fn GetPose(&self) -> Option<Root<VRPose>> {
+        self.pose.as_ref().map(|p| Root::from_ref(&**p))
+    }
+
+    // https://w3c.github.io/webvr/spec/1.1/#gamepad-getvrdisplays-attribute
+    fn DisplayId(&self) -> u32 {
+        self.display_id
+    }
+}
+
+impl Gamepad {
+    #[allow(unsafe_code)]
+    pub fn update_from_vr(&self, state: &WebVRGamepadState) {
+        self.timestamp.set(state.timestamp);
+        unsafe {
+            let cx = self.global().get_cx();
+            typedarray!(in(cx) let axes: Float64Array = self.axes.get());
+            if let Ok(mut array) = axes {
+                array.update(&state.axes);
+            }
+        }
+        self.buttons.sync_from_vr(&state.buttons);
+        if let Some(ref pose) = self.pose {
+            pose.update(&state.pose);
+        }
+        self.update_connected(state.connected);
+    }
+
+    pub fn gamepad_id(&self) -> u32 {
+        self.gamepad_id
+    }
+
+    pub fn update_connected(&self, connected: bool) {
+        if self.connected.get() == connected {
+            return;
+        }
+        self.connected.set(connected);
+
+        let event_type = if connected {
+            GamepadEventType::Connected
+        } else {
+            GamepadEventType::Disconnected
+        };
+
+        self.notify_event(event_type);
+    }
+
+    pub fn update_index(&self, index: i32) {
+        self.index.set(index);
+    }
+
+    pub fn notify_event(&self, event_type: GamepadEventType) {
+        let event = GamepadEvent::new_with_type(&self.global(), event_type, &self);
+        event.upcast::<Event>().fire(self.global().as_window().upcast::<EventTarget>());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/gamepadbutton.rs
@@ -0,0 +1,61 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::codegen::Bindings::GamepadButtonBinding;
+use dom::bindings::codegen::Bindings::GamepadButtonBinding::GamepadButtonMethods;
+use dom::bindings::js::Root;
+use dom::bindings::num::Finite;
+use dom::bindings::reflector::{Reflector, reflect_dom_object};
+use dom::globalscope::GlobalScope;
+use dom_struct::dom_struct;
+use std::cell::Cell;
+
+#[dom_struct]
+pub struct GamepadButton {
+    reflector_: Reflector,
+    pressed: Cell<bool>,
+    touched: Cell<bool>,
+    value: Cell<f64>,
+}
+
+impl GamepadButton {
+    pub fn new_inherited(pressed: bool, touched: bool) -> GamepadButton {
+        Self {
+            reflector_: Reflector::new(),
+            pressed: Cell::new(pressed),
+            touched: Cell::new(touched),
+            value: Cell::new(0.0),
+        }
+    }
+
+    pub fn new(global: &GlobalScope, pressed: bool, touched: bool) -> Root<GamepadButton> {
+        reflect_dom_object(box GamepadButton::new_inherited(pressed, touched),
+                           global,
+                           GamepadButtonBinding::Wrap)
+    }
+}
+
+impl GamepadButtonMethods for GamepadButton {
+    // https://www.w3.org/TR/gamepad/#widl-GamepadButton-pressed
+    fn Pressed(&self) -> bool {
+        self.pressed.get()
+    }
+
+    // https://www.w3.org/TR/gamepad/#widl-GamepadButton-touched
+    fn Touched(&self) -> bool {
+        self.touched.get()
+    }
+
+    // https://www.w3.org/TR/gamepad/#widl-GamepadButton-value
+    fn Value(&self) -> Finite<f64> {
+        Finite::wrap(self.value.get())
+    }
+}
+
+impl GamepadButton {
+    pub fn update(&self, pressed: bool, touched: bool) {
+        self.pressed.set(pressed);
+        self.touched.set(touched);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/gamepadbuttonlist.rs
@@ -0,0 +1,63 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::codegen::Bindings::GamepadButtonListBinding;
+use dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
+use dom::bindings::js::{JS, Root, RootedReference};
+use dom::bindings::reflector::{Reflector, reflect_dom_object};
+use dom::gamepadbutton::GamepadButton;
+use dom::globalscope::GlobalScope;
+use dom_struct::dom_struct;
+use webvr_traits::WebVRGamepadButton;
+
+// https://w3c.github.io/gamepad/#gamepadbutton-interface
+#[dom_struct]
+pub struct GamepadButtonList {
+    reflector_: Reflector,
+    list: Vec<JS<GamepadButton>>
+}
+
+impl GamepadButtonList {
+    #[allow(unrooted_must_root)]
+    fn new_inherited(list: &[&GamepadButton]) -> GamepadButtonList {
+        GamepadButtonList {
+            reflector_: Reflector::new(),
+            list: list.iter().map(|button| JS::from_ref(*button)).collect(),
+        }
+    }
+
+    pub fn new_from_vr(global: &GlobalScope, buttons: &[WebVRGamepadButton]) -> Root<GamepadButtonList> {
+        rooted_vec!(let list <- buttons.iter()
+                                       .map(|btn| GamepadButton::new(&global, btn.pressed, btn.touched)));
+
+        reflect_dom_object(box GamepadButtonList::new_inherited(list.r()),
+                           global,
+                           GamepadButtonListBinding::Wrap)
+    }
+
+    pub fn sync_from_vr(&self, vr_buttons: &[WebVRGamepadButton]) {
+        let mut index = 0;
+        for btn in vr_buttons {
+            self.list.get(index).as_ref().unwrap().update(btn.pressed, btn.touched);
+            index += 1;
+        }
+    }
+}
+
+impl GamepadButtonListMethods for GamepadButtonList {
+    // https://w3c.github.io/gamepad/#dom-gamepad-buttons
+    fn Length(&self) -> u32 {
+        self.list.len() as u32
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-buttons
+    fn Item(&self, index: u32) -> Option<Root<GamepadButton>> {
+        self.list.get(index as usize).map(|button| Root::from_ref(&**button))
+    }
+
+    // https://w3c.github.io/gamepad/#dom-gamepad-buttons
+    fn IndexedGetter(&self, index: u32) -> Option<Root<GamepadButton>> {
+        self.Item(index)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/gamepadevent.rs
@@ -0,0 +1,92 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::codegen::Bindings::EventBinding::EventBinding::EventMethods;
+use dom::bindings::codegen::Bindings::GamepadEventBinding;
+use dom::bindings::codegen::Bindings::GamepadEventBinding::GamepadEventMethods;
+use dom::bindings::error::Fallible;
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::reflector::{DomObject, reflect_dom_object};
+use dom::bindings::str::DOMString;
+use dom::event::Event;
+use dom::gamepad::Gamepad;
+use dom::globalscope::GlobalScope;
+use dom::window::Window;
+use dom_struct::dom_struct;
+use servo_atoms::Atom;
+
+#[dom_struct]
+pub struct GamepadEvent {
+    event: Event,
+    gamepad: JS<Gamepad>,
+}
+
+pub enum GamepadEventType {
+    Connected,
+    Disconnected
+}
+
+impl GamepadEvent {
+    fn new_inherited(gamepad: &Gamepad) -> GamepadEvent {
+        GamepadEvent {
+            event: Event::new_inherited(),
+            gamepad: JS::from_ref(gamepad),
+        }
+    }
+
+    pub fn new(global: &GlobalScope,
+               type_: Atom,
+               bubbles: bool,
+               cancelable: bool,
+               gamepad: &Gamepad)
+               -> Root<GamepadEvent> {
+        let ev = reflect_dom_object(box GamepadEvent::new_inherited(&gamepad),
+                           global,
+                           GamepadEventBinding::Wrap);
+        {
+            let event = ev.upcast::<Event>();
+            event.init_event(type_, bubbles, cancelable);
+        }
+        ev
+    }
+
+    pub fn new_with_type(global: &GlobalScope, event_type: GamepadEventType, gamepad: &Gamepad)
+                         -> Root<GamepadEvent> {
+        let name = match event_type {
+            GamepadEventType::Connected => "gamepadconnected",
+            GamepadEventType::Disconnected => "gamepaddisconnected"
+        };
+
+        GamepadEvent::new(&global,
+                          name.into(),
+                          false,
+                          false,
+                          &gamepad)
+    }
+
+    // https://w3c.github.io/gamepad/#gamepadevent-interface
+    pub fn Constructor(window: &Window,
+                       type_: DOMString,
+                       init: &GamepadEventBinding::GamepadEventInit)
+                       -> Fallible<Root<GamepadEvent>> {
+        Ok(GamepadEvent::new(&window.global(),
+                             Atom::from(type_),
+                             init.parent.bubbles,
+                             init.parent.cancelable,
+                             &init.gamepad))
+    }
+}
+
+impl GamepadEventMethods for GamepadEvent {
+    // https://w3c.github.io/gamepad/#gamepadevent-interface
+    fn Gamepad(&self) -> Root<Gamepad> {
+        Root::from_ref(&*self.gamepad)
+    }
+
+    // https://dom.spec.whatwg.org/#dom-event-istrusted
+    fn IsTrusted(&self) -> bool {
+        self.event.IsTrusted()
+    }
+}
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/gamepadlist.rs
@@ -0,0 +1,61 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::GamepadListBinding;
+use dom::bindings::codegen::Bindings::GamepadListBinding::GamepadListMethods;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::reflector::{Reflector, reflect_dom_object};
+use dom::gamepad::Gamepad;
+use dom::globalscope::GlobalScope;
+use dom_struct::dom_struct;
+
+// https://www.w3.org/TR/gamepad/
+#[dom_struct]
+pub struct GamepadList {
+    reflector_: Reflector,
+    list: DOMRefCell<Vec<JS<Gamepad>>>
+}
+
+impl GamepadList {
+    fn new_inherited(list: &[&Gamepad]) -> GamepadList {
+        GamepadList {
+            reflector_: Reflector::new(),
+            list: DOMRefCell::new(list.iter().map(|g| JS::from_ref(&**g)).collect())
+        }
+    }
+
+    pub fn new(global: &GlobalScope, list: &[&Gamepad]) -> Root<GamepadList> {
+        reflect_dom_object(box GamepadList::new_inherited(list),
+                           global,
+                           GamepadListBinding::Wrap)
+    }
+
+    pub fn add_if_not_exists(&self, gamepads: &[Root<Gamepad>]) {
+        for gamepad in gamepads {
+            if !self.list.borrow().iter().any(|g| g.gamepad_id() == gamepad.gamepad_id()) {
+                self.list.borrow_mut().push(JS::from_ref(&*gamepad));
+                // Ensure that the gamepad has the correct index
+                gamepad.update_index(self.list.borrow().len() as i32 - 1);
+            }
+        }
+    }
+}
+
+impl GamepadListMethods for GamepadList {
+    // https://w3c.github.io/gamepad/#dom-navigator-getgamepads
+    fn Length(&self) -> u32 {
+        self.list.borrow().len() as u32
+    }
+
+    // https://w3c.github.io/gamepad/#dom-navigator-getgamepads
+    fn Item(&self, index: u32) -> Option<Root<Gamepad>> {
+        self.list.borrow().get(index as usize).map(|gamepad| Root::from_ref(&**gamepad))
+    }
+
+    // https://w3c.github.io/gamepad/#dom-navigator-getgamepads
+    fn IndexedGetter(&self, index: u32) -> Option<Root<Gamepad>> {
+        self.Item(index)
+    }
+}
--- a/servo/components/script/dom/mod.rs
+++ b/servo/components/script/dom/mod.rs
@@ -285,16 +285,21 @@ pub mod extendableevent;
 pub mod extendablemessageevent;
 pub mod file;
 pub mod filelist;
 pub mod filereader;
 pub mod filereadersync;
 pub mod focusevent;
 pub mod forcetouchevent;
 pub mod formdata;
+pub mod gamepad;
+pub mod gamepadbutton;
+pub mod gamepadbuttonlist;
+pub mod gamepadevent;
+pub mod gamepadlist;
 pub mod globalscope;
 pub mod hashchangeevent;
 pub mod headers;
 pub mod history;
 pub mod htmlanchorelement;
 pub mod htmlappletelement;
 pub mod htmlareaelement;
 pub mod htmlaudioelement;
--- a/servo/components/script/dom/navigator.rs
+++ b/servo/components/script/dom/navigator.rs
@@ -3,46 +3,48 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use dom::bindings::codegen::Bindings::NavigatorBinding;
 use dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
 use dom::bindings::js::{MutNullableJS, Root};
 use dom::bindings::reflector::{Reflector, DomObject, reflect_dom_object};
 use dom::bindings::str::DOMString;
 use dom::bluetooth::Bluetooth;
+use dom::gamepadlist::GamepadList;
 use dom::mimetypearray::MimeTypeArray;
 use dom::navigatorinfo;
 use dom::permissions::Permissions;
 use dom::pluginarray::PluginArray;
 use dom::serviceworkercontainer::ServiceWorkerContainer;
 use dom::vr::VR;
 use dom::window::Window;
 use dom_struct::dom_struct;
-use script_traits::WebVREventMsg;
 
 #[dom_struct]
 pub struct Navigator {
     reflector_: Reflector,
     bluetooth: MutNullableJS<Bluetooth>,
     plugins: MutNullableJS<PluginArray>,
     mime_types: MutNullableJS<MimeTypeArray>,
     service_worker: MutNullableJS<ServiceWorkerContainer>,
     vr: MutNullableJS<VR>,
+    gamepads: MutNullableJS<GamepadList>,
     permissions: MutNullableJS<Permissions>,
 }
 
 impl Navigator {
     fn new_inherited() -> Navigator {
         Navigator {
             reflector_: Reflector::new(),
             bluetooth: Default::default(),
             plugins: Default::default(),
             mime_types: Default::default(),
             service_worker: Default::default(),
             vr: Default::default(),
+            gamepads: Default::default(),
             permissions: Default::default(),
         }
     }
 
     pub fn new(window: &Window) -> Root<Navigator> {
         reflect_dom_object(box Navigator::new_inherited(),
                            window,
                            NavigatorBinding::Wrap)
@@ -123,20 +125,24 @@ impl NavigatorMethods for Navigator {
     }
 
     #[allow(unrooted_must_root)]
     // https://w3c.github.io/webvr/#interface-navigator
     fn Vr(&self) -> Root<VR> {
         self.vr.or_init(|| VR::new(&self.global()))
     }
 
+    // https://www.w3.org/TR/gamepad/#navigator-interface-extension
+    fn GetGamepads(&self) -> Root<GamepadList> {
+        let root = self.gamepads.or_init(|| {
+            GamepadList::new(&self.global(), &[])
+        });
+
+        let vr_gamepads = self.Vr().get_gamepads();
+        root.add_if_not_exists(&vr_gamepads);
+        // TODO: Add not VR related gamepads
+        root
+    }
     // https://w3c.github.io/permissions/#navigator-and-workernavigator-extension
     fn Permissions(&self) -> Root<Permissions> {
         self.permissions.or_init(|| Permissions::new(&self.global()))
     }
 }
-
-impl Navigator {
-     pub fn handle_webvr_event(&self, event: WebVREventMsg) {
-         self.vr.get().expect("Shouldn't arrive here with an empty VR instance")
-                      .handle_webvr_event(event);
-     }
-}
--- a/servo/components/script/dom/vr.rs
+++ b/servo/components/script/dom/vr.rs
@@ -1,44 +1,48 @@
 /* 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 http://mozilla.org/MPL/2.0/. */
 
 use dom::bindings::cell::DOMRefCell;
 use dom::bindings::codegen::Bindings::VRBinding;
 use dom::bindings::codegen::Bindings::VRBinding::VRMethods;
+use dom::bindings::codegen::Bindings::VRDisplayBinding::VRDisplayMethods;
 use dom::bindings::error::Error;
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{JS, Root};
 use dom::bindings::reflector::{DomObject, reflect_dom_object};
 use dom::event::Event;
 use dom::eventtarget::EventTarget;
+use dom::gamepad::Gamepad;
+use dom::gamepadevent::GamepadEventType;
 use dom::globalscope::GlobalScope;
 use dom::promise::Promise;
 use dom::vrdisplay::VRDisplay;
 use dom::vrdisplayevent::VRDisplayEvent;
 use dom_struct::dom_struct;
 use ipc_channel::ipc;
 use ipc_channel::ipc::IpcSender;
-use script_traits::WebVREventMsg;
 use std::rc::Rc;
-use webvr_traits::WebVRMsg;
-use webvr_traits::webvr;
+use webvr_traits::{WebVRDisplayData, WebVRDisplayEvent, WebVREvent, WebVRMsg};
+use webvr_traits::{WebVRGamepadData, WebVRGamepadEvent, WebVRGamepadState};
 
 #[dom_struct]
 pub struct VR {
     eventtarget: EventTarget,
-    displays: DOMRefCell<Vec<JS<VRDisplay>>>
+    displays: DOMRefCell<Vec<JS<VRDisplay>>>,
+    gamepads: DOMRefCell<Vec<JS<Gamepad>>>
 }
 
 impl VR {
     fn new_inherited() -> VR {
         VR {
             eventtarget: EventTarget::new_inherited(),
-            displays: DOMRefCell::new(Vec::new())
+            displays: DOMRefCell::new(Vec::new()),
+            gamepads: DOMRefCell::new(Vec::new()),
         }
     }
 
     pub fn new(global: &GlobalScope) -> Root<VR> {
         let root = reflect_dom_object(box VR::new_inherited(),
                            global,
                            VRBinding::Wrap);
         root.register();
@@ -90,20 +94,20 @@ impl VRMethods for VR {
 }
 
 
 impl VR {
     fn webvr_thread(&self) -> Option<IpcSender<WebVRMsg>> {
         self.global().as_window().webvr_thread()
     }
 
-    fn find_display(&self, display_id: u64) -> Option<Root<VRDisplay>> {
+    fn find_display(&self, display_id: u32) -> Option<Root<VRDisplay>> {
         self.displays.borrow()
                      .iter()
-                     .find(|d| d.get_display_id() == display_id)
+                     .find(|d| d.DisplayId() == display_id)
                      .map(|d| Root::from_ref(&**d))
     }
 
     fn register(&self) {
         if let Some(webvr_thread) = self.webvr_thread() {
              let msg = WebVRMsg::RegisterContext(self.global().pipeline_id());
              webvr_thread.send(msg).unwrap();
         }
@@ -111,51 +115,137 @@ impl VR {
 
     fn unregister(&self) {
         if let Some(webvr_thread) = self.webvr_thread() {
              let msg = WebVRMsg::UnregisterContext(self.global().pipeline_id());
              webvr_thread.send(msg).unwrap();
         }
     }
 
-    fn sync_display(&self, display: &webvr::VRDisplayData) -> Root<VRDisplay> {
+    fn sync_display(&self, display: &WebVRDisplayData) -> Root<VRDisplay> {
         if let Some(existing) = self.find_display(display.display_id) {
             existing.update_display(&display);
             existing
         } else {
             let root = VRDisplay::new(&self.global(), display.clone());
             self.displays.borrow_mut().push(JS::from_ref(&*root));
             root
         }
     }
 
-    pub fn handle_webvr_event(&self, event: WebVREventMsg) {
-        let WebVREventMsg::DisplayEvent(event) = event;
-        match &event {
-            &webvr::VRDisplayEvent::Connect(ref display) => {
+    fn handle_display_event(&self, event: WebVRDisplayEvent) {
+        match event {
+            WebVRDisplayEvent::Connect(ref display) => {
                 let display = self.sync_display(&display);
                 display.handle_webvr_event(&event);
-                self.notify_event(&display, &event);
+                self.notify_display_event(&display, &event);
             },
-            &webvr::VRDisplayEvent::Disconnect(id) => {
+            WebVRDisplayEvent::Disconnect(id) => {
                 if let Some(display) = self.find_display(id) {
                     display.handle_webvr_event(&event);
-                    self.notify_event(&display, &event);
+                    self.notify_display_event(&display, &event);
                 }
             },
-            &webvr::VRDisplayEvent::Activate(ref display, _) |
-            &webvr::VRDisplayEvent::Deactivate(ref display, _) |
-            &webvr::VRDisplayEvent::Blur(ref display) |
-            &webvr::VRDisplayEvent::Focus(ref display) |
-            &webvr::VRDisplayEvent::PresentChange(ref display, _) |
-            &webvr::VRDisplayEvent::Change(ref display) => {
+            WebVRDisplayEvent::Activate(ref display, _) |
+            WebVRDisplayEvent::Deactivate(ref display, _) |
+            WebVRDisplayEvent::Blur(ref display) |
+            WebVRDisplayEvent::Focus(ref display) |
+            WebVRDisplayEvent::PresentChange(ref display, _) |
+            WebVRDisplayEvent::Change(ref display) => {
                 let display = self.sync_display(&display);
                 display.handle_webvr_event(&event);
             }
         };
     }
 
-    fn notify_event(&self, display: &VRDisplay, event: &webvr::VRDisplayEvent) {
+    fn handle_gamepad_event(&self, event: WebVRGamepadEvent) {
+        match event {
+            WebVRGamepadEvent::Connect(data, state) => {
+                if let Some(gamepad) = self.find_gamepad(state.gamepad_id) {
+                    gamepad.update_from_vr(&state);
+                } else {
+                    // new gamepad
+                    self.sync_gamepad(Some(data), &state);
+                }
+            },
+            WebVRGamepadEvent::Disconnect(id) => {
+                if let Some(gamepad) = self.find_gamepad(id) {
+                    gamepad.update_connected(false);
+                }
+            }
+        };
+    }
+
+    pub fn handle_webvr_event(&self, event: WebVREvent) {
+        match event {
+            WebVREvent::Display(event) => {
+                self.handle_display_event(event);
+            },
+            WebVREvent::Gamepad(event) => {
+                self.handle_gamepad_event(event);
+            }
+        };
+    }
+
+    pub fn handle_webvr_events(&self, events: Vec<WebVREvent>) {
+        for event in events {
+            self.handle_webvr_event(event);
+        }
+    }
+
+    fn notify_display_event(&self, display: &VRDisplay, event: &WebVRDisplayEvent) {
         let event = VRDisplayEvent::new_from_webvr(&self.global(), &display, &event);
         event.upcast::<Event>().fire(self.upcast());
     }
 }
 
+// Gamepad
+impl VR {
+    fn find_gamepad(&self, gamepad_id: u32) -> Option<Root<Gamepad>> {
+        self.gamepads.borrow()
+                     .iter()
+                     .find(|g| g.gamepad_id() == gamepad_id)
+                     .map(|g| Root::from_ref(&**g))
+    }
+
+    fn sync_gamepad(&self, data: Option<WebVRGamepadData>, state: &WebVRGamepadState) {
+        if let Some(existing) = self.find_gamepad(state.gamepad_id) {
+            existing.update_from_vr(&state);
+        } else {
+            let index = self.gamepads.borrow().len();
+            let data = data.unwrap_or_default();
+            let root = Gamepad::new_from_vr(&self.global(),
+                                            index as i32,
+                                            &data,
+                                            &state);
+            self.gamepads.borrow_mut().push(JS::from_ref(&*root));
+            if state.connected {
+                root.notify_event(GamepadEventType::Connected);
+            }
+        }
+    }
+
+    // Gamepads are synced immediately in response to the API call.
+    // The current approach allows the to sample gamepad state multiple times per frame. This
+    // guarantees that the gamepads always have a valid state and can be very useful for
+    // motion capture or drawing applications.
+    pub fn get_gamepads(&self) -> Vec<Root<Gamepad>> {
+        if let Some(wevbr_sender) = self.webvr_thread() {
+            let (sender, receiver) = ipc::channel().unwrap();
+            let synced_ids = self.gamepads.borrow().iter().map(|g| g.gamepad_id()).collect();
+            wevbr_sender.send(WebVRMsg::GetGamepads(synced_ids, sender)).unwrap();
+            match receiver.recv().unwrap() {
+                Ok(gamepads) => {
+                    // Sync displays
+                    for gamepad in gamepads {
+                        self.sync_gamepad(gamepad.0, &gamepad.1);
+                    }
+                },
+                Err(_) => {}
+            }
+        }
+
+        // We can add other not VR related gamepad providers here
+        self.gamepads.borrow().iter()
+                              .map(|g| Root::from_ref(&**g))
+                              .collect()
+    }
+}
--- a/servo/components/script/dom/vrdisplay.rs
+++ b/servo/components/script/dom/vrdisplay.rs
@@ -156,17 +156,17 @@ impl VRDisplayMethods for VRDisplay {
         match eye {
             VREye::Left => Root::from_ref(&*self.left_eye_params.get()),
             VREye::Right => Root::from_ref(&*self.right_eye_params.get())
         }
     }
 
     // https://w3c.github.io/webvr/#dom-vrdisplay-displayid
     fn DisplayId(&self) -> u32 {
-        self.display.borrow().display_id as u32
+        self.display.borrow().display_id
     }
 
     // https://w3c.github.io/webvr/#dom-vrdisplay-displayname
     fn DisplayName(&self) -> DOMString {
         DOMString::from(self.display.borrow().display_name.clone())
     }
 
     // https://w3c.github.io/webvr/#dom-vrdisplay-getframedata-framedata-framedata
@@ -183,17 +183,17 @@ impl VRDisplayMethods for VRDisplay {
             }
             frameData.update(& self.frame_data.borrow());
             return true;
         }
 
         // If not presenting we fetch inmediante VRFrameData
         let (sender, receiver) = ipc::channel().unwrap();
         self.webvr_thread().send(WebVRMsg::GetFrameData(self.global().pipeline_id(),
-                                                        self.get_display_id(),
+                                                        self.DisplayId(),
                                                         self.depth_near.get(),
                                                         self.depth_far.get(),
                                                         sender)).unwrap();
         return match receiver.recv().unwrap() {
             Ok(data) => {
                 frameData.update(&data);
                 true
             },
@@ -208,17 +208,17 @@ impl VRDisplayMethods for VRDisplay {
     fn GetPose(&self) -> Root<VRPose> {
         VRPose::new(&self.global(), &self.frame_data.borrow().pose)
     }
 
     // https://w3c.github.io/webvr/#dom-vrdisplay-resetpose
     fn ResetPose(&self) {
         let (sender, receiver) = ipc::channel().unwrap();
         self.webvr_thread().send(WebVRMsg::ResetPose(self.global().pipeline_id(),
-                                                     self.get_display_id(),
+                                                     self.DisplayId(),
                                                      sender)).unwrap();
         if let Ok(data) = receiver.recv().unwrap() {
             // Some VRDisplay data might change after calling ResetPose()
             *self.display.borrow_mut() = data;
         }
     }
 
     // https://w3c.github.io/webvr/#dom-vrdisplay-depthnear
@@ -373,32 +373,28 @@ impl VRDisplayMethods for VRDisplay {
     // https://w3c.github.io/webvr/#dom-vrdisplay-submitframe
     fn SubmitFrame(&self) {
         if !self.presenting.get() {
             warn!("VRDisplay not presenting");
             return;
         }
 
         let api_sender = self.layer_ctx.get().unwrap().ipc_renderer();
-        let display_id = self.display.borrow().display_id;
+        let display_id = self.display.borrow().display_id as u64;
         let layer = self.layer.borrow();
         let msg = VRCompositorCommand::SubmitFrame(display_id, layer.left_bounds, layer.right_bounds);
         api_sender.send(CanvasMsg::WebVR(msg)).unwrap();
     }
 }
 
 impl VRDisplay {
     fn webvr_thread(&self) -> IpcSender<WebVRMsg> {
         self.global().as_window().webvr_thread().expect("Shouldn't arrive here with WebVR disabled")
     }
 
-    pub fn get_display_id(&self) -> u64 {
-        self.display.borrow().display_id
-    }
-
     pub fn update_display(&self, display: &WebVRDisplayData) {
         *self.display.borrow_mut() = display.clone();
         if let Some(ref stage) = display.stage_parameters {
             if self.stage_params.get().is_none() {
                 let params = Some(VRStageParameters::new(stage.clone(), &self.global()));
                 self.stage_params.set(params.as_ref().map(|v| v.deref()));
             } else {
                 self.stage_params.get().unwrap().update(&stage);
@@ -442,17 +438,17 @@ impl VRDisplay {
         event.upcast::<Event>().fire(self.upcast());
     }
 
     fn init_present(&self) {
         self.presenting.set(true);
         let (sync_sender, sync_receiver) = ipc::channel().unwrap();
         *self.frame_data_receiver.borrow_mut() = Some(sync_receiver);
 
-        let display_id = self.display.borrow().display_id;
+        let display_id = self.display.borrow().display_id as u64;
         let api_sender = self.layer_ctx.get().unwrap().ipc_renderer();
         let js_sender = self.global().script_chan();
         let address = Trusted::new(&*self);
         let near_init = self.depth_near.get();
         let far_init = self.depth_far.get();
 
         // The render loop at native headset frame rate is implemented using a dedicated thread.
         // Every loop iteration syncs pose data with the HMD, submits the pixels to the display and waits for Vsync.
@@ -492,17 +488,17 @@ impl VRDisplay {
         }).expect("Thread spawning failed");
     }
 
     fn stop_present(&self) {
         self.presenting.set(false);
         *self.frame_data_receiver.borrow_mut() = None;
 
         let api_sender = self.layer_ctx.get().unwrap().ipc_renderer();
-        let display_id = self.display.borrow().display_id;
+        let display_id = self.display.borrow().display_id as u64;
         let msg = VRCompositorCommand::Release(display_id);
         api_sender.send(CanvasMsg::WebVR(msg)).unwrap();
     }
 
     // Only called when the JSContext is destroyed while presenting.
     // In this case we don't want to wait for WebVR Thread response.
     fn force_stop_present(&self) {
         self.webvr_thread().send(WebVRMsg::ExitPresent(self.global().pipeline_id(),
--- a/servo/components/script/dom/vrpose.rs
+++ b/servo/components/script/dom/vrpose.rs
@@ -27,17 +27,19 @@ pub struct VRPose {
 
 #[allow(unsafe_code)]
 unsafe fn update_or_create_typed_array(cx: *mut JSContext,
                       src: Option<&[f32]>,
                       dst: &Heap<*mut JSObject>) {
     match src {
         Some(data) => {
             if dst.get().is_null() {
-                let _ = Float32Array::create(cx, CreateWith::Slice(data), dst.handle_mut());
+                rooted!(in (cx) let mut array = ptr::null_mut());
+                let _ = Float32Array::create(cx, CreateWith::Slice(data), array.handle_mut());
+                (*dst).set(array.get());
             } else {
                 typedarray!(in(cx) let array: Float32Array = dst.get());
                 if let Ok(mut array) = array {
                     array.update(data);
                 }
             }
         },
         None => {
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/Gamepad.webidl
@@ -0,0 +1,26 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+// https://w3c.github.io/gamepad/#gamepad-interface
+[Pref="dom.gamepad.enabled"]
+interface Gamepad {
+    readonly attribute DOMString id;
+    readonly attribute long index;
+    readonly attribute boolean connected;
+    readonly attribute DOMHighResTimeStamp timestamp;
+    readonly attribute DOMString mapping;
+    readonly attribute Float64Array axes;
+    [SameObject] readonly attribute GamepadButtonList buttons;
+};
+
+// https://w3c.github.io/gamepad/extensions.html#dom-gamepad
+partial interface Gamepad {
+  readonly attribute DOMString hand;
+  readonly attribute VRPose? pose;
+};
+
+// https://w3c.github.io/webvr/spec/1.1/#interface-gamepad
+partial interface Gamepad {
+  readonly attribute unsigned long displayId;
+};
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/GamepadButton.webidl
@@ -0,0 +1,11 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+// https://w3c.github.io/gamepad/#gamepadbutton-interface
+[Pref="dom.gamepad.enabled"]
+interface GamepadButton {
+    readonly attribute boolean pressed;
+    readonly attribute boolean touched;
+    readonly attribute double value;
+};
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/GamepadButtonList.webidl
@@ -0,0 +1,10 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+// https://w3c.github.io/gamepad/#dom-gamepad-buttons
+[Pref="dom.gamepad.enabled"]
+interface GamepadButtonList {
+  getter GamepadButton? item(unsigned long index);
+  readonly attribute unsigned long length;
+};
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/GamepadEvent.webidl
@@ -0,0 +1,13 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+// https://w3c.github.io/gamepad/#gamepadevent-interface
+[Pref="dom.gamepad.enabled", Constructor(DOMString type, GamepadEventInit eventInitDict)]
+interface GamepadEvent : Event {
+  readonly attribute Gamepad gamepad;
+};
+
+dictionary GamepadEventInit : EventInit {
+  required Gamepad gamepad;
+};
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/GamepadList.webidl
@@ -0,0 +1,10 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+// https://w3c.github.io/gamepad/#navigator-interface-extension
+[Pref="dom.gamepad.enabled"]
+interface GamepadList {
+  getter Gamepad? item(unsigned long index);
+  readonly attribute unsigned long length;
+};
--- a/servo/components/script/dom/webidls/Navigator.webidl
+++ b/servo/components/script/dom/webidls/Navigator.webidl
@@ -63,8 +63,13 @@ partial interface Navigator {
   [SameObject, Pref="dom.webvr.enabled"] readonly attribute VR vr;
 };
 
 // https://w3c.github.io/permissions/#navigator-and-workernavigator-extension
 [Exposed=(Window)]
 partial interface Navigator {
   [Pref="dom.permissions.enabled"] readonly attribute Permissions permissions;
 };
+
+// https://w3c.github.io/gamepad/#navigator-interface-extension
+partial interface Navigator {
+    [Pref="dom.gamepad.enabled"] GamepadList getGamepads();
+};
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -22,16 +22,17 @@ use devtools;
 use devtools_traits::{DevtoolScriptControlMsg, DevtoolsPageInfo};
 use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
 use devtools_traits::CSSError;
 use document_loader::DocumentLoader;
 use dom::bindings::cell::DOMRefCell;
 use dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
 use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, DocumentReadyState};
 use dom::bindings::codegen::Bindings::EventBinding::EventInit;
+use dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
 use dom::bindings::codegen::Bindings::TransitionEventBinding::TransitionEventInit;
 use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
 use dom::bindings::conversions::{ConversionResult, FromJSValConvertible, StringificationBehavior};
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{JS, MutNullableJS, Root, RootCollection};
 use dom::bindings::js::{RootCollectionPtr, RootedReference};
 use dom::bindings::num::Finite;
 use dom::bindings::reflector::DomObject;
@@ -86,17 +87,16 @@ use script_runtime::{ScriptPort, StackRo
 use script_traits::{CompositorEvent, ConstellationControlMsg};
 use script_traits::{DocumentActivity, DiscardBrowsingContext, EventResult};
 use script_traits::{InitialScriptState, LayoutMsg, LoadData, MouseButton, MouseEventType, MozBrowserEvent};
 use script_traits::{NewLayoutInfo, ScriptMsg as ConstellationMsg};
 use script_traits::{ScriptThreadFactory, TimerEvent, TimerSchedulerMsg, TimerSource};
 use script_traits::{TouchEventType, TouchId, UntrustedNodeAddress, WindowSizeData, WindowSizeType};
 use script_traits::CompositorEvent::{KeyEvent, MouseButtonEvent, MouseMoveEvent, ResizeEvent};
 use script_traits::CompositorEvent::{TouchEvent, TouchpadPressureEvent};
-use script_traits::WebVREventMsg;
 use script_traits::webdriver_msg::WebDriverScriptCommand;
 use serviceworkerjob::{Job, JobQueue, AsyncJobHandler};
 use servo_config::opts;
 use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
 use std::cell::Cell;
 use std::collections::{hash_map, HashMap, HashSet};
 use std::default::Default;
 use std::ops::Deref;
@@ -114,17 +114,17 @@ use style::thread_state;
 use task_source::dom_manipulation::{DOMManipulationTask, DOMManipulationTaskSource};
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::user_interaction::{UserInteractionTask, UserInteractionTaskSource};
 use time::Tm;
 use url::Position;
 use webdriver_handlers;
-use webvr_traits::WebVRMsg;
+use webvr_traits::{WebVREvent, WebVRMsg};
 
 pub type ImageCacheMsg = (PipelineId, PendingImageResponse);
 
 thread_local!(pub static STACK_ROOTS: Cell<Option<RootCollectionPtr>> = Cell::new(None));
 thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = Cell::new(None));
 
 pub unsafe fn trace_thread(tr: *mut JSTracer) {
     SCRIPT_THREAD_ROOT.with(|root| {
@@ -1065,18 +1065,18 @@ impl ScriptThread {
             ConstellationControlMsg::FramedContentChanged(parent_pipeline_id, frame_id) =>
                 self.handle_framed_content_changed(parent_pipeline_id, frame_id),
             ConstellationControlMsg::ReportCSSError(pipeline_id, filename, line, column, msg) =>
                 self.handle_css_error_reporting(pipeline_id, filename, line, column, msg),
             ConstellationControlMsg::Reload(pipeline_id) =>
                 self.handle_reload(pipeline_id),
             ConstellationControlMsg::ExitPipeline(pipeline_id, discard_browsing_context) =>
                 self.handle_exit_pipeline_msg(pipeline_id, discard_browsing_context),
-            ConstellationControlMsg::WebVREvent(pipeline_id, event) =>
-                self.handle_webvr_event(pipeline_id, event),
+            ConstellationControlMsg::WebVREvents(pipeline_id, events) =>
+                self.handle_webvr_events(pipeline_id, events),
             msg @ ConstellationControlMsg::AttachLayout(..) |
             msg @ ConstellationControlMsg::Viewport(..) |
             msg @ ConstellationControlMsg::SetScrollState(..) |
             msg @ ConstellationControlMsg::Resize(..) |
             msg @ ConstellationControlMsg::ExitScriptThread =>
                       panic!("should have handled {:?} already", msg),
         }
     }
@@ -2181,21 +2181,21 @@ impl ScriptThread {
 
     fn handle_reload(&self, pipeline_id: PipelineId) {
         let window = self.documents.borrow().find_window(pipeline_id);
         if let Some(window) = window {
             window.Location().reload_without_origin_check();
         }
     }
 
-    fn handle_webvr_event(&self, pipeline_id: PipelineId, event: WebVREventMsg) {
+    fn handle_webvr_events(&self, pipeline_id: PipelineId, events: Vec<WebVREvent>) {
         let window = self.documents.borrow().find_window(pipeline_id);
         if let Some(window) = window {
-            let navigator = window.Navigator();
-            navigator.handle_webvr_event(event);
+            let vr = window.Navigator().Vr();
+            vr.handle_webvr_events(events);
         }
     }
 
     pub fn enqueue_microtask(job: Microtask) {
         SCRIPT_THREAD_ROOT.with(|root| {
             let script_thread = unsafe { &*root.get().unwrap() };
             script_thread.microtask_queue.enqueue(job);
         });
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -66,17 +66,17 @@ use serde::{Deserialize, Deserializer, S
 use servo_url::ImmutableOrigin;
 use servo_url::ServoUrl;
 use std::collections::HashMap;
 use std::fmt;
 use std::sync::Arc;
 use std::sync::mpsc::{Receiver, Sender};
 use style_traits::{CSSPixel, UnsafeNode};
 use webdriver_msg::{LoadStatus, WebDriverScriptCommand};
-use webvr_traits::{WebVRDisplayEvent, WebVRMsg};
+use webvr_traits::{WebVREvent, WebVRMsg};
 
 pub use script_msg::{LayoutMsg, ScriptMsg, EventResult, LogEntry};
 pub use script_msg::{ServiceWorkerMsg, ScopeThings, SWManagerMsg, SWManagerSenders, DOMMessage};
 
 /// The address of a node. Layout sends these back. They must be validated via
 /// `from_untrusted_node_address` before they can be used, because we do not trust layout.
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct UntrustedNodeAddress(pub *const c_void);
@@ -275,18 +275,18 @@ pub enum ConstellationControlMsg {
     DispatchStorageEvent(PipelineId, StorageType, ServoUrl, Option<String>, Option<String>, Option<String>),
     /// Notifies a parent pipeline that one of its child frames is now active.
     /// PipelineId is for the parent, FrameId is the child frame.
     FramedContentChanged(PipelineId, FrameId),
     /// Report an error from a CSS parser for the given pipeline
     ReportCSSError(PipelineId, String, usize, usize, String),
     /// Reload the given page.
     Reload(PipelineId),
-    /// Notifies the script thread of a WebVR device event
-    WebVREvent(PipelineId, WebVREventMsg)
+    /// Notifies the script thread of WebVR events.
+    WebVREvents(PipelineId, Vec<WebVREvent>)
 }
 
 impl fmt::Debug for ConstellationControlMsg {
     fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
         use self::ConstellationControlMsg::*;
         let variant = match *self {
             AttachLayout(..) => "AttachLayout",
             Resize(..) => "Resize",
@@ -309,17 +309,17 @@ impl fmt::Debug for ConstellationControl
             TickAllAnimations(..) => "TickAllAnimations",
             TransitionEnd(..) => "TransitionEnd",
             WebFontLoaded(..) => "WebFontLoaded",
             DispatchFrameLoadEvent { .. } => "DispatchFrameLoadEvent",
             DispatchStorageEvent(..) => "DispatchStorageEvent",
             FramedContentChanged(..) => "FramedContentChanged",
             ReportCSSError(..) => "ReportCSSError",
             Reload(..) => "Reload",
-            WebVREvent(..) => "WebVREvent",
+            WebVREvents(..) => "WebVREvents",
         };
         write!(formatter, "ConstellationMsg::{}", variant)
     }
 }
 
 /// Used to determine if a script has any pending asynchronous activity.
 #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
 pub enum DocumentState {
@@ -746,26 +746,18 @@ pub enum ConstellationMsg {
     /// Dispatch a webdriver command
     WebDriverCommand(WebDriverCommandMsg),
     /// Reload the current page.
     Reload,
     /// A log entry, with the top-level frame id and thread name
     LogEntry(Option<FrameId>, Option<String>, LogEntry),
     /// Set the WebVR thread channel.
     SetWebVRThread(IpcSender<WebVRMsg>),
-    /// Dispatch a WebVR event to the subscribed script threads.
-    WebVREvent(Vec<PipelineId>, WebVREventMsg),
-}
-
-/// Messages to the constellation originating from the WebVR thread.
-/// Used to dispatch VR Headset state events: connected, unconnected, and more.
-#[derive(Deserialize, Serialize, Clone)]
-pub enum WebVREventMsg {
-    /// Inform the constellation of a VR display event.
-    DisplayEvent(WebVRDisplayEvent)
+    /// Dispatch WebVR events to the subscribed script threads.
+    WebVREvents(Vec<PipelineId>, Vec<WebVREvent>),
 }
 
 /// Resources required by workerglobalscopes
 #[derive(Serialize, Deserialize, Clone)]
 pub struct WorkerGlobalScopeInit {
     /// Chan to a resource thread
     pub resource_threads: ResourceThreads,
     /// Chan to the memory profiler
--- a/servo/components/webvr/webvr_thread.rs
+++ b/servo/components/webvr/webvr_thread.rs
@@ -1,16 +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 http://mozilla.org/MPL/2.0/. */
 
 use ipc_channel::ipc;
 use ipc_channel::ipc::{IpcReceiver, IpcSender};
 use msg::constellation_msg::PipelineId;
-use script_traits::{ConstellationMsg, WebVREventMsg};
+use script_traits::ConstellationMsg;
 use servo_config::prefs::PREFS;
 use std::{thread, time};
 use std::collections::{HashMap, HashSet};
 use std::sync::mpsc;
 use std::sync::mpsc::{Receiver, Sender};
 use webrender_traits;
 use webvr_traits::{WebVRMsg, WebVRResult};
 use webvr_traits::webvr::*;
@@ -37,17 +37,17 @@ use webvr_traits::webvr::*;
 pub struct WebVRThread {
     receiver: IpcReceiver<WebVRMsg>,
     sender: IpcSender<WebVRMsg>,
     service: VRServiceManager,
     contexts: HashSet<PipelineId>,
     constellation_chan: Sender<ConstellationMsg>,
     vr_compositor_chan: WebVRCompositorSender,
     polling_events: bool,
-    presenting: HashMap<u64, PipelineId>
+    presenting: HashMap<u32, PipelineId>
 }
 
 impl WebVRThread {
     fn new(receiver: IpcReceiver<WebVRMsg>,
            sender: IpcSender<WebVRMsg>,
            constellation_chan: Sender<ConstellationMsg>,
            vr_compositor_chan: WebVRCompositorSender)
            -> WebVRThread {
@@ -103,16 +103,19 @@ impl WebVRThread {
                     self.handle_request_present(pipeline_id, display_id, sender);
                 },
                 WebVRMsg::ExitPresent(pipeline_id, display_id, sender) => {
                     self.handle_exit_present(pipeline_id, display_id, sender);
                 },
                 WebVRMsg::CreateCompositor(display_id) => {
                     self.handle_create_compositor(display_id);
                 },
+                WebVRMsg::GetGamepads(synced_ids, sender) => {
+                    self.handle_get_gamepads(synced_ids, sender);
+                }
                 WebVRMsg::Exit => {
                     break
                 },
             }
         }
     }
 
     fn handle_register_context(&mut self, ctx: PipelineId) {
@@ -129,31 +132,31 @@ impl WebVRThread {
         for display in displays {
             result.push(display.borrow().data());
         }
         sender.send(Ok(result)).unwrap();
     }
 
     fn handle_framedata(&mut self,
                         pipeline: PipelineId,
-                        display_id: u64,
+                        display_id: u32,
                         near: f64,
                         far: f64,
                         sender: IpcSender<WebVRResult<VRFrameData>>) {
       match self.access_check(pipeline, display_id) {
             Ok(display) => {
                 sender.send(Ok(display.borrow().inmediate_frame_data(near, far))).unwrap()
             },
             Err(msg) => sender.send(Err(msg.into())).unwrap()
         }
     }
 
     fn handle_reset_pose(&mut self,
                          pipeline: PipelineId,
-                         display_id: u64,
+                         display_id: u32,
                          sender: IpcSender<WebVRResult<VRDisplayData>>) {
         match self.access_check(pipeline, display_id) {
             Ok(display) => {
                 display.borrow_mut().reset_pose();
                 sender.send(Ok(display.borrow().data())).unwrap();
             },
             Err(msg) => {
                 sender.send(Err(msg.into())).unwrap()
@@ -161,90 +164,104 @@ impl WebVRThread {
         }
     }
 
     // This method implements the privacy and security guidelines defined in the WebVR spec.
     // For example a secondary tab is not allowed to read VRDisplay data or stop a VR presentation
     // while the user is having a VR experience in the current tab.
     // These security rules also avoid multithreading race conditions between WebVRThread and
     // Webrender thread. See WebVRCompositorHandler implementation notes for more details about this.
-    fn access_check(&self, pipeline: PipelineId, display_id: u64) -> Result<&VRDisplayPtr, &'static str> {
+    fn access_check(&self, pipeline: PipelineId, display_id: u32) -> Result<&VRDisplayPtr, &'static str> {
         if *self.presenting.get(&display_id).unwrap_or(&pipeline) != pipeline {
             return Err("No access granted to this Display because it's presenting on other JavaScript Tab");
         }
         self.service.get_display(display_id).ok_or("Device not found")
     }
 
     fn handle_request_present(&mut self,
                               pipeline: PipelineId,
-                              display_id: u64,
+                              display_id: u32,
                               sender: IpcSender<WebVRResult<()>>) {
         match self.access_check(pipeline, display_id).map(|d| d.clone()) {
             Ok(display) => {
                 self.presenting.insert(display_id, pipeline);
                 let data = display.borrow().data();
                 sender.send(Ok(())).unwrap();
-                self.notify_event(VRDisplayEvent::PresentChange(data, true));
+                self.notify_event(VRDisplayEvent::PresentChange(data, true).into());
             },
             Err(msg) => {
                 sender.send(Err(msg.into())).unwrap();
             }
         }
     }
 
     fn handle_exit_present(&mut self,
                          pipeline: PipelineId,
-                         display_id: u64,
+                         display_id: u32,
                          sender: Option<IpcSender<WebVRResult<()>>>) {
         match self.access_check(pipeline, display_id).map(|d| d.clone()) {
             Ok(display) => {
                 self.presenting.remove(&display_id);
                 if let Some(sender) = sender {
                     sender.send(Ok(())).unwrap();
                 }
                 let data = display.borrow().data();
-                self.notify_event(VRDisplayEvent::PresentChange(data, false));
+                self.notify_event(VRDisplayEvent::PresentChange(data, false).into());
             },
             Err(msg) => {
                 if let Some(sender) = sender {
                     sender.send(Err(msg.into())).unwrap();
                 }
             }
         }
     }
 
-    fn handle_create_compositor(&mut self, display_id: u64) {
+    fn handle_create_compositor(&mut self, display_id: u32) {
         let compositor = self.service.get_display(display_id).map(|d| WebVRCompositor(d.as_ptr()));
         self.vr_compositor_chan.send(compositor).unwrap();
     }
 
+    fn handle_get_gamepads(&mut self,
+                           synced_ids: Vec<u32>,
+                           sender: IpcSender<WebVRResult<Vec<(Option<VRGamepadData>, VRGamepadState)>>>) {
+        let gamepads = self.service.get_gamepads();
+        let data = gamepads.iter().map(|g| {
+            let g = g.borrow();
+            // Optimization, don't fetch and send gamepad static data when the gamepad is already synced.
+            let data = if synced_ids.iter().any(|v| *v == g.id()) {
+                None
+            } else {
+                Some(g.data())
+            };
+            (data, g.state())
+        }).collect();
+        sender.send(Ok(data)).unwrap();
+    }
+
     fn poll_events(&mut self, sender: IpcSender<bool>) {
         loop {
             let events = self.service.poll_events();
             if events.is_empty() {
                 break;
             }
             self.notify_events(events)
         }
 
         // Stop polling events if the callers are not using VR
         self.polling_events = self.contexts.len() > 0;
         sender.send(self.polling_events).unwrap();
     }
 
-    fn notify_events(&self, events: Vec<VRDisplayEvent>) {
+    fn notify_events(&self, events: Vec<VREvent>) {
         let pipeline_ids: Vec<PipelineId> = self.contexts.iter().map(|c| *c).collect();
-        for event in events {
-            let event = WebVREventMsg::DisplayEvent(event);
-            self.constellation_chan.send(ConstellationMsg::WebVREvent(pipeline_ids.clone(), event)).unwrap();
-        }
+        self.constellation_chan.send(ConstellationMsg::WebVREvents(pipeline_ids.clone(), events)).unwrap();
     }
 
     #[inline]
-    fn notify_event(&self, event: VRDisplayEvent) {
+    fn notify_event(&self, event: VREvent) {
         self.notify_events(vec![event]);
     }
 
     fn schedule_poll_events(&mut self) {
         if !self.service.is_initialized() || self.polling_events {
             return;
         }
         self.polling_events = true;
@@ -329,17 +346,18 @@ impl webrender_traits::VRCompositorHandl
                 }
             }
             webrender_traits::VRCompositorCommand::SubmitFrame(compositor_id, left_bounds, right_bounds) => {
                 if let Some(compositor) = self.compositors.get(&compositor_id) {
                     if let Some(texture_id) = texture_id {
                         let layer = VRLayer {
                             texture_id: texture_id,
                             left_bounds: left_bounds,
-                            right_bounds: right_bounds
+                            right_bounds: right_bounds,
+                            texture_size: None
                         };
                         unsafe {
                             (*compositor.0).submit_frame(&layer);
                         }
                     }
                 }
             }
             webrender_traits::VRCompositorCommand::Release(compositor_id) => {
@@ -352,17 +370,17 @@ impl webrender_traits::VRCompositorHandl
 impl WebVRCompositorHandler {
     #[allow(unsafe_code)]
     fn create_compositor(&mut self, display_id: webrender_traits::VRCompositorId) {
         let sender = match self.webvr_thread_sender {
             Some(ref s) => s,
             None => return,
         };
 
-        sender.send(WebVRMsg::CreateCompositor(display_id)).unwrap();
+        sender.send(WebVRMsg::CreateCompositor(display_id as u32)).unwrap();
         let display = self.webvr_thread_receiver.recv().unwrap();
 
         match display {
             Some(display) => {
                 self.compositors.insert(display_id, display);
             },
             None => {
                 error!("VRDisplay not found when creating a new VRCompositor");
--- a/servo/components/webvr_traits/Cargo.toml
+++ b/servo/components/webvr_traits/Cargo.toml
@@ -7,11 +7,11 @@ publish = false
 
 [lib]
 name = "webvr_traits"
 path = "lib.rs"
 
 [dependencies]
 ipc-channel = "0.7"
 msg = {path = "../msg"}
-rust-webvr = {version = "0.2", features = ["serde-serialization"]}
+rust-webvr = {version = "0.3", features = ["serde-serialization"]}
 serde = "0.9"
 serde_derive = "0.9"
--- a/servo/components/webvr_traits/lib.rs
+++ b/servo/components/webvr_traits/lib.rs
@@ -11,16 +11,22 @@ extern crate serde_derive;
 pub extern crate rust_webvr as webvr;
 
 mod webvr_traits;
 
 pub use webvr::VRDisplayData as WebVRDisplayData;
 pub use webvr::VRDisplayCapabilities as WebVRDisplayCapabilities;
 pub use webvr::VRDisplayEvent as WebVRDisplayEvent;
 pub use webvr::VRDisplayEventReason as WebVRDisplayEventReason;
+pub use webvr::VREvent as WebVREvent;
 pub use webvr::VREye as WebVREye;
 pub use webvr::VREyeParameters as WebVREyeParameters;
 pub use webvr::VRFieldOfView as WebVRFieldOfView;
+pub use webvr::VRGamepadButton as WebVRGamepadButton;
+pub use webvr::VRGamepadData as WebVRGamepadData;
+pub use webvr::VRGamepadEvent as WebVRGamepadEvent;
+pub use webvr::VRGamepadHand as WebVRGamepadHand;
+pub use webvr::VRGamepadState as WebVRGamepadState;
 pub use webvr::VRFrameData as WebVRFrameData;
 pub use webvr::VRLayer as WebVRLayer;
 pub use webvr::VRPose as WebVRPose;
 pub use webvr::VRStageParameters as WebVRStageParameters;
 pub use webvr_traits::{WebVRMsg, WebVRResult};
--- a/servo/components/webvr_traits/webvr_traits.rs
+++ b/servo/components/webvr_traits/webvr_traits.rs
@@ -10,15 +10,16 @@ pub type WebVRResult<T> = Result<T, Stri
 
 // Messages from Script thread to WebVR thread.
 #[derive(Deserialize, Serialize)]
 pub enum WebVRMsg {
     RegisterContext(PipelineId),
     UnregisterContext(PipelineId),
     PollEvents(IpcSender<bool>),
     GetDisplays(IpcSender<WebVRResult<Vec<VRDisplayData>>>),
-    GetFrameData(PipelineId, u64, f64, f64, IpcSender<WebVRResult<VRFrameData>>),
-    ResetPose(PipelineId, u64, IpcSender<WebVRResult<VRDisplayData>>),
-    RequestPresent(PipelineId, u64, IpcSender<WebVRResult<()>>),
-    ExitPresent(PipelineId, u64, Option<IpcSender<WebVRResult<()>>>),
-    CreateCompositor(u64),
+    GetFrameData(PipelineId, u32, f64, f64, IpcSender<WebVRResult<VRFrameData>>),
+    ResetPose(PipelineId, u32, IpcSender<WebVRResult<VRDisplayData>>),
+    RequestPresent(PipelineId, u32, IpcSender<WebVRResult<()>>),
+    ExitPresent(PipelineId, u32, Option<IpcSender<WebVRResult<()>>>),
+    CreateCompositor(u32),
+    GetGamepads(Vec<u32>, IpcSender<WebVRResult<Vec<(Option<VRGamepadData>, VRGamepadState)>>>),
     Exit,
 }
--- a/servo/resources/prefs.json
+++ b/servo/resources/prefs.json
@@ -1,12 +1,13 @@
 {
   "dom.bluetooth.enabled": false,
   "dom.bluetooth.testing.enabled": false,
   "dom.forcetouch.enabled": false,
+  "dom.gamepad.enabled": false,
   "dom.mouseevent.which.enabled": false,
   "dom.mozbrowser.enabled": false,
   "dom.mutation_observer.enabled": false,
   "dom.permissions.enabled": false,
   "dom.permissions.testing.allowed_in_nonsecure_contexts": false,
   "dom.serviceworker.timeout_seconds": 60,
   "dom.testable_crash.enabled": false,
   "dom.testbinding.enabled": false,
new file mode 100644
--- /dev/null
+++ b/servo/tests/html/webvr/vr-controllers.html
@@ -0,0 +1,420 @@
+<!doctype html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+
+    <!--Chrome origin trial header-->
+
+    <meta http-equiv="origin-trial" data-feature="WebVR" data-expires="2017-04-13" content="AtpEVUJTjLpU/IMdKA/u8TRWqVUKfA6aJQsonwi01IPxqA16zX7L4BMa9E4g4DdJW80v3N6jqde4pXeqd2GYCg4AAABJeyJvcmlnaW4iOiJodHRwczovL3dlYnZyLmluZm86NDQzIiwiZmVhdHVyZSI6IldlYlZSIiwiZXhwaXJ5IjoxNDkyMTAzODUyfQ==">
+    <title>XX - VR Controllers</title>
+
+    <!--
+      This sample demonstrates how to handle gamepads with 6DoF support, such as
+      the Vive controllers or Oculus touch. PLEASE NOTE: The additions to the
+      gamepad API used here are not yet part of the standard, and subject to
+      change at any time!
+    -->
+
+    <style>
+      #webgl-canvas {
+        box-sizing: border-box;
+        height: 100%;
+        left: 0;
+        margin: 0;
+        position: absolute;
+        top: 0;
+        width: 100%;
+      }
+    </style>
+
+    <!-- This entire block in only to facilitate dynamically enabling and
+    disabling the WebVR polyfill, and is not necessary for most WebVR apps.
+    If you want to use the polyfill in your app, just include the js file and
+    everything will work the way you want it to by default. -->
+    <script>
+      var WebVRConfig = {
+        // Prevents the polyfill from initializing automatically.
+        DEFER_INITIALIZATION: true,
+        // Ensures the polyfill is always active when initialized, even if the
+        // native API is available. This is probably NOT what most pages want.
+        POLYFILL_MODE: "ALWAYS",
+        // Polyfill optimizations
+        DIRTY_SUBMIT_FRAME_BINDINGS: true,
+        BUFFER_SCALE: 0.75,
+      };
+    </script>
+    <script src="js/third-party/webvr-polyfill.js"></script>
+    <script src="js/third-party/wglu/wglu-url.js"></script>
+    <script>
+      // Dynamically turn the polyfill on if requested by the query args.
+      if (WGLUUrl.getBool('polyfill', false)) {
+        InitializeWebVRPolyfill();
+      } else {
+        // Shim for migration from older version of WebVR. Shouldn't be necessary for very long.
+        InitializeSpecShim();
+      }
+    </script>
+    <!-- End sample polyfill enabling logic -->
+
+    <script src="js/third-party/gl-matrix-min.js"></script>
+
+    <script src="js/third-party/wglu/wglu-debug-geometry.js"></script>
+    <script src="js/third-party/wglu/wglu-program.js"></script>
+    <script src="js/third-party/wglu/wglu-stats.js"></script>
+    <script src="js/third-party/wglu/wglu-texture.js"></script>
+
+    <script src="js/vr-cube-island.js"></script>
+    <script src="js/vr-samples-util.js"></script>
+  </head>
+  <body>
+    <canvas id="webgl-canvas"></canvas>
+    <script>
+      /* global mat4, vec3, VRCubeIsland, WGLUDebugGeometry, WGLUStats, WGLUTextureLoader, VRSamplesUtil */
+      (function () {
+      "use strict";
+
+      var PLAYER_HEIGHT = 1.65;
+
+      var vrDisplay = null;
+      var frameData = null;
+      var projectionMat = mat4.create();
+      var viewMat = mat4.create();
+      var poseMat = mat4.create();
+      var gamepadMat = mat4.create();
+      var gamepadMat2 = mat4.create();
+      var gamepadColor = vec4.create();
+      var standingPosition = vec3.create();
+      var vrPresentButton = null;
+      var orientation = [0, 0, 0, 1];
+      var position = [0, 0, 0];
+
+
+      window.addEventListener("gamepadconnected", function(ev) {
+        var gamepad = ev.gamepad;
+        console.log("Gamepad connected", gamepad.index, gamepad.id, gamepad.displayId);
+      });
+
+        window.addEventListener("gamepaddisconnected", function(ev) {
+        var gamepad = ev.gamepad;
+        console.log("Gamepad disconnected", gamepad.index, gamepad.id, gamepad.displayId);
+      });
+
+      // ===================================================
+      // WebGL scene setup. This code is not WebVR specific.
+      // ===================================================
+
+      // WebGL setup.
+      var webglCanvas = document.getElementById("webgl-canvas");
+      var gl = null;
+      var cubeIsland = null;
+      var stats = null;
+      var debugGeom = null;
+
+      function initWebGL (preserveDrawingBuffer) {
+        var glAttribs = {
+          alpha: false,
+          antialias: false,
+          preserveDrawingBuffer: false
+        };
+        gl = webglCanvas.getContext("webgl", glAttribs);
+        if (!gl) {
+          gl = webglCanvas.getContext("experimental-webgl", glAttribs);
+          if (!gl) {
+            VRSamplesUtil.addError("Your browser does not support WebGL.");
+            return;
+          }
+        }
+        //gl.clearColor(0.1, 0.2, 0.3, 1.0);
+        gl.clearColor(0.0, 119.0/255.0, 51.0/255.0, 1.0);
+        gl.enable(gl.DEPTH_TEST);
+        gl.enable(gl.CULL_FACE);
+
+        var textureLoader = new WGLUTextureLoader(gl);
+        var texture = textureLoader.loadTexture("media/textures/cube-sea.png");
+
+        cubeIsland = new VRCubeIsland(gl, texture, 2, 2);
+
+        stats = new WGLUStats(gl);
+        debugGeom = new WGLUDebugGeometry(gl);
+
+        // Wait until we have a WebGL context to resize and start rendering.
+        window.addEventListener("resize", onResize, false);
+        onResize();
+        window.requestAnimationFrame(onAnimationFrame);
+      }
+
+      // ================================
+      // WebVR-specific code begins here.
+      // ================================
+
+      function onVRRequestPresent () {
+        vrDisplay.requestPresent([{ source: webglCanvas }]).then(function () {
+        }, function (err) {
+          var errMsg = "requestPresent failed.";
+          if (err && err.message) {
+            errMsg += "<br/>" + err.message
+          }
+          VRSamplesUtil.addError(errMsg, 2000);
+        });
+      }
+
+      function onVRExitPresent () {
+        if (!vrDisplay.isPresenting)
+          return;
+
+        vrDisplay.exitPresent().then(function () {
+        }, function () {
+          VRSamplesUtil.addError("exitPresent failed.", 2000);
+        });
+      }
+
+      function onVRPresentChange () {
+        onResize();
+
+        if (vrDisplay.isPresenting) {
+          if (vrDisplay.capabilities.hasExternalDisplay) {
+            VRSamplesUtil.removeButton(vrPresentButton);
+            vrPresentButton = VRSamplesUtil.addButton("Exit VR", "E", "media/icons/cardboard64.png", onVRExitPresent);
+          }
+        } else {
+          if (vrDisplay.capabilities.hasExternalDisplay) {
+            VRSamplesUtil.removeButton(vrPresentButton);
+            vrPresentButton = VRSamplesUtil.addButton("Enter VR", "E", "media/icons/cardboard64.png", onVRRequestPresent);
+          }
+        }
+      }
+
+      if (navigator.vr || navigator.getVRDisplays) {
+        frameData = new VRFrameData();
+        navigator.vr.getDisplays().then(function (displays) {
+         // navigator.getVRDisplays().then(function (displays) {
+          if (displays.length > 0) {
+            vrDisplay = displays[displays.length - 1];
+            vrDisplay.depthNear = 0.1;
+            vrDisplay.depthFar = 1024.0;
+
+            initWebGL(true);
+
+            if (vrDisplay.stageParameters &&
+                vrDisplay.stageParameters.sizeX > 0 &&
+                vrDisplay.stageParameters.sizeZ > 0) {
+              cubeIsland.resize(vrDisplay.stageParameters.sizeX, vrDisplay.stageParameters.sizeZ);
+            }
+
+            VRSamplesUtil.addButton("Reset Pose", "R", null, function () { vrDisplay.resetPose(); });
+
+            if (vrDisplay.capabilities.canPresent)
+              vrPresentButton = VRSamplesUtil.addButton("Enter VR", "E", "media/icons/cardboard64.png", onVRRequestPresent);
+
+            vrDisplay.addEventListener('presentchange', onVRPresentChange, false);
+            //vrDisplay.addEventListener('vrdisplayactivate', onVRRequestPresent, false);
+            //vrDisplay.addEventListener('vrdisplaydeactivate', onVRExitPresent, false);
+            console.log(navigator.getGamepads());
+            setTimeout(function(){
+                onVRRequestPresent();
+            }, 5);
+
+          } else {
+            initWebGL(false);
+            VRSamplesUtil.addInfo("WebVR supported, but no VRDisplays found.", 3000);
+          }
+        });
+      } else if (navigator.getVRDevices) {
+        initWebGL(false);
+        VRSamplesUtil.addError("Your browser supports WebVR but not the latest version. See <a href='http://webvr.info'>webvr.info</a> for more info.");
+      } else {
+        initWebGL(false);
+        VRSamplesUtil.addError("Your browser does not support WebVR. See <a href='http://webvr.info'>webvr.info</a> for assistance.");
+      }
+
+      function onResize () {
+        if (vrDisplay && vrDisplay.isPresenting) {
+          var leftEye = vrDisplay.getEyeParameters("left");
+          var rightEye = vrDisplay.getEyeParameters("right");
+
+          webglCanvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2;
+          webglCanvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight);
+        } else {
+          webglCanvas.width = window.innerWidth * 2.0 * window.devicePixelRatio;
+          webglCanvas.height = window.innerHeight * 2.0 *  window.devicePixelRatio;
+        }
+      }
+
+      function onClick() {
+        //onVRRequestPresent();
+      }
+      webglCanvas.addEventListener("click", onClick, false);
+
+      function getStandingViewMatrix (out, view) {
+        if (vrDisplay.stageParameters) {
+          mat4.invert(out, vrDisplay.stageParameters.sittingToStandingTransform);
+          mat4.multiply(out, view, out);
+        } else {
+          mat4.identity(out);
+          mat4.translate(out, out, [0, PLAYER_HEIGHT, 0]);
+          mat4.invert(out, out);
+          mat4.multiply(out, view, out);
+        }
+      }
+
+      function getPoseMatrix (out, pose, isGamepad) {
+        orientation = pose.orientation;
+        position = pose.position;
+        if (!orientation) { orientation = [0, 0, 0, 1]; }
+        if (!position) {
+          // If this is a gamepad without a pose set it out in front of us so
+          // we can see it.
+          position = isGamepad ? [0.1, -0.1, -0.5] : [0, 0, 0];
+        }
+
+
+        if (vrDisplay.stageParameters) {
+          mat4.fromRotationTranslation(out, orientation, position);
+          mat4.multiply(out, vrDisplay.stageParameters.sittingToStandingTransform, out);
+        } else {
+          vec3.add(standingPosition, position, [0, PLAYER_HEIGHT, 0]);
+          mat4.fromRotationTranslation(out, orientation, standingPosition);
+        }
+      }
+
+      function renderSceneView (projection, view, gamepads) {
+        cubeIsland.render(projection, view, stats);
+
+        debugGeom.bind(projection, view);
+
+        // Render every gamepad with a pose we found
+        for (var i = 0; i < gamepads.length; ++i) {
+          var gamepad = gamepads[i];
+
+          // Because this sample is done in standing space we need to apply
+          // the same transformation to the gamepad pose as we did the
+          // VRDisplay's pose.
+          getPoseMatrix(gamepadMat, gamepad.pose, true);
+
+          // Loop through all the gamepad's axes and scale the gamepad geometry
+          // by their value.
+          var scale = [0.1, 0.1, 0.1];
+          for (var j = 0; j < gamepad.axes.length; ++j) {
+            switch (j%3) {
+              case 0:
+                scale[0] *= 1.0 + gamepad.axes[j];
+                break;
+              case 1:
+                scale[1] *= 1.0 + gamepad.axes[j];
+                break;
+              case 2:
+                scale[2] *= 1.0 + gamepad.axes[j];
+                break;
+            }
+          }
+
+          // Scaled down to from 1 meter to be something closer to the size of
+          // a hand.
+          mat4.scale(gamepadMat, gamepadMat, scale);
+
+          // Rotate -90 deg so the point of the cone faces "forward"
+          mat4.rotateX(gamepadMat, gamepadMat, -Math.PI * 0.5);
+
+          // Show the gamepad's cube as red if any buttons are pressed, blue
+          // otherwise.
+          vec4.set(gamepadColor, 0, 0, 1, 1);
+          var buttons = gamepad.buttons;
+          for (var j = 0; j < buttons.length; ++j) {
+            if (buttons[j].pressed) {
+              vec4.set(gamepadColor, buttons[j].value || 1.0, 0, 0, 1);
+              break;
+            }
+          }
+
+          debugGeom.drawConeWithMatrix(gamepadMat, gamepadColor);
+
+          // Draw a "handle" for the gamepad
+          mat4.identity(gamepadMat2);
+          mat4.translate(gamepadMat2, gamepadMat2, [0, -0.5, -0.3]);
+          mat4.rotateX(gamepadMat2, gamepadMat2, -Math.PI * 0.2);
+          mat4.scale(gamepadMat2, gamepadMat2, [0.25, 0.25, 0.5]);
+
+          mat4.multiply(gamepadMat, gamepadMat, gamepadMat2);
+
+          debugGeom.drawBoxWithMatrix(gamepadMat, gamepadColor);
+        }
+      }
+
+      function onAnimationFrame (t) {
+        stats.begin();
+
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+        if (vrDisplay) {
+          vrDisplay.requestAnimationFrame(onAnimationFrame);
+
+          vrDisplay.getFrameData(frameData);
+
+          // Loop over every gamepad and if we find any that have a pose use it.
+          var vrGamepads = [];
+          var gamepads = navigator.getGamepads();
+          for (var i = 0; i < gamepads.length; ++i) {
+            var gamepad = gamepads[i];
+            // The array may contain undefined gamepads, so check for that as
+            // well as a non-null pose.
+            if (gamepad) {
+              if (gamepad.pose)
+                vrGamepads.push(gamepad);
+              if ("hapticActuators" in gamepad && gamepad.hapticActuators.length > 0) {
+                for (var j = 0; j < gamepad.buttons.length; ++j) {
+                  if (gamepad.buttons[j].pressed) {
+                    // Vibrate the gamepad using to the value of the button as
+                    // the vibration intensity.
+                    gamepad.hapticActuators[0].pulse(gamepad.buttons[j].value, 100);
+                    break;
+                  }
+                }
+              }
+            }
+          }
+
+
+          if (vrDisplay.isPresenting) {
+            gl.viewport(0, 0, webglCanvas.width * 0.5, webglCanvas.height);
+            getStandingViewMatrix(viewMat, frameData.leftViewMatrix);
+            renderSceneView(frameData.leftProjectionMatrix, viewMat, vrGamepads);
+
+            gl.viewport(webglCanvas.width * 0.5, 0, webglCanvas.width * 0.5, webglCanvas.height);
+            getStandingViewMatrix(viewMat, frameData.rightViewMatrix);
+            renderSceneView(frameData.rightProjectionMatrix, viewMat, vrGamepads);
+
+            vrDisplay.submitFrame();
+          } else {
+            gl.viewport(0, 0, webglCanvas.width, webglCanvas.height);
+            mat4.perspective(projectionMat, Math.PI*0.4, webglCanvas.width / webglCanvas.height, 0.1, 1024.0);
+            getStandingViewMatrix(viewMat, frameData.leftViewMatrix);
+            renderSceneView(projectionMat, viewMat, vrGamepads);
+            stats.renderOrtho();
+          }
+        } else {
+          window.requestAnimationFrame(onAnimationFrame);
+
+          // No VRDisplay found.
+          gl.viewport(0, 0, webglCanvas.width, webglCanvas.height);
+          mat4.perspective(projectionMat, Math.PI*0.4, webglCanvas.width / webglCanvas.height, 0.1, 1024.0);
+          mat4.identity(viewMat);
+          mat4.translate(viewMat, viewMat, [0, -PLAYER_HEIGHT, 0]);
+          cubeIsland.render(projectionMat, viewMat, stats);
+
+          stats.renderOrtho();
+        }
+
+        stats.end();
+      }
+      })();
+    </script>
+  </body>
+</html>