From 89d702763d22aa29458dab06265bcd18a23e3ccd Mon Sep 17 00:00:00 2001
From: Daniel Adams <70986246+msub2@users.noreply.github.com>
Date: Fri, 26 Jul 2024 02:29:35 -1000
Subject: [PATCH] Surface supported interaction profiles from OpenXR runtime
 (#229)

* Surface interaction profiles from OpenXR runtime

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Remove generic hand as the default input profile

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Export ext_string macro

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update touch controller profile to use v3 input profiles

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Use &'static str consistently, code styling

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update openxrs version, update meta touch plus profile

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix errors in touch pro/touch plus profiles

Signed-off-by: Daniel Adams <msub2official@gmail.com>

---------

Signed-off-by: Daniel Adams <msub2official@gmail.com>
---
 webxr/Cargo.toml                     |   2 +-
 webxr/openxr/input.rs                |  80 +++---
 webxr/openxr/interaction_profiles.rs | 409 +++++++++++++++++++++++++++
 webxr/openxr/mod.rs                  |  50 +++-
 4 files changed, 502 insertions(+), 39 deletions(-)
 create mode 100644 webxr/openxr/interaction_profiles.rs

diff --git a/webxr/Cargo.toml b/webxr/Cargo.toml
index 35cacf3..a86fe02 100644
--- a/webxr/Cargo.toml
+++ b/webxr/Cargo.toml
@@ -31,7 +31,7 @@ webxr-api = { path = "../webxr-api" }
 crossbeam-channel = "0.5"
 euclid = "0.22"
 log = "0.4.6"
-openxr = { version = "0.18", optional = true }
+openxr = { version = "0.19", optional = true }
 serde = { version = "1.0", optional = true }
 sparkle = "0.1"
 surfman = { version = "0.9", features = ["chains"] }
diff --git a/webxr/openxr/input.rs b/webxr/openxr/input.rs
index 83380c6..4ce5f68 100644
--- a/webxr/openxr/input.rs
+++ b/webxr/openxr/input.rs
@@ -1,4 +1,5 @@
 use euclid::RigidTransform3D;
+use log::debug;
 use openxr::d3d::D3D11;
 use openxr::{
     self, Action, ActionSet, Binding, FrameState, Hand as HandEnum, HandJoint, HandTracker,
@@ -19,6 +20,9 @@ use webxr_api::Viewer;
 
 use super::IDENTITY_POSE;
 
+use crate::ext_string;
+use crate::openxr::interaction_profiles::INTERACTION_PROFILES;
+
 /// Number of frames to wait with the menu gesture before
 /// opening the menu.
 const MENU_GESTURE_SUSTAIN_THRESHOLD: u8 = 60;
@@ -173,6 +177,7 @@ impl OpenXRInput {
         instance: &Instance,
         session: &Session<D3D11>,
         needs_hands: bool,
+        supported_interaction_profiles: Vec<&'static str>,
     ) -> (ActionSet, Self, Self) {
         let action_set = instance.create_action_set("hands", "Hands", 0).unwrap();
         let right_hand = OpenXRInput::new(
@@ -190,32 +195,33 @@ impl OpenXRInput {
             needs_hands,
         );
 
-        let mut bindings =
-            right_hand.get_bindings(instance, "trigger/value", Some("squeeze/click"));
-        bindings.extend(
-            left_hand
-                .get_bindings(instance, "trigger/value", Some("squeeze/click"))
-                .into_iter(),
-        );
-        let path_controller = instance
-            .string_to_path("/interaction_profiles/microsoft/motion_controller")
-            .unwrap();
-        instance
-            .suggest_interaction_profile_bindings(path_controller, &bindings)
-            .unwrap();
+        for profile in INTERACTION_PROFILES {
+            if let Some(extension_name) = profile.required_extension {
+                if !supported_interaction_profiles.contains(&ext_string!(extension_name)) {
+                    continue;
+                }
+            }
+            let select = profile.standard_buttons[0];
+            let squeeze = Option::from(profile.standard_buttons[1]).filter(|&s| !s.is_empty());
+            let mut bindings = right_hand.get_bindings(instance, select, squeeze);
+            bindings.extend(
+                left_hand
+                    .get_bindings(instance, select, squeeze)
+                    .into_iter(),
+            );
+            let path_controller = instance
+                .string_to_path(profile.path)
+                .expect(format!("Invalid interaction profile path: {}", profile.path).as_str());
+            if let Err(_) =
+                instance.suggest_interaction_profile_bindings(path_controller, &bindings)
+            {
+                debug!(
+                    "Interaction profile path not available for this runtime: {:?}",
+                    profile.path
+                );
+            }
+        }
 
-        let mut bindings = right_hand.get_bindings(instance, "select/click", None);
-        bindings.extend(
-            left_hand
-                .get_bindings(instance, "select/click", None)
-                .into_iter(),
-        );
-        let path_controller = instance
-            .string_to_path("/interaction_profiles/khr/simple_controller")
-            .unwrap();
-        instance
-            .suggest_interaction_profile_bindings(path_controller, &bindings)
-            .unwrap();
         session.attach_action_sets(&[&action_set]).unwrap();
 
         (action_set, right_hand, left_hand)
@@ -230,22 +236,34 @@ impl OpenXRInput {
         let hand = hand_str(self.handedness);
         let path_aim_pose = instance
             .string_to_path(&format!("/user/hand/{}/input/aim/pose", hand))
-            .unwrap();
+            .expect(&format!(
+                "Failed to create path for /user/hand/{}/input/aim/pose",
+                hand
+            ));
         let binding_aim_pose = Binding::new(&self.action_aim_pose, path_aim_pose);
         let path_grip_pose = instance
             .string_to_path(&format!("/user/hand/{}/input/grip/pose", hand))
-            .unwrap();
+            .expect(&format!(
+                "Failed to create path for /user/hand/{}/input/grip/pose",
+                hand
+            ));
         let binding_grip_pose = Binding::new(&self.action_grip_pose, path_grip_pose);
         let path_click = instance
             .string_to_path(&format!("/user/hand/{}/input/{}", hand, select_name))
-            .unwrap();
+            .expect(&format!(
+                "Failed to create path for /user/hand/{}/input/{}",
+                hand, select_name
+            ));
         let binding_click = Binding::new(&self.action_click, path_click);
 
         let mut ret = vec![binding_aim_pose, binding_grip_pose, binding_click];
         if let Some(squeeze_name) = squeeze_name {
             let path_squeeze = instance
                 .string_to_path(&format!("/user/hand/{}/input/{}", hand, squeeze_name))
-                .unwrap();
+                .expect(&format!(
+                    "Failed to create path for /user/hand/{}/input/{}",
+                    hand, squeeze_name
+                ));
             let binding_squeeze = Binding::new(&self.action_squeeze, path_squeeze);
             ret.push(binding_squeeze);
         }
@@ -349,9 +367,7 @@ impl OpenXRInput {
             id: self.id,
             target_ray_mode: TargetRayMode::TrackedPointer,
             supports_grip: true,
-            // XXXManishearth update with whatever we decide
-            // in https://github.com/immersive-web/webxr-input-profiles/issues/105
-            profiles: vec!["generic-hand".into()],
+            profiles: vec![],
             hand_support,
         }
     }
diff --git a/webxr/openxr/interaction_profiles.rs b/webxr/openxr/interaction_profiles.rs
new file mode 100644
index 0000000..0e3041a
--- /dev/null
+++ b/webxr/openxr/interaction_profiles.rs
@@ -0,0 +1,409 @@
+use openxr::{
+    sys::{
+        BD_CONTROLLER_INTERACTION_EXTENSION_NAME, EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME,
+        EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME, FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME,
+        HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME,
+        HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME,
+        META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME, ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME,
+    },
+    ExtensionSet,
+};
+
+#[macro_export]
+macro_rules! ext_string {
+    ($ext_name:expr) => {
+        std::str::from_utf8($ext_name).unwrap()
+    };
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum InteractionProfileType {
+    KhrSimpleController,
+    BytedancePicoNeo3Controller,
+    BytedancePico4Controller,
+    BytedancePicoG3Controller,
+    GoogleDaydreamController,
+    HpMixedRealityController,
+    HtcViveController,
+    HtcViveCosmosController,
+    HtcViveFocus3Controller,
+    MagicLeap2Controller,
+    MicrosoftMixedRealityMotionController,
+    OculusGoController,
+    OculusTouchController,
+    FacebookTouchControllerPro,
+    MetaTouchPlusController,
+    MetaTouchControllerRiftCv1,
+    MetaTouchControllerQuest1RiftS,
+    MetaTouchControllerQuest2,
+    SamsungOdysseyController,
+    ValveIndexController,
+}
+
+#[derive(Clone, Copy, Debug)]
+#[allow(unused)]
+pub struct InteractionProfile<'a> {
+    pub profile_type: InteractionProfileType,
+    /// The interaction profile path
+    pub path: &'static str,
+    /// The OpenXR extension, if any, required to use this profile
+    pub required_extension: Option<&'a [u8]>,
+    /// Trigger, Grip, Touchpad, Thumbstick
+    pub standard_buttons: &'a [&'a str],
+    /// Touchpad X, Touchpad Y, Thumbstick X, Thumbstick Y
+    pub standard_axes: &'a [&'a str],
+    /// Any additional buttons on the left controller
+    pub left_buttons: &'a [&'a str],
+    /// Any additional buttons on the right controller
+    pub right_buttons: &'a [&'a str],
+    /// The corresponding WebXR Input Profile names
+    pub profiles: &'a [&'a str],
+}
+
+pub static KHR_SIMPLE_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::KhrSimpleController,
+    path: "/interaction_profiles/khr/simple_controller",
+    required_extension: None,
+    standard_buttons: &["select/click", "", "", ""],
+    standard_axes: &["", "", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    profiles: &["generic-trigger"],
+};
+
+pub static BYTEDANCE_PICO_NEO3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::BytedancePicoNeo3Controller,
+    path: "/interaction_profiles/bytedance/pico_neo3_controller",
+    required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["pico-neo3", "generic-trigger-squeeze-thumbstick"],
+};
+
+pub static BYTEDANCE_PICO_4_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::BytedancePico4Controller,
+    path: "/interaction_profiles/bytedance/pico4_controller",
+    required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["pico-4", "generic-trigger-squeeze-thumbstick"],
+};
+
+pub static BYTEDANCE_PICO_G3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::BytedancePicoG3Controller,
+    path: "/interaction_profiles/bytedance/pico_g3_controller",
+    required_extension: Some(BD_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "", "", "thumbstick/click"],
+    // Note: X/Y components not listed in the OpenXR spec currently due to vendor error.
+    // See <https://github.com/KhronosGroup/OpenXR-Docs/issues/158>
+    // It also uses the thumbstick path despite clearly being a touchpad, so
+    // move those values into the touchpad axes slots
+    standard_axes: &["thumbstick/x", "thumbstick/y", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    // Note: There is no corresponding WebXR Input profile for the Pico G3,
+    // but the controller seems identical to the G2, so use that instead.
+    profiles: &["pico-g2", "generic-trigger-touchpad"],
+};
+
+pub static GOOGLE_DAYDREAM_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::GoogleDaydreamController,
+    path: "/interaction_profiles/google/daydream_controller",
+    required_extension: None,
+    standard_buttons: &["select/click", "", "trackpad/click", ""],
+    standard_axes: &["trackpad/x", "trackpad/y", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    profiles: &["google-daydream", "generic-touchpad"],
+};
+
+pub static HP_MIXED_REALITY_MOTION_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::HpMixedRealityController,
+    path: "/interaction_profiles/hp/mixed_reality_controller",
+    required_extension: Some(EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "hp-mixed-reality",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static HTC_VIVE_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::HtcViveController,
+    path: "/interaction_profiles/htc/vive_controller",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/click", "trackpad/click", ""],
+    standard_axes: &["trackpad/x", "trackpad/y", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    profiles: &["htc-vive", "generic-trigger-squeeze-touchpad"],
+};
+
+pub static HTC_VIVE_COSMOS_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::HtcViveCosmosController,
+    path: "/interaction_profiles/htc/vive_cosmos_controller",
+    required_extension: Some(HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/click", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["htc-vive-cosmos", "generic-trigger-squeeze-thumbstick"],
+};
+
+pub static HTC_VIVE_FOCUS3_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::HtcViveFocus3Controller,
+    path: "/interaction_profiles/htc/vive_focus3_controller",
+    required_extension: Some(HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["htc-vive-focus-3", "generic-trigger-squeeze-thumbstick"],
+};
+
+pub static MAGIC_LEAP_2_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::MagicLeap2Controller,
+    path: "/interaction_profiles/ml/ml2_controller",
+    required_extension: Some(ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "", "trackpad/click", ""],
+    standard_axes: &["trackpad/x", "trackpad/y", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    // Note: There is no corresponding WebXR Input profile for the Magic Leap 2,
+    // but the controller seems mostly identical to the 1, so use that instead.
+    profiles: &["magicleap-one", "generic-trigger-squeeze-touchpad"],
+};
+
+pub static MICROSOFT_MIXED_REALITY_MOTION_CONTROLLER_PROFILE: InteractionProfile =
+    InteractionProfile {
+        profile_type: InteractionProfileType::MicrosoftMixedRealityMotionController,
+        path: "/interaction_profiles/microsoft/motion_controller",
+        required_extension: None,
+        standard_buttons: &[
+            "trigger/value",
+            "squeeze/click",
+            "trackpad/click",
+            "thumbstick/click",
+        ],
+        standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"],
+        left_buttons: &[],
+        right_buttons: &[],
+        profiles: &[
+            "microsoft-mixed-reality",
+            "generic-trigger-squeeze-touchpad-thumbstick",
+        ],
+    };
+
+pub static OCULUS_GO_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::OculusGoController,
+    path: "/interaction_profiles/oculus/go_controller",
+    required_extension: None,
+    standard_buttons: &["trigger/click", "", "trackpad/click", ""],
+    standard_axes: &["trackpad/x", "trackpad/y", "", ""],
+    left_buttons: &[],
+    right_buttons: &[],
+    profiles: &["oculus-go", "generic-trigger-touchpad"],
+};
+
+pub static OCULUS_TOUCH_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::OculusTouchController,
+    path: "/interaction_profiles/oculus/touch_controller",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "oculus-touch-v3",
+        "oculus-touch-v2",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static FACEBOOK_TOUCH_CONTROLLER_PRO_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::FacebookTouchControllerPro,
+    path: "/interaction_profiles/facebook/touch_controller_pro",
+    required_extension: Some(FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "meta-quest-touch-pro",
+        "oculus-touch-v2",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static META_TOUCH_CONTROLLER_PLUS_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::MetaTouchPlusController,
+    path: "/interaction_profiles/meta/touch_controller_plus",
+    required_extension: Some(META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME),
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "meta-quest-touch-plus",
+        "oculus-touch-v3",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static META_TOUCH_CONTROLLER_RIFT_CV1_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::MetaTouchControllerRiftCv1,
+    path: "/interaction_profiles/meta/touch_controller_rift_cv1",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["oculus-touch", "generic-trigger-squeeze-thumbstick"],
+};
+
+pub static META_TOUCH_CONTROLLER_QUEST_1_RIFT_S_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::MetaTouchControllerQuest1RiftS,
+    path: "/interaction_profiles/meta/touch_controller_quest_1_rift_s",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "oculus-touch-v2",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static META_TOUCH_CONTROLLER_QUEST_2_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::MetaTouchControllerQuest2,
+    path: "/interaction_profiles/meta/touch_controller_quest_2",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["", "", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["x/click", "y/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &[
+        "oculus-touch-v3",
+        "oculus-touch-v2",
+        "oculus-touch",
+        "generic-trigger-squeeze-thumbstick",
+    ],
+};
+
+pub static SAMSUNG_ODYSSEY_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::SamsungOdysseyController,
+    path: "/interaction_profiles/samsung/odyssey_controller",
+    required_extension: Some(EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME),
+    standard_buttons: &[
+        "trigger/value",
+        "squeeze/click",
+        "trackpad/click",
+        "thumbstick/click",
+    ],
+    standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &[],
+    right_buttons: &[],
+    profiles: &[
+        "samsung-odyssey",
+        "microsoft-mixed-reality",
+        "generic-trigger-squeeze-touchpad-thumbstick",
+    ],
+};
+
+pub static VALVE_INDEX_CONTROLLER_PROFILE: InteractionProfile = InteractionProfile {
+    profile_type: InteractionProfileType::ValveIndexController,
+    path: "/interaction_profiles/valve/index_controller",
+    required_extension: None,
+    standard_buttons: &["trigger/value", "squeeze/value", "", "thumbstick/click"],
+    standard_axes: &["trackpad/x", "trackpad/y", "thumbstick/x", "thumbstick/y"],
+    left_buttons: &["a/click", "b/click"],
+    right_buttons: &["a/click", "b/click"],
+    profiles: &["valve-index", "generic-trigger-squeeze-touchpad-thumbstick"],
+};
+
+pub static INTERACTION_PROFILES: [InteractionProfile; 20] = [
+    KHR_SIMPLE_CONTROLLER_PROFILE,
+    BYTEDANCE_PICO_NEO3_CONTROLLER_PROFILE,
+    BYTEDANCE_PICO_4_CONTROLLER_PROFILE,
+    BYTEDANCE_PICO_G3_CONTROLLER_PROFILE,
+    GOOGLE_DAYDREAM_CONTROLLER_PROFILE,
+    HP_MIXED_REALITY_MOTION_CONTROLLER_PROFILE,
+    HTC_VIVE_CONTROLLER_PROFILE,
+    HTC_VIVE_COSMOS_CONTROLLER_PROFILE,
+    HTC_VIVE_FOCUS3_CONTROLLER_PROFILE,
+    MAGIC_LEAP_2_CONTROLLER_PROFILE,
+    MICROSOFT_MIXED_REALITY_MOTION_CONTROLLER_PROFILE,
+    OCULUS_GO_CONTROLLER_PROFILE,
+    OCULUS_TOUCH_CONTROLLER_PROFILE,
+    FACEBOOK_TOUCH_CONTROLLER_PRO_PROFILE,
+    META_TOUCH_CONTROLLER_PLUS_PROFILE,
+    META_TOUCH_CONTROLLER_RIFT_CV1_PROFILE,
+    META_TOUCH_CONTROLLER_QUEST_1_RIFT_S_PROFILE,
+    META_TOUCH_CONTROLLER_QUEST_2_PROFILE,
+    SAMSUNG_ODYSSEY_CONTROLLER_PROFILE,
+    VALVE_INDEX_CONTROLLER_PROFILE,
+];
+
+pub fn get_profiles_from_path(path: String) -> &'static [&'static str] {
+    INTERACTION_PROFILES
+        .iter()
+        .find(|profile| profile.path == path)
+        .map_or(&[], |profile| profile.profiles)
+}
+
+pub fn get_supported_interaction_profiles(
+    supported_extensions: &ExtensionSet,
+    enabled_extensions: &mut ExtensionSet,
+) -> Vec<&'static str> {
+    let mut extensions = Vec::new();
+    if supported_extensions.bd_controller_interaction {
+        extensions.push(ext_string!(BD_CONTROLLER_INTERACTION_EXTENSION_NAME));
+        enabled_extensions.bd_controller_interaction = true;
+    }
+    if supported_extensions.ext_hp_mixed_reality_controller {
+        extensions.push(ext_string!(EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME));
+        enabled_extensions.ext_hp_mixed_reality_controller = true;
+    }
+    if supported_extensions.ext_samsung_odyssey_controller {
+        extensions.push(ext_string!(EXT_SAMSUNG_ODYSSEY_CONTROLLER_EXTENSION_NAME));
+        enabled_extensions.ext_samsung_odyssey_controller = true;
+    }
+    if supported_extensions.ml_ml2_controller_interaction {
+        extensions.push(ext_string!(ML_ML2_CONTROLLER_INTERACTION_EXTENSION_NAME));
+        enabled_extensions.ml_ml2_controller_interaction = true;
+    }
+    if supported_extensions.htc_vive_cosmos_controller_interaction {
+        extensions.push(ext_string!(
+            HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME
+        ));
+        enabled_extensions.htc_vive_cosmos_controller_interaction = true;
+    }
+    if supported_extensions.htc_vive_focus3_controller_interaction {
+        extensions.push(ext_string!(
+            HTC_VIVE_FOCUS3_CONTROLLER_INTERACTION_EXTENSION_NAME
+        ));
+        enabled_extensions.htc_vive_focus3_controller_interaction = true;
+    }
+    if supported_extensions.fb_touch_controller_pro {
+        extensions.push(ext_string!(FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME));
+        enabled_extensions.fb_touch_controller_pro = true;
+    }
+    if supported_extensions.meta_touch_controller_plus {
+        extensions.push(ext_string!(META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME));
+        enabled_extensions.meta_touch_controller_plus = true;
+    }
+    extensions
+}
diff --git a/webxr/openxr/mod.rs b/webxr/openxr/mod.rs
index 0b99cf1..e78b238 100644
--- a/webxr/openxr/mod.rs
+++ b/webxr/openxr/mod.rs
@@ -8,15 +8,15 @@ use euclid::Rotation3D;
 use euclid::Size2D;
 use euclid::Transform3D;
 use euclid::Vector3D;
