Compare commits

..
Author SHA1 Message Date
spoopyghosty0 2abb694043 fexrift: route and VR arguments from the build's files; local copies use profile rules
Ideas from RiftLift (MIT) and xrizer (reviewed, nothing copied), see docs/rift-fex/references.md:
- oculus_facts: LibOVR / Platform SDK / OpenVR / Unity from the game's files and exe markers
- vr_route: OpenVR builds without the Platform SDK (incl. Unity dual builds) launch straight on SteamVR,
  without Revive and without Meta's runtime (launch.sh FP_META_RUNTIME=0); everything else keeps the
  proven Meta runtime + Revive route; explicit profile revive/meta_runtime and verified launches win
- vr_args: unverified games get the engine's VR switch when their arguments don't pick one
  (Unreal -vr, Unity -vrmode OpenVR on the direct route); profile vr_args: off
- launch.sh: opt-in Wine debug channels via <anchor>/debug-wine
- fix: local copies ran no compatibility hooks and counted any same-named profile as verified
  (Chronos, a likely profile); they now follow the same rules as games in Meta's library

Entitlement and Oculus signature checks are untouched (RiftLift's Platform SDK shim is not adopted).
Not device-tested yet.
2026-10-17 17:52:26 -04:00
spoopyghosty0 b33cc2de09 fexrift: prefix setup creates the Quick Launch folder (Meta's runtime quit right after starting without it)
Meta's app framework stops with "Error acquiring quick launch directory" when
%APPDATA%\Microsoft\Internet Explorer\Quick Launch is missing, and OVRServer then
leaves its event loop as soon as it has initialised. The Frame's prefix had the folder,
a fresh one doesn't.
2026-10-17 15:27:00 -04:00
spoopyghosty0 6a71734552 fexrift perf: default settings launch exactly like the verified headset run
First Contact and Dreamdeck didn't render in the headset with the new defaults, which differed from the launch
verified in the headset on 2026-10-10: a regenerated Revive eye-scale copy, a dxvk.conf and
WINE_LARGE_ADDRESS_AWARE=1. Defaults now add none of them (eye scale 1.5 = the uploaded Revive itself, no
dxvk.conf unless a DXVK setting changed, large-address-aware off); a changed setting still applies at launch.
2026-10-16 11:06:39 -04:00
spoopyghosty0 3441f3ee39 fexrift bench: report SteamVR's IMU fallback (no tracking universe)
Without a tracking universe Rift games wait at startup and a headless run only counts SteamVR's own frames
(~1.9/s, game GPU ~0 %): First Contact, Dreamdeck and Chronos all looked broken on the Frame (2026-10-16). The
summary now carries imu_fallback (SteamVR logged "Rejecting new committed universe since IMU fallback is active"
during the run) and such a run is never valid.
2026-10-16 10:51:18 -04:00
spoopyghosty0 b9a9757a64 fexrift bench: Steam's controller log isn't owner input once the game starts
The starting OpenVR game loads its input bindings, which writes Steam's controller.txt: the bench stopped itself
("owner interaction (activity 0)") with Dreamdeck. During the wait and the window only activity 1 (headset
worn/moved) stops it; the controller log stays a pre-start guard.
2026-10-16 10:41:11 -04:00
spoopyghosty0 f9f3a63e0c fexrift install: launch arguments from the PC's Meta app library
Meta launches many Rift games with arguments (Unreal: -vr) that only its manifest carries. A copy installed from
elsewhere now takes the launch arguments of the same build (same exe name) from this PC's Meta Horizon/Oculus
libraries (Program Files + the libraries registered under HKCU\Software\Oculus VR, LLC\Oculus\Libraries),
matched by app ID or canonical name.
2026-10-16 10:07:37 -04:00
spoopyghosty0 95fd2e5b0b fexrift install: a folder from a Meta app library uses Meta's manifest
Installing <library>/Software/<name> (e.g. the Windows Meta app's C:\Program Files\Meta Horizon\Software) takes
exe, launch arguments, app ID and version from <library>/Manifests/<name>.json. Unreal Rift games need its
-vr argument (Dreamdeck: -fullscreen -vr), which nothing else provided for a folder install.
2026-10-16 09:54:43 -04:00
spoopyghosty0 98f91abe97 fexrift: per-game ime_patch "off"; Chronos renders without the IME fix
On the Frame Chronos (UE 4.10) with the generic IME patch (found at member +0x100) connected to SteamVR but never
submitted a frame; with its original exe a bounded headless run submitted 202 frames in 105 s. The profile can
now turn the finder off (ime_patch: "off"), set for Chronos.
2026-10-16 09:33:54 -04:00
spoopyghosty0 5eb6c8389d fexrift bench: start the window when the game's process appears
The launcher waits 60-100 s for Meta's runtime before it starts the game, so a 90 s window ended before the
game existed (0 frames on the Frame). The bench now waits up to START_WAIT (180 s) for the game's process,
still stopping on real interaction, and the agent/CLI timeouts grow to match (agent 94).
2026-10-16 09:26:24 -04:00
spoopyghosty0 c811dbabe5 fexrift bench: the launched game waking the compositor isn't owner interaction
The run must start with the headset in standby (activity 3), but the game itself takes it out of standby, so
the first Frame run stopped itself after a few seconds. During the run only activity 1 (worn/moved) or new
controller input stops it; the error names the level.
2026-10-16 09:22:10 -04:00
spoopyghosty0 14a58443d3 fexrift bench: SteamVR settings writes in their own process
Reproduced on the Frame: cmd_vrsetting in the process holding the bench's VR() connection segfaults on its second
call. The bench's first write (pauseCompositorOnStandby=false) succeeded and the second (screen timeout)
crashed it, leaving the display on. vr_set now runs fexrift.py vrsetting in a fresh process per write and checks
its answer.
2026-10-16 09:20:40 -04:00
spoopyghosty0 e27dce210f fexrift bench: measure in a child, restore the display even if it crashes
On the Frame the bench helper died silently before launching the game and left SteamVR's
pauseCompositorOnStandby=false (display kept on): a native crash in the ctypes SteamVR calls skips Python's
finally. The measurement now runs in a forked child that records the original screen timeout and the game's
process group in a guard file; when no result comes back the parent kills the game group, runs wineserver -k and
restores both display settings, each through its own vrsetting process.
2026-10-16 09:19:23 -04:00
spoopyghosty0 5e38e4b46e fexrift: re-upload the helper scripts before every install
Only fex setup uploaded fexrift.py, its extensions and games json, so a helper fixed on the PC never reached
a Frame set up earlier (the Frame kept the old runtime probe: Chronos timed out although the runtime listed
it as installed). sync_helpers uploads them (~200 KB) at the start of both install paths.
2026-10-16 00:50:22 -04:00
spoopyghosty0 387c0eb28c fexrift: Meta's session folder is <user id>-<n>, not only digits
The signed-in PC prefix names it sessions/<numeric user id>-<n>/ (7dcb601 required digits only and refused
that prefix for import-login).
2026-10-15 23:57:08 -04:00
spoopyghosty0 7dcb601792 fexrift: a Meta sign-in is sessions/<user id>/, not any file in sessions/
On the Frame Meta's app framework created sessions/_oaf/data.sqlite as soon as the runtime started (the
install's runtime probe), so fex status said login: true without a sign-in. A real sign-in (PC prefix,
2026-10-09) adds sessions/<numeric Meta user id>/. fex login's done signal looked for files directly under
sessions/, which a real sign-in never has: it would have waited forever. One check (signed_in) for status,
fex login and the PC import.
2026-10-15 23:52:41 -04:00
spoopyghosty0 3733a38e4e fexrift install: count Meta's 'Currently installed package' line as registered
On the Frame the runtime lists every registered package at start (PackageManager "Currently installed
package: lies-beneath, version code: 1") but writes no "state:" line before an app tracker / sign-in, so the
probe timed out although the registration worked. Seen with Lies Beneath, 2026-10-15.
2026-10-15 20:43:47 -04:00
spoopyghosty0 e1c16a4bf2 fexrift perf: accept the Revive upload fex setup actually makes
prepare_scales hashed the upload after undoing the eye-size redirect but compared it with the hash of the
upload as made (with the redirect): setup failed on the device. Compare the raw upload against both valid
forms; a new test runs prepare_scales on the real Revive 3.2.0 DLL patched like setup does.
2026-10-15 20:06:15 -04:00
spoopyghosty0 61d728fd21 fexrift artifacts: fp_pipesend.exe (Meta sign-in callback) and fp_mem v3 (FP_MEM_ZERO)
Built from native/fexwine/pipesend.c with llvm-mingw 20261006 and fp_mem.c with gcc (glibc 2.34 symbols),
both with -ffile-prefix-map; SHA256SUMS updated.
2026-10-15 19:59:20 -04:00
spoopyghosty0 122845308f agent 93: fex login/local-install/settings/bench commands 2026-10-15 19:58:12 -04:00
spoopyghosty0 451b4d6e5b fexrift: one per-game dxvk.conf for compat profiles and perf settings
With the perf extension loaded the compat profile's dxvk lines go into perf_prepare's per-game config (its
managed keys win) instead of a second DXVK_CONFIG_FILE that perf overrode. perf_prepare creates the settings
folder itself (a first launch without settings_set failed). Compat launcher test expects the per-game core
mask; the temporary-handler test only checks the handler's own files.
2026-10-15 19:54:45 -04:00
spoopyghosty0 c21126ec7d Merge rift/perf: launch-time perf settings, fp_mem zero-on-reuse, bounded bench
# Conflicts:
#	agent/fexrift.py
2026-10-15 19:42:16 -04:00
spoopyghosty0 879bcbf9b0 Merge rift/compat: per-game profiles, graphics routes, triage
# Conflicts:
#	agent/fexrift.py
2026-10-15 19:41:38 -04:00
spoopyghosty0 351a1af31d Merge rift/login: sign in to Meta on the Frame
# Conflicts:
#	agent/frameport_agent.py
2026-10-15 19:41:09 -04:00
spoopyghosty0 5739646ce1 Merge rift/install: install local Rift copies + Meta library registration 2026-10-15 19:40:31 -04:00
spoopyghosty0 6b002d0799 Validate login callbacks, broker expiry and desktop handler quoting 2026-10-15 19:40:06 -04:00
spoopyghosty0 6b4e50cf7c Record offline validation and preserve direct OpenVR survey profiles 2026-10-15 19:37:12 -04:00
spoopyghosty0 30b1c78b99 Tighten benchmark attribution and document Rift performance experiments 2026-10-15 19:37:04 -04:00
spoopyghosty0 45563c5047 Add surveyed Rift game profiles and maintainer validation steps 2026-10-15 19:33:57 -04:00
spoopyghosty0 58489eeb75 Document local Rift installation device validation 2026-10-15 19:32:42 -04:00
spoopyghosty0 0ce121f2dc Harden local Rift resume and runtime probing 2026-10-15 19:32:00 -04:00
spoopyghosty0 06b289f7fc Stop the Meta sign-in launcher before restoring browser handlers 2026-10-15 19:30:57 -04:00
spoopyghosty0 25ac577365 Add launch-time Rift performance settings and bounded benchmarks 2026-10-15 19:28:36 -04:00
spoopyghosty0 c9240670d4 Add Meta browser sign-in for the Frame FEX prefix 2026-10-15 19:28:00 -04:00
spoopyghosty0 ebda4d644a Install local Rift copies in FEX Meta libraries 2026-10-15 19:26:22 -04:00
spoopyghosty0 9e96fb8fb4 Add FEX compatibility profiles, graphics routes and runtime diagnostics 2026-10-15 19:26:18 -04:00
spoopyghosty0 98786ab110 fexrift: per-game data in fexrift_games.json, extension modules per area
GAMES moves to agent/fexrift_games.json (uploaded with the helper). fexrift.py loads fexrift_<area>.py
extensions (login, install, compat, perf) by path; each registers commands and launcher-env / install-check
hooks and reads the core's live globals. The PC uploads every fexrift*.py/json, release bundles carry
.txt copies of all of them. Launcher and install accept a game_dir outside Meta's library.
2026-10-15 19:14:43 -04:00
spoopyghosty0 18f45702de fexrift: keep build paths out of the arm64 wineserver
The committed wineserver.arm64 carried its Frame build folder in wine's assert paths, which main's
test_no_local_paths_in_artifacts rejects. The 41-byte prefix is replaced in place by one of the same
length (code and offsets unchanged), SHA256SUMS updated, and build_wineserver.sh now maps source and
build dirs with -ffile-prefix-map.
2026-10-15 17:44:54 -04:00
spoopyghosty0 3b71e5a560 rift_fex_survey: static survey of Rift games for the FEX mode
Engine, VR APIs and the Platform SDK from FramePort's Rift analysis; Unreal version from the branch string
(any ++… form naming a version) else the pak format's range; the RHI from DefaultEngine.ini (TargetedRHIs /
DefaultGraphicsRHI, else Unreal's D3D11 default) instead of the import list, which names every RHI; pak
indexes v1-v11 (v10+ bit-packed entries + full directory index, zlib/gzip; encrypted index and Oodle noted);
IME fix site in the exe or modular ApplicationCore/Slate; CryEngine detected. The report stays in the user
data dir (local paths).
2026-10-15 17:38:48 -04:00
spoopyghosty0 c023ea28f7 fexrift: generic install (manifest exe/args, Unreal IME fix found in the binaries, --force-unverified)
GAMES keeps only verified games and their per-game opt-ins (fp_mem, msaa_ini). Any other game Meta's app
downloaded gets its exe and launch arguments from Meta's manifest, the IME-deadlock fix located by
find_ime_patch (also in modular ApplicationCore/Slate DLLs) and static checks (engine, imported graphics
DLLs); installing it needs --force-unverified. import-login takes --app. Launcher arguments are quoted,
the title sanitised. Lint fixes in the fexrift files.
2026-10-15 17:38:46 -04:00
spoopyghosty0 8b1e2cdf43 fex install: mark --msaa as unverified (both Steam launches with r.MobileMSAA=2 never rendered) 2026-10-15 17:38:45 -04:00
spoopyghosty0 633fcde50d fexrift launcher: gamescope's WSI layer off, no Steam overlay preload
Started from Steam, gamescope's Vulkan WSI layer took over First Contact's desktop mirror window, which gamescope
doesn't show while SteamVR runs: the game delivered about one frame a second (research launches, started outside
Steam, never had the layer and ran at 72 fps).
2026-10-15 17:38:44 -04:00
spoopyghosty0 d063bad128 fexrift: register Meta's default library in the prefix
Found on the device: in a fresh prefix with an imported login, Meta's runtime listed First Contact as
"install_available" (no HKCU\Software\Oculus VR, LLC\Oculus\Libraries entry, which Meta's app had written in
the research prefix), so the game waited forever at startup when launched from Steam. The prefix fixes now write
DefaultLibrary + its OriginalPath/Path (C: by its volume name) like Meta's app; the runtime then reports the game
"ready" (checked on the dev Frame).
2026-10-15 17:38:43 -04:00
spoopyghosty0 90cb7b575e fexrift: copy Revive's files without their permission bits (a system install's are read-only) 2026-10-15 17:38:42 -04:00
spoopyghosty0 e1bc723f71 Experimental: Rift games with Meta's runtime on the Frame alone (CLI, FRAMEPORT_EXPERIMENTAL_FEX=1)
Step 1 of making the Rift-on-Frame research usable: a hidden `frameport fex` group (setup, import-login,
install, status) that sets up x86_64 GE-Proton under FEX + Meta's PC runtime + Revive on the Frame and installs
Oculus First Contact as a Steam game.

- agent/fexrift.py (Frame, stdlib): GE-Proton11-7 x86_64 (SHA-512), FEX wrappers + native arm64 wineserver,
  patched DLLs, x86 GnuTLS from the Arch archive (repo SHA-256), Vulkan thunk, prefix, Meta's runtime from Meta's
  CDN with Meta's signature checks, prefix fixes, login import (owner-only, archive deleted), per-game fixes
  (First Contact IME deadlock patch, optional r.MobileMSAA) and the launcher (ntsync, fp_mem, motion smoothing
  off for the game, OVRLibrarian ended, cores 4-7)
- agent v77: fex_prepare/status/setup/import_login/install, kind frame_fex (uninstall keeps the shared prefix,
  launch tests refused)
- src/frameport/fexrift.py: uploads the helper + artifacts/fexrift + Revive 3.2.0 with 6 byte patches (other
  builds refused), packs the login from a signed-in PC prefix
- artifacts/fexrift: crypt32/sechost/dnsapi/DeviceWatcher DLLs rebuilt from GE's exact source +
  native/fexwine/patches (build_pe_dlls.sh; exports = GE's + 8 DnsService*), Wi-Fi WinRT stand-in, wineserver.arm64,
  fp_mem.so, DigiCert Assured ID Root CA (public)
- tests/test_fexrift.py (16 tests)

Not yet run end to end on the device.
2026-10-15 17:38:39 -04:00
spoopyghosty0 73839b2dda Research: fp_mem cache default 12 regions / 6 GB
Second headset run (MSAA off + multi-block cache): frames shown once 83 -> 95 %, shown twice 16 -> 3.2 %,
~125 ms hitches 17 -> 6; 95 of 101 released blocks were reused. The remaining long hitches came when ~2 GB
were already kept and new blocks did not fit the 3 GB cap.
2026-10-15 17:38:00 -04:00
spoopyghosty0 e4f9ca6bd3 Research: fp_mem keeps several big blocks and reuses them at other sizes
In the headset First Contact allocates and releases 0.5-1.6 GB blocks, often several at once (~100-128k page
faults per 100 ms, RSS up to 3.5 GB), which the single exact-size cache missed: 17 hitches of ~125 ms in 4 min.
Up to 8 kept regions (3 GB), reuse by start address (shorter: rest released, longer: only the tail mapped fresh),
no protection change while kept. PC test: same size 41.6 -> 1.7 ms, shorter 31.5 -> 1.6 ms, longer 95.5 -> 25.1 ms.
2026-10-15 17:37:59 -04:00
spoopyghosty0 aa9c605ccc Research: native arm64 wineserver + ntsync and a big-block cache fix First Contact's hitches under FEX
native/fexwine: prep_ge_wine.sh recreates GE-Proton's exact Wine source, build_wineserver.sh builds a
native aarch64 wineserver from it (same protocol as GE's x86_64 server, x86_64 machine model), fp_mem.c is
a guest preload that keeps Unreal's 514 MB block between uses instead of re-faulting it.
Headless on the dev Frame: hitches 17 -> 0 per 20 s, timed-out frames 40 % -> 5.5 %.
2026-10-15 17:37:57 -04:00
spoopyghosty0 607b0946da Research note: Meta login under Wine and Oculus First Contact running in VR on the Frame (stutters, CPU-bound) 2026-10-15 17:37:56 -04:00
spoopyghosty0 772d08a1a1 Research note: Rift Platform SDK handshake on the Frame blocked by OVRLibrarian never exiting under FEX 2026-10-15 17:37:55 -04:00
spoopyghosty0 d32c49d4f5 CLAUDE.md: Rift via x86_64 Wine under FEX (research): x86_64 GE-Proton + Revive injection + OpenVR to SteamVR work headless; Platform SDK / Meta app is the blocker 2026-10-15 17:37:54 -04:00
484 changed files with 11467 additions and 40795 deletions

No files matched your search

-2
View File
@@ -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/)
+530 -134
View File
@@ -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).
-6
View File
@@ -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).
![Screenshots](docs/images/screenshots.png)
![Room (experimental): the scanned room in 3D, clean-up and labels](docs/images/room.png)
## 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 |
+1327
View File
File diff suppressed because it is too large. Load diff
+221
View File
@@ -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)
+396
View File
@@ -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"
}
}
+367
View File
@@ -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 {})
+400
View File
@@ -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")}))
+575
View File
@@ -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]
+246 -2607
View File
File diff suppressed because it is too large. Load diff
-40
View File
@@ -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`.
-16
View File
@@ -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.
+8 -41
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
3e9099b5015e8f486c00bcea9d111ee721faba355a89bcf1df69561e3dc6325c ./digicert-assured-id-root-ca.der
e04db8044185c1c77941252e61794e84af7156c449fedcda4a826d7a9fc91989 ./dlls/crypt32.dll
ec1387fad1f9b65503077e5aa24e3a3b60b3b04b85fd9412e0f2c451772f9b25 ./dlls/dnsapi.dll
00ebfab0f3a066d9c85820332e0ec91cf4040797e580516dcafa048dfe758aa7 ./dlls/sechost.dll
41584c41db41991f20a0b55011efa94618e0817ab351579f793ca01dbb488a0e ./dlls/windows.devices.enumeration.dll
420b21045ddc4f166733bf6d2d7227f0131e20f161c4568accb2b9c15a6d8e86 ./fp_mem.so
f8d6aaaa123337c47837d0be5852f7f6687bb9c4bfaaa108aaab3f04af1ad5b9 ./fp_pipesend.exe
13d455b700b08f40eb3e3452ab264bf535cb3d176c92ec9a3324a635ce4f8588 ./windows.devices.wifi.dll
941a65274007db76b1c40880117fe7d881db32c5394ad09f80ccc44a08213a75 ./wineserver.arm64
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -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"
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

+1 -5
View File
@@ -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
-22
View File
@@ -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
-6
View File
@@ -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
-20
View File
@@ -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
View File
@@ -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
-22
View File
@@ -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
+10 -12
View File
@@ -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
+1 -6
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
Loaded 100 of 484 files, more files were not shown because too many files have changed in this diff. Show more