mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-20 22:01:42 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5dab15149 | ||
|
|
54effe9ab0 | ||
|
|
d78fcbd20a | ||
|
|
0322b8b4d6 |
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 -> false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -> 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".
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
@@ -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}})]
|
||||
Reference in new issue
Block a user