| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/xr/xr_frame_provider.h" |
| |
| #include <utility> |
| |
| #include "build/build_config.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/dom/frame_request_callback_collection.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/navigator.h" |
| #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu_device.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu_texture.h" |
| #include "third_party/blink/renderer/modules/xr/xr_gpu_binding.h" |
| #include "third_party/blink/renderer/modules/xr/xr_gpu_drawing_context.h" |
| #include "third_party/blink/renderer/modules/xr/xr_gpu_swap_chain.h" |
| #include "third_party/blink/renderer/modules/xr/xr_graphics_binding.h" |
| #include "third_party/blink/renderer/modules/xr/xr_layer_client.h" |
| #include "third_party/blink/renderer/modules/xr/xr_projection_layer.h" |
| #include "third_party/blink/renderer/modules/xr/xr_session.h" |
| #include "third_party/blink/renderer/modules/xr/xr_system.h" |
| #include "third_party/blink/renderer/modules/xr/xr_viewport.h" |
| #include "third_party/blink/renderer/modules/xr/xr_webgl_drawing_context.h" |
| #include "third_party/blink/renderer/modules/xr/xr_webgl_layer.h" |
| #include "third_party/blink/renderer/platform/graphics/gpu/xr_frame_transport.h" |
| #include "third_party/blink/renderer/platform/graphics/gpu/xr_frame_transport_delegate.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "ui/display/display.h" |
| #include "ui/gfx/geometry/transform.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| class XRFrameProviderRequestCallback : public FrameCallback { |
| public: |
| explicit XRFrameProviderRequestCallback(XRFrameProvider* frame_provider) |
| : frame_provider_(frame_provider) {} |
| ~XRFrameProviderRequestCallback() override = default; |
| void Invoke(double high_res_time_ms) override { |
| frame_provider_->OnNonImmersiveVSync(high_res_time_ms); |
| } |
| |
| void Trace(Visitor* visitor) const override { |
| visitor->Trace(frame_provider_); |
| FrameCallback::Trace(visitor); |
| } |
| |
| Member<XRFrameProvider> frame_provider_; |
| }; |
| |
| gfx::RectF NormalizeViewport(gfx::Rect viewport, |
| uint32_t width, |
| uint32_t height) { |
| return gfx::RectF( |
| static_cast<float>(viewport.x()) / width, |
| static_cast<float>(height - (viewport.y() + viewport.height())) / height, |
| static_cast<float>(viewport.width()) / width, |
| static_cast<float>(viewport.height()) / height); |
| } |
| |
| } // namespace |
| |
| XRFrameProvider::XRFrameProvider(XRSystem* xr) |
| : xr_(xr), |
| frame_transport_(MakeGarbageCollected<XRFrameTransport>( |
| xr->GetExecutionContext(), |
| xr->GetExecutionContext()->GetTaskRunner( |
| TaskType::kMiscPlatformAPI))), |
| immersive_data_provider_(xr->GetExecutionContext()), |
| immersive_presentation_provider_(xr->GetExecutionContext()), |
| layer_manager_(xr->GetExecutionContext()), |
| last_has_focus_(xr->IsFrameFocused()) {} |
| |
| void XRFrameProvider::AddImmersiveSessionObserver( |
| ImmersiveSessionObserver* observer) { |
| immersive_observers_.insert(observer); |
| } |
| |
| void XRFrameProvider::OnSessionStarted( |
| XRSession* session, |
| device::mojom::blink::XRSessionPtr session_ptr) { |
| DCHECK(session); |
| |
| if (session->immersive()) { |
| // Ensure we can only have one immersive session at a time. |
| DCHECK(!immersive_session_); |
| DCHECK(session_ptr->data_provider); |
| DCHECK(session_ptr->submit_frame_sink); |
| immersive_session_ = session; |
| |
| for (auto& observer : immersive_observers_) { |
| observer->OnImmersiveSessionStart(); |
| } |
| |
| immersive_data_provider_.Bind( |
| std::move(session_ptr->data_provider), |
| xr_->GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI)); |
| immersive_data_provider_.set_disconnect_handler( |
| BindOnce(&XRFrameProvider::OnProviderConnectionError, |
| WrapWeakPersistent(this), WrapWeakPersistent(session))); |
| |
| immersive_presentation_provider_.Bind( |
| std::move(session_ptr->submit_frame_sink->provider), |
| xr_->GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI)); |
| immersive_presentation_provider_.set_disconnect_handler( |
| BindOnce(&XRFrameProvider::OnProviderConnectionError, |
| WrapWeakPersistent(this), WrapWeakPersistent(session))); |
| |
| frame_transport_->RegisterFrameRenderedCallback(BindRepeating( |
| &XRFrameProvider::OnRenderComplete, WrapWeakPersistent(this))); |
| |
| if (session_ptr->layer_manager) { |
| layer_manager_.Bind(std::move(session_ptr->layer_manager), |
| xr_->GetExecutionContext()->GetTaskRunner( |
| TaskType::kMiscPlatformAPI)); |
| layer_manager_.set_disconnect_handler( |
| BindOnce(&XRFrameProvider::OnProviderConnectionError, |
| WrapWeakPersistent(this), WrapWeakPersistent(session))); |
| } |
| |
| frame_transport_->BindSubmitFrameClient( |
| std::move(session_ptr->submit_frame_sink->client_receiver)); |
| frame_transport_->SetTransportOptions( |
| std::move(session_ptr->submit_frame_sink->transport_options)); |
| frame_transport_->PresentChange(); |
| |
| last_frame_statistics_sent_time_ = base::TimeTicks::Now(); |
| |
| repeating_timer_.Start(FROM_HERE, base::Seconds(1), |
| BindRepeating(&XRFrameProvider::SendFrameData, |
| WrapWeakPersistent(this))); |
| } else { |
| // If a non-immersive session doesn't have a data provider, we don't |
| // need to store a reference to it. |
| if (!session_ptr->data_provider) { |
| return; |
| } |
| |
| HeapMojoRemote<device::mojom::blink::XRFrameDataProvider> data_provider( |
| xr_->GetExecutionContext()); |
| data_provider.Bind( |
| std::move(session_ptr->data_provider), |
| xr_->GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI)); |
| data_provider.set_disconnect_handler( |
| BindOnce(&XRFrameProvider::OnProviderConnectionError, |
| WrapWeakPersistent(this), WrapWeakPersistent(session))); |
| |
| non_immersive_data_providers_.insert( |
| session, WrapDisallowNew(std::move(data_provider))); |
| } |
| } |
| |
| void XRFrameProvider::OnFocusChanged() { |
| bool focus = xr_->IsFrameFocused(); |
| |
| // If we are gaining focus, schedule a frame for magic window. This accounts |
| // for skipping RAFs in ProcessScheduledFrame. Only do this when there are |
| // magic window sessions but no immersive session. Note that immersive |
| // sessions don't stop scheduling RAFs when focus is lost, so there is no need |
| // to schedule immersive frames when focus is acquired. |
| if (focus && !last_has_focus_ && requesting_sessions_.size() > 0 && |
| !immersive_session_) { |
| ScheduleNonImmersiveFrame(nullptr); |
| } |
| last_has_focus_ = focus; |
| } |
| |
| // Ends the immersive session when the presentation or immersive data provider |
| // got disconnected. |
| void XRFrameProvider::OnProviderConnectionError(XRSession* session) { |
| DVLOG(2) << __func__; |
| // This will call into |OnSessionEnded|, unless it has already ended. |
| session->ForceEnd(XRSession::ShutdownPolicy::kImmediate); |
| } |
| |
| void XRFrameProvider::OnSessionEnded(XRSession* session) { |
| DVLOG(2) << __func__; |
| if (session->immersive()) { |
| DCHECK(session == immersive_session_); |
| |
| immersive_session_ = nullptr; |
| pending_immersive_vsync_ = false; |
| frame_id_ = -1; |
| immersive_presentation_provider_.reset(); |
| immersive_data_provider_.reset(); |
| layer_manager_.reset(); |
| |
| first_immersive_frame_time_ = std::nullopt; |
| first_immersive_frame_time_delta_ = std::nullopt; |
| |
| frame_transport_ = MakeGarbageCollected<XRFrameTransport>( |
| session->GetExecutionContext(), |
| session->GetExecutionContext()->GetTaskRunner( |
| TaskType::kMiscPlatformAPI)); |
| |
| repeating_timer_.Stop(); |
| for (auto& observer : immersive_observers_) { |
| observer->OnImmersiveSessionEnd(); |
| } |
| } else { |
| non_immersive_data_providers_.erase(session); |
| requesting_sessions_.erase(session); |
| } |
| } |
| |
| void XRFrameProvider::RestartNonImmersiveFrameLoop() { |
| // When we no longer have an active immersive session schedule all the |
| // outstanding frames that were requested while the immersive session was |
| // active. |
| if (immersive_session_ || requesting_sessions_.size() == 0) |
| return; |
| |
| for (auto& session : requesting_sessions_) { |
| RequestNonImmersiveFrameData(session.key.Get()); |
| } |
| |
| ScheduleNonImmersiveFrame(nullptr); |
| } |
| |
| // Schedule a session to be notified when the next XR frame is available. |
| void XRFrameProvider::RequestFrame(XRSession* session) { |
| DVLOG(3) << __func__; |
| TRACE_EVENT0("gpu", "RequestFrame"); |
| DCHECK(session); |
| |
| auto options = device::mojom::blink::XRFrameDataRequestOptions::New(); |
| options->include_lighting_estimation_data = session->LightEstimationEnabled(); |
| options->stage_parameters_id = session->StageParametersId(); |
| options->depth_active = session->IsDepthActive(); |
| |
| // Immersive frame logic. |
| if (session->immersive()) { |
| ScheduleImmersiveFrame(std::move(options)); |
| return; |
| } |
| |
| // Non-immersive frame logic. |
| |
| // Duplicate frame requests are treated as a no-op. |
| if (requesting_sessions_.Contains(session)) { |
| DVLOG(2) << __func__ << ": session requested duplicate frame"; |
| return; |
| } |
| |
| requesting_sessions_.insert(session, nullptr); |
| |
| // If there's an active immersive session save the request but suppress |
| // processing it until the immersive session is no longer active. |
| if (immersive_session_) { |
| return; |
| } |
| |
| RequestNonImmersiveFrameData(session); |
| ScheduleNonImmersiveFrame(std::move(options)); |
| } |
| |
| void XRFrameProvider::ScheduleImmersiveFrame( |
| device::mojom::blink::XRFrameDataRequestOptionsPtr options) { |
| TRACE_EVENT0("gpu", "ScheduleImmersiveFrame"); |
| if (pending_immersive_vsync_) |
| return; |
| |
| pending_immersive_vsync_ = true; |
| frame_data_time_.StartTimer(); |
| // `this` is an okay TRACE ID here, since we are only allowed one immersive |
| // session at a time. |
| TRACE_EVENT_BEGIN("xr", "RequestImmersiveFrame", |
| perfetto::Track::FromPointer(this)); |
| immersive_data_provider_->GetFrameData( |
| std::move(options), BindOnce(&XRFrameProvider::OnImmersiveFrameData, |
| WrapWeakPersistent(this))); |
| } |
| |
| void XRFrameProvider::ScheduleNonImmersiveFrame( |
| device::mojom::blink::XRFrameDataRequestOptionsPtr options) { |
| DVLOG(3) << __func__; |
| TRACE_EVENT0("gpu", "ScheduleNonImmersiveFrame"); |
| |
| DCHECK(!immersive_session_) |
| << "Scheduling should be done via the exclusive session if present."; |
| |
| if (pending_non_immersive_vsync_) { |
| DVLOG(3) << __func__ << ": non immersive vsync already pending"; |
| return; |
| } |
| |
| LocalDOMWindow* window = xr_->DomWindow(); |
| if (!window) |
| return; |
| |
| pending_non_immersive_vsync_ = true; |
| |
| // Calls |OnNonImmersiveVSync| |
| window->document()->RequestAnimationFrame( |
| MakeGarbageCollected<XRFrameProviderRequestCallback>(this)); |
| } |
| |
| void XRFrameProvider::OnImmersiveFrameData( |
| device::mojom::blink::XRFrameDataPtr data) { |
| frame_data_time_.StopTimer(); |
| TRACE_EVENT_END("xr", /*RequestImmersiveFrame*/ |
| perfetto::Track::FromPointer(this)); |
| TRACE_EVENT0("gpu", "OnImmersiveFrameData"); |
| |
| if (data.is_null()) { |
| DVLOG(2) << __func__ << ": no data, current frame_id=" << frame_id_; |
| } else { |
| DVLOG(2) << __func__ |
| << ": have data, frame_id=" << data->render_info->frame_id; |
| } |
| |
| // We may have lost the immersive session since the last VSync request. |
| if (!immersive_session_) { |
| DVLOG(1) << __func__ << ": immersive session lost"; |
| return; |
| } |
| |
| if (!data) { |
| DVLOG(3) << __func__ << ": frame data not received, re-requesting frame"; |
| // We have not received any frame data from the device. We could try to run |
| // an XR animation frame [1], but that may cause issues with APIs that |
| // receive state updates via XRFrameData (e.g. anchors, planes) for |
| // maintaining their current state - they would behave as if all entities |
| // lost tracking and got removed on the device. Let's behave as if we have |
| // not received anything from the device and request a new frame. |
| // |
| // [1] https://immersive-web.github.io/webxr/#xr-animation-frame) |
| |
| pending_immersive_vsync_ = false; |
| RequestFrame(immersive_session_); |
| return; |
| } |
| |
| LocalDOMWindow* window = xr_->DomWindow(); |
| if (!window) { |
| DVLOG(3) << __func__ << ": unable to get local DOM window!"; |
| return; |
| } |
| |
| // Note: The |high_res_now_ms| is computed based on frame time returned from |
| // immersive frame and eventually passed in to requestAnimationFrame callback |
| // as `DOMHighResTimeStamp time`. This means that in case of immersive frames, |
| // the `now` is the same as `frameTime` in XR animation frame algorithm [1]. |
| // |
| // [1] https://immersive-web.github.io/webxr/#xr-animation-frame |
| double high_res_now_ms = UpdateImmersiveFrameTime(window, *data); |
| |
| shared_images_.clear(); |
| |
| frame_id_ = data->render_info->frame_id; |
| if (data->buffer_shared_image.has_value()) { |
| shared_images_.emplace_back( |
| XRSharedImageSource::kBaseLayer, device::kInvalidLayerId, |
| gpu::ClientSharedImage::ImportUnowned( |
| std::move(data->buffer_shared_image.value())), |
| data->buffer_sync_token.value()); |
| } |
| |
| if (data->camera_image_buffer_shared_image.has_value()) { |
| shared_images_.emplace_back( |
| XRSharedImageSource::kCamera, device::kInvalidLayerId, |
| gpu::ClientSharedImage::ImportUnowned( |
| std::move(data->camera_image_buffer_shared_image.value())), |
| data->camera_image_buffer_sync_token.value()); |
| } |
| |
| // Cache composition layer buffers. |
| if (data->composition_layers_data.has_value()) { |
| base::span<const device::mojom::blink::XRLayerFrameDataPtr> layers_data = |
| data->composition_layers_data.value(); |
| for (const auto& layer_data : layers_data) { |
| shared_images_.emplace_back( |
| XRSharedImageSource::kCompositionLayer, layer_data->layer_id, |
| gpu::ClientSharedImage::ImportUnowned( |
| std::move(layer_data->buffer_shared_image)), |
| layer_data->buffer_sync_token); |
| } |
| } |
| |
| pending_immersive_vsync_ = false; |
| |
| for (auto& observer : immersive_observers_) { |
| observer->OnImmersiveFrame(); |
| } |
| |
| // Post a task to handle scheduled animations after the current |
| // execution context finishes, so that we yield to non-mojo tasks in |
| // between frames. Executing mojo tasks back to back within the same |
| // execution context caused extreme input delay due to processing |
| // multiple frames without yielding, see crbug.com/701444. |
| // |
| // Used kInternalMedia since 1) this is not spec-ed and 2) this is media |
| // related then tasks should not be throttled or frozen in background tabs. |
| window->GetTaskRunner(blink::TaskType::kInternalMedia) |
| ->PostTask(FROM_HERE, |
| BindOnce(&XRFrameProvider::ProcessScheduledFrame, |
| WrapWeakPersistent(this), std::move(data), |
| high_res_now_ms, ScheduledFrameType::kImmersive)); |
| } |
| |
| void XRFrameProvider::OnNonImmersiveVSync(double high_res_now_ms) { |
| TRACE_EVENT0("gpu", "OnNonImmersiveVSync"); |
| DVLOG(2) << __func__; |
| |
| pending_non_immersive_vsync_ = false; |
| |
| // Suppress non-immersive vsyncs when there's an immersive session active. |
| if (immersive_session_) |
| return; |
| |
| LocalDOMWindow* window = xr_->DomWindow(); |
| if (!window) |
| return; |
| |
| window->GetTaskRunner(blink::TaskType::kInternalMedia) |
| ->PostTask(FROM_HERE, |
| BindOnce(&XRFrameProvider::ProcessScheduledFrame, |
| WrapWeakPersistent(this), nullptr, high_res_now_ms, |
| ScheduledFrameType::kInline)); |
| } |
| |
| void XRFrameProvider::OnNonImmersiveFrameData( |
| XRSession* session, |
| device::mojom::blink::XRFrameDataPtr frame_data) { |
| TRACE_EVENT0("gpu", "OnNonImmersiveFrameData"); |
| DVLOG(2) << __func__; |
| |
| // TODO(https://crbug.com/837834): add unit tests for this code path. |
| LocalDOMWindow* window = xr_->DomWindow(); |
| if (!window) |
| return; |
| |
| // Look up the request for this session. The session may have ended between |
| // when the request was sent and this callback, so skip it in that case. |
| auto request = requesting_sessions_.find(session); |
| if (request == requesting_sessions_.end()) { |
| DVLOG(3) << __func__ |
| << ": request corresponding to received frame data not found"; |
| if (!session->ended()) { |
| DVLOG(2) << __func__ |
| << ": the session's frame data provider missed the vsync"; |
| } |
| |
| return; |
| } |
| |
| if (frame_data) { |
| DVLOG(3) << __func__ << ": frame data for session stored"; |
| request->value = std::move(frame_data); |
| } else { |
| // Unexpectedly didn't get frame data, and we don't have a timestamp. |
| // Try to request a regular animation frame to avoid getting stuck. |
| DVLOG(1) << __func__ << ": NO FRAME DATA!"; |
| request->value = nullptr; |
| window->document()->RequestAnimationFrame( |
| MakeGarbageCollected<XRFrameProviderRequestCallback>(this)); |
| } |
| } |
| |
| void XRFrameProvider::RequestNonImmersiveFrameData(XRSession* session) { |
| DVLOG(3) << __func__; |
| |
| DCHECK(session); |
| DCHECK(!session->immersive()); |
| DCHECK(!immersive_session_); |
| |
| // The requesting_sessions_ entry for this session must have already |
| // been created in |RequestFrame|. |
| auto request = requesting_sessions_.find(session); |
| CHECK(request != requesting_sessions_.end()); |
| |
| auto provider = non_immersive_data_providers_.find(session); |
| if (provider == non_immersive_data_providers_.end()) { |
| request->value = nullptr; |
| } else { |
| auto& data_provider = provider->value->Value(); |
| auto options = device::mojom::blink::XRFrameDataRequestOptions::New(); |
| options->include_lighting_estimation_data = |
| session->LightEstimationEnabled(); |
| options->stage_parameters_id = session->StageParametersId(); |
| |
| data_provider->GetFrameData( |
| std::move(options), |
| BindOnce(&XRFrameProvider::OnNonImmersiveFrameData, |
| WrapWeakPersistent(this), WrapWeakPersistent(session))); |
| } |
| } |
| |
| void XRFrameProvider::ProcessScheduledFrame( |
| device::mojom::blink::XRFrameDataPtr frame_data, |
| double high_res_now_ms, |
| ScheduledFrameType frame_type) { |
| DVLOG(2) << __func__ << ": frame_id_=" << frame_id_ |
| << ", high_res_now_ms=" << high_res_now_ms; |
| |
| TRACE_EVENT2("gpu", "XRFrameProvider::ProcessScheduledFrame", "frame", |
| frame_id_, "timestamp", high_res_now_ms); |
| |
| LocalDOMWindow* window = xr_->DomWindow(); |
| if (!window) |
| return; |
| |
| if (!xr_->IsFrameFocused() && !immersive_session_) { |
| return; // Not currently focused, so we won't expose poses (except to |
| // immersive sessions). |
| } |
| |
| if (immersive_session_) { |
| if (frame_type != ScheduledFrameType::kImmersive) { |
| DVLOG(1) |
| << __func__ |
| << " Attempted to process non-immersive scheduled frame as immersive"; |
| return; |
| } |
| |
| // Check if immersive session is still valid, it may have ended and be |
| // waiting for shutdown acknowledgement. |
| if (immersive_session_->ended()) { |
| return; |
| } |
| |
| bool emulated_position = false; |
| if (frame_data && frame_data->render_info->mojo_from_viewer) { |
| DVLOG(3) << __func__ << ": pose available, emulated_position=" |
| << frame_data->render_info->mojo_from_viewer->emulated_position; |
| emulated_position = |
| frame_data->render_info->mojo_from_viewer->emulated_position; |
| } else { |
| DVLOG(2) << __func__ << ": emulating immersive frame position"; |
| emulated_position = true; |
| } |
| |
| immersive_session_->UpdatePresentationFrameState( |
| high_res_now_ms, std::move(frame_data), frame_id_, emulated_position); |
| |
| // Check if immersive session is still set as any events dispatched to the |
| // page may have allowed a ForceEndSession to be triggered. |
| if (!immersive_session_ || immersive_session_->ended()) { |
| return; |
| } |
| |
| #if DCHECK_IS_ON() |
| // Sanity check: if drawing into a shared buffer, the optional shared image |
| // must be present. Exception is the first immersive frame after a |
| // transition where the frame ID wasn't set yet. In that case, drawing can |
| // proceed, but the result will be discarded in SubmitWebGLLayer(). |
| if (frame_transport_->DrawingIntoSharedBuffer() && frame_id_ >= 0) { |
| DCHECK(shared_images_.size()); |
| } |
| #endif |
| // Run immersive_session_->OnFrame() in a posted task to ensure that |
| // createAnchor promises get a chance to run - the presentation frame state |
| // is already updated. |
| window->GetTaskRunner(blink::TaskType::kInternalMedia) |
| ->PostTask(FROM_HERE, |
| blink::BindOnce(&XRSession::OnFrame, |
| WrapWeakPersistent(immersive_session_.Get()), |
| high_res_now_ms, std::move(shared_images_))); |
| } else { |
| // In the process of fulfilling the frame requests for each session they are |
| // extremely likely to request another frame. Work off of a separate list |
| // from the requests to prevent infinite loops. |
| decltype(requesting_sessions_) processing_sessions; |
| |
| DVLOG(3) << __func__ << ": clearing requesting_sessions_"; |
| swap(requesting_sessions_, processing_sessions); |
| |
| // Inform sessions with a pending request of the new frame |
| for (auto& request : processing_sessions) { |
| XRSession* session = request.key.Get(); |
| |
| // If the session was terminated between requesting and now, we shouldn't |
| // process anything further. |
| if (session->ended()) { |
| continue; |
| } |
| |
| session->UpdatePresentationFrameState( |
| high_res_now_ms, std::move(request.value), frame_id_, |
| true /* Non-immersive positions are always emulated */); |
| |
| // If any events dispatched to the page caused this session to end, we |
| // should stop processing. |
| if (session->ended()) { |
| continue; |
| } |
| |
| // Run session->OnFrame() in a posted task to ensure that createAnchor |
| // promises get a chance to run - the presentation frame state is already |
| // updated. |
| // Note that rather than call session->OnFrame() directly, we dispatch to |
| // a helper method who can determine if the state requirements are still |
| // met that would allow the frame to be served. |
| window->GetTaskRunner(blink::TaskType::kInternalMedia) |
| ->PostTask(FROM_HERE, |
| BindOnce(&XRFrameProvider::OnPreDispatchInlineFrame, |
| WrapWeakPersistent(this), |
| WrapWeakPersistent(session), high_res_now_ms)); |
| } |
| } |
| } |
| |
| void XRFrameProvider::OnPreDispatchInlineFrame(XRSession* session, |
| double timestamp) { |
| // Do nothing if the session was cleaned up or ended before we were schedueld. |
| if (!session || session->ended()) |
| return; |
| |
| // If we have an immersive session, we shouldn't serve frames to the inline |
| // session; however, we need to ensure that we don't stall out its frame loop, |
| // so add a new frame request to get served after the immersive session exits. |
| if (immersive_session_) { |
| RequestFrame(session); |
| return; |
| } |
| |
| // If we still have the session and don't have an immersive session, then we |
| // should serve the frame. |
| session->OnFrame(timestamp, Vector<XRSharedImageData>()); |
| } |
| |
| double XRFrameProvider::UpdateImmersiveFrameTime( |
| LocalDOMWindow* window, |
| const device::mojom::blink::XRFrameData& data) { |
| DVLOG(3) << __func__; |
| |
| // `data.time_delta` is in unspecified base. Because of that, we capture the |
| // `time_delta` of the first frame we see (this gives us device_t_0), along |
| // with the time we saw it (renderer_t_0). That allows us to translate from |
| // device time to renderer time, so when we see frame N, we perform: |
| // renderer_t_N = renderer_t_0 + (device_t_N - device_t_0) |
| |
| if (!first_immersive_frame_time_) { |
| DCHECK(!first_immersive_frame_time_delta_); |
| |
| // This is the first time we got a frame data from an immersive session. |
| // Let's capture device_t_0 and renderer_t_0. |
| |
| first_immersive_frame_time_ = base::TimeTicks::Now(); |
| first_immersive_frame_time_delta_ = data.time_delta; |
| } |
| |
| // (device_t_N - device_t_0) is: |
| base::TimeDelta current_frame_time_from_first_frame = |
| data.time_delta - *first_immersive_frame_time_delta_; |
| // renderer_t_N is then: |
| base::TimeTicks current_frame_time = |
| *first_immersive_frame_time_ + current_frame_time_from_first_frame; |
| |
| double high_res_now_ms = |
| window->document() |
| ->Loader() |
| ->GetTiming() |
| .MonotonicTimeToZeroBasedDocumentTime(current_frame_time) |
| .InMillisecondsF(); |
| |
| return high_res_now_ms; |
| } |
| |
| void XRFrameProvider::SubmitLayer(device::LayerId layer_id, |
| XrLayerClient* client, |
| bool was_changed) { |
| CHECK(client); |
| CHECK(immersive_session_); |
| CHECK(client->session()); |
| CHECK_EQ(client->session(), immersive_session_); |
| |
| if (!immersive_presentation_provider_.is_bound()) { |
| return; |
| } |
| |
| TRACE_EVENT1("gpu", "XRFrameProvider::SubmitLayer", "frame", frame_id_); |
| DVLOG(3) << __func__ << ": frame=" << frame_id_; |
| |
| if (!was_changed) { |
| return; |
| } |
| |
| if (frame_id_ < 0) { |
| // There is no valid frame_id_, and the browser side is not currently |
| // expecting a frame to be submitted. That can happen for the first |
| // immersive frame if the animation loop submits without a preceding |
| // immersive GetFrameData response, in that case frame_id_ is -1 (see |
| // https://crbug.com/855722). |
| return; |
| } |
| |
| if (frame_transport_->DrawingIntoSharedBuffer()) { |
| // Image is written to shared buffer already. No need to hold it. |
| DVLOG(3) << __func__ << ": FrameSubmit for SharedBuffer mode"; |
| any_layer_changed_ = true; |
| layer_ids_.push_back(layer_id); |
| return; |
| } else { |
| CHECK_NE(client->session()->GraphicsApi(), XRGraphicsBinding::Api::kWebGPU) |
| << "WebGPU layers only support shared buffer submission modes"; |
| } |
| |
| scoped_refptr<StaticBitmapImage> image_ref = |
| client->TransferToStaticBitmapImage(); |
| |
| if (!image_ref) { |
| return; |
| } |
| |
| // Hardware-accelerated rendering should always be texture backed. Ensure this |
| // is the case, don't attempt to render if using an unexpected drawing path. |
| if (!image_ref->IsTextureBacked()) { |
| NOTREACHED() << "WebXR requires hardware-accelerated rendering to texture"; |
| } |
| |
| any_layer_changed_ = true; |
| current_frame_images_.push_back(image_ref); |
| layer_ids_.push_back(layer_id); |
| } |
| |
| // TODO(bajones): This only works because we're restricted to a single layer at |
| // the moment. Will need an overhaul when we get more robust layering support. |
| void XRFrameProvider::UpdateWebGLLayerViewports(XRWebGLLayer* layer) { |
| DCHECK(layer->session() == immersive_session_); |
| DCHECK(immersive_presentation_provider_.is_bound()); |
| |
| XRViewport* left = |
| layer->GetViewportForEye(device::mojom::blink::XREye::kLeft); |
| XRViewport* right = |
| layer->GetViewportForEye(device::mojom::blink::XREye::kRight); |
| float width = layer->framebufferWidth(); |
| float height = layer->framebufferHeight(); |
| |
| // We may only have one eye view, i.e. in smartphone immersive AR mode. |
| // Use all-zero bounds for unused views. |
| gfx::RectF left_coords = |
| left ? gfx::RectF( |
| static_cast<float>(left->x()) / width, |
| static_cast<float>(height - (left->y() + left->height())) / |
| height, |
| static_cast<float>(left->width()) / width, |
| static_cast<float>(left->height()) / height) |
| : gfx::RectF(); |
| gfx::RectF right_coords = |
| right ? gfx::RectF( |
| static_cast<float>(right->x()) / width, |
| static_cast<float>(height - (right->y() + right->height())) / |
| height, |
| static_cast<float>(right->width()) / width, |
| static_cast<float>(right->height()) / height) |
| : gfx::RectF(); |
| |
| immersive_presentation_provider_->UpdateLayerBounds( |
| frame_id_, left_coords, right_coords, gfx::Size(width, height)); |
| } |
| |
| // TODO(bajones): This only works because we're restricted to a single layer at |
| // the moment. Will need an overhaul when we get more robust layering support. |
| void XRFrameProvider::UpdateLayerViewports(XRProjectionLayer* layer) { |
| DCHECK(layer->session() == immersive_session_); |
| DCHECK(immersive_presentation_provider_.is_bound()); |
| |
| XRGraphicsBinding* binding = layer->binding(); |
| |
| // TODO(crbug.com/359418629): Currently we have no way to submit texture |
| // arrays to the compositor, so any array textures produced by the page will |
| // be copied to a side-by-side texture prior to submission. That does mean |
| // that we need to adjust the viewports from those reported to the page, |
| // however, by altering the texture width here... |
| float width = layer->textureWidth() * layer->textureArrayLength(); |
| float height = layer->textureHeight(); |
| |
| gfx::RectF left_coords; |
| gfx::RectF right_coords; |
| if (immersive_session_->StereoscopicViews()) { |
| XRViewData* left_view = |
| immersive_session_->ViewDataForEye(device::mojom::blink::XREye::kLeft); |
| XRViewData* right_view = |
| immersive_session_->ViewDataForEye(device::mojom::blink::XREye::kRight); |
| |
| gfx::Rect left = binding->GetViewportForView(layer, left_view); |
| gfx::Rect right = binding->GetViewportForView(layer, right_view); |
| |
| // (continued from prior comment) ...and offsetting the viewports here. |
| if (layer->textureArrayLength() > 1) { |
| right.set_x(right.x() + layer->textureWidth()); |
| } |
| |
| left_coords = NormalizeViewport(left, width, height); |
| right_coords = NormalizeViewport(right, width, height); |
| } else { |
| XRViewData* mono_view = |
| immersive_session_->ViewDataForEye(device::mojom::blink::XREye::kNone); |
| gfx::Rect viewport = binding->GetViewportForView(layer, mono_view); |
| |
| left_coords = NormalizeViewport(viewport, width, height); |
| |
| // Non-stereo modes (i.e. smartphone immersive AR mode) |
| // use the default all-zero bounds for right view. |
| } |
| |
| immersive_presentation_provider_->UpdateLayerBounds( |
| frame_id_, left_coords, right_coords, gfx::Size(width, height)); |
| } |
| |
| void XRFrameProvider::ClearCachedLayersData() { |
| any_layer_changed_ = false; |
| current_frame_images_.clear(); |
| layer_ids_.clear(); |
| } |
| |
| void XRFrameProvider::SubmitFrame( |
| XRFrameTransportDelegate* transport_delegate) { |
| CHECK(transport_delegate); |
| |
| if (!immersive_presentation_provider_.is_bound()) { |
| return; |
| } |
| |
| TRACE_EVENT1("gpu", "XRFrameProvider::SubmitFrame", "frame", frame_id_); |
| DVLOG(3) << __func__ << ": frame=" << frame_id_; |
| |
| // Ensure temporary data is always reset. |
| bool was_any_layer_changed = any_layer_changed_; |
| any_layer_changed_ = false; |
| auto image_refs = std::move(current_frame_images_); |
| auto layer_ids = std::move(layer_ids_); |
| |
| if (frame_id_ < 0) { |
| // There is no valid frame_id_, and the browser side is not currently |
| // expecting a frame to be submitted. That can happen for the first |
| // immersive frame if the animation loop submits without a preceding |
| // immersive GetFrameData response, in that case frame_id_ is -1 (see |
| // https://crbug.com/855722). |
| return; |
| } |
| |
| auto this_frame_id = frame_id_; |
| |
| // Reset an active frame id, since anything we'd want to do (resizing/etc) can |
| // no-longer happen to this frame. |
| frame_id_ = -1; |
| |
| if (!immersive_presentation_provider_.is_bound()) { |
| return; |
| } |
| |
| if (!was_any_layer_changed) { |
| // Just tell the device side that there was no submitted frame instead of |
| // executing the implicit end-of-frame submit. |
| frame_transport_->FrameSubmitMissing(immersive_presentation_provider_.get(), |
| transport_delegate, this_frame_id); |
| dropped_frames_++; |
| |
| return; |
| } |
| |
| frame_transport_->FramePreImage(transport_delegate); |
| |
| // The backend expects an empty layer ID list if the 'layers' feature is not |
| // enabled. |
| if (!layer_manager_.is_bound()) { |
| // At this case, only a single layer should exist since the |
| // layers feature is not enabled. |
| CHECK_EQ(layer_ids.size(), 1U); |
| layer_ids.clear(); |
| } |
| |
| bool succeeded = frame_transport_->FrameSubmit( |
| immersive_presentation_provider_.get(), transport_delegate, |
| std::move(layer_ids), std::move(image_refs), this_frame_id); |
| |
| succeeded ? num_frames_++ : dropped_frames_++; |
| if (succeeded) { |
| submit_frame_time_.StartTimer(); |
| } |
| } |
| |
| void XRFrameProvider::Dispose() { |
| DVLOG(2) << __func__; |
| immersive_presentation_provider_.reset(); |
| immersive_data_provider_.reset(); |
| // Reset layers data. |
| ClearCachedLayersData(); |
| if (immersive_session_) |
| immersive_session_->ForceEnd(XRSession::ShutdownPolicy::kImmediate); |
| // TODO(bajones): Do something for outstanding frame requests? |
| } |
| |
| void XRFrameProvider::SendFrameData() { |
| if (!immersive_session_) { |
| return; |
| } |
| |
| device::mojom::blink::XrFrameStatisticsPtr xr_frame_stat = |
| device::mojom::blink::XrFrameStatistics::New(); |
| |
| xr_frame_stat->trace_id = immersive_session_->GetTraceId(); |
| |
| base::TimeTicks now = base::TimeTicks::Now(); |
| xr_frame_stat->duration = now - last_frame_statistics_sent_time_; |
| last_frame_statistics_sent_time_ = now; |
| |
| xr_frame_stat->num_frames = num_frames_; |
| xr_frame_stat->dropped_frames = dropped_frames_; |
| |
| num_frames_ = 0; |
| dropped_frames_ = 0; |
| |
| xr_frame_stat->frame_data_time = frame_data_time_.TakeAverageMicroseconds(); |
| |
| xr_frame_stat->page_animation_frame_time = |
| immersive_session()->TakeAnimationFrameTimerAverage(); |
| |
| xr_frame_stat->submit_frame_time = |
| submit_frame_time_.TakeAverageMicroseconds(); |
| |
| if (xr_->GetWebXrInternalsRendererListener()) { |
| xr_->GetWebXrInternalsRendererListener()->OnFrameData( |
| std::move(xr_frame_stat)); |
| } |
| } |
| |
| void XRFrameProvider::OnRenderComplete() { |
| submit_frame_time_.StopTimer(); |
| } |
| |
| bool XRFrameProvider::DrawingIntoSharedBuffer() const { |
| return frame_transport_->DrawingIntoSharedBuffer(); |
| } |
| |
| void XRFrameProvider::Trace(Visitor* visitor) const { |
| visitor->Trace(xr_); |
| visitor->Trace(frame_transport_); |
| visitor->Trace(immersive_session_); |
| visitor->Trace(immersive_data_provider_); |
| visitor->Trace(immersive_presentation_provider_); |
| visitor->Trace(non_immersive_data_providers_); |
| visitor->Trace(requesting_sessions_); |
| visitor->Trace(immersive_observers_); |
| visitor->Trace(layer_manager_); |
| } |
| |
| } // namespace blink |