// 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/dcheck_is_on.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/public/mojom/hit_test_subscription_id.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/renderer/bindings/core/v8/frozen_array.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/bindings/modules/v8/v8_xr_visibility_state.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/html/canvas/html_canvas_element.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/vr_service_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_composition_layer.h"
#include "third_party/blink/renderer/modules/xr/xr_cube_map.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_mesh_manager.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_visibility_mask_change_event.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 "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/transform.h"

#if BUILDFLAG(ENABLE_VR)
#include "device/vr/public/cpp/features.h"
#endif

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[] =
    "XRLayer 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 kTooManyLayers[] =
    "The number of layers to be enabled exceeds maxRenderLayers.";

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 kNotInAnimationFrame[] =
    "Not called from inside an active animation frame.";

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.
std::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();
}

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());
}

device::mojom::blink::EntityTypeForHitTest EntityTypeForHitTestFromEnum(
    V8XRHitTestTrackableType::Enum type) {
  switch (type) {
    case V8XRHitTestTrackableType::Enum::kPlane:
      return device::mojom::blink::EntityTypeForHitTest::PLANE;
    case V8XRHitTestTrackableType::Enum::kPoint:
      return device::mojom::blink::EntityTypeForHitTest::POINT;
  }
  NOTREACHED();
}

// 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& v8_entity_type : options_init->entityTypes()) {
      result_set.insert(EntityTypeForHitTestFromEnum(v8_entity_type.AsEnum()));
    }
  } 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<device::HitTestSubscriptionId> GetIdsOfUnusedHitTestSources(
    const HeapHashMap<device::HitTestSubscriptionId, WeakMember<T>>&
        id_to_hit_test_source,
    const HashSet<device::HitTestSubscriptionId>& all_ids) {
  // Gather all IDs of unused hit test sources:
  HashSet<device::HitTestSubscriptionId> unused_hit_test_source_ids;
  for (auto& id : all_ids) {
    if (!id_to_hit_test_source.Contains(id)) {
      unused_hit_test_source_ids.insert(id);
    }
  }

  return unused_hit_test_source_ids;
}

V8XRDepthUsage::Enum DepthUsageToEnum(device::mojom::XRDepthUsage usage) {
  switch (usage) {
    case device::mojom::XRDepthUsage::kCPUOptimized:
      return V8XRDepthUsage::Enum::kCpuOptimized;
    case device::mojom::XRDepthUsage::kGPUOptimized:
      return V8XRDepthUsage::Enum::kGpuOptimized;
  }
  NOTREACHED();
}

V8XRDepthDataFormat::Enum DepthDataFormatToEnum(
    device::mojom::XRDepthDataFormat data_format) {
  switch (data_format) {
    case device::mojom::XRDepthDataFormat::kLuminanceAlpha:
      return V8XRDepthDataFormat::Enum::kLuminanceAlpha;
    case device::mojom::XRDepthDataFormat::kFloat32:
      return V8XRDepthDataFormat::Enum::kFloat32;
    case device::mojom::XRDepthDataFormat::kUnsignedShort:
      return V8XRDepthDataFormat::Enum::kUnsignedShort;
  }
  NOTREACHED();
}