+use interaction_profiles::{get_profiles_from_path, get_supported_interaction_profiles};
 use log::{error, warn};
 use openxr::d3d::{Requirements, SessionCreateInfoD3D11, D3D11};
-use openxr::Graphics;
 use openxr::{
     self, ActionSet, ActiveActionSet, ApplicationInfo, CompositionLayerFlags,
     CompositionLayerProjection, Entry, EnvironmentBlendMode, ExtensionSet, Extent2Di, FormFactor,
-    Fovf, FrameState, FrameStream, FrameWaiter, Instance, Posef, Quaternionf, ReferenceSpaceType,
-    SecondaryEndInfo, Session, Space, Swapchain, SwapchainCreateFlags, SwapchainCreateInfo,
-    SwapchainUsageFlags, SystemId, Vector3f, ViewConfigurationType,
+    Fovf, FrameState, FrameStream, FrameWaiter, Graphics, Instance, Posef, Quaternionf,
+    ReferenceSpaceType, SecondaryEndInfo, Session, Space, Swapchain, SwapchainCreateFlags,
+    SwapchainCreateInfo, SwapchainUsageFlags, SystemId, Vector3f, Version, ViewConfigurationType,
 };
 use sparkle::gl;
 use sparkle::gl::GLuint;
@@ -79,6 +79,7 @@ use wio::com::ComPtr;
 
 mod input;
 use input::OpenXRInput;
