Files
spoopyghosty0--frameport/native/adapter/frame_adapter.c
T

2679 lines
158 KiB
C

// SPDX-License-Identifier: GPL-3.0-only
// Steam Frame OpenXR adapter for overport-patched Quest games.
//
// Installed as lib/arm64-v8a/libopenxr_loader_generic.so; overport's real generic
// loader is renamed to libopenxr_loader_original.so and loaded from the same dir.
//
// Fixes applied:
// * adds XR_KHR_android_create_instance when the app chains
// XrInstanceCreateInfoAndroidKHR but forgets to enable the extension;
// * hides foveation extensions and strips XrSwapchainCreateInfoFoveationFB
// (foveation_fix);
// * reports Valve / generic controller profiles as Oculus Touch and suppresses
// synthetic hand-tracking data (controller_fix);
// * scales the recommended eye-buffer size (scale);
// * drops requested instance extensions the runtime lacks (e.g. XR_FB_passthrough);
// * retries rejected swapchains with 1 sample / a supported format (swapchain_fix, overport #71);
// * drops composition layers that reference swapchains that failed to create (layer_fix);
// * answers xrGetDisplayRefreshRateFB with the measured display rate when the runtime lacks it or reports 0 Hz
// (session_fixes.c);
// * serves refused cube swapchains as GL/Vulkan cube maps and emulates cube-only frames with quads (cube_standin, cube_standin.c);
// * optionally requests mutable-format swapchain images (mutable_fix, off by default);
// * optionally swaps left/right images of stereo projection layers (swap_eyes, off by default);
// * emulates XR_FB_passthrough with XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND when the runtime lacks it
// (passthrough_emul);
// * optionally tells the app its reference spaces changed once the session is focused, so apps that
// created them before tracking started recreate them (respace_kick);
// * optionally flips quad layers vertically for apps whose UI panels show upside down (flip_quads);
// * optionally serves Steam Frame controller models through XR_FB_render_model (controller_models,
// render_model.c);
// * per-game session fixes (session_fixes.c, all off by default): layer_debug diagnostics, stable_local,
// focus_hold, aim pose correction (aim_pitch/aim_yaw/aim_forward), refresh_rate;
// * optionally logs controller-input diagnostics: suggested interaction profiles and the runtime's answers, each
// hand's current profile, functions the runtime lacks, failing input/haptics/perf calls (input_diag,
// input_diag.c, off by default);
// * optionally shows 360° (equirect/equirect2) layers as cube-face quads drawn by a worker thread with a shared
// GLES context (equirect_emul, layer_emul_gl.c; GLES sessions only, off by default).
//
// Settings (key=value lines), later sources override earlier ones:
// <libdir>/libframe_settings.so
// /sdcard/Android/data/<package>/files/framebridge.conf
// $FRAMEBRIDGE_CONFIG
#define _GNU_SOURCE
#define VK_USE_PLATFORM_ANDROID_KHR
#define XR_EXTENSION_PROTOTYPES
#include <openxr/openxr.h>
#include <android/log.h>
#include <dlfcn.h>
#include <math.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAG "FrameBridge"
#include <stdarg.h>
#include <time.h>
// Everything also goes to <app external files>/framebridge.log when eye_debug is on: some games end Lepton's logcat
// mirror early (WiiCompiled), so a headset session's diagnostics would otherwise be lost.
static FILE *log_file;
static void fb_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void fb_log(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
__android_log_vprint(ANDROID_LOG_INFO, TAG, fmt, args);
va_end(args);
if (!log_file) return;
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
flockfile(log_file);
fprintf(log_file, "%lld.%03ld ", (long long)now.tv_sec, now.tv_nsec / 1000000);
va_start(args, fmt);
vfprintf(log_file, fmt, args);
va_end(args);
fputc('\n', log_file);
fflush(log_file);
funlockfile(log_file);
}
#define LOG(...) fb_log(__VA_ARGS__)
#include "audio_metadata.h"
#include "batman_audio_geometry.h"
#include "surface_views.h"
static void *loader;
static PFN_xrGetInstanceProcAddr next_gipa;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static XrInstance active_instance = XR_NULL_HANDLE;
static void *android_vm; // JavaVM from XrInstanceCreateInfoAndroidKHR (surface_swapchain.c)
static int surface_emul = 1; // setting: emulate Android surface swapchains
static int surface_native; // internal: GPU shared video buffers (panoramic video clients)
static int eye_debug; // setting: per-eye diagnostics + file log (eye_debug.c)
static int release_wait; // setting: wait for the app's GPU work before releasing images (2 = after 60 s: A/B)
static void surf_scene_destroy(XrSwapchain handle);
static void surf_on_destroy(XrSwapchain handle);
static float scale = 1.0f;
static int foveation_fix = 1;
static int foveation_storage_fix; // ordinary (not SUBSAMPLED) swapchain storage; on with device.foveation
static int foveation_eye; // experimental profile upgrade; does not inject Vulkan gaze offsets
static int foveation_eye_enabled, foveation_eye_supported;
#include "foveation_storage.h"
#include "hide_fb_foveation.h"
static int hide_fb_foveation;
static int hide_fb_foveation_vulkan;
// hide XR_FB_space_warp: games then render every frame themselves (UE5 Application SpaceWarp flickered on the Frame,
// e.g. Into The Radius 2; OVRPort's patch_disable_space_warp doesn't reach UE5's OpenXR plugin)
static int hide_space_warp = 0;
static int strip_color_bias = 0;
// fade_quad.c: draw colour-scale fades (fade to black) as a black quad over the picture; 2 = also remove the scale
static int fade_quad = 0;
static uint32_t runtime_max_layers = 16; // XrSystemGraphicsProperties::maxLayerCount (the Frame: 16)
static int snapshot = 0; // seconds between eye-image snapshots (snapshot_gl.c; 0 = off)
static char snap_dir[256];
static int gl_error_clear; // GLES: clear GL errors the app left pending before calling the runtime (Vader)
static int gles_session;
static int flip_dump; // diagnostics: save the first N flipped quads before/after the flip (flip_vk.c)
static void snap_on_create(XrSwapchain handle, const XrSwapchainCreateInfo *info);
static void snap_on_destroy(XrSwapchain handle);
static void snap_on_enumerate(XrSwapchain handle, uint32_t count, const XrSwapchainImageBaseHeader *images);
static void snap_on_acquire(XrSwapchain handle, uint32_t index);
static void snap_end_frame(const XrFrameEndInfo *info);
static int frame_balance = 0; // end a still-open frame before the next xrBeginFrame
// diagnostics (headless launch tests, GitHub #323): report shouldRender=TRUE from xrWaitFrame. Without a worn headset the
// session never becomes VISIBLE, the runtime answers shouldRender=FALSE and engines that honour it (Unreal's OpenXR
// plugin) submit no layers at all, so a headless run never exercises the game's real frame submission.
static int force_render = 0;
static int frame_begins, frame_discarded, frame_ends, frame_balanced; // per pacing period (layer_debug)
static int frame_open;
static int sc_acquires, sc_waits, sc_releases, sc_wait_fails; // per pacing period (layer_debug) // drop XrCompositionLayerColorScaleBiasKHR (the runtime's color pass)
static int controller_fix = 1;
static int swapchain_fix = 1;
static int cube_quads = 1;
static int cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
static int rect_clamp = 1;
static int fixed_resolution;
static int swapchain_pool = 0;
static int fb_color_space; // fb_color_space: offer XR_FB_color_space when the runtime lacks it (#336)
static int action_dedup = 1; // rename duplicate action names in a set (session_fixes.c; GitHub #323)
static int pose_consistency = 0; // repeat xrLocateViews queries for one display time get the same poses (GitHub #8) // clamp submitted image rects to their swapchain (SteamVR rejects a 1 px overrun)
static int pose_time_fix; // poses asked for at CLOCK_MONOTONIC "now" or far in the past: at XrTime "now" (pose_time.c)
static int pose_debug; // diagnostics: requested pose times vs the predicted display time (pose_time.c)
static int layer_fix = 1;
static int mutable_fix = 0;
static int swap_eyes = 0;
static int passthrough_emul = 1;
static int alpha_blend_emul = 1; // alpha_blend_vk.c: honour XR_FB_composition_layer_alpha_blend with passthrough
static int respace_kick = 0;
static int strip_depth = 0;
static int flip_quads = 0;
static int flip_emul_setting = 1;
static int cylinder_strips_on = 1; // show cylinder layers as flat quad strips (cylinder_strips)
static int scene_emul; // scene emulation setting (per game)
static int scene_scan_request = 1;
static int scene_hide_bounds; // per game: the app's STAGE bounds query answers "no play area" (Demeter)
static int scene_events_via_shim; // the shim delivers room events: the layer's xrPollEvent leaves them alone
static float scene_height = 2.5f; // scene emulation ceiling height (m)
static float scene_width, scene_depth; // optional room size override (m)
static int64_t xr_time_offset; // XrTime minus CLOCK_MONOTONIC ns, measured at xrWaitFrame
static int xr_time_calibrated;
static int runtime_has_image_layout; // runtime supports XR_FB_composition_layer_image_layout natively
// Layer types whose extension the runtime lacks (dropped at xrCreateInstance) are removed from frames.
static int no_equirect, no_equirect2, no_cylinder, no_cube;
static int runtime_has_fb_passthrough, emulate_passthrough, alpha_blend_failed;
static int controller_models; // serve Frame controller models via XR_FB_render_model (render_model.c)
// Per-game settings, all off by default (session_fixes.c, layer_emul_gl.c).
static int sync_guard; // xrSyncActions one at a time with xrPollEvent, paused briefly after focus returns
static int profile_remap = 1; // Meta's newer controller profiles (rejected by the Frame) -> oculus/touch_controller
static int proximity_emul; // finger proximity from capacitive touch: 1 thumb, 2 thumb + index (session_fixes.c)
static int runtime_has_proximity; // the runtime has XR_FB_touch_controller_proximity itself
static int runtime_has_detached_controllers; // the runtime has XR_META_detached_controllers (detached_controllers.c)
static int layer_debug; // diagnostics: layers, swapchains, session states, spaces, aim/grip, refresh rates
static int input_diag; // diagnostics: controller profiles, bindings, missing functions, failing input calls
static int stable_local; // keep every LOCAL space the app creates on the session-start origin
static int focus_hold = 1; // hide brief focus dips once the session has been focused for a while
static float focus_hold_ms = 5000; // longest focus dip focus_hold hides
static float haptic_scale = 1.0f; // vibration strength (0-1), all vibration types (xrApplyHapticFeedback)
static float aim_pitch, aim_yaw, aim_forward; // aim pose correction (degrees, degrees, metres)
static float refresh_rate; // requested display refresh rate (Hz), 0 = the app's choice
static int equirect_emul; // show 360 layers as cube faces (GLES)
static int equirect_face = 1536; // max cube map face size (px)
static int equirect_res = 1536, equirect_res_set; // projection image size per eye (px)
static int equirect_flip; // source orientation fix: 1 upside down, 2 mirrored, 4 turned 180 degrees
static float equirect_fps = 60; // max redraws per second per 360 layer (0 = no limit)
static int equirect_stereo; // 0 auto (mono if the layer limit can't hold both eyes), 1 stereo, 2 mono
static XrTime last_predicted_time;
static void render_model_init(const char *package);
#include "headset_model.c"
#include "quest_buttons.c"
static void read_settings(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return;
char line[256];
float value;
while (fgets(line, sizeof(line), f)) {
headset_model_setting(line);
quest_buttons_setting(line);
if (sscanf(line, "scale=%f", &value) == 1 && value >= 0.5f && value <= 2.0f) scale = value;
if (sscanf(line, "foveation_fix=%f", &value) == 1) foveation_fix = value != 0;
if (sscanf(line, "foveation_storage_fix=%f", &value) == 1) foveation_storage_fix = value != 0;
if (sscanf(line, "foveation_eye=%f", &value) == 1) foveation_eye = value != 0;
if (sscanf(line, "hide_fb_foveation=%f", &value) == 1) hide_fb_foveation = value != 0;
if (sscanf(line, "hide_space_warp=%f", &value) == 1) hide_space_warp = value != 0;
if (sscanf(line, "strip_color_bias=%f", &value) == 1) strip_color_bias = (int)value;
if (sscanf(line, "fade_quad=%f", &value) == 1) fade_quad = value > 0 ? (int)value : 0;
if (sscanf(line, "frame_balance=%f", &value) == 1) frame_balance = value != 0;
if (sscanf(line, "force_render=%f", &value) == 1) force_render = value != 0;
if (sscanf(line, "snapshot=%f", &value) == 1) snapshot = value > 0 ? (int)value : 0;
if (sscanf(line, "flip_dump=%f", &value) == 1) flip_dump = value > 0 ? (int)value : 0;
if (sscanf(line, "gl_error_clear=%f", &value) == 1) gl_error_clear = value != 0;
if (sscanf(line, "controller_fix=%f", &value) == 1) controller_fix = value != 0;
if (sscanf(line, "swapchain_fix=%f", &value) == 1) swapchain_fix = value != 0;
if (sscanf(line, "cube_quads=%f", &value) == 1) cube_quads = value != 0;
if (sscanf(line, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
if (sscanf(line, "fixed_resolution=%f", &value) == 1) fixed_resolution = value != 0;
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
if (sscanf(line, "swapchain_pool=%f", &value) == 1) swapchain_pool = value != 0;
if (sscanf(line, "fb_color_space=%f", &value) == 1) fb_color_space = value != 0;
if (sscanf(line, "action_dedup=%f", &value) == 1) action_dedup = value != 0;
if (sscanf(line, "pose_consistency=%f", &value) == 1) pose_consistency = value != 0;
if (sscanf(line, "pose_debug=%f", &value) == 1) pose_debug = value != 0;
if (sscanf(line, "pose_time_fix=%f", &value) == 1) pose_time_fix = value != 0;
if (sscanf(line, "layer_fix=%f", &value) == 1) layer_fix = value != 0;
if (sscanf(line, "mutable_fix=%f", &value) == 1) mutable_fix = value != 0;
if (sscanf(line, "swap_eyes=%f", &value) == 1) swap_eyes = value != 0;
if (sscanf(line, "passthrough_emul=%f", &value) == 1) passthrough_emul = value != 0;
if (sscanf(line, "alpha_blend_emul=%f", &value) == 1) alpha_blend_emul = value != 0;
if (sscanf(line, "respace_kick=%f", &value) == 1) respace_kick = value != 0;
if (sscanf(line, "strip_depth=%f", &value) == 1) strip_depth = value != 0;
if (sscanf(line, "flip_quads=%f", &value) == 1) flip_quads = value != 0;
if (sscanf(line, "flip_emul=%f", &value) == 1) flip_emul_setting = value != 0;
if (sscanf(line, "cylinder_strips=%f", &value) == 1) cylinder_strips_on = value != 0;
if (sscanf(line, "scene_emul=%f", &value) == 1) scene_emul = value != 0;
if (sscanf(line, "scene_scan_request=%f", &value) == 1) scene_scan_request = value != 0;
if (sscanf(line, "scene_hide_bounds=%f", &value) == 1) scene_hide_bounds = value != 0;
if (sscanf(line, "scene_height=%f", &value) == 1 && value > 1.5f && value < 5.0f) scene_height = value;
if (sscanf(line, "scene_width=%f", &value) == 1 && value > 0.5f && value < 20.0f) scene_width = value;
if (sscanf(line, "scene_depth=%f", &value) == 1 && value > 0.5f && value < 20.0f) scene_depth = value;
if (sscanf(line, "controller_models=%f", &value) == 1) controller_models = value != 0;
if (sscanf(line, "layer_debug=%f", &value) == 1) layer_debug = value != 0;
if (sscanf(line, "input_diag=%f", &value) == 1) input_diag = value != 0;
if (sscanf(line, "surface_emul=%f", &value) == 1) surface_emul = value != 0;
if (sscanf(line, "surface_native=%f", &value) == 1) surface_native = value != 0;
if (sscanf(line, "eye_debug=%f", &value) == 1) eye_debug = value != 0;
if (sscanf(line, "release_wait=%f", &value) == 1) release_wait = (int)value;
if (sscanf(line, "profile_remap=%f", &value) == 1) profile_remap = value != 0;
if (sscanf(line, "proximity_emul=%f", &value) == 1 && value >= 0 && value <= 2) proximity_emul = (int)value;
if (sscanf(line, "sync_guard=%f", &value) == 1) sync_guard = value != 0;
if (sscanf(line, "stable_local=%f", &value) == 1) stable_local = value != 0;
if (sscanf(line, "focus_hold=%f", &value) == 1) focus_hold = value != 0;
if (sscanf(line, "focus_hold_ms=%f", &value) == 1 && value >= 100 && value <= 5000) focus_hold_ms = value;
if (sscanf(line, "haptic_scale=%f", &value) == 1 && value >= 0 && value <= 1) haptic_scale = value;
if (sscanf(line, "aim_pitch=%f", &value) == 1 && fabsf(value) <= 90) aim_pitch = value;
if (sscanf(line, "aim_yaw=%f", &value) == 1 && fabsf(value) <= 90) aim_yaw = value;
if (sscanf(line, "aim_forward=%f", &value) == 1 && fabsf(value) <= 0.5f) aim_forward = value;
if (sscanf(line, "refresh_rate=%f", &value) == 1 && (value == 0 || (value >= 60 && value <= 144))) refresh_rate = value;
if (sscanf(line, "equirect_emul=%f", &value) == 1) equirect_emul = value != 0;
if (sscanf(line, "equirect_face=%f", &value) == 1 && value >= 256 && value <= 2730) equirect_face = (int)value;
if (sscanf(line, "equirect_res=%f", &value) == 1 && value >= 512 && value <= 4096) {equirect_res = (int)value;equirect_res_set=1;}
if (sscanf(line, "equirect_flip=%f", &value) == 1 && value >= 0 && value <= 7) equirect_flip = (int)value;
if (sscanf(line, "equirect_fps=%f", &value) == 1 && value >= 0 && value <= 144) equirect_fps = value;
if (sscanf(line, "equirect_stereo=%f", &value) == 1 && value >= 0 && value <= 2) equirect_stereo = (int)value;
}
fclose(f);
LOG("settings read from %s", path);
}
static void initialize(void) {
Dl_info self;
char path[4096];
if (!dladdr((void *)&initialize, &self) || !self.dli_fname) return;
snprintf(path, sizeof(path), "%s", self.dli_fname);
char *slash = strrchr(path, '/');
if (!slash) return;
size_t room = sizeof(path) - (size_t)(slash + 1 - path);
snprintf(slash + 1, room, "libframe_settings.so");
read_settings(path);
snprintf(slash + 1, room, "libopenxr_loader_original.so");
loader = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (loader) next_gipa = (PFN_xrGetInstanceProcAddr)dlsym(loader, "xrGetInstanceProcAddr");
char process[256] = {0};
FILE *f = fopen("/proc/self/cmdline", "r");
if (f) {
size_t n = fread(process, 1, sizeof(process) - 1, f);
process[n] = 0;
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);
read_settings(path);
}
const char *env = getenv("FRAMEBRIDGE_CONFIG");
if (env && *env) read_settings(env);
if ((snapshot || flip_dump) && *process && !strchr(process, '/')) {
snprintf(snap_dir, sizeof(snap_dir), "/sdcard/Android/data/%s/files", process);
LOG("per-game: snapshot every %d s to %s/fb_snap_N.ppm", snapshot, snap_dir);
}
if (eye_debug && *process && !strchr(process, '/')) {
snprintf(path, sizeof(path), "/sdcard/Android/data/%s/files/framebridge.log", process);
log_file = fopen(path, "a");
}
if (input_diag) LOG("per-game: input_diag=1 (controller-input diagnostics)");
if (proximity_emul) LOG("per-game: proximity_emul=%d (finger proximity from capacitive touch: %s)", proximity_emul,
proximity_emul > 1 ? "thumb + index" : "thumb");
if (fixed_resolution) LOG("per-game: fixed_resolution=1 (full swapchain recommendations)");
if (hide_fb_foveation) LOG("per-game: hide_fb_foveation=1 (Quest Vulkan foveation suppressed)");
if (hide_space_warp) LOG("per-game: hide_space_warp=1 (XR_FB_space_warp hidden, space warp info removed)");
if (strip_color_bias) LOG("per-game: strip_color_bias=%d (layer color scale/bias removed)", strip_color_bias);
if (fade_quad) LOG("per-game: fade_quad=%d (colour-scale fades drawn as a black quad%s)", fade_quad,
fade_quad > 1 ? "; the layer's own scale removed" : "");
if (frame_balance) LOG("per-game: frame_balance=1 (an open frame is ended before the next one begins)");
if (force_render) LOG("per-game: force_render=1 (xrWaitFrame reports shouldRender=TRUE; diagnostics)");
if (eye_debug || release_wait) LOG("per-game: eye_debug=%d release_wait=%d log_file=%s", eye_debug, release_wait,
log_file ? path : "none");
if (controller_models && *process && !strchr(process, '/')) render_model_init(process);
LOG("scale=%.2f foveation_fix=%d controller_fix=%d swapchain_fix=%d layer_fix=%d mutable_fix=%d swap_eyes=%d passthrough_emul=%d respace_kick=%d flip_quads=%d controller_models=%d loader=%s",
scale, foveation_fix, controller_fix, swapchain_fix, layer_fix, mutable_fix, swap_eyes, passthrough_emul,
respace_kick, flip_quads, controller_models, next_gipa ? "OK" : (dlerror() ?: "FAILED"));
if (layer_debug || stable_local || focus_hold || aim_pitch || aim_yaw || aim_forward || refresh_rate || equirect_emul)
LOG("per-game: layer_debug=%d stable_local=%d focus_hold=%d aim=%.1f/%.1f/%.3f refresh_rate=%.0f equirect_emul=%d "
"(face %d, res %d, flip %d, fps %.0f, stereo %d)", layer_debug, stable_local, focus_hold, aim_pitch, aim_yaw,
aim_forward, refresh_rate, equirect_emul, equirect_face, equirect_res, equirect_flip, equirect_fps,
equirect_stereo);
}
static PFN_xrVoidFunction lookup(XrInstance instance, const char *name) {
pthread_once(&init_once, initialize);
PFN_xrVoidFunction fn = NULL;
if (next_gipa) next_gipa(instance, name, &fn);
if (!fn && loader) fn = (PFN_xrVoidFunction)dlsym(loader, name);
return fn;
}
#include "scene_emu.c"
#include "render_model.c"
#include "color_space.c"
XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info, XrInstance *instance) {
PFN_xrCreateInstance fn = (PFN_xrCreateInstance)lookup(XR_NULL_HANDLE, "xrCreateInstance");
if (!fn) return XR_ERROR_INITIALIZATION_FAILED;
if (!info) return XR_ERROR_VALIDATION_FAILURE;
hide_fb_foveation_vulkan = hide_fb_foveation && fb_instance_uses_vulkan(info);
int chained = 0, enabled = 0;
for (const XrBaseInStructure *p = (const XrBaseInStructure *)info->next; p; p = p->next)
if (p->type == XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR) {
chained = 1;
android_vm = ((const struct { XrStructureType type; const void *next; void *vm; void *activity; } *)p)->vm;
}
for (uint32_t i = 0; i < info->enabledExtensionCount; ++i)
if (!strcmp(info->enabledExtensionNames[i], "XR_KHR_android_create_instance")) enabled = 1;
// Runtime's real extension list (bypassing our own filtered hook), used to drop requested
// extensions the Frame runtime lacks (e.g. XR_FB_passthrough) instead of failing outright.
PFN_xrEnumerateInstanceExtensionProperties enum_ext = (PFN_xrEnumerateInstanceExtensionProperties)lookup(
XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
uint32_t available_count = 0;
XrExtensionProperties *available = NULL;
int extensions_known = enum_ext && XR_SUCCEEDED(enum_ext(NULL, 0, &available_count, NULL));
if (extensions_known && available_count) {
available = calloc(available_count, sizeof(*available));
if (available) {
for (uint32_t i = 0; i < available_count; ++i) available[i].type = XR_TYPE_EXTENSION_PROPERTIES;
if (XR_FAILED(enum_ext(NULL, available_count, &available_count, available))) {
free(available);
available = NULL;
}
}
if (!available) extensions_known = 0;
}
XrInstanceCreateInfo fixed = *info;
const char **names = calloc(info->enabledExtensionCount + 8, sizeof(*names));
if (!names) { free(available); return XR_ERROR_OUT_OF_MEMORY; }
int requested_color_space = 0;
uint32_t kept = 0;
for (uint32_t i = 0; i < info->enabledExtensionCount; ++i) {
const char *ext = info->enabledExtensionNames[i];
int supported = !available; // if we could not enumerate, pass everything through
if (extensions_known && !strcmp(ext, XR_FB_COLOR_SPACE_EXTENSION_NAME)) supported = 0;
for (uint32_t j = 0; available && j < available_count && !supported; ++j)
supported = !strcmp(available[j].extensionName, ext);
if (hide_fb_foveation_vulkan && fb_is_foveation_extension(ext)) {
LOG("hide_fb_foveation: %s not enabled", ext);
continue;
}
if (hide_space_warp && !strcmp(ext, "XR_FB_space_warp")) {
LOG("hide_space_warp: XR_FB_space_warp not enabled");
continue;
}
if (supported) names[kept++] = ext;
else if (!strcmp(ext, XR_FB_COLOR_SPACE_EXTENSION_NAME)) {
requested_color_space = 1;
} else if (scene_emul && is_scene_extension(ext)) {
if (!emulate_scene) LOG("emulating Meta scene/spatial-entity extensions from room file or guardian bounds");
emulate_scene = 1;
if (!strcmp(ext, "XR_META_spatial_entity_mesh")) scene_app_mesh = 1;
} else if (controller_models && render_models_available && !strcmp(ext, XR_FB_RENDER_MODEL_EXTENSION_NAME)) {
emulate_render_model = 1;
LOG("emulating XR_FB_render_model with Steam Frame controller models");
} else if (passthrough_emul && !strcmp(ext, "XR_FB_passthrough")) {
emulate_passthrough = 1;
LOG("emulating XR_FB_passthrough with alpha-blended environment");
} else LOG("dropped unsupported extension %s", ext);
}
if (chained && !enabled) {
names[kept++] = "XR_KHR_android_create_instance";
LOG("added %s", "XR_KHR_android_create_instance");
}
int frame_enabled = quest_buttons_extensions(available, available_count, names, &kept);
{ // no graphics API extension at all: Meta's runtime still accepts a GLES session, the Frame's doesn't
// (Lambda1VR's TBXR enables only XR_EXT_local_floor). Add GLES, which every such app uses, if offered.
int graphics = 0, gles_offered = 0;
for (uint32_t i = 0; i < kept; ++i)
graphics |= !strcmp(names[i], "XR_KHR_opengl_es_enable") || !strcmp(names[i], "XR_KHR_vulkan_enable") ||
!strcmp(names[i], "XR_KHR_vulkan_enable2");
for (uint32_t j = 0; available && j < available_count; ++j)
gles_offered |= !strcmp(available[j].extensionName, "XR_KHR_opengl_es_enable");
if (!graphics && gles_offered) {
names[kept++] = "XR_KHR_opengl_es_enable";
LOG("added XR_KHR_opengl_es_enable (the app enabled no graphics API extension)");
}
}
if (refresh_rate > 0) { // the refresh_rate setting needs XR_FB_display_refresh_rate even if the app doesn't use it
int requested = 0, offered = 0;
for (uint32_t i = 0; i < kept; ++i) requested |= !strcmp(names[i], "XR_FB_display_refresh_rate");
for (uint32_t j = 0; available && j < available_count; ++j) offered |= !strcmp(available[j].extensionName, "XR_FB_display_refresh_rate");
if (!requested && offered) { names[kept++] = "XR_FB_display_refresh_rate"; LOG("added XR_FB_display_refresh_rate (refresh_rate)"); }
}
foveation_eye_enabled = 0;
if (foveation_eye && !foveation_fix && !hide_fb_foveation_vulkan) {
int fb = 0, config = 0, offered = 0;
for (uint32_t i = 0; i < kept; ++i) {
fb |= !strcmp(names[i], "XR_FB_foveation");
config |= !strcmp(names[i], "XR_FB_foveation_configuration");
foveation_eye_enabled |= !strcmp(names[i], "XR_META_foveation_eye_tracked");
}
for (uint32_t i = 0; available && i < available_count; ++i)
offered |= !strcmp(available[i].extensionName, "XR_META_foveation_eye_tracked");
if (!foveation_eye_enabled && fb && config && offered) {
names[kept++] = "XR_META_foveation_eye_tracked";
foveation_eye_enabled = 1;
}
}
// Adapter-owned video outputs need an explicit full-rate profile even when
// the shared injection layer enables foveation for application swapchains.
if (surface_emul && !hide_fb_foveation_vulkan) {
const char *video_extensions[] = {"XR_FB_foveation", "XR_FB_foveation_configuration",
"XR_FB_swapchain_update_state"};
for (unsigned e = 0; e < 3; ++e) {
int requested = 0, offered = 0;
for (uint32_t i = 0; i < kept; ++i) requested |= !strcmp(names[i], video_extensions[e]);
for (uint32_t i = 0; available && i < available_count; ++i)
offered |= !strcmp(available[i].extensionName, video_extensions[e]);
if (offered && !requested) names[kept++] = video_extensions[e];
}
}
fixed.enabledExtensionNames = names;
fixed.enabledExtensionCount = kept;
{ // what the runtime gets (diagnostics: e.g. a graphics extension the app forgot)
char list[1024] = "";
for (uint32_t i = 0; i < kept; ++i) {
size_t len = strlen(list);
snprintf(list + len, sizeof list - len, "%s%s", i ? " " : "", names[i] + (strncmp(names[i], "XR_", 3) ? 0 : 3));
}
LOG("xrCreateInstance with %u extension(s): %s", kept, list);
}
for (uint32_t j = 0; available && j < available_count; ++j)
if (!strcmp(available[j].extensionName, "XR_FB_composition_layer_image_layout")) runtime_has_image_layout = 1;
else if (!strcmp(available[j].extensionName, "XR_FB_touch_controller_proximity")) runtime_has_proximity = 1;
else if (!strcmp(available[j].extensionName, "XR_META_detached_controllers"))
runtime_has_detached_controllers = 1;
// Layer types are only valid if their extension ends up enabled; some apps submit them regardless.
int has_equirect = 0, has_equirect2 = 0, has_cylinder = 0, has_cube = 0;
for (uint32_t i = 0; i < kept; ++i) {
has_equirect |= !strcmp(names[i], "XR_KHR_composition_layer_equirect");
has_equirect2 |= !strcmp(names[i], "XR_KHR_composition_layer_equirect2");
has_cylinder |= !strcmp(names[i], "XR_KHR_composition_layer_cylinder");
has_cube |= !strcmp(names[i], "XR_KHR_composition_layer_cube");
}
no_equirect = !has_equirect; no_equirect2 = !has_equirect2; no_cylinder = !has_cylinder; no_cube = !has_cube;
free(available);
mx_audio_initialize(); mx_batman_geometry_initialize();
XrResult result = fn(&fixed, instance);
free(names);
LOG("xrCreateInstance result=%d", result);
emulate_color_space = fb_color_space && XR_SUCCEEDED(result) && requested_color_space;
if (emulate_color_space) {
static int logged;
if (!logged) {
logged = 1;
LOG("color_space: emulating XR_FB_color_space; REC709 SDR compatibility, no colour conversion");
}
}
if (XR_SUCCEEDED(result)) {
pthread_mutex_lock(&bindings_lock);
active_instance = *instance;
reset_bindings(frame_enabled);
pthread_mutex_unlock(&bindings_lock);
mx_audio_initialize();
mx_batman_geometry_initialize();
}
return result;
}
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)lookup(instance, "xrEnumerateViewConfigurationViews");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result = fn(instance, system, type, capacity, count, views);
static int runtime_logged;
if (XR_SUCCEEDED(result) && views && count && *count && capacity && !runtime_logged++)
// the runtime's own size, before scale (FramePort's agent reads it to keep games' saved resolutions in step)
LOG("runtime recommended eye size %ux%u (max %ux%u, scale %.2f)", views[0].recommendedImageRectWidth,
views[0].recommendedImageRectHeight, views[0].maxImageRectWidth, views[0].maxImageRectHeight, scale);
if (XR_SUCCEEDED(result) && views && count && scale != 1.0f) {
for (uint32_t i = 0; i < *count && i < capacity; ++i) {
uint32_t w = (uint32_t)(views[i].recommendedImageRectWidth * scale + 0.5f);
uint32_t h = (uint32_t)(views[i].recommendedImageRectHeight * scale + 0.5f);
if (w > views[i].maxImageRectWidth) w = views[i].maxImageRectWidth;
if (h > views[i].maxImageRectHeight) h = views[i].maxImageRectHeight;
views[i].recommendedImageRectWidth = w ? w : 1;
views[i].recommendedImageRectHeight = h ? h : 1;
LOG("view %u recommended %ux%u", i, views[i].recommendedImageRectWidth, views[i].recommendedImageRectHeight);
}
}
return result;
}
// Closest runtime-supported swapchain format for one it rejected; 0 if none.
static int64_t pick_format(XrSession session, int64_t wanted) {
PFN_xrEnumerateSwapchainFormats fn =
(PFN_xrEnumerateSwapchainFormats)lookup(active_instance, "xrEnumerateSwapchainFormats");
uint32_t count = 0;
if (!fn || XR_FAILED(fn(session, 0, &count, NULL)) || !count) return 0;
int64_t *formats = calloc(count, sizeof(*formats));
if (!formats || XR_FAILED(fn(session, count, &count, formats))) { free(formats); return 0; }
static int logged;
if (!logged++)
for (uint32_t i = 0; i < count; ++i) LOG("runtime swapchain format[%u]=%lld", i, (long long)formats[i]);
// Same channel layout, preferring the sRGB variant the runtime accepts.
static const int64_t equivalents[][2] = {
{0x8058, 0x8C43}, // GL_RGBA8 -> GL_SRGB8_ALPHA8
{0x8051, 0x8C43}, // GL_RGB8 -> GL_SRGB8_ALPHA8
{0x8C41, 0x8C43}, // GL_SRGB8 -> GL_SRGB8_ALPHA8
{37, 43}, // VK_FORMAT_R8G8B8A8_UNORM -> _SRGB
{44, 50}, // VK_FORMAT_B8G8R8A8_UNORM -> _SRGB
{50, 43}, // VK_FORMAT_B8G8R8A8_SRGB -> R8G8B8A8_SRGB
};
int64_t chosen = 0;
for (size_t e = 0; e < sizeof(equivalents) / sizeof(equivalents[0]) && !chosen; ++e)
if (equivalents[e][0] == wanted)
for (uint32_t i = 0; i < count && !chosen; ++i)
if (formats[i] == equivalents[e][1]) chosen = formats[i];
if (!chosen) chosen = formats[0]; // runtime lists its preferred format first
free(formats);
return chosen;
}
// Known app swapchains, including pooled images rewritten into runtime atlases before submission.
static pthread_mutex_t swapchains_lock = PTHREAD_MUTEX_INITIALIZER;
static XrSwapchain *swapchains;
static size_t swapchain_count, swapchain_capacity;
static void remember_swapchain(XrSwapchain handle) {
pthread_mutex_lock(&swapchains_lock);
if (swapchain_count == swapchain_capacity) {
size_t capacity = swapchain_capacity ? swapchain_capacity * 2 : 64;
XrSwapchain *grown = realloc(swapchains, capacity * sizeof(*grown));
if (grown) { swapchains = grown; swapchain_capacity = capacity; }
}
if (swapchain_count < swapchain_capacity) swapchains[swapchain_count++] = handle;
pthread_mutex_unlock(&swapchains_lock);
}
// Swapchain sizes, to keep submitted image rects inside them (rect_clamp).
#define MAX_SIZES 256
static struct { XrSwapchain handle; uint32_t w, h; } sizes[MAX_SIZES];
static pthread_mutex_t sizes_lock = PTHREAD_MUTEX_INITIALIZER;
static void note_swapchain_size(XrSwapchain handle, uint32_t w, uint32_t h) {
pthread_mutex_lock(&sizes_lock);
int slot = -1;
for (int i = 0; i < MAX_SIZES; ++i) {
if (sizes[i].handle == handle) { slot = i; break; }
if (slot < 0 && sizes[i].handle == XR_NULL_HANDLE) slot = i;
}
if (slot >= 0) { sizes[slot].handle = handle; sizes[slot].w = w; sizes[slot].h = h; }
pthread_mutex_unlock(&sizes_lock);
}
static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
int found = 0;
pthread_mutex_lock(&sizes_lock);
for (int i = 0; i < MAX_SIZES && !found; ++i)
if (sizes[i].handle == handle && handle != XR_NULL_HANDLE) { *w = sizes[i].w; *h = sizes[i].h; found = 1; }
pthread_mutex_unlock(&sizes_lock);
return found;
}
#include "fixed_resolution.h"
static XRAPI_ATTR XrResult XRAPI_CALL fixed_recommended_resolution(XrSession session,
const XrRecommendedLayerResolutionGetInfoMETA *info, XrRecommendedLayerResolutionMETA *out) {
PFN_xrGetRecommendedLayerResolutionMETA fn = (PFN_xrGetRecommendedLayerResolutionMETA)
lookup(active_instance, "xrGetRecommendedLayerResolutionMETA");
if (!info || !out) return XR_ERROR_VALIDATION_FAILURE;
XrExtent2Di full;
if (!fixed_layer_size(info->layer, &full))
return fn ? fn(session, info, out) : XR_ERROR_FUNCTION_UNSUPPORTED;
// Read the runtime recommendation only for diagnostics. The override also works
// when the runtime lacks this function (OVRPort advertises it itself).
XrRecommendedLayerResolutionMETA old = {XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
XrResult result = fn ? fn(session, info, &old) : XR_ERROR_FUNCTION_UNSUPPORTED;
static unsigned logged;
if (logged++ < 5) LOG("fixed_resolution: recommended %dx%d -> %dx%d (runtime=%d)",
old.recommendedImageDimensions.width, old.recommendedImageDimensions.height,
full.width, full.height, result);
out->recommendedImageDimensions = full;
out->isValid = XR_TRUE;
return XR_SUCCESS;
}
// Clamp `r` to a w x h image; returns whether it changed (2 = it was empty and now covers the whole image).
static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
XrRect2Di c = *r;
// An empty rect (BattleTalent's first frames after Unity switched its MSAA level: 0,0 0x0, GitHub #293) is
// rejected the same way, and the game stops submitting after two rejections: show the whole image instead.
if (c.extent.width <= 0 || c.extent.height <= 0) {
if (!w || !h || w > INT32_MAX || h > INT32_MAX) return 0;
r->offset.x = r->offset.y = 0;
r->extent.width = (int32_t)w;
r->extent.height = (int32_t)h;
return 2;
}
if (c.offset.x < 0) { c.extent.width += c.offset.x; c.offset.x = 0; }
if (c.offset.y < 0) { c.extent.height += c.offset.y; c.offset.y = 0; }
if ((uint32_t)c.offset.x >= w || (uint32_t)c.offset.y >= h) return 0; // nothing sensible to keep: leave it
if ((int64_t)c.offset.x + c.extent.width > (int64_t)w) c.extent.width = (int32_t)(w - (uint32_t)c.offset.x);
if ((int64_t)c.offset.y + c.extent.height > (int64_t)h) c.extent.height = (int32_t)(h - (uint32_t)c.offset.y);
if (c.extent.width <= 0 || c.extent.height <= 0) return 0;
int changed = memcmp(&c, r, sizeof c) != 0;
*r = c;
return changed;
}
static void forget_swapchain(XrSwapchain handle) {
pthread_mutex_lock(&sizes_lock);
for (int i = 0; i < MAX_SIZES; ++i)
if (sizes[i].handle == handle) memset(&sizes[i], 0, sizeof sizes[i]);
pthread_mutex_unlock(&sizes_lock);
pthread_mutex_lock(&swapchains_lock);
for (size_t i = 0; i < swapchain_count; ++i)
if (swapchains[i] == handle) { swapchains[i] = swapchains[--swapchain_count]; break; }
pthread_mutex_unlock(&swapchains_lock);
}
static void flip_on_create_swapchain(XrSwapchain handle, const XrSwapchainCreateInfo *info);
static int flip_wants_transfer(const XrSwapchainCreateInfo *info);
static void clear_gl_errors(const char *where);
static void flip_on_destroy(XrSwapchain handle);
static void ab_on_destroy(XrSwapchain handle);
static void emul_on_create_swapchain(XrSwapchain handle, const XrSwapchainCreateInfo *info);
static void emul_on_destroy_swapchain(XrSwapchain handle);
static int emul_virtual_create(XrSession session, const XrSwapchainCreateInfo *info, XrSwapchain *out);
static int emul_is_virtual(XrSwapchain handle);
static void emul_virtual_destroy(XrSwapchain handle);
static XrResult emul_virtual_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
static void emul_on_acquire(XrSwapchain handle, uint32_t index);
static XrResult standin_after_failure(XrSession session, const XrSwapchainCreateInfo *info, XrSwapchain *out, XrResult result);
static int is_standin(XrSwapchain handle);
static void standin_destroy(XrSwapchain handle);
static void cube_outputs_destroy(void);
static void cube_session_destroy(XrSession session);
static XrResult standin_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int pool_find(XrSwapchain h);
static int pool_should_create(XrSession session);
static void pool_note_runtime(XrSession session, XrSwapchain handle);
static void pool_forget_runtime(XrSwapchain handle);
static int pool_create(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited);
static void pool_destroy(int i);
static void pool_cleanup(XrSession session);
static XrResult pool_cycle(int i, int op, uint32_t *index);
static XrResult pool_enumerate(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int known_swapchain(XrSwapchain handle) {
int found = 0;
pthread_mutex_lock(&swapchains_lock);
for (size_t i = 0; i < swapchain_count && !found; ++i) found = swapchains[i] == handle;
pthread_mutex_unlock(&swapchains_lock);
return found;
}
XRAPI_ATTR XrResult XRAPI_CALL xrCreateFoveationProfileFB(XrSession session,
const XrFoveationProfileCreateInfoFB *info, XrFoveationProfileFB *profile) {
if (hide_fb_foveation_vulkan) return XR_ERROR_FEATURE_UNSUPPORTED;
PFN_xrCreateFoveationProfileFB fn = (PFN_xrCreateFoveationProfileFB)lookup(active_instance, "xrCreateFoveationProfileFB");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (!info) return XR_ERROR_VALIDATION_FAILURE;
const XrBaseInStructure *p = info->next;
if (foveation_storage_fix || foveation_eye) LOG("foveation profile create: next_type=%d", p ? p->type : 0);
if (!p || p->type == XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB) {
XrFoveationLevelProfileCreateInfoFB level = {XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB};
if (p) level = *(const XrFoveationLevelProfileCreateInfoFB *)p;
if (foveation_storage_fix || foveation_eye)
LOG("foveation profile: level=%d dynamic=%d eye_requested=%d supported=%d", level.level, level.dynamic,
foveation_eye, foveation_eye_supported);
if (foveation_eye && !foveation_fix && foveation_eye_supported && !level.next && level.level != XR_FOVEATION_LEVEL_NONE_FB) {
XrFoveationEyeTrackedProfileCreateInfoMETA eye = {XR_TYPE_FOVEATION_EYE_TRACKED_PROFILE_CREATE_INFO_META};
XrFoveationProfileCreateInfoFB fixed = *info;
level.next = &eye;
fixed.next = &level;
XrResult result = fn(session, &fixed, profile);
LOG("foveation profile: eye-tracked upgrade result=%d", result);
if (XR_SUCCEEDED(result)) return result;
}
}
return fn(session, info, profile);
}
XRAPI_ATTR XrResult XRAPI_CALL xrGetFoveationEyeTrackedStateMETA(XrSession session, XrFoveationEyeTrackedStateMETA *state) {
if (hide_fb_foveation_vulkan) return XR_ERROR_FEATURE_UNSUPPORTED;
PFN_xrGetFoveationEyeTrackedStateMETA fn = (PFN_xrGetFoveationEyeTrackedStateMETA)lookup(active_instance, "xrGetFoveationEyeTrackedStateMETA");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result = fn(session, state);
static unsigned queries;
if (foveation_eye && state && XR_SUCCEEDED(result) && (++queries % 240 == 1))
LOG("foveation gaze consumed by app: valid=%llu left=%.3f,%.3f right=%.3f,%.3f", (unsigned long long)state->flags,
state->foveationCenter[0].x, state->foveationCenter[0].y, state->foveationCenter[1].x, state->foveationCenter[1].y);
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwapchainCreateInfo *info,
XrSwapchain *swapchain) {
PFN_xrCreateSwapchain fn = (PFN_xrCreateSwapchain)lookup(active_instance, "xrCreateSwapchain");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (!info) return XR_ERROR_VALIDATION_FAILURE;
XrSwapchainCreateInfo fixed = *info;
FbStorageNode storage_nodes[16];
// Also with foveation_fix: Valve's injected FDM (device.foveation) stores a SUBSAMPLED image's coarse bins
// unscaled, and the compositor reads them as garbage around the sharp area (GitHub #303, Medieval Dynasty).
if (!hide_fb_foveation_vulkan && foveation_storage_fix) {
int changed = fb_foveation_storage(&fixed, storage_nodes);
LOG("foveation_storage_fix: shared allocation %s", changed > 0 ? "SUBSAMPLED cleared" :
changed < 0 ? "unchanged (unsupported chain prefix)" : "already ordinary or no META node");
}
if (hide_fb_foveation_vulkan) {
int stripped = fb_hide_swapchain_foveation(&fixed, storage_nodes);
if (stripped < 0) return XR_ERROR_VALIDATION_FAILURE;
if (stripped) LOG("hide_fb_foveation: removed swapchain foveation create info");
}
if (foveation_fix) {
// Drop leading foveation structs; the chain is const so only a prefix can be skipped.
const XrBaseInStructure *p = (const XrBaseInStructure *)fixed.next;
while (p && p->type == XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB) p = p->next;
fixed.next = p;
}
if (mutable_fix) fixed.usageFlags |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT;
// Internal native-video composition snapshots the owned stereo scene before
// release. Other games and UI/depth/MSAA swapchains keep their original usage.
if(surface_native && fixed.arraySize==2 && fixed.sampleCount==1 &&
(fixed.usageFlags&XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT))
fixed.usageFlags |= XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT;
// in-place flips of upside-down quads (flip_vk.c) blit from and back into these images
if (flip_wants_transfer(&fixed))
fixed.usageFlags |= XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
if (equirect_emul && swapchain && emul_virtual_create(session, &fixed, swapchain)) {
remember_swapchain(*swapchain);
emul_on_create_swapchain(*swapchain, &fixed);
snap_on_create(*swapchain, &fixed);
return XR_SUCCESS;
}
// Start before the session's ~15-slot cap; leave headroom for 360/video outputs and a second atlas.
if (pool_should_create(session) && pool_create(session, &fixed, swapchain, 0)) return XR_SUCCESS;
clear_gl_errors("xrCreateSwapchain");
XrResult result = fn(session, &fixed, swapchain);
LOG("xrCreateSwapchain %ux%u format=%lld samples=%u array=%u faces=%u usage=0x%llx flags=0x%llx result=%d handle=%p",
fixed.width, fixed.height, (long long)fixed.format, fixed.sampleCount, fixed.arraySize, fixed.faceCount,
(unsigned long long)fixed.usageFlags, (unsigned long long)fixed.createFlags, result,
(void *)(uintptr_t)(XR_SUCCEEDED(result) && swapchain ? *swapchain : 0));
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
emul_on_create_swapchain(*swapchain, &fixed);
snap_on_create(*swapchain, &fixed);
return result;
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
if (!swapchain_fix) return standin_after_failure(session, &fixed, swapchain, result);
// Frame's runtime rejects some GLES formats (GL_RGBA8) and MSAA swapchains (overport #71).
if (fixed.sampleCount > 1) {
fixed.sampleCount = 1;
result = fn(session, &fixed, swapchain);
LOG(" retry samples=1 result=%d", result);
}
if (result == XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED) {
int64_t replacement = pick_format(session, fixed.format);
if (replacement && replacement != fixed.format) {
fixed.format = replacement;
result = fn(session, &fixed, swapchain);
LOG(" retry format=%lld result=%d", (long long)replacement, result);
}
}
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
emul_on_create_swapchain(*swapchain, &fixed);
snap_on_create(*swapchain, &fixed);
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
return standin_after_failure(session, &fixed, swapchain, result);
}
XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
PFN_xrDestroySwapchain fn = (PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (layer_debug) {
static int logged;
if (logged++ < 200) LOG("layer_debug: xrDestroySwapchain %p", (void *)swapchain);
}
pool_forget_runtime(swapchain);
int pooled = pool_find(swapchain);
if (pooled >= 0) { pool_destroy(pooled); return XR_SUCCESS; }
if (is_standin(swapchain)) { standin_destroy(swapchain); return XR_SUCCESS; }
surf_scene_destroy(swapchain);
forget_swapchain(swapchain);
surf_on_destroy(swapchain);
ab_on_destroy(swapchain);
flip_on_destroy(swapchain);
emul_on_destroy_swapchain(swapchain);
snap_on_destroy(swapchain);
if (equirect_emul && emul_is_virtual(swapchain)) { emul_virtual_destroy(swapchain); return XR_SUCCESS; }
return fn(swapchain);
}
// ---- cylinder layers (XR_KHR_composition_layer_cylinder), which the Frame runtime lacks: shown as a few flat quads
// (chords of the arc), each showing its share of the image. The runtime composites them from the app's own image at
// full resolution, and they stay within centimetres of where the app aims its pointer at the curved surface.
static XrVector3f strip_rotate(XrQuaternionf q, XrVector3f v) { // v' = v + 2w(u x v) + 2 u x (u x v)
XrVector3f u = {q.x, q.y, q.z}, t = {2 * (u.y * v.z - u.z * v.y), 2 * (u.z * v.x - u.x * v.z), 2 * (u.x * v.y - u.y * v.x)};
return (XrVector3f){v.x + q.w * t.x + (u.y * t.z - u.z * t.y), v.y + q.w * t.y + (u.z * t.x - u.x * t.z),
v.z + q.w * t.z + (u.x * t.y - u.y * t.x)};
}
static XrQuaternionf strip_mul(XrQuaternionf a, XrQuaternionf b) {
return (XrQuaternionf){a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z};
}
// Returns the number of quads written to out (0 = not possible). budget = layers still allowed in the frame.
static int cylinder_strips(const XrCompositionLayerCylinderKHR *c, XrCompositionLayerQuad *out, int budget) {
if (c->centralAngle <= 0 || c->aspectRatio <= 0) return 0;
float r = c->radius > 0 && c->radius < 1e4f ? c->radius : 3.0f;
int n = (int)ceilf(c->centralAngle / 0.26f); // ~15 degrees per strip
if (n > 8) n = 8;
if (n > budget) n = budget;
if (n < 1) return 0;
float a = c->centralAngle / (float)n, height = r * c->centralAngle / c->aspectRatio;
const XrRect2Di rect = c->subImage.imageRect;
for (int i = 0; i < n; ++i) {
float phi = -c->centralAngle / 2 + (i + 0.5f) * a; // + = towards +X (the image's right)
XrQuaternionf yaw = {0, sinf(-phi / 2), 0, cosf(-phi / 2)};
XrPosef pose;
pose.orientation = strip_mul(c->pose.orientation, yaw);
XrVector3f d = strip_rotate(pose.orientation, (XrVector3f){0, 0, -r * cosf(a / 2)});
pose.position = (XrVector3f){c->pose.position.x + d.x, c->pose.position.y + d.y, c->pose.position.z + d.z};
int32_t x0 = rect.offset.x + (int32_t)((int64_t)rect.extent.width * i / n);
int32_t x1 = rect.offset.x + (int32_t)((int64_t)rect.extent.width * (i + 1) / n);
XrSwapchainSubImage sub = c->subImage;
sub.imageRect = (XrRect2Di){{x0, rect.offset.y}, {x1 - x0, rect.extent.height}};
out[i] = (XrCompositionLayerQuad){XR_TYPE_COMPOSITION_LAYER_QUAD, c->next, c->layerFlags, c->space,
XR_EYE_VISIBILITY_BOTH, sub, pose, {2 * r * sinf(a / 2), height}};
}
static int logged;
if (logged++ < 3) LOG("cylinder layer -> %d quad strips (r=%.2f angle=%.2f)", n, r, c->centralAngle);
return n;
}
// A layer is usable if every swapchain it references was created successfully.
// Runtime images and pooled images (rewritten after layer fixes). Adapter-served 360 pictures only feed its drawing.
static int runtime_swapchain(XrSwapchain handle) {
return known_swapchain(handle) && !(equirect_emul && emul_is_virtual(handle));
}
static int layer_usable(const XrCompositionLayerBaseHeader *layer) {
switch (layer->type) {
case XR_TYPE_COMPOSITION_LAYER_PROJECTION: {
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)layer;
for (uint32_t v = 0; v < p->viewCount; ++v)
if (!runtime_swapchain(p->views[v].subImage.swapchain)) return 0;
return 1;
}
case XR_TYPE_COMPOSITION_LAYER_QUAD: {
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
if (q->size.width <= 0.0f || q->size.height <= 0.0f) return 0;
if (q->subImage.imageRect.extent.width <= 0 || q->subImage.imageRect.extent.height <= 0) return 0;
return runtime_swapchain(q->subImage.swapchain);
}
case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR:
if (no_cylinder) return 0;
return runtime_swapchain(((const XrCompositionLayerCylinderKHR *)layer)->subImage.swapchain);
case XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR:
if (no_equirect) return 0;
return known_swapchain(((const XrCompositionLayerEquirectKHR *)layer)->subImage.swapchain);
case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR:
if (no_equirect2) return 0;
return known_swapchain(((const XrCompositionLayerEquirect2KHR *)layer)->subImage.swapchain);
case XR_TYPE_COMPOSITION_LAYER_CUBE_KHR:
if (no_cube) return 0;
return runtime_swapchain(((const XrCompositionLayerCubeKHR *)layer)->swapchain);
default:
return 1; // layer types without swapchains (e.g. passthrough) or unknown: pass through
}
}
// ---------------------------------------------------------------- XR_FB_passthrough emulation
static uint64_t next_fake_handle = 0x7f000001;
#define FAKE_HANDLE(type) ((type)(uintptr_t)__atomic_fetch_add(&next_fake_handle, 1, __ATOMIC_RELAXED))
// The app's passthrough calls, logged (each kind at most 8 times) to see where an MR game stops (NOPE: black).
static void passthrough_log(const char *what, uint64_t handle, uint32_t flags, int purpose) {
static int counts[8];
static const char *const kinds[8] = {"create", "start", "pause", "destroy", "layer create", "layer resume",
"layer pause", "layer destroy"};
for (int k = 0; k < 8; ++k)
if (!strcmp(what, kinds[k]) && counts[k]++ < 8)
LOG("passthrough: %s 0x%llx flags=0x%x purpose=%d", what, (unsigned long long)handle, flags, purpose);
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_create_passthrough(XrSession s, const XrPassthroughCreateInfoFB *i, XrPassthroughFB *out) {
(void)s; if (!out) return XR_ERROR_VALIDATION_FAILURE; *out = FAKE_HANDLE(XrPassthroughFB);
passthrough_log("create", (uint64_t)(uintptr_t)*out, i ? (uint32_t)i->flags : 0, -1);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_start(XrPassthroughFB p) {
passthrough_log("start", (uint64_t)(uintptr_t)p, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_pause(XrPassthroughFB p) {
passthrough_log("pause", (uint64_t)(uintptr_t)p, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_handle(XrPassthroughFB p) {
passthrough_log("destroy", (uint64_t)(uintptr_t)p, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_create_passthrough_layer(XrSession s, const XrPassthroughLayerCreateInfoFB *i,
XrPassthroughLayerFB *out) {
(void)s; if (!out) return XR_ERROR_VALIDATION_FAILURE; *out = FAKE_HANDLE(XrPassthroughLayerFB);
passthrough_log("layer create", (uint64_t)(uintptr_t)*out, i ? (uint32_t)i->flags : 0, i ? (int)i->purpose : -1);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_layer_resume(XrPassthroughLayerFB l) {
passthrough_log("layer resume", (uint64_t)(uintptr_t)l, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_layer_pause(XrPassthroughLayerFB l) {
passthrough_log("layer pause", (uint64_t)(uintptr_t)l, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_layer_handle(XrPassthroughLayerFB l) {
passthrough_log("layer destroy", (uint64_t)(uintptr_t)l, 0, -1); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_passthrough_layer_style(XrPassthroughLayerFB l, const XrPassthroughStyleFB *st) {
(void)l; (void)st; return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_create_geometry(XrSession s, const XrGeometryInstanceCreateInfoFB *i, XrGeometryInstanceFB *out) {
(void)s; (void)i; if (!out) return XR_ERROR_VALIDATION_FAILURE; *out = FAKE_HANDLE(XrGeometryInstanceFB); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_geometry_handle(XrGeometryInstanceFB g) { (void)g; return XR_SUCCESS; }
static XRAPI_ATTR XrResult XRAPI_CALL emu_geometry_transform(XrGeometryInstanceFB g, const XrGeometryInstanceTransformFB *t) {
(void)g; (void)t; return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_create_mesh(XrSession s, const XrTriangleMeshCreateInfoFB *i, XrTriangleMeshFB *out) {
(void)s; (void)i; if (!out) return XR_ERROR_VALIDATION_FAILURE; *out = FAKE_HANDLE(XrTriangleMeshFB); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_mesh_handle(XrTriangleMeshFB m) { (void)m; return XR_SUCCESS; }
static XRAPI_ATTR XrResult XRAPI_CALL emu_create_lut(XrPassthroughFB p, const XrPassthroughColorLutCreateInfoMETA *i,
XrPassthroughColorLutMETA *out) {
(void)p; (void)i; if (!out) return XR_ERROR_VALIDATION_FAILURE; *out = FAKE_HANDLE(XrPassthroughColorLutMETA); return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_lut_handle(XrPassthroughColorLutMETA l) { (void)l; return XR_SUCCESS; }
static XRAPI_ATTR XrResult XRAPI_CALL emu_update_lut(XrPassthroughColorLutMETA l, const XrPassthroughColorLutUpdateInfoMETA *u) {
(void)l; (void)u; return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_preferences(XrSession s, XrPassthroughPreferencesMETA *prefs) {
(void)s; if (!prefs) return XR_ERROR_VALIDATION_FAILURE; prefs->flags = 0; return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_keyboard_hands(XrPassthroughLayerFB l, const XrPassthroughKeyboardHandsIntensityFB *i) {
(void)l; (void)i; return XR_SUCCESS;
}
static PFN_xrVoidFunction passthrough_emulation(const char *name) {
if (!emulate_passthrough) return NULL;
static const struct { const char *name; PFN_xrVoidFunction fn; } table[] = {
{"xrCreatePassthroughFB", (PFN_xrVoidFunction)emu_create_passthrough},
{"xrDestroyPassthroughFB", (PFN_xrVoidFunction)emu_passthrough_handle},
{"xrPassthroughStartFB", (PFN_xrVoidFunction)emu_passthrough_start},
{"xrPassthroughPauseFB", (PFN_xrVoidFunction)emu_passthrough_pause},
{"xrCreatePassthroughLayerFB", (PFN_xrVoidFunction)emu_create_passthrough_layer},
{"xrDestroyPassthroughLayerFB", (PFN_xrVoidFunction)emu_passthrough_layer_handle},
{"xrPassthroughLayerPauseFB", (PFN_xrVoidFunction)emu_passthrough_layer_pause},
{"xrPassthroughLayerResumeFB", (PFN_xrVoidFunction)emu_passthrough_layer_resume},
{"xrPassthroughLayerSetStyleFB", (PFN_xrVoidFunction)emu_passthrough_layer_style},
{"xrCreateGeometryInstanceFB", (PFN_xrVoidFunction)emu_create_geometry},
{"xrDestroyGeometryInstanceFB", (PFN_xrVoidFunction)emu_geometry_handle},
{"xrGeometryInstanceSetTransformFB", (PFN_xrVoidFunction)emu_geometry_transform},
// Companion passthrough entry points OVRPlugin's Insight MR init requires.
{"xrCreateTriangleMeshFB", (PFN_xrVoidFunction)emu_create_mesh},
{"xrDestroyTriangleMeshFB", (PFN_xrVoidFunction)emu_mesh_handle},
{"xrCreatePassthroughColorLutMETA", (PFN_xrVoidFunction)emu_create_lut},
{"xrDestroyPassthroughColorLutMETA", (PFN_xrVoidFunction)emu_lut_handle},
{"xrUpdatePassthroughColorLutMETA", (PFN_xrVoidFunction)emu_update_lut},
{"xrGetPassthroughPreferencesMETA", (PFN_xrVoidFunction)emu_preferences},
{"xrPassthroughLayerSetKeyboardHandsIntensityFB", (PFN_xrVoidFunction)emu_keyboard_hands},
};
for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); ++i)
if (!strcmp(name, table[i].name)) return table[i].fn;
return NULL;
}
XRAPI_ATTR XrResult XRAPI_CALL xrGetSystemProperties(XrInstance instance, XrSystemId system,
XrSystemProperties *properties) {
PFN_xrGetSystemProperties fn = (PFN_xrGetSystemProperties)lookup(instance, "xrGetSystemProperties");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result = fn(instance, system, properties);
if (XR_SUCCEEDED(result)) headset_model_properties(properties);
if (XR_SUCCEEDED(result)) color_space_properties(properties);
if (XR_SUCCEEDED(result) && properties && properties->graphicsProperties.maxLayerCount)
runtime_max_layers = properties->graphicsProperties.maxLayerCount;
if (XR_SUCCEEDED(result) && properties && layer_debug)
LOG("layer_debug: system '%s' maxLayerCount=%u max swapchain %ux%u", properties->systemName,
properties->graphicsProperties.maxLayerCount, properties->graphicsProperties.maxSwapchainImageWidth,
properties->graphicsProperties.maxSwapchainImageHeight);
if (XR_SUCCEEDED(result) && (emulate_passthrough || emulate_scene || emulate_render_model) && properties)
for (XrBaseOutStructure *p = (XrBaseOutStructure *)properties->next; p; p = p->next) {
if (p->type == XR_TYPE_SYSTEM_PASSTHROUGH_PROPERTIES_FB)
((XrSystemPassthroughPropertiesFB *)p)->supportsPassthrough = XR_TRUE;
if (p->type == XR_TYPE_SYSTEM_SPATIAL_ENTITY_PROPERTIES_FB && emulate_scene)
((XrSystemSpatialEntityPropertiesFB *)p)->supportsSpatialEntity = XR_TRUE;
// META scene capability chains used by current MRUK.
if (p->type == XR_TYPE_SYSTEM_SPACE_DISCOVERY_PROPERTIES_META && emulate_scene)
((XrSystemSpaceDiscoveryPropertiesMETA *)p)->supportsSpaceDiscovery = XR_TRUE;
if (p->type == XR_TYPE_SYSTEM_SPACE_PERSISTENCE_PROPERTIES_META && emulate_scene)
((XrSystemSpacePersistencePropertiesMETA *)p)->supportsSpacePersistence = XR_TRUE;
if (p->type == XR_TYPE_SYSTEM_BOUNDARY_VISIBILITY_PROPERTIES_META && emulate_scene)
((XrSystemBoundaryVisibilityPropertiesMETA *)p)->supportsBoundaryVisibility = XR_TRUE;
if (p->type == XR_TYPE_SYSTEM_PASSTHROUGH_PROPERTIES2_FB)
((XrSystemPassthroughProperties2FB *)p)->capabilities = XR_PASSTHROUGH_CAPABILITY_BIT_FB;
if (p->type == XR_TYPE_SYSTEM_RENDER_MODEL_PROPERTIES_FB && emulate_render_model)
((XrSystemRenderModelPropertiesFB *)p)->supportsRenderModelLoading = XR_TRUE;
}
return result;
}
#include "flip_vk.c"
#include "alpha_blend_vk.c"
#include "pose_time.c"
// XR_KHR_convert_timespec_time for runtimes that lack it (Frame): XrTime is derived from the monotonic clock
// using the offset measured at the last xrWaitFrame. The runtime's own implementation is preferred.
static PFN_xrConvertTimespecTimeToTimeKHR runtime_timespec_to_time;
static PFN_xrConvertTimeToTimespecTimeKHR runtime_time_to_timespec;
static XRAPI_ATTR XrResult XRAPI_CALL emu_timespec_to_time(XrInstance instance, const struct timespec *ts, XrTime *time) {
if (runtime_timespec_to_time) {
XrResult r = runtime_timespec_to_time(instance, ts, time);
if (r != XR_ERROR_FUNCTION_UNSUPPORTED) {
if (XR_SUCCEEDED(r) && time) pose_time_note("xrConvertTimespecTimeToTimeKHR (runtime)", 0, 0, *time);
return r;
}
}
if (!ts || !time) return XR_ERROR_VALIDATION_FAILURE;
*time = (XrTime)((int64_t)ts->tv_sec * 1000000000ll + ts->tv_nsec + (xr_time_calibrated ? xr_time_offset : 0));
pose_time_note("xrConvertTimespecTimeToTimeKHR", 0, 0, *time);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_time_to_timespec(XrInstance instance, XrTime time, struct timespec *ts) {
if (runtime_time_to_timespec) {
XrResult r = runtime_time_to_timespec(instance, time, ts);
if (r != XR_ERROR_FUNCTION_UNSUPPORTED) return r;
}
if (!ts) return XR_ERROR_VALIDATION_FAILURE;
int64_t mono = (int64_t)time - (xr_time_calibrated ? xr_time_offset : 0);
ts->tv_sec = (time_t)(mono / 1000000000ll);
ts->tv_nsec = (long)(mono % 1000000000ll);
return XR_SUCCESS;
}
XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, XrSpaceLocation *location) {
time = pose_time_fixed(time);
pose_time_note("xrLocateSpace", (uint64_t)(uintptr_t)space, (uint64_t)(uintptr_t)baseSpace, time);
if (emulate_scene && location) {
XrPosef pose; XrSpaceLocationFlags flags;
pthread_mutex_lock(&scene_lock);
int fake = locate_fake(space, baseSpace, time, &pose, &flags);
int entity = fake ? fake_space_index(space) : -1, base_entity = fake ? fake_space_index(baseSpace) : -1;
pthread_mutex_unlock(&scene_lock);
if (fake) {
// MRUK keeps an anchor pending (no load Success, ever) unless both valid bits are set (Pinball FX)
static int logged, logged_invalid;
const XrSpaceLocationFlags need = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
int invalid = (flags & need) != need;
if (logged < 30 || (invalid && logged_invalid < 60)) {
LOG("scene: locate entity %d in %s %d (space 0x%llx) at %lld: flags 0x%llx%s", entity,
base_entity >= 0 ? "entity" : "real space", base_entity, (unsigned long long)(uintptr_t)baseSpace,
(long long)time, (unsigned long long)flags, invalid ? " NOT VALID" : "");
++logged;
if (invalid) ++logged_invalid;
}
location->pose = pose;
location->locationFlags = flags;
return XR_SUCCESS;
}
}
PFN_xrLocateSpace fn = (PFN_xrLocateSpace)lookup(active_instance, "xrLocateSpace");
return fn ? fn(space, baseSpace, time, location) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
static XrResult locate_spaces_common(const char *name, XrSession session, const XrSpacesLocateInfo *info,
XrSpaceLocations *locations) {
int any_fake = 0;
XrSpacesLocateInfo moved;
if (info && pose_time_fix) {
moved = *info;
moved.time = pose_time_fixed(info->time);
info = &moved;
}
if (info) pose_time_note(name, info->spaceCount ? (uint64_t)(uintptr_t)info->spaces[0] : 0,
(uint64_t)(uintptr_t)info->baseSpace, info->time);
if (emulate_scene && info && locations) {
any_fake = is_fake_space(info->baseSpace);
for (uint32_t i = 0; i < info->spaceCount && !any_fake; ++i) any_fake = is_fake_space(info->spaces[i]);
}
if (!any_fake) {
PFN_xrLocateSpaces fn = (PFN_xrLocateSpaces)lookup(active_instance, name);
return fn ? fn(session, info, locations) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
for (uint32_t i = 0; i < info->spaceCount && i < locations->locationCount; ++i) {
XrSpaceLocation one = {XR_TYPE_SPACE_LOCATION, NULL, 0, {{0, 0, 0, 1}, {0, 0, 0}}};
xrLocateSpace(info->spaces[i], info->baseSpace, info->time, &one);
locations->locations[i].locationFlags = one.locationFlags;
locations->locations[i].pose = one.pose;
}
return XR_SUCCESS;
}
XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpaces(XrSession session, const XrSpacesLocateInfo *info, XrSpaceLocations *locations) {
return locate_spaces_common("xrLocateSpaces", session, info, locations);
}
static XRAPI_ATTR XrResult XRAPI_CALL emu_locate_spaces_khr(XrSession session, const XrSpacesLocateInfo *info,
XrSpaceLocations *locations) {
return locate_spaces_common("xrLocateSpacesKHR", session, info, locations);
}
XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
if (is_fake_space(space)) {
pthread_mutex_lock(&scene_lock);
int valid = fake_space_index(space) >= 0;
pthread_mutex_unlock(&scene_lock);
return valid ? XR_SUCCESS : XR_ERROR_HANDLE_INVALID; // entity spaces live for the whole session
}
PFN_xrDestroySpace fn = (PFN_xrDestroySpace)lookup(active_instance, "xrDestroySpace");
return fn ? fn(space) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
#include "input_diag.c"
#include "session_fixes.c"
#include "layer_emul_gl.c"
#include "swapchain_pool.c"
#include "cube_standin.c"
#include "snapshot_gl.c"
#include "surface_swapchain.c"
#include "fade_quad.c"
// GPU video buffers need explicit external-memory support on the app's device.
// Preserve the runtime's extension list and both Vulkan-enable entry points.
XRAPI_ATTR XrResult XRAPI_CALL xrCreateVulkanInstanceKHR(XrInstance instance,const XrVulkanInstanceCreateInfoKHR *info,
VkInstance *created,VkResult *vulkanResult) {
PFN_xrCreateVulkanInstanceKHR fn=(PFN_xrCreateVulkanInstanceKHR)lookup(instance,"xrCreateVulkanInstanceKHR");
if(!fn)return XR_ERROR_FUNCTION_UNSUPPORTED;
if(!surface_native)return fn(instance,info,created,vulkanResult);
XrResult result=fn(instance,info,created,vulkanResult);
if(XR_SUCCEEDED(result) && created && vulkanResult && *vulkanResult==VK_SUCCESS)vk.instance=*created;
return result;
}
static const char *surf_device_exts[] = {"VK_ANDROID_external_memory_android_hardware_buffer",
"VK_KHR_external_memory", "VK_KHR_get_memory_requirements2", "VK_KHR_dedicated_allocation",
"VK_EXT_queue_family_foreign", "VK_KHR_sampler_ycbcr_conversion", "VK_KHR_bind_memory2", "VK_KHR_maintenance1"};
static int surf_extension_present(const char *text,const char *name) {
size_t length=strlen(name);
for(const char *p=text;(p=strstr(p,name));p++)
if((p==text || p[-1]==' ') && (p[length]==' ' || p[length]=='\0'))return 1;
return 0;
}
static int surf_device_supported(PFN_vkGetInstanceProcAddr gipa, VkInstance instance, VkPhysicalDevice physical) {
if (!surface_native || !gipa) return 0;
if(!physical){
// Unity can ask for extension strings before it creates its instance.
// Query a temporary instance instead of depending on that call order.
PFN_vkCreateInstance create=(PFN_vkCreateInstance)gipa(VK_NULL_HANDLE,"vkCreateInstance");
VkApplicationInfo app={VK_STRUCTURE_TYPE_APPLICATION_INFO,NULL,"FrameBridge video capabilities",0,NULL,0,VK_API_VERSION_1_1};
VkInstanceCreateInfo ci={VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,NULL,0,&app};
VkInstance probe=VK_NULL_HANDLE;if(!create || create(&ci,NULL,&probe)!=VK_SUCCESS)return 0;
PFN_vkEnumeratePhysicalDevices devices=(PFN_vkEnumeratePhysicalDevices)gipa(probe,"vkEnumeratePhysicalDevices");
PFN_vkDestroyInstance destroy=(PFN_vkDestroyInstance)gipa(probe,"vkDestroyInstance");
uint32_t count=0;VkPhysicalDevice gpu=VK_NULL_HANDLE;
int ok=devices && devices(probe,&count,NULL)==VK_SUCCESS && count==1 &&
devices(probe,&count,&gpu)==VK_SUCCESS && surf_device_supported(gipa,probe,gpu);
if(destroy)destroy(probe,NULL);return ok;
}
PFN_vkEnumerateDeviceExtensionProperties enumerate=(PFN_vkEnumerateDeviceExtensionProperties)
gipa(instance,"vkEnumerateDeviceExtensionProperties");
uint32_t count=0;
if(!enumerate || enumerate(physical,NULL,&count,NULL)!=VK_SUCCESS || count>2048)return 0;
VkExtensionProperties *exts=calloc(count,sizeof(*exts));
if(!exts)return 0;
int ok=enumerate(physical,NULL,&count,exts)==VK_SUCCESS;
for(unsigned j=0;j<sizeof(surf_device_exts)/sizeof(*surf_device_exts);j++){
int found=0;for(uint32_t i=0;i<count;i++)found|=!strcmp(exts[i].extensionName,surf_device_exts[j]);
ok &= found;
}
free(exts);return ok;
}
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanGraphicsDeviceKHR(XrInstance instance,XrSystemId system,
VkInstance vulkanInstance,VkPhysicalDevice *physical) {
PFN_xrGetVulkanGraphicsDeviceKHR fn=(PFN_xrGetVulkanGraphicsDeviceKHR)lookup(instance,"xrGetVulkanGraphicsDeviceKHR");
if(!surface_native)return fn?fn(instance,system,vulkanInstance,physical):XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result=fn?fn(instance,system,vulkanInstance,physical):XR_ERROR_FUNCTION_UNSUPPORTED;
if(XR_SUCCEEDED(result) && physical){vk.instance=vulkanInstance;vk.physical=*physical;}
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanDeviceExtensionsKHR(XrInstance instance,XrSystemId system,
uint32_t capacity,uint32_t *count,char *buffer) {
PFN_xrGetVulkanDeviceExtensionsKHR fn=(PFN_xrGetVulkanDeviceExtensionsKHR)lookup(instance,"xrGetVulkanDeviceExtensionsKHR");
if(!fn)return XR_ERROR_FUNCTION_UNSUPPORTED;
if(!surface_native)return fn(instance,system,capacity,count,buffer);
void *lib=dlopen("libvulkan.so",RTLD_NOW|RTLD_LOCAL);
PFN_vkGetInstanceProcAddr gipa=lib?(PFN_vkGetInstanceProcAddr)dlsym(lib,"vkGetInstanceProcAddr"):NULL;
if(!surf_device_supported(gipa,vk.instance,vk.physical))return fn(instance,system,capacity,count,buffer);
uint32_t original=0;XrResult result=fn(instance,system,0,&original,NULL);
if(XR_FAILED(result) || !count || original>65536)return result;
char *text=calloc(original+1024,1);if(!text)return XR_ERROR_OUT_OF_MEMORY;
result=fn(instance,system,original,&original,text);
if(XR_SUCCEEDED(result)){
for(unsigned i=0;i<sizeof(surf_device_exts)/sizeof(*surf_device_exts);i++){
if(!surf_extension_present(text,surf_device_exts[i])){if(*text)strcat(text," ");strcat(text,surf_device_exts[i]);}
}
*count=(uint32_t)strlen(text)+1;
if(capacity && (capacity<*count || !buffer))result=XR_ERROR_SIZE_INSUFFICIENT;
else if(capacity)memcpy(buffer,text,*count);
}
free(text);return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrCreateVulkanDeviceKHR(XrInstance instance,const XrVulkanDeviceCreateInfoKHR *info,
VkDevice *device,VkResult *vulkanResult) {
PFN_xrCreateVulkanDeviceKHR fn=(PFN_xrCreateVulkanDeviceKHR)lookup(instance,"xrCreateVulkanDeviceKHR");
if(!fn)return XR_ERROR_FUNCTION_UNSUPPORTED;
if(!surface_native)return fn(instance,info,device,vulkanResult);
if(!info || !info->vulkanCreateInfo || !surf_device_supported(info->pfnGetInstanceProcAddr,
vk.instance,info->vulkanPhysicalDevice))return fn(instance,info,device,vulkanResult);
VkDeviceCreateInfo ci=*info->vulkanCreateInfo;
const char **names=calloc(ci.enabledExtensionCount+8,sizeof(*names));
if(!names)return XR_ERROR_OUT_OF_MEMORY;
for(uint32_t i=0;i<ci.enabledExtensionCount;i++)names[i]=ci.ppEnabledExtensionNames[i];
for(unsigned j=0;j<8;j++){
int found=0;for(uint32_t i=0;i<ci.enabledExtensionCount;i++)found|=!strcmp(names[i],surf_device_exts[j]);
if(!found)names[ci.enabledExtensionCount++]=surf_device_exts[j];
}
ci.ppEnabledExtensionNames=names;
XrVulkanDeviceCreateInfoKHR fixed=*info;fixed.vulkanCreateInfo=&ci;
XrResult result=fn(instance,&fixed,device,vulkanResult);free(names);return result;
}
#include "eye_debug.c"
// OpenXR requires xrGet*GraphicsRequirementsKHR before xrCreateSession; Meta's runtime doesn't enforce it, the Frame's
// does (XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING, e.g. Lambda1VR's TBXR). Ask on the app's behalf, once, and retry.
static int ask_graphics_requirements(XrInstance instance, const XrSessionCreateInfo *info) {
const XrBaseInStructure *binding = info ? (const XrBaseInStructure *)info->next : NULL;
for (; binding; binding = binding->next) {
const char *name = NULL;
XrStructureType type = 0;
if (binding->type == (XrStructureType)1000024001) { // XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR
name = "xrGetOpenGLESGraphicsRequirementsKHR";
type = (XrStructureType)1000024003; // XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR
} else if (binding->type == (XrStructureType)1000025000) { // XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR
name = "xrGetVulkanGraphicsRequirementsKHR";
type = (XrStructureType)1000025002; // XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR
}
if (!name) continue;
// XrGraphicsRequirementsOpenGLESKHR and ...VulkanKHR share this layout
struct { XrStructureType type; void *next; XrVersion min, max; } req = {type, NULL, 0, 0};
typedef XrResult (XRAPI_PTR *PFN_requirements)(XrInstance, XrSystemId, void *);
PFN_requirements get = (PFN_requirements)lookup(instance, name);
if (!get && type == (XrStructureType)1000025002) // XR_KHR_vulkan_enable2 apps: same struct and call
get = (PFN_requirements)lookup(instance, "xrGetVulkanGraphicsRequirements2KHR");
XrResult r = get ? get(instance, info->systemId, &req) : XR_ERROR_FUNCTION_UNSUPPORTED;
LOG("xrCreateSession: the app skipped %s; asked for it (result=%d)", name, (int)r);
return XR_SUCCEEDED(r);
}
return 0;
}
// gl_error_clear: the runtime's GL interop checks glGetError after its own GL calls and fails on any error, also one
// the app left pending on the same context. Vader Immortal leaves GL_INVALID_OPERATION pending before its first
// frame; the runtime then logs "SXR_GL_CHECK: error:0x502 ... glGenTextures", and from that moment allocates a new
// command buffer every frame (CVRCompositorSharedTextures::BeginCommandBuffer, ~20 MB/s, GitHub #285).
static void clear_gl_errors(const char *where) {
if (!gl_error_clear || !gles_session) return;
static unsigned (*get_error)(void);
static void *(*current_context)(void);
static int resolved;
if (!resolved) {
resolved = 1;
get_error = (unsigned (*)(void))dlsym(RTLD_DEFAULT, "glGetError");
current_context = (void *(*)(void))dlsym(RTLD_DEFAULT, "eglGetCurrentContext");
if (!get_error || !current_context) {
void *gl = dlopen("libGLESv2.so", RTLD_NOW | RTLD_NOLOAD), *egl = dlopen("libEGL.so", RTLD_NOW | RTLD_NOLOAD);
if (!get_error && gl) get_error = (unsigned (*)(void))dlsym(gl, "glGetError");
if (!current_context && egl) current_context = (void *(*)(void))dlsym(egl, "eglGetCurrentContext");
}
LOG("gl_error_clear: %s", get_error && current_context ? "on" : "glGetError/eglGetCurrentContext not found");
}
if (!get_error || !current_context || !current_context()) return;
for (int i = 0; i < 16; ++i) {
unsigned err = get_error();
if (!err) break;
static int logged;
if (logged++ < 20) LOG("gl_error_clear: cleared the app's pending GL error 0x%x before %s", err, where);
}
}
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSession(XrInstance instance, const XrSessionCreateInfo *info, XrSession *session) {
PFN_xrCreateSession fn = (PFN_xrCreateSession)lookup(instance, "xrCreateSession");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result = fn(instance, info, session);
if (result == XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING && ask_graphics_requirements(instance, info))
result = fn(instance, info, session);
if (XR_SUCCEEDED(result)) {
gles_session = 0;
for (const XrBaseInStructure *b = info ? (const XrBaseInStructure *)info->next : NULL; b; b = b->next)
gles_session |= b->type == (XrStructureType)1000024001; // XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR
foveation_eye_supported = 0;
if (foveation_eye_enabled) {
PFN_xrGetSystemProperties properties = (PFN_xrGetSystemProperties)lookup(instance, "xrGetSystemProperties");
XrSystemFoveationEyeTrackedPropertiesMETA eye = {XR_TYPE_SYSTEM_FOVEATION_EYE_TRACKED_PROPERTIES_META};
XrSystemProperties sys = {XR_TYPE_SYSTEM_PROPERTIES, &eye};
if (properties && XR_SUCCEEDED(properties(instance, info->systemId, &sys)))
foveation_eye_supported = eye.supportsFoveationEyeTracked;
LOG("foveation eye capability: %d", foveation_eye_supported);
}
flip_emul = flip_emul_setting;
flip_on_create_session(info);
scene_on_create_session(*session);
session_fixes_on_create_session(*session);
emul_on_create_session(*session, info);
if (swapchain_pool) {
for (const XrBaseInStructure *p = info->next; p; p = p->next)
if (p->type == XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR) pool_session = *session;
}
fade_on_create_session(*session, info);
}
return result;
}
// Vibrations: logged (the first few, to see what games ask for) and scaled by haptic_scale. Plain vibrations, Meta's
// amplitude envelopes and PCM buffers all pass here: games' own calls, OVRPort's VrApi bridge and the xrshim alike.
static int haptics_logged;
static XrResult apply_haptics(XrSession session, const XrHapticActionInfo *info, const XrHapticBaseHeader *feedback) {
PFN_xrApplyHapticFeedback fn = (PFN_xrApplyHapticFeedback)lookup(active_instance, "xrApplyHapticFeedback");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (!feedback) return fn(session, info, feedback);
if (feedback->type == XR_TYPE_HAPTIC_VIBRATION) {
XrHapticVibration v = *(const XrHapticVibration *)feedback;
if (haptics_logged++ < 8)
LOG("haptic: vibration amplitude=%.2f duration=%lld ns frequency=%.0f (scale %.2f)", v.amplitude,
(long long)v.duration, v.frequency, haptic_scale);
v.amplitude *= haptic_scale;
if (v.amplitude <= 0.001f) {
// OVRPlugin stops a vibration by sending amplitude 0 with its usual 2 s duration (Jurassic World, The Boys
// VR): on a Quest that stops the motor; OpenXR's stop is xrStopHapticFeedback, and without it the last
// buzz ran its full 2 s
PFN_xrStopHapticFeedback stop = (PFN_xrStopHapticFeedback)lookup(active_instance, "xrStopHapticFeedback");
if (stop) return stop(session, info);
}
return fn(session, info, (const XrHapticBaseHeader *)&v);
}
if (feedback->type == (XrStructureType)1000173001 || feedback->type == (XrStructureType)1000209001) {
// XR_TYPE_HAPTIC_AMPLITUDE_ENVELOPE_VIBRATION_FB / XR_TYPE_HAPTIC_PCM_VIBRATION_FB: a sample buffer
typedef struct { XrStructureType type; const void *next; XrDuration duration; uint32_t count; const float *s; } env_t;
typedef struct { XrStructureType type; const void *next; uint32_t count; const float *s; float rate; XrBool32 append;
uint32_t *consumed; } pcm_t;
int envelope = feedback->type == (XrStructureType)1000173001;
uint32_t n = envelope ? ((const env_t *)feedback)->count : ((const pcm_t *)feedback)->count;
const float *in = envelope ? ((const env_t *)feedback)->s : ((const pcm_t *)feedback)->s;
if (haptics_logged++ < 8) {
float peak = 0;
for (uint32_t i = 0; in && i < n; ++i) peak = fmaxf(peak, in[i]);
LOG("haptic: %s %u samples peak=%.2f (scale %.2f)", envelope ? "envelope" : "pcm", n, peak, haptic_scale);
}
if (haptic_scale >= 0.999f || !in || !n || n > 1u << 20) return fn(session, info, feedback);
float *scaled = malloc(n * sizeof *scaled);
if (!scaled) return fn(session, info, feedback);
for (uint32_t i = 0; i < n; ++i) scaled[i] = in[i] * haptic_scale;
XrResult r;
if (envelope) { env_t e = *(const env_t *)feedback; e.s = scaled; r = fn(session, info, (const XrHapticBaseHeader *)&e); }
else { pcm_t c = *(const pcm_t *)feedback; c.s = scaled; r = fn(session, info, (const XrHapticBaseHeader *)&c); }
free(scaled);
return r;
}
return fn(session, info, feedback);
}
XRAPI_ATTR XrResult XRAPI_CALL xrApplyHapticFeedback(XrSession session, const XrHapticActionInfo *info,
const XrHapticBaseHeader *feedback) {
XrResult result = apply_haptics(session, info, feedback);
if (input_diag && XR_FAILED(result)) // always hooked (haptic_scale), so input_diag reports failures here
diag_call_failed("xrApplyHapticFeedback", result,
feedback && feedback->type != XR_TYPE_HAPTIC_VIBRATION ? "(not XrHapticVibration)" : NULL);
return result;
}
#include "hide_foveation_images.h"
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain, uint32_t capacity, uint32_t *count,
XrSwapchainImageBaseHeader *images) {
PFN_xrEnumerateSwapchainImages fn = (PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_enumerate(pooled, capacity, count, images);
if (XR_SUCCEEDED(r) && capacity && images) emul_on_enumerate(swapchain, 1, images);
return r;
}
if (is_standin(swapchain)) return standin_enumerate(swapchain, capacity, count, images);
XrResult result = equirect_emul && emul_is_virtual(swapchain) ? emul_virtual_enumerate(swapchain, capacity, count, images)
: hide_fb_foveation_vulkan ? fb_hide_foveation_images(fn, swapchain, capacity, count, images)
: fn(swapchain, capacity, count, images);
if (XR_SUCCEEDED(result) && images && capacity && count) {
flip_on_enumerate_images(swapchain, *count, images);
emul_on_enumerate(swapchain, *count < capacity ? *count : capacity, images);
snap_on_enumerate(swapchain, *count < capacity ? *count : capacity, images);
}
return result;
}
// counted for layer_debug (an app that skips the wait or release keeps runtime resources alive)
XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 1, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
XrResult result = fn(swapchain, info);
__atomic_add_fetch(&sc_waits, 1, __ATOMIC_RELAXED);
if (result != XR_SUCCESS) __atomic_add_fetch(&sc_wait_fails, 1, __ATOMIC_RELAXED);
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 2, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
__atomic_add_fetch(&sc_releases, 1, __ATOMIC_RELAXED);
return fn(swapchain, info);
}
XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageAcquireInfo *info,
uint32_t *index) {
PFN_xrAcquireSwapchainImage fn = (PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_cycle(pooled, 0, index);
if (XR_SUCCEEDED(r)) emul_on_acquire(swapchain, *index);
return r;
}
if (is_standin(swapchain)) { // adapter-served cube stand-in: one image
if (!index) return XR_ERROR_VALIDATION_FAILURE;
*index = 0;
return XR_SUCCESS;
}
if (equirect_emul && emul_is_virtual(swapchain)) { // adapter-served write-once picture: one image
if (!index) return XR_ERROR_VALIDATION_FAILURE;
*index = 0;
emul_on_acquire(swapchain, 0);
return XR_SUCCESS;
}
XrResult result = fn(swapchain, info, index);
__atomic_add_fetch(&sc_acquires, 1, __ATOMIC_RELAXED);
if (XR_SUCCEEDED(result) && index) {
flip_on_acquire(swapchain, *index);
emul_on_acquire(swapchain, *index);
snap_on_acquire(swapchain, *index);
}
return result;
}
// Copy scene content only while the oldest acquired image is successfully waited
// and still application-owned. Release itself is forwarded immediately.
static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_release(XrSwapchain handle,const XrSwapchainImageReleaseInfo *info) {
pthread_mutex_lock(&flip_lock);
tracked_swapchain *t=find_tracked(handle,0);
if(t && t->acquired_count && t->waited_count)surf_scene_capture(t,t->acquired[0]);
pthread_mutex_unlock(&flip_lock);
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrReleaseSwapchainImage(handle,info) :
equirect_emul?hook_xrReleaseSwapchainImage(handle,info):
((PFN_xrReleaseSwapchainImage)lookup(active_instance,"xrReleaseSwapchainImage"))(handle,info);
if(XR_SUCCEEDED(result)) {
surf_scene_release_commit(handle);
pthread_mutex_lock(&flip_lock);t=find_tracked(handle,0);
if(t && t->acquired_count) {
memmove(t->acquired,t->acquired+1,(--t->acquired_count)*sizeof(t->acquired[0]));
if(t->waited_count)t->waited_count--;
}
pthread_mutex_unlock(&flip_lock);
}
return result;
}
static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_wait(XrSwapchain handle,const XrSwapchainImageWaitInfo *info) {
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrWaitSwapchainImage(handle,info) :
equirect_emul?hook_xrWaitSwapchainImage(handle,info):
((PFN_xrWaitSwapchainImage)lookup(active_instance,"xrWaitSwapchainImage"))(handle,info);
if(result==XR_SUCCESS) {
pthread_mutex_lock(&flip_lock);tracked_swapchain *t=find_tracked(handle,0);
if(t && t->waited_count<t->acquired_count)t->waited_count++;
pthread_mutex_unlock(&flip_lock);
}
return result;
}
// ---------------------------------------------------------------- tracking diagnostics
static XrSpace view_space_handle = XR_NULL_HANDLE;
// Frame's runtime returns untracked poses from reference spaces created before tracking starts.
// With respace_kick, once the session reaches FOCUSED we deliver synthetic "reference space change
// pending" events so the app (OVRPlugin) recreates its spaces.
static XrSession focused_session = XR_NULL_HANDLE;
static int kicks_pending = -1; // -1: not yet armed
XRAPI_ATTR XrResult XRAPI_CALL xrPollEvent(XrInstance instance, XrEventDataBuffer *event) {
mx_audio_initialize(); mx_batman_geometry_initialize();
PFN_xrPollEvent fn = (PFN_xrPollEvent)lookup(instance, "xrPollEvent");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (emulate_scene && event && !scene_events_via_shim) {
pthread_mutex_lock(&scene_lock);
int got = pop_event(event);
pthread_mutex_unlock(&scene_lock);
if (got) return XR_SUCCESS;
}
if (sync_guard) pthread_mutex_lock(&sync_lock);
focus_log_next = fn; // the runtime's own focus changes are logged, before focus_hold hides any
XrResult result = focus_hold && event ? focus_hold_poll(focus_log_poll, instance, event)
: focus_log_poll(instance, event);
if (sync_guard) {
if (result == XR_SUCCESS && event && session_state_of(event) == XR_SESSION_STATE_FOCUSED)
sync_resume_at = monotonic_ns() + SYNC_GUARD_PAUSE_NS;
pthread_mutex_unlock(&sync_lock);
}
if (result == XR_SUCCESS && event && (layer_debug || stable_local)) {
if (event->type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED && layer_debug)
LOG("layer_debug: session state %d at %.3f", ((const XrEventDataSessionStateChanged *)event)->state,
monotonic_ns() / 1e9);
if (event->type == XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING) {
const XrEventDataReferenceSpaceChangePending *e = (const XrEventDataReferenceSpaceChangePending *)event;
if (layer_debug) LOG("layer_debug: runtime reference space change pending type=%d", e->referenceSpaceType);
if (e->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_LOCAL) local_anchor_stale = 1;
}
}
if (!respace_kick || !event) return result;
if (result == XR_SUCCESS && event->type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
const XrEventDataSessionStateChanged *e = (const XrEventDataSessionStateChanged *)event;
if (e->state == XR_SESSION_STATE_FOCUSED && kicks_pending < 0) {
focused_session = e->session;
kicks_pending = 3; // LOCAL, LOCAL_FLOOR, STAGE
}
return result;
}
if (result == XR_EVENT_UNAVAILABLE && kicks_pending > 0) {
static const XrReferenceSpaceType types[] = {XR_REFERENCE_SPACE_TYPE_STAGE,
(XrReferenceSpaceType)1000426000 /* LOCAL_FLOOR_EXT */, XR_REFERENCE_SPACE_TYPE_LOCAL};
XrEventDataReferenceSpaceChangePending *e = (XrEventDataReferenceSpaceChangePending *)event;
memset(e, 0, sizeof(*e));
e->type = XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING;
e->session = focused_session;
e->referenceSpaceType = types[--kicks_pending];
e->changeTime = 0;
e->poseValid = XR_FALSE;
e->poseInPreviousSpace.orientation.w = 1.0f;
LOG("respace_kick: delivered reference space change for type %d", e->referenceSpaceType);
return XR_SUCCESS;
}
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrCreateReferenceSpace(XrSession session, const XrReferenceSpaceCreateInfo *info,
XrSpace *space) {
PFN_xrCreateReferenceSpace fn = (PFN_xrCreateReferenceSpace)lookup(active_instance, "xrCreateReferenceSpace");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrReferenceSpaceCreateInfo fixed;
if (stable_local && info && info->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_LOCAL) {
fixed = *info;
fixed.poseInReferenceSpace = stable_local_pose(session, info->poseInReferenceSpace);
info = &fixed;
}
XrResult result = fn(session, info, space);
if (info && space) {
LOG("xrCreateReferenceSpace type=%d result=%d space=%p", info->referenceSpaceType, result,
(void *)(uintptr_t)*space);
if (layer_debug && XR_SUCCEEDED(result)) debug_reference_space(session, info, *space);
if (XR_SUCCEEDED(result) && info->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_VIEW) view_space_handle = *space;
}
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrWaitFrame(XrSession session, const XrFrameWaitInfo *info, XrFrameState *state) {
PFN_xrWaitFrame fn = (PFN_xrWaitFrame)lookup(active_instance, "xrWaitFrame");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult result = fn(session, info, state);
if (XR_SUCCEEDED(result) && state) {
last_predicted_time = state->predictedDisplayTime;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t mono = (int64_t)now.tv_sec * 1000000000ll + now.tv_nsec;
// predictedDisplayTime is about one display period ahead of "now".
xr_time_offset = (int64_t)(state->predictedDisplayTime - state->predictedDisplayPeriod) - mono;
xr_time_calibrated = 1;
last_display_period = state->predictedDisplayPeriod;
pose_time_sample(xr_time_offset);
pose_time_display(state->predictedDisplayTime);
pose_time_note_offset((long long)(state->predictedDisplayTime - mono));
if (layer_debug) debug_aim_vs_grip(state->predictedDisplayTime);
// shouldRender changes (a few): a game that submits no layers while the runtime says not to render shows
// nothing, e.g. Unreal's OpenXR plugin in a headless launch test
static int last_should_render = -1, should_render_logged;
if ((int)state->shouldRender != last_should_render && should_render_logged < 12) {
++should_render_logged;
LOG("xrWaitFrame: shouldRender=%d period=%.2f ms%s", (int)state->shouldRender,
state->predictedDisplayPeriod / 1e6, force_render && !state->shouldRender ? " (force_render: reported as 1)" : "");
}
last_should_render = (int)state->shouldRender;
if (force_render) state->shouldRender = XR_TRUE;
}
return result;
}
// pose_consistency (GitHub #8, proposed by Klownicle for I Am Cat): the game asks xrLocateViews ~3 times per frame
// for the same display time and got slightly different poses each time (tenths of a degree), so parts of its pipeline
// rendered with different heads: judder. The first fully tracked answer for (session, space, view configuration,
// display time) is kept and repeated; a new display time, another space or a failed/untracked answer is never cached.
#define POSE_CACHE 8
static struct {
XrSession session; XrSpace space; XrViewConfigurationType config; XrTime time;
XrViewStateFlags flags; uint32_t count; XrPosef pose[2]; XrFovf fov[2];
} pose_cache[POSE_CACHE];
static int pose_cache_next, pose_cache_hits;
static pthread_mutex_t pose_cache_lock = PTHREAD_MUTEX_INITIALIZER;
#define POSE_TRACKED (XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT | \
XR_VIEW_STATE_ORIENTATION_TRACKED_BIT | XR_VIEW_STATE_POSITION_TRACKED_BIT)
static int pose_cache_lookup(XrSession session, const XrViewLocateInfo *info, XrViewState *state, uint32_t capacity,
uint32_t *count, XrView *views) {
int hit = 0;
pthread_mutex_lock(&pose_cache_lock);
for (int i = 0; i < POSE_CACHE && !hit; ++i) {
if (pose_cache[i].session != session || pose_cache[i].space != info->space ||
pose_cache[i].config != info->viewConfigurationType || pose_cache[i].time != info->displayTime ||
!pose_cache[i].count || capacity < pose_cache[i].count)
continue;
state->viewStateFlags = pose_cache[i].flags;
*count = pose_cache[i].count;
for (uint32_t v = 0; v < pose_cache[i].count; ++v) { // only the values: the caller's type/next stay
views[v].pose = pose_cache[i].pose[v];
views[v].fov = pose_cache[i].fov[v];
}
hit = 1;
}
if (hit && pose_cache_hits++ == 0) LOG("pose_consistency: repeated query answered with the frame's first poses");
pthread_mutex_unlock(&pose_cache_lock);
return hit;
}
static void pose_cache_store(XrSession session, const XrViewLocateInfo *info, const XrViewState *state,
uint32_t count, const XrView *views) {
if (count == 0 || count > 2 || (state->viewStateFlags & POSE_TRACKED) != POSE_TRACKED) return;
pthread_mutex_lock(&pose_cache_lock);
int slot = pose_cache_next;
pose_cache_next = (pose_cache_next + 1) % POSE_CACHE;
pose_cache[slot].session = session;
pose_cache[slot].space = info->space;
pose_cache[slot].config = info->viewConfigurationType;
pose_cache[slot].time = info->displayTime;
pose_cache[slot].flags = state->viewStateFlags;
pose_cache[slot].count = count;
for (uint32_t v = 0; v < count; ++v) {
pose_cache[slot].pose[v] = views[v].pose;
pose_cache[slot].fov[v] = views[v].fov;
}
pthread_mutex_unlock(&pose_cache_lock);
}
XRAPI_ATTR XrResult XRAPI_CALL xrLocateViews(XrSession session, const XrViewLocateInfo *info, XrViewState *state,
uint32_t capacity, uint32_t *count, XrView *views) {
PFN_xrLocateViews fn = (PFN_xrLocateViews)lookup(active_instance, "xrLocateViews");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrViewLocateInfo moved;
if (info && pose_time_fix) {
moved = *info;
moved.displayTime = pose_time_fixed(info->displayTime);
info = &moved;
}
if (info) pose_time_note("xrLocateViews", (uint64_t)(uintptr_t)info->space, 0, info->displayTime);
int cacheable = pose_consistency && info && state && count && views && capacity > 0;
if (cacheable && pose_cache_lookup(session, info, state, capacity, count, views)) return XR_SUCCESS;
XrResult result = fn(session, info, state, capacity, count, views);
if (cacheable && result == XR_SUCCESS) pose_cache_store(session, info, state, *count, views);
if(surface_native && surface_emul && XR_SUCCEEDED(result) && count)
surf_views_record(session,info,state,*count<capacity?*count:capacity,views);
if (equirect_emul && XR_SUCCEEDED(result) && info && count && views && state &&
(state->viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT))
emul_on_locate_views(info->space, info->displayTime, *count < capacity ? *count : capacity, views);
// respace_kick trigger: ~90 consecutive untracked head poses in a non-VIEW space.
static int untracked;
if (respace_kick && XR_SUCCEEDED(result) && state && info && info->space != view_space_handle) {
int tracked = (state->viewStateFlags & (XR_VIEW_STATE_ORIENTATION_TRACKED_BIT | XR_VIEW_STATE_POSITION_TRACKED_BIT)) != 0;
untracked = tracked ? 0 : untracked + 1;
if (untracked == 90 && kicks_pending <= 0) {
focused_session = session;
kicks_pending = 3;
LOG("respace_kick: head pose untracked for 90 frames, asking app to recreate its spaces");
// Diagnostic: which freshly created space types give tracked poses right now?
PFN_xrCreateReferenceSpace create = (PFN_xrCreateReferenceSpace)lookup(active_instance, "xrCreateReferenceSpace");
static const XrReferenceSpaceType probe[] = {XR_REFERENCE_SPACE_TYPE_LOCAL, XR_REFERENCE_SPACE_TYPE_STAGE,
(XrReferenceSpaceType)1000426000};
for (size_t k = 0; create && k < 3; ++k) {
XrReferenceSpaceCreateInfo ci = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO, NULL, probe[k], {{0, 0, 0, 1}, {0, 0, 0}}};
XrSpace fresh = XR_NULL_HANDLE;
if (XR_FAILED(create(session, &ci, &fresh))) continue;
XrViewLocateInfo li = *info;
li.space = fresh;
XrViewState st = {XR_TYPE_VIEW_STATE, NULL, 0};
XrView tmp[2] = {{XR_TYPE_VIEW, NULL, {{0, 0, 0, 1}, {0, 0, 0}}, {0, 0, 0, 0}},
{XR_TYPE_VIEW, NULL, {{0, 0, 0, 1}, {0, 0, 0}}, {0, 0, 0, 0}}};
uint32_t n = 0;
XrResult r = fn(session, &li, &st, 2, &n, tmp);
LOG("probe fresh space type=%d locate=%d flags=0x%llx pos=%.3f,%.3f,%.3f", probe[k], r,
(unsigned long long)st.viewStateFlags, tmp[0].pose.position.x, tmp[0].pose.position.y,
tmp[0].pose.position.z);
}
}
}
static int logged;
if (views && count && *count && logged < 400 && (logged++ % 100) == 0)
LOG("xrLocateViews space=%p%s result=%d flags=0x%llx view0 pos=%.3f,%.3f,%.3f time=%lld predicted=%lld",
(void *)(uintptr_t)info->space, info->space == view_space_handle ? "(VIEW)" : "", result,
(unsigned long long)state->viewStateFlags, views[0].pose.position.x, views[0].pose.position.y,
views[0].pose.position.z, (long long)info->displayTime, (long long)last_predicted_time);
return result;
}
// xrBeginFrame: counted for layer_debug. A game that begins a new frame while the previous one is still open
// (never ended) makes the runtime discard it; the Frame's runtime then never recycles that frame's GPU timing
// command buffer (Vader Immortal: ~430 GPU mappings / 20 MB a second). frame_balance ends the open frame first,
// with no layers, as the runtime expects.
static XrTime last_begin_display_time;
XRAPI_ATTR XrResult XRAPI_CALL xrBeginFrame(XrSession session, const XrFrameBeginInfo *info) {
PFN_xrBeginFrame fn = (PFN_xrBeginFrame)lookup(active_instance, "xrBeginFrame");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
clear_gl_errors("xrBeginFrame");
if (frame_open && frame_balance) {
PFN_xrEndFrame end = (PFN_xrEndFrame)lookup(active_instance, "xrEndFrame");
XrFrameEndInfo empty = {XR_TYPE_FRAME_END_INFO, NULL, last_begin_display_time ? last_begin_display_time
: last_predicted_time,
XR_ENVIRONMENT_BLEND_MODE_OPAQUE, 0, NULL};
XrResult r = end ? end(session, &empty) : XR_ERROR_FUNCTION_UNSUPPORTED;
static int logged;
if (logged++ < 3) LOG("frame_balance: ended an open frame before xrBeginFrame (result %d)", r);
__atomic_add_fetch(&frame_balanced, 1, __ATOMIC_RELAXED);
}
XrResult result = fn(session, info);
__atomic_add_fetch(&frame_begins, 1, __ATOMIC_RELAXED);
if (result == XR_FRAME_DISCARDED) __atomic_add_fetch(&frame_discarded, 1, __ATOMIC_RELAXED);
if (XR_SUCCEEDED(result)) {
frame_open = 1;
last_begin_display_time = last_predicted_time;
}
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInfo *info) {
PFN_xrEndFrame fn = (PFN_xrEndFrame)lookup(active_instance, "xrEndFrame");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
frame_open = 0;
clear_gl_errors("xrEndFrame");
__atomic_add_fetch(&frame_ends, 1, __ATOMIC_RELAXED);
snap_end_frame(info);
if(surface_native)++sc_serial;
if (eye_debug) eye_debug_end_frame(session, info);
{ // frame pacing statistics every ~5 s: fps and submitted-vs-predicted display time
static struct timespec start;
static int frames;
static long long drift_sum, drift_max;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (!frames) start = now;
++frames;
long long drift = info ? (long long)(info->displayTime - last_predicted_time) : 0;
drift_sum += drift < 0 ? -drift : drift;
if ((drift < 0 ? -drift : drift) > drift_max) drift_max = drift < 0 ? -drift : drift;
double elapsed = (now.tv_sec - start.tv_sec) + (now.tv_nsec - start.tv_nsec) * 1e-9;
if (elapsed >= 5.0) {
LOG("pacing: %.1f fps, displayTime vs predicted: avg %.2f ms, max %.2f ms", frames / elapsed,
drift_sum / (double)frames / 1e6, drift_max / 1e6);
if (layer_debug)
LOG("layer_debug: swapchain images: %d acquired, %d waited (%d not ok), %d released",
__atomic_exchange_n(&sc_acquires, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&sc_waits, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&sc_wait_fails, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&sc_releases, 0, __ATOMIC_RELAXED));
if (layer_debug)
LOG("layer_debug: frames: %d xrBeginFrame (%d discarded), %d xrEndFrame, %d balanced",
__atomic_exchange_n(&frame_begins, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&frame_discarded, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&frame_ends, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&frame_balanced, 0, __ATOMIC_RELAXED));
if (layer_debug)
LOG("layer_debug: input: %d xrSyncActions (%d ok, last %d), %d bool reads, %d pressed",
__atomic_exchange_n(&input_syncs, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&input_sync_ok, 0, __ATOMIC_RELAXED), input_last_sync_result,
__atomic_exchange_n(&input_bool_reads, 0, __ATOMIC_RELAXED),
__atomic_exchange_n(&input_bool_true, 0, __ATOMIC_RELAXED));
pose_time_report();
frames = 0; drift_sum = drift_max = 0;
}
}
if (swapchain_pool && info && info->layerCount > 64) return XR_ERROR_LAYER_LIMIT_EXCEEDED;
if ((!swapchain_pool && !cube_standin && !layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
info->layerCount > 64)
return fn(session, info);
if (layer_debug) { // the submitted layer list (order, flags, eyes, chained structs) whenever its shape changes
static char last[1024];
char now[1024];
size_t used = 0;
for (uint32_t i = 0; i < info->layerCount && used < sizeof(now) - 64; ++i) {
const XrCompositionLayerBaseHeader *l = info->layers[i];
if (!l) continue;
int eye = -1;
if (l->type == XR_TYPE_COMPOSITION_LAYER_QUAD) eye = ((const XrCompositionLayerQuad *)l)->eyeVisibility;
if (l->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) eye = ((const XrCompositionLayerCylinderKHR *)l)->eyeVisibility;
if (l->type == XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR) eye = ((const XrCompositionLayerEquirect2KHR *)l)->eyeVisibility;
used += (size_t)snprintf(now + used, sizeof(now) - used, " [%d f=0x%llx e=%d", l->type,
(unsigned long long)l->layerFlags, eye);
if (l->type == XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR) { // mapping + source size: 360 vs 180, SBS halves
const XrCompositionLayerEquirect2KHR *q = (const XrCompositionLayerEquirect2KHR *)l;
used += (size_t)snprintf(now + used, sizeof(now) - used, " h=%.2f u=%.2f l=%.2f r=%.1f rect=%d,%d %dx%d",
q->centralHorizontalAngle, q->upperVerticalAngle, q->lowerVerticalAngle,
q->radius, q->subImage.imageRect.offset.x, q->subImage.imageRect.offset.y,
q->subImage.imageRect.extent.width, q->subImage.imageRect.extent.height);
}
for (const XrBaseInStructure *n = (const XrBaseInStructure *)l->next; n && used < sizeof(now) - 48; n = n->next) {
used += (size_t)snprintf(now + used, sizeof(now) - used, " n=%d", n->type);
if (n->type == XR_TYPE_COMPOSITION_LAYER_ALPHA_BLEND_FB) { // src/dst factors: colour, then alpha
const XrCompositionLayerAlphaBlendFB *ab = (const XrCompositionLayerAlphaBlendFB *)n;
used += (size_t)snprintf(now + used, sizeof(now) - used, "(%d,%d/%d,%d)", ab->srcFactorColor,
ab->dstFactorColor, ab->srcFactorAlpha, ab->dstFactorAlpha);
}
}
used += (size_t)snprintf(now + used, sizeof(now) - used, "]");
}
if (strcmp(now, last)) {
LOG("layer_debug: frame layers blend=%d:%s", info->environmentBlendMode, now);
snprintf(last, sizeof(last), "%s", now);
}
}
// equirect_emul: the frame's 360 layers are replaced by one emulated projection layer (at the first one's place)
const XrCompositionLayerBaseHeader *emul_layer_out = NULL;
int emul_first = layer_fix && emul_active() ? emul_prepare_frame(info, &emul_layer_out) : -1;
const XrCompositionLayerBaseHeader *kept[64];
XrCompositionLayerProjection projections[64];
XrCompositionLayerProjectionView views[64][2];
XrCompositionLayerQuad quads[64];
XrCompositionLayerQuad fade_out; // fade_quad: at most one fade layer per frame
int faded = 0;
const XrCompositionLayerBaseHeader *own_fade = NULL; // fade_quad's black quad (FrameBridge's own)
cube_outputs_begin();
uint32_t count = 0, dropped = 0, swapped = 0, passthrough = 0;
const XrCompositionLayerBaseHeader *surface_projections[SURF_MAX];
int surface_projection_count = 0;
for (uint32_t i = 0; i < info->layerCount; ++i) {
const XrCompositionLayerBaseHeader *layer = info->layers[i];
if (!layer) { ++dropped; continue; }
if (emul_is_equirect(layer)) {
const XrCompositionLayerBaseHeader *replacement = surface_native ? surf_projection_frame(session, info, layer) : NULL;
if (replacement) {
int already = 0;
for (int k = 0; k < surface_projection_count; ++k) already |= surface_projections[k] == replacement;
if (!already && surface_projection_count < SURF_MAX) {
surface_projections[surface_projection_count++] = replacement;
// An opaque native hemisphere covering the guarded stereo
// view hides every earlier layer. Cull only that hidden
// prefix: video becomes the primary projection, while
// later subtitles/menus keep their original order. Fades
// and uncovered views use the ordinary layered path.
const XrCompositionLayerBaseHeader *combined=NULL;
if(surface_native && count==1 && kept[0]->type==XR_TYPE_COMPOSITION_LAYER_PROJECTION && surf_composite_candidate(info,layer))
combined=surf_composite_frame(session,info,(const void*)kept[0],replacement);
if(combined) {kept[0]=combined;++swapped;}
else if (surf_projection_occludes(replacement)) {
dropped += count;
count = 0;
passthrough = 0;
}
if(!combined)kept[count++] = replacement;
}
++swapped;
continue;
}
}
if (emulate_passthrough && layer->type == XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB) {
++passthrough; // replaced by the runtime's camera environment below
continue;
}
{
static struct { int type; const void *key; } seen[48];
static int nseen;
const void *key = NULL;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) key = (const void *)(uintptr_t)((const XrCompositionLayerQuad *)layer)->subImage.swapchain;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) key = (const void *)(uintptr_t)((const XrCompositionLayerCylinderKHR *)layer)->subImage.swapchain;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR) key = (const void *)(uintptr_t)((const XrCompositionLayerEquirect2KHR *)layer)->subImage.swapchain;
int known = 0;
for (int k = 0; k < nseen; ++k) known |= seen[k].type == (int)layer->type && seen[k].key == key;
if (!known && nseen < 48) {
seen[nseen].type = layer->type; seen[nseen++].key = key;
int tagged = 0;
for (const XrBaseInStructure *n = (const XrBaseInStructure *)layer->next; n; n = n->next)
tagged |= n->type == 1000040000 && (((const XrCompositionLayerImageLayoutFB *)n)->flags & 1);
LOG("new layer: type=%d swapchain=%p flip_tag=%d usable=%d", layer->type, key, tagged, layer_usable(layer));
}
}
if (emul_first >= 0 && emul_layer_out && emul_is_equirect(layer)) {
if ((int)i == emul_first) kept[count++] = emul_layer_out; // all 360 layers are drawn into it
++swapped;
continue;
}
// A per-eye cylinder pair (eyeVisibility LEFT + RIGHT, e.g. Doom3Quest/PreyVR's curved menu) would expand to
// twice the strips; the Frame's runtime composites a quad to both eyes and rejects a frame with that many
// per-eye quad layers (XR_ERROR_LAYER_INVALID). Drop the right eye and show the left one's strips to both.
if (layer_fix && cylinder_strips_on && no_cylinder && layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR &&
((const XrCompositionLayerCylinderKHR *)layer)->eyeVisibility == XR_EYE_VISIBILITY_RIGHT) {
++dropped;
continue;
}
if (layer_fix && cylinder_strips_on && no_cylinder && layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR &&
runtime_swapchain(((const XrCompositionLayerCylinderKHR *)layer)->subImage.swapchain)) {
// keep the whole frame within the runtime's layer limit: every later layer still needs a slot
int budget = 16 - (int)count - (int)(info->layerCount - i - 1);
int n = cylinder_strips((const XrCompositionLayerCylinderKHR *)layer, &quads[count], budget < 64 - (int)count ? budget : 64 - (int)count);
if (n > 0) {
for (int k = 0; k < n; ++k) kept[count + k] = (const XrCompositionLayerBaseHeader *)&quads[count + k];
count += (uint32_t)n;
++swapped;
continue;
}
}
if (layer && (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD || layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) &&
!surf_prepare_layer(layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD
? ((const XrCompositionLayerQuad *)layer)->subImage.swapchain
: ((const XrCompositionLayerCylinderKHR *)layer)->subImage.swapchain)) {
++dropped; // an emulated video surface without a frame yet
continue;
}
if (cube_quads && layer->type == XR_TYPE_COMPOSITION_LAYER_CUBE_KHR &&
count + 6 + info->layerCount - i - 1 <= 16 && count + 6 <= 64 &&
cube_layer_quads(session, info, i, &quads[count])) {
for (int f = 0; f < 6; ++f) kept[count + f] = (const XrCompositionLayerBaseHeader *)&quads[count + f];
count += 6;
++swapped;
continue;
}
if (layer_uses_standin(layer)) { // a cube layer whose swapchain the runtime refused (cube_standin.c)
static int logged;
if (logged++ < 3) LOG("cube_standin: dropped a layer (type=%d) that shows a stand-in swapchain", layer->type);
++dropped;
continue;
}
if (!layer || (layer_fix && !layer_usable(layer))) { ++dropped; continue; }
if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
const XrCompositionLayerQuad *flipped = flip_quad(session, (const XrCompositionLayerQuad *)layer, &quads[count]);
if (flipped) { layer = (const XrCompositionLayerBaseHeader *)flipped; ++swapped; }
}
if (flip_quads && layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
// Rotate 180 degrees about the quad's local X axis: shows the image upright without mirroring it.
quads[count] = *(const XrCompositionLayerQuad *)layer;
XrQuaternionf q = quads[count].pose.orientation; // q * (1,0,0,0)
quads[count].pose.orientation = (XrQuaternionf){q.w, q.z, -q.y, -q.x}; // (x,y,z,w) of q * (1,0,0 | 0)
layer = (const XrCompositionLayerBaseHeader *)&quads[count];
++swapped;
}
int blend_alpha = passthrough && !alpha_blend_failed && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION;
if (blend_alpha && !(swap_eyes && ((const XrCompositionLayerProjection *)layer)->viewCount == 2)) {
// Let the camera show through where the app rendered transparent pixels.
projections[count] = *(const XrCompositionLayerProjection *)layer;
projections[count].layerFlags |= XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
// the game's XR_FB_composition_layer_alpha_blend factors (ignored by the Frame's runtime), e.g. Unreal's
// inverted scene alpha: the camera showed where the game drew and black where it didn't (GitHub #329)
if (alpha_blend_emul && alpha_blend_apply(&projections[count]) == 2)
projections[count].layerFlags &= ~(XrCompositionLayerFlags)XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
}
if (strip_depth && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
((const XrCompositionLayerProjection *)layer)->viewCount == 2) {
// Drop XrCompositionLayerDepthInfoKHR: bad app depth makes positional reprojection warp the image.
const XrCompositionLayerProjection *src = (const XrCompositionLayerProjection *)layer;
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count]) projections[count] = *src;
views[count][0] = src->views[0];
views[count][1] = src->views[1];
views[count][0].next = NULL;
views[count][1].next = NULL;
projections[count].views = views[count];
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
static int logged;
if (!logged++) LOG("strip_depth: removed depth info from projection layer");
++swapped;
}
{
// XR_KHR_composition_layer_color_scale_bias (1000034000) on the layer itself: logged when its values
// change (layer_debug); with strip_color_bias the runtime never gets it (Vader Immortal: the Frame's
// runtime allocates GPU memory for it every frame and never frees it)
const XrBaseInStructure *head = (const XrBaseInStructure *)layer->next;
const XrCompositionLayerColorScaleBiasKHR *cb = NULL;
for (const XrBaseInStructure *n = head; n; n = n->next)
if (n->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) cb = (const XrCompositionLayerColorScaleBiasKHR *)n;
if (cb && layer_debug) {
static float last[8] = {-1};
float now[8] = {cb->colorScale.r, cb->colorScale.g, cb->colorScale.b, cb->colorScale.a,
cb->colorBias.r, cb->colorBias.g, cb->colorBias.b, cb->colorBias.a};
static int logged;
if (memcmp(now, last, sizeof(now)) && logged++ < 100) {
LOG("layer_debug: layer type=%d color scale %.2f %.2f %.2f %.2f bias %.2f %.2f %.2f %.2f", layer->type,
now[0], now[1], now[2], now[3], now[4], now[5], now[6], now[7]);
memcpy(last, now, sizeof(now));
}
}
if (cb && ((strip_color_bias && (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION ||
layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD)) ||
(fade_quad > 1 && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION))) {
const XrBaseInStructure *rest = head; // skip leading color structs; one further down drops the chain
while (rest && rest->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) rest = rest->next;
for (const XrBaseInStructure *n = rest; n; n = n->next)
if (n->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) rest = NULL;
if (strip_color_bias > 1) rest = NULL; // 2: the layer's whole extension chain (diagnostics)
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count])
projections[count] = *(const XrCompositionLayerProjection *)layer;
projections[count].next = rest;
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
} else {
if (layer != (const XrCompositionLayerBaseHeader *)&quads[count])
quads[count] = *(const XrCompositionLayerQuad *)layer;
quads[count].next = rest;
layer = (const XrCompositionLayerBaseHeader *)&quads[count];
}
static int logged;
if (!logged++) LOG("strip_color_bias: removed the color scale/bias from a layer");
++swapped;
}
}
if (hide_space_warp && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
((const XrCompositionLayerProjection *)layer)->viewCount == 2) {
// The game may still attach XrCompositionLayerSpaceWarpInfoFB (1000171000) to its views: OVRPort's
// dispatcher offers XR_FB_space_warp itself even when hidden here. Unlink it (keeping what follows when
// it heads the chain, else the whole chain) so the runtime never uses the motion vectors.
const XrCompositionLayerProjection *src = (const XrCompositionLayerProjection *)layer;
int found = 0;
for (uint32_t v = 0; v < 2; ++v)
for (const XrBaseInStructure *n = (const XrBaseInStructure *)src->views[v].next; n; n = n->next)
found |= n->type == 1000171000;
if (found) {
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count]) projections[count] = *src;
for (uint32_t v = 0; v < 2; ++v) {
views[count][v] = src->views[v];
const XrBaseInStructure *head = (const XrBaseInStructure *)src->views[v].next;
if (head && head->type == 1000171000) views[count][v].next = head->next;
else if (head) views[count][v].next = NULL;
}
projections[count].views = views[count];
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
static int logged;
if (!logged++) LOG("hide_space_warp: removed space warp info from the eye images");
++swapped;
}
}
if (swap_eyes && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
((const XrCompositionLayerProjection *)layer)->viewCount == 2) {
// Show each eye the image the game rendered for it: keep poses/FOVs, swap sub-images.
const XrCompositionLayerProjection *cur = (const XrCompositionLayerProjection *)layer;
XrCompositionLayerProjectionView v0 = cur->views[0], v1 = cur->views[1];
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count]) projections[count] = *cur;
views[count][0] = v0;
views[count][1] = v1;
views[count][0].subImage = v1.subImage;
views[count][1].subImage = v0.subImage;
projections[count].views = views[count];
if (blend_alpha)
projections[count].layerFlags |= XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
++swapped;
}
if (rect_clamp) {
// Keep every image rect inside its swapchain: Unity/OVRPlugin can size the eye area a few pixels past
// the image on some Frames (PowerWash Simulator: rect 268+1656 on a 1920 wide swapchain) and SteamVR
// then rejects every frame with XR_ERROR_SWAPCHAIN_RECT_INVALID (-25) (GitHub #39).
uint32_t w, h;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
((const XrCompositionLayerProjection *)layer)->viewCount <= 2) {
const XrCompositionLayerProjection *cur = (const XrCompositionLayerProjection *)layer;
int over = 0;
for (uint32_t v = 0; v < cur->viewCount; ++v) {
XrRect2Di r = cur->views[v].subImage.imageRect;
if (swapchain_size(cur->views[v].subImage.swapchain, &w, &h) && clamp_rect(&r, w, h)) over = 1;
}
if (over) {
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count]) projections[count] = *cur;
if (projections[count].views != views[count])
for (uint32_t v = 0; v < cur->viewCount; ++v) views[count][v] = cur->views[v];
for (uint32_t v = 0; v < cur->viewCount; ++v) {
int how = swapchain_size(views[count][v].subImage.swapchain, &w, &h)
? clamp_rect(&views[count][v].subImage.imageRect, w, h) : 0;
if (how == 2) {
static int logged_empty;
if (logged_empty++ < 3)
LOG("rect_clamp: view %u empty image rect -> full swapchain %ux%u", v, w, h);
} else if (how) {
static int logged;
if (logged++ < 3)
LOG("rect_clamp: view %u image rect clamped to %dx%d at %d,%d (swapchain %ux%u)", v,
views[count][v].subImage.imageRect.extent.width,
views[count][v].subImage.imageRect.extent.height,
views[count][v].subImage.imageRect.offset.x,
views[count][v].subImage.imageRect.offset.y, w, h);
}
}
projections[count].views = views[count];
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
++swapped;
}
} else if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
XrRect2Di r = q->subImage.imageRect;
int how = swapchain_size(q->subImage.swapchain, &w, &h) ? clamp_rect(&r, w, h) : 0;
if (how) {
if (layer != (const XrCompositionLayerBaseHeader *)&quads[count]) quads[count] = *q;
quads[count].subImage.imageRect = r;
layer = (const XrCompositionLayerBaseHeader *)&quads[count];
++swapped;
static int logged, logged_empty;
if (how == 2 && logged_empty++ < 3)
LOG("rect_clamp: quad empty image rect -> full swapchain %ux%u", w, h);
else if (how == 1 && !logged++)
LOG("rect_clamp: quad image rect clamped (swapchain %ux%u)", w, h);
}
}
}
// SteamVR ignores Batman's projection colour-scale blackout. An opaque
// frame has a black background, so omit a fully black primary scene;
// later loading UI layers keep their order. Never do this for passthrough,
// alpha-blended projections, partial fades, or an already composed video.
if(surface_native && i==0 && count==0 &&
info->environmentBlendMode==XR_ENVIRONMENT_BLEND_MODE_OPAQUE &&
layer->type==XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
!(layer->layerFlags&XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT)) {
const XrCompositionLayerColorScaleBiasKHR *black=NULL;
for(const XrBaseInStructure *n=layer->next;n;n=n->next)
if(n->type==XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR)black=(const void*)n;
if(black && black->colorScale.r==0 && black->colorScale.g==0 && black->colorScale.b==0 &&
black->colorScale.a==1 && black->colorBias.r==0 && black->colorBias.g==0 &&
black->colorBias.b==0 && black->colorBias.a==0) {
static int black_logged;
if(!black_logged++)LOG("loading blackout: omitted fully black primary projection; later UI retained");
++dropped;continue;
}
}
kept[count++] = layer;
// fade_quad: a dimmed projection layer gets the black quad right after it (the colour scale is read from
// the app's own layer: fade_quad=2 removed it from the copy submitted)
int level = fade_quad && !faded && count < 64 ? fade_level(info->layers[i]) : 0;
const XrCompositionLayerBaseHeader *fade_l = level ? fade_layer(session, level, count, &fade_out) : NULL;
if (fade_l) { kept[count++] = fade_l; own_fade = fade_l; faded = 1; ++swapped; }
}
// The runtime refuses frames with more layers than maxLayerCount (XR_ERROR_LAYER_LIMIT_EXCEEDED: Punch Fit sends
// 21+). Keep every projection layer and FrameBridge's own fade quad, then the app's other layers in submission
// order until the limit (from PR #246; the earliest app layers were what Punch Fit needed).
uint32_t max_layers = runtime_max_layers ? runtime_max_layers : 16, trimmed_layers = 0;
if (count > max_layers) {
const XrCompositionLayerBaseHeader *trimmed[64];
uint32_t tcount = 0, reserved = 0, res_kept = 0, other_kept = 0;
for (uint32_t i = 0; i < count; ++i)
if (kept[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION || kept[i] == own_fade) ++reserved;
if (reserved > max_layers) reserved = max_layers;
uint32_t budget = max_layers - reserved;
for (uint32_t i = 0; i < count; ++i) {
int keep_always = kept[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION || kept[i] == own_fade;
uint32_t *n = keep_always ? &res_kept : &other_kept;
if (*n < (keep_always ? reserved : budget)) { trimmed[tcount++] = kept[i]; ++*n; }
else ++trimmed_layers;
}
static int layer_limit_logged;
if (layer_limit_logged++ < 5)
LOG("xrEndFrame: %u layers exceed the runtime limit %u: left out %u of the app's other layers "
"(kept %u projection/fade layers and %u others)", count, max_layers, trimmed_layers, res_kept,
other_kept);
for (uint32_t i = 0; i < tcount; ++i) kept[i] = trimmed[i];
count = tcount;
}
PoolFrame pool_frame;
if (swapchain_pool && session == pool_session) {
count = pool_rewrite_layers(kept, count, &pool_frame);
++swapped;
}
XrFrameEndInfo fixed = *info;
fixed.layers = kept;
fixed.layerCount = count;
if (passthrough && !alpha_blend_failed) fixed.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND;
static int warned, failures, passthrough_logged;
if (dropped && warned++ < 5) LOG("xrEndFrame: dropped %u unusable layer(s)", dropped);
if (passthrough && !passthrough_logged++) LOG("xrEndFrame: passthrough layer -> ALPHA_BLEND environment");
static struct { XrSwapchain swapchain; XrRect2Di rect; uint32_t array; } last_rect[2];
static double last_rect_log[2];
struct timespec rect_now;
clock_gettime(CLOCK_MONOTONIC, &rect_now);
double rect_seconds = rect_now.tv_sec + rect_now.tv_nsec / 1e9;
for (uint32_t i = 0; i < info->layerCount; ++i) {
const XrCompositionLayerBaseHeader *l = info->layers[i];
if (!l || l->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION) continue;
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)l;
for (uint32_t v = 0; v < p->viewCount && v < 2; ++v) {
const XrSwapchainSubImage *sub = &p->views[v].subImage;
if ((last_rect[v].swapchain != sub->swapchain || last_rect[v].array != sub->imageArrayIndex ||
memcmp(&last_rect[v].rect, &sub->imageRect, sizeof(sub->imageRect))) &&
rect_seconds - last_rect_log[v] >= 1.0) {
LOG("projection view %u rect change: swapchain=%p rect=%d,%d %dx%d array=%u", v,
(void *)(uintptr_t)sub->swapchain, sub->imageRect.offset.x, sub->imageRect.offset.y,
sub->imageRect.extent.width, sub->imageRect.extent.height, sub->imageArrayIndex);
last_rect[v].swapchain = sub->swapchain;
last_rect[v].rect = sub->imageRect;
last_rect[v].array = sub->imageArrayIndex;
last_rect_log[v] = rect_seconds;
}
}
}
static int views_logged, quads_logged;
if (views_logged < 3 || quads_logged < 10) { // describe the stereo layer layout and quads
for (uint32_t i = 0; i < fixed.layerCount; ++i) {
const XrCompositionLayerBaseHeader *l = fixed.layers[i];
if (l && l->type == XR_TYPE_COMPOSITION_LAYER_QUAD && quads_logged < 10) {
const XrCompositionLayerQuad *qd = (const XrCompositionLayerQuad *)l;
char chain[160] = "";
size_t used = 0;
for (const XrBaseInStructure *n = (const XrBaseInStructure *)qd->next; n && used < sizeof(chain) - 16; n = n->next) {
int w = snprintf(chain + used, sizeof(chain) - used, " %d", n->type);
if (n->type == 1000040000) // XrCompositionLayerImageLayoutFB
w += snprintf(chain + used + w, sizeof(chain) - used - w, "(flags=0x%llx)",
(unsigned long long)((const XrCompositionLayerImageLayoutFB *)n)->flags);
used += w;
}
LOG("quad layer %u: swapchain=%p flags=0x%llx size=%.2fx%.2f pos=%.2f,%.2f,%.2f next:%s", i,
(void *)(uintptr_t)qd->subImage.swapchain, (unsigned long long)qd->layerFlags,
qd->size.width, qd->size.height, qd->pose.position.x, qd->pose.position.y, qd->pose.position.z, chain);
++quads_logged;
}
if (!l || l->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION || views_logged >= 3) continue;
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)l;
for (uint32_t v = 0; v < p->viewCount; ++v) {
const XrCompositionLayerProjectionView *pv = &p->views[v];
if (v == 0 && pv->next) LOG("projection view 0 chains struct type %d", ((const XrBaseInStructure *)pv->next)->type);
LOG("projection view %u: space=%p%s swapchain=%p rect=%d,%d %dx%d array=%u pos=%.3f,%.3f,%.3f fov=%.2f/%.2f",
v, (void *)(uintptr_t)p->space, p->space == view_space_handle ? "(VIEW)" : "", (void *)(uintptr_t)pv->subImage.swapchain, pv->subImage.imageRect.offset.x,
pv->subImage.imageRect.offset.y, pv->subImage.imageRect.extent.width,
pv->subImage.imageRect.extent.height, pv->subImage.imageArrayIndex, pv->pose.position.x,
pv->pose.position.y, pv->pose.position.z, pv->fov.angleLeft, pv->fov.angleRight);
}
++views_logged;
}
}
XrResult result = fn(session, (dropped || swapped || passthrough) ? &fixed : info);
if (result == XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED && passthrough && !alpha_blend_failed) {
alpha_blend_failed = 1; // runtime refuses camera blending: resubmit as a normal opaque frame
LOG("xrEndFrame: ALPHA_BLEND unsupported, passthrough falls back to opaque");
return xrEndFrame(session, info);
}
if (XR_FAILED(result) && failures++ < 3) { // describe rejected frames to aid debugging
LOG("xrEndFrame failed %d: blend=%d layers=%u", result, fixed.environmentBlendMode, fixed.layerCount);
for (uint32_t i = 0; i < fixed.layerCount; ++i)
LOG(" layer %u type=%d flags=0x%llx", i, fixed.layers[i]->type,
(unsigned long long)fixed.layers[i]->layerFlags);
}
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrLocateHandJointsEXT(XrHandTrackerEXT tracker,
const XrHandJointsLocateInfoEXT *info, XrHandJointLocationsEXT *locations) {
if (!controller_fix) {
PFN_xrLocateHandJointsEXT fn = (PFN_xrLocateHandJointsEXT)lookup(active_instance, "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)lookup(active_instance, "xrGetCurrentInteractionProfile");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
pthread_mutex_lock(&bindings_lock);
XrResult result = detached_current_profile(user, state, fn(session, user, state));
if (input_diag && XR_SUCCEEDED(result) && state) input_diag_current_profile(user, state->interactionProfile);
if (layer_debug && active_instance) { // which device each hand has, as the runtime reports it
PFN_xrPathToString str = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
char who[XR_MAX_PATH_LENGTH] = "?", what[XR_MAX_PATH_LENGTH] = "(none)";
uint32_t n = 0;
if (str) {
str(active_instance, user, sizeof(who), &n, who);
if (XR_SUCCEEDED(result) && state && state->interactionProfile)
str(active_instance, state->interactionProfile, sizeof(what), &n, what);
}
static int logged;
if (logged++ < 40) LOG("layer_debug: interaction profile of %s: %s (result %d)", who, what, result);
}
if (XR_SUCCEEDED(result) && state) {
// A Frame binding replaces SteamVR's Touch remap; Quest games still expect a Touch-shaped profile.
if (quest_buttons && quest_buttons_enabled && generated_frame_bindings && !app_suggested_frame &&
state->interactionProfile) {
PFN_xrPathToString str = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
PFN_xrStringToPath path = (PFN_xrStringToPath)lookup(active_instance, "xrStringToPath");
char name[XR_MAX_PATH_LENGTH];
uint32_t size = 0;
XrPath touch;
if (str && path && XR_SUCCEEDED(str(active_instance, state->interactionProfile, sizeof(name), &size, name)) &&
!strcmp(name, FRAME_PROFILE) && XR_SUCCEEDED(path(active_instance, TOUCH_PROFILE, &touch)))
state->interactionProfile = touch;
}
current_profile_fix(user, state);
}
pthread_mutex_unlock(&bindings_lock);
return result;
}
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateInstanceExtensionProperties(const char *layer, uint32_t capacity,
uint32_t *count, XrExtensionProperties *properties) {
PFN_xrEnumerateInstanceExtensionProperties fn = (PFN_xrEnumerateInstanceExtensionProperties)lookup(
XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
if (!fn) return XR_ERROR_INITIALIZATION_FAILED;
if (!count || (capacity && !properties)) return XR_ERROR_VALIDATION_FAILURE;
if ((!foveation_fix && !hide_fb_foveation && !hide_space_warp && !passthrough_emul && !scene_emul && !controller_models &&
!fb_color_space) || layer)
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 + 2 + SCENE_EXTENSION_COUNT, 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; }
int has_color_space = 0;
uint32_t kept = 0;
for (uint32_t i = 0; i < total; ++i) {
if (!strcmp(all[i].extensionName, XR_FB_COLOR_SPACE_EXTENSION_NAME)) has_color_space = 1;
if (!strcmp(all[i].extensionName, "XR_FB_passthrough")) runtime_has_fb_passthrough = 1;
if (!strcmp(all[i].extensionName, XR_FB_RENDER_MODEL_EXTENSION_NAME)) runtime_has_render_model = 1;
if (hide_fb_foveation && fb_is_foveation_extension(all[i].extensionName)) continue;
if (foveation_fix && strstr(all[i].extensionName, "foveation")) continue;
if (hide_space_warp && !strcmp(all[i].extensionName, "XR_FB_space_warp")) continue;
if (properties && kept < capacity) properties[kept] = all[i];
++kept;
}
if (scene_emul) {
for (size_t e = 0; e < SCENE_EXTENSION_COUNT; ++e) {
int present = 0;
for (uint32_t i = 0; i < total; ++i) present |= !strcmp(all[i].extensionName, scene_extensions[e]);
if (present) continue;
XrExtensionProperties fake = {XR_TYPE_EXTENSION_PROPERTIES, NULL, "", 1};
snprintf(fake.extensionName, sizeof(fake.extensionName), "%s", scene_extensions[e]);
if (properties && kept < capacity) properties[kept] = fake;
++kept;
}
}
if (passthrough_emul && !runtime_has_fb_passthrough) {
XrExtensionProperties fake = {XR_TYPE_EXTENSION_PROPERTIES, NULL, "XR_FB_passthrough", 4};
if (properties && kept < capacity) properties[kept] = fake;
++kept;
}
if (controller_models && render_models_available && !runtime_has_render_model) {
XrExtensionProperties fake = {XR_TYPE_EXTENSION_PROPERTIES, NULL, XR_FB_RENDER_MODEL_EXTENSION_NAME,
XR_FB_render_model_SPEC_VERSION};
if (properties && kept < capacity) properties[kept] = fake;
++kept;
}
if (fb_color_space && !has_color_space) {
XrExtensionProperties fake = {XR_TYPE_EXTENSION_PROPERTIES, NULL, XR_FB_COLOR_SPACE_EXTENSION_NAME, 3};
if (properties && kept < capacity) properties[kept] = fake;
++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)lookup(XR_NULL_HANDLE, "xrEnumerateApiLayerProperties");
return fn ? fn(capacity, count, properties) : XR_ERROR_INITIALIZATION_FAILED;
}
static XRAPI_ATTR int XRAPI_CALL framebridge_hide_fb_foveation(void) {
pthread_once(&init_once, initialize);
return hide_fb_foveation;
}
static XRAPI_ATTR XrResult XRAPI_CALL fb_hide_update_swapchain(XrSwapchain swapchain, const XrSwapchainStateBaseHeaderFB *state) {
if (state && state->type == XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB) {
LOG("hide_fb_foveation: refused foveation profile update");
return XR_ERROR_FEATURE_UNSUPPORTED;
}
PFN_xrUpdateSwapchainFB fn = (PFN_xrUpdateSwapchainFB)lookup(active_instance, "xrUpdateSwapchainFB");
return fn ? fn(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
// Generic calls the shim routes here for the emulated room (OVRPort's dispatcher refuses fake handles' functions):
// only fake room spaces and FramePort's own room events are handled locally; every real call goes on to OVRPort's
// dispatcher, whose own work (headset identity, events, controller spaces) was skipped for every scene game before.
static PFN_xrVoidFunction dispatcher_proc(const char *name) {
static PFN_xrGetInstanceProcAddr gipa;
static int tried;
if (!tried) {
tried = 1;
void *h = dlopen("libopenxr_loader.so", RTLD_NOW | RTLD_NOLOAD); // OVRPort's dispatcher (xrshim's next)
if (h) gipa = (PFN_xrGetInstanceProcAddr)dlsym(h, "xrGetInstanceProcAddr");
LOG("scene: shim calls for real handles go to OVRPort's dispatcher (%s)", gipa ? "found" : "MISSING");
}
PFN_xrVoidFunction fn = NULL;
if (gipa && active_instance) gipa(active_instance, name, &fn);
return fn;
}
static XRAPI_ATTR XrResult XRAPI_CALL shim_locate_space(XrSpace space, XrSpace base, XrTime time, XrSpaceLocation *loc) {
PFN_xrLocateSpace next = is_fake_space(space) || is_fake_space(base) ? NULL
: (PFN_xrLocateSpace)dispatcher_proc("xrLocateSpace");
return next ? next(space, base, time, loc) : xrLocateSpace(space, base, time, loc);
}
static XrResult shim_locate_spaces_common(const char *name, XrSession session, const XrSpacesLocateInfo *info,
XrSpaceLocations *locations) {
int any_fake = info && is_fake_space(info->baseSpace);
for (uint32_t i = 0; info && i < info->spaceCount && !any_fake; ++i) any_fake = is_fake_space(info->spaces[i]);
PFN_xrLocateSpaces next = any_fake ? NULL : (PFN_xrLocateSpaces)dispatcher_proc(name);
return next ? next(session, info, locations) : locate_spaces_common(name, session, info, locations);
}
static XRAPI_ATTR XrResult XRAPI_CALL shim_locate_spaces(XrSession s, const XrSpacesLocateInfo *i, XrSpaceLocations *l) {
return shim_locate_spaces_common("xrLocateSpaces", s, i, l);
}
static XRAPI_ATTR XrResult XRAPI_CALL shim_locate_spaces_khr(XrSession s, const XrSpacesLocateInfo *i, XrSpaceLocations *l) {
return shim_locate_spaces_common("xrLocateSpacesKHR", s, i, l);
}
static XRAPI_ATTR XrResult XRAPI_CALL shim_destroy_space(XrSpace space) {
PFN_xrDestroySpace next = is_fake_space(space) ? NULL : (PFN_xrDestroySpace)dispatcher_proc("xrDestroySpace");
return next ? next(space) : xrDestroySpace(space);
}
static XRAPI_ATTR XrResult XRAPI_CALL shim_poll_event(XrInstance instance, XrEventDataBuffer *event) {
scene_events_via_shim = 1;
if (event) { // room events first: OVRPort's dispatcher might drop event types it doesn't know
pthread_mutex_lock(&scene_lock);
int got = pop_event(event);
pthread_mutex_unlock(&scene_lock);
if (got) return XR_SUCCESS;
}
PFN_xrPollEvent next = (PFN_xrPollEvent)dispatcher_proc("xrPollEvent");
return next ? next(instance, event) : xrPollEvent(instance, event);
}
// For native/xrshim: emulated functions that overport's dispatcher doesn't know (it never asks us for them).
__attribute__((visibility("default"))) PFN_xrVoidFunction framebridge_extension_proc(const char *name) {
pthread_once(&init_once, initialize);
if (!name) return NULL;
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA"))
return (PFN_xrVoidFunction)fixed_recommended_resolution;
if (!strcmp(name, "framebridge_hide_fb_foveation"))
return (PFN_xrVoidFunction)framebridge_hide_fb_foveation;
if (hide_fb_foveation_vulkan) {
if (!strcmp(name, "xrCreateFoveationProfileFB")) return (PFN_xrVoidFunction)xrCreateFoveationProfileFB;
if (!strcmp(name, "xrUpdateSwapchainFB")) return (PFN_xrVoidFunction)fb_hide_update_swapchain;
}
if (!foveation_fix && (foveation_storage_fix || foveation_eye)) {
if (!strcmp(name, "xrCreateFoveationProfileFB")) return (PFN_xrVoidFunction)xrCreateFoveationProfileFB;
if (!strcmp(name, "xrGetFoveationEyeTrackedStateMETA")) return (PFN_xrVoidFunction)xrGetFoveationEyeTrackedStateMETA;
if (!strcmp(name, "xrDestroyFoveationProfileFB") || !strcmp(name, "xrUpdateSwapchainFB") ||
!strcmp(name, "xrGetSwapchainStateFB")) return lookup(active_instance, name);
}
if (surface_native) {
#define NATIVE_PROC(fn) if(!strcmp(name,#fn))return (PFN_xrVoidFunction)fn;
NATIVE_PROC(xrGetVulkanGraphicsDeviceKHR)
NATIVE_PROC(xrGetVulkanDeviceExtensionsKHR)
NATIVE_PROC(xrCreateVulkanDeviceKHR)
NATIVE_PROC(xrCreateVulkanInstanceKHR)
#undef NATIVE_PROC
}
// The shim bypasses OVRPort's fixed dispatch table for all scene calls, including consumers of fake spaces.
if (name && emulate_scene) {
PFN_xrVoidFunction scene = scene_emulation(name);
if (scene) return scene;
// xrGetSystemProperties stays with OVRPort (its headset identity; it calls down to our hook for the scene flags)
#define SCENE_PROC(fn, impl) if (!strcmp(name, #fn)) return (PFN_xrVoidFunction)impl;
SCENE_PROC(xrPollEvent, shim_poll_event)
SCENE_PROC(xrLocateSpace, shim_locate_space)
SCENE_PROC(xrLocateSpaces, shim_locate_spaces)
SCENE_PROC(xrLocateSpacesKHR, shim_locate_spaces_khr)
SCENE_PROC(xrDestroySpace, shim_destroy_space)
#undef SCENE_PROC
}
PFN_xrVoidFunction color = name ? color_space_emulation(name) : NULL;
return color ? color : (name ? render_model_emulation(name) : NULL);
}
XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const char *name,
PFN_xrVoidFunction *function) {
pthread_once(&init_once, initialize);
if (!next_gipa) return XR_ERROR_INITIALIZATION_FAILED;
if (!name || !function) return XR_ERROR_VALIDATION_FAILURE;
#define HOOK(fn) if (!strcmp(name, #fn)) { *function = (PFN_xrVoidFunction)fn; return XR_SUCCESS; }
if (!strcmp(name, "xrConvertTimespecTimeToTimeKHR") || !strcmp(name, "xrConvertTimeToTimespecTimeKHR")) {
PFN_xrVoidFunction real = NULL;
next_gipa(instance, name, &real);
if (!strcmp(name, "xrConvertTimespecTimeToTimeKHR")) {
runtime_timespec_to_time = (PFN_xrConvertTimespecTimeToTimeKHR)real;
*function = (PFN_xrVoidFunction)emu_timespec_to_time;
} else {
runtime_time_to_timespec = (PFN_xrConvertTimeToTimespecTimeKHR)real;
*function = (PFN_xrVoidFunction)emu_time_to_timespec;
}
return XR_SUCCESS;
}
PFN_xrVoidFunction emulated = passthrough_emulation(name);
if (!emulated) emulated = scene_emulation(name);
if (!emulated) emulated = render_model_emulation(name);
if (!emulated) emulated = color_space_emulation(name);
if (!emulated && emulate_scene && !strcmp(name, "xrLocateSpacesKHR")) emulated = (PFN_xrVoidFunction)emu_locate_spaces_khr;
if (emulated) { *function = emulated; return XR_SUCCESS; }
if (hide_fb_foveation_vulkan && !strcmp(name, "xrUpdateSwapchainFB")) {
*function = (PFN_xrVoidFunction)fb_hide_update_swapchain;
return XR_SUCCESS;
}
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA")) {
*function = (PFN_xrVoidFunction)fixed_recommended_resolution;
return XR_SUCCESS;
}
HOOK(xrGetSystemProperties)
HOOK(xrApplyHapticFeedback)
HOOK(xrCreateReferenceSpace)
HOOK(xrLocateViews)
HOOK(xrPollEvent)
HOOK(xrWaitFrame)
HOOK(xrCreateSession)
HOOK(xrEnumerateSwapchainImages)
HOOK(xrAcquireSwapchainImage)
HOOK(xrLocateSpace)
HOOK(xrLocateSpaces)
HOOK(xrDestroySpace)
HOOK(xrCreateInstance)
HOOK(xrEnumerateViewConfigurationViews)
HOOK(xrCreateSwapchain)
HOOK(xrCreateFoveationProfileFB)
HOOK(xrGetFoveationEyeTrackedStateMETA)
HOOK(xrDestroySwapchain)
HOOK(xrEndFrame)
HOOK(xrBeginFrame)
HOOK(xrLocateHandJointsEXT)
HOOK(xrGetCurrentInteractionProfile)
HOOK(xrEnumerateInstanceExtensionProperties)
// Per-game hooks: only installed when their setting is on, so other games run through exactly the same calls.
#define HOOK_AS(fn, impl) if (!strcmp(name, #fn)) { *function = (PFN_xrVoidFunction)impl; return XR_SUCCESS; }
if (surface_native) {
HOOK(xrGetVulkanGraphicsDeviceKHR)
HOOK(xrGetVulkanDeviceExtensionsKHR)
HOOK(xrCreateVulkanDeviceKHR)
HOOK(xrCreateVulkanInstanceKHR)
HOOK_AS(xrReleaseSwapchainImage, surf_composite_release)
HOOK_AS(xrWaitSwapchainImage, surf_composite_wait)
}
if (swapchain_pool) { HOOK_AS(xrDestroySession, hook_xrDestroySession) }
if (cube_standin) {
HOOK_AS(xrEndSession, cube_end_session)
HOOK_AS(xrDestroySession, hook_xrDestroySession)
}
if (equirect_emul) {
HOOK_AS(xrReleaseSwapchainImage, hook_xrReleaseSwapchainImage)
HOOK_AS(xrWaitSwapchainImage, hook_xrWaitSwapchainImage)
HOOK_AS(xrDestroySession, hook_xrDestroySession)
}
if (layer_debug || refresh_rate > 0 || equirect_emul) HOOK_AS(xrBeginSession, hook_xrBeginSession)
if (layer_debug || refresh_rate > 0) {
HOOK_AS(xrRequestDisplayRefreshRateFB, hook_request_refresh_rate)
}
// The Frame's runtime has no XR_FB_display_refresh_rate, but OVRPort's dispatcher offers it to the game and answers
// 0 Hz without it: the measured rate instead (the runtime's own answer when it has one).
if (instance != XR_NULL_HANDLE) HOOK_AS(xrGetDisplayRefreshRateFB, hook_get_refresh_rate)
if (surface_emul) HOOK_AS(xrCreateSwapchainAndroidSurfaceKHR, hook_xrCreateSwapchainAndroidSurfaceKHR)
if (eye_debug || release_wait) HOOK_AS(xrReleaseSwapchainImage, eye_hook_xrReleaseSwapchainImage)
if (swapchain_pool || layer_debug || cube_standin) { // pooled images, counters + cube stand-ins; every other wait/release hook above takes precedence
HOOK(xrWaitSwapchainImage)
HOOK(xrReleaseSwapchainImage)
}
if (cube_standin && (!strcmp(name, "xrUpdateSwapchainFB") || !strcmp(name, "xrGetSwapchainStateFB"))) {
XrResult result = next_gipa(instance, name, function); // only wrapped where the runtime has it
if (XR_FAILED(result) || !*function) return result;
if (!strcmp(name, "xrUpdateSwapchainFB")) {
real_update_swapchain = (PFN_xrUpdateSwapchainFB)*function;
*function = (PFN_xrVoidFunction)standin_update_swapchain;
} else {
real_get_swapchain_state = (PFN_xrGetSwapchainStateFB)*function;
*function = (PFN_xrVoidFunction)standin_get_swapchain_state;
}
return result;
}
if (sync_guard || layer_debug || input_diag) HOOK_AS(xrSyncActions, hook_xrSyncActions)
if (layer_debug) HOOK_AS(xrGetActionStateBoolean, hook_xrGetActionStateBoolean)
if (layer_debug || aim_correction_on() || profile_remap || input_diag || proximity_emul || quest_buttons)
HOOK_AS(xrSuggestInteractionProfileBindings, hook_xrSuggestInteractionProfileBindings)
if (action_dedup || proximity_emul || profile_remap || input_diag) HOOK_AS(xrCreateAction, hook_xrCreateAction)
if (layer_debug || aim_correction_on()) {
HOOK_AS(xrCreateActionSpace, hook_xrCreateActionSpace)
}
#undef HOOK_AS
#undef HOOK
if (input_diag) { // logs only: wraps a few input/haptics/perf calls, notes lookups the runtime can't answer
PFN_xrVoidFunction diag = input_diag_hook(name);
XrResult result = next_gipa(instance, name, function);
input_diag_lookup(instance, name, result, *function);
if (diag && XR_SUCCEEDED(result) && *function) *function = diag;
return result;
}
return next_gipa(instance, name, function);
}
// ---------------------------------------------------------------------------
// Every other symbol exported by overport's generic loader is forwarded
// unchanged (see forwarders.S), so callers that dlsym() entry points directly
// keep working. Unresolved entries return XR_ERROR_FUNCTION_UNSUPPORTED.
__attribute__((visibility("hidden"))) XrResult frame_unsupported(void) { return XR_ERROR_FUNCTION_UNSUPPORTED; }
#define FORWARD(fn) extern void *fwd_##fn;
#include "forwarders.inc"
#undef FORWARD
__attribute__((constructor)) static void resolve_forwarders(void) {
pthread_once(&init_once, initialize);
if (!loader) return;
void *p;
#define FORWARD(fn) if ((p = dlsym(loader, #fn))) fwd_##fn = p;
#include "forwarders.inc"
#undef FORWARD
}