std::optional<V8XRDepthType::Enum> DepthTypeToEnum(
    std::optional<device::mojom::XRDepthType> maybe_type) {
  if (!maybe_type) {
    return std::nullopt;
  }
  switch (*maybe_type) {
    case device::mojom::XRDepthType::kRaw:
      return V8XRDepthType::Enum::kRaw;
    case device::mojom::XRDepthType::kSmooth:
      return V8XRDepthType::Enum::kSmooth;
  }
  NOTREACHED();
}

}  // 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::MESH_DETECTION:
    case XRSessionFeature::DEPTH:
    case XRSessionFeature::IMAGE_TRACKING:
    case XRSessionFeature::HAND_INPUT:
    case XRSessionFeature::SECONDARY_VIEWS:
    case XRSessionFeature::LAYERS:
    case XRSessionFeature::FRONT_FACING:
    case XRSessionFeature::WEBGPU:
      // Not recording metrics for these features currently.
      break;
  }
}

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_feature_set,
    uint64_t trace_id)
    : ActiveScriptWrappable<XRSession>({}),
      frame_tracked_images_(
          MakeGarbageCollected<FrozenArray<XRImageTrackingResult>>()),
      xr_(xr),
      mode_(mode),
      environment_integration_(
          mode == device::mojom::blink::XRSessionMode::kImmersiveAr),
      device_config_(std::move(device_config)),
      enabled_feature_set_(std::move(enabled_feature_set)),
      mesh_manager_(
          MakeGarbageCollected<XRMeshManager>(base::PassKey<XRSession>{},
                                              this)),
      plane_manager_(
          MakeGarbageCollected<XRPlaneManager>(base::PassKey<XRSession>{},
                                               this)),
      input_sources_(MakeGarbageCollected<XRInputSourceArray>()),
      empty_input_sources_(MakeGarbageCollected<XRInputSourceArray>()),
      client_receiver_(this, xr->GetExecutionContext()),
      callback_collection_(
          MakeGarbageCollected<XRFrameRequestCallbackCollection>(
              xr->GetExecutionContext())),
      supports_viewport_scaling_(immersive() &&
                                 device_config_->supports_viewport_scaling),
      sensorless_session_(sensorless_session),
      trace_id_(trace_id) {
  FrozenArray<IDLString>::VectorType enabled_features;
  for (const auto& feature : enabled_feature_set_) {
    enabled_features.push_back(XRSessionFeatureToString(feature).ToString());
  }
  enabled_features_ =
      MakeGarbageCollected<FrozenArray<IDLString>>(std::move(enabled_features));

  if (IsFeatureEnabled(device::mojom::XRSessionFeature::WEBGPU)) {
    graphics_api_ = XRGraphicsBinding::Api::kWebGPU;
  } else {
    graphics_api_ = XRGraphicsBinding::Api::kWebGL;
  }

  layers_enabled_ = IsFeatureEnabled(device::mojom::XRSessionFeature::LAYERS);

  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();

  // XRSessionDeviceConfig::views are in the unique position of being sent up
  // as an initial value that we should never need to inspect after the first
  // frame is sent to us, so we're okay to move it here, the other values on
  // device_config_ may be referenced throughout the lifetime of the session.
  UpdateViews(std::move(device_config_->views));

  DVLOG(2) << __func__
           << ": supports_viewport_scaling_=" << supports_viewport_scaling_;

  switch (environment_blend_mode) {
    case device::mojom::blink::XREnvironmentBlendMode::kOpaque:
      blend_mode_ = V8XREnvironmentBlendMode::Enum::kOpaque;
      break;
    case device::mojom::blink::XREnvironmentBlendMode::kAdditive:
      blend_mode_ = V8XREnvironmentBlendMode::Enum::kAdditive;
      break;
    case device::mojom::blink::XREnvironmentBlendMode::kAlphaBlend:
      blend_mode_ = V8XREnvironmentBlendMode::Enum::kAlphaBlend;
      break;
    default:
      NOTREACHED() << "Unknown environment blend mode: "
                   << environment_blend_mode;
  }

  switch (interaction_mode) {
    case device::mojom::blink::XRInteractionMode::kScreenSpace:
      interaction_mode_ = V8XRInteractionMode::Enum::kScreenSpace;
      break;
    case device::mojom::blink::XRInteractionMode::kWorldSpace:
      interaction_mode_ = V8XRInteractionMode::Enum::kWorldSpace;
      break;
  }

  if (device_config_->depth_configuration) {
    auto* depth_config = device_config_->depth_configuration.get();
    depth_usage_ = DepthUsageToEnum(depth_config->depth_usage);
    depth_data_format_ = DepthDataFormatToEnum(depth_config->depth_data_format);
    depth_type_ = DepthTypeToEnum(depth_config->depth_type);
  }
}

void XRSession::SetDOMOverlayElement(Element* element) {
  DVLOG(2) << __func__ << ": element=" << element;
  DCHECK(enabled_feature_set_.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>(
      V8XRDOMOverlayType::Enum::kScreen);
}

V8XRVisibilityState XRSession::visibilityState() const {
  switch (visibility_state_) {
    case XRVisibilityState::VISIBLE:
      return V8XRVisibilityState(V8XRVisibilityState::Enum::kVisible);
    case XRVisibilityState::VISIBLE_BLURRED:
      return V8XRVisibilityState(V8XRVisibilityState::Enum::kVisibleBlurred);
    case XRVisibilityState::HIDDEN:
      return V8XRVisibilityState(V8XRVisibilityState::Enum::kHidden);
  }
}

const FrozenArray<IDLString>& XRSession::enabledFeatures() const {
  return *enabled_features_.Get();
}

bool XRSession::isSystemKeyboardSupported() const {
#if BUILDFLAG(ENABLE_VR) && BUILDFLAG(IS_ANDROID)
  // Cardboard and ARCore technically support the keyboard as-is, but to avoid
  // exposing it on Quest/OpenXR before the implementation is ready, we guard
  // it with this flag for all Android for now. This results in false-negatives
  // for non-OpenXR runtimes, which matches the existing behavior where keyboard
  // support was always disabled.
  return base::FeatureList::IsEnabled(
      device::features::kOpenXrAndroidSystemKeyboard);
#else
  return false;
#endif
}

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;
}

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::WebXRLayersCommonEnabled() && 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;
    }

    // Validate that the number of layers is allowed.
    if (init->layers()->size() > maxRenderLayers()) {
      exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                        kTooManyLayers);
      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.ThrowTypeError(kDuplicateLayer);
        return;
      }

      // Validate that all layers were created with this session.
      if (layer->session() != this) {
        exception_state.ThrowTypeError(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::AddGraphicsBinding(XRGraphicsBinding* binding) {
  graphics_bindings_.insert(binding);
}

void XRSession::RemoveGraphicsBinding(XRGraphicsBinding* binding) {
  graphics_bindings_.erase(binding);
}

std::optional<V8XRDepthUsage> XRSession::depthUsage(
    ExceptionState& exception_state) {
  if (!device_config_->depth_configuration) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kDepthSensingFeatureNotSupported);
    return std::nullopt;
  }

  return V8XRDepthUsage(depth_usage_);
}

std::optional<V8XRDepthDataFormat> XRSession::depthDataFormat(
    ExceptionState& exception_state) {
  if (!device_config_->depth_configuration) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kDepthSensingFeatureNotSupported);
    return std::nullopt;
  }

  return V8XRDepthDataFormat(depth_data_format_);
}

