Compare commits

...
Author SHA1 Message Date
Codex b5dab15149 Route embedded Khronos OpenXR loaders through FrameBridge
Detect and redirect static loader manifests and Android brokers, ship a runtime negotiation shim and the inner loader chain, and maintain per-game manifests on install and launch. Add the PrimedGun recipe, triage and host coverage.
2026-10-18 12:39:35 -04:00
spoopyghosty0andClaude Opus 5.5 54effe9ab0 Catalog: HardBullet (issues: cube loading screen, view freezes during loads)
Closes #385

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:17:35 -04:00
spoopyghosty0andClaude Opus 5.5 d78fcbd20a OpenGL ES shader dump/fix offered for Unity games whose boot.config mentions Vulkan; either setting brings in the shader-fix layer (#382)
SynthRiders is analysed as 'Vulkan (Unity boot.config)' but renders with GLES on Zink, so the dump was hidden in
Game settings and session triage's suggestion was dropped; the dump setting alone also never added the layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:14:42 -04:00
spoopyghosty0andClaude Opus 5.5 0322b8b4d6 Catalog: BattleTalent online rooms need Meta's account check, LAN works (#408)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:06:49 -04:00
27 changed files with 714 additions and 30 deletions

No files matched your search

+49 -3
View File
@@ -46,7 +46,7 @@ try:
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
fcntl = None
AGENT_VERSION = 102
AGENT_VERSION = 103
HOME = os.path.expanduser("~")
STEAM = os.path.join(HOME, ".local/share/Steam")
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
@@ -3465,6 +3465,8 @@ def upgrade_launchers():
new = set_codec_line(new, wants_hw_video(os.path.dirname(path)))
new = set_gamepad_line(new, wants_gamepad(os.path.dirname(path)))
pkg = os.path.basename(os.path.dirname(path))
dep = deployment(pkg) or {}
new = set_openxr_manifest_line(new, pkg, dep.get("static_openxr_loader", False))
base = launcher_base(new, pkg)
if base: # games on a drive whose path has a space reach Lepton through a link (agent v83)
new = set_app_dir(new, base, pkg)[0]
@@ -3884,7 +3886,16 @@ def upgrade_linux_launcher(text):
return text
def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False, gamepad=False, config_sync=False):
def set_openxr_manifest_line(text, pkg, enabled):
line = (f"python3 {shlex.quote(os.path.abspath(__file__))} _openxr_manifest {shlex.quote(pkg)}")
text = "\n".join(ln for ln in text.split("\n") if " _openxr_manifest " not in ln)
if enabled:
text = text.replace("\nfix_perms\n", "\n" + line + "\nfix_perms\n", 1)
return text
def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False, gamepad=False,
config_sync=False, static_openxr=False):
extra = "".join(f"export {k}={shlex.quote(str(v))}\n" for k, v in (env or {}).items()
if re.fullmatch(r"[A-Z_][A-Z0-9_]*", k))
text = LAUNCH_SH.format(title=title.replace("\n", " "), pkg=pkg, app_dir=app_dir_lines(base, pkg), appid=appid,
@@ -3895,6 +3906,7 @@ def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False,
plays_start=plays_lines(anchor)[0],
plays_end=plays_lines(anchor)[1], save_snapshot=snapshot_line(pkg),
restart=restart_block(pkg, lepton))
text = set_openxr_manifest_line(text, pkg, static_openxr)
text = set_config_sync_line(text, pkg, config_sync)
text = set_refresh_rate_line(text, pkg)
path = os.path.join(anchor, "launch.sh")
@@ -3904,6 +3916,32 @@ def write_launcher(anchor, base, pkg, title, appid, lepton, env, hw_video=False,
os.replace(path + ".tmp", path)
def write_openxr_runtime_manifest(base, enabled):
"""Lepton maps this per-game external root to /sdcard. Never follow a game's symlink."""
if not enabled:
return
root = os.path.join(base, "lepton-data", "external")
os.makedirs(root, exist_ok=True)
payload = {"file_format_version": "1.0.0",
"runtime": {"library_path": "libfp_xrruntime.so", "name": "FramePort"}}
for rel in ("fp_openxr_runtime.json", "fp/1/active_runtime.json"):
parent = os.path.dirname(os.path.join(root, rel))
if not _inside(os.path.realpath(parent), os.path.realpath(root)):
raise AgentError("refusing linked OpenXR manifest directory outside game storage")
os.makedirs(parent, exist_ok=True)
target = os.path.join(root, rel)
# Atomic replace repairs stale manifests without following the final symlink.
fd, tmp = tempfile.mkstemp(prefix=".fp-openxr-", dir=parent)
try:
with os.fdopen(fd, "w") as f:
json.dump(payload, f, separators=(",", ":"))
os.chmod(tmp, 0o644)
os.replace(tmp, target)
finally:
if os.path.exists(tmp):
os.unlink(tmp)
def data_files_dir(base, pkg):
return os.path.join(base, "lepton-data/external/Android/data", pkg, "files")
@@ -4440,12 +4478,14 @@ def configure_install(args, prep, moved):
new_managed[rel] = {"sha256": sha256_file(target), "backup": had_original}
models = install_controller_models(files_dir, str(settings.get("controller_models", 0)) not in ("0", "0.0"))
recipe = args.get("recipe") if isinstance(args.get("recipe"), dict) else {}
static_openxr = "frame.static_openxr_loader" in (recipe.get("patches") or [])
write_openxr_runtime_manifest(base, static_openxr)
hw_video = HW_VIDEO_PATCH in (recipe.get("patches") or []) # the shared codec (install_video_codec)
gamepad = GAMEPAD_PATCH in (recipe.get("patches") or []) # Steam Input's virtual gamepad (PODMAN_WRAPPER)
if gamepad:
ensure_podman_wrapper()
config_sync = args.get("config_sync") if isinstance(args.get("config_sync"), dict) else {}
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad, bool(config_sync))
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad, bool(config_sync), static_openxr)
art_in = os.path.join(base, "incoming-artwork")
art_moved = False
if args.get("consume_artwork") and os.path.isdir(art_in):
@@ -4457,6 +4497,7 @@ def configure_install(args, prep, moved):
"apk": args.get("apk_name", previous.get("apk", "game.apk")),
"sha256": args.get("apk_sha256") or previous.get("sha256"),
"recipe": args.get("recipe"), "installed_by": "frameport",
"static_openxr_loader": static_openxr,
"hw_video_decode": hw_video, "steam_gamepad": gamepad, "config_sync": config_sync,
"agent_version": AGENT_VERSION, "time": time.time()}
if previous.get("eye_base"): # the runtime's eye size learned from an earlier start (device.config_sync)
@@ -9040,6 +9081,11 @@ COMMANDS = {n[4:]: f for n, f in globals().items() if n.startswith("cmd_")}
def main():
if len(sys.argv) == 3 and sys.argv[1] == "_openxr_manifest":
dep = deployment(check_pkg(sys.argv[2])) or {}
if dep.get("static_openxr_loader"):
write_openxr_runtime_manifest(dep["base"], True)
return
if len(sys.argv) >= 3 and sys.argv[1] == "_snapshot_close":
return snapshot_close(sys.argv[2])
if len(sys.argv) >= 3 and sys.argv[1] == "_fetch_save_snapshot":
+1
View File
@@ -9,6 +9,7 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/libfp_vk.so
3c548b567c6984c91bf3f4a2b0bae1336f569f3e9fa9c3204872fd9876f88284 ./arm64-v8a/libfp_xrruntime.so
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
Binary file not shown.
+2 -1
View File
@@ -2,7 +2,8 @@ package: com.CyDream.BattleTalent
title: BattleTalent
status: issues
notes: "Plays normally. The mixed-reality mode doesn't appear in the start menu (also with an APK from a Quest 3); MR
progress in #355. Multiplayer not tested yet."
progress in #355. LAN multiplayer works; online rooms need Meta's account check (user proof), so online
multiplayer can't work (#408)."
tested_version: '4.7'
engine: Unity
xr: OpenXR
@@ -0,0 +1,25 @@
package: com.GexagonVR.HardBullet
title: HardBullet
status: issues
notes: Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release)
shows them as six panels, not confirmed in the headset yet. Level loads stall frame submission for up to ~1 s, so
the view freezes during loads.
tested_version: 1.0.9.704Q
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-17'
known_good_sha256: ecbb758d98083a590f100d5ac6efe9490ef74ad39b59217e0b4a11db920ecd51
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 96
issue: 385
source_hint: HARD BULLET
+5 -3
View File
@@ -1,14 +1,16 @@
package: org.primedgun.v2
title: PrimedGun v2
status: unknown
notes: 'Without the Vulkan library rename: the game builds the library name from two pieces, so the renamed name didn''t
exist ("libfp_vkn.so"), its renderer fell back to OpenGL and VR couldn''t start (GitHub #254). Not yet confirmed in
the headset.'
notes: 'Vulkan via a statically linked Khronos OpenXR loader bypassed FrameBridge (GitHub #254).
The embedded-loader patch routes it through FrameBridge; device validation is still pending.
Keep the Vulkan library rename disabled: this game assembles its library name from two pieces,
so renaming it prevented Vulkan from loading and VR could not start. Not yet confirmed in the headset.'
tested_version: 2.0.0-alpha.4
engine: Other
xr: OpenXR
frame:
- frame.sdl_clipboard
- frame.static_openxr_loader
frame_remove:
- frame.vk_sanitize
updated: '2026-10-18'
+6
View File
@@ -6,6 +6,12 @@
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
# Sources: every failure we hit porting 34 games to the Steam Frame (see docs/PLAYBOOK.md).
signatures:
- id: static-openxr-loader-bypass
pattern: 'RuntimeManifestFile::FindManifestFiles - using global runtime file /[^\r\n]*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json'
unless: 'FrameBridge[: ]|fp_xrruntime[: ]'
severity: warning
diagnosis: A Khronos loader loaded the system runtime without FrameBridge. A statically linked loader may bypass the bundled loader chain; analyze again, enable the embedded OpenXR loader patch when detected, then rebuild and reinstall to write its per-game manifest.
suggest: [frame.static_openxr_loader]
- id: quest2-headset-identity
pattern: 'venderId[^\r\n]*systemName Oculus Quest2'
severity: info
+66
View File
@@ -785,3 +785,69 @@ SteamVR's own Touch remap (`/opt/steamvr/drivers/frame_controller/resources/inpu
layout `oculus_touch`) is the diagram's "SteamVR default": X -> d-pad down, Y -> d-pad left/right/up, A -> A,
B -> B/X/Y, Menu -> View, plus trigger click -> bumper. Games with their own Valve binding file in that folder
(Beat Saber, Batman, Accounting+, In Death, ...) use that file instead.
## Embedded Khronos OpenXR loaders (GitHub #254)
PrimedGun v2 (`org.primedgun.v2`, 2.0.0-alpha.4) embeds the Khronos loader in
`libmetroid_prime_port.so` (no loader DT_NEEDED). Its diagnostics show both Android
broker queries failing, then `/vendor/etc/openxr/1/active_runtime.json`, runtime
`SteamVR/OpenXR`, and no FrameBridge messages. The supplied `openxr_android.cpp`
initializes the loader with the VM and SDL activity. This is Vulkan OpenXR; retain
the catalog's removal of `frame.vk_sanitize` (the game assembles the Vulkan soname).
`frame.static_openxr_loader` is general: analysis v23 detects arm64 game libraries
with Khronos manifest strings and negotiation/manifest markers. Complete manifest
paths (including ABI variants) are replaced in `.rodata`, NUL padded, with
`/sdcard/fp_openxr_runtime.json`. Khronos also builds paths from fragments: the
verified `platform_utils.hpp` layout uses five partition prefixes, `/etc/openxr/`,
the major API version and `/active_runtime[.aarch64].json`. For this complete table,
prefixes become `/sd` and the subdir becomes `card/fp/`; all candidates then resolve
to `/sdcard/fp/1/active_runtime[.aarch64].json`. The undecorated manifest is written;
a missing ABI-specific file falls back to it. No offsets or sizes change. **Pooled
partition strings can also be used by game code**; this cannot be ruled out from
synthetic tests. Only the complete known fragment table is accepted. Other layouts
need analysis/rewrite support before they can be patched.
Both broker authorities become `org.fp_null.openxr.*`, causing provider lookup to
fail and the loader to fall back to files even if a broker is later installed.
Only the game's copy is changed. Agent v103 writes both manifest spellings into
`<base>/lepton-data/external` (the game's private `/sdcard`), records
`static_openxr_loader` in deployment.json, and repairs them before every launch.
Launcher upgrades preserve that repair. A configuration-only finalize also writes
them. The payload is `{"file_format_version":"1.0.0","runtime":{"library_path":"libfp_xrruntime.so","name":"FramePort"}}`.
The shim exports negotiation and initialization, loads FrameBridge by soname,
forwards Android loader initialization through its GIPA, and returns that GIPA to
the outer loader. FrameBridge then loads `libopenxr_loader_original.so`, an
unmodified Khronos loader reused from OVRPort's downloaded generic runtime.
This works for recipes with `overport: False`: the patch supplies the original
loader before `frame.adapter` installs FrameBridge, its settings payload and
foveation support. Settings still use `settings.conf` through the normal launcher.
No Meta dispatcher is needed. The loader's unmodified system paths keep it from
recursing through the game manifest. Android linker namespaces, symbol binding
between the two loaders, runtime handles across the chain, and FrameBridge's
behavior without the OVRPort dispatcher still require a device check.
Source verification: Khronos OpenXR-SDK-Source
[`release-1.0.34/src/common/platform_utils.hpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/common/platform_utils.hpp)
and the same fragment layout in release-1.1.53;
[`manifest_file.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/manifest_file.cpp)
leaves a bare library filename unchanged for dlopen and tries brokers before files;
[`runtime_interface.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/runtime_interface.cpp)
forwards loader initialization to the runtime export before negotiation (or through
GIPA afterwards). The reporter's diagnostic ELF summary contains no embedded
loader version or raw string table; the exact game's loader revision and binary
rewrite remain unverified. Host tests cover complete and assembled synthetic ELF
layouts, idempotence, unchanged other bytes, APK apply/validate with no OVRPort,
negotiation rejection, Android init forwarding and missing-chain failure. No Frame
connection or device test was performed.
Headless acceptance after rebuilding and installing with this development build:
`uv run frameport test org.primedgun.v2`. Expect `fp_xrruntime: chain resolved:
FrameBridge`, `fp_xrruntime: xrInitializeLoaderKHR forwarded to FrameBridge: 0`,
`fp_xrruntime: negotiated runtime interface 1: FrameBridge`, then FrameBridge's
`scale=... loader=...libopenxr_loader_original.so`, `xrCreateInstance` and pacing /
`new layer:` lines. The outer loader should log the private sdcard manifest, the
inner loader the real vendor manifest. Require RUNNING and submitted frames;
headset rendering and controls need separate confirmation.
+2
View File
@@ -143,6 +143,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Table Troopers | Quest | ✅ Works | |
| Taskmaster | Quest | ✅ Works | |
| Tea For God | Quest | ✅ Works | |
| Test Quest Controllers | Quest | ✅ Works | |
| TetrisEffect | Quest | ✅ Works | |
| The Boys VR | Quest | ✅ Works | |
| The Climb | Quest | ✅ Works | |
@@ -184,6 +185,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| ExploreVR | Quest | ⚠️ Works with issues | The 3D world warps and shifts with head movement (wrong perspective). |
| FGO VR | Quest | ⚠️ Works with issues | Audio stuttering and popping sounds |
| Fisherman | Quest | ⚠️ Works with issues | Loading screens have corruption, but everything works otherwise. |
| HardBullet | Quest | ⚠️ Works with issues | Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release) shows them as six panels, not confirmed in the hea… |
| Homeworld: Vast Reaches | Quest | ⚠️ Works with issues | The game starts but after selecting to start a new game it seems to crash. |
| Lawn Mowing Simulator VR | Quest | ⚠️ Works with issues | Left eye textures flicker only when mowing in specific areas. |
| MacacoVR | Quest | ⚠️ Works with issues | 2d loading screens are glitchy, otherwise main gameplay is fine |
+27 -11
View File
@@ -2,7 +2,7 @@
# Patches
Every patch and game setting FramePort knows: 186 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
Every patch and game setting FramePort knows: 187 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
@@ -11,7 +11,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
## Groups
- [Patches from OVRPort](#group-overport) (25)
- [Steam Frame patches](#group-frame) (61)
- [Steam Frame patches](#group-frame) (62)
- [Game settings](#group-adapter) (77)
- [Files and settings on the Frame](#group-device) (10)
- [PC VR (Proton and Revive)](#group-pcvr) (13)
@@ -58,7 +58,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [GL shim (Mesa GLSL compatibility)](#frame-gl_shim) | `frame.gl_shim` | Steam Frame patches | 2 |
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | 1 |
| [Godot apps: start without a clipboard service](#frame-godot_clipboard) | `frame.godot_clipboard` | Steam Frame patches | 1 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 33 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 34 |
| [Language packs from the game's files](#frame-langpacks) | `frame.langpacks` | Steam Frame patches | |
| [Launcher category (INFO → LAUNCHER)](#frame-launcher) | `frame.launcher` | Steam Frame patches | default |
| [GL wrapper: OpenGL ES depth formats (Minecraft)](#frame-ltw_depth) | `frame.ltw_depth` | Steam Frame patches | default |
@@ -77,6 +77,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 6 |
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
| [Embedded OpenXR loader: use FrameBridge](#frame-static_openxr_loader) | `frame.static_openxr_loader` | Steam Frame patches | 1 |
| [Allow swapchains larger than 4096 px](#frame-swapchain_limit) | `frame.swapchain_limit` | Steam Frame patches | default |
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 1 |
| [Unity App UI: no Android 12 vibrator](#frame-unity_appui_vibrator) | `frame.unity_appui_vibrator` | Steam Frame patches | |
@@ -86,7 +87,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Unity: skip OVROverlay layers (fades, splash screens)](#frame-unity_no_overlay_copy) | `frame.unity_no_overlay_copy` | Steam Frame patches | 2 |
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 14 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 32 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 117 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 118 |
| [Unity: share thread keys (games that run out of them)](#frame-unity_tls_key_pool) | `frame.unity_tls_key_pool` | Steam Frame patches | 1 |
| [Unity: headset always counts as worn](#frame-unity_user_presence) | `frame.unity_user_presence` | Steam Frame patches | 1 |
| [GL shim for Unreal (no multisampled render-to-texture)](#frame-unreal_gl_shim) | `frame.unreal_gl_shim` | Steam Frame patches | 1 |
@@ -137,7 +138,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [GL shim: high-precision vertex shaders](#adapter-gl_highp_markers) | `adapter.gl_highp_markers` | Game settings | 1 |
| [GL shim: integer vertex inputs](#adapter-gl_int_attribs) | `adapter.gl_int_attribs` | Game settings | 1 |
| [Flat panel diagnostics (log)](#adapter-gl_mv_debug) | `adapter.gl_mv_debug` | Game settings | |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 102 |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 103 |
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
| [Turn off Quest Vulkan foveation](#adapter-hide_fb_foveation) | `adapter.hide_fb_foveation` | Game settings | |
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 5 |
@@ -192,7 +193,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Game language](#device-language) | `device.language` | Files and settings on the Frame | |
| [Extra Lepton settings](#device-lepton_env) | `device.lepton_env` | Files and settings on the Frame | 3 |
| [Controllers as a gamepad (Steam Input)](#device-steam_gamepad) | `device.steam_gamepad` | Files and settings on the Frame | |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 120 |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 121 |
| [Launch options (SteamVR or OpenXR)](#pcvr-launch_args) | `pcvr.launch_args` | PC VR (Proton and Revive) | 2 |
| [Provide the Oculus runtime from Revive (Frame)](#pcvr-libovr_redirect) | `pcvr.libovr_redirect` | PC VR (Proton and Revive) | |
| [No Unreal crash reporter](#pcvr-no_crash_reporter) | `pcvr.no_crash_reporter` | PC VR (Proton and Revive) | |
@@ -693,7 +694,7 @@ Plays the game's videos on the Frame's video hardware, so 4K and 8K video runs s
- `frame.hw_video_decode` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** The game plays video through Android's decoders (MediaCodec, ExoPlayer or VLC): decode it on the Frame's hardware.
- **Catalog games (33):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 25 more.
- **Catalog games (34):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 26 more.
- **Source:** [patches/frame/hw_video_decode.py](../src/frameport/patches/frame/hw_video_decode.py)
> Decodes the game's videos on the Frame's hardware video decoder instead of Android's software decoders, which stutter or fall behind with 4K/8K video (for example video players, Batman: Arkham Shadow's cutscenes). Adds FramePort's codec plugin (OMX.frameport.avc/hevc/vp9.decoder) to this game's container only; the APK isn't changed. When the hardware is busy (for example Steam's own hardware video decoding is on) the video is decoded in software as before.
@@ -935,6 +936,21 @@ Opens the app straight in VR, skipping its flat launcher screen. Turn off to use
> Some apps open a flat Android launcher that then starts a separate VR activity (for example WiiCompiled). Shown with Lepton's flat window, that window stays in view after the VR part starts. This makes the VR activity the one Lepton starts (its LAUNCHER category), so the app opens in VR with no flat window. Everything only the launcher does (choosing or importing content) is skipped: turn it off for a run when you need the launcher, and turn off "Show the app's Android window".
### <a id="frame-static_openxr_loader"></a>Embedded OpenXR loader: use FrameBridge
Makes an embedded OpenXR loader use FrameBridge and your game settings.
- `frame.static_openxr_loader`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** The embedded Khronos OpenXR loader bypasses FrameBridge.
- **Requires:** [`frame.adapter`](#frame-adapter)
- **Catalog games (1):** PrimedGun v2.
- **Suggested by launch-test signatures:**
- `static-openxr-loader-bypass` (warning): A Khronos loader loaded the system runtime without FrameBridge.
- **Source:** [patches/frame/static_openxr_loader.py](../src/frameport/patches/frame/static_openxr_loader.py)
> Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its Android runtime broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.
### <a id="frame-swapchain_limit"></a>Allow swapchains larger than 4096 px
Allows very large pictures (8K video, theaters) instead of quitting.
@@ -1066,7 +1082,7 @@ Lets you type into this app's text fields on the Frame (they'd close at once).
- `frame.unity_text_input` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unity app with text fields (…): they close at once on the Frame because it has no system keyboard.
- **Catalog games (117):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 109 more.
- **Catalog games (118):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 110 more.
- **Source:** [patches/frame/unity_text_input.py](../src/frameport/patches/frame/unity_text_input.py)
> Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does this on a Quest), so a selected text field only flashes a caret. Rewrites TouchScreenKeyboardShouldBeUsed -&gt; false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -&gt; true in libil2cpp.so, found per game with Cpp2IL (downloaded on first use), so fields stay selected and take key presses: Steam's keyboard (with "Show the app window") or Type on Frame from the PC.
@@ -1325,7 +1341,7 @@ Repairs shaders that hang the graphics chip at certain effects, from the game's
- **Catalog games (2):** Star Wars: Tales from the Galaxy's Edge, Vader Immortal: Episode I.
- **Source:** [patches/frame/zink_shader_fix.py](../src/frameport/patches/frame/zink_shader_fix.py)
> Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer adds itself to Android's Vulkan debug layer list for the game's process only.
> Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe, and either setting brings it in by itself. The layer adds itself to Android's Vulkan debug layer list for the game's process only.
## <a id="group-adapter"></a>Game settings
@@ -1794,7 +1810,7 @@ For games that freeze the Frame when a controller vibrates (for example Lucky's
- **Options:**
- Value: int, default `0`.
- Game settings → Controllers → "Fix freezes when controllers vibrate" (advanced).
- **Catalog games (102):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 94 more.
- **Catalog games (103):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 95 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Routes OVRPlugin through FramePort's extension shim, which turns Meta's amplitude-envelope vibrations into plain ones: OVRPort's loader reads their nanosecond duration as seconds and allocates gigabytes, freezing the Frame when the game vibrates a controller (for example Lucky's Tale's save slots, GitHub #9). Needs a rebuild.
@@ -2601,7 +2617,7 @@ Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard
- `device.text_input_window`
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** App with text fields: key presses only reach a shown Android window.
- **Catalog games (120):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 112 more.
- **Catalog games (121):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 113 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Lepton runs VR apps headless: their Android window is never shown, so it never gets keyboard focus and no key press (Steam's on-screen keyboard, a USB/Bluetooth keyboard, Type on Frame) reaches the app's text fields. With Lepton's lepton-show-flatscreen marker the window is shown (behind the VR view, which keeps working) and Steam's keyboard opens for a selected text field. Steam lists the shown window as "Gamescope" (the Frame's compositor). Pairs with "Make Unity text fields work without a system keyboard".
+1
View File
@@ -12,6 +12,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
## Startup failures (visible in launch.log)
| Symptom | Cause | Fix |
| `RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json`, no FrameBridge lines (#254) | Embedded Khronos loader bypasses the APK loader chain | `frame.static_openxr_loader`: redirect its manifest paths and broker authorities, bundle runtime shim + inner loader + FrameBridge; reinstall to write `/sdcard/fp_openxr_runtime.json`. Device check pending |
|---|---|---|
| Quest 3-only options missing; OVRPlugin logs `venderId 10291, systemName Oculus Quest2` (#373, Asgard's Wrath 2) | OVRPort's dispatcher overwrites FrameBridge's name and supplies a Quest 2 `XR_META_headset_id`; OVRPlugin prioritizes that UUID | `device.headset_model`: Quest 3 by default, per-game Quest 3S / Quest Pro / Quest 2; rebuild the APK. Byte-checked dispatcher name + UUID edit; Quest 2 leaves the dispatcher unchanged. See DEVICE_NOTES 'Per-game headset identity' |
| Startup exits through `libxlua.so (lj_err_throw)`, then Unity aborts in mutex cleanup; captured Lua error is `bad light userdata pointer` (GitHub #257, BattleTalent 4.7) | This older LuaJIT encodes lightuserdata in 47 bits; Frame supplies valid wider native pointers. Controlled tests with the unchanged library pass a real low-address allocation and fail its own high-address static pointer | `frame.luajit_pointers` automatically selects a full-pointer Lua engine only for the verified library SHA-256, retaining xLua bindings and custom helpers. The scoped patch reaches OpenXR startup for 60 s headless; rendering and gameplay remain unverified. A `lj_err_throw` cleanup stack without the original error message is insufficient to infer this cause |
+1
View File
@@ -22,6 +22,7 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
| `zinkfix/` | `arm64-v8a/libVkLayer_fp_shaderfix.so` | Shader-fix Vulkan layer `VK_LAYER_FP_shader_fix` for OpenGL ES games (patch `frame.zink_shader_fix`): sits between Zink (Mesa's GL on Vulkan) and the driver and inserts the words of `zink_shader_fix` (same format as the Vulkan shim's `vk_shader_fix`: size + SHA-256 match) into SPIR-V modules; `zink_shader_dump=1` writes every distinct module to `files/fp_spirv/`. The engine library loads it first (DT_NEEDED); its constructor puts the layer in front of Android's GraphicsEnv debug layer list (private Android 11 `getDebugLayers`/`setDebugLayers`, found through libvulkan.so's dependencies) before EGL starts, so the loader picks it up from the app's lib dir. Exports only the enumeration functions and `VK_LAYER_FP_shader_fixGet*ProcAddr` (never plain `vk*` entry points). Vader Immortal's lightspeed shaders (Klownicle, GitHub #49). |
| `xrlayer/` | `linux-arm64/libxr_frameport_timefix.so` + `XR_APILAYER_FRAMEPORT_timefix.json` | OpenXR API layer for Windows PC VR games under Proton on the Frame (glibc aarch64, built freestanding with the NDK's clang), on by default (patch `pcvr.xr_timefix`). Retries `xrCreateInstance` as an OpenXR 1.0 app when the runtime rejects 1.1 (the Frame's SteamVR runtime does, and Proton 11's VR helper asks for 1.1). Also emulates `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` if the runtime refuses them (the Frame's Android runtime does; its Linux runtime, used by Proton, supports them as of 2026-09-29 — so this part is only a fallback; Proton's wineopenxr needs them for `XR_KHR_win32_convert_performance_counter_time`, which Revive requires) with the adapter's xrWaitFrame-calibrated offset, and drops the extension from xrCreateInstance if the runtime rejects it. Enabled per game by the Proton launch.sh (`XR_API_LAYER_PATH`, `XR_ENABLE_API_LAYERS`). `tests/test_xrlayer.py` builds it for x86_64 and drives it via ctypes. |
| `oculushmd/` | `win-x64/fp_oculushmd.exe` | PC VR counterpart of overport's `patch_oculus_unreal` (patch `pcvr.oculus_unreal`, default for Unreal Rift games on the Frame). Unreal's OculusHMD and LibOVR's `ovr_Detect` only start when the Windows named event `OculusHMDConnected` exists and is signalled (the Oculus service creates it). `fp_oculushmd.exe <command line>` creates it (manual reset, signalled), runs the command (Revive's injector) in a job object and keeps the event until every process in the job — the game and its children — has exited (fallback without job support: 3 minutes); returns the command's exit code and logs `FramePort oculushmd: …` to stderr (launch.log). Freestanding Windows x64 console exe (no CRT, no Windows SDK): the NDK's clang + `lld-link /Brepro` with a kernel32 import library generated by `llvm-dlltool`, so it's byte-reproducible. Tested on Windows from WSL with a build whose event name is `-DEVENT_NAME=L"…"` (the Oculus service owns the real event on a PC with the Oculus app). |
| `xrruntime/` | `arm64-v8a/libfp_xrruntime.so` | Runtime negotiation endpoint for embedded Khronos loaders (`frame.static_openxr_loader`). Loads FrameBridge by soname, forwards Android loader initialization through its GIPA, and returns FrameBridge GIPA. The unmodified inner Khronos loader is reused from OVRPort’s generic loader (also when `overport: False`); its paths remain system paths. Built with `--only xrruntime`. |
| `xrshim/` | `arm64-v8a/libframe_xrshim.so` | FrameBridge extension shim, only in builds with `controller_models=1`. overport's dispatcher (`libopenxr_loader.so`) answers `xrGetInstanceProcAddr` from a fixed table and refuses everything else (`overportOXR: Unknown proc addr: xrLoadRenderModelFB`), so the adapter never sees XR_FB_render_model lookups. Meta's OVRPlugin gets the function with `dlopen("libopenxr_loader.so")` + `dlsym`, so FramePort rewrites that `.rodata` string in `libOVRPlugin.so` (same length, in place) to `libframe_xrshim.so`. The shim returns the adapter's emulated functions (`framebridge_extension_proc`) and forwards every other name to overport. A DT_NEEDED interposer does not work here (dlsym on a handle searches that library first); tried and dropped. |
| `venc/` | `linux-arm64-bin/fp_venc` | Hardware H.264 encoder for the live view, run on the Frame (spec: `venc/SPEC.md`). Reads SteamVR's "headset view" webcam (v4l2loopback `/dev/video99`, RGB24), box-scales and converts it to NV12 (BT.709 limited range; `convert.c`, NEON with a bit-identical scalar reference, also built for x86_64 by the host test), and feeds the SoC's V4L2 stateful encoder (`dev-encoder.rst`, Qualcomm iris `/dev/video-enc0`): CBR, no B-frames, SPS/PPS on every IDR, one frame per slot of a fixed grid (fps = a divisor of the panel refresh read through DRM KMS, default cap 45), Annex B on stdout. stdin `k` = keyframe, `q`/EOF = drain and stop; `--probe` prints the setup as JSON, `--selftest` compares NEON and scalar output. Fully static and freestanding (raw syscalls, own `_start`; only the NDK's kernel UAPI headers, staged in `.cache/uapi-arm64`), byte-reproducible. Lives in `linux-arm64-bin/` because `linux-arm64/` is uploaded whole as the OpenXR layer. |
| `java-stubs/` | `dex/oculusos-stubs.dex` | No-op `com.oculus.os.AnalyticsEvent` / `UnifiedTelemetryLogger` for native code that looks them up. |
+13 -4
View File
@@ -10,7 +10,7 @@ timefix OpenXR layer for Proton games (linux-arm64, glibc; the NDK's clang build
OculusHMDConnected helper for Rift games under Proton (win-x64 PE; the NDK's clang + lld-link, no Windows SDK), the
live view's hardware H.264 encoder fp_venc (linux-arm64-bin, static freestanding executable), and rewrites artifacts/SHA256SUMS. Run `frameport parity` afterwards to see which games change.
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vrsettings,venc,vroverlay_probe] [--ndk PATH]
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrruntime,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vrsettings,venc,vroverlay_probe] [--ndk PATH]
"""
from __future__ import annotations
@@ -331,6 +331,15 @@ def build_ovrtrace(tc: Path):
"-o", ART / "arm64-v8a/libfp_ovrtrace.so"], cwd=src)
def build_xrruntime(tc: Path):
src = HERE / "xrruntime"
run([exe(tc, "aarch64-linux-android29-clang"), "-shared", "-O2", "-fPIC", "-Wall", "-Wextra", "-Werror",
f"-ffile-prefix-map={ROOT}=.", "-fvisibility=hidden", "-Wl,-Bsymbolic",
"-I", openxr_include("adapter"), "-Wl,-soname,libfp_xrruntime.so",
"-Wl,-z,max-page-size=16384", "xrruntime.c", "-ldl", "-llog",
"-o", ART / "arm64-v8a/libfp_xrruntime.so"], cwd=src)
def build_xrshim(tc: Path):
"""FrameBridge extension shim (see xrshim/xrshim.c): DT_NEEDED-injected in front of overport's libopenxr_loader.so."""
src = HERE / "xrshim"
@@ -440,7 +449,7 @@ def write_sums():
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc,a32")
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrruntime,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc,a32")
ap.add_argument("--ndk")
args = ap.parse_args()
parts = set(args.only.split(","))
@@ -448,7 +457,7 @@ def main():
(ART / d).mkdir(parents=True, exist_ok=True)
tc = clang_dir(ndk(args.ndk)) if parts - {"dex"} else None
steps = {"adapter": lambda: build_adapter(tc), "bridge": lambda: build_bridge(tc), "compat": lambda: build_compat(tc),
"langpack": lambda: build_langpack(tc),
"xrruntime": lambda: build_xrruntime(tc), "langpack": lambda: build_langpack(tc),
"glshim": lambda: build_glshim(tc), "eglfmt": lambda: build_eglfmt(tc), "glmv": lambda: build_glmv(tc), "ovrtrace": lambda: build_ovrtrace(tc), "xrshim": lambda: build_xrshim(tc), "dex": build_dex, "xrlayer": lambda: build_xrlayer(tc),
"oculushmd": lambda: build_oculushmd(tc), "vkshim": lambda: build_vkshim(tc),
"zinkfix": lambda: build_zinkfix(tc),
@@ -457,7 +466,7 @@ def main():
"luajit": lambda: build_luajit(tc),
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc),
"vroverlay_probe": lambda: build_vroverlay_probe(tc), "a32": lambda: build_a32(tc)}
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrruntime", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
"vroverlay_probe", "a32"):
if name in parts:
log(f"build {name}")
+40
View File
@@ -0,0 +1,40 @@
/* SPDX-License-Identifier: MIT */
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <assert.h>
#include <string.h>
extern XrResult xrNegotiateLoaderRuntimeInterface(const XrNegotiateLoaderInfo *, XrNegotiateRuntimeRequest *);
extern XrResult xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *);
int main(int argc, char **argv) {
XrNegotiateLoaderInfo li = {XR_LOADER_INTERFACE_STRUCT_LOADER_INFO, XR_LOADER_INFO_STRUCT_VERSION,
sizeof(li), 1, 1, XR_MAKE_VERSION(1,0,0), XR_CURRENT_API_VERSION};
XrNegotiateRuntimeRequest r = {XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST, XR_RUNTIME_INFO_STRUCT_VERSION, sizeof(r)};
assert(xrNegotiateLoaderRuntimeInterface(NULL, &r) < 0);
assert(xrNegotiateLoaderRuntimeInterface(&li, NULL) < 0);
li.minInterfaceVersion = 2;
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minInterfaceVersion = 1;
r.structSize--;
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
r.structSize++;
li.minApiVersion = XR_MAKE_VERSION(2,0,0);
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
li.minApiVersion = XR_MAKE_VERSION(1,0,1);
li.maxApiVersion = XR_MAKE_VERSION(1,0,0);
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
li.maxApiVersion = XR_CURRENT_API_VERSION;
XrResult result = xrNegotiateLoaderRuntimeInterface(&li, &r);
if (argc > 1 && !strcmp(argv[1], "missing")) {
assert(result == XR_ERROR_INITIALIZATION_FAILED);
return 0;
}
assert(result == XR_SUCCESS && r.getInstanceProcAddr && r.runtimeInterfaceVersion == 1);
assert(r.runtimeApiVersion <= li.maxApiVersion && r.runtimeApiVersion >= li.minApiVersion);
XrLoaderInitInfoBaseHeaderKHR info = {XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR, (void *)0x1234};
assert(xrInitializeLoaderKHR(&info) == XR_SUCCESS);
info.next = NULL;
assert(xrInitializeLoaderKHR(&info) == XR_ERROR_RUNTIME_FAILURE);
return 0;
}
+16
View File
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: MIT */
#include <openxr/openxr.h>
#include <string.h>
static XrResult init(const XrLoaderInitInfoBaseHeaderKHR *info) {
return info->type == XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR && info->next == (void *)0x1234 ?
XR_SUCCESS : XR_ERROR_RUNTIME_FAILURE;
}
XrResult xrGetInstanceProcAddr(XrInstance instance, const char *name, PFN_xrVoidFunction *fn) {
(void)instance;
if (!strcmp(name, "xrInitializeLoaderKHR")) {
*fn = (PFN_xrVoidFunction)init;
return XR_SUCCESS;
}
*fn = NULL;
return XR_ERROR_FUNCTION_UNSUPPORTED;
}
+60
View File
@@ -0,0 +1,60 @@
/* SPDX-License-Identifier: MIT
* Runtime endpoint for an application's embedded Khronos loader.
* Only the application's loader paths change: the inner loader still finds SteamVR.
*/
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <dlfcn.h>
#include <pthread.h>
#include <string.h>
#ifdef __ANDROID__
#include <android/log.h>
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "fp_xrruntime", __VA_ARGS__)
#else
#include <stdio.h>
#define LOG(...) do { fprintf(stderr, "fp_xrruntime: "); fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } while (0)
#endif
#define EXPORT __attribute__((visibility("default")))
static pthread_once_t once = PTHREAD_ONCE_INIT;
static PFN_xrGetInstanceProcAddr chain;
static void resolve(void) {
/* RTLD_LOCAL and handle-specific dlsym prevent binding to the game's own xr* exports. */
void *bridge = dlopen("libopenxr_loader_generic.so", RTLD_NOW | RTLD_LOCAL);
if (bridge) chain = (PFN_xrGetInstanceProcAddr)dlsym(bridge, "xrGetInstanceProcAddr");
if (chain) LOG("chain resolved: FrameBridge");
else LOG("chain resolution failed: %s", dlerror());
}
EXPORT XrResult XRAPI_CALL xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *info) {
pthread_once(&once, resolve);
if (!chain || !info) return XR_ERROR_INITIALIZATION_FAILED;
PFN_xrVoidFunction proc = NULL;
XrResult result = chain(XR_NULL_HANDLE, "xrInitializeLoaderKHR", &proc);
if (XR_SUCCEEDED(result) && proc)
result = ((PFN_xrInitializeLoaderKHR)proc)(info);
else if (XR_SUCCEEDED(result)) result = XR_ERROR_INITIALIZATION_FAILED;
LOG("xrInitializeLoaderKHR forwarded to FrameBridge: %d", result);
return result;
}
EXPORT XrResult XRAPI_CALL xrNegotiateLoaderRuntimeInterface(
const XrNegotiateLoaderInfo *loader, XrNegotiateRuntimeRequest *runtime) {
if (!loader || !runtime ||
loader->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
loader->structVersion != XR_LOADER_INFO_STRUCT_VERSION || loader->structSize != sizeof(*loader) ||
runtime->structType != XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST ||
runtime->structVersion != XR_RUNTIME_INFO_STRUCT_VERSION || runtime->structSize != sizeof(*runtime) ||
loader->minInterfaceVersion > 1 || loader->maxInterfaceVersion < 1 ||
loader->minApiVersion > loader->maxApiVersion ||
loader->minApiVersion > XR_CURRENT_API_VERSION || loader->maxApiVersion < XR_MAKE_VERSION(1, 0, 0))
return XR_ERROR_INITIALIZATION_FAILED;
pthread_once(&once, resolve);
if (!chain) return XR_ERROR_INITIALIZATION_FAILED;
runtime->runtimeInterfaceVersion = 1;
runtime->runtimeApiVersion = loader->maxApiVersion < XR_CURRENT_API_VERSION ?
loader->maxApiVersion : XR_CURRENT_API_VERSION;
runtime->getInstanceProcAddr = chain;
LOG("negotiated runtime interface 1: FrameBridge");
return XR_SUCCESS;
}
+5
View File
@@ -594,6 +594,11 @@
"title": "Start straight in VR (skip the 2D launcher)",
"group": "Steam Frame patches"
},
"frame.static_openxr_loader": {
"anchor": "frame-static_openxr_loader",
"title": "Embedded OpenXR loader: use FrameBridge",
"group": "Steam Frame patches"
},
"frame.swapchain_limit": {
"anchor": "frame-swapchain_limit",
"title": "Allow swapchains larger than 4096 px",
+8 -4
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from ..apk import axml
from ..core.models import Analysis
from . import elf
from . import elf, static_openxr
from .luajit import xlua_47bit, xlua_sha256
from .unity_split import split_build as unity_split_build
@@ -54,7 +54,8 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
# USB MIDI driver polls the missing USB service: frame.usb_midi_watcher, GitHub #297) (2026-10)
# 22: unreal_binned3 (Unreal built with FMallocBinned3 reserves tens of GiB at load: frame.unreal_noreserve,
# GitHub #349) (2026-10)
ANALYSIS_VERSION = 22
# 23: statically linked Khronos OpenXR loaders (GitHub #254)
ANALYSIS_VERSION = 23
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
@@ -339,7 +340,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
libset = set(libs)
engine = ("Unreal" if libset & {"libUE4.so", "libUnreal.so"} else "Unity" if "libunity.so" in libset
else "CryEngine" if "libCrySystem.so" in libset or cry_project else "Other")
has_openxr, has_vrapi = "libopenxr_loader.so" in libset, "libvrapi.so" in libset
static_loaders = [n for n, d in lib_bytes.items() if static_openxr.detected(n, d)] if abi == "arm64-v8a" else []
has_openxr, has_vrapi = bool(static_loaders) or "libopenxr_loader.so" in libset, "libvrapi.so" in libset
xr = "OpenXR+VrApi" if has_openxr and has_vrapi else "OpenXR" if has_openxr else "VrApi" if has_vrapi else "?"
is_overport = "libopenxr_loader_generic.so" in libset or "liboverport.config.so" in libset
# VrApi called directly by the engine (no OVRPlugin): overport cannot translate it.
@@ -404,6 +406,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
is_overport_output=is_overport,
debuggable=bool(axml.Axml(manifest).get_bool("application", "debuggable")),
extra={
"static_openxr_loader": static_loaders,
"analysis_version": ANALYSIS_VERSION,
"xlua_47bit": xlua_47bit(lib_bytes.get("libxlua.so", b"")),
"xlua_sha256": xlua_sha256(lib_bytes.get("libxlua.so", b"")),
@@ -431,7 +434,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
"unreal_version": unreal_version,
"oculus_os_refs": sorted(oculus_os_refs),
"xr_layer_exts": sorted(layer_exts), # composition-layer extensions the game's own libraries request
"vr_kind": vr_kind(libset, manifest_strings),
"vr_kind": ("openxr" if static_loaders
and vr_kind(libset, manifest_strings) == "none" else vr_kind(libset, manifest_strings)),
"headset": headset(meta, abis, vr_kind(libset, manifest_strings), requires_6dof(manifest)),
# a Google Daydream / Cardboard build (GVR / Cardboard SDK): no path to the Frame's headset (GitHub #335)
"google_vr": google_vr(libset, cats, manifest_strings, gvr_java, boot),
+80
View File
@@ -0,0 +1,80 @@
"""Khronos loaders embedded in game libraries (never the separately shipped loader)."""
from __future__ import annotations
import io
import re
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from . import elf
MANIFEST = "/sdcard/fp_openxr_runtime.json"
# Some builds embed complete paths. Match every partition and ABI spelling, rather than a fixed list.
PATH = re.compile(rb"/(?:[A-Za-z0-9_-]+/)*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json\x00")
COMPOSED_MANIFEST = "/sdcard/fp/1/active_runtime.json"
PARTITIONS = ("/product", "/odm", "/oem", "/vendor", "/system")
SUBDIR = "/etc/openxr/"
BROKERS = ("org.khronos.openxr.runtime_broker", "org.khronos.openxr.system_runtime_broker")
def paths(data: bytes) -> list[str]:
if not elf.is_elf(data):
return []
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
if sec is None:
return []
raw = sec.data()
return sorted({m.group()[:-1].decode() for m in PATH.finditer(raw)
if m.start() == 0 or raw[m.start() - 1] == 0})
def detected(name: str, data: bytes) -> bool:
if (name.startswith("libopenxr_loader") or not elf.is_elf(data)
or b"RuntimeManifestFile" not in data or b"xrNegotiateLoaderRuntimeInterface" not in data):
return False
try:
e = ELFFile(io.BytesIO(data))
return e["e_machine"] == "EM_AARCH64" and (bool(paths(data)) or composed(data))
except (ELFError, ValueError):
return False
def composed(data: bytes) -> bool:
"""Khronos platform_utils.hpp builds prefix + subdir + major + suffix.
Require the complete known table before touching these potentially shared literals.
"""
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
strings = set(sec.data().split(b"\0")) if sec else set()
return (all(s.encode() in strings for s in (*PARTITIONS, SUBDIR, "/active_runtime.json"))
and any(s.startswith(b"/active_runtime.") and s.endswith(b".json") for s in strings))
def redirected(data: bytes) -> bool:
if paths(data) or composed(data) or any(b.encode() + b"\0" in data for b in BROKERS):
return False
if MANIFEST.encode() + b"\0" in data:
return True
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
strings = set(sec.data().split(b"\0")) if sec else set()
return (b"card/fp/" in strings and b"/sd" in strings
and not any(p.encode() in strings for p in PARTITIONS))
def rewrite(data: bytes) -> tuple[bytes, int]:
count = 0
if composed(data):
# Keep every candidate identical: /sd + card/fp/ + 1 + /active_runtime[.ABI].json.
# These partition literals may be pooled with app strings; see DEVICE_NOTES.
for old, new in [(p, "/sd") for p in PARTITIONS] + [(SUBDIR, "card/fp/")]:
data, n = elf.replace_rodata_string(data, old, new)
count += n
for old in paths(data):
data, n = elf.replace_rodata_string(data, old, MANIFEST)
count += n
for old in BROKERS:
# Keep URI length unchanged as well as the string's storage; both authorities must miss.
data, n = elf.replace_rodata_string(data, old, old.replace("org.khronos", "org.fp_null"))
count += n
return data, count
+4
View File
@@ -11,6 +11,7 @@ from .core.events import Reporter
from .core.models import Analysis, BuildResult, Recipe, SourceGame
from .core.paths import work_dir
from .patches import base, upstream
from .patches.frame import zink_shader_fix
from .patches.overport import OVERPORT_PATCHES
from .tools import overport as overport_tool
from .validate.static import check_apk
@@ -54,6 +55,9 @@ def apk_patches(selection: dict) -> list[base.Patch]:
if "frame.adapter" in selection and "device.foveation" not in selection:
# Prepare verified game-specific support independently of the player's runtime preference.
patches.append(base.get("device.foveation"))
if zink_shader_fix.configured(selection) and "frame.zink_shader_fix" not in selection:
# the shader dump / fix settings only work through this layer (GitHub #382: the switch alone did nothing)
patches.append(base.get("frame.zink_shader_fix"))
return sorted(patches, key=lambda p: p.order)
+6
View File
@@ -20,6 +20,12 @@ def is_gles(a: Analysis) -> bool:
return "GLES" in a.graphics
def may_be_gles(a: Analysis) -> bool:
"""GLES, or Unity whose boot.config merely mentions Vulkan: many such builds render with OpenGL ES (SynthRiders,
I Am Cat; GitHub #382), so GLES-only diagnostics and fixes stay offered."""
return is_gles(a) or a.graphics == "Vulkan (Unity boot.config)"
def is_vulkan(a: Analysis) -> bool:
return a.graphics.startswith("Vulkan")
@@ -0,0 +1,69 @@
"""Route statically linked Khronos OpenXR loaders through FrameBridge."""
from __future__ import annotations
from ...analysis import static_openxr
from ...tools import overport
from ..base import ApkContext, Patch, Suggestion, register
from . import artifact
from .adapter import GENERIC, ORIGINAL
SHIM = "libfp_xrruntime.so"
class StaticOpenXRLoader(Patch):
id = "frame.static_openxr_loader"
title = "Embedded OpenXR loader: use FrameBridge"
description = ("Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its "
"runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its "
"Android runtime "
"broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner "
"Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.")
requires = ("frame.adapter",)
order = 9 # provide the inner loader before frame.adapter wraps it
def applies(self, a):
return bool((a.extra or {}).get("static_openxr_loader"))
def detect(self, a):
if self.applies(a):
return Suggestion(True, "The embedded Khronos OpenXR loader bypasses FrameBridge.")
return None
def apply(self, ctx: ApkContext) -> bool:
ws = ctx.ws
if ws.abi != "arm64-v8a":
raise RuntimeError("Embedded OpenXR loader redirection requires arm64")
libs = (ctx.analysis.extra or {}).get("static_openxr_loader") or []
if not libs:
raise RuntimeError("No embedded Khronos OpenXR loader detected; analyze the APK again")
changed = False
for name in libs:
data = ws.read_lib(name)
fixed, count = static_openxr.rewrite(data)
if not count and not static_openxr.redirected(data):
raise RuntimeError(f"{name}: embedded loader manifest paths could not be redirected")
if count:
ws.put(ws.lib(name), fixed)
changed = True
# Like a32_bridge, also works when the recipe doesn't run OVRPort at all.
if not ws.has(ws.lib(ORIGINAL)) and not ws.has(ws.lib(GENERIC)):
inner = overport.ensure_runtime_lib(GENERIC, ws.abi, ctx.reporter).read_bytes()
ws.put(ws.lib(ORIGINAL), inner)
ws.put(ws.lib(GENERIC), artifact(ws.abi, GENERIC))
changed = True
shim = artifact(ws.abi, SHIM)
if not ws.has(ws.lib(SHIM)) or ws.read_lib(SHIM) != shim:
ws.put(ws.lib(SHIM), shim)
changed = True
return changed
def validate(self, ctx: ApkContext):
ws = ctx.ws
libs = (ctx.analysis.extra or {}).get("static_openxr_loader") or []
ok = bool(libs) and all(ws.has(ws.lib(n)) and static_openxr.redirected(ws.read_lib(n))
for n in libs)
ok = ok and ws.has(ws.lib(SHIM)) and ws.read_lib(SHIM) == artifact(ws.abi, SHIM)
return [("Embedded OpenXR loader uses FrameBridge", ok, ", ".join(libs))]
register(StaticOpenXRLoader)
@@ -10,6 +10,7 @@ are set hang the GPU) by Klownicle, GitHub #49."""
from __future__ import annotations
from ...analysis import elf
from .. import applicability as ap
from ..base import ApkContext, Patch, register
from . import artifact
@@ -48,12 +49,13 @@ class ZinkShaderFix(Patch):
"the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every "
"other module is untouched), for example stores that set loop counters and accumulators a shader "
"reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, "
"GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer "
"GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe, and either "
"setting brings it in by itself. The layer "
"adds itself to Android's Vulkan debug layer list for the game's process only.")
order = 73
def applies(self, a):
return "GLES" in a.graphics and "arm64-v8a" in a.abis
return ap.may_be_gles(a) and "arm64-v8a" in a.abis
def apply(self, ctx: ApkContext) -> bool:
ws = ctx.ws
+2 -2
View File
@@ -647,8 +647,8 @@ class AdapterSetting(Patch):
"vk_pipeline_fallback": lambda a: a.engine in ("Unreal", "Other") and "arm64-v8a" in a.abis,
# wherever the Vulkan shim can be (vk_sanitize); Unreal's Vulkan builds are rarely detected as such
"vk_shader_dump": lambda a: a.engine in ("Unreal", "Other") and "arm64-v8a" in a.abis,
"zink_shader_fix": ap.is_gles,
"zink_shader_dump": ap.is_gles,
"zink_shader_fix": ap.may_be_gles,
"zink_shader_dump": ap.may_be_gles,
"haptic_fix": lambda a: "libOVRPlugin.so" in a.libs,
"surface_native": lambda a: "arm64-v8a" in a.abis, # the xrshim it needs is arm64 only (also above)
"pose_time_fix": lambda a: "libOVRPlugin.so" in a.libs,
+1
View File
@@ -4,6 +4,7 @@ Game settings (adapter.*) use their Game settings help line (patches/settings.UI
fallback for settings that dialog doesn't show."""
SUMMARIES = {
"frame.static_openxr_loader": "Makes an embedded OpenXR loader use FrameBridge and your game settings.",
# OVRPort (the Quest → OpenXR conversion)
"patch_copy_libraries": "Adds the parts that let a Quest game run on the Steam Frame. Needed for every Quest "
"game.",
+188
View File
@@ -0,0 +1,188 @@
"""Embedded loader redirection, independent of any game or device."""
import json
import shutil
import struct
import subprocess
import zipfile
import pytest
from test_agent import load_agent
from test_patches import _analysis
from frameport.analysis import detect
from frameport.analysis import static_openxr as sx
from frameport.apk.workspace import ApkWorkspace
from frameport.core.events import Reporter
from frameport.patches import base
from frameport.patches.frame import static_openxr_loader as patch
@pytest.fixture
def embedded(tmp_path):
# A real ELF layout with synthetic strings; change only e_machine to model an arm64 game.
compiler = shutil.which("cc")
if not compiler:
pytest.skip("needs host C compiler")
strings = [f"/{p}/etc/openxr/1/active_runtime{abi}.json"
for p in ("product", "odm", "oem", "vendor", "system") for abi in ("", ".aarch64", ".arm64-v8a")]
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface", "unrelated"]
src = tmp_path / "embedded.c"
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
out = tmp_path / "embedded.so"
subprocess.run([compiler, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
data = bytearray(out.read_bytes())
struct.pack_into("<H", data, 18, 183) # EM_AARCH64
return bytes(data)
def test_detection_and_byte_stable_rewrite(embedded):
assert sx.detected("libengine.so", embedded)
assert not sx.detected("libopenxr_loader.so", embedded)
assert not sx.detected("libengine.so", embedded.replace(b"RuntimeManifestFile", b"X" * 19))
wrong_arch = bytearray(embedded)
struct.pack_into("<H", wrong_arch, 18, 62)
assert not sx.detected("libengine.so", bytes(wrong_arch))
fixed, count = sx.rewrite(embedded)
assert count == 17
assert len(fixed) == len(embedded)
expected = embedded
for old in sx.paths(embedded) + list(sx.BROKERS):
new = sx.MANIFEST if old.startswith("/") else old.replace("org.khronos", "org.fp_null")
expected = expected.replace(old.encode() + b"\0", new.encode().ljust(len(old) + 1, b"\0"))
assert fixed == expected # all other bytes, offsets and ELF metadata are identical
assert sx.rewrite(fixed) == (fixed, 0)
assert not sx.paths(fixed)
assert detect.ANALYSIS_VERSION >= 23
def test_patch_and_adapter_without_overport(tmp_path, monkeypatch, embedded):
inner = tmp_path / "inner.so"
inner.write_bytes(b"unmodified Khronos loader")
monkeypatch.setattr(patch.overport, "ensure_runtime_lib", lambda *args: inner)
apk = tmp_path / "in.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("lib/arm64-v8a/libengine.so", embedded)
a = _analysis(extra={"static_openxr_loader": ["libengine.so"]})
selection = {patch.StaticOpenXRLoader.id: {}, "frame.adapter": {}}
p = patch.StaticOpenXRLoader()
assert p.detect(a).recommended
assert not p.applies(_analysis())
with ApkWorkspace(apk) as ws:
ctx = base.ApkContext(ws, a, {}, Reporter(), selection)
assert p.apply(ctx)
assert base.get("frame.adapter").apply(ctx)
assert ws.read_lib("libopenxr_loader_original.so") == inner.read_bytes()
assert ws.has(ws.lib("libframe_settings.so"))
assert all(ok for _, ok, _ in p.validate(ctx))
assert all(ok for _, ok, _ in base.get("frame.adapter").validate(ctx))
assert not p.apply(ctx)
assert not base.get("frame.adapter").apply(ctx)
out = ws.write(tmp_path / "out.apk")
with ApkWorkspace(out) as ws:
ctx.ws = ws
assert not p.apply(ctx)
ws.put(ws.lib("libengine.so"), embedded)
assert not p.validate(ctx)[0][1]
def test_manifest_repair_and_launcher_upgrade(tmp_path, monkeypatch):
a = load_agent(monkeypatch, tmp_path)
root = tmp_path / "game"
a.write_openxr_runtime_manifest(str(root), False)
assert not root.exists()
a.write_openxr_runtime_manifest(str(root), True)
target = root / "lepton-data/external/fp_openxr_runtime.json"
manifest = json.loads(target.read_text())
assert manifest["runtime"]["library_path"] == patch.SHIM
victim = tmp_path / "victim"
victim.write_text("untouched")
target.unlink()
target.symlink_to(victim)
a.write_openxr_runtime_manifest(str(root), True)
assert victim.read_text() == "untouched"
assert not target.is_symlink()
anchor = tmp_path / "anchors/com.example.game"
anchor.mkdir(parents=True)
a.write_launcher(str(anchor), str(root), "com.example.game", "Test", 123, "/lepton", {}, static_openxr=True)
text = (anchor / "launch.sh").read_text()
assert " _openxr_manifest com.example.game\n" in text
assert a.set_openxr_manifest_line(text, "com.example.game", True) == text
assert "_openxr_manifest" not in a.set_openxr_manifest_line(text, "com.example.game", False)
subprocess.run(["bash", "-n", str(anchor / "launch.sh")], check=True)
def test_runtime_shim_host(tmp_path):
import os
from pathlib import Path
root = Path(__file__).resolve().parents[1]
headers = next((root / "native/.cache").glob("openxr-*/openxr/openxr.h"), None)
cc = shutil.which("cc")
if not headers or not cc:
pytest.skip("needs cached OpenXR headers and host C compiler")
src = root / "native/xrruntime"
inc = str(headers.parent.parent)
subprocess.run([cc, "-shared", "-fPIC", "-I", inc, str(src / "test_bridge.c"),
"-o", str(tmp_path / "libopenxr_loader_generic.so")], check=True)
exe = tmp_path / "shim_test"
subprocess.run([cc, "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers", "-I", inc,
str(src / "xrruntime.c"), str(src / "test.c"), "-ldl", "-lpthread", "-o", str(exe)], check=True)
env = dict(os.environ, LD_LIBRARY_PATH=str(tmp_path))
subprocess.run([str(exe)], env=env, check=True)
(tmp_path / "libopenxr_loader_generic.so").unlink()
subprocess.run([str(exe), "missing"], env=env, check=True)
def test_composed_khronos_paths(tmp_path, embedded):
# Rebuild the fixture with Khronos platform_utils.hpp's actual fragment table.
# This is also the layout in OVRPort's unmodified Khronos loader.
from frameport.analysis import elf
cc = shutil.which("cc")
src = tmp_path / "composed.c"
strings = list(sx.PARTITIONS) + [sx.SUBDIR, "/active_runtime.json", "/active_runtime.aarch64.json"]
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface"]
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
out = tmp_path / "composed.so"
subprocess.run([cc, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
buf = bytearray(out.read_bytes())
struct.pack_into("<H", buf, 18, 183)
data = bytes(buf)
assert sx.composed(data) and sx.detected("libgame.so", data)
fixed, count = sx.rewrite(data)
assert count == 8 and len(fixed) == len(data)
assert sx.redirected(fixed) and sx.rewrite(fixed) == (fixed, 0)
expected = data
for old, new in [(p, "/sd") for p in sx.PARTITIONS] + [(sx.SUBDIR, "card/fp/")] + [
(p, p.replace("org.khronos", "org.fp_null")) for p in sx.BROKERS]:
expected, _ = elf.replace_rodata_string(expected, old, new)
assert fixed == expected
assert "/sd" + "card/fp/" + "1" + "/active_runtime.json" == sx.COMPOSED_MANIFEST
def test_analysis_records_embedded_loader(tmp_path, embedded, quest_manifest):
apk = tmp_path / "analyze.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("AndroidManifest.xml", quest_manifest)
z.writestr("lib/arm64-v8a/libengine.so", embedded)
assert detect.analyze(apk).extra["static_openxr_loader"] == ["libengine.so"]
def test_triage_only_when_chain_absent():
from frameport.validate.triage import triage
line = "RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json"
assert "static-openxr-loader-bypass" in {f.id for f in triage(line, "RUNNING").findings}
for chain in ("FrameBridge: settings read", "fp_xrruntime: negotiated"):
assert "static-openxr-loader-bypass" not in {f.id for f in triage(line + "\n" + chain, "RUNNING").findings}
def test_heuristic_selects_chain_without_game_specific_rules():
from frameport.recommend.engine import suggest
a = _analysis(package="com.example.othergame", engine="Other", libs=["libengine.so"],
extra={"static_openxr_loader": ["libengine.so"], "vr_kind": "openxr"})
r = suggest(a, use_catalog=False)
assert {"frame.static_openxr_loader", "frame.adapter"} <= r.patches.keys()
assert not r.as_is
+33
View File
@@ -0,0 +1,33 @@
"""GitHub #382: Unity games whose boot.config merely mentions Vulkan often render with OpenGL ES (SynthRiders), so the
OpenGL ES shader dump/fix stay offered for them, and either setting brings in the shader-fix layer by itself."""
from frameport.build import apk_patches
from frameport.core.models import Analysis
from frameport.patches import base
def analysis(graphics):
return Analysis(package="com.kluge.SynthRiders", version="3.6.6a2", label="Synth Riders", abis=["arm64-v8a"],
engine="Unity", xr="OpenXR", graphics=graphics, direct_vrapi=False,
libs=["libunity.so", "libOVRPlugin.so"], launcher_activity=None, has_info_category=False,
meta_permissions=[], uses_glad_gl=False, unity_msaa_levels=0, oculus_os_classes=False,
is_overport_output=False, debuggable=False, extra={})
def test_shader_dump_offered_for_unity_boot_config_vulkan():
a = analysis("Vulkan (Unity boot.config)")
for pid in ("adapter.zink_shader_dump", "adapter.zink_shader_fix", "frame.zink_shader_fix"):
assert base.get(pid).applies(a), pid
def test_shader_dump_hidden_for_declared_vulkan():
a = analysis("Vulkan (declared in manifest)")
for pid in ("adapter.zink_shader_dump", "adapter.zink_shader_fix", "frame.zink_shader_fix"):
assert not base.get(pid).applies(a), pid
def test_dump_setting_alone_adds_the_layer():
ids = [p.id for p in apk_patches({"frame.adapter": {}, "adapter.zink_shader_dump": {"value": 1}})]
assert ids.count("frame.zink_shader_fix") == 1
ids = [p.id for p in apk_patches({"frame.zink_shader_fix": {}, "adapter.zink_shader_dump": {"value": 1}})]
assert ids.count("frame.zink_shader_fix") == 1
assert "frame.zink_shader_fix" not in [p.id for p in apk_patches({"adapter.zink_shader_dump": {"value": 0}})]