blob: 680398da5942e20e14f5edbbe2ec97d0dc11f728 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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>
#if defined(OS_ANDROID)
#include "base/android/android_hardware_buffer_compat.h"
#endif
#include "base/callback_helpers.h"
#include "base/numerics/ranges.h"
#include "base/stl_util.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/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 "ui/gfx/transform.h"
#include "ui/gfx/transform_util.h"
namespace content {
namespace {
bool IsValidTransform(const gfx::Transform& transform,
float max_translate_meters) {
if (!transform.IsInvertible() || transform.HasPerspective())
return false;
gfx::DecomposedTransform decomp;
if (!DecomposeTransform(&decomp, transform))
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;
if (abs(decomp.translate[i]) > max_translate_meters)
return false;
}
// Only rotate and translate.
return true;
}
device::mojom::VREyeParametersPtr ValidateEyeParameters(
const device::mojom::VREyeParameters* eye) {
if (!eye)
return nullptr;
device::mojom::VREyeParametersPtr ret = device::mojom::VREyeParameters::New();
// FOV
float kDefaultFOV = 45;
ret->field_of_view = device::mojom::VRFieldOfView::New();
if (eye->field_of_view->up_degrees < 90 &&
eye->field_of_view->up_degrees > -90 &&
eye->field_of_view->up_degrees > -eye->field_of_view->down_degrees &&
eye->field_of_view->down_degrees < 90 &&
eye->field_of_view->down_degrees > -90 &&
eye->field_of_view->down_degrees > -eye->field_of_view->up_degrees &&
eye->field_of_view->left_degrees < 90 &&
eye->field_of_view->left_degrees > -90 &&
eye->field_of_view->left_degrees > -eye->field_of_view->right_degrees &&
eye->field_of_view->right_degrees < 90 &&
eye->field_of_view->right_degrees > -90 &&
eye->field_of_view->right_degrees > -eye->field_of_view->left_degrees) {
ret->field_of_view->up_degrees = eye->field_of_view->up_degrees;
ret->field_of_view->down_degrees = eye->field_of_view->down_degrees;
ret->field_of_view->left_degrees = eye->field_of_view->left_degrees;
ret->field_of_view->right_degrees = eye->field_of_view->right_degrees;
} else {
ret->field_of_view->up_degrees = kDefaultFOV;
ret->field_of_view->down_degrees = kDefaultFOV;
ret->field_of_view->left_degrees = kDefaultFOV;
ret->field_of_view->right_degrees = kDefaultFOV;
}
// Head-from-Eye Transform
// Maximum 10m translation.
if (IsValidTransform(eye->head_from_eye, 10)) {
ret->head_from_eye = eye->head_from_eye;
}
// else, ret->head_from_eye remains the identity transform
// Renderwidth/height
uint32_t kMaxSize = 16384;
uint32_t kMinSize = 2;
// DCHECK on debug builds to catch legitimate large sizes, but clamp on
// release builds to ensure valid state.
DCHECK(eye->render_width < kMaxSize);
DCHECK(eye->render_height < kMaxSize);
ret->render_width = base::ClampToRange(eye->render_width, kMinSize, kMaxSize);
ret->render_height =
base::ClampToRange(eye->render_height, kMinSize, kMaxSize);
return ret;
}
device::mojom::VRDisplayInfoPtr ValidateVRDisplayInfo(
const device::mojom::VRDisplayInfo* info) {
if (!info)
return nullptr;
device::mojom::VRDisplayInfoPtr ret = device::mojom::VRDisplayInfo::New();
ret->left_eye = ValidateEyeParameters(info->left_eye.get());
ret->right_eye = ValidateEyeParameters(info->right_eye.get());
return ret;
}
// TODO(crbug.com/995377): Report these from the device runtime instead.
constexpr device::mojom::XRSessionFeature kOrientationDeviceFeatures[] = {
device::mojom::XRSessionFeature::REF_SPACE_VIEWER,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
};
constexpr device::mojom::XRSessionFeature kGVRDeviceFeatures[] = {
device::mojom::XRSessionFeature::REF_SPACE_VIEWER,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
};
constexpr device::mojom::XRSessionFeature kARCoreDeviceFeatures[] = {
device::mojom::XRSessionFeature::REF_SPACE_VIEWER,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED,
device::mojom::XRSessionFeature::DOM_OVERLAY,
device::mojom::XRSessionFeature::LIGHT_ESTIMATION,
device::mojom::XRSessionFeature::ANCHORS,
device::mojom::XRSessionFeature::PLANE_DETECTION,
device::mojom::XRSessionFeature::DEPTH,
device::mojom::XRSessionFeature::IMAGE_TRACKING,
};
#if BUILDFLAG(ENABLE_OPENXR)
constexpr device::mojom::XRSessionFeature kOpenXRFeatures[] = {
device::mojom::XRSessionFeature::REF_SPACE_VIEWER,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL,
device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR,
device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR,
device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED,
device::mojom::XRSessionFeature::ANCHORS,
};
#endif
} // anonymous namespace
BrowserXRRuntimeImpl::BrowserXRRuntimeImpl(
device::mojom::XRDeviceId id,
device::mojom::XRDeviceDataPtr device_data,
mojo::PendingRemote<device::mojom::XRRuntime> runtime,
device::mojom::VRDisplayInfoPtr display_info)
: id_(id),
device_data_(std::move(device_data)),
runtime_(std::move(runtime)),
display_info_(ValidateVRDisplayInfo(display_info.get())) {
DVLOG(2) << __func__ << ": id=" << id;
// Unretained is safe because we are calling through an InterfacePtr we own,
// so we won't be called after runtime_ is destroyed.
runtime_->ListenToDeviceChanges(
receiver_.BindNewEndpointAndPassRemote(),
base::BindOnce(&BrowserXRRuntimeImpl::OnDisplayInfoChanged,
base::Unretained(this)));
// TODO(crbug.com/1031622): 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_);
}
}
BrowserXRRuntimeImpl::~BrowserXRRuntimeImpl() {
DVLOG(2) << __func__ << ": id=" << id_;
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 {
switch (id_) {
// Test/fake devices support all features.
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
return true;
case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
// Only support hit test if the feature flag is enabled.
if (feature == device::mojom::XRSessionFeature::HIT_TEST) {
return base::FeatureList::IsEnabled(features::kWebXrHitTest);
}
#if defined(OS_ANDROID)
// Only support camera access if the feature flag is enabled & the device
// supports shared buffers.
if (feature == device::mojom::XRSessionFeature::CAMERA_ACCESS) {
return base::FeatureList::IsEnabled(features::kWebXrIncubations) &&
base::AndroidHardwareBufferCompat::IsSupportAvailable();
}
#endif
return base::Contains(kARCoreDeviceFeatures, feature);
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
return base::Contains(kOrientationDeviceFeatures, feature);
case device::mojom::XRDeviceId::GVR_DEVICE_ID:
return base::Contains(kGVRDeviceFeatures, feature);
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
return base::Contains(kOpenXRFeatures, feature);
#endif
}
NOTREACHED();
}
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::ARCORE_DEVICE_ID:
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
case device::mojom::XRDeviceId::GVR_DEVICE_ID:
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::ARCORE_DEVICE_ID:
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
return false;
case device::mojom::XRDeviceId::GVR_DEVICE_ID:
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
#endif
return true;
}
NOTREACHED();
}
bool BrowserXRRuntimeImpl::SupportsArBlendMode() {
return device_data_->is_ar_blend_mode_supported;
}
void BrowserXRRuntimeImpl::OnDisplayInfoChanged(
device::mojom::VRDisplayInfoPtr vr_device_info) {
bool had_display_info = !!display_info_;
display_info_ = ValidateVRDisplayInfo(vr_device_info.get());
if (had_display_info) {
for (VRServiceImpl* service : services_) {
service->OnDisplayInfoChanged();
}
}
// Notify observers of the new display info.
for (Observer& observer : observers_) {
observer.SetVRDisplayInfo(display_info_.Clone());
}
}
void BrowserXRRuntimeImpl::StopImmersiveSession(
VRServiceImpl::ExitPresentCallback on_exited) {
DVLOG(2) << __func__;
if (immersive_session_controller_) {
immersive_session_controller_.reset();
if (presenting_service_) {
presenting_service_->OnExitPresent();
presenting_service_ = nullptr;
}
for (Observer& observer : observers_) {
observer.SetWebXRWebContents(nullptr);
}
}
std::move(on_exited).Run();
}
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_) {
ExitPresent(service, base::DoNothing());
}
}
void BrowserXRRuntimeImpl::ExitPresent(
VRServiceImpl* service,
VRServiceImpl::ExitPresentCallback on_exited) {
DVLOG(2) << __func__ << ": id=" << id_ << " service=" << service
<< " presenting_service_=" << presenting_service_;
if (service == presenting_service_) {
runtime_->ShutdownSession(
base::BindOnce(&BrowserXRRuntimeImpl::StopImmersiveSession,
weak_ptr_factory_.GetWeakPtr(), std::move(on_exited)));
}
}
void BrowserXRRuntimeImpl::SetFramesThrottled(const VRServiceImpl* service,
bool throttled) {
if (service == presenting_service_) {
for (Observer& observer : observers_) {
observer.SetFramesThrottled(throttled);
}
}
}
void BrowserXRRuntimeImpl::RequestSession(
VRServiceImpl* service,
const 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.
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::XRSessionPtr session,
mojo::PendingRemote<device::mojom::XRSessionController>
immersive_session_controller) {
if (session && service) {
DVLOG(2) << __func__ << ": id=" << id_;
if (device::XRSessionModeUtils::IsImmersive(options->mode)) {
presenting_service_ = service.get();
immersive_session_controller_.Bind(
std::move(immersive_session_controller));
immersive_session_controller_.set_disconnect_handler(
base::BindOnce(&BrowserXRRuntimeImpl::OnImmersiveSessionError,
base::Unretained(this)));
// Notify observers that we have started presentation.
content::WebContents* web_contents = service->GetWebContents();
for (Observer& observer : observers_) {
observer.SetWebXRWebContents(web_contents);
}
}
std::move(callback).Run(std::move(session));
} else {
std::move(callback).Run(nullptr);
if (session) {
// 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(immersive_session_controller));
StopImmersiveSession(base::DoNothing());
}
}
}
void BrowserXRRuntimeImpl::EnsureInstalled(
int render_process_id,
int render_frame_id,
base::OnceCallback<void(bool)> install_callback) {
// 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(base::DoNothing());
}
void BrowserXRRuntimeImpl::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
observer->SetVRDisplayInfo(display_info_.Clone());
}
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(base::DoNothing());
}
#if defined(OS_WIN)
base::Optional<LUID> BrowserXRRuntimeImpl::GetLuid() const {
return device_data_->luid;
}
#endif
} // namespace content