chromium/device/vr/openxr/openxr_device.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "device/vr/openxr/openxr_device.h"

#include <string>
#include <vector>

#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "build/build_config.h"
#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_render_loop.h"
#include "device/vr/openxr/openxr_util.h"
#include "device/vr/public/cpp/features.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/openxr/src/include/openxr/openxr.h"

namespace device {

namespace {

const std::vector<mojom::XRSessionFeature>& GetSupportedFeatures() {
  static base::NoDestructor<std::vector<mojom::XRSessionFeature>>
      kSupportedFeatures{{mojom::XRSessionFeature::REF_SPACE_VIEWER,
                          mojom::XRSessionFeature::REF_SPACE_LOCAL,
                          mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
                          mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR,
                          mojom::XRSessionFeature::REF_SPACE_UNBOUNDED,
                          mojom::XRSessionFeature::ANCHORS,
                          mojom::XRSessionFeature::SECONDARY_VIEWS}};

  return *kSupportedFeatures;
}

bool AreAllRequiredFeaturesSupported(
    const mojom::XRSessionMode mode,
    const std::vector<mojom::XRSessionFeature>& required_features,
    const OpenXrExtensionHelper& extension_helper) {
  return base::ranges::all_of(
      required_features,
      [&extension_helper, mode](const mojom::XRSessionFeature& feature) {
        // First we check if we will allow the feature to be supported in the
        // mode that has been requested; before querying if the feature can
        // actually be supported by the current runtime.
        // The extension helper returns true for features that are supported
        // entirely by the core spec. We rely on the Browser process
        // pre-filtering and not passing us any features that we haven't already
        // indicated that we could support, which is the union of core spec
        // features and things that could theoretically be supported depending
        // on enabled extensions (which we're now checking if they're actually
        // supported,since we need to create an instance to confirm that).
        return IsFeatureSupportedForMode(feature, mode) &&
               extension_helper.IsFeatureSupported(feature);
      });
}
}  // namespace

OpenXrDevice::OpenXrDevice(
    VizContextProviderFactoryAsync context_provider_factory_async,
    OpenXrPlatformHelper* platform_helper)
    : VRDeviceBase(device::mojom::XRDeviceId::OPENXR_DEVICE_ID),
      context_provider_factory_async_(
          std::move(context_provider_factory_async)),
      platform_helper_(platform_helper) {
  CHECK(platform_helper_);
  CHECK(platform_helper_->EnsureInitialized());

  device::mojom::XRDeviceData device_data = platform_helper_->GetXRDeviceData();

  device_data.supported_features = GetSupportedFeatures();

  // Only support hand input if the feature flag is enabled.
  if (base::FeatureList::IsEnabled(features::kWebXrHandInput))
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::HAND_INPUT);

  // Only support layers if the feature flag is enabled.
  if (base::FeatureList::IsEnabled(features::kWebXrLayers))
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::LAYERS);

  // Only support hit test if the feature flag is enabled.
  if (device::features::IsOpenXrArEnabled()) {
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::HIT_TEST);
    device_data.supported_features.emplace_back(
        mojom::XRSessionFeature::LIGHT_ESTIMATION);
    device_data.supported_features.emplace_back(mojom::XRSessionFeature::DEPTH);
  }

  SetDeviceData(std::move(device_data));
}

OpenXrDevice::~OpenXrDevice() {
  // Wait for the render loop to stop before completing destruction. This will
  // ensure that the render loop doesn't get shutdown while it is processing
  // any requests.
  render_loop_.reset();

  if (instance_ != XR_NULL_HANDLE) {
    platform_helper_->DestroyInstance(instance_);
  }

  // request_session_callback_ may still be active if we're tearing down the
  // OpenXrDevice while we're still making asynchronous calls to setup the GPU
  // process connection. Ensure the callback is run regardless.
  if (request_session_callback_) {
    std::move(request_session_callback_).Run(nullptr);
  }
}

void OpenXrDevice::RequestSession(
    mojom::XRRuntimeSessionOptionsPtr options,
    mojom::XRRuntime::RequestSessionCallback callback) {
  CHECK(!request_session_callback_);
  CHECK(!HasExclusiveSession());

  request_session_callback_ = std::move(callback);

  OpenXrCreateInfo create_info;
  create_info.render_process_id = options->render_process_id;
  create_info.render_frame_id = options->render_frame_id;
  platform_helper_->CreateInstanceWithCreateInfo(
      create_info,
      base::BindOnce(&OpenXrDevice::OnCreateInstanceResult,
                     weak_ptr_factory_.GetWeakPtr(), std::move(options)),
      base::BindOnce(&OpenXrDevice::ForceEndSession,
                     weak_ptr_factory_.GetWeakPtr(),
                     ExitXrPresentReason::kXrPlatformHelperShutdown));
}

