chromium/device/vr/android/arcore/arcore_device.cc

// Copyright 2018 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/android/arcore/arcore_device.h"

#include <algorithm>
#include <optional>

#include "base/android/android_hardware_buffer_compat.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "device/vr/android/arcore/ar_image_transport.h"
#include "device/vr/android/arcore/arcore_gl.h"
#include "device/vr/android/arcore/arcore_gl_thread.h"
#include "device/vr/android/arcore/arcore_impl.h"
#include "device/vr/android/compositor_delegate_provider.h"
#include "device/vr/android/mailbox_to_surface_bridge.h"
#include "device/vr/android/xr_java_coordinator.h"
#include "device/vr/public/cpp/xr_frame_sink_client.h"
#include "ui/android/window_android.h"
#include "ui/display/display.h"

using base::android::JavaRef;

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_UNBOUNDED,
                          mojom::XRSessionFeature::DOM_OVERLAY,
                          mojom::XRSessionFeature::LIGHT_ESTIMATION,
                          mojom::XRSessionFeature::ANCHORS,
                          mojom::XRSessionFeature::PLANE_DETECTION,
                          mojom::XRSessionFeature::DEPTH,
                          mojom::XRSessionFeature::IMAGE_TRACKING,
                          mojom::XRSessionFeature::HIT_TEST,
                          mojom::XRSessionFeature::FRONT_FACING}};

  return *kSupportedFeatures;
}

}  // namespace

ArCoreDevice::SessionState::SessionState() = default;
ArCoreDevice::SessionState::~SessionState() = default;

ArCoreDevice::ArCoreDevice(
    std::unique_ptr<ArCoreFactory> arcore_factory,
    std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory,
    std::unique_ptr<MailboxToSurfaceBridgeFactory>
        mailbox_to_surface_bridge_factory,
    std::unique_ptr<XrJavaCoordinator> xr_java_coordinator,
    std::unique_ptr<CompositorDelegateProvider> compositor_delegate_provider,
    XrFrameSinkClientFactory xr_frame_sink_client_factory)
    : VRDeviceBase(mojom::XRDeviceId::ARCORE_DEVICE_ID),
      main_thread_task_runner_(
          base::SingleThreadTaskRunner::GetCurrentDefault()),
      arcore_factory_(std::move(arcore_factory)),
      ar_image_transport_factory_(std::move(ar_image_transport_factory)),
      mailbox_bridge_factory_(std::move(mailbox_to_surface_bridge_factory)),
      xr_java_coordinator_(std::move(xr_java_coordinator)),
      compositor_delegate_provider_(std::move(compositor_delegate_provider)),
      xr_frame_sink_client_factory_(std::move(xr_frame_sink_client_factory)),
      mailbox_bridge_(mailbox_bridge_factory_->Create()),
      session_state_(std::make_unique<ArCoreDevice::SessionState>()) {
  CHECK(compositor_delegate_provider_);
  // ARCORE always support AR blend modes
  SetArBlendModeSupported(true);

  std::vector<mojom::XRSessionFeature> device_features(
        GetSupportedFeatures());

  // Only support camera access if the device supports shared buffers.
  if (base::AndroidHardwareBufferCompat::IsSupportAvailable())
    device_features.emplace_back(mojom::XRSessionFeature::CAMERA_ACCESS);

  SetSupportedFeatures(device_features);
}

ArCoreDevice::~ArCoreDevice() {
  // If there's still a pending session request, reject it.
  CallDeferredRequestSessionCallback(
      base::unexpected(ArCoreGlInitializeError::kFailure));

  // Ensure that any active sessions are terminated. Terminating the GL thread
  // would normally do so via its session_shutdown_callback_, but that happens
  // asynchronously and it doesn't seem safe to depend on all posted tasks being
  // handled before the thread is shut down.
  // Repeated EndSession calls are a no-op, so it's OK to do this redundantly.
  OnSessionEnded();

  // The GL thread must be terminated since it uses our members. For example,
  // there might still be a posted Initialize() call in flight that uses
  // xr_java_coordinator_ and arcore_factory_. Ensure that the thread is
  // stopped before other members get destructed. Don't call Stop() here,
  // destruction calls Stop() and doing so twice is illegal (null pointer
  // dereference).
  session_state_->arcore_gl_thread_ = nullptr;
}