std::optional<V8XRDepthType> XRSession::depthType(
    ExceptionState& exception_state) {
  if (!device_config_->depth_configuration) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kDepthSensingFeatureNotSupported);
    return std::nullopt;
  }

  if (!depth_type_) {
    return std::nullopt;
  }

  return V8XRDepthType(*depth_type_);
}

std::optional<bool> XRSession::depthActive(ExceptionState& exception_state) {
  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::DEPTH)) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kDepthSensingFeatureNotSupported);
    return std::nullopt;
  }

  return depth_active_;
}

void XRSession::pauseDepthSensing(ExceptionState& exception_state) {
  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return;
  }

  if (!animation_frame_ || !animation_frame_->IsActive()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kNotInAnimationFrame);
    return;
  }

  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::DEPTH)) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      kDepthSensingFeatureNotSupported);
    return;
  }

  depth_active_ = false;
}

void XRSession::resumeDepthSensing(ExceptionState& exception_state) {
  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return;
  }

  if (!animation_frame_ || !animation_frame_->IsActive()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kNotInAnimationFrame);
    return;
  }

  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::DEPTH)) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      kDepthSensingFeatureNotSupported);
    return;
  }

  depth_active_ = true;
}

bool XRSession::IsDepthActive() {
  return IsFeatureEnabled(device::mojom::XRSessionFeature::DEPTH) &&
         depth_active_;
}

void XRSession::UpdateViews(Vector<device::mojom::blink::XRViewPtr> 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 (immersive()) {
    // In immersive mode the projection and view matrices must be aligned with
    // the device's physical optics.

    // If there are no views provided for this frame, keep the views we
    // currently have.
    if (views.empty()) {
      return;
    }

    // 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;
    bool views_resized = false;
    // If we have *no* views yet, then we assume this is the first time we're
    // updating our views.
    bool initial_creation = views_.empty();
    if (views_.size() != views.size()) {
      views_.clear();
      views_.resize(views.size());
      create_views = true;

      // If we're changing the number of views, then we need to notify the base
      // layer that it should resize; but don't do that until the new views have
      // been created and the size known. Since we may also re-create views
      // if the eyes come in a different order, use a separate bool to track if
      // a resize has occurred to cut down on noise to the base layer.
      views_resized = true;
    }

    for (wtf_size_t i = 0; !create_views && i < views.size(); ++i) {
      if (views_[i]->Eye() != views[i]->eye) {
        create_views = true;
      }
    }

    for (wtf_size_t i = 0; i < views.size(); ++i) {
      if (create_views) {
        views_[i] = MakeGarbageCollected<XRViewData>(
            i, std::move(views[i]), render_state_->depthNear(),
            render_state_->depthFar(), *device_config_, enabled_feature_set_,
            graphics_api_);
      } else {
        views_[i]->UpdateView(std::move(views[i]), render_state_->depthNear(),
                              render_state_->depthFar());
      }

      // If this is the first time we're creating the views, then we'll dispatch
      // the visibility change event later.
      if (!initial_creation) {
        XRViewData* view = views_[i];
        if (view->NeedsVisibilityMaskChangeEvent()) {
          DispatchEvent(*XRVisibilityMaskChangeEvent::Create(
              event_type_names::kVisibilitymaskchange, this,
              GetV8Eye(view->Eye()), view->index(), view->visibility_mask()));
          view->OnVisibilityMaskChangeEvent();
        }
      }
    }

    if (views_resized) {
      render_state_->OnResize();
    }
  } else {  // Inline
    UpdateInlineView();
  }
}

void XRSession::DispatchInitialEvents() {
  if (immersive()) {
    for (XRViewData* view : views()) {
      // We will only send visibility mask change events for views that actually
      // *have* a visibility mask, at least for the initial set. If the views
      // change, we'll send empty masks; but until that time don't send an empty
      // mask if it's the first time.
      if (view->visibility_mask()) {
        DispatchEvent(*XRVisibilityMaskChangeEvent::Create(
            event_type_names::kVisibilitymaskchange, this,
            GetV8Eye(view->Eye()), view->index(), view->visibility_mask()));
      }

      // Whether we sent the event or opted not to send the event, indicate that
      // it was sent, so that it'll be skipped until there's an actual update.
      view->OnVisibilityMaskChangeEvent();
    }
  }
}

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<IDLUndefined> XRSession::updateTargetFrameRate(
    float rate,
    ExceptionState& exception_state) {
  exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                    kSessionNotHaveSetFrameRate);
  return EmptyPromise();
}

ScriptPromise<XRReferenceSpace> XRSession::requestReferenceSpace(
    ScriptState* script_state,
    const V8XRReferenceSpaceType& type,
    ExceptionState& exception_state) {
  DVLOG(2) << __func__ << ": type=" << type.AsCStr();

  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return EmptyPromise();
  }

  device::mojom::blink::XRReferenceSpaceType requested_type =
      XRReferenceSpace::V8EnumToReferenceSpaceType(type.AsEnum());

  if (sensorless_session_ &&
      requested_type != device::mojom::blink::XRReferenceSpaceType::kViewer) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      kReferenceSpaceNotSupported);
    return EmptyPromise();
  }

  // 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 EmptyPromise();
  }

  // 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.AsCStr();
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      kReferenceSpaceNotSupported);
    return EmptyPromise();
  }

  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 EmptyPromise();
  }

  DCHECK(reference_space);
  reference_spaces_.push_back(reference_space);
  return ToResolvedPromise<XRReferenceSpace>(script_state, reference_space);
}

