File oxr_relay.cpp
File List > openxr_interface > oxr_relay.cpp
Go to the documentation of this file
#include "oxr_relay.hpp"
const int log_interval = 300; // pose logging interval in frames
#include
#include
#include
#include
#include
using namespace ILLIXR;
using namespace ILLIXR::data_format;
#define TIME_CUTOFF 3.0
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
namespace {
constexpr float kControllerTriggerThreshold = 0.75F;
constexpr float kControllerSqueezeThreshold = 0.85F;
// Convert the runtime path to the transport-stable enum used by Boba. Unknown
// profiles remain explicit so controller presence is not mistaken for absence.
ILLIXR::data_format::quest_controller_profile controller_profile_from_path(const std::string& path) {
using profile = ILLIXR::data_format::quest_controller_profile;
if (path.empty())
return profile::none;
if (path == "/interaction_profiles/khr/simple_controller")
return profile::simple_controller;
if (path == "/interaction_profiles/oculus/touch_controller")
return profile::oculus_touch;
if (path == "/interaction_profiles/htc/vive_controller")
return profile::htc_vive;
if (path == "/interaction_profiles/valve/index_controller")
return profile::valve_index;
if (path == "/interaction_profiles/microsoft/motion_controller")
return profile::microsoft_motion;
return profile::unknown;
}
} // namespace
#endif
oxr_relay::oxr_relay(const std::string& name, phonebook* pb)
: threadloop{name, pb}
, switchboard_{phonebook_->lookup_impl<switchboard>()}
, clock_{phonebook_->lookup_impl<relative_clock>()}
, combined_pose_writer_{switchboard_->get_network_writer<data_format::pose::combined_pose>(
"combined_pose", network::topic_config{network::topic_config::BOOST, network::topic_config::UDP})}
#ifdef ILLIXR_ENABLE_BOBA
, quest_controller_writer_{switchboard_->get_network_writer<data_format::quest_controller_input>(
"quest_controller", network::topic_config{network::topic_config::BOOST, network::topic_config::UDP})}
, openxr_view_writer_{switchboard_->get_network_writer<data_format::openxr_view_frame>(
"openxr_view", network::topic_config{network::topic_config::BOOST, network::topic_config::UDP})}
#endif
, latency_reader_{switchboard_->get_reader<network_latency_result>("network_latency")} {
}
void oxr_relay::destroy() {
// Destroy hand interaction action set and spaces first
destroy_hand_interaction();
// Destroy hand trackers first
destroy_hand_tracking();
}
oxr_relay::~oxr_relay() {
destroy();
}
void oxr_relay::initialize(XrInstance instance, XrSession session, XrSpace local, XrSpace view) {
instance_ = instance;
session_ = session;
local_space_ = local;
view_space_ = view;
// Initialize hand tracking after session is created
if (init_hand_tracking()) {
spdlog::get("illixr")->info("OXR hand tracking initialized successfully");
} else {
spdlog::get("illixr")->warn("OXR hand tracking not available");
}
// Initialize hand interaction (requires session and instance to be ready)
if (init_hand_interaction()) {
spdlog::get("illixr")->info("OXR hand interaction initialized successfully");
} else {
spdlog::get("illixr")->warn("OXR hand interaction not available");
}
initialized_ = true;
spdlog::get("illixr")->info("oxr_relay: initialized");
}
threadloop::skip_option oxr_relay::_p_should_skip() {
if (!initialized_) {
return skip_option::skip_and_yield;
}
if (predicted_time_.load() == 0)
return skip_option::skip_and_spin;
std::this_thread::sleep_for(std::chrono::milliseconds(7));
return skip_option::run;
}
void oxr_relay::_p_one_iteration() {
auto iter_start = std::chrono::steady_clock::now();
auto log_iter_time = [&](const char* exit_point) {
auto ms = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - iter_start).count() /
1000.0;
if (ms > TIME_CUTOFF)
spdlog::get("illixr")->warn("[oxr_timing] _p_one_iteration took {:.2f}ms ({})", ms, exit_point);
};
pose::head_pose_type head_pose;
// Periodically recompute the monotonic-to-system offset to guard
// against NTP clock adjustments invalidating the startup calibration.
// CLOCK_MONOTONIC is never adjusted by NTP so it is always a stable
// reference; only the system_clock side can jump.
auto now = std::chrono::steady_clock::now();
if (!time_offsets_calibrated_ || now - last_offset_calibration_ > std::chrono::seconds(30)) {
calibrate_time_offsets();
last_offset_calibration_ = now;
}
// Verify the XrTime == CLOCK_BOOTTIME assumption once on first iteration.
// If the difference is large (> 1s) something is wrong and we log a warning.
if (!xr_time_verified_) {
struct timespec ts_boot;
clock_gettime(CLOCK_BOOTTIME, &ts_boot);
int64_t boot_ns = ts_boot.tv_sec * 1'000'000'000LL + ts_boot.tv_nsec;
int64_t xr_ns = static_cast<int64_t>(predicted_time_.load());
int64_t diff_ms = std::abs(boot_ns - xr_ns) / 1'000'000;
if (diff_ms > 1000) {
spdlog::get("illixr")->warn("oxr_relay: XrTime ({}) differs from CLOCK_BOOTTIME ({}) "
"by {}ms — xr_to_monotonic_offset assumption may be wrong",
xr_ns, boot_ns, diff_ms);
} else {
// spdlog::get("illixr")->info(
// "oxr_relay: XrTime vs CLOCK_BOOTTIME diff={}ms — "
// "assumption confirmed", diff_ms);
}
xr_time_verified_ = true;
}
// store the predicted time so it is consistent across add data gathering
XrTime pose_time = predicted_time_.load();
if (XR_SUCCEEDED(get_head_pose(pose_time, &head_pose))) {
if (memcmp((void*) &head_pose, (void*) &last_pose_, sizeof(XrPosef)) == 0) {
log_iter_time("pose unchanged");
return;
}
current_head_pose_ = head_pose;
last_pose_ = head_pose;
if (head_pose.relation_flags & pose::XRT_SPACE_RELATION_ORIENTATION_VALID_BIT &&
head_pose.relation_flags & pose::XRT_SPACE_RELATION_POSITION_VALID_BIT) {
// spdlog::get("illixr")->debug("pose valid, writing {}, {}, {}, {}", headPose.orientation.w,
// headPose.orientation.x, headPose.orientation.y, headPose.orientation.z);
} else {
spdlog::get("illixr")->debug("Head pose not tracked");
}
} else {
spdlog::get("illixr")->debug("Update Failed");
}
// Update hand tracking at the predicted display time
update_hand_tracking(pose_time);
// Update hand interaction poses at the predicted display time
update_hand_interaction(pose_time);
push_poses(pose_time);
log_iter_time("normal");
}
bool oxr_relay::init_hand_tracking() {
if (!hand_tracking_supported_) {
spdlog::get("illixr")->debug("Hand tracking not supported, skipping initialization");
return false;
}
// Get function pointers for hand tracking
OXR(xrGetInstanceProcAddr(instance_, "xrCreateHandTrackerEXT", (PFN_xrVoidFunction*) &xr_create_hand_tracker_))
OXR(xrGetInstanceProcAddr(instance_, "xrDestroyHandTrackerEXT", (PFN_xrVoidFunction*) &xr_destroy_hand_tracker_))
OXR(xrGetInstanceProcAddr(instance_, "xrLocateHandJointsEXT", (PFN_xrVoidFunction*) &xr_locate_hand_joints_))
if (!xr_create_hand_tracker_ || !xr_destroy_hand_tracker_ || !xr_locate_hand_joints_) {
spdlog::get("illixr")->error("Failed to get hand tracking function pointers");
hand_tracking_supported_ = false;
return false;
}
// Create left hand tracker
XrHandTrackerCreateInfoEXT create_info = {XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT};
create_info.hand = XR_HAND_LEFT_EXT;
create_info.handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT;
XrResult result = xr_create_hand_tracker_(session_, &create_info, &left_hand_tracker_);
if (XR_FAILED(result)) {
spdlog::get("illixr")->error("Failed to create left hand tracker: {}", static_cast<int>(result));
return false;
}
// Create right hand tracker
create_info.hand = XR_HAND_RIGHT_EXT;
result = xr_create_hand_tracker_(session_, &create_info, &right_hand_tracker_);
if (XR_FAILED(result)) {
spdlog::get("illixr")->error("Failed to create right hand tracker: {}", static_cast<int>(result));
xr_destroy_hand_tracker_(left_hand_tracker_);
left_hand_tracker_ = XR_NULL_HANDLE;
return false;
}
spdlog::get("illixr")->info("Hand trackers created successfully");
return true;
}
void oxr_relay::destroy_hand_tracking() {
if (xr_destroy_hand_tracker_) {
if (left_hand_tracker_ != XR_NULL_HANDLE) {
xr_destroy_hand_tracker_(left_hand_tracker_);
left_hand_tracker_ = XR_NULL_HANDLE;
}
if (right_hand_tracker_ != XR_NULL_HANDLE) {
xr_destroy_hand_tracker_(right_hand_tracker_);
right_hand_tracker_ = XR_NULL_HANDLE;
}
}
}
void oxr_relay::update_hand_tracking(XrTime predicted_time) {
if (!hand_tracking_supported_ || !xr_locate_hand_joints_) {
return;
}
current_hand_poses_.sensor_time = clock_->now();
// Process both hands
XrHandTrackerEXT trackers[2] = {left_hand_tracker_, right_hand_tracker_};
pose::hand_joint_poses* hand_states[2] = {¤t_hand_poses_[pose::LEFT], ¤t_hand_poses_[pose::RIGHT]};
const char* hand_names[2] = {"left", "right"};
for (int hand_idx = 0; hand_idx < 2; hand_idx++) {
if (trackers[hand_idx] == XR_NULL_HANDLE) {
spdlog::get("illixr")->debug("Hand {}: No tracker handle", hand_names[hand_idx]);
continue;
}
// Prepare joint locations array
std::array<XrHandJointLocationEXT, XR_HAND_JOINT_COUNT_EXT> joint_locations{};
// Prepare velocity array (optional but useful)
std::array<XrHandJointVelocityEXT, XR_HAND_JOINT_COUNT_EXT> joint_velocities{};
// Set up velocity container (this one has type/next)
XrHandJointVelocitiesEXT velocities = {XR_TYPE_HAND_JOINT_VELOCITIES_EXT};
velocities.next = nullptr;
velocities.jointCount = XR_HAND_JOINT_COUNT_EXT;
velocities.jointVelocities = joint_velocities.data();
// Set up locations container with velocity chain (this one has type/next)
XrHandJointLocationsEXT locations = {XR_TYPE_HAND_JOINT_LOCATIONS_EXT};
locations.next = &velocities;
locations.jointCount = XR_HAND_JOINT_COUNT_EXT;
locations.jointLocations = joint_locations.data();
locations.isActive = XR_FALSE; // Initialize to false
// Locate info
XrHandJointsLocateInfoEXT locate_info = {XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT};
locate_info.baseSpace = local_space_;
locate_info.time = predicted_time;
auto t0 = std::chrono::steady_clock::now();
XrResult result = xr_locate_hand_joints_(trackers[hand_idx], &locate_info, &locations);
auto t1 = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count() / 1000.0;
if (ms > TIME_CUTOFF)
spdlog::get("illixr")->warn("[oxr_timing] xrLocateHandJointsEXT ({}) took {:.2f}ms", hand_names[hand_idx], ms);
// Throttle per-hand logging to once every 300 frames (~5 s at 60 Hz)
static uint64_t ht_log_counter[2] = {0, 0};
const bool ht_should_log = (++ht_log_counter[hand_idx] % log_interval) == 1;
if (ht_should_log) {
spdlog::get("illixr")->debug("[hand_tracking] {} xrLocateHandJointsEXT result={} isActive={}", hand_names[hand_idx],
static_cast<int>(result), locations.isActive ? "TRUE" : "FALSE");
}
if (XR_SUCCEEDED(result) && locations.isActive) {
// Calculate confidence based on how many joints are tracked
// Convert all joints
hand_states[hand_idx]->update(joint_locations, joint_velocities);
if (ht_should_log) {
const auto& wrist = hand_states[hand_idx]->joints[XR_HAND_JOINT_WRIST_EXT];
const auto& index_tip = hand_states[hand_idx]->joints[XR_HAND_JOINT_INDEX_TIP_EXT];
spdlog::get("illixr")->debug(
"[hand_tracking] {} ACTIVE | active={} | "
"wrist=({:.3f},{:.3f},{:.3f}) flags=0x{:02x} | "
"index_tip=({:.3f},{:.3f},{:.3f}) flags=0x{:02x}",
hand_names[hand_idx], hand_states[hand_idx]->is_active, wrist.relation.pose.position.x,
wrist.relation.pose.position.y, wrist.relation.pose.position.z,
static_cast<unsigned>(wrist.relation.relation_flags), index_tip.relation.pose.position.x,
index_tip.relation.pose.position.y, index_tip.relation.pose.position.z,
static_cast<unsigned>(index_tip.relation.relation_flags));
}
} else {
hand_states[hand_idx]->is_active = false;
if (XR_FAILED(result)) {
spdlog::get("illixr")->warn("[hand_tracking] {} xrLocateHandJointsEXT FAILED result={}", hand_names[hand_idx],
static_cast<int>(result));
} else if (ht_should_log) {
spdlog::get("illixr")->debug("[hand_tracking] {} isActive=FALSE (controllers may be active)",
hand_names[hand_idx]);
}
}
}
if (current_hand_poses_.has_hands()) {
// spdlog::get("illixr")->debug("[hand_tracking] publishing: left={} right={}",
// current_hand_poses_.hands[pose::LEFT].is_active ? "tracked" : "not tracked",
// current_hand_poses_.hands[pose::RIGHT].is_active ? "tracked" : "not tracked");
} else {
// Only log occasionally to avoid spam
static uint64_t no_tracking_count = 0;
if (++no_tracking_count % log_interval == 1) {
spdlog::get("illixr")->info("[hand_tracking] not publishing — has_hands()=false "
"(left tracked={} right tracked={}) count={}",
current_hand_poses_.hands[pose::LEFT].is_active,
current_hand_poses_.hands[pose::RIGHT].is_active, no_tracking_count);
}
}
}
XrResult oxr_relay::get_head_pose(XrTime time, pose::head_pose_type* out_pose) {
// Chain XrSpaceVelocity into the location query so the runtime fills both
// pose and velocity in a single call, guaranteeing they are time-coherent.
XrSpaceVelocity velocity = {XR_TYPE_SPACE_VELOCITY};
XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
location.next = &velocity;
auto t0 = std::chrono::steady_clock::now();
OXR(xrLocateSpace(view_space_, local_space_, time, &location))
auto t1 = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count() / 1000.0;
if (ms > TIME_CUTOFF)
spdlog::get("illixr")->warn("[oxr_timing] xrLocateSpace (head) took {:.2f}ms", ms);
out_pose->pose = location.pose;
out_pose->linear_velocity = velocity.linearVelocity;
out_pose->angular_velocity = velocity.angularVelocity;
out_pose->set_flags(location.locationFlags, velocity.velocityFlags);
return XR_SUCCESS;
}
void oxr_relay::push_poses(XrTime predicted_time) {
// Throttle detailed logging to once every 300 calls (~5 s at 60 Hz)
static uint64_t log_push_counter = 0;
const bool should_log = (++log_push_counter % log_interval) == 1;
if (!current_head_pose_.valid()) {
// spdlog::get("illixr")->debug(" not valid");
return;
}
pose::fast_head_pose_type current_pose;
current_pose.pose = current_head_pose_;
// Hand joint tracking
if (current_hand_poses_.has_hands())
current_hand_poses_.sensor_time = clock_->now();
if (should_log) {
if (current_hand_poses_.has_hands()) {
const char* hand_names[] = {"LEFT", "RIGHT"};
const pose::side hand_enums[] = {pose::LEFT, pose::RIGHT};
for (int h = 0; h < 2; h++) {
const auto& hd = current_hand_poses_.hands.at(hand_enums[h]);
if (!hd.is_active) {
spdlog::get("illixr")->debug("[hand_tracking] {} not tracked", hand_names[h]);
continue;
}
const auto& wrist = hd.joints[pose::WRIST];
const auto& index_tip = hd.joints[pose::INDEX_TIP];
spdlog::get("illixr")->debug(
"[hand_tracking] {} | active={} | "
"wrist=({:.3f},{:.3f},{:.3f}) flags=0x{:02x} | "
"index_tip=({:.3f},{:.3f},{:.3f}) flags=0x{:02x}",
hand_names[h], hd.is_active, wrist.relation.pose.position.x, wrist.relation.pose.position.y,
wrist.relation.pose.position.z, static_cast<unsigned>(wrist.relation.relation_flags),
index_tip.relation.pose.position.x, index_tip.relation.pose.position.y, index_tip.relation.pose.position.z,
static_cast<unsigned>(index_tip.relation.relation_flags));
}
} else {
spdlog::get("illixr")->debug("[hand_tracking] data available but no hands tracked");
}
// Palm poses
if (current_palm_poses_.is_valid()) {
const char* hand_names[] = {"LEFT", "RIGHT"};
const pose::side hand_enums[] = {pose::LEFT, pose::RIGHT};
for (int h = 0; h < 2; h++) {
const auto& pd = current_palm_poses_.hands.at(hand_enums[h]);
if (!pd.is_valid()) {
spdlog::get("illixr")->debug("[palm_pose] {} not valid", hand_names[h]);
continue;
}
spdlog::get("illixr")->debug("[palm_pose] {} | "
"pos=({:.3f},{:.3f},{:.3f}) | "
"ori=({:.3f},{:.3f},{:.3f},{:.3f})",
hand_names[h], pd.pose.position.x, pd.pose.position.y, pd.pose.position.z,
pd.pose.orientation.x, pd.pose.orientation.y, pd.pose.orientation.z,
pd.pose.orientation.w);
}
} else {
spdlog::get("illixr")->debug("[palm_pose] no data on switchboard");
}
// Hand interactions
if (current_hand_interactions_.is_valid()) {
const char* hand_names[] = {"LEFT", "RIGHT"};
const pose::side hand_enums[] = {pose::LEFT, pose::RIGHT};
const char* type_names[] = {"AIM", "GRIP", "PINCH", "POKE"};
const pose::interaction_pose_type type_enums[] = {pose::AIM, pose::GRIP, pose::PINCH, pose::POKE};
for (int h = 0; h < 2; h++) {
const auto& hip = current_hand_interactions_.hands.at(hand_enums[h]);
for (int t = 0; t < pose::NUM_INTERACTION_POSES; t++) {
const auto& ip = hip.at(type_enums[t]);
if (!ip.valid())
continue;
if (type_enums[t] == pose::POKE) {
spdlog::get("illixr")->debug("[hand_interaction] {} {} | pos=({:.3f},{:.3f},{:.3f})", hand_names[h],
type_names[t], ip.pose.position.x, ip.pose.position.y, ip.pose.position.z);
} else {
spdlog::get("illixr")->debug(
"[hand_interaction] {} {} | pos=({:.3f},{:.3f},{:.3f}) | value={:.3f} ready={}", hand_names[h],
type_names[t], ip.pose.position.x, ip.pose.position.y, ip.pose.position.z, ip.value,
ip.ready ? "true" : "false");
}
}
}
} else {
spdlog::get("illixr")->debug("[hand_interaction] no data on switchboard");
}
}
auto now = time_point{std::chrono::duration<long, std::nano>{std::chrono::high_resolution_clock::now().time_since_epoch()}};
current_pose.predict_target_time = predicted_time;
current_pose.predict_computed_time = now;
uint64_t pose_id = ++counter_;
// Store pose in history map so oxr_interface can correlate incoming
// frames back to the original pose measurement via pose_id.
{
std::lock_guard<std::mutex> lock(pose_history_mutex_);
pose_history_entry entry{};
entry.pose = current_head_pose_;
entry.generated_time = now;
entry.xr_time = predicted_time;
pose_history_[pose_id] = entry;
// Prune oldest entries to keep the map bounded
while (pose_history_.size() > MAX_POSE_HISTORY) {
pose_history_.erase(pose_history_.begin());
}
}
// ----------------------------------------------------------------
// Populate time conversion fields so pose_relay on the server can
// convert this XrTime into Monado time.
//
// pose_xr_time_ns = predictedDisplayTime (headset XrTime)
// xr_to_monotonic_offset_ns = CLOCK_MONOTONIC_ns - XrTime
// monotonic_to_system_offset_ns = system_clock_ns - CLOCK_MONOTONIC_ns
// smoothed_clock_offset_ns = server system_clock - headset system_clock
// smoothed_rtt_ns = smoothed round trip time
// ----------------------------------------------------------------
// Read the latest network latency data if available
auto latency_data = latency_reader_.get_ro_nullable();
double smoothed_offset = 0.;
double smoothed_rtt = 0.;
if (latency_data != nullptr) {
smoothed_offset = latency_data->smoothed_clock_offset_ms * 1'000'000.0;
smoothed_rtt = latency_data->smoothed_rtt_ms * 1'000'000.0;
}
// spdlog::get("illixr")->debug("[oxr_relay] pushing pose {}", pose_id);
combined_pose_writer_.put(std::make_shared<pose::combined_pose>(
current_pose, current_hand_poses_, current_palm_poses_, current_hand_interactions_, pose_id, predicted_time,
xr_to_monotonic_offset_ns_, monotonic_to_system_offset_ns_, smoothed_offset, smoothed_rtt));
}
bool oxr_relay::init_hand_interaction() {
if (!hand_interaction_supported_) {
spdlog::get("illixr")->debug("Hand interaction extension not supported, skipping initialization");
return false;
}
// Sub-action paths
const char* hand_path_strings[2] = {"/user/hand/left", "/user/hand/right"};
for (int h = 0; h < 2; h++) {
OXR(xrStringToPath(instance_, hand_path_strings[h], &hand_subaction_paths_[h]))
}
XrPath both_subaction_paths[2] = {hand_subaction_paths_[0], hand_subaction_paths_[1]};
// Action set
XrActionSetCreateInfo action_set_info = {XR_TYPE_ACTION_SET_CREATE_INFO};
strncpy(action_set_info.actionSetName, "hand_interaction", XR_MAX_ACTION_SET_NAME_SIZE);
strncpy(action_set_info.localizedActionSetName, "Hand Interaction", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE);
action_set_info.priority = 0;
OXR(xrCreateActionSet(instance_, &action_set_info, &hand_interaction_action_set_))
// Pose actions (one per interaction_pose_type, shared across both hands)
const char* pose_action_names[pose::NUM_INTERACTION_POSES] = {"aim_pose", "grip_pose", "pinch_pose", "poke_pose"};
const char* pose_action_localized[pose::NUM_INTERACTION_POSES] = {"Aim Pose", "Grip Pose", "Pinch Pose", "Poke Pose"};
for (int i = 0; i < pose::NUM_INTERACTION_POSES; i++) {
XrActionCreateInfo ai = {XR_TYPE_ACTION_CREATE_INFO};
strncpy(ai.actionName, pose_action_names[i], XR_MAX_ACTION_NAME_SIZE);
strncpy(ai.localizedActionName, pose_action_localized[i], XR_MAX_LOCALIZED_ACTION_NAME_SIZE);
ai.actionType = XR_ACTION_TYPE_POSE_INPUT;
ai.countSubactionPaths = 2;
ai.subactionPaths = both_subaction_paths;
OXR(xrCreateAction(hand_interaction_action_set_, &ai, &interaction_pose_actions_[i]))
}
// Float value actions (AIM=0, GRIP=1, PINCH=2; POKE has none)
const char* value_action_names[3] = {"aim_activate_value", "grasp_value", "pinch_value"};
const char* value_action_localized[3] = {"Aim Activate Value", "Grasp Value", "Pinch Value"};
for (int i = 0; i < 3; i++) {
XrActionCreateInfo ai = {XR_TYPE_ACTION_CREATE_INFO};
strncpy(ai.actionName, value_action_names[i], XR_MAX_ACTION_NAME_SIZE);
strncpy(ai.localizedActionName, value_action_localized[i], XR_MAX_LOCALIZED_ACTION_NAME_SIZE);
ai.actionType = XR_ACTION_TYPE_FLOAT_INPUT;
ai.countSubactionPaths = 2;
ai.subactionPaths = both_subaction_paths;
OXR(xrCreateAction(hand_interaction_action_set_, &ai, &interaction_value_actions_[i]))
}
// Boolean ready actions (AIM=0, GRIP=1, PINCH=2)
const char* ready_action_names[3] = {"aim_activate_ready", "grasp_ready", "pinch_ready"};
const char* ready_action_localized[3] = {"Aim Activate Ready", "Grasp Ready", "Pinch Ready"};
for (int i = 0; i < 3; i++) {
XrActionCreateInfo ai = {XR_TYPE_ACTION_CREATE_INFO};
strncpy(ai.actionName, ready_action_names[i], XR_MAX_ACTION_NAME_SIZE);
strncpy(ai.localizedActionName, ready_action_localized[i], XR_MAX_LOCALIZED_ACTION_NAME_SIZE);
ai.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
ai.countSubactionPaths = 2;
ai.subactionPaths = both_subaction_paths;
OXR(xrCreateAction(hand_interaction_action_set_, &ai, &interaction_ready_actions_[i]))
}
// Palm pose action (XR_EXT_palm_pose)
// Created in this same action set so it shares the single xrAttachSessionActionSets
// call that is only allowed once per session.
{
XrActionCreateInfo ai = {XR_TYPE_ACTION_CREATE_INFO};
strncpy(ai.actionName, "palm_pose", XR_MAX_ACTION_NAME_SIZE);
strncpy(ai.localizedActionName, "Palm Pose", XR_MAX_LOCALIZED_ACTION_NAME_SIZE);
ai.actionType = XR_ACTION_TYPE_POSE_INPUT;
ai.countSubactionPaths = 2;
ai.subactionPaths = both_subaction_paths;
OXR(xrCreateAction(hand_interaction_action_set_, &ai, &palm_pose_action_))
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
if (!create_controller_actions()) {
spdlog::get("illixr")->error("Failed to create Quest controller actions");
return false;
}
#endif
// Suggest bindings
// Each profile gets its own xrSuggestInteractionProfileBindings call because
// the spec requires a separate call per profile. We suggest:
// • /interaction_profiles/ext/hand_interaction_ext — bare-hand interaction poses
// • /interaction_profiles/khr/simple_controller — generic fallback for any controller
//
// Keeping to these two vendor-neutral profiles means the code makes no assumptions
// about the physical hardware. The runtime will automatically select whichever
// profile matches the active device.
//
// Palm pose (/input/palm_ext/pose from XR_EXT_palm_pose) is bound ONLY on the
// simple_controller profile. It is NOT a valid binding path on hand_interaction_ext
// — that profile only exposes aim/grip/pinch/poke poses and their gesture scalars.
// When bare hands are active the runtime will derive the palm pose from the hand
// skeleton internally; when a controller is active it will be read from the
// simple_controller binding.
const char* pose_suffixes[pose::NUM_INTERACTION_POSES] = {"/input/aim/pose", "/input/grip/pose", "/input/pinch_ext/pose",
"/input/poke_ext/pose"};
const char* value_suffixes[3] = {"/input/aim_activate_ext/value", "/input/grasp_ext/value", "/input/pinch_ext/value"};
const char* ready_suffixes[3] = {"/input/aim_activate_ext/ready_ext", "/input/grasp_ext/ready_ext",
"/input/pinch_ext/ready_ext"};
// Profile 1: XR_EXT_hand_interaction
{
XrPath profile_path;
OXR(xrStringToPath(instance_, "/interaction_profiles/ext/hand_interaction_ext", &profile_path))
std::vector<XrActionSuggestedBinding> bindings;
for (auto& hand_path_string : hand_path_strings) {
for (int i = 0; i < pose::NUM_INTERACTION_POSES; i++) {
XrPath path;
std::string s = std::string(hand_path_string) + pose_suffixes[i];
OXR(xrStringToPath(instance_, s.c_str(), &path))
bindings.push_back({interaction_pose_actions_[i], path});
}
for (int i = 0; i < 3; i++) {
XrPath path;
std::string s = std::string(hand_path_string) + value_suffixes[i];
OXR(xrStringToPath(instance_, s.c_str(), &path))
bindings.push_back({interaction_value_actions_[i], path});
}
for (int i = 0; i < 3; i++) {
XrPath path;
std::string s = std::string(hand_path_string) + ready_suffixes[i];
OXR(xrStringToPath(instance_, s.c_str(), &path))
bindings.push_back({interaction_ready_actions_[i], path});
}
// NOTE: palm_pose_action_ is NOT bound here. /input/palm_ext/pose is
// defined by XR_EXT_palm_pose and is only valid on controller interaction
// profiles, not on hand_interaction_ext. It is bound in Profile 2 below.
}
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
suggested.interactionProfile = profile_path;
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
OXR(xrSuggestInteractionProfileBindings(instance_, &suggested))
spdlog::get("illixr")->info("Suggested {} bindings for hand_interaction_ext profile", bindings.size());
}
// Profile 2: KHR simple controller (generic fallback)
{
XrPath profile_path;
OXR(xrStringToPath(instance_, "/interaction_profiles/khr/simple_controller", &profile_path))
std::vector<XrActionSuggestedBinding> bindings;
for (auto& hand_path_string : hand_path_strings) {
// Only aim and grip are defined on the simple_controller profile
for (int i : {(int) pose::AIM, (int) pose::GRIP}) {
XrPath path;
std::string s = std::string(hand_path_string) + pose_suffixes[i];
OXR(xrStringToPath(instance_, s.c_str(), &path))
bindings.push_back({interaction_pose_actions_[i], path});
}
{
XrPath path;
std::string s = std::string(hand_path_string) + "/input/palm_ext/pose";
OXR(xrStringToPath(instance_, s.c_str(), &path))
bindings.push_back({palm_pose_action_, path});
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
XrPath select_path;
std::string select = std::string(hand_path_string) + "/input/select/click";
OXR(xrStringToPath(instance_, select.c_str(), &select_path))
bindings.push_back({controller_trigger_click_action_, select_path});
#endif
}
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
suggested.interactionProfile = profile_path;
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
XrResult r = xrSuggestInteractionProfileBindings(instance_, &suggested);
if (XR_FAILED(r)) {
spdlog::get("illixr")->warn("Could not suggest simple_controller bindings ({})", static_cast<int>(r));
} else {
spdlog::get("illixr")->info("Suggested {} bindings for simple_controller profile", bindings.size());
}
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
if (!suggest_controller_bindings()) {
spdlog::get("illixr")->error("Failed to suggest Quest Touch controller bindings");
return false;
}
#endif
// Attach action set to session
XrSessionActionSetsAttachInfo attach_info = {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
attach_info.countActionSets = 1;
attach_info.actionSets = &hand_interaction_action_set_;
XrResult attach_result = xrAttachSessionActionSets(session_, &attach_info);
if (XR_FAILED(attach_result)) {
spdlog::get("illixr")->error("Failed to attach hand interaction action set: {}", static_cast<int>(attach_result));
hand_interaction_supported_ = false;
return false;
}
// Create per-hand action spaces
XrPosef identity = {};
identity.orientation.w = 1.0f;
for (int h = 0; h < 2; h++) {
for (int i = 0; i < pose::NUM_INTERACTION_POSES; i++) {
XrActionSpaceCreateInfo space_info = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
space_info.action = interaction_pose_actions_[i];
space_info.subactionPath = hand_subaction_paths_[h];
space_info.poseInActionSpace = identity;
OXR(xrCreateActionSpace(session_, &space_info, &interaction_pose_spaces_[h][i]))
}
// Palm pose action space — one per hand
XrActionSpaceCreateInfo palm_space_info = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
palm_space_info.action = palm_pose_action_;
palm_space_info.subactionPath = hand_subaction_paths_[h];
palm_space_info.poseInActionSpace = identity;
OXR(xrCreateActionSpace(session_, &palm_space_info, &palm_pose_spaces_[h]))
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
controller_actions_initialized_ = true;
spdlog::get("illixr")->info("Hand interaction and Quest Touch controller actions initialized");
#else
spdlog::get("illixr")->info("Hand interaction action set and action spaces created");
#endif
return true;
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
// ---- Quest controller action creation and sampling -------------------------
bool oxr_relay::create_controller_actions() {
const XrPath subaction_paths[2] = {hand_subaction_paths_[0], hand_subaction_paths_[1]};
const auto create_action = [&](XrActionType type, const char* name, const char* localized_name, XrAction* action) {
XrActionCreateInfo info = {XR_TYPE_ACTION_CREATE_INFO};
std::strncpy(info.actionName, name, XR_MAX_ACTION_NAME_SIZE - 1);
std::strncpy(info.localizedActionName, localized_name, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
info.actionType = type;
info.countSubactionPaths = 2;
info.subactionPaths = subaction_paths;
const XrResult result = xrCreateAction(hand_interaction_action_set_, &info, action);
if (XR_FAILED(result)) {
spdlog::get("illixr")->error("xrCreateAction({}) failed: {}", name, static_cast<int>(result));
return false;
}
return true;
};
return create_action(XR_ACTION_TYPE_BOOLEAN_INPUT, "controller_trigger_click", "Controller Trigger Click",
&controller_trigger_click_action_) &&
create_action(XR_ACTION_TYPE_FLOAT_INPUT, "controller_trigger_value", "Controller Trigger Value",
&controller_trigger_value_action_) &&
create_action(XR_ACTION_TYPE_FLOAT_INPUT, "controller_squeeze_value", "Controller Squeeze Value",
&controller_squeeze_value_action_) &&
create_action(XR_ACTION_TYPE_BOOLEAN_INPUT, "controller_primary_click", "Controller Primary Button",
&controller_primary_click_action_) &&
create_action(XR_ACTION_TYPE_BOOLEAN_INPUT, "controller_secondary_click", "Controller Secondary Button",
&controller_secondary_click_action_) &&
create_action(XR_ACTION_TYPE_BOOLEAN_INPUT, "controller_stick_click", "Controller Thumbstick Click",
&controller_thumbstick_click_action_) &&
create_action(XR_ACTION_TYPE_VECTOR2F_INPUT, "controller_stick_axis", "Controller Thumbstick Axis",
&controller_thumbstick_axis_action_);
}
bool oxr_relay::suggest_controller_bindings() {
XrPath profile_path = XR_NULL_PATH;
XrResult result = xrStringToPath(instance_, "/interaction_profiles/oculus/touch_controller", &profile_path);
if (XR_FAILED(result)) {
spdlog::get("illixr")->error("Could not create Oculus Touch interaction profile path: {}", static_cast<int>(result));
return false;
}
std::vector<XrActionSuggestedBinding> bindings;
const auto add = [&](XrAction action, const std::string& path_string) {
XrPath path = XR_NULL_PATH;
const XrResult path_result = xrStringToPath(instance_, path_string.c_str(), &path);
if (XR_FAILED(path_result)) {
spdlog::get("illixr")->error("Could not create controller binding path {}: {}", path_string,
static_cast<int>(path_result));
return false;
}
bindings.push_back({action, path});
return true;
};
const char* hands[2] = {"/user/hand/left", "/user/hand/right"};
for (const char* hand : hands) {
const std::string prefix{hand};
if (!add(interaction_pose_actions_[pose::GRIP], prefix + "/input/grip/pose") ||
!add(interaction_pose_actions_[pose::AIM], prefix + "/input/aim/pose") ||
!add(controller_trigger_click_action_, prefix + "/input/trigger/value") ||
!add(controller_trigger_value_action_, prefix + "/input/trigger/value") ||
!add(controller_squeeze_value_action_, prefix + "/input/squeeze/value") ||
!add(controller_thumbstick_click_action_, prefix + "/input/thumbstick/click") ||
!add(controller_thumbstick_axis_action_, prefix + "/input/thumbstick")) {
return false;
}
}
if (!add(controller_primary_click_action_, "/user/hand/left/input/x/click") ||
!add(controller_primary_click_action_, "/user/hand/right/input/a/click") ||
!add(controller_secondary_click_action_, "/user/hand/left/input/y/click") ||
!add(controller_secondary_click_action_, "/user/hand/right/input/b/click")) {
return false;
}
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
suggested.interactionProfile = profile_path;
suggested.countSuggestedBindings = static_cast<std::uint32_t>(bindings.size());
suggested.suggestedBindings = bindings.data();
result = xrSuggestInteractionProfileBindings(instance_, &suggested);
if (result == XR_ERROR_PATH_UNSUPPORTED || result == XR_ERROR_PATH_INVALID) {
spdlog::get("illixr")->warn("Quest runtime did not accept Oculus Touch bindings: {}", static_cast<int>(result));
return true;
}
if (XR_FAILED(result)) {
spdlog::get("illixr")->error("xrSuggestInteractionProfileBindings(Oculus Touch) failed: {}", static_cast<int>(result));
return false;
}
spdlog::get("illixr")->info("Suggested {} bindings for Oculus Touch controllers", bindings.size());
return true;
}
#endif
void oxr_relay::destroy_hand_interaction() {
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
controller_actions_initialized_ = false;
#endif
for (int h = 0; h < 2; h++) {
for (int p = 0; p < pose::NUM_INTERACTION_POSES; p++) {
if (interaction_pose_spaces_[h][p] != XR_NULL_HANDLE) {
xrDestroySpace(interaction_pose_spaces_[h][p]);
interaction_pose_spaces_[h][p] = XR_NULL_HANDLE;
}
}
if (palm_pose_spaces_[h] != XR_NULL_HANDLE) {
xrDestroySpace(palm_pose_spaces_[h]);
palm_pose_spaces_[h] = XR_NULL_HANDLE;
}
}
if (hand_interaction_action_set_ != XR_NULL_HANDLE) {
xrDestroyActionSet(hand_interaction_action_set_);
hand_interaction_action_set_ = XR_NULL_HANDLE;
}
}
void oxr_relay::update_hand_interaction(XrTime predicted_time) {
if (!hand_interaction_supported_ || hand_interaction_action_set_ == XR_NULL_HANDLE) {
spdlog::get("illixr")->debug("NO handle");
return;
}
std::lock_guard<std::mutex> actions_lock{actions_mutex_};
if (!sync_actions()) {
return;
}
// Collect poses
current_hand_interactions_.sensor_time = clock_->now();
const pose::side hand_enum[2] = {pose::LEFT, pose::RIGHT};
const char* type_names[] = {"AIM", "GRIP", "PINCH", "POKE"};
const char* hand_names[] = {"LEFT", "RIGHT"};
for (int h = 0; h < 2; h++) {
pose::hand_interaction_poses& hand_poses = current_hand_interactions_.hands[hand_enum[h]];
for (int p = 0; p < pose::NUM_INTERACTION_POSES; p++) {
// spdlog::get("illixr")->debug("Checking {} {}", hand_names[h], type_names[p]);
auto pose_type = static_cast<pose::interaction_pose_type>(p);
pose::hand_interaction_pose& interaction_pose = hand_poses[pose_type];
float f_state = 0.;
bool b_state = false;
// Check if this pose action is active for this hand
XrActionStateGetInfo get_info = {XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = interaction_pose_actions_[p];
get_info.subactionPath = hand_subaction_paths_[h];
XrActionStatePose pose_state = {XR_TYPE_ACTION_STATE_POSE};
if (XR_FAILED(xrGetActionStatePose(session_, &get_info, &pose_state))) {
spdlog::get("illixr")->debug(" {} {} get action state failed", hand_names[h], type_names[p]);
continue;
}
if (!pose_state.isActive) {
spdlog::get("illixr")->debug(" Not active");
continue;
}
// Fetch value/ready FIRST for AIM/GRIP/PINCH — for PINCH this gates
// whether the space will have valid location flags.
// bool gesture_active = true; // assume true for POKE which has no value/ready
if (p < 3) { // AIM=0, GRIP=1, PINCH=2
XrActionStateGetInfo float_info = {XR_TYPE_ACTION_STATE_GET_INFO};
float_info.action = interaction_value_actions_[p];
float_info.subactionPath = hand_subaction_paths_[h];
XrActionStateFloat float_state = {XR_TYPE_ACTION_STATE_FLOAT};
if (XR_SUCCEEDED(xrGetActionStateFloat(session_, &float_info, &float_state)) && float_state.isActive) {
f_state = float_state.currentState;
}
XrActionStateGetInfo bool_info = {XR_TYPE_ACTION_STATE_GET_INFO};
bool_info.action = interaction_ready_actions_[p];
bool_info.subactionPath = hand_subaction_paths_[h];
XrActionStateBoolean bool_state = {XR_TYPE_ACTION_STATE_BOOLEAN};
if (XR_SUCCEEDED(xrGetActionStateBoolean(session_, &bool_info, &bool_state)) && bool_state.isActive) {
b_state = (bool_state.currentState == XR_TRUE);
}
// For PINCH specifically: the pose space is only valid when the
// gesture is occurring. Querying it with zero value will always
// yield locationFlags=0, which is correct per spec but useless.
if (pose_type == pose::PINCH && f_state <= 0.0f) {
// spdlog::get("illixr")->debug(" {} PINCH value={:.3f}, skipping space locate",
// hand_names[h], interaction_pose.value);
interaction_pose.value = f_state;
interaction_pose.ready = b_state;
continue;
} else if (pose_type == pose::PINCH) {
// spdlog::get("illixr")->debug(" {} PINCH value={:.3f}", hand_names[h], interaction_pose.value);
}
}
// spdlog::get("illixr")->debug(" {} is active", type_names[p]);
// Locate the action space relative to the local tracking space
XrSpaceLocation space_loc = {XR_TYPE_SPACE_LOCATION};
auto t3 = std::chrono::steady_clock::now();
XrResult locate_result = xrLocateSpace(interaction_pose_spaces_[h][p], local_space_, predicted_time, &space_loc);
auto t4 = std::chrono::steady_clock::now();
auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(t4 - t3).count() / 1000.0;
if (ms1 > TIME_CUTOFF)
spdlog::get("illixr")->warn("[oxr_timing] xrLocateSpace (interaction {}/{}) took {:.2f}ms", hand_names[h],
type_names[p], ms1);
const bool pos_valid = (space_loc.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0u;
const bool ori_valid = (space_loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0u;
if (XR_SUCCEEDED(locate_result) && pos_valid && ori_valid) {
// spdlog::get("illixr")->debug(" {} {} succeeded", hand_names[h], type_names[p]);
interaction_pose.update(space_loc, f_state, b_state, predicted_time);
} else {
// spdlog::get("illixr")->debug(" {} {} locate failed: result={} flags={}",
// hand_names[h], type_names[p],
// static_cast(locate_result),
// static_cast(space_loc.locationFlags));
}
}
}
if (current_hand_interactions_.is_valid()) {
// Throttle logging to once every 300 frames
static uint64_t hi_log_counter = 0;
if ((++hi_log_counter % 300) == 1) {
// spdlog::get("illixr")->debug("update_hand_interaction: logging");
const pose::side hand_enums[] = {pose::LEFT, pose::RIGHT};
const pose::interaction_pose_type type_enums[] = {pose::AIM, pose::GRIP, pose::PINCH, pose::POKE};
for (auto hand : hand_enums) {
const auto& hip = current_hand_interactions_.hands.at(hand);
for (auto type_enum : type_enums) {
const auto& ip = hip.at(type_enum);
if (!ip.valid()) {
// spdlog::get("illixr")->debug("[hand_interaction] {} {} not valid", hand_names[h], type_names[t]);
continue;
}
// if (type_enums[t] == pose::POKE) {
// spdlog::get("illixr")->debug(
// "[hand_interactionX] {} {} | pos=({:.3f},{:.3f},{:.3f})",
// hand_names[h], type_names[t],
// ip.position.x(), ip.position.y(), ip.position.z());
//} else {
// spdlog::get("illixr")->debug(
// "[hand_interactionX] {} {} | pos=({:.3f},{:.3f},{:.3f}) | value={:.3f} ready={}",
// hand_names[h], type_names[t],
// ip.position.x(), ip.position.y(), ip.position.z(),
// ip.value, ip.ready ? "true" : "false");
//}
}
}
}
}
// Palm poses via XR_EXT_palm_pose
// Located here (after xrSyncActions) so the action-system state is current.
// The palm pose action was bound against multiple interaction profiles in
// init_hand_interaction(), so the runtime resolves it from whichever input
// source is active (bare hands or controllers).
current_palm_poses_.sensor_time = clock_->now();
for (int h = 0; h < 2; h++) {
if (palm_pose_spaces_[h] == XR_NULL_HANDLE)
continue;
XrActionStateGetInfo get_info = {XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = palm_pose_action_;
get_info.subactionPath = hand_subaction_paths_[h];
XrActionStatePose pose_state = {XR_TYPE_ACTION_STATE_POSE};
if (XR_FAILED(xrGetActionStatePose(session_, &get_info, &pose_state)) || !pose_state.isActive) {
current_palm_poses_.hands[(h == 0) ? pose::LEFT : pose::RIGHT].relation_flags = 0;
continue;
}
XrSpaceVelocity velocity = {XR_TYPE_SPACE_VELOCITY};
XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
location.next = &velocity;
XrResult r = xrLocateSpace(palm_pose_spaces_[h], local_space_, predicted_time, &location);
const bool pos_valid = (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0u;
const bool ori_valid = (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0u;
if (XR_SUCCEEDED(r) && pos_valid && ori_valid) {
pose::side palm_hand = (h == 0) ? pose::LEFT : pose::RIGHT;
current_palm_poses_.hands[palm_hand].update(location, velocity);
} else {
current_palm_poses_.hands[(h == 0) ? pose::LEFT : pose::RIGHT].relation_flags = 0;
}
}
if (current_palm_poses_.is_valid()) {
// Throttle logging to once every 300 frames
static uint64_t pp_log_counter = 0;
if ((++pp_log_counter % log_interval) == 1) {
const pose::side hand_enums[] = {pose::LEFT, pose::RIGHT};
for (int h = 0; h < 2; h++) {
const auto& pd = current_palm_poses_.hands.at(hand_enums[h]);
if (!pd.is_valid()) {
spdlog::get("illixr")->debug("[palm_pose] {} not valid", hand_names[h]);
continue;
}
}
}
}
}
// Shared by hand interaction updates and Quest controller sampling. Both callers
// hold actions_mutex_ so they cannot sync/read the same action set concurrently.
bool oxr_relay::sync_actions() {
if (hand_interaction_action_set_ == XR_NULL_HANDLE || session_ == XR_NULL_HANDLE) {
return false;
}
XrActiveActionSet active_set{};
active_set.actionSet = hand_interaction_action_set_;
active_set.subactionPath = XR_NULL_PATH;
XrActionsSyncInfo sync_info{XR_TYPE_ACTIONS_SYNC_INFO};
sync_info.countActiveActionSets = 1;
sync_info.activeActionSets = &active_set;
const auto start = std::chrono::steady_clock::now();
const XrResult result = xrSyncActions(session_, &sync_info);
const auto elapsed =
std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count() / 1000.0;
if (elapsed > TIME_CUTOFF) {
spdlog::get("illixr")->warn("[oxr_timing] xrSyncActions took {:.2f}ms", elapsed);
}
if (result == XR_SESSION_NOT_FOCUSED) {
return false;
}
if (XR_FAILED(result)) {
spdlog::get("illixr")->warn("xrSyncActions failed: {}", static_cast<int>(result));
return false;
}
return true;
}
#ifdef ILLIXR_ENABLE_QUEST_CONTROLLERS
void oxr_relay::refresh_controller_profiles() {
for (std::size_t hand = 0; hand < controller_profiles_.size(); ++hand) {
XrInteractionProfileState state{XR_TYPE_INTERACTION_PROFILE_STATE};
const XrResult result = xrGetCurrentInteractionProfile(session_, hand_subaction_paths_[hand], &state);
if (XR_FAILED(result)) {
continue;
}
std::string path;
if (state.interactionProfile != XR_NULL_PATH) {
std::uint32_t size = 0;
if (XR_SUCCEEDED(xrPathToString(instance_, state.interactionProfile, 0, &size, nullptr)) && size > 0) {
std::vector<char> buffer(size);
if (XR_SUCCEEDED(xrPathToString(instance_, state.interactionProfile, size, &size, buffer.data()))) {
path.assign(buffer.data());
}
}
}
const auto profile = controller_profile_from_path(path);
if (controller_profiles_[hand] != profile) {
controller_profiles_[hand] = profile;
spdlog::get("illixr")->info("{} controller profile -> {}", hand == 0 ? "left" : "right",
path.empty() ? "none" : path);
}
}
}
bool oxr_relay::query_controller_pose(XrAction action, XrSpace space, XrPath hand_path, XrTime sample_time,
quest_controller_pose* pose_out) {
*pose_out = quest_controller_pose{};
XrActionStateGetInfo get_info{XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = action;
get_info.subactionPath = hand_path;
XrActionStatePose state{XR_TYPE_ACTION_STATE_POSE};
XrResult result = xrGetActionStatePose(session_, &get_info, &state);
if (XR_FAILED(result)) {
spdlog::get("illixr")->warn("xrGetActionStatePose(controller) failed: {}", static_cast<int>(result));
return false;
}
pose_out->active = state.isActive == XR_TRUE;
if (!pose_out->active) {
return true;
}
XrSpaceLocation location{XR_TYPE_SPACE_LOCATION};
result = xrLocateSpace(space, local_space_, sample_time, &location);
if (XR_FAILED(result)) {
spdlog::get("illixr")->warn("xrLocateSpace(controller) failed: {}", static_cast<int>(result));
return false;
}
pose_out->position_valid = (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0;
pose_out->orientation_valid = (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0;
pose_out->position_tracked = (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) != 0;
pose_out->orientation_tracked = (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT) != 0;
if (pose_out->position_valid) {
pose_out->position = location.pose.position;
}
if (pose_out->orientation_valid) {
pose_out->orientation = location.pose.orientation;
}
return true;
}
bool oxr_relay::query_controller_boolean(XrAction action, XrPath hand_path, quest_controller_button* button) {
*button = quest_controller_button{};
XrActionStateGetInfo get_info{XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = action;
get_info.subactionPath = hand_path;
XrActionStateBoolean state{XR_TYPE_ACTION_STATE_BOOLEAN};
const XrResult result = xrGetActionStateBoolean(session_, &get_info, &state);
if (XR_FAILED(result)) {
return false;
}
button->active = state.isActive == XR_TRUE;
button->pressed = button->active && state.currentState == XR_TRUE;
button->changed_since_last_sync = state.changedSinceLastSync == XR_TRUE;
button->value = button->pressed ? 1.0F : 0.0F;
button->last_change_time = static_cast<std::int64_t>(state.lastChangeTime);
return true;
}
bool oxr_relay::query_controller_float(XrAction action, XrPath hand_path, float threshold, quest_controller_button* button) {
*button = quest_controller_button{};
XrActionStateGetInfo get_info{XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = action;
get_info.subactionPath = hand_path;
XrActionStateFloat state{XR_TYPE_ACTION_STATE_FLOAT};
const XrResult result = xrGetActionStateFloat(session_, &get_info, &state);
if (XR_FAILED(result)) {
return false;
}
button->active = state.isActive == XR_TRUE;
button->value = button->active ? state.currentState : 0.0F;
button->pressed = button->active && state.currentState >= threshold;
button->changed_since_last_sync = state.changedSinceLastSync == XR_TRUE;
button->last_change_time = static_cast<std::int64_t>(state.lastChangeTime);
return true;
}
bool oxr_relay::query_controller_axis(XrAction action, XrPath hand_path, quest_controller_axis2d* axis) {
*axis = quest_controller_axis2d{};
XrActionStateGetInfo get_info{XR_TYPE_ACTION_STATE_GET_INFO};
get_info.action = action;
get_info.subactionPath = hand_path;
XrActionStateVector2f state{XR_TYPE_ACTION_STATE_VECTOR2F};
const XrResult result = xrGetActionStateVector2f(session_, &get_info, &state);
if (XR_FAILED(result)) {
return false;
}
axis->active = state.isActive == XR_TRUE;
axis->changed_since_last_sync = state.changedSinceLastSync == XR_TRUE;
axis->last_change_time = static_cast<std::int64_t>(state.lastChangeTime);
if (axis->active) {
axis->value = {state.currentState.x, state.currentState.y};
}
return true;
}
void oxr_relay::merge_controller_button(quest_controller_button* destination, const quest_controller_button& source) {
destination->active = destination->active || source.active;
destination->pressed = destination->pressed || source.pressed;
destination->changed_since_last_sync = destination->changed_since_last_sync || source.changed_since_last_sync;
destination->value = std::max(destination->value, source.value);
destination->last_change_time = std::max(destination->last_change_time, source.last_change_time);
}
bool oxr_relay::query_controller_hand(std::size_t hand_index, XrTime sample_time, quest_hand_controller* hand) {
*hand = quest_hand_controller{};
hand->interaction_profile = controller_profiles_[hand_index];
if (!query_controller_pose(interaction_pose_actions_[pose::GRIP], interaction_pose_spaces_[hand_index][pose::GRIP],
hand_subaction_paths_[hand_index], sample_time, &hand->grip_pose) ||
!query_controller_pose(interaction_pose_actions_[pose::AIM], interaction_pose_spaces_[hand_index][pose::AIM],
hand_subaction_paths_[hand_index], sample_time, &hand->aim_pose)) {
return false;
}
quest_controller_button trigger_click;
quest_controller_button trigger_value;
if (!query_controller_boolean(controller_trigger_click_action_, hand_subaction_paths_[hand_index], &trigger_click) ||
!query_controller_float(controller_trigger_value_action_, hand_subaction_paths_[hand_index],
kControllerTriggerThreshold, &trigger_value) ||
!query_controller_float(controller_squeeze_value_action_, hand_subaction_paths_[hand_index],
kControllerSqueezeThreshold, &hand->squeeze) ||
!query_controller_boolean(controller_primary_click_action_, hand_subaction_paths_[hand_index], &hand->primary) ||
!query_controller_boolean(controller_secondary_click_action_, hand_subaction_paths_[hand_index], &hand->secondary) ||
!query_controller_boolean(controller_thumbstick_click_action_, hand_subaction_paths_[hand_index],
&hand->thumbstick_click) ||
!query_controller_axis(controller_thumbstick_axis_action_, hand_subaction_paths_[hand_index], &hand->thumbstick)) {
return false;
}
hand->trigger = trigger_click;
merge_controller_button(&hand->trigger, trigger_value);
hand->available = hand->grip_pose.active || hand->aim_pose.active || hand->trigger.active || hand->squeeze.active ||
hand->primary.active || hand->secondary.active || hand->thumbstick_click.active || hand->thumbstick.active;
return true;
}
#endif
#ifdef ILLIXR_ENABLE_BOBA
void oxr_relay::publish_boba_input(XrTime predicted_time, XrDuration predicted_period, XrBool32 should_render,
XrViewStateFlags view_flags, const XrView views[2],
const XrViewConfigurationView view_configs[2]) {
// One sequence and host timestamp bind the separately transported events
// into a coherent predicted-display-time snapshot on the desktop.
const std::uint64_t sequence = boba_input_sequence_.fetch_add(1, std::memory_order_relaxed);
const time_point now = clock_->now();
quest_controller_input controller;
controller.sequence = sequence;
controller.sample_time = now;
controller.xr_sample_time = static_cast<std::int64_t>(predicted_time);
if (controller_actions_initialized_) {
std::lock_guard<std::mutex> actions_lock{actions_mutex_};
if (sync_actions()) {
refresh_controller_profiles();
if (!query_controller_hand(0, predicted_time, &controller.left) ||
!query_controller_hand(1, predicted_time, &controller.right)) {
spdlog::get("illixr")->warn("Could not sample all Quest controller actions");
}
}
}
openxr_view_frame frame;
frame.sequence = sequence;
frame.sample_time = now;
frame.xr_sample_time = static_cast<std::int64_t>(predicted_time);
frame.xr_predicted_display_period = static_cast<std::int64_t>(predicted_period);
frame.should_render = should_render == XR_TRUE;
const bool pose_valid =
(view_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0 && (view_flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
const bool pose_tracked =
(view_flags & XR_VIEW_STATE_POSITION_TRACKED_BIT) != 0 && (view_flags & XR_VIEW_STATE_ORIENTATION_TRACKED_BIT) != 0;
const auto copy_view = [pose_valid, pose_tracked](const XrView& source, const XrViewConfigurationView& config,
openxr_eye_view* destination) {
destination->valid = pose_valid;
destination->pose_tracked = pose_tracked;
destination->recommended_width = config.recommendedImageRectWidth;
destination->recommended_height = config.recommendedImageRectHeight;
destination->angle_left = source.fov.angleLeft;
destination->angle_right = source.fov.angleRight;
destination->angle_up = source.fov.angleUp;
destination->angle_down = source.fov.angleDown;
if (pose_valid) {
destination->update(source.pose);
}
};
copy_view(views[0], view_configs[0], &frame.left);
copy_view(views[1], view_configs[1], &frame.right);
// Publish only after both objects are complete; consumers reject a sample
// until the matching sequence has arrived on both UDP topics.
quest_controller_writer_.put(std::make_shared<quest_controller_input>(std::move(controller)));
openxr_view_writer_.put(std::make_shared<openxr_view_frame>(std::move(frame)));
}
#endif
void oxr_relay::calibrate_time_offsets() {
// On Quest/Android, XrTime is nanoseconds since device boot,
// equivalent to CLOCK_BOOTTIME. During an active XR session the
// device does not suspend, so CLOCK_BOOTTIME == CLOCK_MONOTONIC.
// We therefore approximate xr_to_monotonic_offset as zero and
// compute monotonic_to_system_offset directly.
//
// Verification: sample XrTime from predicted_time_ alongside
// CLOCK_MONOTONIC to confirm they are in the same timebase.
xr_to_monotonic_offset_ns_ = 0;
// Compute monotonic_to_system_offset_ns_:
// system_clock_ns = CLOCK_MONOTONIC_ns + monotonic_to_system_offset_ns_
constexpr int NUM_SAMPLES = 20;
int64_t best_offset = 0;
int64_t best_rtt = INT64_MAX;
for (int i = 0; i < NUM_SAMPLES; i++) {
auto before_sys = std::chrono::system_clock::now();
struct timespec ts_mono;
clock_gettime(CLOCK_MONOTONIC, &ts_mono);
auto after_sys = std::chrono::system_clock::now();
int64_t before_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(before_sys.time_since_epoch()).count();
int64_t after_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(after_sys.time_since_epoch()).count();
int64_t rtt = after_ns - before_ns;
int64_t mid_ns = before_ns + rtt / 2;
int64_t mono_ns = ts_mono.tv_sec * 1'000'000'000LL + ts_mono.tv_nsec;
int64_t offset = mid_ns - mono_ns;
if (rtt < best_rtt) {
best_rtt = rtt;
best_offset = offset;
}
}
monotonic_to_system_offset_ns_ = best_offset;
time_offsets_calibrated_ = true;
/*spdlog::get("illixr")->info(
"oxr_relay: calibrate_time_offsets: "
"xr_to_monotonic={:.6f}s (assumed 0, Quest has no conversion extension) "
"monotonic_to_system={:.6f}s",
xr_to_monotonic_offset_ns_ / 1'000'000'000.0,
monotonic_to_system_offset_ns_ / 1'000'000'000.0);
*/
}
bool oxr_relay::get_pose_history(uint64_t id, pose_history_entry& out_entry) const {
std::lock_guard<std::mutex> lock(pose_history_mutex_);
auto it = pose_history_.find(id);
if (it == pose_history_.end()) {
return false;
}
out_entry = it->second;
return true;
}