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
This commit is contained in:
Anna Offenwanger 2018-07-25 18:01:54 +00:00 committed by James Graham
parent 82f6466bec
commit c0a8e3243f
5 changed files with 537 additions and 2 deletions

View File

@ -386853,6 +386853,12 @@
{}
]
],
"webxr/xrSession_exclusive_requestAnimationFrame.https.html": [
[
"/webxr/xrSession_exclusive_requestAnimationFrame.https.html",
{}
]
],
"workers/SharedWorkerPerformanceNow.html": [
[
"/workers/SharedWorkerPerformanceNow.html",
@ -630619,13 +630625,17 @@
"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"

View File

@ -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();

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -1,3 +1,25 @@
// 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.
@ -25,4 +47,32 @@ function forEachWebxrObject(callback) {
callback(window.XRStageBoundsPoint, 'XRStageBoundsPoint');
callback(window.XRSessionEvent, 'XRSessionEvent');
callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent');
}
}
// 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;
});

View File

@ -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>