Bug 1472368 [wpt PR 11737] - First Web Platform test using the WebXR Test API., a=testonly
authorAnna Offenwanger <offenwanger@chromium.org>
Wed, 25 Jul 2018 18:01:54 +0000
changeset 429058 8f52b0441d604ac92f0a7da803063dd302e3ecd5
parent 429057 2e4644d3bad6eee9decab1e1d5a8aa7c520b8fb0
child 429059 b16066457aac0a939d7d100ac0afc33d372318bd
push id105805
push userjames@hoppipolla.co.uk
push dateSun, 29 Jul 2018 18:28:58 +0000
treeherdermozilla-inbound@7ca03e42cbdf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1472368, 11737, 1070778, 576869
milestone63.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
Bug 1472368 [wpt PR 11737] - First Web Platform test using the WebXR Test API., a=testonly Automatic update from web-platform-testsFirst Web Platform test using the WebXR Test API. https://github.com/immersive-web/webxr-test-api Change-Id: Ic741d23bf0607726d9a938f08f7964a5f9c957d9 Reviewed-on: https://chromium-review.googlesource.com/1070778 Reviewed-by: Robert Ma <robertma@chromium.org> Reviewed-by: Daniel Cheng <dcheng@chromium.org> Reviewed-by: David Dorwin <ddorwin@chromium.org> Reviewed-by: Brandon Jones <bajones@chromium.org> Reviewed-by: Reilly Grant <reillyg@chromium.org> Commit-Queue: Anna Offenwanger <offenwanger@chromium.org> Cr-Commit-Position: refs/heads/master@{#576869} -- wpt-commits: db532d16883ceb1909285b263948ffffc968f1fd wpt-pr: 11737
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/resources/chromium/webxr-test.js
testing/web-platform/tests/resources/chromium/webxr-test.js.headers
testing/web-platform/tests/webxr/resources/webxr_util.js
testing/web-platform/tests/webxr/xrSession_exclusive_requestAnimationFrame.https.html
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -386848,16 +386848,22 @@
     ]
    ],
    "webxr/webxr_availability.http.sub.html": [
     [
      "/webxr/webxr_availability.http.sub.html",
      {}
     ]
    ],