void ArCoreDevice::RequestSession(
    mojom::XRRuntimeSessionOptionsPtr options,
    mojom::XRRuntime::RequestSessionCallback callback) {
  DVLOG(1) << __func__;
  DCHECK(IsOnMainThread());

  if (session_state_->allow_retry_) {
    session_state_->options_clone_for_retry_ = options.Clone();
  }

  DCHECK(options->mode == device::mojom::XRSessionMode::kImmersiveAr);

  if (HasExclusiveSession()) {
    TRACE_EVENT("xr", "ArCoreDevice::RequestSession: session already exists",
                perfetto::Flow::Global(options->trace_id));

    DVLOG(1) << __func__ << ": Rejecting additional session request";
    std::move(callback).Run(nullptr);
    return;
  }

  // Set HasExclusiveSession status to true. This lasts until OnSessionEnded.
  OnStartPresenting();

  DCHECK(!session_state_->pending_request_session_callback_);
  session_state_->pending_request_session_callback_ = std::move(callback);
  session_state_->required_features_.insert(options->required_features.begin(),
                                            options->required_features.end());
  session_state_->optional_features_.insert(options->optional_features.begin(),
                                            options->optional_features.end());
  session_state_->request_session_trace_id_ = options->trace_id;

  const bool use_dom_overlay =
      base::Contains(options->required_features,
                     device::mojom::XRSessionFeature::DOM_OVERLAY) ||
      base::Contains(options->optional_features,
                     device::mojom::XRSessionFeature::DOM_OVERLAY);

  session_state_->depth_options_ = std::move(options->depth_options);

  // mailbox_bridge_ is either supplied from the constructor, or recreated in
  // OnSessionEnded().
  DCHECK(mailbox_bridge_);

  // We create the FrameSinkClient here and clear it in OnSessionEnded.
  DCHECK(!frame_sink_client_);
  frame_sink_client_ = xr_frame_sink_client_factory_.Run(
      options->render_process_id, options->render_frame_id);
  DCHECK(frame_sink_client_);

  for (auto& image : options->tracked_images) {
    DVLOG(3) << __func__ << ": tracked image size_in_pixels="
             << image->size_in_pixels.ToString();
    session_state_->tracked_images_.push_back(std::move(image));
  }

  session_state_->arcore_gl_thread_ = std::make_unique<ArCoreGlThread>(
      std::move(ar_image_transport_factory_), std::move(mailbox_bridge_),
      base::BindPostTask(
          main_thread_task_runner_,
          base::BindOnce(&ArCoreDevice::OnGlThreadReady, GetWeakPtr(),
                         options->render_process_id, options->render_frame_id,
                         use_dom_overlay)));
  session_state_->arcore_gl_thread_->Start();
}

void ArCoreDevice::OnGlThreadReady(int render_process_id,
                                   int render_frame_id,
                                   bool use_overlay) {
  auto ready_callback =
      base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceReady, GetWeakPtr());
  auto touch_callback =
      base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceTouch, GetWeakPtr());
  auto destroyed_callback =
      base::BindOnce(&ArCoreDevice::OnDrawingSurfaceDestroyed, GetWeakPtr());

  bool can_render_dom_content =
      session_state_->arcore_gl_thread_->GetArCoreGl()->CanRenderDOMContent();

  xr_java_coordinator_->RequestArSession(
      render_process_id, render_frame_id, use_overlay, can_render_dom_content,
      *compositor_delegate_provider_.get(), std::move(ready_callback),
      std::move(touch_callback), std::move(destroyed_callback));
}

