mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-17 18:03:05 +02:00
Compare commits
203
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a71734552 | ||
|
|
3441f3ee39 | ||
|
|
b9a9757a64 | ||
|
|
f9f3a63e0c | ||
|
|
95fd2e5b0b | ||
|
|
98f91abe97 | ||
|
|
5eb6c8389d | ||
|
|
c811dbabe5 | ||
|
|
14a58443d3 | ||
|
|
e27dce210f | ||
|
|
5e38e4b46e | ||
|
|
387c0eb28c | ||
|
|
7dcb601792 | ||
|
|
3733a38e4e | ||
|
|
e1c16a4bf2 | ||
|
|
61d728fd21 | ||
|
|
122845308f | ||
|
|
451b4d6e5b | ||
|
|
c21126ec7d | ||
|
|
879bcbf9b0 | ||
|
|
351a1af31d | ||
|
|
5739646ce1 | ||
|
|
6b002d0799 | ||
|
|
6b4e50cf7c | ||
|
|
30b1c78b99 | ||
|
|
45563c5047 | ||
|
|
58489eeb75 | ||
|
|
0ce121f2dc | ||
|
|
06b289f7fc | ||
|
|
25ac577365 | ||
|
|
c9240670d4 | ||
|
|
ebda4d644a | ||
|
|
9e96fb8fb4 | ||
|
|
98786ab110 | ||
|
|
18f45702de | ||
|
|
3b71e5a560 | ||
|
|
c023ea28f7 | ||
|
|
8b1e2cdf43 | ||
|
|
633fcde50d | ||
|
|
d063bad128 | ||
|
|
90cb7b575e | ||
|
|
e1bc723f71 | ||
|
|
73839b2dda | ||
|
|
e4f9ca6bd3 | ||
|
|
aa9c605ccc | ||
|
|
607b0946da | ||
|
|
772d08a1a1 | ||
|
|
d32c49d4f5 | ||
|
|
b9a1fe67bc | ||
|
|
6cd1eaaf23 | ||
|
|
741af0651e | ||
|
|
a2f46a17fa | ||
|
|
cbc3416ada | ||
|
|
5b6ddc2048 | ||
|
|
8f932dfd9b | ||
|
|
9eef555bf2 | ||
|
|
8169574d89 | ||
|
|
aeabb3df5a | ||
|
|
d073541a83 | ||
|
|
02dce78618 | ||
|
|
5910a8d3c7 | ||
|
|
22413e21e9 | ||
|
|
4d5c5fa091 | ||
|
|
5007ad26a2 | ||
|
|
927dae5ff8 | ||
|
|
57bb69f79c | ||
|
|
a87ccf4c7b | ||
|
|
6b8605a83e | ||
|
|
bef42dfd1b | ||
|
|
c29c3a3fe2 | ||
|
|
2a608e92f4 | ||
|
|
90ebabab01 | ||
|
|
5f2dd4bf27 | ||
|
|
2e2fac2348 | ||
|
|
5b1f09e79a | ||
|
|
fb0fe55c6d | ||
|
|
84ae3c1ce0 | ||
|
|
2ee1dcbfcb | ||
|
|
14f026c529 | ||
|
|
30a71a1ad4 | ||
|
|
a2515f13a7 | ||
|
|
5d6072e11e | ||
|
|
0e588f74f7 | ||
|
|
13a350b3bf | ||
|
|
36dbbe0de7 | ||
|
|
eb251c45b5 | ||
|
|
df0cc6a34f | ||
|
|
3b018b6976 | ||
|
|
7c066b5fea | ||
|
|
095281fca5 | ||
|
|
8453408d24 | ||
|
|
f5368d73ea | ||
|
|
bd92c25a09 | ||
|
|
05024e02bf | ||
|
|
56e122d85f | ||
|
|
d7a26ca181 | ||
|
|
3fcb901f8c | ||
|
|
faea99e837 | ||
|
|
05c4f5603b | ||
|
|
60fa213862 | ||
|
|
b67c403447 | ||
|
|
21e2693568 | ||
|
|
8d8a5dab59 | ||
|
|
185a947ba1 | ||
|
|
8622f8bc5c | ||
|
|
70d722063e | ||
|
|
e9ab42bcab | ||
|
|
6d9944844a | ||
|
|
11c52636c3 | ||
|
|
bf455baeb5 | ||
|
|
986e554fa5 | ||
|
|
f6ae3f7724 | ||
|
|
310863900f | ||
|
|
7462317dcb | ||
|
|
32a2694b05 | ||
|
|
421ee8cb5f | ||
|
|
693e75b19a | ||
|
|
bdbe9550ba | ||
|
|
c1653f21b1 | ||
|
|
f251129c11 | ||
|
|
5acf9f1d7a | ||
|
|
88dd4a99a6 | ||
|
|
e30c9735dc | ||
|
|
f237f64d84 | ||
|
|
fd11975ffb | ||
|
|
a37c75aec8 | ||
|
|
536fba2145 | ||
|
|
a7b5acbd14 | ||
|
|
a305ecc43b | ||
|
|
2acfd64f7f | ||
|
|
324b3401f7 | ||
|
|
694621c71f | ||
|
|
9df1d35003 | ||
|
|
a86a86df3d | ||
|
|
626950e52e | ||
|
|
4be8e3be37 | ||
|
|
98b16b211a | ||
|
|
4f042f7225 | ||
|
|
54fdeaca14 | ||
|
|
1182f8f275 | ||
|
|
90248b6742 | ||
|
|
8b2d24a9f5 | ||
|
|
c8f399da57 | ||
|
|
d787abb5df | ||
|
|
81e33e7a26 | ||
|
|
2a33c218b1 | ||
|
|
fbdeb700d0 | ||
|
|
26ef64ebf9 | ||
|
|
43994d1c93 | ||
|
|
8fead8387d | ||
|
|
6419295a00 | ||
|
|
c29cef5f97 | ||
|
|
b678ce8967 | ||
|
|
289d771cc9 | ||
|
|
78ac1a2865 | ||
|
|
8a96b37df3 | ||
|
|
e1b153915e | ||
|
|
ec552046eb | ||
|
|
697e96c35f | ||
|
|
34d00efe0a | ||
|
|
d6c8315463 | ||
|
|
7202d6ae41 | ||
|
|
2fe932a50f | ||
|
|
1c02ebe1d3 | ||
|
|
4cd7a2b5e8 | ||
|
|
b8d5b3badc | ||
|
|
375a9d9fe2 | ||
|
|
0c05f88c5e | ||
|
|
22a324764d | ||
|
|
62e28f655e | ||
|
|
d4ea0a27bb | ||
|
|
401fbed1d5 | ||
|
|
9bfb2ddd3b | ||
|
|
ea2522a45d | ||
|
|
fa67b36d20 | ||
|
|
972f5c7147 | ||
|
|
c477070772 | ||
|
|
2c1b901728 | ||
|
|
80ef9ca6a7 | ||
|
|
c744f8740b | ||
|
|
859f921361 | ||
|
|
6acf9d048e | ||
|
|
cf18b30be9 | ||
|
|
eec0218b1d | ||
|
|
526ba0a684 | ||
|
|
23b6ff1b5e | ||
|
|
00c6863e54 | ||
|
|
4863035a05 | ||
|
|
a30162ea63 | ||
|
|
24eaa97fde | ||
|
|
767b152423 | ||
|
|
10e7669f70 | ||
|
|
76c4f2d225 | ||
|
|
3a851e911f | ||
|
|
2912d637b5 | ||
|
|
5c070ccf23 | ||
|
|
b979067215 | ||
|
|
4440b459e5 | ||
|
|
09b8046337 | ||
|
|
0d67335775 | ||
|
|
80092098e0 | ||
|
|
feab03e049 | ||
|
|
9713da222a |
No files matched your search
@@ -38,6 +38,6 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
label: Diagnostics
|
||||
description: Drag the FramePort-diag-*.zip here (max 25 MB)
|
||||
description: "Drag the FramePort-diag-*.zip here (max 25 MB). If the box won't take it, submit the report and attach the zip in a comment."
|
||||
validations:
|
||||
required: true
|
||||
+6
-1
@@ -9,7 +9,7 @@ parity-report.md
|
||||
*.keystore
|
||||
*.apk
|
||||
.pytest_cache/
|
||||
native/.cache/
|
||||
native/.cache
|
||||
native/**/build/
|
||||
# per-machine Claude Code settings (contain home paths)
|
||||
.claude/settings.local.json
|
||||
@@ -30,3 +30,8 @@ site/src/assets/media/
|
||||
site/media-hq/
|
||||
site/public/s
|
||||
site/public/setup.sh
|
||||
|
||||
# Impeccable design-tool files (kept local, not in the repo)
|
||||
/PRODUCT.md
|
||||
/DESIGN.md
|
||||
/.impeccable/
|
||||
@@ -69,6 +69,10 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
Live Frame data comes from one app-owned `app.monitor_hub` (`frame/monitor_hub.py`): subscribers name modules +
|
||||
interval, the hub runs one agent `_monitor` stream (union of modules, fastest interval; per-module collection
|
||||
needs agent v73), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries an "error".
|
||||
Games in a sample (agent v81): FramePort's own (launch.sh / container / Proton folder), any other game Steam's
|
||||
reaper runs (`steam:<appid>`, title from the appmanifest, e.g. Job Simulator from Steam's store; not SteamVR, not
|
||||
devkit entries; End game = Steam's Exit game), overlay apps flagged `overlay` and listed last; newest first. The
|
||||
UI takes `frame/monitor.main_game(sample)` (never an overlay app alone). The agent's own workers never count.
|
||||
Subscribers: "monitor" (the Monitor tab, everything, only while shown) and "card" = the sidebar's live Frame card
|
||||
(`ui/frame_card.py` helpers: battery ring, "now playing" row with fps + 2-min sparkline → click opens Monitor;
|
||||
games + battery every 5 s, paused while a job's stage is "Upload…", only while connected and setting
|
||||
@@ -308,6 +312,66 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
- `native/` — sources of the prebuilt binaries in `artifacts/` (adapter, VrApi bridge patches, GL shim, stubs).
|
||||
`native/build.py` rebuilds them with NDK r27c (downloaded on demand into `native/.cache`, git-ignored; uses
|
||||
`-ffile-prefix-map` so no local paths get embedded; zip symlinks are restored as copies). Users never need the NDK.
|
||||
`native/a32translator/` (not an artifact, not shipped) = `a32run`, an AArch32 → AArch64 user-mode translator
|
||||
(interpreter + JIT, ARM/Thumb-2/VFP/NEON) for running 32-bit ARM Linux programs on the Frame; its README has the
|
||||
design, the qemu-arm differential fuzzer and program tests. `native/a32translator/bridge/` = the in-APK Android
|
||||
bridge (32-bit JNI/NativeActivity libraries in a 64-bit app: stub + liba32bridge.so + guest runtime liba32rt.so;
|
||||
harness with a fake JVM; test APK org.frameport.a32test passes in Lepton; GLES/EGL, OpenSL ES via AAudio, VrApi 1.x
|
||||
-> the 64-bit VrApi bridge, controller input as Gear VR touchpad/back events; `bridge/convert.py` converts an APK).
|
||||
First game: AFFECTED The Manor (Gear VR, Unity 5.6 + Mono) runs in the headset with picture and sound
|
||||
(2026-10-10); Spunky Dungeon (Unity 2019.2, OVRPlugin 1.46 on VrApi 1.31, Platform SDK) plays at 72 fps.
|
||||
In FramePort: patch `frame.a32_bridge` (`patches/frame/a32_bridge.py`: `convert()` is shared with the dev script
|
||||
`bridge/convert.py`; artifacts `liba32bridge.so`/`liba32rt.so`/`liba32stub.so` from `native/build.py --only a32`)
|
||||
is suggested for every 32-bit-only APK; the recipe then has `overport: False` (OVRPort is 64-bit only; the patch
|
||||
ships OVRPort's loader chain + its 32-bit platform dispatcher itself, `overport.ensure_runtime_lib`), only
|
||||
frame.a32_bridge/adapter/launcher of the frame patches, and `device.text_input_window` for Unity (`engine.a32_recipe`,
|
||||
also run by `pipeline.set_recipe` + builds, so a bridge turned on by hand drops OVRPort & the 64-bit fixes; which
|
||||
patches work there = `base.works_on_a32` (`Patch.a32`; overport/frame off unless `a32 = True`, adapter settings in
|
||||
`settings.XRSHIM_SETTINGS` off); `build.check_a32` refuses anything else, `engine.warnings(recipe, analysis)` says
|
||||
so; a catalog entry for a 32-bit game that neither names nor removes the bridge predates it: bridge kept, status
|
||||
unknown; parity gives bridge builds without a bridge known-good "new build (no baseline)"; library migration
|
||||
`a32_bridge_on` turned the bridge on once in every 32-bit recipe, user ones too, unless the catalog's frame_remove
|
||||
names it).
|
||||
Analysis `extra.headset` (ANALYSIS_VERSION 15): gearvr / go / quest → "Gear VR" / "Oculus Go" platform + Steam tag.
|
||||
Catalog: `headset:` field; com.fallen.affected, net.mobilemischief.spunkydungeon, com.ccpgames.gunjack (works, 2026-10-12). Learned: Unity 2019 contexts
|
||||
have no EGLConfig (EGL_KHR_no_config_context: the VrApi bridge takes the window's or an RGBA8 ES3 config); Unity's
|
||||
floor origin = vrapi_SetTrackingSpace(1) (the VrApi bridge now honours LOCAL_FLOOR/STAGE); mapped GL buffers are
|
||||
per buffer object (a per-target guest copy leaked ~10 MB/s and garbled vertices); the guest runtime's own scudo
|
||||
names its maps `[anon:scudo:*]` below 4 GiB (watch them for guest leaks). The Go controller = the right Frame
|
||||
controller (stick = touchpad touch, sideways > 0.6 or press = click). Gunjack (CCP, UE 4.10 NativeActivity, Gear VR,
|
||||
2026-10-10): needed the native_app_glue imports (ALooper/AInputQueue/AConfiguration/AAsset*, imports.txt), an arm64
|
||||
libvrapi.so for Java's System.loadLibrary("vrapi") and MOUNTED under OVP_HEADLESS_TEST; it then loads its map and
|
||||
quit on purpose: AndroidMain compares AndroidThunkCpp_GetSignature() with an obfuscated copy. **Works since
|
||||
2026-10-11 (owner-verified in the headset):** GameActivity.AndroidThunkJava_GetSignature() = Base64.DEFAULT(SHA-1(
|
||||
signing certificate DER)) (jadx); `a32_bridge.original_signature` computes it from the original APK's
|
||||
META-INF/*.RSA|DSA|EC, `convert()` stores it as `assets/fp_a32_signature.txt` for libUE4.so games, the bridge loads
|
||||
it at activity creation (native_activity.c) and jni.c answers Call*Method on the method ID of
|
||||
`AndroidThunkJava_GetSignature()Ljava/lang/String;` with it. After loading, Unreal leaves VR mode and enters it again on
|
||||
the same GL context; a second OpenXR session there aborted in vrclient (CSharedSemaphoreLinux::WaitFromGPU from the GL
|
||||
binding's PreBeginFrame, also with OVP_HEADLESS_TEST) → the VrApi bridge keeps a running GLES session across
|
||||
LeaveVrMode and reuses it for an EnterVrMode with the same context (upstream-patches/runtime.cpp.patch).
|
||||
Debugging aids added: A32_BREAK=<guest addr>[,..] (stack of the first hits; guest libs load at fixed addresses, e.g.
|
||||
libUE4 0xf25d5000), A32_STRACE in bridge mode = path/exit calls to logcat, activity lifecycle + LeaveVrMode caller
|
||||
logs. Lepton's logcat mirror often dies for these games: read `podman exec <container> logcat` instead. Gunjack input
|
||||
(2026-10-11): it imports no vrapi input functions and reads the Gear VR touchpad/back as AInputEvents from its
|
||||
AInputQueue (native_app_glue), so dispatchKeyEvent never reached it (no control at all) → input.c queues synthetic
|
||||
events there (ALooper_poll* return the queue's ident while one waits; AInputQueue_getEvent/finishEvent and the
|
||||
AInputEvent/AKeyEvent/AMotionEvent getters are hand-written "!" imports that know them; touch positions scaled to the
|
||||
window size). Same mapping as AFFECTED: trigger = hold touchpad, A = tap, right stick = swipe, B = back; for NativeActivity games the stick's up/down = swipe forward/back (touchpad X: Gunjack's OnFlick = reload at ~0°, special weapon at ~180°). Not yet
|
||||
headset-verified. Quest builds on the bridge (2026-10-13, Sports Scramble = a 32-bit Santa Cruz build,
|
||||
UE 4.21 + OVRPlugin on VrApi 1.21): manifest requires `android.hardware.vr.headtracking` version 1
|
||||
(`detect.requires_6dof`, headset "quest", ANALYSIS_VERSION 16) → convert writes `assets/fp_a32_headset.txt`
|
||||
= "quest" → vrapi.c `quest_touch` (read in vrapi_Initialize via the activity's AssetManager; A32_TOUCH=1/0
|
||||
overrides) passes the 64-bit bridge's Touch controllers through (enumerate/caps/state/tracking/haptics, struct
|
||||
tails per VrApi minor) and starts no synthetic input. It also needed AAsset_getBuffer (guest copy, freed in
|
||||
AAsset_close), AKeyEvent_getFlags and the other AConfiguration getters. Headset (owner, 2026-10-13): plays,
|
||||
both controllers work. Recenter (holding the Frame's menu button) turned VrApi games ~90-120 degrees aside: the bridge's app space is LOCAL + the start yaw and the runtime moves LOCAL to the head → the bridge now takes the origin from the head again on XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING (LOCAL) and on vrapi_RecenterPose (owner-verified with Gunjack, 2026-10-13). Next: the other 32-bit catalog games (Journey of the Gods, Shadow Point: still "unsupported" until
|
||||
tested), translator speed. Profiled on the Frame
|
||||
(2026-10-12, `A32_PERFMAP` + perf): FPCR switches are negligible (8 of 86k samples); the cost was the run loop
|
||||
(a mutex + hash-chain walk on every dispatch, a contended counter, inline-lookup slots never refilled: Gunjack
|
||||
~45 % of its CPU) and `jit_invalidate` scanning every block on each Mono `cacheflush` (72 % of Spunky Dungeon's
|
||||
main thread). Now lock-free lookups, ltab refill, per-thread flags, per-page block lists: Gunjack's dispatcher
|
||||
23.6 % -> 1.6 %, CPU in the same phase about -55 %, still 72 fps.
|
||||
- `scripts/` — `package.py` (flet build/pack), `eval_heuristics.py` (score heuristics), `ui_smoke.py` (GUI screenshots).
|
||||
- `docs/` — PLAYBOOK, FRAME_RUNTIME, ARCHITECTURE, INSTALL (end users), parity reports (offline + device).
|
||||
|
||||
@@ -323,8 +387,17 @@ Games tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>); on-device chec
|
||||
native layer test: `FRAMEPORT_NATIVE_TESTS=1 pytest -m native` (compiles with the NDK, ~2 min on NTFS).
|
||||
Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattributes`).
|
||||
- `FRAMEPORT_HOME=<dir>` isolates all app data (tests use it); `FRAMEPORT_JAVA/_OVERPORT_JAR/_APKSIGNER_JAR` override
|
||||
managed tools. Real app data (WSL): `~/.local/share/frameport` (tools, overport workspace + game keystores, library,
|
||||
artwork, frames.json, the app's SSH key which the dev Frame authorizes).
|
||||
managed tools. **The owner's real app data is the Windows copy's: `%LOCALAPPDATA%\FramePort`** (= `/mnt/c/Users/
|
||||
<user>/AppData/Local/FramePort`; tools, overport workspace + game keystores, library, artwork, frames.json, the app's
|
||||
SSH key which the dev Frame authorizes; moved from WSL's `~/.local/share/frameport` on 2026-10-11, which is now only
|
||||
a stale backup). The desktop shortcut "FramePort (dev)" runs the source checkout natively on Windows
|
||||
(`%LOCALAPPDATA%\FramePort-dev\launch-frameport-dev.vbs`: venv `FramePort-dev\venv` via `UV_PROJECT_ENVIRONMENT`,
|
||||
`uv sync --frozen`, log `FramePort\logs\dev-gui.log`). **WSL runs on that data need `FRAMEPORT_WINDOWS_DATA=1`**
|
||||
(set in `.claude/settings.local.json`; `paths.shares_windows_data`): the data folder becomes the Windows one, saved
|
||||
paths from the other side are translated on load (`paths.localize`: /mnt/d/X <-> D:\X in library.json, tools.json,
|
||||
PC deployment records), Java/Cpp2IL keep one copy per OS (tools.json `java-<os>-<arch>`), and the link handler isn't
|
||||
registered (it would point the Windows copy's links at WSL). Don't run a WSL GUI and the Windows GUI at once
|
||||
(the gui.lock flock/msvcrt locks don't see each other; only the gui.alive heartbeat does).
|
||||
- Device checks: `frameport test <pkg>` / `frameport parity-device --results <parity.json> --baseline <launch.txt>
|
||||
[--test-only]`; the pre-FramePort baseline is `PATCHED/_known-good-2026-09-28/_frame-state/baseline-launch.txt`.
|
||||
- Docs screenshots + videos (docs/SHOWCASE.md): never by hand. `uv run python scripts/showcase/render_docs.py`
|
||||
@@ -352,6 +425,11 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
`T.px(n)`, Material defaults through the theme's text_theme; never hardcode a bare pixel number in ui/.
|
||||
`scripts/ui_smoke.py --scale 1.5 --viewport 2560x1440` renders it. Flet draws a grey box for invalid layouts (e.g.
|
||||
an `expand` child in a `wrap=True` Row).
|
||||
- One GUI per data folder (`core/instance.py`, 2026-10-11): `<data>/gui.lock` (flock/msvcrt, pid inside); a second
|
||||
start asks the open window to come to the front (`gui.focus`, read by `_watch_links`) and exits; a lock held with no
|
||||
window (no `gui.alive`) and older than 30 s is a leftover FramePort process and is ended. `main()` ends the process
|
||||
(`os._exit`) once `ft.run` returns: background threads (re-analysis, session triage) used to keep a windowless
|
||||
process running. Session triage shows one pop-up for all games of a burst (`_announce_sessions`).
|
||||
- Stopping the GUI: `pkill -f` patterns match your own shell — use `pgrep -f "[b]in/frameport-gui|[f]let-desktop-light"`.
|
||||
|
||||
## Round-2 polish (2026-10-02)
|
||||
@@ -375,7 +453,7 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
|
||||
## Hard-won facts (don't re-learn these)
|
||||
**Frame runtime (SteamOS 0.3.0, build 20260922):**
|
||||
- No AArch32: 32-bit-only APKs fail with `INSTALL_FAILED_NO_MATCHING_ABIS`. Unfixable; point to Rift + Revive.
|
||||
- No AArch32: 32-bit-only APKs fail with `INSTALL_FAILED_NO_MATCHING_ABIS` → `frame.a32_bridge` (the a32 translator inside the app, see native/a32translator below).
|
||||
- GLES swapchains: only `GL_SRGB8_ALPHA8`/`GL_SRGB8` (35907/35905), no MSAA. Vulkan: format 43 (sRGB) but not 37 (UNORM).
|
||||
- Missing: XR_FB_passthrough (emulate via ALPHA_BLEND), XR_FB_scene/spatial entities (emulated room from STAGE bounds),
|
||||
XR_KHR_composition_layer_equirect2/cylinder, XR_FB_composition_layer_image_layout (flip emulated by Vulkan blit).
|
||||
@@ -470,6 +548,61 @@ maps a symlink to the already-loaded module) gets past it to **-3021 = ovrError_
|
||||
an Oculus-signed runtime. Next step (not done): make the runtime check pass without Detours, e.g. an injected helper
|
||||
that patches the game's import table (IAT) for LoadLibrary*/WinVerifyTrust, or a prefix-level override. This is
|
||||
runtime interop (what Revive does), not the Platform SDK licence check (which FramePort never touches).
|
||||
**Rift via x86_64 Wine under FEX (research, 2026-10-08, headless on the dev Frame, SteamOS 0.4.5; no product code):**
|
||||
why Revive failed is confirmed upstream: x64 Detours returns ERROR_NOT_SUPPORTED on ARM64EC targets (microsoft/Detours
|
||||
PR #388; only an ARM64EC build of Detours can hook them). A fully x86_64 Wine avoids it: GE-Proton11-7 **x86_64** runs
|
||||
under the FEX tool (`fex-compat-tool run -- <x86 binary>`; RootFS /usr/share/guestos/fex-mesa, no FreeType there)
|
||||
— prefix boot 35 s, kernelbase.dll is x86-64. Proton's script needs help without the x86_64 SLR (app 4183110, not
|
||||
installed): its Python runs natively (the RootFS has none), so `files/bin/wine{,server}` and the two direct
|
||||
`wine-preloader` argv calls must go through a FEX wrapper that also unsets `FEX_APP_CONFIG(_LOCATION)` (Proton sets
|
||||
them on aarch64 for its own ARM64EC FEX → "RootFS path set to ''"); GE's game drive needs STEAM_COMPAT_INSTALL_PATH
|
||||
unset. SteamVR now ships `bin/linux64/vrclient.so` + `bin/vrclient.so` (i386) + `vrclient_x64.dll` (Oct 6 build; not
|
||||
there on 09-30): Proton's x86_64 vrclient bridge reaches the arm64 vrserver. **Rick and Morty ran as the scene app**
|
||||
("OpenVR initialized!", controller tracking, 9176 presents / 6 dropped, CPU 11.6 / GPU 17.9 ms, target 72).
|
||||
**ReviveInjector: "Succesfully injected!"**, LibRevive64.dll loaded next to OVRPlugin.dll in Lies Beneath, which then
|
||||
died on 0xc06d007e = delay-loaded `libovrplatform64_1.dll` (Platform SDK) before connecting to SteamVR — so Revive's
|
||||
signature hook and Revive→SteamVR are still unproven. Every Rift game in the library except Rick and Morty uses the
|
||||
Platform SDK → the Meta (Link) app in the prefix, logged in with the user's own account, is the real blocker; the
|
||||
only public attempt (github.com/michauMiau/oculus-wine-linux, 2026-10) never got OculusSetup.exe (32-bit .NET)
|
||||
past its HTTPS config fetch. Scratch files on the Frame: `~/frameport-rifttest/` (scripts t1-t4.sh).
|
||||
Meta runtime in the same Wine (2026-10-09): Platform SDK games reach OVRServer's IPC on the Frame. The apparent
|
||||
intermittent handshake hang was **OVRLibrarian.exe** (.NET, Wine Mono): under FEX it finishes its work but never exits,
|
||||
its AppTracker entry stays, and the runtime then tracks no later client (Meta app, game), so the game waits forever
|
||||
for the reply event the runtime only sets after identifying it (found with wineserver `+server` traces PC vs Frame).
|
||||
Ending OVRLibrarian before the game → game tracked, "Missing entitlement" exactly as on the PC (needs a login).
|
||||
Logged in (2026-10-09, owner's own account, browser sign-in; the `oculus://` answer had to be forwarded into the Wine
|
||||
app's `\\.\pipe\oculus`), the runtime also needed a WinRT `Windows.Devices.WiFi.WiFiAdapter` stand-in (Air Link code
|
||||
fail-fasts without it), Wine's DeviceWatcher add/remove_Updated/Removed implemented, and the C: volume name
|
||||
(`\\?\Volume{...0043}`) linked in dosdevices. Copying the logged-in `sessions/` + CoreData to the Frame prefix kept the
|
||||
login. **Oculus First Contact then ran in VR on the Frame** (Revive in OpenVR mode; `/openxr` failed in wineopenxr):
|
||||
scene app at 72 Hz target, but 92 % of frames "timed out" (game CPU-bound under FEX) → stutter/flicker/lag.
|
||||
Performance (2026-10-10, headless with patched Revive visibility + SteamVR `pauseCompositorOnStandby=false`, numbers from
|
||||
`IVRCompositor::GetFrameTimings`): the frame loop waited on sync round trips through the FEX-run x86_64 wineserver.
|
||||
`native/fexwine/`: a **native aarch64 wineserver** built from GE's exact source (protocol 938) + **ntsync** (works once
|
||||
OVRLibrarian is ended) took hitches 17 → 0-4 per 20 s. The rest were Unreal's large-block allocator committing,
|
||||
filling (one `rep stos`) and freeing 514 MB every 1-8 s: ~130k 4 KB page faults each under FEX. `fp_mem.so`
|
||||
(`FP_BIGCACHE_MB=256`, guest preload via `FEX_ENV=LD_PRELOAD=…`: fex-compat-tool deletes LD_PRELOAD) keeps that block
|
||||
→ 0 hitches, steady 36 fps + motion smoothing, ~5.5 % timed out. 72 fps would need ~30 % less GPU (game ~55 % busy
|
||||
at 36 fps); THP (fragmentation), MSAA off, Turnip gmem don't help. Also needed: Unreal's
|
||||
`Slate.DeferWindowsMessageProcessing` = 0 (exe patch; the default deadlocks Wine's IME window ~1/3 of starts).
|
||||
Headset (owner, 2026-10-10): with motion smoothing off for the game (SteamVR per-app `motionSmoothingOverride=2`:
|
||||
smoothing cost ~15 % GPU) "much much better", 95 % of frames new at 72 Hz; MSAA off (DXVK `d3d11.disableMsaa`, -25 %
|
||||
GPU) helped but the owner misses smooth edges → the game's MSAA CVar is `r.MobileMSAA` (Oculus' clustered-forward
|
||||
branch; no r.MSAACount in the exe), 2x untested. Headless tests need the headset worn once per boot (else "IMU
|
||||
fallback" and the game waits at startup forever).
|
||||
**Experimental mode on branch `rift-on-frame` (the owner's single linkable branch for this work):** CLI only,
|
||||
`FRAMEPORT_EXPERIMENTAL_FEX=1 frameport fex setup | import-login <signed-in PC prefix> [--app <meta name>] | install
|
||||
[<meta name>] [--msaa N] [--force-unverified] | status` (hidden group). Generic per game: exe + arguments from Meta's
|
||||
manifest (`Software/Manifests/<app>.json` launchFile/launchParameters), the Unreal IME-deadlock fix found in the binaries
|
||||
(`find_ime_patch`: the CVar's UTF-16 name → its `lea` xref → `lea r8` member/global → `mov [reg+disp],1` / int 1;
|
||||
also modular ApplicationCore/Slate DLLs), `static_checks` (engine, imported graphics DLLs: monolithic Unreal imports
|
||||
every RHI, so it can't tell which runs). `agent/fexrift.py` `GAMES` = verified games + per-game opt-ins only (fp_mem's
|
||||
unzeroed big-block cache, `msaa_ini`); others need `--force-unverified`. PC `src/frameport/fexrift.py` uploads `agent/fexrift.py` + `artifacts/fexrift`
|
||||
(sources native/fexwine) + Revive 3.2.0 with 6 byte patches (refused for other builds); the Frame helper downloads
|
||||
GE-Proton11-7 x86_64 (SHA-512), x86 GnuTLS (Arch archive), installs Meta's runtime (Meta's signature checks kept) and
|
||||
writes the game's launcher. Agent v79 `fex_prepare/status/setup/import_login/install`, kind `frame_fex` (uninstall
|
||||
keeps the shared prefix; launch tests refused). Everything under `~/.local/share/frameport/fexrift` (purge removes it,
|
||||
login tokens included). Not yet run end to end on the device; the login still comes from a PC prefix signed in by hand.
|
||||
Uninstall (Quest, saves kept) used to leave `deployment.json` → still "installed"; fixed (agent v16).
|
||||
|
||||
**Discovery/network:** Developer-Mode SteamOS devices announce `_steamos-devkit._tcp` (TXT `login=steamos`) — use it;
|
||||
@@ -625,7 +758,9 @@ reaches its OVRInput cleanly (probe: both Touch connected 0x63, individual butto
|
||||
"outside of frame bounds" warnings stay (~2/frame) and are harmless.
|
||||
Steam library (GitHub #4/#21/#27, agent v38): shortcuts go to every Steam account (signed-in first; MostRecent isn't
|
||||
always the account on the Frame); stop_steam also waits for Steam's helpers; after the restart the agent re-reads
|
||||
shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lost`), else reports it. 0.6.3's Play
|
||||
shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lost`); lost again (agent v89): Steam
|
||||
ignores FramePort's entries on that Frame, so `add_devkit_entry` adds the devkit entry instead (`added` gets `via:
|
||||
devkit`) and only reports an error if that fails too. 0.6.3's Play
|
||||
auto-repair (`_add_then_play`) crashed (its job got a Job, not a reporter). Uploads resume on a transient OSError.
|
||||
Lepton's Android 11 has no clipboard service (134 services; checked with `podman exec … service check clipboard`):
|
||||
SDL/LÖVE apps crashed at start → `frame.sdl_clipboard` (`apk/dex.py`: in-place dex edit, nops one invoke, fixes
|
||||
@@ -723,7 +858,23 @@ own context and saved as `files/fb_snap_0-7.ppm` (quarter size) — headless lau
|
||||
shows what the game draws. Vader: Lucasfilm logo (an OBB mp4: video works), then its loading card (portrait + segmented
|
||||
bar) that never advances; the async loader thread sleeps and OBB reads stop (~168 MB of a 2.7 GB pak).
|
||||
**SteamVR overlay apps (agent v75, 2026-10-10):** host OpenVR overlays (VRApplication_Overlay) are composited over Lepton (Quest) games on the Frame (probe checker seen over 4XVR in the headset view, `native/vroverlay_probe`); Windows ones register through Proton's vrclient too (Temporal Reality's Windows build created its overlays; fpsVR never reaches OpenVR under Proton). Detection `analysis/vroverlay.py` (bundled `.vrmanifest` with `is_dashboard_overlay`, else OpenVR + IVROverlay + self-registration markers, engine Other; Rift fingerprint `RIFT_ANALYSIS`), patch `pcvr.vr_overlay` (param autostart), Linux entry fields `vr_overlay`/`vr_overlay_autostart` (game page switch); agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay` (IVRApplications via ctypes, live; manifest `<anchor>/frameport-overlay.vrmanifest`, the app's own key, binary = launch.sh). **SteamVR on the Frame reads only `binary_path_linux_arm`** (binary_path_linux alone is skipped). Overlay apps: no launch test, no Linux Steam-parent watchdog (`FRAMEPORT_OVERLAY`), not a "running game". A Steam overlay-app shortcut and a game shortcut run together. SteamVR's SetApplicationAutoLaunch didn't persist on the dev Frame (the watch never started by itself) → **agent v76: FramePort's own autostart**: the deployment's `overlay.autostart` is the source of truth; `ensure_host_fixes` (`ensure_overlay_service`) keeps the user service `frameport-vr-overlays.service` (`_vr_overlay_watch`, 5 s polls of the known vrserver's /proc stat) only while an overlay app has autostart on (register/unregister, uninstall, purge and the kill switch `~/.local/share/frameport/vr-overlays.disabled` / `FRAMEPORT_NO_OVERLAY_AUTOSTART` update it); a new vrserver (pid + start time, kept in `~/.cache/frameport-vr-overlays.json` across agent updates) → child `_vr_overlay_round`: wait for IVRApplications, 15 s grace, `launch_vr_overlay` for apps not running (folder process or GetApplicationProcessId); log `~/.local/share/frameport/vr-overlays.log`. Dev Frame: adopted the running vrserver, skipped the already running watch; a start after a real SteamVR start/boot is not yet seen. Details: docs/FRAME_RUNTIME.md "Overlay apps".
|
||||
**Lepton's logcat mirror dies (agent v67, 2026-10-09):** occasionally launch.log ends with `logcat: Unexpected EOF!` right after the game starts (about 1 launch in 50, Lepton 2.8.14 and 3.0.5): no dashboard auto-hide, no launch-test result. launch.sh's `_logcat_keeper` then reads `podman exec lepton-steamlaunch-<appid> logcat` itself into launch.log (up to 5 restarts while the game runs).
|
||||
**Vader's leak solved / AC Nexus flips (2026-10-14):** Vader Immortal leaves GL errors pending on its context; SteamVR's GL interop (`openxr_graphics_gl_openvr.cpp`, `xrclient_<pkg>.txt` in Steam's logs: `SXR_GL_CHECK: error:0x502 ... glGenTextures`) takes the stale error as its own failure once, and from then on `CVRCompositorSharedTextures::BeginCommandBuffer` allocates a command buffer every frame (perf page-fault stacks; ~430 renderD128 mappings + 22 MB/s). Adapter `gl_error_clear` (GLES, catalog on for Vader ep1-3) clears them before xrCreateSwapchain/xrBeginFrame/xrEndFrame: flat memory, no SXR_GL_CHECK (headless). The per-game `xrclient_<pkg>.txt` logs are worth reading for runtime errors. The Frame's runtime caps swapchains per session (XR_ERROR_LIMIT_REACHED from ~15; AC Nexus asks for more): flip emulation now turns quads upright in place (scratch VkImage, once per acquire; transfer usage added to single-layer colour swapchains) instead of a shadow swapchain per quad, which ran into the cap (#138). Headless sessions don't draw AC Nexus's panels (adapter `flip_dump` saves before/after images of flipped quads, for headset runs).
|
||||
**Unreal's self-restart (agent v90, 2026-10-13):** UE 4.21+ restarts the app once after its first-run shader
|
||||
precompile ("optimizing" screen; `r.ProgramBinaryCache.RestartAndroidAfterPrecompile`): it starts its own LAUNCHER
|
||||
activity and calls System.exit(0); Lepton ends the container with the first process (the restarted one with it).
|
||||
Skipping `AndroidThunkJava_RestartApplication` or the cvar = 0 (accepted via UE4CommandLine.txt `-ini:`) left Sports
|
||||
Scramble on that level for good. launch.sh (RESTART_BLOCK, `upgrade_launchers` adds it) starts Lepton once more when
|
||||
`_restart_requested` sees the app's own LAUNCHER start (system log, uid >= 10000) <= 5 s before its exit; first run
|
||||
kept as `launch-before-restart.log`; `FRAMEPORT_NO_RESTART=1` off. Owner-verified with Sports Scramble's first start (2026-10-13).
|
||||
**xrWaitFrame runs ahead (Journey of the Gods, 2026-10-14, owner-verified):** with real frames (headset on) the
|
||||
Frame's runtime sometimes returns from xrWaitFrame at once with a display time less than one refresh after the frame
|
||||
just submitted, and then rejects the second submission for that refresh (`VRCompositorError_AlreadySubmitted` in
|
||||
Steam's `logs/xrclient_<pkg>.txt`; FrameBridge pacing above the display rate). VrApi games that render faster than
|
||||
the display ran ahead (~120 submits/s at 90 Hz) and the scene slid and snapped back. The VrApi bridge re-waits when
|
||||
the new display time is under half a period after the last submitted one (`pacing 2s:` counters). Headless runs only
|
||||
submit empty frames and never show it. Lepton's logcat mirror also missed the whole verifying session (launch.log =
|
||||
Lepton's own lines only): the keeper now catches a silent mirror too (agent v91).
|
||||
**Lepton's logcat mirror dies (agent v67, 2026-10-09; v91 2026-10-14):** Lepton's mirror (`podman exec -it <container> logcat -v threadtime,color`) fails three ways: `logcat: Unexpected EOF!` right after the game starts, `Logcat read failure`, or running and writing nothing at all (Lepton 3.0.5: seen twice on 2026-10-14, once in the owner's session, once headless). launch.sh's `_logcat_keeper` reads `podman exec <container> logcat -v threadtime -T <epoch>` itself into launch.log after a failure line or 20 s without a logcat line (from the launcher's start when the mirror never wrote, else from just before its last line), restarts it from where it ended (≤5), stops it when Lepton's coloured lines come back, sets the container's log buffer to 16 MiB (`logcat -G`), and logs every decision to `<base>/logcat-keeper.log`. Checked on the dev Frame: a mirror killed mid-game (hand-over without a gap) and a silent mirror (the whole session recovered).
|
||||
**Double launch (agent v57, 2026-10-06):** a second Play while Lepton still boots (~10 s with nothing to see) made
|
||||
the second Lepton stop the first one's container ("Waiting for steamlaunch-<appid> (PID …) to exit", exit 137
|
||||
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
|
||||
@@ -790,6 +941,31 @@ index lines; no index (fp_spirv) = the old newest-by-mtime 4 MB. One JSON line (
|
||||
is read whole by `Frame.agent` (fine at that size); `_Writer.fit` drops the least recently used modules last, after
|
||||
cutting logs, to stay under the 24 MB zip limit.
|
||||
**Unreal OBB check (GitHub #159, 2026-10-10):** Epic's DownloaderActivity checks OBB name+size (OBBData), then with `bVerifyOBBOnStartUp=true` CRCs the whole OBB; its screens are invisible in Lepton (stuck at `Displayed …/.DownloaderActivity`, triage `unreal-obb-check-stuck`) → `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`, ANALYSIS_VERSION 11) flips the manifest boolean (`axml.set_meta_data_bool`); skips only the CRC pass. Host-tested only.
|
||||
**Issue round 2026-10-12 (agent v83):** Lepton pastes STEAM_COMPAT_DATA_PATH unquoted into `sh -c "ln -s …/internal/<pkg>
|
||||
/data/data/<pkg>"` (mounting.sh): a game on a card named "Steam Frame 512g" got no internal storage (no saves/prefs;
|
||||
Beyond Sandbox's EOS then looped `createKeychain→resetKeychain` forever, 1.85 GB log). Quest launchers with an unsafe
|
||||
path now `ln -sfn <base> ~/.local/share/frameport/lepton-paths/<pkg>` at every start and give Lepton that path
|
||||
(`app_dir_lines`/`set_app_dir`/`launcher_base`; upgrade_launchers converts; deployment.json keeps the real base).
|
||||
Verified on the device with a spaced copy of Stremio (old: the `ln` error; new: /data/data link, shared_prefs
|
||||
writable). `frame.eos_keychain_loop` (opt-in, triage `eos-keychain-loop`) returns from EOSSDK.resetKeychain. Wander
|
||||
0.3570 (Unity 6.5, Firebase, Gecko) ran out of bionic's 128 pthread keys → `frame.unity_tls_key_pool` (Klownicle's
|
||||
approach, #198: libunity's 4 key imports renamed `fp_tkey_*` via `elf.rename_imports`, `libfp_tkeys.so` pools
|
||||
destructor-less keys behind one real key); device: past Firestore, 72 fps. Cpp2IL releases can't read metadata v107
|
||||
(Unity 6.5) → `tools/cpp2il.py` falls back to the development CI build (nightly.link) on that error. Vermillion's canvas
|
||||
is an OVROverlay quad (drawn above the hand) → `frame.vermillion_scene_canvas` (PaintableMesh getters → false;
|
||||
installed build checked, 72 fps headless; headset not yet). Linux apps: `desktop_icon` switch, a deleted desktop copy
|
||||
stays deleted (#212). Findings: OLAR (#201) RHIThread null deref in libUnreal (`ldr x8,[x19,#792]`, x19=0) not fixed by
|
||||
hide_space_warp / vk_hide_fdm / no Valve layers; FNAF HW (#135) "freeze" = first-start GL program-binary-cache compile
|
||||
on Zink (threads busy in zink/disk cache), not a hang; VR4 (#199) reporter's patch.203 OBB truncated. Upload benchmark
|
||||
(`scripts/upload_bench.py`, #242): home Wi-Fi caps every method at ~11 MB/s; paramiko uses ~3x OpenSSH's CPU; Windows
|
||||
OpenSSH refuses FramePort's key file (ACL "bad permissions") until its permissions are tightened.
|
||||
**Meta user id (GitHub #182, 2026-10-11):** OVRPort's platform loader (runtimes 3.4.3-23204ea and -aa54c3f) answers
|
||||
`ovr_GetLoggedInUserID` (and two other callers) without a Meta account with a made-up id from a helper (`stp; mov x29,
|
||||
sp; bl random; …; mov/movk x9 = 0x53444835ec580000 (6e18); add x0, x8, x9; ldp; ret`), kept only in memory → a new id
|
||||
every start; Walkabout Mini Golf files saves under `Profiles/Oculus/<id>/` and lost all progress. `frame.ovr_stable_user`
|
||||
(default_on, detect = the game has libovrplatformloader.so) rewrites the helper to `mov x0, <6e18 + 32 bits of
|
||||
sha256(package, keystore)>; ret`: stable per game and per user (keystores are personal), so online games never see two
|
||||
FramePort users as one. Found in every PATCHED build checked. Host-tested only; upstream candidate.
|
||||
**Vivox API 31 (GitHub #101, 2026-10-09):** newer Vivox builds (Green Hell VR) call Android 12 AudioManager
|
||||
communication-device methods from `com.vivox.sdk.AudioChangeListener` with no SDK check → NoSuchMethodError on Lepton's
|
||||
Android 11. `frame.vivox_audio_route` (analysis `vivox_api31`, ANALYSIS_VERSION 4) makes every such method return at
|
||||
@@ -820,7 +996,8 @@ on the dev Frame) in `files/framebridge-vrapi-fov.txt` and answers with it at th
|
||||
r_3dwidth/height, r_width/height from the runtime eye size (FrameBridge logs `runtime recommended eye size`) × scale,
|
||||
written at finalize, set_settings and by launch.sh before Lepton starts (`_config_sync`). Headless runs at scale 1.7
|
||||
end in `zink: DEVICE LOST` right after the bridge's 30 s no-tracking deadline tears the session down (gpu fault at the
|
||||
teardown; not at scale 1.0): a headless-only artifact, the headset never hits that deadline.
|
||||
teardown; not at scale 1.0): a headless-only artifact, the headset never hits that deadline. Launch tests count an exit/crash after
|
||||
such a line as a pass (triage.yaml `headless_end`, finding `headless-end`; GitHub #57 "Failed to launch" on install).
|
||||
**Session triage (agent v70, 2026-10-09):** launch tests never reach FOCUSED, so real play sessions are triaged
|
||||
too. list_installed gives each game `last_play` {start, end, test} from `<anchor>/plays.log` (launch tests write a
|
||||
`test <unix>` line first → test sessions are only marked); `session_log` returns the newest session's launch.log
|
||||
@@ -928,6 +1105,49 @@ Installed apps find the release themselves (self-update), so the notes are what
|
||||
- **The repo is public: never commit personal data** — the Frame's IP address, the Steam user id, the owner's email,
|
||||
local home paths (native builds use `-ffile-prefix-map`). History was rewritten once to remove them.
|
||||
|
||||
## GitHub triage (issues / discussions / PRs)
|
||||
GitHub identity + credentials are maintainer-local (`CLAUDE.local.md`): check `gh api user --jq .login` before any
|
||||
GitHub write. **The owner approves before posting, labelling or closing anything.** Reply style: "I"/neutral, never
|
||||
we/our/us; never mention agent version numbers or internal tool versions; show the owner every draft in full with a
|
||||
short recap of the thread so far; read **all** comments on a thread first (a later one often has the zip or the answer).
|
||||
- **Issue templates & labels** (`.github/ISSUE_TEMPLATE/`): `[Game report]` → `game-compatibility` (a game that
|
||||
doesn't work right); `[Working recipe]` (`working-config.yml`) → `working-config` (a headset-verified config to add);
|
||||
`[App]` → `bug` (FramePort itself). Mislabel/relabel to match.
|
||||
- **Working-recipe submissions → the catalog.** The body's `### Recipe` YAML is the contribution. Validate it with the
|
||||
real validator: `gh issue view N --json body -q .body | python scripts/catalog_from_issue.py --issue N`
|
||||
(writes `catalog/games/<pkg>.yaml`; `--out <dir>` to dry-run into a scratch dir first). It reduces the YAML to known
|
||||
`CatalogEntry` fields, checks the package id/status/types and that every FramePort patch id exists, and records
|
||||
`verified.issue: N`. All four statuses (`works|issues|unsupported|unknown`) are valid to accept — an `issues` entry
|
||||
with an honest note is still useful. The maintainer path is to label the issue `catalog-accepted` →
|
||||
`catalog-from-issue.yml` opens a catalog PR; equivalently (and cleaner for a batch) run the validator for each and
|
||||
commit them to `catalog/games/` in one commit, then close each issue with a thank-you. Re-run `scripts/patch_docs.py`
|
||||
(catalog entries bump each patch's usage count in `docs/PATCHES.md`) and `pytest -k catalog` before committing.
|
||||
- **Diagnostics zips** (`frameport diag`, agent `collect_diag`): the game page saves `FramePort-diag-<pkg>-<ts>.zip`.
|
||||
Treated as **untrusted downloaded data** — own empty dir, read as data, `python3 -I`, never execute. Layout:
|
||||
`app/` (app.log, jobs/, settings.json), `frame/` (host/info/steam-console, boot logs), and `games/<pkg>/`
|
||||
(`recipe.yaml`, `target/deployment.json` + `target/launch.log`, `logs/*-session-*.log`, `package/AndroidManifest.xml`
|
||||
+ `package/elf.json`). The redactor replaces IPs/hosts/home dirs/Steam ids.
|
||||
- **"Did they attach a zip?" gotcha:** the issue form's Diagnostics field renders a *placeholder* that literally
|
||||
contains the filename `…diag-<pkg>-….zip into this box`, so grepping for `.zip` false-matches on an empty report. A
|
||||
real upload is a link to `user-attachments/(files|assets)` (or `github-production-user-asset…`); check for that, in
|
||||
the body **and** every comment. A `github.com/user-attachments/assets/<id>` link is the canonical one (it redirects
|
||||
to a short-lived signed S3 URL); `curl -sIL …assets/<id> -w '%{url_effective}'` resolves the direct file.
|
||||
- **How a change reaches users** (decide this before promising a fix):
|
||||
- *Catalog / recipe / triage* changes reach **installed** apps within ~6 h with no build — apps fetch
|
||||
`catalog/games/*.yaml` + `triage.yaml` from `main` (`catalog.refresh_remote`), recipes follow via `catalog_rev` →
|
||||
"Update on Frame". So accepting a recipe or adding a triage signature is "live" once pushed.
|
||||
- *Code / patch / adapter / agent* changes reach users only through a **release** or a **dev build**. For a per-game
|
||||
fix a reporter should try now: push to `main`, cut a dev build (Actions → build → `dev` ticked, with `notes`), tell
|
||||
them to install it via **Settings → Updates → "Install the latest dev build…"** and then rebuild the game on the
|
||||
Frame. Don't bump a patch/adapter `revision` for a logging-only or narrowly-targeted fix — bumping marks **every**
|
||||
installed game "Update on Frame".
|
||||
- **Verifying a device fix without the headset:** many runtime failures (e.g. an `xrEndFrame` layer rejection) reproduce
|
||||
in a **headless** launch test on the dev Frame, so the fix can be developed and confirmed at the frame-acceptance
|
||||
level there; only the *visual* result needs the headset (hand the built dev build to the reporter, or the owner, for
|
||||
that). `frameport settings <pkg> <key>=<value>` toggles adapter settings (e.g. `layer_debug=1`) live, no rebuild.
|
||||
To reproduce a reporter's exact build, grab their APK from its upstream source (e.g. the linked GitHub release), scan
|
||||
→ build → install → test in an isolated `FRAMEPORT_HOME` (see "Dev commands" / the Frame-busy rules).
|
||||
|
||||
## Heuristics (games not in the catalog)
|
||||
Each patch's `detect()` suggests itself from the Analysis; `applies()` says whether it can matter at all (the UI/CLI
|
||||
hide non-applicable patches; enabled ones are always shown). Rules learned from the 34 games:
|
||||
|
||||
@@ -5,6 +5,7 @@ FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the followin
|
||||
## Included in this repository / the release bundles
|
||||
| Component | Where | License |
|
||||
|---|---|---|
|
||||
| LuaJIT (Lua engine for verified xLua pointer compatibility, github.com/LuaJIT/LuaJIT) | `artifacts/arm64-v8a/libfp_luajit.so`; pinned build in `native/luajit/build.py` | MIT (`native/luajit/COPYRIGHT`, included in bundles as `artifacts/licenses/LuaJIT.txt`) |
|
||||
| OVRPort VrApi → OpenXR adapter (`native/vrapi`, github.com/Android-XR-Bridge/OVRPort), with FramePort's changes | `native/vrapi-bridge/`, `artifacts/arm64-v8a/libvrapi.so` | GPL-3.0 (`native/vrapi-bridge/LICENSE.upstream`) |
|
||||
| OVRPort platform compatibility library (`native/platform`) | `native/platformcompat/`, `artifacts/arm64-v8a/libovrplatformcompat.so` | GPL-3.0; Meta Platform SDK headers under the Meta Platform Technologies SDK License (`native/platformcompat/licenses/OCULUS-PLATFORM-SDK.txt`) |
|
||||
| Khronos OpenXR SDK headers | used to build the native layers | Apache-2.0 / MIT (`native/vrapi-bridge/licenses/OPENXR-SDK.txt`) |
|
||||
|
||||
+1308
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,133 @@
|
||||
"""Data-driven FEX game compatibility. No entitlement/runtime-signature changes; stdlib only on the Frame."""
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import struct
|
||||
|
||||
STATUSES = {"verified", "likely", "untested", "blocked"}
|
||||
GRAPHICS = {"d3d11", "d3d9", "d3d12", "opengl", "vulkan"}
|
||||
|
||||
|
||||
def exe_facts(path):
|
||||
"""Read just the PE headers. A 32-bit client still needs GE's WoW64 and a working 32-bit VR/graphics bridge."""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
head = f.read(64)
|
||||
if head[:2] != b"MZ" or len(head) < 64:
|
||||
return {}
|
||||
f.seek(struct.unpack_from("<I", head, 0x3c)[0])
|
||||
pe = f.read(26)
|
||||
if len(pe) < 26 or pe[:4] != b"PE\0\0":
|
||||
return {}
|
||||
machine = struct.unpack_from("<H", pe, 4)[0]
|
||||
return {"exe_32bit": machine == 0x14c, "exe_machine": hex(machine)}
|
||||
except (OSError, struct.error):
|
||||
return {}
|
||||
|
||||
|
||||
def sha256(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def match_profile(games, app, path):
|
||||
"""Exact canonical name OR SHA-256 OR (exe basename AND byte size). No fuzzy title matches: different store
|
||||
builds can need different routes. Multiple matches are refused rather than silently picking one."""
|
||||
hits, digest = [], None
|
||||
for key, profile in games.items():
|
||||
match = profile.get("match", {"canonical_names": [key]})
|
||||
hit = app in match.get("canonical_names", [])
|
||||
if not hit and path and os.path.isfile(path):
|
||||
hit = any(os.path.basename(path).lower() == e["name"].lower() and os.path.getsize(path) == e["size"]
|
||||
for e in match.get("executables", []))
|
||||
if not hit and match.get("sha256"):
|
||||
digest = digest or sha256(path)
|
||||
hit = digest in match["sha256"]
|
||||
if hit:
|
||||
hits.append((key, profile))
|
||||
if len(hits) > 1:
|
||||
raise ValueError("ambiguous FEX compatibility profiles: " + ", ".join(k for k, _ in hits))
|
||||
return hits[0] if hits else (None, None)
|
||||
|
||||
|
||||
def graphics_lines(route):
|
||||
if route not in GRAPHICS:
|
||||
raise ValueError(f"unknown FEX graphics route: {route}")
|
||||
# Proton owns DLL selection/copying: DXVK for D3D9/11, vkd3d-proton for D3D12. Do not override DLLs or copy
|
||||
# anything into the game. Clearing inherited flags also keeps a prior game's wined3d choice from leaking in.
|
||||
lines = ["unset PROTON_USE_WINED3D PROTON_NO_D3D12 PROTON_NO_D3D11"]
|
||||
if route == "opengl":
|
||||
lines.append("export PROTON_USE_WINED3D=1")
|
||||
lines.append(f"echo 'FramePort FEX: graphics={route}'")
|
||||
return lines
|
||||
|
||||
|
||||
def register(core):
|
||||
original_find = core.find_ime_target
|
||||
|
||||
def find_ime_target(bins):
|
||||
# Before ApplicationCore was split out, FWindowsApplication (including its deferral CVar) lived in Core.
|
||||
# Robo Recall's modular UE4 build has the SAME CVar and constructor instruction there. Search that module
|
||||
# with the existing anchored finder; never infer a member offset from bAllowedToDeferMessageProcessing.
|
||||
extra = []
|
||||
for folder in sorted({os.path.dirname(b) for b in bins}):
|
||||
extra += [os.path.join(folder, n) for n in sorted(os.listdir(folder))
|
||||
if re.search(r"(?:^|-)core(?:-win(?:32|64)-shipping)?\.dll$", n, re.I)]
|
||||
return original_find(list(dict.fromkeys(list(bins) + extra)))
|
||||
|
||||
def game_info(app, info):
|
||||
path = os.path.join(info.get("game_dir") or os.path.join(core.META_DIR, "Software", "Software", app),
|
||||
info.get("exe", ""))
|
||||
key, profile = match_profile(core.GAMES, app, path)
|
||||
if profile is None:
|
||||
return
|
||||
status = profile.get("status", "verified") # backward compatible with First Contact's original data
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"invalid FEX profile status: {status}")
|
||||
info.update({k: v for k, v in profile.items() if k != "match"})
|
||||
info.update(profile=key, status=status)
|
||||
|
||||
def launcher_env(app, info):
|
||||
lines = graphics_lines(info.get("graphics", "d3d11"))
|
||||
if info.get("fp_mem") == "enabled":
|
||||
if info.get("status") != "verified":
|
||||
raise ValueError("fp_mem requires a verified per-game opt-in")
|
||||
lines.append("export FP_MEM=1")
|
||||
lines.append("export FP_BIGCACHE_MB=" + str(int(info.get("bigcache_mb", 256))))
|
||||
# candidate is informational: never enable the unzeroed cache merely because the engine uses Binned2.
|
||||
for k, v in info.get("env", {}).items():
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z_0-9]*", k) or not isinstance(v, str):
|
||||
raise ValueError("invalid FEX profile environment")
|
||||
# with the perf extension loaded, perf_prepare writes the game's one dxvk.conf (profile lines + its settings)
|
||||
if info.get("dxvk") and "perf_prepare" not in core.COMMANDS:
|
||||
lines += ["printf '%s\\n' " + " ".join(shlex.quote(line) for line in info["dxvk"]) + ' > "$A/dxvk.conf"',
|
||||
'export DXVK_CONFIG_FILE="$A/dxvk.conf"']
|
||||
return lines
|
||||
|
||||
def checks(app, info, result):
|
||||
facts = exe_facts(result["exe"])
|
||||
facts.update(profile=info.get("profile"), profile_status=info.get("status", "untested"),
|
||||
graphics=info.get("graphics", "d3d11"), fp_mem=info.get("fp_mem", "off"))
|
||||
# frameport_agent forwards checks to the PC and deployment.json; top-level helper warnings aren't forwarded.
|
||||
if result.get("warnings"):
|
||||
facts["profile_warnings"] = result["warnings"]
|
||||
if facts.get("exe_32bit"):
|
||||
facts["wow64"] = "untested: requires GE WoW64, 32-bit graphics and SteamVR vrclient bridge under FEX"
|
||||
if facts["graphics"] == "opengl":
|
||||
facts["opengl"] = "untested: no FEX GL thunk configured; rootfs GL may use software rendering or Zink"
|
||||
if facts["graphics"] == "d3d12":
|
||||
# GE versions have used both split lib/lib64 and architecture-qualified Wine directories. Report what
|
||||
# is actually installed; the Proton script remains responsible for selecting its own layout.
|
||||
paths = [os.path.join(core.GE, "files", lib, "wine", "vkd3d-proton", tail, "d3d12.dll")
|
||||
for lib in ("lib", "lib64") for tail in ("", "x86_64-windows", "i386-windows")]
|
||||
facts["vkd3d_dlls"] = [os.path.relpath(p, core.GE) for p in paths if os.path.isfile(p)]
|
||||
return facts
|
||||
|
||||
core.GAME_INFO_HOOKS.append(game_info)
|
||||
core.find_ime_target = find_ime_target
|
||||
core.LAUNCHER_ENV_HOOKS.append(launcher_env)
|
||||
core.INSTALL_CHECK_HOOKS.append(checks)
|
||||
@@ -0,0 +1,396 @@
|
||||
{
|
||||
"_comment": [
|
||||
"FEX compatibility profiles, loaded by fexrift.py and matched by fexrift_compat.py. No entitlement changes.",
|
||||
"match: OR of canonical_names (exact Meta names), sha256 (list of exe hashes), executables (list of {name,size}, both required). Without match the key is the exact canonical name. Ambiguous matches are refused.",
|
||||
"status: verified (headset checked), likely (static evidence; install with warning), untested (force_unverified required), blocked (refuse even force). graphics: d3d11|d3d9 (DXVK), d3d12 (vkd3d-proton), opengl (PROTON_USE_WINED3D), vulkan.",
|
||||
"env: string-valued environment; args: optional launch arguments override (absent keeps manifest). dxvk: dxvk.conf lines written only to the launcher anchor. notes: evidence and first-launch checks. revive: false for direct OpenVR games, otherwise true.",
|
||||
"ime_patch: optional fallback {rva,old,new}, RVA may be hex; generic finder always wins, including old modular Core.dll. Null means no fallback, never an exemption from IME checks.",
|
||||
"fp_mem: off|candidate|enabled; candidate is informational and NEVER enables the unzeroed cache. enabled requires verified status. bigcache_mb: cache size when enabled, default 256. env FP_MEM is an explicit opt-in retained for First Contact.",
|
||||
"exe is the verified First Contact fallback only; other profiles keep the Meta manifest launch path. msaa_ini allows --msaa for that verified game only."
|
||||
],
|
||||
"oculus-first-contact": {
|
||||
"title": "Oculus First Contact",
|
||||
"exe": "TouchNUX/Binaries/Win64/TouchNUX-Win64-Shipping.exe",
|
||||
"args": [
|
||||
"-gamemode=\"experienceonly\""
|
||||
],
|
||||
"ime_patch": {
|
||||
"rva": "0x22a605",
|
||||
"old": "c7862001000001000000",
|
||||
"new": "c7862001000000000000"
|
||||
},
|
||||
"_env": "Unreal's large-block allocator gives 0.5-1.6 GB blocks back every few seconds and this game overwrites them completely: fp_mem keeps them (native/fexwine/fp_mem.c)",
|
||||
"env": {
|
||||
"FP_MEM": "1",
|
||||
"FP_BIGCACHE_MB": "256"
|
||||
},
|
||||
"msaa_ini": true,
|
||||
"status": "verified",
|
||||
"graphics": "d3d11",
|
||||
"fp_mem": "enabled",
|
||||
"bigcache_mb": 256,
|
||||
"notes": "Headset verified; r.MobileMSAA=2 prevented rendering. Other games remain unverified."
|
||||
},
|
||||
"arktika-1": {
|
||||
"title": "Arktika 1",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "arktika1.exe",
|
||||
"size": 20181504
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "graphics API unclear (?); custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; d3d11 is a trial default, not a detected API"
|
||||
},
|
||||
"asgards-wrath": {
|
||||
"title": "Asgards Wrath",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WrathGame-Win64-Shipping.exe",
|
||||
"size": 63707648
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"brass-tactics": {
|
||||
"title": "Brass Tactics",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Brass-Win64-Shipping.exe",
|
||||
"size": 52790272
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"chronos": {
|
||||
"title": "Chronos",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Chronos-Win64-Shipping.exe",
|
||||
"size": 40722432
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": "off",
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "IME fix OFF: the generic finder matched member +0x100 in this UE 4.10 build and the patched game never submitted a frame; unpatched it renders (Frame headless 2026-10-16: 202 frames / 105 s, 1.9 fps while loading, headset not yet checked). Media Foundation videos need a playback test"
|
||||
},
|
||||
"defector": {
|
||||
"title": "Defector",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "DFCTR.exe",
|
||||
"size": 16204288
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"hubris": {
|
||||
"title": "Hubris",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "HubrisVR-Win64-Shipping.exe",
|
||||
"size": 85706240
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenXR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"lies-beneath": {
|
||||
"title": "Lies Beneath",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Kodiak-Win64-Shipping.exe",
|
||||
"size": 63537664
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"lone-echo": {
|
||||
"title": "Lone Echo",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "loneecho.exe",
|
||||
"size": 22336512
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"lone-echo-2": {
|
||||
"title": "Lone Echo 2",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "loneecho2.exe",
|
||||
"size": 33436160
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d12",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "D3D12: needs vkd3d-proton on Turnip under FEX (untested); custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"rick-and-morty-virtual-rick-ality": {
|
||||
"title": "Rick and Morty - Virtual Rick-ality",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "RickAndMortyVR.exe",
|
||||
"size": 21284352
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "opengl",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "OpenGL: no FEX GL thunk configured; rootfs GL renderer unverified; OpenVR directly, no Platform SDK; guest GL and speech WinRT need a device check",
|
||||
"revive": false
|
||||
},
|
||||
"robo-recall": {
|
||||
"title": "Robo Recall",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "RoboRecall-Win64-Shipping.exe",
|
||||
"size": 198144
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR)"
|
||||
},
|
||||
"skydances-behemoth": {
|
||||
"title": "Skydances BEHEMOTH",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "BHM-Win64-Shipping.exe",
|
||||
"size": 144449024
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenXR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"stormland": {
|
||||
"title": "Stormland",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Stormland.exe",
|
||||
"size": 85195776
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; Media Foundation videos need a playback test"
|
||||
},
|
||||
"the-climb": {
|
||||
"title": "The Climb",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Climb.exe",
|
||||
"size": 95534080
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d9",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "D3D9: DXVK under FEX (untested); CryEngine engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; D3D9 preferred over imported GL; confirm the actual selected renderer"
|
||||
},
|
||||
"the-unspoken": {
|
||||
"title": "The Unspoken",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Unspoken.exe",
|
||||
"size": 69496320
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-i": {
|
||||
"title": "Vader Immortal - Episode I",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 58100736
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-ii": {
|
||||
"title": "Vader Immortal - Episode II",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 59464192
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-iii": {
|
||||
"title": "Vader Immortal - Episode III",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 59814912
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"wilsons-heart": {
|
||||
"title": "Wilsons Heart",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WHVR.exe",
|
||||
"size": 15379968
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Local Rift copies in Meta libraries. Runs on the Frame, stdlib only; never touches entitlement or login data."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
import zlib
|
||||
|
||||
SEGMENT_SIZE = 10000000 # Meta's manifest format uses decimal 10 MB, not MiB
|
||||
|
||||
|
||||
def canonical(value):
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", value):
|
||||
raise ValueError("invalid Meta canonical name")
|
||||
return value
|
||||
|
||||
|
||||
def relative(value):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("invalid relative game path")
|
||||
value = value.replace("\\", "/")
|
||||
if (not value or value.startswith("/") or any(p in ("", ".", "..") for p in value.split("/"))
|
||||
or any(c in value for c in ':\x00\r\n')):
|
||||
raise ValueError("game path escapes its folder")
|
||||
return value
|
||||
|
||||
|
||||
def skipped(name):
|
||||
name = name.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
||||
return (name.endswith((".7z", ".ag")) or bool(re.search(r"\.7z\.0", name))
|
||||
or name in ("data.bin", "agrepackinstaller.exe", "vd.bat"))
|
||||
|
||||
|
||||
def confined(path, root):
|
||||
"""Reject symlink components too: an upload or delete must never follow a game-controlled link."""
|
||||
root, path = os.path.abspath(root), os.path.abspath(path)
|
||||
if os.path.commonpath([path, root]) != root:
|
||||
raise ValueError("path escapes the library")
|
||||
cursor = path
|
||||
while True:
|
||||
if os.path.islink(cursor):
|
||||
raise ValueError("symlink in local game path")
|
||||
if cursor == root:
|
||||
break
|
||||
cursor = os.path.dirname(cursor)
|
||||
if os.path.realpath(root) != root:
|
||||
raise ValueError("symlink in library root")
|
||||
return path
|
||||
|
||||
|
||||
def hash_file(path):
|
||||
total, segments = hashlib.sha256(), []
|
||||
size = 0
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(SEGMENT_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
total.update(chunk)
|
||||
# The fixture's last element is the zlib-compressed length, not the uncompressed chunk size. The first
|
||||
# element is an opaque Meta segment identifier: 0 explicitly means unknown for a local-only copy. These
|
||||
# records register existing bytes; they are NOT a CDN download/repair recipe (device validation needed).
|
||||
segments.append([0, hashlib.sha256(chunk).hexdigest(), len(zlib.compress(chunk))])
|
||||
return {"sha256": total.hexdigest(), "size": size, "segmentSize": SEGMENT_SIZE, "segments": segments}
|
||||
|
||||
|
||||
def register(core):
|
||||
def record_path(app):
|
||||
return os.path.join(core.ROOT, "local-games", canonical(app) + ".json")
|
||||
|
||||
def read_record(app):
|
||||
return core.read_json(record_path(app)) or {}
|
||||
|
||||
def save_record(app, record):
|
||||
os.makedirs(os.path.dirname(record_path(app)), exist_ok=True)
|
||||
path = record_path(app)
|
||||
with open(path + ".tmp", "w") as f:
|
||||
json.dump(record, f, indent=1)
|
||||
os.replace(path + ".tmp", path)
|
||||
|
||||
def layout(app, dest):
|
||||
app = canonical(app)
|
||||
if dest:
|
||||
# The main agent validates this against list_drives / resolve_dest on EVERY command. No fallback when a
|
||||
# drive disappears. Keep the physical game path simple while Meta sees its required Software/ layout.
|
||||
if not os.path.isabs(dest) or not os.path.isdir(dest):
|
||||
raise ValueError("destination drive is not mounted")
|
||||
base = confined(os.path.join(dest, "FramePort", "fexrift-games"), dest)
|
||||
library = confined(os.path.join(dest, "FramePort", "fexrift-library"), dest)
|
||||
guid = str(uuid.uuid5(uuid.NAMESPACE_URL, "frameport-fex-library:" + dest))
|
||||
else:
|
||||
library = os.path.join(core.META_DIR, "Software")
|
||||
base, guid = os.path.join(library, "Software"), core.LIBRARY_GUID
|
||||
return {"game_dir": confined(os.path.join(base, app), base), "library": library,
|
||||
"library_guid": guid, "dest": dest}
|
||||
|
||||
def validated_record(app):
|
||||
rec = read_record(app)
|
||||
if not rec:
|
||||
raise ValueError("no local-copy upload reservation for this game")
|
||||
expected = layout(app, rec.get("dest"))
|
||||
if any(rec.get(k) != v for k, v in expected.items()):
|
||||
raise ValueError("local-copy reservation has an invalid storage path")
|
||||
return rec
|
||||
|
||||
def prepare(args):
|
||||
app = canonical(args["app"])
|
||||
package = args["package"]
|
||||
files, seen = {}, set()
|
||||
for name, st in args["files"].items():
|
||||
rel = relative(name)
|
||||
if skipped(rel) or rel.casefold() in seen:
|
||||
raise ValueError("skipped or duplicate file in upload inventory")
|
||||
if not isinstance(st["size"], int) or st["size"] < 0 or not isinstance(st["mtime_ns"], int):
|
||||
raise ValueError("invalid source file stat")
|
||||
seen.add(rel.casefold())
|
||||
files[rel] = st
|
||||
if not files:
|
||||
raise ValueError("no game files to upload")
|
||||
old = read_record(app)
|
||||
dest = args.get("dest") or None
|
||||
loc = layout(app, dest)
|
||||
if old and (old.get("dest") != dest or old.get("package") != package):
|
||||
raise ValueError("local copy already has another package or drive; uninstall it before changing either")
|
||||
if not old and (os.path.exists(os.path.join(loc["library"], "Manifests", app + ".json"))
|
||||
or os.path.exists(loc["game_dir"])):
|
||||
raise ValueError("game already exists outside FramePort's local-copy installer")
|
||||
existing = {}
|
||||
for rel, st in files.items():
|
||||
path = confined(os.path.join(loc["game_dir"], rel), loc["game_dir"])
|
||||
confined(path + ".part", loc["game_dir"])
|
||||
if old.get("files", {}).get(rel) != st:
|
||||
# Delete completed bytes too: if this new attempt is cancelled before uploading them, the next
|
||||
# prepare must not mistake the old same-size file for a completed transfer of the new source.
|
||||
for stale in (path, path + ".part"):
|
||||
if os.path.isfile(stale):
|
||||
os.unlink(stale)
|
||||
if (old.get("files", {}).get(rel) == st and os.path.isfile(path)
|
||||
and os.path.getsize(path) == st["size"]):
|
||||
existing[rel] = st["size"]
|
||||
if old and set(old.get("files", {})) - set(files):
|
||||
raise ValueError("source removed files since the last upload; uninstall the local copy before replacing it")
|
||||
os.makedirs(loc["game_dir"], exist_ok=True)
|
||||
st = os.statvfs(loc["game_dir"])
|
||||
save_record(app, dict(old, **loc, app=app, package=package, files=files))
|
||||
return dict(loc, existing=existing, free_bytes=st.f_bavail * st.f_frsize)
|
||||
|
||||
def registry_library(rec, remove=False):
|
||||
guid, dest = rec["library_guid"], rec.get("dest")
|
||||
key = rf"HKEY_CURRENT_USER\Software\Oculus VR, LLC\Oculus\Libraries\{guid}"
|
||||
if remove:
|
||||
text = f"Windows Registry Editor Version 5.00\r\n\r\n[-{key}]\r\n"
|
||||
else:
|
||||
if dest:
|
||||
devices = os.path.join(core.PFX, "dosdevices")
|
||||
os.makedirs(devices, exist_ok=True)
|
||||
volume = os.path.join(devices, "volume{" + guid + "}")
|
||||
if os.path.lexists(volume):
|
||||
if not os.path.islink(volume) or os.path.realpath(volume) != dest:
|
||||
raise ValueError("Wine volume mapping is already in use")
|
||||
else:
|
||||
os.symlink(dest, volume)
|
||||
# Do not appropriate a user's drive letter. Reuse our mount mapping, else pick the first unused one.
|
||||
letter = next((c for c in "defghijklmnopqrstuvwxy"
|
||||
if os.path.islink(os.path.join(devices, c + ":"))
|
||||
and os.path.realpath(os.path.join(devices, c + ":")) == dest), None)
|
||||
if not letter:
|
||||
letter = next((c for c in "defghijklmnopqrstuvwxy"
|
||||
if not os.path.lexists(os.path.join(devices, c + ":"))), None)
|
||||
if not letter:
|
||||
raise ValueError("no free Wine drive letter")
|
||||
os.symlink(dest, os.path.join(devices, letter + ":"))
|
||||
rec["drive_letter"] = letter
|
||||
original = letter.upper() + ":\\FramePort\\fexrift-library"
|
||||
path = "\\\\?\\Volume{" + guid + "}\\FramePort\\fexrift-library"
|
||||
else:
|
||||
original = "C:\\Program Files\\Meta Horizon\\Software"
|
||||
path = "\\\\?\\Volume{00000000-0000-0000-0000-000000000043}\\Program Files\\Meta Horizon\\Software"
|
||||
text = "Windows Registry Editor Version 5.00\r\n\r\n"
|
||||
if not dest:
|
||||
text += (r"[HKEY_CURRENT_USER\Software\Oculus VR, LLC\Oculus\Libraries]" + "\r\n"
|
||||
+ '"DefaultLibrary"=' + core.meta_reg_str(guid) + "\r\n\r\n")
|
||||
text += (f"[{key}]\r\n" + '"OriginalPath"=' + core.meta_reg_str(original) + "\r\n"
|
||||
+ '"Path"=' + core.meta_reg_str(path) + "\r\n")
|
||||
reg = os.path.join(core.ROOT, "local-library.reg")
|
||||
core.put_text(reg, text)
|
||||
core.wine("regedit", "/S", core.winpath(reg), timeout=90)
|
||||
|
||||
def register_local(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app)
|
||||
if args.get("package") != rec["package"]:
|
||||
raise ValueError("upload belongs to another package")
|
||||
exe = relative(args["exe"])
|
||||
if exe not in rec["files"]:
|
||||
raise ValueError("launch program is not an uploaded file")
|
||||
app_id = args["app_id"]
|
||||
if not isinstance(app_id, str) or not re.fullmatch(r"[1-9][0-9]*", app_id):
|
||||
raise ValueError("invalid Meta app ID")
|
||||
files = {}
|
||||
for rel, st in rec["files"].items():
|
||||
path = confined(os.path.join(rec["game_dir"], rel), rec["game_dir"])
|
||||
if not os.path.isfile(path) or os.path.getsize(path) != st["size"]:
|
||||
raise ValueError(f"upload incomplete: {rel}")
|
||||
files[rel] = hash_file(path)
|
||||
library = rec["library"]
|
||||
os.makedirs(library, exist_ok=True)
|
||||
if rec.get("dest"):
|
||||
software = os.path.join(library, "Software")
|
||||
target = os.path.dirname(rec["game_dir"])
|
||||
if os.path.lexists(software):
|
||||
if not os.path.islink(software) or os.path.realpath(software) != target:
|
||||
raise ValueError("external Meta library Software path is already in use")
|
||||
else:
|
||||
os.symlink(target, software)
|
||||
args_list = args.get("args") or []
|
||||
if not isinstance(args_list, list) or any(not isinstance(a, str) or "\x00" in a for a in args_list):
|
||||
raise ValueError("launch arguments must be strings")
|
||||
# list2cmdline follows Windows argv quoting; Meta's own launchFile uses forward slashes. The local launcher
|
||||
# keeps the original argv list in the reservation, avoiding POSIX reinterpretation of Windows quotes.
|
||||
manifest = core.dawn_manifest({"packageType": "APP", "isCore": False, "appId": app_id,
|
||||
"canonicalName": app, "launchFile": exe,
|
||||
"launchParameters": subprocess.list2cmdline(args_list),
|
||||
"launchParameters2D": "", "version": str(args.get("version") or "0.0.0"),
|
||||
"versionCode": 1, "files": files})
|
||||
# App fixtures serialize the manifest's derived properties before the package base properties. Match that
|
||||
# order (core's runtime package writer uses base-first); both use the same defaults, CRLF and indentation.
|
||||
first = ("parentCanonicalName", "files", "dlcs", "assetFiles")
|
||||
manifest = {**{k: manifest[k] for k in first}, **{k: v for k, v in manifest.items() if k not in first}}
|
||||
manifests = os.path.join(library, "Manifests")
|
||||
os.makedirs(manifests, exist_ok=True)
|
||||
for suffix, content in ((".json", manifest), (".json.mini", dict(manifest, files={
|
||||
k: v for k, v in manifest["files"].items() if k.lower().endswith(".exe")}))):
|
||||
path = os.path.join(manifests, app + suffix)
|
||||
core.write_manifest(path + ".tmp", content)
|
||||
os.replace(path + ".tmp", path)
|
||||
registry_library(rec)
|
||||
rec.update({k: args.get(k) for k in ("app_id", "app_id_source", "store_app_id", "skipped", "title")})
|
||||
rec.update(source="local_copy", exe=exe, args=args_list, registered=True)
|
||||
save_record(app, rec)
|
||||
return local_info({"app": app})
|
||||
|
||||
def local_info(args):
|
||||
rec = validated_record(args["app"])
|
||||
return {k: rec.get(k) for k in ("source", "app_id", "app_id_source", "store_app_id", "skipped", "dest",
|
||||
"package", "game_dir", "library_guid", "registered")}
|
||||
|
||||
def unregister_local(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app)
|
||||
if args.get("package") != rec["package"]:
|
||||
raise ValueError("local copy belongs to another package")
|
||||
# Shared/default library stays. An external library is removed from the registry only after its last local
|
||||
# game goes; other games, the Wine prefix, sign-in and save directories remain intact.
|
||||
peers = [core.read_json(os.path.join(core.ROOT, "local-games", f)) or {}
|
||||
for f in os.listdir(os.path.dirname(record_path(app))) if f.endswith(".json") and f != app + ".json"]
|
||||
if rec.get("dest") and not any(p.get("library_guid") == rec["library_guid"] for p in peers):
|
||||
registry_library(rec, remove=True)
|
||||
if os.path.isdir(rec["game_dir"]):
|
||||
shutil.rmtree(rec["game_dir"])
|
||||
for suffix in (".json", ".json.mini"):
|
||||
path = os.path.join(rec["library"], "Manifests", app + suffix)
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
os.unlink(record_path(app))
|
||||
return {"removed": True}
|
||||
|
||||
def runtime_state(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app) if read_record(app) else None
|
||||
# Read only newly appended runtime log text. Never return log lines: runtime logs can contain account data.
|
||||
logdir = os.path.join(core.USER_DIR, "AppData", "Local", "Oculus")
|
||||
def logs():
|
||||
return [os.path.join(logdir, n) for n in os.listdir(logdir)
|
||||
if n.lower().endswith((".txt", ".log")) and any(
|
||||
s in n.lower() for s in ("server", "service"))] if os.path.isdir(logdir) else []
|
||||
offsets = {p: os.path.getsize(p) for p in logs()}
|
||||
librarian = os.path.join(logdir, "OVRLibrarian.log")
|
||||
lib_offset = os.path.getsize(librarian) if os.path.isfile(librarian) else 0
|
||||
probe = subprocess.run(["pgrep", "-f", "[O]VRServer_x64.exe"], capture_output=True, timeout=5)
|
||||
started, proc = probe.returncode != 0, None
|
||||
end = time.monotonic() + 95 # leave room for wineserver's bounded cleanup, total <=120 s
|
||||
state = "timeout"
|
||||
try:
|
||||
if started:
|
||||
runtime = os.path.join(core.META_DIR, "Support", "oculus-runtime")
|
||||
proc = subprocess.Popen([os.path.join(core.GEF, "files/bin/wine"), "./OVRServer_x64.exe"],
|
||||
cwd=runtime, env=core.wine_env(), stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
while time.monotonic() < end:
|
||||
if started and os.path.isfile(librarian):
|
||||
with open(librarian, errors="replace") as f:
|
||||
if os.path.getsize(librarian) < lib_offset:
|
||||
lib_offset = 0
|
||||
f.seek(lib_offset)
|
||||
done = "invocation completed" in f.read(65536).lower()
|
||||
lib_offset = f.tell()
|
||||
if done:
|
||||
subprocess.run(["pkill", "-9", "-f", "[O]VRLibrarian.exe"], capture_output=True, timeout=3)
|
||||
for path in logs():
|
||||
with open(path, errors="replace") as f:
|
||||
off = offsets.get(path, 0)
|
||||
f.seek(off if os.path.getsize(path) >= off else 0)
|
||||
text = f.read(1 << 20)
|
||||
offsets[path] = f.tell()
|
||||
for line in text.splitlines():
|
||||
if app not in line and (not rec or rec.get("app_id", "_") not in line):
|
||||
continue
|
||||
match = re.search(r"\bstate\s*:\s*(ready|install_available)\b", line, re.I)
|
||||
if match:
|
||||
state = match[1].lower()
|
||||
# Meta's PackageManager lists every registered package at start ("Currently installed
|
||||
# package: <name>, version code: N"); the "state:" line only comes later (app tracker / sign-in)
|
||||
elif state == "timeout" and re.search(r"Currently installed package:\s*" + re.escape(app) + r"\s*,",
|
||||
line):
|
||||
state = "installed"
|
||||
if state != "timeout" or (proc is not None and proc.poll() is not None):
|
||||
break
|
||||
time.sleep(1)
|
||||
finally:
|
||||
if started:
|
||||
subprocess.run([os.path.join(core.GEF, "files/bin/wineserver"), "-k"], env=core.wine_env(),
|
||||
capture_output=True, timeout=10)
|
||||
if proc is not None:
|
||||
try:
|
||||
proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return {"runtime_state": state, "reused_server": not started}
|
||||
|
||||
original_manifest = core.manifest_launch
|
||||
original_info = core.game_info
|
||||
|
||||
def manifest_launch(app):
|
||||
rec = read_record(app)
|
||||
if not rec.get("registered"):
|
||||
return original_manifest(app)
|
||||
rec = validated_record(app)
|
||||
manifest = core.read_json(os.path.join(rec["library"], "Manifests", app + ".json")) or {}
|
||||
if not manifest.get("launchFile"):
|
||||
raise ValueError("local-copy Meta manifest is missing; register the copy again")
|
||||
return {"exe": relative(manifest["launchFile"]), "args": rec["args"], "title": rec["title"],
|
||||
"game_dir": rec["game_dir"], "args_argv": True}
|
||||
|
||||
def game_info(app):
|
||||
rec = read_record(app)
|
||||
if not rec.get("registered"):
|
||||
return original_info(app)
|
||||
info = dict(core.GAMES.get(app) or {})
|
||||
info.update(manifest_launch(app))
|
||||
return info, app in core.GAMES
|
||||
|
||||
core.manifest_launch = manifest_launch
|
||||
core.game_info = game_info
|
||||
core.COMMANDS.update(prepare_local=prepare, register_local=register_local, unregister_local=unregister_local,
|
||||
local_info=local_info, runtime_state=runtime_state)
|
||||
core.INSTALL_CHECK_HOOKS.append(lambda app, info, result: {"local_copy": True} if read_record(app) else {})
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Meta's own browser sign-in, in the Frame's shared prefix. No credential files are read.
|
||||
|
||||
The broker is a separate, owner-only Unix socket process: native_sso URLs live only in its memory, never in a
|
||||
status file, launcher, journal or argv sent over SSH. Helpers and Wine subprocesses have their output discarded.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
CORE = None
|
||||
DESKTOP = "frameport-oculus.desktop"
|
||||
REGKEY = r"HKCU\Software\Wine\WineBrowser"
|
||||
MAX_URL = 65536
|
||||
|
||||
|
||||
def folder():
|
||||
return Path(CORE.ROOT) / "login"
|
||||
|
||||
|
||||
def request(message):
|
||||
with socket.socket(socket.AF_UNIX) as s:
|
||||
s.settimeout(90)
|
||||
s.connect(str(folder() / "broker.sock"))
|
||||
s.sendall(json.dumps(message).encode())
|
||||
s.shutdown(socket.SHUT_WR)
|
||||
return json.loads(receive(s))
|
||||
|
||||
|
||||
def receive(s):
|
||||
data = bytearray()
|
||||
while True:
|
||||
part = s.recv(8192)
|
||||
if not part:
|
||||
return data.decode()
|
||||
data.extend(part)
|
||||
if len(data) > MAX_URL * 2:
|
||||
raise ValueError("sign-in message too long")
|
||||
|
||||
|
||||
def valid_url(url, callback=False):
|
||||
if not isinstance(url, str) or len(url) > MAX_URL or any(ord(c) < 32 for c in url):
|
||||
return False
|
||||
try:
|
||||
u = urllib.parse.urlsplit(url)
|
||||
return (u.scheme, u.netloc, u.path) == (("oculus", "link", "/frl_login") if callback else
|
||||
("https", "auth.meta.com", "/native_sso/confirm"))
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def host_env():
|
||||
env = dict(os.environ)
|
||||
env.update(CORE.steam_display_env())
|
||||
env.update(XDG_RUNTIME_DIR=f"/run/user/{os.getuid()}",
|
||||
DBUS_SESSION_BUS_ADDRESS=f"unix:path=/run/user/{os.getuid()}/bus")
|
||||
env.pop("LD_PRELOAD", None)
|
||||
return env
|
||||
|
||||
|
||||
def quiet(argv, **kw):
|
||||
return subprocess.run(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60, **kw)
|
||||
|
||||
|
||||
def wine_env():
|
||||
env = CORE.wine_env({"ENABLE_GAMESCOPE_WSI": "0", "PROTON_NO_ZENITY": "1", "PROTONFIXES_DISABLE": "1",
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": CORE.STEAM})
|
||||
env.pop("LD_PRELOAD", None)
|
||||
env.pop("STEAM_COMPAT_INSTALL_PATH", None)
|
||||
return env
|
||||
|
||||
|
||||
def snapshot(paths):
|
||||
return {str(p): base64.b64encode(p.read_bytes()).decode() if p.exists() else None for p in paths}
|
||||
|
||||
|
||||
def restore_files(saved):
|
||||
for name, data in saved.items():
|
||||
p = Path(name)
|
||||
if data is None:
|
||||
p.unlink(missing_ok=True)
|
||||
else:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(base64.b64decode(data))
|
||||
|
||||
|
||||
def desktop_paths():
|
||||
config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
|
||||
apps = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share") / "applications"
|
||||
paths = {apps / DESKTOP, config / "mimeapps.list", apps / "mimeapps.list", apps / "defaults.list"}
|
||||
for base in (config, apps):
|
||||
paths.update(base.glob("*-mimeapps.list"))
|
||||
for desktop in os.environ.get("XDG_CURRENT_DESKTOP", "").lower().split(":"):
|
||||
if desktop and re.fullmatch(r"[a-z0-9_-]+", desktop):
|
||||
paths.add(base / f"{desktop}-mimeapps.list")
|
||||
return apps, paths
|
||||
|
||||
|
||||
def desktop_entry(helper):
|
||||
# Desktop Exec has its own quoting (single shell quotes are not valid here); %% is a literal percent.
|
||||
escaped = str(helper).replace("%", "%%")
|
||||
for c in ("\\", '"', "`", "$"):
|
||||
escaped = escaped.replace(c, "\\" + c)
|
||||
escaped = re.sub(r"[\r\n\t]", " ", escaped.replace("\\", "\\\\")) # Desktop string-value layer
|
||||
return ('[Desktop Entry]\nType=Application\nName=Meta sign-in (FramePort)\n'
|
||||
f'Exec="{escaped}" %u\nNoDisplay=true\nTerminal=false\nMimeType=x-scheme-handler/oculus;\n')
|
||||
|
||||
|
||||
def helper_script(op):
|
||||
# This tiny script never imports the core or prints exceptions (which could contain the supplied URL).
|
||||
return ("#!/usr/bin/python3 -I\nimport json,socket,sys\ntry:\n"
|
||||
" with socket.socket(socket.AF_UNIX) as s:\n"
|
||||
" s.settimeout(90)\n"
|
||||
f" s.connect({str(folder() / 'broker.sock')!r})\n"
|
||||
f" s.sendall(json.dumps({{'op': {op!r}, 'url': sys.argv[1]}}).encode())\n"
|
||||
" s.shutdown(socket.SHUT_WR)\n s.recv(4096)\n"
|
||||
"except Exception:\n sys.exit(1)\n")
|
||||
|
||||
|
||||
def launcher():
|
||||
q = shlex.quote
|
||||
env = wine_env()
|
||||
keys = ("STEAM_COMPAT_DATA_PATH", "WINEPREFIX", "WINEDLLOVERRIDES", "WINEDEBUG", "FP_NATIVE_WSERVER",
|
||||
"PROTON_NO_ZENITY", "PROTONFIXES_DISABLE", "STEAM_COMPAT_CLIENT_INSTALL_PATH", "XDG_RUNTIME_DIR",
|
||||
"DBUS_SESSION_BUS_ADDRESS", "ENABLE_GAMESCOPE_WSI")
|
||||
exports = "".join(f"export {k}={q(env[k])}\n" for k in keys)
|
||||
display = "".join(f"export {k}={q(v)}\n" for k, v in CORE.steam_display_env().items())
|
||||
return f'''#!/usr/bin/env bash
|
||||
# FramePort Meta sign-in: discard Electron output, which can contain native_sso URLs.
|
||||
exec >/dev/null 2>&1
|
||||
exec 9>{q(str(folder() / '.launch.lock'))}; flock -n 9 || exit 0
|
||||
{exports}{display}unset LD_PRELOAD STEAM_COMPAT_INSTALL_PATH STEAM_COMPAT_LIBRARY_PATHS PROTON_LOG PROTON_LOG_DIR
|
||||
python3 -I {q(str(Path(CORE.HERE) / 'fexrift.py'))} login_ping '{{}}' || exit 0
|
||||
G={q(CORE.GEF)}
|
||||
L="$WINEPREFIX/drive_c/users/steamuser/AppData/Local/Oculus"
|
||||
cleanup() {{
|
||||
kill $LK 2>/dev/null
|
||||
"$G/files/bin/wineserver" -k
|
||||
rm -f {q(str(folder() / 'launch.pid'))}
|
||||
}}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 0' TERM INT HUP
|
||||
# The start tick prevents --stop from signalling a reused PID after an interrupted launcher.
|
||||
printf '%s %s\\n' "$$" "$(awk '{{print $22}}' /proc/$$/stat)" > {q(str(folder() / 'launch.pid'))}
|
||||
cd {q(str(Path(CORE.META_DIR) / 'Support/oculus-runtime'))}
|
||||
"$G/files/bin/wine" ./OVRServer_x64.exe </dev/null &
|
||||
for i in $(seq 1 45); do
|
||||
sleep 2
|
||||
tail -n 3 "$L/OVRLibrarian.log" 2>/dev/null | grep -qi "invocation completed" && break
|
||||
done
|
||||
sleep 3; pkill -9 -f "[O]VRLibrarian.exe"; sleep 10
|
||||
( while sleep 5; do pkill -9 -f "[O]VRLibrarian.exe"; done ) & LK=$!
|
||||
cd {q(str(Path(CORE.META_DIR) / 'Support/oculus-client'))}
|
||||
"$G/proton" run ./Client.exe
|
||||
'''
|
||||
|
||||
|
||||
def signed_in():
|
||||
sessions = Path(CORE.PFX) / "drive_c/users/steamuser/AppData/Roaming/Oculus/sessions"
|
||||
return CORE.signed_in(str(sessions)) # sessions/<numeric user id>/, not Meta's _oaf framework folder
|
||||
|
||||
|
||||
def running(name):
|
||||
# /proc, rather than pgrep -f, avoids matching the command requesting status. Scope to this prefix.
|
||||
for p in Path("/proc").glob("[0-9]*/environ"):
|
||||
try:
|
||||
if (f"WINEPREFIX={CORE.PFX}".encode() in p.read_bytes().split(b"\0") and
|
||||
name.encode().lower() in p.with_name("cmdline").read_bytes().lower()):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def status(args):
|
||||
st = CORE.read_json(str(folder() / "state.json"), {}) or {}
|
||||
out = {"runtime_running": running("OVRServer_x64.exe"), "client_running": running("Client.exe"),
|
||||
"page_opened_at": st.get("page_opened_at"), "callback_received_at": st.get("callback_received_at"),
|
||||
"signed_in": signed_in()}
|
||||
if args.get("take_url"):
|
||||
try:
|
||||
out.update(request({"op": "take_url"}))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def deliver(url):
|
||||
if not valid_url(url, callback=True):
|
||||
return {"delivered": False}
|
||||
try:
|
||||
# Never put the callback in argv, even between two local processes. The PE reads UTF-8 on stdin.
|
||||
p = quiet([str(Path(CORE.GEF) / "proton"), "run", str(Path(CORE.HERE) / "fp_pipesend.exe")],
|
||||
input=url.encode(), env=wine_env())
|
||||
return {"delivered": p.returncode == 0}
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return {"delivered": False}
|
||||
|
||||
|
||||
class Broker:
|
||||
def __init__(self, via_pc):
|
||||
self.via_pc = via_pc
|
||||
self.pending = None
|
||||
self.stopped = False
|
||||
|
||||
def handle(self, msg):
|
||||
op = msg.get("op")
|
||||
if op in ("page", "callback"):
|
||||
url = msg.get("url")
|
||||
if not valid_url(url, callback=op == "callback"):
|
||||
return {"accepted": False}
|
||||
st = CORE.read_json(str(folder() / "state.json"), {}) or {}
|
||||
st["page_opened_at" if op == "page" else "callback_received_at"] = time.time()
|
||||
if op == "page":
|
||||
st["page_url"] = "https://auth.meta.com/native_sso/confirm"
|
||||
CORE.write_json(str(folder() / "state.json"), st)
|
||||
if op == "callback":
|
||||
return deliver(url)
|
||||
if self.via_pc:
|
||||
self.pending = url
|
||||
else:
|
||||
quiet(["xdg-open", url], env=host_env())
|
||||
return {"accepted": True}
|
||||
if op == "take_url":
|
||||
url, self.pending = self.pending, None
|
||||
return {"open_url": url} if url else {}
|
||||
if op == "stop":
|
||||
self.pending = None
|
||||
self.stopped = True
|
||||
return {"ready": True}
|
||||
|
||||
|
||||
def broker(args):
|
||||
path = folder() / "broker.sock"
|
||||
service = Broker(bool(args.get("via_pc")))
|
||||
try:
|
||||
with socket.socket(socket.AF_UNIX) as s:
|
||||
s.bind(str(path))
|
||||
os.chmod(path, 0o600)
|
||||
s.listen(4)
|
||||
s.settimeout(1)
|
||||
end = time.monotonic() + int(args.get("timeout", 900))
|
||||
while not service.stopped and time.monotonic() < end:
|
||||
try:
|
||||
conn, _ = s.accept()
|
||||
except TimeoutError:
|
||||
continue
|
||||
with conn:
|
||||
conn.settimeout(5)
|
||||
try:
|
||||
reply = service.handle(json.loads(receive(conn)))
|
||||
conn.sendall(json.dumps(reply).encode())
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass # do not serialize an exception that might contain a URL
|
||||
finally:
|
||||
service.pending = None
|
||||
restore()
|
||||
path.unlink(missing_ok=True)
|
||||
return {"stopped": True}
|
||||
|
||||
|
||||
def restore():
|
||||
path = folder() / "restore.json"
|
||||
saved = CORE.read_json(str(path), None)
|
||||
if saved is None:
|
||||
return
|
||||
# Keep the restore record until every step succeeds: --stop can repair an interrupted cleanup.
|
||||
launcher_pid = stop_launcher()
|
||||
CORE.wineserver_kill()
|
||||
if launcher_pid:
|
||||
for _ in range(100):
|
||||
if process_tick(launcher_pid[0]) != launcher_pid[1]:
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in launcher is still stopping; retry fex login --stop")
|
||||
(folder() / "launch.pid").unlink(missing_ok=True)
|
||||
old = saved.get("browser")
|
||||
if old:
|
||||
CORE.wine("reg", "add", REGKEY, "/v", "Browsers", "/t", old[0], "/d", old[1], "/f")
|
||||
else:
|
||||
CORE.wine("reg", "delete", REGKEY, "/v", "Browsers", "/f", check=False)
|
||||
restore_files(saved["files"])
|
||||
path.unlink()
|
||||
|
||||
|
||||
def stop_launcher():
|
||||
# Killing Wine alone while launch.sh is waiting for OVRLibrarian lets it start Client.exe *after* cleanup.
|
||||
# Signal the owning shell too; its pending TERM exits the startup loop as soon as the current sleep ends.
|
||||
path = folder() / "launch.pid"
|
||||
try:
|
||||
pid, tick = path.read_text().split()
|
||||
pid = int(pid)
|
||||
if pid <= 1:
|
||||
return
|
||||
if process_tick(pid) == tick:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
return pid, tick
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
|
||||
|
||||
def process_tick(pid):
|
||||
try:
|
||||
return Path(f"/proc/{pid}/stat").read_text().rpartition(")")[2].split()[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def stop(args):
|
||||
try:
|
||||
request({"op": "stop"})
|
||||
for _ in range(900):
|
||||
if not (folder() / "broker.sock").exists():
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in is still stopping; retry fex login --stop")
|
||||
except (OSError, ValueError):
|
||||
restore()
|
||||
(folder() / "broker.sock").unlink(missing_ok=True)
|
||||
return {"stopped": True, "signed_in": signed_in()}
|
||||
|
||||
|
||||
def start(args):
|
||||
timeout = int(args.get("timeout") or 900)
|
||||
if not 1 <= timeout <= 86400:
|
||||
raise CORE.FexError("sign-in timeout must be between 1 and 86400 seconds")
|
||||
if not (Path(CORE.HERE) / "fp_pipesend.exe").is_file():
|
||||
raise CORE.FexError("fp_pipesend.exe missing: build native/fexwine/pipesend.c and upload it with fex setup")
|
||||
if not (Path(CORE.META_DIR) / "Support/oculus-client/Client.exe").is_file():
|
||||
raise CORE.FexError("Meta's PC app isn't installed yet; run fex setup first")
|
||||
if running("Client.exe") or running("OVRServer_x64.exe"):
|
||||
raise CORE.FexError("close the Rift game or Meta app before starting sign-in (the prefix is shared)")
|
||||
try:
|
||||
request({"op": "ping"})
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in already active; use fex login --stop first")
|
||||
stop({}) # repair a previous interrupted run before snapshotting its handler
|
||||
folder().mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
folder().chmod(0o700)
|
||||
apps, paths = desktop_paths()
|
||||
previous = subprocess.run(["xdg-mime", "query", "default", "x-scheme-handler/oculus"],
|
||||
capture_output=True, text=True, timeout=10, env=host_env()).stdout.strip()
|
||||
reg = CORE.wine("reg", "query", REGKEY, "/v", "Browsers", check=False)
|
||||
match = re.search(r"Browsers\s+(REG_\w+)\s+(.*)", reg.stdout)
|
||||
CORE.write_json(str(folder() / "restore.json"),
|
||||
{"browser": list(match.groups()) if match else None, "handler": previous,
|
||||
"files": snapshot(paths)})
|
||||
try:
|
||||
for name, text in (("fp-open-url", helper_script("page")),
|
||||
("fp-oculus-callback", helper_script("callback")), ("launch.sh", launcher())):
|
||||
CORE.put_text(str(folder() / name), text, 0o700)
|
||||
CORE.write_json(str(folder() / "state.json"), {})
|
||||
apps.mkdir(parents=True, exist_ok=True)
|
||||
(apps / DESKTOP).write_text(desktop_entry(folder() / "fp-oculus-callback"))
|
||||
if quiet(["xdg-mime", "default", DESKTOP, "x-scheme-handler/oculus"], env=host_env()).returncode:
|
||||
raise CORE.FexError("couldn't register the Frame's temporary oculus link handler")
|
||||
CORE.wine("reg", "add", REGKEY, "/v", "Browsers", "/t", "REG_SZ", "/d",
|
||||
str(folder() / "fp-open-url"), "/f")
|
||||
subprocess.Popen([sys.executable, "-I", str(Path(CORE.HERE) / "fexrift.py"), "login_broker",
|
||||
json.dumps({"via_pc": bool(args.get("via_pc")), "timeout": timeout + 180})],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
for _ in range(100):
|
||||
try:
|
||||
request({"op": "ping"})
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
time.sleep(.05)
|
||||
else:
|
||||
raise CORE.FexError("couldn't start Meta sign-in's private URL broker")
|
||||
if args.get("start_now"):
|
||||
if quiet(["systemd-run", "--user", "--collect", "--quiet", "--unit=frameport-fexlogin",
|
||||
"--property=StandardOutput=null", "--property=StandardError=null",
|
||||
str(folder() / "launch.sh")], env=host_env()).returncode:
|
||||
raise CORE.FexError("couldn't start Meta's app; retry without --start-now and use Steam")
|
||||
return {"launcher": str(folder() / "launch.sh"), "started": bool(args.get("start_now")),
|
||||
"signed_in": signed_in()}
|
||||
except Exception:
|
||||
stop({})
|
||||
raise
|
||||
|
||||
|
||||
def register(core):
|
||||
global CORE
|
||||
CORE = core
|
||||
core.COMMANDS.update(login_start=start, login_status=status, login_stop=stop, login_broker=broker,
|
||||
login_ping=lambda args: request({"op": "ping"}),
|
||||
login_callback=lambda args: request({"op": "callback", "url": args.get("url")}))
|
||||
@@ -0,0 +1,575 @@
|
||||
"""Launch-time performance experiments. Frame side, stdlib only; no account data is read here.
|
||||
|
||||
Settings contain numbers/booleans and a small environment whitelist, never arbitrary shell. A launch prepares its
|
||||
own config and exports it before Proton starts. Bench owns the otherwise-idle shared prefix for at most 120 s.
|
||||
"""
|
||||
import collections
|
||||
import ctypes as C
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
CORE = None
|
||||
FEX_KEYS = {"FEX_TSOENABLED", "FEX_VECTORTSOENABLED", "FEX_MEMCPYSETTSOENABLED", "FEX_MULTIBLOCK",
|
||||
"FEX_X87REDUCEDPRECISION"}
|
||||
DEFAULTS = {"dxvk_disable_msaa": False, "dxgi_sync_interval": -1, "dxgi_max_frame_latency": 0,
|
||||
"eye_scale": 1.5, "motion_smoothing": 2, "cores": "4-7", "ntsync": True,
|
||||
"fp_mem": False, "bigcache_mb": 256, "fp_mem_zero": False, "fex": {},
|
||||
"wine_large_address_aware": False}
|
||||
BOOL_KEYS = {"dxvk_disable_msaa", "ntsync", "fp_mem", "fp_mem_zero", "wine_large_address_aware"}
|
||||
INT_KEYS = {"dxgi_sync_interval": (-1, 4), "dxgi_max_frame_latency": (0, 16), "motion_smoothing": (0, 2),
|
||||
"bigcache_mb": (1, 6144)}
|
||||
|
||||
|
||||
def app_name(app):
|
||||
if not isinstance(app, str) or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}", app):
|
||||
raise ValueError("invalid Meta app name")
|
||||
return app
|
||||
|
||||
|
||||
def validate(values):
|
||||
if not isinstance(values, dict):
|
||||
raise ValueError("settings must be an object")
|
||||
out = {}
|
||||
for key, value in values.items():
|
||||
if key in BOOL_KEYS:
|
||||
if type(value) is not bool:
|
||||
raise ValueError(f"{key} must be true or false")
|
||||
elif key in INT_KEYS:
|
||||
lo, hi = INT_KEYS[key]
|
||||
if type(value) is not int or not lo <= value <= hi:
|
||||
raise ValueError(f"{key} must be an integer in {lo}..{hi}")
|
||||
elif key == "eye_scale":
|
||||
if type(value) not in (float, int) or value not in (1.0, 1.25, 1.5):
|
||||
raise ValueError("eye_scale must be 1.0, 1.25 or 1.5 (a divisor)")
|
||||
elif key == "cores":
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[0-7](?:-[0-7])?(?:,[0-7](?:-[0-7])?)*", value):
|
||||
raise ValueError("cores must be a taskset CPU list within 0..7")
|
||||
if any(len(p) == 3 and p[0] > p[2] for p in value.split(",")):
|
||||
raise ValueError("cores range is reversed")
|
||||
elif key == "fex":
|
||||
if not isinstance(value, dict) or any(k not in FEX_KEYS or type(v) not in (str, int) or
|
||||
str(v) not in ("0", "1") for k, v in value.items()):
|
||||
raise ValueError("fex permits only boolean TSO, MULTIBLOCK and X87REDUCEDPRECISION keys")
|
||||
value = {k: str(v) for k, v in value.items()}
|
||||
else:
|
||||
raise ValueError(f"unknown performance setting: {key}")
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def settings_path(app):
|
||||
return os.path.join(CORE.ROOT, "settings", app_name(app) + ".json")
|
||||
|
||||
|
||||
def settings_get(args):
|
||||
app = app_name(args["app"])
|
||||
info = CORE.GAMES.get(app, {})
|
||||
env = info.get("env", {})
|
||||
defaults = dict(DEFAULTS, fp_mem=env.get("FP_MEM", "") not in ("", "0"),
|
||||
bigcache_mb=int(env.get("FP_BIGCACHE_MB", 256)))
|
||||
defaults.update(validate(info.get("perf", {})))
|
||||
overrides = validate(CORE.read_json(settings_path(app), {}) or {})
|
||||
return {"app": app, "settings": dict(defaults, **overrides), "overrides": overrides}
|
||||
|
||||
|
||||
def settings_set(args):
|
||||
app = app_name(args["app"])
|
||||
# Validate the whole request before writing. Null removes an override (the catalog/default becomes active).
|
||||
updates = {k: v for k, v in args.items() if k != "app"}
|
||||
if any(k not in DEFAULTS for k in updates):
|
||||
raise ValueError("unknown performance setting")
|
||||
values = settings_get({"app": app})["overrides"]
|
||||
values.update(validate({k: v for k, v in updates.items() if v is not None}))
|
||||
for k, v in updates.items():
|
||||
if v is None:
|
||||
values.pop(k, None)
|
||||
CORE.write_json(settings_path(app), values)
|
||||
return settings_get({"app": app})
|
||||
|
||||
|
||||
def scale_folder(scale):
|
||||
return os.path.join(CORE.ROOT, "revive-scales", format(float(scale), "g"))
|
||||
|
||||
|
||||
def eye_variant(data, scale):
|
||||
"""Only the research Revive 3.2.0 eye-size site, with PE RVA translation. Never touch Oculus binaries.
|
||||
|
||||
Accept the stock eye-size load or the research divisor load. Refuse other instructions rather than guessing
|
||||
at a new Revive build. Use a dedicated padding slot, leaving shared constants in the data section unchanged.
|
||||
"""
|
||||
if data[:2] != b"MZ":
|
||||
raise ValueError("Revive eye scale: invalid PE")
|
||||
pe = struct.unpack_from("<I", data, 0x3c)[0]
|
||||
n, opt = struct.unpack_from("<H", data, pe + 6)[0], struct.unpack_from("<H", data, pe + 20)[0]
|
||||
|
||||
def offset(rva):
|
||||
for i in range(n):
|
||||
size, va, rawsize, raw = struct.unpack_from("<IIII", data, pe + 24 + opt + i * 40 + 8)
|
||||
if va <= rva < va + min(size, rawsize):
|
||||
return raw + rva - va
|
||||
raise ValueError("Revive eye scale: RVA outside sections")
|
||||
ins, slot = offset(0x12eab), offset(0x12e9c)
|
||||
# Original movss xmm4,[1.0], or the research patch's load of the existing 1.5 at RVA 0x4eaa4.
|
||||
# Put our divisor in verified function-alignment padding: changing a shared rdata constant affects other code.
|
||||
old = data[ins:ins + 8]
|
||||
original = bytes.fromhex("f30f102515bb0400")
|
||||
research = b"\xf3\x0f\x10\x25" + struct.pack("<i", 0x4eaa4 - 0x12eb3)
|
||||
if old not in (original, research) or data[offset(0x12e91)] != 0xc3 or data[slot:slot + 4] != b"\xcc" * 4:
|
||||
raise ValueError("Revive eye scale: unexpected 3.2.0 eye-size instruction/padding")
|
||||
out = bytearray(data)
|
||||
struct.pack_into("<f", out, slot, scale)
|
||||
out[ins:ins + 8] = b"\xf3\x0f\x10\x25" + struct.pack("<i", 0x12e9c - 0x12eb3)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def prepare_scales():
|
||||
src = Path(CORE.REVIVE)
|
||||
data = (src / "LibRevive64.dll").read_bytes()
|
||||
# PC setup validates stock 3.2.0 and applies REVIVE_PATCHES (src/frameport/fexrift.py): the visibility fixes and
|
||||
# the eye-size redirect to the 1.5 constant. Accept only that upload, or the same without the eye-size redirect.
|
||||
uploaded = {"eda2a449d230832c95734a03e8c83eeb531668ce676693f636e19c315765af4e", # all six patches
|
||||
"0a3a6733f5decec74abb16b85c1f72783a6f421160753f600ddae4e8cb6482c6"} # visibility fixes only
|
||||
if hashlib.sha256(data).hexdigest() not in uploaded:
|
||||
raise ValueError("Revive eye scale: needs FramePort's patched Revive 3.2.0 upload")
|
||||
for scale in (1.0, 1.25, 1.5):
|
||||
patched = eye_variant(data, scale) # validate before replacing any directory
|
||||
dest = Path(scale_folder(scale))
|
||||
tmp = dest.with_name(dest.name + ".new")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
shutil.copytree(src, tmp, copy_function=os.link)
|
||||
# Break the hard link before changing the DLL: shared base and other variants stay intact.
|
||||
dll = tmp / "LibRevive64.dll"
|
||||
dll.unlink()
|
||||
dll.write_bytes(patched)
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
os.replace(tmp, dest)
|
||||
|
||||
|
||||
def perf_prepare(args):
|
||||
app = app_name(args["app"])
|
||||
s = settings_get({"app": app})["settings"]
|
||||
config = os.path.join(CORE.ROOT, "settings", app + ".dxvk.conf")
|
||||
os.makedirs(os.path.dirname(config), exist_ok=True) # first launch of a game nobody changed settings for
|
||||
managed = ("d3d11.disableMsaa", "dxgi.syncInterval", "dxgi.maxFrameLatency") # these settings win
|
||||
profile = [str(line) for line in args.get("dxvk", []) if "\n" not in str(line)
|
||||
and str(line).split("=", 1)[0].strip() not in managed]
|
||||
lines = profile + [f"{k} = {s[key]}" for k, key, default in (
|
||||
("d3d11.disableMsaa", "dxvk_disable_msaa", False), ("dxgi.syncInterval", "dxgi_sync_interval", -1),
|
||||
("dxgi.maxFrameLatency", "dxgi_max_frame_latency", 0)) if s[key] != default]
|
||||
# Defaults = exactly the launch verified in the headset (First Contact, 2026-10-10): no dxvk.conf, the uploaded
|
||||
# Revive (its eye-size patch is 1.5), no large-address-aware flag; only a changed setting adds something.
|
||||
if lines:
|
||||
CORE.put_text(config, "\n".join(lines + [""]))
|
||||
elif os.path.lexists(config):
|
||||
os.remove(config)
|
||||
if s["eye_scale"] == 1.5:
|
||||
revive = CORE.REVIVE
|
||||
else:
|
||||
revive = scale_folder(s["eye_scale"])
|
||||
if not os.path.isfile(os.path.join(revive, "LibRevive64.dll")):
|
||||
prepare_scales()
|
||||
exports = {"FP_CORES": s["cores"], "FP_REVIVE_DIR": revive,
|
||||
"FP_MEM": "1" if s["fp_mem"] else "", "FP_BIGCACHE_MB": str(s["bigcache_mb"]),
|
||||
"FP_MEM_ZERO": "1" if s["fp_mem_zero"] else "0", "PROTON_NO_NTSYNC": "0" if s["ntsync"] else "1",
|
||||
"FP_MOTION_SMOOTHING": json.dumps({"section": f"steam.app.{CORE.GAME_ID}",
|
||||
"key": "motionSmoothingOverride", "kind": "i",
|
||||
"value": s["motion_smoothing"]})}
|
||||
exports.update({k: str(s["fex"].get(k, "0" if "TSO" in k else "1" if k == "FEX_MULTIBLOCK" else "0"))
|
||||
for k in FEX_KEYS})
|
||||
if lines:
|
||||
exports["DXVK_CONFIG_FILE"] = config
|
||||
if s["wine_large_address_aware"]:
|
||||
exports["WINE_LARGE_ADDRESS_AWARE"] = "1"
|
||||
shell = "\n".join(f"export {k}={shlex.quote(v)}" for k, v in exports.items())
|
||||
if s["ntsync"]:
|
||||
shell += "\nunset PROTON_NO_NTSYNC"
|
||||
return {"shell": shell, "settings": s}
|
||||
|
||||
|
||||
def launcher_env(app, info):
|
||||
# stdout is an isolated helper's JSON, then a tiny decoder emits only validated exports. Failure aborts launch:
|
||||
# continuing with an old config would give misleading experimental results (and could enable stale fp_mem).
|
||||
# the compat profile's dxvk.conf lines (stream C) go into the same per-game config as these settings
|
||||
dxvk = [str(line) for line in info.get("dxvk", []) if "\n" not in str(line) and "\r" not in str(line)]
|
||||
arg = shlex.quote(json.dumps({"app": app_name(app), "dxvk": dxvk}))
|
||||
decode = 'import json,sys; print(json.load(sys.stdin)["result"]["shell"])'
|
||||
return [f'FP_PERF=$(python3 -I "$R/artifacts/fexrift.py" perf_prepare {arg}) || exit 1',
|
||||
f'eval "$(printf \'%s\' "$FP_PERF" | python3 -I -c {shlex.quote(decode)})"']
|
||||
|
||||
|
||||
# OpenVR IVRCompositor_022 Compositor_FrameTiming, including its 80-byte TrackedDevicePose_t. Keeping the tail
|
||||
# prevents GetFrameTimings overwriting a truncated ctypes structure. Versions are pinned, never probed by index.
|
||||
class FrameTiming(C.Structure):
|
||||
_fields_ = [(n, C.c_uint32) for n in ("size", "index", "presents", "mis_presents", "dropped", "reprojection")]
|
||||
_fields_ += [("system_time", C.c_double)]
|
||||
_fields_ += [(n, C.c_float) for n in ("pre_gpu", "post_gpu", "gpu_ms", "compositor_gpu", "compositor_cpu",
|
||||
"compositor_idle", "client_interval", "present_call", "wait_present",
|
||||
"submit", "poses_called", "poses_ready", "frame_ready", "update_start",
|
||||
"update_end", "render_start")]
|
||||
_fields_ += [("pose", C.c_byte * 80), ("vsyncs_ready", C.c_uint32), ("vsyncs_first", C.c_uint32)]
|
||||
|
||||
|
||||
class VR:
|
||||
def __init__(self):
|
||||
self.core, self.method, self.settings = CORE.vr_settings()
|
||||
try:
|
||||
self.compositor = self.interface(b"IVRCompositor_022")
|
||||
self.system = self.interface(b"IVRSystem_022")
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def interface(self, name):
|
||||
err = C.c_int()
|
||||
obj = self.method(self.core, 3, C.c_void_p, C.c_char_p, C.POINTER(C.c_int))(name, C.byref(err))
|
||||
if not obj or err.value:
|
||||
raise ValueError("benchmark needs " + name.decode())
|
||||
return obj
|
||||
|
||||
def activity(self):
|
||||
# IVRSystem_022::GetTrackedDeviceActivityLevel(HMD=0). UserInteraction (1) and its timeout (2) refuse.
|
||||
return self.method(self.system, 15, C.c_int, C.c_uint32)(0)
|
||||
|
||||
def screen_timeout(self):
|
||||
err = C.c_int()
|
||||
value = self.method(self.settings, 7, C.c_float, C.c_char_p, C.c_char_p, C.POINTER(C.c_int))(
|
||||
b"power", b"turnOffScreensTimeout", C.byref(err))
|
||||
if err.value or not math.isfinite(value) or value <= 0:
|
||||
raise ValueError("cannot read screen timeout; benchmark refused")
|
||||
return value
|
||||
|
||||
def timings(self):
|
||||
rows = (FrameTiming * 128)()
|
||||
for row in rows:
|
||||
row.size = C.sizeof(FrameTiming)
|
||||
n = self.method(self.compositor, 9, C.c_uint32, C.POINTER(FrameTiming), C.c_uint32)(rows, 128)
|
||||
return [{"index": r.index, "presents": r.presents, "gpu_ms": r.gpu_ms,
|
||||
"cpu_ms": r.client_interval, "system_time": r.system_time} for r in rows[:min(n, 128)]]
|
||||
|
||||
def scene_pid(self):
|
||||
return self.method(self.compositor, 22, C.c_uint32)() # GetCurrentSceneFocusProcess
|
||||
|
||||
def close(self):
|
||||
self.method(self.core, 1, None)()
|
||||
|
||||
|
||||
def drm_engines(text):
|
||||
"""DRM fdinfo nanoseconds, deduplicated by drm-client-id by the caller (dup fds share one client)."""
|
||||
out = {}
|
||||
for line in text.splitlines():
|
||||
m = re.fullmatch(r"drm-engine-([\w-]+):\s*(\d+)\s+ns", line.strip())
|
||||
if m:
|
||||
out[m[1]] = int(m[2])
|
||||
return out
|
||||
|
||||
|
||||
def process_sample(pids, proc="/proc"):
|
||||
engines, waits, seen = {}, collections.Counter(), set()
|
||||
for pid in pids:
|
||||
base = Path(proc) / str(pid)
|
||||
for fd in (base / "fdinfo").glob("*"):
|
||||
try:
|
||||
text = fd.read_text()
|
||||
except OSError:
|
||||
continue
|
||||
client = re.search(r"^drm-client-id:\s*(\d+)", text, re.M)
|
||||
device = re.search(r"^drm-pdev:\s*(.+)", text, re.M)
|
||||
if not client:
|
||||
continue
|
||||
identity = (device[1] if device else "", client[1])
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
for engine, ns in drm_engines(text).items():
|
||||
engines[(identity, engine)] = ns
|
||||
for task in (base / "task").glob("*"):
|
||||
try:
|
||||
waits[task.joinpath("wchan").read_text().strip()] += 1
|
||||
except OSError:
|
||||
pass
|
||||
return engines, waits
|
||||
|
||||
|
||||
def gpu_delta(before, after):
|
||||
# Clients that open during the interval have no baseline; do not attribute their lifetime counters to one tick.
|
||||
return sum(max(0, v - before[k]) for k, v in after.items() if k in before)
|
||||
|
||||
|
||||
def processes(proc="/proc"):
|
||||
out = {}
|
||||
for p in Path(proc).glob("[0-9]*"):
|
||||
try:
|
||||
if p.stat().st_uid != os.getuid():
|
||||
continue
|
||||
cmd = p.joinpath("cmdline").read_bytes().replace(b"\0", b" ").decode(errors="replace")
|
||||
comm = p.joinpath("comm").read_text().strip()
|
||||
out[int(p.name)] = (comm, cmd)
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def busy_processes(rows):
|
||||
# Do not print cmdlines: another stream may be handling an Oculus browser sign-in.
|
||||
return [pid for pid, (comm, cmd) in rows.items() if
|
||||
(re.search(r"(?i)(\.exe(?: |$)|(?:^|/)(?:proton|wine(?:64)?(?:-preloader)?)(?: |$)|lepton-steamlaunch-)",
|
||||
cmd)
|
||||
or (comm == "reaper" and not re.search(r"\bAppId=250820\b", cmd))
|
||||
or comm.startswith("wineserver") or comm in ("FEXInterpreter", "FEXLoader", "pressure-vessel"))]
|
||||
|
||||
|
||||
def recent_steam_input(steam, now=None):
|
||||
"""Steam input log is a supplementary guard, HMD activity is the authoritative one. Never read login logs."""
|
||||
now = time.time() if now is None else now
|
||||
path = Path(steam) / "logs/controller.txt"
|
||||
try:
|
||||
# A recent controller log write can be a device reconnection, so conservatively refuse it too.
|
||||
return now - path.stat().st_mtime < 60
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def vr_set(section, key, kind, value):
|
||||
# In its own process: cmd_vrsetting in the process that holds VR()'s SteamVR connection segfaulted on the
|
||||
# second call (reproduced on the Frame 2026-10-16) after the first had turned the display's standby pause off.
|
||||
result = vrsetting_subprocess(section, key, kind, value)
|
||||
if result.get("error"):
|
||||
raise ValueError(f"SteamVR refused {section}/{key}")
|
||||
|
||||
|
||||
def stop_launcher(child):
|
||||
# The shared prefix was verified idle before launch, so -k cannot interrupt an earlier game or login.
|
||||
try:
|
||||
subprocess.run([os.path.join(CORE.GEF, "files/bin/wineserver"), "-k"], env=CORE.wine_env(),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
finally:
|
||||
if child is not None:
|
||||
try:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
child.wait(timeout=5)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
child.wait(timeout=5)
|
||||
|
||||
|
||||
START_WAIT = 180 # seconds the bench waits for the game's process after starting its launcher
|
||||
|
||||
|
||||
def bench_run(args, launcher, vr):
|
||||
seconds = args.get("seconds", 120)
|
||||
if type(seconds) is not int or not 1 <= seconds <= 120:
|
||||
raise ValueError("seconds must be an integer in 1..120")
|
||||
if busy_processes(processes()) or recent_steam_input(CORE.STEAM) or vr.activity() != 3:
|
||||
raise ValueError("benchmark refused: game, Wine, container, HMD activity or recent Steam input")
|
||||
screen = vr.screen_timeout() # no setting is changed until both safety checks and this read succeed
|
||||
vrserver_log = os.path.join(CORE.STEAM, "logs", "vrserver.txt")
|
||||
log_start = os.path.getsize(vrserver_log) if os.path.isfile(vrserver_log) else 0
|
||||
guard_write(screen=screen) # before any change: the parent restores from it if this process dies
|
||||
child, old_handler = None, None
|
||||
settings = settings_get(args)["settings"]
|
||||
frames, waits, gpu, gpu_seconds = {}, collections.Counter(), 0, 0.0
|
||||
started = time.monotonic()
|
||||
|
||||
def interrupted(signum, frame):
|
||||
raise ValueError("benchmark interrupted")
|
||||
try:
|
||||
old_handler = signal.signal(signal.SIGTERM, interrupted)
|
||||
vr_set("power", "pauseCompositorOnStandby", "b", False)
|
||||
vr_set("power", "turnOffScreensTimeout", "f", float(seconds + START_WAIT + 10))
|
||||
# Save the initial frame index so stale scene frames in the compositor ring are excluded.
|
||||
initial = {r["index"] for r in vr.timings()}
|
||||
with open(os.devnull, "wb") as sink:
|
||||
child = subprocess.Popen([launcher], stdin=subprocess.DEVNULL, stdout=sink, stderr=sink,
|
||||
start_new_session=True, close_fds=True)
|
||||
guard_write(screen=screen, pgid=getattr(child, "pid", None))
|
||||
# The launcher starts Meta's runtime and waits for it (60-100 s under FEX) before the game: the window
|
||||
# starts when the game's process appears, not at the launch.
|
||||
wait_end = time.monotonic() + START_WAIT
|
||||
while not [q for q, (_, cmd) in processes().items() if args["exe"].lower() in cmd.lower()]:
|
||||
if time.monotonic() > wait_end:
|
||||
raise ValueError(f"benchmark: the game didn't start within {START_WAIT} s; inspect launch.log")
|
||||
if child.poll() is not None:
|
||||
raise ValueError("benchmark launcher exited before the game started; inspect launch.log")
|
||||
level = vr.activity()
|
||||
if level == 1: # controller.txt isn't a signal here: the starting game loads its input bindings
|
||||
raise ValueError(f"benchmark stopped: owner interaction (activity {level})")
|
||||
time.sleep(1)
|
||||
before, previous = {}, time.monotonic()
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
# Before the launch the headset must be in standby (3); the game itself then wakes the compositor
|
||||
# (activity leaves standby), so during the run only real use stops it: 1 = user interaction (worn or
|
||||
# moved), or new controller input.
|
||||
level = vr.activity()
|
||||
if level == 1: # controller.txt isn't a signal here: the starting game loads its input bindings
|
||||
raise ValueError(f"benchmark stopped: owner interaction (activity {level})")
|
||||
if child.poll() is not None:
|
||||
raise ValueError("benchmark launcher exited before the sampling window ended; inspect launch.log")
|
||||
rows = processes()
|
||||
# Only the actual game's exe (or FEX's argv containing it), not the runtime/compositor GPU client.
|
||||
pids = [p for p, (_, cmd) in rows.items() if args["exe"].lower() in cmd.lower()]
|
||||
after, w = process_sample(pids)
|
||||
now = time.monotonic()
|
||||
if before.keys() & after.keys():
|
||||
gpu += gpu_delta(before, after)
|
||||
gpu_seconds += now - previous
|
||||
before, previous = after, now
|
||||
waits.update(w)
|
||||
focus = vr.scene_pid()
|
||||
for r in vr.timings():
|
||||
if focus in pids and r["index"] not in initial:
|
||||
frames[r["index"]] = r # ring revisits update presents, not another frame
|
||||
time.sleep(min(0.5, max(0, deadline - time.monotonic())))
|
||||
finally:
|
||||
# Each cleanup is attempted even when the preceding one fails. Display safety outranks benchmark success.
|
||||
try:
|
||||
stop_launcher(child)
|
||||
finally:
|
||||
try:
|
||||
vr_set("power", "pauseCompositorOnStandby", "b", True)
|
||||
finally:
|
||||
try:
|
||||
vr_set("power", "turnOffScreensTimeout", "f", screen)
|
||||
finally:
|
||||
if old_handler is not None:
|
||||
signal.signal(signal.SIGTERM, old_handler)
|
||||
records = list(frames.values())
|
||||
# Without a tracking universe (IMU fallback, e.g. after a boot without the headset worn) Rift games wait at
|
||||
# startup forever: SteamVR's own frames are all a run then counts (seen 2026-10-16, ~1.9/s, game GPU ~0 %).
|
||||
imu_fallback = False
|
||||
try:
|
||||
with open(vrserver_log, errors="replace") as f:
|
||||
f.seek(log_start if os.path.getsize(vrserver_log) >= log_start else 0)
|
||||
imu_fallback = "IMU fallback is active" in f.read(8 << 20)
|
||||
except OSError:
|
||||
pass
|
||||
duration = max(0.001, time.monotonic() - started)
|
||||
times = [r["system_time"] for r in records]
|
||||
span = max(times) - min(times) if len(times) > 1 else 0
|
||||
return {"app": args["app"], "seconds": seconds, "elapsed_s": round(duration, 3), "frames": len(records),
|
||||
"imu_fallback": imu_fallback, # true: the result says nothing about the game (put the headset on once)
|
||||
"valid": len(records) >= 2 and span > 0 and not imu_fallback, "sample_span_s": round(span, 3),
|
||||
"fps": round((len(records) - 1) / span, 2) if span > 0 else None,
|
||||
"hitches": sum(r["presents"] >= 3 for r in records),
|
||||
"hitches_per_20s": round(20 * sum(r["presents"] >= 3 for r in records) / span, 2) if span > 0 else None,
|
||||
"game_gpu_busy_pct": round(100 * gpu / (gpu_seconds * 1e9), 2) if gpu_seconds else None,
|
||||
"render_gpu_ms": round(sum(r["gpu_ms"] for r in records) / len(records), 3) if records else None,
|
||||
"client_interval_ms": round(sum(r["cpu_ms"] for r in records) / len(records), 3) if records else None,
|
||||
"thread_wchan_samples": dict(waits), "settings": settings}
|
||||
|
||||
|
||||
def bench(args):
|
||||
app = app_name(args["app"])
|
||||
# Launcher path is supplied by the owning agent's deployment record, not an arbitrary remote shell command.
|
||||
launcher = args["launcher"]
|
||||
if not os.path.isfile(launcher) or os.path.basename(launcher) != "launch.sh":
|
||||
raise ValueError("install the game before benchmarking")
|
||||
info, _ = CORE.game_info(app)
|
||||
exe = os.path.basename(info["exe"].replace("\\", "/"))
|
||||
os.makedirs(CORE.ROOT, exist_ok=True)
|
||||
with open(os.path.join(CORE.ROOT, ".bench.lock"), "w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return bench_guarded(dict(args, app=app, exe=exe), launcher)
|
||||
|
||||
|
||||
# The measurement talks to SteamVR through ctypes (vrclient.so). A native crash there kills the process without
|
||||
# running bench_run's finally, which once left pauseCompositorOnStandby=false (display kept on) on the Frame
|
||||
# (2026-10-16). So it runs in a forked child, and the parent restores the display settings and stops the game from
|
||||
# a guard file when the child returns no result, each restore in its own process (another crash can't skip it).
|
||||
def guard_path():
|
||||
return os.path.join(CORE.ROOT, ".bench.guard.json")
|
||||
|
||||
|
||||
def guard_write(**kw):
|
||||
CORE.write_json(guard_path(), kw)
|
||||
|
||||
|
||||
def vrsetting_subprocess(section, key, kind, value):
|
||||
"""fexrift.py vrsetting in a fresh process: {"error": code} from SteamVR, or {"error": "..."} if it failed."""
|
||||
arg = json.dumps({"section": section, "key": key, "kind": kind, "value": value})
|
||||
p = subprocess.run([sys.executable, "-I", os.path.join(CORE.HERE, "fexrift.py"), "vrsetting", arg],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
try:
|
||||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||||
except (ValueError, IndexError):
|
||||
return {"error": f"vrsetting exited {p.returncode}"}
|
||||
return out.get("result", {}) if out.get("ok") else {"error": out.get("error", "failed")}
|
||||
|
||||
|
||||
def crash_cleanup():
|
||||
guard = CORE.read_json(guard_path(), {}) or {}
|
||||
try:
|
||||
if guard.get("pgid"):
|
||||
try:
|
||||
os.killpg(int(guard["pgid"]), signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
pass
|
||||
subprocess.run([os.path.join(CORE.GEF, "files/bin/wineserver"), "-k"], env=CORE.wine_env(),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
|
||||
finally:
|
||||
vrsetting_subprocess("power", "pauseCompositorOnStandby", "b", True)
|
||||
if guard.get("screen") is not None:
|
||||
vrsetting_subprocess("power", "turnOffScreensTimeout", "f", float(guard["screen"]))
|
||||
|
||||
|
||||
def bench_guarded(args, launcher):
|
||||
read_fd, write_fd = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0: # child: measure, report through the pipe
|
||||
os.close(read_fd)
|
||||
code = 0
|
||||
try:
|
||||
vr = VR()
|
||||
try:
|
||||
out = {"ok": True, "result": bench_run(args, launcher, vr)}
|
||||
finally:
|
||||
vr.close()
|
||||
except Exception as e: # noqa: BLE001 - reported to the parent as the command's error
|
||||
out, code = {"ok": False, "error": f"{type(e).__name__}: {e}"}, 1
|
||||
with os.fdopen(write_fd, "w") as w:
|
||||
w.write(json.dumps(out))
|
||||
os._exit(code)
|
||||
os.close(write_fd)
|
||||
try:
|
||||
with os.fdopen(read_fd) as r:
|
||||
raw = r.read()
|
||||
_, status = os.waitpid(pid, 0)
|
||||
out = json.loads(raw) if raw.strip() else None
|
||||
if out is None: # crashed or killed: bench_run's own cleanup didn't run
|
||||
crash_cleanup()
|
||||
raise ValueError(f"benchmark process died ({'signal ' + str(-os.waitstatus_to_exitcode(status)) if os.WIFSIGNALED(status) else 'no result'}); display settings restored, game stopped")
|
||||
if not out["ok"]:
|
||||
raise ValueError(out["error"])
|
||||
return out["result"]
|
||||
finally:
|
||||
try:
|
||||
os.remove(guard_path())
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def register(core):
|
||||
global CORE
|
||||
CORE = core
|
||||
core.COMMANDS.update(settings_get=settings_get, settings_set=settings_set, perf_prepare=perf_prepare, bench=bench)
|
||||
core.LAUNCHER_ENV_HOOKS.append(launcher_env)
|
||||
# Use the existing setup step so --steps revive prepares the variants too, without another download.
|
||||
def revive():
|
||||
result = core.step_revive()
|
||||
prepare_scales()
|
||||
return result
|
||||
core.STEPS = [(name, revive if name == "revive" else fn) for name, fn in core.STEPS]
|
||||
+836
-84
File diff suppressed because it is too large.
Load diff
+12
-4
@@ -1,22 +1,30 @@
|
||||
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
|
||||
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
|
||||
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
|
||||
fbc3b0238462d976d234fee6740db92b873c85d6bb22a6b01428e64e9058ed83 ./arm64-v8a/liba32bridge.so
|
||||
c9c3da5cac224c38e2210b781bd12b7f97e02bd6b0ebb0ef85543e4ea4b76671 ./arm64-v8a/liba32rt.so
|
||||
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
|
||||
d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/libfp_langpack.so
|
||||
69102b128e00fa1809489a7308e09c1505f563bf7091bbdd3fe5a58e4cc96c2c ./arm64-v8a/libfp_luajit.so
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
449e13bbe58de6718d4e530177924e09552a95d748a082739899144e2cb3ccf3 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
0f058921e9c1f5b9ef5847cc9e6b80a23e75028c76c1d3bb02d1610b535f81c9 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
1d83eb94ec1df44f29a5f525dce4d5b608dbbb13a9c7a5efeb80a4b1dc54a721 ./arm64-v8a/libvrapi.so
|
||||
47f736dfeffce6aa56be2fc049c2d11b87b0d1f7389db7ee1737fcded9457811 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
9e8a7ce599b62a7c607b0d3637e4ec1b1ea1974c3373a7af3f5920ad2adc5e00 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
|
||||
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
|
||||
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
|
||||
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
|
||||
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
|
||||
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
|
||||
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
3e9099b5015e8f486c00bcea9d111ee721faba355a89bcf1df69561e3dc6325c ./digicert-assured-id-root-ca.der
|
||||
e04db8044185c1c77941252e61794e84af7156c449fedcda4a826d7a9fc91989 ./dlls/crypt32.dll
|
||||
ec1387fad1f9b65503077e5aa24e3a3b60b3b04b85fd9412e0f2c451772f9b25 ./dlls/dnsapi.dll
|
||||
00ebfab0f3a066d9c85820332e0ec91cf4040797e580516dcafa048dfe758aa7 ./dlls/sechost.dll
|
||||
41584c41db41991f20a0b55011efa94618e0817ab351579f793ca01dbb488a0e ./dlls/windows.devices.enumeration.dll
|
||||
420b21045ddc4f166733bf6d2d7227f0131e20f161c4568accb2b9c15a6d8e86 ./fp_mem.so
|
||||
f8d6aaaa123337c47837d0be5852f7f6687bb9c4bfaaa108aaab3f04af1ad5b9 ./fp_pipesend.exe
|
||||
13d455b700b08f40eb3e3452ab264bf535cb3d176c92ec9a3324a635ce4f8588 ./windows.devices.wifi.dll
|
||||
941a65274007db76b1c40880117fe7d881db32c5394ad09f80ccc44a08213a75 ./wineserver.arm64
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
===============================================================================
|
||||
LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
|
||||
|
||||
Copyright (C) 2005-2026 Mike Pall. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
|
||||
|
||||
===============================================================================
|
||||
[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
|
||||
|
||||
Copyright (C) 1994-2012 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
===============================================================================
|
||||
[ LuaJIT includes code from dlmalloc, which has this license statement: ]
|
||||
|
||||
This is a version (aka dlmalloc) of malloc/free/realloc written by
|
||||
Doug Lea and released to the public domain, as explained at
|
||||
https://creativecommons.org/licenses/publicdomain
|
||||
|
||||
===============================================================================
|
||||
@@ -10,12 +10,14 @@ frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-05'
|
||||
known_good_sha256: 522cfebf954e1cab6054798479b0b32c5ba9e38cca83dd98db2ac0f7e3c0802a
|
||||
app: 0.9.1
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 0e55a80ed420395b7eaf8075230fad1fae288f6c25d2ce8cc6aa49899e9b7596
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20260922.6101926'
|
||||
agent: 48
|
||||
issue: 44
|
||||
source_hint: WotW
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 211
|
||||
source_hint: Waltz
|
||||
@@ -4,14 +4,19 @@ tested_version: '1.0'
|
||||
source_hint: Sports Scramble
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
status: unsupported
|
||||
notes: 32-bit only; the Frame has no AArch32.
|
||||
details: 32-bit (armeabi-v7a) only; the Frame has no AArch32.
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
status: works
|
||||
notes: 32-bit only (armeabi-v7a), runs through FramePort's 32-bit bridge (experimental). Both controllers work as
|
||||
Touch controllers. The first start shows a one-time "optimizing" screen (a bit jittery) and then starts the game
|
||||
again by itself.
|
||||
details: 'Unreal Engine 4 NativeActivity with OVRPlugin + VrApi, GLES, OBB. The bridge needed AAsset_getBuffer
|
||||
(copied into guest memory), AKeyEvent_getFlags and the remaining AConfiguration getters. A Santa Cruz (early
|
||||
Quest) build: it requires 6DoF head tracking, so the bridge passes both Frame controllers through as Touch
|
||||
controllers (assets/fp_a32_headset.txt) instead of the Gear VR remote. Unreal restarts the app after its
|
||||
first-run shader precompile; the launcher starts Lepton again for it (agent 86). Plays in the headset (owner,
|
||||
2026-10-13).'
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
pcvr_alternative: Rift only; not on the Revive compatibility list.
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: e2e6788da99e2a38ea8f1e83e997ab44fed5492ce117d99ee3d376e6faeb0de4
|
||||
date: '2026-10-13'
|
||||
min_app: 0.12.1
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.FireproofStudios.GhostTown
|
||||
title: Ghost Town
|
||||
status: works
|
||||
tested_version: '1.01'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-14'
|
||||
known_good_sha256: 248b59ade696bdd4808fc05c2117aa9e8debe133d3d0874a276cc8cb67439f58
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 299
|
||||
source_hint: Ghost Town
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.GenderGames.Ladder
|
||||
title: Ladder
|
||||
status: works
|
||||
tested_version: Release
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 7d23692149d40fe469dbf2e13446ceeb94a8a32807e09ddad7731158de9be0a2
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
issue: 243
|
||||
source_hint: Ladder
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.HarmonixMusic.DCVRQuest
|
||||
title: Dance Central
|
||||
status: works
|
||||
tested_version: 1.2.2
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: cbaf2eb594cb25d6a345b50612f9e7ca3e2b2f05abaa072632e8fc0d91277c61
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 228
|
||||
source_hint: com.HarmonixMusic.DCVRQuest
|
||||
@@ -13,8 +13,9 @@ details: Unreal Engine 4 (GLES). Found and verified in gameplay on a Frame by Kl
|
||||
the captured modules are from Lepton 2.8.14 and stop matching if a Frame update changes the driver's output -
|
||||
the layer logs that). OVRPlugin asks for poses at its own clock (pose_time_fix). Unreal's Oculus input animates the
|
||||
thumbs from near-touch, which the Frame never reports (frame.unreal_thumb_touch reads the touches instead; binding
|
||||
the proximity to the touch inputs in FrameBridge didn't reach the game). The Frame's SteamVR runtime
|
||||
leaks about 20 MB a second while the game runs (reported to Valve).
|
||||
the proximity to the touch inputs in FrameBridge didn't reach the game). The game leaves GL errors
|
||||
pending; the runtime's GL check took one as its own failure and then allocated a command buffer every frame
|
||||
(~20 MB a second until the frame rate collapsed). gl_error_clear clears them first (GitHub #285).
|
||||
tested_version: 1.1.1+667256.cl.387770
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
@@ -27,10 +28,11 @@ frame:
|
||||
- frame.zink_shader_fix
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
gl_error_clear: 1
|
||||
zink_shader_fix: 12016:b2919629761ad268e0b21c72ac8520f81b76e72c3adc26897f97eae9a3e70a66:3092:0x0003003e,166,82,0x0003003e,177,82,0x0003003e,183,82,0x0003003e,189,82,0x0003003e,256,82,0x0003003e,266,82,0x0003003e,272,82,0x0003003e,278,82,0x0003003e,318,82,0x0003003e,328,82,0x0003003e,334,82,0x0003003e,340,82;11436:6f18be49f2aaa2452f962eb16f7af744c5c22a7b25f37ce681e0d6399e35a869:2932:0x0003003e,164,80,0x0003003e,175,80,0x0003003e,181,80,0x0003003e,187,80,0x0003003e,254,80,0x0003003e,264,80,0x0003003e,270,80,0x0003003e,276,80,0x0003003e,316,80,0x0003003e,326,80,0x0003003e,332,80,0x0003003e,338,80
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
issue: 49
|
||||
updated: '2026-10-09'
|
||||
updated: '2026-10-14'
|
||||
min_app: 0.12.1
|
||||
source_hint: Vader Immortal- Episode I
|
||||
@@ -18,7 +18,9 @@ frame:
|
||||
- frame.unreal_thumb_touch
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
gl_error_clear: 1
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
updated: '2026-10-14'
|
||||
min_app: 0.12.1
|
||||
source_hint: Vader Immortal- Episode II
|
||||
@@ -18,7 +18,9 @@ frame:
|
||||
- frame.unreal_thumb_touch
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
gl_error_clear: 1
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
updated: '2026-10-14'
|
||||
min_app: 0.12.1
|
||||
source_hint: Vader Immortal- Episode III
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.InnoverseLtd.FrenzyVR
|
||||
title: FrenzyVR
|
||||
status: works
|
||||
notes: Works, runs good
|
||||
tested_version: '107'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 0d87e78dcbe1c705af0d01cda8eabbea514849f7403e5904f0ef49b5cf0653fb
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 216
|
||||
source_hint: Frenzy VR
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.LocoMotionDevs.Superfly
|
||||
title: Superfly
|
||||
status: works
|
||||
notes: Works, take a bit to launch but works
|
||||
tested_version: 1.3.3
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: e180b6c58b80c1c2d0cc49e75d645a41d3d4bc63ed7ffe1e6f91eac51e486a17
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 217
|
||||
source_hint: Superfly
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.MeatSpace.Nakatomi
|
||||
title: Vendetta Forever
|
||||
status: works
|
||||
tested_version: 1.0.4.0
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.bhaptics_permission
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 72885ad42f1b730a1bd7fad271b87665914e3cf14fde60409395868458ac7b06
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 321
|
||||
source_hint: com.MeatSpace.Nakatomi
|
||||
@@ -0,0 +1,19 @@
|
||||
package: com.NiVision.RacketPinball
|
||||
title: RacketPinball
|
||||
status: works
|
||||
tested_version: '2.06'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: 80e850f81c24112e8c35a560a333846163a3e783da391290db7663b9015ca58f
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
issue: 262
|
||||
source_hint: RacketPinball
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.PotamWorks.SmashDrums
|
||||
title: Smash Drums
|
||||
status: works
|
||||
tested_version: 5.2.1.4
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.bhaptics_permission
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 67d36cdcadaf14c2f744849ae25af4f92bf8ea14ff935318562f40d7f2ddc88f
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 320
|
||||
source_hint: com.PotamWorks.SmashDrums
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.PubTest.PubTest
|
||||
title: Drunkn Bar Fight 2 - Early Access
|
||||
status: works
|
||||
notes: Game launches and plays fine
|
||||
tested_version: '0.515'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: f340b5ba70ee9368ead6bfe2c287eda6c5686280ddb8e12b8c9a8d720326de36
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 218
|
||||
source_hint: Drunkn Bar Fight 2 - Early Access
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.RuneSkovboJohansen.EyeOfTheTemple
|
||||
title: Eye of the Temple
|
||||
status: works
|
||||
tested_version: '1.528'
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.hw_video_decode
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 78ad96eaf4383b02666e0a1af99c5985f718f97982466e5dca9ebb636974af00
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
issue: 302
|
||||
source_hint: Eye of the Temple
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.StatusPro.NFLProEra2
|
||||
title: NFL PRO ERA II
|
||||
status: works
|
||||
tested_version: 2.4.0
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: caf2e8f148ca4929ba5e73112fa7e0958c00d7ee0c5f28926c3f39714db6c923
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.3
|
||||
issue: 226
|
||||
source_hint: com.StatusPro.NFLProEra2
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.TacticalNounours.QuantumVoid
|
||||
title: Quantum Void
|
||||
status: issues
|
||||
notes: no hands
|
||||
tested_version: '1.11'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.bhaptics_permission
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
foveation: tracked
|
||||
verified:
|
||||
date: '2026-10-14'
|
||||
known_good_sha256: 1c7afccbd627b9bd2342884679aeca0efc2429df700f3c73ec162498db320098
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 307
|
||||
source_hint: Quantum Void
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.TinyBuild.killitwithfirevr
|
||||
title: Kill It With Fire VR
|
||||
status: works
|
||||
notes: Works
|
||||
tested_version: 1.0.5.4208
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: fcadbdaf3255e2909edf763d0c61fb55808ba448d4f277e91ba37b0a0ff9f497
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 219
|
||||
source_hint: Kill It With Fire VR
|
||||
@@ -4,9 +4,9 @@ tested_version: MAIN.450412.207706.final
|
||||
source_hint: Assassins Creed Nexus VR
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
status: issues
|
||||
notes: Some launch warning text is still upside down; the rest of the UI is fixed by flip emulation. Ubisoft login
|
||||
may not work.
|
||||
status: works
|
||||
notes: FrameBridge turns its upside-down UI panels upright (flip emulation, in place since FramePort 1.2.0). Ubisoft
|
||||
login may not work.
|
||||
details: Ubisoft online login may not work. Data folder holds raw assets (start0..2, .climb, bundles), not .obb
|
||||
files; install to obb as-is.
|
||||
overport_extra:
|
||||
@@ -17,3 +17,6 @@ verified:
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 89644a12bf8df956fa3d3a9a40540ba9e1fb62f63494714a05c9f05984383ac8
|
||||
no_appsw_90: '2026-10-02, OVRPort 1.2.5 (owner: 90 Hz felt better than 72 Hz; same known issues)'
|
||||
flip_in_place: '2026-10-14, owner in the headset: launch warnings and tutorial pop-ups upright (GitHub #138)'
|
||||
updated: '2026-10-14'
|
||||
min_app: 1.2.0
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.Virtuallyz.OperationWolf
|
||||
title: 'Operation Wolf Returns: First Mission VR'
|
||||
status: works
|
||||
tested_version: '0.28'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: aff311f10d3e938e1eeef22263da7fe84ca38f25786ca4483543596de335c702
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 318
|
||||
source_hint: com.Virtuallyz.OperationWolf
|
||||
@@ -0,0 +1,19 @@
|
||||
package: com.Virtuos.R6DLM
|
||||
title: 'Medal of Honor: Above and Beyond'
|
||||
status: unknown
|
||||
notes: 'Not yet tested on the Frame (the game is ~41 GB, too large to install on the dev unit). Its first-run
|
||||
Downloader screen is invisible in Lepton, so it stayed stuck there; frame.unreal_skip_obb_delivery skips the check.
|
||||
The OBB files still have to be present. Please report back whether it reaches the game (GitHub 159).'
|
||||
details: 'Medal of Honor packs its data in OBBs behind Epic''s DownloaderActivity, whose name/size check is
|
||||
obfuscated (its methods are renamed), so FramePort didn''t recognise it and the "skip the OBB delivery check"
|
||||
option never appeared. Detection now recognises the obfuscated predicate and frame.unreal_skip_obb_delivery
|
||||
rewrites it to pass. frame.hw_video_decode gives the game''s videos the Frame''s hardware decoder.'
|
||||
tested_version: '0.518'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
- frame.unreal_skip_obb_delivery
|
||||
updated: '2026-10-15'
|
||||
min_app: 1.2.0
|
||||
source_hint: Medal of Honor
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.WfelisCorp.MunkrFunkrTheLegacyDays
|
||||
title: Munkr Funkr (The Legacy Days)
|
||||
status: works
|
||||
tested_version: '2'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 94079c1a2a802322f907058964c4ebbb22edba1dc0f3ec26e3c121ed9d8b0379
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 241
|
||||
source_hint: Munkrfunkr
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.WhiteDoorGames.Cosmodread
|
||||
title: Cosmodread
|
||||
status: works
|
||||
notes: First run wouldn't allow you to interact with the "New Game" button. Restarting it fixed it.
|
||||
tested_version: 1.0.0.28
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 44911a4607d0de2770705eefb6339a4c7353cb5dfb49f389d6731edbb9cbea92
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 174
|
||||
source_hint: Cosmodread
|
||||
@@ -5,6 +5,8 @@ notes: Needed the LAUNCHER category fix.
|
||||
details: Source APK is a community 'patch+savefix+90Hz' build. Shares Vader Immortal's Unreal 4 Oculus input (GitHub
|
||||
49) - thumbs follow the touches (frame.unreal_thumb_touch) and controller poses are located at the right time
|
||||
(pose_time_fix); added 2026-10-09 from Vader's headset results, not yet checked in this game.
|
||||
sync_guard added 2026-10-11 for a vrclient xrSyncActions crash on the loading splash after the intro (GitHub
|
||||
#188; the same runtime race as Myst).
|
||||
tested_version: '1.0'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
@@ -14,10 +16,11 @@ frame:
|
||||
- frame.unreal_thumb_touch
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
sync_guard: 1
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 4ce6e3563da131e288645f4b62a634ac98043e55bc42b7c0ad5f461207d5f53c
|
||||
updated: '2026-10-09'
|
||||
updated: '2026-10-11'
|
||||
source_hint: Robo Recall- Unplugged
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.arkio.arkio
|
||||
title: Arkio
|
||||
status: works
|
||||
notes: The quest version allows for passthrough mode
|
||||
tested_version: 2.3.1
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
overport_extra:
|
||||
- patch_force_passthrough
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: e17f9b2dc56cb49b4469d81f2e075ca5953f73152804ff1a00fba0931048e103
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 266
|
||||
source_hint: com.arkio.arkio
|
||||
@@ -2,19 +2,22 @@ package: com.beatgames.beatsaber
|
||||
title: Beat Saber
|
||||
status: works
|
||||
notes: Purchased DLC isn't detected (ownership comes from Meta's servers).
|
||||
tested_version: 1.40.8_7379
|
||||
tested_version: 1.45.2_28848
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
date: '2026-10-03'
|
||||
known_good_sha256: ee720e040931f2547f7611e58ec5ad72d86e800dd4cf3135f576019683f2f359
|
||||
app: 0.6.1
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: b7976f36d39b22eb0fd9c8eb6cce204125430cd2a3e2bfab8ba5d1f7c86f0ac6
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20260930.6234839'
|
||||
agent: 33
|
||||
issue: 6
|
||||
source_hint: Beat Saber
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 210
|
||||
source_hint: beat-saber
|
||||
@@ -16,11 +16,12 @@ frame:
|
||||
adapter:
|
||||
sync_guard: 1
|
||||
surface_native: 1
|
||||
fade_quad: 1
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
|
||||
updated: '2026-10-10'
|
||||
min_app: 0.12.1
|
||||
updated: '2026-10-13'
|
||||
min_app: 1.1.0
|
||||
source_hint: Batman- Arkham Shadow
|
||||
@@ -8,6 +8,8 @@ frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
fade_quad: 1
|
||||
verified:
|
||||
date: '2026-10-05'
|
||||
known_good_sha256: 41e0ab90f622afd50accb0b6e2553520d30a6cc82e5ab4cbb2341b48f7316a71
|
||||
@@ -15,3 +17,5 @@ verified:
|
||||
overport_cli: 1.2.5
|
||||
issue: 47
|
||||
source_hint: Marvel's Iron Man VR
|
||||
updated: '2026-10-13'
|
||||
min_app: 1.1.0
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.ccpgames.gunjack
|
||||
title: Gunjack
|
||||
status: works
|
||||
notes: 'Gear VR game (32-bit only), runs through FramePort''s 32-bit bridge (experimental). Controls (right
|
||||
controller): trigger = fire (hold the touchpad), A = tap, stick down = reload (swipe back), stick up = special
|
||||
weapon (swipe forward), B = back.'
|
||||
details: 'Unreal Engine 4.10 NativeActivity, VrApi, GLES, OBB inside the APK (assets/main.obb.png). The bridge answers
|
||||
the game''s APK signature check with the original APK''s certificate and keeps the GLES session when Unreal leaves
|
||||
and re-enters VR after loading. Controller input goes into the game''s AInputQueue as Gear VR touchpad events.
|
||||
Runs in the headset (owner, 2026-10-12).'
|
||||
tested_version: 1.4.1
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
headset: gearvr
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
min_app: 0.12.1
|
||||
source_hint: Gunjack (Gear VR)
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.coatsink.augmentedempireremastered
|
||||
title: AugmentedEmpire
|
||||
status: works
|
||||
tested_version: '1.1'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: e07afd561c305493cf09efe9c869be892e475a48538dfdf617e33a0be2e6c678
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 311
|
||||
source_hint: com.coatsink.augmentedempireremastered
|
||||
@@ -4,13 +4,16 @@ tested_version: '1.4'
|
||||
source_hint: Shadow Point
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
status: unsupported
|
||||
notes: 32-bit only; the Frame has no AArch32.
|
||||
details: 32-bit (armeabi-v7a) only; the Frame has no AArch32. 64-bit conversion was researched and dropped (IL2CPP
|
||||
+ FMOD + custom rasterizer).
|
||||
status: works
|
||||
notes: 32-bit only (armeabi-v7a), runs through FramePort's 32-bit bridge (experimental). Plays in the headset.
|
||||
details: 'Unity with OVRPlugin + VrApi, GLES. Needed the bridge''s vrapi_GetTrackingTransform/SetTrackingTransform
|
||||
(the game sets its floor level through them; without them the player stood below the floor). Plays in the headset
|
||||
(owner, 2026-10-14).'
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
device:
|
||||
- device.text_input_window
|
||||
pcvr_alternative: Rift version via Revive + SteamVR.
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: ea0c0d09d2c3d8da04d165732ead64875fef7573a5739608de0de9c93654007e
|
||||
date: '2026-10-14'
|
||||
min_app: 1.2.0
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.drawandcode.TaskmasterVR
|
||||
title: Taskmaster
|
||||
status: works
|
||||
notes: Launched and played a few tasks. Seems to work pretty much flawlessly other than some occasionally finicky
|
||||
grip detection - very possible that this is just the game itself though
|
||||
tested_version: 1.0.17557.29562
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.oculusos
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: e3b3c49d77284afb76e9f960828cbfaa4250d66c11b7fd9e4053d489aca662ae
|
||||
app: 1.0.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 78
|
||||
issue: 183
|
||||
source_hint: Taskmaster VR
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.edde746.plezy
|
||||
title: Plezy
|
||||
status: issues
|
||||
notes: Some videos play with strong artifacts and wrong colours; the cause is unknown, possibly hardware video decoding.
|
||||
Otherwise works.
|
||||
tested_version: 2.22.0
|
||||
engine: Other
|
||||
xr: '?'
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
as_is: true
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: 893ae7c01d3d7688b27cbf4e7fc1260fb617614baeaf378fe5ca0af41febc92f
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 270
|
||||
source_hint: plezy
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.fallen.affected
|
||||
title: 'AFFECTED: The Manor'
|
||||
status: issues
|
||||
notes: 'Gear VR game (32-bit only), runs through FramePort''s 32-bit bridge (experimental). Controls:
|
||||
left stick = walk and strafe (the touchpad), right stick sideways = turn, stick press or A = touchpad click, right
|
||||
trigger = trigger, B = back.'
|
||||
details: 'Unity 5.6 + Mono, VrApi 1.0.4, GLES. The bridge translates its 32-bit libraries and VrApi to the 64-bit VrApi
|
||||
bridge on OpenXR. Picture and sound confirmed in the headset by the owner (2026-10-10); the controller mapping was
|
||||
reworked afterwards and is not yet confirmed.'
|
||||
tested_version: '1.52'
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
headset: gearvr
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-10'
|
||||
min_app: 0.12.1
|
||||
source_hint: AFFECTED The Manor (Gear VR)
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.farbridge.vastreaches
|
||||
title: 'Homeworld: Vast Reaches'
|
||||
status: issues
|
||||
notes: The game starts but after selecting to start a new game it seems to crash.
|
||||
tested_version: 1.2.0-25215
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 6c5cb4b56fd01d20cb854e834b1611c2b2e8f10d889585606e631104d2a9323e
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 314
|
||||
source_hint: com.farbridge.vastreaches
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.fulldivegames.majorasmaskvr
|
||||
title: Majora's Mask VR
|
||||
status: issues
|
||||
notes: Audio on the file select screen sounds crunchy. Controls may need rebinding in the game. Quest build from https://github.com/fulldivegames/Majoras-Mask-VR.
|
||||
tested_version: '0.36'
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.sdl_clipboard
|
||||
adapter:
|
||||
refresh_rate: 144.0
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: 220096c70fb515ec7b8db1cc7a9075836b4d09f7df63026908af3c30e9fdb7e5
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 268
|
||||
source_hint: MMVR-Quest-0.36
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.greensky.swarm2
|
||||
title: Swarm 2
|
||||
status: issues
|
||||
notes: MSAA creates visible seams around edge geometry and also the skybox edge
|
||||
tested_version: '8472'
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20260922.6101926'
|
||||
agent: 79
|
||||
issue: 224
|
||||
source_hint: SWARM 2
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.halfbrick.fruitninjavr
|
||||
title: Fruit Ninja
|
||||
status: works
|
||||
notes: Works, launches and plays
|
||||
tested_version: 1.7.3.696775
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.hw_video_decode
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 5afd1383c6f4aab50425bea955202c74e1706a8a8ba4219689ef5b6e0c984318
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 220
|
||||
source_hint: Fruit Ninja
|
||||
@@ -1,15 +1,22 @@
|
||||
package: com.ilmxlab.tales
|
||||
title: 'Star Wars: Tales from the Galaxy''s Edge'
|
||||
status: unknown
|
||||
notes: Not working yet - after the intro logos and a loading animation the picture goes black (GitHub issue 61).
|
||||
Gets the controller fixes found for Vader Immortal (same studio) to test.
|
||||
status: works
|
||||
notes: >-
|
||||
Headset tested for GitHub issue 61: full view, loading into gameplay, both eyes, controller input and sound
|
||||
work with the splash bypass and multiview depth-size fix. Keep the main OBB and all three content/sound-bank PAKs.
|
||||
details: Unreal Engine 4 (GLES) by ILMxLAB, like Vader Immortal, but a different engine build - it has none of Vader's
|
||||
Quest-only branches (no GetQuestShaderPrecompilePercent or RPOC key map; it precompiles shaders through Unreal's
|
||||
own pipeline cache), so frame.unreal_quest_precompile/_keymap don't apply. Its seasons and Wwise banks are Meta
|
||||
platform asset files (frame.asset_files). Vader's OVRPlugin findings apply to it as well - poses asked for at
|
||||
OVRPlugin's own clock (pose_time_fix) and thumbs from near-touch, which the Frame never reports (frame.unreal_thumb_touch).
|
||||
Ruled out for the black picture so far (see the diagnostics in issue 61) - asset-file paks, Valve's foveation,
|
||||
GL errors; the game's own eye image reads back black.
|
||||
Map travel stalls with the game thread waiting in FlushRenderingCommands and the render thread waiting in
|
||||
FSplash::RenderFrame_RenderThread / ovrp_WaitToBeginFrame. frame.unreal_splash_bypass lets loading finish.
|
||||
SideQuest backups with sibling apk/ and obb/ folders must include all three PAKs; old scans omitted them and
|
||||
lost sound. The scanner and library refresh recover that layout. Eye clipping came from cached Android window
|
||||
dimensions for the depth attachment (1920x1016 versus the 1728x1728 eye color texture).
|
||||
frame.unreal_multiview_depth refreshes both cached dimensions from the current eye render size; multiview and
|
||||
multisampling remain enabled. The two engine fixes check exported symbols, function sizes and instruction bytes
|
||||
before changing one instruction each; unrecognized engine builds are left unchanged.
|
||||
tested_version: 1.1.9+734762.cl.439699
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
@@ -18,10 +25,12 @@ alt_overport:
|
||||
frame:
|
||||
- frame.asset_files
|
||||
- frame.unreal_thumb_touch
|
||||
- frame.unreal_splash_bypass
|
||||
- frame.unreal_multiview_depth
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
issue: 61
|
||||
updated: '2026-10-09'
|
||||
min_app: 0.12.1
|
||||
updated: '2026-10-12'
|
||||
min_app: 1.0.4
|
||||
source_hint: Star Wars- Tales from the Galaxys Edge
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.innerspacevr.afishermanstale
|
||||
title: Fisherman
|
||||
status: issues
|
||||
notes: Loading screens have corruption, but everything works otherwise.
|
||||
tested_version: '1.087'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: c645ca6321845bd460b4ff1b8ac871e941adb26d280cccba0285e79e4a96a231
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 175
|
||||
source_hint: A Fisherman's Tale
|
||||
@@ -0,0 +1,26 @@
|
||||
package: com.nDreams.Ghostbusters
|
||||
title: 'Ghostbusters: Rise of the Ghost Lord'
|
||||
status: works
|
||||
notes: 'Needs the game''s offline mode: it is built for online play, and without a profile the player name is a random
|
||||
number. The flat loading screen floats in front of you instead of following your head, so assets can be seen loading
|
||||
in behind it.'
|
||||
tested_version: 1.325635.prod.shipping
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_ovrp_entrypoints
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-14'
|
||||
known_good_sha256: 7556a78c8772aed70c38fc4781677c0900e9dfd4be085a79c421a31323af7888
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 279
|
||||
source_hint: GBRotgl
|
||||
@@ -0,0 +1,26 @@
|
||||
package: com.nextgenreality.mmrx
|
||||
title: Mini Motor Racing X
|
||||
status: works
|
||||
tested_version: 1.3.6
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: f1df011117c918794b9d9da0c5a170fc7ac7cb7ec10eddc2dd4db649d855a531
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 317
|
||||
source_hint: com.nextgenreality.mmrx
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.notdead.COMPOUNDQUEST
|
||||
title: COMPOUND
|
||||
status: works
|
||||
tested_version: 1.0.12
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: 5da7aac9863412daec7ba909dcfd2d5cfa6cb5d078150011c006165058cd76bd
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 260
|
||||
source_hint: COMPOUND
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.othergate.doe
|
||||
title: Dungeons of Eternity
|
||||
status: issues
|
||||
notes: It can not connect to servers
|
||||
tested_version: '1.3'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.vivox_audio_route
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 006a53b81988f1bb088ccb0847c9e4085ee89743ed6b4d5acf161abf99e30365
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 206
|
||||
source_hint: com.othergate.doe
|
||||
@@ -1,13 +1,12 @@
|
||||
package: com.owlchemylabs.jobsimulator
|
||||
title: Job Simulator
|
||||
status: works
|
||||
notes: 'Without Lepton''s Android window: with it, the hands froze after a few seconds (GitHub #193).'
|
||||
tested_version: 1.4.0.6515
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
frame_remove:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-03'
|
||||
known_good_sha256: 64638d7f93bb93cc61f422a43a08bb0d35b9bb4b463e90a42da7002147f03d1b
|
||||
@@ -16,4 +15,5 @@ verified:
|
||||
frame_build: '20260930.6234839'
|
||||
agent: 33
|
||||
issue: 11
|
||||
updated: '2026-10-11'
|
||||
source_hint: Job Simulator
|
||||
@@ -1,14 +1,13 @@
|
||||
package: com.owlchemylabs.vacationsimulator
|
||||
title: Vacation Simulator
|
||||
status: issues
|
||||
notes: Controllers aren't tracked (reported; not yet diagnosed).
|
||||
status: works
|
||||
notes: 'Without Lepton''s Android window: with it (or after clicking it), the hands stayed on the floor (GitHub
|
||||
#163).'
|
||||
tested_version: 1.3.0.40142
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
frame_remove:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
refresh_rate: 90.0
|
||||
@@ -20,4 +19,5 @@ verified:
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 163
|
||||
updated: '2026-10-11'
|
||||
source_hint: com.owlchemylabs.vacationsimulator
|
||||
@@ -1,12 +1,15 @@
|
||||
package: com.parkline.wander
|
||||
title: Wander
|
||||
status: works
|
||||
tested_version: '0.3569'
|
||||
notes: 0.3570 (Unity 6.5) runs out of thread keys at start without the thread-key pool (fix found by Klownicle,
|
||||
GitHub #198).
|
||||
tested_version: '0.3570'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_tls_key_pool
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
@@ -17,4 +20,5 @@ verified:
|
||||
frame_build: '20260930.6234839'
|
||||
agent: 35
|
||||
issue: 17
|
||||
updated: '2026-10-12'
|
||||
source_hint: Wander
|
||||
@@ -0,0 +1,27 @@
|
||||
package: com.picselica.OceanRiftFullQuest
|
||||
title: Ocean Rift
|
||||
status: works
|
||||
notes: eye tracked foveated rendering works great!
|
||||
tested_version: '2.09'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
controller_models: 1
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
foveation: tracked
|
||||
verified:
|
||||
date: '2026-10-14'
|
||||
known_good_sha256: 63020505f27af3d463798679999000e2d9bc671e72913138a16171461e3c95d2
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 304
|
||||
source_hint: Ocean Rift
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.polyarc.Moss2
|
||||
title: 'Moss: Book II'
|
||||
status: works
|
||||
notes: Just played the very begining but controlls worked. Did nothing other than install it.
|
||||
tested_version: 1.0.3.149619
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 686db536ced97d8a151c82955174ec01d67987c466ecd326d3deb557d8c4fd1d
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 248
|
||||
source_hint: Moss- Book II
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.polyarc.MossGame
|
||||
title: Moss
|
||||
status: works
|
||||
notes: The initial loading image looks glitchy, but otherwise it works well.
|
||||
tested_version: 1.0.3.151047
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 4aaae261dcc67de1d04309e31c0ea17dbc980b431adc8ce1917ae72ac4f9d417
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 91
|
||||
issue: 315
|
||||
source_hint: com.polyarc.MossGame
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.poncle.vampiresurvivorsvr
|
||||
title: Vampire Survivors
|
||||
status: works
|
||||
tested_version: 1.0.717
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 42c3c18fd6b69a79d9e337d359aa9940428b9193a9ffc3ef987571be6aa6901a
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 234
|
||||
source_hint: Vampire Survivors
|
||||
@@ -0,0 +1,22 @@
|
||||
package: com.resolutiongames.demeo
|
||||
title: Demeo
|
||||
status: works
|
||||
tested_version: 1.42.40696-release
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: adae217584c956e5574c5248868afdbb50a1f3ffd0609783215412d39b54e9ee
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 313
|
||||
source_hint: com.resolutiongames.demeo
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.resolutiongames.jade
|
||||
title: Battlemarked
|
||||
status: works
|
||||
tested_version: 7.2.39817-release
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 670c7c16ec8d85b3865b120d143143dc73387ec325feaa028320608b1f377e7d
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 312
|
||||
source_hint: com.resolutiongames.jade
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.schellgames.ieytd3
|
||||
title: IEYTD3
|
||||
status: works
|
||||
tested_version: 1.0.1.211419 (ReleaseCandidate_1.0.0_QuestStore)
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 06f901ae22f7c07a79e11fc1af6b3941a51bbc99624336d6a0939f9593f3bfa0
|
||||
app: 0.12.1.dev263
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 78
|
||||
issue: 181
|
||||
source_hint: 2026-10-08T23-57-41.310Z_211419
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.themunky.drunknbarfightQuest
|
||||
title: Drunkn Bar Fight
|
||||
status: works
|
||||
notes: Works
|
||||
tested_version: '11.00'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: a66895bf0fde60a7ccd9bea6b8b1e922650cf1692fe26c1d08bee5b5c866b8b6
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 222
|
||||
source_hint: Drunkn Bar Fight
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.viewerready.totallybaseball
|
||||
title: Totally Baseball
|
||||
status: works
|
||||
notes: Game Launches and works
|
||||
tested_version: '2.5886'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.oculusos
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 80d53ee2c59cd1846e42a1c309bd8b0c365841d0aaea4092854fa01d2902a839
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 223
|
||||
source_hint: TOTALLY BASEBALL
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.voidroom.TeaForGodEmperor
|
||||
title: Tea For God
|
||||
status: works
|
||||
tested_version: 1.1.1
|
||||
engine: Other
|
||||
xr: OpenXR+VrApi
|
||||
frame:
|
||||
- frame.bhaptics_permission
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
equirect_emul: 1
|
||||
foveation: tracked
|
||||
verified:
|
||||
date: '2026-10-14'
|
||||
known_good_sha256: 89b0b20d5a5b414604163def76b50aaed26961ac283dab08ff7eb469f5bacde6
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 295
|
||||
source_hint: Tea For God
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.vrmonkey.skyclimb
|
||||
title: MacacoVR
|
||||
status: issues
|
||||
notes: 2d loading screens are glitchy, otherwise main gameplay is fine
|
||||
tested_version: '1.1325'
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: 78de47cf0200a6e8e06feeef4a02da0737393b5007e71ba8f62f01dfa93cec0a
|
||||
app: 1.0.4
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 236
|
||||
source_hint: MacacoVR
|
||||
@@ -0,0 +1,23 @@
|
||||
package: com.wanadev.ragnarockquest
|
||||
title: Ragnarock
|
||||
status: issues
|
||||
tested_version: 2.8.0
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.bhaptics_permission
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 365f26d9a4991e2340311dfb40d14f097a066653f97fff55c632aa1876519d70
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 319
|
||||
source_hint: com.wanadev.ragnarockquest
|
||||
@@ -4,14 +4,17 @@ tested_version: 1.0.479431
|
||||
source_hint: Journey of the Gods
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
status: unsupported
|
||||
notes: 32-bit only; the Frame has no AArch32.
|
||||
details: 32-bit (armeabi-v7a) only; the Frame has no AArch32. UE4 cannot be rebuilt for arm64.
|
||||
status: works
|
||||
notes: 32-bit only (armeabi-v7a), runs through FramePort's 32-bit bridge (experimental). Plays in the headset.
|
||||
details: 'Unreal Engine 4 NativeActivity with OVRPlugin + VrApi, GLES. Needed DT_ANDROID_RELR relocations in the 32-bit
|
||||
loader (it crashed at start) and the VrApi bridge''s frame pacing: the Frame''s runtime can return from
|
||||
xrWaitFrame early, and this light game then ran at ~120 fps on a 90 Hz display, so the scene slid and snapped
|
||||
back. Plays in the headset (owner, 2026-10-14).'
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
pcvr_alternative: Rift (Meta PC) version via Revive + SteamVR.
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: e85141e3e9a8a3226f653d8ffbd21b44268101f3a207f56e657454a3639b3201
|
||||
date: '2026-10-14'
|
||||
min_app: 1.2.0
|
||||
@@ -0,0 +1,22 @@
|
||||
package: io.doublejack.maestro
|
||||
title: Maestro
|
||||
status: works
|
||||
tested_version: 0.0.397
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
known_good_sha256: 4ca71bf4464691ab2c675fbeca1ecc5f4d34599c0a810fd3a67374c91d47f6ee
|
||||
app: 1.1.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 88
|
||||
issue: 316
|
||||
source_hint: io.doublejack.maestro
|
||||
@@ -0,0 +1,21 @@
|
||||
package: net.mobilemischief.spunkydungeon
|
||||
title: Spunky Dungeon
|
||||
status: issues
|
||||
notes: 'Gear VR / Oculus Go game (32-bit only), runs through FramePort''s 32-bit bridge (experimental).
|
||||
Controls: left stick = walk and strafe (the touchpad), right stick sideways = turn, stick press or A = touchpad
|
||||
click, right trigger = trigger, B = back.'
|
||||
details: 'Unity 2019.2 + Mono, OVRPlugin 1.46 on VrApi 1.31, GLES, Meta Platform SDK (OVRPort''s 32-bit dispatcher).
|
||||
Played in the headset by the owner (2026-10-10): picture at 72 fps, floor-level tracking and walking work; turning
|
||||
without a stick press was added afterwards and is not yet confirmed.'
|
||||
tested_version: '1.4'
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
headset: gearvr
|
||||
frame:
|
||||
- frame.a32_bridge
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-10'
|
||||
min_app: 0.12.1
|
||||
source_hint: Spunky Dungeon
|
||||
@@ -0,0 +1,24 @@
|
||||
package: pl.idreams.zombirds
|
||||
title: Shoot The Zombirds VR
|
||||
status: works
|
||||
tested_version: 1.0.2
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: 2d06d1b6aac83d232e9f559c1cd3188642e7a34ef515605763ee7afa04730156
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 85
|
||||
issue: 259
|
||||
source_hint: Shoot The Zombirds
|
||||
@@ -1,11 +1,16 @@
|
||||
package: quest.eleven.forfunlabs
|
||||
title: Eleven Table Tennis
|
||||
status: unsupported
|
||||
notes: Stops at the splash screen. The game needs Meta's online services after start, which aren't available on
|
||||
the Frame.
|
||||
status: works
|
||||
notes: Works great for me, tried the tutorial and an online match. Quest 2 apk.
|
||||
tested_version: 0.436.37_2026.9.4.18.35.33.415
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
pcvr_alternative: The PC VR (Rift) version works on the Frame through FramePort.
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-10'
|
||||
issue: 160
|
||||
date: '2026-10-13'
|
||||
known_good_sha256: f4d996db1c913639f8b981bf87834e053ee2d9267383da3c1a8372ba755e5ac8
|
||||
app: 1.1.0
|
||||
overport_cli: 1.2.5
|
||||
issue: 267
|
||||
source_hint: quest.eleven.forfunlabs
|
||||
@@ -0,0 +1,25 @@
|
||||
package: xyz.TheAviary.Vermillion
|
||||
title: Vermillion
|
||||
status: issues
|
||||
notes: Runs. The canvas used to hide the hand and brush while painting (the Frame draws the game's canvas layer in
|
||||
front of everything); the recipe makes the game draw canvases in the scene instead (fix found by Klownicle, GitHub
|
||||
#200). Not yet confirmed in the headset with this patch.
|
||||
tested_version: 1.7.2
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.vermillion_scene_canvas
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
app: 1.0.1
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 79
|
||||
issue: 200
|
||||
updated: '2026-10-12'
|
||||
source_hint: Vermillion - VR Painting
|
||||
+147
-7
@@ -6,6 +6,80 @@
|
||||
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
|
||||
# Sources: every failure we hit porting 34 games to the Steam Frame (see docs/PLAYBOOK.md).
|
||||
signatures:
|
||||
# FEX Rift compatibility (stream C): PC runtime/renderer failures; never suggest entitlement workarounds.
|
||||
- id: fex-missing-entitlement
|
||||
kind: pcvr
|
||||
pattern: '(?i:Missing entitlement)'
|
||||
severity: info
|
||||
diagnosis: Meta reports that the signed-in account does not own this game. This is not a FramePort bug. Use an account that owns it; entitlement and Platform SDK checks remain intact.
|
||||
suggest: []
|
||||
- id: fex-registration-missing
|
||||
kind: pcvr
|
||||
pattern: '(?i:OVRServer[^\r\n]*install_available|install_available[^\r\n]*OVRServer)'
|
||||
severity: error
|
||||
diagnosis: Meta's runtime sees the app as available to install rather than registered as installed. Reinstall or register the game through the FEX install flow with its matching Meta manifest, then retry.
|
||||
suggest: []
|
||||
- id: fex-imu-fallback
|
||||
kind: pcvr
|
||||
pattern: 'Rejecting new committed universe since IMU fallback is active'
|
||||
severity: warning
|
||||
diagnosis: SteamVR has not acquired headset tracking since boot. Put the headset on once, wait for tracking, then retry the FEX game. A headless launch before that can wait forever.
|
||||
suggest: []
|
||||
- id: fex-librarian-stuck
|
||||
kind: pcvr
|
||||
pattern: '(?s)\A(?=.*(?i:Service invocation completed))(?=.*OVRLibrarian[^\r\n]*(?i:still running|did not exit))'
|
||||
severity: warning
|
||||
diagnosis: OVRLibrarian completed its work but remains alive under FEX and blocks later client registration. End only OVRLibrarian.exe after completion, then retry; the FEX launcher normally performs this cleanup.
|
||||
suggest: []
|
||||
- id: fex-vkd3d-init
|
||||
kind: pcvr
|
||||
pattern: '(?i:vkd3d[^\r\n]*(?:failed to (?:create|initialize)|could not (?:create|initialize)|no (?:suitable|compatible).*(?:device|adapter)))'
|
||||
severity: error
|
||||
diagnosis: The D3D12 route could not initialize vkd3d-proton or a Vulkan device. Check GE's shipped vkd3d-proton DLLs, clear PROTON_NO_D3D12 and PROTON_USE_WINED3D, and verify the FEX Vulkan thunk reaches Turnip. This route still needs a device test.
|
||||
suggest: []
|
||||
- id: fex-wined3d-init
|
||||
kind: pcvr
|
||||
pattern: '(?i:wined3d[^\r\n]*(?:failed to (?:create|initialize|find)|could not (?:create|initialize)|OpenGL support is disabled))'
|
||||
severity: error
|
||||
diagnosis: Wine's OpenGL renderer could not initialize. The FEX route has no configured GL thunk; verify the guest rootfs GL driver and display access. A Vulkan thunk alone does not provide host OpenGL. Prefer the game's D3D9 or D3D11 route when available.
|
||||
suggest: []
|
||||
- id: fex-revive-injector
|
||||
kind: pcvr
|
||||
pattern: '(?i:ReviveInjector[^\r\n]*(?:failed|unable|error (?:code|inject)))'
|
||||
severity: error
|
||||
diagnosis: ReviveInjector could not start or inject the game. Check the game executable and x86_64 Revive files in the shared GE-Proton prefix, and use OpenVR mode (without /openxr). An OpenVR-only game can launch directly. Do not change entitlement or Platform SDK checks.
|
||||
suggest: []
|
||||
# End FEX Rift compatibility (stream C).
|
||||
- id: meta-asset-status-error
|
||||
pattern: 'Asset status error\s+\(code 100\)'
|
||||
severity: error
|
||||
diagnosis: A DLC manager reports a Meta asset-file status error after VR starts (e.g. PixelRipped 1995). The asset-file check failed; the log does not identify the native call or asset ID. Try the experimental asset-file bypass (empty list, installed status with no file path); this does not supply DLC and has not been verified in the game.
|
||||
suggest: [frame.asset_files_stub]
|
||||
- id: lua-lightuserdata-pointer
|
||||
pattern: 'bad light userdata pointer'
|
||||
severity: fatal
|
||||
diagnosis: The Lua runtime rejected a pointer outside its supported address range. In the verified older xLua build, valid wider Frame pointers trigger this failure; a scoped compatible engine is available. For other builds, inspect the rejected pointer before assuming this cause.
|
||||
suggest: [frame.luajit_pointers]
|
||||
supersedes: [native-crash]
|
||||
- id: lua-startup-exit
|
||||
pattern: '(?s)\A(?=.*libxlua\.so[^\r\n]*lj_err_throw)(?=.*(?:__cxa_finalize|exit\+\d+))(?=.*(?:Fatal signal 6|FORTIFY:))'
|
||||
severity: error
|
||||
diagnosis: An uncaught Lua error triggered process exit, followed by a crash during Unity cleanup. The mutex failure is secondary; capture the original Lua error before choosing a fix. This stack alone does not identify a pointer-range problem.
|
||||
suggest: []
|
||||
supersedes: [native-crash]
|
||||
- id: vrapi-frame-rejected
|
||||
pattern: 'OVRPortVrApi:\s+Unsupported frame flags or swap interval'
|
||||
severity: fatal
|
||||
diagnosis: The VrApi bridge rejected the game's frame options, so no picture reaches the headset even though frame pacing can appear in the log. Rebuild and reinstall with the updated bridge; it accepts the old texture-density debug flag used by The Climb (GitHub #238). Other unsupported options are logged with their flags and swap interval.
|
||||
suggest: [frame.vrapi_bridge]
|
||||
- id: foveation-instance-skipped
|
||||
pattern: 'fdm_injection_CreateInstance: skipped'
|
||||
severity: info
|
||||
diagnosis: Valve's foveation layer skipped a Vulkan instance. This may be a helper instance; it does not prove
|
||||
that every game render pass lacks foveation. Compare with instance-created and render-pass diagnostics.
|
||||
For a compatible renderer, Customize > Eye-tracked foveation > Eye-tracked explicitly enables injection.
|
||||
Verify image quality and GPU time before retaining it; a loaded layer alone is not proof of active foveation.
|
||||
suggest: []
|
||||
- id: no-launcher
|
||||
pattern: 'APP_ACTIVITY is empty'
|
||||
severity: fatal
|
||||
@@ -19,8 +93,8 @@ signatures:
|
||||
- id: no-arm64
|
||||
pattern: 'INSTALL_FAILED_NO_MATCHING_ABIS'
|
||||
severity: fatal
|
||||
diagnosis: This game is 32-bit only and can't run on the Frame (no AArch32). Use the PC (Rift) version through Revive.
|
||||
suggest: []
|
||||
diagnosis: 32-bit-only APK (e.g. a Gear VR or Oculus Go game). The Steam Frame has no 32-bit ARM support; FramePort's 32-bit bridge translates the game (experimental).
|
||||
suggest: [frame.a32_bridge]
|
||||
- id: android-too-new
|
||||
pattern: '(NoClassDefFoundError|ClassNotFoundException|NoSuchMethodError)\b.*(Landroid/window/|android\.window\.|(storeStoreFence|loadLoadFence)\(\)V in class Ljava/lang/invoke/VarHandle)'
|
||||
severity: fatal
|
||||
@@ -41,6 +115,17 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: The game's graphics crashed a few seconds after start. Its render thread jumped to address 0x10000 in the Frame's GL driver (Mesa/Zink), which reads past a render-pass cache when a game uses multisampled render-to-texture (Unreal's mobile MSAA, for example Star Wars Pinball VR); the GL shim hides that extension.
|
||||
suggest: [frame.unreal_gl_shim, frame.unity_gl_shim]
|
||||
- id: unreal-splash-frame-stall
|
||||
pattern: '(?s)\A(?=.*CSxrSession::WaitFrame)(?=.*FSplash::RenderFrame_RenderThread)(?=.*FlushRenderingCommands)'
|
||||
severity: error
|
||||
diagnosis: Unreal's map loading waits for rendering while its separate Oculus splash waits for an unbegun VR frame (Tales from the Galaxy's Edge, issue 61). Bypass the splash renderer to let loading finish; the last image stays visible during loading.
|
||||
suggest: [frame.unreal_splash_bypass]
|
||||
# Attachment sizes captured by the issue 61 GL diagnostic probe, not ordinary launch logs.
|
||||
- id: unreal-multiview-depth-size
|
||||
pattern: '(?s)\A(?=.*ViewLog[^\n]*attachment 8ce0[^\n]*renderbuffer=1728x1728)(?=.*ViewLog[^\n]*attachment 8d00[^\n]*renderbuffer=1920x10(?:16|20))'
|
||||
severity: warning
|
||||
diagnosis: The measured multiview depth attachment keeps the initial Android window dimensions and clips the taller eye color target (Tales from the Galaxy's Edge, issue 61). Refresh Unreal's cached depth dimensions from its eye render size; menu panels use separate layers and remain intact.
|
||||
suggest: [frame.unreal_multiview_depth]
|
||||
- id: pac-unpaired
|
||||
pattern: 'ILL_ILLOPN\), fault addr 0x[0-9a-f]+ \(\*pc=0xd50323bf\)'
|
||||
severity: fatal
|
||||
@@ -50,7 +135,7 @@ signatures:
|
||||
pattern: 'xrCreateSwapchain \d+x\d+ .*faces=6 .*result=-\d+'
|
||||
unless: 'cube_standin: runtime refused'
|
||||
severity: fatal
|
||||
diagnosis: The game asked for a cube-map swapchain (for example an OVROverlay skybox or loading cube), which the Frame's runtime doesn't have. OVRPlugin goes on without images and crashes in ovrp_EndFrame4 (Unity's render thread SIGSEGV in memset, for example Budget Cuts Ultimate). FrameBridge from FramePort 0.12 serves such swapchains itself and drops their cube layers (cube_standin, GLES games) - rebuild and reinstall the game.
|
||||
diagnosis: The game asked for a cube-map swapchain (for example an OVROverlay skybox or loading cube), which the Frame's runtime doesn't have. OVRPlugin goes on without images and crashes in ovrp_EndFrame4 (Unity's render thread SIGSEGV in memset, for example Budget Cuts Ultimate). FrameBridge from FramePort 0.12 serves such swapchains itself and drops their cube layers (cube_standin, GLES games); rebuild and reinstall a GLES game to try this workaround. For a Vulkan game, the current build cannot stand in for the cube swapchain yet.
|
||||
suggest: [frame.adapter]
|
||||
supersedes: [unity-render-crash, native-crash]
|
||||
- id: swapchain-rect-invalid
|
||||
@@ -58,6 +143,11 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: The runtime rejects the game's frames, so it stops drawing (for example stuck at a "Waiting" box). An image rect reaches a few pixels past its swapchain on some Frames; FrameBridge from FramePort 0.11.0 clamps the rects, so rebuild and reinstall the game.
|
||||
suggest: [frame.adapter]
|
||||
- id: cylinder-menu-layers-rejected
|
||||
pattern: 'xrEndFrame failed -16\b'
|
||||
severity: error
|
||||
diagnosis: The game draws a curved (cylinder) screen to each eye separately (for example the Doom3Quest / PreyVR menu). The Frame's runtime has no cylinder layers, so FrameBridge shows each as flat quad strips; a per-eye pair makes too many quad layers for one frame and the runtime rejects the whole frame (XR_ERROR_LAYER_INVALID), leaving the menu black while the game runs. FrameBridge from this build shows the left eye's strips to both eyes to halve the count; rebuild and reinstall the game to try it.
|
||||
suggest: [frame.adapter]
|
||||
- id: missing-ovr-symbol
|
||||
pattern: '(UnsatisfiedLinkError|cannot locate symbol) .*"?(ovr_[A-Za-z0-9_]+|ovr[A-Z][A-Za-z0-9]*_ToString)'
|
||||
severity: fatal
|
||||
@@ -227,6 +317,7 @@ signatures:
|
||||
supersedes: [direct-vrapi, native-crash]
|
||||
- id: direct-vrapi
|
||||
pattern: '(?<!called before )vrapi_Initialize|VrApi.*not supported|libvrapi\.so.*(not found|failed)'
|
||||
unless: 'a32bridge: ' # a 32-bit game on the a32 bridge: it translates VrApi itself
|
||||
severity: warning
|
||||
diagnosis: The game uses Meta's VrApi directly and needs the VrApi bridge.
|
||||
suggest: [frame.vrapi_bridge]
|
||||
@@ -308,6 +399,28 @@ signatures:
|
||||
diagnosis: The game's Vivox voice-chat SDK calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes (for example Green Hell VR). The patch makes Vivox's audio-route check return early (voice chat keeps the default route).
|
||||
suggest: [frame.vivox_audio_route]
|
||||
supersedes: [java-crash]
|
||||
- id: lepton-data-link-failed
|
||||
pattern: "ln: cannot create symbolic link from '[^']*' to '/data/data/|Couldn't create directory for SharedPreferences file /data/user/0/"
|
||||
severity: fatal
|
||||
diagnosis: The game has no internal storage (saves, settings) inside Lepton. Usually its folder's path has a space (a microSD card named for example "Steam Frame 512g"), which Lepton's own scripts can't handle; FramePort's launcher (agent 83 and newer) gives Lepton a path without spaces. Connect FramePort once so the launcher is updated, then start the game again.
|
||||
report: true
|
||||
supersedes: [java-crash, native-crash]
|
||||
- id: bhaptics-permission-prompt
|
||||
pattern: 'GrantPermissionsActivity: Permission grant result .*permission=android\.permission\.BLUETOOTH_CONNECT'
|
||||
severity: error
|
||||
diagnosis: The game asked for Android 12's Bluetooth permission (bHaptics' vest SDK, for example Yupitergrad). Lepton runs Android 11, so Android's permission screen opens for a moment, the game is paused, and Meta's OVRPlugin may stop drawing (a frozen loading screen). The patch makes that request return at once.
|
||||
suggest: [frame.bhaptics_permission]
|
||||
- id: eos-keychain-loop
|
||||
pattern: 'LogEos: Resetting EOS SDK keychain: Failed to create'
|
||||
severity: error
|
||||
diagnosis: Epic Online Services can't save its login keychain and retries without end (a start that never finishes, 100 % CPU). The usual cause is missing internal storage (see lepton-data-link-failed); the patch stops the loop, and EOS then reports the failure to the game once.
|
||||
suggest: [frame.eos_keychain_loop]
|
||||
- id: pthread-keys-exhausted
|
||||
pattern: 'cannot create thread specific key'
|
||||
severity: fatal
|
||||
diagnosis: The game ran out of thread keys (Android allows 128 per process; Unity 6 plus large native libraries such as Firebase or Gecko, and the Frame's own graphics/OpenXR libraries, use them up), so a library aborted at start (for example Wander in Firebase's Firestore). The patch lets Unity's keys without a clean-up function share one real key.
|
||||
suggest: [frame.unity_tls_key_pool]
|
||||
supersedes: [native-crash]
|
||||
- id: unreal-obb-check-stuck
|
||||
pattern: 'Displayed \S+/\S*DownloaderActivity\b'
|
||||
# the downloader returned (the engine's main init resumed, OpenXR started) or the game crashed (its own finding)
|
||||
@@ -317,10 +430,19 @@ signatures:
|
||||
The game stays on Unreal's OBB check screen (DownloaderActivity), which the Frame doesn't show, and never
|
||||
starts VR (for example Contractors, GitHub #159). Unreal checks that the data file (.obb) has the name and size
|
||||
this APK expects, and with bVerifyOBBOnStartUp it also reads the whole file to check it; a failed check waits
|
||||
on an error screen. "Unreal - start without the OBB check" skips the content check. If the game still stops
|
||||
here, the OBB doesn't match this APK version - copy the APK and the OBB from the same install, add the game
|
||||
again and reinstall.
|
||||
suggest: [frame.unreal_skip_obb_check]
|
||||
on an error screen. "Unreal: start without the OBB check" (frame.unreal_skip_obb_check) skips only the CRC content check and applies only when the APK enables bVerifyOBBOnStartUp. Copy the APK and OBB
|
||||
from the same install, add the game again and reinstall.
|
||||
If the downloader still appears with bVerifyOBBOnStartUp=false (already disabled in the APK or by the skip patch), try
|
||||
"Unreal: skip the OBB delivery check" (frame.unreal_skip_obb_delivery), an experimental bypass of the separate Java name and size gate
|
||||
(e.g. Into The Radius, GitHub #283). An external-storage path or visibility failure can reject an uploaded
|
||||
OBB; neither option is a guaranteed fix, and the native engine still needs complete, matching, readable data.
|
||||
suggest: [frame.unreal_skip_obb_check, frame.unreal_skip_obb_delivery]
|
||||
- id: tbxr-fdm-shutdown-crash
|
||||
pattern: '(?s)\A(?=.*Fatal signal 11)(?=.*fdm_injection_xrDestroySwapchain)(?=.*TBXR_LeaveVR)'
|
||||
severity: error
|
||||
diagnosis: The game crashed while leaving VR, inside Valve's foveation layer when it destroys a swapchain (for example Lambda1VR). This can happen after manually quitting; it is not a startup crash and does not explain distorted geometry seen before quitting.
|
||||
suggest: []
|
||||
supersedes: [native-crash]
|
||||
- id: native-crash
|
||||
pattern: 'Fatal signal (\d+) \(SIG[A-Z]+\)'
|
||||
severity: fatal
|
||||
@@ -358,6 +480,11 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: The game can't read its own data files (OBB/data folder) - they are incomplete or from another version of the game than the APK. Copy the whole data folder again from the same install as the APK, add the game again and reinstall.
|
||||
suggest: []
|
||||
- id: fmod-bank-missing
|
||||
pattern: 'Error loading FMOD bank "/(sdcard|storage/emulated/\d+)/Android/obb/[^"\r\n]+\.bank": (ERROR|ERR_[A-Z_]+)\b'
|
||||
severity: fatal
|
||||
diagnosis: The game can't load its external sound banks, so it may show a file-loading error even while rendering frames. Copy the whole obb folder from the same backup as the APK, including the .bank files (not just .obb files). Keep apk/ and obb/ beside each other, add the backup folder again and reinstall.
|
||||
suggest: []
|
||||
- id: permission-denied-save
|
||||
pattern: 'Permission denied.*(Android/data|/sdcard)|EACCES.*Android/data'
|
||||
severity: warning
|
||||
@@ -463,6 +590,12 @@ signatures:
|
||||
severity: error
|
||||
diagnosis: The game can't write to a folder it created, so it may quit at start (for example SUPERHOT's cloud/data, mode 1700). FramePort's launcher (agent 41 or newer) gives such folders owner and group write permission before and during every launch; reinstall the game to get the new launcher. A folder the game creates and checks in the same moment on its very first start can't be repaired in time, so start the game again.
|
||||
suggest: []
|
||||
- id: startup-save-permission
|
||||
pattern: '(?s)UnauthorizedAccessException: Access to the path "/(?:sdcard|storage/emulated/0)/Android/data/[^"\r\n]+/files/cloud/data" is denied\.[^\n]*(?:\n[^\n]*){0,25}?StartupLoading\+<Start>'
|
||||
severity: fatal
|
||||
diagnosis: The startup routine stopped because it couldn't create the cloud-save directory, so the game stays on its loading screen even while rendering frames (Jurassic World Aftermath). The launcher's permission repair happens after this first-start error; close the game and start it again without uninstalling. Agent 84 prepares the cloud-save directory before startup to avoid this race on fresh installs.
|
||||
suggest: []
|
||||
supersedes: [save-folder-permission]
|
||||
- id: surface-swapchain-emulated
|
||||
pattern: 'surface_emul: emulated Android surface swapchain'
|
||||
severity: info
|
||||
@@ -481,6 +614,13 @@ signatures:
|
||||
the video was decoded in software instead, which is slow for 4K/8K. Turn off hardware video decoding in Steam's
|
||||
settings, restart Steam, then start the game again.
|
||||
suggest: []
|
||||
# Launch tests run without a worn headset. Some parts give up on tracking after a while and end the VR session; what
|
||||
# the game does after that (often a crash in the GPU driver while its renderer tears down) never happens in the
|
||||
# headset. In a launch test that already had paced frames before such a line, a later exit or crash still passes,
|
||||
# with a note (finding `headless-end`). Play sessions ignore this list.
|
||||
headless_end:
|
||||
- label: the VrApi bridge's 30 s tracking deadline
|
||||
pattern: 'EnterVrMode: no tracked head pose before deadline'
|
||||
milestones: # progress markers, in order; the furthest one reached is reported
|
||||
- {id: pcvr-launch, kind: pcvr, pattern: 'FramePort: launching|Launched injector with', label: Launcher started}
|
||||
- {id: pcvr-oculus-hmd, kind: pcvr, pattern: 'FramePort oculushmd: OculusHMDConnected event ready', label: Oculus headset event provided}
|
||||
|
||||
Loaded 100 of 446 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user