ScriptPromise<XRAnchor> XRSession::CreateAnchorHelper(
    ScriptState* script_state,
    const gfx::Transform& native_origin_from_anchor,
    const device::mojom::blink::XRNativeOriginInformationPtr&
        native_origin_information,
    std::optional<device::PlaneId> maybe_plane_id,
    ExceptionState& exception_state) {
  DVLOG(2) << __func__;

  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return EmptyPromise();
  }

  // Reject the promise if device doesn't support the anchors API.
  if (!xr_->xrEnvironmentProviderRemote()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        StrCat({kFeatureNotSupportedByDevicePrefix,
                XRSessionFeatureToString(
                    device::mojom::XRSessionFeature::ANCHORS)}));
    return EmptyPromise();
  }

  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 EmptyPromise();
  }

  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<XRAnchor>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  xr_->xrEnvironmentProviderRemote()->CreateAnchor(
      native_origin_information->Clone(), *maybe_native_origin_from_anchor_pose,
      maybe_plane_id,
      resolver->WrapCallbackInScriptScope(blink::BindOnce(
          &XRSession::OnCreateAnchorResult, WrapPersistent(this))));

  create_anchor_promises_.insert(resolver);

  return promise;
}

std::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 std::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();
}

base::TimeDelta XRSession::TakeAnimationFrameTimerAverage() {
  return page_animation_frame_timer_.TakeAverageMicroseconds();
}

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);
}

uint32_t XRSession::requestAnimationFrame(V8XRFrameRequestCallback* callback) {
  DVLOG(3) << __func__;

  TRACE_EVENT0("gpu", "requestAnimationFrame");
  // Don't allow any new frame requests once the session is ended.
  if (ended_)
    return 0;

  uint32_t id = callback_collection_->RegisterCallback(callback);
  MaybeRequestFrame();
  return id;
}

void XRSession::cancelAnimationFrame(uint32_t 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;
  }

  if (!CanReportInputPoses()) {
    return empty_input_sources_.Get();
  }
  return input_sources_.Get();
}

ScriptPromise<XRHitTestSource> 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,
        StrCat({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,
        StrCat({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<XRHitTestSource>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  xr_->xrEnvironmentProviderRemote()->SubscribeToHitTest(
      maybe_native_origin->Clone(), entity_types, std::move(ray_mojo),
      resolver->WrapCallbackInScriptScope(blink::BindOnce(
          &XRSession::OnSubscribeToHitTestResult, WrapPersistent(this))));
  request_hit_test_source_promises_.insert(resolver);

  return promise;
}

ScriptPromise<XRTransientInputHitTestSource>
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,
        StrCat({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,
        StrCat({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<XRTransientInputHitTestSource>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  xr_->xrEnvironmentProviderRemote()->SubscribeToHitTestForTransientInput(
      options_init->profile(), entity_types, std::move(ray_mojo),
      resolver->WrapCallbackInScriptScope(blink::BindOnce(
          &XRSession::OnSubscribeToHitTestForTransientInputResult,
          WrapPersistent(this))));
  request_hit_test_source_promises_.insert(resolver);

  return promise;
}

void XRSession::OnSubscribeToHitTestResult(
    ScriptPromiseResolver<XRHitTestSource>* resolver,
    const std::optional<device::HitTestSubscriptionId>& subscription_id) {
  DVLOG(2) << __func__ << ": subscription_id="
           << subscription_id.value_or(device::kInvalidHitTestSubscriptionId);

  DCHECK(request_hit_test_source_promises_.Contains(resolver));
  request_hit_test_source_promises_.erase(resolver);

  if (!subscription_id) {
    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<XRTransientInputHitTestSource>* resolver,
    const std::optional<device::HitTestSubscriptionId>& subscription_id) {
  DVLOG(2) << __func__ << ": subscription_id="
           << subscription_id.value_or(device::kInvalidHitTestSubscriptionId);

  DCHECK(request_hit_test_source_promises_.Contains(resolver));
  request_hit_test_source_promises_.erase(resolver);

  if (!subscription_id) {
    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<XRAnchor>* resolver,
    const std::optional<device::AnchorId>& id) {
  DVLOG(2) << __func__ << ": id=" << id.value_or(device::kInvalidAnchorId);

  DCHECK(create_anchor_promises_.Contains(resolver));
  create_anchor_promises_.erase(resolver);

  if (id) {
    // 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(BindOnce(
        &XRSession::OnEnvironmentProviderError, WrapWeakPersistent(this)));
  }
}

void XRSession::OnEnvironmentProviderError() {
  HeapHashSet<Member<ScriptPromiseResolverBase>> create_anchor_promises;
  create_anchor_promises_.swap(create_anchor_promises);
  for (ScriptPromiseResolverBase* 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<ScriptPromiseResolverBase>>
      request_hit_test_source_promises;
  request_hit_test_source_promises_.swap(request_hit_test_source_promises);
  for (ScriptPromiseResolverBase* 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<ImageScoreResolverType>> image_score_promises;
  image_scores_resolvers_.swap(image_score_promises);
  for (auto& 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"), "ProcessAnchorsData");

  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<device::AnchorId, 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->DowncastTo<XRAnchor>()->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);
      CHECK(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";
}

XRMeshSet* XRSession::GetDetectedMeshes() const {
  return mesh_manager_->GetDetectedMeshes();
}

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);
    }
  }
}

ScriptPromise<XRLightProbe> XRSession::requestLightProbe(
    ScriptState* script_state,
    XRLightProbeInit* light_probe_init,
    ExceptionState& exception_state) {
  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return EmptyPromise();
  }

  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::LIGHT_ESTIMATION)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({kFeatureNotSupportedBySessionPrefix,
                XRSessionFeatureToString(
                    device::mojom::XRSessionFeature::LIGHT_ESTIMATION)}));
    return EmptyPromise();
  }

  if (light_probe_init->reflectionFormat() !=
          V8XRReflectionFormat::Enum::kSrgba8 &&
      light_probe_init->reflectionFormat() !=
          V8XRReflectionFormat::Enum::kRgba16F) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({"Reflection format \"",
                light_probe_init->reflectionFormat().AsStringView(),
                "\" not supported."}));
    return EmptyPromise();
  }

  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);
  }
  return ToResolvedPromise<XRLightProbe>(script_state, world_light_probe_);
}