void ArCoreDevice::OnDrawingSurfaceReady(gfx::AcceleratedWidget window,
                                         gpu::SurfaceHandle surface_handle,
                                         ui::WindowAndroid* root_window,
                                         display::Display::Rotation rotation,
                                         const gfx::Size& frame_size) {
  DVLOG(1) << __func__ << ": size=" << frame_size.width() << "x"
           << frame_size.height() << " rotation=" << static_cast<int>(rotation);
  DCHECK(!session_state_->is_arcore_gl_initialized_);

  RequestArCoreGlInitialization(window, surface_handle, root_window, rotation,
                                frame_size);
}

void ArCoreDevice::OnDrawingSurfaceTouch(bool is_primary,
                                         bool touching,
                                         int32_t pointer_id,
                                         const gfx::PointF& location) {
  DVLOG(2) << __func__ << ": pointer_id=" << pointer_id
           << " is_primary=" << is_primary << " touching=" << touching;

  if (!session_state_->is_arcore_gl_initialized_ ||
      !session_state_->arcore_gl_thread_)
    return;

  PostTaskToGlThread(base::BindOnce(
      &ArCoreGl::OnScreenTouch,
      session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
      is_primary, touching, pointer_id, location));
}

void ArCoreDevice::OnDrawingSurfaceDestroyed() {
  DVLOG(1) << __func__;

  if (session_state_->initiate_retry_) {
    // If we get here, the drawing surface was destroyed intentionally in
    // OnArCoreGlInitializationComplete due to a driver bug where we want to
    // retry with workarounds applied.
    DVLOG(1) << __func__ << ": initiating retry";

    // Grab the options and callback before they are cleared by OnSessionEnded.
    mojom::XRRuntimeSessionOptionsPtr options =
        std::move(session_state_->options_clone_for_retry_);
    mojom::XRRuntime::RequestSessionCallback callback =
        std::move(session_state_->pending_request_session_callback_);

    // Reset session_state_ back to defaults.
    OnSessionEnded();

    // Update the freshly-reset session state to not allow further retries. We
    // don't want an infinite loop in case of logic errors.
    session_state_->allow_retry_ = false;

    RequestSession(std::move(options), std::move(callback));
    return;
  }

  CallDeferredRequestSessionCallback(
      base::unexpected(ArCoreGlInitializeError::kFailure));

  OnSessionEnded();
}

void ArCoreDevice::ShutdownSession(
    mojom::XRRuntime::ShutdownSessionCallback on_completed) {
  DVLOG(2) << __func__;
  OnDrawingSurfaceDestroyed();
  std::move(on_completed).Run();
}

void ArCoreDevice::OnSessionEnded() {
  DVLOG(1) << __func__;

  if (!HasExclusiveSession())
    return;

  // This may be a no-op in case session end was initiated from the Java side.
  xr_java_coordinator_->EndSession();

  // The GL thread had initialized its context with a drawing_widget based on
  // the XrImmersiveOverlay's Surface, and the one it has is no longer valid.
  // For now, just destroy the GL thread so that it is recreated for the next
  // session with fresh associated resources. Also go through these steps in
  // case the GL thread hadn't completed, or had initialized partially, to
  // ensure consistent state.

  // TODO(crbug.com/41392761): Instead of splitting the initialization
  // of this class between construction and RequestSession, perform all the
  // initialization at once on the first successful RequestSession call.

  // If we have a frame sink client, notify it that it's surface has been
  // destroyed. While this is required in the case of the surface actually being
  // destroyed, it's a good idea to do it before we actually end the session.
  // Note that this may trigger the bindings on the session to disconnect.
  if (frame_sink_client_)
    frame_sink_client_->SurfaceDestroyed();

  // Reset per-session members to initial values.
  session_state_ = std::make_unique<ArCoreDevice::SessionState>();

  // The frame sink client is re-requested when we start a new session, but once
  // a session has ended it should be destroyed. However, it needs to outlive
  // the gl thread.
  frame_sink_client_.reset();

  // The image transport factory should be reusable, but we've std::moved it
  // to the GL thread. Make a new one for next time. (This is cheap, it's
  // just a factory.)
  ar_image_transport_factory_ = std::make_unique<ArImageTransportFactory>();

  // Create a new mailbox bridge for use in the next session. (This is cheap,
  // the constructor doesn't establish a GL context.)
  mailbox_bridge_ = mailbox_bridge_factory_->Create();

  // This sets HasExclusiveSession status to false.
  OnExitPresent();
}

