|  | // Copyright 2025 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "device/vr/openxr/openxr_spatial_anchor_manager.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <array> | 
|  | #include <optional> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/containers/contains.h" | 
|  | #include "base/functional/callback_helpers.h" | 
|  | #include "base/types/expected.h" | 
|  | #include "device/vr/openxr/openxr_api_wrapper.h" | 
|  | #include "device/vr/openxr/openxr_extension_helper.h" | 
|  | #include "device/vr/openxr/openxr_platform.h" | 
|  | #include "device/vr/openxr/openxr_spatial_framework_manager.h" | 
|  | #include "device/vr/openxr/openxr_spatial_plane_manager.h" | 
|  | #include "device/vr/openxr/openxr_spatial_utils.h" | 
|  | #include "device/vr/openxr/openxr_util.h" | 
|  | #include "device/vr/openxr/scoped_openxr_object.h" | 
|  | #include "third_party/abseil-cpp/absl/container/flat_hash_map.h" | 
|  | #include "third_party/abseil-cpp/absl/container/flat_hash_set.h" | 
|  | #include "third_party/openxr/src/include/openxr/openxr.h" | 
|  |  | 
|  | namespace device { | 
|  |  | 
|  | // static | 
|  | bool OpenXrSpatialAnchorManager::IsSupported( | 
|  | const std::vector<XrSpatialCapabilityEXT>& capabilities) { | 
|  | // The only component that we need to support anchors is | 
|  | // XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT, which is guaranteed to be supported | 
|  | // if the XR_SPATIAL_CAPABILITY_ANCHOR_EXT is supported, so that's all we need | 
|  | // to check. | 
|  | return base::Contains(capabilities, XR_SPATIAL_CAPABILITY_ANCHOR_EXT); | 
|  | } | 
|  |  | 
|  | OpenXrSpatialAnchorManager::OpenXrSpatialAnchorManager( | 
|  | const OpenXrExtensionHelper& extension_helper, | 
|  | const OpenXrSpatialFrameworkManager& spatial_framework_manager, | 
|  | OpenXrSpatialPlaneManager* plane_manager, | 
|  | XrSpace mojo_space) | 
|  | : extension_helper_(extension_helper), | 
|  | spatial_framework_manager_(spatial_framework_manager), | 
|  | plane_manager_(plane_manager), | 
|  | mojo_space_(mojo_space) {} | 
|  |  | 
|  | OpenXrSpatialAnchorManager::~OpenXrSpatialAnchorManager() { | 
|  | for (auto const& [id, anchor] : anchors_) { | 
|  | extension_helper_->ExtensionMethods().xrDestroySpatialEntityEXT( | 
|  | anchor.entity); | 
|  | } | 
|  | } | 
|  |  | 
|  | void OpenXrSpatialAnchorManager::PopulateCapabilityConfiguration( | 
|  | absl::flat_hash_map<XrSpatialCapabilityEXT, | 
|  | absl::flat_hash_set<XrSpatialComponentTypeEXT>>& | 
|  | capability_configuration) const { | 
|  | // Operator[] creates an empty entry if it does not exist. | 
|  | capability_configuration[XR_SPATIAL_CAPABILITY_ANCHOR_EXT].insert( | 
|  | XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT); | 
|  | } | 
|  |  | 
|  | AnchorId OpenXrSpatialAnchorManager::CreateAnchor( | 
|  | XrPosef pose, | 
|  | XrSpace space, | 
|  | XrTime predicted_display_time, | 
|  | std::optional<PlaneId> plane_id) { | 
|  | XrSpatialAnchorCreateInfoEXT create_info{ | 
|  | XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_EXT}; | 
|  | create_info.baseSpace = space; | 
|  | create_info.time = predicted_display_time; | 
|  | create_info.pose = pose; | 
|  |  | 
|  | SpatialAnchorData anchor_data; | 
|  | if (XR_FAILED(extension_helper_->ExtensionMethods().xrCreateSpatialAnchorEXT( | 
|  | spatial_framework_manager_->GetSpatialContext(), &create_info, | 
|  | &anchor_data.entity_id, &anchor_data.entity))) { | 
|  | return kInvalidAnchorId; | 
|  | } | 
|  |  | 
|  | AnchorId anchor_id = anchor_id_generator_.GenerateNextId(); | 
|  | CHECK(anchor_id); | 
|  | anchors_.emplace(anchor_id, anchor_data); | 
|  | entity_id_to_anchor_id_.emplace(anchor_data.entity_id, anchor_id); | 
|  | return anchor_id; | 
|  | } | 
|  |  | 
|  | void OpenXrSpatialAnchorManager::DetachAnchor(AnchorId anchor_id) { | 
|  | auto it = anchors_.find(anchor_id); | 
|  | if (it == anchors_.end()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | extension_helper_->ExtensionMethods().xrDestroySpatialEntityEXT( | 
|  | it->second.entity); | 
|  | entity_id_to_anchor_id_.erase(it->second.entity_id); | 
|  | anchors_.erase(it); | 
|  | cached_anchor_poses_.erase(anchor_id); | 
|  | } | 
|  |  | 
|  | std::optional<OpenXrAnchorManager::XrLocation> | 
|  | OpenXrSpatialAnchorManager::GetXrLocationFromAnchor( | 
|  | AnchorId anchor_id, | 
|  | const gfx::Transform& anchor_id_from_new_anchor) const { | 
|  | auto it = cached_anchor_poses_.find(anchor_id); | 
|  | if (it == cached_anchor_poses_.end() || !it->second.has_value()) { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | // The cached anchor poses are in |mojo_space_| hence mojo_from_anchor_id. | 
|  | gfx::Transform mojo_from_anchor_id = it->second->ToTransform(); | 
|  | gfx::Transform mojo_from_new_anchor = | 
|  | mojo_from_anchor_id * anchor_id_from_new_anchor; | 
|  |  | 
|  | return XrLocation{GfxTransformToXrPose(mojo_from_new_anchor), mojo_space_}; | 
|  | } | 
|  |  | 
|  | std::optional<OpenXrAnchorManager::XrLocation> | 
|  | OpenXrSpatialAnchorManager::GetXrLocationFromPlane( | 
|  | PlaneId plane_id, | 
|  | const gfx::Transform& plane_id_from_new_anchor) const { | 
|  | // It should actually be impossible to *not* have a Plane Manager, since the | 
|  | // PlaneId had to come from somewhere, but it could have been spoofed. | 
|  | if (!plane_manager_) { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | // We don't have an xr_space_ for the plane, so we'll just locate the pose | 
|  | // in mojo_space_ and send that up as the base of the XrLocation. | 
|  | std::optional<device::Pose> mojo_from_plane = | 
|  | plane_manager_->TryGetMojoFromPlane(plane_id); | 
|  | if (!mojo_from_plane) { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | gfx::Transform mojo_from_new_anchor = | 
|  | mojo_from_plane->ToTransform() * plane_id_from_new_anchor; | 
|  | return XrLocation{GfxTransformToXrPose(mojo_from_new_anchor), mojo_space_}; | 
|  | } | 
|  |  | 
|  | mojom::XRAnchorsDataPtr OpenXrSpatialAnchorManager::GetCurrentAnchorsData( | 
|  | XrTime predicted_display_time) { | 
|  | cached_anchor_poses_.clear(); | 
|  | if (anchors_.empty()) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | std::vector<XrSpatialEntityEXT> entities; | 
|  | entities.reserve(anchors_.size()); | 
|  | for (auto const& [_, anchor] : anchors_) { | 
|  | entities.push_back(anchor.entity); | 
|  | } | 
|  |  | 
|  | XrSpatialUpdateSnapshotCreateInfoEXT snapshot_create_info{ | 
|  | XR_TYPE_SPATIAL_UPDATE_SNAPSHOT_CREATE_INFO_EXT}; | 
|  | snapshot_create_info.entityCount = entities.size(); | 
|  | snapshot_create_info.entities = entities.data(); | 
|  | snapshot_create_info.baseSpace = mojo_space_; | 
|  | snapshot_create_info.time = predicted_display_time; | 
|  |  | 
|  | ScopedOpenXrObject<XrSpatialSnapshotEXT> snapshot(extension_helper_.get()); | 
|  | if (XR_FAILED(extension_helper_->ExtensionMethods() | 
|  | .xrCreateSpatialUpdateSnapshotEXT( | 
|  | spatial_framework_manager_->GetSpatialContext(), | 
|  | &snapshot_create_info, snapshot.receive()))) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | constexpr std::array<XrSpatialComponentTypeEXT, 1> kComponentTypes = { | 
|  | XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT}; | 
|  | XrSpatialComponentDataQueryConditionEXT query_cond{ | 
|  | XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT}; | 
|  | query_cond.componentTypeCount = kComponentTypes.size(); | 
|  | query_cond.componentTypes = kComponentTypes.data(); | 
|  |  | 
|  | // Locations is an out parameter, so pre-fill it with empty poses. | 
|  | std::vector<XrPosef> locations(anchors_.size()); | 
|  | XrSpatialComponentAnchorListEXT location_list{ | 
|  | XR_TYPE_SPATIAL_COMPONENT_ANCHOR_LIST_EXT}; | 
|  | location_list.locationCount = locations.size(); | 
|  | location_list.locations = locations.data(); | 
|  |  | 
|  | XrSpatialComponentDataQueryResultEXT query_result{ | 
|  | XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT}; | 
|  | query_result.next = &location_list; | 
|  |  | 
|  | // tracking_states is an out parameter, so pre-fill it with empty poses. | 
|  | std::vector<XrSpatialEntityTrackingStateEXT> tracking_states(anchors_.size()); | 
|  | query_result.entityStateCapacityInput = tracking_states.size(); | 
|  | query_result.entityStates = tracking_states.data(); | 
|  |  | 
|  | // entity_ids is an out parameter, so pre-fill it with empty poses. | 
|  | std::vector<XrSpatialEntityIdEXT> entity_ids(anchors_.size()); | 
|  | query_result.entityIdCapacityInput = entity_ids.size(); | 
|  | query_result.entityIds = entity_ids.data(); | 
|  |  | 
|  | if (XR_FAILED( | 
|  | extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT( | 
|  | snapshot.get(), &query_cond, &query_result))) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | // No matter what, we will send all current anchors up to the page, and then | 
|  | // we will remove any deleted ones for the next frame. | 
|  | std::vector<AnchorId> all_anchors_ids; | 
|  | std::vector<mojom::XRAnchorDataPtr> updated_anchors; | 
|  | all_anchors_ids.reserve(anchors_.size()); | 
|  | updated_anchors.reserve(anchors_.size()); | 
|  | absl::flat_hash_set<AnchorId> permanently_stopped_anchors; | 
|  |  | 
|  | // Per the spec, the order of entities in the `entity_ids` list matches the | 
|  | // order of the entities in both the tracking states and locations lists. | 
|  | for (uint32_t i = 0; i < query_result.entityIdCountOutput; i++) { | 
|  | auto it = entity_id_to_anchor_id_.find(entity_ids[i]); | 
|  | if (it == entity_id_to_anchor_id_.end()) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | AnchorId anchor_id = it->second; | 
|  | all_anchors_ids.push_back(anchor_id); | 
|  |  | 
|  | switch (tracking_states[i]) { | 
|  | case XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT: { | 
|  | // If the anchor is tracked, it has a valid pose, and we should cache it | 
|  | // and then send that pose as the updated location. | 
|  | auto pose = XrPoseToDevicePose(locations[i]); | 
|  | cached_anchor_poses_.emplace(anchor_id, pose); | 
|  | updated_anchors.push_back(mojom::XRAnchorData::New(anchor_id, pose)); | 
|  | break; | 
|  | } | 
|  | case XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT: { | 
|  | // If it's paused we'll still put it in the cache as it may come back, | 
|  | // but we don't know a pose for it at present. | 
|  | cached_anchor_poses_.emplace(anchor_id, std::nullopt); | 
|  | updated_anchors.push_back( | 
|  | mojom::XRAnchorData::New(anchor_id, std::nullopt)); | 
|  | break; | 
|  | } | 
|  | case XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT: { | 
|  | // Permanently stopped anchors will never become locatable again. Still | 
|  | // send up a pose for this frame, but don't cache it for future lookups | 
|  | // and remove it from the list after we're done processing. | 
|  | permanently_stopped_anchors.insert(anchor_id); | 
|  | updated_anchors.push_back( | 
|  | mojom::XRAnchorData::New(anchor_id, std::nullopt)); | 
|  | break; | 
|  | } | 
|  | case XR_SPATIAL_ENTITY_TRACKING_STATE_MAX_ENUM_EXT: | 
|  | NOTREACHED(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check that every anchor we are tracking is accounted for in the query | 
|  | // results. If we are tracking an anchor that was not returned in the query, | 
|  | // it's in an unknown state, so we should treat it as paused. | 
|  | if (cached_anchor_poses_.size() + permanently_stopped_anchors.size() != | 
|  | anchors_.size()) { | 
|  | DVLOG(1) << __func__ << " Not all tracked anchors were updated!"; | 
|  | for (const auto& [id, anchor_data] : anchors_) { | 
|  | if (!base::Contains(cached_anchor_poses_, id) && | 
|  | !base::Contains(permanently_stopped_anchors, id)) { | 
|  | DVLOG(3) << __func__ | 
|  | << " Did not receive an update for: " << id.GetUnsafeValue(); | 
|  | cached_anchor_poses_.emplace(id, std::nullopt); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | for (auto const& id : permanently_stopped_anchors) { | 
|  | DetachAnchor(id); | 
|  | } | 
|  |  | 
|  | return mojom::XRAnchorsData::New(std::move(all_anchors_ids), | 
|  | std::move(updated_anchors)); | 
|  | } | 
|  |  | 
|  | }  // namespace device |