ScriptPromise<IDLUndefined> 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 EmptyPromise();
  }

  ForceEnd(ShutdownPolicy::kWaitForResponse);

  end_session_resolver_ =
      MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
          script_state, exception_state.GetContext());
  auto 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;

  prev_transport_delegate_ = nullptr;

  for (unsigned i = 0; i < input_sources_->length(); i++) {
    auto* input_source = (*input_sources_)[i];
    input_source->OnRemoved();
  }

  input_sources_ = empty_input_sources_;

  if (canvas_input_provider_) {
    canvas_input_provider_->Stop();
    canvas_input_provider_ = nullptr;
  }

  xr_->ExitPresent(
      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);
  xr_->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(RecommendedFramebufferScale());

    // 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 / RecommendedFramebufferScale();
  }
  return 1.0;
}

double XRSession::RecommendedFramebufferScale() const {
  // Clamp to a reasonable min/max size for the default framebuffer scale.
  return std::clamp(device_config_->default_framebuffer_scale,
                    kMinDefaultFramebufferScale, kMaxDefaultFramebufferScale);
}

gfx::SizeF XRSession::RecommendedFramebufferSize() const {
  if (!immersive()) {
    return gfx::SizeF(OutputCanvasSize());
  }

  float scale = RecommendedFramebufferScale();
  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 : views_) {
    const auto& viewport = view->Viewport();
    width += viewport.width();
    height = std::max<float>(height, viewport.height());
  }

  return gfx::SizeF(width * scale, height * scale);
}

gfx::SizeF XRSession::RecommendedArrayTextureSize() const {
  float scale = RecommendedFramebufferScale();
  float width = 0;
  float height = 0;

  // When using array textures the texture size should be determined by the
  // maximum size required for any viewport.
  for (const auto& view : views_) {
    const auto& viewport = view->Viewport();
    width = std::max<float>(width, viewport.width());
    height = std::max<float>(height, 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) {
  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;
  }

  // Force visible-blurred state to hidden until the feature to allow it is
  // enabled. This matches the long-term behavior that the feature is trying to
  // change.
  bool can_use_visible_blurred = false;
#if BUILDFLAG(ENABLE_VR)
  can_use_visible_blurred =
      base::FeatureList::IsEnabled(device::features::kWebXrVisibleBlurred);
#endif
  if (!can_use_visible_blurred && state == XRVisibilityState::VISIBLE_BLURRED) {
    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() {
  // Request frame if current or requested render state is valid.
  bool will_have_valid_render_state = render_state_->HasActiveLayer();
  for (const auto& init : pending_render_state_) {
    if (init->hasBaseLayer()) {
      will_have_valid_render_state = !!init->baseLayer();
    } else if (init->hasLayers()) {
      will_have_valid_render_state =
          init->layers() && init->layers()->size() > 0;
    }
  }

  // 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_valid_render_state;

  // 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;
  } else {
    std::stringstream ss;
    ss << __func__
       << ": Not requesting frame, pending_frame_=" << pending_frame_
       << ", page_allowed_frames= " << page_allowed_frames
       << ", page_can_process_frames=" << page_can_process_frames
       << ", page_configured_properly=" << page_configured_properly
       << ", page_wants_frame=" << page_wants_frame
       << ", frames_throttled=" << frames_throttled_;
    xr_->AddWebXrInternalsMessage(String(ss.str()));
  }
}

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_transport_delegate_);
  if (pending_render_state_.size() > 0) {
    prev_transport_delegate_ = render_state_->GetTransportDelegate();
    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();

    should_update_layers_backend_ |= render_state_->NeedLayersUpdate();

    // 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() && should_update_layers_backend_) {
      render_state_->OnResize();
      // For non-immersive sessions, calling OnResize() is sufficient to handle
      // active layer changes.
      should_update_layers_backend_ = false;
      render_state_->OnLayersUpdated();
    }

    // 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,
    device::mojom::blink::XRFrameDataPtr frame_data,
    int16_t frame_id,
    bool emulated_position) {
  TRACE_EVENT0("gpu", "UpdatePresentationFrameState");
  DVLOG(2) << __func__ << " : frame_data valid? " << (frame_data ? true : false)
           << ", emulated_position=" << emulated_position
           << ", frame_id=" << frame_id;
  // Don't process any outstanding frames once the session is ended.
  if (ended_)
    return;

  // If there are pending render state changes, apply them now, as they may
  // update the depthNear/Far used by the views.
  // TODO(crbug.com/452595360): Apply pending frame data on frame submit.
  prev_transport_delegate_ = nullptr;
  ApplyPendingRenderState();

  // Update view related data.
  if (frame_data) {
    // Views need to be updated first, so that views() has valid data.
    UpdateViews(std::move(frame_data->render_info->views));

    // Apply dynamic viewport scaling if available.
    if (supports_viewport_scaling_) {
      float gpu_load = frame_data->rendering_time_ratio;
      std::optional<double> scale = std::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);
      }
    }
  }

  // Update poses
  mojo_from_viewer_ =
      frame_data ? getPoseMatrix(frame_data->render_info->mojo_from_viewer)
                 : nullptr;

