blob: 42875c39d095af251759bd10104cae75092d2b22 [file] [log] [blame]
// 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 "content/browser/xr/service/browser_xr_runtime_impl.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/observer_list.h"
#include "build/build_config.h"
#include "content/browser/xr/service/vr_service_impl.h"
#include "content/browser/xr/xr_utils.h"
#include "content/public/browser/browser_xr_runtime.h"
#include "content/public/browser/xr_install_helper.h"
#include "content/public/browser/xr_integration_client.h"
#include "content/public/common/content_features.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/public/cpp/session_mode.h"
#include "device/vr/public/mojom/vr_service.mojom-shared.h"
#include "device/vr/public/mojom/xr_device.mojom-shared.h"
#include "device/vr/public/mojom/xr_session.mojom-shared.h"
#include "ui/gfx/geometry/decomposed_transform.h"
#include "ui/gfx/geometry/transform.h"
#if BUILDFLAG(IS_WIN)
#include "base/win/windows_types.h"
#endif
#if BUILDFLAG(IS_ANDROID)
#include "base/android/android_hardware_buffer_compat.h"
#endif
namespace content {
namespace {
bool IsValidTransform(const gfx::Transform& transform) {
if (!transform.IsInvertible() || transform.HasPerspective())
return false;
std::optional<gfx::DecomposedTransform> decomp = transform.Decompose();
if (!decomp)
return false;
float kEpsilon = 0.1f;
if (abs(decomp->perspective[3] - 1) > kEpsilon) {
// If testing with unexpectedly high values, catch on debug builds rather
// than silently change data. On release builds its better to be safe and
// validate.
DCHECK(false);
return false;
}
for (int i = 0; i < 3; ++i) {
if (abs(decomp->scale[i] - 1) > kEpsilon)
return false;
if (abs(decomp->skew[i]) > kEpsilon)
return false;
if (abs(decomp->perspective[i]) > kEpsilon)
return false;
}
// Only rotate and translate.
return true;
}
device::mojom::XRViewPtr ValidateXRView(const device::mojom::XRView* view) {
if (!view) {
return nullptr;
}
device::mojom::XRViewPtr ret = device::mojom::XRView::New();
ret->eye = view->eye;
// FOV
float kDefaultFOV = 45;
ret->geometry = device::mojom::XRViewGeometry::New();
ret->geometry->field_of_view = device::mojom::VRFieldOfView::New();
auto& view_fov = view->geometry->field_of_view;
auto& ret_fov = ret->geometry->field_of_view;
if (view_fov->up_degrees < 90 && view_fov->up_degrees > -90 &&
view_fov->up_degrees > -view_fov->down_degrees &&
view_fov->down_degrees < 90 && view_fov->down_degrees > -90 &&
view_fov->down_degrees > -view_fov->up_degrees &&
view_fov->left_degrees < 90 && view_fov->left_degrees > -90 &&
view_fov->left_degrees > -view_fov->right_degrees &&
view_fov->right_degrees < 90 && view_fov->right_degrees > -90 &&
view_fov->right_degrees > -view_fov->left_degrees) {
ret_fov->up_degrees = view_fov->up_degrees;
ret_fov->down_degrees = view_fov->down_degrees;
ret_fov->left_degrees = view_fov->left_degrees;
ret_fov->right_degrees = view_fov->right_degrees;
} else {
ret_fov->up_degrees = kDefaultFOV;
ret_fov->down_degrees = kDefaultFOV;
ret_fov->left_degrees = kDefaultFOV;
ret_fov->right_degrees = kDefaultFOV;
}
if (IsValidTransform(view->geometry->mojo_from_view)) {
ret->geometry->mojo_from_view = view->geometry->mojo_from_view;
}
// else, ret->geometry->mojo_from_view remains the identity transform
// Renderwidth/height
int kMaxSize = 16384;
int kMinSize = 2;
// DCHECK on debug builds to catch legitimate large sizes, but clamp on
// release builds to ensure valid state.
DCHECK_LT(view->viewport.width() + view->viewport.x(), kMaxSize);
DCHECK_LT(view->viewport.height() + view->viewport.y(), kMaxSize);
DCHECK_GT(view->viewport.width() + view->viewport.x(), kMinSize);
DCHECK_GT(view->viewport.height() + view->viewport.y(), kMinSize);
ret->viewport =
gfx::Rect(std::clamp(view->viewport.x(), 0, kMaxSize),
std::clamp(view->viewport.y(), 0, kMaxSize),
std::clamp(view->viewport.width(), kMinSize, kMaxSize),
std::clamp(view->viewport.height(), kMinSize, kMaxSize));
return ret;
}
} // anonymous namespace
BrowserXRRuntimeImpl::BrowserXRRuntimeImpl(
device::mojom::XRDeviceId id,
device::mojom::XRDeviceDataPtr device_data,
mojo::PendingRemote<device::mojom::XRRuntime> runtime)
: id_(id),
device_data_(std::move(device_data)),
runtime_(std::move(runtime)) {
DVLOG(2) << __func__ << ": id=" << id;
runtime_->ListenToDeviceChanges(receiver_.BindNewEndpointAndPassRemote());
// TODO(crbug.com/40662458): Convert this to a query for the client off of
// ContentBrowserClient once BrowserXRRuntimeImpl moves to content.
auto* integration_client = GetXrIntegrationClient();
if (integration_client) {
install_helper_ = integration_client->GetInstallHelper(id_);
runtime_observer_ = integration_client->CreateRuntimeObserver();
if (runtime_observer_) {
AddObserver(runtime_observer_.get());
}
}
}
BrowserXRRuntimeImpl::~BrowserXRRuntimeImpl() {
DVLOG(2) << __func__ << ": id=" << id_;
if (runtime_observer_) {
RemoveObserver(runtime_observer_.get());
}
if (install_finished_callback_) {
std::move(install_finished_callback_).Run(false);
}
}
void BrowserXRRuntimeImpl::ExitActiveImmersiveSession() {
DVLOG(2) << __func__;
auto* service = GetServiceWithActiveImmersiveSession();
if (service) {
service->ExitPresent(base::DoNothing());
}
}
bool BrowserXRRuntimeImpl::SupportsFeature(
device::mojom::XRSessionFeature feature) const {
if(id_ == device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID ||
id_ == device::mojom::XRDeviceId::FAKE_DEVICE_ID)
return true;
return base::Contains(device_data_->supported_features, feature);
}
bool BrowserXRRuntimeImpl::SupportsAllFeatures(
const std::vector<device::mojom::XRSessionFeature>& features) const {
for (const auto& feature : features) {
if (!SupportsFeature(feature))
return false;
}
return true;
}
bool BrowserXRRuntimeImpl::SupportsCustomIPD() const {
switch (id_) {
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
#if BUILDFLAG(ENABLE_ARCORE)
case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
#endif // ENABLE_ARCORE
#if BUILDFLAG(ENABLE_CARDBOARD)
case device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID:
#endif // ENABLE_CARDBOARD
return false;
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
return true;
#endif
}
NOTREACHED();
}
bool BrowserXRRuntimeImpl::SupportsNonEmulatedHeight() const {
switch (id_) {
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
#if BUILDFLAG(ENABLE_ARCORE)
case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
#endif // ENABLE_ARCORE
return false;
#if BUILDFLAG(ENABLE_CARDBOARD)
case device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID:
return true;
#endif // ENABLE_CARDBOARD
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
return true;
#endif // ENABLE_OPENXR
}
NOTREACHED();
}
bool BrowserXRRuntimeImpl::SupportsArBlendMode() {
return device_data_->is_ar_blend_mode_supported;
}
void BrowserXRRuntimeImpl::StopImmersiveSession() {
DVLOG(2) << __func__;
if (immersive_session_has_camera_access_) {
for (Observer& observer : observers_) {
observer.WebXRCameraInUseChanged(nullptr, false);
}
immersive_session_has_camera_access_ = false;
}
if (immersive_session_controller_) {
immersive_session_controller_.reset();
if (presenting_service_) {
presenting_service_->OnExitPresent();
presenting_service_ = nullptr;
}
}
vr_ui_host_.reset();
}
void BrowserXRRuntimeImpl::OnExitPresent() {
DVLOG(2) << __func__;
if (presenting_service_) {
presenting_service_->OnExitPresent();
presenting_service_ = nullptr;
}
}
void BrowserXRRuntimeImpl::OnVisibilityStateChanged(
device::mojom::XRVisibilityState visibility_state) {
for (VRServiceImpl* service : services_) {
service->OnVisibilityStateChanged(visibility_state);
}
}
void BrowserXRRuntimeImpl::OnServiceAdded(VRServiceImpl* service) {
DVLOG(2) << __func__ << ": id=" << id_;
services_.insert(service);
}
void BrowserXRRuntimeImpl::OnServiceRemoved(VRServiceImpl* service) {
DVLOG(2) << __func__ << ": id=" << id_;
DCHECK(service);
services_.erase(service);
if (service == presenting_service_) {
// Our presenting service is no longer valid, so we need to clear it before
// shutting down the session on the runtime side. Note that while
// `ExitPresent` looks similar, it may not be called by the presenting
// service, in which case the service needs to be notified after the
// shutdown is completed, so we can't simply move the check/clear down into
// `ShutdownRuntime`.
presenting_service_ = nullptr;
ShutdownRuntime();
}
}
void BrowserXRRuntimeImpl::ExitPresent(VRServiceImpl* service) {
DVLOG(2) << __func__ << ": id=" << id_ << " service=" << service
<< " presenting_service_=" << presenting_service_;
if (service == presenting_service_) {
ShutdownRuntime();
}
}
void BrowserXRRuntimeImpl::ShutdownRuntime() {
// As part of it's shutdown, the runtime will disconnect this pipe. If we do
// not clear the current disconnect handler we'll essentially signal to blink
// too early that the session has shutdown. This has led to race conditions in
// tests that end the session from blink and then immediately start a new
// session where the pending `StopImmersiveSession` callback happens after a
// new session was granted and then kills the new session.
immersive_session_controller_.set_disconnect_handler(base::DoNothing());
runtime_->ShutdownSession(
base::BindOnce(&BrowserXRRuntimeImpl::StopImmersiveSession,
weak_ptr_factory_.GetWeakPtr()));
}
void BrowserXRRuntimeImpl::SetFramesThrottled(const VRServiceImpl* service,
bool throttled) {
if (service == presenting_service_ && vr_ui_host_) {
vr_ui_host_->WebXRFramesThrottledChanged(throttled);
}
}
void BrowserXRRuntimeImpl::RequestInlineSession(
device::mojom::XRRuntimeSessionOptionsPtr options,
device::mojom::XRRuntime::RequestSessionCallback callback) {
runtime_->RequestSession(std::move(options), std::move(callback));
}
void BrowserXRRuntimeImpl::RequestImmersiveSession(
VRServiceImpl* service,
device::mojom::XRRuntimeSessionOptionsPtr options,
RequestSessionCallback callback) {
DVLOG(2) << __func__ << ": id=" << id_;
// base::Unretained is safe because we won't be called back after runtime_ is
// destroyed.
has_pending_immersive_session_request_ = true;
runtime_->RequestSession(
options->Clone(),
base::BindOnce(&BrowserXRRuntimeImpl::OnRequestSessionResult,
base::Unretained(this), service->GetWeakPtr(),
options->Clone(), std::move(callback)));
}
void BrowserXRRuntimeImpl::OnRequestSessionResult(
base::WeakPtr<VRServiceImpl> service,
device::mojom::XRRuntimeSessionOptionsPtr options,
RequestSessionCallback callback,
device::mojom::XRRuntimeSessionResultPtr session_result) {
has_pending_immersive_session_request_ = false;
if (session_result && service) {
DVLOG(2) << __func__ << ": id=" << id_;
if (device::XRSessionModeUtils::IsImmersive(options->mode)) {
presenting_service_ = service.get();
immersive_session_controller_.Bind(std::move(session_result->controller));
immersive_session_controller_.set_disconnect_handler(
base::BindOnce(&BrowserXRRuntimeImpl::OnImmersiveSessionError,
base::Unretained(this)));
content::WebContents* web_contents = service->GetWebContents();
auto* integration_client = GetXrIntegrationClient();
if (session_result->overlay && integration_client && web_contents) {
// We have enough information to create a VrUiHost, so validate that
// information then attempt to create it.
std::vector<device::mojom::XRViewPtr>& views =
session_result->session->device_config->views;
for (device::mojom::XRViewPtr& view : views) {
view = ValidateXRView(view.get());
}
// The overlay code requires the left and right views to render.
if (!base::Contains(views, device::mojom::XREye::kLeft,
&device::mojom::XRView::eye) ||
!base::Contains(views, device::mojom::XREye::kRight,
&device::mojom::XRView::eye)) {
// Notify the service to cleanup any session that it's started to
// setup, and when that and our corresponding runtime shutdown have
// finished, notify the page that the session request failed.
service->ExitPresent(base::BindOnce(
[](RequestSessionCallback callback) {
std::move(callback).Run(nullptr);
},
std::move(callback)));
return;
}
vr_ui_host_ = integration_client->CreateVrUiHost(
*web_contents, views, std::move(session_result->overlay));
}
immersive_session_has_camera_access_ =
base::Contains(session_result->session->enabled_features,
device::mojom::XRSessionFeature::CAMERA_ACCESS);
if (immersive_session_has_camera_access_) {
for (Observer& observer : observers_) {
observer.WebXRCameraInUseChanged(web_contents, true);
}
}
}
std::move(callback).Run(std::move(session_result));
} else {
std::move(callback).Run(nullptr);
if (session_result) {
// The service has been removed, but we still got a session, so make
// sure to clean up this weird state.
immersive_session_controller_.Bind(std::move(session_result->controller));
StopImmersiveSession();
}
}
}
void BrowserXRRuntimeImpl::EnsureInstalled(
int render_process_id,
int render_frame_id,
base::OnceCallback<void(bool)> install_callback) {
DVLOG(2) << __func__;
// If there's no install helper, then we can assume no install is needed.
if (!install_helper_) {
std::move(install_callback).Run(true);
return;
}
// Only the most recent caller will be notified of a successful install.
bool had_outstanding_callback = false;
if (install_finished_callback_) {
had_outstanding_callback = true;
std::move(install_finished_callback_).Run(false);
}
install_finished_callback_ = std::move(install_callback);
// If we already had a cached install callback, then we don't need to query
// for installation again.
if (had_outstanding_callback)
return;
install_helper_->EnsureInstalled(
render_process_id, render_frame_id,
base::BindOnce(&BrowserXRRuntimeImpl::OnInstallFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void BrowserXRRuntimeImpl::OnInstallFinished(bool succeeded) {
DCHECK(install_finished_callback_);
std::move(install_finished_callback_).Run(succeeded);
}
void BrowserXRRuntimeImpl::OnImmersiveSessionError() {
DVLOG(2) << __func__ << ": id=" << id_;
StopImmersiveSession();
}
void BrowserXRRuntimeImpl::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void BrowserXRRuntimeImpl::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void BrowserXRRuntimeImpl::BeforeRuntimeRemoved() {
DVLOG(1) << __func__ << ": id=" << id_;
// If the device process crashes or otherwise gets removed, it's a race as to
// whether or not our mojo interface to the device gets reset before we're
// deleted as the result of the device provider being destroyed.
// Since this no-ops if we don't have an active immersive session, try to end
// any immersive session we may be currently responsible for.
StopImmersiveSession();
}
std::vector<device::mojom::XRSessionFeature>
BrowserXRRuntimeImpl::GetSupportedFeatures() {
return device_data_->supported_features;
}
#if BUILDFLAG(IS_WIN)
std::optional<CHROME_LUID> BrowserXRRuntimeImpl::GetLuid() const {
return device_data_->luid;
}
#endif
} // namespace content