+   "webxr/xrSession_exclusive_requestAnimationFrame.https.html": [
+    [
+     "/webxr/xrSession_exclusive_requestAnimationFrame.https.html",
+     {}
+    ]
+   ],
    "workers/SharedWorkerPerformanceNow.html": [
     [
      "/workers/SharedWorkerPerformanceNow.html",
      {}
     ]
    ],
    "workers/SharedWorker_blobUrl.html": [
     [
@@ -630614,23 +630620,27 @@
    "b723ad7105c7e5c12ba5fe75ceac3de3d990dfe2",
    "testharness"
   ],
   "webxr/resources/webxr_check.html": [
    "3fa96e39a073bda2e813f0b2c2411c26b73d374e",
    "support"
   ],
   "webxr/resources/webxr_util.js": [
-   "e8e9631d39b75b8e01a583636de765bc9c81dcd1",
+   "e145f999bb691a4d19067c75adc0f1206a63835d",
    "support"
   ],
   "webxr/webxr_availability.http.sub.html": [
    "d8aa0ef8b7b3363fd23af2700dc6d9186201c408",
    "testharness"
   ],
+  "webxr/xrSession_exclusive_requestAnimationFrame.https.html": [
+   "e75f95ee7d2751f7cbeadda9d6219bb29dbedfdc",
+   "testharness"
+  ],
   "workers/META.yml": [
    "e10618bcfad6f80d5d983b9f4da878560b644108",
    "support"
   ],
   "workers/README.md": [
    "b32446a62602baf1a316520ee8e2e5d9e60cada3",
    "support"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js
@@ -0,0 +1,383 @@
+'use strict';
+
+// This polyfill library implements the WebXR Test API as specified here:
+// https://github.com/immersive-web/webxr-test-api
+
+class ChromeXRTest {
+  constructor() {
+    this.mockVRService_ = new MockVRService(mojo.frameInterfaces);
+  }
+
+  simulateDeviceConnection(init_params) {
+    return Promise.resolve(this.mockVRService_.addDevice(init_params));
+  }
+
+  simulateUserActivation(callback) {
+    return new Promise(resolve => {
+      let button = document.createElement('button');
+      button.textContent = 'click to continue test';
+      button.style.display = 'block';
+      button.style.fontSize = '20px';
+      button.style.padding = '10px';
+      button.onclick = () => {
+        resolve(callback());
+        document.body.removeChild(button);
+      };
+      document.body.appendChild(button);
+      test_driver.click(button);
+    });
+  }
+}
+
+// Mocking class definitions
+class MockVRService {
+  constructor() {
+    this.bindingSet_ = new mojo.BindingSet(device.mojom.VRService);
+    this.devices_ = [];
+
+    this.interceptor_ =
+        new MojoInterfaceInterceptor(device.mojom.VRService.name);
+    this.interceptor_.oninterfacerequest = e =>
+        this.bindingSet_.addBinding(this, e.handle);
+    this.interceptor_.start();
+  }
+
+  // Test methods
+  addDevice(fakeDeviceInit) {
+    let device = new MockDevice(fakeDeviceInit, this);
+    this.devices_.push(device);
+
+    return device;
+  }
+
+  // VRService implementation.
+  setClient(client) {
+    this.client_ = client;
+    for (let i = 0; i < this.devices_.length; i++) {
+      this.devices_[i].notifyClientOfDisplay();
+    }
+
+    return Promise.resolve();
+  }
+}
+
+// Implements both VRDisplayHost and VRMagicWindowProvider. Maintains a mock for
+// VRPresentationProvider.
+class MockDevice {
+  constructor(fakeDeviceInit, service) {
+    this.displayClient_ = new device.mojom.VRDisplayClientPtr();
+    this.presentation_provider_ = new MockVRPresentationProvider();
+
+    this.service_ = service;
+
+    this.framesOfReference = {};
+
+    if (fakeDeviceInit.supportsImmersive) {
+      this.displayInfo_ = this.getImmersiveDisplayInfo();
+    } else {
+      this.displayInfo_ = this.getNonImmersiveDisplayInfo();
+    }
+
+    if (service.client_) {
+      this.notifyClientOfDisplay();
+    }
+  }
+
+  // Functions for setup.
+  // This function calls to the backend to add this device to the list.
+  notifyClientOfDisplay() {
+    let displayPtr = new device.mojom.VRDisplayHostPtr();
+    let displayRequest = mojo.makeRequest(displayPtr);
+    let displayBinding =
+        new mojo.Binding(device.mojom.VRDisplayHost, this, displayRequest);
+
+    let clientRequest = mojo.makeRequest(this.displayClient_);
+    this.service_.client_.onDisplayConnected(
+        displayPtr, clientRequest, this.displayInfo_);
+  }
+
+  // Test methods.
+  setXRPresentationFrameData(poseMatrix, views) {
+    if (poseMatrix == null) {
+      this.presentation_provider_.pose_ = null;
+    } else {
+      this.presentation_provider_.setPoseFromMatrix(poseMatrix);
+    }
+
+    if (views) {
+      let changed = false;
+      for (let i = 0; i < views.length; i++) {
+        if (views[i].eye == 'left') {
+          this.displayInfo_.leftEye = this.getEye(views[i]);
+          changed = true;
+        } else if (views[i].eye == 'right') {
+          this.displayInfo_.rightEye = this.getEye(views[i]);
+          changed = true;
+        }
+      }
+
+      if (changed) {
+        this.displayClient_.onChanged(this.displayInfo_);
+      }
+    }
+  }
+
+  getNonImmersiveDisplayInfo() {
+    let displayInfo = this.getImmersiveDisplayInfo();
+
+    displayInfo.capabilities.canPresent = false;
+    displayInfo.leftEye = null;
+    displayInfo.rightEye = null;
+
+    return displayInfo;
+  }
+
+  // Function to generate some valid display information for the device.
+  getImmersiveDisplayInfo() {
+    return {
+      displayName: 'FakeDevice',
+      capabilities: {
+        hasPosition: false,
+        hasExternalDisplay: false,
+        canPresent: true,
+        maxLayers: 1
+      },
+      stageParameters: null,
+      leftEye: {
+        fieldOfView: {
+          upDegrees: 48.316,
+          downDegrees: 50.099,
+          leftDegrees: 50.899,
+          rightDegrees: 35.197
+        },
+        offset: [-0.032, 0, 0],
+        renderWidth: 20,
+        renderHeight: 20
+      },
+      rightEye: {
+        fieldOfView: {
+          upDegrees: 48.316,
+          downDegrees: 50.099,
+          leftDegrees: 50.899,
+          rightDegrees: 35.197
+        },
+        offset: [0.032, 0, 0],
+        renderWidth: 20,
+        renderHeight: 20
+      },
+      webxrDefaultFramebufferScale: 0.7,
+    };
+  }
+
+  // This function converts between the matrix provided by the WebXR test API
+  // and the internal data representation.
+  getEye(fakeXRViewInit) {
+    let m = fakeXRViewInit.projectionMatrix;
+
+    function toDegrees(tan) {
+      return Math.atan(tan) * 180 / Math.PI;
+    }
+
+    let xScale = m[0];
+    let yScale = m[5];
+    let near = m[14] / (m[10] - 1);
+    let far = m[14] / (m[10] - 1);
+    let leftTan = (1 - m[8]) / m[0];
+    let rightTan = (1 + m[8]) / m[0];
+    let upTan = (1 + m[9]) / m[5];
+    let downTan = (1 - m[9]) / m[5];
+
+    return {
+      fieldOfView: {
+        upDegrees: toDegrees(upTan),
+        downDegrees: toDegrees(downTan),
+        leftDegrees: toDegrees(leftTan),
+        rightDegrees: toDegrees(rightTan)
+      },
+      offset: [0, 0, 0],
+      renderWidth: 20,
+      renderHeight: 20
+    };
+  }
+
+  // Mojo function implementations.
+
+  // VRMagicWindowProvider implementation.
+
+  getFrameData() {
+    // Convert current document time to monotonic time.
+    let now = window.performance.now() / 1000.0;
+    let diff = now - internals.monotonicTimeToZeroBasedDocumentTime(now);
+    now += diff;
+    now *= 1000000;
+
+    return Promise.resolve({
+      frameData: {
+        pose: this.presentation_provider_.pose_,
+        bufferHolder: null,
+        bufferSize: {},
+        timeDelta: [],
+        projectionMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+      }
+    });
+  }
+
+  updateSessionGeometry(frame_size, display_rotation) {
+    // This function must exist to ensure that calls to it do not crash, but we
+    // do not have any use for this data at present.
+  }
+
+  // VRDisplayHost implementation.
+
+  requestSession(sessionOptions, was_activation) {
+    return this.supportsSession(sessionOptions).then((result) => {
+      // The JavaScript bindings convert c_style_names to camelCase names.
+      let options = new device.mojom.VRDisplayFrameTransportOptions();
+      options.transportMethod =
+          device.mojom.VRDisplayFrameTransportMethod.SUBMIT_AS_MAILBOX_HOLDER;
+      options.waitForTransferNotification = true;
+      options.waitForRenderNotification = true;
+
+      let connection;
+      if (result.supportsSession) {
+        connection = {
+          clientRequest: this.presentation_provider_.getClientRequest(),
+          provider: this.presentation_provider_.bindProvider(sessionOptions),
+          transportOptions: options
+        };
+
+        let magicWindowPtr = new device.mojom.VRMagicWindowProviderPtr();
+        let magicWindowRequest = mojo.makeRequest(magicWindowPtr);
+        let magicWindowBinding = new mojo.Binding(
+            device.mojom.VRMagicWindowProvider, this, magicWindowRequest);
+
+        return Promise.resolve({
+          session:
+              {connection: connection, magicWindowProvider: magicWindowPtr}
+        });
+      } else {
+        return Promise.resolve({session: null});
+      }
+    });
+  }
+
+  supportsSession(options) {
+    return Promise.resolve({
+      supportsSession:
+          !options.exclusive || this.displayInfo_.capabilities.canPresent
+    });
+  };
+}
+
+class MockVRPresentationProvider {
+  constructor() {
+    this.binding_ = new mojo.Binding(device.mojom.VRPresentationProvider, this);
+    this.pose_ = null;
+    this.next_frame_id_ = 0;
+    this.submit_frame_count_ = 0;
+    this.missing_frame_count_ = 0;
+  }
+
+  bindProvider(request) {
+    let providerPtr = new device.mojom.VRPresentationProviderPtr();
+    let providerRequest = mojo.makeRequest(providerPtr);
+
+    this.binding_.close();
+
+    this.binding_ = new mojo.Binding(
+        device.mojom.VRPresentationProvider, this, providerRequest);
+
+    return providerPtr;
+  }
+
+  getClientRequest() {
+    this.submitFrameClient_ = new device.mojom.VRSubmitFrameClientPtr();
+    return mojo.makeRequest(this.submitFrameClient_);
+  }
+
+  setPoseFromMatrix(poseMatrix) {
+    this.pose_ = {
+      orientation: null,
+      position: null,
+      angularVelocity: null,
+      linearVelocity: null,
+      angularAcceleration: null,
+      linearAcceleration: null,
+      inputState: null,
+      poseIndex: 0
+    };
+
+    let pose = this.poseFromMatrix(poseMatrix);
+    for (let field in pose) {
+      if (this.pose_.hasOwnProperty(field)) {
+        this.pose_[field] = pose[field];
+      }
+    }
+  }
+
+  poseFromMatrix(m) {
+    let orientation = [];
+
+    let m00 = m[0];
+    let m11 = m[5];
+    let m22 = m[10];
+    // The max( 0, ... ) is just a safeguard against rounding error.
+    orientation[3] = Math.sqrt(Math.max(0, 1 + m00 + m11 + m22)) / 2;
+    orientation[0] = Math.sqrt(Math.max(0, 1 + m00 - m11 - m22)) / 2;
+    orientation[1] = Math.sqrt(Math.max(0, 1 - m00 + m11 - m22)) / 2;
+    orientation[2] = Math.sqrt(Math.max(0, 1 - m00 - m11 + m22)) / 2;
+
+    let position = [];
+    position[0] = m[12];
+    position[1] = m[13];
+    position[2] = m[14];
+
+    return {
+      orientation, position
+    }
+  }
+
+  // VRPresentationProvider mojo implementation
+  submitFrameMissing(frameId, mailboxHolder, timeWaited) {
+    this.missing_frame_count_++;
+  }
+
+  submitFrame(frameId, mailboxHolder, timeWaited) {
+    this.submit_frame_count_++;
+
+    // Trigger the submit completion callbacks here. WARNING: The
+    // Javascript-based mojo mocks are *not* re-entrant. It's OK to
+    // wait for these notifications on the next frame, but waiting
+    // within the current frame would never finish since the incoming
+    // calls would be queued until the current execution context finishes.
+    this.submitFrameClient_.onSubmitFrameTransferred(true);
+    this.submitFrameClient_.onSubmitFrameRendered();
+  }
+
+  getFrameData() {
+    if (this.pose_) {
+      this.pose_.poseIndex++;
+    }
+
+    // Convert current document time to monotonic time.
+    let now = window.performance.now() / 1000.0;
+    let diff = now - internals.monotonicTimeToZeroBasedDocumentTime(now);
+    now += diff;
+    now *= 1000000;
+
+    return Promise.resolve({
+      frameData: {
+        pose: this.pose_,
+        timeDelta: {
+          microseconds: now,
+        },
+        frameId: this.next_frame_id_++,
+        projectionMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
+        bufferHolder: null,
+        bufferSize: {}
+      }
+    });
+  }
+}
+
+let XRTest = new ChromeXRTest();
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js.headers
@@ -0,0 +1,1 @@
+Content-Type: text/javascript; charset=utf-8
--- a/testing/web-platform/tests/webxr/resources/webxr_util.js
+++ b/testing/web-platform/tests/webxr/resources/webxr_util.js
@@ -1,8 +1,30 @@
+// These tests rely on the User Agent providing an implementation of the
+// WebXR Testing API (https://github.com/immersive-web/webxr-test-api).
+//
+// In Chromium-based browsers, this implementation is provided by a JavaScript
+// shim in order to reduce the amount of test-only code shipped to users. To
+// enable these tests the browser must be run with these options:
+//
+//   --enable-blink-features=MojoJS,MojoJSTest
+
+function xr_promise_test(func, name, properties) {
+  promise_test(async (t) => {
+    // Perform any required test setup:
+
+    if (window.XRTest === undefined) {
+      // Chrome setup
+      await loadChromiumResources;
+    }
+
+    return func(t);
+  }, name, properties);
+}
+
 // This functions calls a callback with each API object as specified
 // by https://immersive-web.github.io/webxr/spec/latest/, allowing
 // checks to be made on all ojects.
 // Arguements:
 //      callback: A callback function with two arguements, the first
 //                being the API object, the second being the name of
 //                that API object.
 function forEachWebxrObject(callback) {
@@ -20,9 +42,37 @@ function forEachWebxrObject(callback) {
   callback(window.XRWebGLLayer, 'XRWebGLLayer');
   callback(window.XRWebGLLayerInit, 'XRWebGLLayerInit');
   callback(window.XRCoordinateSystem, 'XRCoordinateSystem');
   callback(window.XRFrameOfReference, 'XRFrameOfReference');
   callback(window.XRStageBounds, 'XRStageBounds');
   callback(window.XRStageBoundsPoint, 'XRStageBoundsPoint');
   callback(window.XRSessionEvent, 'XRSessionEvent');
   callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent');
-}
\ No newline at end of file
+}
+
+// Code for loading test api in chromium.
+let loadChromiumResources = Promise.resolve().then(() => {
+  if (!MojoInterfaceInterceptor) {
+    // Do nothing on non-Chromium-based browsers or when the Mojo bindings are
+    // not present in the global namespace.
+    return;
+  }
+
+  let chain = Promise.resolve();
+  ['/gen/layout_test_data/mojo/public/js/mojo_bindings.js',
+   '/gen/ui/gfx/geometry/mojo/geometry.mojom.js',
+   '/gen/mojo/public/mojom/base/time.mojom.js',
+   '/gen/device/vr/public/mojom/vr_service.mojom.js',
+   '/resources/chromium/webxr-test.js', '/resources/testdriver.js',
+   '/resources/testdriver-vendor.js',
+  ].forEach(path => {
+    let script = document.createElement('script');
+    script.src = path;
+    script.async = false;
+    chain = chain.then(() => new Promise(resolve => {
+                         script.onload = () => resolve();
+                       }));
+    document.head.appendChild(script);
+  });
+
+  return chain;
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_exclusive_requestAnimationFrame.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas id="webgl-canvas"></canvas>
+
+  <script>
+
+  const identityMatrix = new Float32Array([
+    1, 0, 0, 0,
+    0, 1, 0, 0,
+    0, 0, 1, 0,
+    0, 0, 0, 1
+  ]);
+
+  const rightFakeXRViewInit =
+    {eye:"right", projectionMatrix: identityMatrix, viewMatrix: identityMatrix};
+
+  const leftFakeXRViewInit =
+    {eye:"left", projectionMatrix: identityMatrix, viewMatrix: identityMatrix};
+
+  const immersiveFakeXRDeviceInit = { supportsImmersive:true };
+
+  const webglCanvas = document.getElementById('webgl-canvas');
+  let gl = webglCanvas.getContext('webgl', { alpha: false, antialias: false });
+
+  let testDevice;
+  let testDeviceController;
+  let testSession;
+
+  xr_promise_test(
+    (t) => XRTest.simulateDeviceConnection(immersiveFakeXRDeviceInit)
+      .then((controller) => {
+        testDeviceController = controller;
+        return navigator.xr.requestDevice();
+      })
+      .then((device) => {
+        testDevice = device;
+        return gl.setCompatibleXRDevice(device);
+      })
+      .then(() => new Promise((resolve, reject) => {
+          // Perform the session request in a user gesture.
+          XRTest.simulateUserActivation(() => {
+            testDevice.requestSession({ immersive: true })
+              .then((session) => {
+                testSession = session;
+                return session.requestFrameOfReference('eye-level');
+              })
+              .then((frameOfRef) => {
+                // Session must have a baseLayer or frame requests will be ignored.
+                testSession.baseLayer = new XRWebGLLayer(testSession, gl);
+
+                function onFrame(time, xrFrame) {
+                  assert_true(xrFrame instanceof XRFrame);
+
+                  assert_not_equals(xrFrame.views, null);
+                  assert_equals(xrFrame.views.length, 2);
+
+                  let devicePose = xrFrame.getDevicePose(frameOfRef);
+
+                  assert_not_equals(devicePose, null);
+                  for(let i = 0; i < identityMatrix.length; i++) {
+                    assert_equals(devicePose.poseModelMatrix[i], identityMatrix[i]);
+                  }
+
+                  assert_not_equals(devicePose.getViewMatrix(xrFrame.views[0]), null);
+                  assert_equals(devicePose.getViewMatrix(xrFrame.views[0]).length, 16);
+                  assert_not_equals(devicePose.getViewMatrix(xrFrame.views[1]), null);
+                  assert_equals(devicePose.getViewMatrix(xrFrame.views[1]).length, 16);
+
+                  // Test does not complete until the returned promise resolves.
+                  resolve();
+                }
+
+                testDeviceController.setXRPresentationFrameData(
+                  identityMatrix,
+                  [rightFakeXRViewInit, leftFakeXRViewInit]
+                );
+
+                testSession.requestAnimationFrame(onFrame);
+              }).catch((err) => {
+                reject("Session was rejected with error: "+err);
+              });
+          });
+        })
+      ),
+    "RequestAnimationFrame resolves with good data"
+  );
+  </script>
+</body>