#if DCHECK_IS_ON()
  if (frame_data && mojo_from_floor_ != frame_data->mojo_from_floor) {
    gfx::Transform identity;
    DVLOG(2) << __func__ << "mojo_from_floor_ changed! Now="
             << frame_data->mojo_from_floor.value_or(identity).ToString()
             << " Was=" << mojo_from_floor_.value_or(identity).ToString();
  }
#endif  // DCHECK_IS_ON()

  mojo_from_floor_ = frame_data ? frame_data->mojo_from_floor : std::nullopt;

  DVLOG(2) << __func__ << " : mojo_from_viewer_ valid? " << !!mojo_from_viewer_
           << " mojo_from_floor_ valid? " << !!mojo_from_floor_;
  // 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;

  // Finish processing reference state data then process input and reset events.
  if (frame_data) {
    // First finish updating positioning
    UpdateStageParameters(frame_data->stage_parameters_id,
                          frame_data->stage_parameters);

    // Now update the input sources
    // Only process input when we can report it to the page.
    base::span<const device::mojom::blink::XRInputSourceStatePtr> input_states;
    if (CanReportInputPoses()) {
      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);

    // Processing input source events may send events to the page. To this end
    // we need to ensure that this is done *after* the WorldUnderstanding state
    // is updated, otherwise we would be sending out input events with the old
    // frame state, which can lead to issues with transient hit test sources.
    if (CanReportInputPoses()) {
      ProcessInputSourceEvents(input_states);
    }

    // Now that all pose data is updated trigger a reset event if it's there.
    if (frame_data->mojo_space_reset) {
      OnMojoSpaceReset();
    }

    // Check if the session was ended by the |OnMojoSpaceReset| callback.
    if (ended_) {
      return;
    }
  } else {
    UpdateWorldUnderstandingStateForFrame(timestamp, frame_data);
  }
}

ScriptPromise<IDLArray<V8XRImageTrackingScore>>
XRSession::getTrackedImageScores(ScriptState* script_state,
                                 ExceptionState& exception_state) {
  DVLOG(3) << __func__;
  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return ScriptPromise<IDLArray<V8XRImageTrackingScore>>();
  }

  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::IMAGE_TRACKING)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({kFeatureNotSupportedBySessionPrefix,
                XRSessionFeatureToString(
                    device::mojom::XRSessionFeature::IMAGE_TRACKING)}));
    return ScriptPromise<IDLArray<V8XRImageTrackingScore>>();
  }

  auto* resolver = MakeGarbageCollected<
      ScriptPromiseResolver<IDLArray<V8XRImageTrackingScore>>>(
      script_state, exception_state.GetContext());
  auto 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;
}

ScriptPromise<IDLUndefined> XRSession::initiateRoomCapture(
    ScriptState* script_state,
    ExceptionState& exception_state) {
  if (ended_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kSessionEnded);
    return EmptyPromise();
  }

  if (has_called_room_capture_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      "initiateRoomCapture already called.");
    return EmptyPromise();
  }

  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::PLANE_DETECTION)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({kFeatureNotSupportedBySessionPrefix,
                XRSessionFeatureToString(
                    device::mojom::XRSessionFeature::PLANE_DETECTION)}));
    return EmptyPromise();
  }

  has_called_room_capture_ = true;
  return ToResolvedUndefinedPromise(script_state);
}

void XRSession::ProcessTrackedImagesData(
    const device::mojom::blink::XRTrackedImagesData* images_data) {
  DVLOG(3) << __func__;

  if (!images_data) {
    frame_tracked_images_ =
        MakeGarbageCollected<FrozenArray<XRImageTrackingResult>>();
    return;
  }

  HeapVector<Member<XRImageTrackingResult>> frame_tracked_images;
  for (const auto& image : images_data->images_data) {
    DVLOG(3) << __func__ << ": image index=" << image->index;
    frame_tracked_images.push_back(
        MakeGarbageCollected<XRImageTrackingResult>(this, *image));
  }
  frame_tracked_images_ =
      MakeGarbageCollected<FrozenArray<XRImageTrackingResult>>(
          std::move(frame_tracked_images));

  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_size_t index = 0; index < scores.size(); ++index) {
      tracked_image_scores_.push_back(V8XRImageTrackingScore(
          scores[index] ? V8XRImageTrackingScore::Enum::kTrackable
                        : V8XRImageTrackingScore::Enum::kUntrackable));
      DVLOG(3) << __func__ << ": score[" << index
               << "]=" << tracked_image_scores_[index].AsCStr();
    }
    HeapVector<Member<ImageScoreResolverType>> image_score_promises;
    image_scores_resolvers_.swap(image_score_promises);
    for (auto& resolver : image_score_promises) {
      DVLOG(3) << __func__ << ": resolving promise";
      resolver->Resolve(tracked_image_scores_);
    }
    tracked_image_scores_available_ = true;
  }
}

