mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-19 08:01:29 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11dd195688 | ||
|
|
5b86843430 | ||
|
|
3e5044ce4a | ||
|
|
5345434ba7 | ||
|
|
0ca7bc45f0 | ||
|
|
a4702d2a13 | ||
|
|
455bc987b2 | ||
|
|
4b0b0daa6d | ||
|
|
91f73fbd03 | ||
|
|
77b527f129 | ||
|
|
6ab6c11f06 | ||
|
|
92e84a1a4b | ||
|
|
a0b71fdfb7 | ||
|
|
3e8f0ce369 | ||
|
|
b65abb4b95 | ||
|
|
0478588886 | ||
|
|
1a5892ee40 | ||
|
|
e3788c0b2b | ||
|
|
feda5f4a03 | ||
|
|
6d9fb0b1e6 | ||
|
|
b78160ca6f | ||
|
|
20c36b112e | ||
|
|
fcfba77f31 | ||
|
|
8870afe18a | ||
|
|
4be04b8a34 | ||
|
|
e231c5a6fe | ||
|
|
7585acd0a8 | ||
|
|
b9fb5791b0 | ||
|
|
a5199495fd | ||
|
|
28a5795323 | ||
|
|
d6cf9a3503 | ||
|
|
2a769dddba | ||
|
|
7a65e86cdc | ||
|
|
2be8873fec | ||
|
|
7d5ed42d18 | ||
|
|
90bf8bcd67 | ||
|
|
d028390da5 | ||
|
|
0a542d849e | ||
|
|
09358fb65e | ||
|
|
ae49d886da | ||
|
|
7afaa2629b | ||
|
|
5e27333f74 | ||
|
|
68f278a744 | ||
|
|
a0c26c9646 | ||
|
|
45900ef294 | ||
|
|
95eed63460 | ||
|
|
04d03b8f87 | ||
|
|
6617941d47 | ||
|
|
05344906ef | ||
|
|
94ece1a232 | ||
|
|
38bd178aa3 | ||
|
|
56c821cce3 | ||
|
|
3298c5f64d | ||
|
|
087ad75f47 | ||
|
|
460c503e43 | ||
|
|
3687a6eb0d | ||
|
|
e07441fb09 | ||
|
|
212801ead2 | ||
|
|
7971354675 | ||
|
|
c3144f65dd | ||
|
|
210d6e80c5 | ||
|
|
f8ee87145f | ||
|
|
52df211266 | ||
|
|
976260ed14 | ||
|
|
893db92985 | ||
|
|
a690d0ee3f | ||
|
|
d48dcaba4e | ||
|
|
f68c7f4232 | ||
|
|
fc5933bb2a | ||
|
|
0e41e1fcca | ||
|
|
bbb05cfc0b | ||
|
|
71a427ac0f | ||
|
|
e8eea8548c | ||
|
|
ab4bb5b68d | ||
|
|
ee902e4c40 | ||
|
|
e1fd4fd244 | ||
|
|
a5172c1e2f | ||
|
|
5c9d2e5c62 | ||
|
|
79c5a210f8 | ||
|
|
4864703e11 | ||
|
|
b3e5b2401e | ||
|
|
301f734dbf | ||
|
|
577b72969a | ||
|
|
11ccdb7fac | ||
|
|
7fe604dbec | ||
|
|
2306b2e788 | ||
|
|
65b39a51ca | ||
|
|
8473fe429e | ||
|
|
3586d520d3 | ||
|
|
79f60057fa | ||
|
|
77b01cbadb | ||
|
|
b9cb73a6c7 | ||
|
|
11a677ed1c | ||
|
|
d3a0846775 | ||
|
|
25db57f468 | ||
|
|
bd2a742356 | ||
|
|
d0a2d89c92 |
No files matched your search
@@ -60,12 +60,12 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
|
||||
FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles with `C.Sparkline` (Flet canvas, no charts
|
||||
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); room (`views/room.py`, persistent, "experimental" pill:
|
||||
scan with progress + Cancel = agent v106 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
|
||||
scan with progress + Cancel = agent v109 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
|
||||
with the Frame page's short card, mesh cached in `<data>/rooms-cache/<frame>/`; in-window 3D `ui/room3d.py`
|
||||
RoomCanvas over `mr/view3d.py` (pure-Python projection, mesh reduced to ≤4k/≤1.5k edges at rest/moving, ≤18 fps
|
||||
worker; each redraw sends NEW shape controls: changing a Points list made Flet diff it item by item, 0.3-1 s);
|
||||
"Open full 3D view" = `mr/room_viewer.py` token server on 127.0.0.1 + hand-written WebGL `mr/viewer_page.py`,
|
||||
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v107):
|
||||
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v109):
|
||||
Clean-up = fp_roomscan's refinement settings, `install/rooms.REFINE_DEFAULTS` = the owner's defaults + slider
|
||||
metadata (agent `ROOM_REFINE_DEFAULTS` must match, a test checks), saved non-defaults in the Frame's
|
||||
`rooms/refine.json`, used by every build; Apply = job → agent `room_refine` (`--refine-only` on a copy of the room
|
||||
|
||||
@@ -47,7 +47,7 @@ try:
|
||||
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
|
||||
fcntl = None
|
||||
|
||||
AGENT_VERSION = 107
|
||||
AGENT_VERSION = 109
|
||||
HOME = os.path.expanduser("~")
|
||||
STEAM = os.path.join(HOME, ".local/share/Steam")
|
||||
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
|
||||
@@ -3466,6 +3466,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]
|
||||
@@ -3927,8 +3929,16 @@ def upgrade_linux_launcher(text):
|
||||
return text
|
||||
|
||||
|
||||
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, scene_room=False):
|
||||
config_sync=False, static_openxr=False, scene_room=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,
|
||||
@@ -3939,6 +3949,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_room_sync_line(text, pkg, scene_room)
|
||||
text = set_refresh_rate_line(text, pkg)
|
||||
@@ -3949,6 +3960,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")
|
||||
|
||||
@@ -5174,7 +5211,8 @@ class _ScanPreview:
|
||||
U, E = ct.c_uint64, ct.c_int
|
||||
signatures = {"Create": (ct.c_char_p, ct.c_char_p, ct.POINTER(U)), "Destroy": (U,),
|
||||
"SortOrder": (U, ct.c_uint32), "Width": (U, ct.c_float),
|
||||
"Absolute": (U, E, ct.POINTER(rv.M34)), "Raw": (U, ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32), "Show": (U,)}
|
||||
"Absolute": (U, E, ct.POINTER(rv.M34)),
|
||||
"Raw": (U, ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32), "Show": (U,)}
|
||||
self.functions = {name: ct.CFUNCTYPE(E, *args)(fn[self.SLOTS[name]])
|
||||
for name, args in signatures.items()}
|
||||
h = U(0)
|
||||
@@ -5282,7 +5320,8 @@ def _scan_complete(started, now, yaw_bins, pitch):
|
||||
and pitch["up"] >= ROOM_SCAN_LOOK_FRAMES and pitch["down"] >= ROOM_SCAN_LOOK_FRAMES)
|
||||
|
||||
|
||||
def capture_roomview(seconds, out_path, wait=0.0, max_hz=3.0, progress=None, prompts=None, preview=True, preview_log=None):
|
||||
def capture_roomview(seconds, out_path, wait=0.0, max_hz=3.0, progress=None, prompts=None, preview=True,
|
||||
preview_log=None):
|
||||
"""Record RoomView 3D meshes into a .fprs dump: `seconds` of fresh meshes (after waiting up to `wait` s for the
|
||||
first one), at most `max_hz` frames per second. progress(dict) is called about once a second. Returns a summary:
|
||||
frames, duration, yaw coverage (30 deg bins seen), frames looking up/down, warnings."""
|
||||
@@ -5455,7 +5494,7 @@ def room_capture_worker(payload):
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
raise AgentError("A room scan is already running")
|
||||
raise AgentError("A room scan is already running") from None
|
||||
return _room_capture_worker(payload)
|
||||
|
||||
|
||||
@@ -5533,8 +5572,10 @@ def set_scan_watch_line(text, pkg, want):
|
||||
return text
|
||||
line = SCAN_WATCH_LINE.format(agent_q=shlex.quote(os.path.abspath(__file__)), pkg_q=shlex.quote(pkg))
|
||||
text = text.replace("\nchild=$!\n", "\nchild=$!\n" + line + "\n", 1)
|
||||
return text.replace(' trap - EXIT INT TERM\n',
|
||||
' trap - EXIT INT TERM\n [[ -z "${scanwatch:-}" ]] || kill "$scanwatch" 2>/dev/null || true\n', 1) if '${scanwatch:-}' not in text else text
|
||||
if '${scanwatch:-}' in text:
|
||||
return text
|
||||
stop = ' [[ -z "${scanwatch:-}" ]] || kill "$scanwatch" 2>/dev/null || true\n'
|
||||
return text.replace(' trap - EXIT INT TERM\n', ' trap - EXIT INT TERM\n' + stop, 1)
|
||||
|
||||
|
||||
def scan_watch_write(folder, name, text):
|
||||
@@ -5821,6 +5862,8 @@ 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:
|
||||
@@ -5829,8 +5872,8 @@ def configure_install(args, prep, moved):
|
||||
scene_value = settings.get("scene_emul", (recipe.get("adapter") or {}).get(
|
||||
"scene_emul", int("adapter.scene_emul" in (recipe.get("patches") or []))))
|
||||
scene_room = float(scene_value) != 0
|
||||
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad,
|
||||
bool(config_sync), scene_room)
|
||||
write_launcher(anchor, base, pkg, title, appid, lepton, args.get("env"), hw_video, gamepad, bool(config_sync),
|
||||
static_openxr, scene_room)
|
||||
art_in = os.path.join(base, "incoming-artwork")
|
||||
art_moved = False
|
||||
if args.get("consume_artwork") and os.path.isdir(art_in):
|
||||
@@ -5842,6 +5885,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,
|
||||
"scene_room": scene_room,
|
||||
"agent_version": AGENT_VERSION, "time": time.time()}
|
||||
@@ -10438,6 +10482,11 @@ COMMANDS = {n[4:]: f for n, f in globals().items() if n.startswith("cmd_")}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "_openxr_manifest":
|
||||
dep = deployment(check_pkg(sys.argv[2])) or {}
|
||||
if dep.get("static_openxr_loader"):
|
||||
write_openxr_runtime_manifest(dep["base"], True)
|
||||
return
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "_snapshot_close":
|
||||
return snapshot_close(sys.argv[2])
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "_fetch_save_snapshot":
|
||||
|
||||
@@ -9,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
|
||||
001e699b64062e2cc260915c75aa3508aff10e590125a38c1a3007f61c13c681 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
7c33d7fb9fd176ca222efb2d87cf525e2f0604457c16896027a5881594495f76 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
b022dc0d48c9ee46e5afa6e169386cdce36d896102fb8677f930abf15408e963 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
19ae0a45e80d8031300eea0d54810c14e4711fa8cd383c29ca65d85a74f577c4 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
|
||||
Executable
BIN
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
|
||||
@@ -16,6 +16,7 @@ frame:
|
||||
- frame.hw_video_decode
|
||||
- frame.unity_text_input
|
||||
adapter:
|
||||
fixed_resolution: 1
|
||||
sync_guard: 1
|
||||
surface_native: 1
|
||||
fade_quad: 1
|
||||
@@ -26,6 +27,6 @@ verified:
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
|
||||
updated: '2026-10-15'
|
||||
min_app: 1.1.0
|
||||
updated: '2026-10-18'
|
||||
min_app: 1.2.2
|
||||
source_hint: Batman- Arkham Shadow
|
||||
@@ -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'
|
||||
|
||||
@@ -18,6 +18,24 @@ signatures:
|
||||
severity: warning
|
||||
diagnosis: 'The game cannot discover a Meta room. Enable room layout (adapter.scene_emul), rebuild and reinstall for the updated extension shim, then save a room under Frame > Room.'
|
||||
suggest: [adapter.scene_emul]
|
||||
- id: dynamic-resolution-foveation
|
||||
pattern: 'projection view [01](?: rect change)?:[^\r\n]*rect=(?:[1-9]\d*,\d+|0,[1-9]\d*) '
|
||||
unless: 'fixed_resolution: recommended'
|
||||
severity: info
|
||||
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
|
||||
|
||||
+119
-1
@@ -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
|
||||
@@ -842,3 +854,109 @@ SteamVR's own Touch remap (`/opt/steamvr/drivers/frame_controller/resources/inpu
|
||||
layout `oculus_touch`) is the diagram's "SteamVR default": X -> d-pad down, Y -> d-pad left/right/up, A -> A,
|
||||
B -> B/X/Y, Menu -> View, plus trigger click -> bumper. Games with their own Valve binding file in that folder
|
||||
(Beat Saber, Batman, Accounting+, In Death, ...) use that file instead.
|
||||
|
||||
|
||||
## Fixed layer resolution / Batman (#406, #394)
|
||||
|
||||
Batman: Arkham Shadow (`com.camouflaj.manta`, Unity + OVRPlugin, OpenXR/Vulkan) submits
|
||||
`2768x2768`, then corner-anchored `rect=0,32 2736x2736`, `rect=0,64 2704x2704` inside the
|
||||
same swapchain. On SteamOS 0.5.6 Valve's FDM injection covers the full swapchain: shrinking
|
||||
viewports can move the sharp centre towards the top right and expose a coarse white right edge.
|
||||
The runtime recommendation driving OVRPlugin's dynamic resolution remains unverified.
|
||||
|
||||
`adapter.fixed_resolution=1` (default off, Batman recipe on) overrides
|
||||
`xrGetRecommendedLayerResolutionMETA`: tracked full swapchain dimensions, `isValid=XR_TRUE`.
|
||||
The existing rect_clamp size table already records successful xrCreateSwapchain calls and
|
||||
retires entries at destruction. Projection views must have matching sizes; quads are supported;
|
||||
unknown swapchains, mixed sizes and other layer types retain the runtime behavior. Submitted
|
||||
rectangles are never enlarged after rendering. Full resolution may increase GPU load.
|
||||
|
||||
The cached OVRPort 3.4.3-aa54c3f dispatcher contains both the extension and function name
|
||||
(and a runtime-missing diagnostic). Its fixed routing table is not a guarantee that every
|
||||
version forwards the call unchanged. Enable the existing arm64 xrshim for this setting:
|
||||
OVRPlugin's loader lookup reaches `framebridge_extension_proc` directly, while native OpenXR
|
||||
loaders use the adapter's xrGetInstanceProcAddr hook. No extension hiding: keeping the
|
||||
recommendation API available avoids relying on an unverified OVRPlugin fallback. The adapter
|
||||
revision stays unchanged because the new setting is opt-in; the catalog requires 1.2.2 so older
|
||||
remote clients keep their bundled recipe (`recommend.catalog.unusable_reason`). Rebuild and
|
||||
reinstall Batman to receive the binary and shim routing; this is not a settings-only fix for
|
||||
an old APK. Device/headset verification is pending.
|
||||
|
||||
Headless check: expect `extension shim: xrGetRecommendedLayerResolutionMETA -> FrameBridge`
|
||||
for OVRPlugin, then up to five `fixed_resolution: recommended WxH -> 2768x2768 (runtime=...)`
|
||||
lines. Compare the size to the actual eye swapchain creation log; scaling can change it.
|
||||
`projection view 0 rect change: ... rect=0,0 2768x2768` (and view 1) should remain full size
|
||||
through gameplay/loading transitions. Rect changes continue to log at most once per second
|
||||
per eye, comparing against the last logged state so a throttled final change is still shown.
|
||||
No recommendation logs means this API path was not exercised: do not claim the fix worked.
|
||||
Headset check: sharp region centred in both eyes, no white right edge, correct menus/loading
|
||||
fades, and acceptable pacing with full-resolution GPU load.
|
||||
|
||||
This is separate from #303's `foveation_storage_fix` (clearing SUBSAMPLED allocation flags;
|
||||
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.
|
||||
@@ -149,6 +149,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Taskmaster | Quest | ✅ Works | |
|
||||
| Tea For God | Quest | ✅ Works | |
|
||||
| Test Quest Controllers | Quest | ✅ Works | |
|
||||
| Test Quest MR | Quest | ✅ Works | |
|
||||
| TetrisEffect | Quest | ✅ Works | |
|
||||
| The Boys VR | Quest | ✅ Works | |
|
||||
| The Climb | Quest | ✅ Works | |
|
||||
|
||||
+53
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 191 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: 194 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) (64)
|
||||
- [Game settings](#group-adapter) (79)
|
||||
- [Steam Frame patches](#group-frame) (65)
|
||||
- [Game settings](#group-adapter) (81)
|
||||
- [Files and settings on the Frame](#group-device) (10)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
|
||||
@@ -79,6 +79,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 6 |
|
||||
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
|
||||
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
|
||||
| [Embedded OpenXR loader: use FrameBridge](#frame-static_openxr_loader) | `frame.static_openxr_loader` | Steam Frame patches | 1 |
|
||||
| [Allow swapchains larger than 4096 px](#frame-swapchain_limit) | `frame.swapchain_limit` | Steam Frame patches | default |
|
||||
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 1 |
|
||||
| [Unity App UI: no Android 12 vibrator](#frame-unity_appui_vibrator) | `frame.unity_appui_vibrator` | Steam Frame patches | |
|
||||
@@ -128,6 +129,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Per-eye diagnostics (log)](#adapter-eye_debug) | `adapter.eye_debug` | Game settings | |
|
||||
| [Draw fades over the picture](#adapter-fade_quad) | `adapter.fade_quad` | Game settings | 2 |
|
||||
| [Colour space support](#adapter-fb_color_space) | `adapter.fb_color_space` | Game settings | |
|
||||
| [Keep full resolution (no dynamic resolution)](#adapter-fixed_resolution) | `adapter.fixed_resolution` | Game settings | 1 |
|
||||
| [Upside-down panel images (diagnostics)](#adapter-flip_dump) | `adapter.flip_dump` | Game settings | |
|
||||
| [Fix upside-down menus](#adapter-flip_emul) | `adapter.flip_emul` | Game settings | |
|
||||
| [Rotate panels 180°](#adapter-flip_quads) | `adapter.flip_quads` | Game settings | |
|
||||
@@ -176,6 +178,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 |
|
||||
@@ -964,6 +967,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.
|
||||
@@ -1648,6 +1666,23 @@ For games that stop at start because the headset reports no colour space (for ex
|
||||
|
||||
> Offers Meta's colour-space extension when the Frame's runtime lacks it: the game is told the headset uses Rec.709 and may choose any colour space (no conversion is done). For games that quit before their VR session with "Rec.709 color space unavailable" (for example OutRun Quest VR, GitHub #336). Only reaches games that use their own OpenXR loader; OVRPort's dispatcher can't forward the new functions.
|
||||
|
||||
### <a id="adapter-fixed_resolution"></a>Keep full resolution (no dynamic resolution)
|
||||
|
||||
Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.
|
||||
|
||||
- `adapter.fixed_resolution`
|
||||
- **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 → Picture → "Keep full resolution (no dynamic resolution)" (advanced).
|
||||
- **Catalog games (1):** Batman: Arkham Shadow.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `dynamic-resolution-foveation` (info): The game submits an offset projection viewport.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> 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.
|
||||
|
||||
### <a id="adapter-flip_dump"></a>Upside-down panel images (diagnostics)
|
||||
|
||||
Saves a few UI panels before and after FrameBridge turns them upright, for testing without the headset.
|
||||
@@ -2354,6 +2389,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).
|
||||
|
||||
+3
-1
@@ -12,7 +12,9 @@ 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' |
|
||||
| Startup exits through `libxlua.so (lj_err_throw)`, then Unity aborts in mutex cleanup; captured Lua error is `bad light userdata pointer` (GitHub #257, BattleTalent 4.7) | This older LuaJIT encodes lightuserdata in 47 bits; Frame supplies valid wider native pointers. Controlled tests with the unchanged library pass a real low-address allocation and fail its own high-address static pointer | `frame.luajit_pointers` automatically selects a full-pointer Lua engine only for the verified library SHA-256, retaining xLua bindings and custom helpers. The scoped patch reaches OpenXR startup for 60 s headless; rendering and gameplay remain unverified. A `lj_err_throw` cleanup stack without the original error message is insufficient to infer this cause |
|
||||
| A GL4ES Team Beef game draws stretched triangles or spikes across characters and the scene (GitHub #191, Lambda1VR) | The newer GL shim exports GLES draw functions that intercept GL4ES's desktop OpenGL calls, bypassing its client-array and immediate-mode translation | `frame.tbxr_vendor` keeps the shim's five draw symbols private when the APK bundles `libgl4es.so`. GL queries and single-sampled framebuffer fixes remain active; other games retain the draw hooks. A/B eye captures on SteamOS 0.4.5: old shim normal, current shim spikes, current shim with private draw symbols normal at ~72 fps. Worn-headset gameplay still needs confirmation |
|
||||
@@ -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) |
|
||||
|
||||
@@ -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. |
|
||||
@@ -31,3 +40,9 @@ missing symbols (no compiler needed).
|
||||
|
||||
After changing anything here: `python native/build.py`, commit artifacts + SHA256SUMS, then run `frameport parity`
|
||||
(the report marks updated binaries as "expected") and a device launch test.
|
||||
|
||||
`adapter.fixed_resolution` (off by default, #406/#394) keeps META recommended layer
|
||||
resolution at tracked full swapchain size. Both adapter GIPA and xrshim's existing
|
||||
extension entry point serve it; the setting adds OVRPlugin shim routing. The extension
|
||||
stays visible, avoiding dependence on a plugin fallback. No adapter revision bump for
|
||||
this opt-in fix. See DEVICE_NOTES "Fixed layer resolution / Batman" for verification.
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// swapchain_size is the adapter's mutex-protected table, shared with rect_clamp.
|
||||
static int fixed_layer_size(const XrCompositionLayerBaseHeader *layer, XrExtent2Di *full) {
|
||||
if (!layer) return 0;
|
||||
uint32_t w = 0, h = 0;
|
||||
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
|
||||
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)layer;
|
||||
if (!p->viewCount || !p->views) return 0;
|
||||
for (uint32_t v = 0; v < p->viewCount; ++v) {
|
||||
uint32_t vw, vh;
|
||||
if (!swapchain_size(p->views[v].subImage.swapchain, &vw, &vh)) return 0;
|
||||
if (v && (vw != w || vh != h)) return 0; // no single full size for this layer
|
||||
w = vw; h = vh;
|
||||
}
|
||||
} else if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
|
||||
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
|
||||
if (!swapchain_size(q->subImage.swapchain, &w, &h)) return 0;
|
||||
} else return 0; // preserve runtime behavior for other layer types
|
||||
if (!w || !h || w > INT32_MAX || h > INT32_MAX) return 0;
|
||||
full->width = (int32_t)w; full->height = (int32_t)h;
|
||||
return 1;
|
||||
}
|
||||
@@ -131,6 +131,8 @@ static int swapchain_fix = 1;
|
||||
static int cube_quads = 1;
|
||||
static int cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
|
||||
static int rect_clamp = 1;
|
||||
static int fixed_resolution;
|
||||
static int swapchain_pool = 0;
|
||||
static int fb_color_space; // fb_color_space: offer XR_FB_color_space when the runtime lacks it (#336)
|
||||
static int action_dedup = 1; // rename duplicate action names in a set (session_fixes.c; GitHub #323)
|
||||
static int pose_consistency = 0; // repeat xrLocateViews queries for one display time get the same poses (GitHub #8) // clamp submitted image rects to their swapchain (SteamVR rejects a 1 px overrun)
|
||||
@@ -210,7 +212,9 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "swapchain_fix=%f", &value) == 1) swapchain_fix = value != 0;
|
||||
if (sscanf(line, "cube_quads=%f", &value) == 1) cube_quads = value != 0;
|
||||
if (sscanf(line, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
|
||||
if (sscanf(line, "fixed_resolution=%f", &value) == 1) fixed_resolution = value != 0;
|
||||
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
|
||||
if (sscanf(line, "swapchain_pool=%f", &value) == 1) swapchain_pool = value != 0;
|
||||
if (sscanf(line, "fb_color_space=%f", &value) == 1) fb_color_space = value != 0;
|
||||
if (sscanf(line, "action_dedup=%f", &value) == 1) action_dedup = value != 0;
|
||||
if (sscanf(line, "pose_consistency=%f", &value) == 1) pose_consistency = value != 0;
|
||||
@@ -303,6 +307,7 @@ static void initialize(void) {
|
||||
if (input_diag) LOG("per-game: input_diag=1 (controller-input diagnostics)");
|
||||
if (proximity_emul) LOG("per-game: proximity_emul=%d (finger proximity from capacitive touch: %s)", proximity_emul,
|
||||
proximity_emul > 1 ? "thumb + index" : "thumb");
|
||||
if (fixed_resolution) LOG("per-game: fixed_resolution=1 (full swapchain recommendations)");
|
||||
if (hide_fb_foveation) LOG("per-game: hide_fb_foveation=1 (Quest Vulkan foveation suppressed)");
|
||||
if (hide_space_warp) LOG("per-game: hide_space_warp=1 (XR_FB_space_warp hidden, space warp info removed)");
|
||||
if (strip_color_bias) LOG("per-game: strip_color_bias=%d (layer color scale/bias removed)", strip_color_bias);
|
||||
@@ -560,7 +565,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;
|
||||
@@ -601,6 +606,29 @@ static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
|
||||
return found;
|
||||
}
|
||||
|
||||
#include "fixed_resolution.h"
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fixed_recommended_resolution(XrSession session,
|
||||
const XrRecommendedLayerResolutionGetInfoMETA *info, XrRecommendedLayerResolutionMETA *out) {
|
||||
PFN_xrGetRecommendedLayerResolutionMETA fn = (PFN_xrGetRecommendedLayerResolutionMETA)
|
||||
lookup(active_instance, "xrGetRecommendedLayerResolutionMETA");
|
||||
if (!info || !out) return XR_ERROR_VALIDATION_FAILURE;
|
||||
XrExtent2Di full;
|
||||
if (!fixed_layer_size(info->layer, &full))
|
||||
return fn ? fn(session, info, out) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
// Read the runtime recommendation only for diagnostics. The override also works
|
||||
// when the runtime lacks this function (OVRPort advertises it itself).
|
||||
XrRecommendedLayerResolutionMETA old = {XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
|
||||
XrResult result = fn ? fn(session, info, &old) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
static unsigned logged;
|
||||
if (logged++ < 5) LOG("fixed_resolution: recommended %dx%d -> %dx%d (runtime=%d)",
|
||||
old.recommendedImageDimensions.width, old.recommendedImageDimensions.height,
|
||||
full.width, full.height, result);
|
||||
out->recommendedImageDimensions = full;
|
||||
out->isValid = XR_TRUE;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
// Clamp `r` to a w x h image; returns whether it changed (2 = it was empty and now covers the whole image).
|
||||
static int clamp_rect(XrRect2Di *r, uint32_t w, uint32_t h) {
|
||||
XrRect2Di c = *r;
|
||||
@@ -625,6 +653,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; }
|
||||
@@ -650,6 +682,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);
|
||||
@@ -737,6 +779,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",
|
||||
@@ -744,6 +788,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);
|
||||
@@ -751,6 +796,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).
|
||||
@@ -768,12 +814,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);
|
||||
}
|
||||
|
||||
@@ -784,6 +832,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);
|
||||
@@ -840,7 +891,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));
|
||||
}
|
||||
@@ -1124,6 +1175,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"
|
||||
@@ -1310,6 +1362,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;
|
||||
@@ -1378,6 +1434,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)
|
||||
@@ -1394,6 +1456,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);
|
||||
@@ -1404,6 +1468,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);
|
||||
@@ -1413,6 +1479,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;
|
||||
@@ -1441,7 +1513,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);
|
||||
@@ -1455,7 +1528,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);
|
||||
@@ -1769,7 +1843,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
|
||||
@@ -2149,6 +2224,11 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
for (uint32_t i = 0; i < tcount; ++i) kept[i] = trimmed[i];
|
||||
count = tcount;
|
||||
}
|
||||
PoolFrame pool_frame;
|
||||
if (swapchain_pool && session == pool_session) {
|
||||
count = pool_rewrite_layers(kept, count, &pool_frame);
|
||||
++swapped;
|
||||
}
|
||||
XrFrameEndInfo fixed = *info;
|
||||
fixed.layers = kept;
|
||||
fixed.layerCount = count;
|
||||
@@ -2156,6 +2236,30 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
static int warned, failures, passthrough_logged;
|
||||
if (dropped && warned++ < 5) LOG("xrEndFrame: dropped %u unusable layer(s)", dropped);
|
||||
if (passthrough && !passthrough_logged++) LOG("xrEndFrame: passthrough layer -> ALPHA_BLEND environment");
|
||||
static struct { XrSwapchain swapchain; XrRect2Di rect; uint32_t array; } last_rect[2];
|
||||
static double last_rect_log[2];
|
||||
struct timespec rect_now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &rect_now);
|
||||
double rect_seconds = rect_now.tv_sec + rect_now.tv_nsec / 1e9;
|
||||
for (uint32_t i = 0; i < info->layerCount; ++i) {
|
||||
const XrCompositionLayerBaseHeader *l = info->layers[i];
|
||||
if (!l || l->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION) continue;
|
||||
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)l;
|
||||
for (uint32_t v = 0; v < p->viewCount && v < 2; ++v) {
|
||||
const XrSwapchainSubImage *sub = &p->views[v].subImage;
|
||||
if ((last_rect[v].swapchain != sub->swapchain || last_rect[v].array != sub->imageArrayIndex ||
|
||||
memcmp(&last_rect[v].rect, &sub->imageRect, sizeof(sub->imageRect))) &&
|
||||
rect_seconds - last_rect_log[v] >= 1.0) {
|
||||
LOG("projection view %u rect change: swapchain=%p rect=%d,%d %dx%d array=%u", v,
|
||||
(void *)(uintptr_t)sub->swapchain, sub->imageRect.offset.x, sub->imageRect.offset.y,
|
||||
sub->imageRect.extent.width, sub->imageRect.extent.height, sub->imageArrayIndex);
|
||||
last_rect[v].swapchain = sub->swapchain;
|
||||
last_rect[v].rect = sub->imageRect;
|
||||
last_rect[v].array = sub->imageArrayIndex;
|
||||
last_rect_log[v] = rect_seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
static int views_logged, quads_logged;
|
||||
if (views_logged < 3 || quads_logged < 10) { // describe the stereo layer layout and quads
|
||||
for (uint32_t i = 0; i < fixed.layerCount; ++i) {
|
||||
@@ -2395,6 +2499,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL shim_poll_event(XrInstance instance, XrEve
|
||||
__attribute__((visibility("default"))) PFN_xrVoidFunction framebridge_extension_proc(const char *name) {
|
||||
pthread_once(&init_once, initialize);
|
||||
if (!name) return NULL;
|
||||
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA"))
|
||||
return (PFN_xrVoidFunction)fixed_recommended_resolution;
|
||||
if (!strcmp(name, "framebridge_hide_fb_foveation"))
|
||||
return (PFN_xrVoidFunction)framebridge_hide_fb_foveation;
|
||||
if (hide_fb_foveation_vulkan) {
|
||||
@@ -2460,6 +2566,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
*function = (PFN_xrVoidFunction)fb_hide_update_swapchain;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
if (fixed_resolution && !strcmp(name, "xrGetRecommendedLayerResolutionMETA")) {
|
||||
*function = (PFN_xrVoidFunction)fixed_recommended_resolution;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
HOOK(xrGetSystemProperties)
|
||||
HOOK(xrApplyHapticFeedback)
|
||||
HOOK(xrCreateReferenceSpace)
|
||||
@@ -2493,6 +2603,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)
|
||||
@@ -2511,7 +2622,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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -343,6 +343,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"
|
||||
@@ -452,7 +461,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,roomscan,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,roomscan,a32")
|
||||
ap.add_argument("--ndk")
|
||||
args = ap.parse_args()
|
||||
parts = set(args.only.split(","))
|
||||
@@ -460,7 +469,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),
|
||||
@@ -470,7 +479,7 @@ def main():
|
||||
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc),
|
||||
"roomscan": lambda: build_roomscan(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",
|
||||
"roomscan", "vroverlay_probe", "a32"):
|
||||
if name in parts:
|
||||
log(f"build {name}")
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
extern XrResult xrNegotiateLoaderRuntimeInterface(const XrNegotiateLoaderInfo *, XrNegotiateRuntimeRequest *);
|
||||
extern XrResult xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *);
|
||||
int main(int argc, char **argv) {
|
||||
XrNegotiateLoaderInfo li = {XR_LOADER_INTERFACE_STRUCT_LOADER_INFO, XR_LOADER_INFO_STRUCT_VERSION,
|
||||
sizeof(li), 1, 1, XR_MAKE_VERSION(1,0,0), XR_CURRENT_API_VERSION};
|
||||
XrNegotiateRuntimeRequest r = {XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST, XR_RUNTIME_INFO_STRUCT_VERSION, sizeof(r)};
|
||||
assert(xrNegotiateLoaderRuntimeInterface(NULL, &r) < 0);
|
||||
assert(xrNegotiateLoaderRuntimeInterface(&li, NULL) < 0);
|
||||
li.minInterfaceVersion = 2;
|
||||
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
|
||||
li.minInterfaceVersion = 1;
|
||||
r.structSize--;
|
||||
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
|
||||
r.structSize++;
|
||||
li.minApiVersion = XR_MAKE_VERSION(2,0,0);
|
||||
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
|
||||
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
|
||||
li.minApiVersion = XR_MAKE_VERSION(1,0,1);
|
||||
li.maxApiVersion = XR_MAKE_VERSION(1,0,0);
|
||||
assert(xrNegotiateLoaderRuntimeInterface(&li, &r) < 0);
|
||||
li.minApiVersion = XR_MAKE_VERSION(1,0,0);
|
||||
li.maxApiVersion = XR_CURRENT_API_VERSION;
|
||||
XrResult result = xrNegotiateLoaderRuntimeInterface(&li, &r);
|
||||
if (argc > 1 && !strcmp(argv[1], "missing")) {
|
||||
assert(result == XR_ERROR_INITIALIZATION_FAILED);
|
||||
return 0;
|
||||
}
|
||||
assert(result == XR_SUCCESS && r.getInstanceProcAddr && r.runtimeInterfaceVersion == 1);
|
||||
assert(r.runtimeApiVersion <= li.maxApiVersion && r.runtimeApiVersion >= li.minApiVersion);
|
||||
XrLoaderInitInfoBaseHeaderKHR info = {XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR, (void *)0x1234};
|
||||
assert(xrInitializeLoaderKHR(&info) == XR_SUCCESS);
|
||||
info.next = NULL;
|
||||
assert(xrInitializeLoaderKHR(&info) == XR_ERROR_RUNTIME_FAILURE);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
#include <openxr/openxr.h>
|
||||
#include <string.h>
|
||||
static XrResult init(const XrLoaderInitInfoBaseHeaderKHR *info) {
|
||||
return info->type == XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR && info->next == (void *)0x1234 ?
|
||||
XR_SUCCESS : XR_ERROR_RUNTIME_FAILURE;
|
||||
}
|
||||
XrResult xrGetInstanceProcAddr(XrInstance instance, const char *name, PFN_xrVoidFunction *fn) {
|
||||
(void)instance;
|
||||
if (!strcmp(name, "xrInitializeLoaderKHR")) {
|
||||
*fn = (PFN_xrVoidFunction)init;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
*fn = NULL;
|
||||
return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
* Runtime endpoint for an application's embedded Khronos loader.
|
||||
* Only the application's loader paths change: the inner loader still finds SteamVR.
|
||||
*/
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "fp_xrruntime", __VA_ARGS__)
|
||||
#else
|
||||
#include <stdio.h>
|
||||
#define LOG(...) do { fprintf(stderr, "fp_xrruntime: "); fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } while (0)
|
||||
#endif
|
||||
#define EXPORT __attribute__((visibility("default")))
|
||||
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
||||
static PFN_xrGetInstanceProcAddr chain;
|
||||
|
||||
static void resolve(void) {
|
||||
/* RTLD_LOCAL and handle-specific dlsym prevent binding to the game's own xr* exports. */
|
||||
void *bridge = dlopen("libopenxr_loader_generic.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (bridge) chain = (PFN_xrGetInstanceProcAddr)dlsym(bridge, "xrGetInstanceProcAddr");
|
||||
if (chain) LOG("chain resolved: FrameBridge");
|
||||
else LOG("chain resolution failed: %s", dlerror());
|
||||
}
|
||||
|
||||
EXPORT XrResult XRAPI_CALL xrInitializeLoaderKHR(const XrLoaderInitInfoBaseHeaderKHR *info) {
|
||||
pthread_once(&once, resolve);
|
||||
if (!chain || !info) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
PFN_xrVoidFunction proc = NULL;
|
||||
XrResult result = chain(XR_NULL_HANDLE, "xrInitializeLoaderKHR", &proc);
|
||||
if (XR_SUCCEEDED(result) && proc)
|
||||
result = ((PFN_xrInitializeLoaderKHR)proc)(info);
|
||||
else if (XR_SUCCEEDED(result)) result = XR_ERROR_INITIALIZATION_FAILED;
|
||||
LOG("xrInitializeLoaderKHR forwarded to FrameBridge: %d", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
EXPORT XrResult XRAPI_CALL xrNegotiateLoaderRuntimeInterface(
|
||||
const XrNegotiateLoaderInfo *loader, XrNegotiateRuntimeRequest *runtime) {
|
||||
if (!loader || !runtime ||
|
||||
loader->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
loader->structVersion != XR_LOADER_INFO_STRUCT_VERSION || loader->structSize != sizeof(*loader) ||
|
||||
runtime->structType != XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST ||
|
||||
runtime->structVersion != XR_RUNTIME_INFO_STRUCT_VERSION || runtime->structSize != sizeof(*runtime) ||
|
||||
loader->minInterfaceVersion > 1 || loader->maxInterfaceVersion < 1 ||
|
||||
loader->minApiVersion > loader->maxApiVersion ||
|
||||
loader->minApiVersion > XR_CURRENT_API_VERSION || loader->maxApiVersion < XR_MAKE_VERSION(1, 0, 0))
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
pthread_once(&once, resolve);
|
||||
if (!chain) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
runtime->runtimeInterfaceVersion = 1;
|
||||
runtime->runtimeApiVersion = loader->maxApiVersion < XR_CURRENT_API_VERSION ?
|
||||
loader->maxApiVersion : XR_CURRENT_API_VERSION;
|
||||
runtime->getInstanceProcAddr = chain;
|
||||
LOG("negotiated runtime interface 1: FrameBridge");
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -76,8 +76,8 @@ def images() -> dict[str, bytes]:
|
||||
"every wall and your furniture.", seen=n, accent=SECONDARY)
|
||||
for n in range(DIRECTIONS + 1): # everything seen before the minimum scan time: the ring is the time left
|
||||
out[f"finish_{n:02d}"] = panel("Almost done", "Walls, ceiling and floor are covered.\n"
|
||||
"Look closely at your furniture\nuntil the ring fills.", seen=n, accent=OK, ring=OK,
|
||||
label=f"{(DIRECTIONS - n) * 5}s")
|
||||
"Look closely at your furniture\nuntil the ring fills.", seen=n, accent=OK,
|
||||
ring=OK, label=f"{(DIRECTIONS - n) * 5}s")
|
||||
data = {}
|
||||
for key, img in out.items():
|
||||
buf = io.BytesIO()
|
||||
|
||||
+186
-96
@@ -1,14 +1,32 @@
|
||||
{
|
||||
"updated": "2026-10-17",
|
||||
"updated": "2026-10-19",
|
||||
"people": [
|
||||
{
|
||||
"login": "spoopyghosty0",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/336754034?v=4",
|
||||
"url": "https://github.com/spoopyghosty0",
|
||||
"commits": 707,
|
||||
"commits": 783,
|
||||
"issues": 6,
|
||||
"prs": 4,
|
||||
"total": 745
|
||||
"prs": 5,
|
||||
"total": 826
|
||||
},
|
||||
{
|
||||
"login": "Izimzizi",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/77581918?v=4",
|
||||
"url": "https://github.com/Izimzizi",
|
||||
"commits": 0,
|
||||
"issues": 21,
|
||||
"prs": 0,
|
||||
"total": 63
|
||||
},
|
||||
{
|
||||
"login": "tkp206093",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/29872453?v=4",
|
||||
"url": "https://github.com/tkp206093",
|
||||
"commits": 0,
|
||||
"issues": 21,
|
||||
"prs": 0,
|
||||
"total": 63
|
||||
},
|
||||
{
|
||||
"login": "Klownicle",
|
||||
@@ -19,15 +37,6 @@
|
||||
"prs": 0,
|
||||
"total": 60
|
||||
},
|
||||
{
|
||||
"login": "tkp206093",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/29872453?v=4",
|
||||
"url": "https://github.com/tkp206093",
|
||||
"commits": 0,
|
||||
"issues": 18,
|
||||
"prs": 0,
|
||||
"total": 54
|
||||
},
|
||||
{
|
||||
"login": "UnicronNL",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/1267745?v=4",
|
||||
@@ -46,6 +55,24 @@
|
||||
"prs": 0,
|
||||
"total": 45
|
||||
},
|
||||
{
|
||||
"login": "Genosystem",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/40860703?v=4",
|
||||
"url": "https://github.com/Genosystem",
|
||||
"commits": 0,
|
||||
"issues": 13,
|
||||
"prs": 0,
|
||||
"total": 39
|
||||
},
|
||||
{
|
||||
"login": "Lucas-Mathieu",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
|
||||
"url": "https://github.com/Lucas-Mathieu",
|
||||
"commits": 12,
|
||||
"issues": 0,
|
||||
"prs": 5,
|
||||
"total": 37
|
||||
},
|
||||
{
|
||||
"login": "Sheebalove",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/176067652?v=4",
|
||||
@@ -91,24 +118,6 @@
|
||||
"prs": 0,
|
||||
"total": 30
|
||||
},
|
||||
{
|
||||
"login": "Lucas-Mathieu",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
|
||||
"url": "https://github.com/Lucas-Mathieu",
|
||||
"commits": 9,
|
||||
"issues": 0,
|
||||
"prs": 4,
|
||||
"total": 29
|
||||
},
|
||||
{
|
||||
"login": "Genosystem",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/40860703?v=4",
|
||||
"url": "https://github.com/Genosystem",
|
||||
"commits": 0,
|
||||
"issues": 9,
|
||||
"prs": 0,
|
||||
"total": 27
|
||||
},
|
||||
{
|
||||
"login": "inseend",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/11666993?v=4",
|
||||
@@ -118,15 +127,6 @@
|
||||
"prs": 0,
|
||||
"total": 27
|
||||
},
|
||||
{
|
||||
"login": "Izimzizi",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/77581918?v=4",
|
||||
"url": "https://github.com/Izimzizi",
|
||||
"commits": 0,
|
||||
"issues": 9,
|
||||
"prs": 0,
|
||||
"total": 27
|
||||
},
|
||||
{
|
||||
"login": "jim2011",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/15848374?v=4",
|
||||
@@ -136,6 +136,15 @@
|
||||
"prs": 0,
|
||||
"total": 24
|
||||
},
|
||||
{
|
||||
"login": "Jmckenzie84",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/71196491?v=4",
|
||||
"url": "https://github.com/Jmckenzie84",
|
||||
"commits": 0,
|
||||
"issues": 8,
|
||||
"prs": 0,
|
||||
"total": 24
|
||||
},
|
||||
{
|
||||
"login": "coah80",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/100182500?v=4",
|
||||
@@ -145,23 +154,14 @@
|
||||
"prs": 2,
|
||||
"total": 20
|
||||
},
|
||||
{
|
||||
"login": "Jmckenzie84",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/71196491?v=4",
|
||||
"url": "https://github.com/Jmckenzie84",
|
||||
"commits": 0,
|
||||
"issues": 6,
|
||||
"prs": 0,
|
||||
"total": 18
|
||||
},
|
||||
{
|
||||
"login": "broli95",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/22918304?v=4",
|
||||
"url": "https://github.com/broli95",
|
||||
"commits": 0,
|
||||
"issues": 5,
|
||||
"issues": 6,
|
||||
"prs": 0,
|
||||
"total": 15
|
||||
"total": 18
|
||||
},
|
||||
{
|
||||
"login": "buildmine10",
|
||||
@@ -199,6 +199,15 @@
|
||||
"prs": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"login": "jeliastam",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/2113009?v=4",
|
||||
"url": "https://github.com/jeliastam",
|
||||
"commits": 0,
|
||||
"issues": 5,
|
||||
"prs": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"login": "Scoota23",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/27732436?v=4",
|
||||
@@ -226,15 +235,6 @@
|
||||
"prs": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"login": "jeliastam",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/2113009?v=4",
|
||||
"url": "https://github.com/jeliastam",
|
||||
"commits": 0,
|
||||
"issues": 4,
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "Loafmeister",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/60628625?v=4",
|
||||
@@ -253,6 +253,15 @@
|
||||
"prs": 1,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "Natouille04",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/60348431?v=4",
|
||||
"url": "https://github.com/Natouille04",
|
||||
"commits": 0,
|
||||
"issues": 4,
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "nickwilde77555",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/180547421?v=4",
|
||||
@@ -352,6 +361,15 @@
|
||||
"prs": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"login": "wanwanwanwano",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/341975109?v=4",
|
||||
"url": "https://github.com/wanwanwanwano",
|
||||
"commits": 0,
|
||||
"issues": 3,
|
||||
"prs": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"login": "Akimya49",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/46827872?v=4",
|
||||
@@ -361,6 +379,33 @@
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "crointer",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/165038970?v=4",
|
||||
"url": "https://github.com/crointer",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "DerMueller",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/82468245?v=4",
|
||||
"url": "https://github.com/DerMueller",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "DexgamingX",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/85261590?v=4",
|
||||
"url": "https://github.com/DexgamingX",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "DJ3thenew23",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/3744521?v=4",
|
||||
@@ -388,6 +433,15 @@
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "Gabiga71",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/20774946?v=4",
|
||||
"url": "https://github.com/Gabiga71",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "ganona",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/112239200?v=4",
|
||||
@@ -424,6 +478,15 @@
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "mamasboi2000",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/344123932?v=4",
|
||||
"url": "https://github.com/mamasboi2000",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "marcoszaballos",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/44778619?v=4",
|
||||
@@ -460,15 +523,6 @@
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "Natouille04",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/60348431?v=4",
|
||||
"url": "https://github.com/Natouille04",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "peetzweg",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/839848?v=4",
|
||||
@@ -496,15 +550,6 @@
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "wanwanwanwano",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/341975109?v=4",
|
||||
"url": "https://github.com/wanwanwanwano",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "yzfsoldier",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/35410319?v=4",
|
||||
@@ -640,24 +685,6 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "DerMueller",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/82468245?v=4",
|
||||
"url": "https://github.com/DerMueller",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "DexgamingX",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/85261590?v=4",
|
||||
"url": "https://github.com/DexgamingX",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "DolphinOfficial",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/92276540?v=4",
|
||||
@@ -685,6 +712,15 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "ehnobigdeal0",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/344167176?v=4",
|
||||
"url": "https://github.com/ehnobigdeal0",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "felis-lynx",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/106830854?v=4",
|
||||
@@ -712,6 +748,15 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "Haseo-sudo",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/344139069?v=4",
|
||||
"url": "https://github.com/Haseo-sudo",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "hokey1981",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/42741984?v=4",
|
||||
@@ -811,6 +856,24 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "MThomason122",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/25598648?v=4",
|
||||
"url": "https://github.com/MThomason122",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "Nikoroq",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/135438574?v=4",
|
||||
"url": "https://github.com/Nikoroq",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "njorbie",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/340724129?v=4",
|
||||
@@ -829,6 +892,15 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "persuaded9041",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/464398?v=4",
|
||||
"url": "https://github.com/persuaded9041",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "pinbear-creator",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/194687704?v=4",
|
||||
@@ -946,6 +1018,15 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "Zomby2D",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/5513720?v=4",
|
||||
"url": "https://github.com/Zomby2D",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "zoomiti",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/34012653?v=4",
|
||||
@@ -954,6 +1035,15 @@
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "codex",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/267193182?v=4",
|
||||
"url": "https://github.com/codex",
|
||||
"commits": 2,
|
||||
"issues": 0,
|
||||
"prs": 0,
|
||||
"total": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -94,6 +94,11 @@
|
||||
"title": "Colour space support",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.fixed_resolution": {
|
||||
"anchor": "adapter-fixed_resolution",
|
||||
"title": "Keep full resolution (no dynamic resolution)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.flip_dump": {
|
||||
"anchor": "adapter-flip_dump",
|
||||
"title": "Upside-down panel images (diagnostics)",
|
||||
@@ -334,6 +339,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",
|
||||
@@ -614,6 +624,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",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"updated": "2026-10-17",
|
||||
"updated": "2026-10-19",
|
||||
"releases": [
|
||||
{
|
||||
"tag": "v1.3.0",
|
||||
"name": "FramePort v1.3.0",
|
||||
"date": "2026-10-19",
|
||||
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/v1.3.0",
|
||||
"dev": false,
|
||||
"notes": "FramePort v1.3.0: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n- **Mixed reality (experimental):** a new Room tab scans your room in the headset, shows it in 3D, cleans up the scan and lets you fix furniture, screen and door labels; mixed-reality games get the room through full Meta scene emulation (thanks @alicecatpaw).\n- **Quest 3 by default:** games see a Quest 3; the headset model, game language, foveation and render resolution can be set per game.\n- **Save backups:** choose the backup folder and keep automatic snapshots made on the Frame when a game closes.\n- **Steam library:** add new games to Steam now, at Steam's next start, or not at all.\n- **Files:** SD cards and other drives, plus sections for PC VR games and Linux apps.\n- **Controllers:** an experimental Quest button mode, and own button layouts for the gamepad in 2D apps.\n- **Game fixes:** Batman: Arkham Shadow's edge line and audio hooks (thanks @Lucas-Mathieu), PrimedGun, 4XVR's menus, Halo CE VR closing during the intro, OutRun, Transformers Beyond Reality, cube loading screens, and Unreal games whose install stopped at a memory patch check.\n- **Fixes:** USB setup without a flashing console, store details after a rename.\n- **22 new recipes.**\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n## Install\n\nDownload the file for your PC, unpack it and start FramePort. Guide:\n[Install and first steps](https://frameport.app/docs/install/).\n\n"
|
||||
},
|
||||
{
|
||||
"tag": "v1.2.1",
|
||||
"name": "FramePort v1.2.1",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
|
||||
__version__ = "1.2.1"
|
||||
__version__ = "1.3.0"
|
||||
@@ -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
|
||||
|
||||
@@ -56,9 +56,10 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
|
||||
# GitHub #349) (2026-10)
|
||||
# 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: meta_depth_occlusion (Unity games with Meta's depth-occlusion keywords: frame.meta_depth_occlusion, PR #246)
|
||||
# 24: statically linked Khronos OpenXR loaders (frame.static_openxr_loader, GitHub #254) (2026-10)
|
||||
# 25: meta_depth_occlusion (Unity games with Meta's depth-occlusion keywords: frame.meta_depth_occlusion, PR #246)
|
||||
# (2026-10)
|
||||
ANALYSIS_VERSION = 24
|
||||
ANALYSIS_VERSION = 25
|
||||
|
||||
|
||||
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
|
||||
@@ -343,7 +344,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.
|
||||
@@ -408,6 +410,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"")),
|
||||
@@ -435,7 +438,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
"unreal_version": unreal_version,
|
||||
"oculus_os_refs": sorted(oculus_os_refs),
|
||||
"xr_layer_exts": sorted(layer_exts), # composition-layer extensions the game's own libraries request
|
||||
"vr_kind": vr_kind(libset, manifest_strings),
|
||||
"vr_kind": ("openxr" if static_loaders
|
||||
and vr_kind(libset, manifest_strings) == "none" else vr_kind(libset, manifest_strings)),
|
||||
"headset": headset(meta, abis, vr_kind(libset, manifest_strings), requires_6dof(manifest)),
|
||||
# a Google Daydream / Cardboard build (GVR / Cardboard SDK): no path to the Frame's headset (GitHub #335)
|
||||
"google_vr": google_vr(libset, cats, manifest_strings, gvr_java, boot),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Khronos loaders embedded in game libraries (never the separately shipped loader)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
|
||||
from elftools.common.exceptions import ELFError
|
||||
from elftools.elf.elffile import ELFFile
|
||||
|
||||
from . import elf
|
||||
|
||||
MANIFEST = "/sdcard/fp_openxr_runtime.json"
|
||||
# Some builds embed complete paths. Match every partition and ABI spelling, rather than a fixed list.
|
||||
PATH = re.compile(rb"/(?:[A-Za-z0-9_-]+/)*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json\x00")
|
||||
COMPOSED_MANIFEST = "/sdcard/fp/1/active_runtime.json"
|
||||
PARTITIONS = ("/product", "/odm", "/oem", "/vendor", "/system")
|
||||
SUBDIR = "/etc/openxr/"
|
||||
BROKERS = ("org.khronos.openxr.runtime_broker", "org.khronos.openxr.system_runtime_broker")
|
||||
|
||||
|
||||
def paths(data: bytes) -> list[str]:
|
||||
if not elf.is_elf(data):
|
||||
return []
|
||||
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
|
||||
if sec is None:
|
||||
return []
|
||||
raw = sec.data()
|
||||
return sorted({m.group()[:-1].decode() for m in PATH.finditer(raw)
|
||||
if m.start() == 0 or raw[m.start() - 1] == 0})
|
||||
|
||||
|
||||
def detected(name: str, data: bytes) -> bool:
|
||||
if (name.startswith("libopenxr_loader") or not elf.is_elf(data)
|
||||
or b"RuntimeManifestFile" not in data or b"xrNegotiateLoaderRuntimeInterface" not in data):
|
||||
return False
|
||||
try:
|
||||
e = ELFFile(io.BytesIO(data))
|
||||
return e["e_machine"] == "EM_AARCH64" and (bool(paths(data)) or composed(data))
|
||||
except (ELFError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def composed(data: bytes) -> bool:
|
||||
"""Khronos platform_utils.hpp builds prefix + subdir + major + suffix.
|
||||
|
||||
Require the complete known table before touching these potentially shared literals.
|
||||
"""
|
||||
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
|
||||
strings = set(sec.data().split(b"\0")) if sec else set()
|
||||
return (all(s.encode() in strings for s in (*PARTITIONS, SUBDIR, "/active_runtime.json"))
|
||||
and any(s.startswith(b"/active_runtime.") and s.endswith(b".json") for s in strings))
|
||||
|
||||
|
||||
def redirected(data: bytes) -> bool:
|
||||
if paths(data) or composed(data) or any(b.encode() + b"\0" in data for b in BROKERS):
|
||||
return False
|
||||
if MANIFEST.encode() + b"\0" in data:
|
||||
return True
|
||||
sec = ELFFile(io.BytesIO(data)).get_section_by_name(".rodata")
|
||||
strings = set(sec.data().split(b"\0")) if sec else set()
|
||||
return (b"card/fp/" in strings and b"/sd" in strings
|
||||
and not any(p.encode() in strings for p in PARTITIONS))
|
||||
|
||||
|
||||
def rewrite(data: bytes) -> tuple[bytes, int]:
|
||||
count = 0
|
||||
if composed(data):
|
||||
# Keep every candidate identical: /sd + card/fp/ + 1 + /active_runtime[.ABI].json.
|
||||
# These partition literals may be pooled with app strings; see DEVICE_NOTES.
|
||||
for old, new in [(p, "/sd") for p in PARTITIONS] + [(SUBDIR, "card/fp/")]:
|
||||
data, n = elf.replace_rodata_string(data, old, new)
|
||||
count += n
|
||||
for old in paths(data):
|
||||
data, n = elf.replace_rodata_string(data, old, MANIFEST)
|
||||
count += n
|
||||
for old in BROKERS:
|
||||
# Keep URI length unchanged as well as the string's storage; both authorities must miss.
|
||||
data, n = elf.replace_rodata_string(data, old, old.replace("org.khronos", "org.fp_null"))
|
||||
count += n
|
||||
return data, count
|
||||
@@ -883,7 +883,8 @@ def room_scan(frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
|
||||
typer.echo(f"{st.get('state')}: {st.get('directions', 0)}/12 directions, "
|
||||
f"ceiling {'seen' if st.get('looked_up', 0) >= 5 else 'not yet'}", err=True)
|
||||
|
||||
typer.echo(json.dumps(rooms.scan(_target(frame).frame, seconds, wait, progress=progress, preview=preview), indent=2))
|
||||
result = rooms.scan(_target(frame).frame, seconds, wait, progress=progress, preview=preview)
|
||||
typer.echo(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
@room_app.command("refine")
|
||||
|
||||
@@ -342,7 +342,8 @@ def _migrate(data: dict) -> bool:
|
||||
if (not isinstance(a, dict) or not isinstance(r, dict) or r.get("source") == "user"
|
||||
or g.get("kind") in ("linux", "rift")):
|
||||
continue
|
||||
if (r.get("patches") or {}).get("adapter.scene_emul", {}).get("value") and uses_scene(analysis_from_dict(a)):
|
||||
scene_on = (r.get("patches") or {}).get("adapter.scene_emul", {}).get("value")
|
||||
if scene_on and uses_scene(analysis_from_dict(a)):
|
||||
r["patches"].setdefault("frame.meta_permissions", {})
|
||||
r.setdefault("reasons", {}).setdefault(
|
||||
"frame.meta_permissions", "Meta's room and anchor permissions must be granted for the saved room.")
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
"Drop your own boxes, doors and windows and use the boxes the scan found.": "",
|
||||
"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}?": "",
|
||||
@@ -943,6 +944,7 @@
|
||||
"just now": "",
|
||||
"keep all": "",
|
||||
"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 past walls": "",
|
||||
@@ -954,10 +956,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 objects visible up close in mixed-reality games that would hide them behind your room using Meta's depth sensor, which the Frame lacks (for example Laser Dance).": "",
|
||||
"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).": "",
|
||||
@@ -1083,6 +1087,7 @@
|
||||
"Low": "",
|
||||
"Made in another play area: scan again or use your play area.": "",
|
||||
"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.": "",
|
||||
@@ -1385,6 +1390,7 @@
|
||||
"Prepare {title}": "",
|
||||
"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": "",
|
||||
@@ -1570,6 +1576,7 @@
|
||||
"Room setup": "",
|
||||
"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.": "",
|
||||
@@ -1666,6 +1673,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": "",
|
||||
|
||||
@@ -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)
|
||||
@@ -16,6 +16,9 @@ BASE_SETTINGS = {"scale": 1.0, "foveation_fix": 1, "controller_fix": 1}
|
||||
|
||||
# key, kind, default, title, description
|
||||
SETTINGS = [
|
||||
("fixed_resolution", "int", 0, "Keep full resolution (no dynamic resolution)",
|
||||
"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."),
|
||||
("scale", "float", 1.0, "Resolution scale",
|
||||
"Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels."),
|
||||
("foveation_fix", "int", 1, "Hide Quest foveation", "Hides Quest foveation extensions the Frame runtime lacks."),
|
||||
@@ -89,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 "
|
||||
@@ -332,6 +338,9 @@ FOVEATION_UI = {
|
||||
}
|
||||
|
||||
UI: dict[str, dict] = {
|
||||
"fixed_resolution": dict(group="picture", level="advanced", label="Keep full resolution (no dynamic resolution)",
|
||||
help="Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. "
|
||||
"May increase GPU load. Used at the next launch.", control=("switch",)),
|
||||
"scale": dict(group="picture", level="common", label="Render resolution (supersampling)",
|
||||
help="Changes the game's render resolution. Higher is sharper but slower; 1.0× is the game's own "
|
||||
"resolution.",
|
||||
@@ -420,7 +429,8 @@ UI: dict[str, dict] = {
|
||||
help="Scene games get your saved room. Without one, they get a simple room from the play area.",
|
||||
control=("switch",)),
|
||||
"scene_scan_request": dict(group="room", level="advanced", label="Scan when requested", depends="scene_emul",
|
||||
help="Scan your room in the headset when the game asks for a room scan.", control=("switch",)),
|
||||
help="Scan your room in the headset when the game asks for a room scan.",
|
||||
control=("switch",)),
|
||||
"scene_hide_bounds": dict(group="room", level="advanced", label="Hide play-area size", depends="scene_emul",
|
||||
help="For games that reject the room because of the play area.", control=("switch",)),
|
||||
"scene_height": dict(group="room", level="advanced", label="Room height", depends="scene_emul",
|
||||
@@ -429,6 +439,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",)),
|
||||
@@ -551,7 +565,8 @@ UI: dict[str, dict] = {
|
||||
# functions OVRPort's dispatcher doesn't know. scene_emul: the dispatcher refuses every scene/anchor function
|
||||
# ("Unknown proc addr: xrQuerySpacesFB", Pinball FX VR 2026-10-16), so the emulated room only reaches the game through
|
||||
# the shim (Demeter/NOPE only got it because haptic_fix happened to be on)
|
||||
XRSHIM_SETTINGS = ("hide_fb_foveation", "controller_models", "haptic_fix", "surface_native", "scene_emul")
|
||||
XRSHIM_SETTINGS = ("fixed_resolution", "hide_fb_foveation", "controller_models", "haptic_fix", "surface_native",
|
||||
"scene_emul")
|
||||
# shown with an "experimental" pill (Game settings, game page, CLI): not proven in the headset on many games yet
|
||||
EXPERIMENTAL_SETTINGS = ("quest_buttons",)
|
||||
|
||||
@@ -634,6 +649,7 @@ class AdapterSetting(Patch):
|
||||
or "com.oculus.feature.PASSTHROUGH" in (a.extra.get("features") or {})),
|
||||
"alpha_blend_emul": lambda a: (a.extra.get("mr_only")
|
||||
or "com.oculus.feature.PASSTHROUGH" in (a.extra.get("features") or {})),
|
||||
"fixed_resolution": lambda a: "libOVRPlugin.so" in a.libs or a.xr == "OpenXR",
|
||||
"hide_fb_foveation": ap.is_vulkan,
|
||||
"flip_emul": ap.is_vulkan,
|
||||
"flip_quads": ap.is_vulkan,
|
||||
@@ -664,8 +680,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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -66,7 +66,8 @@ def suggest(analysis: Analysis, use_catalog: bool = True) -> Recipe:
|
||||
and "frame.meta_permissions" not in entry.frame_remove):
|
||||
# the room needs Meta's scene/anchor permissions granted, or the game never asks for it (NOPE)
|
||||
recipe.patches.setdefault("frame.meta_permissions", {})
|
||||
recipe.reasons.setdefault("frame.meta_permissions", "Meta's room and anchor permissions for the saved room.")
|
||||
recipe.reasons.setdefault("frame.meta_permissions",
|
||||
"Meta's room and anchor permissions for the saved room.")
|
||||
if entry.device_files:
|
||||
recipe.patches["device.files"] = {"files": dict(entry.device_files)}
|
||||
recipe.reasons["device.files"] = why
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <openxr/openxr.h>
|
||||
static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
|
||||
if (!handle) return 0;
|
||||
*w = handle == (XrSwapchain)(uintptr_t)2 ? 1024 : 2768; *h = *w;
|
||||
return 1;
|
||||
}
|
||||
#include "fixed_resolution.h"
|
||||
int main(void) {
|
||||
XrExtent2Di full = {0};
|
||||
XrCompositionLayerProjectionView views[2] = {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
|
||||
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
|
||||
XrCompositionLayerProjection p = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
p.views = views; p.viewCount = 2;
|
||||
for (int i=0; i<2; ++i) {
|
||||
views[i].subImage.swapchain = (XrSwapchain)(uintptr_t)1;
|
||||
views[i].subImage.imageRect = (XrRect2Di){{0,64},{2704,2704}};
|
||||
}
|
||||
assert(fixed_layer_size((void *)&p, &full));
|
||||
assert(full.width == 2768 && full.height == 2768);
|
||||
assert(views[0].subImage.imageRect.extent.width == 2704); // never stretch a submitted subrect
|
||||
views[1].subImage.swapchain = (XrSwapchain)(uintptr_t)2;
|
||||
assert(!fixed_layer_size((void *)&p, &full));
|
||||
views[1].subImage.swapchain = XR_NULL_HANDLE;
|
||||
assert(!fixed_layer_size((void *)&p, &full));
|
||||
p.viewCount=0; assert(!fixed_layer_size((void *)&p, &full));
|
||||
assert(!fixed_layer_size(NULL, &full));
|
||||
XrCompositionLayerQuad q = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
q.subImage.swapchain=(XrSwapchain)(uintptr_t)2;
|
||||
assert(fixed_layer_size((void *)&q, &full) && full.width == 1024);
|
||||
q.type=XR_TYPE_UNKNOWN; assert(!fixed_layer_size((void *)&q, &full));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
#include <openxr/openxr.h>
|
||||
static int missing, valid_size=1, calls;
|
||||
static XrInstance active_instance;
|
||||
static XrResult XRAPI_CALL runtime(XrSession session, const XrRecommendedLayerResolutionGetInfoMETA *info,
|
||||
XrRecommendedLayerResolutionMETA *out) {
|
||||
(void)session; (void)info; ++calls;
|
||||
out->recommendedImageDimensions=(XrExtent2Di){2704,2704};
|
||||
out->isValid=XR_FALSE;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static PFN_xrVoidFunction lookup(XrInstance instance, const char *name) {
|
||||
(void)instance; (void)name; return missing ? NULL : (PFN_xrVoidFunction)runtime;
|
||||
}
|
||||
static int swapchain_size(XrSwapchain handle, uint32_t *w, uint32_t *h) {
|
||||
(void)handle; *w=2768; *h=2768; return valid_size;
|
||||
}
|
||||
static void log_stub(const char *fmt, ...) { (void)fmt; }
|
||||
#define LOG(...) log_stub(__VA_ARGS__)
|
||||
#include "fixed_resolution.h"
|
||||
/* WRAPPER */
|
||||
int main(void) {
|
||||
XrCompositionLayerQuad layer={XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
XrRecommendedLayerResolutionGetInfoMETA info={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_GET_INFO_META};
|
||||
info.layer=(void *)&layer;
|
||||
XrRecommendedLayerResolutionMETA out={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
|
||||
int tail=42; out.next=&tail;
|
||||
for (missing=0; missing<2; ++missing) {
|
||||
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_SUCCESS);
|
||||
assert(out.isValid==XR_TRUE && out.recommendedImageDimensions.width==2768);
|
||||
assert(out.recommendedImageDimensions.height==2768 && out.next==&tail && tail==42);
|
||||
}
|
||||
assert(calls==1);
|
||||
valid_size=0; missing=0;
|
||||
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_SUCCESS);
|
||||
assert(out.recommendedImageDimensions.width==2704 && !out.isValid);
|
||||
missing=1;
|
||||
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,&out)==XR_ERROR_FUNCTION_UNSUPPORTED);
|
||||
assert(fixed_recommended_resolution(XR_NULL_HANDLE,NULL,&out)==XR_ERROR_VALIDATION_FAILURE);
|
||||
assert(fixed_recommended_resolution(XR_NULL_HANDLE,&info,NULL)==XR_ERROR_VALIDATION_FAILURE);
|
||||
return 0;
|
||||
}
|
||||
+15
@@ -5,8 +5,15 @@
|
||||
#include <string.h>
|
||||
#include <openxr/openxr.h>
|
||||
static int enabled, requested;
|
||||
static XrResult XRAPI_CALL recommended(XrSession session, const XrRecommendedLayerResolutionGetInfoMETA *info,
|
||||
XrRecommendedLayerResolutionMETA *out) {
|
||||
(void)session; (void)info;
|
||||
out->recommendedImageDimensions = (XrExtent2Di){2768,2768}; out->isValid = XR_TRUE;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static int query(void) { return enabled; }
|
||||
static PFN_xrVoidFunction extension(const char *name) {
|
||||
if (enabled && !strcmp(name,"xrGetRecommendedLayerResolutionMETA")) return (PFN_xrVoidFunction)recommended;
|
||||
return !strcmp(name,"framebridge_hide_fb_foveation") ? (PFN_xrVoidFunction)query : NULL;
|
||||
}
|
||||
static XrResult XRAPI_CALL enumerate(const char *layer,uint32_t capacity,uint32_t *count,XrExtensionProperties *out) {
|
||||
@@ -57,5 +64,13 @@ int main(void) {
|
||||
assert(((PFN_xrCreateInstance)fn)(&ci,NULL)==XR_SUCCESS);
|
||||
assert(requested==(enabled ? 1 : 2) && ci.enabledExtensionCount==2);
|
||||
}
|
||||
enabled=0;
|
||||
assert(xrGetInstanceProcAddr((XrInstance)(uintptr_t)1,"xrGetRecommendedLayerResolutionMETA",&fn)
|
||||
== XR_ERROR_FUNCTION_UNSUPPORTED);
|
||||
enabled=1;
|
||||
assert(xrGetInstanceProcAddr((XrInstance)(uintptr_t)1,"xrGetRecommendedLayerResolutionMETA",&fn)==XR_SUCCESS);
|
||||
XrRecommendedLayerResolutionMETA out={XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META};
|
||||
assert(((PFN_xrGetRecommendedLayerResolutionMETA)fn)(XR_NULL_HANDLE,NULL,&out)==XR_SUCCESS);
|
||||
assert(out.isValid && out.recommendedImageDimensions.width==2768);
|
||||
return 0;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -94,7 +94,8 @@ def test_launcher_sync_and_upgrade(monkeypatch, tmp_path, capsys):
|
||||
assert '9>&- & scanwatch=$!' in text
|
||||
assert text.index("_room_sync") < text.index("\nsetsid ")
|
||||
assert a.set_room_sync_line(text, pkg, True) == text
|
||||
old = "\n".join(line for line in text.splitlines() if "# frameport-room-sync" not in line and "# frameport-scan-watch" not in line) + "\n"
|
||||
old = "\n".join(line for line in text.splitlines()
|
||||
if "# frameport-room-sync" not in line and "# frameport-scan-watch" not in line) + "\n"
|
||||
(anchor / "launch.sh").write_text(old)
|
||||
a.upgrade_launchers()
|
||||
assert (anchor / "launch.sh").read_text().count("# frameport-room-sync") == 1
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Full swapchain recommendations and opt-in OVRPlugin routing."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
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 UI, adapter_settings
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_setting_is_opt_in_and_routes_ovrplugin():
|
||||
base.load_all()
|
||||
patch = base.get("adapter.fixed_resolution")
|
||||
assert patch.default == 0
|
||||
assert patch.detect(_analysis()) is None
|
||||
assert not adapter_settings({}).get("fixed_resolution")
|
||||
assert needs_xrshim({patch.id: {"value": 1}})
|
||||
assert not needs_xrshim({patch.id: {"value": 0}, "adapter.haptic_fix": {"value": 0}})
|
||||
assert UI[patch.key]["group"] == "picture"
|
||||
assert patch.applies(_analysis(xr="OpenXR", libs=[]))
|
||||
assert patch.applies(_analysis(xr="VrApi", libs=["libOVRPlugin.so"]))
|
||||
assert not patch.applies(_analysis(xr="VrApi", libs=[]))
|
||||
assert not patch.applies(_analysis(abis=["armeabi-v7a"], xr="OpenXR"))
|
||||
|
||||
|
||||
@pytest.mark.native
|
||||
def test_layer_dimensions(tmp_path):
|
||||
inc = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
|
||||
cc = shutil.which("cc")
|
||||
if not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not inc or not cc:
|
||||
pytest.skip("needs FRAMEPORT_NATIVE_TESTS=1, OpenXR headers and cc")
|
||||
exe = tmp_path / "fixed-resolution"
|
||||
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-missing-field-initializers", "-I", str(inc),
|
||||
"-I", str(ROOT / "native/adapter"),
|
||||
str(ROOT / "tests/fixtures/src/fixed_resolution_test.c"), "-o", str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
|
||||
def test_triage_only_offers_fix_for_offset_rects():
|
||||
from frameport.validate.triage import triage
|
||||
log = "projection view 0 rect change: swapchain=0x123 rect=0,32 2736x2736 array=0"
|
||||
found = [f for f in triage(log).findings if f.id == "dynamic-resolution-foveation"]
|
||||
assert found and found[0].suggest == ["adapter.fixed_resolution"] and found[0].question
|
||||
assert not any(f.id == "dynamic-resolution-foveation" for f in
|
||||
triage(log.replace("rect=0,32", "rect=0,0")).findings)
|
||||
assert not any(f.id == "dynamic-resolution-foveation" for f in
|
||||
triage(log + "\nfixed_resolution: recommended 2704x2704 -> 2768x2768").findings)
|
||||
|
||||
|
||||
def test_catalog_requires_app_with_the_fix(monkeypatch):
|
||||
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
|
||||
monkeypatch.setattr(frameport, "__version__", "1.2.1")
|
||||
assert unusable_reason(entry) == "needs FramePort 1.2.2"
|
||||
monkeypatch.setattr(frameport, "__version__", "1.2.2")
|
||||
assert unusable_reason(entry) is None
|
||||
|
||||
|
||||
@pytest.mark.native
|
||||
def test_recommendation_wrapper(tmp_path):
|
||||
inc = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
|
||||
cc = shutil.which("cc")
|
||||
if not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not inc or not cc:
|
||||
pytest.skip("needs FRAMEPORT_NATIVE_TESTS=1, OpenXR headers and cc")
|
||||
# Compile the actual wrapper with a stand-in runtime and swapchain table.
|
||||
source = (ROOT / "native/adapter/frame_adapter.c").read_text()
|
||||
wrapper = source[source.index("static XRAPI_ATTR XrResult XRAPI_CALL fixed_recommended_resolution"):
|
||||
source.index("// Clamp `r` to a w x h image;")]
|
||||
harness = (ROOT / "tests/fixtures/src/fixed_resolution_wrapper_test.c").read_text()
|
||||
unit = tmp_path / "wrapper.c"
|
||||
unit.write_text(harness.replace("/* WRAPPER */", wrapper))
|
||||
exe = tmp_path / "wrapper"
|
||||
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-missing-field-initializers", "-I", str(inc),
|
||||
"-I", str(ROOT / "native/adapter"), str(unit), "-o", str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -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
|
||||
+10
-6
@@ -36,7 +36,8 @@ def harness(tmp_path_factory):
|
||||
def run(harness, folder, mode, *args, process="org.frameport.test"):
|
||||
binary, _ = harness
|
||||
env = {**os.environ, "FRAMEBRIDGE_CONFIG": str(folder / "settings.conf")}
|
||||
result = subprocess.run([process, mode, *map(str, args)], executable=binary, capture_output=True, text=True, env=env)
|
||||
result = subprocess.run([process, mode, *map(str, args)], executable=binary, capture_output=True, text=True,
|
||||
env=env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
return result.stderr
|
||||
|
||||
@@ -50,7 +51,7 @@ def room(folder, *, generated_walls=False):
|
||||
f"volume {uuid(40)} TABLE,OTHER 2 1 0.75 90 2 1 0.75",
|
||||
f"plane {uuid(41)} WINDOW_FRAME 1 1.5 0 0 0 0 1 0.8 1.2"]
|
||||
if not generated_walls:
|
||||
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1])):
|
||||
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1], strict=True)):
|
||||
lines.append(f"wall {uuid(10 + i)} {x} {z} {xx} {zz} 0 2.5")
|
||||
lines += [f"mesh {uuid(42)} room-mesh.bin 4 6", "future-data ignored"]
|
||||
(folder / "room.txt").write_text("\n".join(lines) + "\n")
|
||||
@@ -130,7 +131,8 @@ def test_rejects_corrupt_mesh_without_losing_room(harness, tmp_path, damage):
|
||||
assert "bad room line" in log and "0 mesh triangles" in log
|
||||
|
||||
|
||||
@pytest.mark.parametrize("record", [f"mesh {uuid(80)} ../escape.bin 4 6", f"mesh {uuid(80)} room-mesh.bin 600001 600000",
|
||||
@pytest.mark.parametrize("record", [f"mesh {uuid(80)} ../escape.bin 4 6",
|
||||
f"mesh {uuid(80)} room-mesh.bin 600001 600000",
|
||||
f"mesh {uuid(80)} room-mesh.bin 4 7"])
|
||||
def test_mesh_caps_and_path_confinement(harness, tmp_path, record):
|
||||
room(tmp_path)
|
||||
@@ -139,7 +141,8 @@ def test_mesh_caps_and_path_confinement(harness, tmp_path, record):
|
||||
run(harness, tmp_path, "room", 5, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [None, "", "frameport-room 2\n", "bad 1\n", "# comment before magic\nframeport-room 1\n",
|
||||
@pytest.mark.parametrize("text", [None, "", "frameport-room 2\n", "bad 1\n",
|
||||
"# comment before magic\nframeport-room 1\n",
|
||||
"frameport-room 1\noutline 3 0 0 1 1 2 2\n"])
|
||||
def test_box_fallback_unchanged(harness, tmp_path, text):
|
||||
if text is not None:
|
||||
@@ -191,7 +194,7 @@ def test_failed_anchor_write_is_reported_and_rolled_back(harness, tmp_path):
|
||||
def test_missing_outline_and_ceiling_are_derived(harness, tmp_path):
|
||||
points = [(0, 0), (4, 0), (4, 3), (0, 3)]
|
||||
lines = ["frameport-room 1", f"room {uuid(1)}", f"floor {uuid(2)} 1"]
|
||||
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1])):
|
||||
for i, ((x, z), (xx, zz)) in enumerate(zip(points, points[1:] + points[:1], strict=True)):
|
||||
lines.append(f"wall {uuid(10 + i)} {x} {z} {xx} {zz} 1 4")
|
||||
(tmp_path / "room.txt").write_text("\n".join(lines) + "\n")
|
||||
run(harness, tmp_path, "derived")
|
||||
@@ -221,7 +224,8 @@ def test_real_shim_bypasses_a_dispatcher_that_refuses_scene_calls(harness, tmp_p
|
||||
subprocess.run([*common, "-shared", "-fPIC", "-DFAKE_DISPATCH", str(native / "scene_shim_test.c"),
|
||||
"-o", str(tmp_path / "libopenxr_loader.so")], check=True)
|
||||
shim = tmp_path / "libframe_xrshim.so"
|
||||
subprocess.run([*common, "-shared", "-fPIC", "-fvisibility=hidden", "-I", str(ROOT / "tests/fixtures/src/host_include"),
|
||||
subprocess.run([*common, "-shared", "-fPIC", "-fvisibility=hidden",
|
||||
"-I", str(ROOT / "tests/fixtures/src/host_include"),
|
||||
str(ROOT / "native/xrshim/xrshim.c"), "-ldl", "-pthread", "-lm", "-o", str(shim)], check=True)
|
||||
binary = tmp_path / "test-shim"
|
||||
subprocess.run([*common, str(native / "scene_shim_test.c"), "-ldl", "-o", str(binary)], check=True)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Embedded loader redirection, independent of any game or device."""
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
from test_agent import load_agent
|
||||
from test_patches import _analysis
|
||||
|
||||
from frameport.analysis import detect
|
||||
from frameport.analysis import static_openxr as sx
|
||||
from frameport.apk.workspace import ApkWorkspace
|
||||
from frameport.core.events import Reporter
|
||||
from frameport.patches import base
|
||||
from frameport.patches.frame import static_openxr_loader as patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def embedded(tmp_path):
|
||||
# A real ELF layout with synthetic strings; change only e_machine to model an arm64 game.
|
||||
compiler = shutil.which("cc")
|
||||
if not compiler:
|
||||
pytest.skip("needs host C compiler")
|
||||
strings = [f"/{p}/etc/openxr/1/active_runtime{abi}.json"
|
||||
for p in ("product", "odm", "oem", "vendor", "system") for abi in ("", ".aarch64", ".arm64-v8a")]
|
||||
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface", "unrelated"]
|
||||
src = tmp_path / "embedded.c"
|
||||
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
|
||||
out = tmp_path / "embedded.so"
|
||||
subprocess.run([compiler, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
|
||||
data = bytearray(out.read_bytes())
|
||||
struct.pack_into("<H", data, 18, 183) # EM_AARCH64
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def test_detection_and_byte_stable_rewrite(embedded):
|
||||
assert sx.detected("libengine.so", embedded)
|
||||
assert not sx.detected("libopenxr_loader.so", embedded)
|
||||
assert not sx.detected("libengine.so", embedded.replace(b"RuntimeManifestFile", b"X" * 19))
|
||||
wrong_arch = bytearray(embedded)
|
||||
struct.pack_into("<H", wrong_arch, 18, 62)
|
||||
assert not sx.detected("libengine.so", bytes(wrong_arch))
|
||||
fixed, count = sx.rewrite(embedded)
|
||||
assert count == 17
|
||||
assert len(fixed) == len(embedded)
|
||||
expected = embedded
|
||||
for old in sx.paths(embedded) + list(sx.BROKERS):
|
||||
new = sx.MANIFEST if old.startswith("/") else old.replace("org.khronos", "org.fp_null")
|
||||
expected = expected.replace(old.encode() + b"\0", new.encode().ljust(len(old) + 1, b"\0"))
|
||||
assert fixed == expected # all other bytes, offsets and ELF metadata are identical
|
||||
assert sx.rewrite(fixed) == (fixed, 0)
|
||||
assert not sx.paths(fixed)
|
||||
assert detect.ANALYSIS_VERSION >= 23
|
||||
|
||||
|
||||
def test_patch_and_adapter_without_overport(tmp_path, monkeypatch, embedded):
|
||||
inner = tmp_path / "inner.so"
|
||||
inner.write_bytes(b"unmodified Khronos loader")
|
||||
monkeypatch.setattr(patch.overport, "ensure_runtime_lib", lambda *args: inner)
|
||||
apk = tmp_path / "in.apk"
|
||||
with zipfile.ZipFile(apk, "w") as z:
|
||||
z.writestr("lib/arm64-v8a/libengine.so", embedded)
|
||||
a = _analysis(extra={"static_openxr_loader": ["libengine.so"]})
|
||||
selection = {patch.StaticOpenXRLoader.id: {}, "frame.adapter": {}}
|
||||
p = patch.StaticOpenXRLoader()
|
||||
assert p.detect(a).recommended
|
||||
assert not p.applies(_analysis())
|
||||
with ApkWorkspace(apk) as ws:
|
||||
ctx = base.ApkContext(ws, a, {}, Reporter(), selection)
|
||||
assert p.apply(ctx)
|
||||
assert base.get("frame.adapter").apply(ctx)
|
||||
assert ws.read_lib("libopenxr_loader_original.so") == inner.read_bytes()
|
||||
assert ws.has(ws.lib("libframe_settings.so"))
|
||||
assert all(ok for _, ok, _ in p.validate(ctx))
|
||||
assert all(ok for _, ok, _ in base.get("frame.adapter").validate(ctx))
|
||||
assert not p.apply(ctx)
|
||||
assert not base.get("frame.adapter").apply(ctx)
|
||||
out = ws.write(tmp_path / "out.apk")
|
||||
with ApkWorkspace(out) as ws:
|
||||
ctx.ws = ws
|
||||
assert not p.apply(ctx)
|
||||
ws.put(ws.lib("libengine.so"), embedded)
|
||||
assert not p.validate(ctx)[0][1]
|
||||
|
||||
|
||||
def test_manifest_repair_and_launcher_upgrade(tmp_path, monkeypatch):
|
||||
a = load_agent(monkeypatch, tmp_path)
|
||||
root = tmp_path / "game"
|
||||
a.write_openxr_runtime_manifest(str(root), False)
|
||||
assert not root.exists()
|
||||
a.write_openxr_runtime_manifest(str(root), True)
|
||||
target = root / "lepton-data/external/fp_openxr_runtime.json"
|
||||
manifest = json.loads(target.read_text())
|
||||
assert manifest["runtime"]["library_path"] == patch.SHIM
|
||||
victim = tmp_path / "victim"
|
||||
victim.write_text("untouched")
|
||||
target.unlink()
|
||||
target.symlink_to(victim)
|
||||
a.write_openxr_runtime_manifest(str(root), True)
|
||||
assert victim.read_text() == "untouched"
|
||||
assert not target.is_symlink()
|
||||
anchor = tmp_path / "anchors/com.example.game"
|
||||
anchor.mkdir(parents=True)
|
||||
a.write_launcher(str(anchor), str(root), "com.example.game", "Test", 123, "/lepton", {}, static_openxr=True)
|
||||
text = (anchor / "launch.sh").read_text()
|
||||
assert " _openxr_manifest com.example.game\n" in text
|
||||
assert a.set_openxr_manifest_line(text, "com.example.game", True) == text
|
||||
assert "_openxr_manifest" not in a.set_openxr_manifest_line(text, "com.example.game", False)
|
||||
subprocess.run(["bash", "-n", str(anchor / "launch.sh")], check=True)
|
||||
|
||||
|
||||
def test_runtime_shim_host(tmp_path):
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
headers = next((root / "native/.cache").glob("openxr-*/openxr/openxr.h"), None)
|
||||
cc = shutil.which("cc")
|
||||
if not headers or not cc:
|
||||
pytest.skip("needs cached OpenXR headers and host C compiler")
|
||||
src = root / "native/xrruntime"
|
||||
inc = str(headers.parent.parent)
|
||||
subprocess.run([cc, "-shared", "-fPIC", "-I", inc, str(src / "test_bridge.c"),
|
||||
"-o", str(tmp_path / "libopenxr_loader_generic.so")], check=True)
|
||||
exe = tmp_path / "shim_test"
|
||||
subprocess.run([cc, "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers", "-I", inc,
|
||||
str(src / "xrruntime.c"), str(src / "test.c"), "-ldl", "-lpthread", "-o", str(exe)], check=True)
|
||||
env = dict(os.environ, LD_LIBRARY_PATH=str(tmp_path))
|
||||
subprocess.run([str(exe)], env=env, check=True)
|
||||
(tmp_path / "libopenxr_loader_generic.so").unlink()
|
||||
subprocess.run([str(exe), "missing"], env=env, check=True)
|
||||
|
||||
|
||||
def test_composed_khronos_paths(tmp_path, embedded):
|
||||
# Rebuild the fixture with Khronos platform_utils.hpp's actual fragment table.
|
||||
# This is also the layout in OVRPort's unmodified Khronos loader.
|
||||
|
||||
from frameport.analysis import elf
|
||||
|
||||
cc = shutil.which("cc")
|
||||
src = tmp_path / "composed.c"
|
||||
strings = list(sx.PARTITIONS) + [sx.SUBDIR, "/active_runtime.json", "/active_runtime.aarch64.json"]
|
||||
strings += list(sx.BROKERS) + ["RuntimeManifestFile", "xrNegotiateLoaderRuntimeInterface"]
|
||||
src.write_text("const char *strings[] = {" + ",".join(json.dumps(s) for s in strings) + "};")
|
||||
out = tmp_path / "composed.so"
|
||||
subprocess.run([cc, "-shared", "-fPIC", str(src), "-o", str(out)], check=True)
|
||||
buf = bytearray(out.read_bytes())
|
||||
struct.pack_into("<H", buf, 18, 183)
|
||||
data = bytes(buf)
|
||||
assert sx.composed(data) and sx.detected("libgame.so", data)
|
||||
fixed, count = sx.rewrite(data)
|
||||
assert count == 8 and len(fixed) == len(data)
|
||||
assert sx.redirected(fixed) and sx.rewrite(fixed) == (fixed, 0)
|
||||
expected = data
|
||||
for old, new in [(p, "/sd") for p in sx.PARTITIONS] + [(sx.SUBDIR, "card/fp/")] + [
|
||||
(p, p.replace("org.khronos", "org.fp_null")) for p in sx.BROKERS]:
|
||||
expected, _ = elf.replace_rodata_string(expected, old, new)
|
||||
assert fixed == expected
|
||||
assert "/sd" + "card/fp/" + "1" + "/active_runtime.json" == sx.COMPOSED_MANIFEST
|
||||
|
||||
|
||||
def test_analysis_records_embedded_loader(tmp_path, embedded, quest_manifest):
|
||||
apk = tmp_path / "analyze.apk"
|
||||
with zipfile.ZipFile(apk, "w") as z:
|
||||
z.writestr("AndroidManifest.xml", quest_manifest)
|
||||
z.writestr("lib/arm64-v8a/libengine.so", embedded)
|
||||
assert detect.analyze(apk).extra["static_openxr_loader"] == ["libengine.so"]
|
||||
|
||||
|
||||
def test_triage_only_when_chain_absent():
|
||||
from frameport.validate.triage import triage
|
||||
|
||||
line = "RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json"
|
||||
assert "static-openxr-loader-bypass" in {f.id for f in triage(line, "RUNNING").findings}
|
||||
for chain in ("FrameBridge: settings read", "fp_xrruntime: negotiated"):
|
||||
assert "static-openxr-loader-bypass" not in {f.id for f in triage(line + "\n" + chain, "RUNNING").findings}
|
||||
|
||||
|
||||
def test_heuristic_selects_chain_without_game_specific_rules():
|
||||
from frameport.recommend.engine import suggest
|
||||
|
||||
a = _analysis(package="com.example.othergame", engine="Other", libs=["libengine.so"],
|
||||
extra={"static_openxr_loader": ["libengine.so"], "vr_kind": "openxr"})
|
||||
r = suggest(a, use_catalog=False)
|
||||
assert {"frame.static_openxr_loader", "frame.adapter"} <= r.patches.keys()
|
||||
assert not r.as_is
|
||||
@@ -0,0 +1,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"
|
||||
@@ -647,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}
|
||||
Reference in new issue
Block a user