gecko-dev/testing/web-platform/tests/resources/chromium/webxr-test.js
Alex Cooper 3bae07e881 Bug 1534504 [wpt PR 15735] - Ensure that XRSession promises reject if session destroyed, a=testonly
Automatic update from web-platform-tests
Ensure that XRSession promises reject if session destroyed

This sets up a pattern similar to other chromium/blink code that we
should follow when we are returning promises to the javascript that rely
on outstanding work from mojo interfaces that could become disconnected
while that work is outstanding/running.  Future work will address other
instances from mojo pointers exposed on the xr object that other code
consumes based on this pattern.

Bug: 845520
Change-Id: Ie018ceddb7bf38fcfe5935874fc72b0ba1fed23b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1509905
Commit-Queue: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Brandon Jones <bajones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#639229}

--

wpt-commits: bbf9c2f2292121314cd836aa9890a27c48bc577d
wpt-pr: 15735
2019-04-01 14:43:12 +01:00

439 lines
12 KiB
JavaScript

'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_.addRuntime(init_params));
}
simulateDeviceDisconnection(device) {
this.mockVRService_.removeRuntime(device);
}
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
// Mock service implements both the VRService and XRDevice mojo interfaces.
class MockVRService {
constructor() {
this.bindingSet_ = new mojo.BindingSet(device.mojom.VRService);
this.runtimes_ = [];
this.interceptor_ =
new MojoInterfaceInterceptor(device.mojom.VRService.name);
this.interceptor_.oninterfacerequest = e =>
this.bindingSet_.addBinding(this, e.handle);
this.interceptor_.start();
}
// Test methods
addRuntime(fakeDeviceInit) {
let runtime = new MockRuntime(fakeDeviceInit, this);
this.runtimes_.push(runtime);
if (this.client_) {
this.client_.onDeviceChanged();
}
return runtime;
}
removeRuntime(runtime) {
// We have no way of distinguishing between devices, so just clear the
// entire list for now.
// TODO(http://crbug.com/873409) We also have no way right now to disconnect
// devices.
this.runtimes_ = [];
}
// VRService implementation.
requestDevice() {
if (this.runtimes_.length > 0) {
let devicePtr = new device.mojom.XRDevicePtr();
new mojo.Binding(
device.mojom.XRDevice, this, mojo.makeRequest(devicePtr));
return Promise.resolve({device: devicePtr});
} else {
return Promise.resolve({device: null});
}
}
setClient(client) {
this.client_ = client;
}
// XRDevice implementation.
requestSession(sessionOptions, was_activation) {
let requests = [];
// Request a session from all the runtimes.
for (let i = 0; i < this.runtimes_.length; i++) {
requests[i] = this.runtimes_[i].requestRuntimeSession(sessionOptions);
}
return Promise.all(requests).then((results) => {
// Find and return the first successful result.
for (let i = 0; i < results.length; i++) {
if (results[i].session) {
return results[i];
}
}
// If there were no successful results, returns a null session.
return {session: null};
});
}
supportsSession(sessionOptions) {
let requests = [];
// Check supports on all the runtimes.
for (let i = 0; i < this.runtimes_.length; i++) {
requests[i] = this.runtimes_[i].runtimeSupportsSession(sessionOptions);
}
return Promise.all(requests).then((results) => {
// Find and return the first successful result.
for (let i = 0; i < results.length; i++) {
if (results[i].supportsSession) {
return results[i];
}
}
// If there were no successful results, returns false.
return {supportsSession: false};
});
};
}
// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
// for XRPresentationProvider.
class MockRuntime {
constructor(fakeDeviceInit, service) {
this.sessionClient_ = new device.mojom.XRSessionClientPtr();
this.presentation_provider_ = new MockXRPresentationProvider();
this.pose_ = null;
this.next_frame_id_ = 0;
this.service_ = service;
this.framesOfReference = {};
if (fakeDeviceInit.supportsImmersive) {
this.displayInfo_ = this.getImmersiveDisplayInfo();
} else {
this.displayInfo_ = this.getNonImmersiveDisplayInfo();
}
if (fakeDeviceInit.supportsEnvironmentIntegration) {
this.displayInfo_.capabilities.canProvideEnvironmentIntegration = true;
}
}
// Test methods.
setXRPresentationFrameData(poseMatrix, views) {
if (poseMatrix == null) {
this.pose_ = null;
} else {
this.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.sessionClient_.onChanged(this.displayInfo_);
}
}
}
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
}
}
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.
// XRFrameDataProvider implementation.
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: {}
}
});
}
getEnvironmentIntegrationProvider(environmentProviderRequest) {
this.environmentProviderBinding_ = new mojo.AssociatedBinding(
device.mojom.XREnvironmentIntegrationProvider, this,
environmentProviderRequest);
}
// Note that if getEnvironmentProvider hasn't finished running yet this will
// be undefined. It's recommended that you allow a successful task to post
// first before attempting to close.
closeEnvironmentIntegrationProvider() {
this.environmentProviderBinding_.close();
}
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.
}
// Utility function
requestRuntimeSession(sessionOptions) {
return this.runtimeSupportsSession(sessionOptions).then((result) => {
// The JavaScript bindings convert c_style_names to camelCase names.
let options = new device.mojom.XRPresentationTransportOptions();
options.transportMethod =
device.mojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER;
options.waitForTransferNotification = true;
options.waitForRenderNotification = true;
let submit_frame_sink;
if (result.supportsSession) {
submit_frame_sink = {
clientRequest: this.presentation_provider_.getClientRequest(),
provider: this.presentation_provider_.bindProvider(sessionOptions),
transportOptions: options
};
let dataProviderPtr = new device.mojom.XRFrameDataProviderPtr();
let dataProviderRequest = mojo.makeRequest(dataProviderPtr);
let dataProviderBinding = new mojo.Binding(
device.mojom.XRFrameDataProvider, this, dataProviderRequest);
let clientRequest = mojo.makeRequest(this.sessionClient_);
return Promise.resolve({
session: {
submitFrameSink: submit_frame_sink,
dataProvider: dataProviderPtr,
clientRequest: clientRequest,
displayInfo: this.displayInfo_
}
});
} else {
return Promise.resolve({session: null});
}
});
}
runtimeSupportsSession(options) {
return Promise.resolve({
supportsSession:
!options.immersive || this.displayInfo_.capabilities.canPresent
});
};
}
class MockXRPresentationProvider {
constructor() {
this.binding_ = new mojo.Binding(device.mojom.XRPresentationProvider, this);
this.submit_frame_count_ = 0;
this.missing_frame_count_ = 0;
}
bindProvider(request) {
let providerPtr = new device.mojom.XRPresentationProviderPtr();
let providerRequest = mojo.makeRequest(providerPtr);
this.binding_.close();
this.binding_ = new mojo.Binding(
device.mojom.XRPresentationProvider, this, providerRequest);
return providerPtr;
}
getClientRequest() {
this.submitFrameClient_ = new device.mojom.XRPresentationClientPtr();
return mojo.makeRequest(this.submitFrameClient_);
}
// XRPresentationProvider 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();
}
}
let XRTest = new ChromeXRTest();