const FrozenArray<XRImageTrackingResult>& XRSession::ImageTrackingResults(
    ExceptionState& exception_state) {
  if (!IsFeatureEnabled(device::mojom::XRSessionFeature::IMAGE_TRACKING)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({kFeatureNotSupportedBySessionPrefix,
                XRSessionFeatureToString(
                    device::mojom::XRSessionFeature::IMAGE_TRACKING)}));
    return *MakeGarbageCollected<FrozenArray<XRImageTrackingResult>>();
  }

  return *frame_tracked_images_.Get();
}

void XRSession::UpdateWorldUnderstandingStateForFrame(
    double timestamp,
    const device::mojom::blink::XRFrameDataPtr& frame_data) {
  // Update objects that might change on per-frame basis.
  if (frame_data) {
    mesh_manager_->ProcessMeshInformation(
        frame_data->detected_meshes_data.get(), timestamp);
    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());

    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_ = std::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 {
    mesh_manager_->ProcessMeshInformation(nullptr, timestamp);

    plane_manager_->ProcessPlaneInformation(nullptr, timestamp);
    ProcessAnchorsData(nullptr, timestamp);
    ProcessHitTestData(nullptr);

    ProcessTrackedImagesData(nullptr);

    if (world_light_probe_) {
      world_light_probe_->ProcessLightEstimationData(nullptr, timestamp);
    }

    camera_image_size_ = std::nullopt;
  }
}

bool XRSession::IsFeatureEnabled(
    device::mojom::XRSessionFeature feature) const {
  return enabled_feature_set_.Contains(feature);
}

void XRSession::SetMetricsReporter(std::unique_ptr<MetricsReporter> reporter) {
  DCHECK(!metrics_reporter_);
  metrics_reporter_ = std::move(reporter);
}

void XRSession::OnFrame(double timestamp,
                        Vector<XRSharedImageData> shared_images) {
  TRACE_EVENT0("gpu", "OnFrame");
  DVLOG(2) << __func__ << ": ended_=" << ended_
           << ", pending_frame_=" << pending_frame_;
  // Don't process any outstanding frames once the session is ended.
  if (ended_)
    return;

  layer_shared_image_manager_.Reset();

  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.
    if (!render_state_->HasActiveLayer()) {
      DVLOG(2) << __func__ << ": there is no active layer in a render state.";

      // 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_transport_delegate_) {
        DVLOG(2)
            << __func__
            << ": prev_transport_delegate is valid, submitting frame with it";

        if (should_update_layers_backend_) {
          should_update_layers_backend_ = false;
          // Hides all layers on the backend if the 'layers' feature is enabled.
          render_state_->UpdateLayersBackend(LayerManager());
          render_state_->OnLayersUpdated();
        }
        // Submits the current frame without running the animation frame
        // callback.
        xr_->frameProvider()->ClearCachedLayersData();
        xr_->frameProvider()->SubmitFrame(prev_transport_delegate_);
        prev_transport_delegate_ = 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;
    }

    xr_->frameProvider()->ClearCachedLayersData();

    // Transport delegate should be available because render state is valid.
    auto* transport_delegate = render_state_->GetTransportDelegate();
    CHECK(transport_delegate);

    if (shared_images.empty() && layers_enabled_) {
      DVLOG(2) << __func__ << ": there is no shared images.";
      xr_->frameProvider()->SubmitFrame(transport_delegate);
      return;
    }

    if (should_update_layers_backend_) {
      should_update_layers_backend_ = false;
      if (layers_enabled_) {
        // This means that the page has updated the layers since it last
        // received a new frame, but we haven't updated the backend yet, so the
        // page won't be able to use those layers just yet as they expect. For
        // now, drop this frame and request a new one with the updated layers,
        // which we'll then serve to the page.
        // TODO(crbug.com/452604976): Refactor the frame submission flow to
        // allow the layer sequence to be updated by the backend compositor
        // without dropping the current frame.
        render_state_->UpdateLayersBackend(LayerManager());
        render_state_->OnLayersUpdated();

        // Submit this animation frame without changes and request a new one
        // immediately.
        xr_->frameProvider()->SubmitFrame(transport_delegate);
        MaybeRequestFrame();
        return;
      }
    }

    // If the 'layers' feature is disabled, the shared image lacks an associated
    // layer ID. The shared image must then be bound to the first layer.
    layer_shared_image_manager_.SetSharedImages(
        layers_enabled_ ? nullptr : render_state_->GetFirstLayer(),
        std::move(shared_images));

    // Dispatch the "redraw" event for layers that should be updated.
    if (layers_enabled_) {
      render_state_->MaybeDispatchRedrawEvents();
    }

    // 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.
      xr_->frameProvider()->SubmitFrame(transport_delegate);
      return;
    }

    // Propagate OnFrameStart for all active layers.
    render_state_->OnFrameStart();

    CHECK(animation_frame_ == nullptr);
    animation_frame_ = CreatePresentationFrame(true);

    // 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);
    page_animation_frame_timer_.StartTimer();
    ExecuteVideoFrameCallbacks(timestamp);
    callback_collection_->ExecuteCallbacks(this, timestamp, animation_frame_);
    page_animation_frame_timer_.StopTimer();

    // Calling OnFrameEnd() on the render state will trigger each layer to
    // submit its drawing data to be cached by the frame provider.
    render_state_->OnFrameEnd();

    std::vector<gpu::SyncToken> camera_sync_tokens;
    camera_sync_tokens.reserve(graphics_bindings_.size());

    for (auto& binding : graphics_bindings_) {
      gpu::SyncToken camera_sync_token = binding->OnFrameEnd();
      if (camera_sync_token.HasData()) {
        transport_delegate->VerifySyncToken(camera_sync_token);
        camera_sync_tokens.push_back(camera_sync_token);
      }
    }

    auto shared_image =
        LayerSharedImageManager().CameraSharedImage().shared_image;
    if (shared_image && !camera_sync_tokens.empty()) {
      gpu::SharedImageExportResult camera_export_result =
          shared_image->EndImport(std::move(camera_sync_tokens));
      // Submit frame with cached layers data.
      xr_->frameProvider()->SubmitFrame(transport_delegate,
                                        std::move(camera_export_result));
    } else {
      xr_->frameProvider()->SubmitFrame(transport_delegate);
    }

    // Ensure the XRFrame cannot be used outside the callbacks.
    animation_frame_->Deactivate();
    animation_frame_ = nullptr;
  }
}

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 a "visible" type.
  return visibility_state_ != XRVisibilityState::HIDDEN;
}

