// SPDX-License-Identifier: GPL-3.0-only // Included by frame_adapter.c. Per-game session fixes, all off by default (enabled in a game's recipe): // * layer_debug: diagnostics only — layer/swapchain details, session-state timing, reference-space creation (and how // far each new LOCAL space is from the session's first one), aim vs grip poses, refresh-rate calls; // * stable_local: every LOCAL reference space the app creates coincides with the one at session start (for runtimes // that place each new LOCAL space at the current head yaw, which makes menus jump to where you look); // * focus_hold: hides brief FOCUSED -> VISIBLE -> (SYNCHRONIZED) dips once the session has been focused for a while // (apps that recentre on regaining focus); // * aim_pitch / aim_yaw / aim_forward: correct the aim pose of controllers (pointer rays) by a fixed offset; // * refresh_rate: display refresh rate requested at session start (and instead of the app's own requests). #include "layer_math.h" #include "refresh_rate.h" static void emul_on_begin_session(const XrSessionBeginInfo *info); // layer_emul_gl.c static int64_t monotonic_ns(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); return (int64_t)now.tv_sec * 1000000000ll + now.tv_nsec; } // ---------------------------------------------------------------- stable_local static XrSpace local_anchor = XR_NULL_HANDLE; // private LOCAL space created right after xrCreateSession static int local_anchor_stale; // runtime announced a LOCAL change (user recentre): re-anchor static XrSpace debug_first_local = XR_NULL_HANDLE; // layer_debug: first LOCAL space, to measure later ones against static XrSpace create_local(XrSession session) { PFN_xrCreateReferenceSpace create = (PFN_xrCreateReferenceSpace)lookup(active_instance, "xrCreateReferenceSpace"); XrReferenceSpaceCreateInfo ci = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO, NULL, XR_REFERENCE_SPACE_TYPE_LOCAL, {{0, 0, 0, 1}, {0, 0, 0}}}; XrSpace space = XR_NULL_HANDLE; if (!create || XR_FAILED(create(session, &ci, &space))) return XR_NULL_HANDLE; return space; } static void destroy_space(XrSpace space) { PFN_xrDestroySpace destroy = (PFN_xrDestroySpace)lookup(active_instance, "xrDestroySpace"); if (destroy && space) destroy(space); } // Pose of `space` in `base` at the latest predicted display time; 0 if not (yet) known. static int locate_pose(XrSpace space, XrSpace base, XrPosef *pose) { PFN_xrLocateSpace locate = (PFN_xrLocateSpace)lookup(active_instance, "xrLocateSpace"); if (!locate || !space || !base || !last_predicted_time) return 0; XrSpaceLocation loc = {XR_TYPE_SPACE_LOCATION, NULL, 0, {{0, 0, 0, 1}, {0, 0, 0}}}; if (XR_FAILED(locate(space, base, last_predicted_time, &loc))) return 0; const XrSpaceLocationFlags valid = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT; if ((loc.locationFlags & valid) != valid) return 0; *pose = loc.pose; return 1; } static void session_fixes_on_create_session(XrSession session) { if (stable_local) { local_anchor = create_local(session); LOG("stable_local: session anchor %s", local_anchor ? "created" : "FAILED"); } } // The pose to create an app LOCAL space with, so that it coincides with the session-start LOCAL space. static XrPosef stable_local_pose(XrSession session, XrPosef app_pose) { if (!local_anchor) return app_pose; XrSpace fresh = create_local(session); if (!fresh) return app_pose; XrPosef anchor_in_fresh; int known = locate_pose(local_anchor, fresh, &anchor_in_fresh); if (known && local_anchor_stale) { // the user recentred: from now on the new LOCAL origin is the anchor destroy_space(local_anchor); local_anchor = fresh; local_anchor_stale = 0; LOG("stable_local: re-anchored after a runtime recentre"); return app_pose; } destroy_space(fresh); if (!known) return app_pose; float moved = lm_length(anchor_in_fresh.position), turned = lm_angle(anchor_in_fresh.orientation); if (moved < 0.01f && turned < 0.01f) return app_pose; // runtime kept LOCAL stable: nothing to correct LOG("stable_local: new LOCAL space is %.3f m / %.1f deg from the session's, compensating", moved, turned * 180 / LM_PI); return lm_pose_mul(anchor_in_fresh, app_pose); } static void debug_reference_space(XrSession session, const XrReferenceSpaceCreateInfo *info, XrSpace space) { const XrPosef *p = &info->poseInReferenceSpace; LOG("layer_debug: space created type=%d space=%p pose=(%.3f,%.3f,%.3f | %.3f,%.3f,%.3f,%.3f) t=%.3f", info->referenceSpaceType, (void *)(uintptr_t)space, p->position.x, p->position.y, p->position.z, p->orientation.x, p->orientation.y, p->orientation.z, p->orientation.w, monotonic_ns() / 1e9); if (info->referenceSpaceType != XR_REFERENCE_SPACE_TYPE_LOCAL) return; if (!debug_first_local) { debug_first_local = create_local(session); return; } XrPosef rel; if (locate_pose(space, debug_first_local, &rel)) LOG("layer_debug: that LOCAL space sits at %.3f m / %.1f deg (yaw %.1f) from the session's first LOCAL space", lm_length(rel.position), lm_angle(rel.orientation) * 180 / LM_PI, 2 * atan2f(rel.orientation.y, rel.orientation.w) * 180 / LM_PI); else LOG("layer_debug: that LOCAL space can't be located yet (no tracking)"); } // ---------------------------------------------------------------- sync_guard // The Frame's runtime crashed in xrSyncActions right after focus returned (SIGSEGV in vrclient.so // CVRInputLatest::UpdateActionStateInternal, Myst; a race inside the runtime, not every launch). sync_guard runs // xrSyncActions one at a time with xrPollEvent and skips it for a moment after FOCUSED arrives (XR_SESSION_NOT_FOCUSED // is a success code every app handles: no input for those few frames). #define SYNC_GUARD_PAUSE_NS 250000000ll static pthread_mutex_t sync_lock = PTHREAD_MUTEX_INITIALIZER; static int64_t sync_resume_at; // layer_debug input counters, logged with the pacing line: were actions synced, did any button read as pressed? static int input_syncs, input_sync_ok, input_bool_reads, input_bool_true, input_last_sync_result; static XRAPI_ATTR XrResult XRAPI_CALL hook_xrSyncActions(XrSession session, const XrActionsSyncInfo *info) { PFN_xrSyncActions fn = (PFN_xrSyncActions)lookup(active_instance, "xrSyncActions"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; if (!sync_guard) { // installed for layer_debug / input_diag only: count, report failures XrResult r = fn(session, info); __atomic_add_fetch(&input_syncs, 1, __ATOMIC_RELAXED); if (r == XR_SUCCESS) __atomic_add_fetch(&input_sync_ok, 1, __ATOMIC_RELAXED); input_last_sync_result = r; if (input_diag && XR_FAILED(r)) diag_call_failed("xrSyncActions", r, NULL); return r; } pthread_mutex_lock(&sync_lock); int64_t wait = sync_resume_at - monotonic_ns(); XrResult r = wait > 0 ? XR_SESSION_NOT_FOCUSED : fn(session, info); pthread_mutex_unlock(&sync_lock); static int logged; if (wait > 0 && logged++ < 5) LOG("sync_guard: input paused for %.0f ms after focus returned", wait / 1e6); if (input_diag && XR_FAILED(r)) diag_call_failed("xrSyncActions", r, NULL); return r; } static XRAPI_ATTR XrResult XRAPI_CALL hook_xrGetActionStateBoolean(XrSession session, const XrActionStateGetInfo *info, XrActionStateBoolean *state) { PFN_xrGetActionStateBoolean fn = (PFN_xrGetActionStateBoolean)lookup(active_instance, "xrGetActionStateBoolean"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; XrResult r = fn(session, info, state); __atomic_add_fetch(&input_bool_reads, 1, __ATOMIC_RELAXED); if (r == XR_SUCCESS && state && state->isActive && state->currentState) __atomic_add_fetch(&input_bool_true, 1, __ATOMIC_RELAXED); return r; } // ---------------------------------------------------------------- focus_hold #define FOCUS_HOLD_MIN_FOCUSED_NS 1000000000ll // only after this long in FOCUSED (start-up transitions untouched; // 3 s let a 27 ms dip through in Blade & Sorcery) // dips longer than focus_hold_ms (setting, default 5000) are delivered (late, in order): taking the headset off or the // system menu must still pause the game. A longer limit is for headsets whose wear sensor flickers ("HMD off" for // 0.5-2 s while worn; Blade & Sorcery pauses on each) #define FOCUS_HOLD_MAX_DIP_NS ((int64_t)(focus_hold_ms * 1000000.0f)) static struct { XrEventDataBuffer events[6]; int count, holding, delivering; int64_t since; } held; static int is_focused; static int64_t focused_since; static int session_state_of(const XrEventDataBuffer *event) { return event->type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED ? (int)((const XrEventDataSessionStateChanged *)event)->state : -1; } static void focus_hold_note(int state) { if (state == XR_SESSION_STATE_FOCUSED && !is_focused) { is_focused = 1; focused_since = monotonic_ns(); } else if (state >= 0 && state != XR_SESSION_STATE_FOCUSED) is_focused = 0; } // ---------------------------------------------------------------- focus log (always on: rare events, cheap) // Every time the runtime takes focus away and gives it back, as the runtime reports it (before focus_hold hides a dip // from the app): FramePort's session triage counts these dips after a play session and suggests a longer // focus_hold_ms when they keep reaching the game. static PFN_xrPollEvent focus_log_next; static int focus_log_focused; static int64_t focus_log_lost_at; static int focus_log_lost_state; static XRAPI_ATTR XrResult XRAPI_CALL focus_log_poll(XrInstance instance, XrEventDataBuffer *event) { XrResult r = focus_log_next(instance, event); if (r != XR_SUCCESS || !event) return r; int state = session_state_of(event); if (state < 0) return r; if (state == XR_SESSION_STATE_FOCUSED) { if (focus_log_lost_at) LOG("focus: back after %.0f ms (lost to state %d)", (monotonic_ns() - focus_log_lost_at) / 1e6, focus_log_lost_state); focus_log_focused = 1; focus_log_lost_at = 0; } else if (focus_log_focused && (state == XR_SESSION_STATE_VISIBLE || state == XR_SESSION_STATE_SYNCHRONIZED)) { focus_log_focused = 0; focus_log_lost_at = monotonic_ns(); focus_log_lost_state = state; LOG("focus: lost (state %d)", state); } else if (state != XR_SESSION_STATE_VISIBLE && state != XR_SESSION_STATE_SYNCHRONIZED) { focus_log_focused = 0; // stopping, loss pending, exiting: the session ends, not a dip if (focus_log_lost_at) LOG("focus: session ending (state %d)", state); focus_log_lost_at = 0; } return r; } static XrResult focus_hold_poll(PFN_xrPollEvent fn, XrInstance instance, XrEventDataBuffer *event) { for (;;) { if (held.delivering) { // hand held events to the app, oldest first if (held.count) { *event = held.events[0]; memmove(&held.events[0], &held.events[1], (size_t)--held.count * sizeof(held.events[0])); focus_hold_note(session_state_of(event)); return XR_SUCCESS; } held.delivering = 0; } XrResult result = fn(instance, event); int64_t now = monotonic_ns(); if (result != XR_SUCCESS) { if (held.holding && now - held.since > FOCUS_HOLD_MAX_DIP_NS) { LOG("focus_hold: focus lost for > %lld ms, delivering %d state change(s)", (long long)(FOCUS_HOLD_MAX_DIP_NS / 1000000), held.count); held.holding = 0; held.delivering = 1; continue; } return result; } int state = session_state_of(event); if (state < 0) return result; // other events pass straight through if (held.holding) { if (state == XR_SESSION_STATE_FOCUSED) { LOG("focus_hold: hid a %.0f ms focus dip (%d state change(s))", (now - held.since) / 1e6, held.count); held.count = 0; held.holding = 0; continue; // the app never saw it leave FOCUSED } if (held.count < 6) held.events[held.count++] = *event; if (state != XR_SESSION_STATE_VISIBLE && state != XR_SESSION_STATE_SYNCHRONIZED) { held.holding = 0; held.delivering = 1; // stopping, loss pending, exiting: deliver everything now } continue; } if (state == XR_SESSION_STATE_VISIBLE && is_focused && now - focused_since >= FOCUS_HOLD_MIN_FOCUSED_NS) { held.holding = 1; held.since = now; held.count = 0; held.events[held.count++] = *event; continue; } focus_hold_note(state); return result; } } // ---------------------------------------------------------------- aim pose correction + aim/grip diagnostics #define MAX_POSE_ACTIONS 64 static struct { XrAction action; int aim; } pose_actions[MAX_POSE_ACTIONS]; static int pose_action_count; static struct { XrSpace space; XrPath subaction; int aim; } pose_spaces[MAX_POSE_ACTIONS]; static int pose_space_count; static pthread_mutex_t pose_lock = PTHREAD_MUTEX_INITIALIZER; static int aim_correction_on(void) { return aim_pitch != 0 || aim_yaw != 0 || aim_forward != 0; } static void note_pose_binding(XrAction action, const char *path) { size_t n = strlen(path); int aim = n >= 9 && !strcmp(path + n - 9, "/aim/pose"); int grip = n >= 10 && !strcmp(path + n - 10, "/grip/pose"); if (!aim && !grip) return; pthread_mutex_lock(&pose_lock); int known = 0; for (int i = 0; i < pose_action_count; ++i) known |= pose_actions[i].action == action; if (!known && pose_action_count < MAX_POSE_ACTIONS) { pose_actions[pose_action_count].action = action; pose_actions[pose_action_count++].aim = aim; } pthread_mutex_unlock(&pose_lock); } static int pose_action_kind(XrAction action) { // 1 aim, 0 grip, -1 unknown int kind = -1; pthread_mutex_lock(&pose_lock); for (int i = 0; i < pose_action_count && kind < 0; ++i) if (pose_actions[i].action == action) kind = pose_actions[i].aim; pthread_mutex_unlock(&pose_lock); return kind; } // Meta's newer controller profiles, which the Frame's runtime doesn't know (it has oculus/touch_controller). An app // that suggests bindings only for these gets XR_ERROR_PATH_UNSUPPORTED and no input (e.g. a setup screen that can't be // passed); its bindings are suggested again for Touch, keeping the components Touch controllers have. static const char *const NEWER_TOUCH[] = {"/interaction_profiles/meta/touch_controller_plus", "/interaction_profiles/meta/touch_plus_controller", "/interaction_profiles/facebook/touch_controller_pro", "/interaction_profiles/meta/touch_pro_controller", "/interaction_profiles/meta/touch_controller_rift_cv1", "/interaction_profiles/meta/touch_controller_quest_1_rift_s", "/interaction_profiles/meta/touch_controller_quest_2"}; static const char *const TOUCH_PROFILE = "/interaction_profiles/oculus/touch_controller"; // oculus/touch_controller components (OpenXR spec), after /user/hand// static const char *const TOUCH_COMMON[] = {"input/squeeze/value", "input/trigger/value", "input/trigger/touch", "input/thumbstick", "input/thumbstick/x", "input/thumbstick/y", "input/thumbstick/click", "input/thumbstick/touch", "input/thumbrest/touch", "input/grip/pose", "input/aim/pose", "output/haptic"}; static const char *const TOUCH_LEFT[] = {"input/x/click", "input/x/touch", "input/y/click", "input/y/touch", "input/menu/click"}; static const char *const TOUCH_RIGHT[] = {"input/a/click", "input/a/touch", "input/b/click", "input/b/touch", "input/system/click"}; static int app_suggested_touch; #include "profile_report.c" // remapped_profile, current_profile_fix (xrGetCurrentInteractionProfile) #include "detached_controllers.c" // detached controllers' paths on a runtime without them static void reset_bindings(int frame_enabled) { quest_buttons_enabled = frame_enabled; app_suggested_frame = generated_frame_bindings = app_suggested_touch = 0; remapped_profile[0] = 0; } static int in_list(const char *s, const char *const *list, size_t n) { for (size_t i = 0; i < n; ++i) if (!strcmp(s, list[i])) return 1; return 0; } #define IN(s, list) in_list(s, list, sizeof(list) / sizeof(*list)) // 1 if the binding path exists on oculus/touch_controller static int touch_has(const char *path) { const char *left = "/user/hand/left/", *right = "/user/hand/right/"; if (!strncmp(path, left, strlen(left))) { path += strlen(left); return IN(path, TOUCH_COMMON) || IN(path, TOUCH_LEFT); } if (!strncmp(path, right, strlen(right))) { path += strlen(right); return IN(path, TOUCH_COMMON) || IN(path, TOUCH_RIGHT); } return 0; } // ---------------------------------------------------------------- finger proximity from touch (proximity_emul) // Meta's OVRPlugin reports a thumb or index finger resting near the controller through XR_FB_touch_controller_proximity // (.../thumb_fb/proximity_fb, .../trigger/proximity_fb). The Frame's runtime doesn't have it; OVRPort's loader offers // it to the game anyway, and the proximity actions never get a working binding: "near" stays false, so games that // animate the hands from it show a thumb that never moves (Unreal's ThumbUp axis is "no thumb proximity", e.g. Vader // Immortal; found by Klownicle, GitHub #49). With proximity_emul those actions are bound to the capacitive touch inputs // instead (OpenXR combines several bindings of one boolean action with OR): thumb = thumbstick, face buttons or thumb // rest touched; index (proximity_emul=2) = trigger touched. Only when the runtime lacks the extension itself. static XrAction thumb_proximity_action, trigger_proximity_action; // ---------------------------------------------------------------- duplicate action names (action_dedup) // OpenXR requires action names and localized names to be unique within an action set (XR_ERROR_NAME_DUPLICATED), but // the Frame's runtime accepts duplicates and then can't load the input manifest it builds from them: vrserver logs // "SetXrInputConfig failed. Failed to load manifest", drops the app's connection and treats it as exited, so the // headset goes back to the home environment while the game keeps running with sound (GitHub #323, Scared By Squares: // Unreal's OpenXR plugin creates "turn" twice, from an action mapping and an axis mapping of the same name). The // game only uses the handles, so a later duplicate is created under a free name ("turn_2", "Turn 2"). #include "action_names.h" #define DEDUP_MAX 512 static struct { XrActionSet set; char name[XR_MAX_ACTION_NAME_SIZE]; char localized[XR_MAX_LOCALIZED_ACTION_NAME_SIZE]; } dedup_seen[DEDUP_MAX]; static int dedup_count; static pthread_mutex_t dedup_lock = PTHREAD_MUTEX_INITIALIZER; static int dedup_taken(void *ctx, const char *name, const char *localized) { // caller holds dedup_lock XrActionSet set = *(XrActionSet *)ctx; for (int i = 0; i < dedup_count; ++i) if (dedup_seen[i].set == set && (!strcmp(dedup_seen[i].name, name) || !strcmp(dedup_seen[i].localized, localized))) return 1; return 0; } // Fills `out` (a copy of `info`) with free names when `info`'s are taken in `set`; 1 if it did. static int dedup_rename(XrActionSet set, const XrActionCreateInfo *info, XrActionCreateInfo *out, char name[XR_MAX_ACTION_NAME_SIZE], char localized[XR_MAX_LOCALIZED_ACTION_NAME_SIZE]) { if (!action_unique_names(info->actionName, info->localizedActionName, dedup_taken, &set, name, XR_MAX_ACTION_NAME_SIZE, localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE)) return 0; *out = *info; memcpy(out->actionName, name, XR_MAX_ACTION_NAME_SIZE); memcpy(out->localizedActionName, localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE); return 1; } static void dedup_note(XrActionSet set, const XrActionCreateInfo *info) { // caller holds dedup_lock if (dedup_count >= DEDUP_MAX) return; dedup_seen[dedup_count].set = set; snprintf(dedup_seen[dedup_count].name, XR_MAX_ACTION_NAME_SIZE, "%s", info->actionName); snprintf(dedup_seen[dedup_count].localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, "%s", info->localizedActionName); ++dedup_count; } static XRAPI_ATTR XrResult XRAPI_CALL hook_xrCreateAction(XrActionSet set, const XrActionCreateInfo *info, XrAction *action) { PFN_xrCreateAction fn = (PFN_xrCreateAction)lookup(active_instance, "xrCreateAction"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; XrActionCreateInfo copy, renamed; XrPath *kept = info && info->countSubactionPaths ? calloc(info->countSubactionPaths, sizeof(*kept)) : NULL; const XrActionCreateInfo *use = detached_strip_action(info, ©, kept); char name[XR_MAX_ACTION_NAME_SIZE], localized[XR_MAX_LOCALIZED_ACTION_NAME_SIZE]; if (action_dedup && info) pthread_mutex_lock(&dedup_lock); if (action_dedup && info && dedup_rename(set, use, &renamed, name, localized)) { LOG("action_dedup: action \"%s\" (\"%s\") already exists in its set: created as \"%s\" (\"%s\")", info->actionName, info->localizedActionName, name, localized); use = &renamed; } XrResult r = fn(set, use, action); if (action_dedup && info) { if (XR_SUCCEEDED(r)) dedup_note(set, use); pthread_mutex_unlock(&dedup_lock); } if (input_diag) diag_log_action(use, r); // this hook replaces input_diag's wrapper of xrCreateAction free(kept); if (proximity_emul && XR_SUCCEEDED(r) && info && action) { // OVRPlugin's action names if (!strcmp(info->actionName, "hand_thumb_proximity")) thumb_proximity_action = *action; else if (!strcmp(info->actionName, "hand_trigger_proximity")) trigger_proximity_action = *action; else return r; LOG("proximity_emul: game action %s found", info->actionName); } return r; } static const char *const THUMB_TOUCH_LEFT[] = {"x/touch", "y/touch", "thumbstick/touch", "thumbrest/touch"}; static const char *const THUMB_TOUCH_RIGHT[] = {"a/touch", "b/touch", "thumbstick/touch", "thumbrest/touch"}; static int add_binding(XrInstance instance, PFN_xrStringToPath to_path, XrActionSuggestedBinding *b, uint32_t *n, XrAction action, const char *hand, const char *input) { char path[XR_MAX_PATH_LENGTH]; snprintf(path, sizeof(path), "/user/hand/%s/input/%s", hand, input); XrPath p; if (XR_FAILED(to_path(instance, path, &p))) return 0; for (uint32_t i = 0; i < *n; ++i) if (b[i].action == action && b[i].binding == p) return 0; b[*n].action = action; b[(*n)++].binding = p; return 1; } // Mirrors docs/input/quest_buttons.json; the host test derives its expected bindings from that contract. static const char *const FRAME_COMMON[] = {"squeeze", "squeeze/value", "trigger", "trigger/value", "trigger/touch", "thumbstick", "thumbstick/x", "thumbstick/y", "thumbstick/click", "thumbstick/touch", "grip", "grip/pose", "aim", "aim/pose"}; static const char *const FRAME_DROPPED[] = {"thumbrest/touch", "system/click"}; static const struct { const char *hand, *touch; const char *targets[2][2]; // Mirror, Quest pair } FRAME_BUTTONS[] = { // Quest's second button sits up and toward the other hand: left Y on d-pad right, right B on X {"left", "x", {{"dpad_down", "dpad_left"}, {"dpad_down", NULL}}}, {"left", "y", {{"dpad_up", "dpad_right"}, {"dpad_right", NULL}}}, {"left", "menu", {{"view", NULL}, {"view", NULL}}}, {"right", "a", {{"a", "b"}, {"a", NULL}}}, {"right", "b", {{"x", "y"}, {"x", NULL}}}, }; static void suggest_frame(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested) { if (!quest_buttons || !quest_buttons_enabled || app_suggested_frame) return; PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(instance, "xrStringToPath"); XrPath frame; uint32_t n = suggested->countSuggestedBindings, kept = 0, dropped = 0; XrResult result = XR_ERROR_PATH_UNSUPPORTED; XrActionSuggestedBinding *bindings = NULL; if (!to_path || !to_string || XR_FAILED(to_path(instance, FRAME_PROFILE, &frame))) goto done; // Every source contributes at most two bindings; check before multiplying the uint32_t count. if (n > UINT32_MAX / 2 || !(bindings = calloc(n ? (size_t)n * 2 : 1, sizeof(*bindings)))) { result = XR_ERROR_OUT_OF_MEMORY; goto done; } for (uint32_t i = 0; i < n; ++i) { const XrActionSuggestedBinding *source = &suggested->suggestedBindings[i]; char path[XR_MAX_PATH_LENGTH], target[XR_MAX_PATH_LENGTH]; uint32_t size = 0, before = kept; if (XR_FAILED(to_string(instance, source->binding, sizeof(path), &size, path))) { ++dropped; continue; } const char *hand = NULL, *relative = NULL; if (!strncmp(path, "/user/hand/left/", 16)) { hand = "left"; relative = path + 16; } else if (!strncmp(path, "/user/hand/right/", 17)) { hand = "right"; relative = path + 17; } if (!hand) { ++dropped; continue; } const char *targets[2] = {NULL, NULL}; char button_targets[2][64]; if (!strcmp(relative, "output/haptic")) targets[0] = relative; else if (!strncmp(relative, "input/", 6)) { const char *input = relative + 6; if (IN(input, FRAME_COMMON)) targets[0] = relative; else if (!IN(input, FRAME_DROPPED)) { for (size_t b = 0; b < sizeof(FRAME_BUTTONS) / sizeof(*FRAME_BUTTONS); ++b) { const char *button = FRAME_BUTTONS[b].touch; size_t len = strlen(button); if (strcmp(hand, FRAME_BUTTONS[b].hand) || strncmp(input, button, len) || (input[len] != '/' && input[len])) continue; // a bare button path (input/x) is legal OpenXR: the runtime picks click, so keep it bare const char *component = input[len] ? input + len + 1 : ""; if (*component && strcmp(component, "click") && (strcmp(button, "menu") == 0 || strcmp(component, "touch"))) continue; for (int t = 0; t < 2; ++t) { const char *name = FRAME_BUTTONS[b].targets[quest_buttons - 1][t]; if (!name) continue; snprintf(button_targets[t], sizeof(button_targets[t]), "input/%s%s%s", name, *component ? "/" : "", component); targets[t] = button_targets[t]; } break; } } } for (int t = 0; t < 2; ++t) { if (!targets[t]) continue; snprintf(target, sizeof(target), "/user/hand/%s/%s", hand, targets[t]); XrPath p; if (XR_FAILED(to_path(instance, target, &p))) continue; bindings[kept++] = (XrActionSuggestedBinding){source->action, p}; input_diag_quest_binding(path, target); } if (kept == before) ++dropped; } if (!kept) { LOG("quest_buttons: frame_controller bindings (mode %d, 0 bindings, %u dropped): not suggested", quest_buttons, dropped); free(bindings); return; // A previously accepted Frame set stays in effect: OpenXR cannot replace it with an empty set. } XrInteractionProfileSuggestedBinding copy = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING, NULL, frame, kept, bindings}; result = fn(instance, ©); if (XR_SUCCEEDED(result)) generated_frame_bindings = 1; done: LOG("quest_buttons: frame_controller bindings (mode %d, %u bindings, %u dropped): %d", quest_buttons, kept, dropped, result); free(bindings); } // Run this for every Touch attempt so Frame bindings come from exactly the set the runtime accepted. static XrResult suggest_touch_set(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested) { XrResult r = fn(instance, suggested); if (XR_SUCCEEDED(r)) suggest_frame(instance, fn, to_string, suggested); return r; } // Suggests Touch bindings with finger proximity bound to the touch inputs (proximity_emul), falling back to the // bindings as given when the runtime refuses them. static XrResult suggest_touch(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested) { PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(instance, "xrStringToPath"); if (!proximity_emul || runtime_has_proximity || !to_path || !to_string) return suggest_touch_set(instance, fn, to_string, suggested); XrAction thumb = thumb_proximity_action, index = proximity_emul > 1 ? trigger_proximity_action : XR_NULL_HANDLE; uint32_t n = suggested->countSuggestedBindings, kept = 0, dropped = 0, added = 0; XrActionSuggestedBinding *b = calloc(n + 2 * 5, sizeof(*b)); if (!b) return suggest_touch_set(instance, fn, to_string, suggested); for (uint32_t i = 0; i < n; ++i) { char path[XR_MAX_PATH_LENGTH]; uint32_t size = 0; const XrActionSuggestedBinding *s = &suggested->suggestedBindings[i]; if (XR_SUCCEEDED(to_string(instance, s->binding, sizeof(path), &size, path)) && strstr(path, "/proximity_")) { // the game's own proximity binding, which the runtime doesn't know: its action is bound to touch below if (strstr(path, "/thumb")) { if (!thumb) thumb = s->action; } else if (proximity_emul > 1 && !index) index = s->action; ++dropped; continue; } b[kept++] = *s; } for (int hand = 0; hand < 2; ++hand) { const char *name = hand ? "right" : "left"; const char *const *thumb_inputs = hand ? THUMB_TOUCH_RIGHT : THUMB_TOUCH_LEFT; for (int c = 0; thumb && c < 4; ++c) added += add_binding(instance, to_path, b, &kept, thumb, name, thumb_inputs[c]); if (index) added += add_binding(instance, to_path, b, &kept, index, name, "trigger/touch"); } XrResult r; if (!added && !dropped) r = suggest_touch_set(instance, fn, to_string, suggested); else { XrInteractionProfileSuggestedBinding copy = *suggested; copy.countSuggestedBindings = kept; copy.suggestedBindings = b; r = suggest_touch_set(instance, fn, to_string, ©); LOG("proximity_emul: finger proximity bound to touch (%u binding(s) added, %u proximity path(s) replaced): %d", added, dropped, r); if (XR_FAILED(r)) r = suggest_touch_set(instance, fn, to_string, suggested); } free(b); return r; } static XrResult remap_to_touch(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested, const char *profile) { PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(instance, "xrStringToPath"); XrPath touch; if (!to_path || XR_FAILED(to_path(instance, TOUCH_PROFILE, &touch))) return XR_ERROR_PATH_UNSUPPORTED; uint32_t n = suggested->countSuggestedBindings, kept = 0; XrActionSuggestedBinding *b = calloc(n ? n : 1, sizeof(*b)); if (!b) return XR_ERROR_OUT_OF_MEMORY; for (uint32_t i = 0; i < n; ++i) { char path[XR_MAX_PATH_LENGTH]; uint32_t size = 0; if (XR_SUCCEEDED(to_string(instance, suggested->suggestedBindings[i].binding, sizeof(path), &size, path)) && touch_has(path)) b[kept++] = suggested->suggestedBindings[i]; } XrInteractionProfileSuggestedBinding copy = *suggested; copy.interactionProfile = touch; copy.countSuggestedBindings = kept; copy.suggestedBindings = b; XrResult r = kept ? suggest_touch(instance, fn, to_string, ©) : XR_ERROR_PATH_UNSUPPORTED; free(b); LOG("controller profile %s -> oculus/touch_controller (%u bindings, %u dropped): %d", profile + 22, kept, n - kept, r); return r; } static XrResult suggest_bindings(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, const XrInteractionProfileSuggestedBinding *suggested); static XRAPI_ATTR XrResult XRAPI_CALL hook_xrSuggestInteractionProfileBindings(XrInstance instance, const XrInteractionProfileSuggestedBinding *suggested) { PFN_xrSuggestInteractionProfileBindings fn = (PFN_xrSuggestInteractionProfileBindings)lookup(instance, "xrSuggestInteractionProfileBindings"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; // The last accepted call per profile wins. Keep the game's call, fallback, generated set and flags together. // Internal helpers use the next-layer function directly, so fallback paths never take this mutex twice. pthread_mutex_lock(&bindings_lock); XrInteractionProfileSuggestedBinding copy; XrActionSuggestedBinding *kept = suggested && suggested->countSuggestedBindings ? calloc(suggested->countSuggestedBindings, sizeof(*kept)) : NULL; const XrInteractionProfileSuggestedBinding *use = detached_strip_bindings(suggested, ©, kept); // a profile with only detached-controller bindings: nothing the runtime could bind XrResult r = use != suggested && !use->countSuggestedBindings ? XR_SUCCESS : suggest_bindings(instance, fn, use); free(kept); pthread_mutex_unlock(&bindings_lock); return r; } static XrResult suggest_bindings(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn, const XrInteractionProfileSuggestedBinding *suggested) { PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(instance, "xrPathToString"); if (suggested && to_string && (layer_debug || aim_correction_on())) for (uint32_t i = 0; i < suggested->countSuggestedBindings; ++i) { char path[XR_MAX_PATH_LENGTH]; uint32_t size = 0; if (XR_SUCCEEDED(to_string(instance, suggested->suggestedBindings[i].binding, sizeof(path), &size, path))) note_pose_binding(suggested->suggestedBindings[i].action, path); } char profile[XR_MAX_PATH_LENGTH] = {0}; uint32_t size = 0; int known = suggested && to_string && XR_SUCCEEDED(to_string(instance, suggested->interactionProfile, sizeof(profile), &size, profile)); XrResult r = known && !strcmp(profile, TOUCH_PROFILE) ? suggest_touch(instance, fn, to_string, suggested) : fn(instance, suggested); input_diag_suggested(instance, suggested, r, to_string); // the runtime's answer to the game's own suggestion if (known && XR_SUCCEEDED(r) && !strcmp(profile, FRAME_PROFILE) && quest_buttons && quest_buttons_enabled) { app_suggested_frame = 1; generated_frame_bindings = 0; } if (!profile_remap || !known) return r; if (!strcmp(profile, TOUCH_PROFILE)) { if (XR_SUCCEEDED(r)) app_suggested_touch = 1; // the app's own Touch bindings win over a remap return r; } if (r != XR_ERROR_PATH_UNSUPPORTED || app_suggested_touch || !IN(profile, NEWER_TOUCH)) return r; XrResult remapped = remap_to_touch(instance, fn, to_string, suggested, profile); if (XR_SUCCEEDED(remapped)) snprintf(remapped_profile, sizeof(remapped_profile), "%s", profile); return XR_SUCCEEDED(remapped) ? XR_SUCCESS : r; } static XRAPI_ATTR XrResult XRAPI_CALL hook_xrCreateActionSpace(XrSession session, const XrActionSpaceCreateInfo *info, XrSpace *space) { PFN_xrCreateActionSpace fn = (PFN_xrCreateActionSpace)lookup(active_instance, "xrCreateActionSpace"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; if (!info) return fn(session, info, space); int kind = pose_action_kind(info->action); XrActionSpaceCreateInfo fixed = *info; if (kind == 1 && aim_correction_on()) { XrQuaternionf q = lm_qmul(lm_axis_angle(0, 1, 0, aim_yaw * LM_PI / 180), lm_axis_angle(1, 0, 0, aim_pitch * LM_PI / 180)); XrPosef correction = {q, {0, 0, -aim_forward}}; fixed.poseInActionSpace = lm_pose_mul(correction, info->poseInActionSpace); static int logged; if (logged++ < 4) LOG("aim correction: pitch %.1f deg, yaw %.1f deg, forward %.3f m", aim_pitch, aim_yaw, aim_forward); } XrResult result = fn(session, &fixed, space); if (layer_debug && XR_SUCCEEDED(result) && space && kind >= 0) { pthread_mutex_lock(&pose_lock); if (pose_space_count < MAX_POSE_ACTIONS) { pose_spaces[pose_space_count].space = *space; pose_spaces[pose_space_count].subaction = info->subactionPath; pose_spaces[pose_space_count++].aim = kind; } pthread_mutex_unlock(&pose_lock); LOG("layer_debug: %s action space %p (subaction %llu)", kind ? "aim" : "grip", (void *)(uintptr_t)*space, (unsigned long long)info->subactionPath); } return result; } // layer_debug: about once a second, the aim pose of each hand expressed in its grip pose. static void debug_aim_vs_grip(XrTime time) { static int64_t last; int64_t now = monotonic_ns(); if (!layer_debug || !time || now - last < 1000000000ll) return; last = now; PFN_xrLocateSpace locate = (PFN_xrLocateSpace)lookup(active_instance, "xrLocateSpace"); if (!locate) return; pthread_mutex_lock(&pose_lock); for (int a = 0; a < pose_space_count; ++a) { if (!pose_spaces[a].aim) continue; for (int g = 0; g < pose_space_count; ++g) { if (pose_spaces[g].aim || pose_spaces[g].subaction != pose_spaces[a].subaction) continue; XrSpaceLocation loc = {XR_TYPE_SPACE_LOCATION, NULL, 0, {{0, 0, 0, 1}, {0, 0, 0}}}; if (XR_SUCCEEDED(locate(pose_spaces[a].space, pose_spaces[g].space, time, &loc)) && (loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) { XrQuaternionf q = loc.pose.orientation; float pitch = asinf(fmaxf(-1, fminf(1, 2 * (q.w * q.x - q.y * q.z)))) * 180 / LM_PI; float yaw = atan2f(2 * (q.w * q.y + q.x * q.z), 1 - 2 * (q.x * q.x + q.y * q.y)) * 180 / LM_PI; LOG("layer_debug: aim in grip (subaction %llu): pos %.3f,%.3f,%.3f pitch %.1f yaw %.1f flags 0x%llx", (unsigned long long)pose_spaces[a].subaction, loc.pose.position.x, loc.pose.position.y, loc.pose.position.z, pitch, yaw, (unsigned long long)loc.locationFlags); } break; } } pthread_mutex_unlock(&pose_lock); } // ---------------------------------------------------------------- refresh rate static void debug_refresh_rates(XrSession session) { PFN_xrEnumerateDisplayRefreshRatesFB enumerate = (PFN_xrEnumerateDisplayRefreshRatesFB)lookup(active_instance, "xrEnumerateDisplayRefreshRatesFB"); PFN_xrGetDisplayRefreshRateFB get = (PFN_xrGetDisplayRefreshRateFB)lookup(active_instance, "xrGetDisplayRefreshRateFB"); float rates[32], current = 0; uint32_t n = 0; char text[256] = ""; if (enumerate && XR_SUCCEEDED(enumerate(session, 32, &n, rates))) for (uint32_t i = 0, used = 0; i < n && i < 32 && used < sizeof(text) - 8; ++i) used += (uint32_t)snprintf(text + used, sizeof(text) - used, " %.0f", rates[i]); if (get) get(session, ¤t); LOG("layer_debug: refresh rates offered:%s; current %.1f Hz", *text ? text : " (unknown)", current); } static void refresh_on_begin_session(XrSession session) { if (layer_debug) debug_refresh_rates(session); if (refresh_rate <= 0) return; PFN_xrRequestDisplayRefreshRateFB request = (PFN_xrRequestDisplayRefreshRateFB)lookup(active_instance, "xrRequestDisplayRefreshRateFB"); XrResult result = request ? request(session, refresh_rate) : XR_ERROR_FUNCTION_UNSUPPORTED; LOG("refresh_rate: requested %.1f Hz at session start: result %d", refresh_rate, result); } static XRAPI_ATTR XrResult XRAPI_CALL hook_request_refresh_rate(XrSession session, float rate) { PFN_xrRequestDisplayRefreshRateFB fn = (PFN_xrRequestDisplayRefreshRateFB)lookup(active_instance, "xrRequestDisplayRefreshRateFB"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; float wanted = refresh_rate > 0 ? refresh_rate : rate; XrResult result = fn(session, wanted); LOG("refresh rate: app asked for %.1f Hz, requested %.1f Hz: result %d", rate, wanted, result); // A refused override keeps the current rate (as frame-control's compat layer does) instead of failing the app. return XR_FAILED(result) && refresh_rate > 0 ? XR_SUCCESS : result; } // Always on: the Frame's runtime has no XR_FB_display_refresh_rate; OVRPort's dispatcher still offers the extension and // its xrGetDisplayRefreshRateFB then answers 0 Hz. idTech 4 VR ports (Team Beef's Doom3Quest engine: PreyVR, GitHub // #306) run their game clock at 1000 / refresh ms per tic: with 0 Hz the tic length is 0, the async thread spins and // the frame/GUI time never advances (main menu stuck at its first animation step: black, only the cursor). A rate the // runtime doesn't give is replaced by the one the frame timing shows (refresh_rate.h). static XRAPI_ATTR XrResult XRAPI_CALL hook_get_refresh_rate(XrSession session, float *rate) { if (!rate) return XR_ERROR_VALIDATION_FAILURE; PFN_xrGetDisplayRefreshRateFB fn = (PFN_xrGetDisplayRefreshRateFB)lookup(active_instance, "xrGetDisplayRefreshRateFB"); XrResult result = fn ? fn(session, rate) : XR_ERROR_FUNCTION_UNSUPPORTED; if ((XR_SUCCEEDED(result) && *rate > 0.0f) || result == XR_ERROR_HANDLE_INVALID) return result; float runtime = XR_SUCCEEDED(result) ? *rate : 0.0f; *rate = fb_refresh_rate((int64_t)last_display_period, refresh_rate); static int logs; if (logs < 4) { ++logs; LOG("refresh rate: runtime reported %.1f Hz (result %d), reporting %.1f Hz", runtime, result, *rate); } return XR_SUCCESS; } static XRAPI_ATTR XrResult XRAPI_CALL hook_xrBeginSession(XrSession session, const XrSessionBeginInfo *info) { PFN_xrBeginSession fn = (PFN_xrBeginSession)lookup(active_instance, "xrBeginSession"); if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED; XrResult result = fn(session, info); if (XR_SUCCEEDED(result)) { emul_on_begin_session(info); refresh_on_begin_session(session); } return result; }