// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "device/vr/openxr/openxr_controller.h"
#include <stdint.h>
#include "base/check.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "device/gamepad/public/cpp/gamepad.h"
#include "device/vr/openxr/openxr_extension_helper.h"
#include "device/vr/openxr/openxr_util.h"
#include "device/vr/public/mojom/openxr_interaction_profile_type.mojom.h"
#include "device/vr/util/xr_standard_gamepad_builder.h"
#include "third_party/openxr/src/include/openxr/openxr.h"
#include "ui/gfx/geometry/decomposed_transform.h"
#include "ui/gfx/geometry/quaternion.h"
#include "ui/gfx/geometry/transform.h"
namespace device {
namespace {
const char* GetStringFromType(OpenXrHandednessType type) {
switch (type) {
case OpenXrHandednessType::kLeft:
return "left";
case OpenXrHandednessType::kRight:
return "right";
case OpenXrHandednessType::kCount:
NOTREACHED_IN_MIGRATION();
return "";
}
}
std::string GetTopLevelUserPath(OpenXrHandednessType type) {
return std::string("/user/hand/") + GetStringFromType(type);
}
std::optional<gfx::Transform> GetOriginFromTarget(XrTime predicted_display_time,
XrSpace origin,
XrSpace target,
bool* emulated_position) {
XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
// emulated_position indicates when there is a fallback from a fully-tracked
// (i.e. 6DOF) type case to some form of orientation-only type tracking
// (i.e. 3DOF/IMU type sensors)
// Thus we have to make sure orientation is tracked.
// Valid Bit only indicates it's either tracked or emulated, we have to check
// for XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT to make sure orientation is
// tracked.
if (XR_FAILED(
xrLocateSpace(target, origin, predicted_display_time, &location)) ||
!(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT) ||
!(location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT)) {
return std::nullopt;
}
*emulated_position = true;
if (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) {
*emulated_position = false;
}
// Convert the orientation and translation given by runtime into a
// transformation matrix.
gfx::DecomposedTransform decomp;
decomp.quaternion =
gfx::Quaternion(location.pose.orientation.x, location.pose.orientation.y,
location.pose.orientation.z, location.pose.orientation.w);
decomp.translate[0] = location.pose.position.x;
decomp.translate[1] = location.pose.position.y;
decomp.translate[2] = location.pose.position.z;
*emulated_position = true;
if (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) {
*emulated_position = false;
}
return gfx::Transform::Compose(decomp);
}
std::optional<GamepadBuilder::ButtonData> GetAxisButtonData(
OpenXrAxisType openxr_button_type,
std::optional<GamepadButton> button_data,
std::vector<double> axis) {
GamepadBuilder::ButtonData data;
if (!button_data || axis.size() != 2) {
return std::nullopt;
}
switch (openxr_button_type) {
case OpenXrAxisType::kThumbstick:
data.type = GamepadBuilder::ButtonData::Type::kThumbstick;
break;
case OpenXrAxisType::kTrackpad:
data.type = GamepadBuilder::ButtonData::Type::kTouchpad;
break;
}
data.touched = button_data->touched;
data.pressed = button_data->pressed;
data.value = button_data->value;
// Invert the y axis because -1 is up in the Gamepad API, but down in
// OpenXR.
data.x_axis = axis.at(0);
data.y_axis = -axis.at(1);
return data;
}
} // namespace
OpenXrController::OpenXrController()
: description_(nullptr),
type_(OpenXrHandednessType::kCount), // COUNT refers to invalid.
instance_(XR_NULL_HANDLE),
session_(XR_NULL_HANDLE),
action_set_(XR_NULL_HANDLE),
grip_pose_action_{XR_NULL_HANDLE},
grip_pose_space_(XR_NULL_HANDLE),
pointer_pose_action_(XR_NULL_HANDLE),
pointer_pose_space_(XR_NULL_HANDLE),
interaction_profile_(mojom::OpenXrInteractionProfileType::kInvalid) {}
OpenXrController::~OpenXrController() {
// We don't need to destroy all of the actions because destroying an
// action set destroys all actions that are part of the set.
if (action_set_ != XR_NULL_HANDLE) {
xrDestroyActionSet(action_set_);
}
if (grip_pose_space_ != XR_NULL_HANDLE) {
xrDestroySpace(grip_pose_space_);
}
if (pointer_pose_space_ != XR_NULL_HANDLE) {
xrDestroySpace(pointer_pose_space_);
}
}
XrResult OpenXrController::Initialize(
OpenXrHandednessType type,
XrInstance instance,
XrSession session,
const OpenXRPathHelper* path_helper,
const OpenXrExtensionHelper& extension_helper,
bool hand_input_enabled,
std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings) {
DCHECK(bindings);
type_ = type;
instance_ = instance;
session_ = session;
path_helper_ = path_helper;
extension_helper_ = &extension_helper;
hand_joints_enabled_ = hand_input_enabled;
// Note that we always create the hand tracker because we may be able to use
// it to supply a controller even if we aren't supplying it with joints.
hand_tracker_ = extension_helper_->CreateHandTracker(session_, type_);
std::string action_set_name =
std::string(GetStringFromType(type_)) + "_action_set";
XrActionSetCreateInfo action_set_create_info = {
XR_TYPE_ACTION_SET_CREATE_INFO};
size_t dest_size = std::size(action_set_create_info.actionSetName);
size_t src_size = base::strlcpy(action_set_create_info.actionSetName,
action_set_name.c_str(), dest_size);
DCHECK_LT(src_size, dest_size);
dest_size = std::size(action_set_create_info.localizedActionSetName);
src_size = base::strlcpy(action_set_create_info.localizedActionSetName,
action_set_name.c_str(), dest_size);
DCHECK_LT(src_size, dest_size);
RETURN_IF_XR_FAILED(
xrCreateActionSet(instance_, &action_set_create_info, &action_set_));
RETURN_IF_XR_FAILED(InitializeControllerActions());
SuggestBindings(bindings);
RETURN_IF_XR_FAILED(InitializeControllerSpaces());
return XR_SUCCESS;
}
XrResult OpenXrController::InitializeControllerActions() {
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kTrigger));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kSqueeze));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kTrackpad));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kThumbstick));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kThumbrest));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kButton1));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kButton2));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kGrasp));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kShoulder));
RETURN_IF_XR_FAILED(CreateActionsForButton(OpenXrButtonType::kMenu));
const std::string type_string = GetStringFromType(type_);
const std::string name_prefix = type_string + "_controller_";
// Axis Actions
RETURN_IF_XR_FAILED(
CreateAction(XR_ACTION_TYPE_VECTOR2F_INPUT, name_prefix + "trackpad_axis",
&(axis_action_map_[OpenXrAxisType::kTrackpad])));
RETURN_IF_XR_FAILED(CreateAction(
XR_ACTION_TYPE_VECTOR2F_INPUT, name_prefix + "thumbstick_axis",
&(axis_action_map_[OpenXrAxisType::kThumbstick])));
// Generic Pose Actions
RETURN_IF_XR_FAILED(CreateAction(XR_ACTION_TYPE_POSE_INPUT,
name_prefix + "grip_pose",
&grip_pose_action_));
RETURN_IF_XR_FAILED(CreateAction(XR_ACTION_TYPE_POSE_INPUT,
name_prefix + "aim_pose",
&pointer_pose_action_));
return XR_SUCCESS;
}
XrResult OpenXrController::SuggestBindingsForButtonMaps(
std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings,
const std::vector<OpenXrButtonPathMap>& button_maps,
XrPath interaction_profile_path,
const std::string& binding_prefix) const {
for (const auto& cur_button_map : button_maps) {
OpenXrButtonType button_type = cur_button_map.type;
for (const auto& cur_action_map : cur_button_map.action_maps) {
RETURN_IF_XR_FAILED(SuggestActionBinding(
bindings, interaction_profile_path,
button_action_map_.at(button_type).at(cur_action_map.type),
binding_prefix + cur_action_map.path));
}
}
return XR_SUCCESS;
}
XrResult OpenXrController::SuggestBindings(
std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings) const {
const std::string binding_prefix = GetTopLevelUserPath(type_);
for (const auto& interaction_profile :
GetOpenXrControllerInteractionProfiles()) {
// If the interaction profile is defined by an extension, check it here,
// otherwise continue
if (!interaction_profile.required_extension.empty() &&
!extension_helper_->ExtensionEnumeration()->ExtensionSupported(
interaction_profile.required_extension.c_str())) {
continue;
}
XrPath interaction_profile_path =
path_helper_->GetInteractionProfileXrPath(interaction_profile.type);
RETURN_IF_XR_FAILED(SuggestActionBinding(
bindings, interaction_profile_path, grip_pose_action_,
binding_prefix + "/input/grip/pose"));
RETURN_IF_XR_FAILED(SuggestActionBinding(
bindings, interaction_profile_path, pointer_pose_action_,
binding_prefix + "/input/aim/pose"));
RETURN_IF_XR_FAILED(SuggestBindingsForButtonMaps(
bindings, interaction_profile.common_button_maps,
interaction_profile_path, binding_prefix));
switch (type_) {
case OpenXrHandednessType::kLeft:
RETURN_IF_XR_FAILED(SuggestBindingsForButtonMaps(
bindings, interaction_profile.left_button_maps,
interaction_profile_path, binding_prefix));
break;
case OpenXrHandednessType::kRight:
RETURN_IF_XR_FAILED(SuggestBindingsForButtonMaps(
bindings, interaction_profile.right_button_maps,
interaction_profile_path, binding_prefix));
break;
case OpenXrHandednessType::kCount:
NOTREACHED_IN_MIGRATION() << "Controller can only be left or right";
return XR_ERROR_VALIDATION_FAILURE;
}
for (const auto& cur_axis_map : interaction_profile.axis_maps) {
RETURN_IF_XR_FAILED(
SuggestActionBinding(bindings, interaction_profile_path,
axis_action_map_.at(cur_axis_map.type),
binding_prefix + cur_axis_map.path));
}
}
return XR_SUCCESS;
}
XrResult OpenXrController::InitializeControllerSpaces() {
RETURN_IF_XR_FAILED(CreateActionSpace(grip_pose_action_, &grip_pose_space_));
RETURN_IF_XR_FAILED(
CreateActionSpace(pointer_pose_action_, &pointer_pose_space_));
return XR_SUCCESS;
}
device::mojom::XRHandedness OpenXrController::GetHandness() const {
switch (type_) {
case OpenXrHandednessType::kLeft:
return device::mojom::XRHandedness::LEFT;
case OpenXrHandednessType::kRight:
return device::mojom::XRHandedness::RIGHT;
case OpenXrHandednessType::kCount:
// LEFT controller and RIGHT controller are currently the only supported
// controllers. In the future, other controllers such as sound (which
// does not have a handedness) will be added here.
NOTREACHED_IN_MIGRATION();
return device::mojom::XRHandedness::NONE;
}
}
XrResult OpenXrController::Update(XrSpace base_space,
XrTime predicted_display_time) {
if (interaction_profile_ == mojom::OpenXrInteractionProfileType::kInvalid) {
RETURN_IF_XR_FAILED(UpdateInteractionProfile());
}
if (IsHandTrackingEnabled() || IsCurrentProfileFromHandTracker()) {
RETURN_IF_XR_FAILED(
hand_tracker_->Update(base_space, predicted_display_time));
}
return XR_SUCCESS;
}
mojom::XRTargetRayMode OpenXrController::GetTargetRayMode() const {
return device::mojom::XRTargetRayMode::POINTING;
}
mojom::XRInputSourceDescriptionPtr OpenXrController::GetDescription(
XrTime predicted_display_time) {
// Description only need to be set once unless interaction profiles changes.
if (!description_) {
// UpdateInteractionProfile() can not be called inside Initialize() function
// because XrGetCurrentInteractionProfile can't be called before
// xrSuggestInteractionProfileBindings getting called.
if (XR_FAILED(UpdateInteractionProfile())) {
return nullptr;
}
description_ = device::mojom::XRInputSourceDescription::New();
description_->handedness = GetHandness();
description_->target_ray_mode = GetTargetRayMode();
description_->profiles = path_helper_->GetInputProfiles(
interaction_profile_, hand_joints_enabled_);
}
description_->input_from_pointer =
GetGripFromPointerTransform(predicted_display_time);
return description_.Clone();
}
bool OpenXrController::IsCurrentProfileFromHandTracker() const {
return hand_tracker_ && hand_tracker_->controller() != nullptr &&
interaction_profile_ ==
hand_tracker_->controller()->interaction_profile();
}
std::optional<GamepadButton> OpenXrController::GetButton(
OpenXrButtonType type) const {
if (IsCurrentProfileFromHandTracker()) {
return hand_tracker_->controller()->GetButton(type);
}
GamepadButton ret;
// Button should at least have one of the three actions;
bool has_value = false;
DCHECK(button_action_map_.count(type) == 1);
auto button = button_action_map_.at(type);
XrActionStateBoolean press_state_bool = {XR_TYPE_ACTION_STATE_BOOLEAN};
if (XR_SUCCEEDED(QueryState(button[OpenXrButtonActionType::kPress],
&press_state_bool)) &&
press_state_bool.isActive) {
ret.pressed = press_state_bool.currentState;
has_value = true;
} else {
ret.pressed = false;
}
XrActionStateBoolean touch_state_bool = {XR_TYPE_ACTION_STATE_BOOLEAN};
if (XR_SUCCEEDED(QueryState(button[OpenXrButtonActionType::kTouch],
&touch_state_bool)) &&
touch_state_bool.isActive) {
ret.touched = touch_state_bool.currentState;
has_value = true;
} else {
ret.touched = ret.pressed;
}
XrActionStateFloat value_state_float = {XR_TYPE_ACTION_STATE_FLOAT};
if (XR_SUCCEEDED(QueryState(button[OpenXrButtonActionType::kValue],
&value_state_float)) &&
value_state_float.isActive) {
ret.value = value_state_float.currentState;
has_value = true;
} else {
ret.value = ret.pressed ? 1.0 : 0.0;
}
if (!has_value) {
return std::nullopt;
}
return ret;
}
std::vector<double> OpenXrController::GetAxis(OpenXrAxisType type) const {
XrActionStateVector2f axis_state_v2f = {XR_TYPE_ACTION_STATE_VECTOR2F};
if (XR_FAILED(QueryState(axis_action_map_.at(type), &axis_state_v2f)) ||
!axis_state_v2f.isActive) {
return {};
}
return {axis_state_v2f.currentState.x, axis_state_v2f.currentState.y};
}
std::optional<Gamepad> OpenXrController::GetWebXRGamepad() const {
// We can return an XR-Standard gamepad as long as the following are true:
// 1) It targets via a tracked-pointer
// 2) It has a non-null grip space
// 3) It has a primary input button.
// We assume that any null grip space is due to transient errors, and thus
// ignore that requirement for simplicity of developers rather than sending
// gamepad add/removed and input source change events due to temporary
// tracking loss. We validate the other two requirements below before building
// the gamepad.
if (GetTargetRayMode() != mojom::XRTargetRayMode::POINTING) {
return std::nullopt;
}
std::optional<GamepadButton> trigger_button =
GetButton(OpenXrButtonType::kTrigger);
if (!trigger_button) {
return std::nullopt;
}
XRStandardGamepadBuilder builder(GetHandness());
builder.SetPrimaryButton(trigger_button.value());
std::optional<GamepadButton> squeeze_button =
GetButton(OpenXrButtonType::kSqueeze);
if (squeeze_button) {
builder.SetSecondaryButton(squeeze_button.value());
}
std::optional<GamepadButton> trackpad_button =
GetButton(OpenXrButtonType::kTrackpad);
std::vector<double> trackpad_axis = GetAxis(OpenXrAxisType::kTrackpad);
std::optional<GamepadBuilder::ButtonData> trackpad_button_data =
GetAxisButtonData(OpenXrAxisType::kTrackpad, trackpad_button,
trackpad_axis);
if (trackpad_button_data) {
builder.SetTouchpadData(trackpad_button_data.value());
}
std::optional<GamepadButton> thumbstick_button =
GetButton(OpenXrButtonType::kThumbstick);
std::vector<double> thumbstick_axis = GetAxis(OpenXrAxisType::kThumbstick);
std::optional<GamepadBuilder::ButtonData> thumbstick_button_data =
GetAxisButtonData(OpenXrAxisType::kThumbstick, thumbstick_button,
thumbstick_axis);
if (thumbstick_button_data) {
builder.SetThumbstickData(thumbstick_button_data.value());
}
std::optional<GamepadButton> x_button = GetButton(OpenXrButtonType::kButton1);
if (x_button) {
builder.AddOptionalButtonData(x_button.value());
}
std::optional<GamepadButton> y_button = GetButton(OpenXrButtonType::kButton2);
if (y_button) {
builder.AddOptionalButtonData(y_button.value());
}
std::optional<GamepadButton> thumbrest_button =
GetButton(OpenXrButtonType::kThumbrest);
if (thumbrest_button) {
builder.AddOptionalButtonData(thumbrest_button.value());
}
std::optional<GamepadButton> grasp_button =
GetButton(OpenXrButtonType::kGrasp);
if (grasp_button) {
builder.AddOptionalButtonData(grasp_button.value());
}
std::optional<GamepadButton> shoulder_button =
GetButton(OpenXrButtonType::kShoulder);
if (shoulder_button) {
builder.AddOptionalButtonData(shoulder_button.value());
}
return builder.GetGamepad();
}
XrResult OpenXrController::UpdateInteractionProfile() {
XrPath top_level_user_path;
std::string top_level_user_path_string = GetTopLevelUserPath(type_);
RETURN_IF_XR_FAILED(xrStringToPath(
instance_, top_level_user_path_string.c_str(), &top_level_user_path));
XrInteractionProfileState interaction_profile_state = {
XR_TYPE_INTERACTION_PROFILE_STATE};
RETURN_IF_XR_FAILED(xrGetCurrentInteractionProfile(
session_, top_level_user_path, &interaction_profile_state));
if (interaction_profile_state.interactionProfile == XR_NULL_PATH) {
if (hand_tracker_ && hand_tracker_->controller()) {
interaction_profile_ = hand_tracker_->controller()->interaction_profile();
// If the HandTracker returns a controller, that controller should not
// return kInvalid.
CHECK(interaction_profile_ !=
mojom::OpenXrInteractionProfileType::kInvalid);
} else {
interaction_profile_ = mojom::OpenXrInteractionProfileType::kInvalid;
}
} else {
interaction_profile_ = path_helper_->GetInputProfileType(
interaction_profile_state.interactionProfile);
}
if (description_) {
description_->profiles = path_helper_->GetInputProfiles(
interaction_profile_, hand_joints_enabled_);
}
return XR_SUCCESS;
}
bool OpenXrController::IsHandTrackingEnabled() const {
return hand_joints_enabled_ && hand_tracker_;
}
mojom::XRHandTrackingDataPtr OpenXrController::GetHandTrackingData() {
if (!IsHandTrackingEnabled()) {
return nullptr;
}
return hand_tracker_->GetHandTrackingData();
}
std::optional<gfx::Transform> OpenXrController::GetMojoFromGripTransform(
XrTime predicted_display_time,
XrSpace local_space,
bool* emulated_position) const {
if (IsCurrentProfileFromHandTracker()) {
*emulated_position = false;
return hand_tracker_->controller()->GetBaseFromGripTransform();
}
return GetOriginFromTarget(predicted_display_time, local_space,
grip_pose_space_, emulated_position);
}
std::optional<gfx::Transform> OpenXrController::GetGripFromPointerTransform(
XrTime predicted_display_time) const {
if (IsCurrentProfileFromHandTracker()) {
return hand_tracker_->controller()->GetGripFromPointerTransform();
}
bool emulated_position;
return GetOriginFromTarget(predicted_display_time, grip_pose_space_,
pointer_pose_space_, &emulated_position);
}
XrResult OpenXrController::CreateActionsForButton(
OpenXrButtonType button_type) {
const std::string type_string = GetStringFromType(type_);
std::string name_prefix = type_string + "_controller_";
switch (button_type) {
case OpenXrButtonType::kTrigger:
name_prefix += "trigger_";
break;
case OpenXrButtonType::kSqueeze:
name_prefix += "squeeze_";
break;
case OpenXrButtonType::kTrackpad:
name_prefix += "trackpad_";
break;
case OpenXrButtonType::kThumbstick:
name_prefix += "thumbstick_";
break;
case OpenXrButtonType::kThumbrest:
name_prefix += "thumbrest_";
break;
case OpenXrButtonType::kButton1:
name_prefix += "upper_button_";
break;
case OpenXrButtonType::kButton2:
name_prefix += "lower_button_";
break;
case OpenXrButtonType::kGrasp:
name_prefix += "grasp_";
break;
case OpenXrButtonType::kShoulder:
name_prefix += "shoulder_";
break;
case OpenXrButtonType::kMenu:
name_prefix += "menu_";
break;
}
std::unordered_map<OpenXrButtonActionType, XrAction>& cur_button =
button_action_map_[button_type];
XrAction new_action;
RETURN_IF_XR_FAILED(CreateAction(XR_ACTION_TYPE_BOOLEAN_INPUT,
name_prefix + "button_press", &new_action));
cur_button[OpenXrButtonActionType::kPress] = new_action;
RETURN_IF_XR_FAILED(CreateAction(XR_ACTION_TYPE_FLOAT_INPUT,
name_prefix + "button_value", &new_action));
cur_button[OpenXrButtonActionType::kValue] = new_action;
RETURN_IF_XR_FAILED(CreateAction(XR_ACTION_TYPE_BOOLEAN_INPUT,
name_prefix + "button_touch", &new_action));
cur_button[OpenXrButtonActionType::kTouch] = new_action;
return XR_SUCCESS;
}
XrResult OpenXrController::CreateAction(XrActionType type,
const std::string& action_name,
XrAction* action) {
DCHECK(action);
XrActionCreateInfo action_create_info = {XR_TYPE_ACTION_CREATE_INFO};
action_create_info.actionType = type;
size_t dest_size = std::size(action_create_info.actionName);
size_t src_size = base::strlcpy(action_create_info.actionName,
action_name.data(), dest_size);
DCHECK_LT(src_size, dest_size);
dest_size = std::size(action_create_info.localizedActionName);
src_size = base::strlcpy(action_create_info.localizedActionName,
action_name.data(), dest_size);
DCHECK_LT(src_size, dest_size);
return xrCreateAction(action_set_, &action_create_info, action);
}
XrResult OpenXrController::SuggestActionBinding(
std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings,
XrPath interaction_profile_path,
XrAction action,
std::string binding_string) const {
XrPath binding_path;
// make sure all actions we try to suggest binding are initialized.
DCHECK(action != XR_NULL_HANDLE);
RETURN_IF_XR_FAILED(
xrStringToPath(instance_, binding_string.c_str(), &binding_path));
(*bindings)[interaction_profile_path].push_back({action, binding_path});
return XR_SUCCESS;
}
XrResult OpenXrController::CreateActionSpace(XrAction action, XrSpace* space) {
DCHECK(space);
XrActionSpaceCreateInfo action_space_create_info = {};
action_space_create_info.type = XR_TYPE_ACTION_SPACE_CREATE_INFO;
action_space_create_info.action = action;
action_space_create_info.subactionPath = XR_NULL_PATH;
action_space_create_info.poseInActionSpace = PoseIdentity();
return xrCreateActionSpace(session_, &action_space_create_info, space);
}
} // namespace device