Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2abb694043 | ||
|
|
b33cc2de09 | ||
|
|
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 |
No files matched your search
@@ -19,8 +19,6 @@ CLAUDE.local.md
|
||||
# outputs of `frameport report` / `parity` / ruff
|
||||
REPORT.md
|
||||
.ruff_cache/
|
||||
.eval-home/
|
||||
.eval-results/
|
||||
.claude/worktrees/
|
||||
|
||||
# project page (site/)
|
||||
|
||||
@@ -6,7 +6,6 @@ FramePort ports Meta Quest standalone APKs to the **Valve Steam Frame** (SteamOS
|
||||
install over SSH (agent) → Steam shortcut → headless launch test + log triage`.
|
||||
|
||||
Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRAME_RUNTIME.md` for runtime facts.
|
||||
Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debugging a game).
|
||||
|
||||
## Layout
|
||||
- `src/frameport/` — Python package. `pipeline.py` is the API the CLI (`cli.py`) and GUI (`ui/`, Flet 1.0) share.
|
||||
@@ -59,21 +58,7 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
|
||||
agent ends at EOF); sources and costs in docs/FRAME_RUNTIME.md "Monitoring sources"; game card (fps from
|
||||
FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles with `C.Sparkline` (Flet canvas, no charts
|
||||
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); room (`views/room.py`, persistent, "experimental" pill:
|
||||
scan with progress + Cancel = agent v109 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
|
||||
with the Frame page's short card, mesh cached in `<data>/rooms-cache/<frame>/`; in-window 3D `ui/room3d.py`
|
||||
RoomCanvas over `mr/view3d.py` (pure-Python projection, mesh reduced to ≤4k/≤1.5k edges at rest/moving, ≤18 fps
|
||||
worker; each redraw sends NEW shape controls: changing a Points list made Flet diff it item by item, 0.3-1 s);
|
||||
"Open full 3D view" = `mr/room_viewer.py` token server on 127.0.0.1 + hand-written WebGL `mr/viewer_page.py`,
|
||||
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v109):
|
||||
Clean-up = fp_roomscan's refinement settings, `install/rooms.REFINE_DEFAULTS` = the owner's defaults + slider
|
||||
metadata (agent `ROOM_REFINE_DEFAULTS` must match, a test checks), saved non-defaults in the Frame's
|
||||
`rooms/refine.json`, used by every build; Apply = job → agent `room_refine` (`--refine-only` on a copy of the room
|
||||
folder from `room-raw.txt`/`room-mesh-raw.bin`, scan.json → status `report`/`labels`/`suggestions`); Labels =
|
||||
the label rules' decisions + Add as door / Add as closet; Furniture & screens = box editor (click a box in the
|
||||
canvas: `view3d.pick`) + doors/windows on a wall (`mr.room.plane_on_wall`), Save = `rooms.put_room` → `room_put
|
||||
edited` (keeps raw files, `edits.txt` survives a new clean-up; "Back to the scan's boxes" = keep_edits false); no
|
||||
eraser in the GUI (`rooms.refine(erase=)` only)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
|
||||
game; MON_CRITICAL needs force, MON_NEVER is refused)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
|
||||
(one `ft.ContextMenu` per view, filled on right-click; on one of several selected items they act on the whole
|
||||
selection, `C.menu_targets`) and click-and-drag multi-select (`C.DragSelect`: pan start/end on the area + item
|
||||
hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band).
|
||||
@@ -138,9 +123,8 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
|
||||
`ft.AnimatedSwitcher` (`app._show_view`): a new route gets a new wrapper (150 ms fade), a same-route redraw swaps
|
||||
the wrapper's content (no animation; persistent views just remount); the wordmark's portal (`wordmark().data`)
|
||||
pulses once when frame_state turns "connected"; both off with setting `ui.reduce_motion` (Settings → Appearance
|
||||
"Reduce motion", `app.reduce_motion`). Settings has a fixed index column (`settings.SECTIONS` order: most users'
|
||||
needs first (Updates, which also holds game recipe + install-link updates, Installing, Library folders,
|
||||
Appearance), niche ones later (Install links, Tools, This PC), Remove FramePort last): entries scroll the section column via `scroll_to(scroll_key=…)` (sections wrapped
|
||||
"Reduce motion", `app.reduce_motion`). Settings has a fixed index column (`settings.SECTIONS` order, Appearance
|
||||
first, Remove FramePort last): entries scroll the section column via `scroll_to(scroll_key=…)` (sections wrapped
|
||||
in Containers with `ft.ScrollKey`), the header stays; `app.settings_view.show_section(key)` (ui_smoke
|
||||
`settings-index`).
|
||||
Help hints: wording for non-obvious terms lives in `ui/help.py` (`HELP`); show it with `C.help_icon(key)` or the
|
||||
@@ -154,7 +138,6 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
|
||||
uploads are interruptible (Cancel checked per MiB) and resumable (big files via SFTP `.part` append, small files
|
||||
streamed in tar batches; the agent counts files already in `incoming/`). Multi-select in the Library queues
|
||||
installs after asking every needed question up front. Failures end in one pop-up (Resume / Uninstall / log).
|
||||
- Library folders (GitHub discussion #351): `core/folders.py` (no Flet) owns setting `scan.roots` (remember/add/remove/status/rescan_roots; the origin-based fallback only while the setting was never set, so removing every folder doesn't guess them back), Settings → Library folders (`settings.library_folders`, status read via run_bg), CLI `frameport folders [--add/--remove/--json]` + `frameport rescan`. Scans relink moved games (`pipeline.ScanReport`: a library game found elsewhere whose own files are gone and the copy is the same build (Quest: source_sha256, else versionName; Rift: same program path + size from the fingerprint, matched by game id or, renamed, by program) → only paths change (`relink_quest`/`_relink_rift`), user fields/builds/installs stay; another version → Scan a folder… takes it via `replace_apk(found=)`/`replace_rift_game`, Rescan only reports it; the old copy still there → kept). `add_game` no longer overwrites a user recipe (refreshes `suggested` only). Linux apps aren't scanned. PC installs of a moved Rift game (Steam shortcut path) untested.
|
||||
- Other drives (GitHub #90, agent v63): anchors stay in ~/Applications/quest-frame, a game's files may live in
|
||||
`<mount>/FramePort/<pkg>` (`deployment.json` base). Agent `drives` (/proc/mounts, /run/media + Steam library
|
||||
drives; vfat/exfat/ntfs/read-only refused), prepare*/finalize_linux `dest` (unmounted = error, never a fallback;
|
||||
@@ -240,13 +223,6 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
store matches and links then only update `auto_title`; a locked name gets no twin suffix); `sync_title` → Frame
|
||||
agent v77 `rename` (deployment.json title, same shortcut appid: upsert matches by Exe) + art + one Steam restart, PC
|
||||
`PcReviveTarget.rename`; an unreachable Frame → `steam_name_stale` (game page "Update name on Frame", next install).
|
||||
- Newer versions (GitHub #161): game menu "Update with a newer APK…" (`pipeline.replace_apk`, outdated via
|
||||
`build.source_version`) and, for Linux apps / PC VR games, "Update with a newer version…" (`replace_linux_app` /
|
||||
`replace_rift_game` via `add_linux_app(replace=)` / `add_rift_game(replace=)`, CLI `frameport new-version`): the
|
||||
entry keeps package, name, art, tags, switches, user recipe (a confirmed exe if the new folder has it); a new
|
||||
`source_id` goes into the deployment's `recipe` dict at install (no agent change) → `C.source_replaced` = outdated
|
||||
(Frame + `pc_outdated`); such an install re-sends same-size files changed after the last install (mtime/ctime,
|
||||
`installer.changed_since`); the old Rift folder is in `replaced_dirs` (rescans skip it). Untested on the device.
|
||||
- `Recipe.as_is` = install unchanged (pre-patched libraries): `pipeline.prepare_as_is`; auto for APKs that already
|
||||
contain FrameBridge (`frame_patched`). For Rift it changes nothing (the dump is never modified; the Frame copy
|
||||
still gets launch fixes like the crash-reporter rename — `-nocrashreports` alone doesn't stop UE 4.23) — it does
|
||||
@@ -258,18 +234,6 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
flask icon (SCIENCE_OUTLINED) so they aren't confused with Play.
|
||||
- `uninstall.py` + `frameport uninstall-app` + Settings: removes the data dir (after an optional key backup zip),
|
||||
PC Steam shortcuts, the WSL Revive copy, and via agent `purge` FramePort's games/files on the Frame.
|
||||
- Save backups (discussion #352, agent v95; docs/FRAME_RUNTIME.md "Save backups"): agent `_backup_saves`/
|
||||
`_restore_saves '<json>'` stream a tar.gz of `lepton-data/internal/<pkg>` + `external/Android/data/<pkg>` (caches,
|
||||
IL2CPP/shader caches, FrameBridge files, videos/OBBs, files >64 MiB left out; manifest `frameport-saves.json` last)
|
||||
over the exec channel, file work in `podman unshare` (`_saves_ns`), launch lock held, refused while running;
|
||||
restore = untrusted input (staged + checked first), rollback copy `<base>/saves-before-restore.tar.gz` (undo =
|
||||
`rollback: true`). PC `install/saves.py` (`<data>/save-backups/<pkg>/<ts>.tar.gz` + `.json`, keep `saves.keep`
|
||||
= 10), GUI `ui/views/saves_dialog.py` (game menu section "Saves", Frame page "Back up all saves", uninstall's
|
||||
"back up before deleting"), CLI `frameport saves backup|list|restore|undo`. Agent v101: PC folder `saves.folder`
|
||||
(Settings → Data; old folders stay listed in `saves.previous_folders`), automatic snapshots `saves.auto` (default
|
||||
off; flag `~/.local/share/frameport/save-snapshots/enabled`, Quest launchers run `_snapshot_close` in the EXIT
|
||||
trap under the launch lock, newest 3 per game on the Frame; `install/saves.sync_snapshots` imports them as
|
||||
"automatic" once, ledger `<data>/save-snapshots-synced.json`). Device-checked 2026-10-17 with BlazeRush.
|
||||
- `patches/` — **the unit of modularity**. `base.py` (Patch interface, registry), `overport.py` (overport CLI patch ids,
|
||||
discovered dynamically via `overport patches`), `frame/*.py` (one module per Frame fix), `settings.py` (FrameBridge
|
||||
adapter keys + device files as patches). Add a patch = add a module that calls `register(...)`.
|
||||
@@ -303,15 +267,6 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
`working-config.yml` issue → maintainer label `catalog-accepted` (a plain issue needs `working-config` added by
|
||||
hand) → `catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
|
||||
`core/applog.py` (`<data>/logs/app.log`, finished GUI jobs in `<data>/logs/jobs/`).
|
||||
- Announcements (2026-10-15, announcements/README.md; app only, the owner dropped the site pop-up): source of
|
||||
truth `announcements/current.md` on `main` (front matter id/title/date + optional image/image_alt/button+url/
|
||||
expires/active, Markdown body; ships `active: false`). `announcements.py` (no Flet; raw GitHub via
|
||||
`cache.cached_text`, 6 h) + `ui/announcer.py` (pop-up 20 s after start / every 6 h when no dialog is open and not
|
||||
on welcome; `C.dialog` with the portal edge, image downloaded + Pillow-checked into `<data>/announcements/`, shown
|
||||
by asset URL). Shown only if the user was around before `date` (setting `announce.since`, set once at the GUI's
|
||||
first start: 0 when `USED_BEFORE` settings exist, else now), once: any close (X, outside click, Escape, button)
|
||||
stores the id (`announce.dismissed`, last 50). New announcement = new id; same id = typo fix.
|
||||
`FRAMEPORT_NO_ANNOUNCEMENTS`; `ui_smoke.py --announcement` renders it.
|
||||
- Self-update (docs/ARCHITECTURE.md "Self-update", docs/INSTALL.md "Updating"): `_version.py` = the only version
|
||||
(`frameport.__version__`; pyproject reads it via hatch `dynamic`; app log, diagnostics, User-Agent, Settings use
|
||||
it — never `importlib.metadata`, bundles have no dist-info). `updates.py` (no Flet): `check()` = GitHub
|
||||
@@ -378,7 +333,45 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
`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). Per-game a32 findings (AFFECTED, Spunky Dungeon, Gunjack's signature check `fp_a32_signature.txt`, synthetic Gear VR input, Quest builds `fp_a32_headset.txt` / `quest_touch`, recenter, debugging aids `A32_BREAK` / `A32_STRACE` / `A32_PERFMAP`, translator profiling): docs/DEVICE_NOTES.md#a32-bridge-games-lessons-debugging-aids-profiling.
|
||||
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).
|
||||
|
||||
@@ -466,12 +459,7 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
XR_KHR_composition_layer_equirect2/cylinder, XR_FB_composition_layer_image_layout (flip emulated by Vulkan blit).
|
||||
- `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` return FUNCTION_UNSUPPORTED → adapter emulates
|
||||
(offset = predictedDisplayTime − period − CLOCK_MONOTONIC, sampled in xrWaitFrame).
|
||||
- No XR_FB_display_refresh_rate (`xrGetDisplayRefreshRateFB` -7), yet OVRPort's dispatcher offers it and answered 0 Hz
|
||||
→ FrameBridge always answers with the measured rate (1/predictedDisplayPeriod, else 72). PreyVR (#306, idTech 4:
|
||||
`USERCMD_MSEC = 1000 / GetRefresh()`) had 0 ms tics: async thread spinning, `com_frameTime` stuck at 0, menu black.
|
||||
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m). OVRPlugin treats that rect as a configured
|
||||
boundary but can't outline it ("ovrpBoundary_Outer is not supported"): Demeter's guardian check indexed an empty
|
||||
array → with `scene_emul` the app's STAGE `xrGetReferenceSpaceBoundsRect` returns XR_SPACE_BOUNDS_UNAVAILABLE.
|
||||
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m).
|
||||
- GL goes through **Zink** (Mesa GL on Vulkan). Mesa GLSL is strict: `#pragma` before `#extension` fails, implicit
|
||||
int/float conversions fail (enable `GL_EXT_shader_implicit_conversions`), num_views=2 shaders on single-view FBOs
|
||||
give GL_INVALID_OPERATION. Some GLES games hit `zink: DEVICE LOST` (Unity MSAA RTT; Sniper Elite VR even without).
|
||||
@@ -540,14 +528,83 @@ Launch tests ignore log files older than the launch (stale Proton/game logs used
|
||||
(`\\wsl$` is unreliable), so PC mode copies Revive to `%LOCALAPPDATA%\FramePort`.
|
||||
FramePort never bypasses Oculus entitlement checks (Platform SDK games: flagged, PC mode with the Oculus app only).
|
||||
|
||||
**Upload speed:** home Wi-Fi caps uploads at ~11-18 MB/s whatever the SSH method; the Frame's hotspot (`wlanap`) ~83-97 MB/s; `Frame.fast_link()` (installer `transfer_link`, uploads ≥64 MiB) prefers usb0 > wlanap when the host key matches. → docs/DEVICE_NOTES.md#upload-speed
|
||||
**Upload speed (measured 2026-09-29):** via the home router (PC Wi-Fi → router → Frame wlan0) only 15-18 MB/s, the
|
||||
same for paramiko, OpenSSH and 4 parallel streams, so the network path is the limit, not the SSH code. Direct to the
|
||||
Frame's hotspot (`wlanap` 10.35.78.1; the owner's PC has Valve's USB Wi-Fi dongle on it) 83 MB/s OpenSSH / 97 MB/s
|
||||
paramiko; reading dumps from D: via WSL drvfs is then the cap (~82 MB/s). `Frame.fast_link()` (installer
|
||||
`transfer_link`, uploads ≥64 MiB) uses usb0 > wlanap when reachable **and** the host key matches the paired Frame.
|
||||
**Platform SDK detection** must include delay imports (`PEInfo.delay_imports`), modular Unreal's
|
||||
`*-OnlineSubsystemOculus-*.dll` and Ready At Dawn's `pnsovr.dll`: Robo Recall, Lies Beneath and Lone Echo I/II all use
|
||||
it; without the Oculus app they crash with 0xc06d007e (triage `delayload-missing`) — not fixable legitimately.
|
||||
GUI: Frame → Installed → folder icon = file browser (agent `list_files`, flags files missing vs the install
|
||||
manifest); Settings → About shows the bundled agent version and the Frame's.
|
||||
|
||||
**Revive under Proton arm64:** Revive's Detours hooks don't take effect (Wine's kernelbase is ARM64EC); a system32 `LibOVRRT64_1.dll` symlink gets past -3001 to -3021 = ovrError_LibSignCheck (Oculus-signed runtime required; not bypassed). → docs/DEVICE_NOTES.md#revive-under-proton-arm64-lone-echo
|
||||
**Revive under Proton arm64 (Lone Echo, 2026-09-29):** Revive's Detours hooks (LoadLibraryW/ExW, OpenEventW, the
|
||||
signature check) don't take effect: Wine's kernelbase is ARM64EC. The LibOVR shim in the game then does a plain
|
||||
`LoadLibrary("LibOVRRT64_1.dll")` search (exe dir, cwd, system32, windows, PATH — no registry, `OculusBase` or
|
||||
`LIBOVR_DLL_DIR` used by this build) → `Failed to initialize Oculus API (-3001)` (= Lies Beneath's "Unable to load
|
||||
LibOVRRT DLL"). A symlink `pfx/drive_c/windows/system32/LibOVRRT64_1.dll -> <base>/revive/LibReviveXR64.dll` (Wine
|
||||
maps a symlink to the already-loaded module) gets past it to **-3021 = ovrError_LibSignCheck**: the shim only accepts
|
||||
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;
|
||||
they don't publish `_ssh._tcp`. The Frame has several links: `wlan0` (home Wi-Fi), `wlanap` = its own hotspot at
|
||||
10.35.78.1/24 (a PC can join it directly), `usb0` = USB gadget network 10.86.200.233/29 (up when cabled to a PC).
|
||||
@@ -561,7 +618,23 @@ stopping Steam kills them → always run that work via `systemd-run --user` (the
|
||||
**overport:** always `--version=latest`; `--workspace` holds runtimes and **per-package keystores (password
|
||||
"password", alias "key") — never lose them**: updates must be signed with the same key or saves are lost on reinstall.
|
||||
Output is deterministic (same input + runtime → same bytes), which is what makes parity testing possible.
|
||||
**OVRPort 1.2.5 (2026-10-01):** `patch` rejects unknown/duplicate args, unknown ids and an empty `--patches=`; it adds `libovrplatformcompat.so` itself (`frame.ovrplatformcompat` then skips). New patches `patch_ac_nexus_no_appsw_72/_90` (AC Nexus build 207706 only; `_90` = catalog default; STRICT patches only from a catalog recipe where `applies()` holds), `patch_disable_meta_xr_audio_telemetry`, `patch_vrapi_openxr` (OVRPort's VrApi translator fails on the Frame → keep `frame.vrapi_bridge`). The owner prefers OVRPort's fixes over ours where they work as well. `frameport install <pkg> --apk <file>` installs a test build. → docs/DEVICE_NOTES.md#ovrport-125
|
||||
**OVRPort 1.2.5 (2026-10-01, the fork's first release; CLI-only):** same commands (`patches [--json]`, `patch`, `help`,
|
||||
`install`), but `patch` rejects unknown/duplicate args, unknown patch ids and an empty `--patches=`
|
||||
(`tools/overport.patch` refuses empty lists). After patching it adds `libovrplatformcompat.so` itself when the platform
|
||||
loader lacks `ovrMessageType_ToString` (our `frame.ovrplatformcompat` then skips: same library). New patches (all off;
|
||||
`patches/overport.py`): `patch_ac_nexus_no_appsw_72/_90` (AC Nexus build 207706 only, exclusive),
|
||||
`patch_disable_meta_xr_audio_telemetry` (x86_64 emulators: hidden), `patch_vrapi_openxr` = OVRPort's VrApi adapter =
|
||||
the **unpatched** upstream of our `frame.vrapi_bridge` (`native/vrapi` unchanged since our 5e7df52), only usable with an
|
||||
experimental CLI built with `-PwithVrApi=true` (stable jars list it but fail). The owner prefers OVRPort's fixes over
|
||||
ours where they work as well (less to maintain): compare in the headset before switching a default.
|
||||
`frameport install <pkg> --apk <file>` installs a specific (test) build signed with the game's key.
|
||||
Headset results (2026-10-02, `PATCHED/_test-ovrport-1.2.5/TESTING.md`): OVRPort's own VrApi translator fails on the
|
||||
Frame (Climb 2: requests VkFormat 37 → crash; POTW: no GLES path, missing `vrapi_GetTextureSwapChainHandle`) → keep
|
||||
`frame.vrapi_bridge`; those two changes are upstream candidates. 1.2.5 builds (its platform compat, new permissions)
|
||||
work. AC Nexus: `patch_ac_nexus_no_appsw_90` is the catalog default (owner preferred 90 Hz); `STRICT` patches are only
|
||||
taken from a catalog recipe where `applies()` holds (the AC Nexus ones need build MAIN.450412.207706.final, else the
|
||||
whole overport run fails).
|
||||
|
||||
**Patching gotchas:**
|
||||
- UnityPy re-serialization breaks scene loading → patch QualitySettings ints in place.
|
||||
- LIEF's DT_NEEDED injection shifts segments; our `elf.add_needed` appends a new PT_LOAD (reuses PT_NOTE, else moves the
|
||||
@@ -573,66 +646,414 @@ Output is deterministic (same input + runtime → same bytes), which is what mak
|
||||
- VrApi-direct engines (CryEngine Climb 2, POTW) need the VrApi bridge; the bridge drops whole frames on unknown layer
|
||||
types (→ black screen with audio).
|
||||
|
||||
**Quest controller mode:** adapter `quest_buttons` (0/1/2, per game + library `input.quest_buttons`) suggests Frame Controller bindings translated from the game's Touch set (`suggest_frame`, mapping docs/input/quest_buttons.json = single source for C table, tests, diagram `scripts/controller_diagram.py`); runtime accepts them headless. → docs/DEVICE_NOTES.md#quest-controller-mode-quest_buttons-2026-10-17
|
||||
**Steam Frame controller models:** adapter setting `controller_models` (`native/adapter/render_model.c`, emulates XR_FB_render_model) + agent `install_controller_models` (Frame's SteamVR models → glb at finalize); no catalog game uses runtime models; Valve's models are never committed or copied off the Frame. → docs/DEVICE_NOTES.md#steam-frame-controller-models
|
||||
**Steam Frame controller models (2026-09-30, not yet seen in a game):** adapter setting `controller_models`
|
||||
(`native/adapter/render_model.c`) emulates XR_FB_render_model and serves `files/framebridge/controller_{left,right}.glb`;
|
||||
agent v20 (`install_controller_models`, command `controller_models`) converts the Frame's SteamVR render models
|
||||
(folder name matching "frame" + left/right, OBJ+PNG, `openxr_grip` component → grip space) at finalize/set_settings.
|
||||
Valve's models are never committed or copied off the Frame. Only games that use Meta's runtime controller models
|
||||
benefit (manifest `RENDER_MODEL` permission/feature → suggested); **none of the 34 catalog games do** (they ship their
|
||||
own meshes: that would need per-game asset replacement). Model discovery and conversion
|
||||
checked on the device 2026-09-30: `/opt/steamvr/drivers/frame_controller/resources/rendermodels/frame_controller_{left,right}`
|
||||
(component OBJs in model space = the whole `<name>.obj`, one `_color.png` 2048² near-black, `openxr_grip` rotates about
|
||||
X only; hidden-by-default components like `status` are skipped) → ~2 MB glb each, ~1 s. Not yet seen in a game (none
|
||||
of the installed builds has the new adapter, and none requests runtime models), so whether Meta's SDK attaches runtime models at the grip pose is unverified.
|
||||
**overport's dispatcher (`libopenxr_loader.so`) only forwards functions in its own table** (`overportOXR: Unknown
|
||||
proc addr: …`), so adapter emulations of functions it doesn't know are unreachable. For XR_FB_render_model,
|
||||
`native/xrshim` fills the gap: OVRPlugin's `dlopen("libopenxr_loader.so")` string is rewritten to the shim (see
|
||||
native/README). Verified on the device with Toy Master (2026-09-30): `extension shim: xrLoadRenderModelFB -> FrameBridge`.
|
||||
Toy Master doesn't request a model after that (it uses its own), so the glb loading path is only unit-tested.
|
||||
|
||||
**Crash backtraces:** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (launch tests return `crash_log`). Fossilize loads into every app (userdebug guest) → `frame.vk_sanitize` (`native/vkshim`); overport's dispatcher aborts on swapchains > 4096 px → `frame.swapchain_limit`; cylinder layers → quad strips (`cylinder_strips`). Drawing equirect layers ourselves and a focus debounce were tried and removed: don't retry without a new approach. → docs/DEVICE_NOTES.md#crash-backtraces-fossilize-swapchain-limit-cylinder-and-equirect-layers
|
||||
**360° layers / 4XVR:** the Frame's Android runtime composites only quad + projection layers (quads always above projection layers). Per-game adapter settings (off by default): `equirect_emul` (GLES), `equirect_flip/face/res/fps/stereo`, `stable_local`, `focus_hold` (default on), `aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug`; agent `link_media` / `app_media_dirs` put sent files into a player's own folder. → docs/DEVICE_NOTES.md#360-layers--4xvr
|
||||
**Companion apps / intents:** a second APK can run inside a game's Lepton container, but Lepton turns every `android.intent.action.VIEW` with data into `steam://openurl` → "open in external player" hand-offs can't work; Lepton installs exactly one `*.apk` per app folder. → docs/DEVICE_NOTES.md#companion-apps--intents-stremio--4xvr
|
||||
**Text input:** Lepton's Android has no IME and VR apps run headless (no focused window); `lepton-show-flatscreen` keeps VR and gives focus + Steam's keyboard (`device.text_input_window`); `frame.unity_text_input` patches Unity's TMP_InputField/InputField at Cpp2IL offsets; agent `_keyboard` = uinput "Type on Frame". `pm install` of a second APK corrupts the main app (`cmd_real package install` skips the hook). → docs/DEVICE_NOTES.md#text-input-stremio-vr
|
||||
**User reports #4-#10:** shortcuts go to the signed-in account (agent `library_users`), `launch` errors `NOT_IN_LIBRARY`; shared Cpp2IL run (`unity_text_input.Il2cppReturnPatch`) for `frame.unity_runtime_msaa_off` and `frame.unity_multipass`; VR4 → catalog `use_alt`. **Catalog changes reach existing games**: recipes store `catalog_rev`, `library._follow_catalog` re-derives non-user recipes. → docs/DEVICE_NOTES.md#user-reports-2026-10-04-issues-4-10
|
||||
**Round 2 of reports / headset rounds 2-3 (2026-10-04):** vkshim `vk_shader_fix` (VR4); `Il2cppReturnPatch.field_loads` (I Am Cat); builds record `recipe_fp` (changed recipe/revised patch → "Update on Frame"); `Frame.relocate`; adapter `profile_remap` (Touch Plus/Pro); `sync_guard` (Myst); `focus_hold` tuning; ovrpshim for Unity 2017 (Accounting+); Steam library to every account, `shortcuts_lost` / `add_devkit_entry`; no clipboard service → `frame.sdl_clipboard`, `frame.godot_clipboard`; 2D apps keep non-VR patches (`needs_vr = False`); `frame/local.py` (FramePort on the Frame); after every app update `library._follow_catalog` re-derives non-user recipes once (`recipes.app_version`, `library.REFRESH_ON_UPDATE`). Analysis fields added later: **bump `analysis/detect.ANALYSIS_VERSION`** (re-analysis at GUI start + `pipeline.refresh_analyses`; `analysis_failed`). Roblox and BONELAB crash (unsupported). → docs/DEVICE_NOTES.md#round-2-of-reports-headset-rounds-2-3-2026-10-04
|
||||
**Round 4:** XR_KHR_android_surface_swapchain is listed but unsupported → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`); save folders without u/g+rwx → launch.sh `fix_perms`; Lucky's Tale OOM (no fix); `perf` works as steamos (gdb blocked); QuestCraft JRE, WiiCompiled import by hand (Lepton has no document picker). → docs/DEVICE_NOTES.md#round-4-2026-10-04-surface-swapchains-save-folder-permissions
|
||||
**Steam library failures (GitHub #21/#30/#41/#42):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (Play → AppError_9) → fallback: register through Steam's devkit interface (`devkit-1 … create-shortcut`, `~/devkit-game/<id>/`; `devkit_appid`, `devkit_appid_from_log`, `devkit_unregister`, `copy_grid_art`; ids: letter first, letters/digits/_ only); `collect_diag` adds `steam_library`. Cause still unknown. → docs/DEVICE_NOTES.md#steam-library-failures-and-the-devkit-fallback-github-21304142
|
||||
**Exit game (GitHub #36):** `Apps.TerminateApp` (Steam CEF devtools on 127.0.0.1:8080) closes games; launch.sh ends the game when its parent reaper is gone (`upgrade_launchers` adds it). → docs/DEVICE_NOTES.md#exit-game-github-36
|
||||
**SteamVR dashboard at game start:** launch.sh's `_dashboard_worker` hides the dashboard via `SteamClient.OpenVR.VROverlay` (agent `steam_js`) from the first FrameBridge `new layer:` line (120 s, ≤10 times, stops after `toggle_dashboard_action`); log `<base>/dashboard.log`, opt out `FRAMEPORT_KEEP_DASHBOARD=1`. → docs/DEVICE_NOTES.md#steamvr-dashboard-at-game-start
|
||||
**Space warp / ITR2:** OVRPort's dispatcher offers XR_FB_space_warp itself (`patch_disable_space_warp` doesn't reach UE5) → FrameBridge `hide_space_warp` (per game, off; suggested for UE5 + OVRPlugin). `equirect_emul` switches itself off on Vulkan. → docs/DEVICE_NOTES.md#space-warp--itr2
|
||||
**PR #34 (Lucas-Mathieu):** `native/adapter/audio_metadata.h` keeps Meta XR Audio Wwise metadata alive (Batman: Arkham Shadow 1.4.1 smoke-bomb crash). Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally. → docs/DEVICE_NOTES.md#pr-34-meta-xr-audio-metadata-batman
|
||||
**Heuristics eval (2026-10-04):** 19/44 recipes exact (superseded by the 2026-10-15 numbers under Heuristics). → docs/DEVICE_NOTES.md#heuristics-eval-2026-10-04
|
||||
**Crash backtraces (2026-09-30):** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (after "Dumping logcat"), not launch.log; agent v23 launch tests wait for it and return `crash_log`, triage gets it via `triage(..., crash=)` (not pid-filtered: tombstones come from crash_dump's pid). The guest is `userdebug` (`ro.debuggable=1`), so Lepton's Fossilize layer loads into every app whatever the APK's debuggable flag; it has no off switch. Fossilize crashed Deadpool VR on uninitialized attachment-reference pNexts (several, in FVulkanRenderPass and the render-target layout) → `frame.vk_sanitize` = `native/vkshim` (engine's `dlopen("libvulkan.so")` string → `libfp_vk.so`, wraps vkCreateRenderPass2/KHR, drops unreadable/wrong-sType pNexts; verified: game runs). overport's dispatcher aborts on swapchains > 4096 px (`cmp wN,#4096` in xrCreateSwapchain) → `frame.swapchain_limit` (verified with 4XVR, 7680×3840 accepted by the runtime). Both are default-on (parity classifies their rewrites as
|
||||
"expected"; library migration `quest_binary_fixes_v2`). The adapter shows cylinder layers (4XVR's movie screen) as ~15° quad strips (`cylinder_strips`, setting
|
||||
`cylinder_strips`); the runtime composites them at full resolution. Equirect (360°) layers are dropped: drawing
|
||||
them ourselves (GLES renderer in xrEndFrame: stutter, never showed 4XVR's VR video; Vulkan renderer: hung the
|
||||
Frame's GPU in AC Nexus; reading the newest *released* image broke 4XVR's theatre) was removed again on the owner's
|
||||
request — don't retry without a new approach. Converted layers must count as "swapped" or the original layer list
|
||||
is submitted (runtime returns -2 for every frame). A focus debounce (hiding the Frame's brief
|
||||
FOCUSED→VISIBLE→SYNCHRONIZED dips, which make 4XVR recenter) was also removed: AC Nexus stayed black with it (it
|
||||
hid 521 ms focus changes at start). The new approach (below) is per-game, off the app's render thread, GLES-only.
|
||||
|
||||
**360° layers / 4XVR (2026-09-30):** the Frame's Android SteamVR runtime (`/opt/steamvr/bin/androidarm64/vrclient.so`,
|
||||
mounted in Lepton as `/data/steamvr/runtime`) only composites quad + projection layers (cube/cylinder/equirect(2)
|
||||
are enum names only) — nothing to unlock. Per-game adapter settings (off by default, hooks installed only when on):
|
||||
`equirect_emul` (GLES only: a worker thread with a shared EGL context converts each 360° image to a cube map when it
|
||||
changes and draws one adapter projection layer from it for every frame with exactly that frame's views (the app's own
|
||||
projection views, else xrLocateViews at displayTime); xrEndFrame waits ≤6 ms for the worker's CPU submit, never the
|
||||
GPU. Learned in the headset: quads can't be a background (the Frame draws quad layers above all projection layers
|
||||
whatever the order: hid 4XVR's balcony/controllers), and the Frame doesn't reproject a projection layer from its own
|
||||
pose (images drawn only after head turns, or ahead from xrWaitFrame and sometimes late, wobbled);
|
||||
`equirect_flip/face/res/fps/stereo`), `stable_local`, `focus_hold` (only after 3 s FOCUSED, dips <600 ms),
|
||||
`aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug` (diagnostics); focus_hold is default-on since 2026-10-05. 4XVR re-creates LOCAL spaces every 2–4 s
|
||||
(menu recentring suspect); its theatres are baked 7680×3840 equirect2 images (`assets/100.png` …). Test 360° videos
|
||||
(NASA, public domain) are in the Frame's ~/Videos; copies in `~/Downloads/frameport-360`. 4XVR's "Internal Storage"
|
||||
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
|
||||
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
|
||||
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
|
||||
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
|
||||
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
|
||||
Not yet verified in the headset.
|
||||
|
||||
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
|
||||
Lepton container (`podman exec -i lepton-steamlaunch-<appid> pm install -g -S <size> < apk`) and runs there, VR
|
||||
included (4XVR in Stremio's flatscreen container: FrameBridge 72 fps, settings via LEPTON_ENV_FRAMEBRIDGE_CONFIG). But
|
||||
Lepton's services.jar (`ActivityStarter.execute`) intercepts **every** `android.intent.action.VIEW` with data (any
|
||||
scheme, explicit component or not, no property to disable): it writes `steam://openurl/<uri>` to `/lepton/steam.pipe`
|
||||
(= the host Steam client's `~/.steam/steam.pipe`) and starts nothing. So "open in external player" hand-offs (Stremio
|
||||
→ 4XVR) can't work inside Lepton; don't retry without Valve changing it. Lepton installs exactly one `*.apk` per app
|
||||
folder (two break `get_apk_path`).
|
||||
**Text input (2026-10-03, verified in the headset with Stremio VR):** Lepton's Android has no IME (`ime list` empty)
|
||||
and runs VR apps headless (`lepton.headless=true`): no Android window has input focus (`dumpsys input` FocusedWindows
|
||||
empty), so neither `input text`, a USB keyboard nor Steam's keyboard reach the app. `lepton-show-flatscreen` on a VR
|
||||
app keeps VR working (FrameBridge 72 fps) and gives the window focus; Steam's on-screen keyboard then opens for text
|
||||
fields. Unity's TMP_InputField on non-Quest Android waits for the system keyboard (`TouchScreenKeyboardShouldBeUsed`)
|
||||
and deselects a frame later unless `isKeyboardUsingEvents` (Android: `InPlaceEditing() && m_HideSoftKeyboard`);
|
||||
uGUI InputField's LateUpdate keeps the field when `InPlaceEditing()`. Patch `frame.unity_text_input` rewrites them
|
||||
(`mov w0,#0|#1; ret`) at Cpp2IL's **Offset** (= file offset; RVA differs by 0x4000 in Stremio's lib), Cpp2IL
|
||||
2022.1 pre-release (Unity 6 / metadata v31; Il2CppDumper can't), cached per libil2cpp sha. `/dev/uinput` has an ACL
|
||||
for steamos (Steam Input) → agent v33 `_keyboard` uinput keyboard ("Type on Frame") reaches everything with focus.
|
||||
Installing a second APK into a container with `pm install` re-runs Lepton's post-install hook on the **main** app
|
||||
(`lepton.active_app_id`; moves its files to /data/steam_app) and corrupts it on the next start (fix: touch the APK →
|
||||
re-bake); `cmd_real package install` skips the hook.
|
||||
**User reports (2026-10-04, issues #4-#10):** Steam shortcuts go to the signed-in account (`loginusers.vdf`
|
||||
MostRecent) else every account (agent v35 `library_users`; it used to refuse with >1 account → Play gave Steam's "Game
|
||||
configuration unavailable"); `launch` uses the shortcut's own appid and errors `NOT_IN_LIBRARY`, the app then adds it
|
||||
and plays. Unity IL2CPP fixes share one Cpp2IL run (`unity_text_input.Il2cppReturnPatch`, `ALL_TARGETS`, cache keyed by
|
||||
global-metadata.dat): `frame.unity_runtime_msaa_off` (OVRManager raises MSAA to 4x at runtime: "Switching to the
|
||||
recommended level" → Lucky's Tale restarted the headset; `OVRDisplay.get_recommendedMSAALevel` → 0) and
|
||||
`frame.unity_multipass` (Oculus XR Plugin multiview → MultiPass via `OculusSettings.GetStereoRenderingMode`; I Am Cat's
|
||||
right eye grey). Both checked against Toy Master's Cpp2IL output, not yet in a headset. VR4 quits itself (System.exit
|
||||
after the intro movie) → catalog `use_alt`. **Catalog changes reach existing games** without hardcoded migrations:
|
||||
recipes store `catalog_rev` (= `CatalogEntry.rev()`), `library._follow_catalog` re-derives non-user recipes whose entry
|
||||
changed; a maintained entry beats the user's shared copy when its `updated`/verified date is later (`catalog._newer`).
|
||||
**Round 2 of reports (2026-10-04):** VR4's campaign hang = one fragment shader reading an uninitialized loop counter
|
||||
(reporter's capture): vkshim `vk_shader_fix` setting (`<size>:<sha256>:<offset>:<words>`, from the recipe's `adapter:`;
|
||||
matched by size + SHA-256, words inserted into a copy) — on the device the module is fixed at load ("fixed shader
|
||||
module (6488 -> 6512 bytes)"); campaign itself not yet seen in a headset by us. Text settings (kind "str") stay out of
|
||||
the Game settings dialog (saving it dropped them). I Am Cat: `GetStereoRenderingMode` is inlined (no call sites) →
|
||||
`Il2cppReturnPatch.field_loads` rewrites the field read in `OculusLoader.Initialize` (Cpp2IL gives the field offset,
|
||||
`field:<name>`); verified with Toy Master: eye swapchains array=1 ×2 instead of array=2. Builds record `recipe_fp`
|
||||
(`patches/base.recipe_fingerprint`, `Patch.revision`): a changed recipe or revised patch shows "Update on Frame".
|
||||
Remembered Frames are found again after an address change (`Frame.relocate`: scan, same SSH host key, before
|
||||
login). Packaged apps show artwork by file path (`thumbs.use_file_paths`, #16). Diagnostics: this boot's kernel log.
|
||||
Device tests of requested games (2026-10-04, headless): I Am Cat, Myst 3.3.0, BattleGlide, Blade & Sorcery: Nomad
|
||||
start (RUNNING, frames). Roblox: SIGSEGV in je_free from libroblox.so ~3 s in (likely its anti-tamper vs the re-signed
|
||||
APK). BONELAB (Unity 2021.3 Vulkan, OVRPlugin 1.94): SIGSEGV with pc == fault addr in vkCreateInstance, called from
|
||||
libSLZQuestNative.so's Vulkan hooks into an unmapped (unloaded) library right after OVRPlugin's pre-init
|
||||
xrDestroyInstance; pinning openxr/vrclient libraries (RTLD_NODELETE) did NOT help — find the unloaded library with the
|
||||
linker's dlopen/dlclose logging next. Accounting+ (Unity 2017.4 built-in Oculus, libOVRPlugin + libvrapi): Unity
|
||||
never starts VR on the Frame (no OVRPlugin/OpenXR lines; runs as a 2D app), which is why its "controller setup"
|
||||
screen can't be passed — Unity 2017's Oculus device check, not the controller profiles. Cpp2IL can't read Unity 2017.
|
||||
The Frame's Android runtime (vrclient.so) knows oculus/touch_controller but not Meta's Touch Plus/Pro profiles
|
||||
(OVRPlugin suggests those too, routinely) → adapter `profile_remap`.
|
||||
Headset round 2 (2026-10-04): the Frame drops focus often (ms to 7 s; only some are presence/standby) → games pause
|
||||
or recentre (B&S Player.OnVRPresence → Teleport, BattleGlide/Unreal pauses); focus_hold now 1 s focused / 1 s max dip
|
||||
(longer must still pause). Myst crash = vrclient xrSyncActions race after FOCUSED → `sync_guard` (confirmed); its
|
||||
object glitches: app space warp suspected (FB_space_warp is advertised and used) → recipe `patch_disable_space_warp`.
|
||||
I Am Cat multipass: poses/times consistent, but every frame is submitted after the next xrWaitFrame (one period late;
|
||||
Zink + doubled draws) and the game clamps its physics step to 10 ms → judder while still; testing scale 0.8. Roblox's
|
||||
newest crash is on Fossilize's recording thread → vk_sanitize extended to own-engine libs (engine "Other").
|
||||
Accounting+: unity_oculus_check + native/ovrpshim (libfp_ovrp.so waits via ovrp_WaitToBeginFrame before Unity
|
||||
2017's ovrp_Update2) → VR in the headset at ~72 fps (2026-10-04), but stuck at "press any button" although input
|
||||
reaches its OVRInput cleanly (probe: both Touch connected 0x63, individual buttons, input focus 1) → works with issues.
|
||||
"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`); 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
|
||||
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). Godot 4.2-4.4 (Kotlin `as ClipboardManager`,
|
||||
GitHub #134 EndoparasiticVR) → `frame.godot_clipboard` (nops the `Intrinsics.checkNotNull` before that check-cast; Godot's
|
||||
clipboard methods return at once; analysis `godot_clipboard`, ANALYSIS_VERSION 10; checked on real godot-lib/templates
|
||||
4.2.2-4.4.1, not on the device; 4.1 and 4.5+ don't throw). 2D apps (vr_kind none)
|
||||
keep every suggested patch that isn't about VR (`needs_vr = False`), not a fixed list. FramePort on the Frame:
|
||||
`frame/local.py` (127.0.0.1, own key authorized; "This Frame (experimental)"; app data in
|
||||
~/.local/share/frameport-app because ~/.local/share/frameport is the agent's) and Steam library changes wait while
|
||||
Desktop Mode is open. Verified on the device 2026-10-04 (install + launch test from the Frame). Library updates on the
|
||||
Frame run one at a time (agent v40 flock): two quick uninstalls used to bring a removed shortcut back (Roblox).
|
||||
Headset round 3 (owner's verdicts, 2026-10-04): Blade & Sorcery works — its freezes were the headset's wear sensor
|
||||
flickering off while worn (vrserver.txt "HMD off/on" 0.5-2 s) → per-game `focus_hold_ms=2500` (only this game reacts;
|
||||
not a default). BattleGlide works (focus_hold). Myst works with issues (object glitches; space warp ruled out, its
|
||||
recipe keeps it on). I Am Cat works with issues (judder; scale 0.8 no help). Roblox unsupported (je_free in
|
||||
libroblox, also with the Vulkan shim). Installs skip the Steam restart when the shortcut is unchanged and wait while a
|
||||
game runs (agent v37). New default fixes in FramePort's code reach games already in a library: after every app update
|
||||
`library._follow_catalog` re-derives each non-user recipe once (setting `recipes.app_version`; tests switch it off via
|
||||
`library.REFRESH_ON_UPDATE`). Analysis fields added later: **bump `analysis/detect.ANALYSIS_VERSION`** (stored as `analysis.extra.analysis_version`); older entries whose APK is still there are analysed again at the GUI's start (background thread, not a job: ~8 s per 900 MB APK) and before a build (`pipeline.refresh_analyses`, GitHub #104); only `analysis`/`suggested` change, user recipes stay; an unreadable APK gets `analysis_failed` = the version (not retried until the next bump). Troubleshooting techniques: docs/PLAYBOOK.md "Debugging techniques". Unity `boot.config` "vulkan" substring mislabels GLES games as Vulkan
|
||||
(I Am Cat ran GLES); OVRPlugin's "Unavailable OpenXR extension: XR_FB_scene" is routine (no longer triaged).
|
||||
**Round 4 (2026-10-04):** XR_KHR_android_surface_swapchain is listed by the Frame's runtime but returns
|
||||
FUNCTION_UNSUPPORTED → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`): an ordinary runtime
|
||||
swapchain is returned, the game gets a Surface from a SurfaceTexture on a worker thread (own EGL context, JavaVM from
|
||||
XrInstanceCreateInfoAndroidKHR), frames are read back (glReadPixels) and uploaded in xrEndFrame (Vulkan: own command
|
||||
buffer + fence per panel, never waits; GLES: glTexSubImage2D). I Am Monkey's intro works (owner-confirmed). SUPERHOT
|
||||
(Quest) quit at start: its cloud save folder was mode 1700 and Lepton's app writes through the folder's group →
|
||||
launch.sh `fix_perms` adds u+rwx,g+rwx (agent v41). Lucky's Tale: Unity's Loading.PreloadManager thread grows the
|
||||
native heap ~1 GB/s until the OOM killer (Frame freezes); no fix. `perf` works as steamos (paranoid 2; ptrace_scope 1
|
||||
blocks gdb/eu-stack/debuggerd) — see PLAYBOOK "Debugging techniques". QuestCraft downloads its JRE at first run into
|
||||
`files/runtimes/JRE` (Pojlib Installer); on the Frame it was missing (unpacked by hand, unverified). WiiCompiled: its launcher shows
|
||||
(lepton-show-flatscreen) but imports .wcgame files through Android's document picker (OPEN_DOCUMENT), which Lepton
|
||||
lacks (ActivityNotFoundException) → imported by hand: libmain.so + game.json → internal files/game/<profile>,
|
||||
DATA/ → external files/WiiCompiledOpenXRVR/DATA, MOD/ → .../RetroRewind6 (decompiled with jadx). Lepton has no
|
||||
picker at all: a FramePort fix would be an injected picker activity. Generated cover/banner art used to suppress the
|
||||
Steam placeholder set (no Steam art at all) → fixed in artwork/steam.py.
|
||||
**Steam library failures (GitHub #21/#30):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (same SteamOS build and arm64 beta client as the dev Frame; IDs correct; file written before Steam starts): Play → console_log `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` (= "Game configuration unavailable", Steam treats the id as a store app). Cause unknown; agent v42/43 diagnostics: `launch` returns `steam` (started / error + code / silent from console_log), `collect_diag` adds `steam_library` (real Steam dir, beta, Steam start vs vdf write time, entries per account, devkit games) + `steam-console.txt` (incl. Steam's `logs/shortcuts.previous.txt` if present; steamclient.so has "LoadShortcuts: rejecting attempt to load shortcuts: invalid account ID"). **Fallback (agent v43):** when Play gets AppError_9 the agent registers the game through Steam's devkit interface (what Valve's Devkit Management Tool does, MIT: `devkit-1 steam://devkit-1/<~/.steam/steam.token>/create-shortcut?response=<file>&gameid=<id>&directory=~/devkit-game` written to ~/.steam/steam.pipe; answer file / `.error`; `~/devkit-game/<id>/launch.sh` = link to the game's launch.sh, `<id>-argv.json` ["launch.sh"] (relative to the folder), `<id>-settings.json` {steam_play 0, compat_tool ""}). Steam adds it live (no restart), picks its own appid (read back from shortcuts.vdf by DevkitGameID), shows it as a normal non-Steam game named "Devkit Game: <id>" (OpenVR 0, but VR works: owner-verified 2026-10-04), art copied to grid/<appid>*. Ids: letter first, then letters/digits/_ only (spaces, '-', leading digit → "missing/invalid arguments"). Later Plays use the devkit entry; uninstall (`devkit_unregister`, `delete-shortcut` live) and purge remove it. Verified on the dev Frame: register/launch/unregister; the AppError_9 trigger itself only in unit tests. GitHub #42 (two Steam accounts, neither MostRecent): Steam rewrote the active account's shortcuts.vdf without FramePort's entries right after starting, and added the devkit entry live without saving it, so `devkit_appid` (vdf only) failed ("Steam added the devkit entry but didn't save it"); agent v55 `devkit_appid_from_log` reads Steam's `sanitize shortcut app id "~/devkit-game/<id>/launch.sh": replacing 0 with N` console line. GitHub #41 (one account): same Steam behaviour, and every install restarts Steam, which forgets the never-saved devkit entry. Agent v56: the log fallback reads only the current session's console_log.txt; Play re-registers a devkit entry that gets AppError_9; art updates also write the devkit entry's grid art (`copy_grid_art`). Open: why some Frames' Steam never loads or saves shortcuts.vdf (single account too).
|
||||
**Exit game (GitHub #36, 2026-10-04):** `Apps.TerminateApp` (what Exit game calls; reachable for tests through Steam's CEF devtools on 127.0.0.1:8080, SharedJSContext, `Runtime.evaluate` — a stdlib websocket client is enough) closed both a normal shortcut and a devkit entry on the dev Frame within 1-3 s. SIGTERM to Steam's `reaper` alone left launch.sh + Lepton (setsid) + the container running (an earlier headset launch ran on for 87 min); SIGTERM to launch.sh cleans up in ~4 s. Agent v44: launch.sh's 2 s loop ends the game when its parent (the reaper) is gone; `upgrade_launchers` (from ensure_host_fixes) adds that to existing launchers in place. Which in-headset exit path fails is still unknown. Screenshots on the Frame: all under SteamVR's appid 250820 (`760/remote/250820/screenshots`, screenshots.vdf has creation time, no game).
|
||||
**SteamVR dashboard at game start (2026-10-04):** the dashboard (Resume game / controller / VR options) was open whenever a FramePort game started (also over Lepton's 2D launcher). Steam's UI exposes `SteamClient.OpenVR.VROverlay.{IsDashboardVisible,HideDashboard,ShowDashboard}` (CEF devtools, `steam_js` in the agent). Agent v44: launch.sh starts `_dashboard_worker` (waits for FrameBridge's first `pacing:` line in launch.log, then hides a visible dashboard for 25 s, at most 3 times; log `<base>/dashboard.log`; opt out with env FRAMEPORT_KEEP_DASHBOARD=1). Verified on the dev Frame (dashboard shown → 4XVR launched via Steam → hidden ~5 s after the first frames); not yet seen in the headset. **Headset (ITR2, 2026-10-05): too late** — Steam shows its frame menu (`valve.steam.gamepadui.frame.menu`, vrwebhelper_systemui.txt `[PooledPopups] Showing`) ~0.3 s after the game's first submitted frame (FrameBridge `new layer:`), but the first `pacing:` summary comes ~8 s later; the owner had pressed Resume (`[HideDashboard] return_to_game`) before the worker looked. Agent v52: watches from the first `new layer:` line (incremental log reads, 0.5 s polls, 30 s window). Agent v59: watches 120 s and hides up to 10 times (the menu came back after 30 s in the owner's sessions). Agent v53: stops for good once the player opens the dashboard with the controller (`toggle_dashboard_action` in vrwebhelper_systemui.txt since the worker started): v52 closed it 60 ms after each press, and the game, paused for it, stayed paused.
|
||||
**Space warp / ITR2 (2026-10-04):** Into The Radius 2 (UE5, `libUnreal.so` + OVRPlugin, Vulkan) uses Application SpaceWarp (extra swapchains `376x376 format=97` motion vectors + `format=129` depth); #35 reports flickering textures. OVRPort's `patch_disable_space_warp` has no effect on UE5, and hiding XR_FB_space_warp in FrameBridge's enumerate doesn't reach the game either: **OVRPort's dispatcher offers/enables XR_FB_space_warp itself**. FrameBridge `hide_space_warp` (per game, off) hides it and strips `XrCompositionLayerSpaceWarpInfoFB` (1000171000) from the projection views (log `hide_space_warp: removed space warp info`); the game still renders its MV swapchains. Test build installed on the dev Frame; flicker not yet checked in a headset. Heuristic: UE5 + OVRPlugin shows it as an option (off). `equirect_emul` is suggested for Unreal games whose graphics API isn't detected ("GLES or unknown"); on Vulkan it switches itself off (Myst/Riven/ITR2): harmless, but Unreal Vulkan detection is a known gap.
|
||||
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). Verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
|
||||
**Heuristics eval (2026-10-04):** `scripts/eval_heuristics.py "<VR CyberDeck Portable>"` → 19/44 recipes exact, 322/352 fields (most diffs: `frame.unity_text_input` suggested where older catalog recipes lack it; the rest are runtime-only findings: use_alt, vk_shader_fix, focus_hold, sync_guard, multipass). `PATCHED/` copies aren't scored (already converted).
|
||||
**Power (0.9, agent v47):** sidebar bar below the Frame card: Sleep / Restart / Shut down, each confirmed (`app.frame_power`, a running game asks again). logind answers CanSuspend/CanReboot/CanPowerOff = "challenge" for an SSH session but "yes" inside a user unit, so agent `power` runs `systemctl suspend|reboot|poweroff` from a transient `systemd-run --user --on-active=3` timer (the command answers first). Restart verified on the device (back in ~84 s, new boot id).
|
||||
**Proton default:** the newest **stable** ARM64 Proton (`pick_proton`; owner's headset A/B); `pcvr.proton_tool` dropdown (Experimental = alias `proton-experimental`), `ensure_proton`, `proton_alternative_worth_trying`. → docs/DEVICE_NOTES.md#proton-default-stable-vs-experimental
|
||||
**USB cable link:** `usb0` is a USB NCM gadget (Developer Mode only); Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py`); ~37 MB/s; `scripts/usb_autotest.py`, `frameport frame usb-check`; discovery prefers USB > hotspot > network (`discovery.dedupe`). → docs/DEVICE_NOTES.md#usb-cable-link
|
||||
**Proton default (agent v51, owner's choice 2026-10-05):** the newest **stable** ARM64 Proton is the default for PC VR games (`pick_proton`: asked-for name/alias, else newest installed stable, else any stable). Agent v49-50 defaulted to Experimental; the owner's headset A/B with Rick and Morty (Experimental 69 % reprojected frames, stable felt much smoother) reverted it. The game page's Customize shows `pcvr.proton_tool` as a dropdown (Stable (default) / Experimental = alias `proton-experimental`); `ensure_proton` installs a chosen one (it asks once more for the runtime Steam only names after Proton is installed; Experimental installed unattended on the device in 53 s). A PC VR launch test that fails on stable suggests Experimental (`pipeline.PROTON_TOOL`, `proton_alternative_worth_trying`, button "Try Proton Experimental and reinstall"). Remaining Rick and Morty stutter on head turns = the game rendering below the refresh rate (reprojection), not Proton.
|
||||
**USB cable link (verified 2026-10-05, cabled to the dev PC):** `usb0` is a USB **NCM** gadget (configfs g1: `ncm.usb0` + `ffs.adb`, Valve 28de:2460) set up by `usb-ncm-gadget@usb0.service`, which only runs with `ConditionPathExists=|/etc/systemd/system/adbd.service.d/steamos-devkit-enabled` = **Developer Mode only**. `usb-ncm-dnsmasq@usb0` serves DHCP with fixed `dhcp-host` entries: Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py` FRAME_USB_IP/PC_USB_IP). Windows 11 binds its built-in "UsbNcm Host Device" (no driver); WSL mirrored sees eth 10.86.200.234. USB is high-speed (USB 2.0): upload 36.9 MB/s vs 11.3 MB/s home Wi-Fi. Frame→PC over the cable worked without a firewall change. `scripts/usb_autotest.py` (Windows via powershell.exe from WSL), `frameport frame usb-check`. Wizard "Set up with a USB cable" (`PairingServer(host=PC_USB_IP)`; the real bootstrap ran end to end over USB). Discovery dedupes by SSH host key and prefers USB > Frame hotspot > network (`discovery.dedupe`); a lost address is re-found by key (`Frame.relocate`, tested with a dead saved address).
|
||||
**Catalog updates without a release (2026-10-05):** apps fetch `catalog/games/*.yaml` from GitHub `main` (`recommend/catalog.refresh_remote`: one API call `git/trees/main?recursive=1` + raw.githubusercontent downloads of changed blobs, cached as `catalog-gh-<sha>.yaml`, every 6 h; GUI at start + Settings → Data "Check now"; CLI background thread + `frameport catalog-update`). `catalog.load()` only reads the cache. A remote entry is skipped (`unusable_reason`) when it names a patch/adapter key this app doesn't register, has a field `CatalogEntry` doesn't know, or `min_app` > this version: add `min_app` (or rely on new patch ids) when a config needs an unreleased FramePort. Remote beats bundled unless the bundled entry has a later updated/verified date; the user's own entries still win as before. Off: setting `catalog.auto_update`, env FRAMEPORT_NO_CATALOG_UPDATE; FRAMEPORT_CATALOG_URL still overrides the source. So: **pushing a catalog change to main reaches users within 6 h** (their recipes follow via catalog_rev → Update on Frame).
|
||||
**Start activity / QuestCraft / ITR2 pop-in:** `frame.start_activity` (analysis `extra.vr_activity`, `axml.set_start_activity`; OVRPort gives every MAIN activity LAUNCHER, so take the VR activity from the original APK); Unreal reads user config from **internal** storage; Lepton passes `LEPTON_ENV_<NAME>` to the app as `<NAME>` (QuestCraft `LIBGL_NOERROR`); vkshim `vk_query_slots` left off. → docs/DEVICE_NOTES.md#start-activity--questcraft--itr2-pop-in
|
||||
**Vulkan validation / ITR2 solved:** bundle Khronos' `libVkLayer_khronos_validation.so` into a test APK + vkshim `vk_validation=1` (logcat tag VALIDATION); `vk_spec_fixes=1` drops ITR2's invalid depth resolve; VALVE_rpo removed its fog (catalog `lepton_env: VK_INSTANCE_LAYERS`). Agent `not_started` waits 30 s for "Boot complete!". → docs/DEVICE_NOTES.md#vulkan-validation--itr2-solved
|
||||
**GitHub #38/#39:** SteamVR rejects image rects past their swapchain (-25) → FrameBridge `rect_clamp` (default on), triage `swapchain-rect-invalid`; The Room VR MSAA crash in libgallium. → docs/DEVICE_NOTES.md#github-3839-powerwash-simulator-the-room-vr
|
||||
**Headset round 2026-10-05:** FrameBridge `pose_consistency` (I Am Cat judder), xrshim `haptic_fix` (Lucky's Tale), **`focus_hold` on by default with 5 s** (wear-sensor dips, `logs/eyetracking.txt`), `unity_no_overlay_copy` (The Room VR), ovrpshim frame wait for Unity 2019 (`UnityOculusCheck.legacy_loop`), `frame.ovr_trace` (opt-in, `native/ovrtrace`, logcat `fp_ovrtrace`). → docs/DEVICE_NOTES.md#headset-round-2026-10-05-evening
|
||||
**Unity built-in VR input:** probe builds `FRAMEPORT_INPUT_PROBE=1`; ovrpshim adds a `GetMouseButtonDown` icall click (Accounting+); libunity 2019 only polls Touch when the device model starts with "Oculus" → `unity_oculus_check` revision 4 `oculus_model_checks` (BattleSisters); `haptic_fix` suggested for every game with libOVRPlugin.so. → docs/DEVICE_NOTES.md#unity-built-in-vr-input-accounting-battlesisters
|
||||
**Vader Immortal (GitHub #49):** stuck on its loading card and leaked GPU mappings (solved below: `gl_error_clear`); diagnostics FrameBridge `layer_debug`, `strip_color_bias`, `frame_balance`, `snapshot=N` (`snapshot_gl.c`: saves the submitted left-eye image as `files/fb_snap_*.ppm`). → docs/DEVICE_NOTES.md#vader-immortal-headless-investigation
|
||||
**SteamVR overlay apps:** host OpenVR overlays composite over Lepton games; detection `analysis/vroverlay.py`, patch `pcvr.vr_overlay`, Linux fields `vr_overlay`/`vr_overlay_autostart`, agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay`; SteamVR on the Frame reads only `binary_path_linux_arm`; FramePort's own autostart (`ensure_overlay_service`, `frameport-vr-overlays.service`). See also docs/FRAME_RUNTIME.md "Overlay apps". → docs/DEVICE_NOTES.md#steamvr-overlay-apps
|
||||
**Vader's leak solved / AC Nexus flips:** stale GL errors broke SteamVR's GL interop → adapter `gl_error_clear` (GLES, catalog for Vader ep1-3); read `xrclient_<pkg>.txt` in Steam's logs for runtime errors; the runtime caps swapchains per session (~15) → flip emulation in place; adapter `flip_dump`. → docs/DEVICE_NOTES.md#vaders-leak-solved--ac-nexus-flips
|
||||
**Mixed reality: scanned rooms (branch mr-scene):** agent `room_capture` records Valve's RoomView 3D depth mesh -> `fp_roomscan` -> `room.txt` + `room-mesh.bin` (docs/mr/ROOM_FORMAT.md; refined by default: the owner's cleanup passes + label rules in `native/roomscan/refine.c`/`rules.c`, unrefined output kept as `room-raw.txt` + `room-mesh-raw.bin`, head path/settings/report/labels/door suggestions in `scan.json`, `--refine-only <dir>` re-runs it, `--refine 0` = old output; docs/mr/scan.md "Refinement") in `~/.local/share/frameport/rooms/<universe>/`, copied next to settings.conf (`_room_sync`); FrameBridge (`scene_room.c/scene_meta.c/scene_anchors.c`, all through xrshim) serves walls, volumes, planes, the room mesh, META discovery/persistence, saved anchors and `XR_META_boundary_visibility`; `scene_emul` + `frame.meta_permissions` for every USE_SCENE/USE_ANCHOR_API game (migrations `scene_emul_all`, `scene_permissions`); per-game `scene_hide_bounds` (Demeter). → docs/DEVICE_NOTES.md#mixed-reality-scanned-rooms
|
||||
**Unreal's self-restart:** UE 4.21+ restarts itself after the first shader precompile and Lepton ends the container → launch.sh RESTART_BLOCK starts Lepton once more (`upgrade_launchers`; `FRAMEPORT_NO_RESTART=1` off). → docs/DEVICE_NOTES.md#unreals-self-restart
|
||||
**xrWaitFrame runs ahead:** the runtime sometimes returns a display time <1 refresh after the last submit and rejects the second submission (`VRCompositorError_AlreadySubmitted`) → the VrApi bridge re-waits (`pacing 2s:` counters). → docs/DEVICE_NOTES.md#xrwaitframe-runs-ahead-journey-of-the-gods
|
||||
**Lepton's logcat mirror dies** (EOF, read failure, or silent) → launch.sh `_logcat_keeper` reads `podman exec <container> logcat` itself into launch.log (log `<base>/logcat-keeper.log`). → docs/DEVICE_NOTES.md#leptons-logcat-mirror-dies
|
||||
**Double launch:** launch.sh takes `flock` on `<base>/.launch.lock` (fd 9); a second launch exits 0 and logs to `<base>/launch-dup.log` (`upgrade_launchers` adds it). → docs/DEVICE_NOTES.md#double-launch
|
||||
**Issue triage round 2026-10-06:** FrameBridge turns haptic amplitude 0 into xrStopHapticFeedback, per-game `haptic_scale`; Lambda1VR → `frame.tbxr_vendor`; Jurassic World → `frame.vrapi_stub`; Metro Awakening / Pinball FX → `hide_space_warp`; `frame.ovr_trace` can't trace Unity games; the VrApi bridge's 30 s head-pose deadline ends headless VR mode. → docs/DEVICE_NOTES.md#issue-triage-round-2026-10-06
|
||||
**Lepton storage:** each app's /sdcard has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (agent `storage_targets`); the media index never works; Lepton grants all runtime permissions. → docs/DEVICE_NOTES.md#lepton-storage
|
||||
**SteamVR per-app settings:** `native/vrsettings` = `fp_vrsettings.exe` sets `steam.app.<appid>` `preferredRefreshRate` / `motionSmoothingOverride` live; `pcvr.steamvr_tuning` (default on, PC only). → docs/DEVICE_NOTES.md#steamvr-per-app-settings
|
||||
**Language packs:** `frame.langpacks` (opt-in, `native/langpack`, `elf.hide_exports`, logcat tag `fp_langpack`) serves `<tag>.lang` files; Unreal needs the pack's Metadata = the game's version (`with_metadata`, `FRAMEPORT_LANGPACK_META`) and the `/storage/emulated/0/...` path spelling; `FRAMEPORT_LANGPACK_SKIP`. → docs/DEVICE_NOTES.md#language-packs
|
||||
**Cube swapchains:** the runtime refuses `faceCount=6` → FrameBridge `cube_standin` (default on, GLES only, `native/adapter/cube_standin.c`). → docs/DEVICE_NOTES.md#cube-swapchains
|
||||
**Hardware video decoding:** `native/hevc` OMX plugin (`OMX.frameport.{avc,hevc,vp9}.decoder`), installed once per Frame (`ensure_video_codec`, agent `video_codec_status`/`install_video_codec`/`video_codec_switch`); **bump `revision` in native/hevc/build.py with every artifact change**; per game `frame.hw_video_decode` (analysis `media_codec`), Settings `video.hw_decode`, `FRAMEPORT_NO_HW_VIDEO=1`; Batman migration `batman_video_patches` + `surface_native`. → docs/DEVICE_NOTES.md#hardware-video-decoding
|
||||
**Vulkan shader dump:** adapter `vk_shader_dump=1` (`native/vkshim/shader_dump.h`) writes SPIR-V modules + `index.txt` to `files/fp_vk_shaders/`; `collect_diag` returns the newest session's modules (`shader_dumps`); triage `gpu-hang`, `triage.graphics_api`. → docs/DEVICE_NOTES.md#vulkan-shader-dump
|
||||
**Unreal OBB check (#159):** `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`) turns off Epic's OBB CRC pass; triage `unreal-obb-check-stuck`. → docs/DEVICE_NOTES.md#unreal-obb-check
|
||||
**Issue round 2026-10-12:** Lepton breaks on unquoted paths with spaces → launchers give Lepton a `~/.local/share/frameport/lepton-paths/<pkg>` link (`app_dir_lines`/`set_app_dir`/`launcher_base`); `frame.eos_keychain_loop`, `frame.unity_tls_key_pool` (`libfp_tkeys.so`, `elf.rename_imports`), `frame.vermillion_scene_canvas`; `tools/cpp2il.py` falls back to Cpp2IL's dev build for metadata v107; Linux `desktop_icon`; `scripts/upload_bench.py`. → docs/DEVICE_NOTES.md#issue-round-2026-10-12
|
||||
**OpenSSH uploads (#324/#242):** `native_upload.quote` escapes only backslash and `"`, Win32 local paths go as `C:/…`; `Frame.auth` records the login that worked (app_key / user_key from `ssh.extra_keys`, `frame ssh-keys`, agent, password); strict IdentitiesOnly first, relaxed retry. → docs/DEVICE_NOTES.md#openssh-uploads
|
||||
**Meta user id (#182):** OVRPort's platform loader makes up a new user id every start → `frame.ovr_stable_user` (default on) returns a stable per-game, per-user id. → docs/DEVICE_NOTES.md#meta-user-id
|
||||
**Vivox API 31 (#101):** newer Vivox calls Android 12 AudioManager methods → `frame.vivox_audio_route` (analysis `vivox_api31`, `Dex.return_early`). → docs/DEVICE_NOTES.md#vivox-api-31
|
||||
**Steam Input gamepad for 2D apps (#162):** opt-in `device.steam_gamepad` (deployment `steam_gamepad` → launcher `GAMEPAD_LINE` → `PODMAN_WRAPPER` / `podman_run_args` bind-mount Steam's virtual pads). → docs/DEVICE_NOTES.md#steam-input-gamepad-for-2d-apps
|
||||
**Multiview programs on flat framebuffers (#77):** Mesa drops num_views=2 draws on single-view FBOs → `frame.gl_multiview_fbo` (`native/glmv` = `libfpglmv.so`, analysis `gl_multiview_libs`); headset-verified with Doom3Quest. → docs/DEVICE_NOTES.md#multiview-programs-on-flat-framebuffers-doom3quest
|
||||
**BlazeRush (#57):** `frame.blazerush` (1.0.349 only), GL shim `gl_int_attribs` / `gl_highp_markers`, saved VrApi FOV (`files/framebridge-vrapi-fov.txt`), `device.config_sync` (catalog `config_sync`); launch tests treat an end after the headless deadline as a pass (triage `headless_end`). → docs/DEVICE_NOTES.md#blazerush
|
||||
**Session triage:** real play sessions are triaged too: agent `last_play` / `session_log` (`<anchor>/plays.log`), `pipeline.sessions_due` / `triage_session` = `validate/session.analyze` (incl. `space-warp-used`, `gpu-hang`, `slow-frames`, `focus-dips`) → library `last_session` → game page "Last session"; `apply_suggestions_live`; CLI `frameport session <pkg> [--apply]`; pac_hints stays triage-only. → docs/DEVICE_NOTES.md#session-triage
|
||||
**Start activity / QuestCraft / ITR2 pop-in (2026-10-05):** WiiCompiled's VR part (`QuestActivity`, SDL, process `:game`) stays behind Lepton's flat window when the launcher is shown (`device.text_input_window`). `frame.start_activity` (opt-in; analysis `extra.vr_activity` = the original APK's activity with Meta's VR category when it isn't the launcher) gives that activity's filter LAUNCHER and every other LAUNCHER becomes INFO, per element (`axml.set_start_activity`); Lepton's own `apk-info-extractor --print-activity-name` (on the Frame under Lepton/liblepton/apk_extractor) then answers QuestActivity. **OVRPort gives every MAIN activity LAUNCHER + VR categories**, so the VR activity must come from the original APK's analysis, not the converted manifest. Lepton has no activity override (`APP_ACTIVITY` is reset when app_metadata.sh is sourced). QuestCraft reaches Minecraft 1.21.5 (Fabric, Vivecraft, Sodium; LTW "Large Thin Wrapper" GL 3.0 on the Frame's Zink) and stops at `OpenGL error 1282` in `WindowFramebuffer` createTexture (Minecraft's crash report in `files/instances/1.21.5/crash-reports/`, Pojlib's `files/latestlog.txt`); the vrclient SIGSEGV / "pthread_mutex_lock called on a destroyed mutex" afterwards is only the shutdown (Unity's render thread still in vrclient while the instance is destroyed). ITR2 "models popping in and out" in game: Unreal reads user config from **internal** storage (`lepton-data/internal/<pkg>/files/UnrealGame/<Project>/<Project>/Saved/Config/Android/Engine.ini`, not external); `r.AllowOcclusionQueries=0` there broke its menus (removed again). Vulkan shim `vk_query_slots` (two slots per occlusion query) never saw ITR2 create an occlusion query pool, and the run with it crashed (Unreal RenderThread SIGSEGV, caught by sentry-native: no tombstone; the backtrace is in `cache/sentry/*/.sentry-native/*.run/*.envelope` with module offsets) - left off. Next suspect: Unreal 5 mobile HZB occlusion reading depth stored with Valve's FDM injection layer; test = launcher `VK_INSTANCE_LAYERS=''` (Lepton then loads neither VALVE_rpo nor VALVE_fdm_injection, Fossilize still). QuestCraft: LTW overrides glGetError and returns 0 with `LIBGL_NOERROR` set; Lepton passes `LEPTON_ENV_<NAME>` to the app as `<NAME>`, so catalog `lepton_env: {LEPTON_ENV_LIBGL_NOERROR: '1'}` gets Minecraft past the GL error 1282; then black picture (see the catalog entry). FrameBridge logs `new layer` once per layer type per process and projection views only for the first frames, so a second session in the same process (Vivecraft after Unity) needs `layer_debug=1`.
|
||||
**Vulkan validation / ITR2 solved (2026-10-05):** Khronos' validation layer can't come from Lepton (its layer dir is in the read-only guest image; the Linux copies on the Frame are glibc) and a global `debug.vulkan.layers` property (via the LEPTON_GFXRECON_FP_PROPS trick) kills the container at boot. Working recipe: bundle `libVkLayer_khronos_validation.so` (Khronos android-binaries release, arm64-v8a, stored uncompressed) into a test APK re-signed with the game's key (`apk.sign.sign`), install with `frameport install <pkg> --apk`, and set the Vulkan shim's `vk_validation=1` (it adds the layer at vkCreateInstance; findings under the logcat tag VALIDATION in launch.log, each message ID capped at 10). ITR2's flicker + windows behind models = Unreal's Qualcomm shader-resolve subpasses with a depth resolve of a single-sampled depth attachment (VUID-04908/-03179); `vk_spec_fixes=1` drops that resolve (its TRANSFER_DST half never applies to ITR2: the main depth is transient). Valve's VALVE_rpo layer removed ITR2's fog (catalog `lepton_env: VK_INSTANCE_LAYERS: VK_LAYER_VALVE_fdm_injection`). `r.ViewDistanceScale=2` in the internal Saved/Config/Android/Engine.ini only exists on the owner's Frame (the catalog can't write internal storage yet). Launch tests: Lepton prints a transient "is not a running context" on the first start after an APK change (agent v54 `not_started` waits 30 s for "Boot complete!").
|
||||
**GitHub #38/#39 (2026-10-05):** PowerWash Simulator stuck at "Waiting.." on a reporter's Frame = Unity sized the right-eye image rect past its swapchain (rect 268+1656 on a 1920-wide swapchain; the runtime's recommended size differs per Frame) and SteamVR rejected every frame (`xrEndFrame failed -25`, XR_ERROR_SWAPCHAIN_RECT_INVALID), after which the game stopped calling xrEndFrame. FrameBridge `rect_clamp` (default on; sizes noted in xrCreateSwapchain; clamped layers count as swapped) keeps projection/quad rects inside their swapchain; triage `swapchain-rect-invalid`. Not reproducible on the dev Frame with `scale` (Unity's eye sizing isn't just the recommended width); no regression there (72 fps). The adapter's revision was NOT bumped (that would mark every installed game outdated): affected users rebuild. The Room VR (#38) = Unity's render thread SIGSEGV in libgallium_dri.so ~30 ms after the game recreates its eye swapchain with samples=4 (FrameBridge retries samples=1); the game's own code turns MSAA on (not OVRManager); candidate fix: hide GL_EXT_multisampled_render_to_texture (GL shim) - needs the APK to test. The launch test passed both (render thread dead / no frames): triage gap.
|
||||
**Headset round 2026-10-05 (evening):** I Am Cat's judder = repeated xrLocateViews of one display time returning
|
||||
slightly different poses → FrameBridge `pose_consistency` (owner-verified, works). Lucky's Tale freeze on touching a
|
||||
save slot = OVRPort's OpenXR dispatcher converting XR_FB_haptic_amplitude_envelope with ns read as s (GitHub #9) →
|
||||
xrshim `haptic_fix` (owner-verified); its remaining short pauses were the wear sensor ("HMD off/on" 0.5-1 s in
|
||||
`logs/eyetracking.txt`, not vrserver.txt) → **`focus_hold` is on by default with 5 s** (owner's choice; C defaults
|
||||
too). The Room VR works (`unity_no_overlay_copy`). BattleSisters (Unity 2019.4 built-in VR, no libOculusXRPlugin.so)
|
||||
flooded "outside of frame bounds" ~2400/s and hung the GPU (kernel hangcheck, zink DEVICE LOST) → the ovrpshim frame
|
||||
wait now also applies to Unity 2019 without the Oculus XR Plugin (`UnityOculusCheck.legacy_loop`). Vader Immortal
|
||||
(UE4, stuck on the loading image after the intro, GitHub #49): VRP repacks carry a Frida gadget (`libfrda.so`,
|
||||
config `hijack_responses`), but OVRPort's `patch_clean_up_frida` removes its loadLibrary call, so it never runs on
|
||||
the Frame; `frame.ovr_trace` (opt-in, `native/ovrtrace`: 1138 exported ovr_* stubs → real loader functions, logcat
|
||||
tag `fp_ovrtrace`: calls, PopMessage answers, unanswered requests every 10 s) is the next diagnostic.
|
||||
**Unity built-in VR input (2026-10-05):** probe builds (`FRAMEPORT_INPUT_PROBE=1 frameport build …`; it logs
|
||||
trigger/grip crossings too) showed OVRPlugin returns the full Touch state for every controller mask (also Go masks);
|
||||
presses reach the games' OVRInput. Accounting+ (Il2CppDumper v6.7.46 reads its Unity 2017 metadata v24, run on Windows)
|
||||
passes stance selection (NewtonVR grip/trigger) but its motion warning waits for `Input.GetMouseButtonDown(0/1)`,
|
||||
which Lepton never delivers (no focused Android window) → ovrpshim registers its own
|
||||
`UnityEngine.Input::GetMouseButtonDown(System.Int32)` icall (il2cpp_add_internal_call, after Unity's: resolve first)
|
||||
that adds a click in the frame a Touch trigger or A/B/X/Y is newly pressed (`unity_oculus_check` revision 3).
|
||||
BattleSisters (Unity 2019 InputSystem/XR InputDevices via `VrHandInput`): libunity's Oculus module (OVRPlugin
|
||||
function table: a global pointer at 0x16b57c0 in this build, slots filled by name, e.g. +0x160 GetControllerState,
|
||||
+0x168 State2, +0xe8 GetNodePresent) only reports controllers when `strncmp(deviceModel, "Oculus", 6) == 0` (next to
|
||||
the Go check `deviceModel == "Oculus Pacific"`); Lepton's model is "Valve Lepton" → Go/unknown → only Go masks polled,
|
||||
buttons dead. `unity_oculus_check` revision 4 (`oculus_model_checks`, Unity 2019 only) turns the 17 compares' length
|
||||
into 0 (`orr w2, wzr, #6` → `mov w2, #0` after the adrp/add of the "Oculus" literal; the string stays: it is also
|
||||
Unity's VR device name). Device result: libunity now polls `ovrp_GetControllerState(0x3)` (Touch); owner-confirmed
|
||||
2026-10-06: buttons work. Its first controller vibration then hit OVRPort's haptic-envelope bug (11 GB, OOM kill) →
|
||||
`haptic_fix` is now suggested for every game with libOVRPlugin.so (settings detect), BattleSisters works. Ruled out before: ProductName, GetNodePresent, the device-model string itself, exports.
|
||||
Accounting+ works (owner, 2026-10-06).
|
||||
**Vader Immortal (UE4, GitHub #49, 2026-10-06, headless):** stuck after the intro on an in-game image (the splash
|
||||
quad ends ~6 s in; then the game's own projection frames, 72 fps, balanced xrBeginFrame/xrEndFrame). Not the Platform
|
||||
SDK (`frame.ovr_trace`: only user + entitlement, both answered) and not the repack's Frida gadget (OVRPort's
|
||||
`patch_clean_up_frida` removes its loadLibrary). It **leaks ~430 GPU mappings (/dev/dri/renderD128) and ~20 MB a
|
||||
second** (6.5 GB + swap after 5 min, then 26 fps): page-fault stacks (perf -e page-faults, offsets resolved with the
|
||||
process maps + vrclient.so's own symbols; its text segment is at file offset + 0x4000) end in SteamVR's runtime:
|
||||
`xrBeginFrame → CSxrCompositorOpenVR::BeginFrame → SubmitExplicitTimingData → CVRCompositorSharedTextures::
|
||||
BeginGPUTimingCommandBuffer` and `xrEndFrame → CVRCompositorClient::SubmitWithArrayIndexAndTime`. Ruled out: the
|
||||
layer color scale/bias + image layout structs (FrameBridge `strip_color_bias` 1/2, diagnostic), Valve's Vulkan
|
||||
layers (VK_INSTANCE_LAYERS=""), array swapchains in general (Lucky's Tale/I Am Cat flat). Kernel tracepoints aren't
|
||||
allowed for steamos. FrameBridge: `layer_debug` logs xrDestroySwapchain and per-5 s xrBeginFrame/xrEndFrame counts;
|
||||
`frame_balance` (ends an open frame before the next begin) exists but Vader never leaves one open. Next (built,
|
||||
not yet run: the Frame slept): `layer_debug` also counts xrAcquire/Wait/ReleaseSwapchainImage per 5 s (hooked only
|
||||
with layer_debug, after the other conditional hooks) to see whether Vader skips a wait or release: it doesn't (361/361/361 per 5 s).
|
||||
FrameBridge `snapshot=N` (`snapshot_gl.c`, GLES): every N s the left-eye image the game submits is read back on its
|
||||
own context and saved as `files/fb_snap_0-7.ppm` (quarter size) — headless launches show a black headset view, this
|
||||
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".
|
||||
**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
|
||||
`flock` on `<base>/.launch.lock` (fd 9, inherited by Lepton; the watchdog and dashboard helpers close it with
|
||||
`9>&-`); a second launch exits 0 and logs to `<base>/launch-dup.log`. `upgrade_launchers` adds it to existing
|
||||
launchers. Verified on the device (second launch during boot: ignored, the first kept running). Vader on its loading
|
||||
card ignores input too (owner pressed/held every button: presses reach the runtime, the card never changes).
|
||||
**Issue triage round (2026-10-06, owner's headset):** vibration in OVRPlugin games = 2 s vibrations updated per frame and
|
||||
stopped with amplitude 0; FrameBridge turns amplitude 0 into xrStopHapticFeedback (else each buzz ran 2 s), haptic_fix
|
||||
uses the envelope RMS (was its peak), per-game `haptic_scale` → Creed/The Boys/Jurassic World fine. Lambda1VR (TBXR):
|
||||
loader named by Build.MANUFACTURER (`frame.tbxr_vendor`), no graphics extension enabled (FrameBridge adds
|
||||
XR_KHR_opengl_es_enable + asks for the requirements on -50), always multisampled render-to-texture (GL shim gives
|
||||
single-sampled stand-ins when GL_EXT_multisampled_render_to_texture is hidden) + its xash/ data → works. Jurassic World:
|
||||
`frame.vrapi_stub`. Metro Awakening: contributor's vkshim fixes + hide_space_warp + no VALVE_rpo (jumping polygons like
|
||||
ITR2). Pinball FX VR: hide_space_warp (stutters). Eleven: a Meta online request fails after platform init → needs Meta
|
||||
services. Star Wars Tales: top half black + freeze after loading; ruled out: asset-file paks (never requested), Valve
|
||||
foveation (off: no change), GL errors (MESA_DEBUG=1: none), EGL_BAD_ACCESS once in OVRPlugin init (harmless); the game's
|
||||
eye image reads back black. BlazeRush: VrApi bridge exports + ovr*_ToString stubs + avatar stub get it to the menu room
|
||||
at 72 fps, full input reaches it (diagnostics `input 5s`: Touch type only, sticks 1.0, buttons, poses 0x8f), but the room
|
||||
draws no controllers and ignores input; avatar loader forced to its "Failed" path (no logged-in user) changed nothing.
|
||||
`frame.ovr_trace` can't trace Unity games (P/Invoke dlsym; nothing imports ovr_*). Headless: the VrApi bridge's 30 s
|
||||
head-pose deadline ends VR mode without a worn headset (not a game bug).
|
||||
**Lepton storage (2026-09-30):** each app's /sdcard (= /storage/emulated/0 → `<base>/lepton-data/external`) has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (liblepton/mounting.sh, only if they exist at start); agent v24 `storage_targets` reads that mapping. Android's MediaProvider canonicalises paths to /home/steamos/... and rejects every file ("doesn't appear under [/system/media...]"), `sm list-volumes` is empty: the media index never works, apps must browse folders. Lepton installs with `adb install -g` (runtime permissions granted, MANAGE_EXTERNAL_STORAGE too). Files: `install/files.py`, `frameport frame send|storage`, GUI Files tab (formerly Frame → Send files).
|
||||
**SteamVR per-app settings (2026-09-30):** editing steamvr.vrsettings while SteamVR runs is lost; the web API (127.0.0.1:27062 /app/setsettings) needs `x-steamvr-secret`. `native/vrsettings` = `fp_vrsettings.exe` (freestanding, OpenVR `FnTable:IVRSettings_003` as a Utility app, loads SteamVR's bin/win64/openvr_api.dll) sets them live and SteamVR persists them: section `steam.app.<shortcut appid>`, keys `preferredRefreshRate` (float) and `motionSmoothingOverride` (0 global, 1 on, 2 off, 3 always). Steam Link (vrlink) lists the Frame's rates 72/80/90/96/108/120/144 in vrserver.txt and follows the per-app preference ("host preferred N Hz"; whether the key is honoured is unverified in-headset yet). Judder metric: vrcompositor.txt session summary dropped + "Timed out. N total" (Stormland: 0 dropped but 313 timeouts in 2 min); fpsVR (`%LOCALAPPDATA%\fpsVR\*.json`, 0.1 ms histograms) gives p99 CPU/GPU ms. `pcvr.steamvr_tuning` (default on, PC only) applies on Play: highest rate whose budget ≥ p99×1.05, at least one step down, smoothing on.
|
||||
|
||||
**Language packs (merged from PR #20, 2026-10-04):** overport's `libovrplatformloader.so` is a dispatcher that `dlopen`s Meta's own loader (`libovrplatformloader_meta.so` / `_meta_q1.so`, also `libpxrplatformloader.so`) and keeps its own message queue; `ovr_LanguagePack_GetCurrent/SetCurrent` are 8-byte `return 0` stubs in it, `ovr_AssetFile_GetList` forwards to Meta's loader. `frame.langpacks` (opt-in, `native/langpack`) serves `<tag>.lang` files from the game's data; `elf.hide_exports` marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's `is_symbol_global_and_defined` is from memory). `libfp_langpack.so` is built here (`python native/build.py --only langpack`) and committed. Owner-verified 2026-10-04: Deadpool VR with only `en.lang` and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get `lang_packs` filled after an app update (`library._refresh_data_fields`). The library logs to logcat under the tag `fp_langpack` (dirs looked in, packs found, every language-pack call), so it shows up in the game's `launch.log`; an `ovr_AssetFile_GetList` the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its `Metadata` equals the game's own version string (`ULanguagePacksSubsystem` compares it with `%s.%s.%s.%s.%s` built from the build info, e.g. `1.0.40.356975.Quest` = versionName; found by disassembling `libUE4.so`): the patch writes the APK's versionName into the library (`@FPMETA@` slot, `with_metadata`), `FRAMEPORT_LANGPACK_META` overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled `/sdcard/Android/obb/<pkg>/x.lang`; with the `/storage/emulated/0/Android/obb/<pkg>/x.lang` spelling (Unreal's own, now listed first in `scan_dirs`) German dialogue plays. `FRAMEPORT_LANGPACK_SKIP=<tags>` or a file `fp_langpack_skip` in the obb folder leaves packs out of the list (experiments).
|
||||
|
||||
**Cube swapchains (GitHub #107, 2026-10-09):** the Frame's runtime refuses `faceCount=6` swapchains (-2,
|
||||
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
|
||||
ovrp_EndFrame4 (memset). FrameBridge `cube_standin` (default on, `native/adapter/cube_standin.c`) serves a refused cube
|
||||
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
|
||||
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
|
||||
**Hardware video decoding (PR #96, PR #128 by Lucas-Mathieu, adapted 2026-10-10, agent v71):** `native/hevc` = one
|
||||
OMX plugin `OMX.frameport.{avc,hevc,vp9}.decoder` (FFmpeg v4l2m2m on Iris /dev/video-dec0, FFmpeg software decoders as
|
||||
fallback inside the component, Vulkan copy for big native surfaces). The connection installs it once per Frame
|
||||
(`ensure_video_codec` → agent `video_codec_status`/`install_video_codec`: `~/.local/share/frameport/video-codec/versions/
|
||||
<manifest sha>` + `current` symlink). A Frame keeps the same or a newer `revision` (no flip-flop between PCs) → **bump
|
||||
`revision` in native/hevc/build.py with every artifact change**; a failed install isn't retried during the connection.
|
||||
Per game: only launchers of games whose recipe has `frame.hw_video_decode` (install stage, no APK change; suggested
|
||||
from analysis `media_codec`, ANALYSIS_VERSION 8; deployment.json `hw_video_decode`) get the codec line → the Podman
|
||||
wrapper (native/hevc/podman.py) mounts the plugin into that container (merged media_codecs.xml in
|
||||
$XDG_RUNTIME_DIR/frameport-video). Off for every game: Settings → Installing (library `video.hw_decode` → agent
|
||||
`video_codec_switch` = video-codec/disabled); one game: `FRAMEPORT_NO_HW_VIDEO=1 %command%`. `upgrade_launchers`
|
||||
converts agent ≤70 launchers (Batman's `<base>/frameport-codec` → keeps the line, folder then removed). Batman: catalog
|
||||
+ migration `batman_video_patches` (`frame.hw_video_decode` + hidden adapter setting `surface_native`). Iris: 8K
|
||||
refused (ENOMEM) while any other decoder session is open; SteamVR's vrlinkrunthread holds one ~12 s at game start →
|
||||
refused opens retry until 20 s after the plugin loaded (else 2 s), then software; VP9 7680x3840 never returned a
|
||||
picture → VP9 ≤4096x2304; hidden VP9 frames come back as empty capture buffers (bytesused 0, no LAST):
|
||||
FFmpeg's wrapper ended the EOS drain at the first one (two-pass VP9 lost its last ~25 frames) → build.py requeues
|
||||
them (codec revision 8; drain ends at LAST, or 1 s after a skipped empty buffer; dev Frame 2026-10-10: 600/600 =
|
||||
OMX.google.vp9 Y hashes). Rebuilds are deterministic (ext4 + NTFS path
|
||||
with spaces, two NDK copies → same; rev 8 141de01f…; PR #128's own sources → its 27a2d749…). Not yet run on the device.
|
||||
**Vulkan shader dump (GitHub #140, 2026-10-10):** adapter `vk_shader_dump=1` (Vulkan shim, `native/vkshim/shader_dump.h`;
|
||||
applies where vk_sanitize can: Unreal/Other arm64) writes each distinct SPIR-V module once to
|
||||
`files/fp_vk_shaders/<size>_<sha256>.spv` (tmp + rename) and one `index.txt` line per vkCreateShaderModule (`<seq> <ms>
|
||||
<unix ms> <name> new|known|again|failed`, O_APPEND) to find the module behind a GPU hang. Agent v72 `collect_diag`
|
||||
returns `shaders` (newest modules of fp_vk_shaders + fp_spirv, ≤4 MB each, base64) → bundle
|
||||
`games/<pkg>/target/shaders/`. Triage `gpu-hang` suggests both dumps; `triage.graphics_api` (FrameBridge's
|
||||
xrCreateSwapchain formats: <0x1000 Vulkan, else GL) keeps only the session's API's (`API_ONLY`). Host-tested only.
|
||||
Agent v74: a reporter's hang came ~10 s after the last new module, so the culprit wasn't among the newest written →
|
||||
with an index naming modules, `shader_dumps` sends every module of the index's **newest session** (after the last
|
||||
`# start`), deduplicated, last used first, each ≤4 MB, ≤30 MB in all (`session` = how many it names) + that session's
|
||||
index lines; no index (fp_spirv) = the old newest-by-mtime 4 MB. One JSON line (~40 MB base64) over the SSH channel
|
||||
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
|
||||
once (`Dex.return_early`: return-void / `const/4 v0,0; return v0`; nopping the invoke would leave a move-result the
|
||||
verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified
|
||||
headless: Vivox initialises, 150 s at ~65-70 fps.
|
||||
**Steam Input gamepad for 2D apps (GitHub #162, agent v72, 2026-10-10):** Lepton's Android only has the Wayland seat's `wayland_touch/keyboard/pointer`. Steam makes its virtual pad (uinput, `/devices/virtual/input`, 28de:11ff, "Microsoft X-Box 360 pad N", ACL for steamos) only while the Frame's controllers are on (controller.txt "Steam Controller reserving XInput slot 0"; gone when they sleep), also with no game running; steamos-manager's 28de:0000 keys device isn't a pad (no BTN_SOUTH). Lepton's container root (system_server too) = steamos (`keep-id:uid=0`), so a bind-mounted `/dev/input/eventN` opens. Opt-in `device.steam_gamepad` (vr_kind none; suggested for `android.hardware.gamepad`/`LEANBACK_LAUNCHER`, analysis `gamepad`, ANALYSIS_VERSION 9): deployment.json `steam_gamepad` → launcher `GAMEPAD_LINE` (after the codec line: exports FRAMEPORT_GAMEPAD + LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD, puts `agent/bin` first on PATH) → `PODMAN_WRAPPER` (written by ensure_host_fixes; non-`run` calls go straight on; `run` imports the agent's `podman_run_args`; hands on to the next Podman after its own PATH entry with itself removed from PATH, so it chains to the codec wrapper either way round) adds the pads + `Vendor_28de_Product_<pid>.kl` (Xbox 360 layout). Verified headless with a uinput stand-in pad (Stremio's container: EventHub `classes=0x80000141`, our key layout, KeyEvent BUTTON_A dispatched); with Steam's real pad and a game: not yet. Pads made after the start aren't seen until the next one.
|
||||
**Multiview programs on flat framebuffers (GitHub #77, Doom3Quest, 2026-10-09, prototype):** Mesa enforces OVR_multiview's
|
||||
"program num_views == draw framebuffer views" rule (`draw_validate.c`, the draw is dropped silently); Qualcomm doesn't.
|
||||
Doom3Quest compiles every VS with `layout(num_views=2) in;` and draws its HUD/PDA into 2D-texture FBOs → black. Opt-in
|
||||
`frame.gl_multiview_fbo` (analysis `gl_multiview_libs`, ANALYSIS_VERSION 6; own-engine GLES only): `native/glmv` =
|
||||
`libfpglmv.so` (12 chars = "libGLESv3.so": libdoom3.so's one `.rodata` dlopen string is rewritten in place, its qgl*
|
||||
table comes from dlsym on that handle; also first DT_NEEDED for its direct gl*/egl* imports). Such draws use a lazily
|
||||
built single-view twin (view 0, uniforms copied per draw); eglMakeCurrent resets the per-thread cache. Host-tested only
|
||||
(rewriter on all 19 Doom3Quest shaders + glmv.c against a stand-in GL, `tests/test_gl_multiview_fbo.py`); the host's
|
||||
Mesa 23.2 llvmpipe has no OVR_multiview. Headset-verified by Xandrix1987 (2026-10-09: HUD/PDA shown, no fps drop) -> in the Doom3Quest catalog recipe.
|
||||
**BlazeRush (GitHub #57, Klownicle's verified guide, 2026-10-10; FramePort's version headless-tested only):**
|
||||
`frame.blazerush` (1.0.349 only: exec-segment SHA + every word checked, offsets from PT_LOAD) NOPs the avatar-readiness
|
||||
wait (0x4b2dbc), RETs the avatar renderer (0x4b2adc), renames `cache/shader_cache` → `cache/br_shader_v1`. GL shim
|
||||
(now also exports glShaderSource/glLinkProgram/glProgramBinary/glDeleteProgram/glDraw*: pass-through unless a setting
|
||||
is on) `gl_int_attribs` (integer inputs fed via glVertexAttribPointer re-set with glVertexAttribIPointer per draw,
|
||||
reflection cached per EGL context + program, dropped on link/binary/delete) and `gl_highp_markers` (vertex shaders with
|
||||
every '|' marker: mediump→highp except on in/out/uniform lines); host test `tests/test_blazerush.py` with a stand-in
|
||||
GL. FOV: the game reads VrApi props 7/8 right after vrapi_Initialize (no session; the Frame has no
|
||||
XR_EPIC_view_configuration_fov) → the bridge saves the live FOV (also located before tracking, headless: 118.87×120.04
|
||||
on the dev Frame) in `files/framebridge-vrapi-fov.txt` and answers with it at the next start (log `FOV properties: …
|
||||
(saved by an earlier session)`). `device.config_sync` (catalog field `config_sync`, agent v78): user_config.xml
|
||||
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. 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
|
||||
(`<base>/session.log`, ≤4 MB: head + tail + FrameBridge/crash lines between) + that session's logcat-crash.log +
|
||||
`journalctl -k` GPU lines ("kernel: …"). The GUI's connection refresh (`app._check_sessions` → `pipeline.sessions_due`,
|
||||
background thread; sessions >7 days old are skipped) runs `pipeline.triage_session` = `validate/session.analyze`
|
||||
(triage.yaml signatures incl. `space-warp-used` (info, `question:`, never auto-applied), `gpu-hang` (`report: true`)
|
||||
+ computed `slow-frames` (pacing windows vs the nearest refresh rate) and `focus-dips` (FrameBridge always logs
|
||||
`focus: lost` / `focus: back after N ms`, session_fixes.c)) → library `last_session` / `last_session_checked` → game
|
||||
page "Last session" callout (Try this fix / Rebuild with this fix / Yes-No question / Report / Dismiss). Suggestions may
|
||||
be values (`adapter.scale=0.85`, `triage.split_suggestion`); adapter-only fixes are pushed live (`apply_suggestions_live`
|
||||
→ agent set_settings, like the Game settings dialog). CLI `frameport session <pkg> [--apply]`. PC VR (Proton) launchers
|
||||
log no session end, so they aren't triaged yet. pac_hints stays triage-only: a survey of all 68 dump APKs found unpaired
|
||||
PAC hints in 21 libraries of 18 games (OpenSSL's 38/40 in most UE4 libUE4.so and libEOSSDK.so, UE5 libUnreal.so ~400
|
||||
vs +2-3, libass, libopencv, …), most of them games that work, so default-on would rewrite many working builds.
|
||||
**Unresolved (as of 2026-09-28):** Arcsmith (right-eye distortion) and Time Stall (both eyes) — swap, tracking, Valve
|
||||
layers, depth, pacing ruled out. Sniper Elite VR (DEVICE LOST), Espire 1 (Mesa GL upload crash), HITMAN 3 (freedreno
|
||||
crash): use PC versions.
|
||||
|
||||
**Install links / FrameDrop button protocol:** `deeplink.py` parses framedrop://, frameport:// and https install links (strict URL rules, manifest ≤256 KiB, downloads into `<data>/downloads/`), `pipeline.add_from_link`, `views/link_dialog.py`, CLI `frameport open-link`; `urlhandler.py` registers a script (bundles can't take URL arguments), `<data>/links/*.link` + `gui.alive` heartbeat; Settings → Install links (`links.framedrop`/`links.frameport`); `ui/dropped.py`, `pipeline.add_windows_exe`, `device.display_mode`; demo link `deeplink.DEMO_MANIFEST`. → docs/DEVICE_NOTES.md#install-links--framedrop-button-protocol
|
||||
**Install link updates:** manifest `version` → library `link.version`; `pipeline.check_link_updates` (GUI start, 6 h, setting `links.check_updates` = Settings → Updates → Games) marks `link_update` → Library bar / game page → `link_dialog.update_from_link`; CLI `frameport link-updates`; the demo link is never checked. → docs/DEVICE_NOTES.md#install-links--framedrop-button-protocol
|
||||
**Install links / FrameDrop button protocol (2026-10-07, not yet clicked end to end from a browser):** FrameDrop
|
||||
(framedropvr.com, a closed-source Windows sideloader) defines "Install with FrameDrop" buttons:
|
||||
`https://framedropvr.com/install?manifest=<url>|url=<file>` → that page opens `framedrop://install?…` (1.6 s, else its
|
||||
home page); manifest `{"schema":"framedrop.install/v1","name","files":[{"url","sha256"}]}` (name = Steam title; .apk
|
||||
or Linux .zip). `deeplink.py` (no Flet) parses framedrop://, frameport:// and the pasted https link, enforces its
|
||||
rules (https; http only on loopback; no credentials; no LAN/loopback/link-local IPs, also after DNS and redirects; URL
|
||||
ends in a file name), caps manifests at 256 KiB, ignores non-hex sha256 (FrameDrop's own example has a placeholder),
|
||||
downloads into `<data>/downloads/<slug>-<hash>/` (OBBs → `obb/` next to the APK, `.part` removed on cancel/mismatch);
|
||||
`pipeline.add_from_link` routes APK / Linux / exe and sets `title` + `title_locked` + `link` (a bare file link keeps
|
||||
FramePort's title). GUI: `views/link_dialog.py` (always asks first), Add games → "Install from a link…", CLI
|
||||
`frameport open-link [--yes --no-install --gui]`. **A `flet build` bundle can't take a URL argument** (the Flutter
|
||||
host treats any argv as a developer page URL), so `urlhandler.py` registers a script, not FramePort.exe: Windows
|
||||
HKCU `Software\Classes\{framedrop,frameport}` → hidden PowerShell `frameport-link-handler.ps1`; WSL (source runs)
|
||||
the same keys → `wsl.exe -d <distro> -e sh frameport-link-handler.sh`; Linux `frameport-links.desktop` +
|
||||
`xdg-mime`; macOS unsupported (Apple Events, paste instead). The script drops the link into `<data>/links/*.link`
|
||||
and starts FramePort unless `<data>/gui.alive` is < 10 s old (`gui.starting` stops double starts); the GUI's
|
||||
`_watch_links` thread touches the heartbeat and opens links (newest window session). Settings → Install links: one
|
||||
switch per scheme (`links.framedrop`, `links.frameport`, default on); a scheme another program owns (FrameDrop) is
|
||||
only taken with "Use FramePort for these links" (`register([s], force=True)`); off removes only FramePort's own
|
||||
registration (`MARK` in the command). Never registered with FRAMEPORT_HOME/FRAMEPORT_NO_LINK_HANDLER (tests,
|
||||
screenshots). Same round: files dropped on the Library (`ui/dropped.py`, bundles only like the Files tab), "Add a
|
||||
Windows program (.exe)…" (`pipeline.add_windows_exe`: exe in Downloads/home/drive root copied alone into
|
||||
`<data>/windows-apps/<slug>/`), patch `device.display_mode` (Automatic / VR / Flat window → `InstallContext.display`,
|
||||
`installer.show_window`). FramePort-only manifest extension `"frameport": {"description", "icon"}` (bad values ignored; icon: same URL rules,
|
||||
≤2 MiB, Pillow-checked, ≥32 px, saved as PNG in `<data>/downloads/icons/`, shown in the question by asset URL, then
|
||||
`sources.apply_custom(pkg, "icon")` unless `.picked` exists; description fills `details.description` only when
|
||||
empty). Bare file links get a title guessed from the file name (`title_from_filename`: version/arch dropped, package
|
||||
names → last part). Windows test of 0.12.1.dev191 (2026-10-07): a button click opened the dialog; but a click right
|
||||
after closing FramePort did nothing (the closed window's heartbeat was < 10 s old) → the handler scripts now wait
|
||||
up to 4 s for the link file to be taken before trusting the heartbeat, the window's CLOSE event / atexit delete
|
||||
`gui.alive`, and the Windows command runs under `conhost.exe --headless` (plain `-WindowStyle Hidden` flashed a
|
||||
console). Screens: `scripts/ui_smoke.py --links --fake-frame --game <pkg>` (tall pages: `--viewport
|
||||
1280x7000`; Flutter's popup menu ignores Escape).
|
||||
Demo link: the homepage's example button = `deeplink.DEMO_MANIFEST` (https://frameport.app/demo/cool-game.json, published
|
||||
from site/public/demo/); every form (https button page, frameport://, framedrop://, pasted) → `InstallRequest.demo`,
|
||||
no network (fetch_manifest returns `demo_manifest()`, download refuses), `link_dialog.show_demo` (Install =
|
||||
`easter.demo_install`: Cool Game's cover out of the portal; eggs off → "Nice, it works!"), CLI just says so.
|
||||
|
||||
## Releases, CI, GitHub
|
||||
Maintainer-only notes (accounts, credentials, key locations) live in the git-ignored `CLAUDE.local.md`.
|
||||
@@ -689,13 +1110,6 @@ GitHub identity + credentials are maintainer-local (`CLAUDE.local.md`): check `g
|
||||
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).
|
||||
- **Every triage also sweeps for unanswered comments**, not just new threads: open AND recently closed issues, PRs
|
||||
(incl. merged/closed ones) and discussions (top-level comments and their replies) whose latest comment isn't the
|
||||
maintainer's, plus earlier user comments a later maintainer reply skipped. Users often ask follow-ups after a fix or
|
||||
after closing (#294: two questions after the reporter closed it; #249: a Q&A discussion with no reply for days;
|
||||
#161/#110: requests posted after the close). Quick check: `gh issue list --state all --search "updated:>=<date>"
|
||||
--json number,comments --jq '.[] | select(.comments[-1].author.login != "spoopyghosty0")'`; discussions via GraphQL
|
||||
(`discussions(orderBy:UPDATED_AT)` with `comments{replies}`).
|
||||
- **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.
|
||||
@@ -741,15 +1155,9 @@ MR-only (PASSTHROUGH required + BOUNDARYLESS_APP) → force_passthrough, + USE_S
|
||||
hand tracking required → controller_fix=0; OVRPlugin + ≥20 GiB → disable_space_warp; Unreal ≤4.21 or Unreal Meta XR
|
||||
Audio → nodebug; Oculus-OS class referenced by the Unreal audio build or ≥2 Meta libs → oculusos; legacy-VrApi Unity
|
||||
GLES with MSAA → unity_no_msaa; CryEngine → user.cfg r_variable_rate_shading=0; direct VrApi → bridge (+GL shim for
|
||||
GLAD/GLES); Unreal → alternate no-ForceQuit build. Scoring (docs/HEURISTICS_EVAL.md, 2026-10-15): analyse the dumps
|
||||
once (`scripts/heuristics_snapshot.py <out.json> "<VR CyberDeck Portable>" "<PATCHED>"`, ~30 min, keep out of git),
|
||||
then `scripts/eval_heuristics.py --snapshot <out.json> --split dev|holdout|all [--fit-report]` (catalog-only truth,
|
||||
per-patch precision/recall, a fixed 20% holdout to catch rules fitted to one game). Read the **dated** score: an
|
||||
"extra" on an entry verified before the patch existed is a catalog gap, and adapter/device picks are inherited by
|
||||
catalog recipes anyway. 2026-10-15: strict 5/65 exact, F1 73.8%; dated 39/65, F1 88.6%; remaining misses are
|
||||
one-game runtime fixes. Don't key a rule on one game's code fingerprint (an Unreal splash/depth "detect" matching
|
||||
only Star Wars Tales + FNAF HW was rejected: it would also have bumped ANALYSIS_VERSION for every user). Add a rule →
|
||||
re-run dev + holdout + `pytest -m games`.
|
||||
GLAD/GLES); Unreal → alternate no-ForceQuit build. Score changes with
|
||||
`python scripts/eval_heuristics.py "<dumps>"` (catalog off vs verified recipes; currently 33/34 exact — Phantom's
|
||||
"use the no-ForceQuit build" is only detectable at runtime via triage). Add a rule → re-run the eval + `pytest -m games`.
|
||||
|
||||
## Frame operations
|
||||
- Find/connect: `frameport frame discover` / `frame info` (the remembered Frame is in `<user data>/frames.json`).
|
||||
@@ -783,15 +1191,3 @@ the unresolved eye distortion, and testing the release bundles on real Windows/m
|
||||
(regenerates docs/PATCHES.md + site/src/data/patches.json, the site's id → anchor map; a test runs `--check`).
|
||||
- Known-good backups of every working APK and the signing keys: `PATCHED/_known-good-2026-09-28/` (don't delete).
|
||||
- Your development Frame: `frameport frame info` (remembered in `<user data>/frames.json`); SSH as `steamos@<frame>`.
|
||||
|
||||
|
||||
Steam library scheduling (#305, agent v99): `install.steam_library` = now/later/off; agent `library_mode` persists it.
|
||||
Automatic restarts also hold on a fresh local `frameport-app/gui.alive` (10 s; existing 12 h cap).
|
||||
Later journals changes and installs steam.service ExecStartPre/ExecStartPost hooks to reapply after Steam's exit
|
||||
writes and verify before clearing; preserve this ordering and avoid locking across systemctl's startup hooks.
|
||||
Off Play (also pending Later Play) uses detached systemd-run launch.sh, with no shortcut/devkit registration;
|
||||
Steam's per-game controller/overlay/Exit game integration is lost. Restart Steam now explicitly bypasses holds,
|
||||
asks before ending a running game, and closes local FramePort/Desktop Mode. Purge removes the startup hook.
|
||||
Hook hardening: both lines `-` + `/usr/bin/timeout -k 5 20`; ExecStartPost only `systemd-run --no-block`s the
|
||||
verify (unit frameport-library-verify) so Steam's start never waits; drop-in removed once mode != later and nothing
|
||||
pending (`drop_library_hooks_if_idle`). Steam = static user unit steam.service (Type=simple, Restart=always, 90 s).
|
||||
@@ -36,9 +36,6 @@ Frame, patches each game so it runs there, uploads it and adds it to your Steam
|
||||
in FramePort.
|
||||
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
|
||||
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
|
||||
- **Your room for mixed reality (experimental):** scan the room in the headset, see it in 3D, clean up the scan and
|
||||
fix furniture, screen and door labels, so mixed-reality games get your room. Try it with
|
||||
[Test Quest MR](https://github.com/spoopyghosty0/TestQuestMR).
|
||||
- **Updates itself**, and reports problems without personal data.
|
||||
|
||||
Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
@@ -47,8 +44,6 @@ Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
|
||||
@@ -129,7 +124,6 @@ What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
|
||||
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
|
||||
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
|
||||
| [FAQ](docs/FAQ.md) | Game folders and common questions |
|
||||
| [Controller buttons](docs/CONTROLLERS.md) | Quest Touch buttons on the Frame: SteamVR default, Mirror and Quest pair |
|
||||
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
|
||||
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""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
|
||||
|
||||
|
||||
# Which VR runtimes a build can use, from its files (after RiftLift's detection.py, MIT). Over-reporting LibOVR or
|
||||
# the Platform SDK is safe (the game keeps Meta's runtime + Revive); only a build with neither leaves that route.
|
||||
LIBOVR_FILES = {"libovrrt64_1.dll", "libovrrt32_1.dll", "ovrplugin.dll", "oculusxrplugin.dll"}
|
||||
PLATFORM_FILES = {"libovrplatform64_1.dll", "libovrplatform32_1.dll", "pnsovr.dll"}
|
||||
LIBOVR_MARKERS = (b"libovrrt", "libovrrt".encode("utf-16-le")) # LibOVR shim's runtime name (static or
|
||||
PLATFORM_MARKERS = (b"libovrplatform", "libovrplatform".encode("utf-16-le")) # format string), Platform SDK imports
|
||||
# arguments that already pick a VR mode/runtime: Unreal (-vr, -hmd=, ...) and Unity (-vrmode <name>)
|
||||
VR_SELECTORS = ("-vr", "-nohmd", "-emulatestereo", "-openvr", "-steamvr", "-openxr", "-oculus", "-vrmode")
|
||||
|
||||
|
||||
def _contains(path, needles):
|
||||
"""Case-insensitive search of a file in 16 MiB chunks (the exe can be ~100 MB)."""
|
||||
keep = max(map(len, needles)) - 1
|
||||
tail = b""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
while chunk := f.read(16 << 20):
|
||||
block = (tail + chunk).lower()
|
||||
if any(n in block for n in needles):
|
||||
return True
|
||||
tail = block[-keep:]
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def oculus_facts(game_dir, exe):
|
||||
"""{libovr, platform_sdk, openvr, unity}: file names in the game's folder tree + markers in the exe."""
|
||||
names = set()
|
||||
for _root, dirs, files in os.walk(game_dir or os.path.dirname(exe)):
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
names.update(n.lower() for n in files)
|
||||
folder = os.path.dirname(exe)
|
||||
unity = os.path.isfile(os.path.join(folder, "UnityPlayer.dll")) or \
|
||||
os.path.isdir(os.path.splitext(exe)[0] + "_Data")
|
||||
return {
|
||||
# modular Unreal ships the plugins as DLLs (*-OculusHMD-*, *-OnlineSubsystemOculus-*)
|
||||
"libovr": bool(names & LIBOVR_FILES) or any("oculushmd" in n for n in names) or
|
||||
_contains(exe, LIBOVR_MARKERS),
|
||||
"platform_sdk": bool(names & PLATFORM_FILES) or any("onlinesubsystemoculus" in n for n in names) or
|
||||
_contains(exe, PLATFORM_MARKERS),
|
||||
"openvr": "openvr_api.dll" in names,
|
||||
"unity": unity,
|
||||
}
|
||||
|
||||
|
||||
def vr_route(facts):
|
||||
""""revive" (Meta's runtime + Revive, the proven route) or "openvr" (straight to SteamVR, nothing of Meta's):
|
||||
only builds without the Platform SDK that carry OpenVR and either no LibOVR or Unity's per-launch VR choice."""
|
||||
if facts["platform_sdk"] or not facts["openvr"]:
|
||||
return "revive"
|
||||
return "openvr" if not facts["libovr"] or facts["unity"] else "revive"
|
||||
|
||||
|
||||
def vr_args(engine, route, args):
|
||||
"""Arguments that make the engine use the route's runtime (after RiftLift's oculus_launch_arguments), unless
|
||||
the arguments already choose one: Unity dual builds take -vrmode OpenVR, Unreal on Revive gets -vr."""
|
||||
low = [a.lower() for a in args]
|
||||
if any(a in VR_SELECTORS or a.startswith("-hmd=") for a in low):
|
||||
return []
|
||||
if engine == "unity" and route == "openvr":
|
||||
return ["-vrmode", "OpenVR"]
|
||||
if engine == "unreal" and route == "revive":
|
||||
return ["-vr"]
|
||||
return []
|
||||
|
||||
|
||||
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}")
|
||||
if "args" in profile:
|
||||
info.pop("args_argv", None) # profile args are shell fragments like the legacy entries
|
||||
info.update({k: v for k, v in profile.items() if k != "match"})
|
||||
info.update(profile=key, status=status)
|
||||
|
||||
def install_info(app, info, result):
|
||||
"""Route and VR arguments from the build's files. A profile's explicit revive/meta_runtime/args win, and a
|
||||
verified profile keeps exactly the launch it was verified with."""
|
||||
facts = oculus_facts(result.get("game_dir"), result["exe"])
|
||||
engine = "unity" if facts["unity"] else result["checks"].get("engine")
|
||||
explicit = "revive" in info or "meta_runtime" in info
|
||||
route = ("revive" if info.get("revive", True) else "openvr") if explicit or result["verified"] \
|
||||
else vr_route(facts)
|
||||
info.setdefault("revive", route == "revive")
|
||||
# without Revive, Meta's runtime only matters to the Platform SDK
|
||||
info.setdefault("meta_runtime", info["revive"] or facts["platform_sdk"])
|
||||
added = []
|
||||
if not result["verified"] and "args" not in (core.GAMES.get(info.get("profile")) or {}) and \
|
||||
info.get("vr_args") != "off":
|
||||
added = vr_args(engine, route, info.get("args", []))
|
||||
info["args"] = list(info.get("args", [])) + added
|
||||
return {"vr": dict(facts, route=route, meta_runtime=info["meta_runtime"], added_args=added)}
|
||||
|
||||
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.INSTALL_INFO_HOOKS.append(install_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. Without revive/meta_runtime the install picks the route from the build's files (fexrift_compat.vr_route). meta_runtime: false skips Meta's runtime (default: revive or the Platform SDK). vr_args: off keeps unverified games' arguments as they are (else Unreal gets -vr, Unity OpenVR routes -vrmode OpenVR).",
|
||||
"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,367 @@
|
||||
"""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)
|
||||
# the same profile rules as a game in Meta's library: a legacy entry (no "match") by name, else the
|
||||
# compatibility hooks match the build; "verified" only from a verified profile
|
||||
known = core.GAMES.get(app) or {}
|
||||
legacy = app in core.GAMES and "match" not in known
|
||||
info = dict(known) if legacy else {}
|
||||
info.update(manifest_launch(app))
|
||||
for hook in core.GAME_INFO_HOOKS:
|
||||
hook(app, info)
|
||||
return info, info.get("status", "verified" if legacy else "untested") == "verified"
|
||||
|
||||
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]
|
||||
@@ -1,40 +0,0 @@
|
||||
# Announcements
|
||||
|
||||
`current.md` is the one announcement FramePort shows as a pop-up in the app. Edit it on `main`: the app reads it
|
||||
from GitHub `main` (raw.githubusercontent.com) at most every 6 hours, so a push reaches everyone without a release.
|
||||
|
||||
## Who sees it
|
||||
|
||||
- **Only people who already used FramePort before the announcement's `date`.** A new install never shows one, and
|
||||
nor does an announcement published before FramePort was installed.
|
||||
- **Once.** Closing it in any way (the X, a click outside it, Escape, or its button) dismisses it for good. The app
|
||||
remembers dismissals in its settings (`announce.dismissed`).
|
||||
- **A new announcement = a new `id`.** Fixing a typo with the same `id` doesn't show it again to anyone who closed it.
|
||||
A new `id` shows to everyone who used FramePort before its `date`, whether they closed the earlier one or not.
|
||||
|
||||
## The file
|
||||
|
||||
```markdown
|
||||
---
|
||||
active: true # false = nothing is shown (the fields are still checked)
|
||||
id: meetup-2026-11 # letters, digits, . _ - — change it for every new announcement
|
||||
title: Steam Frame meetup on Discord # required
|
||||
date: 2026-11-02T18:00:00Z # required: when it was published (UTC; a bare date = midnight UTC)
|
||||
image: announcements/meetup.png # optional: a file in this folder, or an https URL (PNG, JPEG, WebP, GIF; ≤ 5 MiB)
|
||||
image_alt: The meetup poster # optional: what the picture shows (screen readers)
|
||||
button: Join the meetup # optional: an action button, with url
|
||||
url: https://example.com/meetup
|
||||
expires: 2026-11-30 # optional: not shown after this day (UTC, inclusive)
|
||||
---
|
||||
The body, in Markdown: **bold**, *italic*, `code`, [links](https://frameport.app/), lists, headings.
|
||||
```
|
||||
|
||||
The body is Markdown (no HTML). A `date` in the future works as a schedule: nobody sees it before then.
|
||||
|
||||
Images look best at about 2:1 (e.g. 1200×600). The pop-up is 600 px wide and shows between 140 and 320 px of the
|
||||
picture's height.
|
||||
|
||||
`pytest tests/test_announcements.py` checks this file, so a broken announcement fails CI instead of silently showing
|
||||
nothing.
|
||||
|
||||
To turn announcements off for testing: `FRAMEPORT_NO_ANNOUNCEMENTS=1`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
# The announcement FramePort shows once (announcements/README.md). Switched off: set active: true.
|
||||
active: false
|
||||
id: example-2026-10
|
||||
title: Example announcement
|
||||
date: 2026-10-15T12:00:00Z
|
||||
# image: announcements/example.png
|
||||
# image_alt: What the picture shows
|
||||
# button: Read more
|
||||
# url: https://frameport.app/changelog/
|
||||
# expires: 2026-11-30
|
||||
---
|
||||
This is where the text goes, in **Markdown**: *emphasis*, `code`, [links](https://frameport.app/) and lists.
|
||||
|
||||
- Keep it short: one or two paragraphs.
|
||||
- Change `id` for every new announcement.
|
||||
@@ -1,6 +1,6 @@
|
||||
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
|
||||
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
|
||||
222727ddabf7eaee08c92e86c64f02edc6639667e9041ab2629032b885c9ae3a ./arm64-v8a/liba32bridge.so
|
||||
fbc3b0238462d976d234fee6740db92b873c85d6bb22a6b01428e64e9058ed83 ./arm64-v8a/liba32bridge.so
|
||||
c9c3da5cac224c38e2210b781bd12b7f97e02bd6b0ebb0ef85543e4ea4b76671 ./arm64-v8a/liba32rt.so
|
||||
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
|
||||
d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/libfp_langpack.so
|
||||
@@ -8,58 +8,25 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
|
||||
e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/libfp_vk.so
|
||||
3c548b567c6984c91bf3f4a2b0bae1336f569f3e9fa9c3204872fd9876f88284 ./arm64-v8a/libfp_xrruntime.so
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
|
||||
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
7c33d7fb9fd176ca222efb2d87cf525e2f0604457c16896027a5881594495f76 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
0f058921e9c1f5b9ef5847cc9e6b80a23e75028c76c1d3bb02d1610b535f81c9 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
19ae0a45e80d8031300eea0d54810c14e4711fa8cd383c29ca65d85a74f577c4 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
9e8a7ce599b62a7c607b0d3637e4ec1b1ea1974c3373a7af3f5920ad2adc5e00 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
82b11650e8eaab014aaab7e67ec781499df670c2519484f4ec9f2316db17c03c ./hevc/libstagefrighthw.so
|
||||
96c7c947963dce72c3c2633c005f2801de54d16896d14d7afd91304ed1399d22 ./hevc/manifest.json
|
||||
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
|
||||
2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec ./linux-arm64-bin/fp_roomscan
|
||||
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
|
||||
00c2dead0fd7497929833467f0e55699d57d26be1b85dce35402993852f7b111 ./roomscan/ceiling.png
|
||||
c1962de31ed2810d31c760cf9ee63316ac2b8c9528ccbcd9299ebf5ba39b2995 ./roomscan/done.png
|
||||
0dfaea4c68979b0a45a9fef06fdf1a92468ffc8f46f80a3a014fce1d805972d5 ./roomscan/failed.png
|
||||
f2bae84f1d1512f87b25e688acdddb29b3f360f3f0d0cd4fdeeb741a79e7b19a ./roomscan/finish_00.png
|
||||
9be7720c45f162e99813cb9a7a03e026fed53b41f06637b30ded309eeac91759 ./roomscan/finish_01.png
|
||||
220d699f879b3a8669cfaf4dc714496e8c1db7e7878e7cd12b1a41aec9b62faa ./roomscan/finish_02.png
|
||||
42871b440b77449d4fe41be1c809a930148a874caa8bc92b13b5d25bb6effa83 ./roomscan/finish_03.png
|
||||
a470f62624c94ed6791cc7ee03066e1ffb0a676d520658a0a8b02a6af1605b3b ./roomscan/finish_04.png
|
||||
195ff1c649fd9b42bfa6eea29c43a8b67116fd6ffe9b0fe53dcbdb1074bd34fa ./roomscan/finish_05.png
|
||||
a45accab86b02d7b78327a17b84a6fea543ecc549282055dd3182c686fc2b1b8 ./roomscan/finish_06.png
|
||||
b88d6c590ea839a9d11b3a4de8c3effb39752f7114318966fddae09995e0fd37 ./roomscan/finish_07.png
|
||||
4a3615db26cf765b855a997be3e067aed58464894dab4a6700fdec5c2b854501 ./roomscan/finish_08.png
|
||||
f532a83044e707c66dbc74787ce7b844890bc05829d3e9c8f7e33dae9db246a5 ./roomscan/finish_09.png
|
||||
ffce61a9f13bbb250fad3582396a5bec82a1bad1d585b7ef9b8f65d343806b26 ./roomscan/finish_10.png
|
||||
4a63772adacb31004749ab04b592076bd36cda61eef52f59037d5abdc9e527db ./roomscan/finish_11.png
|
||||
f373e7021cfce0d1078227f473425a10035ee426102b78f13e0fea4833b736d5 ./roomscan/finish_12.png
|
||||
1147391822be3f1aa5351374b8fd9ec85f8b5ce40e3cb9f3146d778e6fbc652a ./roomscan/floor.png
|
||||
eb3c8b55a9dbafa030797ca9f06a22c3ee3bbef96cb7adea26d433ebad43d43f ./roomscan/look_00.png
|
||||
6ba965ff6d22a469c558f2a68229bd2ff48fc28badb95889ca245310d1dd3e96 ./roomscan/look_01.png
|
||||
0a11cec25ebc76fe11d505d1f2ec162c9b4a6754393065800e73b1f81e2d94c8 ./roomscan/look_02.png
|
||||
fa5a4c94d20af56797b9fcc111148767847a6633ade9217cab373de232efad90 ./roomscan/look_03.png
|
||||
e80fa493e380fba39084be2ee62859adcff4e39aa66566abee10a4d6339c52e9 ./roomscan/look_04.png
|
||||
133617f0d8d1e69c37408aac80033f71856b592b62e3b780cc12b446bd974680 ./roomscan/look_05.png
|
||||
d5dd296ff23641c6973c57ac139147e99c7b783316ab60537adb2155dd196880 ./roomscan/look_06.png
|
||||
c08570aa78f966ae937b2e21d0bad887c5352fbca56aa579a2a7ef8541fb96ad ./roomscan/look_07.png
|
||||
3767e7db5fb6babc872fa62c42604f749c65936717d332101745b201cd450cd5 ./roomscan/look_08.png
|
||||
2924dc65e5c497545340209edc16060e12e1834d11af6871d1c1baa50b7b2562 ./roomscan/look_09.png
|
||||
cd0150dfc10b2aea39403c74eff72fd05fb0111178ef39b4c98657a6d16d105e ./roomscan/look_10.png
|
||||
011355e412f73ade3896e9037490e607c4365d0cf8ceb807d236bf68b414d3ed ./roomscan/look_11.png
|
||||
1b8b47ffb47568d540921f065b5958eeb44a6489498d58e0f33f5d1f535154e2 ./roomscan/look_12.png
|
||||
926c7bc6a103adee5b9bf938f5d4e39095ab77fd6270b5301be3b0428edd9ae6 ./roomscan/start.png
|
||||
c2c70f4af1a5c3e6089dff1130161dd417f8044f99ec639eeecf1083930aab16 ./win-x64/fp_oculushmd.exe
|
||||
60ebc5fea05b8945082e2ca8b9f2eb74616012a26eb50133af273dff0fcddc42 ./win-x64/fp_vrsettings.exe
|
||||
@@ -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
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"revision": 10,
|
||||
"revision": 8,
|
||||
"runtime_sha256": "456e912c75cd389abcf6a63bc80e2a53bdc334371d00b200c93680388ae955e2",
|
||||
"files": {
|
||||
"libstagefrighthw.so": "82b11650e8eaab014aaab7e67ec781499df670c2519484f4ec9f2316db17c03c",
|
||||
"libstagefrighthw.so": "141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c",
|
||||
"podman.py": "529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca",
|
||||
"media_codecs_frameport.xml": "46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405",
|
||||
"COPYING.FFmpeg": "b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe"
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -34,11 +34,7 @@ say "Letting FramePort log in"
|
||||
key=$(curl -fsS "$PC_URL/key?code=$PAIR_CODE")
|
||||
[[ "$key" == ssh-ed25519\ * ]] || { echo "Couldn't reach FramePort at $PC_URL. Is it still open?"; exit 1; }
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
|
||||
# a file without a final newline would glue our key onto its last line, where sshd never matches it (GitHub #242)
|
||||
grep -qxF "$key" ~/.ssh/authorized_keys || {
|
||||
[ -n "$(tail -c1 ~/.ssh/authorized_keys)" ] && echo >> ~/.ssh/authorized_keys
|
||||
echo "$key" >> ~/.ssh/authorized_keys
|
||||
}
|
||||
grep -qxF "$key" ~/.ssh/authorized_keys || echo "$key" >> ~/.ssh/authorized_keys
|
||||
|
||||
say "Setting up Lepton's containers"
|
||||
# rootless podman leaks one kernel keyring per container start; ~200 game launches would exhaust the quota
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package: ScaredBySquares.Game
|
||||
title: Scared By Squares
|
||||
status: works
|
||||
notes: 'Showed black and dropped back to the home environment while the game kept running (sound only, GitHub 323).
|
||||
Fixed in FramePort 1.2.1 (confirmed in the headset by the reporter). Rebuild and reinstall the game after updating
|
||||
FramePort.'
|
||||
details: 'The game (Unreal 4.27, OpenXR) creates two controller actions named "turn" (an action mapping and an axis
|
||||
mapping called Turn). The Frame''s runtime accepts the duplicate, then fails to load the input manifest it builds
|
||||
from the actions ("SetXrInputConfig failed. Failed to load manifest") and treats the game as closed. FrameBridge''s
|
||||
action_dedup (on by default) creates the second one as "turn_2".'
|
||||
tested_version: '2'
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
- frame.unreal_skip_obb_check
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 323
|
||||
updated: '2026-10-17'
|
||||
min_app: 1.2.1
|
||||
source_hint: Scared By Squares
|
||||
@@ -8,21 +8,19 @@ notes: 'Native OpenXR GLES video player. 360 theatres/videos (equirect2 layers,
|
||||
details: 'Settings: equirect_emul (GLES worker draws 360 layers into a projection layer), stable_local, focus_hold,
|
||||
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains. Its hardware decoding mode (Java MediaCodec,
|
||||
Vr4pMediaPlayer; its own FFmpeg is the software path) gets the Frame''s video hardware with frame.hw_video_decode
|
||||
(added 2026-10-09, not yet checked in the headset). swapchain_pool shares extra GLES panel images to avoid the
|
||||
runtime swapchain count cap (#387); host-tested only, menu interaction needs a headset check.'
|
||||
(added 2026-10-09, not yet checked in the headset).'
|
||||
tested_version: 2.0.22
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
swapchain_pool: 1
|
||||
equirect_emul: 1
|
||||
stable_local: 1
|
||||
focus_hold: 1
|
||||
scale: 0.75
|
||||
verified:
|
||||
date: '2026-10-01'
|
||||
updated: '2026-10-18'
|
||||
updated: '2026-10-10'
|
||||
min_app: 0.12.1
|
||||
source_hint: 4XVR Video Player
|
||||
@@ -1,16 +0,0 @@
|
||||
package: com.And7Limited.Hitstream
|
||||
title: Hitstream
|
||||
status: works
|
||||
tested_version: 2.06bQuest
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
adapter:
|
||||
controller_models: 1
|
||||
haptic_fix: 1
|
||||
notes: Fitness rhythm game; uses controller models and haptics fix.
|
||||
verified:
|
||||
date: '2026-10-12'
|
||||
known_good_sha256: ecc9e2eee79073ae97ad5d3f70c21e8f049f899f21b51dfd541430bb7dd04cac
|
||||
source_hint: Hitstream
|
||||
@@ -1,23 +0,0 @@
|
||||
package: com.Appnori.SummerSports
|
||||
title: Summer Sports
|
||||
status: works
|
||||
tested_version: 0.5.1
|
||||
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-17'
|
||||
known_good_sha256: 582adf5a538eb9565921cc0384e61bf113ab9215c1ad462fade82d06ad3302fa
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 395
|
||||
source_hint: All in One summer Sports VR
|
||||
@@ -7,14 +7,8 @@ xr: OpenXR
|
||||
status: works
|
||||
notes: Uses Meta XR Audio via FMOD (not covered by patch_meta_xr_audio); if audio is missing, that is the likely
|
||||
cause.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 63fff470777f2753d55ad50f8355573ced8fa48bcaee0f5e6a0c10369f951a38
|
||||
updated: '2026-10-15'
|
||||
@@ -6,15 +6,9 @@ engine: Unity
|
||||
xr: VrApi
|
||||
status: issues
|
||||
notes: Right eye distorts during movement (unresolved; swap, tracking, Valve layers, depth and pacing ruled out).
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: Legacy VrApi via OVRPlugin; overport swaps in an OpenXR OVRPlugin. Do not remove VrApi.
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 192ff250f5fbec38b9c4e898f0b8408edc25901f46bb4b227f7ca60bbd9f0bba
|
||||
updated: '2026-10-15'
|
||||
@@ -6,15 +6,9 @@ engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Needed the GLES/sRGB swapchain fallback.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: Also available as a native PC SteamVR title.
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 158f7c4f5e8558029dc8cc5e8dcb4e67e846b261bf34e3e448fee7f657c74597
|
||||
updated: '2026-10-15'
|
||||
@@ -1,23 +0,0 @@
|
||||
package: com.BolverkGamesFlat2VR.SurvivingMarsPioneer
|
||||
title: 'Surviving Mars: Pioneer'
|
||||
status: works
|
||||
tested_version: 1.0.14
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 3a9ff281417c219736ee10969e6ce4f7d8395472cb60718db8839d65edac4777
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 399
|
||||
source_hint: Surviving Mars Pioneer
|
||||
@@ -1,24 +0,0 @@
|
||||
package: com.CarbonStudio.TheWizards
|
||||
title: The Wizards
|
||||
status: works
|
||||
tested_version: '1.11'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
use_alt: true
|
||||
frame:
|
||||
- frame.unreal_thumb_touch
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: c1d979d746c2be941e150e948aec755d85034de829abfe30e5bf0761a49135ef
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 397
|
||||
source_hint: the wizards
|
||||
@@ -1,25 +0,0 @@
|
||||
package: com.CortopiaStudios.Spectre
|
||||
title: Spectre
|
||||
status: works
|
||||
notes: Works since 1.2.1 (the loading screen waited for a Meta analytics request).
|
||||
tested_version: 1.0.1.1354
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
- frame.cortopia_org_id
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: a37a5400ef58906b575f6da1649b62ac7ce024eab831fae105457c1d5d12c2de
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 390
|
||||
source_hint: Wonder
|
||||
@@ -1,20 +0,0 @@
|
||||
package: com.CreaTeam.Sclerosis
|
||||
title: Sclerosis
|
||||
status: works
|
||||
notes: Copy the "Amnesia The Dark Descent" folder from a Windows/Proton install to "FramePort/com.CreaTeam.Sclerosis/lepton-data/external".
|
||||
Default hand positions are rather askew and should be amended via VR Controller Mappings in Steam itself. FOV
|
||||
warping effects are EXTREMELY disorienting in VR, beware. Otherwise seems to work without issue.
|
||||
tested_version: 1.10.7
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: e17421ad9894f5e57fb9b1b8110544715166756ef924b5a86831f6f20e70a6d5
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
issue: 339
|
||||
source_hint: Sclerosis_v1.10.7
|
||||
@@ -1,24 +0,0 @@
|
||||
package: com.Curve.LawnMowingSimulatorVR
|
||||
title: Lawn Mowing Simulator VR
|
||||
status: issues
|
||||
notes: Left eye textures flicker only when mowing in specific areas.
|
||||
tested_version: 1.0.50
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_consistency: 1
|
||||
hide_space_warp: 1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 2653e9b1dfbd11d607b5dd17b9c07ab19d9e98e1004ef9b14cb52867b5f959dc
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 91
|
||||
issue: 344
|
||||
source_hint: com.Curve.LawnMowingSimulatorVR
|
||||
@@ -1,30 +0,0 @@
|
||||
package: com.CyDream.BattleTalent
|
||||
title: BattleTalent
|
||||
status: issues
|
||||
notes: "Plays normally. The mixed-reality mode doesn't appear in the start menu (also with an APK from a Quest 3); MR
|
||||
progress in #355. LAN multiplayer works; online rooms need Meta's account check (user proof), so online
|
||||
multiplayer can't work (#408)."
|
||||
tested_version: '4.7'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
- frame.luajit_pointers
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.meta_permissions
|
||||
- frame.unity_multipass
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: 17bbf8d7fc0f24d61986a0c5e1e9175cfe1239de6f8bcb2f588a57f80d83a1d3
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 381
|
||||
source_hint: com.CyDream.BattleTalent
|
||||
@@ -1,10 +0,0 @@
|
||||
package: com.FlatHeadStudio.DeadlyDelivery
|
||||
title: Deadly Delivery
|
||||
status: unsupported
|
||||
notes: "Quits after Meta's entitlement check fails: it needs Meta's online services."
|
||||
tested_version: 1.0.30
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
issue: 322
|
||||
@@ -6,14 +6,8 @@ engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Recommended pilot title.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 2300f628ce04c1da60b52fed18335d6a31b88cc88313fd6d2c0e1a8e7785126f
|
||||
updated: '2026-10-15'
|
||||
@@ -1,25 +0,0 @@
|
||||
package: com.GexagonVR.HardBullet
|
||||
title: HardBullet
|
||||
status: issues
|
||||
notes: Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release)
|
||||
shows them as six panels, not confirmed in the headset yet. Level loads stall frame submission for up to ~1 s, so
|
||||
the view freezes during loads.
|
||||
tested_version: 1.0.9.704Q
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.hw_video_decode
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: ecbb758d98083a590f100d5ac6efe9490ef74ad39b59217e0b4a11db920ecd51
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 96
|
||||
issue: 385
|
||||
source_hint: HARD BULLET
|
||||
@@ -1,22 +0,0 @@
|
||||
package: com.HypetrainDigitalLtd.Breathedge
|
||||
title: Breathedge VR
|
||||
status: works
|
||||
notes: Loading fade is broken and only shows a small square but the game works!
|
||||
tested_version: 1.0.7
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 4975a32fbf36b4d417142a944bf7d771c18a184dc0320b5937ac2c67bd013575
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 91
|
||||
issue: 347
|
||||
source_hint: Breathadge
|
||||
@@ -5,14 +5,9 @@ source_hint: 'Sniper Elite VR- Winter Warrior'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
notes: ''
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 6b8170da5826b9a35326a20967a0e5a242d46fb569a0a3c669e1cbd368102dd3
|
||||
updated: '2026-10-15'
|
||||
@@ -1,20 +0,0 @@
|
||||
package: com.Meta4.TFBeyondReality
|
||||
title: 'Transformers Beyond Reality: Redux'
|
||||
status: unknown
|
||||
notes: Starts and runs at 72 fps in headless launch tests (2026-10-17); not yet checked in the headset. Steam's shader
|
||||
cache layer is left out and a few shaders the Frame's driver refuses are skipped, so some objects may be missing.
|
||||
details: Unreal Engine game (Binned3 allocator, GitHub issue 349). Crash 1 - the allocator's 76 GiB up-front
|
||||
reservation is refused by the Frame's kernel (frame.unreal_noreserve, suggested by analysis). Crash 2 - Unreal's
|
||||
"Shader compilation failures are Fatal." With Lepton's Fossilize layer loaded every graphics pipeline failed with
|
||||
VK_ERROR_OUT_OF_HOST_MEMORY, even a constant-colour one (not the pipeline cache, not the CPU allocator, not Valve's
|
||||
FDM/RPO layers); vk_no_fossilize takes the layer out of the game's instance. A few pipelines with the game's
|
||||
multiview vertex shaders then still fail in Turnip; vk_pipeline_fallback creates them with blank shaders.
|
||||
tested_version: 1.495.34291
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
adapter:
|
||||
vk_no_fossilize: 1
|
||||
vk_pipeline_fallback: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 349
|
||||
@@ -1,28 +0,0 @@
|
||||
package: com.Miragesoft.RealVrFishingQ
|
||||
title: Real VR Fishing
|
||||
status: issues
|
||||
notes: Seems to be almost 100% functional with meta permissions enabled. In the new Mixed Reality mode, the room
|
||||
scan doesn't seem to do anything and I can't seem to manually place anything either.
|
||||
tested_version: '3.06096'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.vivox_audio_route
|
||||
- frame.unity_text_input
|
||||
- frame.oculusos
|
||||
- frame.meta_permissions
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scene_emul: 1
|
||||
controller_models: 1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 8b17dd26ef0486618ec006f9d948a60dabea4d7763bbe6061db07ee8aad27c76
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 91
|
||||
issue: 333
|
||||
source_hint: Real VR Fishing
|
||||
@@ -6,15 +6,11 @@ engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Passthrough + Meta scene/room emulated from the Steam guardian (scene_emul=1).
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: Mixed-reality-only; builds its level from the room model, which the adapter emulates from the Steam guardian.
|
||||
overport_extra:
|
||||
- patch_force_passthrough
|
||||
adapter:
|
||||
scene_emul: 1
|
||||
scene_hide_bounds: 1
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
@@ -22,5 +18,3 @@ verified:
|
||||
known_good_sha256: 9f900fc86fbb2663abe83c82ab79cc72bbf1f93fea54f7583fd274dd07e3d09c
|
||||
frame:
|
||||
- frame.meta_permissions
|
||||
- frame.unity_text_input
|
||||
updated: '2026-10-17'
|
||||
@@ -1,25 +0,0 @@
|
||||
package: com.RealitiesIO.puzzlingPlaces
|
||||
title: Puzzling Places
|
||||
status: issues
|
||||
notes: Works fine, but pointers dont show well on main menu, easy to work around.
|
||||
tested_version: '2.14'
|
||||
engine: Unreal
|
||||
xr: OpenXR
|
||||
overport_extra:
|
||||
- patch_disable_space_warp
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_skip_obb_check
|
||||
- frame.unreal_ovrp_entrypoints
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
equirect_emul: 1
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 8d9bb64b0af4c34269fffb64c5ef7515d6ba4715f5234f95517975b8e2b56c00
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
issue: 359
|
||||
source_hint: Puzzling Places
|
||||
@@ -1,22 +0,0 @@
|
||||
package: com.SDI.TWD
|
||||
title: 'The Walking Dead: Saints & Sinners'
|
||||
status: works
|
||||
tested_version: Shipping / 2024.01.19 / build 400355 - STAGECH1
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.ovr_microphone
|
||||
- frame.hw_video_decode
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: 4b172a5abba0901c75278fb55b867f344120c658c26ae07c8558d90e0c38cec3
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 91
|
||||
issue: 376
|
||||
source_hint: The Walking Dead- Saints & Sinners
|
||||
@@ -1,22 +0,0 @@
|
||||
package: com.SchellGames.LostRecipes
|
||||
title: Lost Recipes
|
||||
status: works
|
||||
tested_version: 1.0.1+273175
|
||||
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
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: f071a1deea3e3b6dce0c3a923127f540a48b88532106536c1838f496e9d3e6c9
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 91
|
||||
issue: 340
|
||||
source_hint: Lost Recipes
|
||||
@@ -6,18 +6,12 @@ engine: Unity
|
||||
xr: OpenXR
|
||||
status: issues
|
||||
notes: Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: Hand-tracking-only game; Steam Frame has no optical hand tracking. Built with controller_fix=0 so hand
|
||||
data passes through if the runtime ever provides it.
|
||||
adapter:
|
||||
controller_fix: 0
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: a19b80db0c300d422cbba35a4924ab59211b39bcb6485e200d310e3e1372ddb7
|
||||
updated: '2026-10-15'
|
||||
@@ -1,11 +0,0 @@
|
||||
package: com.TriangleFactory.Breachers
|
||||
title: Breachers
|
||||
status: unsupported
|
||||
notes: Online-only multiplayer that needs Meta's online services, which aren't available on the Frame. The game's own
|
||||
copy of Meta's platform library (libovr_custom.so) quits at start ("Failed to launch SystemActivities").
|
||||
tested_version: 40.0.23
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
issue: 357
|
||||
@@ -1,24 +1,22 @@
|
||||
package: com.UNIVRS.Freedom
|
||||
title: 'Attack on Titan VR: Unbreakable'
|
||||
status: works
|
||||
notes: Keep the extra files that come with the APK and OBB next to the OBB (they hold the audio). Cutscenes are
|
||||
detached from the head but still viewable.
|
||||
title: Freedom
|
||||
status: issues
|
||||
notes: Volume can't be changed in the game.
|
||||
tested_version: 1.0.2950
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.asset_files
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 1b3ced1a24b3b708f66aba6aa879863b667c0e502ef153d8a8cfa03e529d21fd
|
||||
app: 1.2.0
|
||||
date: '2026-10-10'
|
||||
known_good_sha256: 24662272c4cbe5dc7c4f3c803bdb9dead9661ac3d59bf80cca2e260256acc4bc
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 91
|
||||
issue: 337
|
||||
source_hint: Aot
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 132
|
||||
source_hint: AOT VR
|
||||
@@ -7,15 +7,10 @@ xr: OpenXR
|
||||
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.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
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:
|
||||
- patch_ac_nexus_no_appsw_90
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
@@ -23,5 +18,5 @@ verified:
|
||||
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-15'
|
||||
updated: '2026-10-14'
|
||||
min_app: 1.2.0
|
||||
@@ -1,27 +0,0 @@
|
||||
package: com.VertigoGames.The7thGuestVR
|
||||
title: The 7th Guest VR
|
||||
status: issues
|
||||
notes: Runs and is playable. Foveated rendering (fixed and eye-tracked) sits offset toward the bottom right, and
|
||||
changing the foveated region size has no visible effect.
|
||||
tested_version: 1.1.3.12072
|
||||
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
|
||||
scale: 2.0
|
||||
foveation: fixed
|
||||
foveation_radius: 0.1
|
||||
verified:
|
||||
date: '2026-10-16'
|
||||
known_good_sha256: 11a1208888adedf4aa48a672b2af5535f0e8f11e5bb1ce0a28bafe5d272bb672
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 91
|
||||
issue: 363
|
||||
source_hint: Guest
|
||||
@@ -1,21 +0,0 @@
|
||||
package: com.WenklyStudio.SurvivalNation
|
||||
title: Survival Nation
|
||||
status: works
|
||||
tested_version: 1.14.1
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
scale: 1.8
|
||||
verified:
|
||||
date: '2026-10-18'
|
||||
known_good_sha256: 7903dfee1c9c36d19e1c6c78a1f0ede1f0b25ce6a6065811997297e701009250
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 401
|
||||
source_hint: survival nation
|
||||
@@ -1,5 +1,5 @@
|
||||
package: com.asg.clockworkdev
|
||||
title: The Last Clockwinder
|
||||
title: Clockwork
|
||||
status: works
|
||||
tested_version: '1.29'
|
||||
engine: Unity
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package: com.baywilly.crazytaxivr
|
||||
title: CrazyTaxiVR
|
||||
status: works
|
||||
tested_version: CrazyTaxiVR-42
|
||||
engine: Other
|
||||
xr: OpenXR
|
||||
adapter:
|
||||
scale: 0.85
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: 6801a09fec435776457dd2abd1c60d7db240be26cb846699946303aa94f53b04
|
||||
app: 1.2.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261015.6141207'
|
||||
agent: 91
|
||||
issue: 380
|
||||
source_hint: CrazyTaxiVR
|
||||
@@ -2,7 +2,6 @@ package: com.camouflaj.manta
|
||||
title: 'Batman: Arkham Shadow'
|
||||
status: works
|
||||
notes: Large data set (~27 GiB) incl. all language packs.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: 'Uses Meta XR Audio (Wwise), handled by patch_meta_xr_audio. Cutscenes are 8K HEVC panoramas: frame.hw_video_decode
|
||||
decodes them on the Frame''s hardware, adapter.surface_native shows them in stereo (PRs #96, #128). Includes all
|
||||
language packs (~28 GB). sync_guard (to test, GitHub #102) - a reporter''s game quit to Steam''s waiting screen
|
||||
@@ -14,19 +13,15 @@ overport_extra:
|
||||
- patch_disable_space_warp
|
||||
frame:
|
||||
- frame.hw_video_decode
|
||||
- frame.unity_text_input
|
||||
adapter:
|
||||
fixed_resolution: 1
|
||||
sync_guard: 1
|
||||
surface_native: 1
|
||||
fade_quad: 1
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
|
||||
updated: '2026-10-18'
|
||||
min_app: 1.2.2
|
||||
updated: '2026-10-13'
|
||||
min_app: 1.1.0
|
||||
source_hint: Batman- Arkham Shadow
|
||||
@@ -6,15 +6,9 @@ engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Co-op unavailable.
|
||||
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
|
||||
details: 'Release notes: for DLC use an older version. Co-op likely unavailable.'
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 06428a6328a49b3ca769e070dd0d2bb55c2122c8fba5d07a69b1b206a532c212
|
||||
updated: '2026-10-15'
|
||||
@@ -1,24 +0,0 @@
|
||||
package: com.cosmorama.tabletroopers
|
||||
title: Table Troopers
|
||||
status: works
|
||||
tested_version: 2026.40.4
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.gms_platform_version
|
||||
- frame.photon_voice_audio_route
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
scale: 2.0
|
||||
verified:
|
||||
date: '2026-10-17'
|
||||
known_good_sha256: b0a4b0e29855277712b580bd5507721fff137bde77b1911b34cca89c627feef1
|
||||
app: 1.2.1
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 96
|
||||
issue: 391
|
||||
source_hint: Troopers
|
||||