'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(); } } // 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: {} } }); } 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 environmentProviderPtr = new device.mojom.XREnvironmentIntegrationProviderPtr(); let environmentProviderRequest = mojo.makeRequest(environmentProviderPtr); let environmentProviderBinding = new mojo.Binding( device.mojom.XREnvironmentIntegrationProvider, this, environmentProviderRequest); let clientRequest = mojo.makeRequest(this.sessionClient_); return Promise.resolve({ session: { submitFrameSink: submit_frame_sink, dataProvider: dataProviderPtr, environmentProvider: environmentProviderPtr, 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();