Files
spoopyghosty0--frameport/native/xrlayer/timefix_layer.c
T
spoopyghosty0 0cb195ec2c Oculus Rift / PC VR support, GUI overhaul, artwork and Frame tooling
Adds Oculus Rift (PC VR) games alongside Quest:
- analysis/rift.py (own PE reader: imports incl. delay-imports, GUI-subsystem
  filter, exe ranking, icons), sources/rift_dump.py (recursive scan, one game
  per folder, collections), catalog rift fields + Rick and Morty recipe.
- patches/pcvr.py (category "pcvr", shown as patches): Revive (LibOVR->OpenXR,
  default on for Oculus games), revive_openvr, no_crash_reporter (Unreal),
  oculus_unreal (OculusHMDConnected helper), xr_timefix (Frame OpenXR layer),
  proton_*. tools/revive.py (find/install Revive, never replaces the user's).
- targets/pc_revive.py (run on this Windows/WSL PC via Revive + local Steam;
  core/winhost.py helpers) and frame_lepton Proton path; agent handles Proton
  install, prepare/finalize_pcvr, launch, list_files (AGENT_VERSION 17).
- native/oculushmd (fp_oculushmd.exe) and native/xrlayer (Frame OpenXR 1.1->1.0
  + timespec-time layer); prebuilt artifacts + SHA256SUMS.

Runtime findings (Frame, SteamOS 0.4.2 / Proton 11 arm64): the SteamVR runtime
only accepts OpenXR 1.0, so Proton's vrhelper (1.1) needs the compat layer;
Oculus/LibOVR games need a VR runtime (Revive on PC with SteamVR, or SteamVR-
native builds run directly). Platform-SDK / signature-checked repacks can't run
on the Frame; those are flagged, never bypassed.

GUI overhaul (ui/): streaming persistent Library with tags/filters/sort, a
background job queue + activity panel, per-game page, Frame tab with installed
list + file browser, Settings (incl. agent version), Play + launch-test actions,
resumable/interruptible installs with up-front interventions, dark theme, help
hints, uninstaller (uninstall.py). Faster uploads via a direct link (USB /
Frame hotspot) when reachable. Artwork/details from OculusDB + Steam + ovrp,
composed Steam art sets and tags, thumbnails served by URL.

Docs (PLAYBOOK/FRAME_RUNTIME/ARCHITECTURE/INSTALL/CLAUDE.md), triage signatures,
and unit tests throughout (130 pass).
2026-09-30 00:35:19 -04:00

158 lines
8.2 KiB
C

/* FramePort timefix: an OpenXR API layer for Windows PC VR games running under Proton on the Steam Frame.
*
* Proton's wineopenxr implements XR_KHR_win32_convert_performance_counter_time (required by Revive's OpenXR backend)
* on top of the host's XR_KHR_convert_timespec_time, but the Frame runtime returns XR_ERROR_FUNCTION_UNSUPPORTED for
* xrConvertTimespecTimeToTimeKHR / xrConvertTimeToTimespecTimeKHR. This layer supplies them with the same emulation
* the FrameBridge adapter uses for Quest games: XrTime = CLOCK_MONOTONIC + offset, where the offset is measured at
* every xrWaitFrame (predictedDisplayTime - predictedDisplayPeriod - now). The runtime's own functions are preferred
* whenever they work. The layer also advertises XR_KHR_convert_timespec_time (manifest) and removes it from
* xrCreateInstance if the runtime rejects it.
*
* OpenXR version fallback: the Frame's SteamVR runtime rejects apiVersion 1.1 with XR_ERROR_API_VERSION_UNSUPPORTED
* (checked 2026-09-29; 1.0 works), which is what Proton 11's VR helper requests, so VR never started for Proton games
* (flat window / "Unable to load LibOVRRT DLL" under Revive). The layer retries such a create as a 1.0 app.
*
* Enabled per game by FramePort's Proton launcher: XR_API_LAYER_PATH=<game>/xrlayer,
* XR_ENABLE_API_LAYERS=XR_APILAYER_FRAMEPORT_timefix. Built freestanding (see ../build.py).
*/
#define XR_USE_TIMESPEC 1
#include <time.h>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <openxr/openxr_loader_negotiation.h>
#define EXPORT __attribute__((visibility("default")))
#define LAYER_NAME "XR_APILAYER_FRAMEPORT_timefix"
#define TIMESPEC_EXT "XR_KHR_convert_timespec_time"
#define MAX_EXTS 256
static PFN_xrGetInstanceProcAddr next_gipa;
static PFN_xrConvertTimespecTimeToTimeKHR runtime_timespec_to_time;
static PFN_xrConvertTimeToTimespecTimeKHR runtime_time_to_timespec;
static PFN_xrWaitFrame runtime_wait_frame;
static volatile long long time_offset;
static volatile int calibrated;
static int str_eq(const char *a, const char *b) {
if (!a || !b) return 0;
while (*a && *a == *b) { ++a; ++b; }
return *a == *b;
}
static long long mono_ns(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (long long)now.tv_sec * 1000000000ll + now.tv_nsec;
}
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) return r;
}
if (!ts || !time) return XR_ERROR_VALIDATION_FAILURE;
*time = (XrTime)((long long)ts->tv_sec * 1000000000ll + ts->tv_nsec + (calibrated ? time_offset : 0));
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;
long long mono = (long long)time - (calibrated ? time_offset : 0);
ts->tv_sec = (time_t)(mono / 1000000000ll);
ts->tv_nsec = (long)(mono % 1000000000ll);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_wait_frame(XrSession session, const XrFrameWaitInfo *info, XrFrameState *state) {
if (!runtime_wait_frame) return XR_ERROR_FUNCTION_UNSUPPORTED;
XrResult r = runtime_wait_frame(session, info, state);
if (XR_SUCCEEDED(r) && state && state->predictedDisplayTime) {
// predictedDisplayTime is about one display period ahead of "now"
time_offset = (long long)(state->predictedDisplayTime - state->predictedDisplayPeriod) - mono_ns();
calibrated = 1;
}
return r;
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
PFN_xrVoidFunction *function) {
if (!function || !name) return XR_ERROR_VALIDATION_FAILURE;
if (!next_gipa) return XR_ERROR_HANDLE_INVALID;
if (str_eq(name, "xrConvertTimespecTimeToTimeKHR") || str_eq(name, "xrConvertTimeToTimespecTimeKHR")) {
PFN_xrVoidFunction real = 0;
if (instance == XR_NULL_HANDLE) return XR_ERROR_HANDLE_INVALID;
if (XR_FAILED(next_gipa(instance, name, &real))) real = 0;
if (str_eq(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;
}
if (str_eq(name, "xrWaitFrame") && instance != XR_NULL_HANDLE) {
XrResult r = next_gipa(instance, name, (PFN_xrVoidFunction *)&runtime_wait_frame);
if (XR_FAILED(r)) return r;
*function = (PFN_xrVoidFunction)layer_wait_frame;
return XR_SUCCESS;
}
return next_gipa(instance, name, function);
}
static XrResult create_stripped(XrApiLayerNextInfo *next, const XrInstanceCreateInfo *info,
const XrApiLayerCreateInfo *chained, XrInstance *instance) {
XrResult r = next->nextCreateApiLayerInstance(info, chained, instance);
if (r != XR_ERROR_EXTENSION_NOT_PRESENT) return r;
// The runtime doesn't know XR_KHR_convert_timespec_time: we provide it, so don't pass it down.
const char *exts[MAX_EXTS];
uint32_t n = 0, dropped = 0;
for (uint32_t i = 0; i < info->enabledExtensionCount && n < MAX_EXTS; ++i) {
if (str_eq(info->enabledExtensionNames[i], TIMESPEC_EXT)) { ++dropped; continue; }
exts[n++] = info->enabledExtensionNames[i];
}
if (!dropped) return r;
XrInstanceCreateInfo stripped = *info;
stripped.enabledExtensionCount = n;
stripped.enabledExtensionNames = exts;
return next->nextCreateApiLayerInstance(&stripped, chained, instance);
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_instance(const XrInstanceCreateInfo *info,
const XrApiLayerCreateInfo *layer_info, XrInstance *instance) {
if (!layer_info || !layer_info->nextInfo || !info) return XR_ERROR_INITIALIZATION_FAILED;
XrApiLayerNextInfo *next = layer_info->nextInfo;
next_gipa = next->nextGetInstanceProcAddr;
XrApiLayerCreateInfo chained = *layer_info;
chained.nextInfo = next->next;
XrResult r = create_stripped(next, info, &chained, instance);
if (r != XR_ERROR_API_VERSION_UNSUPPORTED || XR_VERSION_MAJOR(info->applicationInfo.apiVersion) != 1 ||
XR_VERSION_MINOR(info->applicationInfo.apiVersion) == 0)
return r;
// The Frame's SteamVR runtime only accepts OpenXR 1.0, but Proton 11's VR helper (and newer apps) ask for 1.1:
// retry as a 1.0 app (1.1 core functions the runtime lacks are simply not returned by xrGetInstanceProcAddr).
XrInstanceCreateInfo v10 = *info;
v10.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
return create_stripped(next, &v10, &chained, instance);
}
EXPORT XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
const char *layer_name,
XrNegotiateApiLayerRequest *request) {
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
return XR_ERROR_INITIALIZATION_FAILED;
if (layer_name && !str_eq(layer_name, LAYER_NAME)) return XR_ERROR_INITIALIZATION_FAILED;
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
request->layerApiVersion = XR_CURRENT_API_VERSION;
request->getInstanceProcAddr = layer_get_instance_proc_addr;
request->createApiLayerInstance = layer_create_instance;
return XR_SUCCESS;
}