void OpenXrDevice::OnCreateInstanceResult(
    mojom::XRRuntimeSessionOptionsPtr options,
    XrResult result,
    XrInstance instance) {
  if (XR_FAILED(result)) {
    DVLOG(1) << __func__ << " Failed to create an XrInstance";
    instance_ = XR_NULL_HANDLE;
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  instance_ = instance;

  extension_helper_ = std::make_unique<OpenXrExtensionHelper>(
      instance_, platform_helper_->GetExtensionEnumeration());

  // Now that we have an instance, check if it's even theoretically possible
  // to support all of our required features. While this check isn't final, as
  // the OpenXrRenderLoop will make that ultimate determination, it can help
  // us early-exit and avoid spinning it up if we know we don't even have the
  // extensions necessary to support a required feature.
  if (!AreAllRequiredFeaturesSupported(
          options->mode, options->required_features, *extension_helper_)) {
    DVLOG(1) << __func__ << " Missing a required feature";
    // Reject session request, and call ForceEndSession to ensure that we clean
    // up any objects that were already created.
    ForceEndSession(ExitXrPresentReason::kOpenXrStartFailed);
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  if (!render_loop_) {
    render_loop_ = std::make_unique<OpenXrRenderLoop>(
        context_provider_factory_async_, instance_, *extension_helper_,
        platform_helper_);
    render_loop_->Start();
  }

  auto my_callback = base::BindOnce(&OpenXrDevice::OnRequestSessionResult,
                                    weak_ptr_factory_.GetWeakPtr());

  auto on_visibility_state_changed = base::BindRepeating(
      &OpenXrDevice::OnVisibilityStateChanged, weak_ptr_factory_.GetWeakPtr());

  render_loop_->task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&OpenXrRenderLoop::RequestSession,
                                base::Unretained(render_loop_.get()),
                                std::move(on_visibility_state_changed),
                                std::move(options), std::move(my_callback)));
}

void OpenXrDevice::OnRequestSessionResult(
    bool result,
    mojom::XRSessionPtr session,
    mojo::PendingRemote<mojom::ImmersiveOverlay> overlay) {
  DCHECK(request_session_callback_);

  if (!result) {
    // Reject session request, and call ForceEndSession to ensure that we clean
    // up any objects that were already created.
    ForceEndSession(ExitXrPresentReason::kOpenXrStartFailed);
    std::move(request_session_callback_).Run(nullptr);
    return;
  }

  OnStartPresenting();

  auto session_result = mojom::XRRuntimeSessionResult::New();
  session_result->session = std::move(session);
  session_result->controller =
      exclusive_controller_receiver_.BindNewPipeAndPassRemote();
  session_result->overlay = std::move(overlay);

  std::move(request_session_callback_).Run(std::move(session_result));

  // Use of Unretained is safe because the callback will only occur if the
  // binding is not destroyed.
  exclusive_controller_receiver_.set_disconnect_handler(
      base::BindOnce(&OpenXrDevice::OnPresentingControllerMojoConnectionError,
                     base::Unretained(this)));
}

void OpenXrDevice::ForceEndSession(ExitXrPresentReason reason) {
  // This method is called when the rendering process exit presents.
  if (render_loop_) {
    render_loop_->task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&OpenXrRenderLoop::ExitPresent,
                       base::Unretained(render_loop_.get()), reason));
    render_loop_.reset();
  }

  OnExitPresent();
  exclusive_controller_receiver_.reset();

  extension_helper_.reset();
  if (instance_ != XR_NULL_HANDLE) {
    platform_helper_->DestroyInstance(instance_);
  }
}

void OpenXrDevice::OnPresentingControllerMojoConnectionError() {
  ForceEndSession(ExitXrPresentReason::kMojoConnectionError);
}

void OpenXrDevice::ShutdownSession(
    mojom::XRRuntime::ShutdownSessionCallback callback) {
  ForceEndSession(ExitXrPresentReason::kBrowserShutdown);
  std::move(callback).Run();
}

void OpenXrDevice::SetFrameDataRestricted(bool restricted) {
  // Presentation sessions can not currently be restricted.
  NOTREACHED_IN_MIGRATION();
}

}  // namespace device