blob: b6577892a7b33c52bd60c54365c9a7e9bd486cf0 [file] [log] [blame]
// 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_session.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "base/types/pass_key.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/bindings/modules/v8/v8_xr_hit_test_options_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_image_tracking_result.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_render_state_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_transient_input_hit_test_options_init.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_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/probe/async_task_context.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/xr/type_converters.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_camera.h"
#include "third_party/blink/renderer/modules/xr/xr_canvas_input_provider.h"
#include "third_party/blink/renderer/modules/xr/xr_cube_map.h"
#include "third_party/blink/renderer/modules/xr/xr_depth_information.h"
#include "third_party/blink/renderer/modules/xr/xr_depth_manager.h"
#include "third_party/blink/renderer/modules/xr/xr_dom_overlay_state.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_test_source.h"
#include "third_party/blink/renderer/modules/xr/xr_image_tracking_result.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_light_probe.h"
#include "third_party/blink/renderer/modules/xr/xr_plane_manager.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_session_event.h"
#include "third_party/blink/renderer/modules/xr/xr_session_viewport_scaler.h"
#include "third_party/blink/renderer/modules/xr/xr_system.h"
#include "third_party/blink/renderer/modules/xr/xr_transient_input_hit_test_source.h"
#include "third_party/blink/renderer/modules/xr/xr_utils.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/platform/bindings/enumeration_base.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/text/string_operators.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/transform.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 kBaseLayerAndLayers[] =
"Both baseLayer and layers should not be set at the same time when "
"updating render state.";
const char kMultiLayersNotEnabled[] =
"This session does not support multiple layers.";
const char kDuplicateLayer[] = "All layers in render state must be unique.";
const char kInlineVerticalFOVNotSupported[] =
"This session does not support inlineVerticalFieldOfView";
const char kFeatureNotSupportedByDevicePrefix[] =
"Device does not support feature ";
const char kFeatureNotSupportedBySessionPrefix[] =
"Session does not support feature ";
const char kDeviceDisconnected[] = "The XR device has been disconnected.";
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 char kAnchorCreationFailed[] = "Anchor creation failed.";
const char kEntityTypesNotSpecified[] =
"No entityTypes specified: the array cannot be empty!";
const char kSessionNotHaveSetFrameRate[] =
"Session does not have a set frame rate.";
const float kMinDefaultFramebufferScale = 0.1f;
const float kMaxDefaultFramebufferScale = 1.0f;
// Indices into the views array.
const unsigned int kMonoView = 0;
// Returns the session feature corresponding to the given reference space type.
absl::optional<device::mojom::XRSessionFeature> MapReferenceSpaceTypeToFeature(
device::mojom::blink::XRReferenceSpaceType type) {
switch (type) {
case device::mojom::blink::XRReferenceSpaceType::kViewer:
return device::mojom::XRSessionFeature::REF_SPACE_VIEWER;
case device::mojom::blink::XRReferenceSpaceType::kLocal:
return device::mojom::XRSessionFeature::REF_SPACE_LOCAL;
case device::mojom::blink::XRReferenceSpaceType::kLocalFloor:
return device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR;
case device::mojom::blink::XRReferenceSpaceType::kBoundedFloor:
return device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR;
case device::mojom::blink::XRReferenceSpaceType::kUnbounded:
return device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED;
}
NOTREACHED();
return absl::nullopt;
}
std::unique_ptr<gfx::Transform> getPoseMatrix(
const device::mojom::blink::VRPosePtr& pose) {
if (!pose)
return nullptr;
device::Pose device_pose =
device::Pose(pose->position.value_or(gfx::Point3F()),
pose->orientation.value_or(gfx::Quaternion()));
return std::make_unique<gfx::Transform>(device_pose.ToTransform());
}
absl::optional<device::mojom::blink::EntityTypeForHitTest>
EntityTypeForHitTestFromString(const String& string) {
if (string == "plane")
return device::mojom::blink::EntityTypeForHitTest::PLANE;
if (string == "point")
return device::mojom::blink::EntityTypeForHitTest::POINT;
NOTREACHED();
return absl::nullopt;
}
// Returns a vector of entity types from hit test options, without duplicates.
// OptionsType can be either XRHitTestOptionsInit or
// XRTransientInputHitTestOptionsInit.
template <typename OptionsType>
Vector<device::mojom::blink::EntityTypeForHitTest> GetEntityTypesForHitTest(
OptionsType* options_init) {
DCHECK(options_init);
HashSet<device::mojom::blink::EntityTypeForHitTest> result_set;
if (RuntimeEnabledFeatures::WebXRHitTestEntityTypesEnabled() &&
options_init->hasEntityTypes()) {
DVLOG(2) << __func__ << ": options_init->entityTypes().size()="
<< options_init->entityTypes().size();
for (const auto& entity_type_string : options_init->entityTypes()) {
auto maybe_entity_type =
EntityTypeForHitTestFromString(entity_type_string);
if (maybe_entity_type) {
result_set.insert(*maybe_entity_type);
} else {
DVLOG(1) << __func__ << ": entityTypes entry ignored:"
<< IDLEnumAsString(entity_type_string);
}
}
} else {
result_set.insert(device::mojom::blink::EntityTypeForHitTest::PLANE);
}
DVLOG(2) << __func__ << ": result_set.size()=" << result_set.size();
DCHECK(!result_set.empty());
Vector<device::mojom::blink::EntityTypeForHitTest> result(result_set);
DVLOG(2) << __func__ << ": result.size()=" << result.size();
return result;
}
template <typename T>
HashSet<uint64_t> GetIdsOfUnusedHitTestSources(
const HeapHashMap<uint64_t, WeakMember<T>>& id_to_hit_test_source,
const HashSet<uint64_t>& all_ids) {
// Gather all IDs of unused hit test sources:
HashSet<uint64_t> unused_hit_test_source_ids;
for (auto& id : all_ids) {
if (!base::Contains(id_to_hit_test_source, id)) {
unused_hit_test_source_ids.insert(id);
}
}
return unused_hit_test_source_ids;
}
} // namespace
#define DCHECK_HIT_TEST_SOURCES() \
do { \
DCHECK_EQ(hit_test_source_ids_.size(), \
hit_test_source_ids_to_hit_test_sources_.size()); \
DCHECK_EQ( \
hit_test_source_for_transient_input_ids_.size(), \
hit_test_source_ids_to_transient_input_hit_test_sources_.size()); \
} while (0)
constexpr char XRSession::kNoRigidTransformSpecified[];
constexpr char XRSession::kUnableToRetrieveMatrix[];
constexpr char XRSession::kNoSpaceSpecified[];
constexpr char XRSession::kAnchorsFeatureNotSupported[];
constexpr char XRSession::kPlanesFeatureNotSupported[];
constexpr char XRSession::kDepthSensingFeatureNotSupported[];
constexpr char XRSession::kRawCameraAccessFeatureNotSupported[];
constexpr char XRSession::kCannotCancelHitTestSource[];
constexpr char XRSession::kCannotReportPoses[];
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(Visitor* visitor) const override {
visitor->Trace(session_);
ResizeObserver::Delegate::Trace(visitor);
}
private:
Member<XRSession> session_;
};
XRSession::MetricsReporter::MetricsReporter(
mojo::Remote<device::mojom::blink::XRSessionMetricsRecorder> recorder)
: recorder_(std::move(recorder)) {}
void XRSession::MetricsReporter::ReportFeatureUsed(
device::mojom::blink::XRSessionFeature feature) {
using device::mojom::blink::XRSessionFeature;
// If we've already reported using this feature, no need to report again.
if (!reported_features_.insert(feature).is_new_entry) {
return;
}
switch (feature) {
case XRSessionFeature::REF_SPACE_VIEWER:
recorder_->ReportFeatureUsed(XRSessionFeature::REF_SPACE_VIEWER);
break;
case XRSessionFeature::REF_SPACE_LOCAL:
recorder_->ReportFeatureUsed(XRSessionFeature::REF_SPACE_LOCAL);
break;
case XRSessionFeature::REF_SPACE_LOCAL_FLOOR:
recorder_->ReportFeatureUsed(XRSessionFeature::REF_SPACE_LOCAL_FLOOR);
break;
case XRSessionFeature::REF_SPACE_BOUNDED_FLOOR:
recorder_->ReportFeatureUsed(XRSessionFeature::REF_SPACE_BOUNDED_FLOOR);
break;
case XRSessionFeature::REF_SPACE_UNBOUNDED:
recorder_->ReportFeatureUsed(XRSessionFeature::REF_SPACE_UNBOUNDED);
break;
case XRSessionFeature::DOM_OVERLAY:
case XRSessionFeature::HIT_TEST:
case XRSessionFeature::LIGHT_ESTIMATION:
case XRSessionFeature::ANCHORS:
case XRSessionFeature::CAMERA_ACCESS:
case XRSessionFeature::PLANE_DETECTION:
case XRSessionFeature::DEPTH:
case XRSessionFeature::IMAGE_TRACKING:
case XRSessionFeature::HAND_INPUT:
case XRSessionFeature::SECONDARY_VIEWS:
case XRSessionFeature::LAYERS:
case XRSessionFeature::FRONT_FACING:
// Not recording metrics for these features currently.
break;
}
}
XRDepthManager* XRSession::CreateDepthManagerIfEnabled(
const XRSessionFeatureSet& feature_set,
const device::mojom::blink::XRSessionDeviceConfig& device_config) {
DVLOG(2) << __func__;
if (!base::Contains(feature_set, device::mojom::XRSessionFeature::DEPTH)) {
return nullptr;
}
if (!device_config.depth_configuration) {
DCHECK(false) << "The session reports that depth sensing is supported but "
"did not report depth sensing API configuration!";
return nullptr;
}
return MakeGarbageCollected<XRDepthManager>(
base::PassKey<XRSession>{}, this, *device_config.depth_configuration);
}
XRSession::XRSession(
XRSystem* xr,
mojo::PendingReceiver<device::mojom::blink::XRSessionClient>
client_receiver,
device::mojom::blink::XRSessionMode mode,
device::mojom::blink::XREnvironmentBlendMode environment_blend_mode,
device::mojom::blink::XRInteractionMode interaction_mode,
device::mojom::blink::XRSessionDeviceConfigPtr device_config,
bool sensorless_session,
XRSessionFeatureSet enabled_features)
: ActiveScriptWrappable<XRSession>({}),
xr_(xr),
mode_(mode),
environment_integration_(
mode == device::mojom::blink::XRSessionMode::kImmersiveAr),
enabled_features_(std::move(enabled_features)),
plane_manager_(
MakeGarbageCollected<XRPlaneManager>(base::PassKey<XRSession>{},
this)),
depth_manager_(
CreateDepthManagerIfEnabled(enabled_features_, *device_config)),
input_sources_(MakeGarbageCollected<XRInputSourceArray>()),
client_receiver_(this, xr->GetExecutionContext()),
input_receiver_(this, xr->GetExecutionContext()),
callback_collection_(
MakeGarbageCollected<XRFrameRequestCallbackCollection>(
xr->GetExecutionContext())),
uses_input_eventing_(device_config->uses_input_eventing),
supports_viewport_scaling_(immersive() &&
device_config->supports_viewport_scaling),
enable_anti_aliasing_(device_config->enable_anti_aliasing),
sensorless_session_(sensorless_session) {
client_receiver_.Bind(
std::move(client_receiver),
xr->GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI));
render_state_ = MakeGarbageCollected<XRRenderState>(immersive());
// Ensure that frame focus is considered in the initial visibilityState.
UpdateVisibilityState();
// Clamp to a reasonable min/max size for the default framebuffer scale.
recommended_framebuffer_scale_ =
std::clamp(device_config->default_framebuffer_scale,
kMinDefaultFramebufferScale, kMaxDefaultFramebufferScale);
UpdateViews(device_config->views);
DVLOG(2) << __func__
<< ": supports_viewport_scaling_=" << supports_viewport_scaling_;
switch (environment_blend_mode) {
case device::mojom::blink::XREnvironmentBlendMode::kOpaque:
blend_mode_string_ = "opaque";
break;
case device::mojom::blink::XREnvironmentBlendMode::kAdditive:
blend_mode_string_ = "additive";
break;
case device::mojom::blink::XREnvironmentBlendMode::kAlphaBlend:
blend_mode_string_ = "alpha-blend";
break;
default:
NOTREACHED() << "Unknown environment blend mode: "
<< environment_blend_mode;
}
switch (interaction_mode) {
case device::mojom::blink::XRInteractionMode::kScreenSpace:
interaction_mode_string_ = "screen-space";
break;
case device::mojom::blink::XRInteractionMode::kWorldSpace:
interaction_mode_string_ = "world-space";
break;
}
}
void XRSession::SetDOMOverlayElement(Element* element) {
DVLOG(2) << __func__ << ": element=" << element;
DCHECK(
enabled_features_.Contains(device::mojom::XRSessionFeature::DOM_OVERLAY));
DCHECK(element);
overlay_element_ = element;
// Set up the domOverlayState attribute. This could be done lazily on first
// access, but it's a tiny object and it's unclear if the memory that might
// save during XR sessions is worth the code size increase to do so. This
// should be revisited if the state gets more complex in the future.
//
// At this time, "screen" is the only supported DOM Overlay type.
dom_overlay_state_ = MakeGarbageCollected<XRDOMOverlayState>(
XRDOMOverlayState::DOMOverlayType::kScreen);
}
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";
}
}
Vector<String> XRSession::enabledFeatures() const {
Vector<String> enabled_features;
for (const auto& feature : enabled_features_) {
enabled_features.push_back(XRSessionFeatureToString(feature));
}
return enabled_features;
}
XRAnchorSet* XRSession::TrackedAnchors() const {
DVLOG(3) << __func__;
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::ANCHORS)) {
return MakeGarbageCollected<XRAnchorSet>(HeapHashSet<Member<XRAnchor>>{});
}
HeapHashSet<Member<XRAnchor>> result;
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_ == device::mojom::blink::XRSessionMode::kImmersiveVr ||
mode_ == device::mojom::blink::XRSessionMode::kImmersiveAr;
}
ExecutionContext* XRSession::GetExecutionContext() const {
return xr_->GetExecutionContext();
}
const AtomicString& XRSession::InterfaceName() const {
return event_target_names::kXRSession;
}
mojo::PendingAssociatedRemote<device::mojom::blink::XRInputSourceButtonListener>
XRSession::GetInputClickListener() {
DCHECK(!input_receiver_.is_bound());
return input_receiver_.BindNewEndpointAndPassRemote(
xr_->GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI));
}
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;
}
if (RuntimeEnabledFeatures::WebXRLayersEnabled() && init->hasLayers() &&
init->layers() && !init->layers()->empty()) {
// Validate that we don't have both layers and baseLayer set.
if (init->hasBaseLayer() && init->baseLayer()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
kBaseLayerAndLayers);
return;
}
// Validate that the session was created with the layers feature enabled
// when the user wishes to render multiple layers at once.
if (init->layers()->size() > 1 &&
!IsFeatureEnabled(device::mojom::XRSessionFeature::LAYERS)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
kMultiLayersNotEnabled);
return;
}
HeapHashSet<Member<const XRLayer>> unique_layers;
for (const XRLayer* layer : *init->layers()) {
// Check for duplicate layers.
if (!unique_layers.insert(layer).is_new_entry) {
exception_state.ThrowException(ToExceptionCode(ESErrorType::kTypeError),
kDuplicateLayer);
return;
}
// Validate that all layers were created with this session.
if (layer->session() != this) {
exception_state.ThrowException(ToExceptionCode(ESErrorType::kTypeError),
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();
}
const String& XRSession::depthUsage(ExceptionState& exception_state) {
if (!depth_manager_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kDepthSensingFeatureNotSupported);
return g_empty_string;
}
return depth_manager_->depthUsage();
}
const String& XRSession::depthDataFormat(ExceptionState& exception_state) {
if (!depth_manager_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kDepthSensingFeatureNotSupported);
return g_empty_string;
}
return depth_manager_->depthDataFormat();
}
void XRSession::UpdateViews(
const Vector<device::mojom::blink::XRViewPtr>& views) {
if (views.empty()) {
// If there are no views provided for this frame, keep the views we
// currently have from the previous frame.
return;
}
if (pending_views_.size() != views.size()) {
pending_views_.resize(views.size());
}
for (wtf_size_t i = 0; i < views.size(); i++) {
pending_views_[i] = views[i].Clone();
}
}
void XRSession::UpdateStageParameters(
uint32_t stage_parameters_id,
const device::mojom::blink::VRStageParametersPtr& stage_parameters) {
// Only update if the ID is different, indicating a change.
if (stage_parameters_id_ != stage_parameters_id) {
stage_parameters_id_ = stage_parameters_id;
stage_parameters_ = stage_parameters.Clone();
}
}
ScriptPromise XRSession::updateTargetFrameRate(float rate,
ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionNotHaveSetFrameRate);
return ScriptPromise();
}
ScriptPromise XRSession::requestReferenceSpace(
ScriptState* script_state,
const String& type,
ExceptionState& exception_state) {
DVLOG(2) << __func__ << ": type=" << type;
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return ScriptPromise();
}
device::mojom::blink::XRReferenceSpaceType requested_type =
XRReferenceSpace::StringToReferenceSpaceType(type);
if (sensorless_session_ &&
requested_type != device::mojom::blink::XRReferenceSpaceType::kViewer) {
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) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
kReferenceSpaceNotSupported);
return ScriptPromise();
}
// Report attempt to use this feature
if (metrics_reporter_) {
metrics_reporter_->ReportFeatureUsed(type_as_feature.value());
}
if (!IsFeatureEnabled(type_as_feature.value())) {
DVLOG(2) << __func__ << ": feature not enabled, type=" << type;
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
kReferenceSpaceNotSupported);
return ScriptPromise();
}
XRReferenceSpace* reference_space = nullptr;
switch (requested_type) {
case device::mojom::blink::XRReferenceSpaceType::kViewer:
case device::mojom::blink::XRReferenceSpaceType::kLocal:
case device::mojom::blink::XRReferenceSpaceType::kLocalFloor:
reference_space =
MakeGarbageCollected<XRReferenceSpace>(this, requested_type);
break;
case device::mojom::blink::XRReferenceSpaceType::kBoundedFloor: {
if (immersive()) {
reference_space = MakeGarbageCollected<XRBoundedReferenceSpace>(this);
}
break;
}
case device::mojom::blink::XRReferenceSpaceType::kUnbounded:
if (immersive()) {
reference_space =
MakeGarbageCollected<XRReferenceSpace>(this, requested_type);
}
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, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
resolver->Resolve(reference_space);
return promise;
}
ScriptPromise XRSession::CreateAnchorHelper(
ScriptState* script_state,
const gfx::Transform& native_origin_from_anchor,
const device::mojom::blink::XRNativeOriginInformationPtr&
native_origin_information,
absl::optional<uint64_t> maybe_plane_id,
ExceptionState& exception_state) {
DVLOG(2) << __func__;
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return ScriptPromise();
}
// Reject the promise if device doesn't support the anchors API.
if (!xr_->xrEnvironmentProviderRemote()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
kFeatureNotSupportedByDevicePrefix +
XRSessionFeatureToString(device::mojom::XRSessionFeature::ANCHORS));
return ScriptPromise();
}
auto maybe_native_origin_from_anchor_pose =
CreatePose(native_origin_from_anchor);
if (!maybe_native_origin_from_anchor_pose) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kUnableToDecomposeMatrix);
return ScriptPromise();
}
DVLOG(3) << __func__
<< ": maybe_native_origin_from_anchor_pose->orientation()= "
<< maybe_native_origin_from_anchor_pose->orientation().ToString()
<< ", maybe_native_origin_from_anchor_pose->position()= "
<< maybe_native_origin_from_anchor_pose->position().ToString();
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
if (maybe_plane_id) {
xr_->xrEnvironmentProviderRemote()->CreatePlaneAnchor(
native_origin_information->Clone(),
*maybe_native_origin_from_anchor_pose, *maybe_plane_id,
resolver->WrapCallbackInScriptScope(WTF::BindOnce(
&XRSession::OnCreateAnchorResult, WrapPersistent(this))));
} else {
xr_->xrEnvironmentProviderRemote()->CreateAnchor(
native_origin_information->Clone(),
*maybe_native_origin_from_anchor_pose,
resolver->WrapCallbackInScriptScope(WTF::BindOnce(
&XRSession::OnCreateAnchorResult, WrapPersistent(this))));
}
create_anchor_promises_.insert(resolver);
return promise;
}
absl::optional<XRSession::ReferenceSpaceInformation>
XRSession::GetStationaryReferenceSpace() const {
// For anchor creation, we should first attempt to use the local space as it
// is supposed to be more stable, but if that is unavailable, we can try using
// unbounded space. Otherwise, there's not much we can do & we have to return
// nullopt.
// Try to get mojo_from_local:
auto reference_space_type = device::mojom::XRReferenceSpaceType::kLocal;
auto mojo_from_space = GetMojoFrom(reference_space_type);
if (!mojo_from_space) {
// Local space is not available, try to get mojo_from_unbounded:
reference_space_type = device::mojom::XRReferenceSpaceType::kUnbounded;
mojo_from_space = GetMojoFrom(reference_space_type);
}
if (!mojo_from_space) {
// Unbounded is also not available.
return absl::nullopt;
}
ReferenceSpaceInformation result;
result.mojo_from_space = *mojo_from_space;
result.native_origin =
device::mojom::blink::XRNativeOriginInformation::NewReferenceSpaceType(
reference_space_type);
return result;
}
void XRSession::ScheduleVideoFrameCallbacksExecution(
ExecuteVfcCallback execute_vfc_callback) {
vfc_execution_queue_.push_back(std::move(execute_vfc_callback));
MaybeRequestFrame();
}
void XRSession::ExecuteVideoFrameCallbacks(double timestamp) {
Vector<ExecuteVfcCallback> execute_vfc_callbacks;
vfc_execution_queue_.swap(execute_vfc_callbacks);
for (auto& callback : execute_vfc_callbacks)
std::move(callback).Run(timestamp);
}
int XRSession::requestAnimationFrame(V8XRFrameRequestCallback* callback) {
DVLOG(3) << __func__;
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(ScriptState* script_state) const {
if (!did_log_getInputSources_ && script_state->ContextIsValid()) {
ukm::builders::XR_WebXR(GetExecutionContext()->UkmSourceID())
.SetDidGetXRInputSources(1)
.Record(LocalDOMWindow::From(script_state)->UkmRecorder());
did_log_getInputSources_ = true;
}
return input_sources_;
}
ScriptPromise XRSession::requestHitTestSource(
ScriptState* script_state,
XRHitTestOptionsInit* options_init,
ExceptionState& exception_state) {
DVLOG(2) << __func__;
DCHECK(options_init);
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::HIT_TEST)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kFeatureNotSupportedBySessionPrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::HIT_TEST));
return {};
}
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return {};
}
if (!xr_->xrEnvironmentProviderRemote()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
kFeatureNotSupportedByDevicePrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::HIT_TEST));
return {};
}
// 1. Grab the native origin from the passed in XRSpace.
device::mojom::blink::XRNativeOriginInformationPtr maybe_native_origin =
options_init && options_init->hasSpace()
? options_init->space()->NativeOrigin()
: nullptr;
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).
// Null checks not needed since native origin wouldn't be set if options_init
// or space() were null.
gfx::Transform native_from_offset =
options_init->space()->NativeFromOffsetMatrix();
if (RuntimeEnabledFeatures::WebXRHitTestEntityTypesEnabled() &&
options_init->hasEntityTypes() && options_init->entityTypes().empty()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kEntityTypesNotSpecified);
return {};
}
auto entity_types = GetEntityTypesForHitTest(options_init);
DVLOG(3) << __func__
<< ": native_from_offset = " << native_from_offset.ToString();
// Transformation from passed in pose to |space|.
XRRay* offsetRay = options_init && options_init->hasOffsetRay()
? options_init->offsetRay()
: MakeGarbageCollected<XRRay>();
auto space_from_ray = offsetRay->RawMatrix();
auto origin_from_ray = native_from_offset * space_from_ray;
DVLOG(3) << __func__ << ": space_from_ray = " << space_from_ray.ToString();
DVLOG(3) << __func__ << ": origin_from_ray = " << origin_from_ray.ToString();
device::mojom::blink::XRRayPtr ray_mojo = device::mojom::blink::XRRay::New();
ray_mojo->origin = origin_from_ray.MapPoint({0, 0, 0});
// Zero out the translation of origin_from_ray matrix to correctly map a 3D
// vector.
gfx::Vector3dF translation = origin_from_ray.To3dTranslation();
origin_from_ray.Translate3d(-translation.x(), -translation.y(),
-translation.z());
ray_mojo->direction = origin_from_ray.MapPoint({0, 0, -1}).OffsetFromOrigin();
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
xr_->xrEnvironmentProviderRemote()->SubscribeToHitTest(
maybe_native_origin->Clone(), entity_types, std::move(ray_mojo),
resolver->WrapCallbackInScriptScope(WTF::BindOnce(
&XRSession::OnSubscribeToHitTestResult, WrapPersistent(this))));
request_hit_test_source_promises_.insert(resolver);
return promise;
}
ScriptPromise XRSession::requestHitTestSourceForTransientInput(
ScriptState* script_state,
XRTransientInputHitTestOptionsInit* options_init,
ExceptionState& exception_state) {
DVLOG(2) << __func__;
DCHECK(options_init);
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::HIT_TEST)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kFeatureNotSupportedBySessionPrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::HIT_TEST));
return {};
}
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return {};
}
if (!xr_->xrEnvironmentProviderRemote()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
kFeatureNotSupportedByDevicePrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::HIT_TEST));
return {};
}
if (RuntimeEnabledFeatures::WebXRHitTestEntityTypesEnabled() &&
options_init->hasEntityTypes() && options_init->entityTypes().empty()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kEntityTypesNotSpecified);
return {};
}
auto entity_types = GetEntityTypesForHitTest(options_init);
XRRay* offsetRay = options_init && options_init->hasOffsetRay()
? options_init->offsetRay()
: MakeGarbageCollected<XRRay>();
device::mojom::blink::XRRayPtr ray_mojo = device::mojom::blink::XRRay::New();
ray_mojo->origin = {static_cast<float>(offsetRay->origin()->x()),
static_cast<float>(offsetRay->origin()->y()),
static_cast<float>(offsetRay->origin()->z())};
ray_mojo->direction = {static_cast<float>(offsetRay->direction()->x()),
static_cast<float>(offsetRay->direction()->y()),
static_cast<float>(offsetRay->direction()->z())};
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
xr_->xrEnvironmentProviderRemote()->SubscribeToHitTestForTransientInput(
options_init->profile(), entity_types, std::move(ray_mojo),
resolver->WrapCallbackInScriptScope(
WTF::BindOnce(&XRSession::OnSubscribeToHitTestForTransientInputResult,
WrapPersistent(this))));
request_hit_test_source_promises_.insert(resolver);
return promise;
}
void XRSession::OnSubscribeToHitTestResult(
ScriptPromiseResolver* resolver,
device::mojom::SubscribeToHitTestResult result,
uint64_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->RejectWithDOMException(DOMExceptionCode::kOperationError,
kHitTestSubscriptionFailed);
return;
}
XRHitTestSource* hit_test_source =
MakeGarbageCollected<XRHitTestSource>(subscription_id, this);
hit_test_source_ids_to_hit_test_sources_.insert(subscription_id,
hit_test_source);
hit_test_source_ids_.insert(subscription_id);
resolver->Resolve(hit_test_source);
}
void XRSession::OnSubscribeToHitTestForTransientInputResult(
ScriptPromiseResolver* resolver,
device::mojom::SubscribeToHitTestResult result,
uint64_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->RejectWithDOMException(DOMExceptionCode::kOperationError,
kHitTestSubscriptionFailed);
return;
}
XRTransientInputHitTestSource* hit_test_source =
MakeGarbageCollected<XRTransientInputHitTestSource>(subscription_id,
this);
hit_test_source_ids_to_transient_input_hit_test_sources_.insert(
subscription_id, hit_test_source);
hit_test_source_for_transient_input_ids_.insert(subscription_id);
resolver->Resolve(hit_test_source);
}
void XRSession::OnCreateAnchorResult(ScriptPromiseResolver* resolver,
device::mojom::CreateAnchorResult result,
uint64_t id) {
DVLOG(2) << __func__ << ": result=" << result << ", id=" << id;
DCHECK(create_anchor_promises_.Contains(resolver));
create_anchor_promises_.erase(resolver);
if (result == device::mojom::CreateAnchorResult::SUCCESS) {
// Anchor was created successfully on the device. Subsequent frame update
// must contain newly created anchor data.
anchor_ids_to_pending_anchor_promises_.insert(id, resolver);
} else {
resolver->RejectWithDOMException(DOMExceptionCode::kOperationError,
kAnchorCreationFailed);
}
}
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_->xrEnvironmentProviderRemote()) {
environment_error_handler_subscribed_ = true;
xr_->AddEnvironmentProviderErrorHandler(WTF::BindOnce(
&XRSession::OnEnvironmentProviderError, WrapWeakPersistent(this)));
}
}
void XRSession::OnEnvironmentProviderError() {
HeapHashSet<Member<ScriptPromiseResolver>> create_anchor_promises;
create_anchor_promises_.swap(create_anchor_promises);
for (ScriptPromiseResolver* resolver : create_anchor_promises) {
ScriptState* resolver_script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
resolver_script_state)) {
continue;
}
ScriptState::Scope script_state_scope(resolver_script_state);
resolver->RejectWithDOMException(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) {
ScriptState* resolver_script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
resolver_script_state)) {
continue;
}
ScriptState::Scope script_state_scope(resolver_script_state);
resolver->RejectWithDOMException(DOMExceptionCode::kInvalidStateError,
kDeviceDisconnected);
}
HeapVector<Member<ScriptPromiseResolver>> image_score_promises;
image_scores_resolvers_.swap(image_score_promises);
for (ScriptPromiseResolver* resolver : image_score_promises) {
ScriptState* resolver_script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
resolver_script_state)) {
continue;
}
ScriptState::Scope script_state_scope(resolver_script_state);
resolver->RejectWithDOMException(DOMExceptionCode::kInvalidStateError,
kDeviceDisconnected);
}
}
void XRSession::ProcessAnchorsData(
const device::mojom::blink::XRAnchorsData* tracked_anchors_data,
double timestamp) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("xr.debug"), __func__);
if (!tracked_anchors_data) {
DVLOG(3) << __func__ << ": tracked_anchors_data is null";
// We have received a nullptr. Clear stored anchors.
// The device can send either null or empty data - in both cases, it means
// that there are no anchors available.
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();
HeapHashMap<uint64_t, Member<XRAnchor>> updated_anchors;
// First, process all anchors that had their information updated (new anchors
// are also processed here).
for (const auto& anchor : tracked_anchors_data->updated_anchors_data) {
DCHECK(anchor);
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);
} else {
DVLOG(3) << __func__ << ": processing newly created anchor, anchor->id="
<< anchor->id;
auto resolver_it =
anchor_ids_to_pending_anchor_promises_.find(anchor->id);
if (resolver_it == anchor_ids_to_pending_anchor_promises_.end()) {
DCHECK(false)
<< "Newly created anchor must have a corresponding resolver!";
continue;
}
XRAnchor* xr_anchor =
MakeGarbageCollected<XRAnchor>(anchor->id, this, *anchor);
resolver_it->value->Resolve(xr_anchor);
anchor_ids_to_pending_anchor_promises_.erase(resolver_it);
updated_anchors.insert(anchor->id, xr_anchor);
}
}
// Then, copy over the anchors 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 anchor 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);
}
}
DVLOG(3) << __func__
<< ": anchor count before update=" << anchor_ids_to_anchors_.size()
<< ", after update=" << updated_anchors.size();
anchor_ids_to_anchors_.swap(updated_anchors);
DCHECK(anchor_ids_to_pending_anchor_promises_.empty())
<< "All anchors should be updated in the frame in which they were "
"created, got "
<< anchor_ids_to_pending_anchor_promises_.size()
<< " anchors that have not been updated";
}
XRPlaneSet* XRSession::GetDetectedPlanes() const {
return plane_manager_->GetDetectedPlanes();
}
void XRSession::CleanUpUnusedHitTestSources() {
auto unused_hit_test_source_ids = GetIdsOfUnusedHitTestSources(
hit_test_source_ids_to_hit_test_sources_, hit_test_source_ids_);
for (auto id : unused_hit_test_source_ids) {
xr_->xrEnvironmentProviderRemote()->UnsubscribeFromHitTest(id);
}
hit_test_source_ids_.RemoveAll(unused_hit_test_source_ids);
auto unused_transient_hit_source_ids = GetIdsOfUnusedHitTestSources(
hit_test_source_ids_to_transient_input_hit_test_sources_,
hit_test_source_for_transient_input_ids_);
for (auto id : unused_transient_hit_source_ids) {
xr_->xrEnvironmentProviderRemote()->UnsubscribeFromHitTest(id);
}
hit_test_source_for_transient_input_ids_.RemoveAll(
unused_transient_hit_source_ids);
DCHECK_HIT_TEST_SOURCES();
DVLOG(3) << __func__ << ": Number of active hit test sources: "
<< hit_test_source_ids_.size()
<< ", number of active hit test sources for transient input: "
<< hit_test_source_for_transient_input_ids_.size();
}
void XRSession::ProcessHitTestData(
const device::mojom::blink::XRHitTestSubscriptionResultsData*
hit_test_subscriptions_data) {
DVLOG(2) << __func__;
// Application's code can just drop references to hit test sources w/o first
// canceling them - ensure that we communicate that the subscriptions are no
// longer present to the device.
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.
DVLOG(3) << __func__ << ": hit_test_subscriptions_data->results.size()="
<< hit_test_subscriptions_data->results.size() << ", "
<< "hit_test_subscriptions_data->transient_input_results.size()="
<< hit_test_subscriptions_data->transient_input_results.size();
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);
}
}
for (auto& transient_input_hit_test_subscription_data :
hit_test_subscriptions_data->transient_input_results) {
auto it = hit_test_source_ids_to_transient_input_hit_test_sources_.find(
transient_input_hit_test_subscription_data->subscription_id);
if (it !=
hit_test_source_ids_to_transient_input_hit_test_sources_.end()) {
it->value->Update(transient_input_hit_test_subscription_data
->input_source_id_to_hit_test_results,
input_sources_);
}
}
} else {
DVLOG(3) << __func__ << ": hit_test_subscriptions_data unavailable";
// 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({});
}
for (auto& subscription_id_and_transient_input_hit_test_source :
hit_test_source_ids_to_transient_input_hit_test_sources_) {
subscription_id_and_transient_input_hit_test_source.value->Update(
{}, nullptr);
}
}
}
XRCPUDepthInformation* XRSession::GetCpuDepthInformation(
const XRFrame* xr_frame,
ExceptionState& exception_state) const {
if (!depth_manager_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kDepthSensingFeatureNotSupported);
return nullptr;
}
return depth_manager_->GetCpuDepthInformation(xr_frame, exception_state);
}
XRWebGLDepthInformation* XRSession::GetWebGLDepthInformation(
const XRFrame* xr_frame,
ExceptionState& exception_state) const {
if (!depth_manager_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kDepthSensingFeatureNotSupported);
return nullptr;
}
return depth_manager_->GetWebGLDepthInformation(xr_frame, exception_state);
}
ScriptPromise XRSession::requestLightProbe(ScriptState* script_state,
XRLightProbeInit* light_probe_init,
ExceptionState& exception_state) {
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return ScriptPromise();
}
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::LIGHT_ESTIMATION)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kFeatureNotSupportedBySessionPrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::LIGHT_ESTIMATION));
return ScriptPromise();
}
if (light_probe_init->reflectionFormat() != "srgba8" &&
light_probe_init->reflectionFormat() != "rgba16f") {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Reflection format \"" +
IDLEnumAsString(light_probe_init->reflectionFormat()) +
"\" not supported.");
return ScriptPromise();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
if (!world_light_probe_) {
// TODO(https://crbug.com/1147569): This is problematic because it means the
// first reflection format that gets requested is the only one that can be
// returned.
world_light_probe_ =
MakeGarbageCollected<XRLightProbe>(this, light_probe_init);
}
resolver->Resolve(world_light_probe_);
return promise;
}
ScriptPromise XRSession::end(ScriptState* script_state,
ExceptionState& exception_state) {
DVLOG(2) << __func__;
// Don't allow a session to end twice.
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return ScriptPromise();
}
ForceEnd(ShutdownPolicy::kWaitForResponse);
end_session_resolver_ = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = end_session_resolver_->Promise();
DVLOG(1) << __func__ << ": returning promise";
return promise;
}
void XRSession::ForceEnd(ShutdownPolicy shutdown_policy) {
bool wait_for_response;
switch (shutdown_policy) {
case ShutdownPolicy::kWaitForResponse:
wait_for_response = true;
break;
case ShutdownPolicy::kImmediate:
wait_for_response = false;
break;
}
DVLOG(3) << __func__ << ": wait_for_response=" << wait_for_response
<< " ended_=" << ended_
<< " waiting_for_shutdown_=" << waiting_for_shutdown_;
// 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_) {
// If we're currently waiting for an OnExitPresent, but are told not
// to expect that anymore (i.e. due to a connection error), proceed
// to full shutdown now.
if (!wait_for_response && waiting_for_shutdown_) {
HandleShutdown();
}
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;
}
xr_->ExitPresent(
WTF::BindOnce(&XRSession::OnExitPresent, WrapWeakPersistent(this)));
if (wait_for_response) {
waiting_for_shutdown_ = true;
} else {
HandleShutdown();
}
}
void XRSession::HandleShutdown() {
DVLOG(2) << __func__;
DCHECK(ended_);
waiting_for_shutdown_ = false;
if (xr_->IsContextDestroyed()) {
// If this is being called due to the context being destroyed,
// it's illegal to run JavaScript code, so we cannot emit an
// end event or resolve the stored promise. Don't bother calling
// the frame provider's OnSessionEnded, that's being disposed of
// also.
DVLOG(3) << __func__ << ": Context destroyed";
if (end_session_resolver_) {
end_session_resolver_->Detach();
end_session_resolver_ = nullptr;
}
return;
}
// Notify the frame provider that we've ended. Do this before notifying the
// page, so that if the page tries (and is able to) create a session within
// either the promise or the event callback, it's not blocked by the frame
// provider thinking there's still an active immersive session.
xr_->frameProvider()->OnSessionEnded(this);
if (end_session_resolver_) {
DVLOG(3) << __func__ << ": Resolving end_session_resolver_";
end_session_resolver_->Resolve();
end_session_resolver_ = nullptr;
}
DispatchEvent(*XRSessionEvent::Create(event_type_names::kEnd, this));
DVLOG(3) << __func__ << ": session end event dispatched";
// Now that we've notified the page that we've ended, try to restart the non-
// immersive frame loop. Note that if the page was able to request a new
// session in the end event, this may be a no-op.
xr_->frameProvider()->RestartNonImmersiveFrameLoop();
}
double XRSession::NativeFramebufferScale() const {
if (immersive()) {
DCHECK(recommended_framebuffer_scale_);
// Return the inverse of the recommended scale, since that's what we'll need
// to multiply the recommended size by to get back to the native size.
return 1.0 / recommended_framebuffer_scale_;
}
return 1.0;
}
double XRSession::RecommendedFramebufferScale() const {
return recommended_framebuffer_scale_;
}
gfx::SizeF XRSession::RecommendedFramebufferSize() const {
if (!immersive()) {
return gfx::SizeF(OutputCanvasSize());
}
float scale = recommended_framebuffer_scale_;
float width = 0;
float height = 0;
// For the moment, concatenate all the views into a big strip.
// Won't scale well for displays that use more than a stereo pair.
for (const auto& view : pending_views_) {
width += view->viewport.width();
height = std::max<float>(height, view->viewport.height());
}
return gfx::SizeF(width * scale, height * scale);
}
gfx::Size XRSession::OutputCanvasSize() const {
if (!render_state_->output_canvas()) {
return gfx::Size();
}
return gfx::Size(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() || !vfc_execution_queue_.empty();
// 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();
// 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().domWindow(),
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,
const device::mojom::blink::VRPosePtr& mojo_from_viewer_pose,
const device::mojom::blink::XRFrameDataPtr& frame_data,
int16_t frame_id,
bool emulated_position) {
TRACE_EVENT0("gpu", __func__);
DVLOG(2) << __func__ << " : frame_data valid? " << (frame_data ? true : false)
<< ", emulated_position=" << emulated_position;
// Don't process any outstanding frames once the session is ended.
if (ended_)
return;
if (frame_data) {
// Views need to be updated first, since views() creates a new set of views.
UpdateViews(frame_data->views);
// Apply dynamic viewport scaling if available.
if (supports_viewport_scaling_) {
float gpu_load = frame_data->rendering_time_ratio;
absl::optional<double> scale = absl::nullopt;
if (gpu_load > 0.0f) {
if (!viewport_scaler_) {
// Lazily create an instance of the viewport scaler on first use.
viewport_scaler_ = std::make_unique<XRSessionViewportScaler>();
}
viewport_scaler_->UpdateRenderingTimeRatio(gpu_load);
scale = viewport_scaler_->Scale();
DVLOG(3) << __func__ << ": gpu_load=" << gpu_load
<< " scale=" << *scale;
}
for (XRViewData* view : views()) {
view->SetRecommendedViewportScale(scale);
}
}
}
mojo_from_viewer_ = getPoseMatrix(mojo_from_viewer_pose);
DVLOG(2) << __func__ << " : mojo_from_viewer_ valid? "
<< (mojo_from_viewer_ ? true : false);
// TODO(https://crbug.com/1430868): We need to do this because inline sessions
// don't have enough data to send up a mojo::XRView; but blink::XRViews rely
// on having mojo_from_view set in a blink::XRViewData based upon the value
// sent up in a mojo::XRView. Really, mojo::XRView should only be setting
// viewer_from_view, and inline can go back to ignoring it, since the current
// behavior essentially has two out of sync mojo_from_viewer transforms, one
// is just implicitly embedded into an XRView. See
// https://crbug.com/1428489#c7 for more details.
if (!immersive() && mojo_from_viewer_) {
for (XRViewData* view : views()) {
// viewer_from_view multiplication omitted as it is identity.
view->SetMojoFromView(*mojo_from_viewer_.get() /* * viewer_from_view */);
}
}
emulated_position_ = emulated_position;
// Process XR input sources
if (frame_data) {
base::span<const device::mojom::blink::XRInputSourceStatePtr> input_states;
if (frame_data->input_state.has_value())
input_states = frame_data->input_state.value();
OnInputStateChangeInternal(frame_id, input_states);
// World understanding includes hit testing for transient input sources, and
// these sources may have been hidden when touching DOM Overlay content
// that's inside cross-origin iframes. Since hit test subscriptions only
// happen for existing input_sources_ entries, these touches will not
// generate hit test results. For this to work, this step must happen
// after OnInputStateChangeInternal which updated input sources.
UpdateWorldUnderstandingStateForFrame(timestamp, frame_data);
// If this session uses input eventing, XR select events are handled via
// OnButtonEvent, so they need to be ignored here to avoid duplicate events.
if (!uses_input_eventing_) {
ProcessInputSourceEvents(input_states);
}
} else {
UpdateWorldUnderstandingStateForFrame(timestamp, frame_data);
}
}
ScriptPromise XRSession::getTrackedImageScores(
ScriptState* script_state,
ExceptionState& exception_state) {
DVLOG(3) << __func__;
if (ended_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kSessionEnded);
return ScriptPromise();
}
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::IMAGE_TRACKING)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kFeatureNotSupportedBySessionPrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::IMAGE_TRACKING));
return ScriptPromise();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
ScriptPromise promise = resolver->Promise();
if (tracked_image_scores_available_) {
DVLOG(3) << __func__ << ": returning existing results";
resolver->Resolve(tracked_image_scores_);
} else {
DVLOG(3) << __func__ << ": storing promise";
image_scores_resolvers_.push_back(resolver);
}
return promise;
}
void XRSession::ProcessTrackedImagesData(
const device::mojom::blink::XRTrackedImagesData* images_data) {
DVLOG(3) << __func__;
frame_tracked_images_.clear();
if (!images_data) {
return;
}
for (const auto& image : images_data->images_data) {
DVLOG(3) << __func__ << ": image index=" << image->index;
XRImageTrackingResult* result =
MakeGarbageCollected<XRImageTrackingResult>(this, *image);
frame_tracked_images_.push_back(result);
}
if (images_data->image_trackable_scores) {
DVLOG(3) << ": got image_trackable_scores";
DCHECK(!tracked_image_scores_available_);
auto& scores = images_data->image_trackable_scores.value();
for (WTF::wtf_size_t index = 0; index < scores.size(); ++index) {
tracked_image_scores_.push_back(scores[index] ? "trackable"
: "untrackable");
DVLOG(3) << __func__ << ": score[" << index
<< "]=" << tracked_image_scores_[index];
}
HeapVector<Member<ScriptPromiseResolver>> image_score_promises;
image_scores_resolvers_.swap(image_score_promises);
for (ScriptPromiseResolver* resolver : image_score_promises) {
DVLOG(3) << __func__ << ": resolving promise";
resolver->Resolve(tracked_image_scores_);
}
tracked_image_scores_available_ = true;
}
}
HeapVector<Member<XRImageTrackingResult>> XRSession::ImageTrackingResults(
ExceptionState& exception_state) {
if (!IsFeatureEnabled(device::mojom::XRSessionFeature::IMAGE_TRACKING)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kFeatureNotSupportedBySessionPrefix +
XRSessionFeatureToString(
device::mojom::XRSessionFeature::IMAGE_TRACKING));
return {};
}
return frame_tracked_images_;
}
void XRSession::UpdateWorldUnderstandingStateForFrame(
double timestamp,
const device::mojom::blink::XRFrameDataPtr& frame_data) {
// Update objects that might change on per-frame basis.
if (frame_data) {
plane_manager_->ProcessPlaneInformation(
frame_data->detected_planes_data.get(), timestamp);
ProcessAnchorsData(frame_data->anchors_data.get(), timestamp);
ProcessHitTestData(frame_data->hit_test_subscription_results.get());
if (depth_manager_) {
depth_manager_->ProcessDepthInformation(
std::move(frame_data->depth_data));
}
ProcessTrackedImagesData(frame_data->tracked_images.get());
const device::mojom::blink::XRLightEstimationData* light_data =
frame_data->light_estimation_data.get();
if (world_light_probe_ && light_data) {
world_light_probe_->ProcessLightEstimationData(light_data, timestamp);
}
camera_image_size_ = absl::nullopt;
if (frame_data->camera_image_size.has_value()) {
// Let's store the camera image size. The texture ID will be filled out on
// the XRWebGLLayer by the session once the frame starts
// (in XRSession::OnFrame()).
camera_image_size_ = frame_data->camera_image_size;
}
} else {
plane_manager_->ProcessPlaneInformation(nullptr, timestamp);
ProcessAnchorsData(nullptr, timestamp);
ProcessHitTestData(nullptr);
if (depth_manager_) {
depth_manager_->ProcessDepthInformation(nullptr);
}
ProcessTrackedImagesData(nullptr);
if (world_light_probe_) {
world_light_probe_->ProcessLightEstimationData(nullptr, timestamp);
}
camera_image_size_ = absl::nullopt;
}
}
bool XRSession::IsFeatureEnabled(
device::mojom::XRSessionFeature feature) const {
return enabled_features_.Contains(feature);
}
void XRSession::SetMetricsReporter(std::unique_ptr<MetricsReporter> reporter) {
DCHECK(!metrics_reporter_);
metrics_reporter_ = std::move(reporter);
}
void XRSession::OnFrame(
double timestamp,
const absl::optional<gpu::MailboxHolder>& output_mailbox_holder,
const absl::optional<gpu::MailboxHolder>& camera_image_mailbox_holder) {
TRACE_EVENT0("gpu", __func__);
DVLOG(2) << __func__ << ": ended_=" << ended_
<< ", pending_frame_=" << pending_frame_;
// 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) {
DVLOG(2) << __func__ << ": frame_base_layer not present";
// 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_) {
DVLOG(2) << __func__
<< ": prev_base_layer_ is valid, submitting frame to it";
prev_base_layer_->OnFrameStart(output_mailbox_holder,
camera_image_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()) {
DVLOG(2) << __func__
<< ": frames are not to be processed if an inline session "
"doesn't have an output canvas";
return;
}
frame_base_layer->OnFrameStart(output_mailbox_holder,
camera_image_mailbox_holder);
// Don't allow frames to be processed if the session's visibility state is
// "hidden".
if (visibility_state_ == XRVisibilityState::HIDDEN) {
DVLOG(2) << __func__
<< ": frames to be processed if the session's visibility state "
"is \"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(true);
views_updated_this_frame_ = false;
// If the device has opted in, mark the viewports as modifiable
// at the start of an animation frame:
// https://immersive-web.github.io/webxr/#ref-for-view-viewport-modifiable
if (supports_viewport_scaling_) {
for (XRViewData* view : views()) {
view->SetViewportModifiable(true);
}
}
// 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);
ExecuteVideoFrameCallbacks(timestamp);
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 {
if (!did_log_getViewerPose_ && GetExecutionContext()) {
did_log_getViewerPose_ = true;
ukm::builders::XR_WebXR(GetExecutionContext()->UkmSourceID())
.SetDidRequestPose(1)
.Record(GetExecutionContext()->UkmRecorder());
}
}
bool XRSession::CanReportPoses() const {
// 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;
}
bool XRSession::CanEnableAntiAliasing() const {
return enable_anti_aliasing_;
}
absl::optional<gfx::Transform> XRSession::GetMojoFrom(
device::mojom::blink::XRReferenceSpaceType space_type) const {
if (!CanReportPoses()) {
DVLOG(2) << __func__ << ": cannot report poses, returning nullopt";
return absl::nullopt;
}
switch (space_type) {
case device::mojom::blink::XRReferenceSpaceType::kViewer:
if (!mojo_from_viewer_) {
if (sensorless_session_) {
return gfx::Transform();
}
return absl::nullopt;
}
return *mojo_from_viewer_;
case device::mojom::blink::XRReferenceSpaceType::kLocal:
// TODO(https://crbug.com/1070380): This assumes that local space is
// equivalent to mojo space! Remove the assumption once the bug is fixed.
return gfx::Transform();
case device::mojom::blink::XRReferenceSpaceType::kUnbounded:
// TODO(https://crbug.com/1070380): This assumes that unbounded space is
// equivalent to mojo space! Remove the assumption once the bug is fixed.
return gfx::Transform();
case device::mojom::blink::XRReferenceSpaceType::kLocalFloor:
case device::mojom::blink::XRReferenceSpaceType::kBoundedFloor:
// Information about -floor spaces is currently stored elsewhere (in
// stage_parameters_). It probably should eventually move here.
return absl::nullopt;
}
}
XRFrame* XRSession::CreatePresentationFrame(bool is_animation_frame) {
DVLOG(2) << __func__ << ": is_animation_frame=" << is_animation_frame;
XRFrame* presentation_frame =
MakeGarbageCollected<XRFrame>(this, is_animation_frame);
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;
LocalDOMWindow* window = To<LocalDOMWindow>(xr_->GetExecutionContext());
if (window) {
devicePixelRatio = window->GetFrame()->DevicePixelRatio();
}
output_width_ = element->OffsetWidth() * devicePixelRatio;
output_height_ = element->OffsetHeight() * devicePixelRatio;
if (render_state_->baseLayer()) {
render_state_->baseLayer()->OnResize();
}
canvas_was_resized_ = true;
}
void XRSession::OnButtonEvent(
device::mojom::blink::XRInputSourceStatePtr input_state) {
DCHECK(uses_input_eventing_);
auto input_states = base::make_span(&input_state, 1u);
OnInputStateChangeInternal(last_frame_id_, input_states);
ProcessInputSourceEvents(input_states);
}
void XRSession::OnInputStateChangeInternal(
int16_t frame_id,
base::span<const device::mojom::blink::XRInputSourceStatePtr>
input_states) {
// 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;
DVLOG(2) << __func__ << ": frame_id=" << frame_id
<< " input_states.size()=" << input_states.size();
// 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) {
DVLOG(2) << __func__
<< ": input_state->source_id=" << input_state->source_id
<< " input_state->primary_input_pressed="
<< input_state->primary_input_pressed
<< " clicked=" << input_state->primary_input_clicked;
XRInputSource* stored_input_source =
input_sources_->GetWithSourceId(input_state->source_id);
DVLOG(2) << __func__ << ": stored_input_source=" << stored_input_source;
XRInputSource* input_source = XRInputSource::CreateOrUpdateFrom(
stored_input_source, this, input_state);
// Input sources should use DOM overlay hit test to check if they intersect
// cross-origin content. If that's the case, the input source is set as
// invisible, and must not return poses or hit test results.
bool hide_input_source = false;
if (IsFeatureEnabled(device::mojom::XRSessionFeature::DOM_OVERLAY) &&
overlay_element_ && input_state->overlay_pointer_position) {
input_source->ProcessOverlayHitTest(overlay_element_, input_state);
if (!stored_input_source && !input_source->IsVisible()) {
DVLOG(2) << __func__ << ": (new) hidden_input_source";
hide_input_source = true;
}
}
// Using pointer equality to determine if the pointer needs to be set.
if (stored_input_source != input_source) {
DVLOG(2) << __func__ << ": stored_input_source != input_source";
if (!hide_input_source) {
input_sources_->SetWithSourceId(input_state->source_id, input_source);
added.push_back(input_source);
DVLOG(2) << __func__ << ": ADDED input_source "
<< input_state->source_id;
}
// If we previously had a stored_input_source, disconnect its gamepad
// and mark that it was removed.
if (stored_input_source) {
stored_input_source->SetGamepadConnected(false);
DVLOG(2) << __func__ << ": REMOVED stored_input_source";
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.empty() || !removed.empty()) {
DispatchEvent(*XRInputSourcesChangeEvent::Create(
event_type_names::kInputsourceschange, this, added, removed));
}
}
void XRSession::ProcessInputSourceEvents(
base::span<const device::mojom::blink::XRInputSourceStatePtr>
input_states) {
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;
XRInputSource* input_source =
input_sources_->GetWithSourceId(input_state->source_id);
// The input source might not be in input_sources_ if it was created hidden.
if (input_source) {
input_source->UpdateButtonStates(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::OnMojoSpaceReset() {
// Since this eventually dispatches an event to the page, the page could
// create a new reference space which would invalidate our iterators; so
// iterate over a copy of the reference space list.
HeapVector<Member<XRReferenceSpace>> ref_spaces_copy = reference_spaces_;
for (const auto& reference_space : ref_spaces_copy) {
reference_space->OnReset();
}
}
void XRSession::OnExitPresent() {
DVLOG(2) << __func__ << ": immersive()=" << immersive()
<< " waiting_for_shutdown_=" << waiting_for_shutdown_;
if (immersive()) {
ForceEnd(ShutdownPolicy::kImmediate);
} else if (waiting_for_shutdown_) {
HandleShutdown();
}
}
bool XRSession::ValidateHitTestSourceExists(
XRHitTestSource* hit_test_source) const {
DCHECK(hit_test_source);
return base::Contains(hit_test_source_ids_, hit_test_source->id());
}
bool XRSession::ValidateHitTestSourceExists(
XRTransientInputHitTestSource* hit_test_source) const {
DCHECK(hit_test_source);
return base::Contains(hit_test_source_for_transient_input_ids_,
hit_test_source->id());
}
bool XRSession::RemoveHitTestSource(XRHitTestSource* hit_test_source) {
DVLOG(2) << __func__;
DCHECK(hit_test_source);
if (!base::Contains(hit_test_source_ids_, hit_test_source->id())) {
DVLOG(2) << __func__
<< ": hit test source was already removed, hit_test_source->id()="
<< hit_test_source->id();
return false;
}
if (ended_) {
DVLOG(1) << __func__
<< ": attempted to remove a hit test source on a session that has "
"already ended.";
// Since the session has ended, we won't be able to reach out to the device
// to remove a hit test source subscription. Just notify the caller that the
// removal was successful.
return true;
}
DCHECK_HIT_TEST_SOURCES();
hit_test_source_ids_to_hit_test_sources_.erase(hit_test_source->id());
hit_test_source_ids_.erase(hit_test_source->id());
DCHECK(xr_->xrEnvironmentProviderRemote());
xr_->xrEnvironmentProviderRemote()->UnsubscribeFromHitTest(
hit_test_source->id());
DCHECK_HIT_TEST_SOURCES();
return true;
}
bool XRSession::RemoveHitTestSource(
XRTransientInputHitTestSource* hit_test_source) {
DVLOG(2) << __func__;
DCHECK(hit_test_source);
if (!base::Contains(hit_test_source_for_transient_input_ids_,
hit_test_source->id())) {
DVLOG(2) << __func__
<< ": hit test source was already removed, hit_test_source->id()="
<< hit_test_source->id();
return false;
}
if (ended_) {
DVLOG(1) << __func__
<< ": attempted to remove a hit test source on a session that has "
"already ended.";
// Since the session has ended, we won't be able to reach out to the device
// to remove a hit test source subscription. Just notify the caller that the
// removal was successful.
return true;
}
DCHECK_HIT_TEST_SOURCES();
hit_test_source_ids_to_transient_input_hit_test_sources_.erase(
hit_test_source->id());
hit_test_source_for_transient_input_ids_.erase(hit_test_source->id());
DCHECK(xr_->xrEnvironmentProviderRemote());
xr_->xrEnvironmentProviderRemote()->UnsubscribeFromHitTest(
hit_test_source->id());
DCHECK_HIT_TEST_SOURCES();
return true;
}
const HeapVector<Member<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. See also XRWebGLLayer::UpdateViewports() which
// assumes that the views are arranged as follows.
if (!views_updated_this_frame_) {
if (immersive()) {
// In immersive mode the projection and view matrices must be aligned with
// the device's physical optics.
// Views shouldn't be re-created on each frame because they contain
// viewport scaling information, such as requested viewport scales.
// However, if the number of views changed or if the order of the views
// changed, we should recreate the views since we aren't able to match
// the old views to the new views.
bool create_views = false;
if (views_.size() != pending_views_.size()) {
views_.clear();
views_.resize(pending_views_.size());
create_views = true;
if (render_state_->baseLayer()) {
render_state_->baseLayer()->OnResize();
}
}
for (wtf_size_t i = 0; !create_views && i < pending_views_.size(); ++i) {
if (views_[i]->Eye() == pending_views_[i]->eye) {
views_[i]->UpdateView(pending_views_[i], render_state_->depthNear(),
render_state_->depthFar());
} else {
create_views = true;
}
}
if (create_views) {
for (wtf_size_t i = 0; i < pending_views_.size(); ++i) {
views_[i] = MakeGarbageCollected<XRViewData>(
pending_views_[i], render_state_->depthNear(),
render_state_->depthFar());
}
}
} else {
if (canvas_was_resized_) {
views_.clear();
canvas_was_resized_ = false;
}
if (views_.empty()) {
views_.emplace_back(MakeGarbageCollected<XRViewData>(
device::mojom::blink::XREye::kNone,
gfx::Rect(0, 0, output_width_, output_height_)));
}
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.
absl::optional<double> inline_vertical_fov =
render_state_->inlineVerticalFieldOfView();
// inlineVerticalFieldOfView should only be null in immersive mode.
DCHECK(inline_vertical_fov.has_value());
views_[kMonoView]->UpdateProjectionMatrixFromAspect(
inline_vertical_fov.value(), aspect, render_state_->depthNear(),
render_state_->depthFar());
}
views_updated_this_frame_ = true;
}
return views_;
}
bool XRSession::HasPendingActivity() const {
return (!callback_collection_->IsEmpty() || !vfc_execution_queue_.empty()) &&
!ended_;
}
void XRSession::Trace(Visitor* visitor) const {
visitor->Trace(xr_);
visitor->Trace(render_state_);
visitor->Trace(world_light_probe_);
visitor->Trace(pending_render_state_);
visitor->Trace(end_session_resolver_);
visitor->Trace(input_sources_);
visitor->Trace(resize_observer_);
visitor->Trace(canvas_input_provider_);
visitor->Trace(overlay_element_);
visitor->Trace(dom_overlay_state_);
visitor->Trace(client_receiver_);
visitor->Trace(input_receiver_);
visitor->Trace(callback_collection_);
visitor->Trace(create_anchor_promises_);
visitor->Trace(request_hit_test_source_promises_);
visitor->Trace(reference_spaces_);
visitor->Trace(plane_manager_);
visitor->Trace(depth_manager_);
visitor->Trace(anchor_ids_to_anchors_);
visitor->Trace(anchor_ids_to_pending_anchor_promises_);
visitor->Trace(prev_base_layer_);
visitor->Trace(hit_test_source_ids_to_hit_test_sources_);
visitor->Trace(hit_test_source_ids_to_transient_input_hit_test_sources_);
visitor->Trace(views_);
visitor->Trace(frame_tracked_images_);
visitor->Trace(image_scores_resolvers_);
EventTargetWithInlineData::Trace(visitor);
}
} // namespace blink