| // 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 "chrome/browser/vr/vr_browser_renderer_thread.h" |
| |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/vr/browser_renderer.h" |
| #include "chrome/browser/vr/ui.h" |
| #include "ui/gfx/geometry/quaternion.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "chrome/browser/vr/graphics_delegate_win.h" |
| #endif |
| |
| // To avoid conflicts with the macro from the Windows SDK... |
| #undef DrawState |
| |
| namespace { |
| constexpr base::TimeDelta kWebVrInitialFrameTimeout = base::Seconds(5); |
| constexpr base::TimeDelta kWebVrSpinnerTimeout = base::Seconds(2); |
| |
| constexpr float kEpsilon = 0.1f; |
| constexpr float kMaxPosition = 1000000; |
| constexpr float kMinPosition = -kMaxPosition; |
| bool g_overlay_ui_disabled_for_testing_ = false; |
| |
| bool InRange(float val, float min = kMinPosition, float max = kMaxPosition) { |
| return val > min && val < max; |
| } |
| |
| } // namespace |
| |
| namespace vr { |
| |
| VRBrowserRendererThread* VRBrowserRendererThread::instance_for_testing_ = |
| nullptr; |
| |
| VRBrowserRendererThread::VRBrowserRendererThread( |
| mojo::PendingRemote<device::mojom::ImmersiveOverlay> overlay, |
| const std::vector<device::mojom::XRViewPtr>& views) |
| : overlay_(std::move(overlay)), |
| task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) { |
| DCHECK(instance_for_testing_ == nullptr); |
| instance_for_testing_ = this; |
| |
| for (auto& view : views) { |
| if (view->eye == device::mojom::XREye::kLeft || |
| view->eye == device::mojom::XREye::kRight) { |
| default_views_.push_back(view.Clone()); |
| } |
| } |
| |
| StartWebXrTimeout(); |
| } |
| |
| VRBrowserRendererThread::~VRBrowserRendererThread() { |
| StopWebXrTimeout(); |
| |
| // Call Cleanup to ensure correct destruction order of VR-UI classes. |
| StopOverlay(); |
| instance_for_testing_ = nullptr; |
| } |
| |
| void VRBrowserRendererThread::StopOverlay() { |
| browser_renderer_ = nullptr; |
| started_ = false; |
| graphics_ = nullptr; |
| ui_ = nullptr; |
| scheduler_ui_ = nullptr; |
| } |
| |
| void VRBrowserRendererThread::StartWebXrTimeout() { |
| if (g_overlay_ui_disabled_for_testing_) { |
| return; |
| } |
| |
| frame_timeout_running_ = true; |
| overlay_->SetOverlayAndWebXRVisibility(draw_state_.ShouldDrawUI(), |
| draw_state_.ShouldDrawWebXR()); |
| |
| if (!waiting_for_webxr_frame_) { |
| waiting_for_webxr_frame_ = true; |
| overlay_->RequestNotificationOnWebXrSubmitted(base::BindOnce( |
| &VRBrowserRendererThread::OnWebXRSubmitted, base::Unretained(this))); |
| } |
| |
| webxr_spinner_timeout_closure_.Reset(base::BindOnce( |
| &VRBrowserRendererThread::OnWebXrTimeoutImminent, |
| base::Unretained( |
| this))); // Unretained safe because we explicitly cancel. |
| task_runner_->PostDelayedTask(FROM_HERE, |
| webxr_spinner_timeout_closure_.callback(), |
| kWebVrSpinnerTimeout); |
| webxr_frame_timeout_closure_.Reset(base::BindOnce( |
| &VRBrowserRendererThread::OnWebXrTimedOut, |
| base::Unretained( |
| this))); // Unretained safe because we explicitly cancel. |
| task_runner_->PostDelayedTask(FROM_HERE, |
| webxr_frame_timeout_closure_.callback(), |
| kWebVrInitialFrameTimeout); |
| } |
| |
| void VRBrowserRendererThread::StopWebXrTimeout() { |
| if (g_overlay_ui_disabled_for_testing_) { |
| return; |
| } |
| |
| if (!webxr_spinner_timeout_closure_.IsCancelled()) |
| webxr_spinner_timeout_closure_.Cancel(); |
| if (!webxr_frame_timeout_closure_.IsCancelled()) |
| webxr_frame_timeout_closure_.Cancel(); |
| OnSpinnerVisibilityChanged(false); |
| frame_timeout_running_ = false; |
| } |
| |
| int VRBrowserRendererThread::GetNextRequestId() { |
| current_request_id_++; |
| if (current_request_id_ >= 0x10000) |
| current_request_id_ = 0; |
| return current_request_id_; |
| } |
| |
| void VRBrowserRendererThread::OnWebXrTimeoutImminent() { |
| OnSpinnerVisibilityChanged(true); |
| if (scheduler_ui_) { |
| scheduler_ui_->OnWebXrTimeoutImminent(); |
| } |
| } |
| |
| void VRBrowserRendererThread::OnWebXrTimedOut() { |
| OnSpinnerVisibilityChanged(true); |
| if (scheduler_ui_) { |
| scheduler_ui_->OnWebXrTimedOut(); |
| } |
| } |
| |
| void VRBrowserRendererThread::UpdateOverlayState() { |
| if (draw_state_.ShouldDrawUI()) { |
| StartOverlay(); |
| } |
| |
| if (!g_overlay_ui_disabled_for_testing_) { |
| overlay_->SetOverlayAndWebXRVisibility(draw_state_.ShouldDrawUI(), |
| draw_state_.ShouldDrawWebXR()); |
| } |
| |
| if (draw_state_.ShouldDrawUI()) { |
| // Note that this is intentionally checked separately from if we should draw |
| // the UI to prevent just auto-stopping the Overlay for tests, so that the |
| // other logic can potentially run. |
| if (!g_overlay_ui_disabled_for_testing_) { |
| // If we don't have a graphics yet (because StartOverlay hasn't finished), |
| // then postpone running the overlay update until it is. |
| if (!graphics_) { |
| // Unretained is safe since we maintain ownership of this callback. |
| pending_overlay_update_ = |
| base::BindOnce(&VRBrowserRendererThread::UpdateOverlayState, |
| base::Unretained(this)); |
| return; |
| } |
| |
| overlay_->RequestNextOverlayPose( |
| base::BindOnce(&VRBrowserRendererThread::OnPose, |
| base::Unretained(this), GetNextRequestId())); |
| } |
| } else { |
| StopOverlay(); |
| } |
| } |
| |
| void VRBrowserRendererThread::SetFramesThrottled(bool throttled) { |
| if (frames_throttled_ == throttled) |
| return; |
| |
| frames_throttled_ = throttled; |
| |
| if (g_overlay_ui_disabled_for_testing_) { |
| return; |
| } |
| |
| // TODO(crbug.com/40653353): If we try to re-start the timeouts after UI has |
| // already been shown (e.g. a user takes their headset off for a permissions |
| // prompt). Then the prompt UI doesn't seem to be dismissed immediately. |
| if (!waiting_for_webxr_frame_) |
| return; |
| |
| if (frames_throttled_) { |
| StopWebXrTimeout(); |
| |
| // TODO(alcooper): This is not necessarily the best thing to show, but it's |
| // the best that we have right now. It ensures that we submit *something* |
| // rather than letting the default system "Stalled" UI take over, without |
| // showing a message that the page is behaving badly. |
| OnWebXrTimeoutImminent(); |
| } else { |
| StartWebXrTimeout(); |
| } |
| } |
| |
| void VRBrowserRendererThread::SetVisibleExternalPromptNotification( |
| ExternalPromptNotificationType prompt) { |
| if (!draw_state_.SetPrompt(prompt)) |
| return; |
| |
| UpdateOverlayState(); |
| |
| if (!ui_) { |
| // If the ui is dismissed, make sure that we don't *actually* have a prompt |
| // state that we needed to set. Note that spinning the ui back up is async; |
| // so if we don't have a ui_ object and we need one, ensure that it's being |
| // spun back up. |
| if (prompt != ExternalPromptNotificationType::kPromptNone) { |
| DCHECK(started_); |
| } |
| return; |
| } |
| |
| ui_->SetVisibleExternalPromptNotification(prompt); |
| } |
| |
| void VRBrowserRendererThread::SetIndicatorsVisible(bool visible) { |
| if (draw_state_.SetIndicatorsVisible(visible)) |
| UpdateOverlayState(); |
| } |
| |
| void VRBrowserRendererThread::OnSpinnerVisibilityChanged(bool visible) { |
| if (draw_state_.SetSpinnerVisible(visible)) |
| UpdateOverlayState(); |
| } |
| |
| void VRBrowserRendererThread::SetCapturingState( |
| const CapturingStateModel& active_capturing, |
| const CapturingStateModel& background_capturing, |
| const CapturingStateModel& potential_capturing) { |
| if (ui_) |
| ui_->SetCapturingState(active_capturing, background_capturing, |
| potential_capturing); |
| } |
| |
| VRBrowserRendererThread* |
| VRBrowserRendererThread::GetInstanceForTesting() { |
| return instance_for_testing_; |
| } |
| |
| BrowserRenderer* VRBrowserRendererThread::GetBrowserRendererForTesting() { |
| return browser_renderer_.get(); |
| } |
| |
| namespace { |
| // Number of frames to use for sliding averages for pose timings, |
| // as used for estimating prediction times. |
| constexpr unsigned kSlidingAverageSize = 5; |
| } // namespace |
| |
| void VRBrowserRendererThread::DisableOverlayForTesting() { |
| g_overlay_ui_disabled_for_testing_ = true; |
| } |
| |
| void VRBrowserRendererThread::StartOverlay() { |
| if (started_) |
| return; |
| |
| started_ = true; |
| std::unique_ptr<GraphicsDelegate> graphics = GraphicsDelegate::Create(); |
| |
| // We're going to pass the unique_ptr into the callback so grab a temporary |
| // reference to it here to prevent a use-after-move. This keeps the member |
| // null until we've been fully initialized. |
| auto* initializing_graphics = graphics.get(); |
| initializing_graphics->Initialize( |
| base::BindOnce(&VRBrowserRendererThread::OnGraphicsReady, |
| weak_ptr_factory_.GetWeakPtr(), std::move(graphics))); |
| } |
| |
| void VRBrowserRendererThread::OnGraphicsReady( |
| std::unique_ptr<GraphicsDelegate> initializing_graphics) { |
| DVLOG(2) << __func__; |
| // The graphics delegate will eventually be owned by the browser_renderer_, |
| // but we need to keep a raw pointer to it. |
| graphics_ = initializing_graphics.get(); |
| |
| // We should have received valid views from the ui host before rendering. |
| DCHECK(!default_views_.empty()); |
| graphics_->SetXrViews(default_views_); |
| |
| graphics_->BindContext(); |
| |
| // Create a vr::Ui |
| std::unique_ptr<Ui> ui = std::make_unique<Ui>(); |
| static_cast<UiInterface*>(ui.get())->OnGlInitialized(); |
| ui_ = static_cast<BrowserUiInterface*>(ui.get()); |
| scheduler_ui_ = static_cast<UiInterface*>(ui.get())->GetSchedulerUiPtr(); |
| |
| if (draw_state_.GetPrompt() != ExternalPromptNotificationType::kPromptNone) { |
| ui_->SetVisibleExternalPromptNotification(draw_state_.GetPrompt()); |
| } |
| |
| // Create the BrowserRenderer to drive UI rendering based on the delegates. |
| browser_renderer_ = std::make_unique<BrowserRenderer>( |
| std::move(ui), std::move(initializing_graphics), kSlidingAverageSize); |
| |
| graphics_->ClearContext(); |
| |
| if (pending_overlay_update_) { |
| std::move(pending_overlay_update_).Run(); |
| } |
| } |
| |
| void VRBrowserRendererThread::OnWebXRSubmitted() { |
| waiting_for_webxr_frame_ = false; |
| if (scheduler_ui_) |
| scheduler_ui_->OnWebXrFrameAvailable(); |
| |
| StopWebXrTimeout(); |
| } |
| |
| // Ensures that relevant XRRendererInfo entries are valid and returns patched up |
| // XRRendererInfo to ensure that we always use normalized orientation |
| // quaternion, and that we do not use position with out-of-range values. |
| // In case the received data does not contain position and/or orientation, they |
| // will be set to default values. |
| device::mojom::XRRenderInfoPtr ValidateFrameData( |
| device::mojom::XRRenderInfoPtr data) { |
| device::mojom::XRRenderInfoPtr ret = device::mojom::XRRenderInfo::New(); |
| ret->mojo_from_viewer = device::mojom::VRPose::New(); |
| |
| if (data->mojo_from_viewer) { |
| if (data->mojo_from_viewer->orientation) { |
| if (abs(data->mojo_from_viewer->orientation->Length() - 1) < kEpsilon) { |
| ret->mojo_from_viewer->orientation = |
| data->mojo_from_viewer->orientation->Normalized(); |
| } |
| } |
| |
| if (data->mojo_from_viewer->position) { |
| ret->mojo_from_viewer->position = data->mojo_from_viewer->position; |
| |
| bool any_out_of_range = !(InRange(ret->mojo_from_viewer->position->x()) && |
| InRange(ret->mojo_from_viewer->position->y()) && |
| InRange(ret->mojo_from_viewer->position->z())); |
| if (any_out_of_range) { |
| ret->mojo_from_viewer->position = std::nullopt; |
| // 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); |
| } |
| } |
| } // if (data->mojo_from_viewer) |
| |
| if (!ret->mojo_from_viewer->orientation) { |
| ret->mojo_from_viewer->orientation = gfx::Quaternion(); |
| } |
| |
| if (!ret->mojo_from_viewer->position) { |
| ret->mojo_from_viewer->position = gfx::Point3F(); |
| } |
| |
| ret->views.resize(data->views.size()); |
| for (size_t i = 0; i < data->views.size(); i++) { |
| ret->views[i] = std::move(data->views[i]); |
| } |
| |
| ret->frame_id = data->frame_id; |
| |
| return ret; |
| } |
| |
| void VRBrowserRendererThread::OnPose(int request_id, |
| device::mojom::XRRenderInfoPtr data) { |
| if (request_id != current_request_id_) { |
| // Old request. Do nothing. |
| return; |
| } |
| |
| if (!draw_state_.ShouldDrawUI()) { |
| // We shouldn't be showing UI. |
| overlay_->SetOverlayAndWebXRVisibility(draw_state_.ShouldDrawUI(), |
| draw_state_.ShouldDrawWebXR()); |
| if (graphics_) |
| graphics_->ResetMemoryBuffer(); |
| return; |
| } |
| |
| data = ValidateFrameData(std::move(data)); |
| |
| // If we're getting poses and should be drawing, StartOverlay() should have |
| // initialized graphics_. |
| DCHECK(graphics_); |
| graphics_->SetXrViews(std::move(data->views)); |
| |
| if (!PreRender()) |
| return; |
| |
| // Deliver pose to input and scheduler. |
| DCHECK(data); |
| DCHECK(data->mojo_from_viewer); |
| DCHECK(data->mojo_from_viewer->orientation); |
| DCHECK(data->mojo_from_viewer->position); |
| const gfx::Point3F& pos = *data->mojo_from_viewer->position; |
| |
| // The incoming pose represents where the headset is in "world space". So |
| // we'll need to invert to get the view transform. |
| gfx::Transform head_from_unoriented_head( |
| data->mojo_from_viewer->orientation->inverse()); |
| |
| // Negating all components will invert the translation. |
| gfx::Transform unoriented_head_from_world; |
| unoriented_head_from_world.Translate3d(-pos.x(), -pos.y(), -pos.z()); |
| |
| // Compose these to get the base "view" matrix (before accounting for per-eye |
| // transforms). |
| gfx::Transform head_from_world = |
| head_from_unoriented_head * unoriented_head_from_world; |
| |
| base::TimeTicks now = base::TimeTicks::Now(); |
| bool need_submit = false; |
| if (draw_state_.ShouldDrawWebXR()) { |
| browser_renderer_->DrawWebXrFrame(now, head_from_world); |
| need_submit = true; |
| } else if (draw_state_.ShouldDrawUI()) { |
| browser_renderer_->DrawBrowserFrame(now, head_from_world); |
| need_submit = true; |
| } |
| |
| if (need_submit) { |
| SubmitFrame(data->frame_id); |
| } |
| } |
| |
| bool VRBrowserRendererThread::PreRender() { |
| // GraphicsDelegate::PreRender can fail if the context has become lost |
| // due to hybrid adapter switching. Giving up on life means no overlays are |
| // submitted to the XR process, causing it hang, waiting forever. Instead, |
| // we shutdown and restart the overlay system, re-establishing the GPU process |
| // connection and all of the graphics related state in vr::Ui. |
| if (!graphics_->PreRender()) { |
| StopOverlay(); |
| StartOverlay(); |
| // StartOverlay is asynchronous, so we may not have a graphics_ again |
| // immediately. We'll essentially bail on this pose and ask for a new one |
| // once the connection has been re-established. |
| if (!graphics_) { |
| // Unretained is safe since we maintain ownership of this callback. |
| pending_overlay_update_ = |
| base::BindOnce(&VRBrowserRendererThread::UpdateOverlayState, |
| base::Unretained(this)); |
| return false; |
| } |
| return graphics_->PreRender(); |
| } |
| return true; |
| } |
| |
| void VRBrowserRendererThread::SubmitFrame(int16_t frame_id) { |
| DVLOG(3) << __func__ << " frame_id=" << frame_id; |
| graphics_->PostRender(); |
| |
| overlay_->SubmitOverlayTexture( |
| frame_id, graphics_->GetTexture(), graphics_->GetSyncToken(), |
| graphics_->GetLeft(), graphics_->GetRight(), |
| base::BindOnce(&VRBrowserRendererThread::SubmitResult, |
| base::Unretained(this))); |
| } |
| |
| void VRBrowserRendererThread::SubmitResult(bool success) { |
| DVLOG(3) << __func__ << " success=" << success; |
| if (!success && graphics_) { |
| graphics_->ResetMemoryBuffer(); |
| } |
| |
| // Make sure that we only notify that a WebXr frame is now |
| if (scheduler_ui_ && success && !frame_timeout_running_) { |
| scheduler_ui_->OnWebXrFrameAvailable(); |
| } |
| |
| if (draw_state_.ShouldDrawUI() && started_) { |
| DVLOG(3) << __func__ << " Requesting Overlay Pose"; |
| overlay_->RequestNextOverlayPose( |
| base::BindOnce(&VRBrowserRendererThread::OnPose, |
| base::Unretained(this), GetNextRequestId())); |
| } |
| } |
| |
| // VRBrowserRendererThread::DrawContentType functions. |
| bool VRBrowserRendererThread::DrawState::ShouldDrawUI() { |
| return prompt_ != ExternalPromptNotificationType::kPromptNone || |
| spinner_visible_ || indicators_visible_; |
| } |
| |
| bool VRBrowserRendererThread::DrawState::ShouldDrawWebXR() { |
| return ((prompt_ == ExternalPromptNotificationType::kPromptNone || |
| indicators_visible_) && |
| !spinner_visible_); |
| } |
| |
| bool VRBrowserRendererThread::DrawState::SetPrompt( |
| ExternalPromptNotificationType prompt) { |
| bool old_ui = ShouldDrawUI(); |
| bool old_webxr = ShouldDrawWebXR(); |
| prompt_ = prompt; |
| return old_ui != ShouldDrawUI() || old_webxr != ShouldDrawWebXR(); |
| } |
| |
| bool VRBrowserRendererThread::DrawState::SetSpinnerVisible(bool visible) { |
| bool old_ui = ShouldDrawUI(); |
| bool old_webxr = ShouldDrawWebXR(); |
| spinner_visible_ = visible; |
| return old_ui != ShouldDrawUI() || old_webxr != ShouldDrawWebXR(); |
| } |
| |
| bool VRBrowserRendererThread::DrawState::SetIndicatorsVisible(bool visible) { |
| bool old_ui = ShouldDrawUI(); |
| bool old_webxr = ShouldDrawWebXR(); |
| indicators_visible_ = visible; |
| return old_ui != ShouldDrawUI() || old_webxr != ShouldDrawWebXR(); |
| } |
| |
| } // namespace vr |