blob: 4e6a0fefef4e72f163677675b965bf27391781a4 [file]
// 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 <algorithm>
#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 std::ranges::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,
bool mesh_detection_enabled)
: mojo_space_(mojo_space),
extension_helper_(extension_helper),
framework_manager_(framework_manager),
mesh_detection_enabled_(mesh_detection_enabled),
enabled_components_({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);
polygon_enabled_ = std::ranges::contains(
plane_tracking_components, XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
if (polygon_enabled_) {
enabled_components_.insert(XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
}
semantic_label_enabled_ =
std::ranges::contains(plane_tracking_components,
XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
if (semantic_label_enabled_) {
enabled_components_.insert(
XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
}
if (mesh_detection_enabled_) {
bool mesh2d_supported = std::ranges::contains(
plane_tracking_components, XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT);
if (mesh2d_supported) {
enabled_components_.insert(XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT);
DVLOG(1) << __func__ << ": Mesh2D component is SUPPORTED and will "
<< "be enabled under PLANE_TRACKING.";
} else {
DVLOG(1) << __func__ << ": Mesh2D component is NOT supported for "
<< "PLANE_TRACKING on this runtime.";
}
}
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 std::ranges::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 std::ranges::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};
if (polygon_enabled_) {
component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
}
if (semantic_label_enabled_) {
component_types.push_back(
XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_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();
XrNextChainBuilder next_chain(&query_result);
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()};
next_chain.Add(&plane_alignment_list);
std::vector<XrSpatialBounded2DDataEXT> bounded_2d_data(
query_result.entityIdCountOutput);
XrSpatialComponentBounded2DListEXT bounded_2d_list{
.type = XR_TYPE_SPATIAL_COMPONENT_BOUNDED_2D_LIST_EXT,
.boundCount = static_cast<uint32_t>(bounded_2d_data.size()),
.bounds = bounded_2d_data.data()};
next_chain.Add(&bounded_2d_list);
std::vector<XrSpatialPolygon2DDataEXT> polygons;
XrSpatialComponentPolygon2DListEXT polygon_list{
XR_TYPE_SPATIAL_COMPONENT_POLYGON_2D_LIST_EXT};
if (polygon_enabled_) {
polygons.resize(query_result.entityIdCountOutput);
polygon_list.polygonCount = static_cast<uint32_t>(polygons.size());
polygon_list.polygons = polygons.data();
next_chain.Add(&polygon_list);
}
std::vector<XrSpatialPlaneSemanticLabelEXT> semantic_labels;
XrSpatialComponentPlaneSemanticLabelListEXT semantic_label_list{
XR_TYPE_SPATIAL_COMPONENT_PLANE_SEMANTIC_LABEL_LIST_EXT};
if (semantic_label_enabled_) {
semantic_labels.resize(query_result.entityIdCountOutput);
semantic_label_list.semanticLabelCount =
static_cast<uint32_t>(semantic_labels.size());
semantic_label_list.semanticLabels = semantic_labels.data();
next_chain.Add(&semantic_label_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();
mesh_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]);
if (semantic_label_enabled_) {
plane_data->semantic_label = ToMojomSemanticLabel(semantic_labels[i]);
}
plane_data->polygon.clear();
bool has_polygon = false;
if (polygon_enabled_ && i < polygons.size()) {
has_polygon = GetPolygonFromBuffer(snapshot, polygons[i], plane_data);
}
if (!has_polygon) {
GetPolygonFromExtent(bounded_2d_data[i], plane_data);
}
// Synthesize mesh data from the same plane entity.
if (mesh_detection_enabled_) {
if (!mesh_entity_id_to_data_.contains(entity_id)) {
mesh_entity_id_to_data_[entity_id] = mojom::XRMeshData::New();
}
mesh_updated_entity_ids_.insert(entity_id);
mojom::XRMeshDataPtr& mesh_data = mesh_entity_id_to_data_[entity_id];
mesh_data->id = GetMeshId(entity_id);
mesh_data->semantic_label = plane_data->semantic_label;
mesh_data->mojo_from_mesh = plane_data->mojo_from_plane;
mesh_data->vertices.clear();
mesh_data->indices.clear();
const auto& polygon = plane_data->polygon;
for (const auto& point : polygon) {
mesh_data->vertices.push_back(point->x);
mesh_data->vertices.push_back(point->z);
mesh_data->vertices.push_back(0.0f);
}
// TODO(crbug.com/499239200): Ensure the plane polygons returned by OpenXR Runtime are guaranteed to be convex
if (IsConvexPolygon(polygon)) {
for (uint32_t j = 1; j + 1 < polygon.size(); ++j) {
mesh_data->indices.push_back(0);
mesh_data->indices.push_back(j);
mesh_data->indices.push_back(j + 1);
}
} else {
mesh_data->indices = EarClipTriangulate(polygon);
}
}
}
// 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++);
}
}
if (mesh_detection_enabled_) {
auto mesh_it = mesh_entity_id_to_data_.begin();
while (mesh_it != mesh_entity_id_to_data_.end()) {
if (mesh_updated_entity_ids_.contains(mesh_it->first) ||
paused_entity_ids.contains(mesh_it->first)) {
++mesh_it;
} else {
mesh_entity_id_to_data_.erase(mesh_it++);
}
}
}
}
bool OpenXrSpatialPlaneManager::GetPolygonFromBuffer(
XrSpatialSnapshotEXT snapshot,
const XrSpatialPolygon2DDataEXT& polygon_data,
mojom::XRPlaneDataPtr& plane_data) const {
// There is currently a runtime error where some runtimes may report the
// presence of a polygon, but not actually have populated the buffer yet.
// Before querying for the spatial buffer, ensure we have a valid buffer id.
if (polygon_data.vertexBuffer.bufferId == XR_NULL_SPATIAL_BUFFER_ID_EXT) {
DLOG(ERROR) << __func__
<< " Runtime reported polygon, but returned invalid buffer id";
return false;
}
XrSpatialBufferGetInfoEXT buffer_info{XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT};
buffer_info.bufferId = polygon_data.vertexBuffer.bufferId;
uint32_t buffer_count_output = 0;
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrGetSpatialBufferVector2fEXT(
snapshot, &buffer_info, 0, &buffer_count_output, nullptr))) {
return false;
}
std::vector<XrVector2f> vertices(buffer_count_output);
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrGetSpatialBufferVector2fEXT(
snapshot, &buffer_info, buffer_count_output, &buffer_count_output,
vertices.data()))) {
return false;
}
// 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(polygon_data.origin);
// OpenXR provides a counterclockwise polygon that is guaranteed to not self
// intersect; however, we do need to transform from XY space to XZ space as
// expected by the spec.
for (const auto& vertex : vertices) {
// Vertices are 2D (X, Y) in the polygon's space (Z=0).
// We need to transform them to WebXR's plane space (Y-up).
// Construct a point for the vertex.
gfx::Point3F vertex_point = {vertex.x, vertex.y, 0};
// Transform from Z-normal to Y-normal.
auto webxr_vertex_pose = ZNormalPositionToYNormalPosition(vertex_point);
// Now that it's Y-Normal, Y should be 0, and we send up the expected XZ
// coordinates.
plane_data->polygon.push_back(mojom::XRPlanePointData::New(
webxr_vertex_pose.x(), webxr_vertex_pose.z()));
}
return true;
}
void OpenXrSpatialPlaneManager::GetPolygonFromExtent(
const XrSpatialBounded2DDataEXT& bounded_2d_data,
mojom::XRPlaneDataPtr& plane_data) const {
// 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.center);
plane_data->polygon.clear();
// Create a rectangle from the extents with a counter-clockwise winding.
const auto& extents = bounded_2d_data.extents;
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));
}
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_};
}
mojom::XRMeshDetectionDataPtr
OpenXrSpatialPlaneManager::GetDetectedMeshesData(XrTime frame_time,
XrSpace view_space) {
auto meshes_data = mojom::XRMeshDetectionData::New();
for (const auto& [entity_id, data] : mesh_entity_id_to_data_) {
meshes_data->all_meshes_ids.emplace_back(GetMeshId(entity_id));
if (mesh_updated_entity_ids_.contains(entity_id)) {
meshes_data->updated_meshes_data.push_back(data.Clone());
}
}
mesh_updated_entity_ids_.clear();
return meshes_data;
}
std::optional<XrLocation> OpenXrSpatialPlaneManager::GetXrLocationFromMesh(
MeshId mesh_id,
const gfx::Transform& mesh_id_from_object) const {
// TODO(crbug.com/498979573): Implement mesh-based native origin lookup.
return std::nullopt;
}
void OpenXrSpatialPlaneManager::OnReferenceSpaceChanged() {
mesh_entity_id_to_data_.clear();
mesh_updated_entity_ids_.clear();
}
std::optional<device::Pose> OpenXrSpatialPlaneManager::TryGetMojoFromMesh(
MeshId mesh_id) const {
auto it = mesh_entity_id_to_data_.find(GetMeshEntityId(mesh_id));
if (it == mesh_entity_id_to_data_.end() || !it->second->mojo_from_mesh) {
return std::nullopt;
}
return it->second->mojo_from_mesh;
}
MeshId OpenXrSpatialPlaneManager::GetMeshId(
XrSpatialEntityIdEXT entity_id) const {
if (entity_id == XR_NULL_SPATIAL_ENTITY_ID_EXT ||
!mesh_entity_id_to_data_.contains(entity_id)) {
return kInvalidMeshId;
}
return MeshId(static_cast<uint64_t>(entity_id));
}
XrSpatialEntityIdEXT OpenXrSpatialPlaneManager::GetMeshEntityId(
MeshId mesh_id) const {
if (mesh_id == kInvalidMeshId) {
return XR_NULL_SPATIAL_ENTITY_ID_EXT;
}
auto entity_id = static_cast<XrSpatialEntityIdEXT>(mesh_id.GetUnsafeValue());
if (!mesh_entity_id_to_data_.contains(entity_id)) {
return XR_NULL_SPATIAL_ENTITY_ID_EXT;
}
return entity_id;
}
} // namespace device