Compare commits

..
Author SHA1 Message Date
spoopyghosty0 2e89bd83d3 Docs: update screenshots and videos 2026-10-19 04:22:00 +00:00
spoopyghosty0 d3a0846775 Release prep: the #406, #254 and #387 fixes together on main
Adapter rebuilt from the combined source (fixed_resolution + swapchain_pool), libfp_xrruntime.so back in SHA256SUMS, ANALYSIS_VERSION 24 for static OpenXR loaders (23 is frame.main_process), agent v108 for the static-loader launcher line (above the MR branches' 104-107 so a Frame with one of those still updates), patch docs and translations template regenerated, Ruff line length.
2026-10-19 00:10:31 -04:00
Codex 25db57f468 Fix #387: pool GLES panel swapchains before the runtime count limit
Add opt-in swapchain_pool with private GLES images and up to two packed
runtime atlases. Start at eight live app runtime swapchains to reserve
headroom before the session cap. Preserve image ownership, GL state,
source fences, crops and the existing cylinder/equirect layer ordering.

Enable for 4XVR, add count-limit triage without unknown suggestions for
older apps, and document the pending headset menu check. Leave default
recipes and adapter revisions unchanged. Rebuild arm64/arm32 artifacts.

Validation: 132 targeted tests passed (2 skipped), including the native
runtime/GL harness; Ruff and generated docs pass. Full host suite:
2095 passed, 137 skipped, 12 failures and 16 setup errors, all denied
socket creation in this sandbox. No headset validation or push.
2026-10-19 00:04:16 -04:00
Codex bd2a742356 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-19 00:03:49 -04:00
spoopyghosty0 d0a2d89c92 Keep opt-in OpenXR layer recommendations at full swapchain resolution (#406)
Route OVRPlugin recommendations through the existing extension shim, retain native loader hooks, and log projection rect changes once per second per eye. Enable the fix for Batman, document verification, and rebuild both adapter architectures.
2026-10-19 00:03:20 -04:00
spoopyghosty0 12025e73dd Catalog: Test Quest MR (works)
Verified on the Frame 2026-10-18 with 0.2.1 (room, planes, mesh and anchors through FramePort; with frame.meta_permissions and scene_emul, the MR heuristics' picks).
2026-10-18 19:36:03 -04:00
spoopyghosty0 8d4fb7b6e1 Catalog: KillerFrequency and cubism (works)
Closes #402
Closes #403
2026-10-18 14:23:19 -04:00
spoopyghosty0 4b513e195f Gamepad for 2D apps: own button layout (swap sticks, swap A/B, own .kl lines) and Steam's controller log in diagnostics (#162); fix main's CI
Steam Input's per-game remapping doesn't reach a 2D app; device.steam_gamepad's options now change the buttons
inside the app: the launcher exports FRAMEPORT_GAMEPAD_LAYOUT/_KEYS, the agent's Podman wrapper mounts a per-game
key layout (agent v103). Diagnostics of such games carry gamepad.log and Steam's controller activation lines. The
game page shows a patch's on/off options under it while it's on.
CI: locales template refreshed; frame.main_process counts as a patch that works on the 32-bit bridge.
2026-10-18 12:57:04 -04:00
spoopyghosty0 206a2448b3 Triage scene-missing: extension listings don't count (OVRPlugin Name=, OVRPort's list, Unity's Extensions=) (#404) 2026-10-18 12:40:10 -04:00
spoopyghosty0 104c003c44 frame.main_process: run a game activity that has its own process in the main one (#178)
Halo CE VR's HaloActivity runs in :halo_game; Lepton starts it directly and watches only the main process, which
never exists, so it closed the game about a minute in ('Target app ... did not start'). Analysis field
separate_process (ANALYSIS_VERSION 23), default-on patch, triage app-separate-process, PLAYBOOK row.
2026-10-18 12:35:48 -04:00
spoopyghosty0 b281cd5fdb Catalog: HardBullet (issues: cube loading screen, view freezes during loads)
Closes #385
2026-10-18 12:17:35 -04:00
spoopyghosty0 4fec0656ff 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.
2026-10-18 12:14:42 -04:00
spoopyghosty0 a205743561 Catalog: BattleTalent online rooms need Meta's account check, LAN works (#408) 2026-10-18 12:06:49 -04:00
50 changed files with 2067 additions and 70 deletions

No files matched your search

+105 -8
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 = 108
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]
@@ -3634,6 +3636,38 @@ axis 0x10 HAT_X
axis 0x11 HAT_Y
"""
BTN_SOUTH = 0x130 # BTN_A / BTN_GAMEPAD: Linux's gamepad button range starts here
# A game's own button layout (device.steam_gamepad's options, GitHub #162: Steam Input's per-game remapping doesn't
# reach a 2D app): its launcher exports FRAMEPORT_GAMEPAD_LAYOUT (comma list: swap_sticks = the right stick is
# Android's main stick, which Android's own D-pad navigation follows; swap_ab) and FRAMEPORT_GAMEPAD_KEYS (Android .kl
# lines separated by ';', each replacing the line for the same key code or axis), read by podman_run_args.
KL_LINE = re.compile(r"(key [0-9]{1,4} [A-Z][A-Z0-9_]{0,40}( (FUNCTION|WAKE))?"
r"|axis 0x[0-9a-f]{1,2} (invert )?[A-Z][A-Z0-9_]{0,40}( flat [0-9]{1,5})?)")
def gamepad_kl(layout="", keys=""):
"""GAMEPAD_KL with a game's layout options and own lines (invalid ones left out); the text and what was left out."""
lines = {}
for line in GAMEPAD_KL.splitlines()[1:]:
kind, code = line.split()[:2]
lines[(kind, code if kind == "axis" else str(int(code)))] = line
opts = {o.strip() for o in (layout or "").split(",") if o.strip()}
if "swap_sticks" in opts:
lines[("axis", "0x00")], lines[("axis", "0x03")] = "axis 0x00 Z flat 4096", "axis 0x03 X flat 4096"
lines[("axis", "0x01")], lines[("axis", "0x04")] = "axis 0x01 RZ flat 4096", "axis 0x04 Y flat 4096"
if "swap_ab" in opts:
lines[("key", "304")], lines[("key", "305")] = "key 304 BUTTON_B", "key 305 BUTTON_A"
skipped = []
for line in (keys or "").split(";"):
line = " ".join(line.split())
if not line:
continue
if not KL_LINE.fullmatch(line):
skipped.append(line)
continue
kind, code = line.split()[:2]
lines[(kind, code if kind == "axis" else str(int(code)))] = line
head = GAMEPAD_KL.splitlines()[0] + (f" Options: {layout or '-'}; own lines: {keys}" if opts or keys else "")
return "\n".join([head, *lines.values()]) + "\n", skipped
def has_key_bit(caps, code):
@@ -3706,7 +3740,13 @@ def podman_run_args(args, env, sys_root="/sys", dev_root="/dev"):
extra = []
if PAD_DIR not in taken:
extra += ["--mount", f"type=bind,source={os.path.join(dev_root, 'input')},destination={PAD_DIR},rw"]
layout = gamepad_layout_file() if extra else ""
text, skipped = gamepad_kl(env.get("FRAMEPORT_GAMEPAD_LAYOUT", ""), env.get("FRAMEPORT_GAMEPAD_KEYS", ""))
layout = gamepad_layout_file(text) if extra else ""
if skipped:
print(f"FramePort gamepad: key layout lines left out (not .kl syntax): {'; '.join(skipped)}", file=sys.stderr)
if text != GAMEPAD_KL:
print(f"FramePort gamepad: own button layout ({env.get('FRAMEPORT_GAMEPAD_LAYOUT') or '-'}"
f"{'; ' + env['FRAMEPORT_GAMEPAD_KEYS'] if env.get('FRAMEPORT_GAMEPAD_KEYS') else ''})", file=sys.stderr)
for product in sorted({"11ff", *(p["product"] for p in pads)}): # 11ff: Steam's pad, also when it comes later
target = f"/system/usr/keylayout/Vendor_28de_Product_{product}.kl"
if layout and target not in taken and re.fullmatch(r"[0-9a-f]{4}", product):
@@ -3754,18 +3794,21 @@ def gamepad_links(appid, parent, poll=1.0, refresh=15.0, exec_=None, pads=steam_
time.sleep(poll)
def gamepad_layout_file():
path = os.path.join(os.path.dirname(PODMAN_BIN), "steam-gamepad.kl")
def gamepad_layout_file(text=GAMEPAD_KL):
"""The key layout file for `text`: steam-gamepad.kl, or one per distinct own layout (steam-gamepad-<hash>.kl)."""
name = "steam-gamepad.kl" if text == GAMEPAD_KL else \
f"steam-gamepad-{hashlib.sha256(text.encode()).hexdigest()[:12]}.kl"
path = os.path.join(os.path.dirname(PODMAN_BIN), name)
try:
with open(path) as f:
if f.read() == GAMEPAD_KL:
if f.read() == text:
return path
except OSError:
pass
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = f"{path}.{os.getpid()}.tmp"
with open(tmp, "w") as f:
f.write(GAMEPAD_KL)
f.write(text)
os.replace(tmp, path)
return path
@@ -3884,7 +3927,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 +3947,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 +3957,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 +4519,15 @@ 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 +4539,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)
@@ -6882,6 +6965,15 @@ def cmd_collect_diag(args):
for i, part in enumerate(game_logs(base)):
files[f"game-log-{i}.txt"] = part[-max_bytes:]
logs = os.path.join(STEAM, "logs")
if dep.get("steam_gamepad"): # which controller layout Steam activated, and the pads the container got (#162)
text = _tail(os.path.join(base, "gamepad.log"), max_bytes)
if text is not None:
files["gamepad.log"] = text
text = _tail(os.path.join(logs, "controller.txt"), 4 * max_bytes)
if text:
keep = [ln for ln in text.splitlines() if re.search(
r"Queueing activation|warmed config|Opted-in Controller|Loaded Config|virtual gamepad|xinput slot", ln)]
files["steam-controller.txt"] = "\n".join(keep[-400:]) + "\n"
text = _tail(os.path.join(logs, f"lepton-steamlaunch-{appid}.log"), max_bytes) # Lepton's launcher log
if text is not None:
files[f"lepton-steamlaunch-{appid}.log"] = text
@@ -9040,6 +9132,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":
+3 -2
View File
@@ -9,15 +9,16 @@ 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
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
b085db012dfcdf2690fc3c53e8c3fef0c5d2011e35d4307756b0c6347e29d219 ./arm64-v8a/libopenxr_loader_generic.so
95f4ad50a33ac1bc6c58b70771cd22963c78a8d8fef3e33a7e41f2e2ffe6f940 ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
fcd8f37b3acc3a2829f255c2f2fa16d20b975c8831d72518a659cb78ed6e3ae2 ./armeabi-v7a/libopenxr_loader_generic.so
ff9e03b526ef9f67ce36f8ebe8943c0d5b765dc31b1ca4c9d5655ded141cf39f ./armeabi-v7a/libopenxr_loader_generic.so
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,19 +8,21 @@ notes: 'Native OpenXR GLES video player. 360 theatres/videos (equirect2 layers,
details: 'Settings: equirect_emul (GLES worker draws 360 layers into a projection layer), stable_local, focus_hold,
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains. Its hardware decoding mode (Java MediaCodec,
Vr4pMediaPlayer; its own FFmpeg is the software path) gets the Frame''s video hardware with frame.hw_video_decode
(added 2026-10-09, not yet checked in the headset).'
(added 2026-10-09, not yet checked in the headset). swapchain_pool shares extra GLES panel images to avoid the
runtime swapchain count cap (#387); host-tested only, menu interaction needs a headset check.'
tested_version: 2.0.22
engine: Other
xr: OpenXR
frame:
- frame.hw_video_decode
adapter:
swapchain_pool: 1
equirect_emul: 1
stable_local: 1
focus_hold: 1
scale: 0.75
verified:
date: '2026-10-01'
updated: '2026-10-10'
updated: '2026-10-18'
min_app: 0.12.1
source_hint: 4XVR Video Player
+23
View File
@@ -0,0 +1,23 @@
package: com.mrf.tvb.cubism
title: cubism
status: works
notes: works and passthrough play works also
tested_version: 1.8.0
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-17'
known_good_sha256: fd1ba8eae19efe0f2a17fc146847de677263125f9ec5437cea5c632f0cd88dcf
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 91
issue: 403
source_hint: Cubism
@@ -0,0 +1,26 @@
package: com.team17.killerfrequency
title: KillerFrequency
status: works
tested_version: 1.0.767.38513
engine: Unity
xr: VrApi
frame:
- frame.unity_no_msaa
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.hw_video_decode
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
scale: 1.4000000000000001
verified:
date: '2026-10-18'
known_good_sha256: a11b008fcc0f5bc7aa5a3bb04d4267144d5ec60b26529a9b20e6499e12dcf079
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 402
source_hint: killer freq
@@ -0,0 +1,20 @@
package: io.github.spoopyghosty0.testquestmr
title: Test Quest MR
status: works
notes: An MR debug viewer that shows what the Meta MR APIs return (room, planes, mesh, anchors, play area). With
FramePort's emulated room it shows a room made from the play area.
tested_version: 0.2.1
engine: Other
xr: OpenXR
frame:
- frame.meta_permissions
adapter:
scene_emul: 1
verified:
date: '2026-10-18'
known_good_sha256: 98936ca80d548a753a6645ab89610af19f6e8c3190b5f5f67854cf854691fba5
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 102
source_hint: Test Quest MR
+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'
+21 -2
View File
@@ -13,6 +13,17 @@ signatures:
diagnosis: The game submits an offset projection viewport. With Valve foveation, dynamic resolution can move the sharp centre or expose a white edge. Keeping full resolution may help; it increases GPU load and needs the updated adapter and OVRPlugin shim in a rebuilt APK.
question: With foveated rendering enabled, was the sharp centre misplaced or was there a white line at the picture edge?
suggest: [adapter.fixed_resolution]
- 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: swapchain-limit-reached
pattern: 'FrameBridge:\s+xrCreateSwapchain [^\r\n]*\bresult=-10\b'
severity: error
diagnosis: The Frame runtime reached its swapchain count limit (about 15 live swapchains per session), not necessarily a memory limit. Extra menus or video panels may close or stay gray. Updated FramePort builds provide Share panel images (swapchain limit) for GLES apps; Vulkan apps are unaffected.
suggest: []
- id: quest2-headset-identity
pattern: 'venderId[^\r\n]*systemName Oculus Quest2'
severity: info
@@ -390,8 +401,10 @@ signatures:
diagnosis: Frame controller models are on, but the converted models are missing from the game's files. The agent found no Frame controller render models in SteamVR at install time; see the install log, or run the agent's controller_models command.
suggest: []
- id: scene-missing
# not OVRPlugin's routine "Unavailable OpenXR extension: XR_FB_scene" (printed by every OVRPlugin game)
pattern: '(?<!Unavailable OpenXR extension: )XR_FB_scene|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
# not extension listings, which every Meta XR game prints: OVRPlugin's "Unavailable OpenXR extension:" and
# "Name=XR_FB_scene SpecVersion=1", OVRPort's "overportOXR: - XR_FB_scene 1", Unity's 'Meta XR Feature: ...
# Extensions="..."' (GitHub #404); XR_FB_scene_capture alone doesn't count either
pattern: '(?m)^(?!.*(?:Unavailable OpenXR extension: |Name=|overportOXR: - |Extensions=")).*XR_FB_scene\b|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
severity: warning
diagnosis: The game wants Meta's room data (scene API), which the Frame lacks. The adapter can emulate a room from the guardian bounds.
suggest: [adapter.scene_emul]
@@ -566,6 +579,12 @@ signatures:
severity: fatal
diagnosis: 'Games can''t start until the Frame restarts: it ran out of kernel keys. Rootless podman leaks one session keyring per container start; the FramePort agent sets keyring=false in ~/.config/containers/containers.conf, and a reboot frees the leaked keys.'
suggest: []
- id: app-separate-process
pattern: 'Start proc \d+:[\w.]+:\w+/\S+ for (pre-)?top-activity'
severity: error
diagnosis: The game's activity runs in an Android process of its own, while Lepton watches only the app's main process, which never starts. About a minute in, Lepton decides the app "did not start" and closes it, wherever the game is. Newer FramePort versions run the activity in the main process (patch "Start the game in the app's main process"); update FramePort and use Update on Frame.
suggest: []
supersedes: [app-not-started, force-quit]
- id: app-not-started
pattern: 'Target app \(.*\) did not start'
severity: error
+77 -1
View File
@@ -146,11 +146,23 @@ pose (images drawn only after head turns, or ahead from xrWaitFrame and sometime
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
swapchain was originally halved (1536/eye) while investigating memory. Issue #387 below confirms a swapchain
count cap; reducing image sizes does not avoid it. Frame data snapshot
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
Not yet verified in the headset.
**Issue #387 (2026-10-18, host-tested only):** 4XVR's extra sub-panels hit the session cap at roughly 15–16 live
swapchains: the same 256×128 create succeeds then fails in the same millisecond, so shrinking images is not a fix.
Default-off GLES `swapchain_pool` (catalog enabled for 4XVR) starts at eight live app runtime swapchains, reserving runtime
headroom for up to two 4096² SRGB atlases. Single-face/layer/sample/mip colour images use private app-context textures;
submitted panels are blitted into packed atlas rectangles after cylinder strips and rect_clamp. Destroy frees rectangles.
Vulkan is untouched. No adapter revision bump: this opt-in fix should not mark every installed game for update.
The new catalog key already makes older apps reject the recipe via `unusable_reason`; triage suggests no unknown setting.
Headset check: repeatedly open/close all 4XVR submenus while playing stereo and 360 video, verify both eyes, panel crops,
curved strips, alpha and frame pacing; confirm no new -10 errors or menu closures and reuse after closing panels.
## Companion apps / intents (Stremio + 4XVR)
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
@@ -827,3 +839,67 @@ This is separate from #303's `foveation_storage_fix` (clearing SUBSAMPLED alloca
see PLAYBOOK and FRAME_RUNTIME), which fixes black/coarse storage corruption. It also leaves
#394's `FOVE_LEVEL=lo/med/hi` presets intact (commits 01ad5e6b, d7f0d514): those select the
full-swapchain density pattern, not the game's dynamic viewport.
## 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 v108 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
@@ -42,6 +42,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Creed | Quest | ✅ Works | |
| Crisis Zone VR | Quest | ✅ Works | |
| cubism | Quest | ✅ Works | |
| cubism | Quest | ✅ Works | |
| Dance Central | Quest | ✅ Works | |
| Deep Cuts | Quest | ✅ Works | |
| Demeo | Quest | ✅ Works | |
@@ -77,6 +78,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Jurassic World Aftermath Collection | Quest | ✅ Works | |
| Keep Talking and Nobody Explodes | Quest | ✅ Works | |
| Kill It With Fire VR | Quest | ✅ Works | |
| KillerFrequency | Quest | ✅ Works | |
| Ladder | Quest | ✅ Works | |
| Lambda1VR | Quest | ✅ Works | |
| LEGO® Bricktales | Quest | ✅ Works | |
+72 -24
View File
@@ -2,7 +2,7 @@
# Patches
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.
Every patch and game setting FramePort knows: 190 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,8 +11,8 @@ 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)
- [Game settings](#group-adapter) (78)
- [Steam Frame patches](#group-frame) (63)
- [Game settings](#group-adapter) (79)
- [Files and settings on the Frame](#group-device) (10)
- [PC VR (Proton and Revive)](#group-pcvr) (13)
@@ -58,12 +58,13 @@ 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 | 34 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 35 |
| [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 |
| [Fix Lua startup crashes on Frame](#frame-luajit_pointers) | `frame.luajit_pointers` | Steam Frame patches | 1 |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 4 |
| [Start the game in the app's main process](#frame-main_process) | `frame.main_process` | Steam Frame patches | default |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 5 |
| [Meta XR Audio: skip Quest telemetry class](#frame-metaxr_telemetry) | `frame.metaxr_telemetry` | Steam Frame patches | default |
| [Clear android:debuggable](#frame-nodebug) | `frame.nodebug` | Steam Frame patches | 3 |
| [Oculus OS telemetry class stubs](#frame-oculusos) | `frame.oculusos` | Steam Frame patches | 17 |
@@ -77,16 +78,17 @@ 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 | |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 26 |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 27 |
| [Unity: render each eye separately (no multiview)](#frame-unity_multipass) | `frame.unity_multipass` | Steam Frame patches | 2 |
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 10 |
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 11 |
| [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 | 118 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 33 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 119 |
| [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 |
@@ -138,7 +140,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 | 103 |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 104 |
| [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 |
@@ -159,9 +161,9 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 3 |
| [Wait for the game's GPU](#adapter-release_wait) | `adapter.release_wait` | Game settings | |
| [Re-create play space](#adapter-respace_kick) | `adapter.respace_kick` | Game settings | |
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 14 |
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 15 |
| [Room depth](#adapter-scene_depth) | `adapter.scene_depth` | Game settings | |
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 12 |
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 14 |
| [Room height](#adapter-scene_height) | `adapter.scene_height` | Game settings | |
| [Room width](#adapter-scene_width) | `adapter.scene_width` | Game settings | |
| [Eye-image snapshots (diagnostics)](#adapter-snapshot) | `adapter.snapshot` | Game settings | |
@@ -172,6 +174,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Native video surfaces (Batman renderer)](#adapter-surface_native) | `adapter.surface_native` | Game settings | 1 |
| [Swap eyes](#adapter-swap_eyes) | `adapter.swap_eyes` | Game settings | |
| [Retry refused picture formats](#adapter-swapchain_fix) | `adapter.swapchain_fix` | Game settings | |
| [Share panel images (swapchain limit)](#adapter-swapchain_pool) | `adapter.swapchain_pool` | Game settings | 1 |
| [Steady controller input after focus](#adapter-sync_guard) | `adapter.sync_guard` | Game settings | 3 |
| [Hide Vulkan foveation](#adapter-vk_hide_fdm) | `adapter.vk_hide_fdm` | Game settings | 1 |
| [Without Steam's shader cache layer](#adapter-vk_no_fossilize) | `adapter.vk_no_fossilize` | Game settings | 1 |
@@ -193,7 +196,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 | 121 |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 122 |
| [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) | |
@@ -694,7 +697,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 (34):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 26 more.
- **Catalog games (35):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 27 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.
@@ -749,6 +752,17 @@ Lets games with a verified older Lua engine get past a startup crash on the Fram
> Older xLua/LuaJIT engines reject native pointers outside their 47-bit range, causing a startup error and sometimes a second crash during Unity shutdown (e.g. BattleTalent). Uses a Lua engine that preserves full pointer values while keeping the game's original xLua bindings and native helpers. Applied only to the verified compatible library build.
### <a id="frame-main_process"></a>Start the game in the app's main process
Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).
- `frame.main_process` · on by default · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** … runs in its own process, which Lepton doesn't watch: it would close the game about a minute in.
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
> Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's ":halo_game"), started from a 2D launcher activity. Lepton starts the game activity directly and watches only the app's main process, which then never exists: about a minute in, Lepton decides the app "did not start" and closes it, at whatever point the game has reached. This runs those activities in the main process (only their android:process value changes).
### <a id="frame-meta_permissions"></a>Declare Meta-only permissions
Declares Meta's permissions some mixed-reality games ask for, so they don't stop.
@@ -756,7 +770,7 @@ Declares Meta's permissions some mixed-reality games ask for, so they don't stop
- `frame.meta_permissions`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Mixed-reality game that needs the room model: its 'use spatial data' permission must be granted (for example Demeter).
- **Catalog games (4):** Arkio, BattleTalent, Demeter, Real VR Fishing.
- **Catalog games (5):** Arkio, BattleTalent, Demeter, Real VR Fishing, Test Quest MR.
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
> Declares com.oculus.permission.* / horizonos.permission.* that the app uses, so Android grants them at install (for example the 'use spatial data' permission of mixed-reality games like Demeter).
@@ -936,6 +950,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.
@@ -982,7 +1011,7 @@ Stops a gray or frozen screen in Unity games that turn on anti-aliasing while ru
- `frame.unity_gl_shim`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unity GLES game that may switch to MSAA at runtime: hide multisampled render-to-texture, which crashes the Frame's GL driver (for example The Room VR).
- **Catalog games (26):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 18 more.
- **Catalog games (27):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 19 more.
- **Suggested by launch-test signatures:**
- `unreal-msrtt-crash` (fatal): The game's graphics crashed a few seconds after start.
- **Source:** [patches/frame/vrapi_bridge.py](../src/frameport/patches/frame/vrapi_bridge.py)
@@ -1009,7 +1038,7 @@ Turns off a smoothing setting that crashes some older Unity games on the Frame.
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Legacy VrApi Unity game on GLES with MSAA (… quality levels): multisampled render-to-texture hangs the Frame's GL driver (for example Sniper Elite VR).
- **Offered (off):** GLES Unity game with MSAA on (… quality levels); enable if it hangs the GPU ('zink: DEVICE LOST').
- **Catalog games (10):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, Mini Motor Racing X, Sniper Elite VR and 2 more.
- **Catalog games (11):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, KillerFrequency, Mini Motor Racing X and 3 more.
- **Suggested by launch-test signatures:**
- `zink-device-lost` (fatal): The GPU hung, so the game froze or closed.
- **Source:** [patches/frame/unity_no_msaa.py](../src/frameport/patches/frame/unity_no_msaa.py)
@@ -1051,7 +1080,7 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
- `frame.unity_runtime_msaa_off` · revision 2
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** GLES Unity game whose OVRManager turns 4x MSAA on at runtime: multisampled render-to-texture can hang the Frame's GPU.
- **Catalog games (32):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 24 more.
- **Catalog games (33):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 25 more.
- **Suggested by launch-test signatures:**
- `unity-render-crash` (fatal): The game's graphics crashed, so the picture freezes or stays gray.
- `unity-runtime-msaa` (info): The game turns MSAA on at runtime, which can hang the Frame's GPU.
@@ -1067,7 +1096,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 (118):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 110 more.
- **Catalog games (119):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 111 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.
@@ -1812,7 +1841,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 (103):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 95 more.
- **Catalog games (104):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 96 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.
@@ -2120,7 +2149,7 @@ Changes the game's render resolution. Higher is sharper but slower; 1.0× is the
- **Options:**
- Value: float, default `1.0` (always written).
- Game settings → Picture → "Render resolution (supersampling)": slider 0.5–2 in steps of 0.1.
- **Catalog games (14):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, Spectre, Survival Nation and 6 more.
- **Catalog games (15):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, KillerFrequency, Spectre and 7 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels.
@@ -2150,7 +2179,7 @@ Mixed-reality games that build their level from your room get a simple room with
- **Options:**
- Value: int, default `0`.
- Game settings → Mixed reality → "Pretend room layout".
- **Catalog games (12):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord, Neko Atsume Purrfect and 4 more.
- **Catalog games (14):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, cubism, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord and 6 more.
- **Suggested by launch-test signatures:**
- `scene-missing` (warning): The game wants Meta's room data (scene API), which the Frame lacks.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
@@ -2301,6 +2330,21 @@ Lets OpenGL ES games start when the Frame refuses their picture format. Leave it
> Retry rejected GLES formats/MSAA with sRGB, samples=1.
### <a id="adapter-swapchain_pool"></a>Share panel images (swapchain limit)
Keeps extra menus and panels working when the Frame reaches its image count limit. GLES apps only; Vulkan apps are unaffected.
- `adapter.swapchain_pool`
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Options:**
- Value: int, default `0`.
- Game settings → Troubleshooting → "Share panel images (swapchain limit)" (advanced).
- **Catalog games (1):** 4XVR Video Player.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected.
### <a id="adapter-sync_guard"></a>Steady controller input after focus
For games that crash right after you return to them (for example after the Steam menu).
@@ -2608,9 +2652,13 @@ Lets the game use your controllers as a gamepad instead of a pointer, for 2D gam
- `device.steam_gamepad` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** 2D app that declares gamepad support (android.hardware.gamepad or Android TV's launcher): pass the controllers to it as a gamepad.
- **Options:**
- `swap_sticks`: bool, default `False` — Swap the sticks: the right stick moves through menus like the left one (for using one controller)
- `swap_ab`: bool, default `False` — Swap A and B
- `keys`: str, default empty — Own key layout lines (Android .kl), separated by ';', e.g. 'key 304 DPAD_DOWN' (A presses D-pad down) or 'key 305 BACK'
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may not reach the app (GitHub #162); the options below change the buttons inside the app instead (applied with Update on Frame).
### <a id="device-text_input_window"></a>Show the app's Android window (for typing)
@@ -2619,7 +2667,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 (121):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 113 more.
- **Catalog games (122):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 114 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".
+3 -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 |
|---|---|---|
| Valve foveation sharp centre moves to the top right or a white line appears at the right edge as Batman shrinks its projection rect (#406 / #394) | Dynamic resolution draws a corner-anchored subrect inside the full swapchain used by Valve FDM | Enable `adapter.fixed_resolution` (Game settings → Picture), rebuild/reinstall for the new adapter and OVRPlugin shim; verify `fixed_resolution: recommended ... -> ...` and full-size `projection view ... rect change` logs. Headset verification pending; more GPU load |
| 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' |
@@ -61,6 +62,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| The game quits right after start: `Something failed to initialize. Quitting!` / `We don't have write permission to …/files/cloud/data` (for example SUPERHOT); or stays at loading with `UnauthorizedAccessException` in `StartupLoading.Start` (GitHub #239, Jurassic World Aftermath) | The game created a cloud-save folder with mode 1700; inside Lepton the app writes through the folder's group. Its startup routine can fail before the permission watchdog repairs the newly created folder | launch.sh `fix_perms` adds owner + group write to every folder in the game's storage (agent 41). Close and start the game again without uninstalling: the folders are now repaired (Aftermath confirmed in the headset). Agent 84 creates `files/cloud/data` before `fix_perms` and startup, including in upgraded launchers, to prevent the first-start race. Triage `startup-save-permission` fails the launch test despite rendering frames |
| The whole Frame slows to a halt, then the game is killed (`Out of memory: Killed process … anon-rss:10+ GB`), for example Lucky's Tale | The game's own memory grows ~1 GB/s on Unity's `Loading.PreloadManager` thread (perf page-fault profile); not the flat window, not the text-input patches | none yet: mark unsupported; profile with `perf record -p <pid> -e page-faults --call-graph fp` (works as steamos, `perf_event_paranoid` 2) |
| A Linux app exits at once: `cannot execute binary file: Exec format error` in launch.log | an x86_64 program started directly (installed before FramePort ran x86 builds through FEX, or by hand) | reinstall it: x86_64 Linux apps install FEX on the Frame and start through it (agent v61, `kind: linux_x86`; FEX also needs `STEAM_COMPAT_DATA_PATH`: "No compat data path?" in launch.log = an agent v60 launcher, reinstall); triage `linux-x86-no-fex`. Prefer an arm64 build when the app offers one |
| The game closes about a minute after starting, at a different spot each time; `Start proc …:<pkg>:<name> for pre-top-activity`, then `Target app (<pkg>) did not start` (GitHub #178) | The started activity runs in its own process (`android:process`); Lepton watches only the main process (triage `app-separate-process`) | `frame.main_process` (automatic) |
| `APP_ACTIVITY is empty`, nothing starts | Manifest has category INFO only; Lepton needs LAUNCHER | `frame.launcher` (automatic) |
| PC VR game on the Frame shows as a flat window / Revive: `Unable to load LibOVRRT DLL` / `LoaderInstance::CreateInstance chained CreateInstance call failed` | Frame SteamVR runtime rejects OpenXR apiVersion 1.1 (`XR_ERROR_API_VERSION_UNSUPPORTED`), which Proton 11's VR helper requests | `pcvr.xr_timefix` (Frame OpenXR layer, default on): retries xrCreateInstance as 1.0 |
| Unreal PC VR game on the Frame runs as a flat window although OpenXR works (no `LogHMD` OVRPlugin lines; Unreal logs nothing when it skips the Oculus plugin) / launch.log: `FramePort oculushmd: could not create the OculusHMDConnected event` | UE's OculusHMD (and LibOVR's `ovr_Detect`) only start when the Windows event `OculusHMDConnected` exists and is signaled; on a PC the Oculus service creates it. Revive hooks `OpenEventW` for it, but that relies on Detours patching Wine's (ARM64EC) kernelbase | `pcvr.oculus_unreal` (default for Unreal Rift games; PC VR counterpart of overport's `patch_oculus_unreal`): launch.sh runs the injector through `fp_oculushmd.exe`, which provides the real event until the game exits |
@@ -117,7 +119,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Vermillion: the hand and brush disappear over the canvas while painting, but still paint (GitHub #200) | the game shows a canvas being painted as its own compositor (quad) layer and turns the canvas's scene renderer off; the Frame draws quad layers above every projection layer whatever the order, so the canvas covers the hand (Vulkan game: no GL layer emulation) | `frame.vermillion_scene_canvas` (catalog): `PaintableMesh.HasCompositorLayerAvailable` / `ShouldUseCompositorOverlay` -> false in libil2cpp.so (Cpp2IL), so the game's own fallback draws the canvas in the scene (Klownicle's finding) |
| Unity game hangs right after its first frames, no `pacing:` lines; SIGQUIT dump shows UnityMain in a game library's `JNI_OnLoad` → `usleep` (for example SKYBOX: `libskybox.so`, `SignatureChecker`) | the game checks its APK signing certificate; every port is re-signed | can't be fixed without defeating anti-tamper (FramePort doesn't): mark "Can't run". Diagnose hangs: `podman exec <container> kill -3 <pid>` writes all thread stacks to launch.log |
| Video player doesn't list sent videos (for example 4XVR "Internal Storage" only shows some) | the player lists its own /sdcard folder (4XVR: /sdcard/4XPlayer), not /sdcard/Movies; Android's media index doesn't work in Lepton | game page "Add videos" / game menu "Add videos and files" (Videos + the app's own folder, hard links) or `frameport frame send <files> --app <pkg>` |
| One eye gray in a stereo video, log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED) | fewer/smaller swapchains: lower `equirect_res`; unresolved |
| One eye gray in a stereo video, log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED) | GLES: `adapter.swapchain_pool=1` shares extra panel images in atlases (#387; headset validation pending). This is a count cap, so lowering resolution does not fix it; Vulkan unaffected |
| VrApi game (32-bit bridge or `frame.vrapi_bridge`): holding the menu button to recenter turns the view ~90-120 degrees to the side | the bridge's app space is LOCAL + the head's yaw at the start; the Frame's recenter moves LOCAL to the head, so the old yaw was added again | fixed in the VrApi bridge (2026-10-13): on XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING (LOCAL) and on `vrapi_RecenterPose` it takes the origin from the head again (log `recentered: head yaw …`); rebuild the game |
| Menu/screen jumps to where you look (for example 4XVR) | the app re-creates LOCAL spaces (4XVR: every 2–4 s) and the Frame places them at the current head yaw, or the app recenters after the Frame's brief focus dips | turn on `adapter.layer_debug`, check `that LOCAL space sits at … deg` vs `focus_hold: hid a … ms focus dip`; patches: `adapter.stable_local` (new LOCAL spaces line up with the first) / `adapter.focus_hold` (on by default: hides dips up to `focus_hold_ms` once the game has been focused for a second; never at start-up: a global debounce broke AC Nexus) |
| Pointer misses menu items | the Frame's aim pose differs from Touch's | `adapter.layer_debug` logs `aim in grip … pitch/yaw`; correct with `aim_pitch`/`aim_yaw`/`aim_forward` (`frameport settings`, no rebuild) |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 102 KiB

+9
View File
@@ -4,6 +4,14 @@ FrameBridge includes automatic metadata retirement for the verified Meta XR Audi
Wwise build; see [AUDIO_METADATA.md](../docs/AUDIO_METADATA.md) for its lifetime
invariant and regression coverage. It uses the existing adapter installation.
`adapter/swapchain_pool.c` implements default-off GLES `swapchain_pool` (#387): at eight live app runtime
swapchains, eligible single-layer colour images use private GL textures packed into up to two lazy 4096² sRGB
runtime atlases. EndFrame copies each referenced source once and rewrites the final quads (including cylinder
strips), cylinders and stereo projection views after existing layer fixes. OpenXR image ownership, GL bindings,
scissor state, source fences, rectangle reuse and session cleanup are covered by `tests/test_swapchain_pool.py`
(`FRAMEPORT_NATIVE_TESTS=1`, host compiler + cached OpenXR and GLES/EGL headers). Vulkan is unaffected. Host-tested;
4XVR submenu interaction, visual correctness and copy cost still need a headset check.
Sources of the prebuilt binaries in `../artifacts/` (committed, with `SHA256SUMS`). End users never compile anything;
developers rebuild with `python native/build.py` (downloads NDK r27c, OpenXR headers at pinned commits, d8 into
`native/.cache/`, git-ignored). The adapter, GL shim and platform compat rebuild byte-identically; the VrApi bridge
@@ -22,6 +30,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. |
+2 -1
View File
@@ -42,7 +42,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL eye_hook_xrReleaseSwapchainImage(XrSwapcha
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (is_standin(swapchain)) return XR_SUCCESS;
if (release_wait_active()) eye_wait_for_gpu();
XrResult result = fn(swapchain, info);
int pooled = pool_find(swapchain);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 2, NULL) : fn(swapchain, info);
if (eye_debug) {
int64_t now = monotonic_ns();
pthread_mutex_lock(&eye_lock);
+61 -6
View File
@@ -132,6 +132,7 @@ static int cube_quads = 1;
static int cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
static int rect_clamp = 1;
static int fixed_resolution;
static int swapchain_pool = 0;
static int fb_color_space; // fb_color_space: offer XR_FB_color_space when the runtime lacks it (#336)
static int action_dedup = 1; // rename duplicate action names in a set (session_fixes.c; GitHub #323)
static int pose_consistency = 0; // repeat xrLocateViews queries for one display time get the same poses (GitHub #8) // clamp submitted image rects to their swapchain (SteamVR rejects a 1 px overrun)
@@ -210,6 +211,7 @@ static void read_settings(const char *path) {
if (sscanf(line, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
if (sscanf(line, "fixed_resolution=%f", &value) == 1) fixed_resolution = value != 0;
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
if (sscanf(line, "swapchain_pool=%f", &value) == 1) swapchain_pool = value != 0;
if (sscanf(line, "fb_color_space=%f", &value) == 1) fb_color_space = value != 0;
if (sscanf(line, "action_dedup=%f", &value) == 1) action_dedup = value != 0;
if (sscanf(line, "pose_consistency=%f", &value) == 1) pose_consistency = value != 0;
@@ -557,7 +559,7 @@ static int64_t pick_format(XrSession session, int64_t wanted) {
return chosen;
}
// Swapchains the runtime actually created; layers referencing anything else are dropped.
// Known app swapchains, including pooled images rewritten into runtime atlases before submission.
static pthread_mutex_t swapchains_lock = PTHREAD_MUTEX_INITIALIZER;
static XrSwapchain *swapchains;
static size_t swapchain_count, swapchain_capacity;
@@ -645,6 +647,10 @@ static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
}
static void forget_swapchain(XrSwapchain handle) {
pthread_mutex_lock(&sizes_lock);
for (int i = 0; i < MAX_SIZES; ++i)
if (sizes[i].handle == handle) memset(&sizes[i], 0, sizeof sizes[i]);
pthread_mutex_unlock(&sizes_lock);
pthread_mutex_lock(&swapchains_lock);
for (size_t i = 0; i < swapchain_count; ++i)
if (swapchains[i] == handle) { swapchains[i] = swapchains[--swapchain_count]; break; }
@@ -670,6 +676,16 @@ static void cube_outputs_destroy(void);
static void cube_session_destroy(XrSession session);
static XrResult standin_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int pool_find(XrSwapchain h);
static int pool_should_create(XrSession session);
static void pool_note_runtime(XrSession session, XrSwapchain handle);
static void pool_forget_runtime(XrSwapchain handle);
static int pool_create(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited);
static void pool_destroy(int i);
static void pool_cleanup(XrSession session);
static XrResult pool_cycle(int i, int op, uint32_t *index);
static XrResult pool_enumerate(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images);
static int known_swapchain(XrSwapchain handle) {
int found = 0;
pthread_mutex_lock(&swapchains_lock);
@@ -757,6 +773,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
snap_on_create(*swapchain, &fixed);
return XR_SUCCESS;
}
// Start before the session's ~15-slot cap; leave headroom for 360/video outputs and a second atlas.
if (pool_should_create(session) && pool_create(session, &fixed, swapchain, 0)) return XR_SUCCESS;
clear_gl_errors("xrCreateSwapchain");
XrResult result = fn(session, &fixed, swapchain);
LOG("xrCreateSwapchain %ux%u format=%lld samples=%u array=%u faces=%u usage=0x%llx flags=0x%llx result=%d handle=%p",
@@ -764,6 +782,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
(unsigned long long)fixed.usageFlags, (unsigned long long)fixed.createFlags, result,
(void *)(uintptr_t)(XR_SUCCEEDED(result) && swapchain ? *swapchain : 0));
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
@@ -771,6 +790,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
snap_on_create(*swapchain, &fixed);
return result;
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
if (!swapchain_fix) return standin_after_failure(session, &fixed, swapchain, result);
// Frame's runtime rejects some GLES formats (GL_RGBA8) and MSAA swapchains (overport #71).
@@ -788,12 +808,14 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
}
}
if (XR_SUCCEEDED(result)) {
pool_note_runtime(session, *swapchain);
remember_swapchain(*swapchain);
note_swapchain_size(*swapchain, fixed.width, fixed.height);
flip_on_create_swapchain(*swapchain, &fixed);
emul_on_create_swapchain(*swapchain, &fixed);
snap_on_create(*swapchain, &fixed);
}
if (result == XR_ERROR_LIMIT_REACHED && pool_create(session, &fixed, swapchain, 1)) return XR_SUCCESS;
return standin_after_failure(session, &fixed, swapchain, result);
}
@@ -804,6 +826,9 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
static int logged;
if (logged++ < 200) LOG("layer_debug: xrDestroySwapchain %p", (void *)swapchain);
}
pool_forget_runtime(swapchain);
int pooled = pool_find(swapchain);
if (pooled >= 0) { pool_destroy(pooled); return XR_SUCCESS; }
if (is_standin(swapchain)) { standin_destroy(swapchain); return XR_SUCCESS; }
surf_scene_destroy(swapchain);
forget_swapchain(swapchain);
@@ -860,7 +885,7 @@ static int cylinder_strips(const XrCompositionLayerCylinderKHR *c, XrComposition
}
// A layer is usable if every swapchain it references was created successfully.
// Swapchains the runtime can show: created by it (adapter-served 360 pictures only feed the adapter's own drawing).
// Runtime images and pooled images (rewritten after layer fixes). Adapter-served 360 pictures only feed its drawing.
static int runtime_swapchain(XrSwapchain handle) {
return known_swapchain(handle) && !(equirect_emul && emul_is_virtual(handle));
}
@@ -1084,6 +1109,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
#include "input_diag.c"
#include "session_fixes.c"
#include "layer_emul_gl.c"
#include "swapchain_pool.c"
#include "cube_standin.c"
#include "snapshot_gl.c"
#include "surface_swapchain.c"
@@ -1270,6 +1296,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSession(XrInstance instance, const XrSess
scene_on_create_session(*session);
session_fixes_on_create_session(*session);
emul_on_create_session(*session, info);
if (swapchain_pool) {
for (const XrBaseInStructure *p = info->next; p; p = p->next)
if (p->type == XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR) pool_session = *session;
}
fade_on_create_session(*session, info);
}
return result;
@@ -1338,6 +1368,12 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
XrSwapchainImageBaseHeader *images) {
PFN_xrEnumerateSwapchainImages fn = (PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_enumerate(pooled, capacity, count, images);
if (XR_SUCCEEDED(r) && capacity && images) emul_on_enumerate(swapchain, 1, images);
return r;
}
if (is_standin(swapchain)) return standin_enumerate(swapchain, capacity, count, images);
XrResult result = equirect_emul && emul_is_virtual(swapchain) ? emul_virtual_enumerate(swapchain, capacity, count, images)
: hide_fb_foveation_vulkan ? fb_hide_foveation_images(fn, swapchain, capacity, count, images)
@@ -1354,6 +1390,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 1, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
XrResult result = fn(swapchain, info);
__atomic_add_fetch(&sc_waits, 1, __ATOMIC_RELAXED);
@@ -1364,6 +1402,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const
XRAPI_ATTR XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) return pool_cycle(pooled, 2, NULL);
if (is_standin(swapchain)) return XR_SUCCESS;
__atomic_add_fetch(&sc_releases, 1, __ATOMIC_RELAXED);
return fn(swapchain, info);
@@ -1373,6 +1413,12 @@ XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, co
uint32_t *index) {
PFN_xrAcquireSwapchainImage fn = (PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (pooled >= 0) {
XrResult r = pool_cycle(pooled, 0, index);
if (XR_SUCCEEDED(r)) emul_on_acquire(swapchain, *index);
return r;
}
if (is_standin(swapchain)) { // adapter-served cube stand-in: one image
if (!index) return XR_ERROR_VALIDATION_FAILURE;
*index = 0;
@@ -1401,7 +1447,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_release(XrSwapchain handle,
tracked_swapchain *t=find_tracked(handle,0);
if(t && t->acquired_count && t->waited_count)surf_scene_capture(t,t->acquired[0]);
pthread_mutex_unlock(&flip_lock);
XrResult result=equirect_emul?hook_xrReleaseSwapchainImage(handle,info):
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrReleaseSwapchainImage(handle,info) :
equirect_emul?hook_xrReleaseSwapchainImage(handle,info):
((PFN_xrReleaseSwapchainImage)lookup(active_instance,"xrReleaseSwapchainImage"))(handle,info);
if(XR_SUCCEEDED(result)) {
surf_scene_release_commit(handle);
@@ -1415,7 +1462,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_release(XrSwapchain handle,
return result;
}
static XRAPI_ATTR XrResult XRAPI_CALL surf_composite_wait(XrSwapchain handle,const XrSwapchainImageWaitInfo *info) {
XrResult result=equirect_emul?hook_xrWaitSwapchainImage(handle,info):
XrResult result=pool_find(handle)>=0 && !equirect_emul ? xrWaitSwapchainImage(handle,info) :
equirect_emul?hook_xrWaitSwapchainImage(handle,info):
((PFN_xrWaitSwapchainImage)lookup(active_instance,"xrWaitSwapchainImage"))(handle,info);
if(result==XR_SUCCESS) {
pthread_mutex_lock(&flip_lock);tracked_swapchain *t=find_tracked(handle,0);
@@ -1729,7 +1777,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
frames = 0; drift_sum = drift_max = 0;
}
}
if ((!cube_standin && !layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
if (swapchain_pool && info && info->layerCount > 64) return XR_ERROR_LAYER_LIMIT_EXCEEDED;
if ((!swapchain_pool && !cube_standin && !layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
info->layerCount > 64)
return fn(session, info);
if (layer_debug) { // the submitted layer list (order, flags, eyes, chained structs) whenever its shape changes
@@ -2083,6 +2132,11 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
const XrCompositionLayerBaseHeader *fade_l = level ? fade_layer(session, level, count, &fade_out) : NULL;
if (fade_l) { kept[count++] = fade_l; faded = 1; ++swapped; }
}
PoolFrame pool_frame;
if (swapchain_pool && session == pool_session) {
count = pool_rewrite_layers(kept, count, &pool_frame);
++swapped;
}
XrFrameEndInfo fixed = *info;
fixed.layers = kept;
fixed.layerCount = count;
@@ -2391,6 +2445,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
HOOK_AS(xrReleaseSwapchainImage, surf_composite_release)
HOOK_AS(xrWaitSwapchainImage, surf_composite_wait)
}
if (swapchain_pool) { HOOK_AS(xrDestroySession, hook_xrDestroySession) }
if (cube_standin) {
HOOK_AS(xrEndSession, cube_end_session)
HOOK_AS(xrDestroySession, hook_xrDestroySession)
@@ -2409,7 +2464,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
if (instance != XR_NULL_HANDLE) HOOK_AS(xrGetDisplayRefreshRateFB, hook_get_refresh_rate)
if (surface_emul) HOOK_AS(xrCreateSwapchainAndroidSurfaceKHR, hook_xrCreateSwapchainAndroidSurfaceKHR)
if (eye_debug || release_wait) HOOK_AS(xrReleaseSwapchainImage, eye_hook_xrReleaseSwapchainImage)
if (layer_debug || cube_standin) { // counters + cube stand-ins; every other wait/release hook above takes precedence
if (swapchain_pool || layer_debug || cube_standin) { // pooled images, counters + cube stand-ins; every other wait/release hook above takes precedence
HOOK(xrWaitSwapchainImage)
HOOK(xrReleaseSwapchainImage)
}
+7 -2
View File
@@ -304,6 +304,7 @@ static void emul_on_destroy_swapchain(XrSwapchain handle) {
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (is_standin(swapchain)) return XR_SUCCESS;
pthread_mutex_lock(&emul_lock);
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
@@ -314,7 +315,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain s
if (fence) p_glFlush();
}
pthread_mutex_unlock(&emul_lock);
XrResult result = emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 2, NULL) :
emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
pthread_mutex_lock(&emul_lock);
s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
if (s && XR_SUCCEEDED(result) && s->acquired_count) {
@@ -336,8 +338,10 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain s
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
int pooled = pool_find(swapchain);
if (is_standin(swapchain)) return XR_SUCCESS;
XrResult result = emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 1, NULL) :
emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
pthread_mutex_lock(&emul_lock);
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
if (s && s->reading >= 0 && s->reading == s->last_acquired) {
@@ -1005,6 +1009,7 @@ static void emul_on_destroy_session(XrSession session) {
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrDestroySession(XrSession session) {
cube_session_destroy(session);
emul_on_destroy_session(session);
pool_cleanup(session);
PFN_xrDestroySession fn = (PFN_xrDestroySession)lookup(active_instance, "xrDestroySession");
return fn ? fn(session) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
+26
View File
@@ -0,0 +1,26 @@
// First-fit edge packer. Removing a live rectangle immediately frees its space.
#pragma once
#include <stdint.h>
typedef struct {
int x, y, w, h;
} PoolRect;
static int pool_place(const PoolRect *used, int n, int side, int w, int h, PoolRect *out) {
if (w <= 0 || h <= 0 || w > side || h > side) return 0;
for (int yi = -1; yi < n; ++yi)
for (int xi = -1; xi < n; ++xi) {
int x = xi < 0 ? 0 : used[xi].x + used[xi].w;
int y = yi < 0 ? 0 : used[yi].y + used[yi].h;
if (x > side - w || y > side - h) continue;
int fits = 1;
for (int i = 0; i < n; ++i)
if (x < used[i].x + used[i].w && x + w > used[i].x && y < used[i].y + used[i].h && y + h > used[i].y) {
fits = 0;
break;
}
if (fits) {
*out = (PoolRect){x, y, w, h};
return 1;
}
}
return 0;
}
+389
View File
@@ -0,0 +1,389 @@
// Included after layer_emul_gl.c: reuses its dynamically loaded GLES entry points.
// Proactive at eight app swapchains: an atlas cannot be allocated after the runtime cap.
#include "pool_rect.h"
#define POOL_MAX 128
#define POOL_SIDE 4096
static XrSession pool_session;
static struct {
XrSwapchain handle;
GLuint images[16];
uint32_t count, index;
int acquired, waited;
} pool_atlas[2];
static struct {
XrSwapchain handle;
GLuint tex;
PoolRect rect;
int atlas, state, released, copied;
GLsync fence, read_fence;
} pool_images[POOL_MAX];
static unsigned pool_errors;
static XrSwapchain pool_runtime[256];
static unsigned pool_runtime_count;
static void pool_note_runtime_locked(XrSession session, XrSwapchain handle) {
if (!swapchain_pool || session != pool_session) return;
for (int i = 0; i < 256; ++i)
if (!pool_runtime[i]) {
pool_runtime[i] = handle;
++pool_runtime_count;
return;
}
}
static void pool_forget_runtime_locked(XrSwapchain handle) {
for (int i = 0; i < 256; ++i)
if (pool_runtime[i] == handle && handle) {
pool_runtime[i] = XR_NULL_HANDLE;
--pool_runtime_count;
return;
}
}
static int pool_should_create_locked(XrSession session) {
return swapchain_pool && session == pool_session && pool_runtime_count >= 8;
}
static int pool_find_locked(XrSwapchain h) {
for (int i = 0; h && i < POOL_MAX; ++i)
if (pool_images[i].handle == h) return i;
return -1;
}
static void pool_error(void) {
if (pool_errors++ % 240 == 0) LOG("swapchain_pool: atlas update failed; omitting affected layers");
}
static int pool_create_locked(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited) {
if (!swapchain_pool || session != pool_session || !out || ci->next || ci->faceCount != 1 || ci->arraySize != 1 ||
ci->sampleCount != 1 || ci->mipCount != 1 || !(ci->usageFlags & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT) ||
(ci->createFlags & ~XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT) ||
(ci->format != GL_RGBA8 && ci->format != GL_SRGB8_ALPHA8) || !ci->width || !ci->height ||
ci->width > POOL_SIDE || ci->height > POOL_SIDE)
return 0;
if (!p_eglCreateContext && !emul_load_gl()) return 0;
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return 0;
// Like emul_virtual_create: stale application GL errors must not look like allocation failures.
for (int i = 0; i < 8 && p_glGetError() != GL_NO_ERROR; ++i) {
}
int slot = -1;
for (int i = 0; i < POOL_MAX; ++i)
if (!pool_images[i].handle) {
slot = i;
break;
}
if (slot < 0) return 0;
for (int a = 0; a < 2; ++a) {
PoolRect used[POOL_MAX], rect;
int n = 0;
for (int i = 0; i < POOL_MAX; ++i)
if (pool_images[i].handle && pool_images[i].atlas == a) used[n++] = pool_images[i].rect;
if (!pool_place(used, n, POOL_SIDE, ci->width, ci->height, &rect)) continue;
if (!pool_atlas[a].handle) {
XrSwapchainCreateInfo ai = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
ai.width = ai.height = POOL_SIDE;
ai.arraySize = ai.faceCount = ai.sampleCount = ai.mipCount = 1;
ai.format = GL_SRGB8_ALPHA8;
ai.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
PFN_xrCreateSwapchain create = (PFN_xrCreateSwapchain)lookup(active_instance, "xrCreateSwapchain");
PFN_xrEnumerateSwapchainImages enumerate =
(PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
if (XR_FAILED(create(session, &ai, &pool_atlas[a].handle))) return 0;
emul_gles_image images[16];
for (int j = 0; j < 16; ++j) images[j] = (emul_gles_image){XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR};
if (XR_FAILED(enumerate(pool_atlas[a].handle, 16, &pool_atlas[a].count, (void *)images)) ||
!pool_atlas[a].count || pool_atlas[a].count > 16) {
((PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain"))(pool_atlas[a].handle);
memset(&pool_atlas[a], 0, sizeof pool_atlas[a]);
return 0;
}
for (uint32_t j = 0; j < pool_atlas[a].count; ++j) pool_atlas[a].images[j] = images[j].image;
}
GLint old;
GLuint tex = 0;
p_glGetIntegerv(GL_TEXTURE_BINDING_2D, &old);
p_glGenTextures(1, &tex);
p_glBindTexture(GL_TEXTURE_2D, tex);
p_glTexStorage2D(GL_TEXTURE_2D, 1, ci->format, ci->width, ci->height);
GLenum err = p_glGetError();
p_glBindTexture(GL_TEXTURE_2D, old);
if (err != GL_NO_ERROR) {
p_glDeleteTextures(1, &tex);
return 0;
}
pool_images[slot].handle = FAKE_HANDLE(XrSwapchain);
pool_images[slot].tex = tex;
pool_images[slot].rect = rect;
pool_images[slot].atlas = a;
*out = pool_images[slot].handle;
note_swapchain_size(*out, ci->width, ci->height);
remember_swapchain(*out);
emul_on_create_swapchain(*out, ci);
LOG("swapchain_pool: virtual %ux%u (%s)", ci->width, ci->height,
limited ? "runtime limit reached" : "runtime limit proactively avoided");
return 1;
}
return 0;
}
static void pool_destroy_locked(int i) {
emul_on_destroy_swapchain(pool_images[i].handle);
if (pool_images[i].fence && p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteSync(pool_images[i].fence);
if (p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteTextures(1, &pool_images[i].tex);
if (pool_images[i].read_fence && p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT)
p_glDeleteSync(pool_images[i].read_fence);
forget_swapchain(pool_images[i].handle);
memset(&pool_images[i], 0, sizeof pool_images[i]);
}
static XrResult pool_enumerate_locked(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images) {
if (!count) return XR_ERROR_VALIDATION_FAILURE;
*count = 1;
if (!cap) return XR_SUCCESS;
if (!images || images->type != XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR) return XR_ERROR_VALIDATION_FAILURE;
((emul_gles_image *)images)->image = pool_images[i].tex;
return XR_SUCCESS;
}
static XrResult pool_cycle_locked(int i, int op, uint32_t *index) {
int *state = &pool_images[i].state;
if (op == 0) {
if (!index) return XR_ERROR_VALIDATION_FAILURE;
if (*state) return XR_ERROR_CALL_ORDER_INVALID;
if (pool_images[i].read_fence) {
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return XR_ERROR_RUNTIME_FAILURE;
p_glWaitSync(pool_images[i].read_fence, 0, GL_TIMEOUT_IGNORED);
}
*index = 0;
*state = 1;
}
if (op == 1) {
if (*state != 1) return XR_ERROR_CALL_ORDER_INVALID;
*state = 2;
}
if (op == 2) {
if (*state != 2) return XR_ERROR_CALL_ORDER_INVALID;
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return XR_ERROR_RUNTIME_FAILURE;
if (pool_images[i].fence) p_glDeleteSync(pool_images[i].fence);
pool_images[i].fence = p_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!pool_images[i].fence) return XR_ERROR_RUNTIME_FAILURE;
p_glFlush();
*state = 0;
pool_images[i].released = 1;
}
return XR_SUCCESS;
}
static void pool_cleanup_locked(XrSession session) {
if (session != pool_session) return;
for (int i = 0; i < POOL_MAX; ++i)
if (pool_images[i].handle) pool_destroy_locked(i);
for (int a = 0; a < 2; ++a)
if (pool_atlas[a].handle) {
((PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain"))(pool_atlas[a].handle);
memset(&pool_atlas[a], 0, sizeof pool_atlas[a]);
}
memset(pool_runtime, 0, sizeof pool_runtime);
pool_runtime_count = 0;
pool_session = XR_NULL_HANDLE;
}
// Called on final layers, after strips, flips and rect_clamp. Copy every referenced image
// each frame: runtime atlas images rotate, and untouched areas are not preserved.
static void pool_begin_frame_locked(void) {
for (int i = 0; i < POOL_MAX; ++i) pool_images[i].copied = 0;
}
static int pool_subimage_locked(XrSwapchainSubImage *sub) {
int i = pool_find_locked(sub->swapchain);
if (i < 0) return 1;
int a = pool_images[i].atlas;
if (!pool_images[i].released || pool_images[i].state || sub->imageArrayIndex) return 0;
PFN_xrAcquireSwapchainImage acquire =
(PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
PFN_xrWaitSwapchainImage wait = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
if (!pool_atlas[a].acquired) {
XrSwapchainImageAcquireInfo ai = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
if (XR_FAILED(acquire(pool_atlas[a].handle, &ai, &pool_atlas[a].index))) return 0;
pool_atlas[a].acquired = 1;
}
if (!pool_atlas[a].waited) {
XrSwapchainImageWaitInfo wi = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, NULL, XR_INFINITE_DURATION};
if (wait(pool_atlas[a].handle, &wi) != XR_SUCCESS) return 0;
pool_atlas[a].waited = 1;
}
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) return 0;
PoolRect r = pool_images[i].rect;
if (!pool_images[i].copied) {
for (int e = 0; e < 8 && p_glGetError() != GL_NO_ERROR; ++e) {
}
if (pool_images[i].fence) p_glWaitSync(pool_images[i].fence, 0, GL_TIMEOUT_IGNORED);
GLint read, draw;
GLuint fb[2];
GLboolean scissor = p_glIsEnabled(GL_SCISSOR_TEST);
p_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read);
p_glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
p_glGenFramebuffers(2, fb);
p_glDisable(GL_SCISSOR_TEST);
p_glBindFramebuffer(GL_READ_FRAMEBUFFER, fb[0]);
p_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pool_images[i].tex, 0);
p_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb[1]);
p_glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
pool_atlas[a].images[pool_atlas[a].index], 0);
int ok = p_glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE &&
p_glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
if (ok) p_glBlitFramebuffer(0, 0, r.w, r.h, r.x, r.y, r.x + r.w, r.y + r.h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
ok &= p_glGetError() == GL_NO_ERROR;
p_glBindFramebuffer(GL_READ_FRAMEBUFFER, read);
p_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw);
if (scissor) p_glEnable(GL_SCISSOR_TEST);
p_glDeleteFramebuffers(2, fb);
if (!ok) return 0;
if (pool_images[i].read_fence) p_glDeleteSync(pool_images[i].read_fence);
pool_images[i].read_fence = p_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!pool_images[i].read_fence) return 0;
pool_images[i].copied = 1;
}
clamp_rect(&sub->imageRect, r.w, r.h);
if (sub->imageRect.offset.x < 0 || sub->imageRect.offset.y < 0 ||
(int64_t)sub->imageRect.offset.x + sub->imageRect.extent.width > r.w ||
(int64_t)sub->imageRect.offset.y + sub->imageRect.extent.height > r.h)
return 0;
sub->swapchain = pool_atlas[a].handle;
sub->imageRect.offset.x += r.x;
sub->imageRect.offset.y += r.y;
return 1;
}
static int pool_finish_locked(void) {
int ok = 1;
for (int a = 0; a < 2; ++a)
if (pool_atlas[a].acquired && pool_atlas[a].waited) {
p_glFlush();
XrSwapchainImageReleaseInfo ri = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
XrResult r = ((PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage"))(
pool_atlas[a].handle, &ri);
if (XR_FAILED(r))
ok = 0;
else
pool_atlas[a].acquired = pool_atlas[a].waited = 0;
}
return ok;
}
// Each OpenXR call may run on a different thread, including creates of different panels.
static pthread_mutex_t pool_lock = PTHREAD_MUTEX_INITIALIZER;
static int pool_find(XrSwapchain h) {
pthread_mutex_lock(&pool_lock);
int result = pool_find_locked(h);
pthread_mutex_unlock(&pool_lock);
return result;
}
static int pool_create(XrSession session, const XrSwapchainCreateInfo *ci, XrSwapchain *out, int limited) {
pthread_mutex_lock(&pool_lock);
int result = pool_create_locked(session, ci, out, limited);
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_destroy(int i) {
pthread_mutex_lock(&pool_lock);
pool_destroy_locked(i);
pthread_mutex_unlock(&pool_lock);
}
static void pool_cleanup(XrSession session) {
pthread_mutex_lock(&pool_lock);
pool_cleanup_locked(session);
pthread_mutex_unlock(&pool_lock);
}
static XrResult pool_cycle(int i, int op, uint32_t *index) {
pthread_mutex_lock(&pool_lock);
XrResult result = pool_cycle_locked(i, op, index);
pthread_mutex_unlock(&pool_lock);
return result;
}
static XrResult pool_enumerate(int i, uint32_t cap, uint32_t *count, XrSwapchainImageBaseHeader *images) {
pthread_mutex_lock(&pool_lock);
XrResult result = pool_enumerate_locked(i, cap, count, images);
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_begin_frame(void) {
pthread_mutex_lock(&pool_lock);
pool_begin_frame_locked();
pthread_mutex_unlock(&pool_lock);
}
static int pool_subimage(XrSwapchainSubImage *sub) {
pthread_mutex_lock(&pool_lock);
int result = pool_subimage_locked(sub);
pthread_mutex_unlock(&pool_lock);
return result;
}
static int pool_finish(void) {
pthread_mutex_lock(&pool_lock);
int result = pool_finish_locked();
pthread_mutex_unlock(&pool_lock);
return result;
}
static void pool_note_runtime(XrSession session, XrSwapchain handle) {
pthread_mutex_lock(&pool_lock);
pool_note_runtime_locked(session, handle);
pthread_mutex_unlock(&pool_lock);
}
static void pool_forget_runtime(XrSwapchain handle) {
pthread_mutex_lock(&pool_lock);
pool_forget_runtime_locked(handle);
pthread_mutex_unlock(&pool_lock);
}
static int pool_should_create(XrSession session) {
pthread_mutex_lock(&pool_lock);
int result = pool_should_create_locked(session);
pthread_mutex_unlock(&pool_lock);
return result;
}
// Caller-owned storage stays live until the runtime's xrEndFrame returns. The input layer
// structs are const: never mutate them, including repeated references from strips or stereo.
typedef struct {
XrCompositionLayerQuad quads[64];
XrCompositionLayerCylinderKHR cylinders[64];
XrCompositionLayerProjection projections[64];
XrCompositionLayerProjectionView views[64][2];
} PoolFrame;
static uint32_t pool_rewrite_layers(const XrCompositionLayerBaseHeader **layers, uint32_t count, PoolFrame *frame) {
pool_begin_frame();
uint32_t n = 0;
for (uint32_t j = 0; j < count; ++j) {
const XrCompositionLayerBaseHeader *l = layers[j];
int ok = 1;
if (l->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
frame->quads[j] = *(const XrCompositionLayerQuad *)l;
ok = pool_subimage(&frame->quads[j].subImage);
l = (const void *)&frame->quads[j];
} else if (l->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR) {
frame->cylinders[j] = *(const XrCompositionLayerCylinderKHR *)l;
ok = pool_subimage(&frame->cylinders[j].subImage);
l = (const void *)&frame->cylinders[j];
} else if (l->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
const XrCompositionLayerProjection *p = (const void *)l;
if (p->viewCount <= 2) {
frame->projections[j] = *p;
for (uint32_t v = 0; v < p->viewCount; ++v) {
frame->views[j][v] = p->views[v];
ok &= pool_subimage(&frame->views[j][v].subImage);
}
frame->projections[j].views = frame->views[j];
l = (const void *)&frame->projections[j];
}
}
if (ok)
layers[n++] = l;
else
pool_error();
}
if (!pool_finish()) {
pool_error();
return 0;
}
return n;
}
+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;
}
+15
View File
@@ -329,6 +329,11 @@
"title": "Retry refused picture formats",
"group": "Game settings"
},
"adapter.swapchain_pool": {
"anchor": "adapter-swapchain_pool",
"title": "Share panel images (swapchain limit)",
"group": "Game settings"
},
"adapter.sync_guard": {
"anchor": "adapter-sync_guard",
"title": "Steady controller input after focus",
@@ -529,6 +534,11 @@
"title": "Fix Lua startup crashes on Frame",
"group": "Steam Frame patches"
},
"frame.main_process": {
"anchor": "frame-main_process",
"title": "Start the game in the app's main process",
"group": "Steam Frame patches"
},
"frame.meta_permissions": {
"anchor": "frame-meta_permissions",
"title": "Declare Meta-only permissions",
@@ -599,6 +609,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",
+12 -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,10 @@ 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: separate_process (MAIN activities in another process than the main one, which Lepton doesn't watch:
# frame.main_process, GitHub #178) (2026-10)
# 24: statically linked Khronos OpenXR loaders (frame.static_openxr_loader, GitHub #254) (2026-10)
ANALYSIS_VERSION = 24
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
@@ -339,7 +342,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 +408,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 +436,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),
@@ -462,6 +468,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
"unity_oculus_check": b"\0com.oculus.systemactivities\0" in lib_bytes.get("libunity.so", b""),
# a 2D launcher activity that starts a separate VR activity (frame.start_activity)
"vr_activity": axml.vr_activity(manifest),
# a MAIN activity in its own process: Lepton watches only the main one (frame.main_process)
"separate_process": axml.separate_process_activities(manifest),
"unity_version": unity_version(ggm, lib_bytes.get("libunity.so")) if engine == "Unity" else None,
# the minimum Android version the APK declares (read from the original: OVRPort lowers it to 29)
"min_sdk": axml.min_sdk(manifest),
+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
+47
View File
@@ -376,6 +376,53 @@ def vr_activity(manifest: bytes) -> str | None:
return vr if vr and vr != launcher else None
def _package(x: Axml) -> str | None:
return next((x.attr_str(el, "package") for el in x.elements() if el.name == "manifest"), None)
def separate_process_activities(manifest: bytes) -> list[str]:
"""Activities with a MAIN intent-filter that run in another process than the app's main one (android:process
":name" or another package's name). Lepton only watches the main process: started directly, such an activity
leaves none, and Lepton closes the app about a minute later ("Target app ... did not start", GitHub #178)."""
x = Axml(manifest)
package = _package(x)
process = {}
for el in x.elements():
if el.name == "activity":
process[x.attr_str(el, "name") or ""] = x.attr_str(el, "process")
names = x.strings()
out = []
for el, name, items in component_filters(manifest):
proc = process.get(name) if el == "activity" else None
if proc and proc != package and any(k == "action" and i < len(names) and names[i] == MAIN
for k, _, i in items):
out.append(name)
return out
def set_main_process(manifest: bytes, activities: list[str]) -> bytes | None:
"""Run `activities` in the app's main process: their android:process becomes the package name (only those
elements' attribute values change). None when there is nothing to change."""
x = Axml(manifest)
package = _package(x)
if not package:
return None
hits = [(x.attr_str(el, "name"), el) for el in x.elements() if el.name == "activity"]
if not any(n in activities and x.attr_str(el, "process") not in (None, package) for n, el in hits):
return None
index = x.index(package)
if index is None:
index = x.append_string(package)
names = x.strings()
for el in x.elements(): # again: append_string moved the elements
if el.name != "activity" or x.attr_str(el, "name") not in activities:
continue
for a in el.attrs:
if a.name < len(names) and names[a.name] == "process":
_set_string_attr(x, a.offset, index)
return x.bytes()
def set_start_activity(manifest: bytes, activity: str) -> bytes | None:
"""Make `activity` the one Lepton starts: its MAIN intent-filter gets category LAUNCHER (in place of its VR or
DEFAULT category), every other component's LAUNCHER category becomes INFO. Only those attribute values change
+14 -2
View File
@@ -486,6 +486,7 @@
"Drop to upload": "",
"Drops Quest-only shared library requirements.": "",
"Each game start used to use up one kernel key; after about 200 starts no game started. FramePort stops that, but used keys only come back after a restart of the Frame.": "",
"Embedded OpenXR loader: use FrameBridge": "",
"Enabled by you.": "",
"End all": "",
"End all {n} {name}?": "",
@@ -858,6 +859,7 @@
"Java, OVRPort and apksigner are ready.": "",
"just now": "",
"Keep converted copies on this PC (only needed to inspect a build)": "",
"Keep full resolution (no dynamic resolution)": "",
"Keep game saves on the Frame": "",
"Keep menus in place": "",
"Keep pictures in bounds": "",
@@ -866,9 +868,12 @@
"Keep the signing keys: without them, updating a game loses its saves.": "",
"Keeps a smoothing setting off that the game turns on itself and that can freeze the Frame.": "",
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at \"Waiting\"). An empty rect (0x0, e.g. BattleTalent's first frames, GitHub #293) is rejected the same way and becomes the whole swapchain image.": "",
"Keeps extra menus and panels working when the Frame reaches its image count limit. GLES apps only; Vulkan apps are unaffected.": "",
"Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.": "",
"Keeps one label so the title is stable.": "",
"Keeps one name for the game, so it shows the same everywhere.": "",
"Keeps OpenXR layer recommendations at the full swapchain size. Prevents dynamic resolution from moving Valve foveation off-centre or leaving a coarse white edge line. May increase GPU load.": "",
"Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).": "",
"Keeps the hand and brush visible over Vermillion's painting canvas (the canvas is drawn in the scene instead of as a layer in front).": "",
"Keeps the resolution the game saves in step with the headset's picture size (else the picture is shifted, with black borders, after a render resolution change).": "",
"Keeps three snapshots per game on the Frame and copies new ones here when connected.": "",
@@ -907,7 +912,7 @@
"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\".": "",
"Lepton's Android has no clipboard service, and SDL's Java code (SDL2 and LÖVE games, for example Dramatic Shape) registers a clipboard listener at start without checking, so the app crashes with a NullPointerException in SDLClipboardHandler. That call is skipped (an in-place edit of classes.dex).": "",
"Lepton's Android has no clipboard service, and the Android code of Godot 4.2-4.4 (for example EndoparasiticVR) casts the missing service to a ClipboardManager without checking, so the app crashes at start with a NullPointerException in Godot.<init>. That check is skipped and Godot's clipboard functions do nothing instead (an in-place edit of classes.dex).": "",
"Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad (\"Microsoft X-Box 360 pad\", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.": "",
"Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad (\"Microsoft X-Box 360 pad\", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may not reach the app (GitHub #162); the options below change the buttons inside the app instead (applied with Update on Frame).": "",
"Lets apps with a USB MIDI driver start on the Frame (they'd crash looking for USB devices); USB MIDI devices aren't available there.": "",
"Lets games built on Meta's oldest VR interface run on the Frame.": "",
"Lets games built with Unity's App UI start on the Frame (they'd quit at once on an Android 12 vibrator call).": "",
@@ -952,7 +957,7 @@
"Live view": "",
"Loading…": "",
"Loads FramePort's OpenXR layer under Proton on the Frame. The Frame's SteamVR runtime only accepts OpenXR 1.0 apps, but Proton's VR helper asks for 1.1, so without the layer VR never starts (the game shows as a flat window, or Revive fails with 'Unable to load LibOVRRT DLL'); the layer retries as 1.0. It also emulates xrConvertTimespecTimeToTimeKHR if a runtime refuses it. Ignored on this PC.": "",
"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.": "",
"Loads libglshim.so ahead of libEGL in every engine library that fetches GL through eglGetProcAddress. Fixes Quest-only leniency that Mesa/Zink rejects: #pragma before #extension, implicit int/float conversions (enables GL_EXT_shader_implicit_conversions), and multiview shaders used on single-view framebuffers (hides GL_OVR_multiview; gl_hide_multiview=0 turns that off). Logs every failed shader with its source lines (logcat tag GLShim). Symptom: black screen with working audio in a GLES game.": "",
"Loads the GL shim into a Unity GLES game and hides GL_EXT_multisampled_render_to_texture (and the multiview variant). Some Unity games switch their eye buffer to 4x MSAA at runtime; the Frame only gives single-sampled swapchains, Unity then renders through multisampled render-to-texture and Mesa/Zink crashes (SIGSEGV in libgallium_dri.so on the render thread: gray or frozen screen, for example The Room VR). Hidden, Unity uses an ordinary MSAA buffer and resolves it. Unity's multiview stays on (gl_hide_multiview defaults to 0 here).": "",
"Loads the GL shim into an Unreal GLES game and hides GL_EXT_multisampled_render_to_texture. Unreal's mobile MSAA renders through multisampled render-to-texture, and the Frame's GL driver (Mesa/Zink) can crash on it: SIGSEGV with fault address 0x10000 in libgallium_dri.so on the RHIThread a few seconds after start (Zink looks up a render-pass cache slot past its end, for example Star Wars Pinball VR). Hidden, Unreal renders without that path. Unreal's multiview stays on: Unreal only uses it when GL_OVR_multiview_multisampled_render_to_texture is listed too, so for Unreal the shim keeps that one visible and draws its multiview MSAA attachments single-sampled (glFramebufferTextureMultiviewOVR).": "",
@@ -979,6 +984,7 @@
"Looking up the store description and screenshots…": "",
"Low": "",
"Make Unity text fields work": "",
"Makes an embedded OpenXR loader use FrameBridge and your game settings.": "",
"Makes BlazeRush's menus and controllers work without Meta's avatars, and lets its shader fixes take effect.": "",
"Makes grip, trigger and touch controls reach the game's hands (it picked the empty Gear VR controls on the Frame, for example Vader Immortal).": "",
"Makes Meta's 3D audio library work outside Quest headsets.": "",
@@ -1261,6 +1267,7 @@
"Pretend room layout": "",
"Pretty smooth": "",
"Prevents a start-up crash in some Unreal games.": "",
"Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.": "",
"Previous": "",
"Private SSH key file (not the .pub file)": "",
"Problems and feedback": "",
@@ -1431,6 +1438,7 @@
"Room height": "",
"Room width": "",
"Rotate panels 180°": "",
"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.": "",
"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.": "",
"Run a launch test after installing on the Frame": "",
"Run one line on the Frame, then click Allow here. You do this once. No password, and nothing changes on the Frame until you allow it.": "",
@@ -1496,6 +1504,8 @@
"shader cache: faster starts; rebuilt if deleted": "",
"Shader-fix Vulkan layer (frame.zink_shader_fix) for OpenGL ES games, whose GLSL the Frame's GL driver (Zink) turns into SPIR-V: modules matching size + SHA-256 get words inserted at a byte offset, for example stores that set loop counters and accumulators a shader reads before writing (Vader Immortal's lightspeed jump hung the GPU; found by Klownicle, GitHub #49). Same format as vk_shader_fix. Comes from a game's recipe; the driver's output can change with a Frame update, then the fix no longer matches (logged).": "",
"Shader-fix Vulkan layer (frame.zink_shader_fix): writes every distinct SPIR-V module the GL driver creates to Android/data/<package>/files/fp_spirv/<size>_<sha256>.spv, to capture a shader for a new zink_shader_fix. No effect on the game.": "",
"Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected.": "",
"Share panel images (swapchain limit)": "",
"Share working recipe · {title}": "",
"Share working recipe…": "",
"Show 360° backgrounds and videos": "",
@@ -1550,6 +1560,7 @@
"Smooth as butter": "",
"software encoder": "",
"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\".": "",
"Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's \":halo_game\"), started from a 2D launcher activity. Lepton starts the game activity directly and watches only the app's main process, which then never exists: about a minute in, Lepton decides the app \"did not start\" and closes it, at whatever point the game has reached. This runs those activities in the main process (only their android:process value changes).": "",
"Some engines contain assembly (for example OpenSSL's) that checks a return-address signature it never made. The Quest's CPU ignores those checks, the Steam Frame's enforces them: the game crashes with SIGILL as soon as it uses that code, for example on its first network connection (Star Wars Pinball VR). In a library with unpaired checks, the signing and checking instructions become no-ops.": "",
"Some engines leave the pNext of unused Vulkan attachment references uninitialized. The Frame's driver never reads it, but Lepton always loads Steam's Fossilize shader-cache layer, which follows it and crashes on the first frame (SIGSEGV in libVkLayer_fossilize.so from FVulkanRenderPass, for example Deadpool VR). Loads Vulkan through a small shim that keeps valid pointers and drops only unreadable ones or ones pointing at the wrong structure type. It also drops a depth resolve named in a subpass without a depth attachment, which crashes the Frame's driver at the first render pass (for example Metro Awakening).": "",
"Some game folders are already set up for SteamVR: a Revive copy (LibRevive64.dll) next to the game, started by a small loader DLL (for example xinput1_3.dll) that Windows loads from the game's folder. Then FramePort starts the game program directly — like double-clicking it — instead of through its own Revive (two Revives conflict; for example the Oculus Platform check then fails). On the Frame, Wine is told to use those loader DLLs from the game folder instead of its own.": "",
@@ -1576,6 +1587,7 @@
"Start live view": "",
"Start setup": "",
"Start straight in VR (skip the 2D launcher)": "",
"Start the game in the app's main process": "",
"Start the live view to watch the headset here.": "",
"Start the live view to watch the headset in an mpv window.": "",
"Start the live view to watch the headset in your browser.": "",
@@ -146,7 +146,43 @@ class StartActivity(Patch):
return [("Only the VR activity is a launcher", ok, ", ".join(starts))]
class MainProcess(Patch):
id = "frame.main_process"
a32 = True
needs_vr = False
title = "Start the game in the app's main process"
description = ("Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's "
"\":halo_game\"), started from a 2D launcher activity. Lepton starts the game activity directly "
"and watches only the app's main process, which then never exists: about a minute in, Lepton "
"decides the app \"did not start\" and closes it, at whatever point the game has reached. This "
"runs those activities in the main process (only their android:process value changes).")
order = 21
default_on = True
def applies(self, a):
return bool((a.extra or {}).get("separate_process"))
def detect(self, a):
found = (a.extra or {}).get("separate_process")
if not found:
return None
return Suggestion(True, f"{found[0].rsplit('.', 1)[-1]} runs in its own process, which Lepton doesn't "
"watch: it would close the game about a minute in.")
def apply(self, ctx: ApkContext) -> bool:
manifest = ctx.ws.read(MANIFEST)
fixed = axml.set_main_process(manifest, axml.separate_process_activities(manifest))
if fixed:
ctx.ws.put(MANIFEST, fixed)
return bool(fixed)
def validate(self, ctx):
left = axml.separate_process_activities(ctx.ws.read(MANIFEST))
return [("Game activities run in the main process", not left, ", ".join(left) or "all")]
register(Launcher)
register(NoDebuggable)
register(MetaPermissions)
register(StartActivity)
register(MainProcess)
@@ -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)
+32 -4
View File
@@ -92,6 +92,9 @@ SETTINGS = [
"Monkey's intro video), which the Frame's runtime refuses, get an Android Surface from FrameBridge: each video "
"frame is copied into an ordinary swapchain the panel shows. Without it these panels are missing and a game "
"waiting for its video stays black. 0 = leave them to the runtime."),
("swapchain_pool", "int", 0, "Share panel images (swapchain limit)",
"Share GLES panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). "
"Useful when opening extra menus closes them or leaves panels gray. Vulkan apps are unaffected."),
("equirect_emul", "int", 0, "Show 360° layers",
"360° (equirect) layers, for example a video player's virtual theater or 360° videos (for example 4XVR), which "
"the Frame's runtime lacks, are drawn by a background thread into a layer behind the game's own picture: the "
@@ -424,6 +427,10 @@ UI: dict[str, dict] = {
help="0 = the size of your play area.", control=("slider", 0.0, 10.0, 0.5, "{:.1f} m")),
"scene_depth": dict(group="room", level="advanced", label="Room depth", depends="scene_emul",
help="0 = the size of your play area.", control=("slider", 0.0, 10.0, 0.5, "{:.1f} m")),
"swapchain_pool": dict(group="troubleshooting", level="advanced",
label="Share panel images (swapchain limit)",
help="Keeps extra menus and panels working when the Frame reaches its image count limit. "
"GLES apps only; Vulkan apps are unaffected.", control=("switch",)),
"equirect_emul": dict(group="video360", level="common", label="Show 360° backgrounds and videos",
help="Video players' virtual theaters and 360° videos would otherwise stay black.",
control=("switch",)),
@@ -662,8 +669,8 @@ class AdapterSetting(Patch):
"proximity_emul": lambda a: "libOVRPlugin.so" in a.libs,
"vk_spec_fixes": lambda a: a.engine == "Unreal" and ap.is_vulkan(a),
"controller_models": ap.may_use_render_models,
**{k: ap.is_gles for k in ("equirect_emul", "equirect_face", "equirect_res", "equirect_flip",
"equirect_fps", "equirect_stereo")},
**{k: ap.is_gles for k in ("swapchain_pool", "equirect_emul", "equirect_face", "equirect_res",
"equirect_flip", "equirect_fps", "equirect_stereo")},
}
rule = rules.get(self.key)
return bool(rule(a)) if rule else True
@@ -831,10 +838,21 @@ class SteamGamepad(Patch):
"tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be "
"on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes "
"later isn't seen until the next start). The APK isn't changed. Off for one start: "
"FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.")
"FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may "
"not reach the app (GitHub #162); the options below change the buttons inside the app instead "
"(applied with Update on Frame).")
category = "device"
needs_vr = False
stage = "install" # the agent reads it from the recipe at finalize (deployment.json steam_gamepad)
params = [
Param("swap_sticks", "bool", False, "Swap the sticks: the right stick moves through menus like the left one "
"(for using one controller)"),
Param("swap_ab", "bool", False, "Swap A and B"),
Param("keys", "str", "", "Own key layout lines (Android .kl), separated by ';', e.g. 'key 304 DPAD_DOWN' "
"(A presses D-pad down) or 'key 305 BACK'"),
]
KL_LINE = re.compile(r"(key [0-9]{1,4} [A-Z][A-Z0-9_]{0,40}( (FUNCTION|WAKE))?"
r"|axis 0x[0-9a-f]{1,2} (invert )?[A-Z][A-Z0-9_]{0,40}( flat [0-9]{1,5})?)")
def applies(self, a):
return a.vr_kind == "none"
@@ -846,7 +864,17 @@ class SteamGamepad(Patch):
return None
def install(self, ctx: InstallContext) -> None:
pass # the launcher line (agent: GAMEPAD_LINE) exports SDL's hint and puts the Podman wrapper on PATH
# the launcher line (agent: GAMEPAD_LINE) exports SDL's hint and puts the Podman wrapper on PATH; the wrapper
# builds the app's key layout from these (agent: gamepad_kl)
layout = ",".join(k for k in ("swap_sticks", "swap_ab") if ctx.params.get(k))
lines = [" ".join(ln.split()) for ln in str(ctx.params.get("keys") or "").split(";") if ln.strip()]
bad = [ln for ln in lines if not self.KL_LINE.fullmatch(ln)]
if bad:
raise ValueError(f"Not a key layout line: {bad[0]!r} (for example 'key 304 DPAD_DOWN')")
if layout:
ctx.env["FRAMEPORT_GAMEPAD_LAYOUT"] = layout
if lines:
ctx.env["FRAMEPORT_GAMEPAD_KEYS"] = "; ".join(lines)
def foveation_options(params):
+3
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.",
@@ -40,6 +41,8 @@ SUMMARIES = {
"frame.adapter": "FramePort's layer between the game and the Steam Frame: fills in what the Frame lacks "
"(controllers, room, passthrough). Needed for every converted game.",
"frame.launcher": "Makes the game start from the Frame's Steam library.",
"frame.main_process": "Keeps the game from being closed about a minute after it starts (it runs in a separate "
"Android process the Frame doesn't watch).",
"frame.start_activity": "Opens the app straight in VR, skipping its flat launcher screen. Turn off to use the "
"launcher, for example to import new content.",
"frame.gl_multiview_fbo": "Experimental: shows HUDs, menus and other flat panels that stay black on the Frame "
+17 -1
View File
@@ -796,6 +796,14 @@ class GameView:
app.open_game(package, advanced=True, show_all=self.show_all) # the switch shows it's on now
return handler
def set_flag(pid, key):
def handler(e):
r = state["recipe"]
r.patches.setdefault(pid, {})[key] = bool(e.control.value)
r.reasons[pid] = r.reasons.get(pid) or tr("Set by you.")
save(r)
return handler
sections = []
for cat in ("pcvr", "frame", "overport", "adapter", "device"):
if cat == "adapter": # one row: the settings themselves are in the Game settings dialog (plain words)
@@ -855,7 +863,15 @@ class GameView:
], spacing=T.S3), padding=ft.Padding(T.S4, T.px(10), T.S4, T.px(10)), border=ft.Border(
top=ft.BorderSide(1, T.BORDER))))
continue
if not technical:
flags = any(q.kind == "bool" for q in p.params)
if on and flags: # a patch's own options (e.g. the gamepad's button layout), under it while it's on
params = recipe.params(p.id)
for q in p.params:
sub.append(C.switch(tr(q.help), value=bool(params.get(q.key, q.default)),
on_change=set_flag(p.id, q.key)) if q.kind == "bool" else
C.field(value=str(params.get(q.key, q.default) or ""), hint_text=tr(q.help),
on_blur=set_param(p.id, q.key)))
if not technical or flags:
pass
elif cat == "adapter":
val = recipe.params(p.id).get("value", p.params[0].default)
+354
View File
@@ -0,0 +1,354 @@
// Host runtime/GL doubles exercise the actual adapter implementation, without a headset.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <openxr/openxr.h>
#define CHECK(c) \
do { \
if (!(c)) { \
fprintf(stderr, "line %d: %s\n", __LINE__, #c); \
return 1; \
} \
} while (0)
#include <stdarg.h>
static void log_ignore(const char *fmt, ...) { (void)fmt; }
#define LOG(...) log_ignore(__VA_ARGS__)
static uintptr_t fake = 100;
#define FAKE_HANDLE(t) ((t)(++fake))
static XrInstance active_instance;
static int swapchain_pool = 1;
static int creates, destroys, acquires, waits, releases, blits, deleted, fail_create, fail_enum, fail_wait,
fail_release;
static int bad_gl, fail_gl, scissor = 1, read_fb = 31, draw_fb = 32, texture = 33;
static GLuint next_texture = 1000;
typedef struct {
XrStructureType type;
void *next;
uint32_t image;
} emul_gles_image;
static XrResult create(XrSession s, const XrSwapchainCreateInfo *ci, XrSwapchain *h) {
(void)s;
(void)ci;
++creates;
if (fail_create) return XR_ERROR_LIMIT_REACHED;
*h = FAKE_HANDLE(XrSwapchain);
return XR_SUCCESS;
}
static XrResult destroy(XrSwapchain h) {
(void)h;
++destroys;
return XR_SUCCESS;
}
static XrResult enumerate(XrSwapchain h, uint32_t cap, uint32_t *n, XrSwapchainImageBaseHeader *images) {
(void)h;
(void)cap;
if (fail_enum) return XR_ERROR_RUNTIME_FAILURE;
*n = 3;
for (int j = 0; j < 3; ++j) ((emul_gles_image *)images)[j].image = 2000 + j;
return XR_SUCCESS;
}
static XrResult acquire(XrSwapchain h, const XrSwapchainImageAcquireInfo *ai, uint32_t *index) {
(void)h;
(void)ai;
*index = acquires++ % 3;
return XR_SUCCESS;
}
static XrResult wait_image(XrSwapchain h, const XrSwapchainImageWaitInfo *wi) {
(void)h;
(void)wi;
++waits;
return fail_wait ? XR_TIMEOUT_EXPIRED : XR_SUCCESS;
}
static XrResult release(XrSwapchain h, const XrSwapchainImageReleaseInfo *ri) {
(void)h;
(void)ri;
++releases;
return fail_release ? XR_ERROR_RUNTIME_FAILURE : XR_SUCCESS;
}
static PFN_xrVoidFunction lookup(XrInstance instance, const char *name) {
(void)instance;
#define PROC(n, f) \
if (!strcmp(name, n)) return (PFN_xrVoidFunction)f
PROC("xrCreateSwapchain", create);
PROC("xrDestroySwapchain", destroy);
PROC("xrEnumerateSwapchainImages", enumerate);
PROC("xrAcquireSwapchainImage", acquire);
PROC("xrWaitSwapchainImage", wait_image);
PROC("xrReleaseSwapchainImage", release);
return NULL;
}
static void note_swapchain_size(XrSwapchain h, uint32_t w, uint32_t he) {
(void)h;
(void)w;
(void)he;
}
static void remember_swapchain(XrSwapchain h) { (void)h; }
static void forget_swapchain(XrSwapchain h) { (void)h; }
static void emul_on_create_swapchain(XrSwapchain h, const XrSwapchainCreateInfo *ci) {
(void)h;
(void)ci;
}
static void emul_on_destroy_swapchain(XrSwapchain h) { (void)h; }
#include "pool_clamp.h"
static EGLContext current(void) { return (EGLContext)1; }
static EGLContext (*p_eglGetCurrentContext)(void) = current;
static void (*p_eglCreateContext)(void);
static int emul_load_gl(void) { return 1; }
static void p_glGetIntegerv(GLenum p, GLint *v) {
*v = p == GL_TEXTURE_BINDING_2D ? texture : p == GL_READ_FRAMEBUFFER_BINDING ? read_fb : draw_fb;
}
static void p_glGenTextures(GLsizei n, GLuint *v) {
while (n--) *v++ = next_texture++;
}
static void p_glBindTexture(GLenum target, GLuint t) {
(void)target;
texture = t;
}
static void p_glTexStorage2D(GLenum t, GLsizei levels, GLenum f, GLsizei w, GLsizei h) {
(void)t;
(void)levels;
(void)f;
(void)w;
(void)h;
}
static GLenum p_glGetError(void) { return fail_gl ? GL_INVALID_OPERATION : GL_NO_ERROR; }
static void p_glDeleteTextures(GLsizei n, const GLuint *v) {
(void)v;
deleted += n;
}
static GLsync p_glFenceSync(GLenum c, GLbitfield f) {
(void)c;
(void)f;
return (GLsync)1;
}
static void p_glDeleteSync(GLsync s) { (void)s; }
static void p_glWaitSync(GLsync s, GLbitfield f, GLuint64 t) {
(void)s;
(void)f;
(void)t;
}
static void p_glFlush(void) {}
static GLboolean p_glIsEnabled(GLenum c) {
(void)c;
return scissor;
}
static void p_glEnable(GLenum c) {
(void)c;
scissor = 1;
}
static void p_glDisable(GLenum c) {
(void)c;
scissor = 0;
}
static void p_glGenFramebuffers(GLsizei n, GLuint *f) {
while (n--) *f++ = next_texture++;
}
static void p_glDeleteFramebuffers(GLsizei n, const GLuint *f) {
(void)n;
(void)f;
}
static void p_glBindFramebuffer(GLenum t, GLuint f) {
if (t == GL_READ_FRAMEBUFFER)
read_fb = f;
else
draw_fb = f;
}
static void p_glFramebufferTexture2D(GLenum t, GLenum a, GLenum tex, GLuint id, GLint level) {
(void)t;
(void)a;
(void)tex;
(void)id;
(void)level;
}
static GLenum p_glCheckFramebufferStatus(GLenum target) {
(void)target;
return bad_gl ? 0 : GL_FRAMEBUFFER_COMPLETE;
}
static void p_glBlitFramebuffer(GLint x, GLint y, GLint w, GLint h, GLint dx, GLint dy, GLint dw, GLint dh,
GLbitfield mask, GLenum filter) {
(void)x;
(void)y;
(void)w;
(void)h;
(void)dx;
(void)dy;
(void)dw;
(void)dh;
(void)mask;
(void)filter;
++blits;
}
#include "swapchain_pool.c"
static int released(XrSwapchain h) {
int i = pool_find(h);
uint32_t index;
CHECK(pool_cycle(i, 0, &index) == XR_SUCCESS && index == 0);
CHECK(pool_cycle(i, 1, NULL) == XR_SUCCESS);
CHECK(pool_cycle(i, 2, NULL) == XR_SUCCESS);
return 0;
}
int main(void) {
pool_session = (XrSession)1;
XrSwapchainCreateInfo ci = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
ci.width = 1024;
ci.height = 128;
ci.format = GL_SRGB8_ALPHA8;
ci.faceCount = ci.arraySize = ci.sampleCount = ci.mipCount = 1;
ci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
XrSwapchain first, second, h;
CHECK(!pool_should_create(pool_session));
for (int k = 0; k < 8; ++k) pool_note_runtime(pool_session, (XrSwapchain)(uintptr_t)(k + 500));
CHECK(pool_should_create(pool_session));
pool_forget_runtime((XrSwapchain)500);
CHECK(!pool_should_create(pool_session));
CHECK(!pool_create((XrSession)2, &ci, &h, 0)); // Vulkan/other session
swapchain_pool = 0;
CHECK(!pool_create(pool_session, &ci, &h, 0));
swapchain_pool = 1;
ci.arraySize = 2;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.arraySize = 1;
ci.sampleCount = 4;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.sampleCount = 1;
ci.format = GL_DEPTH_COMPONENT24;
CHECK(!pool_create(pool_session, &ci, &h, 0));
ci.format = GL_SRGB8_ALPHA8;
CHECK(pool_create(pool_session, &ci, &first, 0) && texture == 33 && creates == 1);
CHECK(pool_create(pool_session, &ci, &second, 1) && creates == 1);
int i = pool_find(first), j = pool_find(second);
uint32_t n, index;
CHECK(pool_enumerate(i, 0, &n, NULL) == XR_SUCCESS && n == 1);
emul_gles_image image = {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR};
CHECK(pool_enumerate(i, 1, &n, (void *)&image) == XR_SUCCESS && image.image == pool_images[i].tex);
CHECK(pool_cycle(i, 1, NULL) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 0, NULL) == XR_ERROR_VALIDATION_FAILURE);
CHECK(pool_cycle(i, 0, &index) == XR_SUCCESS);
CHECK(pool_cycle(i, 0, &index) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 2, NULL) == XR_ERROR_CALL_ORDER_INVALID);
CHECK(pool_cycle(i, 1, NULL) == XR_SUCCESS && pool_cycle(i, 2, NULL) == XR_SUCCESS);
CHECK(released(second) == 0);
XrSwapchainSubImage src = {second, {{10, 5}, {100, 50}}, 0}, sub = src;
pool_begin_frame();
CHECK(pool_subimage(&sub));
CHECK(sub.swapchain == pool_atlas[0].handle && sub.imageRect.offset.x == pool_images[j].rect.x + 10);
CHECK(sub.imageRect.offset.y == 5 && sub.imageRect.extent.width == 100);
CHECK(read_fb == 31 && draw_fb == 32 && scissor == 1 && texture == 33);
sub = src;
CHECK(pool_subimage(&sub) && blits == 1); // two eyes/strips copy source once
CHECK(acquires == 1 && waits == 1 && releases == 0 && pool_finish() && releases == 1);
pool_begin_frame();
sub = src;
CHECK(pool_subimage(&sub) && blits == 2 && acquires == 2);
CHECK(pool_finish());
// Failed wait keeps ownership, retries the same acquired image, never releases before wait.
pool_begin_frame();
fail_wait = 1;
sub = src;
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
CHECK(acquires == 3 && releases == 2);
fail_wait = 0;
CHECK(pool_subimage(&sub) && acquires == 3);
CHECK(pool_finish());
pool_begin_frame();
bad_gl = 1;
sub = src;
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
bad_gl = 0;
CHECK(read_fb == 31 && draw_fb == 32 && scissor == 1);
pool_begin_frame();
sub = src;
CHECK(pool_subimage(&sub));
fail_release = 1;
CHECK(!pool_finish());
fail_release = 0;
CHECK(pool_finish());
// Final layer submission preserves order, const inputs, strip crops, stereo and cylinder rects.
XrCompositionLayerQuad q = {XR_TYPE_COMPOSITION_LAYER_QUAD};
q.subImage = src;
XrCompositionLayerCylinderKHR cylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
cylinder.subImage = src;
XrCompositionLayerProjectionView views[2] = {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
views[0].subImage = views[1].subImage = src;
XrCompositionLayerProjection projection = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
projection.viewCount = 2;
projection.views = views;
const XrCompositionLayerBaseHeader *layers[] = {(void *)&projection, (void *)&q, (void *)&cylinder, (void *)&q};
PoolFrame frame;
int before_blits = blits, before_acquires = acquires, before_releases = releases;
CHECK(pool_rewrite_layers(layers, 4, &frame) == 4);
CHECK(layers[0]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
layers[2]->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR);
CHECK(frame.views[0][1].subImage.swapchain == pool_atlas[0].handle &&
frame.cylinders[2].subImage.imageRect.offset.x == pool_images[j].rect.x + 10);
CHECK(q.subImage.swapchain == second && views[1].subImage.swapchain == second);
CHECK(blits == before_blits + 1 && acquires == before_acquires + 1 && releases == before_releases + 1);
sub = src;
sub.imageRect = (XrRect2Di){{0, 0}, {0, 0}};
pool_begin_frame();
CHECK(pool_subimage(&sub) && sub.imageRect.extent.width == 1024);
CHECK(pool_finish());
sub = src;
sub.imageRect.offset.x = 2000;
pool_begin_frame();
CHECK(!pool_subimage(&sub));
CHECK(pool_finish());
sub = src;
sub.imageRect.offset.x = -10;
pool_begin_frame();
CHECK(pool_subimage(&sub) && sub.imageRect.offset.x == pool_images[j].rect.x && sub.imageRect.extent.width == 90);
CHECK(pool_finish());
PoolRect old = pool_images[i].rect;
pool_destroy(i);
CHECK(pool_create(pool_session, &ci, &h, 0) && pool_images[pool_find(h)].rect.x == old.x);
// Reporter-sized submenu images keep sharing one runtime handle even when further creates are refused.
int before_creates = creates;
fail_create = 1;
int dimensions[][2] = {{1024, 128}, {1120, 440}, {2048, 256}, {600, 440}, {1000, 440}, {512, 256}, {400, 60}};
for (int k = 0; k < 7; ++k) {
ci.width = dimensions[k][0];
ci.height = dimensions[k][1];
CHECK(pool_create(pool_session, &ci, &h, 1));
}
CHECK(creates == before_creates);
fail_create = 0;
// Full atlas spills to a second one, then refuses instead of overlapping.
pool_cleanup(pool_session);
pool_session = (XrSession)1;
ci.width = ci.height = 4096;
CHECK(pool_create(pool_session, &ci, &first, 0));
CHECK(pool_create(pool_session, &ci, &second, 0));
CHECK(pool_images[pool_find(first)].atlas != pool_images[pool_find(second)].atlas);
CHECK(!pool_create(pool_session, &ci, &h, 0));
pool_cleanup(pool_session);
CHECK(!pool_session && pool_find(first) < 0);
pool_session = (XrSession)1;
fail_create = 1;
CHECK(!pool_create(pool_session, &ci, &h, 1));
fail_create = 0;
fail_enum = 1;
CHECK(!pool_create(pool_session, &ci, &h, 0));
CHECK(!pool_atlas[0].handle);
fail_enum = 0;
fail_gl = 1;
CHECK(!pool_create(pool_session, &ci, &h, 0));
fail_gl = 0;
pool_cleanup(pool_session);
pool_error();
CHECK(pool_errors == 1 && deleted > 0 && destroys > 0);
PoolRect occupied[4] = {{0, 0, 2, 2}, {2, 0, 2, 2}, {0, 2, 2, 2}, {2, 2, 2, 2}}, out;
CHECK(!pool_place(occupied, 4, 4, 1, 1, &out));
CHECK(!pool_place(occupied, 0, 4, 5, 1, &out));
CHECK(!pool_place(occupied, 0, 4, 0, 1, &out));
occupied[1] = occupied[3];
CHECK(pool_place(occupied, 3, 4, 2, 2, &out) && out.x == 2 && out.y == 0);
puts("ok");
return 0;
}
+1 -1
View File
@@ -330,7 +330,7 @@ def test_which_patches_work_on_the_bridge():
ok = {p.id for p in base.all_patches() if base.works_on_a32(p)}
assert {p for p in ok if base.get(p).category in ("overport", "frame")} == {
"frame.a32_bridge", "frame.adapter", "frame.launcher"}
"frame.a32_bridge", "frame.adapter", "frame.launcher", "frame.main_process"} # manifest edits: any ABI
assert "adapter.scale" in ok and "device.text_input_window" in ok
assert not {f"adapter.{k}" for k in XRSHIM_SETTINGS} & ok
a = _analysis()
+16
View File
@@ -94,3 +94,19 @@ def test_library_load_with_a_cold_catalog_does_not_recurse(monkeypatch):
monkeypatch.setattr(library, "load", counting)
library.load()
assert catalog.remote_enabled() is False and len(loads) <= 2
def test_swapchain_pool_catalog_is_gated_for_older_apps(monkeypatch):
from pathlib import Path
import yaml
from frameport.patches import base
path = Path(__file__).resolve().parents[1] / "catalog/games/cn.vr4p.oculus4xvrplayerov.yaml"
entry = yaml.safe_load(path.read_text())
assert entry["adapter"]["swapchain_pool"] == 1
assert catalog.unusable_reason(entry) is None
# Older installed apps see this catalog from main, but cannot register the new adapter key.
monkeypatch.delitem(base.REGISTRY, "adapter.swapchain_pool")
assert "adapter.swapchain_pool" in catalog.unusable_reason(entry)
+4 -2
View File
@@ -6,9 +6,10 @@ from pathlib import Path
import pytest
from test_patches import _analysis
from frameport.patches import base
from frameport.patches.frame.adapter import needs_xrshim
from frameport.patches.settings import adapter_settings, UI
from frameport.patches.settings import UI, adapter_settings
ROOT = Path(__file__).resolve().parents[1]
@@ -54,8 +55,9 @@ def test_triage_only_offers_fix_for_offset_rects():
def test_catalog_requires_app_with_the_fix(monkeypatch):
import frameport
import yaml
import frameport
from frameport.recommend.catalog import unusable_reason
entry = yaml.safe_load((ROOT / "catalog/games/com.camouflaj.manta.yaml").read_text())
assert entry["adapter"]["fixed_resolution"] == 1
+51
View File
@@ -0,0 +1,51 @@
"""device.steam_gamepad's own button layout (GitHub #162: Steam Input's per-game remapping doesn't reach a 2D app):
the patch's options become launcher env, the agent's Podman wrapper turns them into the app's key layout."""
import pytest
from test_steam_gamepad import ENV, NEEDS_POSIX, RUN, agent_module, frame_inputs
from frameport.patches import base
from frameport.patches.base import InstallContext
def install_env(**params):
ctx = InstallContext(package="org.smarttube.stable", params=params, files={}, env={}, adapter_settings={})
base.get("device.steam_gamepad").install(ctx)
return ctx.env
def test_options_become_launcher_env():
assert install_env() == {}
assert install_env(swap_sticks=True, swap_ab=True, keys=" key 304 DPAD_DOWN ;; key 305 BACK") == {
"FRAMEPORT_GAMEPAD_LAYOUT": "swap_sticks,swap_ab", "FRAMEPORT_GAMEPAD_KEYS": "key 304 DPAD_DOWN; key 305 BACK"}
with pytest.raises(ValueError):
install_env(keys="key 304 DPAD_DOWN; rm -rf /")
def test_layout_options(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
assert agent.gamepad_kl() == (agent.GAMEPAD_KL, [])
text, skipped = agent.gamepad_kl("swap_sticks,swap_ab", "key 304 DPAD_DOWN; key 316 HOME; axis 0x10 HAT_X; nope")
lines = text.splitlines()
assert skipped == ["nope"]
assert "axis 0x00 Z flat 4096" in lines and "axis 0x03 X flat 4096" in lines # right stick = main stick
assert "axis 0x01 RZ flat 4096" in lines and "axis 0x04 Y flat 4096" in lines
assert "key 305 BUTTON_A" in lines
assert "key 304 DPAD_DOWN" in lines and "key 304 BUTTON_B" not in lines # own lines replace that key's line
assert "key 316 HOME" in lines and sum(ln.startswith("key 316") for ln in lines) == 1
assert len([ln for ln in lines if not ln.startswith("#")]) == len(agent.GAMEPAD_KL.splitlines()) - 1
@NEEDS_POSIX
def test_wrapper_mounts_the_games_own_layout(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
frame_inputs(tmp_path)
env = dict(ENV, FRAMEPORT_GAMEPAD_LAYOUT="swap_sticks", FRAMEPORT_GAMEPAD_KEYS="key 304 DPAD_DOWN")
out = agent.podman_run_args(RUN, env, str(tmp_path / "sys"), str(tmp_path / "dev"))
mount = next(a for a in out if "Vendor_28de_Product_11ff.kl" in a)
source = mount.split("source=")[1].split(",")[0]
assert source.endswith(".kl") and "steam-gamepad-" in source
text = open(source).read()
assert "key 304 DPAD_DOWN" in text and "axis 0x03 X flat 4096" in text
# the default layout keeps its shared file
out = agent.podman_run_args(RUN, ENV, str(tmp_path / "sys"), str(tmp_path / "dev"))
assert any("agent/steam-gamepad.kl," in a for a in out)
+74
View File
@@ -0,0 +1,74 @@
"""frame.main_process (GitHub #178): a game activity in its own process leaves no main process, which is the one
Lepton watches; it closed Halo CE VR about a minute in ("Target app ... did not start")."""
from conftest import build_axml
from frameport.apk import axml
from frameport.patches import base
PKG = "com.halo.decomp.vr"
def _activity(name, process=None, category=axml.LAUNCHER):
attrs = [("name", "str", name)] + ([("process", "str", process)] if process else [])
return [
("start", "activity", attrs),
("start", "intent-filter", []),
("start", "action", [("name", "str", axml.MAIN)]), ("end", "action"),
("start", "category", [("name", "str", category)]), ("end", "category"),
("end", "intent-filter"),
("end", "activity"),
]
def _halo_manifest():
"""Halo CE VR's layout: the VR activity runs in ":halo_game", the 2D launcher starts it."""
return build_axml([
("start", "manifest", [("package", "str", PKG)]),
("start", "application", []),
*_activity("com.halo.decomp.HaloActivity", ":halo_game", axml.VR_CATEGORY),
*_activity("com.halo.decomp.LauncherActivity"),
("start", "service", [("name", "str", "com.halo.decomp.Crash"), ("process", "str", ":crash")]),
("end", "service"),
("end", "application"),
("end", "manifest"),
])
def _processes(manifest):
x = axml.Axml(manifest)
return {x.attr_str(e, "name"): x.attr_str(e, "process") for e in x.elements() if e.name in ("activity", "service")}
def test_detects_and_moves_the_game_activity():
m = _halo_manifest()
assert axml.separate_process_activities(m) == ["com.halo.decomp.HaloActivity"]
fixed = axml.set_main_process(m, axml.separate_process_activities(m))
assert axml.separate_process_activities(fixed) == []
assert _processes(fixed) == {"com.halo.decomp.HaloActivity": PKG, "com.halo.decomp.LauncherActivity": None,
"com.halo.decomp.Crash": ":crash"} # services keep their own process
assert len(fixed) == len(m) # the package name is already in the pool: one value changes, nothing moves
assert axml.set_main_process(fixed, ["com.halo.decomp.HaloActivity"]) is None
def test_main_process_activities_are_left_alone():
m = build_axml([
("start", "manifest", [("package", "str", PKG)]),
("start", "application", []),
*_activity("com.x.Game", PKG), # explicitly the main process
*_activity("com.x.Other"),
("end", "application"),
("end", "manifest"),
])
assert axml.separate_process_activities(m) == []
assert axml.set_main_process(m, ["com.x.Game"]) is None
def test_patch_is_offered_only_when_detected():
patch = base.get("frame.main_process")
assert patch.default_on
class A:
extra = {"separate_process": ["com.halo.decomp.HaloActivity"]}
assert patch.applies(A()) and patch.detect(A()).recommended
A.extra = {}
assert not patch.applies(A()) and patch.detect(A()) is None
+11
View File
@@ -1065,3 +1065,14 @@ def test_hide_fb_foveation_setting():
assert "hide_fb_foveation" in XRSHIM_SETTINGS
assert needs_xrshim(recipe)
assert UI["hide_fb_foveation"]["level"] == "advanced"
def test_swapchain_pool_is_opt_in_and_gles_only():
patch = base.get("adapter.swapchain_pool")
assert patch.default == 0 and patch.revision == 1
assert "swapchain_pool" not in adapter_settings({})
assert adapter_settings({"adapter.swapchain_pool": {"value": "1"}})["swapchain_pool"] == 1
gles = _analysis(graphics="GLES or unknown (no Vulkan declaration)")
assert patch.applies(gles) and patch.detect(gles) is None
assert not patch.applies(_analysis(graphics="Vulkan"))
assert "Vulkan" in patch.description
+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
+40
View File
@@ -0,0 +1,40 @@
"""Host harness for the GLES swapchain pool, with runtime and GL doubles."""
import os
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
INC = ROOT / "native/.cache/openxr-f2448a8797"
if not INC.is_dir():
INC = None
CC = shutil.which("cc") or shutil.which("clang")
@pytest.mark.native
@pytest.mark.skipif(not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not INC or not CC,
reason="set FRAMEPORT_NATIVE_TESTS=1; needs cached OpenXR and host GLES/EGL headers")
def test_pool_runtime_and_gl(tmp_path):
# Copy only graphics headers: adding the NDK sysroot itself shadows host libc headers.
inc = tmp_path / "include"
candidates = [Path("/usr/include"), *(ROOT / "native/.cache").glob(
"ndk-*/toolchains/llvm/prebuilt/*/sysroot/usr/include")]
graphics = next((p for p in candidates if all((p / d).is_dir() for d in ("GLES3", "EGL", "KHR"))), None)
if graphics is None:
pytest.skip("needs GLES3/EGL/KHR headers (host development packages or cached NDK)")
for directory in ("GLES3", "EGL", "KHR"):
shutil.copytree(graphics / directory, inc / directory)
# Exercise production rect_clamp too, rather than a second implementation in the harness.
adapter = (ROOT / "native/adapter/frame_adapter.c").read_text()
start = adapter.index("static int clamp_rect(")
end = adapter.index("\n}", start) + 2
(inc / "pool_clamp.h").write_text(adapter[start:end])
exe = tmp_path / "pool"
subprocess.run([CC, "-std=c11", "-pthread", "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers",
"-I", str(INC), "-I", str(inc), "-I", str(ROOT / "native/adapter"),
str(ROOT / "tests/native/swapchain_pool_harness.c"), "-o", str(exe)], check=True)
out = subprocess.run([str(exe)], capture_output=True, text=True)
assert out.returncode == 0, out.stderr
assert out.stdout.strip() == "ok"
+19
View File
@@ -171,6 +171,14 @@ def test_tbxr_fdm_crash_is_identified_as_vr_teardown():
def test_ovrplugin_extension_probe_is_not_a_scene_game():
probe = LOG_OK + "09-28 17:39:02.200 1147 1174 I OVRPlugin: Unavailable OpenXR extension: XR_FB_scene\n"
assert "scene-missing" not in {f.id for f in triage(probe, "RUNNING", "com.example.game").findings}
# extension listings (GitHub #404): OVRPlugin's enumeration, OVRPort's dispatcher, Unity's Meta XR feature
for listing in ("I OVRPlugin: Name=XR_FB_scene SpecVersion=1", "I overportOXR: - XR_FB_scene 1",
'I Unity : Meta XR Feature: Extensions="XR_FB_passthrough XR_FB_scene XR_META_boundary"',
"I overportOXR: - XR_FB_scene_capture 1"):
log = LOG_OK + f"09-28 17:39:02.200 1147 1174 {listing}\n"
assert "scene-missing" not in {f.id for f in triage(log, "RUNNING", "com.example.game").findings}, listing
wants = LOG_OK + "09-28 17:39:02.200 1147 1174 E Unity : Required extension XR_FB_scene not available\n"
assert "scene-missing" in {f.id for f in triage(wants, "RUNNING", "com.example.game").findings}
used = LOG_OK + "09-28 17:39:04.000 1147 1174 E Unity : xrQuerySpacesFB failed: -12\n"
assert "scene-missing" in {f.id for f in triage(used, "RUNNING", "com.example.game").findings}
@@ -639,3 +647,14 @@ def test_missing_quest_foveation_image_is_diagnostic():
assert "adapter.hide_fb_foveation" not in result.suggestions()
result = triage(message + "\nFrameBridge: hide_fb_foveation: filtering dispatcher extensions", "EXITED")
assert "quest-foveation-image-missing" not in {f.id for f in result.findings}
def test_swapchain_count_limit_signature():
line = ("10-18 12:00:00 I FrameBridge: xrCreateSwapchain 256x128 format=35907 samples=1 array=1 "
"faces=1 usage=0x1 flags=0x0 result=-10 handle=0x0")
findings = triage(line, "RUNNING").findings
finding = next(f for f in findings if f.id == "swapchain-limit-reached")
assert finding.suggest == [] and "count limit" in finding.diagnosis
for other in (line.replace("result=-10", "result=0"), line.replace("result=-10", "result=-100"),
line.replace("FrameBridge:", "Other:"), "FrameBridge: xrEndFrame result=-10"):
assert "swapchain-limit-reached" not in {f.id for f in triage(other, "RUNNING").findings}