blob: bb108116ee85e43fdf19ddcfe1712ed84193902e [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_hit_test_manager.h"
#include <algorithm>
#include "base/containers/contains.h"
#include "device/vr/openxr/openxr_spatial_capability_configuration_base.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/openxr/dev/xr_android.h"
namespace device {
namespace {
struct SpatialHitTestResult {
XrSpatialEntityIdEXT id;
device::Pose pose;
float distance_squared;
};
bool SupportsPlaneBasedHitTest(
XrInstance instance,
XrSystemId system,
PFN_xrEnumerateSpatialCapabilityComponentTypesEXT
xrEnumerateSpatialCapabilityComponentTypesEXT,
const std::vector<XrSpatialCapabilityEXT>& capabilities) {
// To check for plane based hit test, we'll need to check for the
// XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID component on
// XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT, which is not guaranteed.
if (!base::Contains(capabilities, XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT)) {
return false;
}
std::vector<XrSpatialComponentTypeEXT> plane_tracking_components =
GetSupportedComponentTypes(xrEnumerateSpatialCapabilityComponentTypesEXT,
instance, system,
XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);
return base::Contains(plane_tracking_components,
XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID);
}
} // namespace
// static
bool OpenXrSpatialHitTestManager::IsSupported(
XrInstance instance,
XrSystemId system,
PFN_xrEnumerateSpatialCapabilityComponentTypesEXT
xrEnumerateSpatialCapabilityComponentTypesEXT,
const std::vector<XrSpatialCapabilityEXT>& capabilities) {
// The only component that we need to support depth-based hit tests is
// XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID, which is guaranteed to be
// supported if the XR_SPATIAL_CAPABILITY_DEPTH_RAYCAST_ANDROID is supported,
// so that's all we need to check for it.
const bool supports_depth_hit_test =
base::Contains(capabilities, XR_SPATIAL_CAPABILITY_DEPTH_RAYCAST_ANDROID);
const bool supports_plane_based_hit_test = SupportsPlaneBasedHitTest(
instance, system, xrEnumerateSpatialCapabilityComponentTypesEXT,
capabilities);
return supports_depth_hit_test || supports_plane_based_hit_test;
}
OpenXrSpatialHitTestManager::OpenXrSpatialHitTestManager(
const OpenXrExtensionHelper& extension_helper,
const OpenXrSpatialFrameworkManager& spatial_framework_manager,
OpenXrSpatialPlaneManager* plane_manager,
XrSpace mojo_space,
XrInstance instance,
XrSystemId system)
: extension_helper_(extension_helper),
spatial_framework_manager_(spatial_framework_manager),
plane_manager_(plane_manager),
mojo_space_(mojo_space),
instance_(instance),
system_(system) {}
OpenXrSpatialHitTestManager::~OpenXrSpatialHitTestManager() = default;
void OpenXrSpatialHitTestManager::PopulateCapabilityConfiguration(
absl::flat_hash_map<XrSpatialCapabilityEXT,
absl::flat_hash_set<XrSpatialComponentTypeEXT>>&
capability_components) const {
// We need to query the capabilities to determine which ones to enable.
std::vector<XrSpatialCapabilityEXT> capabilities = GetCapabilities(
extension_helper_->ExtensionMethods().xrEnumerateSpatialCapabilitiesEXT,
instance_, system_);
// If depth-based hit test is supported, we can enable it.
if (base::Contains(capabilities,
XR_SPATIAL_CAPABILITY_DEPTH_RAYCAST_ANDROID)) {
DVLOG(1) << __func__ << " Enabling depth hit test";
capability_components[XR_SPATIAL_CAPABILITY_DEPTH_RAYCAST_ANDROID].insert(
XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID);
}
// If plane-based hit test is supported, we can enable it.
if (SupportsPlaneBasedHitTest(
instance_, system_,
extension_helper_->ExtensionMethods()
.xrEnumerateSpatialCapabilityComponentTypesEXT,
capabilities)) {
DVLOG(1) << __func__ << " Enabling plane hit test";
capability_components[XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT].insert(
XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID);
}
}
XrSpatialSnapshotEXT OpenXrSpatialHitTestManager::GetSnapshot(
const gfx::Point3F& origin,
const gfx::Vector3dF& direction) {
DVLOG(3) << __func__ << " origin=" << origin.ToString()
<< " direction=" << direction.ToString();
// In order to get hit test data from a snapshot that snapshot must be created
// with our ray passed in.
XrSpatialContextEXT spatial_context =
spatial_framework_manager_->GetSpatialContext();
if (spatial_context == XR_NULL_HANDLE) {
// Cannot query a snapshot without the context being ready.
return XR_NULL_HANDLE;
}
XrSpatialBoundsRaycastANDROID raycast_info = {
.type = XR_TYPE_SPATIAL_BOUNDS_RAYCAST_ANDROID,
.space = mojo_space_,
.time = predicted_display_time_,
.origin = {origin.x(), origin.y(), origin.z()},
.direction = {direction.x(), direction.y(), direction.z()},
.maxDistance = 0.0f, // Unbounded
};
// We only need to create a snapshot with the raycast results. It's the same
// component type for both plane and depth based hit tests.
std::array<XrSpatialComponentTypeEXT, 1> enabled_components = {
XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID};
XrSpatialDiscoverySnapshotCreateInfoEXT snapshot_create_info = {
.type = XR_TYPE_SPATIAL_DISCOVERY_SNAPSHOT_CREATE_INFO_EXT,
.next = &raycast_info,
.componentTypeCount = static_cast<uint32_t>(enabled_components.size()),
.componentTypes = enabled_components.data(),
};
XrFutureEXT future;
if (XR_FAILED(extension_helper_->ExtensionMethods()
.xrCreateSpatialDiscoverySnapshotAsyncEXT(
spatial_context, &snapshot_create_info, &future))) {
DLOG(ERROR) << __func__
<< " Failed to create discovery snapshot for hit test";
return XR_NULL_HANDLE;
}
// This is a temporary solution that is only safe because the current
// implementation is synchronous. This should be replaced with a proper
// synchronous API when it is available.
// TODO(https://crbug.com/394772465)
XrFuturePollInfoEXT poll_info = {XR_TYPE_FUTURE_POLL_INFO_EXT};
poll_info.future = future;
XrFuturePollResultEXT poll_result = {XR_TYPE_FUTURE_POLL_RESULT_EXT};
while (poll_result.state != XR_FUTURE_STATE_READY_EXT) {
if (XR_FAILED(extension_helper_->ExtensionMethods().xrPollFutureEXT(
instance_, &poll_info, &poll_result))) {
DLOG(ERROR) << __func__ << " Failed to poll future for hit test snapshot";
return XR_NULL_HANDLE;
}
}
XrCreateSpatialDiscoverySnapshotCompletionInfoEXT completion_info = {
.type = XR_TYPE_CREATE_SPATIAL_DISCOVERY_SNAPSHOT_COMPLETION_INFO_EXT,
.baseSpace = mojo_space_,
.time = predicted_display_time_,
.future = future,
};
XrCreateSpatialDiscoverySnapshotCompletionEXT completion = {
.type = XR_TYPE_CREATE_SPATIAL_DISCOVERY_SNAPSHOT_COMPLETION_EXT};
if (XR_FAILED(extension_helper_->ExtensionMethods()
.xrCreateSpatialDiscoverySnapshotCompleteEXT(
spatial_context, &completion_info, &completion))) {
DLOG(ERROR) << __func__
<< " Failed to complete snapshot creation for hit test";
return XR_NULL_HANDLE;
}
if (XR_FAILED(completion.futureResult)) {
DLOG(ERROR) << __func__
<< " Hit test snapshot creation resulted in an error: "
<< std::hex << completion.futureResult;
return XR_NULL_HANDLE;
}
return completion.snapshot;
}
std::vector<mojom::XRHitResultPtr> OpenXrSpatialHitTestManager::RequestHitTest(
const gfx::Point3F& origin,
const gfx::Vector3dF& direction) {
// We're responsible for destroying the snapshot when we're done, since it's
// one we've created.
ScopedOpenXrObject<XrSpatialSnapshotEXT> snapshot(
extension_helper_.get(), GetSnapshot(origin, direction));
if (snapshot.get() == XR_NULL_HANDLE) {
DLOG(ERROR) << __func__ << " Failed to extract snapshot";
return {};
}
// Query hit result components.
// Both plane and depth based results use the same component.
std::array<XrSpatialComponentTypeEXT, 1> enabled_components = {
XR_SPATIAL_COMPONENT_TYPE_RAYCAST_RESULT_ANDROID};
XrSpatialComponentDataQueryConditionEXT query_condition = {
.type = XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT,
.componentTypeCount = static_cast<uint32_t>(enabled_components.size()),
.componentTypes = enabled_components.data(),
};
XrSpatialComponentDataQueryResultEXT query_result = {
.type = XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT,
};
// Need to do an initial query to know how many results there are.
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
snapshot.get(), &query_condition, &query_result))) {
DLOG(ERROR) << __func__ << " Failed to query hit result components";
return {};
}
std::vector<XrSpatialEntityIdEXT> entity_ids(
query_result.entityIdCountOutput);
query_result.entityIds = entity_ids.data();
query_result.entityIdCapacityInput = entity_ids.size();
std::vector<XrSpatialEntityTrackingStateEXT> entity_states(
query_result.entityIdCountOutput);
query_result.entityStates = entity_states.data();
query_result.entityStateCapacityInput = entity_states.size();
std::vector<XrSpatialRaycastResultANDROID> raycast_results(
query_result.entityIdCountOutput);
XrSpatialComponentRaycastResultListANDROID raycast_result_list = {
.type = XR_TYPE_SPATIAL_COMPONENT_RAYCAST_RESULT_LIST_ANDROID,
.raycastResultCount = static_cast<uint32_t>(raycast_results.size()),
.raycastResults = raycast_results.data(),
};
query_result.next = &raycast_result_list;
// Second query to populate all of the result data.
if (XR_FAILED(
extension_helper_->ExtensionMethods().xrQuerySpatialComponentDataEXT(
snapshot.get(), &query_condition, &query_result))) {
DLOG(ERROR) << __func__ << " Second query result call failed.";
return {};
}
// The API essentially returns three main pieces of data back via three arrays
// where data is guaranteed to be presented in the same order in each list.
// However, when we return the results across mojom, we need the data to be
// returned in ascending order of distance. Thus we create and then sort a
// simple struct that keeps all of the bits of data together.
std::vector<SpatialHitTestResult> sorted_results;
sorted_results.reserve(raycast_results.size());
for (size_t i = 0; i < raycast_results.size(); i++) {
// If we get an entry that's not actively tracking, we don't want to return
// that data, so just skip it.
if (entity_states[i] != XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
continue;
}
// The `hitPose` provided to us returns a pose where +Z is the normal to the
// hit surface, while WebXR wants +Y to be the normal to the hit surface.
// Transform the `hitPose` now while building our results list.
sorted_results.emplace_back(
entity_ids[i],
ZNormalXrPoseToYNormalDevicePose(raycast_results[i].hitPose),
raycast_results[i].distanceSquared);
}
// WebXR expects to receive the hit poses in increasing distance from the item
// that they hit, so sort them now.
std::sort(sorted_results.begin(), sorted_results.end(),
[](const SpatialHitTestResult& a, const SpatialHitTestResult& b) {
return a.distance_squared < b.distance_squared;
});
std::vector<mojom::XRHitResultPtr> hit_results;
hit_results.reserve(sorted_results.size());
for (const auto& raycast_result : sorted_results) {
DVLOG(3) << __func__ << "Id= " << raycast_result.id
<< " pose=" << raycast_result.pose.ToString();
mojom::XRHitResultPtr hit_result = mojom::XRHitResult::New();
hit_result->mojo_from_result = raycast_result.pose;
// If the ID can't be found, we'll get an invalid plane_id to send up, which
// is what we should be sending up for the other hit tests/the default value
// anyways.
if (plane_manager_ && raycast_result.id != XR_NULL_SPATIAL_ENTITY_ID_EXT) {
hit_result->plane_id = plane_manager_->GetPlaneId(raycast_result.id);
}
hit_results.push_back(std::move(hit_result));
}
DVLOG(3) << __func__ << ": hit_results->size()=" << hit_results.size();
return hit_results;
}
void OpenXrSpatialHitTestManager::OnStartProcessingHitTests(
XrTime predicted_display_time) {
predicted_display_time_ = predicted_display_time;
}
} // namespace device