tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

webxr-test.js (81998B)


      1 import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js';
      2 import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js';
      3 import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js';
      4 
      5 // This polyfill library implements the WebXR Test API as specified here:
      6 // https://github.com/immersive-web/webxr-test-api
      7 
      8 const defaultMojoFromStage = {
      9  data: { matrix: [1, 0,     0, 0,
     10                   0, 1,     0, 0,
     11                   0, 0,     1, 0,
     12                   0, -1.65, 0, 1] }
     13 };
     14 const default_stage_parameters = {
     15  mojoFromStage: defaultMojoFromStage,
     16  bounds: null
     17 };
     18 
     19 const default_framebuffer_scale = 0.7;
     20 
     21 function getMatrixFromTransform(transform) {
     22  const x = transform.orientation[0];
     23  const y = transform.orientation[1];
     24  const z = transform.orientation[2];
     25  const w = transform.orientation[3];
     26 
     27  const m11 = 1.0 - 2.0 * (y * y + z * z);
     28  const m21 = 2.0 * (x * y + z * w);
     29  const m31 = 2.0 * (x * z - y * w);
     30 
     31  const m12 = 2.0 * (x * y - z * w);
     32  const m22 = 1.0 - 2.0 * (x * x + z * z);
     33  const m32 = 2.0 * (y * z + x * w);
     34 
     35  const m13 = 2.0 * (x * z + y * w);
     36  const m23 = 2.0 * (y * z - x * w);
     37  const m33 = 1.0 - 2.0 * (x * x + y * y);
     38 
     39  const m14 = transform.position[0];
     40  const m24 = transform.position[1];
     41  const m34 = transform.position[2];
     42 
     43  // Column-major linearized order is expected.
     44  return [m11, m21, m31, 0,
     45          m12, m22, m32, 0,
     46          m13, m23, m33, 0,
     47          m14, m24, m34, 1];
     48 }
     49 
     50 function getPoseFromTransform(transform) {
     51  const [px, py, pz] = transform.position;
     52  const [ox, oy, oz, ow] = transform.orientation;
     53  return {
     54    position: {x: px, y: py, z: pz},
     55    orientation: {x: ox, y: oy, z: oz, w: ow},
     56  };
     57 }
     58 
     59 function composeGFXTransform(fakeTransformInit) {
     60  return {data: {matrix: getMatrixFromTransform(fakeTransformInit)}};
     61 }
     62 
     63 // Value equality for camera image init objects - they must contain `width` &
     64 // `height` properties and may contain `pixels` property.
     65 function isSameCameraImageInit(rhs, lhs) {
     66  return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels;
     67 }
     68 
     69 class ChromeXRTest {
     70  constructor() {
     71    this.mockVRService_ = new MockVRService();
     72  }
     73 
     74  // WebXR Test API
     75  simulateDeviceConnection(init_params) {
     76    return Promise.resolve(this.mockVRService_._addRuntime(init_params));
     77  }
     78 
     79  disconnectAllDevices() {
     80    this.mockVRService_._removeAllRuntimes();
     81    return Promise.resolve();
     82  }
     83 
     84  simulateUserActivation(callback) {
     85    if (window.top !== window) {
     86      // test_driver.click only works for the toplevel frame. This alternate
     87      // Chrome-specific method is sufficient for starting an XR session in an
     88      // iframe, and is used in platform-specific tests.
     89      //
     90      // TODO(https://github.com/web-platform-tests/wpt/issues/20282): use
     91      // a cross-platform method if available.
     92      xr_debug('simulateUserActivation', 'use eventSender');
     93      document.addEventListener('click', callback);
     94      eventSender.mouseMoveTo(0, 0);
     95      eventSender.mouseDown();
     96      eventSender.mouseUp();
     97      document.removeEventListener('click', callback);
     98      return;
     99    }
    100    const button = document.createElement('button');
    101    button.textContent = 'click to continue test';
    102    button.style.display = 'block';
    103    button.style.fontSize = '20px';
    104    button.style.padding = '10px';
    105    button.onclick = () => {
    106      callback();
    107      document.body.removeChild(button);
    108    };
    109    document.body.appendChild(button);
    110    test_driver.click(button);
    111  }
    112 
    113  // Helper method leveraged by chrome-specific setups.
    114  Debug(name, msg) {
    115    console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg);
    116  }
    117 }
    118 
    119 // Mocking class definitions
    120 
    121 // Mock service implements the VRService mojo interface.
    122 class MockVRService {
    123  constructor() {
    124    this.receiver_ = new vrMojom.VRServiceReceiver(this);
    125    this.runtimes_ = [];
    126 
    127    this.interceptor_ =
    128        new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName);
    129    this.interceptor_.oninterfacerequest =
    130        e => this.receiver_.$.bindHandle(e.handle);
    131    this.interceptor_.start();
    132  }
    133 
    134  // WebXR Test API Implementation Helpers
    135  _addRuntime(fakeDeviceInit) {
    136    const runtime = new MockRuntime(fakeDeviceInit, this);
    137    this.runtimes_.push(runtime);
    138 
    139    if (this.client_) {
    140      this.client_.onDeviceChanged();
    141    }
    142 
    143    return runtime;
    144  }
    145 
    146  _removeAllRuntimes() {
    147    if (this.client_) {
    148      this.client_.onDeviceChanged();
    149    }
    150 
    151    this.runtimes_ = [];
    152  }
    153 
    154  _removeRuntime(device) {
    155    const index = this.runtimes_.indexOf(device);
    156    if (index >= 0) {
    157      this.runtimes_.splice(index, 1);
    158      if (this.client_) {
    159        this.client_.onDeviceChanged();
    160      }
    161    }
    162  }
    163 
    164  // VRService overrides
    165  setClient(client) {
    166    if (this.client_) {
    167      throw new Error("setClient should only be called once");
    168    }
    169 
    170    this.client_ = client;
    171  }
    172 
    173  requestSession(sessionOptions) {
    174    const requests = [];
    175    // Request a session from all the runtimes.
    176    for (let i = 0; i < this.runtimes_.length; i++) {
    177      requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions);
    178    }
    179 
    180    return Promise.all(requests).then((results) => {
    181      // Find and return the first successful result.
    182      for (let i = 0; i < results.length; i++) {
    183        if (results[i].session) {
    184          // Construct a dummy metrics recorder
    185          const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote();
    186          metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close();
    187 
    188          const success = {
    189            session: results[i].session,
    190            metricsRecorder: metricsRecorderPtr,
    191          };
    192 
    193          return {result: {success}};
    194        }
    195      }
    196 
    197      // If there were no successful results, returns a null session.
    198      return {
    199        result: {failureReason: xrSessionMojom.RequestSessionError.NO_RUNTIME_FOUND}
    200      };
    201    });
    202  }
    203 
    204  supportsSession(sessionOptions) {
    205    const requests = [];
    206    // Check supports on all the runtimes.
    207    for (let i = 0; i < this.runtimes_.length; i++) {
    208      requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions);
    209    }
    210 
    211    return Promise.all(requests).then((results) => {
    212      // Find and return the first successful result.
    213      for (let i = 0; i < results.length; i++) {
    214        if (results[i].supportsSession) {
    215          return results[i];
    216        }
    217      }
    218 
    219      // If there were no successful results, returns false.
    220      return {supportsSession: false};
    221    });
    222  }
    223 
    224  exitPresent() {
    225    return Promise.resolve();
    226  }
    227 
    228  setFramesThrottled(throttled) {
    229    this.setFramesThrottledImpl(throttled);
    230  }
    231 
    232  // We cannot override the mojom interceptors via the prototype; so this method
    233  // and the above indirection exist to allow overrides by internal code.
    234  setFramesThrottledImpl(throttled) {}
    235 
    236  // Only handles asynchronous calls to makeXrCompatible. Synchronous calls are
    237  // not supported in Javascript.
    238  makeXrCompatible() {
    239    if (this.runtimes_.length == 0) {
    240      return {
    241        xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable
    242      };
    243    }
    244    return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible};
    245  }
    246 }
    247 
    248 class FakeXRAnchorController {
    249  constructor() {
    250    // Private properties.
    251    this.device_ = null;
    252    this.id_ = null;
    253    this.dirty_ = true;
    254 
    255    // Properties backing up public attributes / methods.
    256    this.deleted_ = false;
    257    this.paused_ = false;
    258    this.anchorOrigin_ = XRMathHelper.identity();
    259  }
    260 
    261  // WebXR Test API (Anchors Extension)
    262  get deleted() {
    263    return this.deleted_;
    264  }
    265 
    266  pauseTracking() {
    267    if(!this.paused_) {
    268      this.paused_ = true;
    269      this.dirty_ = true;
    270    }
    271  }
    272 
    273  resumeTracking() {
    274    if(this.paused_) {
    275      this.paused_ = false;
    276      this.dirty_ = true;
    277    }
    278  }
    279 
    280  stopTracking() {
    281    if(!this.deleted_) {
    282      this.device_._deleteAnchorController(this.id_);
    283 
    284      this.deleted_ = true;
    285      this.dirty_ = true;
    286    }
    287  }
    288 
    289  setAnchorOrigin(anchorOrigin) {
    290    this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
    291    this.dirty_ = true;
    292  }
    293 
    294  // Internal implementation:
    295  set id(value) {
    296    this.id_ = value;
    297  }
    298 
    299  set device(value) {
    300    this.device_ = value;
    301  }
    302 
    303  get dirty() {
    304    return this.dirty_;
    305  }
    306 
    307  get paused() {
    308    return this.paused_;
    309  }
    310 
    311  _markProcessed() {
    312    this.dirty_ = false;
    313  }
    314 
    315  _getAnchorOrigin() {
    316    return this.anchorOrigin_;
    317  }
    318 }
    319 
    320 // Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
    321 // for XRPresentationProvider. Implements FakeXRDevice test API.
    322 class MockRuntime {
    323  // Mapping from string feature names to the corresponding mojo types.
    324  // This is exposed as a member for extensibility.
    325  static _featureToMojoMap = {
    326    'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER,
    327    'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL,
    328    'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR,
    329    'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR,
    330    'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED,
    331    'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST,
    332    'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY,
    333    'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION,
    334    'anchors': xrSessionMojom.XRSessionFeature.ANCHORS,
    335    'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH,
    336    'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS,
    337    'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS,
    338    'layers': xrSessionMojom.XRSessionFeature.LAYERS,
    339  };
    340 
    341  static _sessionModeToMojoMap = {
    342    "inline": xrSessionMojom.XRSessionMode.kInline,
    343    "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr,
    344    "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr,
    345  };
    346 
    347  static _environmentBlendModeToMojoMap = {
    348    "opaque": vrMojom.XREnvironmentBlendMode.kOpaque,
    349    "alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend,
    350    "additive": vrMojom.XREnvironmentBlendMode.kAdditive,
    351  };
    352 
    353  static _interactionModeToMojoMap = {
    354    "screen-space": vrMojom.XRInteractionMode.kScreenSpace,
    355    "world-space": vrMojom.XRInteractionMode.kWorldSpace,
    356  };
    357 
    358  static _depthTypeToMojoMap = {
    359    "raw": xrSessionMojom.XRDepthType.kRawDepth,
    360    "smooth": xrSessionMojom.XRDepthType.kSmoothDepth,
    361  };
    362 
    363  static _depthUsageToMojoMap = {
    364    "cpu-optimized": xrSessionMojom.XRDepthUsage.kCPUOptimized,
    365    "gpu-optimized": xrSessionMojom.XRDepthUsage.kGPUOptimized,
    366  };
    367 
    368  static _depthDataFormatToMojoMap = {
    369    "luminance-alpha": xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha,
    370    "float32": xrSessionMojom.XRDepthDataFormat.kFloat32,
    371    "unsigned-short": xrSessionMojom.XRDepthDataFormat.kUnsignedShort,
    372  };
    373 
    374 
    375  constructor(fakeDeviceInit, service) {
    376    this.sessionClient_ = null;
    377    this.presentation_provider_ = new MockXRPresentationProvider();
    378 
    379    this.pose_ = null;
    380    this.next_frame_id_ = 0;
    381    this.bounds_ = null;
    382    this.send_mojo_space_reset_ = false;
    383    this.stageParameters_ = null;
    384    this.stageParametersId_ = 1;
    385    this.nextVisibilityMaskId_ = 1;
    386 
    387    this.service_ = service;
    388 
    389    this.framesOfReference = {};
    390 
    391    this.input_sources_ = new Map();
    392    this.next_input_source_index_ = 1;
    393 
    394    // Currently active hit test subscriptons.
    395    this.hitTestSubscriptions_ = new Map();
    396    // Currently active transient hit test subscriptions.
    397    this.transientHitTestSubscriptions_ = new Map();
    398    // ID of the next subscription to be assigned.
    399    this.next_hit_test_id_ = 1n;
    400 
    401    this.anchor_controllers_ = new Map();
    402    // ID of the next anchor to be assigned.
    403    this.next_anchor_id_ = 1n;
    404    // Anchor creation callback (initially null, can be set by tests).
    405    this.anchor_creation_callback_ = null;
    406 
    407    this.depthSensingData_ = null;
    408    this.depthSensingDataDirty_ = false;
    409 
    410    let supportedModes = [];
    411    if (fakeDeviceInit.supportedModes) {
    412      supportedModes = fakeDeviceInit.supportedModes.slice();
    413      if (fakeDeviceInit.supportedModes.length === 0) {
    414        supportedModes = ["inline"];
    415      }
    416    } else {
    417      // Back-compat mode.
    418      console.warn("Please use `supportedModes` to signal which modes are supported by this device.");
    419      if (fakeDeviceInit.supportsImmersive == null) {
    420        throw new TypeError("'supportsImmersive' must be set");
    421      }
    422 
    423      supportedModes = ["inline"];
    424      if (fakeDeviceInit.supportsImmersive) {
    425        supportedModes.push("immersive-vr");
    426      }
    427    }
    428 
    429    this.supportedModes_ = this._convertModesToEnum(supportedModes);
    430    if (this.supportedModes_.length == 0) {
    431      console.error("Device has empty supported modes array!");
    432      throw new InvalidStateError();
    433    }
    434 
    435    if (fakeDeviceInit.viewerOrigin != null) {
    436      this.setViewerOrigin(fakeDeviceInit.viewerOrigin);
    437    }
    438 
    439    if (fakeDeviceInit.floorOrigin != null) {
    440      this.setFloorOrigin(fakeDeviceInit.floorOrigin);
    441    }
    442 
    443    if (fakeDeviceInit.world) {
    444      this.setWorld(fakeDeviceInit.world);
    445    }
    446 
    447    if (fakeDeviceInit.depthSensingData) {
    448      this.setDepthSensingData(fakeDeviceInit.depthSensingData);
    449    }
    450 
    451    this.defaultFramebufferScale_ = default_framebuffer_scale;
    452    this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode);
    453    this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode);
    454 
    455    // This appropriately handles if the coordinates are null
    456    this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates);
    457 
    458    this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews);
    459 
    460    this._setDepthSupport(fakeDeviceInit.depthSupport || {});
    461 
    462    // Need to support webVR which doesn't have a notion of features
    463    this._setFeatures(fakeDeviceInit.supportedFeatures || []);
    464  }
    465 
    466  // WebXR Test API
    467  setViews(primaryViews, secondaryViews) {
    468    this.cameraImage_ = null;
    469    this.primaryViews_ = [];
    470    this.secondaryViews_ = [];
    471    let xOffset = 0;
    472    if (primaryViews) {
    473      this.primaryViews_ = [];
    474      xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_);
    475      const cameraImage = this._findCameraImage(primaryViews);
    476 
    477      if (cameraImage) {
    478        this.cameraImage_ = cameraImage;
    479      }
    480    }
    481 
    482    if (secondaryViews) {
    483      this.secondaryViews_ = [];
    484      this._setViews(secondaryViews, xOffset, this.secondaryViews_);
    485      const cameraImage = this._findCameraImage(secondaryViews);
    486 
    487      if (cameraImage) {
    488        if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) {
    489          throw new Error("If present, camera resolutions on each view must match each other!"
    490                          + " Secondary views' camera doesn't match primary views.");
    491        }
    492 
    493        this.cameraImage_ = cameraImage;
    494      }
    495    }
    496  }
    497 
    498  disconnect() {
    499    this.service_._removeRuntime(this);
    500    this.presentation_provider_._close();
    501    if (this.sessionClient_) {
    502      this.sessionClient_.$.close();
    503      this.sessionClient_ = null;
    504    }
    505 
    506    return Promise.resolve();
    507  }
    508 
    509  setViewerOrigin(origin, emulatedPosition = false) {
    510    const p = origin.position;
    511    const q = origin.orientation;
    512    this.pose_ = {
    513      orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
    514      position: { x: p[0], y: p[1], z: p[2] },
    515      emulatedPosition: emulatedPosition,
    516      angularVelocity: null,
    517      linearVelocity: null,
    518      angularAcceleration: null,
    519      linearAcceleration: null,
    520      inputState: null,
    521      poseIndex: 0
    522    };
    523  }
    524 
    525  clearViewerOrigin() {
    526    this.pose_ = null;
    527  }
    528 
    529  setFloorOrigin(floorOrigin) {
    530    if (!this.stageParameters_) {
    531      this.stageParameters_ = default_stage_parameters;
    532      this.stageParameters_.bounds = this.bounds_;
    533    }
    534 
    535    // floorOrigin is passed in as mojoFromStage.
    536    this.stageParameters_.mojoFromStage =
    537        {data: {matrix: getMatrixFromTransform(floorOrigin)}};
    538 
    539    this._onStageParametersUpdated();
    540  }
    541 
    542  clearFloorOrigin() {
    543    if (this.stageParameters_) {
    544      this.stageParameters_ = null;
    545      this._onStageParametersUpdated();
    546    }
    547  }
    548 
    549  setBoundsGeometry(bounds) {
    550    if (bounds == null) {
    551      this.bounds_ = null;
    552    } else if (bounds.length < 3) {
    553      throw new Error("Bounds must have a length of at least 3");
    554    } else {
    555      this.bounds_ = bounds;
    556    }
    557 
    558    // We can only set bounds if we have stageParameters set; otherwise, we
    559    // don't know the transform from local space to bounds space.
    560    // We'll cache the bounds so that they can be set in the future if the
    561    // floorLevel transform is set, but we won't update them just yet.
    562    if (this.stageParameters_) {
    563      this.stageParameters_.bounds = this.bounds_;
    564      this._onStageParametersUpdated();
    565    }
    566  }
    567 
    568  simulateResetPose() {
    569    this.send_mojo_space_reset_ = true;
    570  }
    571 
    572  simulateVisibilityChange(visibilityState) {
    573    let mojoState = null;
    574    switch (visibilityState) {
    575      case "visible":
    576        mojoState = vrMojom.XRVisibilityState.VISIBLE;
    577        break;
    578      case "visible-blurred":
    579        mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED;
    580        break;
    581      case "hidden":
    582        mojoState = vrMojom.XRVisibilityState.HIDDEN;
    583        break;
    584    }
    585    if (mojoState && this.sessionClient_) {
    586      this.sessionClient_.onVisibilityStateChanged(mojoState);
    587    }
    588  }
    589 
    590  simulateInputSourceConnection(fakeInputSourceInit) {
    591    const index = this.next_input_source_index_;
    592    this.next_input_source_index_++;
    593 
    594    const source = new MockXRInputSource(fakeInputSourceInit, index, this);
    595    this.input_sources_.set(index, source);
    596    return source;
    597  }
    598 
    599  // WebXR Test API Hit Test extensions
    600  setWorld(world) {
    601    this.world_ = world;
    602  }
    603 
    604  clearWorld() {
    605    this.world_ = null;
    606  }
    607 
    608  // WebXR Test API Anchor extensions
    609  setAnchorCreationCallback(callback) {
    610    this.anchor_creation_callback_ = callback;
    611  }
    612 
    613  setHitTestSourceCreationCallback(callback) {
    614    this.hit_test_source_creation_callback_ = callback;
    615  }
    616 
    617  // WebXR Test API Lighting estimation extensions
    618  setLightEstimate(fakeXrLightEstimateInit) {
    619    if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) {
    620      throw new TypeError("sphericalHarmonicsCoefficients must be set");
    621    }
    622 
    623    if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) {
    624      throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients");
    625    }
    626 
    627    if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) {
    628      throw new TypeError("W component of primaryLightDirection must be 0");
    629    }
    630 
    631    if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) {
    632      throw new TypeError("W component of primaryLightIntensity must be 1");
    633    }
    634 
    635    // If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them
    636    // to the defaults that the spec expects. ArCore will either give us everything or nothing,
    637    // so these aren't nullable on the mojom.
    638    if (!fakeXrLightEstimateInit.primaryLightDirection) {
    639      fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };
    640    }
    641 
    642    if (!fakeXrLightEstimateInit.primaryLightIntensity) {
    643      fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
    644    }
    645 
    646    let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients;
    647 
    648    this.light_estimate_ = {
    649      lightProbe: {
    650        // XRSphereicalHarmonics
    651        sphericalHarmonics: {
    652          coefficients: [
    653            { red: c[0],  green: c[1],  blue: c[2] },
    654            { red: c[3],  green: c[4],  blue: c[5] },
    655            { red: c[6],  green: c[7],  blue: c[8] },
    656            { red: c[9],  green: c[10], blue: c[11] },
    657            { red: c[12], green: c[13], blue: c[14] },
    658            { red: c[15], green: c[16], blue: c[17] },
    659            { red: c[18], green: c[19], blue: c[20] },
    660            { red: c[21], green: c[22], blue: c[23] },
    661            { red: c[24], green: c[25], blue: c[26] }
    662          ]
    663        },
    664        // Vector3dF
    665        mainLightDirection: {
    666          x: fakeXrLightEstimateInit.primaryLightDirection.x,
    667          y: fakeXrLightEstimateInit.primaryLightDirection.y,
    668          z: fakeXrLightEstimateInit.primaryLightDirection.z
    669        },
    670        // RgbTupleF32
    671        mainLightIntensity: {
    672          red:   fakeXrLightEstimateInit.primaryLightIntensity.x,
    673          green: fakeXrLightEstimateInit.primaryLightIntensity.y,
    674          blue:  fakeXrLightEstimateInit.primaryLightIntensity.z
    675        }
    676      }
    677    }
    678  }
    679 
    680  // WebXR Test API depth Sensing Extensions
    681  _setDepthSupport(depthSupport) {
    682    this.depthSupport_ = {};
    683 
    684    this.depthSupport_.depthTypes = [];
    685    for (const type of (depthSupport.depthTypes || [])) {
    686      this.depthSupport_.depthTypes.push(MockRuntime._depthTypeToMojoMap[type]);
    687    }
    688 
    689    this.depthSupport_.depthFormats = [];
    690    for (const format of (depthSupport.depthFormats || [])) {
    691      this.depthSupport_.depthFormats.push(MockRuntime._depthDataFormatToMojoMap[format]);
    692    }
    693 
    694    this.depthSupport_.depthUsages = [];
    695    for (const usage of (depthSupport.depthUsages || [])) {
    696      // Because chrome doesn't support gpu-optimized for any devices at present
    697      // avoid "false positive" WPTs by indicating that we don't support
    698      // gpu-optimized.
    699      if (usage === "gpu-optimized") {
    700        continue;
    701      }
    702 
    703      this.depthSupport_.depthUsages.push(MockRuntime._depthUsageToMojoMap[usage]);
    704    }
    705  }
    706 
    707  setDepthSensingData(depthSensingData) {
    708    for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) {
    709      if(!(key in depthSensingData)) {
    710        throw new TypeError("Required key not present. Key: " + key);
    711      }
    712    }
    713 
    714    if(depthSensingData.depthData != null) {
    715      // Create new object w/ properties based on the depthSensingData, but
    716      // convert the FakeXRRigidTransformInit into a transformation matrix object.
    717      this.depthSensingData_ = Object.assign({},
    718        depthSensingData, {
    719          normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView),
    720        });
    721    } else {
    722      throw new TypeError("`depthData` is not set");
    723    }
    724 
    725    this.depthSensingDataDirty_ = true;
    726  }
    727 
    728  clearDepthSensingData() {
    729    this.depthSensingData_ = null;
    730    this.depthSensingDataDirty_ = true;
    731  }
    732 
    733  // Internal Implementation/Helper Methods
    734  _convertModeToEnum(sessionMode) {
    735    if (sessionMode in MockRuntime._sessionModeToMojoMap) {
    736      return MockRuntime._sessionModeToMojoMap[sessionMode];
    737    }
    738 
    739    throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode);
    740  }
    741 
    742  _convertModesToEnum(sessionModes) {
    743    return sessionModes.map(mode => this._convertModeToEnum(mode));
    744  }
    745 
    746  _convertBlendModeToEnum(blendMode) {
    747    if (blendMode in MockRuntime._environmentBlendModeToMojoMap) {
    748      return MockRuntime._environmentBlendModeToMojoMap[blendMode];
    749    } else {
    750      if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
    751        return vrMojom.XREnvironmentBlendMode.kAdditive;
    752      } else if (this.supportedModes_.includes(
    753        xrSessionMojom.XRSessionMode.kImmersiveVr)) {
    754        return vrMojom.XREnvironmentBlendMode.kOpaque;
    755      }
    756    }
    757  }
    758 
    759  _convertInteractionModeToEnum(interactionMode) {
    760    if (interactionMode in MockRuntime._interactionModeToMojoMap) {
    761      return MockRuntime._interactionModeToMojoMap[interactionMode];
    762    } else {
    763      return vrMojom.XRInteractionMode.kWorldSpace;
    764    }
    765  }
    766 
    767  _setViews(deviceViews, xOffset, views) {
    768    for (let i = 0; i < deviceViews.length; i++) {
    769      views[i] = this._getView(deviceViews[i], xOffset);
    770      xOffset += deviceViews[i].resolution.width;
    771    }
    772 
    773    return xOffset;
    774  }
    775 
    776  _findCameraImage(views) {
    777    const viewWithCamera = views.find(view => view.cameraImageInit);
    778    if (viewWithCamera) {
    779      //If we have one view with a camera resolution, all views should have the same camera resolution.
    780      const allViewsHaveSameCamera = views.every(
    781        view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit));
    782 
    783      if (!allViewsHaveSameCamera) {
    784        throw new Error("If present, camera resolutions on each view must match each other!");
    785      }
    786 
    787      return viewWithCamera.cameraImageInit;
    788    }
    789 
    790    return null;
    791  }
    792 
    793  _onStageParametersUpdated() {
    794    // Indicate for the frame loop that the stage parameters have been updated.
    795    this.stageParametersId_++;
    796  }
    797 
    798  _getDefaultViews() {
    799    if (this.primaryViews_) {
    800      return this.primaryViews_;
    801    }
    802 
    803    const viewport_size = 20;
    804    return [{
    805        eye: vrMojom.XREye.kLeft,
    806        geometry: {
    807          fieldOfView: {
    808            upDegrees: 48.316,
    809            downDegrees: 50.099,
    810            leftDegrees: 50.899,
    811            rightDegrees: 35.197
    812          },
    813          mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
    814            position: [-0.032, 0, 0],
    815            orientation: [0, 0, 0, 1]
    816          }))
    817        },
    818        viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size }
    819      },
    820      {
    821        eye: vrMojom.XREye.kRight,
    822        geometry: {
    823          fieldOfView: {
    824            upDegrees: 48.316,
    825            downDegrees: 50.099,
    826            leftDegrees: 50.899,
    827            rightDegrees: 35.197
    828          },
    829          mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
    830            position: [0.032, 0, 0],
    831            orientation: [0, 0, 0, 1]
    832          }))
    833        },
    834        viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size }
    835      }];
    836  }
    837 
    838  _getFovFromProjectionMatrix(projectionMatrix) {
    839    const m = projectionMatrix;
    840 
    841    function toDegrees(tan) {
    842      return Math.atan(tan) * 180 / Math.PI;
    843    }
    844 
    845    const leftTan = (1 - m[8]) / m[0];
    846    const rightTan = (1 + m[8]) / m[0];
    847    const upTan = (1 + m[9]) / m[5];
    848    const downTan = (1 - m[9]) / m[5];
    849 
    850    return {
    851      upDegrees: toDegrees(upTan),
    852      downDegrees: toDegrees(downTan),
    853      leftDegrees: toDegrees(leftTan),
    854      rightDegrees: toDegrees(rightTan)
    855    };
    856  }
    857 
    858  // This function converts between the matrix provided by the WebXR test API
    859  // and the internal data representation.
    860  _getView(fakeXRViewInit, xOffset) {
    861    let fov = null;
    862 
    863    if (fakeXRViewInit.fieldOfView) {
    864      fov = {
    865        upDegrees: fakeXRViewInit.fieldOfView.upDegrees,
    866        downDegrees: fakeXRViewInit.fieldOfView.downDegrees,
    867        leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees,
    868        rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees
    869      };
    870    } else {
    871      fov = this._getFovFromProjectionMatrix(fakeXRViewInit.projectionMatrix);
    872    }
    873 
    874    let viewEye = vrMojom.XREye.kNone;
    875    // The eye passed in corresponds to the values in the WebXR spec, which are
    876    // the strings "none", "left", and "right". They should be converted to the
    877    // corresponding values of XREye in vr_service.mojom.
    878    switch(fakeXRViewInit.eye) {
    879      case "none":
    880        viewEye = vrMojom.XREye.kNone;
    881        break;
    882      case "left":
    883        viewEye = vrMojom.XREye.kLeft;
    884        break;
    885      case "right":
    886        viewEye = vrMojom.XREye.kRight;
    887        break;
    888    }
    889 
    890    let visibilityMask = null;
    891    if (fakeXRViewInit.visibilityMask) {
    892      let maskInit = fakeXRViewInit.visibilityMask;
    893      visibilityMask = {
    894        unvalidatedIndices: maskInit.indices,
    895        vertices: []
    896      };
    897      for (let i = 0; i + 1 < maskInit.vertices.length; i+= 2) {
    898        visibilityMask.vertices.push( { x: maskInit.vertices[i], y: maskInit.vertices[i+1]});
    899      }
    900    }
    901 
    902    return {
    903      eye: viewEye,
    904      geometry: {
    905        fieldOfView: fov,
    906        mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)),
    907        // Mojo will ignore extra members, we stash the raw projection matrix
    908        // here for ease of use with the depth extensions.
    909        projectionMatrix: fakeXRViewInit.projectionMatrix,
    910      },
    911      viewport: {
    912        x: xOffset,
    913      y: 0,
    914      width: fakeXRViewInit.resolution.width,
    915      height: fakeXRViewInit.resolution.height
    916      },
    917      isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false,
    918      viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset),
    919      visibilityMask: visibilityMask,
    920      visibilityMaskId: { idValue : this.nextVisibilityMaskId_++ }
    921    };
    922 
    923  }
    924 
    925  _setFeatures(supportedFeatures) {
    926    function convertFeatureToMojom(feature) {
    927      if (feature in MockRuntime._featureToMojoMap) {
    928        return MockRuntime._featureToMojoMap[feature];
    929      } else {
    930        return xrSessionMojom.XRSessionFeature.INVALID;
    931      }
    932    }
    933 
    934    this.supportedFeatures_ = [];
    935 
    936    for (let i = 0; i < supportedFeatures.length; i++) {
    937      const feature = convertFeatureToMojom(supportedFeatures[i]);
    938      if (feature !== xrSessionMojom.XRSessionFeature.INVALID) {
    939        this.supportedFeatures_.push(feature);
    940      }
    941    }
    942  }
    943 
    944  // These methods are intended to be used by MockXRInputSource only.
    945  _addInputSource(source) {
    946    if (!this.input_sources_.has(source.source_id_)) {
    947      this.input_sources_.set(source.source_id_, source);
    948    }
    949  }
    950 
    951  _removeInputSource(source) {
    952    this.input_sources_.delete(source.source_id_);
    953  }
    954 
    955  // These methods are intended to be used by FakeXRAnchorController only.
    956  _deleteAnchorController(controllerId) {
    957    this.anchor_controllers_.delete(controllerId);
    958  }
    959 
    960  // Extension point for non-standard modules.
    961  _injectAdditionalFrameData(options, frameData) {
    962  }
    963 
    964  // Mojo function implementations.
    965 
    966  // XRFrameDataProvider implementation.
    967  getFrameData(options) {
    968    return new Promise((resolve) => {
    969 
    970      const populatePose = () => {
    971        const mojo_space_reset = this.send_mojo_space_reset_;
    972        this.send_mojo_space_reset_ = false;
    973 
    974        if (this.pose_) {
    975          this.pose_.poseIndex++;
    976        }
    977 
    978        // Setting the input_state to null tests a slightly different path than
    979        // the browser tests where if the last input source is removed, the device
    980        // code always sends up an empty array, but it's also valid mojom to send
    981        // up a null array.
    982        let input_state = null;
    983        if (this.input_sources_.size > 0) {
    984          input_state = [];
    985          for (const input_source of this.input_sources_.values()) {
    986            input_state.push(input_source._getInputSourceState());
    987          }
    988        }
    989 
    990        let frame_views = this.primaryViews_;
    991        for (let i = 0; i < this.primaryViews_.length; i++) {
    992          this.primaryViews_[i].geometry.mojoFromView =
    993            this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset);
    994        }
    995        if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
    996          for (let i = 0; i < this.secondaryViews_.length; i++) {
    997            this.secondaryViews_[i].geometry.mojoFromView =
    998              this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset);
    999          }
   1000 
   1001          frame_views = frame_views.concat(this.secondaryViews_);
   1002        }
   1003 
   1004        const frameData = {
   1005          renderInfo: {
   1006            frameId: this.next_frame_id_,
   1007            mojoFromViewer: this.pose_,
   1008            views: frame_views
   1009          },
   1010          mojoSpaceReset: mojo_space_reset,
   1011          inputState: input_state,
   1012          timeDelta: {
   1013            // window.performance.now() is in milliseconds, so convert to microseconds.
   1014            microseconds: BigInt(Math.floor(window.performance.now() * 1000)),
   1015          },
   1016          bufferHolder: null,
   1017          cameraImageSize: this.cameraImage_ ? {
   1018            width: this.cameraImage_.width,
   1019            height: this.cameraImage_.height
   1020          } : null,
   1021          renderingTimeRatio: 0,
   1022          stageParameters: this.stageParameters_,
   1023          stageParametersId: this.stageParametersId_,
   1024          lightEstimationData: this.light_estimate_
   1025        };
   1026 
   1027        this.next_frame_id_++;
   1028 
   1029        this._calculateHitTestResults(frameData);
   1030 
   1031        this._calculateAnchorInformation(frameData);
   1032 
   1033        if (options.depthActive) {
   1034          this._calculateDepthInformation(frameData);
   1035        }
   1036 
   1037        this._injectAdditionalFrameData(options, frameData);
   1038 
   1039        resolve({frameData});
   1040      };
   1041 
   1042      if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) {
   1043        // Inline sessions should not have a delay introduced since it causes them
   1044        // to miss a vsync blink-side and delays propagation of changes that happened
   1045        // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames
   1046        // to propagate).
   1047        populatePose();
   1048      } else {
   1049        // For immerive sessions, add additional delay to allow for anchor creation
   1050        // promises to run.
   1051        setTimeout(populatePose, 3);  // note: according to MDN, the timeout is not exact
   1052      }
   1053    });
   1054  }
   1055 
   1056  getEnvironmentIntegrationProvider(environmentProviderRequest) {
   1057    if (this.environmentProviderReceiver_) {
   1058      this.environmentProviderReceiver_.$.close();
   1059    }
   1060    this.environmentProviderReceiver_ =
   1061        new vrMojom.XREnvironmentIntegrationProviderReceiver(this);
   1062    this.environmentProviderReceiver_.$.bindHandle(
   1063        environmentProviderRequest.handle);
   1064  }
   1065 
   1066  // XREnvironmentIntegrationProvider implementation:
   1067  subscribeToHitTest(nativeOriginInformation, entityTypes, ray) {
   1068    if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
   1069      // Reject outside of AR.
   1070      return Promise.resolve({
   1071        subscriptionId : null
   1072      });
   1073    }
   1074 
   1075    if (!this._nativeOriginKnown(nativeOriginInformation)) {
   1076      return Promise.resolve({
   1077        subscriptionId : null
   1078      });
   1079    }
   1080 
   1081    // Reserve the id for hit test source:
   1082    const id = this.next_hit_test_id_++;
   1083    const hitTestParameters = { isTransient: false, profileName: null };
   1084    const controller = new FakeXRHitTestSourceController(id);
   1085 
   1086 
   1087    return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
   1088      .then((succeeded) => {
   1089        if(succeeded) {
   1090          // Store the subscription information as-is (including controller):
   1091          this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller });
   1092 
   1093          return Promise.resolve({
   1094            subscriptionId : { idValue : id }
   1095          });
   1096        } else {
   1097          return Promise.resolve({
   1098            subscriptionId : null
   1099          });
   1100        }
   1101      });
   1102  }
   1103 
   1104  subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
   1105    if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
   1106      // Reject outside of AR.
   1107      return Promise.resolve({
   1108        subscriptionId : null
   1109      });
   1110    }
   1111 
   1112    const id = this.next_hit_test_id_++;
   1113    const hitTestParameters = { isTransient: true, profileName: profileName };
   1114    const controller = new FakeXRHitTestSourceController(id);
   1115 
   1116    // Check if we have hit test source creation callback.
   1117    // If yes, ask it if the hit test source creation should succeed.
   1118    // If no, for back-compat, assume the hit test source creation succeeded.
   1119    return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
   1120      .then((succeeded) => {
   1121        if(succeeded) {
   1122          // Store the subscription information as-is (including controller):
   1123          this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller });
   1124 
   1125          return Promise.resolve({
   1126            subscriptionId : { idValue : id }
   1127          });
   1128        } else {
   1129          return Promise.resolve({
   1130            subscriptionId : null
   1131          });
   1132        }
   1133      });
   1134  }
   1135 
   1136  unsubscribeFromHitTest(subscriptionId) {
   1137    let id = subscriptionId.idValue;
   1138    let controller = null;
   1139    if(this.transientHitTestSubscriptions_.has(id)){
   1140      controller = this.transientHitTestSubscriptions_.get(id).controller;
   1141      this.transientHitTestSubscriptions_.delete(id);
   1142    } else if(this.hitTestSubscriptions_.has(id)){
   1143      controller = this.hitTestSubscriptions_.get(id).controller;
   1144      this.hitTestSubscriptions_.delete(id);
   1145    }
   1146 
   1147    if(controller) {
   1148      controller.deleted = true;
   1149    }
   1150  }
   1151 
   1152  createAnchor(nativeOriginInformation, nativeOriginFromAnchor, planeId) {
   1153    return new Promise((resolve) => {
   1154      if(this.anchor_creation_callback_ == null) {
   1155        resolve({
   1156          anchorId : null
   1157        });
   1158 
   1159        return;
   1160      }
   1161 
   1162      const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
   1163      if(mojoFromNativeOrigin == null) {
   1164        resolve({
   1165          anchorId : null
   1166        });
   1167 
   1168        return;
   1169      }
   1170 
   1171      const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
   1172 
   1173      const anchorCreationParameters = {
   1174        requestedAnchorOrigin: mojoFromAnchor,
   1175        isAttachedToEntity: false,
   1176      };
   1177 
   1178      const anchorController = new FakeXRAnchorController();
   1179 
   1180      this.anchor_creation_callback_(anchorCreationParameters, anchorController)
   1181            .then((result) => {
   1182              if(result) {
   1183                // If the test allowed the anchor creation,
   1184                // store the anchor controller & return success.
   1185 
   1186                const anchor_id = this.next_anchor_id_;
   1187                this.next_anchor_id_++;
   1188 
   1189                this.anchor_controllers_.set(anchor_id, anchorController);
   1190                anchorController.device = this;
   1191                anchorController.id = anchor_id;
   1192 
   1193                resolve({
   1194                  anchorId : {
   1195                    idValue: anchor_id
   1196                  }
   1197                });
   1198              } else {
   1199                // The test has rejected anchor creation.
   1200                resolve({
   1201                  anchorId : null
   1202                });
   1203              }
   1204            })
   1205            .catch(() => {
   1206              // The test threw an error, treat anchor creation as failed.
   1207              resolve({
   1208                anchorId : null
   1209              });
   1210            });
   1211    });
   1212  }
   1213 
   1214  detachAnchor(anchorId) {}
   1215 
   1216  // Utility function
   1217  _requestRuntimeSession(sessionOptions) {
   1218    return this._runtimeSupportsSession(sessionOptions).then((result) => {
   1219      // The JavaScript bindings convert c_style_names to camelCase names.
   1220      const options = {
   1221        transportMethod:
   1222            vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER,
   1223        waitForTransferNotification: true,
   1224        waitForRenderNotification: true,
   1225        waitForGpuFence: false,
   1226      };
   1227 
   1228      let submit_frame_sink;
   1229      if (result.supportsSession) {
   1230        submit_frame_sink = {
   1231          clientReceiver: this.presentation_provider_._getClientReceiver(),
   1232          provider: this.presentation_provider_._bindProvider(sessionOptions),
   1233          transportOptions: options
   1234        };
   1235 
   1236        const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote();
   1237        this.dataProviderReceiver_ =
   1238            new vrMojom.XRFrameDataProviderReceiver(this);
   1239        this.dataProviderReceiver_.$.bindHandle(
   1240            dataProviderPtr.$.bindNewPipeAndPassReceiver().handle);
   1241        this.sessionOptions_ = sessionOptions;
   1242 
   1243        this.sessionClient_ = new vrMojom.XRSessionClientRemote();
   1244        const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver();
   1245 
   1246        const enabled_features = [];
   1247        for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) {
   1248          const feature = sessionOptions.requiredFeatures[i];
   1249          if (this._maybeEnableFeature(feature, sessionOptions)) {
   1250            enabled_features.push(feature);
   1251          } else {
   1252            return Promise.resolve({session: null});
   1253          }
   1254        }
   1255 
   1256        for (let i =0; i < sessionOptions.optionalFeatures.length; i++) {
   1257          const feature = sessionOptions.optionalFeatures[i];
   1258          if (this._maybeEnableFeature(feature, sessionOptions)) {
   1259            enabled_features.push(feature);
   1260          }
   1261        }
   1262 
   1263        this.enabledFeatures_ = enabled_features;
   1264 
   1265        return Promise.resolve({
   1266          session: {
   1267            submitFrameSink: submit_frame_sink,
   1268            dataProvider: dataProviderPtr,
   1269            clientReceiver: clientReceiver,
   1270            enabledFeatures: enabled_features,
   1271            deviceConfig: {
   1272              defaultFramebufferScale: this.defaultFramebufferScale_,
   1273              supportsViewportScaling: true,
   1274              // If depth was not enabled above, this should be null.
   1275              depthConfiguration: this.depthConfiguration_,
   1276              views: this._getDefaultViews(),
   1277            },
   1278            enviromentBlendMode: this.enviromentBlendMode_,
   1279            interactionMode: this.interactionMode_
   1280          }
   1281        });
   1282      } else {
   1283        return Promise.resolve({session: null});
   1284      }
   1285    });
   1286  }
   1287 
   1288  _runtimeSupportsSession(options) {
   1289    let result = this.supportedModes_.includes(options.mode);
   1290    return Promise.resolve({
   1291      supportsSession: result,
   1292    });
   1293  }
   1294 
   1295  _tryGetDepthConfig(options) {
   1296    if (!options.depthOptions) {
   1297      return null;
   1298    }
   1299 
   1300    // At present, there are only two depth usages, and we only support CPU.
   1301    if (options.depthOptions.usagePreferences.length !== 0 &&
   1302        !options.depthOptions.usagePreferences.includes(
   1303          xrSessionMojom.XRDepthUsage.kCPUOptimized)) {
   1304      return null;
   1305    }
   1306    const selectedUsage = xrSessionMojom.XRDepthUsage.kCPUOptimized;
   1307 
   1308    let selectedFormat = null;
   1309    if (options.depthOptions.dataFormatPreferences.length === 0) {
   1310      selectedFormat = this.depthSupport_.depthFormats.length === 0 ?
   1311        xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha : this.depthSupport_.depthFormats[0];
   1312    } else {
   1313      for (const dataFormatRequest of options.depthOptions.dataFormatPreferences) {
   1314        if (this.depthSupport_.depthFormats.length === 0 ||
   1315            this.depthSupport_.depthFormats.includes(dataFormatRequest)) {
   1316          selectedFormat = dataFormatRequest;
   1317          break;
   1318        }
   1319      }
   1320    }
   1321 
   1322    if (selectedFormat === null) {
   1323      return null;
   1324    }
   1325 
   1326    // Default to our first supported depth type. If it's empty (meaning all),
   1327    // then just default to raw.
   1328    let selectedDepthType = this.depthSupport_.depthTypes.length === 0 ?
   1329    xrSessionMojom.XRDepthType.kRawDepth : this.depthSupport_.depthTypes[0];
   1330    // Try to set the depthType to the earliest requested one if it's supported.
   1331    for (const depthTypeRequest of options.depthOptions.depthTypeRequest) {
   1332      if (this.depthSupport_.depthTypes.length === 0 ||
   1333          this.depthSupport_.depthTypes.includes(depthTypeRequest)) {
   1334        selectedDepthType = depthTypeRequest;
   1335        break;
   1336      }
   1337    }
   1338 
   1339    return {
   1340        depthUsage: selectedUsage,
   1341        depthDataFormat: selectedFormat,
   1342        depthType: selectedDepthType,
   1343      };
   1344  }
   1345 
   1346  _maybeEnableFeature(feature, options) {
   1347    if (this.supportedFeatures_.indexOf(feature) === -1) {
   1348      return false;
   1349    }
   1350 
   1351    switch (feature) {
   1352      case xrSessionMojom.XRSessionFeature.DEPTH:
   1353        this.depthConfiguration_ = this._tryGetDepthConfig(options);
   1354        this.matchDepthView_ = options.depthOptions && options.depthOptions.matchDepthView;
   1355        return this.depthConfiguration_ != null;
   1356      default:
   1357        return true;
   1358    }
   1359  }
   1360 
   1361  // Private functions - utilities:
   1362  _nativeOriginKnown(nativeOriginInformation){
   1363 
   1364    if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
   1365      if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
   1366        // Unknown input source.
   1367        return false;
   1368      }
   1369 
   1370      return true;
   1371    } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
   1372      // Bounded_floor & unbounded ref spaces are not yet supported for AR:
   1373      if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded
   1374       || nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) {
   1375        return false;
   1376      }
   1377 
   1378      return true;
   1379    } else {
   1380      // Planes and anchors are not yet supported by the mock interface.
   1381      return false;
   1382    }
   1383  }
   1384 
   1385  // Private functions - anchors implementation:
   1386 
   1387  // Modifies passed in frameData to add anchor information.
   1388  _calculateAnchorInformation(frameData) {
   1389    if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
   1390      return;
   1391    }
   1392 
   1393    frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []};
   1394    for(const [id, controller] of this.anchor_controllers_) {
   1395      frameData.anchorsData.allAnchorsIds.push({ idValue : id });
   1396 
   1397      // Send the entire anchor data over if there was a change since last GetFrameData().
   1398      if(controller.dirty) {
   1399        const anchorData = { id : { idValue : id }};
   1400        if(!controller.paused) {
   1401          anchorData.mojoFromAnchor = getPoseFromTransform(
   1402              XRMathHelper.decomposeRigidTransform(
   1403                  controller._getAnchorOrigin()));
   1404        }
   1405 
   1406        controller._markProcessed();
   1407 
   1408        frameData.anchorsData.updatedAnchorsData.push(anchorData);
   1409      }
   1410    }
   1411  }
   1412 
   1413  // Private functions - depth sensing implementation:
   1414 
   1415  /**
   1416   * Helper to get a TypedArray view for a given depth format.
   1417   * @param {ArrayBuffer} buffer The ArrayBuffer.
   1418   * @param {xrSessionMojom.XRDepthDataFormat} format The depth format.
   1419   * @return {Uint16Array|Float32Array} A typed array view.
   1420   */
   1421  static _getTypedArrayForFormat(buffer, format) {
   1422    if (format === xrSessionMojom.XRDepthDataFormat.kFloat32) {
   1423      return new Float32Array(buffer);
   1424    } else { // "luminance-alpha" or "unsigned-short"
   1425      return new Uint16Array(buffer);
   1426    }
   1427  }
   1428 
   1429  /**
   1430   * Helper to get a TypedArray for the given depth format.
   1431   * @param {xrSessionMojom.XRDepthDataFormat} format - The Depth format
   1432   * @param {number} size - The size of the array to be created.
   1433   * @return {Uint16Array|Float32Array} A typed array view.
   1434   */
   1435  static _getEmptyTypedArrayForFormat(format, size) {
   1436    if (format === xrSessionMojom.XRDepthDataFormat.kFloat32) {
   1437        return new Float32Array(size).fill(0.0);
   1438    } else { // "luminance-alpha" or "unsigned-short" (Uint16)
   1439        return new Uint16Array(size).fill(0);
   1440    }
   1441  }
   1442 
   1443  /**
   1444   * Reprojects depth data from a source view to a target view.
   1445   * The returned array will be the same width/height, but will be returned as a
   1446   * Uint8Array of the targetFormat (essentially a byte array that can be sent
   1447   * across mojo), so the overall returned size may be different.
   1448   *
   1449   * @param {ArrayBuffer} sourceDepthArrayBuffer - Raw depth data for the source.
   1450   * @param {number} width - Width of the depth data.
   1451   * @param {number} height - Height of the depth data.
   1452   * @param {xrSessionMojom.XRDepthDataFormat} sourceFormatEnum - Format of the source depth data.
   1453   * @param {Float32Array} sourceClipFromSourceView - Projection matrix for the source view.
   1454   * @param {Float32Array} mojoFromSourceView- Matrix of the transform for the source view.
   1455   * @param {xrSessionMojom.XRDepthDataFormat} targetFormatEnum - Format of the target depth data.
   1456   * @param {Float32Array} targetClipFromTargetView - Projection matrix for the target view.
   1457   * @param {Float32Array} mojoFromTargetView - Matrix of the transform for the target view.
   1458   * @return {Uint8Array | null} The reprojected depth data as an Uint8Array, or null on matrix error.
   1459   */
   1460  static copyDepthData(
   1461      sourceDepthArrayBuffer, width, height, sourceFormatEnum,
   1462      sourceClipFromSourceView, mojoFromSourceView,
   1463      targetFormatEnum,
   1464      targetClipFromTargetView, mojoFromTargetView) {
   1465 
   1466      const targetViewFromTargetClip = XRMathHelper.inverse(targetClipFromTargetView);
   1467      const sourceViewFromMojo = XRMathHelper.inverse(mojoFromSourceView);
   1468 
   1469      // Check if any matrices were not supplied or matrix inversions failed.
   1470      if (!targetViewFromTargetClip || !sourceViewFromMojo || !mojoFromTargetView) {
   1471          return null;
   1472      }
   1473 
   1474      // Build the full transformation from Target Clip space to Source Clip space.
   1475      const mojoFromTargetClip = XRMathHelper.mul4x4(mojoFromTargetView, targetViewFromTargetClip);
   1476      if (!mojoFromTargetClip) return null;
   1477 
   1478      const sourceViewFromTargetClip = XRMathHelper.mul4x4(sourceViewFromMojo, mojoFromTargetClip);
   1479      if (!sourceViewFromTargetClip) return null;
   1480 
   1481      const sourceClipFromTargetClip = XRMathHelper.mul4x4(sourceClipFromSourceView, sourceViewFromTargetClip);
   1482      if (!sourceClipFromTargetClip) return null;
   1483 
   1484      const sourceTypedArray = MockRuntime._getTypedArrayForFormat(sourceDepthArrayBuffer, sourceFormatEnum);
   1485      let internalTargetDepthTypedArray = MockRuntime._getEmptyTypedArrayForFormat(targetFormatEnum, width * height);
   1486 
   1487      // Iterate over target pixels (Backward Mapping)
   1488      for (let ty = 0; ty < height; ++ty) {
   1489          for (let tx = 0; tx < width; ++tx) {
   1490              // Convert target pixel (tx, ty) to target NDC coordinates
   1491              const u_tgt_pixel = (tx + 0.5) / width;  // u in [0, 1], Y-down from top-left
   1492              const v_tgt_pixel = (ty + 0.5) / height; // v in [0, 1], Y-down from top-left
   1493 
   1494              const ndc_x_tgt = u_tgt_pixel * 2.0 - 1.0;   // NDC X in [-1, 1]
   1495              const ndc_y_tgt = 1.0 - v_tgt_pixel * 2.0;   // NDC Y in [-1, 1], Y-up
   1496 
   1497              // Define a point on the near plane in target clip space
   1498              const P_clip_tgt = { x: ndc_x_tgt, y: ndc_y_tgt, z: -1.0, w: 1.0 };
   1499 
   1500              // Transform this point to source clip space
   1501              const P_clip_src = XRMathHelper.transform_by_matrix(sourceClipFromTargetClip, P_clip_tgt);
   1502 
   1503              // Homogenize to get source NDC coordinates
   1504              if (Math.abs(P_clip_src.w) < XRMathHelper.EPSILON) {
   1505                  internalTargetDepthTypedArray[ty * width + tx] = 0; // Cannot project
   1506                  continue;
   1507              }
   1508              const ndc_x_src = P_clip_src.x / P_clip_src.w;
   1509              const ndc_y_src = P_clip_src.y / P_clip_src.w;
   1510 
   1511              // Convert source NDC to source pixel coordinates
   1512              const u_src_pixel = (ndc_x_src + 1.0) / 2.0;
   1513              const v_src_pixel = (1.0 - ndc_y_src) / 2.0; // Convert source NDC Y-up to pixel Y-down
   1514 
   1515              const sx = Math.floor(u_src_pixel * width);
   1516              const sy = Math.floor(v_src_pixel * height);
   1517 
   1518              let target_raw_depth = 0; // Default to 0 (no data)
   1519 
   1520              // Check if the calculated source pixel is within bounds
   1521              if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
   1522                  const source_raw_value = sourceTypedArray[sy * width + sx];
   1523 
   1524                  let isValidSourceDepth = false;
   1525                  if (sourceFormatEnum === xrSessionMojom.XRDepthDataFormat.kFloat32) {
   1526                      if (source_raw_value > 0 && isFinite(source_raw_value)) {
   1527                          isValidSourceDepth = true;
   1528                      }
   1529                  } else { // Uint16 source
   1530                      if (source_raw_value > 0) {
   1531                          isValidSourceDepth = true;
   1532                      }
   1533                  }
   1534 
   1535                  if (isValidSourceDepth) {
   1536                      if (targetFormatEnum === xrSessionMojom.XRDepthDataFormat.kFloat32) {
   1537                          target_raw_depth = source_raw_value;
   1538                      } else {
   1539                          // Clamp to the valid range for Uint16
   1540                          target_raw_depth = Math.max(0, Math.min(0xFFFF, Math.round(source_raw_value)));
   1541                      }
   1542                  }
   1543              }
   1544 
   1545              // If not in bounds or source depth invalid, target_raw_depth remains 0.
   1546              internalTargetDepthTypedArray[ty * width + tx] = target_raw_depth;
   1547          }
   1548      }
   1549 
   1550      return new Uint8Array(internalTargetDepthTypedArray.buffer);
   1551  }
   1552 
   1553  _getDepthPixelData(depthGeometry) {
   1554    if (!this.matchDepthView_ || !depthGeometry) {
   1555      return { bytes: this.depthSensingData_.depthData };
   1556    }
   1557 
   1558    const sourceProjectionMatrix = depthGeometry.projectionMatrix;
   1559    const sourceViewOffset = depthGeometry.mojoFromView;
   1560    if (!sourceProjectionMatrix || !sourceViewOffset) {
   1561      return { bytes: this.depthSensingData_.depthData };
   1562    }
   1563 
   1564    if (this.primaryViews_.length === 0) {
   1565      return { bytes: this.depthSensingData_.depthData };
   1566    }
   1567 
   1568    const targetView = this.primaryViews_[0];
   1569    const targetProjectionMatrix = targetView.geometry.projectionMatrix;
   1570    const targetViewOffset = targetView.geometry.mojoFromView;
   1571    if (!targetProjectionMatrix || !targetViewOffset) {
   1572      return { bytes: this.depthSensingData_.depthData };
   1573    }
   1574 
   1575    return { bytes: MockRuntime.copyDepthData(
   1576      this.depthSensingData_.depthData,
   1577      this.depthSensingData_.width,
   1578      this.depthSensingData_.height,
   1579      MockRuntime._depthDataFormatToMojoMap[this.depthSensingData_.depthFormat],
   1580      sourceProjectionMatrix,
   1581      sourceViewOffset.data.matrix,
   1582      this.depthConfiguration_.depthDataFormat,
   1583      targetProjectionMatrix,
   1584      targetViewOffset.data.matrix
   1585    )};
   1586  }
   1587 
   1588  // Modifies passed in frameData to add anchor information.
   1589  _calculateDepthInformation(frameData) {
   1590    if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
   1591      return;
   1592    }
   1593 
   1594    if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
   1595      return;
   1596    }
   1597 
   1598    let newDepthData;
   1599 
   1600    // If we don't have a current depth data, we'll return null
   1601    // (i.e. no data is not a valid data, so it cannot be "StillValid").
   1602    if (this.depthSensingData_ == null) {
   1603      newDepthData = null;
   1604    } else if(!this.depthSensingDataDirty_) {
   1605      newDepthData = { dataStillValid: {}};
   1606    } else {
   1607      let viewGeometry = null;
   1608      const projectionMatrix = this.depthSensingData_.projectionMatrix;
   1609      const viewOffset = this.depthSensingData_.viewOffset;
   1610 
   1611      if (projectionMatrix && viewOffset) {
   1612        const fov = this._getFovFromProjectionMatrix(projectionMatrix);
   1613 
   1614        viewGeometry = {
   1615          fieldOfView: fov,
   1616          mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(viewOffset)),
   1617          // Convenience member for `_getDepthPixelData`
   1618          projectionMatrix: projectionMatrix
   1619        };
   1620      }
   1621 
   1622      newDepthData = {
   1623        updatedDepthData: {
   1624          timeDelta: frameData.timeDelta,
   1625          normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView,
   1626          rawValueToMeters: this.depthSensingData_.rawValueToMeters,
   1627          size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height },
   1628          pixelData: this._getDepthPixelData(viewGeometry),
   1629          viewGeometry: this.matchDepthView_ ? null : viewGeometry
   1630        }
   1631      };
   1632    }
   1633 
   1634    for (let i = 0; i < this.primaryViews_.length; i++) {
   1635      this.primaryViews_[i].depthData = newDepthData;
   1636    }
   1637    if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
   1638      for (let i = 0; i < this.secondaryViews_.length; i++) {
   1639        this.secondaryViews_[i].depthData = newDepthData;
   1640      }
   1641    }
   1642 
   1643    this.depthSensingDataDirty_ = false;
   1644  }
   1645 
   1646  // Private functions - hit test implementation:
   1647 
   1648  // Returns a Promise<bool> that signifies whether hit test source creation should succeed.
   1649  // If we have a hit test source creation callback installed, invoke it and return its result.
   1650  // If it's not installed, for back-compat just return a promise that resolves to true.
   1651  _shouldHitTestSourceCreationSucceed(hitTestParameters, controller) {
   1652    if(this.hit_test_source_creation_callback_) {
   1653      return this.hit_test_source_creation_callback_(hitTestParameters, controller);
   1654    } else {
   1655      return Promise.resolve(true);
   1656    }
   1657  }
   1658 
   1659  // Modifies passed in frameData to add hit test results.
   1660  _calculateHitTestResults(frameData) {
   1661    if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
   1662      return;
   1663    }
   1664 
   1665    frameData.hitTestSubscriptionResults = {results: [],
   1666                                            transientInputResults: []};
   1667    if (!this.world_) {
   1668      return;
   1669    }
   1670 
   1671    // Non-transient hit test:
   1672    for (const [id, subscription] of this.hitTestSubscriptions_) {
   1673      const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
   1674      if (!mojo_from_native_origin) continue;
   1675 
   1676      const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
   1677        subscription.ray,
   1678        mojo_from_native_origin
   1679      );
   1680 
   1681      const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
   1682      frameData.hitTestSubscriptionResults.results.push(
   1683          {subscriptionId: { idValue: id }, hitTestResults: results});
   1684    }
   1685 
   1686    // Transient hit test:
   1687    const mojo_from_viewer = this._getMojoFromViewer();
   1688 
   1689    for (const [id, subscription] of this.transientHitTestSubscriptions_) {
   1690      const result = {subscriptionId: { idValue: id },
   1691                      inputSourceIdToHitTestResults: new Map()};
   1692 
   1693      // Find all input sources that match the profile name:
   1694      const matching_input_sources = Array.from(this.input_sources_.values())
   1695                                                        .filter(input_source => input_source.profiles_.includes(subscription.profileName));
   1696 
   1697      for (const input_source of matching_input_sources) {
   1698        const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer);
   1699 
   1700        const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
   1701          subscription.ray,
   1702          mojo_from_native_origin
   1703        );
   1704 
   1705        const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
   1706 
   1707        result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
   1708      }
   1709 
   1710      frameData.hitTestSubscriptionResults.transientInputResults.push(result);
   1711    }
   1712  }
   1713 
   1714  // Returns 2-element array [origin, direction] of a ray in mojo space.
   1715  // |ray| is expressed relative to native origin.
   1716  _transformRayToMojoSpace(ray, mojo_from_native_origin) {
   1717    const ray_origin = {
   1718      x: ray.origin.x,
   1719      y: ray.origin.y,
   1720      z: ray.origin.z,
   1721      w: 1
   1722    };
   1723    const ray_direction = {
   1724      x: ray.direction.x,
   1725      y: ray.direction.y,
   1726      z: ray.direction.z,
   1727      w: 0
   1728    };
   1729 
   1730    const mojo_ray_origin = XRMathHelper.transform_by_matrix(
   1731      mojo_from_native_origin,
   1732      ray_origin);
   1733    const mojo_ray_direction = XRMathHelper.transform_by_matrix(
   1734      mojo_from_native_origin,
   1735      ray_direction);
   1736 
   1737    return [mojo_ray_origin, mojo_ray_direction];
   1738  }
   1739 
   1740  // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
   1741  _hitTestWorld(origin, direction, entityTypes) {
   1742    let result = [];
   1743 
   1744    for (const region of this.world_.hitTestRegions) {
   1745      const partial_result = this._hitTestRegion(
   1746        region,
   1747        origin, direction,
   1748        entityTypes);
   1749 
   1750      result = result.concat(partial_result);
   1751    }
   1752 
   1753    return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => {
   1754      delete hitTest.distance;
   1755      return hitTest;
   1756    });
   1757  }
   1758 
   1759  // Hit tests the passed in ray (expressed as origin and direction) against world region.
   1760  // |entityTypes| is a set of FakeXRRegionTypes.
   1761  // |region| is FakeXRRegion.
   1762  // Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray).
   1763  _hitTestRegion(region, origin, direction, entityTypes) {
   1764    const regionNameToMojoEnum = {
   1765      "point": vrMojom.EntityTypeForHitTest.POINT,
   1766      "plane": vrMojom.EntityTypeForHitTest.PLANE,
   1767      "mesh":null
   1768    };
   1769 
   1770    if (!entityTypes.includes(regionNameToMojoEnum[region.type])) {
   1771      return [];
   1772    }
   1773 
   1774    const result = [];
   1775    for (const face of region.faces) {
   1776      const maybe_hit = this._hitTestFace(face, origin, direction);
   1777      if (maybe_hit) {
   1778        result.push(maybe_hit);
   1779      }
   1780    }
   1781 
   1782    // The results should be sorted by distance and there should be no 2 entries with
   1783    // the same distance from ray origin - that would mean they are the same point.
   1784    // This situation is possible when a ray intersects the region through an edge shared
   1785    // by 2 faces.
   1786    return result.sort((lhs, rhs) => lhs.distance - rhs.distance)
   1787                 .filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance);
   1788  }
   1789 
   1790  // Hit tests the passed in ray (expressed as origin and direction) against a single face.
   1791  // |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates.
   1792  // |face| is an array of DOMPointInits.
   1793  // Returns null if the face does not intersect with the ray, otherwise the result is
   1794  // an XRHitResult with matrix describing the pose of the intersection point.
   1795  _hitTestFace(face, origin, direction) {
   1796    const add = XRMathHelper.add;
   1797    const sub = XRMathHelper.sub;
   1798    const mul = XRMathHelper.mul;
   1799    const normalize = XRMathHelper.normalize;
   1800    const dot = XRMathHelper.dot;
   1801    const cross = XRMathHelper.cross;
   1802    const neg = XRMathHelper.neg;
   1803 
   1804    //1. Calculate plane normal in world coordinates.
   1805    const point_A = face.vertices[0];
   1806    const point_B = face.vertices[1];
   1807    const point_C = face.vertices[2];
   1808 
   1809    const edge_AB = sub(point_B, point_A);
   1810    const edge_AC = sub(point_C, point_A);
   1811 
   1812    const normal = normalize(cross(edge_AB, edge_AC));
   1813 
   1814    const numerator = dot(sub(point_A, origin), normal);
   1815    const denominator = dot(direction, normal);
   1816 
   1817    if (Math.abs(denominator) < XRMathHelper.EPSILON) {
   1818      // Planes are nearly parallel - there's either infinitely many intersection points or 0.
   1819      // Both cases signify a "no hit" for us.
   1820      return null;
   1821    } else {
   1822      // Single intersection point between the infinite plane and the line (*not* ray).
   1823      // Need to calculate the hit test matrix taking into account the face vertices.
   1824      const distance = numerator / denominator;
   1825      if (distance < 0) {
   1826        // Line - plane intersection exists, but not the half-line - plane does not.
   1827        return null;
   1828      } else {
   1829        const intersection_point = add(origin, mul(distance, direction));
   1830        // Since we are treating the face as a solid, flip the normal so that its
   1831        // half-space will contain the ray origin.
   1832        const y_axis = denominator > 0 ? neg(normal) : normal;
   1833 
   1834        let z_axis = null;
   1835        const cos_direction_and_y_axis = dot(direction, y_axis);
   1836        if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) {
   1837          // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis.
   1838          // Note: this edge case is currently not covered by the spec.
   1839          const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0};
   1840          const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0};
   1841 
   1842          z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON)
   1843                        ? sub(up, mul(dot(right, y_axis), y_axis))  // `up is also co-linear with hit test normal, use `right`
   1844                        : sub(up, mul(dot(up, y_axis), y_axis));    // `up` is not co-linear with hit test normal, use it
   1845        } else {
   1846          // Project the ray direction onto the plane, negate it and use as a Z axis.
   1847          z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away.
   1848        }
   1849 
   1850        z_axis = normalize(z_axis);
   1851        const x_axis = normalize(cross(y_axis, z_axis));
   1852 
   1853        // Filter out the points not in polygon.
   1854        if (!XRMathHelper.pointInFace(intersection_point, face)) {
   1855          return null;
   1856        }
   1857 
   1858        const hitResult = {};
   1859        hitResult.distance = distance;  // Extend the object with additional information used by higher layers.
   1860                                        // It will not be serialized over mojom.
   1861 
   1862        const matrix = new Array(16);
   1863 
   1864        matrix[0] = x_axis.x;
   1865        matrix[1] = x_axis.y;
   1866        matrix[2] = x_axis.z;
   1867        matrix[3] = 0;
   1868 
   1869        matrix[4] = y_axis.x;
   1870        matrix[5] = y_axis.y;
   1871        matrix[6] = y_axis.z;
   1872        matrix[7] = 0;
   1873 
   1874        matrix[8] = z_axis.x;
   1875        matrix[9] = z_axis.y;
   1876        matrix[10] = z_axis.z;
   1877        matrix[11] = 0;
   1878 
   1879        matrix[12] = intersection_point.x;
   1880        matrix[13] = intersection_point.y;
   1881        matrix[14] = intersection_point.z;
   1882        matrix[15] = 1;
   1883 
   1884        hitResult.mojoFromResult = getPoseFromTransform(
   1885            XRMathHelper.decomposeRigidTransform(matrix));
   1886        return hitResult;
   1887      }
   1888    }
   1889  }
   1890 
   1891  _getMojoFromViewer() {
   1892    if (!this.pose_) {
   1893      return XRMathHelper.identity();
   1894    }
   1895    const transform = {
   1896      position: [
   1897        this.pose_.position.x,
   1898        this.pose_.position.y,
   1899        this.pose_.position.z],
   1900      orientation: [
   1901        this.pose_.orientation.x,
   1902        this.pose_.orientation.y,
   1903        this.pose_.orientation.z,
   1904        this.pose_.orientation.w],
   1905    };
   1906 
   1907    return getMatrixFromTransform(transform);
   1908  }
   1909 
   1910  _getMojoFromViewerWithOffset(viewOffset) {
   1911    return {data: { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.data.matrix) }};
   1912  }
   1913 
   1914  _getMojoFromNativeOrigin(nativeOriginInformation) {
   1915    const mojo_from_viewer = this._getMojoFromViewer();
   1916 
   1917    if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
   1918      if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
   1919        return null;
   1920      } else {
   1921        const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId);
   1922        return inputSource._getMojoFromInputSource(mojo_from_viewer);
   1923      }
   1924    } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
   1925      switch (nativeOriginInformation.referenceSpaceType) {
   1926        case vrMojom.XRReferenceSpaceType.kLocal:
   1927          return XRMathHelper.identity();
   1928        case vrMojom.XRReferenceSpaceType.kLocalFloor:
   1929          if (this.stageParameters_ == null || this.stageParameters_.mojoFromStage == null) {
   1930            console.warn("Standing transform not available.");
   1931            return null;
   1932          }
   1933          return this.stageParameters_.mojoFromStage.data.matrix;
   1934        case vrMojom.XRReferenceSpaceType.kViewer:
   1935          return mojo_from_viewer;
   1936        case vrMojom.XRReferenceSpaceType.kBoundedFloor:
   1937          return null;
   1938        case vrMojom.XRReferenceSpaceType.kUnbounded:
   1939          return null;
   1940        default:
   1941          throw new TypeError("Unrecognized XRReferenceSpaceType!");
   1942      }
   1943    } else {
   1944      // Anchors & planes are not yet supported for hit test.
   1945      return null;
   1946    }
   1947  }
   1948 }
   1949 
   1950 class MockXRInputSource {
   1951  constructor(fakeInputSourceInit, id, pairedDevice) {
   1952    this.source_id_ = id;
   1953    this.pairedDevice_ = pairedDevice;
   1954    this.handedness_ = fakeInputSourceInit.handedness;
   1955    this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
   1956 
   1957    if (fakeInputSourceInit.pointerOrigin == null) {
   1958      throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
   1959    }
   1960 
   1961    this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
   1962    this.setProfiles(fakeInputSourceInit.profiles);
   1963 
   1964    this.primary_input_pressed_ = false;
   1965    if (fakeInputSourceInit.selectionStarted != null) {
   1966      this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted;
   1967    }
   1968 
   1969    this.primary_input_clicked_ = false;
   1970    if (fakeInputSourceInit.selectionClicked != null) {
   1971      this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked;
   1972    }
   1973 
   1974    this.primary_squeeze_pressed_ = false;
   1975    this.primary_squeeze_clicked_ = false;
   1976 
   1977    this.mojo_from_input_ = null;
   1978    if (fakeInputSourceInit.gripOrigin != null) {
   1979      this.setGripOrigin(fakeInputSourceInit.gripOrigin);
   1980    }
   1981 
   1982    // This properly handles if supportedButtons were not specified.
   1983    this.setSupportedButtons(fakeInputSourceInit.supportedButtons);
   1984 
   1985    this.emulated_position_ = false;
   1986    this.desc_dirty_ = true;
   1987  }
   1988 
   1989  // WebXR Test API
   1990  setHandedness(handedness) {
   1991    if (this.handedness_ != handedness) {
   1992      this.desc_dirty_ = true;
   1993      this.handedness_ = handedness;
   1994    }
   1995  }
   1996 
   1997  setTargetRayMode(targetRayMode) {
   1998    if (this.target_ray_mode_ != targetRayMode) {
   1999      this.desc_dirty_ = true;
   2000      this.target_ray_mode_ = targetRayMode;
   2001    }
   2002  }
   2003 
   2004  setProfiles(profiles) {
   2005    this.desc_dirty_ = true;
   2006    this.profiles_ = profiles;
   2007  }
   2008 
   2009  setGripOrigin(transform, emulatedPosition = false) {
   2010    // grip_origin was renamed to mojo_from_input in mojo
   2011    this.mojo_from_input_ = composeGFXTransform(transform);
   2012    this.emulated_position_ = emulatedPosition;
   2013 
   2014    // Technically, setting the grip shouldn't make the description dirty, but
   2015    // the webxr-test-api sets our pointer as mojoFromPointer; however, we only
   2016    // support it across mojom as inputFromPointer, so we need to recalculate it
   2017    // whenever the grip moves.
   2018    this.desc_dirty_ = true;
   2019  }
   2020 
   2021  clearGripOrigin() {
   2022    // grip_origin was renamed to mojo_from_input in mojo
   2023    if (this.mojo_from_input_ != null) {
   2024      this.mojo_from_input_ = null;
   2025      this.emulated_position_ = false;
   2026      this.desc_dirty_ = true;
   2027    }
   2028  }
   2029 
   2030  setPointerOrigin(transform, emulatedPosition = false) {
   2031    // pointer_origin is mojo_from_pointer.
   2032    this.desc_dirty_ = true;
   2033    this.mojo_from_pointer_ = composeGFXTransform(transform);
   2034    this.emulated_position_ = emulatedPosition;
   2035  }
   2036 
   2037  disconnect() {
   2038    this.pairedDevice_._removeInputSource(this);
   2039  }
   2040 
   2041  reconnect() {
   2042    this.pairedDevice_._addInputSource(this);
   2043  }
   2044 
   2045  startSelection() {
   2046    this.primary_input_pressed_ = true;
   2047    if (this.gamepad_) {
   2048      this.gamepad_.buttons[0].pressed = true;
   2049      this.gamepad_.buttons[0].touched = true;
   2050    }
   2051  }
   2052 
   2053  endSelection() {
   2054    if (!this.primary_input_pressed_) {
   2055      throw new Error("Attempted to end selection which was not started");
   2056    }
   2057 
   2058    this.primary_input_pressed_ = false;
   2059    this.primary_input_clicked_ = true;
   2060 
   2061    if (this.gamepad_) {
   2062      this.gamepad_.buttons[0].pressed = false;
   2063      this.gamepad_.buttons[0].touched = false;
   2064    }
   2065  }
   2066 
   2067  simulateSelect() {
   2068    this.primary_input_clicked_ = true;
   2069  }
   2070 
   2071  setSupportedButtons(supportedButtons) {
   2072    this.gamepad_ = null;
   2073    this.supported_buttons_ = [];
   2074 
   2075    // If there are no supported buttons, we can stop now.
   2076    if (supportedButtons == null || supportedButtons.length < 1) {
   2077      return;
   2078    }
   2079 
   2080    const supported_button_map = {};
   2081    this.gamepad_ = this._getEmptyGamepad();
   2082    for (let i = 0; i < supportedButtons.length; i++) {
   2083      const buttonType = supportedButtons[i].buttonType;
   2084      this.supported_buttons_.push(buttonType);
   2085      supported_button_map[buttonType] = supportedButtons[i];
   2086    }
   2087 
   2088    // Let's start by building the button state in order of priority:
   2089    // Primary button is index 0.
   2090    this.gamepad_.buttons.push({
   2091      pressed: this.primary_input_pressed_,
   2092      touched: this.primary_input_pressed_,
   2093      value: this.primary_input_pressed_ ? 1.0 : 0.0
   2094    });
   2095 
   2096    // Now add the rest of our buttons
   2097    this._addGamepadButton(supported_button_map['grip']);
   2098    this._addGamepadButton(supported_button_map['touchpad']);
   2099    this._addGamepadButton(supported_button_map['thumbstick']);
   2100    this._addGamepadButton(supported_button_map['optional-button']);
   2101    this._addGamepadButton(supported_button_map['optional-thumbstick']);
   2102 
   2103    // Finally, back-fill placeholder buttons/axes
   2104    for (let i = 0; i < this.gamepad_.buttons.length; i++) {
   2105      if (this.gamepad_.buttons[i] == null) {
   2106        this.gamepad_.buttons[i] = {
   2107          pressed: false,
   2108          touched: false,
   2109          value: 0
   2110        };
   2111      }
   2112    }
   2113 
   2114    for (let i=0; i < this.gamepad_.axes.length; i++) {
   2115      if (this.gamepad_.axes[i] == null) {
   2116        this.gamepad_.axes[i] = 0;
   2117      }
   2118    }
   2119  }
   2120 
   2121  updateButtonState(buttonState) {
   2122    if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) {
   2123      throw new Error("Tried to update state on an unsupported button");
   2124    }
   2125 
   2126    const buttonIndex = this._getButtonIndex(buttonState.buttonType);
   2127    const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
   2128 
   2129    if (buttonIndex == -1) {
   2130      throw new Error("Unknown Button Type!");
   2131    }
   2132 
   2133    // is this a 'squeeze' button?
   2134    if (buttonIndex === this._getButtonIndex('grip')) {
   2135      // squeeze
   2136      if (buttonState.pressed) {
   2137        this.primary_squeeze_pressed_ = true;
   2138      } else if (this.gamepad_.buttons[buttonIndex].pressed) {
   2139        this.primary_squeeze_clicked_ = true;
   2140        this.primary_squeeze_pressed_ = false;
   2141      } else {
   2142        this.primary_squeeze_clicked_ = false;
   2143        this.primary_squeeze_pressed_ = false;
   2144      }
   2145    }
   2146 
   2147    this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed;
   2148    this.gamepad_.buttons[buttonIndex].touched = buttonState.touched;
   2149    this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue;
   2150 
   2151    if (axesStartIndex != -1) {
   2152      this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue;
   2153      this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue;
   2154    }
   2155  }
   2156 
   2157  // DOM Overlay Extensions
   2158  setOverlayPointerPosition(x, y) {
   2159    this.overlay_pointer_position_ = {x: x, y: y};
   2160  }
   2161 
   2162  // Helpers for Mojom
   2163  _getInputSourceState() {
   2164    const input_state = {};
   2165 
   2166    input_state.sourceId = this.source_id_;
   2167    input_state.isAuxiliary = false;
   2168 
   2169    input_state.primaryInputPressed = this.primary_input_pressed_;
   2170    input_state.primaryInputClicked = this.primary_input_clicked_;
   2171 
   2172    input_state.primarySqueezePressed = this.primary_squeeze_pressed_;
   2173    input_state.primarySqueezeClicked = this.primary_squeeze_clicked_;
   2174    // Setting the input source's "clicked" state should generate one "select"
   2175    // event. Reset the input value to prevent it from continuously generating
   2176    // events.
   2177    this.primary_input_clicked_ = false;
   2178    // Setting the input source's "clicked" state should generate one "squeeze"
   2179    // event. Reset the input value to prevent it from continuously generating
   2180    // events.
   2181    this.primary_squeeze_clicked_ = false;
   2182 
   2183    input_state.mojoFromInput = this.mojo_from_input_;
   2184 
   2185    input_state.gamepad = this.gamepad_;
   2186 
   2187    input_state.emulatedPosition = this.emulated_position_;
   2188 
   2189    if (this.desc_dirty_) {
   2190      const input_desc = {};
   2191 
   2192      switch (this.target_ray_mode_) {
   2193        case 'gaze':
   2194          input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING;
   2195          break;
   2196        case 'tracked-pointer':
   2197          input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING;
   2198          break;
   2199        case 'screen':
   2200          input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING;
   2201          break;
   2202        default:
   2203          throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
   2204      }
   2205 
   2206      switch (this.handedness_) {
   2207        case 'left':
   2208          input_desc.handedness = vrMojom.XRHandedness.LEFT;
   2209          break;
   2210        case 'right':
   2211          input_desc.handedness = vrMojom.XRHandedness.RIGHT;
   2212          break;
   2213        default:
   2214          input_desc.handedness = vrMojom.XRHandedness.NONE;
   2215          break;
   2216      }
   2217 
   2218      // Mojo requires us to send the pointerOrigin as relative to the grip
   2219      // space. If we don't have a grip space, we'll just assume that there
   2220      // is a grip at identity. This allows tests to simulate controllers that
   2221      // are really just a pointer with no tracked grip, though we will end up
   2222      // exposing that grip space.
   2223      let mojo_from_input = XRMathHelper.identity();
   2224      switch (this.target_ray_mode_) {
   2225        case 'gaze':
   2226        case 'screen':
   2227          // For gaze and screen space, we won't have a mojo_from_input; however
   2228          // the "input" position is just the viewer, so use mojo_from_viewer.
   2229          mojo_from_input = this.pairedDevice_._getMojoFromViewer();
   2230          break;
   2231        case 'tracked-pointer':
   2232          // If we have a tracked grip position (e.g. mojo_from_input), then use
   2233          // that. If we don't, then we'll just set the pointer offset directly,
   2234          // using identity as set above.
   2235          if (this.mojo_from_input_) {
   2236            mojo_from_input = this.mojo_from_input_.data.matrix;
   2237          }
   2238          break;
   2239        default:
   2240          throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
   2241      }
   2242 
   2243      // To convert mojo_from_pointer to input_from_pointer, we need:
   2244      // input_from_pointer = input_from_mojo * mojo_from_pointer
   2245      // Since we store mojo_from_input, we need to invert it here before
   2246      // multiplying.
   2247      let input_from_mojo = XRMathHelper.inverse(mojo_from_input);
   2248      input_desc.inputFromPointer = {};
   2249      input_desc.inputFromPointer.data = {
   2250        matrix : XRMathHelper.mul4x4(input_from_mojo,
   2251                                     this.mojo_from_pointer_.data.matrix)};
   2252 
   2253      input_desc.profiles = this.profiles_;
   2254 
   2255      input_state.description = input_desc;
   2256 
   2257      this.desc_dirty_ = false;
   2258    }
   2259 
   2260    // Pointer data for DOM Overlay, set by setOverlayPointerPosition()
   2261    if (this.overlay_pointer_position_) {
   2262      input_state.overlayPointerPosition = this.overlay_pointer_position_;
   2263      this.overlay_pointer_position_ = null;
   2264    }
   2265 
   2266    return input_state;
   2267  }
   2268 
   2269  _getEmptyGamepad() {
   2270    // Mojo complains if some of the properties on Gamepad are null, so set
   2271    // everything to reasonable defaults that tests can override.
   2272    const gamepad = {
   2273      connected: true,
   2274      id: [],
   2275      timestamp: 0n,
   2276      axes: [],
   2277      buttons: [],
   2278      touchEvents: [],
   2279      mapping: GamepadMapping.GamepadMappingStandard,
   2280      displayId: 0,
   2281    };
   2282 
   2283    switch (this.handedness_) {
   2284      case 'left':
   2285      gamepad.hand = GamepadHand.GamepadHandLeft;
   2286      break;
   2287      case 'right':
   2288      gamepad.hand = GamepadHand.GamepadHandRight;
   2289      break;
   2290      default:
   2291      gamepad.hand = GamepadHand.GamepadHandNone;
   2292      break;
   2293    }
   2294 
   2295    return gamepad;
   2296  }
   2297 
   2298  _addGamepadButton(buttonState) {
   2299    if (buttonState == null) {
   2300      return;
   2301    }
   2302 
   2303    const buttonIndex = this._getButtonIndex(buttonState.buttonType);
   2304    const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
   2305 
   2306    if (buttonIndex == -1) {
   2307      throw new Error("Unknown Button Type!");
   2308    }
   2309 
   2310    this.gamepad_.buttons[buttonIndex] = {
   2311      pressed: buttonState.pressed,
   2312      touched: buttonState.touched,
   2313      value: buttonState.pressedValue
   2314    };
   2315 
   2316    // Add x/y value if supported.
   2317    if (axesStartIndex != -1) {
   2318      this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue);
   2319      this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue);
   2320    }
   2321  }
   2322 
   2323  // General Helper methods
   2324  _getButtonIndex(buttonType) {
   2325    switch (buttonType) {
   2326      case 'grip':
   2327        return 1;
   2328      case 'touchpad':
   2329        return 2;
   2330      case 'thumbstick':
   2331        return 3;
   2332      case 'optional-button':
   2333        return 4;
   2334      case 'optional-thumbstick':
   2335        return 5;
   2336      default:
   2337        return -1;
   2338    }
   2339  }
   2340 
   2341  _getAxesStartIndex(buttonType) {
   2342    switch (buttonType) {
   2343      case 'touchpad':
   2344        return 0;
   2345      case 'thumbstick':
   2346        return 2;
   2347      case 'optional-thumbstick':
   2348        return 4;
   2349      default:
   2350        return -1;
   2351    }
   2352  }
   2353 
   2354  _getMojoFromInputSource(mojo_from_viewer) {
   2355    return this.mojo_from_pointer_.data.matrix;
   2356  }
   2357 }
   2358 
   2359 // Mojo helper classes
   2360 class FakeXRHitTestSourceController {
   2361  constructor(id) {
   2362    this.id_ = id;
   2363    this.deleted_ = false;
   2364  }
   2365 
   2366  get deleted() {
   2367    return this.deleted_;
   2368  }
   2369 
   2370  // Internal setter:
   2371  set deleted(value) {
   2372    this.deleted_ = value;
   2373  }
   2374 }
   2375 
   2376 class MockXRPresentationProvider {
   2377  constructor() {
   2378    this.receiver_ = null;
   2379    this.submit_frame_count_ = 0;
   2380    this.missing_frame_count_ = 0;
   2381  }
   2382 
   2383  _bindProvider() {
   2384    const provider = new vrMojom.XRPresentationProviderRemote();
   2385 
   2386    if (this.receiver_) {
   2387      this.receiver_.$.close();
   2388    }
   2389    this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this);
   2390    this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle);
   2391    return provider;
   2392  }
   2393 
   2394  _getClientReceiver() {
   2395    this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote();
   2396    return this.submitFrameClient_.$.bindNewPipeAndPassReceiver();
   2397  }
   2398 
   2399  // XRPresentationProvider mojo implementation
   2400  updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {}
   2401 
   2402  submitFrameMissing(frameId, mailboxHolder, timeWaited) {
   2403    this.missing_frame_count_++;
   2404  }
   2405 
   2406  submitFrame(frameId, mailboxHolder, timeWaited) {
   2407    this.submit_frame_count_++;
   2408 
   2409    // Trigger the submit completion callbacks here. WARNING: The
   2410    // Javascript-based mojo mocks are *not* re-entrant. It's OK to
   2411    // wait for these notifications on the next frame, but waiting
   2412    // within the current frame would never finish since the incoming
   2413    // calls would be queued until the current execution context finishes.
   2414    this.submitFrameClient_.onSubmitFrameTransferred(true);
   2415    this.submitFrameClient_.onSubmitFrameRendered();
   2416  }
   2417 
   2418  submitFrameWithTextureHandle(frameId, texture, syncToken) {}
   2419 
   2420  submitFrameDrawnIntoTexture(frameId, layer_ids, syncToken, timeWaited) {}
   2421 
   2422  // Utility methods
   2423  _close() {
   2424    if (this.receiver_) {
   2425      this.receiver_.$.close();
   2426    }
   2427  }
   2428 }
   2429 
   2430 // Export these into the global object as a side effect of importing this
   2431 // module.
   2432 self.ChromeXRTest = ChromeXRTest;
   2433 self.MockRuntime = MockRuntime;
   2434 self.MockVRService = MockVRService;
   2435 self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult;
   2436 
   2437 navigator.xr.test = new ChromeXRTest();