void ArCoreDevice::CallDeferredRequestSessionCallback(
    ArCoreGlInitializeStatus initialize_result) {
  DVLOG(1) << __func__ << " success=" << initialize_result.has_value();
  DCHECK(IsOnMainThread());

  // We might not have any pending session requests, i.e. if destroyed
  // immediately after construction.
  if (!session_state_->pending_request_session_callback_)
    return;

  mojom::XRRuntime::RequestSessionCallback deferred_callback =
      std::move(session_state_->pending_request_session_callback_);

  if (!initialize_result.has_value()) {
    TRACE_EVENT_WITH_FLOW0(
        "xr",
        "ArCoreDevice::CallDeferredRequestSessionCallback: GL initialization "
        "failed",
        TRACE_ID_GLOBAL(session_state_->request_session_trace_id_),
        TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);

    std::move(deferred_callback).Run(nullptr);
    return;
  }

  // Success case should only happen after GL thread is ready.
  auto create_callback = base::BindOnce(
      &ArCoreDevice::OnCreateSessionCallback, GetWeakPtr(),
      std::move(deferred_callback), std::move(*initialize_result));

  auto shutdown_callback =
      base::BindOnce(&ArCoreDevice::OnSessionEnded, GetWeakPtr());

  PostTaskToGlThread(base::BindOnce(
      &ArCoreGl::CreateSession,
      session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
      base::BindPostTask(main_thread_task_runner_, std::move(create_callback)),
      base::BindPostTask(main_thread_task_runner_,
                         std::move(shutdown_callback))));
}

void ArCoreDevice::OnCreateSessionCallback(
    mojom::XRRuntime::RequestSessionCallback deferred_callback,
    ArCoreGlInitializeResult initialize_result,
    ArCoreGlCreateSessionResult create_session_result) {
  DVLOG(2) << __func__;
  DCHECK(IsOnMainThread());

  auto session_result = mojom::XRRuntimeSessionResult::New();
  session_result->controller =
      std::move(create_session_result.session_controller);

  if (initialize_result.frame_sink_id.is_valid()) {
    session_result->frame_sink_id = initialize_result.frame_sink_id;
  }

  session_result->session = mojom::XRSession::New();
  auto* session = session_result->session.get();

  session->data_provider = std::move(create_session_result.frame_data_provider);
  session->submit_frame_sink =
      std::move(create_session_result.presentation_connection);
  session->enabled_features.assign(initialize_result.enabled_features.begin(),
                                   initialize_result.enabled_features.end());
  session->device_config = device::mojom::XRSessionDeviceConfig::New();
  auto* config = session->device_config.get();

  config->supports_viewport_scaling = true;
  config->depth_configuration =
      initialize_result.depth_configuration
          ? initialize_result.depth_configuration->Clone()
          : nullptr;
  config->views.push_back(std::move(create_session_result.view));

  // ARCORE only supports immersive-ar sessions
  session->enviroment_blend_mode =
      device::mojom::XREnvironmentBlendMode::kAlphaBlend;
  session->interaction_mode = device::mojom::XRInteractionMode::kScreenSpace;

  std::move(deferred_callback).Run(std::move(session_result));
}