bool XRSession::CanReportInputPoses() const {
  // We can only report input-based poses if we're fully visible.
  return visibility_state_ == XRVisibilityState::VISIBLE;
}

bool XRSession::CanEnableAntiAliasing() const {
  return device_config_->enable_anti_aliasing;
}

std::optional<gfx::Transform> XRSession::GetMojoFrom(
    device::mojom::blink::XRReferenceSpaceType space_type) const {
  if (!CanReportPoses()) {
    DVLOG(2) << __func__ << ": cannot report poses, returning nullopt";
    return std::nullopt;
  }

  switch (space_type) {
    case device::mojom::blink::XRReferenceSpaceType::kViewer:
      if (!mojo_from_viewer_) {
        if (sensorless_session_) {
          return gfx::Transform();
        }

        return std::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:
      return mojo_from_floor_;
    case device::mojom::blink::XRReferenceSpaceType::kBoundedFloor:
      // If we have stage_parameters_ MojoFrom(BoundedFloor) is the value of
      // mojo_from_stage.
      if (stage_parameters_) {
        return stage_parameters_->mojo_from_stage;
      }

      return std::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;
}

void XRSession::UpdateInlineView() {
  if (canvas_was_resized_) {
    views_.clear();
    canvas_was_resized_ = false;
  }
  if (views_.empty()) {
    views_.emplace_back(MakeGarbageCollected<XRViewData>(
        /*index=*/0, device::mojom::blink::XREye::kNone,
        gfx::Rect(0, 0, output_width_, output_height_), graphics_api_));
  }

  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.
  std::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());
}

// 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;

  render_state_->OnResize();

  canvas_was_resized_ = true;
  UpdateInlineView();
}

void XRSession::OnInputStateChangeInternal(
    int16_t frame_id,
    base::span<const device::mojom::blink::XRInputSourceStatePtr>
        input_states) {
  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 hit_test_source_ids_.Contains(hit_test_source->id());
}

bool XRSession::ValidateHitTestSourceExists(
    XRTransientInputHitTestSource* hit_test_source) const {
  DCHECK(hit_test_source);
  return hit_test_source_for_transient_input_ids_.Contains(
      hit_test_source->id());
}

bool XRSession::RemoveHitTestSource(XRHitTestSource* hit_test_source) {
  DVLOG(2) << __func__;

  DCHECK(hit_test_source);

  if (!hit_test_source_ids_.Contains(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 (!hit_test_source_for_transient_input_ids_.Contains(
          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() {
  return views_;
}

XRViewData* XRSession::ViewDataForEye(device::mojom::blink::XREye eye) {
  switch (eye) {
    case device::mojom::blink::XREye::kLeft:
    case device::mojom::blink::XREye::kNone:
      return views_[0].Get();
    case device::mojom::blink::XREye::kRight:
      return views_[1].Get();
  }
}

bool XRSession::HasPendingActivity() const {
  return (!callback_collection_->IsEmpty() || !vfc_execution_queue_.empty()) &&
         !ended_;
}

device::mojom::blink::XRLayerManager* XRSession::LayerManager() {
  if (ended_ || !immersive()) {
    return nullptr;
  }
  return xr()->frameProvider()->layer_manager();
}

void XRSession::OnTransferComplete(const Vector<device::LayerId>& layer_ids) {
  if (render_state_) {
    render_state_->OnTransferComplete(layer_ids);
  }
}

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(enabled_features_);
  visitor->Trace(animation_frame_);
  visitor->Trace(empty_input_sources_);
  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(callback_collection_);
  visitor->Trace(create_anchor_promises_);
  visitor->Trace(request_hit_test_source_promises_);
  visitor->Trace(graphics_bindings_);
  visitor->Trace(reference_spaces_);
  visitor->Trace(mesh_manager_);
  visitor->Trace(plane_manager_);
  visitor->Trace(anchor_ids_to_anchors_);
  visitor->Trace(anchor_ids_to_pending_anchor_promises_);
  visitor->Trace(prev_transport_delegate_);
  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_);
  EventTarget::Trace(visitor);
}

}  // namespace blink
