Compare commits

...
Author SHA1 Message Date
spoopyghosty0 21e2693568 Refresh catalog PR #276 against main 2026-10-14 09:43:11 -04:00
spoopyghosty0 8d8a5dab59 Merge pull request #275 from spoopyghosty0/catalog/issue-267
catalog: quest.eleven.forfunlabs
2026-10-14 09:43:00 -04:00
spoopyghosty0 185a947ba1 Refresh catalog PR #275 against main 2026-10-14 09:42:54 -04:00
spoopyghosty0 8622f8bc5c Merge pull request #274 from spoopyghosty0/catalog/issue-260
catalog: com.notdead.COMPOUNDQUEST
2026-10-14 09:42:31 -04:00
spoopyghosty0 70d722063e Refresh catalog PR #274 against main 2026-10-14 09:42:11 -04:00
spoopyghosty0 e9ab42bcab Merge pull request #273 from spoopyghosty0/catalog/issue-248
catalog: com.polyarc.Moss2
2026-10-14 09:42:02 -04:00
spoopyghosty0 6d9944844a Refresh catalog PR #273 against main 2026-10-14 09:41:57 -04:00
spoopyghosty0 11c52636c3 Merge pull request #272 from spoopyghosty0/catalog/issue-259
catalog: pl.idreams.zombirds
2026-10-14 09:41:44 -04:00
spoopyghosty0 bf455baeb5 Refresh catalog PR #272 against main 2026-10-14 09:41:37 -04:00
spoopyghosty0 986e554fa5 Fix sunglasses lens symmetry in Cool Game demo artwork 2026-10-14 09:34:49 -04:00
spoopyghosty0 f6ae3f7724 Fix xLua startup on Frame with full-pointer Lua engine 2026-10-14 09:19:19 -04:00
spoopyghosty0 310863900f Add lightweight Windows display diagnostics 2026-10-13 23:41:13 -04:00
spoopyghosty0 7462317dcb Fix GL4ES draw dispatch in Team Beef ports 2026-10-13 23:25:29 -04:00
spoopyghosty0 32a2694b05 Recover windowless FramePort instances when Windows hides the lock owner 2026-10-13 20:46:47 -04:00
spoopyghosty0 421ee8cb5f Enable reported working fades for Batman and Iron Man VR (#197) 2026-10-13 20:29:29 -04:00
spoopyghosty0 693e75b19a Retry OVRPort's missing application label failure (#269) 2026-10-13 20:21:57 -04:00
spoopyghosty0 bdbe9550ba Keep saved Android recipes out of empty OVRPort conversions (#271) 2026-10-13 20:12:50 -04:00
spoopyghosty0 c1653f21b1 Fix case-insensitive Steam shortcut keys (#263) 2026-10-13 19:20:39 -04:00
github-actions[bot] 5acf9f1d7a catalog: quest.eleven.forfunlabs (from #267) 2026-10-13 22:36:55 +00:00
github-actions[bot] 88dd4a99a6 catalog: com.NiVision.RacketPinball (from #262) 2026-10-13 22:36:54 +00:00
github-actions[bot] e30c9735dc catalog: com.notdead.COMPOUNDQUEST (from #260) 2026-10-13 22:36:52 +00:00
github-actions[bot] f237f64d84 catalog: com.polyarc.Moss2 (from #248) 2026-10-13 22:36:50 +00:00
github-actions[bot] fd11975ffb catalog: pl.idreams.zombirds (from #259) 2026-10-13 22:36:48 +00:00
spoopyghosty0 54fdeaca14 Site: the download page's arriving badge uses the site's easing again (reverts that part of 1182f8f) 2026-10-13 00:00:52 -04:00
spoopyghosty0 1182f8f275 Site: release and contributor data refreshed (1.1.0); the download page's arriving badge overshoots like before (1.0.3) 2026-10-12 23:55:20 -04:00
spoopyghosty0 90248b6742 Merge pull request #253 from spoopyghosty0/showcase/update
Update docs screenshots and videos
2026-10-12 23:47:48 -04:00
spoopyghosty0 c8f399da57 Docs: update screenshots and videos 2026-10-13 03:22:34 +00:00
55 changed files with 1738 additions and 104 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the followin
## Included in this repository / the release bundles
| Component | Where | License |
|---|---|---|
| LuaJIT (Lua engine for verified xLua pointer compatibility, github.com/LuaJIT/LuaJIT) | `artifacts/arm64-v8a/libfp_luajit.so`; pinned build in `native/luajit/build.py` | MIT (`native/luajit/COPYRIGHT`, included in bundles as `artifacts/licenses/LuaJIT.txt`) |
| OVRPort VrApi → OpenXR adapter (`native/vrapi`, github.com/Android-XR-Bridge/OVRPort), with FramePort's changes | `native/vrapi-bridge/`, `artifacts/arm64-v8a/libvrapi.so` | GPL-3.0 (`native/vrapi-bridge/LICENSE.upstream`) |
| OVRPort platform compatibility library (`native/platform`) | `native/platformcompat/`, `artifacts/arm64-v8a/libovrplatformcompat.so` | GPL-3.0; Meta Platform SDK headers under the Meta Platform Technologies SDK License (`native/platformcompat/licenses/OCULUS-PLATFORM-SDK.txt`) |
| Khronos OpenXR SDK headers | used to build the native layers | Apache-2.0 / MIT (`native/vrapi-bridge/licenses/OPENXR-SDK.txt`) |
+28 -2
View File
@@ -44,7 +44,7 @@ try:
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
fcntl = None
AGENT_VERSION = 85
AGENT_VERSION = 86
HOME = os.path.expanduser("~")
STEAM = os.path.join(HOME, ".local/share/Steam")
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
@@ -902,6 +902,32 @@ def shortcut_appid(exe, title):
return zlib.crc32((exe + title).encode("utf-8")) | 0x80000000
class VdfMap(dict):
"""Steam's KeyValues names are case-insensitive; keep their spelling when writing back (#263)."""
def _key(self, key):
return next((k for k in self if k.casefold() == key.casefold()), key)
def __contains__(self, key):
return super().__contains__(self._key(key))
def __getitem__(self, key):
return super().__getitem__(self._key(key))
def __setitem__(self, key, value):
super().__setitem__(self._key(key), value)
def get(self, key, default=None):
return super().get(self._key(key), default)
def setdefault(self, key, default=None):
return super().setdefault(self._key(key), default)
def update(self, *args, **kwargs):
for key, value in dict(*args, **kwargs).items():
self[key] = value
def vdf_decode(data):
pos = 0
@@ -914,7 +940,7 @@ def vdf_decode(data):
def node():
nonlocal pos
out = {}
out = VdfMap()
while True:
kind = data[pos]
pos += 1
+2
View File
@@ -1,6 +1,7 @@
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
69102b128e00fa1809489a7308e09c1505f563bf7091bbdd3fe5a58e4cc96c2c ./arm64-v8a/libfp_luajit.so
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
@@ -20,6 +21,7 @@ b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
===============================================================================
LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
Copyright (C) 2005-2026 Mike Pall. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
===============================================================================
[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
Copyright (C) 1994-2012 Lua.org, PUC-Rio.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================================================
[ LuaJIT includes code from dlmalloc, which has this license statement: ]
This is a version (aka dlmalloc) of malloc/free/realloc written by
Doug Lea and released to the public domain, as explained at
https://creativecommons.org/licenses/publicdomain
===============================================================================
@@ -0,0 +1,19 @@
package: com.NiVision.RacketPinball
title: RacketPinball
status: works
tested_version: '2.06'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-13'
known_good_sha256: 80e850f81c24112e8c35a560a333846163a3e783da391290db7663b9015ca58f
app: 1.0.4
overport_cli: 1.2.5
issue: 262
source_hint: RacketPinball
+3 -2
View File
@@ -16,11 +16,12 @@ frame:
adapter:
sync_guard: 1
surface_native: 1
fade_quad: 1
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
updated: '2026-10-10'
min_app: 0.12.1
updated: '2026-10-13'
min_app: 1.1.0
source_hint: Batman- Arkham Shadow
+4
View File
@@ -8,6 +8,8 @@ frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
fade_quad: 1
verified:
date: '2026-10-05'
known_good_sha256: 41e0ab90f622afd50accb0b6e2553520d30a6cc82e5ab4cbb2341b48f7316a71
@@ -15,3 +17,5 @@ verified:
overport_cli: 1.2.5
issue: 47
source_hint: Marvel's Iron Man VR
updated: '2026-10-13'
min_app: 1.1.0
@@ -0,0 +1,23 @@
package: com.notdead.COMPOUNDQUEST
title: COMPOUND
status: works
tested_version: 1.0.12
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-13'
known_good_sha256: 5da7aac9863412daec7ba909dcfd2d5cfa6cb5d078150011c006165058cd76bd
app: 1.1.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 85
issue: 260
source_hint: COMPOUND
+22
View File
@@ -0,0 +1,22 @@
package: com.polyarc.Moss2
title: 'Moss: Book II'
status: works
notes: Just played the very begining but controlls worked. Did nothing other than install it.
tested_version: 1.0.3.149619
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.hw_video_decode
adapter:
haptic_fix: 1
verified:
date: '2026-10-12'
known_good_sha256: 686db536ced97d8a151c82955174ec01d67987c466ecd326d3deb557d8c4fd1d
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 248
source_hint: Moss- Book II
+24
View File
@@ -0,0 +1,24 @@
package: pl.idreams.zombirds
title: Shoot The Zombirds VR
status: works
tested_version: 1.0.2
engine: Unity
xr: VrApi
frame:
- frame.unity_oculus_check
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-13'
known_good_sha256: 2d06d1b6aac83d232e9f559c1cd3188642e7a34ef515605763ee7afa04730156
app: 1.1.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 85
issue: 259
source_hint: Shoot The Zombirds
+11 -6
View File
@@ -1,11 +1,16 @@
package: quest.eleven.forfunlabs
title: Eleven Table Tennis
status: unsupported
notes: Stops at the splash screen. The game needs Meta's online services after start, which aren't available on
the Frame.
status: works
notes: Works great for me, tried the tutorial and an online match. Quest 2 apk.
tested_version: 0.436.37_2026.9.4.18.35.33.415
engine: Unity
xr: OpenXR
pcvr_alternative: The PC VR (Rift) version works on the Frame through FramePort.
adapter:
haptic_fix: 1
verified:
date: '2026-10-10'
issue: 160
date: '2026-10-13'
known_good_sha256: f4d996db1c913639f8b981bf87834e053ee2d9267383da3c1a8372ba755e5ac8
app: 1.1.0
overport_cli: 1.2.5
issue: 267
source_hint: quest.eleven.forfunlabs
+18
View File
@@ -6,6 +6,18 @@
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
# Sources: every failure we hit porting 34 games to the Steam Frame (see docs/PLAYBOOK.md).
signatures:
- id: lua-lightuserdata-pointer
pattern: 'bad light userdata pointer'
severity: fatal
diagnosis: The Lua runtime rejected a pointer outside its supported address range. In the verified older xLua build, valid wider Frame pointers trigger this failure; a scoped compatible engine is available. For other builds, inspect the rejected pointer before assuming this cause.
suggest: [frame.luajit_pointers]
supersedes: [native-crash]
- id: lua-startup-exit
pattern: '(?s)\A(?=.*libxlua\.so[^\r\n]*lj_err_throw)(?=.*(?:__cxa_finalize|exit\+\d+))(?=.*(?:Fatal signal 6|FORTIFY:))'
severity: error
diagnosis: An uncaught Lua error triggered process exit, followed by a crash during Unity cleanup. The mutex failure is secondary; capture the original Lua error before choosing a fix. This stack alone does not identify a pointer-range problem.
suggest: []
supersedes: [native-crash]
- id: vrapi-frame-rejected
pattern: 'OVRPortVrApi:\s+Unsupported frame flags or swap interval'
severity: fatal
@@ -367,6 +379,12 @@ signatures:
here, the OBB doesn't match this APK version - copy the APK and the OBB from the same install, add the game
again and reinstall.
suggest: [frame.unreal_skip_obb_check]
- id: tbxr-fdm-shutdown-crash
pattern: '(?s)\A(?=.*Fatal signal 11)(?=.*fdm_injection_xrDestroySwapchain)(?=.*TBXR_LeaveVR)'
severity: error
diagnosis: The game crashed while leaving VR, inside Valve's foveation layer when it destroys a swapchain (for example Lambda1VR). This can happen after manually quitting; it is not a startup crash and does not explain distorted geometry seen before quitting.
suggest: []
supersedes: [native-crash]
- id: native-crash
pattern: 'Fatal signal (\d+) \(SIG[A-Z]+\)'
severity: fatal
+9
View File
@@ -23,6 +23,9 @@ manifest.json versions (app, agent, tools, SteamOS build, Proton)
app/app.log the app's log (<data>/logs/app.log, rotated 2 MB × 3)
app/jobs/*.log saved GUI job logs (<data>/logs/jobs, last 30): build, install, test, ...
app/settings.json library settings, catalog sources
app/display.json Flet/Flutter versions and last window state; on native Windows, display layout,
resolution, refresh rate, GPU driver metadata and up to 60 recent display/device
System event records (last 30 minutes; timestamps, providers and IDs only)
frame/info.json agent `info` (SteamOS build, Lepton, Proton, installed games, kernel keys)
frame/host.json OpenXR runtime + explicit layers, podman containers, containers.conf, uptime
frame/XRService-*.log the Frame's OpenXR runtime (SteamVR XRService) log, newest
@@ -57,6 +60,12 @@ after the logs were cut); OpenGL ES (no index): the newest modules (by time writ
the kernel's `hangcheck detected gpu lockup` line (this-boot-kernel.txt). All modules stay in the game's storage on
the Frame (`Android/data/<pkg>/files/fp_vk_shaders/`, Files tab → the game's storage).
Display queries run only while collecting logs. The Windows event query has a five-second timeout and runs without
a console window; the result distinguishes no matching events from a failed or timed-out query. Window visibility,
focus and completed move/resize events are recorded in `app.log`, with UTC timestamps for comparison with Windows
events. There is no display polling loop or change to display settings. Empty System event results do not rule out
a rendering or monitor-link problem.
## Redaction
`diag/redact.py` runs on every file and on the issue text.
- **Exact values:** the data dir → `<data>`, home dirs → `<home>`, user names → `<user>`, PC and Frame host names and
+19
View File
@@ -84,7 +84,24 @@
- Display 72 Hz by default in tests. Head pose is only tracked while the headset is worn; otherwise flags 0x3 and
the session stays below FOCUSED.
## Lua native pointer compatibility
Frame's Android processes can map libraries above the lowest 2^47 bytes of address space. The verified xLua API 105
engine in BattleTalent 4.7 rejects those otherwise valid pointers in `lua_pushlightuserdata`. This was isolated using
the unchanged library from the source APK: a real allocation at `0x10000000` round-trips, but its own native `xlua_tag`
pointer above 47 bits raises `bad light userdata pointer` and exits. With `frame.luajit_pointers`, both round-trip
exactly. No game scripts or account checks are changed. This validates an address-layout compatibility requirement;
the APK was not run on an actual Quest during this investigation.
The patch is restricted to SHA-256 `6d5860e4a4f1cc2b05c600c63297ed467369739666edca79413fff4374ff2684`.
It redirects only the Lua engine's public API to a pinned full-pointer LuaJIT, keeping the original xLua bindings,
LuaSocket and game helpers. The patched build survives a 60 s headless launch and creates OpenXR swapchains.
Early frame-rectangle errors and backend login errors remain in the capture; this does not verify rendering or play.
An arbitrary `lj_err_throw` followed by Unity destructor failures does not prove this pointer issue: first capture the
Lua error text. `Unknown Platform` also appears after this crash is fixed, so it was not the fatal cause here.
## Graphics stack
- Explicit foveation injection probe (SteamOS 0.4.5, 20261007.6125817, Lepton 3.0.5, 2026-10-11):
Batman's normal Steam environment requested the FDM Vulkan layer but supplied no `FDM_DEBUG` flag.
Appending `debug` while preserving inherited flags showed `fdm_injection_CreateInstance: skipped`.
@@ -172,6 +189,8 @@
## Steam integration
- Non-Steam shortcut: binary `userdata/<id>/config/shortcuts.vdf`; appid = crc32(exe+title) | 0x80000000;
artwork in `config/grid/<appid>p.*` (portrait), `<appid>.*` (landscape), `<appid>_hero.*`, `<appid>_logo.*`.
KeyValues names are case-insensitive: Steam may save `Shortcuts` / `AppName` after a devkit registration
(GitHub #263). Agent v86 reads and updates keys case-insensitively, keeping Steam's spelling and existing appids.
Steam reads the file only at start → stop steam.service, edit, start (FramePort does it once per batch).
- Processes started from Steam (Konsole, SSH sessions?) share steam.service's cgroup: use `systemd-run --user`.
- SSH: `sshd` must be enabled (`sudo systemctl enable --now sshd`), which needs a user password (`passwd`).
+3
View File
@@ -19,6 +19,9 @@ On first start FramePort downloads the tools it uses (Settings → Tools shows t
## First launch
Starting FramePort again brings its existing window to the front. If a previous instance was left running after
its window closed, FramePort recovers it automatically, including on Windows where the GUI lock cannot be read.
FramePort isn't signed with a paid certificate, so the first start shows a warning:
- **Windows:** "Windows protected your PC" → **More info** → **Run anyway**.
+35 -20
View File
@@ -2,7 +2,7 @@
# Patches
Every patch and game setting FramePort knows: 163 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
Every patch and game setting FramePort knows: 164 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
@@ -11,7 +11,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
## Groups
- [Patches from OVRPort](#group-overport) (25)
- [Steam Frame patches](#group-frame) (51)
- [Steam Frame patches](#group-frame) (52)
- [Game settings](#group-adapter) (66)
- [Files and settings on the Frame](#group-device) (8)
- [PC VR (Proton and Revive)](#group-pcvr) (13)
@@ -39,7 +39,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Patch Oculus detection for Unity](#patch_oculus_unity) | `patch_oculus_unity` | Patches from OVRPort | default |
| [Patch Oculus detection for Unreal](#patch_oculus_unreal) | `patch_oculus_unreal` | Patches from OVRPort | default |
| [Remove localized app names](#patch_remove_localized_names) | `patch_remove_localized_names` | Patches from OVRPort | default |
| [Remove Unreal's ForceQuit](#patch_remove_unreal_force_quit) | `patch_remove_unreal_force_quit` | Patches from OVRPort | 34 |
| [Remove Unreal's ForceQuit](#patch_remove_unreal_force_quit) | `patch_remove_unreal_force_quit` | Patches from OVRPort | 35 |
| [Remove uses-library](#patch_remove_uses_library) | `patch_remove_uses_library` | Patches from OVRPort | default |
| [Remove VrApi library](#patch_remove_vrapi) | `patch_remove_vrapi` | Patches from OVRPort | |
| [Replace application label and icon](#patch_replace_icon_label) | `patch_replace_icon_label` | Patches from OVRPort | default |
@@ -54,10 +54,11 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [OpenGL ES: multiview shaders on flat panels (HUD, menus)](#frame-gl_multiview_fbo) | `frame.gl_multiview_fbo` | Steam Frame patches | 1 |
| [GL shim (Mesa GLSL compatibility)](#frame-gl_shim) | `frame.gl_shim` | Steam Frame patches | 2 |
| [Godot apps: start without a clipboard service](#frame-godot_clipboard) | `frame.godot_clipboard` | Steam Frame patches | |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 6 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 7 |
| [Language packs from the game's files](#frame-langpacks) | `frame.langpacks` | Steam Frame patches | |
| [Launcher category (INFO → LAUNCHER)](#frame-launcher) | `frame.launcher` | Steam Frame patches | default |
| [GL wrapper: OpenGL ES depth formats (Minecraft)](#frame-ltw_depth) | `frame.ltw_depth` | Steam Frame patches | default |
| [Fix Lua startup crashes on Frame](#frame-luajit_pointers) | `frame.luajit_pointers` | Steam Frame patches | |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 1 |
| [Meta XR Audio: skip Quest telemetry class](#frame-metaxr_telemetry) | `frame.metaxr_telemetry` | Steam Frame patches | default |
| [Clear android:debuggable](#frame-nodebug) | `frame.nodebug` | Steam Frame patches | 3 |
@@ -73,13 +74,13 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
| [Allow swapchains larger than 4096 px](#frame-swapchain_limit) | `frame.swapchain_limit` | Steam Frame patches | default |
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 1 |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 17 |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 19 |
| [Unity: render each eye separately (no multiview)](#frame-unity_multipass) | `frame.unity_multipass` | Steam Frame patches | 1 |
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 7 |
| [Unity: skip OVROverlay layers (fades, splash screens)](#frame-unity_no_overlay_copy) | `frame.unity_no_overlay_copy` | Steam Frame patches | 2 |
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 9 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 22 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 69 |
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 10 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 24 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 72 |
| [Unity: share thread keys (games that run out of them)](#frame-unity_tls_key_pool) | `frame.unity_tls_key_pool` | Steam Frame patches | 1 |
| [Unity: headset always counts as worn](#frame-unity_user_presence) | `frame.unity_user_presence` | Steam Frame patches | 1 |
| [GL shim for Unreal (no multisampled render-to-texture)](#frame-unreal_gl_shim) | `frame.unreal_gl_shim` | Steam Frame patches | 1 |
@@ -110,7 +111,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [360° sharpness](#adapter-equirect_res) | `adapter.equirect_res` | Game settings | |
| [360° 3D](#adapter-equirect_stereo) | `adapter.equirect_stereo` | Game settings | |
| [Per-eye diagnostics (log)](#adapter-eye_debug) | `adapter.eye_debug` | Game settings | |
| [Draw fades over the picture](#adapter-fade_quad) | `adapter.fade_quad` | Game settings | |
| [Draw fades over the picture](#adapter-fade_quad) | `adapter.fade_quad` | Game settings | 2 |
| [Fix upside-down menus](#adapter-flip_emul) | `adapter.flip_emul` | Game settings | |
| [Rotate panels 180°](#adapter-flip_quads) | `adapter.flip_quads` | Game settings | |
| [Don't pause on short interruptions](#adapter-focus_hold) | `adapter.focus_hold` | Game settings | 4 |
@@ -121,7 +122,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [GL shim: high-precision vertex shaders](#adapter-gl_highp_markers) | `adapter.gl_highp_markers` | Game settings | 1 |
| [GL shim: integer vertex inputs](#adapter-gl_int_attribs) | `adapter.gl_int_attribs` | Game settings | 1 |
| [Flat panel diagnostics (log)](#adapter-gl_mv_debug) | `adapter.gl_mv_debug` | Game settings | |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 60 |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 65 |
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 4 |
| [Controller input diagnostics (log)](#adapter-input_diag) | `adapter.input_diag` | Game settings | |
@@ -169,7 +170,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Hide Android's navigation bar](#device-hide_navbar) | `device.hide_navbar` | Files and settings on the Frame | |
| [Extra Lepton settings](#device-lepton_env) | `device.lepton_env` | Files and settings on the Frame | 3 |
| [Controllers as a gamepad (Steam Input)](#device-steam_gamepad) | `device.steam_gamepad` | Files and settings on the Frame | |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 69 |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 72 |
| [Launch options (SteamVR or OpenXR)](#pcvr-launch_args) | `pcvr.launch_args` | PC VR (Proton and Revive) | 2 |
| [Provide the Oculus runtime from Revive (Frame)](#pcvr-libovr_redirect) | `pcvr.libovr_redirect` | PC VR (Proton and Revive) | |
| [No Unreal crash reporter](#pcvr-no_crash_reporter) | `pcvr.no_crash_reporter` | PC VR (Proton and Revive) | |
@@ -414,7 +415,7 @@ Stops some Unreal games from closing themselves right after starting.
- `patch_remove_unreal_force_quit`
- **Changes:** Changes the APK (during the OVRPort conversion).
- **Offered (off):** Built as the alternate APK for Unreal games (use it if the game quits itself).
- **Catalog games (34):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Carve Snowboarding, Creed, Dance Central, Espire 1: VR Operative (Quest Edition), Espire 2, ExploreVR and 26 more.
- **Catalog games (35):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Carve Snowboarding, Creed, Dance Central, Espire 1: VR Operative (Quest Edition), Espire 2, ExploreVR and 27 more.
- **Suggested by launch-test signatures:**
- `force-quit` (error): The game quit itself right after starting.
- **Source:** [patches/overport.py](../src/frameport/patches/overport.py)
@@ -610,7 +611,7 @@ Plays the game's videos on the Frame's video hardware, so 4K and 8K video runs s
- `frame.hw_video_decode` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** The game plays video through Android's decoders (MediaCodec, ExoPlayer or VLC): decode it on the Frame's hardware.
- **Catalog games (6):** 4XVR Video Player, Batman: Arkham Shadow, Dance Central, Fruit Ninja, Kill It With Fire VR, MacacoVR.
- **Catalog games (7):** 4XVR Video Player, Batman: Arkham Shadow, Dance Central, Fruit Ninja, Kill It With Fire VR, MacacoVR, Moss: Book II.
- **Source:** [patches/frame/hw_video_decode.py](../src/frameport/patches/frame/hw_video_decode.py)
> Decodes the game's videos on the Frame's hardware video decoder instead of Android's software decoders, which stutter or fall behind with 4K/8K video (for example video players, Batman: Arkham Shadow's cutscenes). Adds FramePort's codec plugin (OMX.frameport.avc/hevc/vp9.decoder) to this game's container only; the APK isn't changed. When the hardware is busy (for example Steam's own hardware video decoding is on) the video is decoded in software as before.
@@ -651,6 +652,19 @@ Stops a black picture in Minecraft launchers (for example QuestCraft).
> Minecraft asks for desktop OpenGL's GL_DEPTH_COMPONENT32 depth textures. Some builds of LTW, the OpenGL-to-OpenGL ES wrapper in Minecraft launchers such as QuestCraft, pass that on unchanged; Quest's driver accepts it, the Frame's Mesa rejects it (GL_INVALID_OPERATION in glTexImage2D), so the depth buffer never exists and the picture stays black. LTW then loads OpenGL ES through a small shim that asks for GL_DEPTH_COMPONENT32F (or 24-bit) instead.
### <a id="frame-luajit_pointers"></a>Fix Lua startup crashes on Frame
Lets games with a verified older Lua engine get past a startup crash on the Frame.
- `frame.luajit_pointers`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** The bundled Lua engine rejects Frame's wider native pointers; use the compatible full-pointer engine while retaining the game's bindings.
- **Suggested by launch-test signatures:**
- `lua-lightuserdata-pointer` (fatal): The Lua runtime rejected a pointer outside its supported address range.
- **Source:** [patches/frame/luajit_pointers.py](../src/frameport/patches/frame/luajit_pointers.py)
> Older xLua/LuaJIT engines reject native pointers outside their 47-bit range, causing a startup error and sometimes a second crash during Unity shutdown (e.g. BattleTalent). Uses a Lua engine that preserves full pointer values while keeping the game's original xLua bindings and native helpers. Applied only to the verified compatible library build.
### <a id="frame-meta_permissions"></a>Declare Meta-only permissions
Declares Meta's permissions some mixed-reality games ask for, so they don't stop.
@@ -847,7 +861,7 @@ Lets Team Beef ports (for example Lambda1VR) start on the Frame.
- **Catalog games (1):** Lambda1VR.
- **Source:** [patches/frame/tbxr_vendor.py](../src/frameport/patches/frame/tbxr_vendor.py)
> Team Beef's ports (for example Lambda1VR, RTCWQuest) load an OpenXR library named after the headset maker and pick their VR setup by maker; for "valve" they stop at start (libopenxr_loader_valve.so not found) or take the Pico path. Adds that library and lets them take the Meta path.
> Team Beef's ports (for example Lambda1VR, RTCWQuest) load an OpenXR library named after the headset maker and pick their VR setup by maker; for "valve" they stop at start (libopenxr_loader_valve.so not found) or take the Pico path. Adds that library and lets them take the Meta path. Keeps GL4ES in charge of its desktop OpenGL draw calls so animated models don't turn into stretched triangles.
### <a id="frame-unity_gl_shim"></a>GL shim for Unity (no multisampled render-to-texture)
@@ -856,7 +870,7 @@ Stops a gray or frozen screen in Unity games that turn on anti-aliasing while ru
- `frame.unity_gl_shim`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unity GLES game that may switch to MSAA at runtime: hide multisampled render-to-texture, which crashes the Frame's GL driver (for example The Room VR).
- **Catalog games (17):** Audica, BattleSisters, Beat Saber, Clockwork, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 9 more.
- **Catalog games (19):** Audica, BattleSisters, Beat Saber, Clockwork, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 11 more.
- **Suggested by launch-test signatures:**
- `unreal-msrtt-crash` (fatal): The game's graphics crashed a few seconds after start.
- **Source:** [patches/frame/vrapi_bridge.py](../src/frameport/patches/frame/vrapi_bridge.py)
@@ -911,7 +925,7 @@ Lets older Unity games start VR without Meta's system apps (they'd stay on a 2D
- `frame.unity_oculus_check` · revision 5
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unity with built-in Oculus support: it checks for Meta's system apps before starting VR, else it runs as a 2D app (for example Accounting+, BattleSisters).
- **Catalog games (9):** Accounting+, Audica, BattleSisters, Cosmodread, End Space, Jurassic World Aftermath Collection, Sniper Elite VR, SUPERHOT VR and 1 more.
- **Catalog games (10):** Accounting+, Audica, BattleSisters, Cosmodread, End Space, Jurassic World Aftermath Collection, Shoot The Zombirds VR, Sniper Elite VR and 2 more.
- **Suggested by launch-test signatures:**
- `unity-vr-device-none` (fatal): Unity didn't start VR, so the game runs as a hidden 2D app.
- **Source:** [patches/frame/unity_oculus_check.py](../src/frameport/patches/frame/unity_oculus_check.py)
@@ -925,7 +939,7 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
- `frame.unity_runtime_msaa_off` · revision 2
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** GLES Unity game whose OVRManager turns 4x MSAA on at runtime: multisampled render-to-texture can hang the Frame's GPU.
- **Catalog games (22):** Audica, BattleSisters, Beat Saber, Clockwork, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 14 more.
- **Catalog games (24):** Audica, BattleSisters, Beat Saber, Clockwork, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 16 more.
- **Suggested by launch-test signatures:**
- `unity-render-crash` (fatal): The game's graphics crashed, so the picture freezes or stays gray.
- `unity-runtime-msaa` (info): The game turns MSAA on at runtime, which can hang the Frame's GPU.
@@ -941,7 +955,7 @@ Lets you type into this app's text fields on the Frame (they'd close at once).
- `frame.unity_text_input` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unity app with text fields (…): they close at once on the Frame because it has no system keyboard.
- **Catalog games (69):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Audica, BattleSisters, Beat Saber, Beat Saber (co-existence build), Blade & Sorcery: Nomad and 61 more.
- **Catalog games (72):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Audica, BattleSisters, Beat Saber, Beat Saber (co-existence build), Blade & Sorcery: Nomad and 64 more.
- **Source:** [patches/frame/unity_text_input.py](../src/frameport/patches/frame/unity_text_input.py)
> Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does this on a Quest), so a selected text field only flashes a caret. Rewrites TouchScreenKeyboardShouldBeUsed -&gt; false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -&gt; true in libil2cpp.so, found per game with Cpp2IL (downloaded on first use), so fields stay selected and take key presses: Steam's keyboard (with "Show the app window") or Type on Frame from the PC.
@@ -1378,6 +1392,7 @@ For games whose fades to black don't show, so you see the level loading (for exa
- **Options:**
- Value: int, default `0`.
- Game settings → Picture → "Draw fades over the picture" (advanced).
- **Catalog games (2):** Batman: Arkham Shadow, Marvel's Iron Man VR.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Games that fade to black by dimming their picture with a layer color scale (XR_KHR_composition_layer_color_scale_bias, for example Batman: Arkham Shadow's loading screens) can show the level loading instead of black on the Frame. FrameBridge then draws the fade itself: a black quad in front of the eyes whose opacity follows the game's dimming. 2 also removes the game's own dimming (if both show, the fade is doubled).
@@ -1540,7 +1555,7 @@ For games that freeze the Frame when a controller vibrates (for example Lucky's
- **Options:**
- Value: int, default `0`.
- Game settings → Controllers → "Fix freezes when controllers vibrate" (advanced).
- **Catalog games (60):** AgeOfJoy, Ancient_Dungeon, Audica, BattleSisters, Beat Saber, Caves, Clockwork, Cosmodread and 52 more.
- **Catalog games (65):** AgeOfJoy, Ancient_Dungeon, Audica, BattleSisters, Beat Saber, Caves, Clockwork, COMPOUND and 57 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Routes OVRPlugin through FramePort's extension shim, which turns Meta's amplitude-envelope vibrations into plain ones: OVRPort's loader reads their nanosecond duration as seconds and allocates gigabytes, freezing the Frame when the game vibrates a controller (for example Lucky's Tale's save slots, GitHub #9). Needs a rebuild.
@@ -2241,7 +2256,7 @@ Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard
- `device.text_input_window`
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** App with text fields: key presses only reach a shown Android window.
- **Catalog games (69):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Audica, Beat Saber, Beat Saber (co-existence build), Blade & Sorcery: Nomad, BodyCombat and 61 more.
- **Catalog games (72):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Audica, Beat Saber, Beat Saber (co-existence build), Blade & Sorcery: Nomad, BodyCombat and 64 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Lepton runs VR apps headless: their Android window is never shown, so it never gets keyboard focus and no key press (Steam's on-screen keyboard, a USB/Bluetooth keyboard, Type on Frame) reaches the app's text fields. With Lepton's lepton-show-flatscreen marker the window is shown (behind the VR view, which keeps working) and Steam's keyboard opens for a selected text field. Steam lists the shown window as "Gamescope" (the Frame's compositor). Pairs with "Make Unity text fields work without a system keyboard".
+7 -1
View File
@@ -13,6 +13,11 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
## Startup failures (visible in launch.log)
| Symptom | Cause | Fix |
|---|---|---|
| Startup exits through `libxlua.so (lj_err_throw)`, then Unity aborts in mutex cleanup; captured Lua error is `bad light userdata pointer` (GitHub #257, BattleTalent 4.7) | This older LuaJIT encodes lightuserdata in 47 bits; Frame supplies valid wider native pointers. Controlled tests with the unchanged library pass a real low-address allocation and fail its own high-address static pointer | `frame.luajit_pointers` automatically selects a full-pointer Lua engine only for the verified library SHA-256, retaining xLua bindings and custom helpers. The scoped patch reaches OpenXR startup for 60 s headless; rendering and gameplay remain unverified. A `lj_err_throw` cleanup stack without the original error message is insufficient to infer this cause |
| A GL4ES Team Beef game draws stretched triangles or spikes across characters and the scene (GitHub #191, Lambda1VR) | The newer GL shim exports GLES draw functions that intercept GL4ES's desktop OpenGL calls, bypassing its client-array and immediate-mode translation | `frame.tbxr_vendor` keeps the shim's five draw symbols private when the APK bundles `libgl4es.so`. GL queries and single-sampled framebuffer fixes remain active; other games retain the draw hooks. A/B eye captures on SteamOS 0.4.5: old shim normal, current shim spikes, current shim with private draw symbols normal at ~72 fps. Worn-headset gameplay still needs confirmation |
| A Team Beef game reports a crash after manually quitting: `fdm_injection_xrDestroySwapchain` → `TBXR_LeaveVR` (GitHub #191, Lambda1VR) | Valve's foveation layer dereferences a null pointer during swapchain cleanup | Triage `tbxr-fdm-shutdown-crash` identifies this as a teardown crash, separate from startup and distorted geometry. No verified cleanup fix yet |
| Conversion fails with `NoSuchElementException: Sequence is empty` in `PatcherContext.appName` (GitHub #269, Accounting+) | OVRPort assumes an application label exists; some APKs only label their activity | FramePort retries that exact failure once using a temporary APK whose application inherits the activity's label; existing labels and the original APK are kept |
| Updating a flat Android app after saving its recipe fails with `no OVRPort patches selected` (GitHub #271, Plezy) | older FramePort loads the saved recipe without restoring its unchanged-install setting or applying the flat-app build rules, then tries an empty VR conversion | update FramePort: saved recipes keep `as_is`, and flat apps still skip OVRPort; existing catalog-derived recipes refresh at the next app update. Until then, enable "Install as is" for an app whose APK needs no fixes |
| A launch test suggests a patch (for example triage `sdl-no-clipboard` → `frame.sdl_clipboard`) but the game page doesn't show it / lists it as not applicable; or a game added long ago lacks warnings newer ones get (Android version, web wrapper, missing OBB) (GitHub #104, Dramatic Shape) | the library entry was analyzed by an older FramePort: the analysis field the patch reads (`sdl_java`, `min_sdk`, `web_wrapper`, `expects_obb`, …) is missing, so `applies()` is false | entries whose `analysis.extra.analysis_version` is below `detect.ANALYSIS_VERSION` are analyzed again at the GUI's start (background) and before a build (`pipeline.refresh_analyses`); by hand: game menu → "Analyze again", then Update on Frame. An unreadable APK is marked `analysis_failed` (retried after the next version bump). Adding an analysis field: bump `ANALYSIS_VERSION` |
| FDM layer loads but diagnostic logging reports `fdm_injection_CreateInstance: skipped` | Layer loading alone does not enable injection on the tested SteamOS 0.4.5 build; a skipped helper instance alone also does not establish game-wide inactivity | Opt-in `device.foveation` mode `tracked`: loads Valve's layer and sets `FDM_DEBUG=enable`. Batman probe reached foveated render passes with changing per-eye offsets. Validate graphics and matched gameplay GPU time; do not enable globally. Use `debug` only for brief diagnostics, preserving existing flags; never benchmark with debug spam. See FRAME_RUNTIME.md. |
| One eye (usually the right) shimmers or jitters in the 3D view while menus and panels look steady; pacing is clean (GitHub #69, e.g. RTCWQuest, Moss) | Valve's eye-tracked foveation layer (`VALVE_fdm_injection`) moves its low-density regions with each eye's gaze, and the right eye's gaze filter restarts often (`CEyePoseUKF R: Large dt … Bootstrapped` in `~/.local/share/Steam/logs/eyetracking.txt`) | Customize → "Eye-tracked foveation (Valve)" (`device.foveation`): **Fixed** (`FDM_DEBUG=disable_offsets`; verify injection remains active on the installed runtime) or **Off** (`VK_INSTANCE_LAYERS=""`); catalog field `foveation:` |
@@ -39,7 +44,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Launch test EXITED, launch.log has `Target app (<pkg>) did not start` and no `Start proc …:<pkg>` (GitHub #179) | Lepton occasionally doesn't start the app on the first start after an APK change; Android's `WebViewLoader-: System.exit` at boot is not the game (triage `app-not-started`) | start it again |
| A game keeps no progress between starts although it saves (its storage gets a new `Profiles/Oculus/<id>/`-style folder per start; Walkabout Mini Golf, GitHub #182) | OVRPort's platform loader answers `ovr_GetLoggedInUserID` with a made-up id (6e18 + `random()`), new at every start; games file saves under the user id | `frame.ovr_stable_user` (default for games with the platform library): a fixed id per game and keystore. Old progress: copy the newest old profile into the new id's folder |
| A Unity app's text field shows a caret for a moment and loses focus; no keyboard appears (for example Stremio VR login) | Unity's TMP_InputField/InputField wait for Android's on-screen keyboard and close themselves without one (Lepton has none); headless VR apps also have no focused Android window, so no key press reaches them | `frame.unity_text_input` (Cpp2IL finds `TouchScreenKeyboardShouldBeUsed`/`isKeyboardUsingEvents`, rewritten to false/true) + `device.text_input_window` (lepton-show-flatscreen: Steam's keyboard and Type on Frame work) |
| Play gives Steam's "Game configuration unavailable"; Steam's console_log: `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` | this Frame's Steam never loaded FramePort's shortcuts.vdf entry (it treats the id as a store app); cause unknown (GitHub #21/#30) | automatic since agent v43: Play registers the game through Steam's devkit interface ("Devkit Game: …", `devkit_register`) and starts that; uninstall removes it. Agent v58: a devkit entry Steam forgot after a restart is added again live on Play (no NOT_IN_LIBRARY → no Steam restart), and installs/art updates of devkit games only copy art (no Steam restart, GitHub #41) |
| FramePort's games disappear after Steam restarts, especially after a devkit registration (GitHub #263) | Steam saves `Shortcuts` / `AppName` with capital letters; older FramePort reads them case-sensitively and writes a second shortcut map that Steam ignores | agent v86 reads and updates keys case-insensitively, keeping Steam's spelling, appids and tags; update FramePort and add the missing games to the library again |
| Play gives Steam's "Game configuration unavailable"; Steam's console_log: `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` | this Frame's Steam never loaded FramePort's shortcuts.vdf entry (it treats the id as a store app); see the casing fix above, other causes remain unconfirmed (GitHub #21/#30) | automatic since agent v43: Play registers the game through Steam's devkit interface ("Devkit Game: …", `devkit_register`) and starts that; uninstall removes it. Agent v58: a devkit entry Steam forgot after a restart is added again live on Play (no NOT_IN_LIBRARY → no Steam restart), and installs/art updates of devkit games only copy art (no Steam restart, GitHub #41) |
| Steam's Exit game leaves the game running (container `lepton-steamlaunch-<appid>` still up) | Steam stopped only its `reaper`; launch.sh and Lepton (setsid) never got a signal (GitHub #36) | agent v44: launch.sh's 2 s loop ends the game when its parent is gone (`upgrade_launchers` adds it to existing launchers); by hand: `podman kill lepton-steamlaunch-<appid>` |
| Black screen at start while audio/ExoPlayer runs; log `xrEndFrame: dropped N unusable layer(s)`, a quad layer with swapchain 0x0 (for example I Am Monkey's intro video) | The game plays video into an Android-surface swapchain (XR_KHR_android_surface_swapchain); the Frame's runtime lists the extension but returns FUNCTION_UNSUPPORTED | adapter `surface_emul` (default on): FrameBridge gives the player a SurfaceTexture-backed Surface and copies each frame into an ordinary swapchain (log `surface_emul: …`) |
| The game quits right after start: `Something failed to initialize. Quitting!` / `We don't have write permission to …/files/cloud/data` (for example SUPERHOT); or stays at loading with `UnauthorizedAccessException` in `StartupLoading.Start` (GitHub #239, Jurassic World Aftermath) | The game created a cloud-save folder with mode 1700; inside Lepton the app writes through the folder's group. Its startup routine can fail before the permission watchdog repairs the newly created folder | launch.sh `fix_perms` adds owner + group write to every folder in the game's storage (agent 41). Close and start the game again without uninstalling: the folders are now repaired (Aftermath confirmed in the headset). Agent 84 creates `files/cloud/data` before `fix_perms` and startup, including in upgraded launchers, to prevent the first-start race. Triage `startup-save-permission` fails the launch test despite rendering frames |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 217 KiB

+1
View File
@@ -11,6 +11,7 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
| Dir | Artifact | What / why |
|---|---|---|
| `luajit/` | `arm64-v8a/libfp_luajit.so` | Pinned upstream LuaJIT with full-width lightuserdata (`frame.luajit_pointers`). Only the verified old Lua engine's shared public functions become imports; the original xLua API 105 bindings, LuaSocket helpers and `luaopen_perflib` remain in the game. No game library is bundled. Build: `python native/build.py --only luajit`; source revision + archive SHA-256 in `luajit/build.py`, MIT notices in `luajit/COPYRIGHT` and `artifacts/licenses/LuaJIT.txt`. `runtime.c` checks GC64 and the retained `xlua_gl` global-reference offset. `smoke.c` compares a real low-address allocation with the original library's own native static pointer; the unmodified library fails only the high-address case, the patched one round-trips both exactly. Startup verified on Frame; gameplay unverified. |
| `adapter/` | `{arm64-v8a,armeabi-v7a}/libopenxr_loader_generic.so` | **FrameBridge**: replaces overport's generic loader (which is renamed `libopenxr_loader_original.so`) and fixes Steam Frame runtime gaps. Hooks xrCreateInstance, swapchains, xrEndFrame, spaces, xrPollEvent, … everything else is forwarded by `gen_forwarders.py`-generated tail calls. `scene_emu.c` = Meta scene/spatial-entity emulation; `flip_vk.c` = Vulkan blit for VERTICAL_FLIP quads. `session_fixes.c` = per-game session fixes, all off by default and hooked only when on (`layer_debug` diagnostics, `stable_local`, `focus_hold`, aim correction `aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `proximity_emul` = finger proximity (XR_FB_touch_controller_proximity, which the Frame lacks) from the capacitive touch inputs: OVRPlugin's `hand_thumb_proximity`/`hand_trigger_proximity` actions get extra Touch bindings); `input_diag.c` = controller-input diagnostics (setting `input_diag`, off by default and hooked only when on; logs suggested interaction profiles and the runtime's answers, each hand's current profile, functions the runtime lacks and failing input/haptics/perf calls, each line once; tested on the host by `tests/test_input_diag.py`); `layer_emul_gl.c` = `equirect_emul` (GLES sessions only): 360° equirect/equirect2 layers become one adapter projection layer, drawn by a worker thread with its own EGL context shared with the app's: each 360° image is converted to a cube map only when it changes, and the view is redrawn only after a 4° head turn (GL/EGL resolved with dlopen, so Vulkan games never load them); `layer_math.h` = their pose/face math, tested on the host by `tests/test_layer_emul.py`. `render_model.c` = XR_FB_render_model emulation (setting `controller_models`, off by default): serves `files/framebridge/controller_{left,right}.glb`, which the agent converts on the Frame from SteamVR's own Frame controller render models (never shipped by FramePort); tested on the host by `tests/test_render_model.py`. Settings: `libframe_settings.so` in the APK + `settings.conf`/`framebridge.conf` on the Frame. |
| `vrapi-bridge/` | `arm64-v8a/libvrapi.so` | VrApi → OpenXR bridge from [Android-XR-Bridge/OVRPort](https://github.com/Android-XR-Bridge/OVRPort) `native/vrapi` at 5e7df52 (GPL-3.0, `LICENSE.upstream`; unchanged upstream as of OVRPort v1.2.5, which ships the unpatched code as its opt-in `patch_vrapi_openxr` in experimental CLI builds), with our changes in `upstream-patches/`: GLES sessions + GL texture swapchains, cylinder→quad layers, GL vertical flip, UNORM↔sRGB format twins, 30 s VR-mode deadline, VALID-only recenter, loading-icon layers skipped, `vrapi_PollEvent`/`RecenterPose`/`SetDisplayRefreshRate`/`GetSystemPropertyFloatArray` (BlazeRush imports them, GitHub #57), legacy compositor debug/volume frame flags accepted, full-rate Vulkan foveation images allocated as density attachments (The Climb, GitHub #238), diagnostics behind `-DOVP_GL_DIAG`. `upstream-patches/` reproduces these sources from the fork's `native/vrapi` (checked 2026-10-06 against its latest commit); upstream PRs Android-XR-Bridge/OVRPort#3-#8, tracked in GitHub #75. |
| `platformcompat/` | `arm64-v8a/libovrplatformcompat.so` | Real `ovrMessageType_ToString` (same fork, `native/platform`). |
+8 -2
View File
@@ -380,6 +380,11 @@ def build_tlskeys(tc: Path):
"-Wl,-z,max-page-size=16384", "tlskeys.c", "-llog", "-o", ART / "arm64-v8a/libfp_tkeys.so"], cwd=src)
def build_luajit(tc: Path):
run([sys.executable, HERE / "luajit/build.py", "--clang-dir", tc / "bin",
"--out", ART / "arm64-v8a/libfp_luajit.so"])
def build_dex():
javac = shutil.which("javac")
if not javac:
@@ -424,7 +429,7 @@ def write_sums():
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vrsettings,venc")
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc")
ap.add_argument("--ndk")
args = ap.parse_args()
parts = set(args.only.split(","))
@@ -438,9 +443,10 @@ def main():
"zinkfix": lambda: build_zinkfix(tc),
"foveation": lambda: build_foveation(tc),
"ovrpshim": lambda: build_ovrpshim(tc), "tlskeys": lambda: build_tlskeys(tc),
"luajit": lambda: build_luajit(tc),
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc),
"vroverlay_probe": lambda: build_vroverlay_probe(tc)}
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
"vroverlay_probe"):
if name in parts:
log(f"build {name}")
+56
View File
@@ -0,0 +1,56 @@
===============================================================================
LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
Copyright (C) 2005-2026 Mike Pall. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
===============================================================================
[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
Copyright (C) 1994-2012 Lua.org, PUC-Rio.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================================================
[ LuaJIT includes code from dlmalloc, which has this license statement: ]
This is a version (aka dlmalloc) of malloc/free/realloc written by
Doug Lea and released to the public domain, as explained at
https://creativecommons.org/licenses/publicdomain
===============================================================================
+63
View File
@@ -0,0 +1,63 @@
"""Build the pinned LuaJIT engine; the game's xLua bindings are retained, never bundled here."""
from __future__ import annotations
import argparse
import hashlib
import shlex
import shutil
import subprocess
import tarfile
import urllib.request
from pathlib import Path
REVISION = "634daf2453559fd5e0ec282f3b7aedaa09e9f344"
ARCHIVE_SHA256 = "a66bc46efe5417c949fe08e224fd5362a5d395c7d96ff5acbd428580fc6b3448"
HERE = Path(__file__).resolve().parent
CACHE = HERE.parent / ".cache" / "luajit"
def sources() -> Path:
CACHE.mkdir(parents=True, exist_ok=True)
archive = CACHE / f"{REVISION}.tar.gz"
if not archive.exists():
url = f"https://codeload.github.com/LuaJIT/LuaJIT/tar.gz/{REVISION}"
with urllib.request.urlopen(url) as response:
archive.write_bytes(response.read())
if hashlib.sha256(archive.read_bytes()).hexdigest() != ARCHIVE_SHA256:
raise RuntimeError("LuaJIT source archive checksum mismatch")
root = CACHE / f"LuaJIT-{REVISION}"
if not root.exists():
with tarfile.open(archive) as tar:
tar.extractall(CACHE, filter="data")
return root / "src"
def build(tc: Path, output: Path) -> None:
src = sources()
clang = tc / "aarch64-linux-android21-clang"
flags = "-fPIC -DLUAJIT_UNWIND_INTERNAL " + shlex.quote(f"-ffile-prefix-map={src}=LuaJIT/src")
# Quotes are part of Make's variable value: the toolchain can live in a path with spaces.
settings = ["HOST_CC=cc", f"TARGET_CC={shlex.quote(str(clang))}",
f"TARGET_LD={shlex.quote(str(clang))}",
f"TARGET_AR={shlex.quote(str(tc / 'llvm-ar'))} rcus",
f"TARGET_STRIP={shlex.quote(str(tc / 'llvm-strip'))}",
"TARGET_SYS=Linux", f"TARGET_FLAGS={flags}", "BUILDMODE=static"]
subprocess.run(["make", "-C", str(src), "clean", *settings], check=True)
subprocess.run(["make", "-C", str(src), "-j4", "libluajit.a", *settings], check=True)
output.parent.mkdir(parents=True, exist_ok=True)
subprocess.run([str(clang), "-shared", "-fPIC", "-O2",
"-DLUAJIT_UNWIND_INTERNAL", "-I", str(src), "runtime.c", "-Wl,--whole-archive",
str(src / "libluajit.a"), "-Wl,--no-whole-archive", "-lm", "-ldl", "-llog",
"-Wl,-soname,libfp_luajit.so", "-Wl,-z,max-page-size=16384", "-o", str(output)],
cwd=HERE, check=True)
notices = output.parent.parent / "licenses"
notices.mkdir(exist_ok=True)
shutil.copyfile(HERE / "COPYRIGHT", notices / "LuaJIT.txt")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--clang-dir", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
build(args.clang_dir.resolve(), args.out.resolve())
+12
View File
@@ -0,0 +1,12 @@
/* FramePort's startup marker and the layout contract used by retained xLua bindings. */
#include <stddef.h>
#include <android/log.h>
#include "lj_obj.h"
_Static_assert(LJ_GC64, "Retained xLua bindings require GC64");
_Static_assert(offsetof(lua_State, glref) == 16, "xLua's xlua_gl must keep its global-state reference");
__attribute__((constructor)) static void ready(void)
{
__android_log_print(ANDROID_LOG_INFO, "FrameBridge", "lua: full-width lightuserdata runtime enabled");
}
+38
View File
@@ -0,0 +1,38 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include "lua.h"
#include "lauxlib.h"
static const char *(*tolstring)(lua_State *,int,size_t *);
static int panic(lua_State *L) {
fprintf(stderr,"PANIC: %s\n",tolstring(L,-1,NULL)); fflush(stderr); return 0;
}
#define RESOLVE(name) __typeof__(&name) f_##name=(__typeof__(&name))dlsym(h,#name); if(!f_##name){fprintf(stderr,"missing %s: %s\n",#name,dlerror());return 4;}
int main(int argc,char **argv) {
if(argc!=3)return 2;
void*h=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL); if(!h){fprintf(stderr,"dlopen: %s\n",dlerror());return 3;}
RESOLVE(luaL_newstate); RESOLVE(lua_pushlightuserdata); RESOLVE(lua_touserdata); RESOLVE(lua_atpanic); RESOLVE(lua_close);
tolstring=(void*)dlsym(h,"lua_tolstring");
void*(*tag)(void)=dlsym(h,"xlua_tag");if(!tag)return 4;
lua_State*L=f_luaL_newstate();if(!L)return 5;f_lua_atpanic(L,panic);
void*p=tag();
if(argv[2][0]=='l') {p=mmap((void*)0x10000000,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);if(p==MAP_FAILED)return 6;}
printf("native pointer=%p, above47=%u, LuaState=%p\n",p,(unsigned)((uintptr_t)p>>47!=0),L);fflush(stdout);
f_lua_pushlightuserdata(L,p);
void*back=f_lua_touserdata(L,-1);
printf("roundtrip=%p exact=%u\n",back,back==p);fflush(stdout);
if(argv[2][0]=='h' && argv[2][1]=='e') {
RESOLVE(lua_getfield); RESOLVE(lua_type); RESOLVE(lua_settop);
int(*open_perf)(lua_State*)=dlsym(h,"luaopen_perflib");
void*(*global)(lua_State*)=dlsym(h,"xlua_gl");
if(!open_perf || !global)return 8;
if(global(L)!=*(void**)((char*)L+16))return 9;
open_perf(L);f_lua_getfield(L,LUA_GLOBALSINDEX,"perf");
if(f_lua_type(L,-1)!=LUA_TTABLE)return 10;
printf("retained xLua global reference and perf helper: PASS\n");fflush(stdout);
f_lua_settop(L,0);
}
f_lua_close(L);return back==p?0:7;
}
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="96" viewBox="0 0 72 96"><defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2D7FF9"/><stop offset=".55" stop-color="#7A4BD8"/><stop offset="1" stop-color="#FF8A3D"/></linearGradient><radialGradient id="sun" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#FFE48A"/><stop offset="1" stop-color="#FFB42E"/></radialGradient></defs><rect width="72" height="96" fill="url(#bg)"/><path d="M0 74 18 60 30 68 46 54 72 72V96H0z" fill="#101218" fill-opacity=".35"/><circle cx="36" cy="40" r="20" fill="url(#sun)" stroke="#101218" stroke-width="2.4"/><path d="M17.5 35.5h37M20 35.5h13l-1.4 6.2a3.4 3.4 0 0 1-3.3 2.6h-4.6A3.4 3.4 0 0 1 20.4 42zM39 35.5h13l-.4 6.5a3.4 3.4 0 0 1-3.3 2.3h-4.6a3.4 3.4 0 0 1-3.3-2.6z" fill="#101218" stroke="#101218" stroke-width="2" stroke-linejoin="round"/><path d="M23.5 37.5h4" stroke="#FFFFFF" stroke-opacity=".7" stroke-width="1.4" stroke-linecap="round"/><path d="M29 50.5c4 3.2 10 3.2 14 0" fill="none" stroke="#101218" stroke-width="2.4" stroke-linecap="round"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="96" viewBox="0 0 72 96"><defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2D7FF9"/><stop offset=".55" stop-color="#7A4BD8"/><stop offset="1" stop-color="#FF8A3D"/></linearGradient><radialGradient id="sun" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#FFE48A"/><stop offset="1" stop-color="#FFB42E"/></radialGradient></defs><rect width="72" height="96" fill="url(#bg)"/><path d="M0 74 18 60 30 68 46 54 72 72V96H0z" fill="#101218" fill-opacity=".35"/><circle cx="36" cy="40" r="20" fill="url(#sun)" stroke="#101218" stroke-width="2.4"/><path d="M17.5 35.5h37M20 35.5h13l-1.4 6.2q-.6 2.6-3.3 2.6h-4.6q-2.9 0-3.3-2.6zM39 35.5h13l-.4 6.2q-.4 2.6-3.3 2.6h-4.6q-2.7 0-3.3-2.6z" fill="#101218" stroke="#101218" stroke-width="2" stroke-linejoin="round"/><path d="M23.5 37.5h4" stroke="#FFFFFF" stroke-opacity=".7" stroke-width="1.4" stroke-linecap="round"/><path d="M29 50.5c4 3.2 10 3.2 14 0" fill="none" stroke="#101218" stroke-width="2.4" stroke-linecap="round"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+130 -40
View File
@@ -1,14 +1,14 @@
{
"updated": "2026-10-12",
"updated": "2026-10-13",
"people": [
{
"login": "spoopyghosty0",
"avatar": "https://avatars.githubusercontent.com/u/336754034?v=4",
"url": "https://github.com/spoopyghosty0",
"commits": 542,
"commits": 578,
"issues": 5,
"prs": 3,
"total": 572
"total": 608
},
{
"login": "Klownicle",
@@ -46,6 +46,24 @@
"prs": 0,
"total": 33
},
{
"login": "Lucas-Mathieu",
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
"url": "https://github.com/Lucas-Mathieu",
"commits": 9,
"issues": 0,
"prs": 4,
"total": 29
},
{
"login": "UnicronNL",
"avatar": "https://avatars.githubusercontent.com/u/1267745?v=4",
"url": "https://github.com/UnicronNL",
"commits": 5,
"issues": 6,
"prs": 1,
"total": 28
},
{
"login": "inseend",
"avatar": "https://avatars.githubusercontent.com/u/11666993?v=4",
@@ -65,31 +83,22 @@
"total": 24
},
{
"login": "UnicronNL",
"avatar": "https://avatars.githubusercontent.com/u/1267745?v=4",
"url": "https://github.com/UnicronNL",
"commits": 5,
"issues": 6,
"prs": 0,
"total": 23
},
{
"login": "Lucas-Mathieu",
"avatar": "https://avatars.githubusercontent.com/u/184943223?v=4",
"url": "https://github.com/Lucas-Mathieu",
"commits": 7,
"issues": 0,
"login": "nleach7",
"avatar": "https://avatars.githubusercontent.com/u/1406346?v=4",
"url": "https://github.com/nleach7",
"commits": 3,
"issues": 2,
"prs": 3,
"total": 22
"total": 24
},
{
"login": "jim2011",
"avatar": "https://avatars.githubusercontent.com/u/15848374?v=4",
"url": "https://github.com/jim2011",
"commits": 0,
"issues": 6,
"issues": 7,
"prs": 0,
"total": 18
"total": 21
},
{
"login": "Jmckenzie84",
@@ -109,6 +118,15 @@
"prs": 0,
"total": 15
},
{
"login": "eepykith",
"avatar": "https://avatars.githubusercontent.com/u/338898488?v=4",
"url": "https://github.com/eepykith",
"commits": 0,
"issues": 5,
"prs": 0,
"total": 15
},
{
"login": "Scoota23",
"avatar": "https://avatars.githubusercontent.com/u/27732436?v=4",
@@ -119,13 +137,13 @@
"total": 15
},
{
"login": "eepykith",
"avatar": "https://avatars.githubusercontent.com/u/338898488?v=4",
"url": "https://github.com/eepykith",
"login": "ssgod101",
"avatar": "https://avatars.githubusercontent.com/u/49173466?v=4",
"url": "https://github.com/ssgod101",
"commits": 0,
"issues": 4,
"issues": 5,
"prs": 0,
"total": 12
"total": 15
},
{
"login": "nickwilde77555",
@@ -208,6 +226,15 @@
"prs": 0,
"total": 9
},
{
"login": "greggray24",
"avatar": "https://avatars.githubusercontent.com/u/255593097?v=4",
"url": "https://github.com/greggray24",
"commits": 0,
"issues": 3,
"prs": 0,
"total": 9
},
{
"login": "m4ddh7jm7c-bit",
"avatar": "https://avatars.githubusercontent.com/u/338352097?v=4",
@@ -262,6 +289,15 @@
"prs": 1,
"total": 6
},
{
"login": "Loafmeister",
"avatar": "https://avatars.githubusercontent.com/u/60628625?v=4",
"url": "https://github.com/Loafmeister",
"commits": 0,
"issues": 2,
"prs": 0,
"total": 6
},
{
"login": "maxfvd",
"avatar": "https://avatars.githubusercontent.com/u/337777776?v=4",
@@ -316,6 +352,24 @@
"prs": 1,
"total": 6
},
{
"login": "alicecatpaw",
"avatar": "https://avatars.githubusercontent.com/u/7423859?v=4",
"url": "https://github.com/alicecatpaw",
"commits": 0,
"issues": 0,
"prs": 1,
"total": 5
},
{
"login": "Akimya49",
"avatar": "https://avatars.githubusercontent.com/u/46827872?v=4",
"url": "https://github.com/Akimya49",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "AlexanderCowell",
"avatar": "https://avatars.githubusercontent.com/u/225416878?v=4",
@@ -334,6 +388,15 @@
"prs": 0,
"total": 3
},
{
"login": "astromaddie",
"avatar": "https://avatars.githubusercontent.com/u/5488456?v=4",
"url": "https://github.com/astromaddie",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "bcain2005-gif",
"avatar": "https://avatars.githubusercontent.com/u/258048498?v=4",
@@ -343,6 +406,24 @@
"prs": 0,
"total": 3
},
{
"login": "chaseflow",
"avatar": "https://avatars.githubusercontent.com/u/235484785?v=4",
"url": "https://github.com/chaseflow",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "coah80",
"avatar": "https://avatars.githubusercontent.com/u/100182500?v=4",
"url": "https://github.com/coah80",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "darrenltw",
"avatar": "https://avatars.githubusercontent.com/u/151712415?v=4",
@@ -452,18 +533,18 @@
"total": 3
},
{
"login": "leoloopfiets",
"avatar": "https://avatars.githubusercontent.com/u/215600281?v=4",
"url": "https://github.com/leoloopfiets",
"login": "JMQuehl",
"avatar": "https://avatars.githubusercontent.com/u/110048375?v=4",
"url": "https://github.com/JMQuehl",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "Loafmeister",
"avatar": "https://avatars.githubusercontent.com/u/60628625?v=4",
"url": "https://github.com/Loafmeister",
"login": "leoloopfiets",
"avatar": "https://avatars.githubusercontent.com/u/215600281?v=4",
"url": "https://github.com/leoloopfiets",
"commits": 0,
"issues": 1,
"prs": 0,
@@ -523,15 +604,6 @@
"prs": 0,
"total": 3
},
{
"login": "ssgod101",
"avatar": "https://avatars.githubusercontent.com/u/49173466?v=4",
"url": "https://github.com/ssgod101",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "TainakaRitsu",
"avatar": "https://avatars.githubusercontent.com/u/12560774?v=4",
@@ -550,6 +622,15 @@
"prs": 0,
"total": 3
},
{
"login": "tirehabitat",
"avatar": "https://avatars.githubusercontent.com/u/97759168?v=4",
"url": "https://github.com/tirehabitat",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "TylerJaacks",
"avatar": "https://avatars.githubusercontent.com/u/1505374?v=4",
@@ -576,6 +657,15 @@
"issues": 1,
"prs": 0,
"total": 3
},
{
"login": "xenon2",
"avatar": "https://avatars.githubusercontent.com/u/4399658?v=4",
"url": "https://github.com/xenon2",
"commits": 0,
"issues": 1,
"prs": 0,
"total": 3
}
]
}
+5
View File
@@ -434,6 +434,11 @@
"title": "GL wrapper: OpenGL ES depth formats (Minecraft)",
"group": "Steam Frame patches"
},
"frame.luajit_pointers": {
"anchor": "frame-luajit_pointers",
"title": "Fix Lua startup crashes on Frame",
"group": "Steam Frame patches"
},
"frame.meta_permissions": {
"anchor": "frame-meta_permissions",
"title": "Declare Meta-only permissions",
+9 -1
View File
@@ -1,6 +1,14 @@
{
"updated": "2026-10-12",
"updated": "2026-10-13",
"releases": [
{
"tag": "v1.1.0",
"name": "FramePort v1.1.0",
"date": "2026-10-13",
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/v1.1.0",
"dev": false,
"notes": "FramePort v1.1.0: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n- **Games on an SD card whose name has a space start again** (they got no storage before).\n- **Experimental eye-tracked foveation** per game, through Valve's foveation layer (thanks Lucas-Mathieu, #247).\n- **Game menu: \"Update with a newer APK…\" and \"Update app only\"** (keeps modded game data on the Frame).\n- **Game fixes:** Wander 0.3570, Vermillion, Star Wars: Tales from the Galaxy's Edge, The Climb, Jurassic World Aftermath backups, Yupitergrad, Beyond Sandbox, and an optional fade fix for Batman's loading screens (thanks nleach7 and Klownicle).\n- **Controllers switched on after a 2D app started** now reach it as a gamepad.\n- **Fixes:** a changed Frame SSH key can be trusted with one click, uninstalling no longer stops on its own lock file, file pickers work without a desktop portal on Linux, separate desktop-icon switch for Linux apps, and the low-space warning checks the right drive.\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n## Install\n\nDownload the file for your PC, unpack it and start FramePort. Guide:\n[Install and first steps](https://frameport.app/docs/install/).\n"
},
{
"tag": "v1.0.4",
"name": "FramePort v1.0.4",
+5 -1
View File
@@ -10,6 +10,7 @@ from pathlib import Path
from ..apk import axml
from ..core.models import Analysis
from . import elf
from .luajit import xlua_47bit, xlua_sha256
from .unity_split import split_build as unity_split_build
logging.getLogger("pyaxmlparser").setLevel(logging.ERROR)
@@ -37,7 +38,8 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
# libAndroidLauncher.so + a .cryproject (Climb 1 has no libCrySystem.so, GitHub #238) (2026-10)
# 13: validate monolithic CryEngine projects/config, OBB filename formats and foveation cvar (The Climb, GitHub #238)
# 14: bhaptics_permission (bHaptics asks for Android 12's BLUETOOTH_CONNECT: frame.bhaptics_permission, GitHub #235)
ANALYSIS_VERSION = 14
# 15: verified xLua/LuaJIT 47-bit lightuserdata engine (frame.luajit_pointers, GitHub #257)
ANALYSIS_VERSION = 15
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
@@ -302,6 +304,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
debuggable=bool(axml.Axml(manifest).get_bool("application", "debuggable")),
extra={
"analysis_version": ANALYSIS_VERSION,
"xlua_47bit": xlua_47bit(lib_bytes.get("libxlua.so", b"")),
"xlua_sha256": xlua_sha256(lib_bytes.get("libxlua.so", b"")),
"frame_patched": "libframe_settings.so" in libset, # already has FramePort's FrameBridge adapter
# Team Beef's TBXR ports pick their OpenXR path by headset maker (frame.tbxr_vendor): their libraries
# the game asks Meta's platform for asset files (content shipped as separate files, frame.asset_files)
+25
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import io
import struct
from functools import lru_cache
from elftools.elf.dynamic import DynamicSection
@@ -161,6 +162,30 @@ def hide_exports(data: bytes, names) -> tuple[bytes, list[str]]:
return bytes(buf), sorted(hidden)
def redirect_exports(data: bytes, names) -> tuple[bytes, list[str]]:
"""Turn definitions into imports, so both dlsym and this ELF's relocations use a dependency's implementation.
Unlike hide_exports, this also redirects self-references through the PLT. Code and symbol indices stay put;
callers using direct branch instructions are NOT redirected and must be reviewed before using this operation.
ELF64, little endian only. Returns (data, definitions actually redirected).
"""
if not is_elf(data) or not is_64bit(data) or data[5] != 1:
return data, []
sec = _elf(data).get_section_by_name(".dynsym")
if not isinstance(sec, SymbolTableSection):
return data, []
wanted = set(names)
buf, redirected = bytearray(data), []
for i, sym in enumerate(sec.iter_symbols()):
if (sym.name in wanted and sym["st_shndx"] != "SHN_UNDEF"
and sym["st_info"]["bind"] in ("STB_GLOBAL", "STB_WEAK")
and sym["st_info"]["type"] == "STT_FUNC"):
at = sec["sh_offset"] + i * (sec["sh_entsize"] or 24)
struct.pack_into("<HQQ", buf, at + 6, 0, 0, 0) # shndx, value, size; keep GLOBAL and the name
redirected.append(sym.name)
return bytes(buf), sorted(redirected)
def rename_imports(data: bytes, renames: dict[str, str]) -> tuple[bytes, list[str]]:
"""Rename imported (undefined) dynamic symbols in place: `.dynstr` bytes of each import's name are overwritten
with the new name (not longer; NUL-padded), so its relocations bind to whatever library exports the new name
+20
View File
@@ -0,0 +1,20 @@
"""Verified xLua/LuaJIT engines whose lightuserdata is limited to 47-bit pointers."""
from __future__ import annotations
import hashlib
# xLua API 105, LuaJIT 2.1 beta3, GC64. The bindings use the public Lua API;
# xlua_gl reads lua_State.glref at offset 16, unchanged in the replacement.
# Match the complete original library: custom builds need their own ABI review.
XLUA_47BIT_SHA256 = frozenset({
"6d5860e4a4f1cc2b05c600c63297ed467369739666edca79413fff4374ff2684",
})
def xlua_sha256(data: bytes) -> str | None:
"""Diagnostics can identify an unreviewed engine without sharing the library itself."""
return hashlib.sha256(data).hexdigest() if data else None
def xlua_47bit(data: bytes) -> bool:
return xlua_sha256(data) in XLUA_47BIT_SHA256
+49
View File
@@ -202,6 +202,55 @@ class Axml:
# ---------------------------------------------------------------------- high-level queries/fixes
def copy_activity_label_to_application(manifest: bytes) -> bytes | None:
"""Fill a missing application label from an activity, retaining its resource/string value (#269).
OVRPort's appName() assumes the application has a label; Android permits activity-only labels. Existing
application labels and the activity itself are untouched. No strings or resource ids are added.
"""
x = Axml(manifest)
names = x.strings()
def label(el):
return next((a for a in el.attrs if a.ns < len(names) and a.name < len(names) and names[a.ns] == ANDROID_NS
and names[a.name] == "label"), None)
elements = list(x.elements())
app = next((el for el in elements if el.name == "application"), None)
if app is None or label(app) is not None:
return None
source = next((a for el in elements if el.name in ("activity", "activity-alias")
and (a := label(el)) is not None), None)
if source is None:
return None
# Attributes are sorted by resource id in Android's binary XML. Reuse the existing resource map.
ids = {}
pos = x.pool_off + x.pool_size
while pos < app.offset:
typ, hsz, size = struct.unpack_from("<HHI", x.data, pos)
if size < hsz or size == 0:
raise ValueError("invalid binary XML chunk")
if typ == 0x0180: # RES_XML_RESOURCE_MAP
ids = dict(enumerate(struct.unpack_from(f"<{(size - hsz) // 4}I", x.data, pos + hsz)))
break
pos += size
ext = app.offset + struct.unpack_from("<H", x.data, app.offset + 2)[0]
start, stride, count = struct.unpack_from("<HHH", x.data, ext + 8)
index = next((i for i, a in enumerate(app.attrs)
if ids.get(a.name, 0xFFFFFFFF) > ids.get(source.name, 0xFFFFFFFF)), count)
at = ext + start + index * stride
record = x.data[source.offset:source.offset + 20] + bytes(stride - 20)
data = x.data[:at] + record + x.data[at:]
struct.pack_into("<I", data, app.offset + 4, app.size + stride)
struct.pack_into("<H", data, ext + 12, count + 1)
for offset in (14, 16, 18): # one-based id/class/style attribute indices
value = struct.unpack_from("<H", data, ext + offset)[0]
if value > index:
struct.pack_into("<H", data, ext + offset, value + 1)
struct.pack_into("<I", data, 4, len(data))
return bytes(data)
def used_and_declared_permissions(manifest: bytes) -> tuple[list[str], set[str]]:
x = Axml(manifest)
used, declared = [], set()
+51 -5
View File
@@ -9,12 +9,13 @@ play sessions (the Library flickered under a stream of pop-ups). The GUI holds a
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
from .paths import user_data_dir
from .paths import user_data_dir, write_atomic
_held = None # the open lock file (kept for the process lifetime)
@@ -27,6 +28,10 @@ def focus_file() -> Path:
return user_data_dir() / "gui.focus"
def _owner_file(path: Path) -> Path:
return path.with_suffix(".pid")
def _try_lock(f) -> bool:
try:
if sys.platform == "win32":
@@ -57,6 +62,10 @@ def acquire(path: Path | None = None) -> bool:
f.write(str(os.getpid()))
f.flush()
_held = f
import psutil
# Windows denies reading the locked byte, so another launch cannot read the PID from gui.lock itself.
write_atomic(_owner_file(path), json.dumps({"pid": os.getpid(), "created": psutil.Process().create_time()}))
return True
@@ -67,6 +76,11 @@ def release() -> None:
f, _held = _held, None
if f is None:
return
path = Path(f.name)
try:
_owner_file(path).unlink(missing_ok=True)
except OSError:
pass
try:
if sys.platform == "win32":
import msvcrt
@@ -75,7 +89,6 @@ def release() -> None:
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
except OSError:
pass
path = Path(f.name)
f.close()
try:
path.unlink(missing_ok=True)
@@ -84,8 +97,18 @@ def release() -> None:
def holder(path: Path | None = None) -> int | None:
path = path or lock_file()
try:
return int((path or lock_file()).read_text().strip() or 0) or None
import psutil
owner = json.loads(_owner_file(path).read_text())
pid = int(owner["pid"])
if abs(psutil.Process(pid).create_time() - float(owner["created"])) < 0.01:
return pid # creation time guards against stale metadata and recycled PIDs
except (OSError, ValueError, KeyError, TypeError, psutil.Error):
pass
try:
return int(path.read_text().strip() or 0) or None
except (OSError, ValueError):
return None
@@ -94,10 +117,30 @@ def _is_frameport_gui(pid: int) -> bool:
try:
import psutil
cmd = " ".join(psutil.Process(pid).cmdline()).lower()
cmd = psutil.Process(pid).cmdline()
except Exception: # noqa: BLE001 (gone, or not ours to inspect)
return False
return "frameport" in cmd
if any(a == "-m" and b == "frameport.ui.app" for a, b in zip(cmd, cmd[1:], strict=False)):
return True
return any(Path(arg).name.lower() in ("frameport", "frameport.exe", "frameport-gui", "frameport-gui.exe")
for arg in cmd[:2])
def _legacy_holders(path: Path) -> list[int]:
"""Older Windows GUIs have no readable PID record. Only recover processes holding this exact lock."""
import psutil
found = []
wanted = os.path.normcase(str(path.resolve()))
for process in psutil.process_iter(["pid"]):
if process.pid == os.getpid() or not _is_frameport_gui(process.pid):
continue
try:
if any(os.path.normcase(f.path) == wanted for f in process.open_files()):
found.append(process.pid)
except (OSError, psutil.Error):
pass
return found
STARTUP_GRACE = 30.0 # a GUI this young may still be opening its window (no heartbeat yet): never end it
@@ -153,6 +196,9 @@ def claim(window_open, retries: int = 20) -> bool:
pid = holder()
if pid:
end_leftover(pid)
elif sys.platform == "win32":
for pid in _legacy_holders(lock_file()):
end_leftover(pid)
for _ in range(retries): # the OS releases the lock when the process is gone
if acquire():
return True
+7
View File
@@ -34,6 +34,7 @@ Created by FramePort {version} on {created}. Personal data was replaced by place
| manifest.json | app / agent / tool versions, OS, which games are included, warnings |
| app/app.log, app/jobs/*.log | the app's log and recent GUI job logs (build, install, launch test, ...) |
| app/settings.json | library settings and catalog sources |
| app/display.json | GUI versions, window state; Windows display modes, GPU drivers and display/device event metadata |
| frame/info.json, frame/host.json | the Frame: SteamOS build, Lepton/Proton, OpenXR runtime + layers, podman, kernel"""
""" keys |
| games/<pkg>/entry.json | the library entry: analysis, recipe, build checks, installs, last launch test |
@@ -372,6 +373,12 @@ def collect(packages: list[str] | None, target=None, reporter: Reporter | None =
red.add("source", d)
w = _Writer(red)
# app-wide
try:
from . import display
w.json("app/display.json", display.collect())
except Exception as exc: # noqa: BLE001
w.warnings.append(f"display diagnostics: {exc}")
for p in sorted(applog.logs_dir().glob("app.log*"))[:2]:
w.text("app/" + p.name, _read_tail(p, 2 << 20))
jobs = sorted(applog.jobs_dir().glob("*.log"))
+189
View File
@@ -0,0 +1,189 @@
"""Small, read-only desktop diagnostics. Native Windows display queries run only when collecting a bundle."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import threading
from datetime import UTC, datetime
EVENT_TIMEOUT = 5
EVENT_LIMIT = 60
_lock = threading.Lock()
_window: dict = {}
def _now() -> str:
return datetime.now(UTC).isoformat(timespec="milliseconds")
def _is_windows() -> bool:
return sys.platform == "win32"
def record_window(event: str, window, *, theme: str, reduce_motion: bool) -> None:
"""Cache and log existing window events; no display queries, timers or extra UI updates."""
if not _is_windows():
return
state = {"event": event, "time_utc": _now(), "size": [window.width, window.height],
"position": [window.left, window.top], "minimized": window.minimized,
"maximized": window.maximized, "full_screen": window.full_screen,
"theme": theme, "reduce_motion": reduce_motion}
with _lock:
state["focused"] = (event == "focus") if event in ("focus", "blur") else _window.get("focused")
if event in ("minimize", "restore"):
state["minimized"] = event == "minimize"
if event in ("maximize", "unmaximize"):
state["maximized"] = event == "maximize"
if event in ("enter-full-screen", "leave-full-screen"):
state["full_screen"] = event == "enter-full-screen"
_window.clear()
_window.update(state)
from ..core import applog
applog.log.info("desktop window: %s", json.dumps(state, separators=(",", ":")))
def _runtime() -> dict:
# Flet's constants are available in bundles even when wheel metadata is absent. Do not import the GUI here.
version = sys.modules.get("flet.version")
return {"flet": getattr(version, "flet_version", None),
"flutter": getattr(version, "flutter_version", None),
"executable": os.path.basename(sys.executable),
"engine_switches": [os.environ[f"FLUTTER_ENGINE_SWITCH_{i}"][:256] for i in range(1, 17)
if f"FLUTTER_ENGINE_SWITCH_{i}" in os.environ]}
def _driver(device_key: str) -> dict:
"""Read driver metadata only, without device IDs, registry paths or monitor serials."""
import winreg
prefix = "\\Registry\\Machine\\"
if not device_key.startswith(prefix):
return {"status": "unavailable"}
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, device_key[len(prefix):]) as key:
out = {}
for value, field in (("DriverVersion", "version"), ("DriverDate", "date"),
("ProviderName", "provider")):
try:
out[field] = str(winreg.QueryValueEx(key, value)[0])
except OSError:
pass
return {"status": "ok" if out else "unavailable", **out}
except OSError as exc:
return {"status": "error", "error": str(exc)}
def _windows_displays() -> dict:
import ctypes as C
WORD, DWORD, LONG = C.c_uint16, C.c_uint32, C.c_int32
class Device(C.Structure):
_fields_ = [("cb", DWORD), ("name", C.c_wchar * 32), ("description", C.c_wchar * 128),
("flags", DWORD), ("device_id", C.c_wchar * 128), ("key", C.c_wchar * 128)]
class Mode(C.Structure):
_fields_ = [("device", C.c_wchar * 32), ("spec", WORD), ("driver", WORD), ("size", WORD),
("extra", WORD), ("fields", DWORD), ("x", LONG), ("y", LONG),
("orientation", DWORD), ("fixed_output", DWORD), ("color", WORD), ("duplex", WORD),
("y_resolution", WORD), ("tt_option", WORD), ("collate", WORD),
("form", C.c_wchar * 32), ("log_pixels", WORD), ("bits", DWORD), ("width", DWORD),
("height", DWORD), ("flags", DWORD), ("frequency", DWORD), ("icm_method", DWORD),
("icm_intent", DWORD), ("media_type", DWORD), ("dither", DWORD),
("reserved1", DWORD), ("reserved2", DWORD), ("panning_width", DWORD),
("panning_height", DWORD)]
user = C.WinDLL("user32", use_last_error=True)
enum = user.EnumDisplayDevicesW
enum.argtypes, enum.restype = [C.c_wchar_p, DWORD, C.POINTER(Device), DWORD], C.c_int
settings = user.EnumDisplaySettingsW
settings.argtypes, settings.restype = [C.c_wchar_p, DWORD, C.POINTER(Mode)], C.c_int
adapters = []
for i in range(16):
device = Device(cb=C.sizeof(Device))
if not enum(None, i, C.byref(device), 0):
break
adapter = {"name": device.name, "description": device.description,
"attached": bool(device.flags & 1), "primary": bool(device.flags & 4),
"driver": _driver(device.key)}
mode = Mode(size=C.sizeof(Mode))
if settings(device.name, 0xFFFFFFFF, C.byref(mode)): # ENUM_CURRENT_SETTINGS
adapter["mode"] = {"size": [mode.width, mode.height], "position": [mode.x, mode.y],
"refresh_hz": mode.frequency, "bits_per_pixel": mode.bits,
"orientation": mode.orientation}
else:
adapter["mode"] = {"status": "unavailable"}
monitors = []
for j in range(16):
monitor = Device(cb=C.sizeof(Device))
if not enum(device.name, j, C.byref(monitor), 0):
break
monitors.append({"name": monitor.name, "description": monitor.description,
"active": bool(monitor.flags & 1)})
adapter["monitors"] = monitors
adapters.append(adapter)
return {"status": "ok", "adapters": adapters, "limit_reached": len(adapters) == 16}
# Filter on the event-log service, not by scanning every System event. Metadata is enough to correlate resets
# and device changes; event messages can contain unrelated device IDs and personal data.
_EVENT_SCRIPT = r"""
$ErrorActionPreference = 'Stop'
$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$providers = "Provider[@Name='Display'] or Provider[@Name='nvlddmkm'] or " +
"Provider[@Name='Microsoft-Windows-Kernel-PnP'] or Provider[@Name='Microsoft-Windows-Dwm-Core'] or " +
"Provider[@Name='Microsoft-Windows-DxgKrnl']"
$xpath = "*[System[TimeCreated[timediff(@SystemTime) <= 1800000] and ($providers)]]"
try {
$events = @(Get-WinEvent -LogName System -FilterXPath $xpath -MaxEvents 60 -ErrorAction Stop | ForEach-Object {
@{time_utc=$_.TimeCreated.ToUniversalTime().ToString('o'); provider=$_.ProviderName;
id=$_.Id; level=$_.Level; record_id=$_.RecordId}
})
@{status='ok'; events=$events} | ConvertTo-Json -Depth 4 -Compress
} catch {
if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') {
@{status='ok'; events=@()} | ConvertTo-Json -Compress
} else {
@{status='error'; error=$_.FullyQualifiedErrorId; events=@()} | ConvertTo-Json -Compress
}
}
"""
def _recent_events() -> dict:
try:
result = subprocess.run(["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command",
_EVENT_SCRIPT], capture_output=True, encoding="utf-8", errors="replace",
timeout=EVENT_TIMEOUT, creationflags=0x08000000)
if result.returncode:
return {"status": "error", "error": (result.stderr or "PowerShell query failed")[-512:]}
data = json.loads(result.stdout.lstrip("\ufeff"))
if not isinstance(data, dict) or data.get("status") not in ("ok", "error"):
raise ValueError("unexpected event query result")
if not isinstance(data.get("events"), list):
raise ValueError("unexpected event list")
data["events"] = data["events"][:EVENT_LIMIT]
return data
except subprocess.TimeoutExpired:
return {"status": "timeout", "timeout_seconds": EVENT_TIMEOUT}
except (OSError, ValueError, TypeError) as exc:
return {"status": "error", "error": str(exc)[:512]}
def collect() -> dict:
"""One on-demand snapshot. Unsupported platforms and query failures are explicit, never empty successes."""
with _lock:
window = dict(_window)
out = {"collected_at_utc": _now(), "platform": sys.platform, "runtime": _runtime(), "window": window}
if not _is_windows():
out["windows"] = {"status": "not_applicable"}
return out
try:
displays = _windows_displays()
except Exception as exc: # noqa: BLE001 diagnostics must not prevent collecting the rest of the bundle
displays = {"status": "error", "error": str(exc)[:512]}
out["windows"] = {"displays": displays, "system_events": {"lookback_minutes": 30, **_recent_events()}}
return out
+4 -1
View File
@@ -481,6 +481,7 @@
"Fix freezes when controllers vibrate": "",
"Fix hands lagging behind controllers": "",
"Fix launcher icon entry": "",
"Fix Lua startup crashes on Frame": "",
"Fix minimal Android SDK": "",
"Fix UE4 crash with Unity stub": "",
"Fix upside-down menus": "",
@@ -808,6 +809,7 @@
"Lets games built on Meta's oldest VR interface run on the Frame.": "",
"Lets games made for Oculus PCs run on SteamVR.": "",
"Lets games start that would stop at Meta's avatars (for example BlazeRush). No Meta avatars.": "",
"Lets games with a verified older Lua engine get past a startup crash on the Frame.": "",
"Lets games with Vivox voice chat start on the Frame (they'd crash on an Android 12 audio call).": "",
"Lets games written for newer OpenXR versions run on the Frame.": "",
"Lets Godot games start on the Frame (they'd crash looking for a clipboard).": "",
@@ -994,6 +996,7 @@
"OK": "",
"Older way to turn upside-down panels; prefer Fix upside-down menus.": "",
"Older workaround for upside-down quads: rotates them 180° (quads are single-sided; prefer flip_emul).": "",
"Older xLua/LuaJIT engines reject native pointers outside their 47-bit range, causing a startup error and sometimes a second crash during Unity shutdown (e.g. BattleTalent). Uses a Lua engine that preserves full pointer values while keeping the game's original xLua bindings and native helpers. Applied only to the verified compatible library build.": "",
"ON AIR": "",
"On Frame": "",
"On the desktop": "",
@@ -1481,7 +1484,7 @@
"Take screenshot": "",
"tall, 600 × 900": "",
"Team Beef ports: treat the Frame as a Meta headset": "",
"Team Beef's ports (for example Lambda1VR, RTCWQuest) load an OpenXR library named after the headset maker and pick their VR setup by maker; for \"valve\" they stop at start (libopenxr_loader_valve.so not found) or take the Pico path. Adds that library and lets them take the Meta path.": "",
"Team Beef's ports (for example Lambda1VR, RTCWQuest) load an OpenXR library named after the headset maker and pick their VR setup by maker; for \"valve\" they stop at start (libopenxr_loader_valve.so not found) or take the Pico path. Adds that library and lets them take the Meta path. Keeps GL4ES in charge of its desktop OpenGL draw calls so animated models don't turn into stretched triangles.": "",
"Temperature": "",
"Temperatures and fan": "",
"Test": "",
@@ -0,0 +1,66 @@
"""Use a full-pointer Lua engine while retaining the game's xLua bindings and custom helpers."""
from __future__ import annotations
from ...analysis import elf
from ...analysis.luajit import xlua_47bit
from ..base import ApkContext, Patch, Suggestion, register
from . import artifact
LOADER = "libxlua.so"
RUNTIME = "libfp_luajit.so"
class LuaJitPointers(Patch):
id = "frame.luajit_pointers"
title = "Fix Lua startup crashes on Frame"
description = ("Older xLua/LuaJIT engines reject native pointers outside their 47-bit range, causing a startup "
"error and sometimes a second crash during Unity shutdown (e.g. BattleTalent). Uses a Lua engine "
"that preserves full pointer values while keeping the game's original xLua bindings and native "
"helpers. Applied only to the verified compatible library build.")
order = 48
def applies(self, a):
return "arm64-v8a" in a.abis and bool((a.extra or {}).get("xlua_47bit"))
def detect(self, a):
if self.applies(a):
return Suggestion(True, "The bundled Lua engine rejects Frame's wider native pointers; use the "
"compatible full-pointer engine while retaining the game's bindings.")
return None
def apply(self, ctx: ApkContext) -> bool:
ws = ctx.ws
if ws.abi != "arm64-v8a" or not ws.has(ws.lib(LOADER)):
return False
original = ws.read_lib(LOADER)
runtime = artifact(ws.abi, RUNTIME)
if RUNTIME in elf.needed(original):
if ws.has(ws.lib(RUNTIME)) and ws.read_lib(RUNTIME) == runtime:
return False
ws.put(ws.lib(RUNTIME), runtime)
return True
if not xlua_47bit(original):
raise ValueError("Lua pointer fix refused: this xLua library has not been verified compatible")
names = {name for name in elf.dyn_symbols(original, True) & elf.dyn_symbols(runtime, True)
if name.startswith(("lua_", "luaL_", "luaJIT_", "luaopen_"))}
patched, redirected = elf.redirect_exports(original, names)
if not redirected:
raise ValueError("Lua pointer fix could not redirect the verified engine's API")
ws.put(ws.lib(LOADER), elf.add_needed(patched, RUNTIME))
ws.put(ws.lib(RUNTIME), runtime)
ctx.notes.append(f"{LOADER}: {len(redirected)} Lua engine functions redirected; original bindings retained")
return True
def validate(self, ctx: ApkContext):
ws = ctx.ws
if not ws.has(ws.lib(LOADER)):
return []
data = ws.read_lib(LOADER)
exported = elf.dyn_symbols(data, True)
ready = (ws.has(ws.lib(RUNTIME)) and RUNTIME in elf.needed(data)
and {"lua_pushlightuserdata", "lua_touserdata"} <= elf.dyn_symbols(data, False)
and {"xlua_get_lib_version", "xlua_gl", "luaopen_perflib"} <= exported)
return [("Full-pointer Lua engine with original xLua bindings", ready, RUNTIME)]
register(LuaJitPointers)
+23 -2
View File
@@ -11,6 +11,10 @@ TBXR also draws through multisampled render-to-texture (glFramebufferTexture2DMu
glRenderbufferStorageMultisampleEXT) whatever its --msaa setting; on the Frame that framebuffer is incomplete
(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE, black eyes). The GL shim (libglshim.so, linked into the TBXR library) hands
it single-sampled versions of both.
GL4ES implements desktop GL draw calls, including client arrays and immediate-mode conversion. The shim's GLES
draw exports must not take those calls away from GL4ES: doing so submits its untranslated arrays and stretches
animated models into spikes (#191). Keep those five symbols private only in APKs that bundle GL4ES.
"""
from __future__ import annotations
@@ -21,6 +25,8 @@ from . import artifact
from .vrapi_bridge import SHIM
VENDOR_LOADER = "libopenxr_loader_valve.so"
GL4ES_DRAW_EXPORTS = ("glDrawArrays", "glDrawElements", "glDrawArraysInstanced",
"glDrawElementsInstanced", "glDrawRangeElements")
class TbxrVendor(Patch):
@@ -29,7 +35,8 @@ class TbxrVendor(Patch):
description = ("Team Beef's ports (for example Lambda1VR, RTCWQuest) load an OpenXR library named after the "
"headset maker and pick their VR setup by maker; for \"valve\" they stop at start "
"(libopenxr_loader_valve.so not found) or take the Pico path. Adds that library and lets them take "
"the Meta path.")
"the Meta path. Keeps GL4ES in charge of its desktop OpenGL draw calls so animated models "
"don't turn into stretched triangles.")
order = 47
def applies(self, a):
@@ -60,11 +67,25 @@ class TbxrVendor(Patch):
ctx.notes.append(f"GL shim loaded by {lib} (single-sampled render-to-texture)")
changed = True
if changed or ws.has(ws.lib(SHIM)):
ws.put(ws.lib(SHIM), artifact(ws.abi, SHIM))
shim = artifact(ws.abi, SHIM)
if ws.has(ws.lib("libgl4es.so")):
shim, hidden = elf.hide_exports(shim, GL4ES_DRAW_EXPORTS)
if hidden:
ctx.notes.append("GL4ES keeps its desktop GL draw calls (shim draw exports made private)")
if not ws.has(ws.lib(SHIM)) or ws.read_lib(SHIM) != shim:
ws.put(ws.lib(SHIM), shim)
changed = True
if not ws.has(ws.lib(VENDOR_LOADER)):
ws.put(ws.lib(VENDOR_LOADER), build_stub_library([], soname=VENDOR_LOADER))
changed = True
return changed
def validate(self, ctx: ApkContext):
ws = ctx.ws
if ws.abi != "arm64-v8a" or not ws.has(ws.lib("libgl4es.so")) or not ws.has(ws.lib(SHIM)):
return []
_, public = elf.hide_exports(ws.read_lib(SHIM), GL4ES_DRAW_EXPORTS)
return [("GL4ES handles desktop GL draw calls", not public, ", ".join(public))]
register(TbxrVendor)
+1
View File
@@ -97,6 +97,7 @@ SUMMARIES = {
"seasons).",
"frame.ovr_microphone": "Stops a crash a few seconds after the logo in games with voice chat.",
"frame.ovr_stable_user": "Keeps your progress in games that file their saves under your Meta user.",
"frame.luajit_pointers": "Lets games with a verified older Lua engine get past a startup crash on the Frame.",
"frame.avatar_stub": "Lets games start that would stop at Meta's avatars (for example BlazeRush). No Meta "
"avatars.",
"frame.vrapi_bridge": "Lets games built on Meta's oldest VR interface run on the Frame.",
+1 -1
View File
@@ -55,7 +55,7 @@ class CatalogEntry:
pcvr_remove: list[str] = field(default_factory=list)
proton_env: dict = field(default_factory=dict)
proton_tool: str = ""
as_is: bool = False # Rift: the dump runs unchanged (e.g. a SteamVR build: no Revive)
as_is: bool = False # install the APK or Rift dump unchanged; device and launch settings still apply
verified: dict = field(default_factory=dict)
updated: str = "" # date of the last maintainer change without a new headset test (e.g. a fix from a report)
min_app: str = "" # oldest FramePort version this entry is for (remote entries for a newer app are skipped)
+4 -1
View File
@@ -37,6 +37,7 @@ def suggest(analysis: Analysis, use_catalog: bool = True) -> Recipe:
elif entry:
recipe.source = f"catalog ({entry.origin})"
recipe.status, recipe.notes, recipe.title = entry.status, entry.notes, entry.title or recipe.title
recipe.as_is = entry.as_is
why = f"Known-good recipe for {entry.title} (tested {entry.verified.get('date', '?')})."
for pid in entry.overport_remove:
recipe.patches.pop(pid, None)
@@ -88,7 +89,9 @@ def suggest(analysis: Analysis, use_catalog: bool = True) -> Recipe:
recipe.notes = f"No 64-bit ARM code ({', '.join(analysis.abis)}): the Steam Frame can't run it."
if not rift:
_static_blockers(analysis, recipe)
_non_quest(analysis, recipe)
if not rift and (not entry or analysis.vr_kind == "none"):
# Saved recipes still need the flat-app build rules: no OVRPort, and APK fixes only when needed (#271).
_non_quest(analysis, recipe)
if not rift and not entry and (analysis.extra or {}).get("frame_patched"):
# already has FramePort's adapter (e.g. a PATCHED/ build): installing it unchanged is the safe default
recipe.as_is = True
+31 -9
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import re
import shutil
import tempfile
from pathlib import Path
from ..core.events import Reporter
@@ -59,15 +60,36 @@ def patch(apk: Path, outdir: Path, name: str, patches: list[str], reporter: Repo
outdir.mkdir(parents=True, exist_ok=True)
target = outdir / name
target.unlink(missing_ok=True)
args = ["-jar", _jar(), "patch", f"--input={apk}", f"--output={outdir}", f"--output-name={name}",
f"--workspace={workspace()}", f"--version={version}", "--patches=" + ";".join(patches)]
reporter.log("OVRPort " + " ".join(a for a in args[3:] if not a.startswith("--patches")))
p = toolchain.run_java(args)
# overport draws spinners with \r; keep only final line states
for line in (p.stdout + p.stderr).splitlines():
line = line.split("\r")[-1].strip()
if line:
reporter.log(" " + line)
jar, work = _jar(), workspace()
def run(input_apk):
args = ["-jar", jar, "patch", f"--input={input_apk}", f"--output={outdir}", f"--output-name={name}",
f"--workspace={work}", f"--version={version}", "--patches=" + ";".join(patches)]
reporter.log("OVRPort " + " ".join(a for a in args[3:] if not a.startswith("--patches")))
result = toolchain.run_java(args)
# overport draws spinners with \r; keep only final line states
for line in (result.stdout + result.stderr).splitlines():
line = line.split("\r")[-1].strip()
if line:
reporter.log(" " + line)
return result
p = run(apk)
output = p.stdout + p.stderr
if p.returncode and "java.util.NoSuchElementException: Sequence is empty." in output \
and "PatcherContext.appName(" in output:
from ..apk.axml import copy_activity_label_to_application
from ..apk.workspace import ApkWorkspace
with ApkWorkspace(apk) as staged:
manifest = copy_activity_label_to_application(staged.read("AndroidManifest.xml"))
if manifest is not None:
reporter.log("OVRPort could not read the app name; retrying with the activity's label")
with tempfile.TemporaryDirectory(prefix="ovrport-label-", dir=outdir) as tmp:
staged.put("AndroidManifest.xml", manifest)
prepared = staged.write(Path(tmp) / apk.name)
target.unlink(missing_ok=True)
p = run(prepared)
if p.returncode or not target.exists():
raise RuntimeError(f"OVRPort failed (exit {p.returncode}); see log")
return target
+9
View File
@@ -164,6 +164,7 @@ class FramePortApp:
for key, value in window_geometry(library.setting("ui.window"), T.SCALE).items():
setattr(page.window, key, value)
page.window.on_event = self._on_window_event
self._log_window("ready")
if sys.platform == "win32" and (icon := G.window_icon(Path(assets_dir()))):
page.window.icon = str(icon) # source runs: FramePort's icon instead of Flet's
page.on_keyboard_event = self._on_key
@@ -263,10 +264,18 @@ class FramePortApp:
self.activity.set_open(open_activity)
self.render()
def _log_window(self, event: str) -> None:
if sys.platform == "win32":
from ..diag import display
display.record_window(event, self.page.window, theme=T.THEME, reduce_motion=self.reduce_motion)
def _on_window_event(self, e) -> None:
"""Remember the window's size and position (once a resize/move ends; not while maximized or full screen) and
whether it is maximized."""
win, change = self.page.window, None
if e.type not in (ft.WindowEventType.MOVE, ft.WindowEventType.RESIZE):
self._log_window(getattr(e.type, "value", str(e.type)))
if e.type == ft.WindowEventType.CLOSE:
_stop_link_watch() # also when maximized (that returns below)
if e.type in (ft.WindowEventType.MAXIMIZE, ft.WindowEventType.UNMAXIMIZE):
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="96" viewBox="0 0 72 96"><defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2D7FF9"/><stop offset=".55" stop-color="#7A4BD8"/><stop offset="1" stop-color="#FF8A3D"/></linearGradient><radialGradient id="sun" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#FFE48A"/><stop offset="1" stop-color="#FFB42E"/></radialGradient></defs><rect width="72" height="96" fill="url(#bg)"/><path d="M0 74 18 60 30 68 46 54 72 72V96H0z" fill="#101218" fill-opacity=".35"/><circle cx="36" cy="40" r="20" fill="url(#sun)" stroke="#101218" stroke-width="2.4"/><path d="M17.5 35.5h37M20 35.5h13l-1.4 6.2a3.4 3.4 0 0 1-3.3 2.6h-4.6A3.4 3.4 0 0 1 20.4 42zM39 35.5h13l-.4 6.5a3.4 3.4 0 0 1-3.3 2.3h-4.6a3.4 3.4 0 0 1-3.3-2.6z" fill="#101218" stroke="#101218" stroke-width="2" stroke-linejoin="round"/><path d="M23.5 37.5h4" stroke="#FFFFFF" stroke-opacity=".7" stroke-width="1.4" stroke-linecap="round"/><path d="M29 50.5c4 3.2 10 3.2 14 0" fill="none" stroke="#101218" stroke-width="2.4" stroke-linecap="round"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="96" viewBox="0 0 72 96"><defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2D7FF9"/><stop offset=".55" stop-color="#7A4BD8"/><stop offset="1" stop-color="#FF8A3D"/></linearGradient><radialGradient id="sun" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#FFE48A"/><stop offset="1" stop-color="#FFB42E"/></radialGradient></defs><rect width="72" height="96" fill="url(#bg)"/><path d="M0 74 18 60 30 68 46 54 72 72V96H0z" fill="#101218" fill-opacity=".35"/><circle cx="36" cy="40" r="20" fill="url(#sun)" stroke="#101218" stroke-width="2.4"/><path d="M17.5 35.5h37M20 35.5h13l-1.4 6.2q-.6 2.6-3.3 2.6h-4.6q-2.9 0-3.3-2.6zM39 35.5h13l-.4 6.2q-.4 2.6-3.3 2.6h-4.6q-2.7 0-3.3-2.6z" fill="#101218" stroke="#101218" stroke-width="2" stroke-linejoin="round"/><path d="M23.5 37.5h4" stroke="#FFFFFF" stroke-opacity=".7" stroke-width="1.4" stroke-linecap="round"/><path d="M29 50.5c4 3.2 10 3.2 14 0" fill="none" stroke="#101218" stroke-width="2.4" stroke-linecap="round"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+59
View File
@@ -37,6 +37,65 @@ def test_vdf_roundtrip_and_upsert(monkeypatch, tmp_path):
assert a.vdf_decode(vdf.read_bytes())["shortcuts"] == {}
@pytest.mark.parametrize("root_key", ["Shortcuts", "SHORTCUTS", "shortcuts"])
def test_shortcuts_keep_steams_key_spelling_and_existing_games(monkeypatch, tmp_path, root_key):
a = load_agent(monkeypatch, tmp_path)
vdf = tmp_path / "shortcuts.vdf"
exe = '"/game/launch.sh"'
other = {"AppID": 123, "AppName": "Other", "EXE": '"/other"', "Tags": {"0": "Favorite"}}
original = {root_key: {"0": {"AppID": 456, "AppName": "Old", "EXE": exe, "ICON": "/old.png",
"Tags": {"0": "Favorite"}, "LastPlayTime": 99}, "1": other}}
data = a.vdf_encode(original)
vdf.write_bytes(data)
assert a.vdf_encode(a.vdf_decode(data)) == data
assert a.upsert_shortcut(str(vdf), exe, "New", "/game", tags=["Action"], write=False) == (456, True)
assert vdf.read_bytes() == data
assert a.upsert_shortcut(str(vdf), exe, "New", "/game", tags=["Action"]) == 456
root = a.vdf_decode(vdf.read_bytes())
assert list(root) == [root_key]
entry = root["shortcuts"]["0"]
assert entry["appname"] == "New" and "AppName" in list(entry) and "appname" not in list(entry)
assert entry["appid"] == 456 and entry["icon"] == "/old.png" and entry["LastPlayTime"] == 99
assert list(entry["tags"].values()) == ["Favorite", "Quest on Frame", "Action"]
assert root["shortcuts"]["1"] == other
assert a.upsert_shortcut(str(vdf), exe, "New", "/game", tags=["Action"], write=False) == (456, False)
a.upsert_shortcut(str(vdf), '"/third"', "Third", "/")
assert len(a.vdf_decode(vdf.read_bytes())["shortcuts"]) == 3
assert a.prune_shortcuts(str(vdf), "New", '"/replacement"', "Quest on Frame") == [456]
assert a.remove_shortcut(str(vdf), '"/other"')
root = a.vdf_decode(vdf.read_bytes())
assert list(root) == [root_key] and len(root["shortcuts"]) == 1
assert root["shortcuts"]["0"]["appname"] == "Third"
def test_capitalized_shortcuts_are_found_by_library_and_devkit_checks(monkeypatch, tmp_path):
a = load_agent(monkeypatch, tmp_path)
cfg = Path(a.STEAM) / "userdata/42/config"
cfg.mkdir(parents=True)
exe = '"/game/launch.sh"'
vdf = cfg / "shortcuts.vdf"
vdf.write_bytes(a.vdf_encode({"Shortcuts": {"0": {"APPID": 0xF0000001, "EXE": exe,
"APPNAME": "Game", "DEVKITGAMEID": "fp_game"}}}))
monkeypatch.setattr(a, "library_users", lambda: ["42"])
monkeypatch.setattr(a, "steam_users", lambda: ["42"])
monkeypatch.setattr(a, "shortcut_args", lambda pkg: (exe,))
monkeypatch.setattr(a.time, "sleep", lambda seconds: None)
assert a.shortcut_appid_for(exe) == 0xF0000001
assert a.devkit_appid("fp_game") == 0xF0000001
assert a.shortcut_names("42") == {str(0xF0000001): "Game"}
assert a.shortcuts_lost(["42"], ["com.x.y"], wait=0) == []
def test_case_variant_duplicate_vdf_keys_are_not_overwritten(monkeypatch, tmp_path):
a = load_agent(monkeypatch, tmp_path)
vdf = tmp_path / "shortcuts.vdf"
data = a.vdf_encode({"Shortcuts": {"0": {"appid": 1}}, "shortcuts": {"0": {"appid": 2}}})
vdf.write_bytes(data)
with pytest.raises(a.AgentError, match="duplicate VDF key"):
a.upsert_shortcut(str(vdf), '"/game"', "Game", "/")
assert vdf.read_bytes() == data
def test_launcher_template(monkeypatch, tmp_path):
a = load_agent(monkeypatch, tmp_path)
anchor = tmp_path / "anchor"
+70
View File
@@ -1,6 +1,76 @@
from frameport.apk import axml
def test_missing_application_label_uses_activity_resource():
from conftest import build_axml
manifest = build_axml([
("start", "manifest", [("package", "str", "org.example.game")]),
("start", "application", [("debuggable", "bool", False)]),
("start", "activity", [("name", "str", "Main"), ("label", "ref", 0x7F020000)]),
("end", "activity"), ("end", "application"), ("end", "manifest"),
])
fixed = axml.copy_activity_label_to_application(manifest)
x = axml.Axml(fixed)
app = next(el for el in x.elements() if el.name == "application")
label = next(a for a in app.attrs if x.string(a.name) == "label")
assert label.dtype == axml.TYPE_REFERENCE and label.value == 0x7F020000
assert len(fixed) == len(manifest) + 20 and x.get_bool("application", "debuggable") is False
original = axml.Axml(manifest)
activity = next(el for el in original.elements() if el.name == "activity")
new_activity = next(el for el in x.elements() if el.name == "activity")
assert manifest[activity.offset:activity.offset + activity.size] == fixed[
new_activity.offset:new_activity.offset + new_activity.size]
assert axml.copy_activity_label_to_application(fixed) is None
def test_application_label_is_never_replaced():
from conftest import build_axml
for kind, value in [("str", "Original"), ("ref", 0x7F020001)]:
manifest = build_axml([
("start", "manifest", []), ("start", "application", [("label", kind, value)]),
("start", "activity", [("label", "str", "Other")]), ("end", "activity"),
("end", "application"), ("end", "manifest"),
])
assert axml.copy_activity_label_to_application(manifest) is None
assert axml.copy_activity_label_to_application(build_axml([
("start", "manifest", []), ("start", "application", []), ("end", "application"),
("end", "manifest"),
])) is None
def test_copied_label_preserves_resource_order_and_special_attribute_indices():
import struct
from conftest import build_axml
manifest = build_axml([
("start", "manifest", []),
("start", "application", [("theme", "ref", 0x0103000A), ("debuggable", "bool", False)]),
("start", "activity", [("label", "str", "Game")]), ("end", "activity"),
("end", "application"), ("end", "manifest"),
])
x = axml.Axml(manifest)
ids = [0] * x.count
for name, resource in [("theme", 0x01010000), ("label", 0x01010001), ("debuggable", 0x0101000F)]:
ids[x.index(name)] = resource
resource_map = struct.pack("<HHI", 0x0180, 8, 8 + len(ids) * 4) + struct.pack(f"<{len(ids)}I", *ids)
pos = x.pool_off + x.pool_size
data = bytearray(manifest[:pos] + resource_map + manifest[pos:])
struct.pack_into("<I", data, 4, len(data))
x = axml.Axml(data)
app = next(el for el in x.elements() if el.name == "application")
# All three special indices point at the second attribute, which the insertion moves to third.
ext = app.offset + 16
struct.pack_into("<HHH", data, ext + 14, 2, 2, 2)
fixed = axml.copy_activity_label_to_application(bytes(data))
x = axml.Axml(fixed)
app = next(el for el in x.elements() if el.name == "application")
assert [x.string(a.name) for a in app.attrs] == ["theme", "label", "debuggable"]
assert struct.unpack_from("<HHH", fixed, app.offset + 16 + 14) == (3, 3, 3)
def test_parse(quest_manifest):
x = axml.Axml(quest_manifest)
names = [e.name for e in x.elements()]
+13
View File
@@ -131,6 +131,19 @@ def test_bundle_offline_and_app_only(tmp_path):
assert m["target"] is None and m["games"] == []
def test_display_diagnostics_use_bundle_redaction(tmp_path, monkeypatch):
from frameport.diag import display
monkeypatch.setattr(display, "collect", lambda: {"windows": {"status": "error",
"error": r"C:\Users\Alice\display.txt at 192.168.50.2"},
"driver": "32.0.15.61742"})
out = bundle.collect(None, dest=tmp_path / "display.zip")
with zipfile.ZipFile(out) as z:
text = z.read("app/display.json").decode()
assert "Alice" not in text and "192.168.50.2" not in text
assert json.loads(text)["driver"] == "32.0.15.61742"
def test_bundle_size_cap():
w = bundle._Writer(redact.Redactor())
import os
+87
View File
@@ -0,0 +1,87 @@
"""Desktop diagnostics stay on demand, bounded and useful when Windows queries fail or return no events."""
import json
import subprocess
import sys
from types import SimpleNamespace
import pytest
from frameport.diag import display
@pytest.fixture(autouse=True)
def reset_window(monkeypatch):
monkeypatch.setattr(display, "_window", {})
def test_non_windows_does_not_start_display_queries(monkeypatch):
monkeypatch.setattr(display, "_is_windows", lambda: False)
def unexpected():
pytest.fail("Windows query ran on another platform")
monkeypatch.setattr(display, "_windows_displays", unexpected)
monkeypatch.setattr(display, "_recent_events", unexpected)
assert display.collect()["windows"] == {"status": "not_applicable"}
def test_window_events_are_cached_without_running_queries(monkeypatch, caplog):
monkeypatch.setattr(display, "_is_windows", lambda: True)
monkeypatch.setitem(sys.modules, "flet.version", SimpleNamespace(flet_version="1.0.2", flutter_version="3.44.8"))
window = SimpleNamespace(width=1280, height=720, left=3086, top=172,
minimized=False, maximized=False, full_screen=False)
with caplog.at_level("INFO", logger="frameport"):
display.record_window("minimize", window, theme="portal", reduce_motion=True)
assert "desktop window:" in caplog.text and '"event":"minimize"' in caplog.text
assert display._window["minimized"] is True and display._window["position"] == [3086, 172]
monkeypatch.setattr(display, "_windows_displays", lambda: {"status": "ok", "adapters": []})
monkeypatch.setattr(display, "_recent_events", lambda: {"status": "ok", "events": []})
result = display.collect()
assert result["runtime"]["flutter"] == "3.44.8"
assert result["window"]["event"] == "minimize"
display.record_window("focus", window, theme="portal", reduce_motion=True)
display.record_window("restore", window, theme="portal", reduce_motion=True)
assert display._window["focused"] is True and display._window["minimized"] is False
@pytest.mark.parametrize("response,status", [
('{"status":"ok","events":[]}', "ok"),
('{"status":"error","error":"AccessDenied","events":[]}', "error"),
('{"status":"ok","events":"invalid"}', "error"),
("not JSON", "error"),
])
def test_event_query_distinguishes_empty_results_from_failures(monkeypatch, response, status):
def run(args, **kwargs):
assert args[:4] == ["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive"]
assert kwargs["timeout"] == 5 and kwargs["creationflags"] == 0x08000000
assert "-FilterXPath" in args[-1] and "-MaxEvents 60" in args[-1]
assert ".Message" not in args[-1] # event metadata, not unrelated free text or device IDs
return subprocess.CompletedProcess(args, 0, response, "")
monkeypatch.setattr(display.subprocess, "run", run)
result = display._recent_events()
assert result["status"] == status
if status == "ok":
assert result["events"] == []
def test_event_query_timeout_does_not_look_like_no_events(monkeypatch):
def run(*args, **kwargs):
raise subprocess.TimeoutExpired(args[0], kwargs["timeout"])
monkeypatch.setattr(display.subprocess, "run", run)
assert display._recent_events() == {"status": "timeout", "timeout_seconds": 5}
def test_display_failure_does_not_discard_event_or_window_data(monkeypatch):
monkeypatch.setattr(display, "_is_windows", lambda: True)
def broken():
raise OSError("display API unavailable")
monkeypatch.setattr(display, "_windows_displays", broken)
monkeypatch.setattr(display, "_recent_events", lambda: {"status": "ok", "events": []})
result = display.collect()
assert result["windows"]["displays"]["status"] == "error"
assert result["windows"]["system_events"]["status"] == "ok"
json.dumps(result)
+85 -8
View File
@@ -1,7 +1,13 @@
"""One FramePort window at a time (core/instance.py)."""
import json
import os
import subprocess
import sys
import textwrap
from pathlib import Path
from types import SimpleNamespace
import pytest
from frameport.core import instance
@@ -9,24 +15,36 @@ from frameport.core import instance
def _hold_lock(path):
"""A separate process holding the GUI lock (like a running FramePort)."""
code = textwrap.dedent(f"""
import sys, time
import os, sys, time
sys.path.insert(0, {str(instance.__file__.rsplit('/frameport/', 1)[0])!r})
from pathlib import Path
from frameport.core import instance
assert instance.acquire(Path({str(path)!r}))
print("locked", flush=True)
print("locked", os.getpid(), flush=True)
time.sleep(30)
""")
p = subprocess.Popen([sys.executable, "-c", code], stdout=subprocess.PIPE, text=True)
assert p.stdout.readline().strip() == "locked"
status = p.stdout.readline().strip().split()
assert status[0] == "locked"
p.gui_pid = int(status[1]) # Windows venv's python.exe can launch another interpreter process
return p
def _end_holder(holder):
import psutil
try:
psutil.Process(holder.gui_pid).kill()
except psutil.NoSuchProcess:
pass
holder.wait(5)
def test_second_start_is_refused_while_a_window_is_open(tmp_path, monkeypatch):
monkeypatch.setenv("FRAMEPORT_HOME", str(tmp_path))
holder = _hold_lock(instance.lock_file())
try:
assert instance.holder() == holder.pid
assert instance.holder() == holder.gui_pid
assert not instance.acquire()
assert instance.claim(lambda: True) is False # a window answers: it's asked to come to the front
assert instance.take_focus_request() and not instance.take_focus_request()
@@ -34,19 +52,78 @@ def test_second_start_is_refused_while_a_window_is_open(tmp_path, monkeypatch):
assert instance.claim(lambda: False, retries=1) is False
assert holder.poll() is None
finally:
holder.kill()
holder.wait()
_end_holder(holder)
assert instance.acquire() # released by the OS when the holder ends
def test_leftover_frameport_gui_is_ended(tmp_path, monkeypatch):
monkeypatch.setenv("FRAMEPORT_HOME", str(tmp_path))
holder = _hold_lock(instance.lock_file())
monkeypatch.setattr(instance, "_is_frameport_gui", lambda pid: pid == holder.pid)
monkeypatch.setattr(instance, "_is_frameport_gui", lambda pid: pid == holder.gui_pid)
monkeypatch.setattr(instance, "STARTUP_GRACE", 0.0)
try:
assert instance.claim(lambda: False) is True # no window: the leftover process is ended, the lock taken
assert holder.wait(5) is not None
finally:
if holder.poll() is None:
holder.kill()
_end_holder(holder)
def test_holder_is_readable_when_windows_blocks_the_lock_contents(tmp_path, monkeypatch):
path = tmp_path / "gui.lock"
assert instance.acquire(path)
read_text = Path.read_text
def locked(self, *args, **kwargs):
if self == path:
raise PermissionError("Windows byte-range lock")
return read_text(self, *args, **kwargs)
monkeypatch.setattr(Path, "read_text", locked)
try:
assert instance.holder(path) == os.getpid()
owner = instance._owner_file(path)
data = json.loads(owner.read_text())
data["created"] -= 100 # a PID reused by another process must not be accepted
owner.write_text(json.dumps(data))
assert instance.holder(path) is None
finally:
instance.release()
assert not instance._owner_file(path).exists()
@pytest.mark.parametrize("cmd,expected", [
(["python", "-m", "frameport.ui.app"], True),
(["python", "-m", "frameport.cli"], False),
(["python", "-c", "import frameport"], False),
(["/apps/FramePort.exe"], True),
])
def test_recovery_only_matches_gui_commands(monkeypatch, cmd, expected):
import psutil
monkeypatch.setattr(psutil, "Process", lambda pid: SimpleNamespace(cmdline=lambda: cmd))
assert instance._is_frameport_gui(123) is expected
def test_legacy_recovery_only_finds_the_same_data_folders_lock(tmp_path, monkeypatch):
import psutil
lock = tmp_path / "gui.lock"
other = tmp_path / "another-app/gui.lock"
processes = [SimpleNamespace(pid=123, open_files=lambda: [SimpleNamespace(path=str(lock))]),
SimpleNamespace(pid=124, open_files=lambda: [SimpleNamespace(path=str(other))])]
monkeypatch.setattr(psutil, "process_iter", lambda attrs: processes)
monkeypatch.setattr(instance, "_is_frameport_gui", lambda pid: True)
assert instance._legacy_holders(lock) == [123]
def test_claim_recovers_a_legacy_windows_holder_without_readable_pid(monkeypatch):
monkeypatch.setattr(instance, "sys", SimpleNamespace(platform="win32"))
attempts = iter([False, True])
ended = []
monkeypatch.setattr(instance, "acquire", lambda: next(attempts))
monkeypatch.setattr(instance, "holder", lambda: None)
monkeypatch.setattr(instance, "_legacy_holders", lambda path: [123])
monkeypatch.setattr(instance, "end_leftover", lambda pid: ended.append(pid))
assert instance.claim(lambda: False)
assert ended == [123]
+144
View File
@@ -0,0 +1,144 @@
"""Lua engine substitution must preserve native bindings and their self-references through the PLT."""
import hashlib
import json
import shutil
import subprocess
import sys
import zipfile
import pytest
from elftools.elf.elffile import ELFFile
from frameport.analysis import elf, luajit
from frameport.apk.workspace import ApkWorkspace
from frameport.core.events import Reporter
from frameport.core.models import Analysis
from frameport.patches import base
from frameport.patches.frame import artifact, luajit_pointers
def _analysis(extra=None):
return Analysis(package="example.game", version="1", label="Game", abis=["arm64-v8a"], engine="Unity",
xr="OpenXR", graphics="GLES", direct_vrapi=False, libs=["libxlua.so"], launcher_activity=None,
has_info_category=False, meta_permissions=[], uses_glad_gl=False, unity_msaa_levels=0,
oculus_os_classes=False, is_overport_output=False, debuggable=True, extra=extra or {})
@pytest.fixture
def libraries(tmp_path):
cc = shutil.which("cc")
if not cc or sys.platform != "linux":
pytest.skip("native loader regression uses Linux cc")
original = tmp_path / "original.c"
original.write_text("""
int lua_gettop(void *state) { (void)state; return -77; }
void lua_pushlightuserdata(void **state, void *p) { *state = p; }
void *lua_touserdata(void **state, int index) { (void)index; return *state; }
int xlua_get_lib_version(void) { return 105; }
void *xlua_gl(void *state) { return state; }
int luaopen_perflib(void *state) { return lua_gettop(state); }
int game_custom_binding(void *state) { return lua_gettop(state) + 1; }
""")
runtime = tmp_path / "runtime.c"
runtime.write_text("""
int lua_gettop(void *state) { (void)state; return 42; }
void lua_pushlightuserdata(void **state, void *p) { *state = p; }
void *lua_touserdata(void **state, int index) { (void)index; return *state; }
""")
loader = tmp_path / "libxlua.so"
core = tmp_path / luajit_pointers.RUNTIME
for source, dest in [(original, loader), (runtime, core)]:
subprocess.run([cc, "-shared", "-fPIC", "-O2", str(source), "-Wl,-rpath,$ORIGIN",
f"-Wl,-soname,{dest.name}", "-o", str(dest)], check=True, capture_output=True)
return loader, core
def _call_library(path):
# A fresh process avoids reusing a dlopen handle after changing the file.
script = """
import ctypes, json, sys
d = ctypes.CDLL(sys.argv[1])
print(json.dumps([d.xlua_get_lib_version(), d.luaopen_perflib(None), d.game_custom_binding(None), d.lua_gettop(None)]))
"""
result = subprocess.run([sys.executable, "-c", script, str(path)], check=True, text=True, capture_output=True)
return json.loads(result.stdout)
def test_redirect_definitions_redirects_dlsym_and_internal_calls(libraries):
loader, core = libraries
assert _call_library(loader) == [105, -77, -76, -77]
original = loader.read_bytes()
names = elf.dyn_symbols(original, True) & elf.dyn_symbols(core.read_bytes(), True)
redirected, changed = elf.redirect_exports(original, names)
assert set(changed) == names
assert names <= elf.dyn_symbols(redirected, False)
assert "luaopen_perflib" in elf.dyn_symbols(redirected, True)
loader.write_bytes(elf.add_needed(redirected, core.name))
assert _call_library(loader) == [105, 42, 43, 42]
again, changed = elf.redirect_exports(redirected, names)
assert again == redirected and changed == []
def test_patch_keeps_binding_code_and_refuses_unreviewed_builds(tmp_path, libraries, monkeypatch):
loader, core = libraries
original = loader.read_bytes()
digest = hashlib.sha256(original).hexdigest()
monkeypatch.setattr(luajit, "XLUA_47BIT_SHA256", frozenset({digest}))
monkeypatch.setattr(luajit_pointers, "artifact", lambda abi, name: core.read_bytes())
apk = tmp_path / "game.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("lib/arm64-v8a/libxlua.so", original)
analysis = _analysis({"xlua_47bit": True})
patch = base.get("frame.luajit_pointers")
assert patch.detect(analysis).recommended
assert not patch.applies(_analysis())
with ApkWorkspace(apk) as ws:
ctx = base.ApkContext(ws, analysis, {}, Reporter(), {patch.id: {}})
assert patch.apply(ctx)
patched = ws.read_lib("libxlua.so")
with loader.open("rb") as f:
section = ELFFile(f).get_section_by_name(".text")
start, size = section["sh_offset"], section["sh_size"]
assert patched[start:start + size] == original[start:start + size]
kept = {"xlua_gl", "xlua_get_lib_version", "luaopen_perflib", "game_custom_binding"}
assert kept <= elf.dyn_symbols(patched, True)
assert all(ok for _, ok, _ in patch.validate(ctx))
assert not patch.apply(ctx)
ws.put(ws.lib(luajit_pointers.RUNTIME), b"old artifact")
assert patch.apply(ctx) # repair only a stale engine artifact
assert ws.read_lib("libxlua.so") == patched
ws.put(ws.lib("libxlua.so"), original + b"different build")
with pytest.raises(ValueError, match="not been verified"):
patch.apply(ctx)
def test_shipped_engine_is_complete_and_preserves_glref_contract():
data = artifact("arm64-v8a", luajit_pointers.RUNTIME)
assert elf.soname(data) == luajit_pointers.RUNTIME
assert {"luaL_newstate", "lua_pushlightuserdata", "lua_touserdata", "lua_topointer",
"luaopen_ffi", "luaL_register", "lua_close"} <= elf.dyn_symbols(data, True)
assert "xlua_get_lib_version" not in elf.dyn_symbols(data, True) # game's bindings stay in the original ELF
assert b"full-width lightuserdata runtime enabled" in data
assert all(va % 16384 == off % 16384 for va, off, _ in elf.load_segments(data))
def test_analysis_recommends_only_the_reviewed_engine(tmp_path, quest_manifest, libraries, monkeypatch):
from frameport.analysis.detect import analyze
from frameport.recommend.engine import suggest
loader, _ = libraries
data = loader.read_bytes()
monkeypatch.setattr(luajit, "XLUA_47BIT_SHA256", frozenset({hashlib.sha256(data).hexdigest()}))
apk = tmp_path / "analysis.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("AndroidManifest.xml", quest_manifest)
z.writestr("lib/arm64-v8a/libxlua.so", data)
analysis = analyze(apk)
assert analysis.extra["xlua_47bit"]
assert analysis.extra["xlua_sha256"] == hashlib.sha256(data).hexdigest()
assert "frame.luajit_pointers" in suggest(analysis, use_catalog=False).patches
monkeypatch.setattr(luajit, "XLUA_47BIT_SHA256", frozenset())
analysis = analyze(apk)
assert not analysis.extra["xlua_47bit"]
assert analysis.extra["xlua_sha256"] == hashlib.sha256(data).hexdigest() # review an unknown build from diagnostics
assert "frame.luajit_pointers" not in suggest(analysis, use_catalog=False).patches
+78
View File
@@ -0,0 +1,78 @@
import subprocess
import zipfile
from pathlib import Path
import pytest
from frameport.apk import axml
from frameport.core.events import Reporter
from frameport.tools import overport
NAME_FAILURE = ("java.util.NoSuchElementException: Sequence is empty.\n"
"at moe.crx.overport.patching.PatcherContext.appName(PatcherContext.kt:231)")
def manifest(app_label=False):
from conftest import build_axml
return build_axml([
("start", "manifest", []), ("start", "application", [("label", "str", "Game")] if app_label else []),
("start", "activity", [("label", "str", "Game")]), ("end", "activity"),
("end", "application"), ("end", "manifest"),
])
@pytest.mark.parametrize("retry_fails", [False, True])
def test_overport_retries_missing_app_name_with_a_temporary_apk(monkeypatch, tmp_path, retry_fails):
apk = tmp_path / "source.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("AndroidManifest.xml", manifest())
z.writestr("assets/unchanged", b"game data")
original = apk.read_bytes()
calls = []
outdir = tmp_path / "out"
monkeypatch.setattr(overport, "_jar", lambda: "overport.jar")
def run(args):
source = Path(next(a.removeprefix("--input=") for a in args if a.startswith("--input=")))
calls.append(source)
if len(calls) == 1:
return subprocess.CompletedProcess(args, 1, "", NAME_FAILURE)
with zipfile.ZipFile(source) as z:
x = axml.Axml(z.read("AndroidManifest.xml"))
app = next(el for el in x.elements() if el.name == "application")
assert x.attr_str(app, "label") == "Game"
assert z.read("assets/unchanged") == b"game data"
if retry_fails:
return subprocess.CompletedProcess(args, 1, "", NAME_FAILURE)
(outdir / "patched.apk").write_bytes(b"output")
return subprocess.CompletedProcess(args, 0, "done", "")
monkeypatch.setattr(overport.toolchain, "run_java", run)
if retry_fails:
with pytest.raises(RuntimeError, match="OVRPort failed"):
overport.patch(apk, outdir, "patched.apk", ["patch_copy_libraries"], Reporter())
else:
result = overport.patch(apk, outdir, "patched.apk", ["patch_copy_libraries"], Reporter())
assert result == outdir / "patched.apk"
assert len(calls) == 2
assert calls[0] == apk and calls[1] != apk and not calls[1].exists()
assert apk.read_bytes() == original
@pytest.mark.parametrize("error,app_label", [("some other failure", False), (NAME_FAILURE, True)])
def test_overport_does_not_retry_unrelated_failures(monkeypatch, tmp_path, error, app_label):
apk = tmp_path / "source.apk"
with zipfile.ZipFile(apk, "w") as z:
z.writestr("AndroidManifest.xml", manifest(app_label))
calls = []
monkeypatch.setattr(overport, "_jar", lambda: "overport.jar")
def run(args):
calls.append(args)
return subprocess.CompletedProcess(args, 1, "", error)
monkeypatch.setattr(overport.toolchain, "run_java", run)
with pytest.raises(RuntimeError, match="OVRPort failed"):
overport.patch(apk, tmp_path / "out", "patched.apk", ["patch_copy_libraries"], Reporter())
assert len(calls) == 1
+81
View File
@@ -1,5 +1,7 @@
import zipfile
import pytest
from frameport.analysis.stubgen import build_stub_library
from frameport.apk.workspace import ApkWorkspace
from frameport.core.events import Reporter
@@ -443,6 +445,53 @@ def test_android_app_without_vr_gets_no_vr_translation():
@pytest.mark.parametrize("info_activity,sdl_clipboard", [(False, False), (True, False), (False, True)])
def test_saved_flat_android_recipe_keeps_its_build_mode(monkeypatch, info_activity, sdl_clipboard):
"""Saving Plezy as known-good must not start an empty OVRPort conversion (#271)."""
from frameport.core import library
from frameport.recommend import catalog, engine
analysis = _analysis(package="org.example.savedflat", engine="Other", xr="?", libs=[],
has_info_category=info_activity, is_overport_output=False,
extra={"size": 1, "vr_kind": "none", "media_codec": True, "sdl_java": sdl_clipboard})
original = engine.suggest(analysis, use_catalog=False)
original.status, original.notes = "issues", "Some videos have color artifacts."
game = {"package": analysis.package, "title": "Flat app", "analysis": analysis.to_dict(),
"recipe": library.recipe_to_dict(original)}
catalog.save_user_entry(catalog.entry_from_library(game))
restored = engine.suggest(analysis)
assert restored.source == "catalog (user)"
assert not restored.overport and restored.as_is == original.as_is
assert restored.patches == original.patches
assert restored.status == "issues" and original.notes in restored.notes
assert "frame.hw_video_decode" in restored.patches and not engine.warnings(restored)
# Existing affected entries recover when the next app update re-derives their saved recipe.
broken = library.recipe_to_dict(original)
broken.update(as_is=False, overport=True, source="catalog (user)")
data = {"settings": {"recipes.app_version": "0.0.0"},
"games": {analysis.package: {**game, "recipe": broken}}}
monkeypatch.setattr(library, "REFRESH_ON_UPDATE", True)
assert library._follow_catalog(data)
repaired = library.recipe_from_dict(data["games"][analysis.package]["recipe"])
assert not repaired.overport and repaired.as_is == original.as_is
def test_saved_already_patched_android_recipe_keeps_install_as_is():
from frameport.core import library
from frameport.recommend import catalog, engine
analysis = _analysis(package="org.example.savedpatched", has_info_category=False,
extra={"size": 1, "frame_patched": True})
original = engine.suggest(analysis, use_catalog=False)
assert original.as_is
game = {"package": analysis.package, "title": "Patched app", "analysis": analysis.to_dict(),
"recipe": library.recipe_to_dict(original)}
catalog.save_user_entry(catalog.entry_from_library(game))
restored = engine.suggest(analysis)
assert restored.as_is
def test_patch_registry_is_complete_for_every_thread(monkeypatch):
"""A lookup from a second thread while the first is still importing the patch modules waits for them."""
import threading
@@ -776,6 +825,38 @@ def test_tbxr_vendor_on_lambda1vr():
assert count == 1 and b"\0alve\0" in fixed and len(fixed) == len(xash)
@pytest.mark.parametrize("gl4es", [True, False])
def test_tbxr_shim_leaves_desktop_draws_to_gl4es(tmp_path, quest_manifest, gl4es):
"""#191: the same shim must preserve GL4ES dispatch without removing other games' GLES hooks."""
from frameport.analysis import elf
from frameport.patches.frame import artifact
from frameport.patches.frame.tbxr_vendor import GL4ES_DRAW_EXPORTS
original = artifact("arm64-v8a", "libglshim.so")
patch = base.get("frame.tbxr_vendor")
a = _analysis(libs=["libxash.so"], extra={"tbxr_libs": ["libxash.so"]})
with ApkWorkspace(_apk(tmp_path, quest_manifest)) as ws:
ws.put(ws.lib("libglshim.so"), original)
if gl4es:
ws.put(ws.lib("libgl4es.so"), build_stub_library(["glDrawElements"], soname="libgl4es.so"))
ctx = base.ApkContext(ws, a, {}, Reporter(), {patch.id: {}})
if gl4es:
assert patch.validate(ctx)[0][1] is False
assert patch.apply(ctx)
shim = ws.read_lib("libglshim.so")
_, visible = elf.hide_exports(shim, GL4ES_DRAW_EXPORTS)
assert bool(visible) is not gl4es
if gl4es:
assert patch.validate(ctx)[0][1] is True
# Functions remain callable inside the shim; its GL queries and framebuffer fix stay intact.
assert elf.find_symbols(shim, GL4ES_DRAW_EXPORTS) == elf.find_symbols(original, GL4ES_DRAW_EXPORTS)
assert elf.find_symbols(shim, ["eglGetProcAddress", "glGetString", "glGetIntegerv"]) == \
elf.find_symbols(original, ["eglGetProcAddress", "glGetString", "glGetIntegerv"])
else:
assert shim == original
assert not patch.apply(ctx)
def test_unity_user_presence_points_the_xr_plugin_at_the_shim(tmp_path, quest_manifest):
"""BONELAB: OVRPlugin reported the worn headset as not worn and the Marrow rig froze (no tracking, no controls)."""
from frameport.analysis import elf
+28
View File
@@ -17,6 +17,20 @@ LOG_BAD = ("""
""")
def test_lua_pointer_error_and_cleanup_stack_are_distinguished():
pointer = triage("PANIC: bad light userdata pointer", "EXITED")
finding = next(f for f in pointer.findings if f.id == "lua-lightuserdata-pointer")
assert finding.suggest == ["frame.luajit_pointers"] and finding.severity == "fatal"
cleanup = ("FORTIFY: pthread_mutex_lock called on a destroyed mutex\n"
"libc.so (__cxa_finalize+148)\nlibc.so (exit+24)\n"
"libxlua.so (lj_err_throw+428)\nFatal signal 6 (SIGABRT)\n")
result = triage(cleanup, "EXITED")
finding = next(f for f in result.findings if f.id == "lua-startup-exit")
assert finding.suggest == [] # unhandled Lua errors have other causes too
assert "lua-lightuserdata-pointer" not in {f.id for f in result.findings}
assert "lua-startup-exit" not in {f.id for f in triage("Unknown Platform\n" + LOG_OK, "RUNNING").findings}
def test_healthy_log():
r = triage(LOG_OK, "RUNNING", "com.example.game")
assert r.verdict == "pass"
@@ -119,6 +133,20 @@ def test_crash_logcat_signatures():
assert [(f.id, f.suggest) for f in r.findings] == [("swapchain-size-abort", ["frame.swapchain_limit"])]
def test_tbxr_fdm_crash_is_identified_as_vr_teardown():
log = "F libc: Fatal signal 11 (SIGSEGV), fault addr 0x0 in AppThreadFunction\n"
crash = ("#00 libVkLayer_VALVE_fdm_injection.so (fdm_injection_xrDestroySwapchain+136)\n"
"#01 libopenxr_loader_generic.so (xrDestroySwapchain+2600)\n"
"#03 libxash.so (ovrRenderer_Destroy+44)\n"
"#04 libxash.so (TBXR_LeaveVR+76)\n")
r = triage(log, "EXITED", "com.drbeef.lambda1vr", crash=crash)
assert [f.id for f in r.findings] == ["tbxr-fdm-shutdown-crash"]
assert r.suggestions() == [] # no unverified graphics or foveation fix
assert [f.id for f in triage(log, "EXITED", crash=crash.replace("TBXR_LeaveVR", "OtherCall")).findings] == \
["native-crash"]
assert not triage(crash, "RUNNING").findings # symbol names alone are not a crash
def test_ovrplugin_extension_probe_is_not_a_scene_game():
probe = LOG_OK + "09-28 17:39:02.200 1147 1174 I OVRPlugin: Unavailable OpenXR extension: XR_FB_scene\n"
assert "scene-missing" not in {f.id for f in triage(probe, "RUNNING", "com.example.game").findings}