| // Copyright 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 "third_party/blink/renderer/modules/xr/xr_session.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| |
| #include "base/auto_reset.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_xr_frame_request_callback.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/frame/frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/resize_observer/resize_observer.h" |
| #include "third_party/blink/renderer/core/resize_observer/resize_observer_entry.h" |
| #include "third_party/blink/renderer/modules/event_target_modules.h" |
| #include "third_party/blink/renderer/modules/screen_orientation/screen_orientation.h" |
| #include "third_party/blink/renderer/modules/xr/xr.h" |
| #include "third_party/blink/renderer/modules/xr/xr_anchor_set.h" |
| #include "third_party/blink/renderer/modules/xr/xr_bounded_reference_space.h" |
| #include "third_party/blink/renderer/modules/xr/xr_canvas_input_provider.h" |
| #include "third_party/blink/renderer/modules/xr/xr_frame.h" |
| #include "third_party/blink/renderer/modules/xr/xr_frame_provider.h" |
| #include "third_party/blink/renderer/modules/xr/xr_hit_result.h" |
| #include "third_party/blink/renderer/modules/xr/xr_hit_test_options.h" |
| #include "third_party/blink/renderer/modules/xr/xr_hit_test_source.h" |
| #include "third_party/blink/renderer/modules/xr/xr_input_source_event.h" |
| #include "third_party/blink/renderer/modules/xr/xr_input_sources_change_event.h" |
| #include "third_party/blink/renderer/modules/xr/xr_plane.h" |
| #include "third_party/blink/renderer/modules/xr/xr_ray.h" |
| #include "third_party/blink/renderer/modules/xr/xr_reference_space.h" |
| #include "third_party/blink/renderer/modules/xr/xr_render_state.h" |
| #include "third_party/blink/renderer/modules/xr/xr_render_state_init.h" |
| #include "third_party/blink/renderer/modules/xr/xr_session_event.h" |
| #include "third_party/blink/renderer/modules/xr/xr_view.h" |
| #include "third_party/blink/renderer/modules/xr/xr_webgl_layer.h" |
| #include "third_party/blink/renderer/modules/xr/xr_world_information.h" |
| #include "third_party/blink/renderer/modules/xr/xr_world_tracking_state.h" |
| #include "third_party/blink/renderer/modules/xr/xr_world_tracking_state_init.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/transforms/transformation_matrix.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| const char kSessionEnded[] = "XRSession has already ended."; |
| |
| const char kReferenceSpaceNotSupported[] = |
| "This device does not support the requested reference space type."; |
| |
| const char kIncompatibleLayer[] = |
| "XRWebGLLayer was created with a different session."; |
| |
| const char kInlineVerticalFOVNotSupported[] = |
| "This session does not support inlineVerticalFieldOfView"; |
| |
| const char kNoSpaceSpecified[] = "No XRSpace specified."; |
| |
| const char kNoRigidTransformSpecified[] = "No XRRigidTransform specified."; |
| |
| const char kHitTestNotSupported[] = "Device does not support hit-test!"; |
| |
| const char kAnchorsNotSupported[] = "Device does not support anchors!"; |
| |
| const char kDeviceDisconnected[] = "The XR device has been disconnected."; |
| |
| const char kNonInvertibleMatrix[] = |
| "The operation encountered non-invertible matrix and could not be " |
| "completed."; |
| |
| const char kUnableToDecomposeMatrix[] = |
| "The operation was unable to decompose a matrix and could not be " |
| "completed."; |
| |
| const char kUnableToRetrieveNativeOrigin[] = |
| "The operation was unable to retrieve the native origin from XRSpace and " |
| "could not be completed."; |
| |
| const char kHitTestSubscriptionFailed[] = "Hit test subscription failed."; |
| |
| const double kDegToRad = M_PI / 180.0; |
| |
| // Indices into the views array. |
| const unsigned int kMonoOrStereoLeftView = 0; |
| const unsigned int kStereoRightView = 1; |
| |
| void UpdateViewFromEyeParameters( |
| XRViewData& view, |
| const device::mojom::blink::VREyeParametersPtr& eye, |
| double depth_near, |
| double depth_far) { |
| const device::mojom::blink::VRFieldOfViewPtr& fov = eye->field_of_view; |
| |
| view.UpdateProjectionMatrixFromFoV( |
| fov->up_degrees * kDegToRad, fov->down_degrees * kDegToRad, |
| fov->left_degrees * kDegToRad, fov->right_degrees * kDegToRad, depth_near, |
| depth_far); |
| |
| const TransformationMatrix matrix(eye->head_from_eye.matrix()); |
| view.SetHeadFromEyeTransform(matrix); |
| } |
| |
| // Returns the session feature corresponding to the given reference space type. |
| base::Optional<device::mojom::XRSessionFeature> MapReferenceSpaceTypeToFeature( |
| XRReferenceSpace::Type type) { |
| switch (type) { |
| case XRReferenceSpace::Type::kTypeViewer: |
| return device::mojom::XRSessionFeature::REF_SPACE_VIEWER; |
| case XRReferenceSpace::Type::kTypeLocal: |
| return device::mojom::XRSessionFeature::REF_SPACE_LOCAL; |
| case XRReferenceSpace::Type::kTypeLocalFloor: |
| return device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR; |
| case XRReferenceSpace::Type::kTypeBoundedFloor: |
| return device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR; |
| case XRReferenceSpace::Type::kTypeUnbounded: |
| return device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED; |
| } |
| |
| NOTREACHED(); |
| return base::nullopt; |
| } |
| |
| } // namespace |
| |
| class XRSession::XRSessionResizeObserverDelegate final |
| : public ResizeObserver::Delegate { |
| public: |
| explicit XRSessionResizeObserverDelegate(XRSession* session) |
| : session_(session) { |
| DCHECK(session); |
| } |
| ~XRSessionResizeObserverDelegate() override = default; |
| |
| void OnResize( |
| const HeapVector<Member<ResizeObserverEntry>>& entries) override { |
| DCHECK_EQ(1u, entries.size()); |
| session_->UpdateCanvasDimensions(entries[0]->target()); |
| } |
| |
| void Trace(blink::Visitor* visitor) override { |
| visitor->Trace(session_); |
| ResizeObserver::Delegate::Trace(visitor); |
| } |
| |
| private: |
| Member<XRSession> session_; |
| }; |
| |
| XRSession::XRSession( |
| XR* xr, |
| mojo::PendingReceiver<device::mojom::blink::XRSessionClient> |
| client_receiver, |
| XRSession::SessionMode mode, |
| EnvironmentBlendMode environment_blend_mode, |
| bool uses_input_eventing, |
| bool sensorless_session, |
| XRSessionFeatureSet enabled_features) |
| : xr_(xr), |
| mode_(mode), |
| environment_integration_(mode == kModeImmersiveAR), |
| world_tracking_state_(MakeGarbageCollected<XRWorldTrackingState>()), |
| world_information_(MakeGarbageCollected<XRWorldInformation>(this)), |
| enabled_features_(std::move(enabled_features)), |
| input_sources_(MakeGarbageCollected<XRInputSourceArray>()), |
| client_receiver_(this, std::move(client_receiver)), |
| input_binding_(this), |
| callback_collection_( |
| MakeGarbageCollected<XRFrameRequestCallbackCollection>( |
| xr_->GetExecutionContext())), |
| uses_input_eventing_(uses_input_eventing), |
| sensorless_session_(sensorless_session) { |
| render_state_ = MakeGarbageCollected<XRRenderState>(immersive()); |
| // Ensure that frame focus is considered in the initial visibilityState. |
| UpdateVisibilityState(); |
| |
| switch (environment_blend_mode) { |
| case kBlendModeOpaque: |
| blend_mode_string_ = "opaque"; |
| break; |
| case kBlendModeAdditive: |
| blend_mode_string_ = "additive"; |
| break; |
| case kBlendModeAlphaBlend: |
| blend_mode_string_ = "alpha-blend"; |
| break; |
| default: |
| NOTREACHED() << "Unknown environment blend mode: " |
| << environment_blend_mode; |
| } |
| } |
| |
| const String XRSession::visibilityState() const { |
| switch (visibility_state_) { |
| case XRVisibilityState::VISIBLE: |
| return "visible"; |
| case XRVisibilityState::VISIBLE_BLURRED: |
| return "visible-blurred"; |
| case XRVisibilityState::HIDDEN: |
| return "hidden"; |
| } |
| } |
| |
| XRAnchorSet* XRSession::trackedAnchors() const { |
| DVLOG(3) << __func__; |
| |
| HeapHashSet<Member<XRAnchor>> result; |
| |
| if (is_tracked_anchors_null_) |
| return nullptr; |
| |
| for (auto& anchor_id_and_anchor : anchor_ids_to_anchors_) { |
| result.insert(anchor_id_and_anchor.value); |
| } |
| |
| return MakeGarbageCollected<XRAnchorSet>(result); |
| } |
| |
| bool XRSession::immersive() const { |
| return mode_ == kModeImmersiveVR || mode_ == kModeImmersiveAR; |
| } |
| |
| ExecutionContext* XRSession::GetExecutionContext() const { |
| return xr_->GetExecutionContext(); |
| } |
| |
| const AtomicString& XRSession::InterfaceName() const { |
| return event_target_names::kXRSession; |
| } |
| |
| device::mojom::blink::XRInputSourceButtonListenerAssociatedPtrInfo |
| XRSession::GetInputClickListener() { |
| DCHECK(!input_binding_); |
| device::mojom::blink::XRInputSourceButtonListenerAssociatedPtrInfo |
| input_listener; |
| input_binding_.Bind(MakeRequest(&input_listener)); |
| return input_listener; |
| } |
| |
| void XRSession::updateRenderState(XRRenderStateInit* init, |
| ExceptionState& exception_state) { |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return; |
| } |
| |
| if (immersive() && init->hasInlineVerticalFieldOfView()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kInlineVerticalFOVNotSupported); |
| return; |
| } |
| |
| // Validate that any baseLayer provided was created with this session. |
| if (init->hasBaseLayer() && init->baseLayer() && |
| init->baseLayer()->session() != this) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kIncompatibleLayer); |
| return; |
| } |
| |
| pending_render_state_.push_back(init); |
| |
| // Updating our render state may have caused us to be in a state where we |
| // should be requesting frames again. Kick off a new frame request in case |
| // there are any pending callbacks to flush them out. |
| MaybeRequestFrame(); |
| } |
| |
| void XRSession::updateWorldTrackingState( |
| XRWorldTrackingStateInit* world_tracking_state_init, |
| ExceptionState& exception_state) { |
| DVLOG(3) << __func__; |
| |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return; |
| } |
| |
| world_tracking_state_ = |
| MakeGarbageCollected<XRWorldTrackingState>(world_tracking_state_init); |
| } |
| |
| void XRSession::UpdateEyeParameters( |
| const device::mojom::blink::VREyeParametersPtr& left_eye, |
| const device::mojom::blink::VREyeParametersPtr& right_eye) { |
| auto display_info = display_info_.Clone(); |
| display_info->left_eye = left_eye.Clone(); |
| display_info->right_eye = right_eye.Clone(); |
| SetXRDisplayInfo(std::move(display_info)); |
| } |
| |
| void XRSession::UpdateStageParameters( |
| const device::mojom::blink::VRStageParametersPtr& stage_parameters) { |
| auto display_info = display_info_.Clone(); |
| display_info->stage_parameters = stage_parameters.Clone(); |
| SetXRDisplayInfo(std::move(display_info)); |
| } |
| |
| ScriptPromise XRSession::requestReferenceSpace( |
| ScriptState* script_state, |
| const String& type, |
| ExceptionState& exception_state) { |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return ScriptPromise(); |
| } |
| |
| XRReferenceSpace::Type requested_type = |
| XRReferenceSpace::StringToReferenceSpaceType(type); |
| |
| UMA_HISTOGRAM_ENUMERATION("XR.WebXR.ReferenceSpace.Requested", |
| requested_type); |
| |
| if (sensorless_session_ && |
| requested_type != XRReferenceSpace::Type::kTypeViewer) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, |
| kReferenceSpaceNotSupported); |
| return ScriptPromise(); |
| } |
| |
| // If the session feature required by this reference space type is not |
| // enabled, reject the session. |
| auto type_as_feature = MapReferenceSpaceTypeToFeature(requested_type); |
| if (!type_as_feature || |
| !enabled_features_.Contains(type_as_feature.value())) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, |
| kReferenceSpaceNotSupported); |
| return ScriptPromise(); |
| } |
| |
| XRReferenceSpace* reference_space = nullptr; |
| switch (requested_type) { |
| case XRReferenceSpace::Type::kTypeViewer: |
| case XRReferenceSpace::Type::kTypeLocal: |
| case XRReferenceSpace::Type::kTypeLocalFloor: |
| reference_space = |
| MakeGarbageCollected<XRReferenceSpace>(this, requested_type); |
| break; |
| case XRReferenceSpace::Type::kTypeBoundedFloor: { |
| bool supports_bounded = false; |
| if (immersive() && display_info_->stage_parameters) { |
| if (display_info_->stage_parameters->bounds) { |
| supports_bounded = true; |
| } else if (display_info_->stage_parameters->size_x > 0 && |
| display_info_->stage_parameters->size_z > 0) { |
| supports_bounded = true; |
| } |
| } |
| |
| if (supports_bounded) { |
| reference_space = MakeGarbageCollected<XRBoundedReferenceSpace>(this); |
| } |
| break; |
| } |
| case XRReferenceSpace::Type::kTypeUnbounded: |
| if (immersive() && environment_integration_) { |
| reference_space = MakeGarbageCollected<XRReferenceSpace>( |
| this, XRReferenceSpace::Type::kTypeUnbounded); |
| } |
| break; |
| } |
| |
| // If the above switch statement failed to assign to reference_space, |
| // it's because the reference space wasn't supported by the device. |
| if (!reference_space) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, |
| kReferenceSpaceNotSupported); |
| return ScriptPromise(); |
| } |
| |
| DCHECK(reference_space); |
| reference_spaces_.push_back(reference_space); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| resolver->Resolve(reference_space); |
| |
| UMA_HISTOGRAM_ENUMERATION("XR.WebXR.ReferenceSpace.Succeeded", |
| requested_type); |
| |
| return promise; |
| } |
| |
| ScriptPromise XRSession::CreateAnchor(ScriptState* script_state, |
| XRRigidTransform* initial_pose, |
| XRSpace* space, |
| XRPlane* plane, |
| ExceptionState& exception_state) { |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return ScriptPromise(); |
| } |
| |
| if (!initial_pose) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kNoRigidTransformSpecified); |
| return ScriptPromise(); |
| } |
| |
| if (!space) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kNoSpaceSpecified); |
| return ScriptPromise(); |
| } |
| |
| // Reject the promise if device doesn't support the anchors API. |
| if (!xr_->xrEnvironmentProviderPtr()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kAnchorsNotSupported); |
| return ScriptPromise(); |
| } |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| // Transformation from passed in |space| to mojo space. |
| std::unique_ptr<TransformationMatrix> mojo_from_space = |
| space->MojoFromSpace(); |
| |
| DVLOG(3) << __func__ |
| << ": mojo_from_space = " << mojo_from_space->ToString(true); |
| |
| // Matrix will be null if transformation from object space to mojo space is |
| // not invertible, log & bail out in that case. |
| if (!mojo_from_space || !mojo_from_space->IsInvertible()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kNonInvertibleMatrix); |
| return ScriptPromise(); |
| } |
| |
| auto space_from_mojo = mojo_from_space->Inverse(); |
| |
| DVLOG(3) << __func__ |
| << ": space_from_mojo = " << space_from_mojo.ToString(true); |
| |
| // Transformation from passed in pose to |space|. |
| auto mojo_from_initial_pose = initial_pose->TransformMatrix(); |
| auto space_from_initial_pose = space_from_mojo * mojo_from_initial_pose; |
| |
| DVLOG(3) << __func__ << ": mojo_from_initial_pose = " |
| << mojo_from_initial_pose.ToString(true); |
| |
| DVLOG(3) << __func__ << ": space_from_initial_pose = " |
| << space_from_initial_pose.ToString(true); |
| |
| TransformationMatrix::DecomposedType decomposed; |
| if (!space_from_initial_pose.Decompose(decomposed)) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kUnableToDecomposeMatrix); |
| return ScriptPromise(); |
| } |
| |
| device::mojom::blink::VRPosePtr pose_ptr = |
| device::mojom::blink::VRPose::New(); |
| |
| pose_ptr->orientation = |
| gfx::Quaternion(-decomposed.quaternion_x, -decomposed.quaternion_y, |
| -decomposed.quaternion_z, decomposed.quaternion_w); |
| pose_ptr->position = blink::WebFloatPoint3D( |
| decomposed.translate_x, decomposed.translate_y, decomposed.translate_z); |
| |
| DVLOG(3) << __func__ |
| << ": pose_ptr->orientation = " << pose_ptr->orientation->ToString() |
| << ", pose_ptr->position = [" << pose_ptr->position->x << ", " |
| << pose_ptr->position->y << ", " << pose_ptr->position->z << "]"; |
| |
| if (plane) { |
| xr_->xrEnvironmentProviderPtr()->CreatePlaneAnchor( |
| std::move(pose_ptr), plane->id(), |
| WTF::Bind(&XRSession::OnCreateAnchorResult, WrapPersistent(this), |
| WrapPersistent(resolver))); |
| } else { |
| xr_->xrEnvironmentProviderPtr()->CreateAnchor( |
| std::move(pose_ptr), |
| WTF::Bind(&XRSession::OnCreateAnchorResult, WrapPersistent(this), |
| WrapPersistent(resolver))); |
| } |
| create_anchor_promises_.insert(resolver); |
| |
| return promise; |
| } |
| |
| ScriptPromise XRSession::createAnchor(ScriptState* script_state, |
| XRRigidTransform* initial_pose, |
| XRSpace* space, |
| ExceptionState& exception_state) { |
| return CreateAnchor(script_state, initial_pose, space, nullptr, |
| exception_state); |
| } |
| |
| int XRSession::requestAnimationFrame(V8XRFrameRequestCallback* callback) { |
| TRACE_EVENT0("gpu", __func__); |
| // Don't allow any new frame requests once the session is ended. |
| if (ended_) |
| return 0; |
| |
| int id = callback_collection_->RegisterCallback(callback); |
| MaybeRequestFrame(); |
| return id; |
| } |
| |
| void XRSession::cancelAnimationFrame(int id) { |
| callback_collection_->CancelCallback(id); |
| } |
| |
| XRInputSourceArray* XRSession::inputSources() const { |
| Document* doc = To<Document>(GetExecutionContext()); |
| if (!did_log_getInputSources_ && doc) { |
| ukm::builders::XR_WebXR(xr_->GetSourceId()) |
| .SetDidGetXRInputSources(1) |
| .Record(doc->UkmRecorder()); |
| did_log_getInputSources_ = true; |
| } |
| |
| return input_sources_; |
| } |
| |
| ScriptPromise XRSession::requestHitTest(ScriptState* script_state, |
| XRRay* ray, |
| XRSpace* space, |
| ExceptionState& exception_state) { |
| DVLOG(2) << __func__; |
| |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return ScriptPromise(); |
| } |
| |
| if (!space) { |
| exception_state.ThrowTypeError(kNoSpaceSpecified); |
| return ScriptPromise(); |
| } |
| |
| // Reject the promise if device doesn't support the hit-test API. |
| if (!xr_->xrEnvironmentProviderPtr()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, |
| kHitTestNotSupported); |
| return ScriptPromise(); |
| } |
| |
| device::mojom::blink::XRRayPtr ray_mojo = device::mojom::blink::XRRay::New(); |
| |
| ray_mojo->origin = WebFloatPoint3D(ray->origin()->x(), ray->origin()->y(), |
| ray->origin()->z()); |
| |
| ray_mojo->direction = {ray->direction()->x(), ray->direction()->y(), |
| ray->direction()->z()}; |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| xr_->xrEnvironmentProviderPtr()->RequestHitTest( |
| std::move(ray_mojo), |
| WTF::Bind(&XRSession::OnHitTestResults, WrapPersistent(this), |
| WrapPersistent(resolver))); |
| hit_test_promises_.insert(resolver); |
| |
| return promise; |
| } |
| |
| ScriptPromise XRSession::requestHitTestSource( |
| ScriptState* script_state, |
| XRHitTestOptionsInit* options_init, |
| ExceptionState& exception_state) { |
| DVLOG(2) << __func__; |
| |
| DCHECK(options_init); // is this enforced by generated bindings? |
| |
| XRHitTestOptions* options = |
| MakeGarbageCollected<XRHitTestOptions>(options_init); |
| |
| // 1. Grab the native origin from the passed in XRSpace. |
| base::Optional<XRNativeOriginInformation> maybe_native_origin = |
| options->space()->NativeOrigin(); |
| |
| if (!maybe_native_origin) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kUnableToRetrieveNativeOrigin); |
| return {}; |
| } |
| |
| // 2. Convert the XRRay to be expressed in terms of passed in XRSpace. This |
| // should only matter for spaces whose transforms are not fully known on the |
| // device (for example any space containing origin-offset). |
| TransformationMatrix origin_from_space = |
| options->space()->OriginOffsetMatrix(); |
| |
| DVLOG(3) << __func__ |
| << ": origin_from_space = " << origin_from_space.ToString(true); |
| |
| // Transformation from passed in pose to |space|. |
| auto space_from_ray = options->offsetRay()->RawMatrix(); |
| auto origin_from_ray = origin_from_space * space_from_ray; |
| |
| DVLOG(3) << __func__ |
| << ": space_from_ray = " << space_from_ray.ToString(true); |
| |
| DVLOG(3) << __func__ |
| << ": origin_from_ray = " << origin_from_ray.ToString(true); |
| |
| device::mojom::blink::XRRayPtr ray_mojo = device::mojom::blink::XRRay::New(); |
| |
| ray_mojo->origin = WebFloatPoint3D(origin_from_ray.MapPoint({0, 0, 0})); |
| |
| // Zero out the translation of origin_from_ray matrix to correctly map a 3D |
| // vector. |
| origin_from_ray.Translate3d(-origin_from_ray.M41(), -origin_from_ray.M42(), |
| -origin_from_ray.M43()); |
| |
| auto direction = origin_from_ray.MapPoint({0, 0, -1}); |
| ray_mojo->direction = {direction.X(), direction.Y(), direction.Z()}; |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| xr_->xrEnvironmentProviderPtr()->SubscribeToHitTest( |
| maybe_native_origin->ToMojo(), std::move(ray_mojo), |
| WTF::Bind(&XRSession::OnSubscribeToHitTestResult, WrapPersistent(this), |
| WrapPersistent(resolver), WrapPersistent(options))); |
| request_hit_test_source_promises_.insert(resolver); |
| |
| return promise; |
| } |
| |
| void XRSession::OnHitTestResults( |
| ScriptPromiseResolver* resolver, |
| base::Optional<WTF::Vector<device::mojom::blink::XRHitResultPtr>> results) { |
| DCHECK(hit_test_promises_.Contains(resolver)); |
| hit_test_promises_.erase(resolver); |
| |
| if (!results) { |
| resolver->Reject(); |
| return; |
| } |
| |
| HeapVector<Member<XRHitResult>> hit_results; |
| for (const auto& mojom_result : results.value()) { |
| XRHitResult* hit_result = MakeGarbageCollected<XRHitResult>( |
| TransformationMatrix(mojom_result->hit_matrix.matrix())); |
| hit_results.push_back(hit_result); |
| } |
| resolver->Resolve(hit_results); |
| } |
| |
| void XRSession::OnSubscribeToHitTestResult( |
| ScriptPromiseResolver* resolver, |
| XRHitTestOptions* options, |
| device::mojom::SubscribeToHitTestResult result, |
| uint32_t subscription_id) { |
| DVLOG(2) << __func__ << ": result=" << result |
| << ", subscription_id=" << subscription_id; |
| |
| DCHECK(request_hit_test_source_promises_.Contains(resolver)); |
| request_hit_test_source_promises_.erase(resolver); |
| |
| if (result != device::mojom::SubscribeToHitTestResult::SUCCESS) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kOperationError, kHitTestSubscriptionFailed)); |
| return; |
| } |
| |
| XRHitTestSource* hit_test_source = |
| MakeGarbageCollected<XRHitTestSource>(subscription_id, options); |
| |
| hit_test_source_ids_to_hit_test_sources_.insert(subscription_id, |
| hit_test_source); |
| |
| resolver->Resolve(hit_test_source); |
| } |
| |
| void XRSession::OnCreateAnchorResult(ScriptPromiseResolver* resolver, |
| device::mojom::CreateAnchorResult result, |
| uint32_t id) { |
| DCHECK(create_anchor_promises_.Contains(resolver)); |
| create_anchor_promises_.erase(resolver); |
| |
| XRAnchor* anchor = MakeGarbageCollected<XRAnchor>(id, this); |
| |
| anchor_ids_to_anchors_.insert(id, anchor); |
| |
| resolver->Resolve(anchor); |
| } |
| |
| void XRSession::OnEnvironmentProviderCreated() { |
| EnsureEnvironmentErrorHandler(); |
| } |
| |
| void XRSession::EnsureEnvironmentErrorHandler() { |
| // Install error handler on environment provider to ensure that we get |
| // notified so that we can clean up all relevant pending promises. |
| if (!environment_error_handler_subscribed_ && |
| xr_->xrEnvironmentProviderPtr()) { |
| environment_error_handler_subscribed_ = true; |
| xr_->AddEnvironmentProviderErrorHandler(WTF::Bind( |
| &XRSession::OnEnvironmentProviderError, WrapWeakPersistent(this))); |
| } |
| } |
| |
| void XRSession::OnEnvironmentProviderError() { |
| HeapHashSet<Member<ScriptPromiseResolver>> hit_test_promises; |
| hit_test_promises_.swap(hit_test_promises); |
| for (ScriptPromiseResolver* resolver : hit_test_promises) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, kDeviceDisconnected)); |
| } |
| |
| HeapHashSet<Member<ScriptPromiseResolver>> create_anchor_promises; |
| create_anchor_promises_.swap(create_anchor_promises); |
| for (ScriptPromiseResolver* resolver : create_anchor_promises) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, kDeviceDisconnected)); |
| } |
| |
| HeapHashSet<Member<ScriptPromiseResolver>> request_hit_test_source_promises; |
| request_hit_test_source_promises_.swap(request_hit_test_source_promises); |
| for (ScriptPromiseResolver* resolver : request_hit_test_source_promises) { |
| resolver->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kInvalidStateError, kDeviceDisconnected)); |
| } |
| } |
| |
| void XRSession::ProcessAnchorsData( |
| const device::mojom::blink::XRAnchorsDataPtr& tracked_anchors_data, |
| double timestamp) { |
| TRACE_EVENT0("xr", __func__); |
| |
| if (!tracked_anchors_data) { |
| DVLOG(3) << __func__ << ": tracked_anchors_data is null"; |
| |
| // We have received a null ptr. Mark tracked_anchors as null & clear stored |
| // anchors. |
| is_tracked_anchors_null_ = true; |
| anchor_ids_to_anchors_.clear(); |
| return; |
| } |
| |
| TRACE_COUNTER2("xr", "Anchor statistics", "All anchors", |
| tracked_anchors_data->all_anchors_ids.size(), |
| "Updated anchors", |
| tracked_anchors_data->updated_anchors_data.size()); |
| |
| DVLOG(3) << __func__ << ": updated anchors size=" |
| << tracked_anchors_data->updated_anchors_data.size() |
| << ", all anchors size=" |
| << tracked_anchors_data->all_anchors_ids.size(); |
| |
| is_tracked_anchors_null_ = false; |
| |
| HeapHashMap<uint32_t, Member<XRAnchor>> updated_anchors; |
| |
| // First, process all planes that had their information updated (new planes |
| // are also processed here). |
| for (const auto& anchor : tracked_anchors_data->updated_anchors_data) { |
| auto it = anchor_ids_to_anchors_.find(anchor->id); |
| if (it != anchor_ids_to_anchors_.end()) { |
| updated_anchors.insert(anchor->id, it->value); |
| it->value->Update(anchor, timestamp); |
| } else { |
| updated_anchors.insert( |
| anchor->id, |
| MakeGarbageCollected<XRAnchor>(anchor->id, this, anchor, timestamp)); |
| } |
| } |
| |
| // Then, copy over the planes that were not updated but are still present. |
| for (const auto& anchor_id : tracked_anchors_data->all_anchors_ids) { |
| auto it_updated = updated_anchors.find(anchor_id); |
| |
| // If the plane was already updated, there is nothing to do as it was |
| // already moved to |updated_anchors|. Otherwise just copy it over as-is. |
| if (it_updated == updated_anchors.end()) { |
| auto it = anchor_ids_to_anchors_.find(anchor_id); |
| DCHECK(it != anchor_ids_to_anchors_.end()); |
| updated_anchors.insert(anchor_id, it->value); |
| } |
| } |
| |
| anchor_ids_to_anchors_.swap(updated_anchors); |
| } |
| |
| void XRSession::CleanUpUnusedHitTestSources() { |
| // Gather all IDs of unused hit test sources. |
| HashSet<uint32_t> unused_hit_test_source_ids; |
| for (auto& subscription_id_and_hit_test_source : |
| hit_test_source_ids_to_hit_test_sources_) { |
| if (!subscription_id_and_hit_test_source.value) { |
| unused_hit_test_source_ids.insert( |
| subscription_id_and_hit_test_source.key); |
| } |
| } |
| |
| // Remove all of the unused hit test sources. |
| hit_test_source_ids_to_hit_test_sources_.RemoveAll( |
| unused_hit_test_source_ids); |
| |
| DVLOG(3) << __func__ << ": removed unused hit test sources, amount: " |
| << unused_hit_test_source_ids.size(); |
| } |
| |
| void XRSession::ProcessHitTestData( |
| const device::mojom::blink::XRHitTestSubscriptionResultsDataPtr& |
| hit_test_subscriptions_data) { |
| DVLOG(2) << __func__; |
| |
| CleanUpUnusedHitTestSources(); |
| |
| if (hit_test_subscriptions_data) { |
| // We have received hit test results for hit test subscriptions - process |
| // each result and notify its corresponding hit test source about new |
| // results for the current frame. |
| for (auto& hit_test_subscription_data : |
| hit_test_subscriptions_data->results) { |
| auto it = hit_test_source_ids_to_hit_test_sources_.find( |
| hit_test_subscription_data->subscription_id); |
| if (it != hit_test_source_ids_to_hit_test_sources_.end()) { |
| it->value->Update(hit_test_subscription_data->hit_test_results); |
| } |
| } |
| } else { |
| // We have not received hit test results for any of the hit test |
| // subscriptions in the current frame - clean up the results on all hit test |
| // source objects. |
| for (auto& subscription_id_and_hit_test_source : |
| hit_test_source_ids_to_hit_test_sources_) { |
| subscription_id_and_hit_test_source.value->Update({}); |
| } |
| } |
| } |
| |
| ScriptPromise XRSession::end(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| // Don't allow a session to end twice. |
| if (ended_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kSessionEnded); |
| return ScriptPromise(); |
| } |
| |
| ForceEnd(); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| // TODO(bajones): If there's any work that needs to be done asynchronously on |
| // session end it should be completed before this promise is resolved. |
| |
| resolver->Resolve(); |
| return promise; |
| } |
| |
| void XRSession::ForceEnd() { |
| // If we've already ended, then just abort. Since this is called only by C++ |
| // code, and predominantly just to ensure that the session is shut down, this |
| // is fine. |
| if (ended_) |
| return; |
| |
| // Detach this session from the XR system. |
| ended_ = true; |
| pending_frame_ = false; |
| |
| for (unsigned i = 0; i < input_sources_->length(); i++) { |
| auto* input_source = (*input_sources_)[i]; |
| input_source->OnRemoved(); |
| } |
| |
| input_sources_ = nullptr; |
| |
| if (canvas_input_provider_) { |
| canvas_input_provider_->Stop(); |
| canvas_input_provider_ = nullptr; |
| } |
| |
| // Notify the frame provider that we've ended |
| xr_->frameProvider()->OnSessionEnded(this); |
| |
| DispatchEvent(*XRSessionEvent::Create(event_type_names::kEnd, this)); |
| } |
| |
| double XRSession::NativeFramebufferScale() const { |
| if (immersive()) { |
| double scale = display_info_->webxr_default_framebuffer_scale; |
| DCHECK(scale); |
| |
| // Return the inverse of the default scale, since that's what we'll need to |
| // multiply the default size by to get back to the native size. |
| return 1.0 / scale; |
| } |
| return 1.0; |
| } |
| |
| DoubleSize XRSession::DefaultFramebufferSize() const { |
| if (!immersive()) { |
| return OutputCanvasSize(); |
| } |
| |
| double scale = display_info_->webxr_default_framebuffer_scale; |
| double width = display_info_->left_eye->render_width; |
| double height = display_info_->left_eye->render_height; |
| |
| if (display_info_->right_eye) { |
| width += display_info_->right_eye->render_width; |
| height = std::max(display_info_->left_eye->render_height, |
| display_info_->right_eye->render_height); |
| } |
| |
| return DoubleSize(width * scale, height * scale); |
| } |
| |
| DoubleSize XRSession::OutputCanvasSize() const { |
| if (!render_state_->output_canvas()) { |
| return DoubleSize(); |
| } |
| |
| return DoubleSize(output_width_, output_height_); |
| } |
| |
| void XRSession::OnFocusChanged() { |
| UpdateVisibilityState(); |
| } |
| |
| void XRSession::OnVisibilityStateChanged(XRVisibilityState visibility_state) { |
| // TODO(crbug.com/1002742): Until some ambiguities in the spec are cleared up, |
| // force "visible-blurred" states from the device to report as "hidden" |
| if (visibility_state == XRVisibilityState::VISIBLE_BLURRED) { |
| visibility_state = XRVisibilityState::HIDDEN; |
| } |
| |
| if (device_visibility_state_ != visibility_state) { |
| device_visibility_state_ = visibility_state; |
| UpdateVisibilityState(); |
| } |
| } |
| |
| // The ultimate visibility state of the session is a combination of the devices |
| // reported visibility state and, for inline sessions, the frame focus, which |
| // will override the device visibility to "hidden" if the frame is not currently |
| // focused. |
| void XRSession::UpdateVisibilityState() { |
| // Don't need to track the visibility state if the session has ended. |
| if (ended_) { |
| return; |
| } |
| |
| XRVisibilityState state = device_visibility_state_; |
| |
| // The WebXR spec requires that if our document is not focused, that we don't |
| // hand out real poses. For immersive sessions, we have to rely on the device |
| // to tell us it's visibility state, as some runtimes (WMR) put focus in the |
| // headset, and thus we cannot rely on Document Focus state. This is fine |
| // because while the runtime reports us as focused the content owned by the |
| // session should be focued, which is owned by the document. For inline, we |
| // can and must rely on frame focus. |
| if (!immersive() && !xr_->IsFrameFocused()) { |
| state = XRVisibilityState::HIDDEN; |
| } |
| |
| if (visibility_state_ != state) { |
| visibility_state_ = state; |
| |
| // If the visibility state was changed to something other than hidden, we |
| // may be able to restart the frame loop. |
| MaybeRequestFrame(); |
| |
| DispatchEvent( |
| *XRSessionEvent::Create(event_type_names::kVisibilitychange, this)); |
| } |
| } |
| |
| void XRSession::MaybeRequestFrame() { |
| bool will_have_base_layer = !!render_state_->baseLayer(); |
| for (const auto& init : pending_render_state_) { |
| if (init->hasBaseLayer()) { |
| will_have_base_layer = !!init->baseLayer(); |
| } |
| } |
| |
| // A page will not be allowed to get frames if its visibility state is hidden. |
| bool page_allowed_frames = visibility_state_ != XRVisibilityState::HIDDEN; |
| |
| // A page is configured properly if it will have a base layer when the frame |
| // callback gets resolved. |
| bool page_configured_properly = will_have_base_layer; |
| |
| // If we have an outstanding callback registered, then we know that the page |
| // actually wants frames. |
| bool page_wants_frame = !callback_collection_->IsEmpty(); |
| |
| // A page can process frames if it has its appropriate base layer set and has |
| // indicated that it actually wants frames. |
| bool page_can_process_frames = page_configured_properly && page_wants_frame; |
| |
| // We consider frames to be throttled if the page is not allowed frames, but |
| // otherwise would be able to receive them. Therefore, if the page isn't in a |
| // state to process frames, it doesn't matter if we are throttling it, any |
| // "stalls" should be attributed to the page being poorly behaved. |
| bool frames_throttled = page_can_process_frames && !page_allowed_frames; |
| |
| // If our throttled state has changed, notify anyone who may care |
| if (frames_throttled_ != frames_throttled) { |
| frames_throttled_ = frames_throttled; |
| xr_->SetFramesThrottled(this, frames_throttled_); |
| } |
| |
| // We can request a frame if we don't have one already pending, the page is |
| // allowed to request frames, and the page is set up to properly handle frames |
| // and wants one. |
| bool request_frame = |
| !pending_frame_ && page_allowed_frames && page_can_process_frames; |
| if (request_frame) { |
| xr_->frameProvider()->RequestFrame(this); |
| pending_frame_ = true; |
| } |
| } |
| |
| void XRSession::DetachOutputCanvas(HTMLCanvasElement* canvas) { |
| if (!canvas) |
| return; |
| |
| // Remove anything in this session observing the given output canvas. |
| if (resize_observer_) { |
| resize_observer_->unobserve(canvas); |
| } |
| |
| if (canvas_input_provider_ && canvas_input_provider_->canvas() == canvas) { |
| canvas_input_provider_->Stop(); |
| canvas_input_provider_ = nullptr; |
| } |
| } |
| |
| void XRSession::ApplyPendingRenderState() { |
| DCHECK(!prev_base_layer_); |
| if (pending_render_state_.size() > 0) { |
| prev_base_layer_ = render_state_->baseLayer(); |
| HTMLCanvasElement* prev_ouput_canvas = render_state_->output_canvas(); |
| update_views_next_frame_ = true; |
| |
| // Loop through each pending render state and apply it to the active one. |
| for (auto& init : pending_render_state_) { |
| render_state_->Update(init); |
| } |
| pending_render_state_.clear(); |
| |
| // If this is an inline session and the base layer has changed, give it an |
| // opportunity to update it's drawing buffer size. |
| if (!immersive() && render_state_->baseLayer() && |
| render_state_->baseLayer() != prev_base_layer_) { |
| render_state_->baseLayer()->OnResize(); |
| } |
| |
| // If the output canvas changed, remove listeners from the old one and add |
| // listeners to the new one as appropriate. |
| if (prev_ouput_canvas != render_state_->output_canvas()) { |
| // Remove anything observing the previous canvas. |
| if (prev_ouput_canvas) { |
| DetachOutputCanvas(prev_ouput_canvas); |
| } |
| |
| // Monitor the new canvas for resize/input events. |
| HTMLCanvasElement* canvas = render_state_->output_canvas(); |
| if (canvas) { |
| if (!resize_observer_) { |
| resize_observer_ = ResizeObserver::Create( |
| canvas->GetDocument(), |
| MakeGarbageCollected<XRSessionResizeObserverDelegate>(this)); |
| } |
| resize_observer_->observe(canvas); |
| |
| // Begin processing input events on the output context's canvas. |
| if (!immersive()) { |
| canvas_input_provider_ = |
| MakeGarbageCollected<XRCanvasInputProvider>(this, canvas); |
| } |
| |
| // Get the new canvas dimensions |
| UpdateCanvasDimensions(canvas); |
| } |
| } |
| } |
| } |
| |
| void XRSession::UpdatePresentationFrameState( |
| double timestamp, |
| std::unique_ptr<TransformationMatrix> mojo_from_viewer, |
| const device::mojom::blink::XRFrameDataPtr& frame_data, |
| bool emulated_position) { |
| TRACE_EVENT0("gpu", __func__); |
| DVLOG(2) << __func__ << " : frame_data valid? " |
| << (frame_data ? true : false); |
| // Don't process any outstanding frames once the session is ended. |
| if (ended_) |
| return; |
| |
| mojo_from_viewer_ = std::move(mojo_from_viewer); |
| DVLOG(2) << __func__ << " : mojo_from_viewer_ valid? " |
| << (mojo_from_viewer_ ? true : false); |
| |
| emulated_position_ = emulated_position; |
| |
| // Update objects that might change on per-frame basis. |
| if (frame_data) { |
| world_information_->ProcessPlaneInformation( |
| frame_data->detected_planes_data, timestamp); |
| ProcessAnchorsData(frame_data->anchors_data, timestamp); |
| ProcessHitTestData(frame_data->hit_test_subscription_results); |
| } else { |
| world_information_->ProcessPlaneInformation(nullptr, timestamp); |
| ProcessAnchorsData(nullptr, timestamp); |
| ProcessHitTestData(nullptr); |
| } |
| } |
| |
| void XRSession::OnFrame( |
| double timestamp, |
| const base::Optional<gpu::MailboxHolder>& output_mailbox_holder) { |
| TRACE_EVENT0("gpu", __func__); |
| DVLOG(2) << __func__; |
| // Don't process any outstanding frames once the session is ended. |
| if (ended_) |
| return; |
| |
| // If there are pending render state changes, apply them now. |
| prev_base_layer_ = nullptr; |
| ApplyPendingRenderState(); |
| |
| if (pending_frame_) { |
| pending_frame_ = false; |
| |
| // Don't allow frames to be processed if there's no layers attached to the |
| // session. That would allow tracking with no associated visuals. |
| XRWebGLLayer* frame_base_layer = render_state_->baseLayer(); |
| if (!frame_base_layer) { |
| // If we previously had a frame base layer, we need to still attempt to |
| // submit a frame back to the runtime, as all "GetFrameData" calls need a |
| // matching submit. |
| if (prev_base_layer_) { |
| prev_base_layer_->OnFrameStart(output_mailbox_holder); |
| prev_base_layer_->OnFrameEnd(); |
| prev_base_layer_ = nullptr; |
| } |
| return; |
| } |
| |
| // Don't allow frames to be processed if an inline session doesn't have an |
| // output canvas. |
| if (!immersive() && !render_state_->output_canvas()) |
| return; |
| |
| frame_base_layer->OnFrameStart(output_mailbox_holder); |
| |
| // Don't allow frames to be processed if the session's visibility state is |
| // "hidden". |
| if (visibility_state_ == XRVisibilityState::HIDDEN) { |
| // If the frame is skipped because of the visibility state, make sure we |
| // end the frame anyway. |
| frame_base_layer->OnFrameEnd(); |
| return; |
| } |
| |
| XRFrame* presentation_frame = CreatePresentationFrame(); |
| presentation_frame->SetAnimationFrame(true); |
| |
| // Make sure that any frame-bounded changed to the views array take effect. |
| if (update_views_next_frame_) { |
| views_dirty_ = true; |
| update_views_next_frame_ = false; |
| } |
| |
| // Resolve the queued requestAnimationFrame callbacks. All XR rendering will |
| // happen within these calls. resolving_frame_ will be true for the duration |
| // of the callbacks. |
| base::AutoReset<bool> resolving(&resolving_frame_, true); |
| callback_collection_->ExecuteCallbacks(this, timestamp, presentation_frame); |
| |
| // The session might have ended in the middle of the frame. Only call |
| // OnFrameEnd if it's still valid. |
| if (!ended_) |
| frame_base_layer->OnFrameEnd(); |
| |
| // Ensure the XRFrame cannot be used outside the callbacks. |
| presentation_frame->Deactivate(); |
| } |
| } |
| |
| void XRSession::LogGetPose() const { |
| Document* doc = To<Document>(GetExecutionContext()); |
| if (!did_log_getViewerPose_ && doc) { |
| did_log_getViewerPose_ = true; |
| |
| ukm::builders::XR_WebXR(xr_->GetSourceId()) |
| .SetDidRequestPose(1) |
| .Record(doc->UkmRecorder()); |
| } |
| } |
| |
| bool XRSession::CanReportPoses() { |
| // The spec has a few requirements for if poses can be reported. |
| // If we have a session, then user intent is understood. Therefore, (due to |
| // the way visibility state is updatd), the rest of the steps really just |
| // boil down to whether or not the XRVisibilityState is Visible. |
| return visibility_state_ == XRVisibilityState::VISIBLE; |
| } |
| |
| XRFrame* XRSession::CreatePresentationFrame() { |
| DVLOG(2) << __func__; |
| |
| XRFrame* presentation_frame = |
| MakeGarbageCollected<XRFrame>(this, world_information_); |
| |
| // TODO(https://crbug.com/1004201): Determine if world_information_ should be |
| // treated similarly to mojo_from_viewer_. |
| if (mojo_from_viewer_ && visibility_state_ != XRVisibilityState::HIDDEN) { |
| DVLOG(2) << __func__ << " : mojo_from_viewer_ is set and not hidden," |
| << " updating presentation frame"; |
| |
| presentation_frame->SetMojoFromViewer(*mojo_from_viewer_, |
| EmulatedPosition()); |
| } |
| return presentation_frame; |
| } |
| |
| // Called when the canvas element for this session's output context is resized. |
| void XRSession::UpdateCanvasDimensions(Element* element) { |
| DCHECK(element); |
| |
| double devicePixelRatio = 1.0; |
| LocalFrame* frame = xr_->GetFrame(); |
| if (frame) { |
| devicePixelRatio = frame->DevicePixelRatio(); |
| } |
| |
| update_views_next_frame_ = true; |
| output_width_ = element->OffsetWidth() * devicePixelRatio; |
| output_height_ = element->OffsetHeight() * devicePixelRatio; |
| int output_angle = 0; |
| |
| // TODO(crbug.com/836948): handle square canvases. |
| // TODO(crbug.com/840346): we should not need to use ScreenOrientation here. |
| ScreenOrientation* orientation = ScreenOrientation::Create(frame); |
| |
| if (orientation) { |
| output_angle = orientation->angle(); |
| DVLOG(2) << __func__ << ": got angle=" << output_angle; |
| } |
| |
| if (render_state_->baseLayer()) { |
| render_state_->baseLayer()->OnResize(); |
| } |
| } |
| |
| void XRSession::OnButtonEvent( |
| device::mojom::blink::XRInputSourceStatePtr input_state) { |
| DCHECK(uses_input_eventing_); |
| OnInputStateChangeInternal(last_frame_id_, base::make_span(&input_state, 1), |
| true /* from_eventing */); |
| } |
| |
| void XRSession::OnInputStateChange( |
| int16_t frame_id, |
| base::span<const device::mojom::blink::XRInputSourceStatePtr> |
| input_states) { |
| OnInputStateChangeInternal(frame_id, input_states, false /* from_eventing */); |
| } |
| |
| void XRSession::OnInputStateChangeInternal( |
| int16_t frame_id, |
| base::span<const device::mojom::blink::XRInputSourceStatePtr> input_states, |
| bool from_eventing) { |
| // If we're in any state other than visible, input should not be processed |
| if (visibility_state_ != XRVisibilityState::VISIBLE) { |
| return; |
| } |
| |
| HeapVector<Member<XRInputSource>> added; |
| HeapVector<Member<XRInputSource>> removed; |
| last_frame_id_ = frame_id; |
| |
| // Build up our added array, and update the frame id of any active input |
| // sources so we can flag the ones that are no longer active. |
| for (const auto& input_state : input_states) { |
| XRInputSource* stored_input_source = |
| input_sources_->GetWithSourceId(input_state->source_id); |
| XRInputSource* input_source = XRInputSource::CreateOrUpdateFrom( |
| stored_input_source, this, input_state); |
| |
| // Using pointer equality to determine if the pointer needs to be set. |
| if (stored_input_source != input_source) { |
| input_sources_->SetWithSourceId(input_state->source_id, input_source); |
| added.push_back(input_source); |
| |
| // If we previously had a stored_input_source, disconnect it's gamepad |
| // and mark that it was removed. |
| if (stored_input_source) { |
| stored_input_source->SetGamepadConnected(false); |
| removed.push_back(stored_input_source); |
| } |
| } |
| |
| input_source->setActiveFrameId(frame_id); |
| } |
| |
| // Remove any input sources that are inactive, and disconnect their gamepad. |
| // Note that this is done in two passes because HeapHashMap makes no |
| // guarantees about iterators on removal. |
| // We use a separate array of inactive sources here rather than just |
| // processing removed, because if we replaced any input sources, they would |
| // also be in removed, and we'd remove our newly added source. |
| Vector<uint32_t> inactive_sources; |
| for (unsigned i = 0; i < input_sources_->length(); i++) { |
| auto* input_source = (*input_sources_)[i]; |
| if (input_source->activeFrameId() != frame_id) { |
| inactive_sources.push_back(input_source->source_id()); |
| input_source->OnRemoved(); |
| removed.push_back(input_source); |
| } |
| } |
| |
| for (uint32_t source_id : inactive_sources) { |
| input_sources_->RemoveWithSourceId(source_id); |
| } |
| |
| // If there have been any changes, fire the input sources change event. |
| if (!added.IsEmpty() || !removed.IsEmpty()) { |
| DispatchEvent(*XRInputSourcesChangeEvent::Create( |
| event_type_names::kInputsourceschange, this, added, removed)); |
| } |
| |
| // Now that we've fired the input sources change event (if needed), update |
| // and fire events for any select state changes. |
| for (const auto& input_state : input_states) { |
| // If anything during the process of updating the select state caused us |
| // to end our session, we should stop processing select state updates. |
| if (ended_) |
| break; |
| |
| // If this data is not from eventing and we support it, ignore it's click |
| // states. |
| // If this data is from eventing, but we don't support it, then ignore it |
| if (from_eventing != uses_input_eventing_) |
| continue; |
| |
| XRInputSource* input_source = |
| input_sources_->GetWithSourceId(input_state->source_id); |
| DCHECK(input_source); |
| input_source->UpdateSelectState(input_state); |
| } |
| } |
| |
| void XRSession::AddTransientInputSource(XRInputSource* input_source) { |
| if (ended_) |
| return; |
| |
| // Ensure we're not overriding an input source that's already present. |
| DCHECK(!input_sources_->GetWithSourceId(input_source->source_id())); |
| input_sources_->SetWithSourceId(input_source->source_id(), input_source); |
| |
| DispatchEvent(*XRInputSourcesChangeEvent::Create( |
| event_type_names::kInputsourceschange, this, {input_source}, {})); |
| } |
| |
| void XRSession::RemoveTransientInputSource(XRInputSource* input_source) { |
| if (ended_) |
| return; |
| |
| input_sources_->RemoveWithSourceId(input_source->source_id()); |
| |
| DispatchEvent(*XRInputSourcesChangeEvent::Create( |
| event_type_names::kInputsourceschange, this, {}, {input_source})); |
| } |
| |
| void XRSession::OnPoseReset() { |
| for (const auto& reference_space : reference_spaces_) { |
| reference_space->OnReset(); |
| } |
| } |
| |
| void XRSession::OnChanged(device::mojom::blink::VRDisplayInfoPtr display_info) { |
| DCHECK(display_info); |
| SetXRDisplayInfo(std::move(display_info)); |
| } |
| |
| void XRSession::OnExitPresent() { |
| if (immersive()) { |
| ForceEnd(); |
| } |
| } |
| |
| bool XRSession::ValidateHitTestSourceExists(XRHitTestSource* hit_test_source) { |
| auto it = |
| hit_test_source_ids_to_hit_test_sources_.find(hit_test_source->id()); |
| if (it == hit_test_source_ids_to_hit_test_sources_.end()) { |
| return false; |
| } |
| |
| if (!it->value) { |
| hit_test_source_ids_to_hit_test_sources_.erase(it); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void XRSession::SetXRDisplayInfo( |
| device::mojom::blink::VRDisplayInfoPtr display_info) { |
| // We don't necessarily trust the backend to only send us display info changes |
| // when something has actually changed, and a change here can trigger several |
| // other interfaces to recompute data or fire events, so it's worthwhile to |
| // validate that an actual change has occurred. |
| if (display_info_) { |
| if (display_info_->Equals(*display_info)) |
| return; |
| |
| if (display_info_->stage_parameters && display_info->stage_parameters && |
| !display_info_->stage_parameters->Equals( |
| *(display_info->stage_parameters))) { |
| // Stage parameters changed. |
| stage_parameters_id_++; |
| } else if (!!(display_info_->stage_parameters) != |
| !!(display_info->stage_parameters)) { |
| // Either stage parameters just became available (sometimes happens if |
| // detecting the bounds doesn't happen until a few seconds into the |
| // session for platforms such as WMR), or the stage parameters just went |
| // away (probably due to tracking loss). |
| stage_parameters_id_++; |
| } |
| } else if (display_info && display_info->stage_parameters) { |
| // Got stage parameters for the first time this session. |
| stage_parameters_id_++; |
| } |
| |
| display_info_id_++; |
| display_info_ = std::move(display_info); |
| is_external_ = display_info_->capabilities->has_external_display; |
| } |
| |
| WTF::Vector<XRViewData>& XRSession::views() { |
| // TODO(bajones): For now we assume that immersive sessions render a stereo |
| // pair of views and non-immersive sessions render a single view. That doesn't |
| // always hold true, however, so the view configuration should ultimately come |
| // from the backing service. |
| if (views_dirty_) { |
| if (immersive()) { |
| // If we don't already have the views allocated, do so now. |
| if (views_.IsEmpty()) { |
| views_.emplace_back(XRView::kEyeLeft); |
| if (display_info_->right_eye) { |
| views_.emplace_back(XRView::kEyeRight); |
| } |
| } |
| // In immersive mode the projection and view matrices must be aligned with |
| // the device's physical optics. |
| UpdateViewFromEyeParameters( |
| views_[kMonoOrStereoLeftView], display_info_->left_eye, |
| render_state_->depthNear(), render_state_->depthFar()); |
| if (display_info_->right_eye) { |
| UpdateViewFromEyeParameters( |
| views_[kStereoRightView], display_info_->right_eye, |
| render_state_->depthNear(), render_state_->depthFar()); |
| } |
| } else { |
| if (views_.IsEmpty()) { |
| views_.emplace_back(XRView::kEyeNone); |
| } |
| |
| float aspect = 1.0f; |
| if (output_width_ && output_height_) { |
| aspect = static_cast<float>(output_width_) / |
| static_cast<float>(output_height_); |
| } |
| |
| // In non-immersive mode, if there is no explicit projection matrix |
| // provided, the projection matrix must be aligned with the |
| // output canvas dimensions. |
| bool is_null = true; |
| double inline_vertical_fov = |
| render_state_->inlineVerticalFieldOfView(is_null); |
| |
| // inlineVerticalFieldOfView should only be null in immersive mode. |
| DCHECK(!is_null); |
| views_[kMonoOrStereoLeftView].UpdateProjectionMatrixFromAspect( |
| inline_vertical_fov, aspect, render_state_->depthNear(), |
| render_state_->depthFar()); |
| } |
| |
| views_dirty_ = false; |
| } |
| |
| return views_; |
| } |
| |
| bool XRSession::HasPendingActivity() const { |
| return !callback_collection_->IsEmpty() && !ended_; |
| } |
| |
| void XRSession::Trace(blink::Visitor* visitor) { |
| visitor->Trace(xr_); |
| visitor->Trace(render_state_); |
| visitor->Trace(world_tracking_state_); |
| visitor->Trace(world_information_); |
| visitor->Trace(pending_render_state_); |
| visitor->Trace(input_sources_); |
| visitor->Trace(resize_observer_); |
| visitor->Trace(canvas_input_provider_); |
| visitor->Trace(callback_collection_); |
| visitor->Trace(hit_test_promises_); |
| visitor->Trace(create_anchor_promises_); |
| visitor->Trace(request_hit_test_source_promises_); |
| visitor->Trace(reference_spaces_); |
| visitor->Trace(anchor_ids_to_anchors_); |
| visitor->Trace(prev_base_layer_); |
| visitor->Trace(hit_test_source_ids_to_hit_test_sources_); |
| EventTargetWithInlineData::Trace(visitor); |
| } |
| |
| } // namespace blink |