Compare commits

...
Author SHA1 Message Date
spoopyghosty0andClaude Opus 5.5 903d884b34 Catalog: KillerFrequency and cubism (works)
Closes #402
Closes #403

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZSCqaPGwuUf4vdLQdVT4J
2026-10-18 14:23:19 -04:00
spoopyghosty0andClaude Opus 5.5 a8f99929ba Gamepad for 2D apps: own button layout (swap sticks, swap A/B, own .kl lines) and Steam's controller log in diagnostics (#162); fix main's CI
Steam Input's per-game remapping doesn't reach a 2D app; device.steam_gamepad's options now change the buttons
inside the app: the launcher exports FRAMEPORT_GAMEPAD_LAYOUT/_KEYS, the agent's Podman wrapper mounts a per-game
key layout (agent v103). Diagnostics of such games carry gamepad.log and Steam's controller activation lines. The
game page shows a patch's on/off options under it while it's on.
CI: locales template refreshed; frame.main_process counts as a patch that works on the 32-bit bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZSCqaPGwuUf4vdLQdVT4J
2026-10-18 12:57:04 -04:00
spoopyghosty0andClaude Opus 5.5 d04179502d Triage scene-missing: extension listings don't count (OVRPlugin Name=, OVRPort's list, Unity's Extensions=) (#404)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZSCqaPGwuUf4vdLQdVT4J
2026-10-18 12:40:10 -04:00
spoopyghosty0andClaude Opus 5.5 4af1a8e987 frame.main_process: run a game activity that has its own process in the main one (#178)
Halo CE VR's HaloActivity runs in :halo_game; Lepton starts it directly and watches only the main process, which
never exists, so it closed the game about a minute in ('Target app ... did not start'). Analysis field
separate_process (ANALYSIS_VERSION 23), default-on patch, triage app-separate-process, PLAYBOOK row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZSCqaPGwuUf4vdLQdVT4J
2026-10-18 12:35:48 -04:00
19 changed files with 429 additions and 36 deletions

No files matched your search

+56 -6
View File
@@ -46,7 +46,7 @@ try:
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
fcntl = None
AGENT_VERSION = 102
AGENT_VERSION = 103
HOME = os.path.expanduser("~")
STEAM = os.path.join(HOME, ".local/share/Steam")
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
@@ -3634,6 +3634,38 @@ axis 0x10 HAT_X
axis 0x11 HAT_Y
"""
BTN_SOUTH = 0x130 # BTN_A / BTN_GAMEPAD: Linux's gamepad button range starts here
# A game's own button layout (device.steam_gamepad's options, GitHub #162: Steam Input's per-game remapping doesn't
# reach a 2D app): its launcher exports FRAMEPORT_GAMEPAD_LAYOUT (comma list: swap_sticks = the right stick is
# Android's main stick, which Android's own D-pad navigation follows; swap_ab) and FRAMEPORT_GAMEPAD_KEYS (Android .kl
# lines separated by ';', each replacing the line for the same key code or axis), read by podman_run_args.
KL_LINE = re.compile(r"(key [0-9]{1,4} [A-Z][A-Z0-9_]{0,40}( (FUNCTION|WAKE))?"
r"|axis 0x[0-9a-f]{1,2} (invert )?[A-Z][A-Z0-9_]{0,40}( flat [0-9]{1,5})?)")
def gamepad_kl(layout="", keys=""):
"""GAMEPAD_KL with a game's layout options and own lines (invalid ones left out); the text and what was left out."""
lines = {}
for line in GAMEPAD_KL.splitlines()[1:]:
kind, code = line.split()[:2]
lines[(kind, code if kind == "axis" else str(int(code)))] = line
opts = {o.strip() for o in (layout or "").split(",") if o.strip()}
if "swap_sticks" in opts:
lines[("axis", "0x00")], lines[("axis", "0x03")] = "axis 0x00 Z flat 4096", "axis 0x03 X flat 4096"
lines[("axis", "0x01")], lines[("axis", "0x04")] = "axis 0x01 RZ flat 4096", "axis 0x04 Y flat 4096"
if "swap_ab" in opts:
lines[("key", "304")], lines[("key", "305")] = "key 304 BUTTON_B", "key 305 BUTTON_A"
skipped = []
for line in (keys or "").split(";"):
line = " ".join(line.split())
if not line:
continue
if not KL_LINE.fullmatch(line):
skipped.append(line)
continue
kind, code = line.split()[:2]
lines[(kind, code if kind == "axis" else str(int(code)))] = line
head = GAMEPAD_KL.splitlines()[0] + (f" Options: {layout or '-'}; own lines: {keys}" if opts or keys else "")
return "\n".join([head, *lines.values()]) + "\n", skipped
def has_key_bit(caps, code):
@@ -3706,7 +3738,13 @@ def podman_run_args(args, env, sys_root="/sys", dev_root="/dev"):
extra = []
if PAD_DIR not in taken:
extra += ["--mount", f"type=bind,source={os.path.join(dev_root, 'input')},destination={PAD_DIR},rw"]
layout = gamepad_layout_file() if extra else ""
text, skipped = gamepad_kl(env.get("FRAMEPORT_GAMEPAD_LAYOUT", ""), env.get("FRAMEPORT_GAMEPAD_KEYS", ""))
layout = gamepad_layout_file(text) if extra else ""
if skipped:
print(f"FramePort gamepad: key layout lines left out (not .kl syntax): {'; '.join(skipped)}", file=sys.stderr)
if text != GAMEPAD_KL:
print(f"FramePort gamepad: own button layout ({env.get('FRAMEPORT_GAMEPAD_LAYOUT') or '-'}"
f"{'; ' + env['FRAMEPORT_GAMEPAD_KEYS'] if env.get('FRAMEPORT_GAMEPAD_KEYS') else ''})", file=sys.stderr)
for product in sorted({"11ff", *(p["product"] for p in pads)}): # 11ff: Steam's pad, also when it comes later
target = f"/system/usr/keylayout/Vendor_28de_Product_{product}.kl"
if layout and target not in taken and re.fullmatch(r"[0-9a-f]{4}", product):
@@ -3754,18 +3792,21 @@ def gamepad_links(appid, parent, poll=1.0, refresh=15.0, exec_=None, pads=steam_
time.sleep(poll)
def gamepad_layout_file():
path = os.path.join(os.path.dirname(PODMAN_BIN), "steam-gamepad.kl")
def gamepad_layout_file(text=GAMEPAD_KL):
"""The key layout file for `text`: steam-gamepad.kl, or one per distinct own layout (steam-gamepad-<hash>.kl)."""
name = "steam-gamepad.kl" if text == GAMEPAD_KL else \
f"steam-gamepad-{hashlib.sha256(text.encode()).hexdigest()[:12]}.kl"
path = os.path.join(os.path.dirname(PODMAN_BIN), name)
try:
with open(path) as f:
if f.read() == GAMEPAD_KL:
if f.read() == text:
return path
except OSError:
pass
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = f"{path}.{os.getpid()}.tmp"
with open(tmp, "w") as f:
f.write(GAMEPAD_KL)
f.write(text)
os.replace(tmp, path)
return path
@@ -6882,6 +6923,15 @@ def cmd_collect_diag(args):
for i, part in enumerate(game_logs(base)):
files[f"game-log-{i}.txt"] = part[-max_bytes:]
logs = os.path.join(STEAM, "logs")
if dep.get("steam_gamepad"): # which controller layout Steam activated, and the pads the container got (#162)
text = _tail(os.path.join(base, "gamepad.log"), max_bytes)
if text is not None:
files["gamepad.log"] = text
text = _tail(os.path.join(logs, "controller.txt"), 4 * max_bytes)
if text:
keep = [ln for ln in text.splitlines() if re.search(
r"Queueing activation|warmed config|Opted-in Controller|Loaded Config|virtual gamepad|xinput slot", ln)]
files["steam-controller.txt"] = "\n".join(keep[-400:]) + "\n"
text = _tail(os.path.join(logs, f"lepton-steamlaunch-{appid}.log"), max_bytes) # Lepton's launcher log
if text is not None:
files[f"lepton-steamlaunch-{appid}.log"] = text
+23
View File
@@ -0,0 +1,23 @@
package: com.mrf.tvb.cubism
title: cubism
status: works
notes: works and passthrough play works also
tested_version: 1.8.0
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-17'
known_good_sha256: fd1ba8eae19efe0f2a17fc146847de677263125f9ec5437cea5c632f0cd88dcf
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 91
issue: 403
source_hint: Cubism
@@ -0,0 +1,26 @@
package: com.team17.killerfrequency
title: KillerFrequency
status: works
tested_version: 1.0.767.38513
engine: Unity
xr: VrApi
frame:
- frame.unity_no_msaa
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.hw_video_decode
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
scale: 1.4000000000000001
verified:
date: '2026-10-18'
known_good_sha256: a11b008fcc0f5bc7aa5a3bb04d4267144d5ec60b26529a9b20e6499e12dcf079
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 402
source_hint: killer freq
+10 -2
View File
@@ -383,8 +383,10 @@ signatures:
diagnosis: Frame controller models are on, but the converted models are missing from the game's files. The agent found no Frame controller render models in SteamVR at install time; see the install log, or run the agent's controller_models command.
suggest: []
- id: scene-missing
# not OVRPlugin's routine "Unavailable OpenXR extension: XR_FB_scene" (printed by every OVRPlugin game)
pattern: '(?<!Unavailable OpenXR extension: )XR_FB_scene|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
# not extension listings, which every Meta XR game prints: OVRPlugin's "Unavailable OpenXR extension:" and
# "Name=XR_FB_scene SpecVersion=1", OVRPort's "overportOXR: - XR_FB_scene 1", Unity's 'Meta XR Feature: ...
# Extensions="..."' (GitHub #404); XR_FB_scene_capture alone doesn't count either
pattern: '(?m)^(?!.*(?:Unavailable OpenXR extension: |Name=|overportOXR: - |Extensions=")).*XR_FB_scene\b|XR_FB_spatial_entity.*(not supported|UNSUPPORTED)|xrQuerySpacesFB'
severity: warning
diagnosis: The game wants Meta's room data (scene API), which the Frame lacks. The adapter can emulate a room from the guardian bounds.
suggest: [adapter.scene_emul]
@@ -559,6 +561,12 @@ signatures:
severity: fatal
diagnosis: 'Games can''t start until the Frame restarts: it ran out of kernel keys. Rootless podman leaks one session keyring per container start; the FramePort agent sets keyring=false in ~/.config/containers/containers.conf, and a reboot frees the leaked keys.'
suggest: []
- id: app-separate-process
pattern: 'Start proc \d+:[\w.]+:\w+/\S+ for (pre-)?top-activity'
severity: error
diagnosis: The game's activity runs in an Android process of its own, while Lepton watches only the app's main process, which never starts. About a minute in, Lepton decides the app "did not start" and closes it, wherever the game is. Newer FramePort versions run the activity in the main process (patch "Start the game in the app's main process"); update FramePort and use Update on Frame.
suggest: []
supersedes: [app-not-started, force-quit]
- id: app-not-started
pattern: 'Target app \(.*\) did not start'
severity: error
+2
View File
@@ -42,6 +42,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Creed | Quest | ✅ Works | |
| Crisis Zone VR | Quest | ✅ Works | |
| cubism | Quest | ✅ Works | |
| cubism | Quest | ✅ Works | |
| Dance Central | Quest | ✅ Works | |
| Deep Cuts | Quest | ✅ Works | |
| Demeo | Quest | ✅ Works | |
@@ -77,6 +78,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Jurassic World Aftermath Collection | Quest | ✅ Works | |
| Keep Talking and Nobody Explodes | Quest | ✅ Works | |
| Kill It With Fire VR | Quest | ✅ Works | |
| KillerFrequency | Quest | ✅ Works | |
| Ladder | Quest | ✅ Works | |
| Lambda1VR | Quest | ✅ Works | |
| LEGO® Bricktales | Quest | ✅ Works | |
+37 -21
View File
@@ -2,7 +2,7 @@
# Patches
Every patch and game setting FramePort knows: 186 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: 187 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) (61)
- [Steam Frame patches](#group-frame) (62)
- [Game settings](#group-adapter) (77)
- [Files and settings on the Frame](#group-device) (10)
- [PC VR (Proton and Revive)](#group-pcvr) (13)
@@ -58,11 +58,12 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [GL shim (Mesa GLSL compatibility)](#frame-gl_shim) | `frame.gl_shim` | Steam Frame patches | 2 |
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | 1 |
| [Godot apps: start without a clipboard service](#frame-godot_clipboard) | `frame.godot_clipboard` | Steam Frame patches | 1 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 34 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 35 |
| [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 | 1 |
| [Start the game in the app's main process](#frame-main_process) | `frame.main_process` | Steam Frame patches | default |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 4 |
| [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 |
@@ -80,13 +81,13 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Allow swapchains larger than 4096 px](#frame-swapchain_limit) | `frame.swapchain_limit` | Steam Frame patches | default |
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 1 |
| [Unity App UI: no Android 12 vibrator](#frame-unity_appui_vibrator) | `frame.unity_appui_vibrator` | Steam Frame patches | |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 26 |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 27 |
| [Unity: render each eye separately (no multiview)](#frame-unity_multipass) | `frame.unity_multipass` | Steam Frame patches | 2 |
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 10 |
| [Unity: disable MSAA](#frame-unity_no_msaa) | `frame.unity_no_msaa` | Steam Frame patches | 11 |
| [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 | 14 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 32 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 118 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 33 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 119 |
| [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 |
@@ -137,7 +138,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 | 103 |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 104 |
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
| [Turn off Quest Vulkan foveation](#adapter-hide_fb_foveation) | `adapter.hide_fb_foveation` | Game settings | |
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 5 |
@@ -158,9 +159,9 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 3 |
| [Wait for the game's GPU](#adapter-release_wait) | `adapter.release_wait` | Game settings | |
| [Re-create play space](#adapter-respace_kick) | `adapter.respace_kick` | Game settings | |
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 14 |
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 15 |
| [Room depth](#adapter-scene_depth) | `adapter.scene_depth` | Game settings | |
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 12 |
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 13 |
| [Room height](#adapter-scene_height) | `adapter.scene_height` | Game settings | |
| [Room width](#adapter-scene_width) | `adapter.scene_width` | Game settings | |
| [Eye-image snapshots (diagnostics)](#adapter-snapshot) | `adapter.snapshot` | Game settings | |
@@ -192,7 +193,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Game language](#device-language) | `device.language` | 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 | 121 |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 122 |
| [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) | |
@@ -693,7 +694,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 (34):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 26 more.
- **Catalog games (35):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 27 more.
- **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.
@@ -748,6 +749,17 @@ Lets games with a verified older Lua engine get past a startup crash on the Fram
> 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-main_process"></a>Start the game in the app's main process
Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).
- `frame.main_process` · on by default · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** … runs in its own process, which Lepton doesn't watch: it would close the game about a minute in.
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
> Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's ":halo_game"), started from a 2D launcher activity. Lepton starts the game activity directly and watches only the app's main process, which then never exists: about a minute in, Lepton decides the app "did not start" and closes it, at whatever point the game has reached. This runs those activities in the main process (only their android:process value changes).
### <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.
@@ -981,7 +993,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 (26):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 18 more.
- **Catalog games (27):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 19 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)
@@ -1008,7 +1020,7 @@ Turns off a smoothing setting that crashes some older Unity games on the Frame.
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Legacy VrApi Unity game on GLES with MSAA (… quality levels): multisampled render-to-texture hangs the Frame's GL driver (for example Sniper Elite VR).
- **Offered (off):** GLES Unity game with MSAA on (… quality levels); enable if it hangs the GPU ('zink: DEVICE LOST').
- **Catalog games (10):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, Mini Motor Racing X, Sniper Elite VR and 2 more.
- **Catalog games (11):** Audica, BattleSisters, Eye of the Temple, I Expect You To Die 2, IEYTD3, Jurassic World Aftermath Collection, KillerFrequency, Mini Motor Racing X and 3 more.
- **Suggested by launch-test signatures:**
- `zink-device-lost` (fatal): The GPU hung, so the game froze or closed.
- **Source:** [patches/frame/unity_no_msaa.py](../src/frameport/patches/frame/unity_no_msaa.py)
@@ -1050,7 +1062,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 (32):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 24 more.
- **Catalog games (33):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 25 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.
@@ -1066,7 +1078,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 (118):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 110 more.
- **Catalog games (119):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 111 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.
@@ -1794,7 +1806,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 (103):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 95 more.
- **Catalog games (104):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 96 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.
@@ -2102,7 +2114,7 @@ Changes the game's render resolution. Higher is sharper but slower; 1.0× is the
- **Options:**
- Value: float, default `1.0` (always written).
- Game settings → Picture → "Render resolution (supersampling)": slider 0.5–2 in steps of 0.1.
- **Catalog games (14):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, Spectre, Survival Nation and 6 more.
- **Catalog games (15):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, KillerFrequency, Spectre and 7 more.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels.
@@ -2132,7 +2144,7 @@ Mixed-reality games that build their level from your room get a simple room with
- **Options:**
- Value: int, default `0`.
- Game settings → Mixed reality → "Pretend room layout".
- **Catalog games (12):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord, Neko Atsume Purrfect and 4 more.
- **Catalog games (13):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, cubism, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord and 5 more.
- **Suggested by launch-test signatures:**
- `scene-missing` (warning): The game wants Meta's room data (scene API), which the Frame lacks.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
@@ -2590,9 +2602,13 @@ Lets the game use your controllers as a gamepad instead of a pointer, for 2D gam
- `device.steam_gamepad` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested (on):** 2D app that declares gamepad support (android.hardware.gamepad or Android TV's launcher): pass the controllers to it as a gamepad.
- **Options:**
- `swap_sticks`: bool, default `False` — Swap the sticks: the right stick moves through menus like the left one (for using one controller)
- `swap_ab`: bool, default `False` — Swap A and B
- `keys`: str, default empty — Own key layout lines (Android .kl), separated by ';', e.g. 'key 304 DPAD_DOWN' (A presses D-pad down) or 'key 305 BACK'
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.
> Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad ("Microsoft X-Box 360 pad", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may not reach the app (GitHub #162); the options below change the buttons inside the app instead (applied with Update on Frame).
### <a id="device-text_input_window"></a>Show the app's Android window (for typing)
@@ -2601,7 +2617,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 (121):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 113 more.
- **Catalog games (122):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 114 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".
+1
View File
@@ -60,6 +60,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| 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 |
| The whole Frame slows to a halt, then the game is killed (`Out of memory: Killed process … anon-rss:10+ GB`), for example Lucky's Tale | The game's own memory grows ~1 GB/s on Unity's `Loading.PreloadManager` thread (perf page-fault profile); not the flat window, not the text-input patches | none yet: mark unsupported; profile with `perf record -p <pid> -e page-faults --call-graph fp` (works as steamos, `perf_event_paranoid` 2) |
| A Linux app exits at once: `cannot execute binary file: Exec format error` in launch.log | an x86_64 program started directly (installed before FramePort ran x86 builds through FEX, or by hand) | reinstall it: x86_64 Linux apps install FEX on the Frame and start through it (agent v61, `kind: linux_x86`; FEX also needs `STEAM_COMPAT_DATA_PATH`: "No compat data path?" in launch.log = an agent v60 launcher, reinstall); triage `linux-x86-no-fex`. Prefer an arm64 build when the app offers one |
| The game closes about a minute after starting, at a different spot each time; `Start proc …:<pkg>:<name> for pre-top-activity`, then `Target app (<pkg>) did not start` (GitHub #178) | The started activity runs in its own process (`android:process`); Lepton watches only the main process (triage `app-separate-process`) | `frame.main_process` (automatic) |
| `APP_ACTIVITY is empty`, nothing starts | Manifest has category INFO only; Lepton needs LAUNCHER | `frame.launcher` (automatic) |
| PC VR game on the Frame shows as a flat window / Revive: `Unable to load LibOVRRT DLL` / `LoaderInstance::CreateInstance chained CreateInstance call failed` | Frame SteamVR runtime rejects OpenXR apiVersion 1.1 (`XR_ERROR_API_VERSION_UNSUPPORTED`), which Proton 11's VR helper requests | `pcvr.xr_timefix` (Frame OpenXR layer, default on): retries xrCreateInstance as 1.0 |
| Unreal PC VR game on the Frame runs as a flat window although OpenXR works (no `LogHMD` OVRPlugin lines; Unreal logs nothing when it skips the Oculus plugin) / launch.log: `FramePort oculushmd: could not create the OculusHMDConnected event` | UE's OculusHMD (and LibOVR's `ovr_Detect`) only start when the Windows event `OculusHMDConnected` exists and is signaled; on a PC the Oculus service creates it. Revive hooks `OpenEventW` for it, but that relies on Detours patching Wine's (ARM64EC) kernelbase | `pcvr.oculus_unreal` (default for Unreal Rift games; PC VR counterpart of overport's `patch_oculus_unreal`): launch.sh runs the injector through `fp_oculushmd.exe`, which provides the real event until the game exits |
+5
View File
@@ -524,6 +524,11 @@
"title": "Fix Lua startup crashes on Frame",
"group": "Steam Frame patches"
},
"frame.main_process": {
"anchor": "frame-main_process",
"title": "Start the game in the app's main process",
"group": "Steam Frame patches"
},
"frame.meta_permissions": {
"anchor": "frame-meta_permissions",
"title": "Declare Meta-only permissions",
+5 -1
View File
@@ -54,7 +54,9 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
# USB MIDI driver polls the missing USB service: frame.usb_midi_watcher, GitHub #297) (2026-10)
# 22: unreal_binned3 (Unreal built with FMallocBinned3 reserves tens of GiB at load: frame.unreal_noreserve,
# GitHub #349) (2026-10)
ANALYSIS_VERSION = 22
# 23: separate_process (MAIN activities in another process than the main one, which Lepton doesn't watch:
# frame.main_process, GitHub #178) (2026-10)
ANALYSIS_VERSION = 23
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
@@ -462,6 +464,8 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
"unity_oculus_check": b"\0com.oculus.systemactivities\0" in lib_bytes.get("libunity.so", b""),
# a 2D launcher activity that starts a separate VR activity (frame.start_activity)
"vr_activity": axml.vr_activity(manifest),
# a MAIN activity in its own process: Lepton watches only the main one (frame.main_process)
"separate_process": axml.separate_process_activities(manifest),
"unity_version": unity_version(ggm, lib_bytes.get("libunity.so")) if engine == "Unity" else None,
# the minimum Android version the APK declares (read from the original: OVRPort lowers it to 29)
"min_sdk": axml.min_sdk(manifest),
+47
View File
@@ -376,6 +376,53 @@ def vr_activity(manifest: bytes) -> str | None:
return vr if vr and vr != launcher else None
def _package(x: Axml) -> str | None:
return next((x.attr_str(el, "package") for el in x.elements() if el.name == "manifest"), None)
def separate_process_activities(manifest: bytes) -> list[str]:
"""Activities with a MAIN intent-filter that run in another process than the app's main one (android:process
":name" or another package's name). Lepton only watches the main process: started directly, such an activity
leaves none, and Lepton closes the app about a minute later ("Target app ... did not start", GitHub #178)."""
x = Axml(manifest)
package = _package(x)
process = {}
for el in x.elements():
if el.name == "activity":
process[x.attr_str(el, "name") or ""] = x.attr_str(el, "process")
names = x.strings()
out = []
for el, name, items in component_filters(manifest):
proc = process.get(name) if el == "activity" else None
if proc and proc != package and any(k == "action" and i < len(names) and names[i] == MAIN
for k, _, i in items):
out.append(name)
return out
def set_main_process(manifest: bytes, activities: list[str]) -> bytes | None:
"""Run `activities` in the app's main process: their android:process becomes the package name (only those
elements' attribute values change). None when there is nothing to change."""
x = Axml(manifest)
package = _package(x)
if not package:
return None
hits = [(x.attr_str(el, "name"), el) for el in x.elements() if el.name == "activity"]
if not any(n in activities and x.attr_str(el, "process") not in (None, package) for n, el in hits):
return None
index = x.index(package)
if index is None:
index = x.append_string(package)
names = x.strings()
for el in x.elements(): # again: append_string moved the elements
if el.name != "activity" or x.attr_str(el, "name") not in activities:
continue
for a in el.attrs:
if a.name < len(names) and names[a.name] == "process":
_set_string_attr(x, a.offset, index)
return x.bytes()
def set_start_activity(manifest: bytes, activity: str) -> bytes | None:
"""Make `activity` the one Lepton starts: its MAIN intent-filter gets category LAUNCHER (in place of its VR or
DEFAULT category), every other component's LAUNCHER category becomes INFO. Only those attribute values change
+5 -2
View File
@@ -869,6 +869,7 @@
"Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.": "",
"Keeps one label so the title is stable.": "",
"Keeps one name for the game, so it shows the same everywhere.": "",
"Keeps the game from being closed about a minute after it starts (it runs in a separate Android process the Frame doesn't watch).": "",
"Keeps the hand and brush visible over Vermillion's painting canvas (the canvas is drawn in the scene instead of as a layer in front).": "",
"Keeps the resolution the game saves in step with the headset's picture size (else the picture is shifted, with black borders, after a render resolution change).": "",
"Keeps three snapshots per game on the Frame and copies new ones here when connected.": "",
@@ -907,7 +908,7 @@
"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\".": "",
"Lepton's Android has no clipboard service, and SDL's Java code (SDL2 and LÖVE games, for example Dramatic Shape) registers a clipboard listener at start without checking, so the app crashes with a NullPointerException in SDLClipboardHandler. That call is skipped (an in-place edit of classes.dex).": "",
"Lepton's Android has no clipboard service, and the Android code of Godot 4.2-4.4 (for example EndoparasiticVR) casts the missing service to a ClipboardManager without checking, so the app crashes at start with a NullPointerException in Godot.<init>. That check is skipped and Godot's clipboard functions do nothing instead (an in-place edit of classes.dex).": "",
"Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad (\"Microsoft X-Box 360 pad\", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.": "",
"Lepton's Android only gets a keyboard, a pointer and touch from the Frame, so a 2D game never sees a controller (the controllers only move the pointer; SDL games show their touch controls). This passes Steam Input's virtual gamepad (\"Microsoft X-Box 360 pad\", made by Steam for the Frame's controllers and paired Bluetooth pads) into this app's container with an Xbox 360 key layout, and tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes later isn't seen until the next start). The APK isn't changed. Off for one start: FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may not reach the app (GitHub #162); the options below change the buttons inside the app instead (applied with Update on Frame).": "",
"Lets apps with a USB MIDI driver start on the Frame (they'd crash looking for USB devices); USB MIDI devices aren't available there.": "",
"Lets games built on Meta's oldest VR interface run on the Frame.": "",
"Lets games built with Unity's App UI start on the Frame (they'd quit at once on an Android 12 vibrator call).": "",
@@ -952,7 +953,7 @@
"Live view": "",
"Loading…": "",
"Loads FramePort's OpenXR layer under Proton on the Frame. The Frame's SteamVR runtime only accepts OpenXR 1.0 apps, but Proton's VR helper asks for 1.1, so without the layer VR never starts (the game shows as a flat window, or Revive fails with 'Unable to load LibOVRRT DLL'); the layer retries as 1.0. It also emulates xrConvertTimespecTimeToTimeKHR if a runtime refuses it. Ignored on this PC.": "",
"Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer adds itself to Android's Vulkan debug layer list for the game's process only.": "",
"Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), for example stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe, and either setting brings it in by itself. The layer adds itself to Android's Vulkan debug layer list for the game's process only.": "",
"Loads libglshim.so ahead of libEGL in every engine library that fetches GL through eglGetProcAddress. Fixes Quest-only leniency that Mesa/Zink rejects: #pragma before #extension, implicit int/float conversions (enables GL_EXT_shader_implicit_conversions), and multiview shaders used on single-view framebuffers (hides GL_OVR_multiview; gl_hide_multiview=0 turns that off). Logs every failed shader with its source lines (logcat tag GLShim). Symptom: black screen with working audio in a GLES game.": "",
"Loads the GL shim into a Unity GLES game and hides GL_EXT_multisampled_render_to_texture (and the multiview variant). Some Unity games switch their eye buffer to 4x MSAA at runtime; the Frame only gives single-sampled swapchains, Unity then renders through multisampled render-to-texture and Mesa/Zink crashes (SIGSEGV in libgallium_dri.so on the render thread: gray or frozen screen, for example The Room VR). Hidden, Unity uses an ordinary MSAA buffer and resolves it. Unity's multiview stays on (gl_hide_multiview defaults to 0 here).": "",
"Loads the GL shim into an Unreal GLES game and hides GL_EXT_multisampled_render_to_texture. Unreal's mobile MSAA renders through multisampled render-to-texture, and the Frame's GL driver (Mesa/Zink) can crash on it: SIGSEGV with fault address 0x10000 in libgallium_dri.so on the RHIThread a few seconds after start (Zink looks up a render-pass cache slot past its end, for example Star Wars Pinball VR). Hidden, Unreal renders without that path. Unreal's multiview stays on: Unreal only uses it when GL_OVR_multiview_multisampled_render_to_texture is listed too, so for Unreal the shim keeps that one visible and draws its multiview MSAA attachments single-sampled (glFramebufferTextureMultiviewOVR).": "",
@@ -1550,6 +1551,7 @@
"Smooth as butter": "",
"software encoder": "",
"Some apps open a flat Android launcher that then starts a separate VR activity (for example WiiCompiled). Shown with Lepton's flat window, that window stays in view after the VR part starts. This makes the VR activity the one Lepton starts (its LAUNCHER category), so the app opens in VR with no flat window. Everything only the launcher does (choosing or importing content) is skipped: turn it off for a run when you need the launcher, and turn off \"Show the app's Android window\".": "",
"Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's \":halo_game\"), started from a 2D launcher activity. Lepton starts the game activity directly and watches only the app's main process, which then never exists: about a minute in, Lepton decides the app \"did not start\" and closes it, at whatever point the game has reached. This runs those activities in the main process (only their android:process value changes).": "",
"Some engines contain assembly (for example OpenSSL's) that checks a return-address signature it never made. The Quest's CPU ignores those checks, the Steam Frame's enforces them: the game crashes with SIGILL as soon as it uses that code, for example on its first network connection (Star Wars Pinball VR). In a library with unpaired checks, the signing and checking instructions become no-ops.": "",
"Some engines leave the pNext of unused Vulkan attachment references uninitialized. The Frame's driver never reads it, but Lepton always loads Steam's Fossilize shader-cache layer, which follows it and crashes on the first frame (SIGSEGV in libVkLayer_fossilize.so from FVulkanRenderPass, for example Deadpool VR). Loads Vulkan through a small shim that keeps valid pointers and drops only unreadable ones or ones pointing at the wrong structure type. It also drops a depth resolve named in a subpass without a depth attachment, which crashes the Frame's driver at the first render pass (for example Metro Awakening).": "",
"Some game folders are already set up for SteamVR: a Revive copy (LibRevive64.dll) next to the game, started by a small loader DLL (for example xinput1_3.dll) that Windows loads from the game's folder. Then FramePort starts the game program directly — like double-clicking it — instead of through its own Revive (two Revives conflict; for example the Oculus Platform check then fails). On the Frame, Wine is told to use those loader DLLs from the game folder instead of its own.": "",
@@ -1576,6 +1578,7 @@
"Start live view": "",
"Start setup": "",
"Start straight in VR (skip the 2D launcher)": "",
"Start the game in the app's main process": "",
"Start the live view to watch the headset here.": "",
"Start the live view to watch the headset in an mpv window.": "",
"Start the live view to watch the headset in your browser.": "",
@@ -146,7 +146,43 @@ class StartActivity(Patch):
return [("Only the VR activity is a launcher", ok, ", ".join(starts))]
class MainProcess(Patch):
id = "frame.main_process"
a32 = True
needs_vr = False
title = "Start the game in the app's main process"
description = ("Some apps run their game activity in a process of its own (android:process, e.g. Halo CE VR's "
"\":halo_game\"), started from a 2D launcher activity. Lepton starts the game activity directly "
"and watches only the app's main process, which then never exists: about a minute in, Lepton "
"decides the app \"did not start\" and closes it, at whatever point the game has reached. This "
"runs those activities in the main process (only their android:process value changes).")
order = 21
default_on = True
def applies(self, a):
return bool((a.extra or {}).get("separate_process"))
def detect(self, a):
found = (a.extra or {}).get("separate_process")
if not found:
return None
return Suggestion(True, f"{found[0].rsplit('.', 1)[-1]} runs in its own process, which Lepton doesn't "
"watch: it would close the game about a minute in.")
def apply(self, ctx: ApkContext) -> bool:
manifest = ctx.ws.read(MANIFEST)
fixed = axml.set_main_process(manifest, axml.separate_process_activities(manifest))
if fixed:
ctx.ws.put(MANIFEST, fixed)
return bool(fixed)
def validate(self, ctx):
left = axml.separate_process_activities(ctx.ws.read(MANIFEST))
return [("Game activities run in the main process", not left, ", ".join(left) or "all")]
register(Launcher)
register(NoDebuggable)
register(MetaPermissions)
register(StartActivity)
register(MainProcess)
+23 -2
View File
@@ -824,10 +824,21 @@ class SteamGamepad(Patch):
"tells SDL games to use it (SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD). Steam Input must be "
"on for the game's shortcut; the controllers must be on when the game starts (a pad Steam makes "
"later isn't seen until the next start). The APK isn't changed. Off for one start: "
"FRAMEPORT_NO_GAMEPAD=1 in the game's launch options.")
"FRAMEPORT_NO_GAMEPAD=1 in the game's launch options. Steam Input's own per-game remapping may "
"not reach the app (GitHub #162); the options below change the buttons inside the app instead "
"(applied with Update on Frame).")
category = "device"
needs_vr = False
stage = "install" # the agent reads it from the recipe at finalize (deployment.json steam_gamepad)
params = [
Param("swap_sticks", "bool", False, "Swap the sticks: the right stick moves through menus like the left one "
"(for using one controller)"),
Param("swap_ab", "bool", False, "Swap A and B"),
Param("keys", "str", "", "Own key layout lines (Android .kl), separated by ';', e.g. 'key 304 DPAD_DOWN' "
"(A presses D-pad down) or 'key 305 BACK'"),
]
KL_LINE = re.compile(r"(key [0-9]{1,4} [A-Z][A-Z0-9_]{0,40}( (FUNCTION|WAKE))?"
r"|axis 0x[0-9a-f]{1,2} (invert )?[A-Z][A-Z0-9_]{0,40}( flat [0-9]{1,5})?)")
def applies(self, a):
return a.vr_kind == "none"
@@ -839,7 +850,17 @@ class SteamGamepad(Patch):
return None
def install(self, ctx: InstallContext) -> None:
pass # the launcher line (agent: GAMEPAD_LINE) exports SDL's hint and puts the Podman wrapper on PATH
# the launcher line (agent: GAMEPAD_LINE) exports SDL's hint and puts the Podman wrapper on PATH; the wrapper
# builds the app's key layout from these (agent: gamepad_kl)
layout = ",".join(k for k in ("swap_sticks", "swap_ab") if ctx.params.get(k))
lines = [" ".join(ln.split()) for ln in str(ctx.params.get("keys") or "").split(";") if ln.strip()]
bad = [ln for ln in lines if not self.KL_LINE.fullmatch(ln)]
if bad:
raise ValueError(f"Not a key layout line: {bad[0]!r} (for example 'key 304 DPAD_DOWN')")
if layout:
ctx.env["FRAMEPORT_GAMEPAD_LAYOUT"] = layout
if lines:
ctx.env["FRAMEPORT_GAMEPAD_KEYS"] = "; ".join(lines)
def foveation_options(params):
+2
View File
@@ -40,6 +40,8 @@ SUMMARIES = {
"frame.adapter": "FramePort's layer between the game and the Steam Frame: fills in what the Frame lacks "
"(controllers, room, passthrough). Needed for every converted game.",
"frame.launcher": "Makes the game start from the Frame's Steam library.",
"frame.main_process": "Keeps the game from being closed about a minute after it starts (it runs in a separate "
"Android process the Frame doesn't watch).",
"frame.start_activity": "Opens the app straight in VR, skipping its flat launcher screen. Turn off to use the "
"launcher, for example to import new content.",
"frame.gl_multiview_fbo": "Experimental: shows HUDs, menus and other flat panels that stay black on the Frame "
+17 -1
View File
@@ -796,6 +796,14 @@ class GameView:
app.open_game(package, advanced=True, show_all=self.show_all) # the switch shows it's on now
return handler
def set_flag(pid, key):
def handler(e):
r = state["recipe"]
r.patches.setdefault(pid, {})[key] = bool(e.control.value)
r.reasons[pid] = r.reasons.get(pid) or tr("Set by you.")
save(r)
return handler
sections = []
for cat in ("pcvr", "frame", "overport", "adapter", "device"):
if cat == "adapter": # one row: the settings themselves are in the Game settings dialog (plain words)
@@ -855,7 +863,15 @@ class GameView:
], spacing=T.S3), padding=ft.Padding(T.S4, T.px(10), T.S4, T.px(10)), border=ft.Border(
top=ft.BorderSide(1, T.BORDER))))
continue
if not technical:
flags = any(q.kind == "bool" for q in p.params)
if on and flags: # a patch's own options (e.g. the gamepad's button layout), under it while it's on
params = recipe.params(p.id)
for q in p.params:
sub.append(C.switch(tr(q.help), value=bool(params.get(q.key, q.default)),
on_change=set_flag(p.id, q.key)) if q.kind == "bool" else
C.field(value=str(params.get(q.key, q.default) or ""), hint_text=tr(q.help),
on_blur=set_param(p.id, q.key)))
if not technical or flags:
pass
elif cat == "adapter":
val = recipe.params(p.id).get("value", p.params[0].default)
+1 -1
View File
@@ -330,7 +330,7 @@ def test_which_patches_work_on_the_bridge():
ok = {p.id for p in base.all_patches() if base.works_on_a32(p)}
assert {p for p in ok if base.get(p).category in ("overport", "frame")} == {
"frame.a32_bridge", "frame.adapter", "frame.launcher"}
"frame.a32_bridge", "frame.adapter", "frame.launcher", "frame.main_process"} # manifest edits: any ABI
assert "adapter.scale" in ok and "device.text_input_window" in ok
assert not {f"adapter.{k}" for k in XRSHIM_SETTINGS} & ok
a = _analysis()
+51
View File
@@ -0,0 +1,51 @@
"""device.steam_gamepad's own button layout (GitHub #162: Steam Input's per-game remapping doesn't reach a 2D app):
the patch's options become launcher env, the agent's Podman wrapper turns them into the app's key layout."""
import pytest
from test_steam_gamepad import ENV, NEEDS_POSIX, RUN, agent_module, frame_inputs
from frameport.patches import base
from frameport.patches.base import InstallContext
def install_env(**params):
ctx = InstallContext(package="org.smarttube.stable", params=params, files={}, env={}, adapter_settings={})
base.get("device.steam_gamepad").install(ctx)
return ctx.env
def test_options_become_launcher_env():
assert install_env() == {}
assert install_env(swap_sticks=True, swap_ab=True, keys=" key 304 DPAD_DOWN ;; key 305 BACK") == {
"FRAMEPORT_GAMEPAD_LAYOUT": "swap_sticks,swap_ab", "FRAMEPORT_GAMEPAD_KEYS": "key 304 DPAD_DOWN; key 305 BACK"}
with pytest.raises(ValueError):
install_env(keys="key 304 DPAD_DOWN; rm -rf /")
def test_layout_options(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
assert agent.gamepad_kl() == (agent.GAMEPAD_KL, [])
text, skipped = agent.gamepad_kl("swap_sticks,swap_ab", "key 304 DPAD_DOWN; key 316 HOME; axis 0x10 HAT_X; nope")
lines = text.splitlines()
assert skipped == ["nope"]
assert "axis 0x00 Z flat 4096" in lines and "axis 0x03 X flat 4096" in lines # right stick = main stick
assert "axis 0x01 RZ flat 4096" in lines and "axis 0x04 Y flat 4096" in lines
assert "key 305 BUTTON_A" in lines
assert "key 304 DPAD_DOWN" in lines and "key 304 BUTTON_B" not in lines # own lines replace that key's line
assert "key 316 HOME" in lines and sum(ln.startswith("key 316") for ln in lines) == 1
assert len([ln for ln in lines if not ln.startswith("#")]) == len(agent.GAMEPAD_KL.splitlines()) - 1
@NEEDS_POSIX
def test_wrapper_mounts_the_games_own_layout(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
frame_inputs(tmp_path)
env = dict(ENV, FRAMEPORT_GAMEPAD_LAYOUT="swap_sticks", FRAMEPORT_GAMEPAD_KEYS="key 304 DPAD_DOWN")
out = agent.podman_run_args(RUN, env, str(tmp_path / "sys"), str(tmp_path / "dev"))
mount = next(a for a in out if "Vendor_28de_Product_11ff.kl" in a)
source = mount.split("source=")[1].split(",")[0]
assert source.endswith(".kl") and "steam-gamepad-" in source
text = open(source).read()
assert "key 304 DPAD_DOWN" in text and "axis 0x03 X flat 4096" in text
# the default layout keeps its shared file
out = agent.podman_run_args(RUN, ENV, str(tmp_path / "sys"), str(tmp_path / "dev"))
assert any("agent/steam-gamepad.kl," in a for a in out)
+74
View File
@@ -0,0 +1,74 @@
"""frame.main_process (GitHub #178): a game activity in its own process leaves no main process, which is the one
Lepton watches; it closed Halo CE VR about a minute in ("Target app ... did not start")."""
from conftest import build_axml
from frameport.apk import axml
from frameport.patches import base
PKG = "com.halo.decomp.vr"
def _activity(name, process=None, category=axml.LAUNCHER):
attrs = [("name", "str", name)] + ([("process", "str", process)] if process else [])
return [
("start", "activity", attrs),
("start", "intent-filter", []),
("start", "action", [("name", "str", axml.MAIN)]), ("end", "action"),
("start", "category", [("name", "str", category)]), ("end", "category"),
("end", "intent-filter"),
("end", "activity"),
]
def _halo_manifest():
"""Halo CE VR's layout: the VR activity runs in ":halo_game", the 2D launcher starts it."""
return build_axml([
("start", "manifest", [("package", "str", PKG)]),
("start", "application", []),
*_activity("com.halo.decomp.HaloActivity", ":halo_game", axml.VR_CATEGORY),
*_activity("com.halo.decomp.LauncherActivity"),
("start", "service", [("name", "str", "com.halo.decomp.Crash"), ("process", "str", ":crash")]),
("end", "service"),
("end", "application"),
("end", "manifest"),
])
def _processes(manifest):
x = axml.Axml(manifest)
return {x.attr_str(e, "name"): x.attr_str(e, "process") for e in x.elements() if e.name in ("activity", "service")}
def test_detects_and_moves_the_game_activity():
m = _halo_manifest()
assert axml.separate_process_activities(m) == ["com.halo.decomp.HaloActivity"]
fixed = axml.set_main_process(m, axml.separate_process_activities(m))
assert axml.separate_process_activities(fixed) == []
assert _processes(fixed) == {"com.halo.decomp.HaloActivity": PKG, "com.halo.decomp.LauncherActivity": None,
"com.halo.decomp.Crash": ":crash"} # services keep their own process
assert len(fixed) == len(m) # the package name is already in the pool: one value changes, nothing moves
assert axml.set_main_process(fixed, ["com.halo.decomp.HaloActivity"]) is None
def test_main_process_activities_are_left_alone():
m = build_axml([
("start", "manifest", [("package", "str", PKG)]),
("start", "application", []),
*_activity("com.x.Game", PKG), # explicitly the main process
*_activity("com.x.Other"),
("end", "application"),
("end", "manifest"),
])
assert axml.separate_process_activities(m) == []
assert axml.set_main_process(m, ["com.x.Game"]) is None
def test_patch_is_offered_only_when_detected():
patch = base.get("frame.main_process")
assert patch.default_on
class A:
extra = {"separate_process": ["com.halo.decomp.HaloActivity"]}
assert patch.applies(A()) and patch.detect(A()).recommended
A.extra = {}
assert not patch.applies(A()) and patch.detect(A()) is None
+8
View File
@@ -171,6 +171,14 @@ def test_tbxr_fdm_crash_is_identified_as_vr_teardown():
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}
# extension listings (GitHub #404): OVRPlugin's enumeration, OVRPort's dispatcher, Unity's Meta XR feature
for listing in ("I OVRPlugin: Name=XR_FB_scene SpecVersion=1", "I overportOXR: - XR_FB_scene 1",
'I Unity : Meta XR Feature: Extensions="XR_FB_passthrough XR_FB_scene XR_META_boundary"',
"I overportOXR: - XR_FB_scene_capture 1"):
log = LOG_OK + f"09-28 17:39:02.200 1147 1174 {listing}\n"
assert "scene-missing" not in {f.id for f in triage(log, "RUNNING", "com.example.game").findings}, listing
wants = LOG_OK + "09-28 17:39:02.200 1147 1174 E Unity : Required extension XR_FB_scene not available\n"
assert "scene-missing" in {f.id for f in triage(wants, "RUNNING", "com.example.game").findings}
used = LOG_OK + "09-28 17:39:04.000 1147 1174 E Unity : xrQuerySpacesFB failed: -12\n"
assert "scene-missing" in {f.id for f in triage(used, "RUNNING", "com.example.game").findings}