void ArCoreDevice::PostTaskToGlThread(base::OnceClosure task) {
  DCHECK(IsOnMainThread());
  session_state_->arcore_gl_thread_->task_runner()->PostTask(FROM_HERE,
                                                             std::move(task));
}

bool ArCoreDevice::IsOnMainThread() {
  return main_thread_task_runner_->BelongsToCurrentThread();
}

void ArCoreDevice::RequestArCoreGlInitialization(
    gfx::AcceleratedWidget drawing_widget,
    gpu::SurfaceHandle surface_handle,
    ui::WindowAndroid* root_window,
    int drawing_rotation,
    const gfx::Size& frame_size) {
  DVLOG(1) << __func__;
  DCHECK(IsOnMainThread());

  if (!xr_java_coordinator_->EnsureARCoreLoaded()) {
    DLOG(ERROR) << "ARCore was not loaded properly.";
    OnArCoreGlInitializationComplete(
        base::unexpected(ArCoreGlInitializeError::kFailure));
    return;
  }

  if (!session_state_->is_arcore_gl_initialized_) {
    // We will only try to initialize ArCoreGl once, at the end of the
    // permission sequence, and will resolve pending requests that have queued
    // up once that initialization completes. We set is_arcore_gl_initialized_
    // in the callback to block operations that require it to be ready.
    auto rotation = static_cast<display::Display::Rotation>(drawing_rotation);
    PostTaskToGlThread(base::BindOnce(
        &ArCoreGl::Initialize,
        session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
        xr_java_coordinator_.get(), arcore_factory_.get(),
        frame_sink_client_.get(), drawing_widget, surface_handle, root_window,
        frame_size, rotation, session_state_->required_features_,
        session_state_->optional_features_,
        std::move(session_state_->tracked_images_),
        std::move(session_state_->depth_options_),
        base::BindPostTask(
            main_thread_task_runner_,
            base::BindOnce(&ArCoreDevice::OnArCoreGlInitializationComplete,
                           GetWeakPtr()))));
    return;
  }

  // Since the GL is already initialized, we already have session_state_ that we
  // can pass along.
  OnArCoreGlInitializationComplete(ArCoreGlInitializeResult(
      session_state_->enabled_features_, session_state_->depth_configuration_,
      session_state_->frame_sink_id_));
}

void ArCoreDevice::OnArCoreGlInitializationComplete(
    ArCoreGlInitializeStatus arcore_initialization_result) {
  DVLOG(1) << __func__ << ": arcore_initialization_result.has_value()="
           << arcore_initialization_result.has_value()
           << " session_state_->allow_retry_=" << session_state_->allow_retry_;
  DCHECK(IsOnMainThread());

  session_state_->is_arcore_gl_initialized_ =
      arcore_initialization_result.has_value();

  if (arcore_initialization_result.has_value()) {
    session_state_->enabled_features_ =
        arcore_initialization_result->enabled_features;
    session_state_->depth_configuration_ =
        arcore_initialization_result->depth_configuration;
    session_state_->frame_sink_id_ =
        arcore_initialization_result->frame_sink_id;
    // Clear the cloned options now that we know we don't need a retry. The
    // object size could be substantial, i.e. if it contains images for the
    // image tracking feature.
    session_state_->options_clone_for_retry_.reset();
  } else if (arcore_initialization_result.error() ==
                 ArCoreGlInitializeError::kRetryableFailure &&
             session_state_->allow_retry_) {
    session_state_->initiate_retry_ = true;
    // Exit the current incomplete session, this will destroy the drawing
    // surface.
    xr_java_coordinator_->EndSession();
    // The retry will happen in OnDrawingSurfaceDestroyed, so skip calling
    // the deferred callback.
    return;
  } else {
    session_state_->enabled_features_ = {};
    session_state_->depth_configuration_ = std::nullopt;
  }

  // We only start GL initialization after the user has granted consent, so we
  // can now start the session.
  CallDeferredRequestSessionCallback(std::move(arcore_initialization_result));
}

}  // namespace device