mirror of
https://github.com/vesper8/Quest2Frame.git
synced 2026-10-23 05:02:01 +02:00
Include managed uninstall and tests. Keep-awake integration is work in progress: Frame authorization currently rejects the inhibitor and blocks wireless installation until resolved.
216 lines
13 KiB
C
216 lines
13 KiB
C
// SPDX-License-Identifier: GPL-3.0-only
|
|
#define _GNU_SOURCE
|
|
#define XR_EXTENSION_PROTOTYPES
|
|
#include <openxr/openxr.h>
|
|
#include <android/log.h>
|
|
#include <dlfcn.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static void *library;
|
|
static PFN_xrGetInstanceProcAddr original;
|
|
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
|
static XrInstance current;
|
|
static float scale = 1.0f;
|
|
static float refresh_rate = 0.0f;
|
|
static int refresh_enabled;
|
|
/* Capability marker also checked by the Windows installer. */
|
|
__attribute__((used)) static const char refresh_capability[] = "Q2F_REFRESH_RATE_V1";
|
|
static int foveation_fix = 1, controller_fix = 1;
|
|
|
|
static void settings(const char *path) {
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return;
|
|
char line[256]; float value;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (sscanf(line, "scale=%f", &value) == 1 && value >= .5f && value <= 2.f) scale = value;
|
|
if (sscanf(line, "refresh_rate=%f", &value) == 1 && (value == 0 || value == 72 || value == 80 || value == 90 || value == 120 || value == 144)) refresh_rate = value;
|
|
if (sscanf(line, "foveation_fix=%f", &value) == 1) foveation_fix = value != 0;
|
|
if (sscanf(line, "controller_fix=%f", &value) == 1) controller_fix = value != 0;
|
|
}
|
|
fclose(f);
|
|
}
|
|
static void initialize(void) {
|
|
Dl_info self; char path[4096], process[256] = {0};
|
|
if (!dladdr((void *)&initialize, &self)) return;
|
|
snprintf(path, sizeof(path), "%s", self.dli_fname);
|
|
char *slash = strrchr(path, '/'); if (!slash) return;
|
|
size_t remaining = sizeof(path) - (size_t)(slash + 1 - path);
|
|
snprintf(slash + 1, remaining, "libframe_settings.so"); settings(path);
|
|
snprintf(slash + 1, remaining, "libopenxr_loader_original.so");
|
|
library = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
|
if (library) original = (PFN_xrGetInstanceProcAddr)dlsym(library, "xrGetInstanceProcAddr");
|
|
FILE *f = fopen("/proc/self/cmdline", "r");
|
|
if (f) { fread(process, 1, sizeof(process)-1, f); fclose(f); }
|
|
char *colon = strchr(process, ':'); if (colon) *colon = 0;
|
|
if (*process && !strchr(process, '/')) {
|
|
snprintf(path, sizeof(path), "/sdcard/Android/data/%s/files/framebridge.conf", process);
|
|
settings(path);
|
|
}
|
|
const char *config = getenv("FRAMEBRIDGE_CONFIG");
|
|
if (config && *config) settings(config);
|
|
__android_log_print(ANDROID_LOG_INFO, "FrameBridge", "scale=%.2f foveation_fix=%d controller_fix=%d loader=%s", scale, foveation_fix, controller_fix, original ? "OK" : "FAILED");
|
|
}
|
|
static PFN_xrVoidFunction resolve(XrInstance instance, const char *name) {
|
|
pthread_once(&once, initialize);
|
|
PFN_xrVoidFunction fn = NULL;
|
|
if (original) original(instance, name, &fn);
|
|
if (!fn && library) fn = (PFN_xrVoidFunction)dlsym(library, name);
|
|
return fn;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info, XrInstance *instance) {
|
|
PFN_xrCreateInstance fn = (PFN_xrCreateInstance)resolve(XR_NULL_HANDLE, "xrCreateInstance");
|
|
if (!fn || !info) return XR_ERROR_INITIALIZATION_FAILED;
|
|
int has = 0, enabled = 0;
|
|
for (const XrBaseInStructure *p = info->next; p; p = p->next)
|
|
if (p->type == XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR) has = 1;
|
|
for (uint32_t i=0; i<info->enabledExtensionCount; ++i)
|
|
if (!strcmp(info->enabledExtensionNames[i], "XR_KHR_android_create_instance")) enabled = 1;
|
|
XrInstanceCreateInfo fixed = *info;
|
|
const char **names = NULL;
|
|
int refresh_present = 0, refresh_supported = 0;
|
|
for (uint32_t i=0; i<info->enabledExtensionCount; ++i)
|
|
if (!strcmp(info->enabledExtensionNames[i], XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME)) refresh_present = 1;
|
|
PFN_xrEnumerateInstanceExtensionProperties enumerate = (PFN_xrEnumerateInstanceExtensionProperties)resolve(XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
|
|
uint32_t count = 0;
|
|
if (enumerate && XR_SUCCEEDED(enumerate(NULL, 0, &count, NULL)) && count) {
|
|
XrExtensionProperties *props = calloc(count, sizeof(*props));
|
|
if (props) {
|
|
uint32_t capacity = count;
|
|
for (uint32_t i=0; i<count; ++i) props[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
|
if (XR_SUCCEEDED(enumerate(NULL, capacity, &count, props)))
|
|
for (uint32_t i=0; i<count && i<capacity; ++i)
|
|
if (!strcmp(props[i].extensionName, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME)) refresh_supported = 1;
|
|
free(props);
|
|
}
|
|
}
|
|
if ((has && !enabled) || (refresh_supported && !refresh_present)) {
|
|
names = calloc(info->enabledExtensionCount + 2, sizeof(*names));
|
|
if (!names) return XR_ERROR_OUT_OF_MEMORY;
|
|
for (uint32_t i=0; i<info->enabledExtensionCount; ++i) names[i] = info->enabledExtensionNames[i];
|
|
if (has && !enabled) names[fixed.enabledExtensionCount++] = "XR_KHR_android_create_instance";
|
|
if (refresh_supported && !refresh_present) names[fixed.enabledExtensionCount++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
|
|
fixed.enabledExtensionNames = names;
|
|
}
|
|
XrResult result = fn(&fixed, instance); free(names);
|
|
if (XR_SUCCEEDED(result)) { current = *instance; refresh_enabled = refresh_present || refresh_supported; }
|
|
return result;
|
|
}
|
|
static int supports_refresh(XrSession session) {
|
|
if (!refresh_enabled || !refresh_rate) return 0;
|
|
PFN_xrEnumerateDisplayRefreshRatesFB fn = (PFN_xrEnumerateDisplayRefreshRatesFB)resolve(current, "xrEnumerateDisplayRefreshRatesFB");
|
|
uint32_t count = 0;
|
|
if (!fn || XR_FAILED(fn(session, 0, &count, NULL)) || !count) return 0;
|
|
float *rates = calloc(count, sizeof(*rates));
|
|
if (!rates) return 0;
|
|
uint32_t capacity = count; int supported = 0;
|
|
if (XR_SUCCEEDED(fn(session, capacity, &count, rates)))
|
|
for (uint32_t i=0; i<count && i<capacity; ++i)
|
|
if (rates[i] == refresh_rate) supported = 1;
|
|
free(rates); return supported;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrBeginSession(XrSession session, const XrSessionBeginInfo *info) {
|
|
PFN_xrBeginSession fn = (PFN_xrBeginSession)resolve(current, "xrBeginSession");
|
|
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
|
XrResult result = fn(session, info);
|
|
if (XR_SUCCEEDED(result) && refresh_rate) {
|
|
PFN_xrRequestDisplayRefreshRateFB request = (PFN_xrRequestDisplayRefreshRateFB)resolve(current, "xrRequestDisplayRefreshRateFB");
|
|
if (supports_refresh(session) && request) {
|
|
XrResult requested = request(session, refresh_rate);
|
|
__android_log_print(ANDROID_LOG_INFO, "FrameBridge", "refresh rate request=%.0f Hz result=%d (runtime decides)", refresh_rate, requested);
|
|
} else __android_log_print(ANDROID_LOG_INFO, "FrameBridge", "refresh rate %.0f Hz unavailable; keeping runtime default", refresh_rate);
|
|
}
|
|
return result;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrRequestDisplayRefreshRateFB(XrSession session, float rate) {
|
|
PFN_xrRequestDisplayRefreshRateFB fn = (PFN_xrRequestDisplayRefreshRateFB)resolve(current, "xrRequestDisplayRefreshRateFB");
|
|
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
|
return fn(session, supports_refresh(session) ? refresh_rate : rate);
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateViewConfigurationViews(XrInstance instance, XrSystemId system, XrViewConfigurationType type, uint32_t capacity, uint32_t *count, XrViewConfigurationView *views) {
|
|
PFN_xrEnumerateViewConfigurationViews fn = (PFN_xrEnumerateViewConfigurationViews)resolve(instance, "xrEnumerateViewConfigurationViews");
|
|
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
|
XrResult result = fn(instance, system, type, capacity, count, views);
|
|
if (XR_SUCCEEDED(result) && views && count) for (uint32_t i=0; i<*count && i<capacity; ++i) {
|
|
uint32_t w = (uint32_t)(views[i].recommendedImageRectWidth * scale + .5f);
|
|
uint32_t h = (uint32_t)(views[i].recommendedImageRectHeight * scale + .5f);
|
|
views[i].recommendedImageRectWidth = w > views[i].maxImageRectWidth ? views[i].maxImageRectWidth : (w ? w : 1);
|
|
views[i].recommendedImageRectHeight = h > views[i].maxImageRectHeight ? views[i].maxImageRectHeight : (h ? h : 1);
|
|
__android_log_print(ANDROID_LOG_INFO, "FrameBridge", "view=%u recommended=%ux%u scale=%.2f", i, views[i].recommendedImageRectWidth, views[i].recommendedImageRectHeight, scale);
|
|
}
|
|
return result;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwapchainCreateInfo *info, XrSwapchain *swapchain) {
|
|
PFN_xrCreateSwapchain fn = (PFN_xrCreateSwapchain)resolve(current, "xrCreateSwapchain");
|
|
if (!fn || !info) return XR_ERROR_INITIALIZATION_FAILED;
|
|
XrSwapchainCreateInfo fixed = *info;
|
|
const XrBaseInStructure *next = fixed.next;
|
|
while (foveation_fix && next && next->type == XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB) next = next->next;
|
|
fixed.next = next;
|
|
XrResult result = fn(session, &fixed, swapchain);
|
|
__android_log_print(ANDROID_LOG_INFO, "FrameBridge", "swapchain=%ux%u result=%d", fixed.width, fixed.height, result);
|
|
return result;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrLocateHandJointsEXT(XrHandTrackerEXT tracker, const XrHandJointsLocateInfoEXT *info, XrHandJointLocationsEXT *locations) {
|
|
if (!controller_fix) {
|
|
PFN_xrLocateHandJointsEXT fn = (PFN_xrLocateHandJointsEXT)resolve(current, "xrLocateHandJointsEXT");
|
|
return fn ? fn(tracker, info, locations) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
|
}
|
|
if (!locations) return XR_ERROR_VALIDATION_FAILURE;
|
|
locations->isActive = XR_FALSE;
|
|
for (XrBaseOutStructure *p=locations->next; p; p=p->next)
|
|
if (p->type == XR_TYPE_HAND_TRACKING_AIM_STATE_FB) ((XrHandTrackingAimStateFB *)p)->status = 0;
|
|
return XR_SUCCESS;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetCurrentInteractionProfile(XrSession session, XrPath user, XrInteractionProfileState *state) {
|
|
PFN_xrGetCurrentInteractionProfile fn = (PFN_xrGetCurrentInteractionProfile)resolve(current, "xrGetCurrentInteractionProfile");
|
|
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
|
XrResult result = fn(session, user, state);
|
|
if (controller_fix && XR_SUCCEEDED(result) && state->interactionProfile && current) {
|
|
PFN_xrPathToString str = (PFN_xrPathToString)resolve(current, "xrPathToString");
|
|
PFN_xrStringToPath path = (PFN_xrStringToPath)resolve(current, "xrStringToPath");
|
|
char name[XR_MAX_PATH_LENGTH]; uint32_t size;
|
|
if (str && path && XR_SUCCEEDED(str(current, state->interactionProfile, sizeof(name), &size, name)) && (strstr(name, "/valve/") || strstr(name, "/khr/generic_controller")))
|
|
path(current, "/interaction_profiles/oculus/touch_controller", &state->interactionProfile);
|
|
}
|
|
return result;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateInstanceExtensionProperties(const char *layer, uint32_t capacity, uint32_t *count, XrExtensionProperties *properties) {
|
|
PFN_xrEnumerateInstanceExtensionProperties fn = (PFN_xrEnumerateInstanceExtensionProperties)resolve(XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
|
|
if (!fn || !count) return XR_ERROR_INITIALIZATION_FAILED;
|
|
if (!foveation_fix) return fn(layer, capacity, count, properties);
|
|
uint32_t total=0; XrResult result = fn(layer, 0, &total, NULL);
|
|
if (XR_FAILED(result)) return result;
|
|
XrExtensionProperties *all = calloc(total ? total : 1, sizeof(*all));
|
|
if (!all) return XR_ERROR_OUT_OF_MEMORY;
|
|
for (uint32_t i=0; i<total; ++i) all[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
|
result = fn(layer, total, &total, all);
|
|
if (XR_FAILED(result)) { free(all); return result; }
|
|
uint32_t kept=0;
|
|
for (uint32_t i=0; i<total; ++i) {
|
|
if (strstr(all[i].extensionName, "foveation")) continue;
|
|
if (properties && kept < capacity) properties[kept] = all[i];
|
|
++kept;
|
|
}
|
|
free(all); *count=kept;
|
|
return capacity && capacity < kept ? XR_ERROR_SIZE_INSUFFICIENT : XR_SUCCESS;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateApiLayerProperties(uint32_t capacity, uint32_t *count, XrApiLayerProperties *properties) {
|
|
PFN_xrEnumerateApiLayerProperties fn = (PFN_xrEnumerateApiLayerProperties)resolve(XR_NULL_HANDLE, "xrEnumerateApiLayerProperties");
|
|
return fn ? fn(capacity, count, properties) : XR_ERROR_INITIALIZATION_FAILED;
|
|
}
|
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const char *name, PFN_xrVoidFunction *function) {
|
|
pthread_once(&once, initialize);
|
|
if (!original) return XR_ERROR_INITIALIZATION_FAILED;
|
|
if (!name || !function) return XR_ERROR_VALIDATION_FAILURE;
|
|
#define HOOK(n) if (!strcmp(name, #n)) { *function=(PFN_xrVoidFunction)n; return XR_SUCCESS; }
|
|
HOOK(xrCreateInstance) HOOK(xrEnumerateViewConfigurationViews)
|
|
HOOK(xrBeginSession)
|
|
if (refresh_enabled) { HOOK(xrRequestDisplayRefreshRateFB) }
|
|
HOOK(xrCreateSwapchain) HOOK(xrLocateHandJointsEXT)
|
|
HOOK(xrGetCurrentInteractionProfile) HOOK(xrEnumerateInstanceExtensionProperties)
|
|
return original(instance, name, function);
|
|
}
|