+mod interaction_profiles;
 
 const HEIGHT: f32 = 1.4;
 
@@ -188,6 +189,7 @@ pub struct CreatedInstance {
     supports_secondary: bool,
     system: SystemId,
     supports_mutable_fov: bool,
+    supported_interaction_profiles: Vec<&'static str>,
 }
 
 pub fn create_instance(
@@ -208,6 +210,7 @@ pub fn create_instance(
         application_version: 1,
         engine_name: "servo",
         engine_version: 1,
+        api_version: Version::new(1, 0, 36),
     };
 
     let mut exts = ExtensionSet::default();
@@ -221,6 +224,8 @@ pub fn create_instance(
         exts.msft_first_person_observer = true;
     }
 
+    let supported_interaction_profiles = get_supported_interaction_profiles(&supported, &mut exts);
+
     let instance = entry
         .create_instance(&app_info, &exts, &[])
         .map_err(|e| format!("Entry::create_instance {:?}", e))?;
@@ -247,6 +252,7 @@ pub fn create_instance(
         supports_secondary,
         system,
         supports_mutable_fov,
+        supported_interaction_profiles,
     })
 }
 
@@ -870,6 +876,7 @@ impl OpenXrDevice {
             supports_secondary,
             system,
             supports_mutable_fov,
+            supported_interaction_profiles,
         } = instance;
 
         let (init_tx, init_rx) = crossbeam_channel::unbounded();
