blob: 040cc84b40816b98f8fd80a4ea23b78263b9b1ee [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 "chrome/browser/vr/service/browser_xr_runtime.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "chrome/browser/vr/service/vr_service_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/vr_device.h"
#include "ui/gfx/transform.h"
#include "ui/gfx/transform_util.h"
namespace vr {
namespace {
bool IsValidStandingTransform(const gfx::Transform& transform) {
if (!transform.IsInvertible() || transform.HasPerspective())
return false;
gfx::DecomposedTransform decomp;
if (!DecomposeTransform(&decomp, transform))
return false;
float kEpsilon = 0.1f;
float kMaxTranslate = 1000000; // Maximum 1000km translation.
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]) > kMaxTranslate)
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;
}
// Offset
float kMaxOffset = 10;
if (abs(eye->offset.x()) < kMaxOffset && abs(eye->offset.y()) < kMaxOffset &&
abs(eye->offset.z()) < kMaxOffset) {
ret->offset = eye->offset;
} else {
ret->offset = gfx::Vector3dF(0, 0, 0);
}
// 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 = std::max(std::min(kMaxSize, eye->render_width), kMinSize);
ret->render_height =
std::max(std::min(kMaxSize, eye->render_height), kMinSize);
return ret;
}
device::mojom::VRDisplayInfoPtr ValidateVRDisplayInfo(
const device::mojom::VRDisplayInfo* info,
device::mojom::XRDeviceId id) {
if (!info)
return nullptr;
device::mojom::VRDisplayInfoPtr ret = device::mojom::VRDisplayInfo::New();
// Rather than just cloning everything, we copy over each field and validate
// individually. This ensures new fields don't bypass validation.
ret->id = id;
ret->display_name = info->display_name;
DCHECK(info->capabilities); // Ensured by mojo.
ret->capabilities = device::mojom::VRDisplayCapabilities::New(
info->capabilities->has_position,
info->capabilities->has_external_display, info->capabilities->can_present,
info->capabilities->can_provide_environment_integration);
if (info->stage_parameters &&
IsValidStandingTransform(info->stage_parameters->standing_transform)) {
ret->stage_parameters = device::mojom::VRStageParameters::New(
info->stage_parameters->standing_transform,
info->stage_parameters->size_x, info->stage_parameters->size_z,
info->stage_parameters->bounds);
}
ret->left_eye = ValidateEyeParameters(info->left_eye.get());
ret->right_eye = ValidateEyeParameters(info->right_eye.get());
float kMinFramebufferScale = 0.1f;
float kMaxFramebufferScale = 1.0f;
if (info->webvr_default_framebuffer_scale <= kMaxFramebufferScale &&
info->webvr_default_framebuffer_scale >= kMinFramebufferScale) {
ret->webvr_default_framebuffer_scale =
info->webvr_default_framebuffer_scale;
} else {
ret->webvr_default_framebuffer_scale = 1;
}
if (info->webxr_default_framebuffer_scale <= kMaxFramebufferScale &&
info->webxr_default_framebuffer_scale >= kMinFramebufferScale) {
ret->webxr_default_framebuffer_scale =
info->webxr_default_framebuffer_scale;
} else {
ret->webxr_default_framebuffer_scale = 1;
}
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,
};
#if BUILDFLAG(ENABLE_OPENVR)
constexpr device::mojom::XRSessionFeature kOpenVRFeatures[] = {
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,
};
#endif
#if BUILDFLAG(ENABLE_WINDOWS_MR)
constexpr device::mojom::XRSessionFeature kWindowsMixedRealityFeatures[] = {
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,
};
#endif
#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,
};
#endif
#if BUILDFLAG(ENABLE_OCULUS_VR)
constexpr device::mojom::XRSessionFeature kOculusFeatures[] = {
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,
};
#endif
bool ContainsFeature(
base::span<const device::mojom::XRSessionFeature> feature_list,
device::mojom::XRSessionFeature feature) {
return std::find(feature_list.begin(), feature_list.end(), feature) !=
feature_list.end();
}
} // anonymous namespace
BrowserXRRuntime::BrowserXRRuntime(device::mojom::XRDeviceId id,
device::mojom::XRRuntimePtr runtime,
device::mojom::VRDisplayInfoPtr display_info)
: id_(id),
runtime_(std::move(runtime)),
display_info_(ValidateVRDisplayInfo(display_info.get(), id)),
binding_(this) {
device::mojom::XRRuntimeEventListenerAssociatedPtr listener;
binding_.Bind(mojo::MakeRequest(&listener));
// Unretained is safe because we are calling through an InterfacePtr we own,
// so we won't be called after runtime_ is destroyed.
runtime_->ListenToDeviceChanges(
listener.PassInterface(),
base::BindOnce(&BrowserXRRuntime::OnDisplayInfoChanged,
base::Unretained(this)));
}
BrowserXRRuntime::~BrowserXRRuntime() = default;
void BrowserXRRuntime::ExitVrFromPresentingService() {
auto* service = GetPresentingVRService();
if (service) {
service->ExitPresent();
}
}
bool BrowserXRRuntime::SupportsFeature(
device::mojom::XRSessionFeature feature) const {
switch (id_) {
// TODO(crbug.com/995370): Add ARCore feature support.
case device::mojom::XRDeviceId::ARCORE_DEVICE_ID:
case device::mojom::XRDeviceId::WEB_TEST_DEVICE_ID:
case device::mojom::XRDeviceId::FAKE_DEVICE_ID:
return true;
case device::mojom::XRDeviceId::ORIENTATION_DEVICE_ID:
return ContainsFeature(kOrientationDeviceFeatures, feature);
case device::mojom::XRDeviceId::GVR_DEVICE_ID:
return ContainsFeature(kGVRDeviceFeatures, feature);
#if BUILDFLAG(ENABLE_OPENVR)
case device::mojom::XRDeviceId::OPENVR_DEVICE_ID:
return ContainsFeature(kOpenVRFeatures, feature);
#endif
#if BUILDFLAG(ENABLE_OCULUS_VR)
case device::mojom::XRDeviceId::OCULUS_DEVICE_ID:
return ContainsFeature(kOculusFeatures, feature);
#endif
#if BUILDFLAG(ENABLE_WINDOWS_MR)
case device::mojom::XRDeviceId::WINDOWS_MIXED_REALITY_ID:
return ContainsFeature(kWindowsMixedRealityFeatures, feature);
#endif
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
return ContainsFeature(kOpenXRFeatures, feature);
#endif
}
NOTREACHED();
}
bool BrowserXRRuntime::SupportsAllFeatures(
const std::vector<device::mojom::XRSessionFeature>& features) const {
for (const auto& feature : features) {
if (!SupportsFeature(feature))
return false;
}
return true;
}
bool BrowserXRRuntime::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_OPENVR)
case device::mojom::XRDeviceId::OPENVR_DEVICE_ID:
return true;
#endif
#if BUILDFLAG(ENABLE_OCULUS_VR)
case device::mojom::XRDeviceId::OCULUS_DEVICE_ID:
return true;
#endif
#if BUILDFLAG(ENABLE_WINDOWS_MR)
case device::mojom::XRDeviceId::WINDOWS_MIXED_REALITY_ID:
return true;
#endif
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
return true;
#endif
}
NOTREACHED();
}
bool BrowserXRRuntime::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_OPENVR)
case device::mojom::XRDeviceId::OPENVR_DEVICE_ID:
#endif
#if BUILDFLAG(ENABLE_OCULUS_VR)
case device::mojom::XRDeviceId::OCULUS_DEVICE_ID:
#endif
#if BUILDFLAG(ENABLE_WINDOWS_MR)
case device::mojom::XRDeviceId::WINDOWS_MIXED_REALITY_ID:
#endif
#if BUILDFLAG(ENABLE_OPENXR)
case device::mojom::XRDeviceId::OPENXR_DEVICE_ID:
#endif
return true;
}
NOTREACHED();
}
void BrowserXRRuntime::OnDisplayInfoChanged(
device::mojom::VRDisplayInfoPtr vr_device_info) {
bool had_display_info = !!display_info_;
display_info_ = ValidateVRDisplayInfo(vr_device_info.get(), id_);
if (had_display_info) {
for (VRServiceImpl* service : services_) {
service->OnDisplayInfoChanged();
}
}
// Notify observers of the new display info.
for (BrowserXRRuntimeObserver& observer : observers_) {
observer.SetVRDisplayInfo(display_info_.Clone());
}
}
void BrowserXRRuntime::StopImmersiveSession() {
if (immersive_session_controller_) {
immersive_session_controller_ = nullptr;
presenting_service_ = nullptr;
for (BrowserXRRuntimeObserver& observer : observers_) {
observer.SetWebXRWebContents(nullptr);
}
}
}
void BrowserXRRuntime::OnExitPresent() {
if (presenting_service_) {
presenting_service_->OnExitPresent();
presenting_service_ = nullptr;
}
}
void BrowserXRRuntime::OnDeviceActivated(
device::mojom::VRDisplayEventReason reason,
base::OnceCallback<void(bool)> on_handled) {
if (listening_for_activation_service_) {
listening_for_activation_service_->OnActivate(reason,
std::move(on_handled));
} else {
std::move(on_handled).Run(true /* will_not_present */);
}
}
void BrowserXRRuntime::OnDeviceIdle(
device::mojom::VRDisplayEventReason reason) {
for (VRServiceImpl* service : services_) {
service->OnDeactivate(reason);
}
}
void BrowserXRRuntime::OnInitialized() {
for (auto& callback : pending_initialization_callbacks_) {
std::move(callback).Run(display_info_.Clone());
}
pending_initialization_callbacks_.clear();
}
void BrowserXRRuntime::OnServiceAdded(VRServiceImpl* service) {
services_.insert(service);
}
void BrowserXRRuntime::OnServiceRemoved(VRServiceImpl* service) {
DCHECK(service);
services_.erase(service);
if (service == presenting_service_) {
ExitPresent(service);
DCHECK(presenting_service_ == nullptr);
}
if (service == listening_for_activation_service_) {
// Not listening for activation.
listening_for_activation_service_ = nullptr;
runtime_->SetListeningForActivate(false);
}
}
void BrowserXRRuntime::ExitPresent(VRServiceImpl* service) {
if (service == presenting_service_) {
StopImmersiveSession();
}
}
void BrowserXRRuntime::RequestSession(
VRServiceImpl* service,
const device::mojom::XRRuntimeSessionOptionsPtr& options,
RequestSessionCallback callback) {
// base::Unretained is safe because we won't be called back after runtime_ is
// destroyed.
runtime_->RequestSession(
options->Clone(),
base::BindOnce(&BrowserXRRuntime::OnRequestSessionResult,
base::Unretained(this), service->GetWeakPtr(),
options->Clone(), std::move(callback)));
}
void BrowserXRRuntime::OnRequestSessionResult(
base::WeakPtr<VRServiceImpl> service,
device::mojom::XRRuntimeSessionOptionsPtr options,
RequestSessionCallback callback,
device::mojom::XRSessionPtr session,
device::mojom::XRSessionControllerPtr immersive_session_controller) {
if (session && service) {
if (options->immersive) {
presenting_service_ = service.get();
immersive_session_controller_ = std::move(immersive_session_controller);
immersive_session_controller_.set_connection_error_handler(base::BindOnce(
&BrowserXRRuntime::OnImmersiveSessionError, base::Unretained(this)));
// Notify observers that we have started presentation.
content::WebContents* web_contents = service->GetWebContents();
for (BrowserXRRuntimeObserver& 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_ = std::move(immersive_session_controller);
StopImmersiveSession();
}
}
}
void BrowserXRRuntime::OnImmersiveSessionError() {
StopImmersiveSession();
}
void BrowserXRRuntime::UpdateListeningForActivate(VRServiceImpl* service) {
if (service->ListeningForActivate() && service->InFocusedFrame()) {
bool was_listening = !!listening_for_activation_service_;
listening_for_activation_service_ = service;
if (!was_listening)
OnListeningForActivate(true);
} else if (listening_for_activation_service_ == service) {
listening_for_activation_service_ = nullptr;
OnListeningForActivate(false);
}
}
void BrowserXRRuntime::InitializeAndGetDisplayInfo(
content::RenderFrameHost* render_frame_host,
device::mojom::VRService::GetImmersiveVRDisplayInfoCallback callback) {
device::mojom::VRDisplayInfoPtr device_info = GetVRDisplayInfo();
if (device_info) {
std::move(callback).Run(std::move(device_info));
return;
}
pending_initialization_callbacks_.push_back(std::move(callback));
runtime_->EnsureInitialized(
base::BindOnce(&BrowserXRRuntime::OnInitialized, base::Unretained(this)));
}
void BrowserXRRuntime::OnListeningForActivate(bool is_listening) {
runtime_->SetListeningForActivate(is_listening);
}
} // namespace vr