blob: b2954ead837513447229a04fc56694a2e5839e4a [file] [log] [blame]
// 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_plane_manager.h"
#include "base/containers/contains.h"
#include "device/vr/openxr/openxr_api_wrapper.h"
#include "device/vr/openxr/openxr_extension_helper.h"
#include "device/vr/openxr/openxr_spatial_framework_manager.h"
#include "device/vr/openxr/openxr_spatial_utils.h"
#include "device/vr/openxr/openxr_util.h"
#include "ui/gfx/geometry/transform.h"
namespace device {
namespace {
mojom::XRPlaneOrientation ToMojomPlaneOrientation(
const XrSpatialPlaneAlignmentEXT& alignment) {
switch (alignment) {
case XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_UPWARD_EXT:
case XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD_EXT:
return mojom::XRPlaneOrientation::HORIZONTAL;
case XR_SPATIAL_PLANE_ALIGNMENT_VERTICAL_EXT:
return mojom::XRPlaneOrientation::VERTICAL;
default:
return mojom::XRPlaneOrientation::UNKNOWN;
}
}
std::vector<XrSpatialComponentTypeEXT> GetAttachableComponentTypes(
const OpenXrExtensionMethods& extension_methods,
XrInstance instance,
XrSystemId system) {
// This is an optional extension.
if (extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID ==
nullptr) {
return {};
}
uint32_t attachable_component_count;
if (XR_FAILED(
extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID(
instance, system, 0, &attachable_component_count, nullptr))) {
return {};
}
std::vector<XrSpatialComponentTypeEXT> attachable_components(
attachable_component_count);
if (XR_FAILED(
extension_methods.xrEnumerateSpatialAnchorAttachableComponentsANDROID(
instance, system,
static_cast<uint32_t>(attachable_components.size()),
&attachable_component_count, attachable_components.data()))) {
attachable_components.clear();
}
return attachable_components;
}
} // namespace
// static
bool OpenXrSpatialPlaneManager::IsSupported(
const std::vector<XrSpatialCapabilityEXT>& capabilities) {
// The only components that we need to support planes are
// XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT and
// XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT which are guaranteed to be
// supported if the XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT is supported, so
// that's all we need to check.
return base::Contains(capabilities, XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);
}
OpenXrSpatialPlaneManager::OpenXrSpatialPlaneManager(
XrSpace mojo_space,
const OpenXrExtensionHelper& extension_helper,
const OpenXrSpatialFrameworkManager& framework_manager,
XrInstance instance,
XrSystemId system)
: mojo_space_(mojo_space),
extension_helper_(extension_helper),
framework_manager_(framework_manager),
enabled_components_({// Begin by enabling the two components required to
// be present for the
// XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT
XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT,
XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT}) {
std::vector<XrSpatialComponentTypeEXT> plane_tracking_components =
GetSupportedComponentTypes(
extension_helper_->ExtensionMethods()
.xrEnumerateSpatialCapabilityComponentTypesEXT,
instance, system, XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);
std::vector<XrSpatialComponentTypeEXT> attachable_components =
GetAttachableComponentTypes(extension_helper_->ExtensionMethods(),
instance, system);
// If any of our attachable components are in the supportable plane components
// list, we can attach anchors to planes.
auto first_attachable_component = std::find_if(
attachable_components.begin(), attachable_components.end(),
[&plane_tracking_components](XrSpatialComponentTypeEXT component) {
return base::Contains(plane_tracking_components, component);
});
if (first_attachable_component != attachable_components.end()) {
// In order to properly support parenting, we have to ensure the component
// of ours that is attachable is enabled. First check if any of our planned
// to be enabled components are attachable.
bool attachable_component_enabled = std::any_of(
enabled_components_.begin(), enabled_components_.end(),
[&attachable_components](XrSpatialComponentTypeEXT component) {
return base::Contains(attachable_components, component);
});
// If not, let's enable the first attachable component that we found, since
// any of them will do. It's only use will be to enable parenting, we don't
// actually need to query for it.
if (!attachable_component_enabled) {
enabled_components_.insert(*first_attachable_component);
}
can_parent_anchors_ = true;
}
}
OpenXrSpatialPlaneManager::~OpenXrSpatialPlaneManager() = default;
void OpenXrSpatialPlaneManager::PopulateCapabilityConfiguration(
absl::flat_hash_map<XrSpatialCapabilityEXT,
absl::flat_hash_set<XrSpatialComponentTypeEXT>>&
capability_configuration) const {
capability_configuration[XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT].insert(
enabled_components_.begin(), enabled_components_.end());
}
void OpenXrSpatialPlaneManager::OnSnapshotChanged() {
const XrSpatialSnapshotEXT snapshot =
framework_manager_->GetDiscoverySnapshot();
if (snapshot == XR_NULL_HANDLE) {
return;
}
// Query the snapshot for all entities that have the necessary plane
// components. Note that we don't use enabled_components_ here, because these
// are the only values we actually care about.
XrSpatialComponentDataQueryConditionEXT query_condition{
XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT};
std::vector<XrSpatialComponentTypeEXT> component_types = {
XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT,
XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT};
query_condition.componentTypeCount = component_types.size();
query_condition.componentTypes = component_types.data();
// First need to query for how many results there are, then we can build the
// arrays to populate.
XrSpatialComponentDataQueryResultEXT query_result{
XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT};
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
snapshot, &query_condition, &query_result))) {
return;
}
std::vector<XrSpatialEntityIdEXT> entity_ids(
query_result.entityIdCountOutput);
query_result.entityIdCapacityInput = entity_ids.size();
query_result.entityIds = entity_ids.data();
std::vector<XrSpatialEntityTrackingStateEXT> entity_states(
query_result.entityIdCountOutput);
query_result.entityStateCapacityInput = entity_states.size();
query_result.entityStates = entity_states.data();
std::vector<XrSpatialPlaneAlignmentEXT> plane_alignments(
query_result.entityIdCountOutput);
XrSpatialComponentPlaneAlignmentListEXT plane_alignment_list{
.type = XR_TYPE_SPATIAL_COMPONENT_PLANE_ALIGNMENT_LIST_EXT,
.planeAlignmentCount = static_cast<uint32_t>(plane_alignments.size()),
.planeAlignments = plane_alignments.data()};
std::vector<XrSpatialBounded2DDataEXT> bounded_2d_data(
query_result.entityIdCountOutput);
XrSpatialComponentBounded2DListEXT bounded_2d_list{
.type = XR_TYPE_SPATIAL_COMPONENT_BOUNDED_2D_LIST_EXT,
.next = &plane_alignment_list,
.boundCount = static_cast<uint32_t>(bounded_2d_data.size()),
.bounds = bounded_2d_data.data()};
query_result.next = &bounded_2d_list;
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
snapshot, &query_condition, &query_result))) {
return;
}
// Reset our list of updated planes. We could potentially be clearing a plane
// that we said had a pending update but now we don't know about. Since we no
// longer know about it, then we shouldn't be reporting it.
updated_entity_ids_.clear();
absl::flat_hash_set<XrSpatialEntityIdEXT> paused_entity_ids;
for (uint32_t i = 0; i < query_result.entityIdCountOutput; i++) {
XrSpatialEntityIdEXT entity_id = entity_ids[i];
// We don't need to send up any information about stopped planes, and since
// planes could be subsumed, we'll just process and clear outdated entries
// every time as well.
// We'll note paused planes differently. They won't count as updated this
// frame, but we'll keep them around/existing.
if (entity_states[i] == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
paused_entity_ids.insert(entity_id);
continue;
}
if (entity_states[i] != XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
continue;
}
// If we don't have an entry for this entity ID, populate the data for it.
if (!entity_id_to_data_.contains(entity_id)) {
entity_id_to_data_[entity_id] = mojom::XRPlaneData::New();
}
updated_entity_ids_.insert(entity_id);
mojom::XRPlaneDataPtr& plane_data = entity_id_to_data_[entity_id];
// Can't use `GetPlaneId` until our entity_id is in the map.
plane_data->id = GetPlaneId(entity_id);
plane_data->orientation = ToMojomPlaneOrientation(plane_alignments[i]);
// The incoming pose has the Z axis as the normal, but WebXR expects the Y
// axis to be the normal.
plane_data->mojo_from_plane =
ZNormalXrPoseToYNormalDevicePose(bounded_2d_data[i].center);
// For now we don't support polygons, so we just create a rectangle from the
// extents.
const auto& extents = bounded_2d_data[i].extents;
plane_data->polygon.clear();
plane_data->polygon.push_back(
mojom::XRPlanePointData::New(-extents.width / 2, -extents.height / 2));
plane_data->polygon.push_back(
mojom::XRPlanePointData::New(extents.width / 2, -extents.height / 2));
plane_data->polygon.push_back(
mojom::XRPlanePointData::New(extents.width / 2, extents.height / 2));
plane_data->polygon.push_back(
mojom::XRPlanePointData::New(-extents.width / 2, extents.height / 2));
}
// Remove any planes that are no longer being tracked.
auto it = entity_id_to_data_.begin();
while (it != entity_id_to_data_.end()) {
// If a plane was updated or marked as paused, keep it around. Otherwise,
// it was either not reported or reported as stopped, so delete it.
if (updated_entity_ids_.contains(it->first) ||
paused_entity_ids.contains(it->first)) {
it++;
} else {
entity_id_to_data_.erase(it++);
}
}
}
mojom::XRPlaneDetectionDataPtr
OpenXrSpatialPlaneManager::GetDetectedPlanesData() {
auto planes_data = mojom::XRPlaneDetectionData::New();
for (const auto& [entity_id, data] : entity_id_to_data_) {
planes_data->all_planes_ids.push_back(GetPlaneId(entity_id));
if (updated_entity_ids_.contains(entity_id)) {
planes_data->updated_planes_data.push_back(data.Clone());
}
}
updated_entity_ids_.clear();
return planes_data;
}
std::optional<device::Pose> OpenXrSpatialPlaneManager::TryGetMojoFromPlane(
PlaneId plane_id) const {
auto it = entity_id_to_data_.find(GetEntityId(plane_id));
if (it == entity_id_to_data_.end() || !it->second->mojo_from_plane) {
return std::nullopt;
}
return it->second->mojo_from_plane;
}
PlaneId OpenXrSpatialPlaneManager::GetPlaneId(
XrSpatialEntityIdEXT entity_id) const {
if (entity_id == XR_NULL_SPATIAL_ENTITY_ID_EXT ||
!entity_id_to_data_.contains(entity_id)) {
return kInvalidPlaneId;
}
return PlaneId(static_cast<uint64_t>(entity_id));
}
XrSpatialEntityIdEXT OpenXrSpatialPlaneManager::GetEntityId(
PlaneId plane_id) const {
if (plane_id == kInvalidPlaneId) {
return XR_NULL_SPATIAL_ENTITY_ID_EXT;
}
auto entity_id = static_cast<XrSpatialEntityIdEXT>(plane_id.GetUnsafeValue());
if (!entity_id_to_data_.contains(entity_id)) {
return XR_NULL_SPATIAL_ENTITY_ID_EXT;
}
return entity_id;
}
std::optional<XrLocation> OpenXrSpatialPlaneManager::GetXrLocationFromPlane(
PlaneId plane_id,
const gfx::Transform& plane_id_from_object) const {
// 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 = TryGetMojoFromPlane(plane_id);
if (!mojo_from_plane) {
return std::nullopt;
}
gfx::Transform mojo_from_new_anchor =
mojo_from_plane->ToTransform() * plane_id_from_object;
return XrLocation{GfxTransformToXrPose(mojo_from_new_anchor), mojo_space_};
}
} // namespace device