@@ -1041,8 +1048,12 @@ impl OpenXrDevice {
         });
         drop(data);
 
-        let (action_set, right_hand, left_hand) =
-            OpenXRInput::setup_inputs(&instance, &session, supports_hands);
+        let (action_set, right_hand, left_hand) = OpenXRInput::setup_inputs(
+            &instance,
+            &session,
+            supports_hands,
+            supported_interaction_profiles,
+        );
 
         Ok(OpenXrDevice {
             instance,
@@ -1115,6 +1126,33 @@ impl OpenXrDevice {
                     self.events.callback(Event::SessionEnd);
                     return false;
                 }
+                Some(InteractionProfileChanged(_)) => {
+                    let path = self.instance.string_to_path("/user/hand/right").unwrap();
+                    let profile_path = self.session.current_interaction_profile(path).unwrap();
+                    let profile = self.instance.path_to_string(profile_path);
+
+                    match profile {
+                        Ok(profile) => {
+                            let profiles = get_profiles_from_path(profile)
+                                .iter()
+                                .map(|s| s.to_string())
+                                .collect();
+
+                            let mut new_left = self.left_hand.input_source();
+                            new_left.profiles.clone_from(&profiles);
+                            self.events
+                                .callback(Event::UpdateInput(new_left.id, new_left));
+
+                            let mut new_right = self.right_hand.input_source();
+                            new_right.profiles.clone_from(&profiles);
+                            self.events
+                                .callback(Event::UpdateInput(new_right.id, new_right));
+                        }
+                        Err(e) => {
+                            error!("Failed to get interaction profile: {:?}", e);
+                        }
+                    }
+                }
                 Some(_) => {
                     // FIXME: Handle other events
                 }