| // 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 "base/macros.h" |
| #include "base/memory/ptr_util.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 { |
| |
| namespace { |
| |
| constexpr float kDefaultIPD = 0.06f; // Default average IPD. |
| 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->upDegrees = gfx::RadToDeg(atanf(down_tan)); |
| out->downDegrees = -gfx::RadToDeg(atanf(up_tan)); |
| out->leftDegrees = -gfx::RadToDeg(atanf(left_tan)); |
| out->rightDegrees = gfx::RadToDeg(atanf(right_tan)); |
| return out; |
| } |
| |
| std::vector<float> HmdMatrix34ToWebVRTransformMatrix( |
| const vr::HmdMatrix34_t& mat) { |
| std::vector<float> transform; |
| transform.resize(16); |
| transform[0] = mat.m[0][0]; |
| transform[1] = mat.m[1][0]; |
| transform[2] = mat.m[2][0]; |
| transform[3] = 0.0f; |
| transform[4] = mat.m[0][1]; |
| transform[5] = mat.m[1][1]; |
| transform[6] = mat.m[2][1]; |
| transform[7] = 0.0f; |
| transform[8] = mat.m[0][2]; |
| transform[9] = mat.m[1][2]; |
| transform[10] = mat.m[2][2]; |
| transform[11] = 0.0f; |
| transform[12] = mat.m[0][3]; |
| transform[13] = mat.m[1][3]; |
| transform[14] = mat.m[2][3]; |
| transform[15] = 1.0f; |
| return transform; |
| } |
| |
| mojom::VRDisplayInfoPtr CreateVRDisplayInfo(vr::IVRSystem* vr_system, |
| device::mojom::XRDeviceId id) { |
| mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); |
| display_info->id = id; |
| display_info->displayName = |
| GetOpenVRString(vr_system, vr::Prop_ManufacturerName_String) + " " + |
| GetOpenVRString(vr_system, vr::Prop_ModelNumber_String); |
| display_info->capabilities = mojom::VRDisplayCapabilities::New(); |
| display_info->capabilities->hasPosition = true; |
| display_info->capabilities->hasExternalDisplay = true; |
| display_info->capabilities->canPresent = true; |
| display_info->webvr_default_framebuffer_scale = 1.0; |
| display_info->webxr_default_framebuffer_scale = 1.0; |
| |
| display_info->leftEye = mojom::VREyeParameters::New(); |
| display_info->rightEye = mojom::VREyeParameters::New(); |
| mojom::VREyeParametersPtr& left_eye = display_info->leftEye; |
| mojom::VREyeParametersPtr& right_eye = display_info->rightEye; |
| |
| left_eye->fieldOfView = OpenVRFovToWebVRFov(vr_system, vr::Eye_Left); |
| right_eye->fieldOfView = OpenVRFovToWebVRFov(vr_system, vr::Eye_Right); |
| |
| vr::TrackedPropertyError error = vr::TrackedProp_Success; |
| float ipd = vr_system->GetFloatTrackedDeviceProperty( |
| vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_UserIpdMeters_Float, &error); |
| |
| if (error != vr::TrackedProp_Success) |
| ipd = kDefaultIPD; |
| |
| left_eye->offset.resize(3); |
| left_eye->offset[0] = -ipd * 0.5; |
| left_eye->offset[1] = 0.0f; |
| left_eye->offset[2] = 0.0f; |
| right_eye->offset.resize(3); |
| right_eye->offset[0] = ipd * 0.5; |
| right_eye->offset[1] = 0.0; |
| right_eye->offset[2] = 0.0; |
| |
| uint32_t width, height; |
| vr_system->GetRecommendedRenderTargetSize(&width, &height); |
| left_eye->renderWidth = width; |
| left_eye->renderHeight = height; |
| right_eye->renderWidth = left_eye->renderWidth; |
| right_eye->renderHeight = left_eye->renderHeight; |
| |
| display_info->stageParameters = mojom::VRStageParameters::New(); |
| vr::HmdMatrix34_t mat = |
| vr_system->GetSeatedZeroPoseToStandingAbsoluteTrackingPose(); |
| display_info->stageParameters->standingTransform = |
| HmdMatrix34ToWebVRTransformMatrix(mat); |
| |
| vr::IVRChaperone* chaperone = vr::VRChaperone(); |
| if (chaperone) { |
| chaperone->GetPlayAreaSize(&display_info->stageParameters->sizeX, |
| &display_info->stageParameters->sizeZ); |
| } else { |
| display_info->stageParameters->sizeX = 0.0f; |
| display_info->stageParameters->sizeZ = 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), |
| weak_ptr_factory_(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; |
| } |
| |
| if (!options->immersive) { |
| ReturnNonImmersiveSession(std::move(callback)); |
| return; |
| } |
| |
| 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), |
| std::move(options), std::move(my_callback))); |
| } |
| |
| void OpenVRDevice::EnsureInitialized(int render_process_id, |
| int render_frame_id, |
| 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_) { |
| 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) { |
| 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(); |
| } |
| |
| void OpenVRDevice::OnGetInlineFrameData( |
| mojom::XRFrameDataProvider::GetFrameDataCallback callback) { |
| if (!openvr_) { |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| const float kPredictionTimeSeconds = 0.03f; |
| vr::TrackedDevicePose_t rendering_poses[vr::k_unMaxTrackedDeviceCount]; |
| openvr_->GetSystem()->GetDeviceToAbsoluteTrackingPose( |
| vr::TrackingUniverseSeated, kPredictionTimeSeconds, rendering_poses, |
| vr::k_unMaxTrackedDeviceCount); |
| mojom::XRFrameDataPtr data = mojom::XRFrameData::New(); |
| data->pose = mojo::ConvertTo<mojom::VRPosePtr>( |
| rendering_poses[vr::k_unTrackedDeviceIndex_Hmd]); |
| std::move(callback).Run(std::move(data)); |
| } |
| |
| // 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 |