blob: 3a2ced675e07a8d0f03e469e32e50168bf991764 [file] [log] [blame]
// Copyright (c) 2017 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 "device/vr/openvr/openvr_device.h"
#include <math.h>
#include <utility>
#include "base/bind.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/math_constants.h"
#include "build/build_config.h"
#include "device/vr/openvr/openvr_render_loop.h"
#include "device/vr/openvr/openvr_type_converters.h"
#include "third_party/openvr/src/headers/openvr.h"
#include "ui/gfx/geometry/angle_conversions.h"
namespace device {
void OpenVRDevice::RecordRuntimeAvailability() {
XrRuntimeAvailable runtime = XrRuntimeAvailable::NONE;
if (vr::VR_IsRuntimeInstalled())
runtime = XrRuntimeAvailable::OPENVR;
UMA_HISTOGRAM_ENUMERATION("XR.RuntimeAvailable", runtime);
}
namespace {
constexpr base::TimeDelta kPollingInterval =
base::TimeDelta::FromSecondsD(0.25);
mojom::VRFieldOfViewPtr OpenVRFovToWebVRFov(vr::IVRSystem* vr_system,
vr::Hmd_Eye eye) {
auto out = mojom::VRFieldOfView::New();
float up_tan, down_tan, left_tan, right_tan;
vr_system->GetProjectionRaw(eye, &left_tan, &right_tan, &up_tan, &down_tan);
// TODO(billorr): Plumb the expected projection matrix over mojo instead of
// using angles. Up and down are intentionally swapped to account for
// differences in expected projection matrix format for GVR and OpenVR.
out->up_degrees = gfx::RadToDeg(atanf(down_tan));
out->down_degrees = -gfx::RadToDeg(atanf(up_tan));
out->left_degrees = -gfx::RadToDeg(atanf(left_tan));
out->right_degrees = gfx::RadToDeg(atanf(right_tan));
return out;
}
gfx::Transform HmdMatrix34ToTransform(const vr::HmdMatrix34_t& mat) {
// Disable formatting so that the 4x4 matrix is more readable
// clang-format off
return gfx::Transform(
mat.m[0][0], mat.m[0][1], mat.m[0][2], mat.m[0][3],
mat.m[1][0], mat.m[1][1], mat.m[1][2], mat.m[1][3],
mat.m[2][0], mat.m[2][1], mat.m[2][2], mat.m[2][3],
0.0f, 0.0f, 0.0f, 1.0f);
// clang-format on
}
// OpenVR uses A_to_B convention for naming transformation matrices, but we pass
// matrices through mojo using the B_from_A naming convention since that what
// blink uses.
gfx::Transform HeadFromEyeTransform(vr::IVRSystem* vr_system, vr::Hmd_Eye eye) {
return HmdMatrix34ToTransform(vr_system->GetEyeToHeadTransform(eye));
}
mojom::VRDisplayInfoPtr CreateVRDisplayInfo(vr::IVRSystem* vr_system,
device::mojom::XRDeviceId id) {
mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New();
display_info->id = id;
display_info->display_name =
GetOpenVRString(vr_system, vr::Prop_ManufacturerName_String) + " " +
GetOpenVRString(vr_system, vr::Prop_ModelNumber_String);
display_info->capabilities = mojom::VRDisplayCapabilities::New();
display_info->capabilities->has_position = true;
display_info->capabilities->has_external_display = true;
display_info->capabilities->can_present = true;
display_info->webvr_default_framebuffer_scale = 1.0;
display_info->webxr_default_framebuffer_scale = 1.0;
display_info->left_eye = mojom::VREyeParameters::New();
display_info->right_eye = mojom::VREyeParameters::New();
mojom::VREyeParametersPtr& left_eye = display_info->left_eye;
mojom::VREyeParametersPtr& right_eye = display_info->right_eye;
left_eye->field_of_view = OpenVRFovToWebVRFov(vr_system, vr::Eye_Left);
right_eye->field_of_view = OpenVRFovToWebVRFov(vr_system, vr::Eye_Right);
left_eye->head_from_eye = HeadFromEyeTransform(vr_system, vr::Eye_Left);
right_eye->head_from_eye = HeadFromEyeTransform(vr_system, vr::Eye_Right);
uint32_t width, height;
vr_system->GetRecommendedRenderTargetSize(&width, &height);
left_eye->render_width = width;
left_eye->render_height = height;
right_eye->render_width = left_eye->render_width;
right_eye->render_height = left_eye->render_height;
display_info->stage_parameters = mojom::VRStageParameters::New();
vr::HmdMatrix34_t mat =
vr_system->GetSeatedZeroPoseToStandingAbsoluteTrackingPose();
display_info->stage_parameters->standing_transform =
HmdMatrix34ToTransform(mat);
vr::IVRChaperone* chaperone = vr::VRChaperone();
if (chaperone) {
chaperone->GetPlayAreaSize(&display_info->stage_parameters->size_x,
&display_info->stage_parameters->size_z);
} else {
display_info->stage_parameters->size_x = 0.0f;
display_info->stage_parameters->size_z = 0.0f;
}
return display_info;
}
} // namespace
OpenVRDevice::OpenVRDevice()
: VRDeviceBase(device::mojom::XRDeviceId::OPENVR_DEVICE_ID),
main_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()),
exclusive_controller_binding_(this),
gamepad_provider_factory_binding_(this),
compositor_host_binding_(this) {
render_loop_ = std::make_unique<OpenVRRenderLoop>();
OnPollingEvents();
}
bool OpenVRDevice::IsHwAvailable() {
return vr::VR_IsHmdPresent();
}
bool OpenVRDevice::IsApiAvailable() {
return vr::VR_IsRuntimeInstalled();
}
mojom::IsolatedXRGamepadProviderFactoryPtr OpenVRDevice::BindGamepadFactory() {
mojom::IsolatedXRGamepadProviderFactoryPtr ret;
gamepad_provider_factory_binding_.Bind(mojo::MakeRequest(&ret));
return ret;
}
mojom::XRCompositorHostPtr OpenVRDevice::BindCompositorHost() {
mojom::XRCompositorHostPtr ret;
compositor_host_binding_.Bind(mojo::MakeRequest(&ret));
return ret;
}
OpenVRDevice::~OpenVRDevice() {
Shutdown();
}
void OpenVRDevice::Shutdown() {
// Wait for the render loop to stop before completing destruction. This will
// ensure that the IVRSystem doesn't get shutdown until the render loop is no
// longer referencing it.
if (render_loop_ && render_loop_->IsRunning())
render_loop_->Stop();
}
void OpenVRDevice::RequestSession(
mojom::XRRuntimeSessionOptionsPtr options,
mojom::XRRuntime::RequestSessionCallback callback) {
if (!EnsureValidDisplayInfo()) {
std::move(callback).Run(nullptr, nullptr);
return;
}
DCHECK(options->immersive);
if (!render_loop_->IsRunning()) {
render_loop_->Start();
if (!render_loop_->IsRunning()) {
std::move(callback).Run(nullptr, nullptr);
return;
}
if (provider_request_) {
render_loop_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestGamepadProvider,
base::Unretained(render_loop_.get()),
std::move(provider_request_)));
}
if (overlay_request_) {
render_loop_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestOverlay,
base::Unretained(render_loop_.get()),
std::move(overlay_request_)));
}
}
// We are done using OpenVR until the presentation session ends.
openvr_ = nullptr;
auto my_callback =
base::BindOnce(&OpenVRDevice::OnRequestSessionResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback));
auto on_presentation_ended = base::BindOnce(
&OpenVRDevice::OnPresentationEnded, weak_ptr_factory_.GetWeakPtr());
render_loop_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&XRCompositorCommon::RequestSession,
base::Unretained(render_loop_.get()),
std::move(on_presentation_ended),
base::DoNothing::Repeatedly<mojom::XRVisibilityState>(),
std::move(options), std::move(my_callback)));
outstanding_session_requests_count_++;
}
void OpenVRDevice::EnsureInitialized(EnsureInitializedCallback callback) {
EnsureValidDisplayInfo();
std::move(callback).Run();
}
bool OpenVRDevice::EnsureValidDisplayInfo() {
// Ensure we have had a valid display_info set at least once.
if (!have_real_display_info_) {
DCHECK(!openvr_);
// Initialize OpenVR.
openvr_ = std::make_unique<OpenVRWrapper>(false /* presenting */);
if (!openvr_->IsInitialized()) {
openvr_ = nullptr;
return false;
}
SetVRDisplayInfo(CreateVRDisplayInfo(openvr_->GetSystem(), GetId()));
have_real_display_info_ = true;
}
return have_real_display_info_;
}
void OpenVRDevice::OnPresentationEnded() {
if (!openvr_ && outstanding_session_requests_count_ == 0) {
openvr_ = std::make_unique<OpenVRWrapper>(false /* presenting */);
if (!openvr_->IsInitialized()) {
openvr_ = nullptr;
return;
}
}
}
void OpenVRDevice::OnRequestSessionResult(
mojom::XRRuntime::RequestSessionCallback callback,
bool result,
mojom::XRSessionPtr session) {
outstanding_session_requests_count_--;
if (!result) {
OnPresentationEnded();
std::move(callback).Run(nullptr, nullptr);
return;
}
OnStartPresenting();
mojom::XRSessionControllerPtr session_controller;
exclusive_controller_binding_.Bind(mojo::MakeRequest(&session_controller));
// Use of Unretained is safe because the callback will only occur if the
// binding is not destroyed.
exclusive_controller_binding_.set_connection_error_handler(
base::BindOnce(&OpenVRDevice::OnPresentingControllerMojoConnectionError,
base::Unretained(this)));
session->display_info = display_info_.Clone();
std::move(callback).Run(std::move(session), std::move(session_controller));
}
bool OpenVRDevice::IsAvailable() {
return vr::VR_IsRuntimeInstalled() && vr::VR_IsHmdPresent();
}
void OpenVRDevice::GetIsolatedXRGamepadProvider(
mojom::IsolatedXRGamepadProviderRequest provider_request) {
if (render_loop_->IsRunning()) {
render_loop_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestGamepadProvider,
base::Unretained(render_loop_.get()),
std::move(provider_request)));
} else {
provider_request_ = std::move(provider_request);
}
}
void OpenVRDevice::CreateImmersiveOverlay(
mojom::ImmersiveOverlayRequest overlay_request) {
if (render_loop_->IsRunning()) {
render_loop_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestOverlay,
base::Unretained(render_loop_.get()),
std::move(overlay_request)));
} else {
overlay_request_ = std::move(overlay_request);
}
}
// XRSessionController
void OpenVRDevice::SetFrameDataRestricted(bool restricted) {
// Presentation sessions can not currently be restricted.
DCHECK(false);
}
void OpenVRDevice::OnPresentingControllerMojoConnectionError() {
render_loop_->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&XRCompositorCommon::ExitPresent,
base::Unretained(render_loop_.get())));
// Don't stop the render loop here. We need to keep the gamepad provider alive
// so that we don't lose a pending mojo gamepad_callback_.
// TODO(https://crbug.com/875187): Alternatively, we could recreate the
// provider on the next session, or look into why the callback gets lost.
OnExitPresent();
exclusive_controller_binding_.Close();
}
// Only deal with events that will cause displayInfo changes for now.
void OpenVRDevice::OnPollingEvents() {
main_thread_task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&OpenVRDevice::OnPollingEvents,
weak_ptr_factory_.GetWeakPtr()),
kPollingInterval);
if (!openvr_)
return;
vr::VREvent_t event;
bool is_changed = false;
while (openvr_->GetSystem()->PollNextEvent(&event, sizeof(event))) {
if (event.trackedDeviceIndex != vr::k_unTrackedDeviceIndex_Hmd &&
event.trackedDeviceIndex != vr::k_unTrackedDeviceIndexInvalid) {
continue;
}
switch (event.eventType) {
case vr::VREvent_TrackedDeviceUpdated:
case vr::VREvent_IpdChanged:
case vr::VREvent_ChaperoneDataHasChanged:
case vr::VREvent_ChaperoneSettingsHaveChanged:
case vr::VREvent_ChaperoneUniverseHasChanged:
is_changed = true;
break;
default:
break;
}
}
if (is_changed)
SetVRDisplayInfo(CreateVRDisplayInfo(openvr_->GetSystem(), GetId()));
}
} // namespace device