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.
This commit is contained in:
Korcan Hussein 2023-06-27 15:52:44 +01:00 committed by Jakob Bornecrantz
parent 258357489c
commit 0b410a7119
3 changed files with 249 additions and 39 deletions

View file

@ -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"

View file

@ -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 <jakob@collabora.com>
* @author Christoph Haag <christoph.haag@collabora.com>
* @author Korcan Hussein <korcan.hussein@collabora.com>
* @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

View file

@ -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 <jakob@collabora.com>
* @author Korcan Hussein <korcan.hussein@collabora.com>
* @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);