From 0b410a7119a4a8f771a9b2c2ff8279d61c7be6fa Mon Sep 17 00:00:00 2001 From: Korcan Hussein Date: Tue, 27 Jun 2023 15:52:44 +0100 Subject: [PATCH] a/bindings: Interaction profile inheritance A requirement of interaction profile (extensions) specify that some/all actions must be supported by all other profiles. This commit modifies the binding generation to support data-inheritance in bindings.json: * Adds support for profiles in bindings.json to inherit & override other profiles * Adds a new concept of virtual profiles for profile like extensions (e.g. `XR_EXT_palm_pose`) which do not define a profile themselves but require their newly defined actions to be supported by all profiles. * Generates verify bindings functions which only check extensions actions only if the extension is enabled. --- src/xrt/auxiliary/bindings/bindings.json | 38 ++++ src/xrt/auxiliary/bindings/bindings.py | 218 +++++++++++++++++--- src/xrt/state_trackers/oxr/oxr_api_action.c | 32 ++- 3 files changed, 249 insertions(+), 39 deletions(-) diff --git a/src/xrt/auxiliary/bindings/bindings.json b/src/xrt/auxiliary/bindings/bindings.json index 55d8e380c..f679a0859 100644 --- a/src/xrt/auxiliary/bindings/bindings.json +++ b/src/xrt/auxiliary/bindings/bindings.json @@ -1,11 +1,33 @@ { "$schema": "./bindings.schema.json", "profiles": { + "/virtual_profiles/ext/palm_pose": { + "title": "Ext Palm Pose", + "type": "tracked_controller", + "extension": "XR_EXT_palm_pose", + "monado_device": "XRT_DEVICE_SIMPLE_CONTROLLER", + "subaction_paths": [ + "/user/hand/left", + "/user/hand/right" + ], + "subpaths": { + "/input/palm_ext": { + "type": "pose", + "localized_name": "Palm Pose", + "components": ["pose"], + "monado_bindings": { + "pose": "XRT_INPUT_GENERIC_PALM_POSE" + } + } + } + }, + "/interaction_profiles/khr/simple_controller": { "title": "Khronos Simple Controller", "type": "tracked_controller", "steamvr_controllertype": "khr_simple_controller", "monado_device": "XRT_DEVICE_SIMPLE_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -58,6 +80,7 @@ "title": "Google Daydream Controller", "type": "tracked_controller", "monado_device": "XRT_DEVICE_DAYDREAM", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -110,6 +133,7 @@ "type": "tracked_controller", "steamvr_controllertype": "vive_controller", "monado_device": "XRT_DEVICE_VIVE_WAND", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -196,6 +220,7 @@ "type": "tracked_hmd", "steamvr_controllertype": "vive_pro", "monado_device": "XRT_DEVICE_VIVE_PRO", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/head" ], @@ -240,6 +265,7 @@ "type": "tracked_controller", "steamvr_controllertype": "holographic_controller", "monado_device": "XRT_DEVICE_WMR_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -329,6 +355,7 @@ "type": "untracked_controller", "steamvr_controllertype": "gamepad", "monado_device": "XRT_DEVICE_XBOX_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/gamepad" ], @@ -510,6 +537,7 @@ "title": "Oculus Go Controller", "type": "untracked_controller", "monado_device": "XRT_DEVICE_GO_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -578,6 +606,7 @@ "type": "tracked_controller", "steamvr_controllertype": "oculus_touch", "monado_device": "XRT_DEVICE_TOUCH_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -712,6 +741,7 @@ "type": "tracked_controller", "steamvr_controllertype": "knuckles", "monado_device": "XRT_DEVICE_INDEX_CONTROLLER", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -825,6 +855,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_HP_REVERB_G2_CONTROLLER", "extension": "XR_EXT_hp_mixed_reality_controller", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -935,6 +966,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_SAMSUNG_ODYSSEY_CONTROLLER", "extension": "XR_EXT_samsung_odyssey_controller", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -1024,6 +1056,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_ML2_CONTROLLER", "extension": "XR_ML_ml2_controller_interaction", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -1110,6 +1143,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_HAND_INTERACTION", "extension": "XR_MSFT_hand_interaction", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -1155,6 +1189,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_PSMV", "extension": "XR_MNDX_ball_on_a_stick_controller", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -1280,6 +1315,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_HYDRA", "extension": "XR_MNDX_hydra", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" @@ -1362,6 +1398,7 @@ "type": "eye_tracker", "monado_device": "XRT_DEVICE_EYE_GAZE_INTERACTION", "extension": "XR_EXT_eye_gaze_interaction", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/eyes_ext" ], @@ -1381,6 +1418,7 @@ "type": "tracked_controller", "monado_device": "XRT_DEVICE_VIVE_TRACKER", "extension": "XR_HTCX_vive_tracker_interaction", + "extended_by": ["ext/palm_pose"], "subaction_paths": [ "/user/hand/left", "/user/hand/right" diff --git a/src/xrt/auxiliary/bindings/bindings.py b/src/xrt/auxiliary/bindings/bindings.py index e463fbe46..e3c659740 100755 --- a/src/xrt/auxiliary/bindings/bindings.py +++ b/src/xrt/auxiliary/bindings/bindings.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -# Copyright 2020-2022, Collabora, Ltd. +# Copyright 2020-2023, Collabora, Ltd. # SPDX-License-Identifier: BSL-1.0 """Generate code from a JSON file describing interaction profiles and bindings.""" import argparse import json +import copy +import itertools def find_component_in_list_by_name(name, component_list, subaction_path=None, identifier_json_path=None): @@ -19,6 +21,7 @@ def find_component_in_list_by_name(name, component_list, subaction_path=None, id return component return None + def steamvr_subpath_name(steamvr_path, subpath_type): if subpath_type == "pose": return steamvr_path.replace("/input/", "/pose/") @@ -31,6 +34,7 @@ def steamvr_subpath_name(steamvr_path, subpath_type): return steamvr_path + class PathsByLengthCollector: """Helper class to sort paths by length, useful for creating fast path validation functions. @@ -56,6 +60,7 @@ class PathsByLengthCollector: ret[length] = list(set_per_length) return ret + def dpad_paths(identifier_path, center): paths = [ identifier_path + "/dpad_up", @@ -69,6 +74,7 @@ def dpad_paths(identifier_path, center): return paths + class DPad: """Class holding per identifier information for dpad emulation.""" @@ -190,6 +196,9 @@ class Component: return paths + def get_full_path(self): + return self.subaction_path + self.identifier_json_path + '/' + self.component_name + def is_input(self): # only haptics is output so far, everything else is input return self.component_name != "haptic" @@ -219,7 +228,8 @@ class Identifier: for subaction_path in json_subaction_paths: # /user/hand/* for json_sub_path_itm in json_subpaths.items(): # /input/*, /output/* json_path = json_sub_path_itm[0] # /input/trackpad - json_subpath = json_sub_path_itm[1] # json object associated with a subpath (type, localized_name, ...) + # json object associated with a subpath (type, localized_name, ...) + json_subpath = json_sub_path_itm[1] # Oculus Touch a,b/x,y components only exist on one controller if "side" in json_subpath and "/user/hand/" + json_subpath["side"] != subaction_path: @@ -266,22 +276,25 @@ class Profile: def __init__(self, profile_name, json_profile): """Construct an profile.""" + self.parent_profiles = set() self.name = profile_name self.localized_name = json_profile['title'] self.profile_type = json_profile["type"] self.monado_device_enum = json_profile["monado_device"] - self.validation_func_name = profile_name.replace("/interaction_profiles/", "").replace("/", "_") + self.validation_func_name = Profile.__strip_profile_prefix( + profile_name).replace("/", "_") + self.extension_name = json_profile.get("extension") + self.extended_by = json_profile.get("extended_by") + if self.extended_by is None: + self.extended_by = [] + self.is_virtual = profile_name.startswith("/virtual_profiles/") self.identifiers = Identifier.parse_identifiers(json_profile) self.steamvr_controller_type = None if "steamvr_controllertype" in json_profile: self.steamvr_controller_type = json_profile["steamvr_controllertype"] - self.components = [] - for identifier in self.identifiers: - for component in identifier.components: - self.components.append(component) - + self.__update_component_list() collector = PathsByLengthCollector() for component in self.components: collector.add_paths(component.get_full_openxr_paths()) @@ -300,9 +313,60 @@ class Profile: continue path = identifier.identifier_path collector.add_paths(identifier.dpad.paths) - self.dpad_paths_by_length = collector.to_dict_of_lists() + @classmethod + def __strip_profile_prefix(cls, profile_path): + return profile_path.replace("/interaction_profiles/", "").replace("/virtual_profiles/", "") + + def is_parent_profile(self, child_profile): + if child_profile == self: + return False + if child_profile.extended_by is None: + return False + parent_path = Profile.__strip_profile_prefix(self.name) + return parent_path in child_profile.extended_by + + def merge_parent_profiles(self): + self.identifiers = self.__get_merged_identifiers_helper({}).values() + self.__update_component_list() + + def __get_merged_identifiers_helper(self, identifier_map): + for ident in self.identifiers: + if ident.identifier_path not in identifier_map: + identifier_map[ident.identifier_path] = copy.deepcopy(ident) + continue + child_indent = identifier_map[ident.identifier_path] + if child_indent.dpad is None: + child_indent.dpad = ident.dpad + child_comps = child_indent.components + for parent_comp in ident.components: + parent_path = parent_comp.get_full_path() + child_exists = False + for child_comp in child_comps: + if child_comp.get_full_path() == parent_path: + child_exists = True + break + if not child_exists: + child_comps.append(parent_comp) + + parent_profiles = self.parent_profiles + if parent_profiles is None or len(parent_profiles) == 0: + return identifier_map + else: + for parent in parent_profiles: + parent.__get_merged_identifiers_helper(identifier_map) + return identifier_map + + def __update_component_list(self): + self.components = [] + for identifier in self.identifiers: + for component in identifier.components: + self.components.append(component) + + +oxr_verify_extension_status_struct_name = "oxr_verify_extension_status" + class Bindings: """A collection of interaction profiles used in bindings.""" @@ -324,6 +388,50 @@ class Bindings: """Construct a bindings from a dictionary of profiles.""" self.profiles = [Profile(profile_name, json_profile) for profile_name, json_profile in json_root["profiles"].items()] + self.__set_parent_profile_refs() + self.__mine_for_diamond_errors() + + self.virtual_profiles = [p for p in self.profiles if p.is_virtual] + self.profiles = [p for p in self.profiles if not p.is_virtual] + for profile in self.profiles: + profile.merge_parent_profiles() + + def __set_parent_profile_refs(self): + for profile1 in self.profiles: + for profile2 in self.profiles: + if profile1.is_parent_profile(profile2): + profile2.parent_profiles.add(profile1) + + def __mine_for_diamond_errors(self): + for profile in self.profiles: + parent_path_set = [] + if self.__has_diamonds(profile, parent_path_set): + msg = f"Interaction Profile: {profile.name} in bindings.json has a diamond hierarchy, this is not supported." + raise RuntimeError(msg) + + def __has_diamonds(self, profile, parent_path_set): + if profile.name in parent_path_set: + return True + parent_path_set.append(profile.name) + for parent in profile.parent_profiles: + if self.__has_diamonds(parent, parent_path_set): + return True + return False + + def make_oxr_verify_extension_status_struct_str(self): + struct_str: str = f"struct {oxr_verify_extension_status_struct_name}{{\n" + ext_set = set() + for profile in itertools.chain(self.virtual_profiles, self.profiles): + ext_name = profile.extension_name + if ext_name is None or len(ext_name) == 0: + continue + if ext_name in ext_set: + continue + ext_set.add(ext_name) + ext_name = ext_name.replace("XR_", "") + struct_str += f"\tbool {ext_name};\n" + struct_str += "};\n" + return struct_str header = '''// Copyright 2020-2022, Collabora, Ltd. @@ -333,34 +441,81 @@ header = '''// Copyright 2020-2022, Collabora, Ltd. * @brief {brief}. * @author Jakob Bornecrantz * @author Christoph Haag + * @author Korcan Hussein * @ingroup {group} */ ''' func_start = ''' bool -{name}(const char *str, size_t length) +{name}(const struct {ext_status_struct_name}* exts, const char *str, size_t length) {{ -\tswitch (length) {{ ''' -if_strcmp = '''if (strcmp(str, "{check}") == 0) {{ -\t\t\treturn true; -\t\t}} else ''' + +def write_verify_func_begin(f, name): + f.write(func_start.format( + name=name, ext_status_struct_name=oxr_verify_extension_status_struct_name)) -def write_verify_func(f, name, dict_of_lists): +def write_verify_func_end(f): + f.write("\treturn false;\n}\n") + + +if_strcmp = '''{exttab}if (strcmp(str, "{check}") == 0) {{ +{exttab}\t\t\treturn true; +{exttab}\t\t}} else ''' + + +def write_verify_func_switch(f, dict_of_lists, profile_name, ext_name): """Generate function to check if a string is in a set of strings. Input is a file to write the code into, a dict where keys are length and the values are lists of strings of that length. And a suffix if any.""" + if len(dict_of_lists) == 0: + return - f.write(func_start.format(name=name)) + f.write(f"\t// generated from: {profile_name}\n") + is_ext = ext_name is not None and len(ext_name) > 0 + ext_tab = "" + if is_ext: + ext_name = ext_name.replace("XR_", "") + f.write(f"\tif (exts->{ext_name}) {{\n") + ext_tab = "\t" + + f.write(f"{ext_tab}\tswitch (length) {{\n") for length in dict_of_lists: - f.write("\tcase " + str(length) + ":\n\t\t") + f.write(f"{ext_tab}\tcase {str(length)}:\n\t\t") for path in dict_of_lists[length]: - f.write(if_strcmp.format(check=path)) - f.write("{\n\t\t\treturn false;\n\t\t}\n") - f.write("\tdefault:\n\t\treturn false;\n\t}\n}\n") + f.write(if_strcmp.format(exttab=ext_tab, check=path)) + f.write(f"{ext_tab}{{\n{ext_tab}\t\t\tbreak;\n{ext_tab}\t\t}}\n") + f.write(f"{ext_tab}\tdefault: break;\n{ext_tab}\t}}\n") + + if is_ext: + f.write("\t}\n") + + +def write_verify_func_body(f, profile, dict_name): + if profile is None or dict_name is None or len(dict_name) == 0: + return + write_verify_func_switch(f, getattr( + profile, dict_name), profile.name, profile.extension_name) + if profile.parent_profiles is None: + return + for pp in profile.parent_profiles: + write_verify_func_body(f, pp, dict_name) + + +def write_verify_func(f, profile, dict_name, suffix): + write_verify_func_begin( + f, f"oxr_verify_{profile.validation_func_name}{suffix}") + write_verify_func_body(f, profile, dict_name) + write_verify_func_end(f) + + +def generate_verify_functions(f, profile): + write_verify_func(f, profile, "subpaths_by_length", "_subpath") + write_verify_func(f, profile, "dpad_paths_by_length", "_dpad_path") + write_verify_func(f, profile, "dpad_emulators_by_length", "_dpad_emulator") def generate_bindings_c(file, p): @@ -375,12 +530,7 @@ def generate_bindings_c(file, p): ''') for profile in p.profiles: - name = "oxr_verify_" + profile.validation_func_name + "_subpath" - write_verify_func(f, name, profile.subpaths_by_length) - name = "oxr_verify_" + profile.validation_func_name + "_dpad_path" - write_verify_func(f, name, profile.dpad_paths_by_length) - name = "oxr_verify_" + profile.validation_func_name + "_dpad_emulator" - write_verify_func(f, name, profile.dpad_emulators_by_length) + generate_verify_functions(f, profile) f.write( f'\n\nstruct profile_template profile_templates[{len(p.profiles)}] = {{ // array of profile_template\n') @@ -404,7 +554,8 @@ def generate_bindings_c(file, p): component: Component for idx, component in enumerate(profile.components): - steamvr_path = component.steamvr_path # @todo Doesn't handle pose yet. + # @todo Doesn't handle pose yet. + steamvr_path = component.steamvr_path if component.component_name in ["click", "touch", "force", "value"]: steamvr_path += "/" + component.component_name @@ -566,13 +717,14 @@ def generate_bindings_h(file, p): // clang-format off ''') + oxr_verify_struct_str = p.make_oxr_verify_extension_status_struct_str() + f.write(oxr_verify_struct_str) + + fn_prefixes = ["_subpath", "_dpad_path", "_dpad_emulator"] for profile in p.profiles: - f.write("\nbool\noxr_verify_" + profile.validation_func_name + - "_subpath(const char *str, size_t length);\n") - f.write("\nbool\noxr_verify_" + profile.validation_func_name + - "_dpad_path(const char *str, size_t length);\n") - f.write("\nbool\noxr_verify_" + profile.validation_func_name + - "_dpad_emulator(const char *str, size_t length);\n") + for fn_suffix in fn_prefixes: + f.write( + f"\nbool\noxr_verify_{profile.validation_func_name}{fn_suffix}(const struct {oxr_verify_extension_status_struct_name}* extensions, const char *str, size_t length);\n") f.write(f''' #define PATHS_PER_BINDING_TEMPLATE 16 diff --git a/src/xrt/state_trackers/oxr/oxr_api_action.c b/src/xrt/state_trackers/oxr/oxr_api_action.c index b59623c9c..6d24d7372 100644 --- a/src/xrt/state_trackers/oxr/oxr_api_action.c +++ b/src/xrt/state_trackers/oxr/oxr_api_action.c @@ -1,9 +1,10 @@ -// Copyright 2019-2022, Collabora, Ltd. +// Copyright 2019-2023, Collabora, Ltd. // SPDX-License-Identifier: BSL-1.0 /*! * @file * @brief Action related API entrypoint functions. * @author Jakob Bornecrantz + * @author Korcan Hussein * @ingroup oxr_api */ @@ -25,7 +26,7 @@ #include "bindings/b_generated_bindings.h" -typedef bool (*path_verify_fn_t)(const char *, size_t); +typedef bool (*path_verify_fn_t)(const struct oxr_verify_extension_status *, const char *, size_t); /* @@ -41,6 +42,7 @@ process_dpad(struct oxr_logger *log, struct oxr_dpad_state *state, const XrInteractionProfileDpadBindingEXT *dpad, path_verify_fn_t dpad_emulator_fn, + const struct oxr_verify_extension_status *verify_ext_status, const char *prefix, const char *ip_str) { @@ -54,7 +56,7 @@ process_dpad(struct oxr_logger *log, dpad->binding); } - if (!dpad_emulator_fn(str, length)) { + if (!dpad_emulator_fn(verify_ext_status, str, length)) { return oxr_error(log, XR_ERROR_PATH_UNSUPPORTED, "(%s->binding == \"%s\") is not a valid dpad binding path for profile \"%s\"", prefix, str, ip_str); @@ -313,6 +315,23 @@ oxr_xrSuggestInteractionProfileBindings(XrInstance instance, // Needed in various paths here. const char *str = NULL; size_t length; + const struct oxr_verify_extension_status verify_ext_status = { +#ifdef OXR_HAVE_EXT_palm_pose + .EXT_palm_pose = inst->extensions.EXT_palm_pose, +#endif + .EXT_hp_mixed_reality_controller = inst->extensions.EXT_hp_mixed_reality_controller, + .EXT_samsung_odyssey_controller = inst->extensions.EXT_samsung_odyssey_controller, + .ML_ml2_controller_interaction = inst->extensions.ML_ml2_controller_interaction, +#ifdef OXR_HAVE_MSFT_hand_interaction + .MSFT_hand_interaction = inst->extensions.MSFT_hand_interaction, +#endif + .MNDX_ball_on_a_stick_controller = inst->extensions.MNDX_ball_on_a_stick_controller, + .MNDX_hydra = inst->extensions.MNDX_hydra, + .EXT_eye_gaze_interaction = inst->extensions.EXT_eye_gaze_interaction, +#ifdef OXR_HAVE_HTCX_vive_tracker_interaction + .HTCX_vive_tracker_interaction = inst->extensions.HTCX_vive_tracker_interaction, +#endif + }; for (size_t i = 0; i < suggestedBindings->countSuggestedBindings; i++) { const XrActionSuggestedBinding *s = &suggestedBindings->suggestedBindings[i]; @@ -335,12 +354,12 @@ oxr_xrSuggestInteractionProfileBindings(XrInstance instance, i, s->binding); } - if (subpath_fn(str, length)) { + if (subpath_fn(&verify_ext_status, str, length)) { continue; } #ifdef XR_EXT_dpad_binding - if (dpad_path_fn(str, length)) { + if (dpad_path_fn(&verify_ext_status, str, length)) { if (!has_dpad) { return oxr_error( &log, XR_ERROR_PATH_UNSUPPORTED, @@ -387,7 +406,8 @@ oxr_xrSuggestInteractionProfileBindings(XrInstance instance, "XrInteractionProfileDpadBindingEXT>", i); - ret = process_dpad(&log, inst, &dpad_state, dpad, dpad_emulator_fn, temp, ip_str); + ret = process_dpad(&log, inst, &dpad_state, dpad, dpad_emulator_fn, &verify_ext_status, temp, + ip_str); if (ret != XR_SUCCESS) { // Teardown the state. oxr_dpad_state_deinit(&dpad_state);