author | Anna Offenwanger <offenwanger@chromium.org> |
Wed, 25 Jul 2018 18:01:54 +0000 | |
changeset 429058 | 8f52b0441d604ac92f0a7da803063dd302e3ecd5 |
parent 429057 | 2e4644d3bad6eee9decab1e1d5a8aa7c520b8fb0 |
child 429059 | b16066457aac0a939d7d100ac0afc33d372318bd |
push id | 105805 |
push user | james@hoppipolla.co.uk |
push date | Sun, 29 Jul 2018 18:28:58 +0000 |
treeherder | mozilla-inbound@7ca03e42cbdf [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | testonly |
bugs | 1472368, 11737, 1070778, 576869 |
milestone | 63.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
|
--- 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>