Compare commits

..
219 Commits
Author SHA1 Message Date
spoopyghosty0 63ad73a485 FramePort 1.4.0 2026-10-25 13:48:50 -04:00
spoopyghosty0 4d40b1c5bb Tests: the refresh-rate request hook is always installed too (#336) 2026-10-25 13:44:31 -04:00
spoopyghosty0 87e34bd9ef Site: release and contributor data refreshed (1.4.0) 2026-10-25 13:41:15 -04:00
spoopyghosty0 af142439ba Catalog: AC Nexus is a Quest 3 again with the Vulkan panel pool (#424; min_app 1.4.0) 2026-10-25 13:40:39 -04:00
spoopyghosty0 6988d54d5c FrameBridge: a refused refresh-rate request keeps the current rate instead of failing the game (OutRun quit on XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB, #336); triage refresh-rate-refused 2026-10-25 13:39:25 -04:00
spoopyghosty0 1eb7f655ba Catalog: First Contact (#489), Invasion (#490), Zero Caliber 2 (#492), SynthRiders' new package (#494), Port0r Cop VR, Time Crisis II VR, House of the Dead VR, Time Crisis VR, Rally VR (#493, #495-#498); QuestZDoom works with gl_error_clear, qzdoom.ini note (#237) 2026-10-25 13:37:06 -04:00
spoopyghosty0 fe8b106560 Java gets a UTF-8 locale on Linux: FramePort on the Frame (C locale) passed 'LEGO® Bricktales' to OVRPort as 'LEGO?? Bricktales' and every build failed (#487) 2026-10-25 13:32:59 -04:00
spoopyghosty0 a8ab024b26 Start activity: an INFO activity counts as the launcher, so a game's only MAIN + INFO + VR activity isn't taken for a VR part behind a flat launcher (#476: theater mode stayed on, the Android window took the controllers); analysis 28 2026-10-25 13:32:57 -04:00
spoopyghosty0 dc98903fd6 Hybrid apps: declare the system feature they check before switching into VR (#484)
YouTube for Quest starts as a 2D panel and only opens its VR part (360 videos, full
screen) when the system has oculus.software.vr.app.hybrid. Opt-in patch
device.hybrid_app (offered when the manifest names the feature): the launcher exports
FRAMEPORT_FEATURES and puts FramePort's Podman wrapper on PATH, which mounts a
permissions file into the container's /system/etc/permissions. Agent v125; triage
hybrid-feature-missing. Checked headless on the Frame: pm list features shows it.
2026-10-25 10:20:40 -04:00
spoopyghosty0 6f307f0fe8 Windows updates: relative paths from the folders' long names (8.3 install paths)
Get-ChildItem lists a short path like C:\Users\RUNNER~1\... under its long name, so the
relative paths cut with the short path's length were wrong: the backup, the removal of
old-only files and the rollback went to wrong folders. CI's update smoke test caught it.
2026-10-25 09:37:12 -04:00
spoopyghosty0 b440656560 Keep Android's shared libraries OVRPort removes (#484)
OVRPort's patch_remove_uses_library drops every <uses-library>, also ones
Lepton's Android 11 provides: YouTube VR (org.apache.http.legacy) then dies
at start with ClassNotFoundException org.apache.http.protocol.HttpRequestHandler.

New patch frame.keep_system_libraries (default on where the original
manifest names such a library) puts those entries back after OVRPort,
always android:required="false". Lepton's libraries are one list
(system_libraries.LEPTON_LIBRARIES); analysis extra.uses_libraries reads the
original manifest (ANALYSIS_VERSION 27). Builds of apps without such an entry
are unchanged. Triage system-library-missing, PLAYBOOK row, tests.
2026-10-24 23:56:06 -04:00
spoopyghosty0 5429ae5c2b Steam library (now): later installs no longer hang behind a waiting library update (#305)
While a now-mode library update waited for FramePort/Desktop Mode/a game to close it held the shortcut lock;
the next install's library_mode call blocked on it until the 600 s agent timeout. library_mode now tries the
lock once, shortcuts journals changes into the waiting worker (merged after Steam closes, one restart), the
later queue waits at most 30 s, and runs only drop the journal entries they applied. Agent v124.
2026-10-24 23:54:47 -04:00
spoopyghosty0 48367c7cf8 Expansion files: other kinds get the game's package spelling too (#415)
Crisis Brigade 2 reloaded's office/alleys/challenges/mansion/ta1-3 .222.<package>.obb
copies with a lower-case package part reached the Frame under names the game can't
open. obb_names renames such <kind>.<vc>.<package>.obb files (case or copy suffix,
kind spelling kept) without an "another app" warning; NAMED_OBB kinds may contain
digits (ta1 was skipped by the scan) and copy-suffixed files of any kind are found.
2026-10-24 23:53:37 -04:00
spoopyghosty0 a6b37f8331 Windows updates never mix two versions (#476)
A failed copy was rolled back without deleting the files the new version had added: the old version then started next to a new patch module and crashed at start (cannot import name 'ORG_MARK'). The swap script now changes nothing while FramePort or a program file is in use, moves the old version's files the new one doesn't have into 'previous' (only inside the bundle's own folders), and a rollback deletes what the update added. Bundles carry frameport/_bundle_files.txt; at start the app removes or ignores leftover package files and tells the user to reinstall when compiled modules don't match. update_smoke.py runs a locked-file rollback and a leftover-module update on Windows.
2026-10-24 23:43:30 -04:00
spoopyghosty0 f2297a4ac1 Site: current app and docs; Controller buttons page; clearer wording
- Home, meta descriptions: Linux apps alongside Quest, PC VR and Android; shorter notice, matching the README.
- Setup page: the app's labels ('Already set up?', 'Connect by address…'); Pair new host asks by itself.
- Tour: the install video is about 90 s.
- Docs: CONTROLLERS.md is a site page now, and the image sync copies docs/images subfolders (its diagram lives in
  controllers/); the 'game won't start' path starts with the FAQ.
- 'it'/'this'/'that' replaced by the noun where the referent was unclear (walkthrough, setup, download, install
  pages, compare, PC VR panel, demo app).
2026-10-24 23:41:58 -04:00
spoopyghosty0 89a501d5d8 Docs: name the thing instead of 'it', 'this' or 'that' where the referent was unclear
Every page this branch touches: sentences that started with 'It'/'This'/'That' after naming two things, and short
answers ('report it', 'click it'), now say what they mean. Two FAQ headings start with 'The game' (no page links to
them).
2026-10-24 23:41:58 -04:00
spoopyghosty0 8359856609 Docs: themes, install button, controller buttons, diagnostics, contributing: tighter wording
Diagnostics starts with what a user sends and moves the maintainer workflow into one paragraph; contributing folds
the tested-game section into the quick contributions.
2026-10-24 23:41:58 -04:00
spoopyghosty0 dfdd6cf0ca Docs: install guide and FAQ: shorter, one idea per sentence
Same instructions in fewer words: setup alternatives as a list, moving from a PC in three short steps, room editing
as numbered steps, save backups once in the FAQ. The FAQ's 32-bit answer was outdated: the 32-bit bridge is
suggested for such games now.
2026-10-24 23:41:58 -04:00
spoopyghosty0 59de1da268 Docs: README and compatibility: no game count, shorter text
'Hundreds of games' instead of a number that goes stale (README, COMPATIBILITY, the site's comparison). Shorter
notice, features, quick start and comparison bullets; one-line docs index rows.
2026-10-24 23:41:58 -04:00
spoopyghosty0 75a016b4ff Docs: architecture: triage signatures now update from GitHub main too 2026-10-24 23:41:58 -04:00
spoopyghosty0 3170b3acc9 Docs: third-party notices list Cpp2IL and APKEditor, which FramePort downloads on first use 2026-10-24 23:41:58 -04:00
spoopyghosty0 bd242be4a7 Docs: transfer benchmark: remove local report paths, say what the uploader does today
The page named report files under a personal Windows home folder; the measurements stay, the paths go. A short
current-state section (OpenSSH SFTP for big files, 3 parallel uploads) heads the page, so the older 'production
still uses Paramiko, default 2' statements read as history.
2026-10-24 23:41:58 -04:00
spoopyghosty0 3dcede3ee3 Docs: runtime reference: version-neutral title, current Revive-under-Proton facts, no agent versions
The title pinned SteamOS 0.3.0 although sections cover up to 0.5.x; PC VR under Proton isn't 'unverified' any more,
and Revive's hooks don't work there (the old line said they did). sshd comes with Developer Mode and FramePort's key,
no password. The headset-view webcam does show games. Foveation preferences moved next to the other foveation
notes (they sat under Monitoring sources), the scan preview below the room overview. Room tab paths.
2026-10-24 23:41:58 -04:00
spoopyghosty0 68aab29e85 Docs: playbook: fix the broken first table, current PC VR model, one section for FramePort's own problems
The startup table had a row above its header separator (it rendered as plain text). PC VR games are installed
unchanged by default, Revive only for LibOVR games; the two duplicate -3001 rows are one now. Upload/SSH, install
link and store-detail rows moved out of the startup and PC VR tables into their own section, the OutRun rows into
the startup table. No agent version numbers; room scanning in the headset exists now; Add from a link.
2026-10-24 23:41:58 -04:00
spoopyghosty0 2e86f97905 Docs: contributor pages: current menu labels, PC VR runs as-is by default, Linux apps, no agent versions
CONTRIBUTING: Share working recipe / Report a problem under their menu groups, Language lives in Appearance,
patch_docs and showcase pointers. ARCHITECTURE: bundles and backups as sources, session triage, remote catalog,
PC VR games installed unchanged with Revive only for LibOVR games, a Linux apps section. FRAME_SETUP: menu entries,
services and FEX that FramePort adds later. DIAGNOSTICS: no agent version numbers. 'for example' instead of e.g.
2026-10-24 23:41:58 -04:00
spoopyghosty0 439cc6c213 Docs: install guide and FAQ follow the new game menu, settings and save backups
INSTALL: menu paths through the game menu's groups, one section on when games appear in Steam (was buried under
running FramePort on the Frame), save backup folder/automatic snapshots, shorter overlay and first-launch notes,
rename/session/settings commands. FAQ: foveation lives in Game settings now, Exit game pauses games first, save
backups as a short list, controller buttons.
2026-10-24 23:41:58 -04:00
spoopyghosty0 26b2ea16fd Docs: README and compatibility pages: current game count, full docs index, Linux apps; regenerate the tested-games list 2026-10-24 23:41:58 -04:00
spoopyghosty0 46863a7c61 Catalog: Hungry Shark Evolution (#486) 2026-10-24 23:35:19 -04:00
spoopyghosty0 79e329afe5 Tests: shorter line in the shader-fix triage test 2026-10-24 22:01:35 -04:00
spoopyghosty0 9973833585 Shader fixes: say once when none of a game's configured fixes matched
vk_shader_fix (vkshim) and zink_shader_fix (shader-fix layer) match a shader by size + SHA-256, so a game update that changes the shader made the fix silently do nothing. Once per session, when fixes are configured and none matched 5 minutes after the first shader module (checked only at module creation), the shim/layer logs '<prefix>: N shader fix(es) configured, none matched after M modules in S s (game updated?)'. Triage shader-fix-unmatched (info, report) + PLAYBOOK row. Artifacts rebuilt; no revision bumps.
2026-10-24 21:59:20 -04:00
spoopyghosty0 c239a8cf32 Headset identity: other OVRPort runtimes found by structure, not fixed offsets
The verified 3.4.3-aa54c3f layout keeps its exact guards. Any other dispatcher (the older 3.4.3-23204ea, later
runtimes) gets Quest 3 when Quest 2's name and UUID each occur once and each is loaded by exactly one adr /
adrp+add / adrp+ldr q sequence; otherwise nothing changes and the build keeps Quest 2 with a WARN, as before.
Checked against both runtimes in the OVRPort workspace.
2026-10-24 21:50:16 -04:00
spoopyghosty0 9e69242001 Just Dance VR local save: no game version check
The patch finds its code by name, leaves a build that already saves unchanged and refuses when the code isn't
there, so it applies to every Just Dance VR build without parsing version names.
2026-10-24 21:45:05 -04:00
spoopyghosty0 6e0958b03d Just Dance VR keeps its progress (#480): frame.justdance_local_save
Just Dance VR (0.5.4 and later) loads its progress from a local save at
start, but LocalSaveEvents.RequestSave is empty (a single ret in
libil2cpp.so), so the SaveValueRequested event that LocalSaveManager
listens to is never raised and the OfflineSave file is never written.
On a Quest the progress lives in PlayFab after a Meta sign-in, which the
Frame doesn't have.

The patch gives RequestSave its declared body (raise SaveValueRequested
with the key and value). Everything is found per build: Cpp2IL (the
shared IL2CPP run) gives the methods and field offsets, capstone decodes
SaveSystemReady for the TypeInfo slot, the metadata init and the
delegate layout, and the new code goes into a new R+X segment
(elf.append_segment); RequestSave's ret branches there. A build whose
RequestSave has a body is left alone; anything unexpected is refused
with the reason. Nothing online is touched.

Triage signature justdance-local-save-off, gated with the new
unless_patch key (not when the fix is in the build); PLAYBOOK row,
DEVICE_NOTES.
2026-10-24 21:25:59 -04:00
spoopyghosty0 4c1cbc9f50 Triage signatures update from GitHub main like the catalog
refresh_remote also caches catalog/triage.yaml from the same tree listing
(triage-gh-<sha>.yaml); triage.database() merges it per signature id over
the bundled file, dropping remote signatures this app can't use (malformed,
unknown field, unknown patch/adapter key, min_app too new). Bundled-only
signatures stay; a bundled one dated later wins.
2026-10-24 21:20:45 -04:00
spoopyghosty0 f393884606 QuestZDoom slowdown (#237): Team Beef ports get gl_error_clear
QuestZDoom leaves GL_INVALID_ENUM pending every frame; the runtime's GL check fails on it at the first
swapchain and its GL interop then leaks ~105 GPU objects a second (debugfs gem, headless), until the
game slows to a crawl and the GPU driver oopses. gl_error_clear keeps the count flat. Suggested for
every Team Beef port; QuestZDoom catalog recipe with it; PLAYBOOK row and device note.
2026-10-24 17:28:05 -04:00
spoopyghosty0 984c1ab4f3 Locales: template with the latest patch texts 2026-10-24 17:19:38 -04:00
spoopyghosty0 53980029bb The Climb 1 starts again: the foveation layer file always ships with FrameBridge, and never interposes libvulkan (#432, #449)
e5921ccb kept libVkLayer_fdm_darken.so out of The Climb 1's libAndroidLauncher.so but also stopped shipping the
layer file, while frame.adapter links every arm64 FrameBridge build against it: FrameBridge didn't load, the VrApi
bridge logged 'xrInitializeLoaderKHR failed: OpenXR result -7' and CryEngine aborted (SIGTRAP; dev build zip, static
check warned the NEEDED was missing). diagnostic_support now always ships the layer; frame.adapter validate fails
without it.

The 1.3.0 start failure itself: the layer exports vkEnumerate{Instance,Device}{Extension,Layer}Properties for the
Vulkan loader and elf.add_needed linked it first, so a renderer importing those from libvulkan.so (libCryRenderVulkan.so
does, The Climb 1's monolithic launcher imports Vulkan directly too) got the layer's answers (no extensions /
VK_ERROR_LAYER_NOT_PRESENT). elf.add_needed(first=False) links it after the existing entries for every other engine.
frame.adapter package revision 5 for com.crytek.climb; triage vrapi-bridge-no-loader.
2026-10-24 17:17:04 -04:00
spoopyghosty0 c95efb92d7 Agent v123: the flat-launcher theater exception (#475) ships after v122's diagnostics change; test follows deployment_recipe 2026-10-24 17:16:47 -04:00
spoopyghosty0 a4d71c529d Apps with a flat launcher before their VR part get the Android window, shown in theater mode (#475)
Xelee opens MeleeActivity (a 2D SDL launcher that picks the game) and starts MeleeXrActivity from there. Lepton
runs VR apps headless, so the launcher was never seen and OpenXR never started. device.text_input_window is now
suggested for apps with an analysis vr_activity, the deployment recipe records flat_launcher (unless
frame.start_activity starts the VR part directly), and agent v122 keeps SteamVR's theater mode on for those, so the
launcher is in front of the player instead of a dashboard page. Catalog: Xelee gets the window.
2026-10-24 17:11:44 -04:00
spoopyghosty0 192bf14f9d Diagnostics: collect_diag returns the last launch's stop-pause, exit-hook, theater and dashboard logs and plays.log, so a zip shows whether the game was paused before it closed and how it was closed (agent v122, #480) 2026-10-24 17:10:08 -04:00
spoopyghosty0 e30361bc9e UI text: "for example" instead of "e.g." in the OBB name warnings 2026-10-24 16:00:03 -04:00
spoopyghosty0 a09fdd7ae3 Menus: a solid grey left edge and a solid soft highlight instead of the portal gradient (#414) 2026-10-24 15:54:48 -04:00
spoopyghosty0 f53b25686f Menus: FramePort's own menu with hover submenus for every context menu and the game page's "…" (#414)
- ui/popmenu.PopMenu draws menus in page.overlay: panels size to their labels (no 280 px cap, wrap past a
  maximum, scale with the UI), open at the click or under the button, flip and clamp into the window; submenus
  open to the side with room (hover with intent delays, click, or the arrow keys); Enter/Esc; closes on an outside
  click, a choice or a view change; portal edge in dual themes, no fade with Reduce motion.
- The game menu (app.game_actions) is one structure for the Library right-click and the page's "…": Play,
  Install/Update, Game settings | Open game page, Select | Update, On the Frame, Saves, Name and artwork, Recipe,
  Troubleshoot submenus | Uninstall, Remove from library. Launch tests are in Troubleshoot.
- Files, Screenshots and Monitor right-click menus use it too; the multi-select menus are unchanged.
- ui_smoke: menu steps open a submenu and use the keyboard.
2026-10-24 15:54:44 -04:00
spoopyghosty0 e542fd1b67 Menus: long items wrap instead of running past the edge; shorter Library right-click menu (#414)
- Menu rows put the label next to the icon themselves so a label wider than Flutter's menu limit wraps (Flet's own
  icon row overflowed); menu buttons scale the width limit with the UI scale.
- Library right-click: Play, Install/Update, Game settings | Open game page, Select, More actions. Rename, the
  newer-version updates and Remove from library stay in the game's menu; uninstalls on the game page and Frame page.
- Right-click on one of several selected games: Install them on Frame / this PC, Select all shown, Clear selection.
- Game page: the recipe's actions (reset, save as known-good, share) are buttons on its recipe card; Back up saves
  is only the button next to Play.
2026-10-24 15:54:40 -04:00
spoopyghosty0 5c9848c0ba Steam's own Exit game pauses FramePort games first: the launcher's _exit_hook worker hooks SteamClient.Apps.TerminateApp in Steam's UI through CEF devtools and lets Steam end the game once Android paused it (cap 3 s; other games pass straight through; #480)
Agent v121. Verified headless on the dev Frame with AC Nexus launched through Steam: OnApplicationPause(true)
0.15-0.29 s after TerminateApp, container gone 1.5-1.7 s after it (0.65-0.8 s before); a game that doesn't
answer is capped (3.2 s). The Monitor's End game calls Steam's TerminateApp past the hook (no second pause).
2026-10-24 15:46:41 -04:00
spoopyghosty0 0a59ebd7d3 App bundles: .xapk/.apks/.apkm/.apkx files and split APK folders are unpacked and merged into one APK (discussion #482)
Scan a folder, Add an APK, drag and drop, install links and the CLI take them. The arm64-v8a split's libraries and assets are merged in Python (split attributes and Play's splits.required flag removed); language/density/feature splits go through REAndroid's APKEditor (downloaded into tools/). OBBs from the bundle or next to it become the game's data. Old encrypted .apkm files are explained, not opened. Headless device check: Escaping Wonderland split into base + arm64 split, merged, built, installed and launch-tested at 72 fps.
2026-10-24 15:27:58 -04:00
spoopyghosty0 5d6f598c31 Installs: OBBs with a download's copy suffix or another case go to the Frame under the name the game looks for (#481)
main.203.com.Armature.VR4-003.obb is sent as main.203.com.Armature.VR4.obb (#450); the files on the PC keep their
names and the game page shows an info note instead of a warning. Another app's name (#129), a copy next to the
right-named file, two copies for one name and a copy suffix without a versionCode stay warnings. plan_update compares
the Frame's names (no repeated Update on Frame), a file installed earlier under the copy name is renamed on the Frame
instead of uploaded again, and on-Frame hardlinks (#433) keep the PC file as their source. Loose copy-named OBBs next
to an APK are found by name. Unreal chunk files and split parts are no longer flagged as named for another app.
2026-10-24 15:15:28 -04:00
spoopyghosty0 4f5b19e48b Games are paused before they end: launch.sh, agent stop and End game send the activity home in the container and wait up to 3 s, so saves written on pause run (Just Dance VR lost progress, #480)
Killing the container (launch.sh podman kill, Lepton's reboot -p + podman stop) never paused the app: no onPause, no Unity OnApplicationPause(true). am start of the HOME intent pauses it in Lepton's Android 11 (input keyevent does nothing headless); the script waits for STOPPED + 0.4 s, capped at 3 s. upgrade_launchers adds pause_game to existing Lepton launchers. Steam's own Exit game signals the whole process tree (container included) at once and isn't covered yet. Agent v120.
2026-10-24 15:14:19 -04:00
spoopyghosty0 ad295e12eb Theater hold: taken first thing in the launcher (a hold with Lepton's start came after the window was theatered), keyed by launcher pid; verified end to end on the Frame 2026-10-24 15:11:08 -04:00
spoopyghosty0 2be68ca37f Typing window no longer takes the controllers: SteamVR's theater mode stays off while a game with its Android window runs (AC Nexus, #476, #477)
device.text_input_window makes the game's Android window a gamescope "desktop game" overlay. SteamVR put it into
theater mode before the game's OpenXR session existed and opened the dashboard on it when the session started: the
Android screen with the controllers as a mouse pointer. The dashboard worker now turns dashboard.autoShowGameTheater
off while such a game runs (holds per worker, restored after the last one, by the next start or ensure_host_fixes
after a killed worker, and by purge). It also recognises SteamOS 0.5.6's open_dashboard_action as the player's
dashboard button instead of closing every press again. Agent 119.
2026-10-24 15:10:51 -04:00
spoopyghosty0 d13381db7f Browser stand-in for sign-ins: apps that look for a browser (Plex's sign-in, #466) find one in their own APK and Lepton opens the page in Steam's browser
Patch frame.browser_handoff (opt-in; offered for 2D apps, suggested-off when the manifest's <queries> ask for a
browser: analysis browser_query, ANALYSIS_VERSION 26) adds an http/https VIEW + BROWSABLE activity (no host) and its
class (native/java-handoff -> artifacts/dex/browser-handoff.dex) to the app. Lepton's ActivityStarter.execute takes
every VIEW intent before resolution and writes steam://openurl to /lepton/steam.pipe, so the stand-in normally never
runs; started directly it writes the same line itself. axml: queries_browser, browser_activities,
add_browser_activity (missing android: attribute names go into the resource-mapped part of the pool).
A 2D app with an APK patch turned on is now built instead of installed unchanged (engine.flat_as_is in set_recipe):
hand-enabled APK patches on 2D apps were never applied. Verified headless on the Frame with a test app.
2026-10-24 15:10:15 -04:00
spoopyghosty0 79bd23e728 Locales: template with the Provide space warp setting 2026-10-24 15:06:03 -04:00
spoopyghosty0 1262880f14 FrameBridge space_warp_emul: XR_FB_space_warp for games that require it (OutRun Quest VR, #336)
OutRun quits with "AppSW recommended buffers unavailable" whether XR_FB_space_warp is offered or hidden.
New per-game adapter setting space_warp_emul (off by default, native/adapter/space_warp.c): offers the
extension (emulated when the runtime lacks it, overrides hide_space_warp), reports a motion-vector size of
eye/4 (the Frame's runtime answers the full 1728x1728 eye), lists RGBA16F and depth formats after the
runtime's, serves motion-vector swapchains (RGBA16F/RG16F at the reported size or at most half the eye) and
their depth swapchains as FrameBridge-owned GL textures / Vulkan images (3 images, round robin, wait/release
in front of every other hook), and strips XrCompositionLayerSpaceWarpInfoFB at xrEndFrame (eye depth info is
now kept when space warp info isn't first in the chain, also for hide_space_warp).

Headless on the dev Frame with Pinball FX VR (UE5 + OVRPlugin, Vulkan): both 376x376 pairs served,
600/599/598 acquire/wait/release by FrameBridge, space warp info removed, 0 xrEndFrame failures, 72.2 fps like
its own recipe. Triage appsw-required, catalog entry for OutRun (fb_color_space + space_warp_emul),
PLAYBOOK row, DEVICE_NOTES section, host harness for the GL/Vulkan paths.
2026-10-24 15:01:03 -04:00
spoopyghosty0 a8f6c7f1e4 Type on Frame: the paste buttons move below the field when it would be squeezed (narrow window, large UI scale)
The paste field (expand) shrank to a few pixels in narrow windows while the buttons kept their size. The view now
measures the buttons and the row (on_size_change) and stacks the buttons, right-aligned and wrapping, below the field
when it would get less than 240 px (scaled).
2026-10-24 14:43:40 -04:00
github-actions[bot] 48805ff810 Docs: update screenshots and videos (#438) 2026-10-24 14:29:32 -04:00
Lucas Oskorep c66d364451 Docs: AUR package (community) install and update notes (#445) 2026-10-24 14:28:28 -04:00
spoopyghosty0 6e701ae33a Docs: moving from a PC to FramePort on the Frame (signing keys, original files, saves; discussion #442) 2026-10-24 14:27:33 -04:00
spoopyghosty0 7daf9aa103 Library: data files an older scan skipped are picked up at start, so the game shows Update on Frame (Asgard's Wrath 2's chunk OBBs: no badge, the user reinstalled, #471) 2026-10-24 14:13:16 -04:00
spoopyghosty0 d70e540482 Catalog: UNDERDOGS (#474, works) and Eternal Starlight (#479, voice commands don't work) 2026-10-24 14:08:42 -04:00
spoopyghosty0 5dee4bac51 Vulkan rename skips Dawn (WebGPU) libraries, which build the library name from two pieces: the in-place rename made Xelee and PrimedGun ask for libfp_vkn.so (#475, #254) 2026-10-24 14:03:56 -04:00
spoopyghosty0 7eec8fdf57 Catalog: Xelee without the Vulkan library rename (#475): the engine assembles libvulkan.so from two pieces 2026-10-24 14:01:56 -04:00
spoopyghosty0 25198bc11c FramePort on the Frame: an update replaces game data an earlier install copied with links to the originals, so the second copy is freed without reinstalling (#433) 2026-10-24 13:59:27 -04:00
spoopyghosty0 e5921ccb4e The Climb 1 starts again: the dormant foveation diagnostic also stays out of CryEngine's monolithic libAndroidLauncher.so (#432, #449)
a8e01c01 skipped only libCryRender*; The Climb 1 has no separate renderer, so engine_lib() picked
libAndroidLauncher.so and the layer was still linked into it. Both reporters' zips (1.3.0 and a dev build with
a8e01c01) crash identically: SIGSEGV fault addr 0xf0 under CreateSystemInterface+320 right after Vulkan instance
setup. frame.adapter package revision 4 for com.crytek.climb marks existing builds for Update on Frame.
2026-10-24 13:56:47 -04:00
spoopyghosty0 a4546e4ee4 Foveation: put Valve's sharp area on each eye (#394), second version; Vulkan eye snapshots
Valve's VALVE_fdm_injection layer gives every eye swapchain a density map covering the whole swapchain with its
FOVE_LEVEL rectangle at the swapchain centre; it never uses the submitted imageRect or the eye's optical axis, so games
with smaller or shifted eye rects (dynamic resolution, AC Nexus 0,0 / 287,0 1726x1728 in 2016x1728) got the sharp area
~14% of the view to the side. VK_LAYER_FRAMEPORT_fdm_centre (native/foveation/centre.h, second layer in
libVkLayer_fdm_darken.so, right below Valve's layer when FDM_DEBUG has "enable"; not for The Climb 2;
FRAMEPORT_FDM_CENTRE=0 turns it off) replaces the per-eye density-map offsets: the eye's optical centre (Fixed) or
Valve's gaze re-based on the eye's rect (Eye-tracked). FrameBridge reports the views (fdm_views.c).

Changes after the first version (b2be40af, reverted 2ce1c505) failed in the headset:
- Eye-tracked black: Valve declares fragmentDensityOffsetCount 1 for two-view passes while Turnip reads one offset
  per view regardless, so the right eye's offset came from uninitialised stack memory. Every offset Turnip reads is
  now written (view count from the pass's view mask, eye from the map view's layer, each eye's gaze from Valve's array).
- Fixed crash: the layer no longer flags images (it had flagged external-memory images Valve leaves alone). Valve's
  own disable_offsets path also crashed AC Nexus without this layer (Turnip under fdm_injection_CmdBeginRendering), so
  Fixed now drops disable_offsets in-process once the layer is in the chain and overrides Valve's gaze offsets with the
  optical centre; if Valve read the option first its maps carry no offset flag and passes are left alone.

Headless on the dev Frame, AC Nexus, 130 s per run, darkening test, snapshots every 15 s: centring Fixed (2 runs) and
Eye-tracked ran without crash at 72 fps, both eyes' sharp area within 2% of the optical centre (Valve only: +14% Fixed,
+25% left eye Eye-tracked; Valve-only Fixed crashed in 1 of 2 runs). FrameBridge snapshot=N reads Vulkan eye images
back; FRAMEPORT_FOVEATION_TEST=2 paints 1/fragment area. Triage valve-fdm-render-crash. Host harness
tests/test_fdm_centre.py. The sharp area's size still snaps to Turnip's GMEM bins (driver design).
2026-10-24 11:58:39 -04:00
spoopyghosty0 2ce1c50544 Revert the foveation centring layer (b2be40af): AC Nexus crashed in Valve's fdm layer (Fixed) and showed black (Eye-tracked) in the headset (#394); back until it is fixed and checked headless 2026-10-24 11:14:55 -04:00
spoopyghosty0 b2be40af35 Foveation: put Valve's sharp area on each eye (#394); Vulkan eye snapshots
Valve's VALVE_fdm_injection layer gives every eye swapchain a density map that covers the whole swapchain and centres
its FOVE_LEVEL rectangle there; it never uses the submitted imageRect or the eye's optical axis. Games that submit a
smaller or shifted rect (dynamic resolution, asymmetric eye rects such as AC Nexus 0,0 / 287,0 1726x1728 in a
2016x1728 swapchain) got the sharp area ~14% of the view off to the side, towards the bottom for corner-anchored rects.

VK_LAYER_FRAMEPORT_fdm_centre (native/foveation/centre.h), a second layer in libVkLayer_fdm_darken.so inserted right
below Valve's layer when FDM_DEBUG has "enable" (not for The Climb 2; FRAMEPORT_FDM_CENTRE=0 turns it off), rewrites
the per-eye density-map offsets at the end of each pass: Fixed puts the sharp area on the eye's optical centre (it adds
the offsets and the offset image flag Valve leaves out with disable_offsets), Eye-tracked re-bases Valve's gaze offset
on the eye's rect. FrameBridge reports the views (fdm_views.c: swapchain size, layer, imageRect, optical centre).

FrameBridge snapshot=N now also reads Vulkan eye images back (both eyes, half size, rect + FOV logged), and
FRAMEPORT_FOVEATION_TEST=2 paints 1/fragment area. Headless on the dev Frame (AC Nexus, Fixed, darkening test):
sharp-area centre vs optical centre +203,+43 px -> +26,+11 at 1x, +276,+65 -> +12,+17 at 1.5x, +407,+87 -> +47,+31
at 2x. The size still snaps to Turnip's GMEM bins (density is chosen per bin), so Small equals Large at 1x and the
shape changes between render scales; that part is the driver's.

Docs: DEVICE_NOTES "Foveation: sharp area centre and size", FRAME_RUNTIME, PLAYBOOK row, native/README; triage
dynamic-resolution-foveation is quiet once the centring layer ran. Host harness tests/test_fdm_centre.py.
2026-10-24 10:28:42 -04:00
spoopyghosty0 3dc7d823e4 Docs: Ghost Giant (#439) checked headless: the black screen after the logo was the intro video (no NDK media); with dcbcce0f/bccd916f both videos play and the main menu renders 2026-10-24 10:18:33 -04:00
spoopyghosty0 33f076e66c VrApi bridges: OVP_HEADLESS_TEST submits images, so headless snapshots show what a VrApi game draws (#439)
Without a worn headset the session stays SYNCHRONIZED: the VrApi bridge ended every frame empty and the a32 bridge dropped every projection with an untracked default pose, so FrameBridge's snapshot=N had nothing to read. Under OVP_HEADLESS_TEST (tests only) the VrApi bridge submits anyway and keeps GetTrackingTransform(3)'s floor 1.6 m below the eye origin (the runtime's headless STAGE sat 0.865 m below it, and Ghost Giant hid everything behind "Return to play area"), and vrapi.c keeps the game's pose. Harness: headless submit keeps the layer. Checked on the Frame: Ghost Giant's intro videos and main menu in the snapshots.
2026-10-24 10:18:30 -04:00
spoopyghosty0 aff56927ec FrameBridge: flipped Vulkan quads are shown as upside-down atlas copies made every frame instead of flipping the app's image in place (AC Nexus' late-drawn notices stayed upside down, re-flips and GPU waits; likely the white start flash)
The in-place flip raced Unity, which finishes some panels after xrEndFrame without acquiring them again, and its
blits/readbacks waited for the GPU inside xrEndFrame. swapchain_pool.c now owns flips (also without swapchain_pool):
a flagged pooled panel's atlas copy is a blit with reversed rows, a flagged runtime swapchain's quad gets a per-frame
atlas area with an upside-down blit of its released image. The app's images are only read; no CPU waits. flip_dump
reads back what the runtime is given (async); layer_debug logs copies and waits per 5 s. The late-drawing
fingerprints, scratch image and in-place flip are gone. Headless: every intro panel upright as submitted, 72 fps.
2026-10-24 00:28:10 -04:00
spoopyghosty0 8a89f821e0 Locales: template with the new patch summary (frame.ovr_org_scoped_id), which failed CI's i18n check 2026-10-24 00:06:41 -04:00
spoopyghosty0 02eb17dd91 FrameBridge: back to the sparse late-drawing check (revert fdaba13f): the every-pixel async version brought AC Nexus' white start flash back without fixing its notice panel (owner, headset 2026-10-23); the 595d12a0 state was headset-verified 2026-10-23 23:53:31 -04:00
spoopyghosty0 2c9a1add30 Agent: battery percent on Steam's scale (Steam shows the fuel gauge rescaled, ~/0.9: the app showed 77 % where the headset showed 85 %, #414); the gauge's own value stays as gauge_percent 2026-10-23 23:44:42 -04:00
spoopyghosty0 33a29a51d5 Triage: meta-org-scoped-id-wait matches Eleven's real log (platform start, then Request failed; the stack names no game code) 2026-10-23 22:33:00 -04:00
spoopyghosty0 9e15706068 Platform: answer Meta's org-scoped user id request (frame.ovr_org_scoped_id, GitHub #452)
OVRPort's platform loader exports ovr_User_GetOrgScopedID, ovr_Message_GetOrgScopedID and ovr_OrgScopedID_GetID as
`mov x0, xzr; ret` (runtimes 3.4.3-23204ea and -aa54c3f): request id 0, no message ever, so Meta's C# wrapper logs
"An unknown error occurred. Request failed." and the callback never runs (Eleven Table Tennis waits in
<BuildIdentity>b__0; Cortopia's Escaping Wonderland #360 / TMNT #422 too).

libfp_langpack.so (native/langpack) now answers it when its @FPORGID@ marker is set (or FRAMEPORT_ORG_SCOPED_ID=1):
a fresh request id and a successful ovrMessage_User_GetOrgScopedID (0x18F0B01B) whose ovr_OrgScopedID_GetID is an
ovrID hashed from the (already stable, frame.ovr_stable_user) user id: FNV-1a 64, top bit cleared, never 0. A new
@FPLANGS@ marker lets the library be linked for that alone with the language-pack calls passed through to the loader
(FRAMEPORT_LANGPACK_SERVE overrides); builds with frame.langpacks/asset_files behave as before.
ovr_User_GetUserProof (Eleven's next request) is answered by OVRPort itself (queued message with a nonce), so it is
left alone.

New patch frame.ovr_org_scoped_id (order 35, default on, suggested for games known to wait: Cortopia's analysis flag,
Eleven Table Tennis, TMNT), triage signature meta-org-scoped-id-wait, PLAYBOOK row, host tests against the stand-in
loader. frame.cortopia_org_id stays until the generic answer is headset-verified.
2026-10-23 22:31:42 -04:00
spoopyghosty0 379bd161cb Scan: Unreal's chunk expansion files next to an APK (patch.pakchunk<N>-Android_ASTC.<package>.obb) are sent too, and installs pick up files an older scan skipped (Asgard's Wrath 2 got only main + patch.0 and hung at start-up, #471) 2026-10-23 22:16:44 -04:00
spoopyghosty0 398081b22f Agent: diagnostics include the three newest XRService logs, SteamVR's vrcompositor/vrserver logs, Steam's previous console log and steam.service's journal (a session that took Steam down kept no host-side logs, #441) 2026-10-23 22:14:02 -04:00
spoopyghosty0 fdaba13f51 FrameBridge: late-drawing check compares every pixel of rarely acquired panels (a sparse sample missed AC Nexus' text-only notice), read back asynchronously so frames never wait on it 2026-10-23 19:14:22 -04:00
spoopyghosty0 7fd9e20e21 Tests: lint fixes (f-string in the shared-folder launcher test, long CryEngine log line) that failed CI's ruff step 2026-10-23 19:14:19 -04:00
spoopyghosty0 595d12a038 FrameBridge: a flipped panel the app finishes drawing after xrEndFrame (no new acquire) is flipped again once its content settles (AC Nexus' calibrate-arms screen stayed upside down) 2026-10-23 17:56:31 -04:00
spoopyghosty0 9793e6dbae FrameBridge flip_dump: a quad still empty whenever it was flipped is saved ~1 s later as shown ("late") 2026-10-23 17:34:28 -04:00
spoopyghosty0 94783f564e Catalog: AC Nexus without device.text_input_window (its Android window took the controllers' input in the headset) 2026-10-23 17:31:05 -04:00
spoopyghosty0 4ab6b670d2 FrameBridge: flips and alpha inversion use the last released image (the one a layer shows), not the last acquired one; layer_debug logs quad swapchains/rects and each swapchain's first flips, flip_dump adds the image as shown ~3 s later 2026-10-23 17:22:43 -04:00
spoopyghosty0 e3e8fc4388 FrameBridge: a flip blit still running after 100 ms counts as done (it was flipped again next frame, so AC Nexus' panels stayed upside down at random); the shared command buffer waits for it before reuse 2026-10-23 17:00:21 -04:00
spoopyghosty0 e0638d6b6d Catalog: a newer remote entry this app can't use (unknown patch, setting, field or min_app) keeps the last usable one instead of falling back to the bundled entry; fields that stand for a patch are checked too 2026-10-23 16:41:34 -04:00
spoopyghosty0 09a329d54a Catalog: AC Nexus keeps the Quest 2 identity until the release with the Vulkan panel pool reaches users, with swapchain_pool on (also eases the panel limit behind the tutorial freeze, #424) 2026-10-23 16:29:40 -04:00
spoopyghosty0 4eff40ca64 swapchain_pool (Vulkan): pool chained and mipmapped panels, log every decline (#424)
The first device run (AC Nexus, Quest 3) pooled nothing and said nothing
about why. The pool declined any chained create info and any mipCount
other than 1; on Vulkan it now accepts a XrSwapchainCreateInfoFoveationFB
(irrelevant to a private image) and honours XrVulkanSwapchainCreateInfoMETA's
extra create flags and usage, and keeps the app's mip levels (barriers
cover every level, level 0 goes into the atlas). GLES keeps its rules.

Every decline is logged once per reason ("swapchain_pool: not pooled:
..."): session mismatch (also from the proactive check), array/faces,
samples, mips, usage, flags, size, format, chained struct type, Vulkan
device, GL context, slots, atlas refused, image allocation, atlas full;
plus one line when the 8-swapchain trigger starts pooling. The
xrCreateSwapchain log gains mips= and the first chained struct type.

Harness: the device sequence (1728 eye set, 2x2 and 1024x1024 usage 0x39,
2016 eye set, twelve 0x39 panels with foveation/META chains and a 4-mip
panel, flip-tracked runtime creates) pools every panel and the 2592x2256
set then fits under a 15-swapchain cap; decline reasons are checked.
2026-10-23 16:22:39 -04:00
spoopyghosty0 7a194ccaa0 Fix #424: Vulkan panel pool for swapchain_pool, AC Nexus back to Quest 3
swapchain_pool now also covers Vulkan sessions (XR_KHR_vulkan_enable/2):
from eight live app swapchains, single-layer format-43 colour panels get
private device-local VkImages (app usage + transfer, returned as
XrSwapchainImageVulkanKHR, UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL at the
first acquire) packed into up to two 4096x4096 sRGB runtime atlases.
xrEndFrame records one command buffer per frame on the binding's queue
(3-deep fence ring, submits under flip_lock): vkCmdCopyImage from each
referenced panel into the waited atlas image, which returns to
COLOR_ATTACHMENT_OPTIMAL before its release. Image ownership, crops and
layer ordering follow the GLES pool; the whole layer rewrite holds the
pool lock so a destroy cannot free an image with recorded copies.
Pooled Vulkan panels stay tracked by flip_vk.c (in-place flips keep
working). Other formats and arraySize-2 eye images keep runtime
swapchains. GLES behaviour is unchanged.

AC Nexus: Quest 3 identity again with swapchain_pool=1 (min_app 1.3.1),
frame.adapter revision 3 for it only. Settings text, triage diagnosis,
PLAYBOOK, DEVICE_NOTES, native README, PATCHES.md and the i18n template
updated. Host harness gains a recording Vulkan double (layouts, copy
regions, ring, failures, cleanup, AC Nexus swapchain count: 12 live with
the new eye set). Not verified on the device.
2026-10-23 16:11:59 -04:00
spoopyghosty0 697b774248 Tests: plain summary for device.shared_folder, outlined icon on the game page, Climb 2's adapter revision in the audio-geometry test 2026-10-23 15:14:39 -04:00
spoopyghosty0 c01257847b Hardware-video launchers log who holds the Iris decoder at start (Steam's web helper blocked 8K cutscenes, #437); room tests run from paths with spaces 2026-10-23 15:08:50 -04:00
spoopyghosty0 cb954b6fd0 device.shared_folder: a folder of /sdcard shared with another game (JKXR's companion app and /sdcard/JKXR, #440) 2026-10-23 14:59:45 -04:00
spoopyghosty0 e4ffc4eb8a Share invitation also for catalog recipes still marked unknown or with issues once the game was played or passed a launch test (#310/#329) 2026-10-23 14:26:29 -04:00
spoopyghosty0 bccd916fab a32 bridge: export libmediandk's AMEDIAFORMAT_KEY_* data symbols, which Unity's video player also looks up (#439)
On the device Unity got past AMediaFormat_new and stopped at AMEDIAFORMAT_KEY_AAC_PROFILE, falling back to its JNI path again. The guest runtime now defines the 25 keys libunity.so references (AOSP's values; the host's libmediandk compares key names by content) and exports them; the harness checks each one via dlsym and uses them as format names. Ghost Giant: package revision 3.
2026-10-23 14:19:17 -04:00
spoopyghosty0 e76366728c cortopia_org_id: TMNT by its package instead of an ANALYSIS_VERSION bump (no library-wide re-analysis for one game) 2026-10-23 14:16:10 -04:00
spoopyghosty0 fffe6c088c frame.cortopia_org_id: also TMNT: Empire City's Login (no .Meta namespace), Cpp2IL nested-type method lookup (#422)
TMNT: Empire City awaits Users.GetOrgScopedID in Cortopia.Scripts.Platform.OculusPlatform.<Login>d__17 (after the
licence check and GetLoggedInUser, behind the same internetReachability == LAN check as Escaping Wonderland), which
OVRPort's platform library never answers. The patch now also looks there (parse_methods: "<Login>d__.MoveNext" =
that nested type's MoveNext; a plain MoveNext would be FetchEntitlements'), the analysis flag also covers
OculusPlatform directly in Cortopia.Scripts.Platform (ANALYSIS_VERSION 26), the triage signature matches the
<Login>d__ stack. The entitlement request is untouched.
2026-10-23 14:14:17 -04:00
spoopyghosty0 dcbcce0f66 a32 bridge: NDK media (AMediaFormat/AMediaCodec/AMediaExtractor) for Unity's video player, codec buffers mirrored in guest memory (#439)
Unity 2017.4 (Ghost Giant) looks up 43 libmediandk functions and fell back to its JNI path when AMediaFormat_new was missing; that path reads MediaCodec ByteBuffers above 4 GiB (GetDirectBufferAddress returned 0), so the intro video never played. Format strings/buffers get guest copies freed with the format; input buffers are mirrored and copied to the codec at queueInputBuffer, output buffers copied in at getOutputBuffer (the range dequeueOutputBuffer reported). Harness: fake libmediandk with buffers above 4 GiB + guest test library. Triage a32-video-ndk; Ghost Giant gets Update on Frame.
2026-10-23 14:09:29 -04:00
spoopyghosty0 0578954826 artifacts: SHA256SUMS for codec revision 11 2026-10-23 14:09:26 -04:00
spoopyghosty0 69c61ac914 Diagnostics: the USB cable link's state on both sides (Frame usb0/UDC/gadget/services, PC addresses and adapters) and a logged cable wait (#398) 2026-10-23 14:08:17 -04:00
spoopyghosty0 0c4493af44 Game page and CLI warn about an .obb named for another package than the APK (a renamed build's data, #129) 2026-10-23 14:04:41 -04:00
spoopyghosty0 9612812609 GAZZLERS: take the Meta Quest platform path instead of stopping with "Current Platform is not supported" (#404)
The game picks Meta/PICO from SystemInfo.deviceModel and throws for Lepton's
Valve Lepton. frame.gazzlers_quest_platform (catalog recipe) makes
PlatformDeterminer.GetAndroidPlatform return Platform.Meta (Cpp2IL lookup);
triage signature, PLAYBOOK row, catalog entry, test.
2026-10-23 13:58:23 -04:00
spoopyghosty0 b1ba609e79 Lepton games get the Frame's time zone: a readlink shim on the launcher's PATH resolves SteamOS' relative /etc/localtime for Lepton (#429) 2026-10-23 13:58:04 -04:00
spoopyghosty0 9e2fce433f Video decoder: videos up to 640x480 decode in software so they never hold Iris (codec revision 11); notes on 8K (#437)
Iris takes an 8K session only while no other is open. Checked in Batman's container on the dev Frame (SteamOS 0.5.6):
the small video now stays off Iris, but DRM2.mp4 (8192x4096 HEVC) is still refused because Steam's web helper keeps
the Iris decoder open, so Batman's 8K cutscenes stay in software. hold_probe.cpp holds a decoder for such tests.
2026-10-23 13:44:54 -04:00
spoopyghosty0 ae2846f852 FramePort on the Frame: game data on the same drive is hardlinked into the install instead of copied, so games don't take twice the space (#433) 2026-10-23 13:32:47 -04:00
spoopyghosty0 913d06afa9 Type on Frame: Copy from Frame puts the Frame's clipboard (Desktop Mode's Plasma, else gamescope's X displays) on this PC's (#413) 2026-10-23 13:30:18 -04:00
spoopyghosty0 fee75576cf Files: Backspace goes to the folder above, and going back up returns to where that folder's list was scrolled (#411) 2026-10-23 13:26:44 -04:00
spoopyghosty0 2cca7d6012 Type on Frame: the agent releases a held key the PC stopped refreshing, so a lost key release can't repeat on the Frame without end (#411) 2026-10-23 13:25:37 -04:00
spoopyghosty0 12ed206a85 frame.launcher: a launcher on an <activity-alias> alone gets moved onto its target activity, which Lepton can start (HOTD2 VR, #400/#469) 2026-10-23 13:19:40 -04:00
spoopyghosty0 4fd46ba023 QuestCraft: frame.pojlib_connection counts the Frame's network as online, so it downloads its Java runtime (#468)
pojlib's API.hasConnection() only counts Wi-Fi; Lepton's Android reports Ethernet, so the JRE download was skipped
("libjvm.so not found", GitHub #18). hasConnection() returns true (in-place classes.dex edit). Catalog recipe +
QuestCraft's package (no ANALYSIS_VERSION bump for one game family; new scans record pojlib_connection), triage
questcraft-no-java-runtime, PLAYBOOK row. Checked on the dev Frame: the installed APK carries the edit and starts
(72 fps headless); the download itself runs only after Play in QuestCraft's menu, which needs the headset.
2026-10-23 13:13:52 -04:00
spoopyghosty0 a6a28aaf91 Scan: other expansion-file kinds named for the APK's package (Crisis Brigade 2's office.<ver>.<pkg>.obb) are sent too (#415) 2026-10-23 12:52:44 -04:00
spoopyghosty0 8fedb37860 Rescan: another version of a game whose files are gone is offered in a dialog with Use this version, instead of a summary line (#427) 2026-10-23 12:50:53 -04:00
spoopyghosty0 2ecaa56ead Steam library: Later is the default when FramePort runs on the Frame itself, and the waiting step says how to add the games now (#425) 2026-10-23 12:46:50 -04:00
spoopyghosty0 890b2bc615 Linux apps: launchers put an unpacked app's own library folders on LD_LIBRARY_PATH, as the install's library check already assumed (#473) 2026-10-23 12:42:53 -04:00
spoopyghosty0 a8e01c01dc The Climb 1/2 start again: the dormant foveation diagnostic stays out of CryEngine's renderer (#432, #449)
1.3.0 linked libVkLayer_fdm_darken.so into every arm64 game's engine library; in libCryRenderVulkan.so CrySystem
initialization then fails right after the renderer lists its Vulkan device extensions, and the shutdown crashes
(SIGSEGV in CSystem::ShutDown). Bisected headless on the dev Frame by swapping libraries between the September
known-good APK and a current build: only the modified renderer breaks it. Climb 2's private VrApi bridge still
loads the layer itself (72 fps headless with this change). frame.adapter package revision 3 for both games;
triage cryengine-init-failed.
2026-10-23 12:41:02 -04:00
spoopyghosty0 a08d46a1b7 Embedded OpenXR loaders: the game's loader skips implicit API layers, so Valve's layer isn't stacked twice (crash at Play, #254) 2026-10-23 12:19:04 -04:00
spoopyghosty0 2fefb1e6d1 Catalog: AC Nexus keeps the Quest 2 headset identity (#424)
With Quest 3 the game switches to a 2592x2256 eye buffer after the intro; by then its UI panels have used up the
runtime's ~15 swapchains per session, so the new eye buffer is refused (XR_ERROR_LIMIT_REACHED) and it stops
drawing. Headless on the dev Frame, same build: Quest 3 frames stop, Quest 2 holds 72 fps.
2026-10-23 12:13:26 -04:00
spoopyghosty0 043955983f Catalog: PrimedGun v2 without the embedded-loader patch until its loader recursion is fixed (crash at Play, #254) 2026-10-23 12:02:54 -04:00
spoopyghosty0 ffe57c5c89 Diagnostics: read the bundled agent and bootstrap.sh as UTF-8; an ASCII default encoding made every diagnostics zip fail (#349) 2026-10-23 12:01:53 -04:00
spoopyghosty0 dd9f20049e Catalog: PianoVision unsupported (needs camera hand tracking and USB MIDI) (#297) 2026-10-23 11:56:41 -04:00
spoopyghosty0 26975686f9 Catalog: 15 community recipes (Drums Rock, Darksword, GoldenEye VR, Barbaria, Little Nightmares VR, Movie Deck for Plex, JKXR, SpaceControl, Solero, Orcs Must Die, Epic Roller Coasters, Simpsons Hit and Run VR, Deisim, Astro VR Host, Umurangi Generation VR)
From issues #409 #410 #412 #418 #421 #435 #441 #447 #448 #453 #456 #461 #462 #464 #472 via
scripts/catalog_from_issue.py. GoldenEye VR (no sound), JKXR (stutters after minutes) and Umurangi Generation
(freezes when saving a photo) are "issues"; Simpsons and Astro VR Host setup steps point to the Files tab, the
long instructions moved to details.
2026-10-23 11:54:10 -04:00
spoopyghosty0 459c62ba2d Room tab: the not-connected screen sits under the heading like the other tabs (the heading's expand, meant for its row with the buttons, took half the window's height when it stood alone) 2026-10-20 08:24:32 -04:00
spoopyghosty0 0df0bd16b5 hevc: tone map HDR (PQ/HLG) Main 10 to SDR BT.709 instead of flat colours
The software Main 10 path narrowed HDR10 pictures to 8 bits as they were, so
PQ shown as SDR looked grey and BT.2020 colours dull. hdr_tonemap.h maps
brightness with BT.2390's EETF (203 cd/m2 reference white = SDR white,
highlights rolled off up to MaxCLL / the mastering peak / 1000 cd/m2) and
converts colour per chroma sample through linear light into BT.709, moving
out-of-gamut colours toward grey of the same luminance. The component then
prefers its bitstream colour aspects, so surfaces and its RGBA conversion see
BT.709. Bands run in the colour helper threads (about 10 ms per 1080p picture
on one big core). Decoder revision 10; artifacts rebuilt with build.py.
2026-10-19 12:41:20 -04:00
Veticia Misumena 7128432e39 hevc: decode Main 10 in software instead of hanging on Iris
Iris on the Frame's kernel (6.18) offers HEVC Main 10 only into its
compressed Q10C capture format. The component always asks for NV12, and a
Main 10 stream in such a session returned no pictures: playback stayed at
0:00, the flush on leaving timed out (ACodec -110) and the next video hit
MediaCodec::reclaim, so every later decoder start in the app failed until it
restarted (seen in Bigscreen Beta with local files over DLNA).

- hevc_profile.h reads general_profile_idc from the parameter sets
  (CODECCONFIG or the first access unit, Annex B or hvcC); streams beyond
  Main never open Iris and decode in FFmpeg's software HEVC decoder, which
  the component already contains.
- Its 10-bit planar pictures are rounded to 8-bit planar before the existing
  outputs (no HDR tone mapping).
- The HEVC component lists Main 10 (level 5.1), so format-aware players
  select it instead of reporting NoSupport and using it anyway.
- Decoder revision 9; artifacts rebuilt with native/hevc/build.py (the same
  toolchain rebuilds revision 8 byte for byte).

Tested on a Steam Frame in Bigscreen Beta: 1080p and 4K Main 10 clips and a
real 1080p Main 10 episode play; 8-bit HEVC and H.264 afterwards play too.
FFmpeg's software HEVC decoder on the Frame's CPU, 4 threads as the
component uses: 1080p Main 10 episode 232 fps, synthetic 4K Main 10 72 fps.
2026-10-19 12:22:36 -04:00
spoopyghosty0 3268c3c412 Site: a Room tab in the clickable app (turnable 3D view of the showcase's made-up bedroom, pretend scan, raw scan vs clean-up, labels with the door suggestion); room data from scripts/showcase/site_room.py 2026-10-19 11:12:48 -04:00
spoopyghosty0 11dd195688 Site: release data refreshed (1.3.0) 2026-10-19 01:12:55 -04:00
spoopyghosty0 5b86843430 FramePort 1.3.0 2026-10-19 00:59:27 -04:00
spoopyghosty0 3e5044ce4a Site: contributor data and games list refreshed (1.3.0) 2026-10-19 00:58:53 -04:00
spoopyghosty0 5345434ba7 Lint: wrap long lines, raise ... from None in the room-scan lock, zip(strict=True) in the scene tests (CI's ruff check failed on the MR merge) 2026-10-19 00:57:08 -04:00
spoopyghosty0 0ca7bc45f0 Room tab: the right column sizes to its content (the Test Quest MR line was clipped when Delete room wrapped or the furniture chips took two rows); the top row stays level by measuring both sides (a short column's last card grows, a tall one makes the 3D view taller); box sizes under 10 cm show two decimals (a 3 cm rug was 0.0 m); room shots re-rendered 2026-10-19 00:23:58 -04:00
spoopyghosty0 a4702d2a13 Docs screenshots: re-rendered with the Room tab in the sidebar 2026-10-19 00:23:57 -04:00
spoopyghosty0 455bc987b2 Docs: the Room tab (experimental) in the README features, INSTALL.md and the site's app tour; showcase shots room + room-editing from a made-up bedroom (bed under a wall TV, desk with two monitors under a window, closet, door, lamp, plant; noisy scan mesh, clean-up report and labels to match); steps open_room / room_editing 2026-10-19 00:23:57 -04:00
spoopyghosty0 4b0b0daa6d Room tab layout: the top row ends level (legend on the 3D view, the right column as tall as it), Test Quest MR as one line in In this room, equal cards and gaps, full-width sliders and segmented tracks, one slider per row in the box editor, dropdown and Delete the same height, even suggestion buttons, aligned Labels columns and counts; clean-up report follows the scanner's scan.json keys (walked_skipped as a note, truncated as a warning) 2026-10-19 00:23:57 -04:00
spoopyghosty0 91f73fbd03 ui_smoke: Room tab editing steps (clean-up, more settings, box selected, doors, a suggestion taken, applying, applied); the pretend Frame cleans up and saves rooms; docs 2026-10-19 00:23:56 -04:00
spoopyghosty0 77b527f129 Room tab: Clean-up card (Recommended or your settings, the key ones in sight, More settings, Apply as a job, the last clean-up's counts), Labels card (rule decisions, Add as door / Add as closet), Furniture & screens editor (select in the list or by clicking the 3D view, type/move/size/turn, add/delete, doors and windows on a wall, Save) 2026-10-19 00:23:23 -04:00
spoopyghosty0 6ab6c11f06 Rooms: REFINE_DEFAULTS (the owner's clean-up defaults + slider metadata), rooms.refine / put_room, doors and windows on a wall and suggestion helpers in mr/room, view3d furniture overlay + picking, CLI frameport frame room refine [--set key=value] [--reset] 2026-10-19 00:23:23 -04:00
spoopyghosty0 92e84a1a4b Agent v107: room scan clean-up settings (rooms/refine.json, passed to every build), room_refine (fp_roomscan --refine-only on a copy of the room folder), raw files + scan.json kept with the room, room_status returns refine/report/labels/suggestions, edited rooms keep their boxes over a new clean-up 2026-10-19 00:23:22 -04:00
spoopyghosty0 a0b71fdfb7 Docs: room refinement (scan.md options + passes + parity, ROOM_FORMAT raw files and scan.json, CLAUDE.md MR line) 2026-10-19 00:23:22 -04:00
spoopyghosty0 3e8f0ce369 Tests: room refinement (floating pole + walked-through column removed, panels wind inward, --refine-only reproduces, --refine 0 = raw output, label rules on an older room folder) 2026-10-19 00:23:22 -04:00
spoopyghosty0 b65abb4b95 Room scanner: refinement + label rules after the mesh (raw files, scan.json, --refine-only)
The owner's tuned room-scan cleanup (2026-10-18) ported to C, pass for pass: plane offsets re-estimated from the
scan, one-sided structural bands, flat wall/floor/ceiling panels (or snap), near clearing, erase spheres, the walked
volume from the recording's head path, furniture tops, clipping with the unseen-edge allowance, islands, thin
floating pieces, Taubin smoothing; label rules on the scanner's volumes (merged/nested/outside boxes removed, shape
relabelling, SCREEN flatness from the raw mesh, door/closet suggestions in scan.json).

room.txt + room-mesh.bin are now refined (ceiling re-estimated); the unrefined output stays as room-raw.txt +
room-mesh-raw.bin with the same UUIDs; scan.json holds heads, settings, report, labels, suggestions. The scan path
refines its raw files as read back, so --refine-only on them reproduces the scan byte for byte; --refine 0 writes
the old output. The fusion grid is freed before the refinement (peak memory unchanged).
2026-10-19 00:23:22 -04:00
spoopyghosty0 0478588886 Tests: map the floor plan back with forward (-Z) at the top, as preview_svg now draws it (4f871b20) 2026-10-19 00:23:21 -04:00
spoopyghosty0 1a5892ee40 RoomCanvas: a room full of furniture draws only its outlines while the view moves (smaller patches) 2026-10-19 00:23:21 -04:00
spoopyghosty0 e3788c0b2b Room tab: fit the view to the scan without its stray far-off bits (2nd-98th percentile), rounded icons, US spelling 2026-10-19 00:23:20 -04:00
spoopyghosty0 feda5f4a03 Docs: the Room tab (INSTALL.md, CLAUDE.md UI notes) 2026-10-19 00:23:20 -04:00
spoopyghosty0 6d9fb0b1e6 ui_smoke: Room tab steps; the pretend Frame has a made-up scanned room with a mesh, a scan that steps forward and can be cancelled 2026-10-19 00:23:20 -04:00
spoopyghosty0 b78160ca6f Room tab: scan in the headset with progress and Cancel, play area, delete, the room in 3D (window + full browser view), what's in it, an experimental pill and a Test Quest MR tip; the room cache moves to ui/room_state.py (mesh cached per Frame in rooms-cache), the Frame page keeps a short room card with Open Room 2026-10-19 00:23:20 -04:00
spoopyghosty0 20c36b112e RoomCanvas: the room in 3D inside the window (Flet canvas, one redraw worker at <=18 fps through a LoopUpdater, fewer mesh edges while moving; drag turns, scroll/pinch zooms, right-drag moves, double-click fits) 2026-10-19 00:23:19 -04:00
spoopyghosty0 fcfba77f31 Room 3D models: mr/view3d (walls, furniture boxes, mesh reduced to a few thousand edges, orbit camera, dollhouse projection) and a token-protected 127.0.0.1 server for a self-contained WebGL room page (FPRM parsed with DataView) 2026-10-19 00:23:19 -04:00
spoopyghosty0 8870afe18a Room scans can be cancelled: agent room_capture_stop (agent 106) stops the scan job and marks it cancelled; rooms.cancel_scan, rooms.scan raises ScanCancelled 2026-10-19 00:23:19 -04:00
spoopyghosty0 4be04b8a34 Room floor plan: seen from above with forward (-Z) at the top; it had +Z at the top with +X still on the right, i.e. the room mirrored 2026-10-19 00:23:18 -04:00
spoopyghosty0 e231c5a6fe scene: drop PR #246's per-call semantic-label log line (it logged every call; mr-scene logs each entity's labels once), as in the PR's merge 2026-10-19 00:23:18 -04:00
spoopyghosty0 7585acd0a8 Tests: the quest_buttons harness declares scene_app_mesh (mr-scene's xrCreateInstance sets it; the native test didn't compile) 2026-10-19 00:23:18 -04:00
spoopyghosty0 b9fb5791b0 Regenerated docs/PATCHES.md, site patches.json and the locale template for the two new patches 2026-10-19 00:23:17 -04:00
spoopyghosty0 a5199495fd Artifacts: FrameBridge (arm64-v8a + armeabi-v7a) rebuilt from the merged source (PR #246's layer limit and quad filter on mr-scene's scene emulation); xrshim rebuilt, unchanged bytes; SHA256SUMS regenerated (now also lists the roomscan finish_*.png prompts, which it had missed). None of PR #246's binaries are kept. 2026-10-19 00:23:17 -04:00
spoopyghosty0 28a5795323 Docs: PLAYBOOK rows for depth occlusion, the player-in-room check and the layer limit; DEVICE_NOTES note on what PR #246 brought into mr-scene 2026-10-19 00:23:16 -04:00
spoopyghosty0 d6cf9a3503 Tests: restore the scan test's APK inside a PC program folder (PR #246 had made it a .dll, which no longer tests anything); ruff fixes in the PR's test 2026-10-19 00:23:16 -04:00
spoopyghosty0 2a769dddba Catalog (PR #246): min_app 1.2.2 on the MR recipes (they need this branch's layer limit and room emulation); drop the com.mrf.* copies of games the catalog already has (com.kluge.SynthRiders, com.PotamWorks.SmashDrums, jp.co.hit_point.vneko: same titles, other package ids, they listed every game twice); LEGO Bricktales back to 'use VR mode' (MR not verified, its recipe turns no MR setting on); honest notes where no tested version was recorded 2026-10-19 00:23:15 -04:00
spoopyghosty0 7a65e86cdc FrameBridge layer limit: FrameBridge's own fade quad is always kept with the projection layers, the app's other layers fill the rest in submission order; one log line per trim (at most 5), trimmed layers no longer counted as unusable 2026-10-19 00:23:15 -04:00
spoopyghosty0 2be8873fec frame.meta_depth_occlusion: offered but not turned on by detection, only for 64-bit IL2CPP Unity games; asset files are streamed and only those with the keywords rewritten (max 512 MiB); Laser Dance's asset fixes only for the build whose original values all match; revision 1
- detect: many Meta XR SDK games carry the Depth API without using it; until a headset check shows more games
  with the symptom (or OVRPort is found advertising XR_META_environment_depth, which a FrameBridge hide setting
  would fix generally), the patch comes from Laser Dance's catalog recipe or the user's choice.
- ApkWorkspace.size/contains: entries are searched in 4 MiB chunks instead of read whole.
- Laser Dance: the fixes' original values at all seven places together identify the build (no versionName was
  recorded with them); another build gets none of them.
- tests: lz4 is skipped when missing, the known/unknown Laser Dance build, chunked search.
2026-10-19 00:23:14 -04:00
spoopyghosty0 7d5ed42d18 frame.unity_scene_active_rooms: never suggested (catalog recipe or the user's choice, Laser Dance), shown only for Unity scene games; plain summaries for both new Unity patches, stray blank lines removed
The PR turned it on for every Unity game with USE_SCENE or MRUK's library: a game-logic change keyed to one
game's symptom (MRUK games mostly don't use OVRSceneManager at all). A scanned room around the player usually
passes IsUserInRoom by itself.
2026-10-19 00:23:14 -04:00
spoopyghosty0 90bf8bcd67 adapter: scene_emul needs the xrshim through XRSHIM_SETTINGS already (mr-scene); drop PR #246's duplicate 2026-10-19 00:23:14 -04:00
spoopyghosty0 d028390da5 Library: drop PR #246's meta_spatial_permissions and meta_depth_occlusion migrations and the re-analysis hook that forced frame.meta_depth_occlusion into user recipes (mr-scene's scene_permissions covers the room games; non-user recipes follow detection and the catalog after the analysis bump; user recipes stay as edited) 2026-10-19 00:23:14 -04:00
spoopyghosty0 0a542d849e xrshim: emulated functions again only for a real instance (OpenXR: XR_NULL_HANDLE serves only the pre-instance functions; with mr-scene the shim would otherwise hand out the routed xrPollEvent/xrLocateSpace) 2026-10-19 00:23:13 -04:00
alicecatpaw 09358fb65e Address review comments on meta_depth_occlusion
- Limit _patch_shader_proximity_fade to known builds (Laser Dance) and known shader signatures/keywords to avoid modifying unrelated shaders
- Rename HARD_OCCLUSION and SOFT_OCCLUSION keywords in .assets files only, leaving global-metadata.dat intact so managed code cannot enable renamed occlusion variants
- Defer meta_depth_occlusion library migration for entries analysed before the field existed until re-analysis, and update user recipes on re-analysis
- Bump ANALYSIS_VERSION to 23 so older library entries are re-analysed for meta_depth_occlusion
2026-10-19 00:23:13 -04:00
alicecatpaw ae49d886da Address review: bump ANALYSIS_VERSION to 14, restore hand-tracking heuristic, and fix library migration 2026-10-19 00:23:13 -04:00
alicecatpaw 7afaa2629b Add catalog recipes for Neko Atsume, Punch Fit, Smash Drums, and Hitstream 2026-10-19 00:23:12 -04:00
alicecatpaw 5e27333f74 Add catalog recipes for Laser Dance and Synth Riders MR modes 2026-10-19 00:23:12 -04:00
alicecatpaw 68f278a744 Add Meta depth occlusion and active room patches for Unity MR games
- Implement frame.meta_depth_occlusion to zero out Meta EnvironmentDepthManager methods in libil2cpp.so and neutralize close-proximity depth shader occlusion in Unity assets

- Implement frame.unity_scene_active_rooms to bypass Meta OVRSceneManager active room position evaluations in libil2cpp.so

- Recommend frame.meta_permissions when spatial data or scene permissions are declared

- Remove redundant hand-tracking heuristic forcing controller_fix=0

- Add unit tests for depth occlusion, active room bypass, and manifest heuristics
2026-10-19 00:23:12 -04:00
alicecatpaw a0c26c9646 Emulate Meta spatial entity discovery and enforce runtime layer limits
- Emulate XR_META_spatial_entity_discovery (xrDiscoverSpacesMETA, xrRetrieveSpaceDiscoveryResultsMETA) and XR_META_boundary_visibility
- Add system properties for space discovery and boundary visibility
- Fix semantic labels length and null termination
- Allow xrGetInstanceProcAddr with null handle for emulated procs in xrshim
- Trim composition layers exceeding runtime maxLayerCount in xrEndFrame and validate quad extents
- Hook xrshim when scene_emul is enabled in adapter settings
- Update prebuilt native artifacts and checksums
2026-10-19 00:23:12 -04:00
spoopyghosty0 45900ef294 Installing a room-emulation game syncs the room scanner and its headset prompts to the Frame (in-game scan requests used whatever copy a PC-started scan had left there, e.g. none on a fresh Frame or without the new "Almost done" prompts) 2026-10-19 00:23:11 -04:00
spoopyghosty0 95eed63460 MR scan: once every direction, the ceiling and the floor are covered before the minimum scan time, the prompt says "Almost done" with the time left instead of a ring stuck at 12/12 (owner turned past 360 degrees waiting) 2026-10-19 00:23:11 -04:00
spoopyghosty0 04d03b8f87 MR scan preview: one calm colour (FramePort's secondary blue, opacity by floor/furniture/wall), filled 10 cm squares instead of 3x3 blotches, fainter, sort order below the scan prompts (owner: colours and layers clashed) 2026-10-19 00:23:11 -04:00
spoopyghosty0 6617941d47 MR scan preview (agent 104): a world-locked floor map overlay shows what the headset scan has covered (10 cm cells: floor grey, furniture orange, walls blue), redrawn at most once a second; also in game-requested scans; --no-preview / preview:false; disables itself on overlay errors or slow uploads 2026-10-19 00:23:10 -04:00
spoopyghosty0 05344906ef MR diagnostics: log room-entity locates (base space, flags; every one without both valid bits): MRUK keeps an anchor pending forever otherwise (Pinball FX's load never succeeds) 2026-10-19 00:23:10 -04:00
spoopyghosty0 94ece1a232 MR scan on request (FrameBridge): a capture request asks FramePort to scan (request/ack/result files), holds the completion, reloads the scanned room on ok, fails with XR_ERROR_RUNTIME_FAILURE otherwise, falls back to the stored room without a scanner (setting scene_scan_request, default on) 2026-10-19 00:23:10 -04:00
spoopyghosty0 38bd178aa3 MR scan on request (Frame side, agent 103): launch.sh watcher _scan_watch runs the headset room scan when FrameBridge requests one (request/ack/result files in the game's external files dir), saves it as the current room and copies it into the game folder 2026-10-19 00:23:09 -04:00
spoopyghosty0 56c821cce3 Triage: no scene-missing / mr-discovery-unsupported warning when FrameBridge serves the room (they matched OVRPort's extension list and the shim's own routing line; TestQuestMR saw the full room) 2026-10-19 00:23:09 -04:00
spoopyghosty0 3298c5f64d MR: scene capture completes after 1.5 s like a real scan (Pinball FX re-asked at once); log each room query per entity and the labels an app gets (with its support info) 2026-10-19 00:19:18 -04:00
spoopyghosty0 087ad75f47 MR: shim-routed generic calls handle only fake room spaces and room events; real ones go to OVRPort's dispatcher (its headset identity, events and controller spaces were skipped for every scene_emul game: Vermillion's controllers, NOPE) 2026-10-19 00:19:18 -04:00
spoopyghosty0 460c503e43 MR diagnostics: log the head against the floor outline when a game looks for the room (MRUK's current room; Pinball FX asks for a scan again and again) 2026-10-19 00:19:17 -04:00
spoopyghosty0 3687a6eb0d MR: Meta scene/anchor permissions with every scene_emul recipe (migration scene_permissions; NOPE/Pinball never asked for the room), emulate XR_META_boundary_visibility, log the app's passthrough calls 2026-10-19 00:19:17 -04:00
spoopyghosty0 e07441fb09 MR: hiding the STAGE bounds becomes per-game setting scene_hide_bounds (Demeter on; NOPE stalled on a black screen without a play area) 2026-10-19 00:19:16 -04:00
spoopyghosty0 212801ead2 Cpp2IL: run with invariant globalization (.NET aborts without libicu on fresh WSL/Linux); don't cache .NET start failures as unreadable games, retry old cached ones; CLAUDE.md/PLAYBOOK: STAGE bounds note 2026-10-19 00:19:16 -04:00
spoopyghosty0 7971354675 MR: hide the STAGE bounds from scene-emulation apps (OVRPlugin can't outline the Frame's 1x1 m rect: Demeter's guardian check threw), box walls in loop order 2026-10-19 00:19:15 -04:00
spoopyghosty0 c3144f65dd MR: room entities offer SHARABLE like Meta's runtime; log refused scene components (Demeter rejected the room) 2026-10-19 00:19:15 -04:00
spoopyghosty0 210d6e80c5 MR: rooms older Meta SDKs accept (labels as Meta's runtime maps them, GLOBAL_MESH only with the mesh extension), rectangular rooms from perpendicular walls, ceilings found after a short look up, 60 s minimum scan 2026-10-19 00:19:15 -04:00
spoopyghosty0 f8ee87145f MR scan: record only while passthrough streams meshes (a leftover block started a scan), pause + prompt when it stops, refuse scans under 30 frames 2026-10-19 00:19:14 -04:00
spoopyghosty0 52df211266 MR scan prompts: starts by itself (no passthrough wait), ceiling then 'look back down', floor step, finish early once everything was seen 2026-10-19 00:19:14 -04:00
spoopyghosty0 976260ed14 MR: agent and PC room parsers accept the optional wall label (scanner output was refused) 2026-10-19 00:19:13 -04:00
spoopyghosty0 893db92985 MR: scene emulation always brings the xrshim (OVRPort's dispatcher refuses scene calls; Pinball FX VR) 2026-10-19 00:19:11 -04:00
spoopyghosty0 a690d0ee3f Ship robust room scanner and document capture performance 2026-10-19 00:18:59 -04:00
spoopyghosty0 d48dcaba4e Fit stable room surfaces from smoothed depth fans 2026-10-19 00:18:58 -04:00
spoopyghosty0 f68c7f4232 Skip unusable tracking frames and reject inpainted depth 2026-10-19 00:18:58 -04:00
spoopyghosty0 fc5933bb2a Ship deterministic room scanner artifact and device validation notes 2026-10-19 00:18:58 -04:00
spoopyghosty0 0e41e1fcca Validate room scanning geometry and harden bounded reconstruction 2026-10-19 00:18:56 -04:00
spoopyghosty0 bbb05cfc0b Add offline room reconstruction and synthetic depth renderer 2026-10-19 00:18:44 -04:00
spoopyghosty0 71a427ac0f CLAUDE.md: mixed-reality room scanning notes 2026-10-19 00:18:44 -04:00
spoopyghosty0 e8eea8548c MR: scan the room in the headset - RoomView 3D capture with headset prompts, wall labels, agent 92
The agent records Valve's passthrough depth mesh (IVRBlockQueue + IVRPaths, Background/Overlay OpenVR app), converts
poses with the final raw->standing transform, shows pre-rendered prompts as a SteamVR overlay, then builds the room
with fp_roomscan (room_capture / room_capture_status). PC: rooms.scan, Frame page progress, frameport frame room scan.
FrameBridge reads the optional wall label (INVISIBLE_WALL_FACE). ROOM_FORMAT: outline orientation stated exactly.
2026-10-19 00:18:42 -04:00
spoopyghosty0 ab4bb5b68d Finish room launcher integration and document device validation 2026-10-19 00:18:40 -04:00
spoopyghosty0 ee902e4c40 Diagnose missing MR rooms and unsupported scene discovery 2026-10-19 00:18:16 -04:00
spoopyghosty0 e1fd4fd244 Enable scene emulation for scene and spatial anchor games 2026-10-19 00:18:15 -04:00
spoopyghosty0 a5172c1e2f Show room readiness and floor plans in the Frame UI 2026-10-19 00:18:12 -04:00
spoopyghosty0 5c9d2e5c62 Add room CLI and reuse host helper synchronization 2026-10-19 00:18:11 -04:00
spoopyghosty0 79c5a210f8 Store rooms on the Frame and synchronize Quest launchers 2026-10-19 00:18:10 -04:00
spoopyghosty0 4864703e11 Add portable room format validation and floor plan previews 2026-10-19 00:17:51 -04:00
spoopyghosty0 b3e5b2401e Document scene implementation validation and exact device handoff 2026-10-19 00:17:51 -04:00
spoopyghosty0 301f734dbf Rebuild FrameBridge scene emulation artifacts deterministically 2026-10-19 00:17:49 -04:00
spoopyghosty0 577b72969a Verify shim routing and protect erased or stale scene handles 2026-10-19 00:17:34 -04:00
spoopyghosty0 11ccdb7fac Test room geometry discovery persistence and malformed data on the host 2026-10-19 00:17:33 -04:00
spoopyghosty0 7fe604dbec Report scanned room or guardian box on scene capture requests 2026-10-19 00:17:33 -04:00
spoopyghosty0 2306b2e788 Route all emulated scene APIs and fake space consumers through the shim 2026-10-19 00:17:33 -04:00
spoopyghosty0 65b39a51ca Persist random UUID application anchors across sessions 2026-10-19 00:17:32 -04:00
spoopyghosty0 8473fe429e Add META discovery and persistence entry points and events 2026-10-19 00:17:32 -04:00
spoopyghosty0 3586d520d3 Expose room polygons volume bounds and triangle meshes 2026-10-19 00:17:32 -04:00
spoopyghosty0 79f60057fa Load scanned STAGE room files and bounded mesh sidecars 2026-10-19 00:17:31 -04:00
spoopyghosty0 77b01cbadb MR: optional wall label in room.txt; in-headset scan prompts (render_prompts.py) 2026-10-19 00:17:31 -04:00
spoopyghosty0 b9cb73a6c7 MR: Phase 0 findings - Valve's RoomView 3D mesh is readable through IVRBlockQueue/IVRPaths 2026-10-19 00:17:31 -04:00
spoopyghosty0 11a677ed1c MR: room file contract (room.txt, room-mesh.bin, scan dumps) 2026-10-19 00:17:30 -04:00
spoopyghosty0 d3a0846775 Release prep: the #406, #254 and #387 fixes together on main
Adapter rebuilt from the combined source (fixed_resolution + swapchain_pool), libfp_xrruntime.so back in SHA256SUMS, ANALYSIS_VERSION 24 for static OpenXR loaders (23 is frame.main_process), agent v108 for the static-loader launcher line (above the MR branches' 104-107 so a Frame with one of those still updates), patch docs and translations template regenerated, Ruff line length.
2026-10-19 00:10:31 -04:00
Codex 25db57f468 Fix #387: pool GLES panel swapchains before the runtime count limit
Add opt-in swapchain_pool with private GLES images and up to two packed
runtime atlases. Start at eight live app runtime swapchains to reserve
headroom before the session cap. Preserve image ownership, GL state,
source fences, crops and the existing cylinder/equirect layer ordering.

Enable for 4XVR, add count-limit triage without unknown suggestions for
older apps, and document the pending headset menu check. Leave default
recipes and adapter revisions unchanged. Rebuild arm64/arm32 artifacts.

Validation: 132 targeted tests passed (2 skipped), including the native
runtime/GL harness; Ruff and generated docs pass. Full host suite:
2095 passed, 137 skipped, 12 failures and 16 setup errors, all denied
socket creation in this sandbox. No headset validation or push.
2026-10-19 00:04:16 -04:00
Codex bd2a742356 Route embedded Khronos OpenXR loaders through FrameBridge
Detect and redirect static loader manifests and Android brokers, ship a runtime negotiation shim and the inner loader chain, and maintain per-game manifests on install and launch. Add the PrimedGun recipe, triage and host coverage.
2026-10-19 00:03:49 -04:00
spoopyghosty0 d0a2d89c92 Keep opt-in OpenXR layer recommendations at full swapchain resolution (#406)
Route OVRPlugin recommendations through the existing extension shim, retain native loader hooks, and log projection rect changes once per second per eye. Enable the fix for Batman, document verification, and rebuild both adapter architectures.
2026-10-19 00:03:20 -04:00
282 changed files with 20085 additions and 2083 deletions

No files matched your search

+41 -12
View File
@@ -60,12 +60,12 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles with `C.Sparkline` (Flet canvas, no charts
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
game; MON_CRITICAL needs force, MON_NEVER is refused)); room (`views/room.py`, persistent, "experimental" pill:
scan with progress + Cancel = agent v106 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
scan with progress + Cancel = agent v109 `room_capture_stop`, play area, delete; cache `ui/room_state.py` shared
with the Frame page's short card, mesh cached in `<data>/rooms-cache/<frame>/`; in-window 3D `ui/room3d.py`
RoomCanvas over `mr/view3d.py` (pure-Python projection, mesh reduced to ≤4k/≤1.5k edges at rest/moving, ≤18 fps
worker; each redraw sends NEW shape controls: changing a Points list made Flet diff it item by item, 0.3-1 s);
"Open full 3D view" = `mr/room_viewer.py` token server on 127.0.0.1 + hand-written WebGL `mr/viewer_page.py`,
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v107):
stopped on disconnect/close; Test Quest MR tip = `mr.TESTER_*`; editing cards `views/room_edit.py` (agent v109):
Clean-up = fp_roomscan's refinement settings, `install/rooms.REFINE_DEFAULTS` = the owner's defaults + slider
metadata (agent `ROOM_REFINE_DEFAULTS` must match, a test checks), saved non-defaults in the Frame's
`rooms/refine.json`, used by every build; Apply = job → agent `room_refine` (`--refine-only` on a copy of the room
@@ -74,7 +74,7 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
canvas: `view3d.pick`) + doors/windows on a wall (`mr.room.plane_on_wall`), Save = `rooms.put_room` → `room_put
edited` (keeps raw files, `edits.txt` survives a new clean-up; "Back to the scan's boxes" = keep_edits false); no
eraser in the GUI (`rooms.refine(erase=)` only)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
(one `ft.ContextMenu` per view, filled on right-click; on one of several selected items they act on the whole
(`app.popmenu`, filled on right-click; on one of several selected items they act on the whole
selection, `C.menu_targets`) and click-and-drag multi-select (`C.DragSelect`: pan start/end on the area + item
hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band).
Selection bars sit below the list: above it, their appearing shifted the items mid-drag. Async handlers (they
@@ -144,10 +144,19 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
in Containers with `ft.ScrollKey`), the header stays; `app.settings_view.show_section(key)` (ui_smoke
`settings-index`).
Help hints: wording for non-obvious terms lives in `ui/help.py` (`HELP`); show it with `C.help_icon(key)` or the
`help=` argument of `section`/`kv` (`C.Check` help key), tooltips via `C.tip()` (wraps). Game actions for the Library
right-click menu (one `ft.ContextMenu` around the grid, filled on right-click; short: `menus.quick_menu`, key actions
+ "More actions…" = the game page with its full "…" menu opened from code, `app.open_game_menu`) and the game
page's "…" menu (`menus.menu_sections` quick=False, sectioned) come from `app.game_actions()`. Picking art (`sources.apply_choice`) downloads into a staging dir and keeps the old
`help=` argument of `section`/`kv` (`C.Check` help key), tooltips via `C.tip()` (wraps). Context menus
(Library, Files, Screenshots, Monitor) and the game page's "…" are FramePort's own (GitHub #414):
`ui/popmenu.PopMenu` = `app.popmenu.show(entries, at=(x, y) | below=box)`, drawn in page.overlay (outside-click
barrier + panels; hover submenus with open/close intent delays, ↑ ↓ → ← Enter Esc via `app._on_key` first, closes
on outside click/Esc/choice/`app.go`; panels size to the estimated label width between MIN_W and MAX_W, longer
labels wrap, flip/clamp into the window, submenus on the side with room; a solid grey left edge (owner: no portal gradient) and a solid soft-accent highlight; fade off with
Reduce motion; hovering only repaints rows). Flet's stock ContextMenu/PopupMenu can't nest and cap at 280 px, so
they're only used for button menus (`C.menu_button`, width scaled; `C.menu_label` wraps). Game menus come from
`app.game_actions()` = `menus.menu_sections`: right-click = Play, Install/Update, Game settings | Open game page,
Select | Update ▸ On the Frame ▸ Saves ▸ Name and artwork ▸ Recipe ▸ Troubleshoot ▸ | Uninstall…, Remove from
library… (`menus.group`: a one-item group stays inline); the page's "…" (quick=False) is the same without what
its hero has; on one of several selected games `menus.selection_menu` (`library.menu_actions`); the recipe card's
buttons = `game_actions(recipe=True)`. Picking art (`sources.apply_choice`) downloads into a staging dir and keeps the old
art if nothing came back; "Update Steam art on Frame" re-sends the art set to the game's anchor (agent ≥ 12).
Patch descriptions/reasons describe the general case, naming games only as "e.g. …".
- Installs: queued/cancelled/failed ones are remembered (library setting `ui.installs`) → Library "Resume" bar;
@@ -232,6 +241,11 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
`FRAMEPORT_DESKTOP=1` skips the Steam-parent watchdog and Steam's display; ensure_host_fixes refreshes entries
(older installs, stale ones removed), uninstall/purge remove them. Per app: library entry field `desktop_entry`
(default on; patches don't apply to Linux apps) → game page switch → agent `desktop_entry`. Untested on device.
- App bundles (discussion #482, `sources/bundles.py`, docs/DEVICE_NOTES.md#app-bundles-xapk--apks--apkm--apkx-discussion-482):
.xapk/.apks/.apkm/.apkx files and folders of split APKs are unpacked + merged into ONE APK (Lepton: one APK per app;
OVRPort: one APK) in `<data>/bundles/<pkg>/<key>/` (lib/assets splits in Python, resource/feature splits via
APKEditor `tools/apkeditor.py`); arm64-v8a split only; library field `bundle`; merged APKs are unsigned (builds sign).
FrameDrop itself takes no bundles; `.apkx` is no defined format (read as a zip of APKs); old encrypted .apkm: refused.
- Scanning: SideQuest backups (`<package>/<time>/apk|obb|data`) and AXRB downloads (`AXRB/<app id>/<binary id>/base.apk`
+ OBBs/assets; its `patched/`/`*-axrb.apk` PC builds are skipped) are recognised (`sources/quest_dump.py`, FAQ).
- Quest/Rift twins stay separate entries, shown and named in Steam "Title (Quest)"/"(Rift)" (`core/titles.py`).
@@ -322,7 +336,11 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
reinstall of the release's `.whl`); `prepare()` → `<data>/updates/<ver>/` (SHA256SUMS.txt required + checked,
layout checked, Windows: the new FramePort.exe must have the running exe's Authenticode signer when that one is
signed, `ready.json`); `apply()` writes + starts detached `apply.ps1`/`apply.sh` (wait for pid → Windows: copy over,
replaced files kept in `updates/<ver>/previous` because the zip has no top folder; macOS/Linux: mv to `.old` + swap,
replaced files kept in `updates/<ver>/previous` because the zip has no top folder; nothing touched while a program file
is in use, old-only files of app/Lib/DLLs/site-packages/data moved to `previous`, a failed copy deletes what it
added (#476: the old rollback kept new modules → old version crashed at start); bundles carry
`frameport/_bundle_files.txt` (package.py) → `core/bundle_files.check` at GUI start removes/ignores leftovers and
reports a mixed install; macOS/Linux: mv to `.old` + swap,
rollback, `xattr -dr` quarantine → relaunch; `<data>/logs/update.log`). `ui/updater.py`: background check (10 s,
then 6 h), sidebar card (hidden control, toggled), Library bar (`library_bar`), changelog dialog, job
`app-update` that restarts only when no other job is active, `apply_pending_at_start()` in `main()` for
@@ -584,20 +602,21 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
**Crash backtraces:** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (launch tests return `crash_log`). Fossilize loads into every app (userdebug guest) → `frame.vk_sanitize` (`native/vkshim`); overport's dispatcher aborts on swapchains > 4096 px → `frame.swapchain_limit`; cylinder layers → quad strips (`cylinder_strips`). Drawing equirect layers ourselves and a focus debounce were tried and removed: don't retry without a new approach. → docs/DEVICE_NOTES.md#crash-backtraces-fossilize-swapchain-limit-cylinder-and-equirect-layers
**360° layers / 4XVR:** the Frame's Android runtime composites only quad + projection layers (quads always above projection layers). Per-game adapter settings (off by default): `equirect_emul` (GLES), `equirect_flip/face/res/fps/stereo`, `stable_local`, `focus_hold` (default on), `aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug`; agent `link_media` / `app_media_dirs` put sent files into a player's own folder. → docs/DEVICE_NOTES.md#360-layers--4xvr
**Companion apps / intents:** a second APK can run inside a game's Lepton container, but Lepton turns every `android.intent.action.VIEW` with data into `steam://openurl` → "open in external player" hand-offs can't work; Lepton installs exactly one `*.apk` per app folder. → docs/DEVICE_NOTES.md#companion-apps--intents-stremio--4xvr
**Text input:** Lepton's Android has no IME and VR apps run headless (no focused window); `lepton-show-flatscreen` keeps VR and gives focus + Steam's keyboard (`device.text_input_window`); `frame.unity_text_input` patches Unity's TMP_InputField/InputField at Cpp2IL offsets; agent `_keyboard` = uinput "Type on Frame". `pm install` of a second APK corrupts the main app (`cmd_real package install` skips the hook). → docs/DEVICE_NOTES.md#text-input-stremio-vr
**Browser stand-in (#466):** Lepton's `ActivityStarter.execute` takes every VIEW intent before resolution → `steam://openurl` into `/lepton/steam.pipe`; apps that first query for a browser (Plex) find none → opt-in `frame.browser_handoff` (activity + `dex/browser-handoff.dex` in the app's own APK, analysis `browser_query`); a 2D app with an APK patch on is built, not as_is (`engine.flat_as_is`). → docs/DEVICE_NOTES.md#browser-stand-in-for-sign-ins-plex-github-466
**Text input:** Lepton's Android has no IME and VR apps run headless (no focused window); `lepton-show-flatscreen` keeps VR and gives focus + Steam's keyboard (`device.text_input_window`); `frame.unity_text_input` patches Unity's TMP_InputField/InputField at Cpp2IL offsets; agent `_keyboard` = uinput "Type on Frame". `pm install` of a second APK corrupts the main app (`cmd_real package install` skips the hook). Its window is a gamescope "desktop game" overlay: SteamVR auto-theaters it before the OpenXR session and opens the dashboard on it at session start (controllers = mouse on the Android screen, AC Nexus/#476/#477) → the launcher (`_theater_hold`, first thing) and dashboard worker keep `dashboard.autoShowGameTheater` off while such a game runs (agent v119, holds per launcher pid in `theater-holds/`); SteamOS 0.5.6 logs the dashboard button as `open_dashboard_action`. → docs/DEVICE_NOTES.md#text-input-stremio-vr, #typing-window-takes-the-controllers-steamvr-theater-mode-476-477-ac-nexus
**User reports #4-#10:** shortcuts go to the signed-in account (agent `library_users`), `launch` errors `NOT_IN_LIBRARY`; shared Cpp2IL run (`unity_text_input.Il2cppReturnPatch`) for `frame.unity_runtime_msaa_off` and `frame.unity_multipass`; VR4 → catalog `use_alt`. **Catalog changes reach existing games**: recipes store `catalog_rev`, `library._follow_catalog` re-derives non-user recipes. → docs/DEVICE_NOTES.md#user-reports-2026-10-04-issues-4-10
**Round 2 of reports / headset rounds 2-3 (2026-10-04):** vkshim `vk_shader_fix` (VR4); `Il2cppReturnPatch.field_loads` (I Am Cat); builds record `recipe_fp` (changed recipe/revised patch → "Update on Frame"); `Frame.relocate`; adapter `profile_remap` (Touch Plus/Pro); `sync_guard` (Myst); `focus_hold` tuning; ovrpshim for Unity 2017 (Accounting+); Steam library to every account, `shortcuts_lost` / `add_devkit_entry`; no clipboard service → `frame.sdl_clipboard`, `frame.godot_clipboard`; 2D apps keep non-VR patches (`needs_vr = False`); `frame/local.py` (FramePort on the Frame); after every app update `library._follow_catalog` re-derives non-user recipes once (`recipes.app_version`, `library.REFRESH_ON_UPDATE`). Analysis fields added later: **bump `analysis/detect.ANALYSIS_VERSION`** (re-analysis at GUI start + `pipeline.refresh_analyses`; `analysis_failed`). Roblox and BONELAB crash (unsupported). → docs/DEVICE_NOTES.md#round-2-of-reports-headset-rounds-2-3-2026-10-04
**Round 4:** XR_KHR_android_surface_swapchain is listed but unsupported → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`); save folders without u/g+rwx → launch.sh `fix_perms`; Lucky's Tale OOM (no fix); `perf` works as steamos (gdb blocked); QuestCraft JRE, WiiCompiled import by hand (Lepton has no document picker). → docs/DEVICE_NOTES.md#round-4-2026-10-04-surface-swapchains-save-folder-permissions
**Steam library failures (GitHub #21/#30/#41/#42):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (Play → AppError_9) → fallback: register through Steam's devkit interface (`devkit-1 … create-shortcut`, `~/devkit-game/<id>/`; `devkit_appid`, `devkit_appid_from_log`, `devkit_unregister`, `copy_grid_art`; ids: letter first, letters/digits/_ only); `collect_diag` adds `steam_library`. Cause still unknown. → docs/DEVICE_NOTES.md#steam-library-failures-and-the-devkit-fallback-github-21304142
**Exit game (GitHub #36):** `Apps.TerminateApp` (Steam CEF devtools on 127.0.0.1:8080) closes games; launch.sh ends the game when its parent reaper is gone (`upgrade_launchers` adds it). → docs/DEVICE_NOTES.md#exit-game-github-36
**SteamVR dashboard at game start:** launch.sh's `_dashboard_worker` hides the dashboard via `SteamClient.OpenVR.VROverlay` (agent `steam_js`) from the first FrameBridge `new layer:` line (120 s, ≤10 times, stops after `toggle_dashboard_action`); log `<base>/dashboard.log`, opt out `FRAMEPORT_KEEP_DASHBOARD=1`. → docs/DEVICE_NOTES.md#steamvr-dashboard-at-game-start
**Space warp / ITR2:** OVRPort's dispatcher offers XR_FB_space_warp itself (`patch_disable_space_warp` doesn't reach UE5) → FrameBridge `hide_space_warp` (per game, off; suggested for UE5 + OVRPlugin). `equirect_emul` switches itself off on Vulkan. → docs/DEVICE_NOTES.md#space-warp--itr2
**Space warp / ITR2:** OVRPort's dispatcher offers XR_FB_space_warp itself (`patch_disable_space_warp` doesn't reach UE5) → FrameBridge `hide_space_warp` (per game, off; suggested for UE5 + OVRPlugin). Games that *require* space warp (OutRun, #336: "AppSW recommended buffers unavailable") → `space_warp_emul` (`native/adapter/space_warp.c`: MV size, FrameBridge-owned MV/depth images, info stripped; overrides hide_space_warp). `equirect_emul` switches itself off on Vulkan. → docs/DEVICE_NOTES.md#space-warp--itr2
**PR #34 (Lucas-Mathieu):** `native/adapter/audio_metadata.h` keeps Meta XR Audio Wwise metadata alive (Batman: Arkham Shadow 1.4.1 smoke-bomb crash). Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally. → docs/DEVICE_NOTES.md#pr-34-meta-xr-audio-metadata-batman
**Heuristics eval (2026-10-04):** 19/44 recipes exact (superseded by the 2026-10-15 numbers under Heuristics). → docs/DEVICE_NOTES.md#heuristics-eval-2026-10-04
**Power (0.9, agent v47):** sidebar bar below the Frame card: Sleep / Restart / Shut down, each confirmed (`app.frame_power`, a running game asks again). logind answers CanSuspend/CanReboot/CanPowerOff = "challenge" for an SSH session but "yes" inside a user unit, so agent `power` runs `systemctl suspend|reboot|poweroff` from a transient `systemd-run --user --on-active=3` timer (the command answers first). Restart verified on the device (back in ~84 s, new boot id).
**Proton default:** the newest **stable** ARM64 Proton (`pick_proton`; owner's headset A/B); `pcvr.proton_tool` dropdown (Experimental = alias `proton-experimental`), `ensure_proton`, `proton_alternative_worth_trying`. → docs/DEVICE_NOTES.md#proton-default-stable-vs-experimental
**USB cable link:** `usb0` is a USB NCM gadget (Developer Mode only); Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py`); ~37 MB/s; `scripts/usb_autotest.py`, `frameport frame usb-check`; discovery prefers USB > hotspot > network (`discovery.dedupe`). → docs/DEVICE_NOTES.md#usb-cable-link
**Catalog updates without a release (2026-10-05):** apps fetch `catalog/games/*.yaml` from GitHub `main` (`recommend/catalog.refresh_remote`: one API call `git/trees/main?recursive=1` + raw.githubusercontent downloads of changed blobs, cached as `catalog-gh-<sha>.yaml`, every 6 h; GUI at start + Settings → Data "Check now"; CLI background thread + `frameport catalog-update`). `catalog.load()` only reads the cache. A remote entry is skipped (`unusable_reason`) when it names a patch/adapter key this app doesn't register, has a field `CatalogEntry` doesn't know, or `min_app` > this version: add `min_app` (or rely on new patch ids) when a config needs an unreleased FramePort. Remote beats bundled unless the bundled entry has a later updated/verified date; the user's own entries still win as before. Off: setting `catalog.auto_update`, env FRAMEPORT_NO_CATALOG_UPDATE; FRAMEPORT_CATALOG_URL still overrides the source. So: **pushing a catalog change to main reaches users within 6 h** (their recipes follow via catalog_rev → Update on Frame).
**Catalog updates without a release (2026-10-05):** apps fetch `catalog/games/*.yaml` from GitHub `main` (`recommend/catalog.refresh_remote`: one API call `git/trees/main?recursive=1` + raw.githubusercontent downloads of changed blobs, cached as `catalog-gh-<sha>.yaml`, every 6 h; GUI at start + Settings → Data "Check now"; CLI background thread + `frameport catalog-update`). `catalog.load()` only reads the cache. A remote entry is skipped (`unusable_reason`) when it names a patch/adapter key this app doesn't register, has a field `CatalogEntry` doesn't know, or `min_app` > this version: add `min_app` (or rely on new patch ids) when a config needs an unreleased FramePort. Remote beats bundled unless the bundled entry has a later updated/verified date; the user's own entries still win as before. Off: setting `catalog.auto_update`, env FRAMEPORT_NO_CATALOG_UPDATE; FRAMEPORT_CATALOG_URL still overrides the source. So: **pushing a catalog change to main reaches users within 6 h** (their recipes follow via catalog_rev → Update on Frame). `catalog/triage.yaml` comes from the same tree listing (cached `triage-gh-<sha>.yaml`, index key `triage`; a new copy → `triage.reset()`; with FRAMEPORT_CATALOG_URL: that folder's optional `triage.yaml`): `validate/triage.database()` = bundled merged with remote per signature id (remote wins unless the bundled one has a later `updated:`; bundled-only ids stay), each remote signature dropped (bundled one kept) when malformed, it has an unknown field, suggests a patch/adapter key this app lacks, or `min_app` (per signature or top-level = whole file) > this version; `triage.skipped()`, `frameport catalog-update` lists them.
**Start activity / QuestCraft / ITR2 pop-in:** `frame.start_activity` (analysis `extra.vr_activity`, `axml.set_start_activity`; OVRPort gives every MAIN activity LAUNCHER, so take the VR activity from the original APK); Unreal reads user config from **internal** storage; Lepton passes `LEPTON_ENV_<NAME>` to the app as `<NAME>` (QuestCraft `LIBGL_NOERROR`); vkshim `vk_query_slots` left off. → docs/DEVICE_NOTES.md#start-activity--questcraft--itr2-pop-in
**Vulkan validation / ITR2 solved:** bundle Khronos' `libVkLayer_khronos_validation.so` into a test APK + vkshim `vk_validation=1` (logcat tag VALIDATION); `vk_spec_fixes=1` drops ITR2's invalid depth resolve; VALVE_rpo removed its fog (catalog `lepton_env: VK_INSTANCE_LAYERS`). Agent `not_started` waits 30 s for "Boot complete!". → docs/DEVICE_NOTES.md#vulkan-validation--itr2-solved
**GitHub #38/#39:** SteamVR rejects image rects past their swapchain (-25) → FrameBridge `rect_clamp` (default on), triage `swapchain-rect-invalid`; The Room VR MSAA crash in libgallium. → docs/DEVICE_NOTES.md#github-3839-powerwash-simulator-the-room-vr
@@ -610,6 +629,7 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
**Unreal's self-restart:** UE 4.21+ restarts itself after the first shader precompile and Lepton ends the container → launch.sh RESTART_BLOCK starts Lepton once more (`upgrade_launchers`; `FRAMEPORT_NO_RESTART=1` off). → docs/DEVICE_NOTES.md#unreals-self-restart
**xrWaitFrame runs ahead:** the runtime sometimes returns a display time <1 refresh after the last submit and rejects the second submission (`VRCompositorError_AlreadySubmitted`) → the VrApi bridge re-waits (`pacing 2s:` counters). → docs/DEVICE_NOTES.md#xrwaitframe-runs-ahead-journey-of-the-gods
**Lepton's logcat mirror dies** (EOF, read failure, or silent) → launch.sh `_logcat_keeper` reads `podman exec <container> logcat` itself into launch.log (log `<base>/logcat-keeper.log`). → docs/DEVICE_NOTES.md#leptons-logcat-mirror-dies
**Graceful stop (#480, agent v120):** killing the container never paused the app (saves in `OnApplicationPause` lost: Just Dance VR) → launch.sh `pause_game` / agent `stop` / End game first `am start` the HOME intent in the container (`input keyevent` does nothing headless) and wait ≤3 s for STOPPED (`PAUSE_SCRIPT`, `<base>/stop-pause.log`). Steam's own Exit game signals the whole process tree (conmon is re-parented to the reaper) at once → agent v121: launch.sh's `_exit_hook` worker (`exit_hook_worker`, `EXIT_HOOK_JS`) wraps `SteamClient.Apps.TerminateApp` in SharedJSContext with a CDP binding of its own: Exit game for that game's ids waits for the worker's pause (cap 3 s, 0.8 s without an answer, lease 10 s), other ids pass straight through; End game calls `terminate_app_js` (past the hook). → docs/DEVICE_NOTES.md#graceful-stop-the-game-is-paused-before-it-ends-github-480-agent-v120
**Double launch:** launch.sh takes `flock` on `<base>/.launch.lock` (fd 9); a second launch exits 0 and logs to `<base>/launch-dup.log` (`upgrade_launchers` adds it). → docs/DEVICE_NOTES.md#double-launch
**Issue triage round 2026-10-06:** FrameBridge turns haptic amplitude 0 into xrStopHapticFeedback, per-game `haptic_scale`; Lambda1VR → `frame.tbxr_vendor`; Jurassic World → `frame.vrapi_stub`; Metro Awakening / Pinball FX → `hide_space_warp`; `frame.ovr_trace` can't trace Unity games; the VrApi bridge's 30 s head-pose deadline ends headless VR mode. → docs/DEVICE_NOTES.md#issue-triage-round-2026-10-06
**Lepton storage:** each app's /sdcard has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (agent `storage_targets`); the media index never works; Lepton grants all runtime permissions. → docs/DEVICE_NOTES.md#lepton-storage
@@ -618,6 +638,8 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
**Cube swapchains:** the runtime refuses `faceCount=6` → FrameBridge `cube_standin` (default on, GLES only, `native/adapter/cube_standin.c`). → docs/DEVICE_NOTES.md#cube-swapchains
**Hardware video decoding:** `native/hevc` OMX plugin (`OMX.frameport.{avc,hevc,vp9}.decoder`), installed once per Frame (`ensure_video_codec`, agent `video_codec_status`/`install_video_codec`/`video_codec_switch`); **bump `revision` in native/hevc/build.py with every artifact change**; per game `frame.hw_video_decode` (analysis `media_codec`), Settings `video.hw_decode`, `FRAMEPORT_NO_HW_VIDEO=1`; Batman migration `batman_video_patches` + `surface_native`. → docs/DEVICE_NOTES.md#hardware-video-decoding
**Vulkan shader dump:** adapter `vk_shader_dump=1` (`native/vkshim/shader_dump.h`) writes SPIR-V modules + `index.txt` to `files/fp_vk_shaders/`; `collect_diag` returns the newest session's modules (`shader_dumps`); triage `gpu-hang`, `triage.graphics_api`. → docs/DEVICE_NOTES.md#vulkan-shader-dump
**Foveation centre (#394):** Valve's FDM layer centres its density map on the whole swapchain (not the eye's rect or optical axis) and Turnip picks density per GMEM bin (the sharp area snaps to bins, so its size shifts with render resolution: driver design); `VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h, below Valve's layer, views from FrameBridge `fdm_views.c`) rewrites the per-eye density-map offsets; FrameBridge `snapshot=N` reads Vulkan eyes back. → docs/DEVICE_NOTES.md#foveation-sharp-area-centre-and-size-394
**Expansion file names (#450/#481):** OBBs with a download's copy suffix (`main.203.com.Armature.VR4-003.obb`) or another case are installed under the game's name (`quest_dump.obb_names`/`frame_manifest`, PC files untouched), other kinds too (#415: Crisis Brigade 2's `office.222.<pkg>.obb`, kind spelling kept, never an "another app" warning); another app's name (#129), duplicates and unclear cases stay warnings. → docs/DEVICE_NOTES.md#expansion-file-names-450-481
**Unreal OBB check (#159):** `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`) turns off Epic's OBB CRC pass; triage `unreal-obb-check-stuck`. → docs/DEVICE_NOTES.md#unreal-obb-check
**Issue round 2026-10-12:** Lepton breaks on unquoted paths with spaces → launchers give Lepton a `~/.local/share/frameport/lepton-paths/<pkg>` link (`app_dir_lines`/`set_app_dir`/`launcher_base`); `frame.eos_keychain_loop`, `frame.unity_tls_key_pool` (`libfp_tkeys.so`, `elf.rename_imports`), `frame.vermillion_scene_canvas`; `tools/cpp2il.py` falls back to Cpp2IL's dev build for metadata v107; Linux `desktop_icon`; `scripts/upload_bench.py`. → docs/DEVICE_NOTES.md#issue-round-2026-10-12
**OpenSSH uploads (#324/#242):** `native_upload.quote` escapes only backslash and `"`, Win32 local paths go as `C:/…`; `Frame.auth` records the login that worked (app_key / user_key from `ssh.extra_keys`, `frame ssh-keys`, agent, password); strict IdentitiesOnly first, relaxed retry. → docs/DEVICE_NOTES.md#openssh-uploads
@@ -721,7 +743,8 @@ short recap of the thread so far; read **all** comments on a thread first (a lat
- **How a change reaches users** (decide this before promising a fix):
- *Catalog / recipe / triage* changes reach **installed** apps within ~6 h with no build — apps fetch
`catalog/games/*.yaml` + `triage.yaml` from `main` (`catalog.refresh_remote`), recipes follow via `catalog_rev` →
"Update on Frame". So accepting a recipe or adding a triage signature is "live" once pushed.
"Update on Frame". So accepting a recipe or adding a triage signature is "live" once pushed (a signature that
suggests a patch/setting the installed app doesn't have yet is skipped there: needs a release; `min_app` gates it).
- *Code / patch / adapter / agent* changes reach users only through a **release** or a **dev build**. For a per-game
fix a reporter should try now: push to `main`, cut a dev build (Actions → build → `dev` ticked, with `notes`), tell
them to install it via **Settings → Updates → "Install the latest dev build…"** and then rebuild the game on the
@@ -795,3 +818,9 @@ asks before ending a running game, and closes local FramePort/Desktop Mode. Purg
Hook hardening: both lines `-` + `/usr/bin/timeout -k 5 20`; ExecStartPost only `systemd-run --no-block`s the
verify (unit frameport-library-verify) so Steam's start never waits; drop-in removed once mode != later and nothing
pending (`drop_library_hooks_if_idle`). Steam = static user unit steam.service (Type=simple, Restart=always, 90 s).
Agent v124: a waiting `now` worker holds the shortcut lock (`shortcut_lock`) for up to 12 h, so nothing an SSH command
reaches blocks on it (an install's `library_mode` hung until the 600 s agent timeout): `library_mode` tries once and
skips the hook cleanup; `cmd_shortcuts` journals into the waiting worker (`pending_lock`, a short separate lock;
the worker writes "running" under it before merging the journal after stop_steam, one restart for all; the PC
shows the same "waiting" reason); `queue_library` waits ≤30 s, else journal only; runs drop what they applied
(`consume_pending`), never later entries.
+28 -30
View File
@@ -2,12 +2,17 @@
Thanks for helping. The quickest contributions:
- **A game works on the Frame?** In FramePort: the game's menu → **Share working config…** opens a prefilled issue
with its recipe. Accepted configs become built-in recipes.
- **Something doesn't work?** The game's menu → **Report a problem…** saves a diagnostics zip (personal data
removed) and opens a prefilled issue to attach it to.
- **A game works on the Frame?** Game menu → **Recipe → Share working recipe…** opens a prefilled issue. After
review the recipe becomes a built-in recipe (`catalog/games/<package>.yaml`), and installed copies of FramePort get
the recipe within 6 hours.
- **Something doesn't work?** Game menu → **Troubleshoot → Report a problem…** saves a diagnostics zip (personal data
removed) and opens a prefilled issue to attach the zip to.
Without the app: [working recipe form](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml)
· [problem report form](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml).
## Development
```
uv sync --extra dev # Python 3.11+, uv (https://docs.astral.sh/uv/)
uv run frameport-gui # the app
@@ -15,39 +20,32 @@ uv run frameport --help # the command line
uv run pytest # tests (no headset or game files needed)
uv run ruff check . # lint
```
- Code and architecture: `CLAUDE.md`, `docs/ARCHITECTURE.md`; per-game symptoms and fixes: `docs/PLAYBOOK.md`.
- Interface wording: `docs/STYLE.md`. GUI texts go through `tr()`; after changing them run
`python scripts/i18n_extract.py`.
- Translations: see [Translating FramePort](#translating-frameport) below.
- New fixes are patch modules (`src/frameport/patches/`), with a triage signature (`catalog/triage.yaml`), a PLAYBOOK
row and a unit test.
- Native binaries in `artifacts/` are rebuilt with `python native/build.py` (downloads the NDK).
## Adding a tested game
Tested a game on the Steam Frame? The easiest way is in FramePort: open the game → **…** → **Share working config…**.
It opens a prefilled GitHub issue with the exact recipe; after review a workflow turns it into
`catalog/games/<package>.yaml`. Without the app: [working config form](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml). Something doesn't run: the game's
**Report a problem…** (diagnostics zip, personal data removed) or the [problem report form](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml).
After catalog changes, regenerate the games list: `python scripts/compat_list.py`.
- Where things are: `docs/ARCHITECTURE.md` (module notes in `CLAUDE.md`), `docs/PLAYBOOK.md` (symptoms and fixes),
`docs/FRAME_RUNTIME.md` (runtime facts).
- A new fix = a patch module (`src/frameport/patches/`) + a triage signature (`catalog/triage.yaml`) + a PLAYBOOK row
+ a unit test.
- After changing a patch, game setting, recipe or triage signature: `python scripts/patch_docs.py`. After catalog
changes also `python scripts/compat_list.py`.
- GUI texts go through `tr()`; after changing them run `python scripts/i18n_extract.py`. Wording rules:
`docs/STYLE.md`.
- Docs screenshots and videos are rendered, never taken by hand: `docs/SHOWCASE.md`.
- Native binaries in `artifacts/`: `python native/build.py` (downloads the NDK).
## Translating FramePort
The app's texts (screens, help, patch and setting descriptions) can be translated; the command line, logs and
diagnostics stay English. No programming needed:
1. Copy `src/frameport/locales/template.json` to `src/frameport/locales/<code>.json`, where `<code>` is the language
code (`de`, `fr`, `pt`, `ja`, …).
2. Add `"_language": "<the language's own name>"` (e.g. `"Deutsch"`); it's shown in Settings → Language.
3. Fill in the values. Each key is the English text; leave a value empty to keep the English text. Plural entries
have a list value: `["<one>", "<other>"]`. Keep `{placeholders}` such as `{title}` or `{n}` exactly as they are,
and keep `…`, `·` and line breaks where the English has them.
4. Try it: `uv run frameport-gui`, choose the language in Settings → Language, restart FramePort.
5. Open a pull request with the file. When the English texts change, `python scripts/i18n_extract.py` refreshes the
template; new texts then show in English until they're translated.
The app's texts can be translated, no programming needed. The command line, logs and diagnostics stay English.
Wording rules for English (and a guide for tone) are in `docs/STYLE.md`.
1. Copy `src/frameport/locales/template.json` to `src/frameport/locales/<code>.json` (`de`, `fr`, `pt`, `ja`, …).
2. Add `"_language": "<the language's own name>"`, for example `"Deutsch"`.
3. Fill in the values. Each key is the English text; an empty value keeps the English. Plural entries take a list:
`["<one>", "<other>"]`. Keep `{placeholders}` such as `{title}` exactly, and `…`, `·` and line breaks where the
English has them.
4. Try the translation: `uv run frameport-gui`, pick the language in Settings → Appearance → Language, restart FramePort.
5. Open a pull request with the file. New English texts show in English until they're translated.
## Ground rules
- Use only games you own. Contributions that help bypass DRM, licence or entitlement checks, or that add game files,
are not accepted.
- Never commit personal data (IP addresses, user names, home paths, Steam IDs).
+44 -52
View File
@@ -8,41 +8,35 @@
![Steam Frame](https://img.shields.io/badge/Steam%20Frame-supported-1b2838?logo=steam&logoColor=white)
Install Quest games, Android apps, Linux apps and PC VR games on the **Valve Steam Frame**. FramePort sets up the
Frame, patches each game so it runs there, uploads it and adds it to your Steam library with artwork.
Frame, patches each game to run there, uploads the game and adds the game to your Steam library with artwork.
[![FramePort: library, one-click install, play, monitor](docs/images/tour-teaser.webp)](docs/media/frameport-tour.mp4)
▶ [Watch the full tour](docs/media/frameport-tour.mp4) · New to FramePort? [Watch the install tutorial](docs/media/frameport-install.mp4)
▶ [Watch the tour](docs/media/frameport-tour.mp4) · [Watch the install tutorial](docs/media/frameport-install.mp4)
(about 90 seconds each)
> **Notice:** FramePort doesn't download, share or unlock games. Use games you got legally, for example from
> [SideQuest](https://sidequestvr.com/). FramePort downloads published open-source tools (see [Built on](#built-on))
> and adds its own patches so games run on the Frame.
> [SideQuest](https://sidequestvr.com/). FramePort downloads published open-source tools ([Built on](#built-on))
> and adds its own patches.
>
> Not every game runs on the Frame: recipes are tested by the community, and some games need Meta's services or
> hardware the Frame lacks.
> Not every game runs on the Frame. Some need Meta's services or hardware the Frame lacks.
## Features
![Library](docs/images/library.png)
- **Easy setup:** one line in the Frame's terminal. No root, no password. [What it changes](docs/FRAME_SETUP.md).
- **One click per game:** patch, upload, add to Steam with artwork and test that it starts.
- **Recipes:** tested patches and settings for 120+ games; other games get suggested patches, each explained in
plain words.
- **One click per game:** patch, upload, add to Steam with artwork, test that the game starts.
- **Recipes:** tested patches and settings for hundreds of games. Other games get suggested patches, each explained.
- **Game settings:** sharpness, refresh rate, controllers, 360° video and mixed reality as simple switches.
- **More than Quest games:** Android apps in a window, Linux apps, PC VR games and Windows programs.
- **Install links:** "Install with FramePort" buttons on websites ([frameport.app](https://frameport.app) links) open
in FramePort.
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
- **Your room for mixed reality (experimental):** scan the room in the headset, see it in 3D, clean up the scan and
fix furniture, screen and door labels, so mixed-reality games get your room. Try it with
- **Install links:** "Install with FramePort" buttons on websites open in FramePort.
- **Your Frame from your PC:** Type on Frame, Files, Screenshots, Live view (what the headset shows) and Monitor
(frame rate, temperatures, battery, processes).
- **Your room for mixed reality (experimental):** scan your room in the headset and fix the scan up in 3D. Try the room with
[Test Quest MR](https://github.com/spoopyghosty0/TestQuestMR).
- **Updates itself**, and reports problems without personal data.
Details for each: [Install and first steps](docs/INSTALL.md).
![Monitor](docs/images/monitor.png)
![Screenshots](docs/images/screenshots.png)
@@ -51,40 +45,36 @@ Details for each: [Install and first steps](docs/INSTALL.md).
## Quick start
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
Silicon) or Linux, unpack it and start FramePort.
2. In FramePort click **Steam Frame → Start setup**. The Frame and your PC must be on the same Wi-Fi (or use a USB
cable).
3. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**, then the app menu → **System →
Konsole**, and run:
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) FramePort for Windows, macOS (Apple
Silicon) or Linux, unpack the download and start FramePort.
2. Click **Steam Frame → Start setup**. The Frame and your PC must be on the same Wi-Fi (or use a USB cable).
3. On the Frame open **SteamVR dashboard → Launch a program → Desktop**, then **app menu → System → Konsole**, and
run:
```
curl -fsSL https://frameport.app/s | bash
```
4. Click **Allow** in FramePort when it shows the same 4-digit code as the Frame. Steam restarts once; if it asks to
install **Lepton** (Valve's Android runtime), confirm it.
5. **Add games → Scan a folder…** with your games, open one and click **Install on Frame**. Play it from the Frame's
Steam library.
4. Click **Allow** when FramePort shows the same 4-digit code as the Frame. Steam restarts once. If Steam asks to install
**Lepton** (Valve's Android runtime), confirm.
5. **Add games → Scan a folder…**, open a game and click **Install on Frame**. Play the game from the Frame's Steam library.
Full guide, firewalls and troubleshooting: [Install and first steps](docs/INSTALL.md). How to lay out game folders:
[FAQ](docs/FAQ.md).
Full guide: [Install and first steps](docs/INSTALL.md). Game folders: [FAQ](docs/FAQ.md).
## Compatibility
Tested games use their recipe (the patches and settings that work for them) automatically; for other games FramePort
suggests patches. See the [list of tested games](docs/GAMES.md). Got a game working? Share its recipe from the game's
**…** menu: [how](docs/INSTALL.md#share-a-recipe-or-report-a-problem).
Tested games get their recipe (the patches and settings that work) automatically. Other games get suggested
patches. See the [tested games](docs/GAMES.md). Got a game working?
[Share its recipe](docs/INSTALL.md#share-a-recipe-or-report-a-problem).
## Compared with other tools
FrameDrop and Valve's own tools install an app as it is. FramePort differs in four ways:
FrameDrop and Valve's own tools install an app as it is. FramePort also:
- **Free and open source** (GPL-3.0). FrameDrop is free (donationware) without published source.
- **Quest games that don't run on the Frame as they are** get converted and patched, with a tested recipe for 120+
games. The others install the game unchanged.
- **Windows, macOS and Linux.** FrameDrop is for Windows.
- **Wi-Fi or a USB cable**, and no **Pair new host** step.
- is **free and open source** (GPL-3.0). FrameDrop is free (donationware), without published source.
- **converts and patches Quest games** that don't run as they are, with tested recipes for hundreds of games.
- runs on **Windows, macOS and Linux**. FrameDrop is for Windows.
- connects over **Wi-Fi or a USB cable**, without a **Pair new host** step.
<details>
<summary>All differences</summary>
@@ -93,12 +83,12 @@ FrameDrop and Valve's own tools install an app as it is. FramePort differs in fo
|---|---|---|---|
| Cost and source code | Free, open source (GPL-3.0) | Free (donationware), source not published | Free, from Valve |
| Runs on | Windows, macOS, Linux | Windows | Depends on the tool |
| First connection | One line in the Frame's terminal; it turns on Developer Mode itself. No password | Turn on Developer Mode, then **Pair new host** | Turn on Developer Mode and pair, or use Android's debug tool (adb) |
| First connection | One line in the Frame's terminal; turns on Developer Mode itself. No password | Turn on Developer Mode, then **Pair new host** | Turn on Developer Mode and pair, or use Android's debug tool (adb) |
| Wireless or cable | Wi-Fi, the Frame's hotspot or a USB cable | Same Wi-Fi network | Wi-Fi, or adb |
| Quest games that don't run as they are | Converted and patched for the Frame | Installed as they are | Installed as they are |
| Knows which patches a game needs | Tested recipes for 120+ games, updated without an app update | Not stated | No |
| Knows which patches a game needs | Tested recipes for hundreds of games, updated without an app update | Not stated | No |
| In the Steam library | Yes, with artwork and tags | As a "Devkit Game" shortcut | As "Devkit Game: &lt;title&gt;"; not with adb |
| Android apps, Linux apps, Windows programs | All three; Proton (runs Windows programs) is installed for you | All three; install Proton first | Yes; 2D Android apps need an extra file |
| Android apps, Linux apps, Windows programs | All three; Proton is installed for you | All three; install Proton first | Yes; 2D Android apps need an extra file |
| PC VR games | On your PC; some also on the Frame | Not supported | Not supported |
| "Install with …" buttons on websites | Yes ([frameport.app](https://frameport.app) links) | Its own | No |
| A game doesn't start | A launch test reads the logs and suggests a patch | A log viewer | No help |
@@ -106,10 +96,10 @@ FrameDrop and Valve's own tools install an app as it is. FramePort differs in fo
</details>
Other tools as described on their own pages, checked 2026-10-09:
Based on each tool's own pages, checked 2026-10-09:
[FrameDrop about](https://framedropvr.com/about) · [how-to](https://framedropvr.com/how-to) ·
[Valve: loading games on Steam Frame](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames).
Out of date? [Report a problem](https://github.com/spoopyghosty0/frameport/issues/new).
Out of date? [Report the problem](https://github.com/spoopyghosty0/frameport/issues/new).
## Built on
@@ -124,15 +114,17 @@ What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
| | |
|---|---|
| [Install and first steps](docs/INSTALL.md) | Install, connect, update, PC VR games, files, problem reports |
| [What the setup changes](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
| [FAQ](docs/FAQ.md) | Game folders and common questions |
| [Controller buttons](docs/CONTROLLERS.md) | Quest Touch buttons on the Frame: SteamVR default, Mirror and Quest pair |
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
| [Install and first steps](docs/INSTALL.md) | Install, connect, install games, update |
| [What the setup changes](docs/FRAME_SETUP.md) | Changes on the Frame, firewalls, undoing it |
| [FAQ](docs/FAQ.md) | Game folders and fixing problems |
| [Compatibility](docs/COMPATIBILITY.md) · [Tested games](docs/GAMES.md) | What runs, and how well |
| [Controller buttons](docs/CONTROLLERS.md) | Quest buttons on the Frame's controllers |
| [Themes](docs/THEMES.md) | Color themes and making your own |
| [Install button](docs/INSTALL_BUTTON.md) | An "Install with FramePort" button for your download page |
| [Diagnostics](docs/DIAGNOSTICS.md) | What a problem report contains |
| [Patch reference](docs/PATCHES.md) | Every patch and game setting |
| [Porting playbook](docs/PLAYBOOK.md) · [Runtime reference](docs/FRAME_RUNTIME.md) | Symptoms and fixes; facts about the Frame |
| [Architecture](docs/ARCHITECTURE.md) · [Screenshots and videos](docs/SHOWCASE.md) | How the code and the docs' pictures are made |
| [Contributing](CONTRIBUTING.md) | Code, recipes, translations |
## Development
+3 -1
View File
@@ -1,6 +1,6 @@
# Third-party notices
FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the following third-party work.
FramePort is GPL-3.0-only (see `LICENSE`). FramePort includes or downloads the following third-party work.
## Included in this repository / the release bundles
| Component | Where | License |
@@ -25,6 +25,8 @@ FramePort's own native code (the FrameBridge adapter, GL/Vulkan/OpenXR shims, th
| Eclipse Temurin JRE | api.adoptium.net | GPL-2.0 with Classpath Exception |
| apksigner (Android build-tools) | dl.google.com Android repository | Apache-2.0 |
| Revive (portable copy, only if Revive isn't installed) | github.com/LibreVR/Revive releases | GPL-3.0 |
| Cpp2IL (only for Unity IL2CPP patches) | github.com/SamboyCoding/Cpp2IL releases | MIT |
| APKEditor (only for app bundles with resource splits) | github.com/REAndroid/APKEditor releases | Apache-2.0 |
Valve's Lepton, Proton and SteamVR, and Meta's software, are used on your devices as installed by Steam / Meta;
FramePort doesn't distribute them.
+1222 -96
View File
File diff suppressed because it is too large. Load diff
+11 -9
View File
@@ -1,16 +1,18 @@
# Announcements
`current.md` is the one announcement FramePort shows as a pop-up in the app. Edit it on `main`: the app reads it
from GitHub `main` (raw.githubusercontent.com) at most every 6 hours, so a push reaches everyone without a release.
`current.md` is the one announcement FramePort shows as a pop-up in the app. Edit `current.md` on `main`: the app
reads the file from GitHub `main` (raw.githubusercontent.com) at most every 6 hours, so a push reaches everyone
without a release.
## Who sees it
- **Only people who already used FramePort before the announcement's `date`.** A new install never shows one, and
nor does an announcement published before FramePort was installed.
- **Once.** Closing it in any way (the X, a click outside it, Escape, or its button) dismisses it for good. The app
remembers dismissals in its settings (`announce.dismissed`).
- **A new announcement = a new `id`.** Fixing a typo with the same `id` doesn't show it again to anyone who closed it.
A new `id` shows to everyone who used FramePort before its `date`, whether they closed the earlier one or not.
- **Only people who used FramePort before the announcement's `date`.** A new install never shows an announcement,
and neither does an announcement published before FramePort was installed.
- **Once.** Closing the pop-up in any way (the X, a click outside, Escape, or the pop-up's button) dismisses the
announcement for good. The app remembers dismissals in its settings (`announce.dismissed`).
- **A new announcement = a new `id`.** A typo fix with the same `id` doesn't show the announcement again to anyone
who closed it. A new `id` shows to everyone who used FramePort before its `date`, whether they closed the earlier
announcement or not.
## The file
@@ -31,7 +33,7 @@ The body, in Markdown: **bold**, *italic*, `code`, [links](https://frameport.app
The body is Markdown (no HTML). A `date` in the future works as a schedule: nobody sees it before then.
Images look best at about 2:1 (e.g. 1200×600). The pop-up is 600 px wide and shows between 140 and 320 px of the
Images look best at about 2:1 (for example 1200×600). The pop-up is 600 px wide and shows between 140 and 320 px of the
picture's height.
`pytest tests/test_announcements.py` checks this file, so a broken announcement fails CI instead of silently showing
+14 -12
View File
@@ -1,27 +1,29 @@
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
222727ddabf7eaee08c92e86c64f02edc6639667e9041ab2629032b885c9ae3a ./arm64-v8a/liba32bridge.so
c9c3da5cac224c38e2210b781bd12b7f97e02bd6b0ebb0ef85543e4ea4b76671 ./arm64-v8a/liba32rt.so
dfeee15b52a530aef0fc90f79140249522adf888d1c88fd57935dd8ce5257fa5 ./arm64-v8a/libVkLayer_fdm_darken.so
86974202436e9209e260a690ae3eb0082d7d0f88e17c621dd77c0788ea66bc6d ./arm64-v8a/libVkLayer_fp_shaderfix.so
e63d93e086df3b7b29501188b80fb113879e2fd325d7659f68a31ac5bc87b561 ./arm64-v8a/liba32bridge.so
81b073b0cdf11fdb09d22dff682884c5046fe13801f18db9aed232e86f3bf173 ./arm64-v8a/liba32rt.so
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/libfp_langpack.so
845905e8c2da617dddcb9625f4ed38e06f1d85f9c729b55dacb2c59488074c3e ./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
e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/libfp_vk.so
0055308856070aa7e09d80bcf66ea52dc0a495829b9da64383e38f4350706226 ./arm64-v8a/libfp_vk.so
3c548b567c6984c91bf3f4a2b0bae1336f569f3e9fa9c3204872fd9876f88284 ./arm64-v8a/libfp_xrruntime.so
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
001e699b64062e2cc260915c75aa3508aff10e590125a38c1a3007f61c13c681 ./arm64-v8a/libopenxr_loader_generic.so
be863bb0001bc60e9bbd691753f5c5968664ae7917b29258dd15b7a9dd1c3ade ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
b022dc0d48c9ee46e5afa6e169386cdce36d896102fb8677f930abf15408e963 ./armeabi-v7a/libopenxr_loader_generic.so
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi.so
c7a14d34e4ddf20ec943641f6431525867d6f030cd01160df1b24afd441af39f ./arm64-v8a/libvrapi_foveation.so
2f978a24bc78a33a4162f5d35ec13d722a5a965a67772bd6128cbd6f8b0b4b17 ./armeabi-v7a/libopenxr_loader_generic.so
6fd59e6c6e533c0a17204e4f12adaf992ffa6cf58d6280aa9b37ca7ba20299d4 ./dex/browser-handoff.dex
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
55aae57d292e9536701f2f8a9dbaf1edcfdd766c5748d07a34da2f5ef197f212 ./hevc/libstagefrighthw.so
389352bf10c03ab54e784a43c9b217451dd814cacec1d7178777e337c9d6f7de ./hevc/manifest.json
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -1,8 +1,8 @@
{
"revision": 8,
"revision": 11,
"runtime_sha256": "456e912c75cd389abcf6a63bc80e2a53bdc334371d00b200c93680388ae955e2",
"files": {
"libstagefrighthw.so": "141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c",
"libstagefrighthw.so": "55aae57d292e9536701f2f8a9dbaf1edcfdd766c5748d07a34da2f5ef197f212",
"podman.py": "529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca",
"media_codecs_frameport.xml": "46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405",
"COPYING.FFmpeg": "b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe"
@@ -8,19 +8,21 @@ notes: 'Native OpenXR GLES video player. 360 theatres/videos (equirect2 layers,
details: 'Settings: equirect_emul (GLES worker draws 360 layers into a projection layer), stable_local, focus_hold,
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains. Its hardware decoding mode (Java MediaCodec,
Vr4pMediaPlayer; its own FFmpeg is the software path) gets the Frame''s video hardware with frame.hw_video_decode
(added 2026-10-09, not yet checked in the headset).'
(added 2026-10-09, not yet checked in the headset). swapchain_pool shares extra GLES panel images to avoid the
runtime swapchain count cap (#387); host-tested only, menu interaction needs a headset check.'
tested_version: 2.0.22
engine: Other
xr: OpenXR
frame:
- frame.hw_video_decode
adapter:
swapchain_pool: 1
equirect_emul: 1
stable_local: 1
focus_hold: 1
scale: 0.75
verified:
date: '2026-10-01'
updated: '2026-10-10'
updated: '2026-10-18'
min_app: 0.12.1
source_hint: 4XVR Video Player
@@ -0,0 +1,29 @@
package: com.BaggyG.JKXR_Companion_App
title: JKXR_Companion_App
status: unknown
notes: 'JKXR''s companion app: it manages JKXR''s mods and start options in /sdcard/JKXR. On the Frame each game
has its own /sdcard, so this recipe shares JKXR''s folder with it (device.shared_folder). Install JKXR first.
Starting JKXR from the companion app can''t work (each game runs in its own container): start JKXR from Steam.
Shared folder not yet confirmed by a player (GitHub 440).'
tested_version: '1.0'
engine: Unity
xr: VrApi
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-20'
known_good_sha256: 75ee0487ed6253a682b388df7ff06c6f7f35b2ce1b5a71341aed815bf6d60b34
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 109
issue: 440
source_hint: jkxrcompanion_115meta
shared_folder:
game: com.drbeef.jkxr
folder: JKXR
min_app: 1.3.1
@@ -0,0 +1,23 @@
package: com.BanzaiStudios.GAZZLERS
title: GAZZLERS
status: unknown
notes: Stopped at start with "Current Platform is not supported" (GitHub issue 404); the recipe makes the game take
its Meta Quest path. The fix builds and applies offline; not yet checked on the Frame.
details: The game chooses between its Meta Quest and PICO code from SystemInfo.deviceModel and throws for any other
device; Lepton's Android reports Valve Lepton (write-once ro.product properties). frame.gazzlers_quest_platform
makes PlatformDeterminer.GetAndroidPlatform return Platform.Meta.
tested_version: 1.4.1.7857
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.gazzlers_quest_platform
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-23'
issue: 404
updated: '2026-10-23'
source_hint: Gazzlers
@@ -0,0 +1,26 @@
package: com.EgerterSoftware.MovieDeckforPlex
title: Movie Deck for Plex
status: works
notes: Works out of the box and absolutely fantastically so.
tested_version: '6.4'
engine: Unity
xr: OpenXR
frame:
- frame.meta_permissions
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
adapter:
controller_models: 1
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-20'
known_good_sha256: 397912f98ded90df0dabeb51d097c56de9b70ef99674303fa0efba84d43c4df0
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 435
source_hint: Movie Deck For Plex
@@ -0,0 +1,20 @@
package: com.Iconik.LittleNightmaresVR
title: Little Nightmares VR
status: works
tested_version: 1.0.31
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
verified:
date: '2026-10-18'
known_good_sha256: 171e586bf35ee4494874c6f2c37fb734f718e5b81744b2df9095919398d70839
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 96
issue: 421
source_hint: Little Nightmares VR- Altered Echoes
@@ -0,0 +1,19 @@
package: com.MoonMonsterStudios.SpaceControl
title: SpaceControl
status: works
tested_version: 1.1.2
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-10-20'
known_good_sha256: 60b900a0a73a693426b85d81a489c63fab6cc959939f80efd29632945e0ce559
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 447
source_hint: Space Control
@@ -0,0 +1,23 @@
package: com.MyronSoftware.Deisim
title: Deisim
status: works
tested_version: 1.58.543
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.photon_voice_audio_route
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-21'
known_good_sha256: 91bc0a28ff7ca22a877f4f920984c3a6a2c91828a2bccff0c5eb4affb4dc13ae
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 462
source_hint: Deisim
@@ -0,0 +1,20 @@
package: com.ORIGAMEDIGITAL.UmurangiGenerationVR
title: Umurangi Generation VR
status: issues
notes: The game freezes after taking a photo, while it saves it, which makes it hard to play.
tested_version: '1.163'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-22'
known_good_sha256: c4dfe227c3e33e611f9a4b8ee542912c0891fe600a7d32e56cbe84b0524ec8d9
app: 1.3.0
overport_cli: 1.2.5
issue: 472
source_hint: com.ORIGAMEDIGITAL.UmurangiGenerationVR.output
@@ -0,0 +1,19 @@
package: com.Oculus.FirstContact
title: First Contact
status: works
notes: '32-bit only, runs through FramePort''s 32-bit bridge (experimental). Played in the headset
(GitHub #489); the headless launch test ends early, which is expected for the bridge.'
tested_version: '1.12'
engine: Unreal
xr: VrApi
frame:
- frame.a32_bridge
verified:
date: '2026-10-25'
known_good_sha256: 7b971d70af24ec554994e59f65b2568c5b86a94df04e962699051f267d6358b7
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 109
issue: 489
source_hint: com.Oculus.FirstContact
+4 -1
View File
@@ -6,7 +6,10 @@ engine: Unreal
xr: OpenXR
status: works
notes: Very large data set (~31 GiB). Built with disable_space_warp.
details: Reported working on Pico 4 with overport runtime 3.0.x. 83 OBB/pakchunk files (~32 GB).
details: 'Reported working on Pico 4 with overport runtime 3.0.x. 83 OBB/pakchunk files (~32 GB): besides main and
patch.0, 81 patch.pakchunk<N>-Android_ASTC.com.Sanzaru.Wrath2.obb files (~27 GB). Without them the game hangs at
engine start-up (GitHub #471); up to FramePort 1.3.0 they are only sent from a com.Sanzaru.Wrath2 folder next to
the APK, not loose next to it.'
overport_extra:
- patch_disable_space_warp
alt_overport:
@@ -0,0 +1,24 @@
package: com.TeravisionGames.OrcsMustDie
title: 'Orcs Must Die: By The Blade'
status: works
notes: Perfect at v151
tested_version: 1.0.164237
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_ovrp_entrypoints
- frame.hw_video_decode
adapter:
equirect_emul: 1
haptic_fix: 1
verified:
date: '2026-10-21'
known_good_sha256: 11e5d45587277a5edea8be4862c7af19aa5c38445676607c541bc1e471f473ff
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 453
source_hint: OrcsMustDie
+12 -6
View File
@@ -5,16 +5,20 @@ source_hint: Assassins Creed Nexus VR
engine: Unity
xr: OpenXR
status: works
notes: FrameBridge turns its upside-down UI panels upright (flip emulation, in place since FramePort 1.2.0). Ubisoft
login may not work.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
notes: 'FrameBridge turns its upside-down UI panels upright (flip emulation, in place since FramePort 1.2.0). Ubisoft
login may not work. frame.unity_text_input added from detection (not headset-verified); no device.text_input_window:
its Android window took the controllers'' input (owner, 2026-10-23).
Runs as a Quest 3 with the Vulkan panel pool (swapchain_pool): as a Quest 3 it switches to larger eye images after
the intro, which the Frame''s swapchain limit refused before the pool (GitHub #424; checked headless 2026-10-23,
FramePort 1.4.0).'
details: Ubisoft online login may not work. Data folder holds raw assets (start0..2, .climb, bundles), not .obb
files; install to obb as-is.
overport_extra:
- patch_ac_nexus_no_appsw_90
frame:
- frame.unity_text_input
device:
frame_remove:
- device.text_input_window
verified:
date: '2026-09-28'
@@ -23,5 +27,7 @@ verified:
known_good_sha256: 89644a12bf8df956fa3d3a9a40540ba9e1fb62f63494714a05c9f05984383ac8
no_appsw_90: '2026-10-02, OVRPort 1.2.5 (owner: 90 Hz felt better than 72 Hz; same known issues)'
flip_in_place: '2026-10-14, owner in the headset: launch warnings and tutorial pop-ups upright (GitHub #138)'
updated: '2026-10-15'
min_app: 1.2.0
updated: '2026-10-25'
min_app: 1.4.0
adapter:
swapchain_pool: 1
+23
View File
@@ -0,0 +1,23 @@
package: com.XrealGames.Zc15
title: Zero Caliber 2
status: works
tested_version: 2.0.1173-S-80237
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.hw_video_decode
- frame.unreal_noreserve
adapter:
haptic_fix: 1
fixed_resolution: 1
verified:
date: '2026-10-25'
known_good_sha256: 48a6fc11f64aba9a9ee95863cb601d64ad8b54eba9034d5b05097f5d3ee00b57
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 109
issue: 492
source_hint: Zero Caliber 2
@@ -0,0 +1,11 @@
package: com.ZarApps.PianoVision
title: PianoVision
status: unsupported
notes: Starts, but needs camera hand tracking (Android games on the Frame only get hands built from the controllers)
and USB MIDI from a piano, which the Frame's Android container doesn't pass through.
tested_version: 3.0.8
engine: Unity
xr: OpenXR
verified:
date: '2026-10-20'
issue: 297
@@ -0,0 +1,22 @@
package: com.astrobotquest.vrhost
title: Astro VR Host
status: works
notes: Runs your own Astro Bot Rescue Mission files (the unpacked CUSA12392 folder, not the .pkg). With the Files
tab, copy the folder to Android/data/com.astrobotquest.vrhost/files/games/ in the game's storage and add Android/data/com.astrobotquest.vrhost/files/vrhost.txt
with the lines language=<your language, e.g. en-US> and refresh_rate=80. Set FoV to 100% in the game.
details: 'Tested with AstroQuest v0.23 (https://github.com/bigmak94/AstroQuest). The port caps the resolution at
1440 px per eye; vrhost.txt can''t change that. Other vrhost.txt options: https://github.com/bigmak94/AstroQuest/blob/main/README-QUEST-VR.md#installing-and-running'
tested_version: '0.23'
engine: Other
xr: OpenXR
adapter:
refresh_rate: 80.0
verified:
date: '2026-10-22'
known_good_sha256: c2e13ab155b947c0037362635bd34112c16b69b804af82b347021f2261ef5bfb
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 464
source_hint: AstroQuest
+24
View File
@@ -0,0 +1,24 @@
package: com.baobab.invasion
title: Invasion
status: works
tested_version: 1.0.250
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-25'
known_good_sha256: 06c8f94556eb8d8d6679e089d2429e70b45148aba7ad0d47c42bd1de932cbdc1
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 109
issue: 490
source_hint: com.baobab.invasion
+3 -2
View File
@@ -16,6 +16,7 @@ frame:
- frame.hw_video_decode
- frame.unity_text_input
adapter:
fixed_resolution: 1
sync_guard: 1
surface_native: 1
fade_quad: 1
@@ -26,6 +27,6 @@ verified:
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
updated: '2026-10-15'
min_app: 1.1.0
updated: '2026-10-18'
min_app: 1.2.2
source_hint: Batman- Arkham Shadow
@@ -0,0 +1,26 @@
package: com.com2usroca.Darksword
title: Darksword
status: works
tested_version: 1.6.2.11
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.bhaptics_permission
- frame.unreal_ovrp_entrypoints
- frame.hw_video_decode
adapter:
haptic_fix: 1
scale: 1.5
foveation: tracked
verified:
date: '2026-10-18'
known_good_sha256: 47f9968e5fac70a5873b996a4d7b9c01d42d2f0cc1d9dc24013eb2e3b08a8a8e
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 410
source_hint: darksword
+19
View File
@@ -0,0 +1,19 @@
package: com.drbeef.jkxr
title: JKXR
status: issues
notes: Tested in the early part of Jedi Outcast. Two players report stutters and freezes after 3-15 minutes of play
(under investigation in issue 441).
tested_version: 1.1.28
engine: Other
xr: OpenXR
frame:
- frame.tbxr_vendor
verified:
date: '2026-10-20'
known_good_sha256: 771e48b22d3a558ea11869aa1e9070b11908b17ab47a9e8866769bb67efc30a5
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 109
issue: 441
source_hint: QuestAPKs
+23
View File
@@ -0,0 +1,23 @@
package: com.drbeef.questzdoom
title: QuestZDoom
status: works
notes: 'Needs a WAD: put it into QuestZDoom/wads in the game''s storage (Files tab) and create QuestZDoom/qzdoom.ini
with the lines "[IWADSearch.Directories]" and "Path=/sdcard/QuestZDoom/wads" (commandline.txt alone is not
enough). Smooth past 15 minutes in the headset with gl_error_clear (issue 237).'
details: GZDoom on OpenGL ES with Team Beef's OpenXR code. The game leaves GL_INVALID_ENUM pending every frame; the
runtime's GL check failed on it at the first swapchain and its GL interop then created about 100 GPU objects a
second that were never freed (8,000 after 75 s headless). Frames slowed down over the minutes, and in a player's
session the GPU driver crashed after 6 minutes and the Frame restarted. gl_error_clear clears the errors before
FrameBridge calls the runtime; GPU objects then stay flat (headless, 2026-10-24).
tested_version: 1.6.2
engine: Other
xr: OpenXR
frame:
- frame.tbxr_vendor
adapter:
gl_error_clear: 1
verified:
date: '2026-10-25'
issue: 237
updated: '2026-10-25'
source_hint: questzdoom-1.6.2
@@ -0,0 +1,24 @@
package: com.fasttravelgames.solero
title: Solero
status: works
tested_version: 1.00.109160
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.gms_platform_version
- frame.unreal_ovrp_entrypoints
- frame.hw_video_decode
adapter:
equirect_emul: 1
haptic_fix: 1
verified:
date: '2026-10-20'
known_good_sha256: 2d19ac1ed61cc12d0976155fbb6697fb87ec5d60c6601fe1598c965bc3e48377
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 448
source_hint: Action Hero
@@ -0,0 +1,36 @@
package: com.fgol.HungrySharkEvolution
title: Hungry Shark Evolution
status: works
notes: Phone game (2D). Tested with 14.6.0 installed from an .xapk bundle; cloud sync and Google services don't work.
tested_version: 14.6.0
engine: Unity
xr: '?'
overport_remove:
- patch_copy_libraries
- patch_copy_ovrplugin_vrapi
- patch_replace_icon_label
- patch_fix_min_android_sdk
- patch_generate_config
- patch_remove_localized_names
- patch_clean_up_frida
- patch_oculus_unity
- patch_oculus_unreal
- patch_vr_metadata
- patch_launcher_entry
- patch_remove_uses_library
- patch_fix_unreal_crash
- patch_meta_xr_audio
- patch_mark_as_debuggable
- patch_mark_allow_backup
frame:
- frame.unity_text_input
- frame.hw_video_decode
verified:
date: '2026-10-25'
known_good_sha256: d912bc914ea396f19e51d1b35777232cb0a4225a5f288870916744fcaacba9b1
app: 1.3.1.dev675
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 123
issue: 486
source_hint: Hungry+Shark+Evolution_14.6.0_APKPure
+24
View File
@@ -0,0 +1,24 @@
package: com.garage.drumsrock
title: Drums Rock
status: works
notes: Let's Rock!
tested_version: '1.401'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
haptic_fix: 1
scale: 2.0
verified:
date: '2026-10-18'
known_good_sha256: 61d724ce51cc5bc17c7f942803922e8606fc5d60bc4ec5134f06754c5172c7eb
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 409
source_hint: drums rock
+16
View File
@@ -0,0 +1,16 @@
package: com.gevr.port
title: GoldenEye VR
status: issues
notes: No sound; otherwise works great.
tested_version: 0.5.8
engine: Other
xr: OpenXR
frame:
- frame.sdl_clipboard
verified:
date: '2026-10-18'
known_good_sha256: 5bd0d5ad71368178f8478193c215459a670d22ca6828bb155d76c5255908759b
app: 1.2.1
overport_cli: 1.2.5
issue: 412
source_hint: GoldenEye-VR-v0.5.8
@@ -0,0 +1,26 @@
package: com.mrf.kluge.SynthRiders
title: SynthRiders
status: works
tested_version: 3.6.7a2
engine: Unity
xr: OpenXR
frame:
- frame.meta_permissions
- frame.bhaptics_permission
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
controller_models: 1
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-25'
known_good_sha256: ef1792f577697c5fa355a61adfc02e9d8a50fe4ddfec0357398bd5867df42f17
app: 1.3.1.dev703
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 125
issue: 494
source_hint: com.mrf.kluge.SynthRiders
+25
View File
@@ -0,0 +1,25 @@
package: com.onehamsa.underdogs
title: UNDERDOGS
status: works
tested_version: 1.5.4.12573
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.photon_voice_audio_route
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
adapter:
controller_models: 1
haptic_fix: 1
verified:
date: '2026-10-23'
known_good_sha256: 0b98b5c71855341abc2ad942dbbdd920a6d362f7fee25a598b1a6ca1248fb27a
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261022.6150929'
agent: 109
issue: 474
source_hint: com.onehamsa.underdogs
+2 -1
View File
@@ -18,11 +18,12 @@ engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.pojlib_connection
device:
- device.text_input_window
lepton_env:
LEPTON_ENV_LIBGL_NOERROR: '1'
verified:
date: '2026-10-05'
updated: '2026-10-05'
updated: '2026-10-23'
source_hint: QuestCraft
@@ -0,0 +1,27 @@
package: com.simpsonsHitAndRun.vr
title: Simpsons Hit and Run VR
status: works
notes: 'Needs the port''s game data: after installing, copy the "SimpsonsHitRun" folder (see the port''s GitHub
instructions) into the game''s storage with FramePort''s Files tab. In-game settings: Vehicle lights off, Enhanced
materials off, CSM shadows off (otherwise it crashes when starting a new game), Render scale 150% at 120 Hz.'
details: 'Tested with v1.2 of https://github.com/kote2345/The-Simpsons-Hit-and-Run-VR. Vehicle lights break entities
and Enhanced materials break textures. Render scale above 150% can''t keep up at 120 Hz (200% runs at about 100
fps). Optional: set the in-game VR tab''s mode to VR to make the menus more comfortable. While paused, a long
black texture can appear from the head when looking up; it goes away when the pause menu closes. Contributed in
issues 443 and 461.'
tested_version: '0.93'
engine: Other
xr: OpenXR
frame:
- frame.sdl_clipboard
adapter:
refresh_rate: 120.0
verified:
date: '2026-10-22'
known_good_sha256: e406a5da1b2f99fc3a9f95af44f458598ad52d1296a8c577bf1f8e2343738ff2
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 461
source_hint: SimpsonsHitRun_1.2
+22
View File
@@ -0,0 +1,22 @@
package: com.stalwart.barbaria
title: Barbaria
status: works
tested_version: 1.10.1.22383.158.OBERON.Quest.release
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
scale: 2.0
verified:
date: '2026-10-18'
known_good_sha256: e0cd063fc14a2bea5503bf63cf83246527b286915f05186ca5a2c42ce6e7d75a
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 418
source_hint: barbaria
@@ -0,0 +1,23 @@
package: com.wnas.eternalstarlight
title: Eternal Starlight
status: issues
notes: Voice commands do not appear to work.
tested_version: 1.7.404
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-24'
known_good_sha256: 37a79332a89c974971df30a38adbeafdc2466c3163348ab4215ff93014d5be12
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261022.6153909'
agent: 109
issue: 479
source_hint: Eternal Starlight
+24
View File
@@ -0,0 +1,24 @@
package: dev.melee.game
title: Xelee
status: unknown
notes: 'The app opens a flat launcher (MeleeActivity) that starts the VR part (MeleeXrActivity): "Show the app''s
Android window" is on so that launcher can be seen in the headset; without it Lepton runs the app headless, the
launcher waits invisibly and OpenXR never starts. Keep the Vulkan library rename disabled: the engine (Aurora/Dawn
WebGPU) builds the name libvulkan.so from two pieces, so the rename made it ask for "libfp_vkn.so" (GitHub #475).
The VR part is not yet checked on the Frame.'
details: 'Vulkan (Dawn) loads and precompiles all 6080 pipelines in about 34 s with the rename off; the launch then
stays in the 2D launcher, so no OpenXR call is made (GitHub #475).'
tested_version: 0.1.0-beta
engine: Other
xr: OpenXR
frame:
- frame.meta_permissions
- frame.sdl_clipboard
frame_remove:
- frame.vk_sanitize
device:
- device.text_input_window
adapter:
scene_emul: 1
updated: '2026-10-24'
source_hint: Xelee-Quest-arm64
@@ -0,0 +1,26 @@
package: games.b4t.epicrollercoasters.oculus
title: Epic Roller Coasters
status: works
notes: You may need to restart the game for the camera to be placed in front of the menu.
tested_version: 8.36.04
engine: Unity
xr: OpenXR
overport_extra:
- patch_disable_space_warp
frame:
- frame.bhaptics_permission
- frame.vivox_audio_route
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-21'
known_good_sha256: 5760a135be2e2df634c78eae4f9d177b2a95a9da26a4739fdd6e1f5159ed9320
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 109
issue: 456
source_hint: Epic Roller Coasters
@@ -0,0 +1,16 @@
package: io.port0r.houseofthedead
title: 'Port0r: House of the Dead VR'
status: works
tested_version: 0.1.1
engine: Other
xr: OpenXR
as_is: true
verified:
date: '2026-10-25'
known_good_sha256: 699a25d696df8af8669feee11037a2c1c56f15a8f9359b1f8d17897aa841b197
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261020.6124238'
agent: 109
issue: 496
source_hint: Port0r-HouseOfTheDeadVR-v0.1.1-quest3
+16
View File
@@ -0,0 +1,16 @@
package: io.port0r.segarally
title: 'Port0r: Rally VR'
status: works
tested_version: 0.2.6
engine: Other
xr: OpenXR
as_is: true
verified:
date: '2026-10-25'
known_good_sha256: 186018b96550c3881e4659293d07a2fbe6bb7a962798de1dbf6bcba3a99b9025
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261020.6124238'
agent: 109
issue: 498
source_hint: Port0r-RallyVR-v0.2.6-quest3
+20
View File
@@ -0,0 +1,20 @@
package: io.port0r.timecrisis
title: 'Port0r: Time Crisis VR'
status: issues
notes: Immersive mode shows a black screen (only the arm and gun); the flat and 3D screen modes work
(game menu, setup button).
tested_version: 0.2.2
engine: Other
xr: OpenXR
adapter:
refresh_rate: 90.0
as_is: true
verified:
date: '2026-10-25'
known_good_sha256: 4208f5bf16ecc2cfcf3fbb6cb5abf2d632eeffdc3a74115c0b03e861c1bdee43
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261020.6124238'
agent: 109
issue: 497
source_hint: Port0r-TimeCrisisVR-v0.2.2-quest3
+18
View File
@@ -0,0 +1,18 @@
package: io.port0r.timecrisis2
title: 'Port0r: Time Crisis II VR'
status: issues
notes: Gameplay runs well; cut scenes and the title and game-selection screens after inserting coins
don't display properly.
tested_version: 0.1.0
engine: Other
xr: OpenXR
as_is: true
verified:
date: '2026-10-25'
known_good_sha256: 3277d77f81fc6ec7e686d2bd74050234dbfb4c90bf3009a1feb027147372aecb
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261020.6124238'
agent: 109
issue: 495
source_hint: Port0r-TimeCrisisIIVR-v0.1.0-quest3
+16
View File
@@ -0,0 +1,16 @@
package: io.port0r.virtuacop
title: 'Port0r: Cop VR'
status: works
tested_version: 0.2.2
engine: Other
xr: OpenXR
as_is: true
verified:
date: '2026-10-25'
known_good_sha256: 67e0e5676054b8c7a05d9e835d312d29716437e2166c2ce558b6b9b1782286bf
app: 1.3.0
overport_cli: 1.2.5
frame_build: '20261020.6124238'
agent: 109
issue: 493
source_hint: Port0r-CopVR-v0.2.1-quest3
+20
View File
@@ -0,0 +1,20 @@
package: org.outrun.quest.vr
title: OutRun Quest VR
status: unknown
notes: Needs colour-space support and FrameBridge's space warp (it quits at start without them). Not yet checked on
the Frame with both (GitHub #336).
details: Own engine with OpenGL ES and its own OpenXR loader. It requires XR_FB_color_space (Rec.709; quit with "Rec.709
color space unavailable") and Meta's Application SpaceWarp (quit with "AppSW recommended buffers unavailable"); the
Frame's runtime offers neither in a usable form. fb_color_space offers the colour-space extension; space_warp_emul
offers XR_FB_space_warp with a motion-vector size, serves the motion-vector and depth images as GL textures and
removes the space warp info from every frame. Turning space warp off can't help, the game refuses to start without it.
tested_version: 0.4.25-music-indicators
engine: Other
xr: OpenXR
adapter:
fb_color_space: 1
space_warp_emul: 1
verified:
issue: 336
updated: '2026-10-24'
source_hint: OutRunQuest-VR-v0.4.25-music-indicators-arm64
+7 -5
View File
@@ -1,9 +1,10 @@
package: org.primedgun.v2
title: PrimedGun v2
status: unknown
notes: 'Without the Vulkan library rename: the game builds the library name from two pieces, so the renamed name didn''t
exist ("libfp_vkn.so"), its renderer fell back to OpenGL and VR couldn''t start (GitHub #254). Not yet confirmed in
the headset.'
status: issues
notes: 'Vulkan via a statically linked Khronos OpenXR loader, which bypasses FrameBridge (the picture jumps).
The embedded-loader patch is off for now: with it, the game''s loader and the one FramePort adds call each other
until the game crashes at Play (GitHub #254). Keep the Vulkan library rename disabled: this game assembles its
library name from two pieces, so renaming it prevented Vulkan from loading and VR could not start.'
tested_version: 2.0.0-alpha.4
engine: Other
xr: OpenXR
@@ -11,5 +12,6 @@ frame:
- frame.sdl_clipboard
frame_remove:
- frame.vk_sanitize
updated: '2026-10-18'
- frame.static_openxr_loader
updated: '2026-10-23'
source_hint: PrimedGun v2 Quest APK (github.com/iChris4/MetroidPrimePortVR)
+135 -4
View File
@@ -3,8 +3,13 @@
# "apply and rebuild"; an entry `adapter.<key>=<value>` sets that value (else 1). `severity`: fatal (the game can't
# run) | error | warning | info. `question`: a symptom only the player sees (play sessions, GUI "Last session"): its
# fix is offered as that question, never applied on its own. `report: true` = the useful next step is a problem report
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
# with diagnostics. `unless_patch: <patch id>` = not when that patch is in the game's build (a log line that stays
# the same with the fix in place). 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).
# Installed apps fetch this file from GitHub main (every 6 h, like catalog/games) and use each remote signature in
# place of their bundled one with the same id, unless it names a patch/adapter setting or a field the app doesn't know,
# or `min_app` (per signature, or top-level for the whole file) is above its version. Optional `updated: 'YYYY-MM-DD'`
# lets a bundled copy that was changed later win over an older remote one.
signatures:
- id: mr-no-room
pattern: 'FrameBridge[^\r\n]*scene: (?:capture requested -> (?:guardian box|using guardian room)|room from guardian|room [^\r\n]*(?:from guardian|configured/default size|box))'
@@ -18,6 +23,47 @@ signatures:
severity: warning
diagnosis: 'The game cannot discover a Meta room. Enable room layout (adapter.scene_emul), rebuild and reinstall for the updated extension shim, then save a room under Frame > Room.'
suggest: [adapter.scene_emul]
- id: dynamic-resolution-foveation
pattern: 'projection view [01](?: rect change)?:[^\r\n]*rect=(?:[1-9]\d*,\d+|0,[1-9]\d*) '
# a rebuilt APK's foveation centring layer re-centres Valve's density map on the submitted rect (#394)
unless: 'fixed_resolution: recommended|fdm_centre: pass'
severity: info
diagnosis: The game submits an offset projection viewport. With Valve foveation, dynamic resolution can move the sharp centre or expose a white edge. A rebuilt APK (Update on Frame) puts the sharp area back on each eye; keeping full resolution may also help for the white edge, increases GPU load and needs the updated adapter and OVRPlugin shim in a rebuilt APK.
question: With foveated rendering enabled, was the sharp centre misplaced or was there a white line at the picture edge?
suggest: [adapter.fixed_resolution]
- id: static-openxr-loader-bypass
pattern: 'RuntimeManifestFile::FindManifestFiles - using global runtime file /[^\r\n]*etc/openxr/1/active_runtime(?:\.[A-Za-z0-9_-]+)?\.json'
unless: 'FrameBridge[: ]|fp_xrruntime[: ]'
severity: warning
diagnosis: A Khronos loader loaded the system runtime without FrameBridge. A statically linked loader may bypass the bundled loader chain; analyze again, enable the embedded OpenXR loader patch when detected, then rebuild and reinstall to write its per-game manifest.
suggest: [frame.static_openxr_loader]
- id: cryengine-init-failed
pattern: 'Cry\s*:[^\r\n]*Engine initialization failed![^\r\n]*CrySystem initialization failed'
severity: fatal
diagnosis: CryEngine's start-up failed (CrySystem initialization), and the crash that follows comes from its shutdown. FramePort 1.3.0 builds of The Climb 1/2 failed like this because a dormant diagnostic layer was linked into CryEngine's renderer; a newer FramePort leaves it out, so update FramePort and press Update on Frame. Otherwise attach the diagnostics zip to a report.
report: true
suggest: []
- id: vrapi-bridge-no-loader
pattern: 'OVRPortVrApi: xrInitializeLoaderKHR failed: OpenXR result -7'
severity: fatal
diagnosis: The VrApi bridge found no OpenXR loader because FrameBridge didn't load (a library it needs is missing from the APK). Dev builds of FramePort before this was fixed left that library out of The Climb 1; update FramePort and press Update on Frame. Otherwise attach the diagnostics zip to a report.
report: true
suggest: []
- id: questcraft-no-java-runtime
pattern: 'libjvm\.so[^\r\n]*(?:not found|No such file)'
severity: fatal
diagnosis: QuestCraft's Java runtime isn't there. QuestCraft downloads it only on Wi-Fi, and the Frame's Android reports its network as Ethernet, so the download was skipped. Rebuild with the pojlib connection fix (frame.pojlib_connection), or unpack QuestCraft's JRE.zip into the game's files/runtimes/JRE with the Files tab.
suggest: [frame.pojlib_connection]
- id: video-decoder-held-by-steam
pattern: 'FramePort: hardware video decoder \S+ in use by:[^\r\n]*steamwebhelper'
severity: warning
diagnosis: Steam's own interface had the Frame's hardware video decoder open when the game started. Large videos (8K cutscenes, e.g. Batman) then decode in software and play choppy. Restarting Steam, or the Frame, before playing frees it.
suggest: []
- id: swapchain-limit-reached
pattern: 'FrameBridge:\s+xrCreateSwapchain [^\r\n]*\bresult=-10\b'
severity: error
diagnosis: The Frame runtime reached its swapchain count limit (about 15 live swapchains per session), not necessarily a memory limit. Extra menus or video panels may close or stay gray. A game may also stop drawing when a new set of eye images is refused (e.g. AC Nexus with the Quest 3 identity). Updated FramePort builds provide Share panel images (swapchain limit), for GLES and Vulkan apps (panels share one or two runtime images, leaving room for the rest).
suggest: []
- id: quest2-headset-identity
pattern: 'venderId[^\r\n]*systemName Oculus Quest2'
severity: info
@@ -87,7 +133,7 @@ signatures:
- id: no-launcher
pattern: 'APP_ACTIVITY is empty'
severity: fatal
diagnosis: 'Nothing starts: Lepton found no entry point in the app (an activity with category LAUNCHER).'
diagnosis: 'Nothing starts: Lepton found no entry point in the app (an activity with category LAUNCHER; one on an activity-alias alone does not count). Rebuild with the launcher fix (frame.launcher) and reinstall.'
suggest: [frame.launcher]
- id: linux-x86-no-fex
pattern: '(cannot execute binary file: Exec format error|exec format error)'
@@ -99,6 +145,13 @@ signatures:
severity: fatal
diagnosis: 32-bit-only APK (e.g. a Gear VR or Oculus Go game). The Steam Frame has no 32-bit ARM support; FramePort's 32-bit bridge translates the game (experimental).
suggest: [frame.a32_bridge]
- id: missing-split
pattern: 'INSTALL_FAILED_MISSING_SPLIT'
severity: fatal
diagnosis: Only the base APK of a split app was installed (the base requires its split APKs). Add the whole app
bundle (.xapk, .apks, .apkm or .apkx) or the folder with all its split APKs instead; FramePort merges them into
one APK. Then install the game again.
suggest: []
- id: a32-jit-overflow
pattern: 'a32: block at 0x[0-9a-f]+ overflowed the code buffer'
severity: fatal
@@ -106,11 +159,40 @@ signatures:
interpreter, for example OpenSSL's ARMv8 SHA-1 code in Pinball FX2 VR. Fixed in newer FramePort versions;
update FramePort and rebuild the game ("Update on Frame").
suggest: [frame.a32_bridge]
- id: a32-video-ndk
pattern: 'AndroidMediaNDK could not load symbol AMedia\w+|GetDirectBufferAddress: buffer at 0x[0-9a-f]+ is outside the guest''s reach'
severity: error
diagnosis: A 32-bit game on the bridge can't play its videos through Unity's video player (Ghost Giant's intro
stays black). Older bridges lacked the NDK media functions (libmediandk), and Unity's Java fallback hands the 32-bit
code decoder buffers it can't reach. Fixed in newer FramePort versions; update FramePort and rebuild the game
("Update on Frame").
suggest: [frame.a32_bridge]
- id: android-too-new
pattern: '(NoClassDefFoundError|ClassNotFoundException|NoSuchMethodError)\b.*(Landroid/window/|android\.window\.|(storeStoreFence|loadLoadFence)\(\)V in class Ljava/lang/invoke/VarHandle)'
severity: fatal
diagnosis: The game needs a newer Android than the Frame has, so it can't run yet. It calls Android 12/13+ classes (android.window.*, VarHandle fences) that the Frame's Android 11 (API 30) container lacks (for example minSdk 34 apps); nothing to patch until Valve updates Lepton's Android.
suggest: []
- id: system-library-missing
# GitHub #484 (YouTube VR): OVRPort's patch_remove_uses_library also drops <uses-library> entries for libraries
# Lepton has; the classes of patches/frame/system_libraries.LEPTON_LIBRARIES then can't be found
pattern: '(ClassNotFoundException|NoClassDefFoundError)\b.*\bL?(org[./]apache[./]http[./]|android[./]test[./]|javax[./]obex[./]|com[./]android[./](location[./]provider|mediadrm[./]signer|media[./]remotedisplay|future[./]usb)[./]|android[./]net[./]ipsec[./]ike[./])'
severity: fatal
diagnosis: The app closed at start because it can't find classes from one of Android's optional libraries (for example org.apache.http.legacy in YouTube VR). The Frame's Android has the library, but OVRPort removed the app's request for it from the manifest; "Keep Android's shared libraries the app uses" puts it back. Turn it on and rebuild the game ("Update on Frame").
suggest: [frame.keep_system_libraries]
- id: refresh-rate-refused
# GitHub #336 (OutRun): the game quits when its xrRequestDisplayRefreshRateFB is refused; FrameBridge now answers
# success, so the game's own error line only shows in older builds
pattern: 'request display Hz: -1000101000'
severity: error
diagnosis: The game asked for a refresh rate the Frame's runtime refuses and may quit on that error. A newer FramePort keeps the current rate instead; press Update on Frame. Setting Game settings → Refresh rate to 90 Hz also avoids the error.
suggest: [adapter.refresh_rate=90]
- id: hybrid-feature-missing
# GitHub #484 (YouTube VR): a Horizon OS hybrid app checks for the system feature oculus.software.vr.app.hybrid
# before it leaves its 2D panel for VR
pattern: 'Missing hybrid app system feature'
severity: warning
diagnosis: 'The app stays in its 2D panel because it can''t find a Quest system feature it checks before switching into VR (360° videos, full screen). "Hybrid app: allow the switch into VR" declares that feature for the app. Turn it on and press Update on Frame; whether the VR part then works depends on the app.'
suggest: [device.hybrid_app]
- id: web-wrapper
pattern: '(Creating TwaLauncher for com\.oculus\.browser|NameNotFoundException:? com\.oculus\.browser)'
severity: fatal
@@ -214,6 +296,16 @@ signatures:
GraphicsEnv functions it uses were not found or behaved differently, for example after a Lepton update), so the game's
OpenGL ES shader fixes (zink_shader_fix) don't apply.
suggest: []
- id: shader-fix-unmatched
# vkshim (vk_shader_fix) / the shader-fix layer (zink_shader_fix), once per session, 5 minutes after the first shader
pattern: '(?:vk shim|shader fix layer): \d+ shader fix\(es\) configured, none matched'
severity: info
diagnosis: This game's recipe has a shader fix (for a GPU hang at one effect or scene), but none of the shaders it
fixes showed up in the first minutes of play. If you haven't reached that part of the game yet, that's expected.
Otherwise a game update may have changed the shader, so the fix no longer applies and the old hang can come back.
If the game freezes or closes there, please send a problem report with diagnostics so the fix can be updated.
suggest: []
report: true
- id: zink-device-lost
pattern: 'zink.*DEVICE[_ ]LOST|VK_ERROR_DEVICE_LOST'
severity: fatal
@@ -325,6 +417,16 @@ signatures:
diagnosis: The game needs Meta's colour-space extension, which the Frame's runtime doesn't offer, and quits before
it starts VR (for example OutRun Quest VR). FrameBridge can offer it.
suggest: [adapter.fb_color_space]
- id: appsw-required
# GitHub #336: OutRun Quest VR requires Application SpaceWarp: without a motion-vector size (the Frame's runtime
# reports none) or without XR_FB_space_warp it logs this line and quits before its first frame
pattern: 'AppSW recommended buffers unavailable'
unless: 'per-game: space_warp_emul=1'
severity: fatal
diagnosis: The game requires Meta's space warp (Application SpaceWarp) and quits because the Frame reports no
buffer size for it (for example OutRun Quest VR). Turning space warp off can't help; FrameBridge can provide
space warp itself (the game renders every frame normally, its motion vectors are not used).
suggest: [adapter.space_warp_emul]
- id: vulkan-pipeline-refused
# GitHub #349: with Lepton's Fossilize layer loaded every Transformers pipeline failed with -1; without it a few
# multiview vertex shaders still fail in Turnip. Unreal ends at the first one.
@@ -452,11 +554,27 @@ signatures:
supersedes: [native-crash]
- id: cortopia-org-id-wait
# Meta's C# wrapper logs this when the native call returned request id 0 (no callback ever follows); Cortopia's
# start-up awaits it forever (GitHub #360)
pattern: 'An unknown error occurred\. Request failed\.[\s\S]{0,600}?Cortopia\.Scripts\.Platform\.Meta'
# start-up awaits it forever (GitHub #360: Meta.<Initialize>d__13; #422 TMNT: Empire City: <Login>d__17)
pattern: 'An unknown error occurred\. Request failed\.[\s\S]{0,600}?Cortopia\.Scripts\.Platform\.(?:Meta|<Login>d__)'
severity: error
diagnosis: The game waits forever on its loading screen for an answer from Meta's platform (an org-scoped user id, used as its analytics id) that OVRPort's platform library doesn't give. The patch makes the game skip that one request, as it does without Wi-Fi.
suggest: [frame.cortopia_org_id]
- id: meta-org-scoped-id-wait
# OVRPort's ovr_User_GetOrgScopedID returns request id 0 and Meta's wrapper logs "Request failed." with a stack
# that names no game code. Eleven Table Tennis (GitHub #452) logs its platform start just before: its
# <BuildIdentity>b__0 asks for the id ~0.3 s after "[META.FUN] in meta platform HandlePlatformInitialization"
pattern: 'meta platform HandlePlatformInitialization[\s\S]{0,3000}?An unknown error occurred\. Request failed\.'
severity: error
diagnosis: The game waits on its loading screen for Meta's org-scoped user id, which OVRPort's platform library
never answers. The patch answers that request with a fixed id derived from the game's user id.
suggest: [frame.ovr_org_scoped_id]
supersedes: [meta-request-failed]
- id: gazzlers-platform-unsupported
# GAZZLERS (GitHub #404): picks Meta/PICO from SystemInfo.deviceModel and throws for Lepton's "Valve Lepton"
pattern: 'Exception: Current Platform is not supported[\s\S]{0,300}?PlatformDeterminer\.GetAndroidPlatform'
severity: fatal
diagnosis: The game decides whether it runs on a Meta Quest or a PICO headset from Android's device name and stops for any other name; the Frame's Android calls itself Valve Lepton. The patch makes the game take its Meta Quest path.
suggest: [frame.gazzlers_quest_platform]
- id: ovr-microphone-crash
pattern: 'ovr_Microphone_GetOutputBufferMaxSize\+'
severity: fatal
@@ -521,6 +639,13 @@ signatures:
severity: error
diagnosis: Epic Online Services can't save its login keychain and retries without end (a start that never finishes, 100 % CPU). The usual cause is missing internal storage (see lepton-data-link-failed); the patch stops the loop, and EOS then reports the failure to the game once.
suggest: [frame.eos_keychain_loop]
- id: justdance-local-save-off
# Just Dance VR's own log line at start (Ubisoft's JdLog): its offline save opens, but this build never writes it
pattern: '\|LocalSave +\|OfflineSave_Init succeeded'
unless_patch: frame.justdance_local_save # the line is the same with the fix in the build
severity: info
diagnosis: Just Dance VR (0.5.4 and later) opens its local save at every start but never writes it (the save request method is empty in this build; on a Quest the progress is kept by Ubisoft's online backend after a Meta sign-in, which the Frame doesn't have), so progress, unlocks and the finished tutorial are lost at every restart. The patch makes the game write its local save; nothing online is touched.
suggest: [frame.justdance_local_save]
- id: pthread-keys-exhausted
pattern: 'cannot create thread specific key'
severity: fatal
@@ -549,6 +674,12 @@ signatures:
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: valve-fdm-render-crash
pattern: '(?s)\A(?=.*(?:Fatal signal 11|SIGSEGV))(?=.*vulkan\.freedreno\.so)(?=.*fdm_injection_CmdBeginRendering)'
severity: error
diagnosis: The game crashed in the graphics driver while Valve's foveation layer started a render pass. Valve's Fixed foveation path crashed AC Nexus like this (#394); builds with FramePort's foveation centring run Fixed on Valve's eye-tracked path instead, so rebuild the game (Update on Frame), or switch Foveated rendering to Eye-tracked or Off.
suggest: []
supersedes: [native-crash]
- id: batman-audio-geometry-allocation
pattern: '(?s)\A(?=.*com\.camouflaj\.manta)(?=.*Trying to allocate: [1-9][0-9]{10,}B)(?=.*MetaXRAudioUnityMemoryAllocator\.cpp)'
severity: fatal
+46 -23
View File
@@ -3,7 +3,9 @@
![FramePort's modules: the desktop app and the CLI drive pipeline.py (add, suggest, build, install, launch test), which uses sources/, analysis/, recommend/, build.py, targets/ and validate/; builds use patches/ and tools/; targets reach the agent on the Steam Frame through frame/ and install/ over SSH](images/architecture.svg)
## Data flow for one game
1. **Source** (`sources/quest_dump.py`): folder with an APK and optional `<package>/` or `obb/` data.
1. **Source** (`sources/quest_dump.py`): folder with an APK and optional `<package>/` or `obb/` data; SideQuest
backups and AXRB downloads are recognised, app bundles (`.xapk`/`.apks`/`.apkm`/`.apkx`, split APKs) are merged
into one APK first (`sources/bundles.py`).
2. **Analysis** (`analysis/detect.py`): package/label/version (pyaxmlparser), ABIs, engine, XR API, graphics API,
direct-VrApi, GLAD/eglGetProcAddress, Unity MSAA levels, Meta permissions, telemetry references.
3. **Recipe** (`recommend/engine.py`): every patch's `detect()` suggests itself with a reason; a catalog entry (exact
@@ -16,37 +18,50 @@
deployment.json, artwork) → `shortcuts` (detached systemd unit stops Steam, writes shortcuts.vdf + grid art,
restarts Steam).
6. **Test** (`validate/device.py`): agent `launch_test` (systemd-run launch.sh, wait, stop) → fetch launch.log →
`triage.py` (milestones + signatures → suggested patches) → UI offers "apply suggestions and rebuild".
`triage.py` (milestones + signatures → suggested patches) → UI offers "apply suggestions and rebuild". Real play
sessions are triaged the same way afterwards (`validate/session.py`, game page "Last session").
## Extending
- **New patch**: `patches/frame/<name>.py` with a `Patch` subclass (`detect`, `apply`, optional `validate`), plus a
signature in `catalog/triage.yaml` and a PLAYBOOK row. Stages: `overport` | `apk` | `install`.
- **Upstream fixed a bug we work around**: register an `UpstreamFix` (`patches/upstream.py`) in the workaround's
- **Upstream fixed a bug FramePort works around**: register an `UpstreamFix` (`patches/upstream.py`) in the workaround's
module: a probe that finds the fix in OVRPort's output (True / False / None = can't tell). Each build runs the probes
on the converted APK and leaves out the workarounds whose fix is there (log "not needed: …", `build.superseded`,
left out of settings.conf at install; game page and `frameport show` say so). The recipe keeps asking for the fix,
so builds made with an older runtime keep the workaround. `FRAMEPORT_KEEP_WORKAROUNDS=1` turns it off.
- **New heuristic**: put it in the patch's `detect()` (and `applies()` for visibility), with the evidence in the
so builds made with an older runtime keep the workaround. `FRAMEPORT_KEEP_WORKAROUNDS=1` turns the probes off.
- **New heuristic**: put the rule in the patch's `detect()` (and `applies()` for visibility), with the evidence in the
reason text; check `scripts/eval_heuristics.py` still reproduces the catalog and add a test in
`tests/test_heuristics.py`.
- **New game recipe**: `catalog/games/<package>.yaml` (or "Save as known-good" in the GUI, which writes to the user
catalog). Publish recipes by serving a folder with `index.json` and pointing `FRAMEPORT_CATALOG_URL` at it.
catalog). Installed apps fetch `catalog/games/` from GitHub `main` every 6 hours (`recommend/catalog.refresh_remote`),
so a merged recipe reaches users without a release; `catalog/triage.yaml` follows the same way. `FRAMEPORT_CATALOG_URL` points the apps at another folder that
serves `index.json` + `<package>.yaml` instead.
- **New target**: implement `targets/base.Target`; the pipeline and UI only use that interface.
## Oculus Rift (PC VR) games
`sources/rift_dump` finds Windows game folders; `analysis/rift` (own PE reader) detects exe, bitness, engine, LibOVR vs
OpenXR, D3D version and Oculus Platform SDK use; ids are `rift.<slug>`. Their only patches are the `pcvr` category
(`patches/pcvr.py`: Revive, OpenVR backend, crash reporter, Oculus detection, OpenXR layer, Proton
log/version/env); `base.for_game` keeps Quest and PC VR patches
apart. "Build" = `pipeline.prepare_rift` (checks + Revive). Revive is a portable tool (`tools/revive.py` unpacks
`ReviveInstaller.exe` in pure Python: NSIS header + deflate blocks). Targets:
- `targets/pc_revive.PcReviveTarget`: Windows/WSL (`core/winhost.py`), non-Steam shortcut running
`ReviveInjector.exe /openxr <exe>` via the agent's VDF code, grid art, local records in `<user data>/pc/`.
## PC VR (Oculus Rift and SteamVR) games
`sources/rift_dump` finds Windows game folders (one game per folder); `analysis/rift` (own PE reader) ranks the
programs, detects bitness, engine, the VR APIs (LibOVR, OpenVR, OpenXR), D3D version and Oculus Platform SDK use; ids
are `rift.<slug>`. Their only patches are the `pcvr` category (`patches/pcvr.py`: Revive, OpenVR backend, crash
reporter, Oculus detection, OpenXR layer, launch arguments, Proton version/env); `base.for_game` keeps Quest and PC
VR patches apart. Repacks are usually pre-patched, so a game is installed unchanged and its program started
directly; Revive is only suggested for games with LibOVR code, and those run only on the PC (Revive's hooks don't
work under Proton on ARM64). "Build" = `pipeline.prepare_rift` (checks, plus Revive when the recipe has it). Revive
is a portable tool (`tools/revive.py` unpacks `ReviveInstaller.exe` in pure Python: NSIS header + deflate blocks).
Targets:
- `targets/pc_revive.PcReviveTarget`: Windows/WSL (`core/winhost.py`), non-Steam shortcut (the game's program, or
`ReviveInjector.exe [/openxr] <exe>`; Revive's OpenVR backend by default) via the agent's VDF code, grid art, local
records in `<user data>/pc/`.
- `FrameLeptonTarget.install_pcvr` → `installer.install_pcvr` → agent `prepare_pcvr`/`finalize_pcvr`: upload
`game/` + `revive/` (+ `xrlayer/` for `pcvr.xr_timefix`, `helpers/` for `pcvr.oculus_unreal`; size-manifest dedupe,
`game/` (+ `revive/`, `xrlayer/` for `pcvr.xr_timefix`, `helpers/` for `pcvr.oculus_unreal`; size-manifest dedupe,
stale files removed), `launch.sh` running the ARM64 Proton chain (built from toolmanifest.vdf; with
`pcvr.oculus_unreal` the injector runs through `helpers/fp_oculushmd.exe`, which provides Unreal's
`pcvr.oculus_unreal` the program runs through `helpers/fp_oculushmd.exe`, which provides Unreal's
`OculusHMDConnected` event); `proton_status`/`install_proton` manage Proton from Valve's ARM64 compat list.
## Linux apps and Windows programs
`pipeline.add_linux_app` (AppImage, archive or folder; library kind `linux`) and `pipeline.add_windows_exe` add
programs that are installed unchanged. The agent's `finalize_linux` unpacks them, writes the launcher (arm64 builds
directly, x86_64 builds through Valve's FEX), a Desktop Mode menu entry and, for SteamVR overlay apps, their
`.vrmanifest` registration; Windows programs run through Proton like PC VR games.
- **Native binaries** (`artifacts/`): edit `native/…`, run `python native/build.py`, commit the new artifacts + SHA256SUMS,
run `frameport parity` to see which games change.
@@ -61,18 +76,26 @@ apart. "Build" = `pipeline.prepare_rift` (checks + Revive). Revive is a portable
- `prepare()` (bundle): `<data>/updates/<ver>/`: download → SHA256SUMS.txt check → extract to `staged/` (ditto on
macOS) → layout check → on Windows the same Authenticode signer as the running exe → `ready.json`.
- `apply()`: writes `<data>/updates/apply.{ps1,sh}` and starts it detached; FramePort quits. The script waits for the
pid. Windows: backs up the files it replaces (`updates/<ver>/previous`) and copies over the folder (the zip has no
folder of its own). macOS/Linux: `mv` target → `.old`, staged → target, rollback on failure, clears the quarantine.
Then it relaunches FramePort. Log: `<data>/logs/update.log`.
pid. Windows: changes nothing while FramePort or a program file (.exe/.dll/.pyd) is still in use (waits 60 s), backs
up the files it replaces (`updates/<ver>/previous`), moves the old version's files the new one doesn't have out of
the bundle's own folders (app, Lib, DLLs, site-packages, data) into `previous` too, and copies over the folder (the
zip has no folder of its own); a failure deletes the files the update added and restores the rest (GitHub #476: a
rollback that kept the new version's added modules made the old version crash at start). Bundles carry
`frameport/_bundle_files.txt` (written by `scripts/package.py`, `core/bundle_files.py`): at start the app deletes
(or at least never imports) package files the list doesn't have and tells the user to reinstall when compiled
modules don't match it — updates made by an older version's script still copy over without cleaning.
macOS/Linux: `mv` target → `.old`, staged → target, rollback on failure, clears the quarantine.
Then the script relaunches FramePort. Log: `<data>/logs/update.log`.
GUI: background check 10 s after start + every 6 h → sidebar card + Library bar → dialog with notes → job
`app-update` (waits for other jobs) → restart. "Install updates automatically": prepare in the background, apply in
`main()` before the window opens. CLI: once-a-day hint from the cached result (a daemon thread refreshes it), `frameport
update [--check] [--yes]`. CI runs `scripts/update_smoke.py` on every OS with the archive it just built.
## Dynamic data (fetched live, cached, bundled fallback)
overport CLI release (Android-XR-Bridge/OVRPort, fallback ovrport/app) + patch list + titles, Temurin JRE (Adoptium API), apksigner (Google repository index),
store artwork/titles (overport image API), catalog (optional remote), Lepton location/appid (Frame appmanifests),
Steam user (Frame userdata).
overport CLI release (Android-XR-Bridge/OVRPort, fallback ovrport/app) + patch list + titles, Temurin JRE (Adoptium
API), apksigner (Google repository index), Cpp2IL and APKEditor (GitHub releases, on first use), store artwork,
titles and details (overport image API, OculusDB, Steam), catalog recipes and triage signatures (GitHub `main`),
announcements, Lepton location/appid (Frame appmanifests), Steam user (Frame userdata).
## Built on
+10 -9
View File
@@ -1,8 +1,8 @@
# Compatibility
FramePort's catalog has a tested recipe (the patches and settings that work) for many games, each marked as working,
working with issues or not running: see the [list of tested games](GAMES.md). Other games get suggested patches,
each with its reason. Every patch can be switched on or off under **Customize** on the game's page.
FramePort has tested recipes (the patches and settings that work) for hundreds of games, each marked as working,
working with issues or not running: see the [tested games](GAMES.md). Other games get suggested patches, each with
its reason. **Customize** on a game's page turns any patch on or off.
Got a game working? [Share its recipe](INSTALL.md#share-a-recipe-or-report-a-problem).
@@ -10,13 +10,14 @@ Got a game working? [Share its recipe](INSTALL.md#share-a-recipe-or-report-a-pro
| Kind of game | On the Steam Frame |
|---|---|
| Quest games | Converted to OpenXR (the VR standard the Frame uses) by OVRPort and patched; they run in Lepton, Valve's Android runtime |
| 32-bit Gear VR, Oculus Go and early Quest games | Run through FramePort's 32-bit bridge (experimental): it translates the game's 32-bit code inside the app; the Frame's right controller acts as the Gear VR / Go controller |
| Other Android VR apps using OpenXR (for example Pico builds) | Converted the same way; the other headset's own features and store services aren't available |
| Android apps without VR | Installed unchanged and shown as a window in the headset |
| PC VR games | On the Frame through Proton (experimental), or on your PC and streamed to the Frame: see [PC VR games](INSTALL.md#pc-vr-games) |
| Quest games | Converted to OpenXR (the Frame's VR standard) by OVRPort, patched, and run in Lepton, Valve's Android runtime |
| 32-bit Gear VR, Oculus Go and early Quest games | Run through FramePort's 32-bit bridge (experimental). The right controller acts as the Gear VR or Go controller |
| Other Android VR apps using OpenXR (for example Pico builds) | Converted the same way, without the other headset's own features and store services |
| Android apps without VR | Installed unchanged and shown as a window |
| PC VR games | On the Frame through Proton (experimental), or on your PC and streamed: see [PC VR games](INSTALL.md#pc-vr-games) |
| Linux apps | Installed unchanged. x86_64 builds run through FEX, slower than arm64 builds: see [Linux apps](INSTALL.md#linux-apps) |
| Can't run | x86-only Android apps, apps for Pico's or HTC's own VR system, Android XR apps, and games that check their license through the Oculus app |
Automatic launch tests show that a game starts; only the headset shows whether it looks right.
Launch tests show that a game starts. Only the headset shows whether the game looks right.
![Steam Frame](images/frame.png)
+35 -48
View File
@@ -1,25 +1,24 @@
# Controller buttons
> **Experimental.** The Frame's runtime accepts these bindings, but the modes haven't been tried in the headset on
> many games yet. If buttons stop working in a game, set it back to **SteamVR default** (Game settings →
> Controllers) and report it.
> **Experimental.** The Frame's runtime accepts these bindings, but few games have been tried in the headset. If
> buttons stop working in a game, set the game back to **SteamVR default** (Game settings → Controllers) and report the problem.
Quest games usually bind Quest Touch controllers. Steam Frame has a d-pad and View button on the left controller,
and an A/B/X/Y diamond and Menu button on the right. Without a game's own Frame bindings, SteamVR maps Touch X/A
to the bottom button and Touch Y/B to all three top buttons. View sends Touch Menu. The right X and Y therefore
send Touch B, rather than the left-hand X and Y of a Quest controller.
Quest games expect Quest Touch controllers. The Frame's left controller has a d-pad and a View button; the right one
has an A/B/X/Y diamond and a Menu button. Without a game's own Frame bindings, SteamVR sends Touch X/A from the bottom
button and Touch Y/B from all three top buttons, so the right X and Y both send Touch B. View sends Touch Menu.
**Controller buttons** chooses an alternative for Quest games running through FrameBridge:
**Controller buttons** offers other mappings for Quest games:
- **SteamVR default (0)** leaves SteamVR's Touch remapping in place. This is the initial default.
- **Mirror (1)** uses every face button. On each controller, bottom and outer send X/A; top and inner send Y/B, as on Quest controllers, whose second button sits up and toward the other hand.
- **Quest pair (2)** uses two face buttons per controller, placed like Touch's: left X = d-pad down, Y = d-pad
right; right A = A, B = X. The other face buttons send no Touch input.
- **SteamVR default (0):** SteamVR's mapping above. The initial default.
- **Mirror (1):** uses every face button. Bottom and outer send X/A; top and inner send Y/B, as on a Quest
controller, whose second button sits up and toward the other hand.
- **Quest pair (2):** two buttons per controller, placed like Touch's: left X = d-pad down, Y = d-pad right; right
A = A, B = X. The other face buttons send nothing.
![Quest Touch reference and the three Steam Frame button mappings](images/controllers/quest-buttons.svg)
The small label identifies the physical button; the larger label is the Touch input the game receives. Blue is
X/A, orange is Y/B or Menu, and a dash means no Touch input. Left and right refer to the hand holding the controller.
The small label names the physical button, the large one the Touch input the game gets. Blue is X/A, orange is Y/B
or Menu, a dash is no input.
<!-- quest-buttons-table:start -->
| Frame input | SteamVR default (0) | Mirror (1) | Quest pair (2) |
@@ -38,32 +37,25 @@ X/A, orange is Y/B or Menu, and a dash means no Touch input. Left and right refe
| Right bumper | — | — | — |
<!-- quest-buttons-table:end -->
The table describes button clicks and face-button touches. View sends Menu click only. Triggers, grip pressure,
thumbsticks, tracked poses and vibration keep their corresponding Touch bindings.
Triggers, grips, thumbsticks, tracking and vibration keep their Touch bindings. View sends Menu click only.
## Choose a mode
For one game, open its **Game settings → Controllers → Controller buttons**. Choose **Use global default** to
follow Settings, or select a specific mode to keep an override for that game. An explicit **SteamVR default**
keeps mode 0 even when the global choice is Mirror or Quest pair.
- **One game:** **Game settings → Controllers → Controller buttons**. **Use global default** follows Settings; any
other choice stays for that game, including an explicit **SteamVR default**.
- **All games:** **Settings → Controllers**. New installs and updates use the default for games without their own
choice.
**Apply to installed games now** sends the default to installed Quest games that follow it.
For the library default, open **Settings → Controllers**. The diagram follows the selected mode. New installs
and configuration updates use this default for games without an override. With a Frame connected, choose
**Apply to installed games now** to send it to installed Quest games that follow the global default. Overrides
are kept. Changing the default does not mark every game as needing an update.
Changes apply at the **next launch**. Games installed before this feature need **Update on Frame** once.
Changes take effect at the **next launch**. Close and relaunch a running game.
Games installed before this feature need **Update on Frame** once for the mode to work.
The CLI can change an installed game's runtime setting:
Command line, for an installed game (writes the Frame's settings only):
```sh
frameport settings <pkg> quest_buttons=2
```
Use `0` for SteamVR default or `1` for Mirror. This command writes the installed settings directly; it does not
save a per-game recipe override. To keep the value through later installs and global applies, also save it in
the recipe, or use Game settings:
To keep the value through later installs, also save the value in the recipe (or use Game settings):
```sh
frameport recipe <pkg> --set quest_buttons=2
@@ -71,23 +63,18 @@ frameport recipe <pkg> --set quest_buttons=2
## Limits
- SteamVR's binding UI can still override these suggested bindings. Check custom bindings if the result differs
from the diagram.
- The d-pad has a single touch sensor: a thumb resting on it touches X and Y together. The right controller's
face buttons sense touch one by one.
- Bumpers have no Touch counterpart and are unbound. Thumbrest touch is dropped; the Frame profile has no
corresponding input. System buttons keep their runtime role.
- Mirror and Quest pair require the Frame runtime's `XR_VALVE_frame_controller_interaction` extension. If it is
absent, FrameBridge logs that fact and leaves SteamVR's remap in place.
- Games suggesting their own Frame Controller bindings keep those bindings and continue seeing the Frame
Controller profile, even if they also suggest Touch bindings. These modes do not replace the game's own mapping.
- Touch suggestions using bare pose, trigger, squeeze or button paths are translated too; explicit components
such as `/pose`, `/value`, `/click` and `/touch` are also supported where the mapping provides them.
- Finger proximity emulation (`proximity_emul`) continues to work with Mirror and Quest pair. Dropping the
unsupported thumbrest path does not disable proximity emulation from the other touch inputs.
- The setting applies to Quest VR games, including 32-bit games through the bridge. It does not change PC VR or
flat Android Steam Input layouts.
- Custom bindings in SteamVR's binding UI override these. Check them if the result differs from the diagram.
- The d-pad has one touch sensor: a thumb resting on the d-pad touches X and Y together. The right face buttons sense touch
one by one.
- Bumpers and thumbrest touch have no Touch counterpart and do nothing. System buttons keep their role.
- Mirror and Quest pair need the runtime's `XR_VALVE_frame_controller_interaction` extension. Without the extension,
FrameBridge logs the fact and keeps SteamVR's mapping.
- Games with their own Frame Controller bindings keep them; these modes don't replace a game's own mapping.
- Touch bindings on bare pose, trigger, squeeze or button paths are translated too, as are `/pose`, `/value`,
`/click` and `/touch` where the mapping has them.
- Finger proximity emulation (`proximity_emul`) keeps working with Mirror and Quest pair.
- Applies to Quest VR games, including 32-bit games on the bridge. PC VR games and flat Android apps aren't changed.
The mapping follows [Valve's Frame Controller input reference](https://partner.steamgames.com/doc/steamhardware/steamframe/input)
and the [shared mapping data](input/quest_buttons.json). To regenerate the diagram, app illustrations and table,
run `uv run python scripts/controller_diagram.py`; add `--check` to verify them without writing files.
and the [shared mapping data](input/quest_buttons.json). `uv run python scripts/controller_diagram.py` regenerates
the diagram, app illustrations and table (`--check` verifies them).
+594 -2
View File
@@ -59,6 +59,25 @@ tested), translator speed. Profiled on the Frame
main thread). Now lock-free lookups, ltab refill, per-thread flags, per-page block lists: Gunjack's dispatcher
23.6 % -> 1.6 %, CPU in the same phase about -55 %, still 72 fps.
**Ghost Giant (GitHub #439, Unity 2017.4 il2cpp + OVRPlugin 1.29 on VrApi 1.16, headless 2026-10-24):** the logo,
then black forever was its intro videos (Thunderful/Zoink logos, Unity VideoPlayer): without libmediandk in the
bridge Unity took its JNI path, `GetDirectBufferAddress` of the codec's ByteBuffers (host heap, above 4 GiB) gave 0
→ `AndroidVideoMedia: Error while getting codec input buffers`, and the Splash scene waits for the video for ever
(never reaches `Scene SplashApp`). With the NDK media functions + `AMEDIAFORMAT_KEY_*` symbols (dcbcce0f, bccd916f)
both videos play (decoded by `OMX.google.h264.decoder` into a SurfaceTexture), then SplashApp → PersistentObjects →
Loading → MainMenu, which renders (left-eye snapshots every 3 s: before = pure black from +9 s, mean 0.0; after = the
two videos, then the island menu from +46 s). Still seen: `VideoPlayback audio buffer overflow. N sample frames
discarded` during the intros (the videos' sound may crackle; unchecked in the headset). The game hides everything
behind "Return to play area" when the head is outside `PlayAreaManager`'s height limits (`PlayAreaSettings.heightData`
min/max above the floor from GetTrackingTransform(3)).
**Headless snapshots of VrApi games** (`device.lepton_env` / launch.sh `LEPTON_ENV_OVP_HEADLESS_TEST=1` + adapter
`snapshot=N`): without a worn headset the session stays SYNCHRONIZED (`shouldRender` false), the VrApi bridge ended
every frame empty (`pacing 2s: … ends=145 (empty 145 …)`) and the a32 bridge dropped every projection with an
untracked default pose (`a32 submit anomaly … dropped=1`), so there was nothing to snapshot. Under OVP_HEADLESS_TEST
the VrApi bridge now submits the images anyway, puts the floor (GetTrackingTransform(3)) 1.6 m below the eye origin
instead of the runtime's headless STAGE (0.865 m: Ghost Giant then showed only "Return to play area"), and the a32
bridge keeps the game's pose. Never set outside tests.
## Upload speed
**Upload speed (measured 2026-09-29):** via the home router (PC Wi-Fi → router → Frame wlan0) only 15-18 MB/s, the
@@ -146,11 +165,34 @@ pose (images drawn only after head turns, or ahead from xrWaitFrame and sometime
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
swapchain was originally halved (1536/eye) while investigating memory. Issue #387 below confirms a swapchain
count cap; reducing image sizes does not avoid it. Frame data snapshot
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
Not yet verified in the headset.
**Issue #387 (2026-10-18, host-tested only):** 4XVR's extra sub-panels hit the session cap at roughly 15–16 live
swapchains: the same 256×128 create succeeds then fails in the same millisecond, so shrinking images is not a fix.
Default-off GLES `swapchain_pool` (catalog enabled for 4XVR) starts at eight live app runtime swapchains, reserving runtime
headroom for up to two 4096² SRGB atlases. Single-face/layer/sample/mip colour images use private app-context textures;
submitted panels are blitted into packed atlas rectangles after cylinder strips and rect_clamp. Destroy frees rectangles.
Vulkan is untouched. No adapter revision bump: this opt-in fix should not mark every installed game for update.
The new catalog key already makes older apps reject the recipe via `unusable_reason`; triage suggests no unknown setting.
Headset check: repeatedly open/close all 4XVR submenus while playing stereo and 360 video, verify both eyes, panel crops,
curved strips, alpha and frame pacing; confirm no new -10 errors or menu closures and reuse after closing panels.
**Issue #424 (2026-10-23, host-tested only): Vulkan panel pool.** AC Nexus (Unity, Vulkan) creates ~12 single-layer
format-43 panels (1200x1020, 768x153, 1459x1228, 855x1500, 1875x1500, 2457x1152, 512x512, 460x102, 691x153, 360x360,
…) plus a 3-swapchain eye set (arraySize 2: colour 43, 384x384 formats 97/129). With the Quest 3 identity it creates a
2592x2256 eye set while the old one lives: the runtime refuses it (-10) and the game stops drawing (the catalog kept
the Quest 2 identity until now). `swapchain_pool` now also covers Vulkan sessions: from eight live app swapchains,
format-43 panels get private VkImages copied into one or two 4096² atlases at xrEndFrame (no CPU wait on this frame's
GPU work; a 3-deep fence ring). Host harness: 8 app runtime swapchains + 1 atlas + the new eye set = 12 live, 7 panels
pooled. Catalog: AC Nexus back to Quest 3 with `swapchain_pool: 1` (`min_app` 1.3.1). Device check (headless first):
the 2592x2256 create succeeds, `pacing` continues, no `atlas update failed`; then the headset: panels upright, crisp and
in place, menus, frame time.
## Companion apps / intents (Stremio + 4XVR)
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
@@ -162,6 +204,36 @@ scheme, explicit component or not, no property to disable): it writes `steam://o
→ 4XVR) can't work inside Lepton; don't retry without Valve changing it. Lepton installs exactly one `*.apk` per app
folder (two break `get_apk_path`).
## Browser stand-in for sign-ins (Plex, GitHub #466)
**Browser hand-off (2026-10-24, GitHub #466 Plex):** Lepton's hand-off is in services.jar
`com.android.server.wm.ActivityStarter.execute()`, before anything is resolved: when the request's intent action is
`android.intent.action.VIEW` it logs `ActivityTaskManager: Intercepted VIEW: <data>`, appends
`steam://openurl/<data>\n` to `/lepton/steam.pipe` (mounted rw from the host's `~/.steam/steam.pipe`, a 0600 FIFO;
Steam's console_log.txt then shows `ExecCommandLine: "steam://openurl/…"`) and returns START_SUCCESS (0) — also for a
VIEW without data (nothing written) and for an explicit component. Other actions start normally. Apps that check for a
browser first (Plex: `<queries>` for VIEW + BROWSABLE http/https, then "no browser installed") never get there: the
container has no browser package. Patch `frame.browser_handoff` (opt-in; offered for 2D apps and suggested-off when
the manifest's `<queries>` ask for a browser or a Custom Tabs service, analysis `browser_query`) adds
`org.frameport.handoff.BrowserHandoff` to the app's own manifest (exported, Theme.NoDisplay, excludeFromRecents,
VIEW + DEFAULT + BROWSABLE, schemes http/https without a host, so the app's own host-specific deep links are
unchanged; missing android: attribute names are inserted into the resource-mapped part of the string pool,
`axml._attr_name`) and its class as the next `classesN.dex` (`native/java-handoff`, `dex/browser-handoff.dex`). The
app's query now finds its own stand-in; its startActivity(VIEW) is still taken by Lepton before resolution, so the stand-in
normally never runs. If it is started directly (another action, explicit component), it writes the same pipe line on a
background thread and finishes. No second package (see Companion apps above). A 2D app turns from "installed
unchanged" into a FramePort build signed with FramePort's key once an APK patch is on (`engine.flat_as_is` in
`pipeline.set_recipe`; before, a hand-enabled APK patch on a 2D app stayed `as_is` and was never applied, e.g.
Plex's `frame.sdl_clipboard`). Whether the app then notices the finished sign-in depends on its flow (PIN/code polling
works from any browser; a redirect back into the app through a link can't, since Lepton sends every VIEW to Steam).
Verified headless on the dev Frame (2026-10-24) with a scratch 2D test app (targetSdk 33, Plex's `<queries>`,
`queryIntentActivities`/`resolveActivity` with MATCH_DEFAULT_ONLY, then startActivity): unpatched `query: 0`,
`resolve: null`; with the patch `query: 1 … org.frameport.handoff.BrowserHandoff`, the VIEW start returned normally
and Steam's console_log had `ExecCommandLine: "steam://openurl/https://example.com/?…=view"`; an explicit start of the
stand-in wrote `…=explicit` itself (logcat `FramePort: browser hand-off: page sent to Steam`): the app uid can open
`/lepton/steam.pipe` for writing. aapt2 reads the edited manifest of 70 real APKs with the original tree unchanged.
Plex's own sign-in flow is unverified.
## Text input (Stremio VR)
**Text input (2026-10-03, verified in the headset with Stremio VR):** Lepton's Android has no IME (`ime list` empty)
@@ -376,7 +448,11 @@ bar) that never advances; the async loader thread sleeps and OBB reads stop (~16
## Vader's leak solved / AC Nexus flips
**Vader's leak solved / AC Nexus flips (2026-10-14):** Vader Immortal leaves GL errors pending on its context; SteamVR's GL interop (`openxr_graphics_gl_openvr.cpp`, `xrclient_<pkg>.txt` in Steam's logs: `SXR_GL_CHECK: error:0x502 ... glGenTextures`) takes the stale error as its own failure once, and from then on `CVRCompositorSharedTextures::BeginCommandBuffer` allocates a command buffer every frame (perf page-fault stacks; ~430 renderD128 mappings + 22 MB/s). Adapter `gl_error_clear` (GLES, catalog on for Vader ep1-3) clears them before xrCreateSwapchain/xrBeginFrame/xrEndFrame: flat memory, no SXR_GL_CHECK (headless). The per-game `xrclient_<pkg>.txt` logs are worth reading for runtime errors. The Frame's runtime caps swapchains per session (XR_ERROR_LIMIT_REACHED from ~15; AC Nexus asks for more): flip emulation now turns quads upright in place (scratch VkImage, once per acquire; transfer usage added to single-layer colour swapchains) instead of a shadow swapchain per quad, which ran into the cap (#138). Headless sessions don't draw AC Nexus's panels (adapter `flip_dump` saves before/after images of flipped quads, for headset runs).
**Vader's leak solved / AC Nexus flips (2026-10-14):** Vader Immortal leaves GL errors pending on its context; SteamVR's GL interop (`openxr_graphics_gl_openvr.cpp`, `xrclient_<pkg>.txt` in Steam's logs: `SXR_GL_CHECK: error:0x502 ... glGenTextures`) takes the stale error as its own failure once, and from then on `CVRCompositorSharedTextures::BeginCommandBuffer` allocates a command buffer every frame (perf page-fault stacks; ~430 renderD128 mappings + 22 MB/s). Adapter `gl_error_clear` (GLES, catalog on for Vader ep1-3) clears them before xrCreateSwapchain/xrBeginFrame/xrEndFrame: flat memory, no SXR_GL_CHECK (headless). The per-game `xrclient_<pkg>.txt` logs are worth reading for runtime errors. The Frame's runtime caps swapchains per session (XR_ERROR_LIMIT_REACHED from ~15; AC Nexus asks for more): flip emulation now turns quads upright in place (scratch VkImage, once per acquire; transfer usage added to single-layer colour swapchains) instead of a shadow swapchain per quad, which ran into the cap (#138). Headless sessions don't draw AC Nexus's panels (adapter `flip_dump` saves before/after images of flipped quads, for headset runs). 2026-10-23: panels that stayed upside down at random were blits that took longer than the 100 ms wait while the GPU was loading; they were counted as failed and flipped again next frame, so both blits ran. A submitted blit now counts as done (log `flip: blit … still running after 100 ms`), and the shared command buffer waits for it before reuse (`vk_cmd_idle`, also fade_quad/cube_standin). Equirect2 layers (type 1000091000, `flip_tag=1 usable=0`) are dropped, not shown upside down. Same day, headset dumps (`flip_dump`, now also a `shown`/`late` image later): the "calibrate arms" panel was finished by the game after xrEndFrame (raw dump half drawn) and never acquired again, so its drawing landed upside down after the flip. Each flipped image is now sampled (16x16 texels) for ~2 s; a settled change without a new acquire flips it again (log `drawn after its flip`). Flips also use the last released image (release tracking). AC Nexus' recipe lost `device.text_input_window`: its Android window took the controllers' input.
**Flips as atlas copies (2026-10-24, replaces the in-place flip):** every in-place variant raced the game: Unity draws some panels after xrEndFrame (or on the GPU after FrameBridge's blit) without acquiring them again, so a one-shot flip of the app's own image either ran before the drawing (content upside down for good) or, when "repaired" by fingerprints, was applied again to an animated panel (AC Nexus' pooled "Loading" panel was re-flipped over and over). The blits also waited for the GPU inside xrEndFrame (up to 100 ms per flip, 1 s per dump), so the compositor missed frames: the likely source of the white start flash, which came and went with the diagnostics' extra waits/readbacks (owner: gone with heavy diagnostics off, back with fdaba13f's readbacks). GLES never had a flip path; what made the GLES pool robust is that it never touches the app's texture and copies it into the atlas every frame. Vulkan now does the same: `swapchain_pool.c` owns flips (also when `swapchain_pool` is off: the Vulkan session gets the atlas machinery, nothing is pooled). A flagged pooled panel's per-frame atlas copy becomes a vkCmdBlitImage with reversed rows (same cost); a flagged runtime swapchain's quad gets an atlas area for that frame (re-packed every frame around the pooled rectangles, shared by both eye quads) with an upside-down blit of its last released image. The app's images are only read, so late drawing shows upright one frame later and no flip can be applied twice; one command buffer per frame on the binding's queue after the app's submits, a 3-deep fence ring, no CPU waits (headless: 0.06 ms avg, 0.2 ms max in xrEndFrame). `flip_dump=N` now reads back the atlas region the runtime is given (async, written when the fence of that frame has signalled, a file per content change: `fb_flip_<n>.ppm`); each file is written on the render thread (45-60 ms hitches while panels change), so leave it off for play. `layer_debug` logs copies/frame and every wait (`swapchain_pool: … copies/frame`). Headless (two runs): "WARNING BEFORE PLAYING", "Please carefully read…", the "work of fiction" notice (pooled), "The game saves automatically…" (animated icon) and both "Loading" panels upright as submitted, through their fades; 72.2 fps after the game's own ~14 s loading stall (same as with flips off). Not reachable headless: the calibrate-arms screen; the headset check is that screen, the start (no white flash) and the menus.
**QuestZDoom slowdown, same leak (GitHub #237, 2026-10-24):** a player's session slowed over ~6 minutes (vrcompositor `WaitForPendingPresent` failures more and more often), then the kernel oopsed in `drm_gpuvm_validate_deferred_collect` (`msm_ioctl_gem_submit` from the game's `AppThreadFunction`, a write past an array at index 0x1600) and the Frame restarted. `xrclient_com.drbeef.questzdoom.txt` had `SXR_GL_CHECK: error:0x500 ... glGenTextures`: the game leaves GL_INVALID_ENUM pending every frame. Headless with freedoom2.wad, `/sys/kernel/debug/dri/0/gem` (readable as steamos; `gemstat`, objects per `vm=`) showed the game's VM gaining ~105 GPU objects a second (8,027 after 75 s), freed when it quit; with `gl_error_clear=1` the total stayed at ~1,915 for two minutes and no SXR_GL_CHECK was logged. Now suggested for every Team Beef port (Lambda1VR's log on the dev Frame has no SXR_GL_CHECK, so not all of them hit it; JKXR #441 has the same compositor stalls, unconfirmed). That debugfs listing is a quick leak check for any game.
## Unreal's self-restart
@@ -680,6 +756,51 @@ per game; add_from_link clears the offer and keeps a name the user gave). CLI `f
OutRun Quest VR (`org.outrun.quest.vr`, its own Khronos loader, no OVRPlugin) logs "Rec.709 color space unavailable" right after xrCreateInstance and quits ~20 s later without a session: the Frame's runtime (and OVRPort's dispatcher) list no XR_FB_color_space. Adapter setting `fb_color_space` (off by default, `native/adapter/color_space.c`) offers the extension when the runtime lacks it, strips it from the runtime's create info, fills XrSystemColorSpacePropertiesFB with Rec.709 and answers xrEnumerateColorSpacesFB / xrSetColorSpaceFB (no conversion: the runtime has no API for it). The functions are reachable through FrameBridge's xrGetInstanceProcAddr and `framebridge_extension_proc` (xrshim); OVRPort's fixed dispatcher table can't forward them, hence off by default. Not yet tried on the device (the build was not downloaded: it ships Sega's game data). Log: `color_space: emulating XR_FB_color_space`.
## XR_FB_space_warp emulation (#336)
**OutRun Quest VR (2026-10-24):** with colour space solved, OutRun logs `E OutRunQuest: AppSW recommended buffers
unavailable` right after its session, reference spaces and view sizes, then quits (ANR). Same line with
XR_FB_space_warp enabled (10-19) and hidden (`hide_space_warp`, 10-24): the game requires Application SpaceWarp, so
hiding can't help. OutRun's binary isn't available (Sega content), so the exact check is unknown; the emulation covers
the two plausible ones: the motion-vector size and the formats.
**What the Frame reports (dev Frame, 2026-10-24):** SteamVR's Android runtime lists XR_FB_space_warp (spec 2) and
answers XrSystemSpaceWarpPropertiesFB with the **full eye size, 1728x1728** (Meta: about a quarter). It accepts
Vulkan motion-vector/depth swapchains (`376x376 format=97` / `format=129`, Pinball FX VR, ITR2); its GLES swapchain
formats are the sRGB colour ones. OVRPlugin/Unreal ignore the reported size and make 376x376 themselves.
**`space_warp_emul` (adapter setting, off by default; `native/adapter/space_warp.c`):** offers XR_FB_space_warp
(passed to the runtime when it has it, else emulated; overrides `hide_space_warp`); reports eye/4 (432x432 on the
Frame) unless the runtime's answer is already below half the eye; adds RGBA16F + D24S8/D24/D32F/D16 (GLES) or 97 +
129/126/125/124 (Vulkan) after the runtime's formats in xrEnumerateSwapchainFormats (exported too: OVRPort's
dispatcher can call core functions by symbol); serves motion-vector swapchains (RGBA16F/RG16F at the reported size
or at most half the eye) and their depth (depth usage at the reported size, or the size of a served motion-vector
swapchain of the session) as FrameBridge-owned GL textures (2D arrays for multiview) / Vulkan images (attachment
layout, META extra usage honoured), three images round robin; wait/release succeed at once (a gipa wrapper sits in
front of whichever wait/release hook the other settings chose); xrEndFrame drops XrCompositionLayerSpaceWarpInfoFB and
depth info showing a served image (`sw_strip_chain` keeps eye depth info: depth infos are copied past a removed node;
the old hide_space_warp strip dropped the whole chain when space warp info wasn't first). The game renders every
frame: SteamVR doesn't halve the frame rate the way Meta's runtime does. No new runtime functions, so OVRPort's
dispatcher table needs nothing (xrshim not involved). Logs: `space warp: runtime motion-vector size`, `space_warp_emul:`.
Triage `appsw-required` suggests it; catalog `org.outrun.quest.vr` = fb_color_space + space_warp_emul. The
reporter's recipe is a user recipe: the setting has to be switched on by hand (Customize → Game settings → Provide
space warp).
**Headless test (Pinball FX VR, Unreal 5 + OVRPlugin, Vulkan, owner's build re-signed with its own key, only
FrameBridge replaced; original APK + settings restored after):** `space_warp_emul=1 force_render=1 layer_debug=1`,
`hide_space_warp=0`: `the app enabled XR_FB_space_warp (the runtime has it too)`, `runtime motion-vector size
1728x1728` → `432x432 reported`, both 376x376 pairs (format 97 + 129, after each eye swapchain) `served by FrameBridge
(Vulkan images, 3 images)`, `space warp images: 600 acquired, 599 waited, 598 released (all by FrameBridge)`, `removed space warp info from the eye images`, `600 frames submitted with their space warp
info removed`, 0 xrEndFrame failures, 72.2 fps steady after the loading phase (the same series as the game's own
recipe: 62/71/56/46/70 then 72.2), RUNNING after 117 s, the same 12 OVRPlugin errors as the baseline (interaction
profiles, perf settings, refresh rate). No GLES space-warp game was available: the GL path is covered by the host
harness (`tests/native/space_warp_harness.c`) only. OutRun itself is unverified.
**Space-warp games that flicker (ITR2, Metro Awakening, Pinball FX, The Boys VR, Lawn Mowing Simulator):** keep
`hide_space_warp`. With the emulation the runtime never sees space warp either (same picture as hidden), but the game
renders motion vectors into images nobody reads; it doesn't make space warp work (no frame synthesis). It is only for
games that refuse to start without the extension.
## Per-game headset identity (#373, 2026-10-17)
Owner decision: all Quest / OVRPort conversions default to Quest 3; Customize → Headset model offers Quest 3,
@@ -842,3 +963,474 @@ SteamVR's own Touch remap (`/opt/steamvr/drivers/frame_controller/resources/inpu
layout `oculus_touch`) is the diagram's "SteamVR default": X -> d-pad down, Y -> d-pad left/right/up, A -> A,
B -> B/X/Y, Menu -> View, plus trigger click -> bumper. Games with their own Valve binding file in that folder
(Beat Saber, Batman, Accounting+, In Death, ...) use that file instead.
## Fixed layer resolution / Batman (#406, #394)
Batman: Arkham Shadow (`com.camouflaj.manta`, Unity + OVRPlugin, OpenXR/Vulkan) submits
`2768x2768`, then corner-anchored `rect=0,32 2736x2736`, `rect=0,64 2704x2704` inside the
same swapchain. On SteamOS 0.5.6 Valve's FDM injection covers the full swapchain: shrinking
viewports can move the sharp centre towards the top right and expose a coarse white right edge.
The runtime recommendation driving OVRPlugin's dynamic resolution remains unverified.
`adapter.fixed_resolution=1` (default off, Batman recipe on) overrides
`xrGetRecommendedLayerResolutionMETA`: tracked full swapchain dimensions, `isValid=XR_TRUE`.
The existing rect_clamp size table already records successful xrCreateSwapchain calls and
retires entries at destruction. Projection views must have matching sizes; quads are supported;
unknown swapchains, mixed sizes and other layer types retain the runtime behavior. Submitted
rectangles are never enlarged after rendering. Full resolution may increase GPU load.
The cached OVRPort 3.4.3-aa54c3f dispatcher contains both the extension and function name
(and a runtime-missing diagnostic). Its fixed routing table is not a guarantee that every
version forwards the call unchanged. Enable the existing arm64 xrshim for this setting:
OVRPlugin's loader lookup reaches `framebridge_extension_proc` directly, while native OpenXR
loaders use the adapter's xrGetInstanceProcAddr hook. No extension hiding: keeping the
recommendation API available avoids relying on an unverified OVRPlugin fallback. The adapter
revision stays unchanged because the new setting is opt-in; the catalog requires 1.2.2 so older
remote clients keep their bundled recipe (`recommend.catalog.unusable_reason`). Rebuild and
reinstall Batman to receive the binary and shim routing; this is not a settings-only fix for
an old APK. Device/headset verification is pending.
Headless check: expect `extension shim: xrGetRecommendedLayerResolutionMETA -> FrameBridge`
for OVRPlugin, then up to five `fixed_resolution: recommended WxH -> 2768x2768 (runtime=...)`
lines. Compare the size to the actual eye swapchain creation log; scaling can change it.
`projection view 0 rect change: ... rect=0,0 2768x2768` (and view 1) should remain full size
through gameplay/loading transitions. Rect changes continue to log at most once per second
per eye, comparing against the last logged state so a throttled final change is still shown.
No recommendation logs means this API path was not exercised: do not claim the fix worked.
Headset check: sharp region centred in both eyes, no white right edge, correct menus/loading
fades, and acceptable pacing with full-resolution GPU load.
This is separate from #303's `foveation_storage_fix` (clearing SUBSAMPLED allocation flags;
see PLAYBOOK and FRAME_RUNTIME), which fixes black/coarse storage corruption. It also leaves
#394's `FOVE_LEVEL=lo/med/hi` presets intact (commits 01ad5e6b, d7f0d514): those select the
full-swapchain density pattern, not the game's dynamic viewport.
## Embedded Khronos OpenXR loaders (GitHub #254)
PrimedGun v2 (`org.primedgun.v2`, 2.0.0-alpha.4) embeds the Khronos loader in
`libmetroid_prime_port.so` (no loader DT_NEEDED). Its diagnostics show both Android
broker queries failing, then `/vendor/etc/openxr/1/active_runtime.json`, runtime
`SteamVR/OpenXR`, and no FrameBridge messages. The supplied `openxr_android.cpp`
initializes the loader with the VM and SDL activity. This is Vulkan OpenXR; retain
the catalog's removal of `frame.vk_sanitize` (the game assembles the Vulkan soname).
`frame.static_openxr_loader` is general: analysis v23 detects arm64 game libraries
with Khronos manifest strings and negotiation/manifest markers. Complete manifest
paths (including ABI variants) are replaced in `.rodata`, NUL padded, with
`/sdcard/fp_openxr_runtime.json`. Khronos also builds paths from fragments: the
verified `platform_utils.hpp` layout uses five partition prefixes, `/etc/openxr/`,
the major API version and `/active_runtime[.aarch64].json`. For this complete table,
prefixes become `/sd` and the subdir becomes `card/fp/`; all candidates then resolve
to `/sdcard/fp/1/active_runtime[.aarch64].json`. The undecorated manifest is written;
a missing ABI-specific file falls back to it. No offsets or sizes change. **Pooled
partition strings can also be used by game code**; this cannot be ruled out from
synthetic tests. Only the complete known fragment table is accepted. Other layouts
need analysis/rewrite support before they can be patched.
Both broker authorities become `org.fp_null.openxr.*`, causing provider lookup to
fail and the loader to fall back to files even if a broker is later installed.
Only the game's copy is changed. Agent v108 writes both manifest spellings into
`<base>/lepton-data/external` (the game's private `/sdcard`), records
`static_openxr_loader` in deployment.json, and repairs them before every launch.
Launcher upgrades preserve that repair. A configuration-only finalize also writes
them. The payload is `{"file_format_version":"1.0.0","runtime":{"library_path":"libfp_xrruntime.so","name":"FramePort"}}`.
The shim exports negotiation and initialization, loads FrameBridge by soname,
forwards Android loader initialization through its GIPA, and returns that GIPA to
the outer loader. FrameBridge then loads `libopenxr_loader_original.so`, an
unmodified Khronos loader reused from OVRPort's downloaded generic runtime.
This works for recipes with `overport: False`: the patch supplies the original
loader before `frame.adapter` installs FrameBridge, its settings payload and
foveation support. Settings still use `settings.conf` through the normal launcher.
No Meta dispatcher is needed. The loader's unmodified system paths keep it from
recursing through the game manifest. Android linker namespaces, symbol binding
between the two loaders, runtime handles across the chain, and FrameBridge's
behavior without the OVRPort dispatcher still require a device check.
Source verification: Khronos OpenXR-SDK-Source
[`release-1.0.34/src/common/platform_utils.hpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/common/platform_utils.hpp)
and the same fragment layout in release-1.1.53;
[`manifest_file.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/manifest_file.cpp)
leaves a bare library filename unchanged for dlopen and tries brokers before files;
[`runtime_interface.cpp`](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.34/src/loader/runtime_interface.cpp)
forwards loader initialization to the runtime export before negotiation (or through
GIPA afterwards). The reporter's diagnostic ELF summary contains no embedded
loader version or raw string table; the exact game's loader revision and binary
rewrite remain unverified. Host tests cover complete and assembled synthetic ELF
layouts, idempotence, unchanged other bytes, APK apply/validate with no OVRPort,
negotiation rejection, Android init forwarding and missing-chain failure. No Frame
connection or device test was performed.
**Crash at Play (GitHub #254, 2026-10-23):** the reporter's 1.3.0 build crashed right after `xrCreateInstance`
(stack overflow in SDLThread: FrameBridge GIPA → `libopenxr_loader_original.so` → the game's library → FrameBridge
…). Lepton mounts Valve's implicit layer manifest at `/vendor/etc/openxr/1/api_layers/implicit.d/`, and the log shows
`XR_APILAYER_VALVE_fdm_injection` loaded by BOTH loaders: one shared layer library stacked into two chains, so the
inner chain's next call landed in the game loader's code. The rewrite now also turns the game loader's implicit-layer
suffix `/api_layers/implicit.d` into `/api_layers/fp_none.d` (Khronos builds `<partition>/etc` + `openxr/1` + suffix),
so only FrameBridge's inner loader loads implicit layers, as in every other FramePort game (patch revision 2). Not
device-verified: headless, PrimedGun stays in its own launcher without the game's disc image, so the catalog keeps
the patch off until a headset check.
Headless acceptance after rebuilding and installing with this development build:
`uv run frameport test org.primedgun.v2`. Expect `fp_xrruntime: chain resolved:
FrameBridge`, `fp_xrruntime: xrInitializeLoaderKHR forwarded to FrameBridge: 0`,
`fp_xrruntime: negotiated runtime interface 1: FrameBridge`, then FrameBridge's
`scale=... loader=...libopenxr_loader_original.so`, `xrCreateInstance` and pacing /
`new layer:` lines. The outer loader should log the private sdcard manifest, the
inner loader the real vendor manifest. Require RUNNING and submitted frames;
headset rendering and controls need separate confirmation.
## Foveation: sharp area centre and size (#394)
Reports: Valve foveation's sharp area sat towards the bottom right in both eyes (In Death: Unchained, The 7th Guest
VR), top right in Batman (dynamic resolution), and its size changed with FramePort's render resolution (RE4,
Asgard's Wrath 2: Large at 1x bigger than at 1.5x/2x; Small "different sizes and shapes").
**Mechanism** (Valve's `libVkLayer_VALVE_fdm_injection.so` disassembled 2026-10-24, Mesa Turnip source, AC Nexus on the
dev Frame): one library is both an implicit OpenXR API layer and a Vulkan layer. Its `xrCreateSwapchain` makes a
density map per eye swapchain (`detected FDM image 63x54[2]` for 2016x1728 array 2: `ceil(w/32) x ceil(h/32)`, one layer
per eye) and fills the `FOVE_LEVEL` preset rectangles in map-normalized coordinates, so every preset is centred at
about (0.51, 0.51) of the **whole swapchain**. Its `xrEndFrame` stores each view's imageRect, but nothing uses it for
the map. Eye-tracked offsets (`calc_offset`) are `foveationCenter NDC x 0.5 x render area`, also from the swapchain
centre; Fixed (`disable_offsets`) passes none and creates images without the density-map-offset flag. Turnip then
picks one density per GMEM **bin**, from the map texel under the bin's centre (`tu_calc_frag_area`); bins have a fixed
pixel size (GMEM / bytes per pixel), roughly 250-300 px for AC Nexus.
**Root causes.** Off-centre: games often submit a rect that isn't the swapchain (AC Nexus rect=0,0 / 287,0 1726x1728
inside 2016x1728 for its asymmetric eyes, then dynamic resolution rect=0,259 1467x1469; Asgard's Wrath 2 0,0 / 347,0
2237x2256 inside 2592x2256), and the eye's optical axis is off the rect centre anyway (FOV left/right -1.03/+0.88 rad,
up/down +0.86/-1.05). The swapchain-centred map then sits ~14% of the view width to one side (and towards the bottom
for corner-anchored dynamic-resolution rects). Size: the sharp area is the set of bins whose centre lies in Valve's
rectangle, so it snaps to whole bins: at 1x the bins are large relative to the eye, at 1.5x/2x smaller. At 1x Small
(hi) and Large (lo) gave the **same** 572x444 px area; at 1.5x they differ (764x444 vs 764x892, also in shape). The
rectangle is also a fraction of the swapchain, not of a smaller dynamic-resolution view.
**Fix (centre):** `VK_LAYER_FRAMEPORT_fdm_centre` (`native/foveation/centre.h`, a second layer in
`libVkLayer_fdm_darken.so`; its constructor inserts it right after Valve's layer in GraphicsEnv's debug-layer list when
`FDM_DEBUG` has `enable`, never for The Climb 2). Below Valve it sees the passes with Valve's map attached (render pass
+ framebuffer, or dynamic rendering) and the offsets at their end, and replaces each eye layer's offset so that Valve's
sharp centre (computed with Turnip's texel size and the preset) lands on the eye's optical centre (Fixed, or
Eye-tracked without a gaze: all-zero offsets) or on the gaze re-based on the eye's rect (Eye-tracked). It never changes
images: for Fixed it drops `disable_offsets` from `FDM_DEBUG` in the process (only once it is in the layer chain, and
before Valve reads the option), so Valve runs exactly as in Eye-tracked (flags its own images, passes offsets) and the
layer replaces Valve's gaze offsets with the optical centre. If Valve read the option first, its maps carry no offset
flag and every pass is left alone (Valve's own Fixed). It writes every offset Turnip reads: Turnip takes one per view
(or per-layer map layer) whatever `fragmentDensityOffsetCount` says, and one-eye passes get their eye's map layer
(the map view's base layer picks the eye). FrameBridge
(`adapter/fdm_views.c`) reports per view: swapchain size, array layer, imageRect and optical centre (GLES: rows from
the bottom, Zink's memory order), through a weak `fp_fdm_set_views`. Double-wide single-layer eyes and passes of other
sizes are left alone. `FRAMEPORT_FDM_CENTRE=0` (as `LEPTON_ENV_`) turns it off. Logs: `FrameBridge: fdm views: …`,
`FoveationDarken: fdm_centre: pass 2016x1728 map 63x54 layer 0+2 (preset 1, Valve centre 1024,880, Fixed): 2
view(s), 2 offset(s) from Valve 0,0 -> -24,-184 (+ more views)`. Not changed: the bin quantization of the size (driver design) and the preset being a fraction of
the swapchain. Ships in the APK (FrameBridge + layer library): a game needs a rebuild (Update on Frame) to get it.
**Headless evidence, first version** (AC Nexus, Fixed, darkening test, `snapshot=2`; its white start flash shows full density white,
coarse grey): FrameBridge now also reads Vulkan eye images back (`snapshot_vk.c`: both eyes, half size, the whole
swapchain layer, rect + FOV in the log). Sharp-area centre minus the left eye's optical centre, before -> after:
| scale | size | swapchain / left-eye rect | before | after | sharp area (of the rect) |
|---|---|---|---|---|---|
| 1.0 | Large (lo) | 2016x1728 / 0,259 1467x1469 | +203,+43 px (+14%,+3%) | +26,+11 (+2%,+1%) | 572x444 (39%x30%) |
| 1.0 | Small (hi) | same | +202,+43 | +26,+11 | 572x444 (same as Large) |
| 1.5 | Large (lo) | 2976x2592 / 0,389 2166x2203 | +276,+65 (+13%,+3%) | +12,+17 (+1%,+1%) | 764x892 (35%x40%) |
| 1.5 | Small (hi) | same | +276,+65 | +13,+17 | 764x444 (35%x20%) |
| 2.0 | Large (lo) | 4032x3456 / 0,519 2933x2937 | +407,+87 (+14%,+3%) | +47,+31 (+2%,+1%) | 1148x892 (39%x30%) |
| 2.0 | Small (hi) | same | (no flash frame caught) | -49,-81 (-2%,-3%) | 956x668 (33%x23%) |
Right eyes match (mirrored rects 244,259 / 360,389 / 488,519). The residual 1-2% is the bin grid. Not checked: GLES/Zink
games (the dynamic-rendering path is implemented, the GL row order is unverified), Eye-tracked with a real gaze
(headless has none), and the picture in the headset. Twice AC Nexus died in a freedreno thread right after a Vulkan
snapshot of its loading frame (not without snapshots): keep `snapshot` for diagnostics.
**Headset round 1 failed (2026-10-24), second version.** The first version (b2be40af, reverted) crashed AC Nexus in
Fixed (SIGSEGV in Turnip's `CmdBeginRendering` called from Valve's `fdm_injection_CmdBeginRendering`, ~33 s) and showed
black in Eye-tracked. Causes, confirmed headless:
- Eye-tracked: Valve declares `fragmentDensityOffsetCount` 1 for a two-view (multiview) pass but computes both eyes'
offsets; Turnip reads one offset per view whatever the count says (`tu_fdm_num_layers`). The first version handed
down an array of `count` entries, so Turnip read the right eye's offset from uninitialised stack memory. Now every
offset Turnip reads is written: the view count comes from the render pass / rendering view mask (else framebuffer
layers), each eye's gaze from Valve's array (the same memory Turnip reads with Valve alone), one-eye passes take the
eye from the map view's base layer.
- Fixed: the first version flagged every 2D render target with the density-map-offset flag, including
external-memory images Valve deliberately leaves alone (`fdm_injection_CreateImage` skips images with
`VkExternalMemoryImageCreateInfo`: Zink/AHardwareBuffer imports such as surface_emul's video). The layer no longer
touches images. Valve's own Fixed path (`disable_offsets`) also crashed AC Nexus without this layer (1 of 2 headless
130 s runs, same `fdm_injection_CmdBeginRendering` -> freedreno stack), so Fixed now runs Valve's offset mode (the
Eye-tracked path, 0 crashes in 6 runs) with the layer replacing the gaze offsets by the optical centre.
Headless, 130 s each, AC Nexus 1x, Large, darkening test, `snapshot=15` (start-flash centre vs optical centre):
| run | result | left eye | right eye |
|---|---|---|---|
| foveation not enabled (`FDM_DEBUG` empty) | runs | whole view sharp | whole view sharp |
| Valve only, Fixed (1) | **crash** after ~20 s, frames stopped | - | - |
| Valve only, Fixed (2) | runs | +14%, +3% | +14%, +2% |
| Valve only, Eye-tracked (headless default gaze) | runs | +25%, -9% | +2%, -8% |
| centring, Fixed (2 runs) | runs, 72 fps | +2%, +1% | +2%, +1% |
| centring, Eye-tracked | runs, 72 fps | +2%, +1% | +2%, +1% |
Later snapshots (dark scene) stay non-black in every surviving run (mean ~20/255 with the darkening test, ~39 without
it): the darkening test dims everything outside the sharp area to 20%, which in AC Nexus' dark start area looks almost
black, so headset checks of the picture should be done with the test off. Headless gaze isn't zero (Valve's
straight-ahead gaze lands on the optical centre, which confirms the NDC re-basing). Not done: Khronos validation, a
GLES game, a real gaze.
## Typing window takes the controllers: SteamVR theater mode (#476, #477, AC Nexus)
**2026-10-24 (SteamOS 0.5.6, gamescope 2.18.2, Lepton 3.0.5).** With `device.text_input_window` (Lepton's
`lepton-show-flatscreen`) some VR games render but ignore the controllers; the owner saw "the Android screen with a
mouse pointer" in AC Nexus (catalog dropped the window, 94783f56), users in PadelVRTraining (#476) and AltspaceVR
(#477); UNDERDOGS (#474) and many others were reported fine with it.
How the window reaches the headset: Lepton then sets `waydroid.wayland_display=wayland-0`, `lepton.headless=false`
(`liblepton/properties.sh`), so Waydroid's window is a client of gamescope. gamescope runs with `--backend openvr
--vr-overlay-show-immediately --virtual-connector-strategy PerAppId --vr-app-overlay-key valve.steam.desktopgame`:
each Steam app's windows become a SteamVR dashboard overlay `valve.steam.desktopgame.<appid>` ("Gamescope", input
method mouse) plus a cursor overlay `gamescope.gamescope-0.cursor.<appid>`. SteamVR's dashboard
(`/opt/steamvr/resources/webinterface/dashboard/chunk~*.js`) does, when that game's frame comes alive:
`!bVRSceneApp && /settings/dashboard/autoShowGameTheater` → `docking.SetInTheater()` + HideDashboard("GameFrame now
alive, auto-theatering"). The Android window always exists before the OpenXR session (Unity: 10-30 s), so the game is
never a scene app at that moment: the Android screen goes in front of the player as a theater screen, the
controllers become a laser mouse (Steam Input switches to app 13830428 at that moment). When the session starts,
SteamVR docks the frame back into the dashboard and opens the dashboard on it (`[ShowDashboardOverlay]
{"overlayKey":"valve.steam.desktopgame.<appid>","sReason":"SetDockLocation"} bOpenDashboard true`): the Android
screen with a pointer again, the session stays VISIBLE (no input) until something closes the dashboard. Steam
sometimes closes it itself ~0.3 s later, FramePort's dashboard worker only after the first VR frame (AC Nexus loads
for seconds after the session starts) and only when `IsDashboardVisible` says so; in one of the owner's three AC
Nexus sessions with the window (2026-10-23 17:23) it stayed open on the Android screen until the owner pressed the
button. The timeline in `vrwebhelper_systemui.txt` was the same for every window session of AC Nexus (16:40, 17:01,
17:23) and for UNDERDOGS headless: overlay created → auto-theatering → session → SetDockLocation; without the
window (17:48 onwards) none of it. So the race between the game's session/first frame and these dashboard steps
decides per game and per start whether the player is left on the Android screen.
Fix (agent 119): the launcher (new line `_theater_hold <pkg> $$`, added to existing launchers by `upgrade_launchers`; log `<base>/theater.log`) and the dashboard worker turn `dashboard.autoShowGameTheater` off through
IVRSettings (ctypes, `SteamVRSettings.bool_setting`) while a game with `device.text_input_window` runs (deployment
recipe has the patch, the marker exists, display mode not "Flat window"), one hold file per launcher pid in
`~/.local/share/frameport/theater-holds/`, and turns it on again after the last such game (a worker killed with its game, e.g. Steam stopped: the next
game's worker or `ensure_host_fixes` puts it back; purge too). Headless with UNDERDOGS: no auto-theatering, no SetDockLocation, the
window overlay never visible, Android's focused window still UnityPlayerActivity. A hold taken only by the worker (with Lepton's start, ~3 s before the window came alive) was too late: the dashboard still auto-theatered (15:01:28), so the hold is taken first thing in the launcher. End-to-end with the agent installed (15:05): theater.log `theater mode off while the game runs` at the launch, no auto-theatering/SetDockLocation, dashboard.log `theater mode back on` at the end; launcher + container killed with -9: the hold stayed, the next `info` (ensure_host_fixes) answered `SteamVR theater mode back on`; SteamVR drops the key from
steamvr.vrsettings again when it's back at the default. 2D apps keep theater mode (it is how they are shown).
Also: SteamOS 0.5.6 logs the dashboard button as `open_dashboard_action`, not `toggle_dashboard_action`; the worker
didn't recognise it and closed each of the owner's presses again (AC Nexus 23:33, ten presses in eight seconds).
Facts for later: `vrcmd --overlays` lists every overlay with visible/not_visible (works headless);
`vrcmd --settings-bool dashboard.autoShowGameTheater` / `--set-settings-bool <section.key> 0|1` (a "true" argument
did nothing); `vrcmd --dock-overlay dashboard|theater|world <overlay key>`. Headless sessions never reach FOCUSED,
so whether the controllers reach the game, and whether Steam's keyboard still opens for a text field with the
window kept in the dashboard, needs the headset.
## Graceful stop: the game is paused before it ends (GitHub #480, agent v120)
Just Dance VR lost all progress on the Frame: its log shows `OfflineSave |WARN |OfflineSaveDico_FromString - NeedLoad:
False` at every start and no save write ever, and its session logs end with the game still running (no
`[OVRManager] OnApplicationPause(true)`). On a Quest an app is always paused before it closes; on the Frame nothing
paused it: launch.sh's `stop()` ran `podman kill`, and Lepton's own teardown (it traps TERM/INT) runs
`podman exec … /system/bin/reboot -p` + `podman stop -t 0` (Lepton 3.0.5, `liblepton.sh kill_container`). `reboot -p`
goes through init, not the activity manager, so no `onPause`/`onStop` either. Games that save in
`OnApplicationPause`/`onPause` (and Unity's own PlayerPrefs flush on pause) never saved.
What works in Lepton's Android 11 (checked 2026-10-24 on the dev Frame, AC Nexus = Unity, headless):
- `input keyevent 3` (HOME) does **nothing** (headless: no focused display); the activity stays RESUMED.
- `am start -a android.intent.action.MAIN -c android.intent.category.HOME` starts Lepton's Launcher3: the game's
activity is PAUSED at once and STOPPED ~0.3 s later; Unity logs `[OVRManager] OnApplicationPause(true)` ~20 ms
after the START and `OnApplicationFocus(false)` right after. Lepton's `LeptonProcessObserver` logs the foreground
change but doesn't react to it.
- `dumpsys activity activities` (~10-30 ms) lists each record as `* Hist #N: ActivityRecord{… <pkg>/<activity> tN}`
followed a few lines later by `state=RESUMED|PAUSED|STOPPING|STOPPED …`.
FramePort (`PAUSE_SCRIPT`, run in the container with `podman exec … sh -c`): if the game's activity is in front, start
the HOME intent, poll its state every 0.1 s until STOPPED (or gone), wait 0.4 s more, give up after 3 s in total. A
game busy loading can't pause sooner (Unity's main thread doesn't answer, the activity sits in STOPPING: `still
state=STOPPING`). A game that isn't in front (it ended itself, or was already sent home) is left alone.
- launch.sh: `pause_game` runs first in `stop()` (before Lepton gets its TERM and before `podman kill`, so before
Lepton's teardown), only while Lepton still runs (a game that ended or crashed adds no delay); output with times in
`<base>/stop-pause.log`. The play log + `_snapshot_close` still follow `podman kill` under the launch lock, so
automatic save snapshots now include what the game saved on pause. `upgrade_launchers` adds it to existing Lepton
launchers (foveation exports and other lines untouched).
- agent `stop` pauses before it stops the launch-test unit (systemd's stop TERMs every process in the unit, Lepton
included, at the same moment) and kills the container; result field `paused`. The Monitor's End game pauses before
Steam's Exit game, whatever Steam then signals.
- Not covered: stopping Steam / restarting the Frame (systemd ends launch.sh and Lepton together and Lepton's
teardown races the pause) and launch tests (their own `systemctl stop` also ends the pause's processes).
**Steam's own Exit game** (the Steam menu's Exit game = `SteamClient.Apps.TerminateApp`; covered since agent v121, see
"Steam's Exit game hook" below). Steam's
`reaper` is only `prctl(PR_SET_CHILD_SUBREAPER)` + fork/exec + wait (no kill logic); Steam itself signals every
process in the game's process tree at once. podman's `conmon` double-forks and is re-parented to the reaper, so the
container's processes (Android init, system_server, the game: `game ← main(zygote) ← init ← conmon ← reaper ←
steam`) are part of that tree, although their cgroup is podman's `libpod-<id>.scope`, not Steam's
`app-steam-app<appid>-<pid>.scope`. Measured: TerminateApp with no pre-pause → launch.sh's `pause_game` starts 0.2 s
later and finds `Can't find service: activity` (system_server already gone), container gone after 0.65-0.8 s; the same
after moving Lepton, `podman run` and conmon out of Steam's scope into another scope (so Steam kills by process tree,
not by cgroup). Lepton itself never logged a TERM (`Exited!` only): `podman run`'s signal proxy killed the container
(journal: `container kill` from Lepton's `podman run`). The other idea, starting Lepton outside Steam's process tree (a `systemd-run --user` service), wasn't taken: it
needs a headset check that SteamVR still attributes the game to its shortcut (bindings, refresh rate, overlay).
FrameBridge can't help: Steam's TERM reaches system_server too.
Timings on the dev Frame (AC Nexus, Unity, headless, 2026-10-24):
| path | OnApplicationPause(true) after the stop request | stop request → container gone |
|---|---|---|
| launch.sh TERM (parent watchdog, Steam stopping only the launcher) | 0.11 s | 1.36 s (pause done after 0.97 s; Lepton's teardown alone ~0.4 s) |
| agent `stop` | 0.31 s (agent start + `podman ps` + exec) | 1.59 s (`stop` returns after 1.47 s) |
| Monitor End game (`monitor_end_game`: pause, then Steam's Exit game) | 0.23 s | 2.06 s |
| a game that doesn't answer (4 of 9 runs: once during its shader warm-up, three times with Unity's main thread already silent before the stop; one Steam run of the same setup had a render-thread crash in Valve's FDM layer with Eye-tracked foveation) | none | capped: pause gives up after 3 s (`still state=STOPPING`), agent `stop` 4.2-4.3 s |
| Steam's own Exit game, before agent v121 | none (see above) | 0.65-0.8 s |
| Steam's own Exit game with the hook (agent v121, Steam launch, `TerminateApp` called in SharedJSContext) | 0.18 s / 0.29 s | 1.50 s / 1.72 s (worker's pause 0.86-0.89 s, `state=STOPPED after 2 polls`) |
| ... a game that doesn't answer (1 of 3 runs, Unity silent before the stop) | none | 3.24 s (pause gave up after 2.56 s, `still state=STOPPING`) |
### Steam's Exit game hook (agent v121)
Every Exit game path in Steam's UI is `SteamClient.Apps.TerminateApp(gameid, false)` in `sp.js`, run in
SharedJSContext: the in-headset frame menu (`valve.steam.gamepadui.frame.menu`, a popup without scripts of its own; it
has its own `SteamClient` object, which Steam's code doesn't use), the exit dialog (`#GameAction_ConfirmExitGameTitle`;
SteamVR itself → `OpenVR.QuitAllVR`), "exit the running game and start this one", CANCEL while waiting for a VR game
window, `ExitRunningGames`. `SteamClient.Apps.TerminateApp` is a writable, configurable property there. SteamVR's own
dashboard (vrwebhelper, `/opt/steamvr/resources/webinterface/dashboard`) has `VRHTML.QuitSceneApp()` ("Exit %1$s")
and a `RequestAppQuit` that goes to Steam's quit dialog; on the Frame the menus are Steam's gamepadui overlays.
Design: launch.sh (Lepton launchers; `upgrade_launchers` adds the line, foveation exports and other lines untouched)
starts `_exit_hook <pkg> "$SteamAppId" $$` (log `<base>/exit-hook.log`, opt out `FRAMEPORT_NO_EXIT_HOOK=1`). The
worker holds one CEF devtools connection (`SteamDevtools`, stdlib websocket), adds a CDP binding of its own
(`Runtime.addBinding fpExitPause<pid>`) and evaluates `EXIT_HOOK_JS` every 2 s: the first run wraps TerminateApp
(`window.__fpExit` = {orig, hook, games, pending, log}), later runs only renew the game's registration (its ids:
`steam_gameid` of the shortcut appid, the devkit entry's, `SteamGameId`; lease 10 s). A reloaded UI or restarted
Steam lost the hook: the next refresh (or reconnect, every 3 s) puts it back. The hook, for a registered id whose
binding exists and whose lease is fresh, calls the binding and holds Steam's call; the worker answers at once (ack),
runs `pause_app` (≤2.7 s) and then lets the hook call Steam's TerminateApp with the original arguments. Never longer
than 3 s (cap), 0.8 s without an ack (worker gone or hung: the binding function outlives its connection, calls then go
nowhere), not at all for other ids (Steam games, other shortcuts: straight through, `__fpExit.log` notes `passed`),
a second press while pending is dropped (one exit follows). If another script wrapped TerminateApp after the hook, the
hook isn't applied again (no loop). The worker removes its registration when its launcher ends. FramePort's own End
game (Monitor) pauses first and then calls `__fpExit.unhooked` (past the hook: no second pause). launch.sh's own
`pause_game` still runs when Steam's TERM arrives; the game is in the background by then (`not in front`).
Checked on the dev Frame (2026-10-24, AC Nexus launched through Steam, headless): `TerminateApp` for the game's id from
SharedJSContext → `exit-hook.log` "Exit game 11660673876089634816: pausing the game first" → logcat `[OVRManager]
OnApplicationPause(true)` 0.18-0.29 s after the call → "paused (state=STOPPED after 2 polls) in 0.86 s; Steam ends it
now" → container gone 1.5-1.7 s after the call (0.65-0.8 s before: ~0.9 s added). Ids of other games (`730`-style,
a made-up one) passed straight through. The End game path went past the hook (no hook entry). Not checked in the
headset: pressing Exit game there (same JS call per Steam's code). Not covered: SteamVR's `QuitSceneApp`, Steam
restarts / power-off.
Diagnostics (agent v122, #480): `collect_diag` returns the last launch's `stop-pause.log`, `exit-hook.log`,
`theater.log`, `dashboard.log` and the anchor's `plays.log`, so a user's zip shows whether the pause ran and how the
game was closed. Open question from #480 (dev build with the pause, progress still lost): whether the pause ran in the
user's session at all, and whether Just Dance VR saves on pause. Its log shows `NetworkApplication.OnApplicationPause`
throwing a NullReferenceException at every pause while it's offline (`ChatApp.SetStatus`, "not connected to front end
server"), so a save hooked to that handler would never run; a Quest's own quit also goes through
XR_SESSION_STATE_EXITING → OVRManager `Application.Quit()` → `OnApplicationQuit`, which FramePort doesn't emulate yet.
### Just Dance VR never writes its local save (`frame.justdance_local_save`)
With the pause in place, Just Dance VR (0.5.4: Unity 2022.3.15f1, IL2CPP) still lost everything. Found statically with
Cpp2IL (diffable-cs + ISIL) and capstone on libil2cpp.so (0.5.4 SHA-256 ff91e080…; file offsets, VA = offset + 0x1000):
- Every system saves through `Neon.Gameplay.LocalSave.LocalSaveEvents.RequestSave(string key, object value)` (14
callers: PlayerProgression.SavePlayerProgression, InventorySaveManager/AvatarSaveManager/GoalsSaveManager.Save,
AchievementSystem, FTUEManager, EmotesAndToysCustomisationManager, SongOfTheWeekManager, PlayerDataTracker,
SocialLocalSavesStore) and loads through `RequestHasSaveData`/`RequestLoad` (PlayerProgression.OnSaveDataReady ->
OnLocalDataLoaded -> RemapProgression, then SyncData with PlayFab). **RequestSave (0x57f99e8) is a single `ret`.**
The only path to `JdOfflineSave.OfflineSave_SetValue` is `LocalSaveManager.OnSaveValueRequested` (subscribed to
`SaveValueRequested`) -> `LocalSaveController.SaveData<T>`, so the OfflineSave dico never gets `NeedSave` and its
write thread (every 1000 ms) and the forced write on focus loss have nothing to write.
- Loading works as written: `RequestLoad` raises `LoadValueRequested` -> `LocalSaveManager.OnLoadValueRequested` ->
`LocalSaveController.LoadData<string>` -> `OfflineSave_GetValue` (refused with error 40/41 only when the file was
marked corrupted) -> the value goes back to the requesting system's callback.
- `OfflineSaveDico_FromString - NeedLoad: False` (a Debug.LogWarning) is printed after every parse that didn't throw,
with a boxed constant `false` after `strh wzr, [dico, #0x48]` (NeedSave = NeedLoad = false): normal, not a
rejection. The version check and `CheckWholeMapHmac` report problems through io_Iret (41 = HMAC mismatch ->
`m_OfflineSaveFileCheckDoneAndFileIsCorrupted`). The HMAC input comes from `GetNativeAccountFields`: two constant
string pairs, no user id. A file is only rewritten when a value changes (NeedSave).
- On a Quest the progress lives in PlayFab: `BackendRequestManager.GetUserPlatformData` (Meta `Users.GetLoggedInUser`)
-> `PlayfabAuthenticate` (`LoginWithCustomID`); `SetPlayerData` -> `PlayFabClientAPI.UpdateUserData`. Without Meta's
platform the game never signs in. FramePort doesn't touch that.
- `PlayerDataTracker.<WaitForAuth>d__41.MoveNext` returns false in its first step (a `yield break`): per-song records
never load either (not changed).
- The patch (per build, nothing hardcoded): Cpp2IL gives RequestSave, SaveSystemReady, the static field offsets of
SaveValueRequested (0x28) / SaveSystemReadyEvent (0) and LocalSaveEventArgs' Key/Value (0/8: a 16-byte struct, so
key and value go in x1/x2). capstone decodes SaveSystemReady: GOT slot of the TypeInfo variable (0.5.4: VA
0x7347c48), the metadata init on it (0x3192e20), static_fields at +0xb8, delegate invoke_impl +0x18 / target +0x40 /
method +0x28. The 24-instruction body goes into a new R+X PT_LOAD (`elf.append_segment`, takes the PT_NOTE header;
0.5.4: VA 0x7a8c000), RequestSave's `ret` becomes `b` there. RequestSave with a body of its own = left alone.
- Device (headless, 2026-10-24, first build of the patch): RUNNING at 72 fps twice; the first launch created
`files/JdNext.OfflineSave/JdOfflineSave_JdLog.OfflineSave.txt` (keys Achievements, OffSaveFormatVersion, UbiHmac),
the second read it. Still to check in the headset: level, unlocks and the finished tutorial after a restart.
## Expansion file names (#450, #481)
The game opens `Android/obb/<package>/main.<versionCode>.<package>.obb` by exact name (Unreal's `OBBData`, Unity's
split binary, CryEngine's versionless `main.<package>.obb`); the Frame's filesystem is case-sensitive. Downloads
sometimes arrive as `main.203.com.Armature.VR4-003.obb` / `patch.203.com.Armature.VR4-001.obb` (#450: VR4 stopped
right after its graphics started). `quest_dump.obb_names` (names only, no disk access) decides per top-level
`main`/`patch` file:
- renamed on the Frame (the PC file keeps its name): a copy suffix after the package (`-NNN`, ` (N)`, ` - Copy`: never
part of a package name) or a different case; the versionCode stays what the file says.
- left alone: exact names, Unreal chunk files (`patch.pakchunk27-Android_ASTC.<pkg>.obb`, #471), split parts
(`main.<pkg>.part0.obb`, Phantom), other data (`Audio_*.obb`, `overflow1.…`, `chunk.N.…`) and files in subfolders.
- warned, sent as they are: another app's name (`main.1716.com.beatgames.beatsaber.lj369vr.obb`, #129), a copy whose
right-named file is there too, two copies for one name, a copy suffix on a versionless name (the number may be the
versionCode).
The installer sends `obb/<Frame name>` and passes the Frame-named manifest as `obb_manifest` (no agent change);
`plan_update` compares `quest_dump.frame_manifest(...)` with the deployment, so a rename doesn't show "Update on Frame"
again. A game installed earlier under the copy name gets its file renamed on the Frame by SFTP (`installer._reuse_renamed`:
moved into `incoming/obb/<new name>`, finalize moves it into place) instead of a second upload. On the Frame itself
(#433) the hardlink keeps the PC's name as its source (`_relink_installed(source=)`). Save backups skip `.obb`
anyway. Loose copy-named OBBs next to an APK are found by name too (`_named_obbs`), the right-named one wins a tie.
Survey of the owner's dumps (2026-10-24, 370 OBBs): no copy suffixes or case differences; the non-standard names were
all the game's own (162 Asgard's Wrath 2 chunks, Phantom's `.partN`, Metro's `chunk.N`, Batman's `Audio_*`, PowerWash's
DLC, Espire 2's `overflowN`). The old #129 check flagged Asgard's Wrath 2's chunks and Phantom's parts as "named for
another app"; they're left alone now.
**Other kinds (#415, 2026-10-24):** Crisis Brigade 2 reloaded ships seven `<kind>.222.com.sumalab.CrisisBrigade2reloaded.obb`
files (office, alleys, challenges, mansion, ta1, ta2, ta3); a user's copies were lower case in the package part, so they
reached the Frame (`NAMED_OBB` scan) under names the game can't open. `obb_names` now also renames any other
`<kind>.<vc>.<package>.obb` whose package part is the game's in another case or with a copy suffix, keeping the kind's
own spelling (an unclear versionless copy only warns, as for main/patch). These kinds never get the "another app"
warning: any `<word>.<n>.<x>.obb` may be unrelated data. `NAMED_OBB` kinds may contain digits now (`ta1`: the scan
skipped them before), and `_named_obbs` takes copy-suffixed files of every kind.
## App bundles (.xapk / .apks / .apkm / .apkx, discussion #482)
**Formats (2026-10-24):** `.xapk` (APKPure: `manifest.json` with `split_apks` [{file, id}] and `expansions`
[{file, install_location, install_path}], numbers sometimes as strings), `.apks` (bundletool: `toc.pb`,
`splits/<module>-master.apk` + `splits/<module>-<targeting>.apk`, `standalones/`, `universal.apk`, plus `instant/`,
`system/`, `archive/` which FramePort skips; SAI: flat `base.apk` + `split_*.apk` + `meta.sai_v1|2.json`), `.apkm`
(APKMirror: `base.apk` + `split_config.*.apk` + `info.json`; plain zips since 2021-01, older ones are libsodium
secretstream files: not `PK`, byte 1 = pwhash algorithm 1/2, see github.com/souramoo/unapkm; FramePort detects and
explains, never decrypts). **`.apkx` isn't a defined format**: no store or tool documents it, FramePort treats it as a
zip of APKs and reads the contents. **FrameDrop takes no bundles** (closed source; framedropvr.com/about: "one .apk
file. Split packages (.apks, .xapk, .aab) need to be merged into a single APK first"; OBBs only from an `obb` folder
next to the APK), so there is no FrameDrop behaviour to match beyond install-link manifests: a manifest `files` entry
may now be a bundle (`deeplink.classify` → APK kind).
**Design (`sources/bundles.py`):** Lepton installs exactly one APK per app folder and OVRPort converts one APK, so
`pm install-multiple` inside the container is out; the parts are merged into one APK at scan time, into
`<data>/bundles/<package>/<key>/<package>.apk` + `obb/<package>/` (key = sha256 of the zip's member names, CRCs and
sizes: a moved/renamed bundle finds the same copy; `bundle.json` records what was used/left out). Library entries keep
`bundle` (the user's file); `check_source` unpacks again when the copy is gone; `library.remove_game` deletes the
copy; "delete files on this PC" takes the bundle + copy. Base = `universal.apk`, else an arm64 standalone, else the
APK without a `split` manifest attribute. Splits: per module one ABI (arm64-v8a; armeabi-v7a only without any 64-bit
code: then the a32 bridge rules apply; x86 never), one texture format (astc > etc2 > …), all density/language/feature
splits. lib/ + assets/-only splits are merged in Python (base entries keep compression and dates, v1 signature files
dropped; manifest: `isSplitRequired`/`requiredSplitTypes`/`splitTypes` attribute records removed by
`axml.remove_attrs`, `com.android.vending.splits.required` = false, else Android 11 refuses the lone base with
INSTALL_FAILED_MISSING_SPLIT and Play Core's missing-split check stops the app). Splits with resources/code go through
REAndroid's APKEditor (Apache-2.0, `tools/apkeditor.py`, newest release ≥ 1.4.9 downloaded into tools/; 1.4.3-1.4.8
corrupted some merges; `m -i <dir of exactly the chosen APKs>`: it merges every split it is given, drops the split
attributes and every `com.android.vending.*`/`com.android.stamp.*` meta-data). Without Java/network a bundle whose
extra splits are only language/density/texture ones falls back to the Python merge (translations/densities lost, base
defaults stay); feature modules then fail with a clear error. The merged APK is unsigned: builds sign it with the
game's key as usual, `prepare_as_is` signs a copy. `analysis.extra.split_apk` now reads the attributes, not the pool
strings (a merged base keeps the strings). A single-APK `.xapk` is extracted unchanged (signature intact).
**Device check (2026-10-24, headless, dev Frame, isolated FRAMEPORT_HOME):** Escaping Wonderland 1.0.1.1354
(`com.CortopiaStudios.Spectre`, Unity, catalog "works", not installed on the Frame) split into a bundletool-style
base (everything but `lib/`) + `config.arm64_v8a` split (the 22 `lib/arm64-v8a` libraries, synthetic split manifest),
packed as `.xapk` and `.apks`. `frameport scan <xapk>` unpacked and merged it (Python path) in ~30 s incl. analysis;
the merged APK has the same 712 entries with identical CRCs/sizes/compression as the original. Built (catalog recipe,
OVRPort + Frame fixes, signed with a fresh test key), installed, launch test: RUNNING, FrameBridge loaded, OpenXR
instance, submitting frames at 72 fps (pass). Uninstalled afterwards (no saves kept, shortcut removed). APKEditor
1.4.9 downloaded and merged the same base + arm64 split in 4 s (split attributes gone, unsigned output); a real
language/density split merge through APKEditor wasn't tried on the device (no such bundle in the dumps).
+28 -24
View File
@@ -1,19 +1,22 @@
# Diagnostics and problem reports
Two ways users feed results back, both without a GitHub token:
FramePort sends results back in two ways, both without a GitHub account token:
- **Share working recipe…** (game menu, `frameport share-recipe <pkg>`): saves the recipe as a user catalog entry and
opens a prefilled issue from `.github/ISSUE_TEMPLATE/working-config.yml`. A maintainer checks it and adds the label
`catalog-accepted`. Then `.github/workflows/catalog-from-issue.yml` runs `scripts/catalog_from_issue.py`, which reads
only the YAML block, validates it, and writes `catalog/games/<pkg>.yaml`, and the workflow opens a PR.
- The PR step needs Settings → Actions → "Allow GitHub Actions to create and approve pull requests".
- Create the labels `working-config`, `catalog-accepted` and `bug` once.
- **Report a problem…** / **Collect logs** (game menu, Settings, the failure pop-up, `frameport diag report|collect`): writes
a redacted zip and opens a prefilled `bug-report.yml` issue.
- GitHub has no API for issue attachments, so the user drags the zip in (25 MB max; the bundle stays under 24 MB).
- Prefilled links are capped at about 7.5k characters. Free text is trimmed first; the recipe is never cut.
- **Report a problem…** / **Collect logs** (game menu → Troubleshoot, Settings, the failure pop-up,
`frameport diag report|collect`) writes a zip of logs with personal data removed ([Redaction](#redaction)) and
opens a prefilled issue (`bug-report.yml`). You drag the zip into the issue: GitHub has no API for attachments.
The zip stays under GitHub's 25 MB limit and holds no game files.
- **Share working recipe…** (game menu → Recipe; `frameport share-recipe <pkg>`) saves the recipe as your own catalog
entry and opens a prefilled issue (`working-config.yml`).
Code: `src/frameport/diag/` (`redact.py`, `bundle.py`, `issue.py`), agent command `collect_diag` (agent ≥ 21),
Prefilled links are capped at about 7.5k characters; free text is trimmed first, the recipe never.
**For maintainers:** labelling a recipe issue `catalog-accepted` runs `.github/workflows/catalog-from-issue.yml`:
`scripts/catalog_from_issue.py` reads only the YAML block, validates it, writes `catalog/games/<pkg>.yaml` and the
workflow opens a PR. The workflow needs Settings → Actions → "Allow GitHub Actions to create and approve pull requests" and the
labels `working-config`, `catalog-accepted` and `bug`.
Code: `src/frameport/diag/` (`redact.py`, `bundle.py`, `issue.py`), agent command `collect_diag`,
`Target.collect_diag`, `core/applog.py` (app log + saved job logs).
## Bundle layout (schema 1)
@@ -41,17 +44,19 @@ games/<pkg>/package/ stand-in for the game files (no content):
games/<pkg>/target/ from the Frame (or the PC): launch.sh, settings.conf, deployment.json, launch.log,
launch-test.log (PC VR), lepton-steamlaunch-<appid>.log, logcat-{main,crash,system,...}.log,
ReviveInjector.txt / steam-<appid>.log / game-log-N.txt (PC VR), files.json (+ missing)
stop-pause.log, exit-hook.log, theater.log, dashboard.log, plays.log: how the last
session ended (pause before the stop, Steam's Exit game hook; play start/end)
(game-log-N.txt: Unreal Saved/Logs + crash summaries, Unity Player.log / Player-prev.log /
output_log.txt + crash error.log from the Proton prefix, agent v67)
threads.txt (agent v79): saved while a Quest game runs (e.g. hangs), each thread's
output_log.txt + crash error.log from the Proton prefix)
threads.txt: saved while a Quest game runs (for example a hang): each thread's
state, CPU ms over 2 s, kernel wait and syscall, plus its TCP sockets (remote = loopback /
local / internet + port, no addresses); read once from /proc, the game isn't paused
games/<pkg>/target/shaders/ shader dumps (agent v72), when the game wrote any:
games/<pkg>/target/shaders/ shader dumps, when the game wrote any:
fp_vk_shaders/ adapter vk_shader_dump=1 (Vulkan shim): <size>_<sha256>.spv + index.txt
fp_spirv/ adapter zink_shader_dump=1 (OpenGL ES, shader-fix layer under Zink): <size>_<sha256>.spv
```
Each log keeps at most its last 4 MB (2 MB per file from the Frame). Shader dumps, SPIR-V unchanged (compiled game
shaders, not redacted): Vulkan (agent v74) every module the game's newest run (the last `# start` in `index.txt`)
shaders, not redacted): Vulkan: every module the game's newest run (the last `# start` in `index.txt`)
created, last used first, each up to 4 MB and up to 30 MB in all, with that run's index lines, so a module created
well before a GPU hang comes along too (if the zip would pass 24 MB, the least recently used ones are left out
after the logs were cut); OpenGL ES (no index): the newest modules (by time written) up to 4 MB.
@@ -60,11 +65,10 @@ 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.
Display queries run only while collecting logs (the Windows event query times out after 5 s and says whether the query
found nothing or failed). `app.log` records window visibility, focus and move/resize events with UTC times, to
compare with Windows events. No display polling, no change to display settings. Empty System events don't rule out
a display problem.
## Redaction
`diag/redact.py` runs on every file and on the issue text.
@@ -77,12 +81,12 @@ a rendering or monitor-link problem.
- **Kept:** generic accounts that identify no one: `steamos`, `steamuser` (Proton), `root`, `deck`.
## Debugging from a bundle (no game, no Frame, no GUI)
1. `frameport diag inspect <zip>` (`--json` for everything). It prints the versions and warnings, the last launch
1. `frameport diag inspect <zip>` (`--json` for everything). The command prints the versions and warnings, the last launch
test, and a fresh triage of the newest launch log with the current `catalog/triage.yaml`.
2. Match the findings and log lines against `docs/PLAYBOOK.md` (symptom → cause → fix) and `docs/FRAME_RUNTIME.md`.
3. Check the analysis in `entry.json` (engine, XR API, `extra`: missing ovr symbols, features, Unreal version) and
`package/elf.json` against the heuristics in CLAUDE.md. Compare `recipe.yaml` with catalog games that use the same
`package/elf.json` against the patches' heuristics (each patch's `detect()`; [HEURISTICS_EVAL.md](HEURISTICS_EVAL.md)). Compare `recipe.yaml` with catalog games that use the same
engine and API.
4. Compare `manifest.json → env.frame.build_id` with the build the docs describe: SteamOS updates change the runtime.
5. A new failure signature means adding a patch module, a `triage.yaml` signature, a PLAYBOOK row and a unit test (see
Conventions). Then `diag inspect` on the same zip should report it.
[CONTRIBUTING.md](../CONTRIBUTING.md)). Then `diag inspect` on the same zip should report the new signature.
+135 -138
View File
@@ -2,8 +2,8 @@
## How should I lay out a game that has OBB files?
Some Quest games come as an APK (the app file) plus `.obb` files (the game's data). Give every game its own folder,
put the APK in it and put the data in a folder named after the game's **package name** (for example `com.Armature.VR4`):
Some Quest games come as an APK (the app) plus `.obb` files (the game's data). Give every game its own folder. Put
the APK in the game's folder, and the data in a folder named after the game's **package name** (for example `com.Armature.VR4`):
```tree
Games/ ← scan this folder (Add games → Scan a folder)
@@ -16,38 +16,35 @@ Games/ ← scan this folder (Add games → Scan
└── com.beatgames.beatsaber.apk ← games without OBBs: just the APK
```
- **Data folder name:** the package name (`com.Armature.VR4` above) or `obb`.
- **Everything in that folder is copied** to the Steam Frame, subfolders included, so other data files work too.
- **Data folder name:** the package name or `obb`. Everything in the data folder is copied to the Frame,
subfolders included.
- **One game per folder.** Several APKs in one folder count as one game.
- **Scanning:** pick the folder that contains the game folders (`Games/` above) to add them all, or one game's folder
to add just that game. FramePort looks up to 5 levels deep.
- **A single game:** **Add games → Add an APK…** also finds the data folder next to the APK.
- **Scanning:** pick the folder with the game folders to add them all, or one game's folder for one game.
FramePort looks up to 5 levels deep. **Add games → Add an APK…** also finds the data folder next to the APK.
Not sure of the package name? It's in the OBB file names (`main.<version>.<package name>.obb`), and the game's page
shows it under **Details**.
The package name is in the OBB file names (`main.<version>.<package name>.obb`) and under **Details** on the game's
page.
## I moved my games to another folder or drive. How do I update the library?
Scan the new folder (**Add games → Scan a folder…**, or **Settings → Library folders → Add folder…**). A game whose
files are gone from their old place and that FramePort finds again in the new folder keeps its library entry: only
where its files are changes. Its name, tags, artwork, your own recipe and what's installed on the Frame stay; you don't
have to remove it from the library first. **Rescan folders** in the Library does the same for every library folder.
Scan the new folder (**Add games → Scan a folder…** or **Settings → Library folders → Add folder…**). Games found
there whose old files are gone keep their library entry: name, tags, artwork, your recipe and the install on the
Frame stay. **Rescan folders** in the Library checks every library folder
the same way.
- **Removing a folder:** **Settings → Library folders** lists every folder FramePort scans, with how many library games
come from it and whether it's there now (a folder on an unplugged drive shows as not found). The bin icon stops
scanning a folder; its games stay in the library. `frameport folders [--add DIR] [--remove DIR]` does the same on the
command line, `frameport rescan` rescans.
- **Another version in the new folder** (a different APK or game build): **Scan a folder…** on that folder takes it, as
**Update with a newer APK…** would (name, tags, art and your own recipe stay). **Rescan folders** leaves such games
alone and says so; use the game's **Update with a newer APK…** / **…version…** to switch.
- **Two copies** (the old folder is still there): the library keeps the copy it has. To use the other one, delete or
rename the old copy and rescan.
- PC VR games are recognised by their folder name, or a renamed folder by its program file. Linux apps aren't part of
folder scans: add the moved app again with **Add a Linux app…** (it keeps its name, art and switches).
- **Remove the old folder** in **Settings → Library folders** (bin icon). The folder's games stay in the library.
- **Another version in the new folder:** **Scan a folder…** takes the new version like **Update with a newer
APK…** would.
**Rescan folders** leaves such games alone; use the game's **Update** menu to switch.
- **Both copies still there:** the library keeps the copy it already has. Delete or rename the old
copy and rescan.
- PC VR games are matched by folder name, or by program file if renamed. Linux apps aren't scanned: add the moved app
again with **Add a Linux app…** (the app keeps its name, art and switches).
- Command line: `frameport folders [--add DIR] [--remove DIR]`, `frameport rescan`.
## Can I scan SideQuest backups or AXRB downloads directly?
Yes. Scan the folder as it is; FramePort finds the APK and its OBB files in both layouts:
Yes. Scan the folder as it is:
```tree
SideQuest Backups/ ← scan this folder
@@ -71,180 +68,180 @@ AXRB/ ← AXRB's download folder (Downloads/AX
└── patched/ ← AXRB's own PC builds: ignored
```
- **Several backups of one game:** FramePort uses the newest one. To use another, scan just that backup's folder.
- **AXRB's patched builds** (`patched/`, `*-axrb.apk`) are converted for AXRB's PC runtime and lack the game's data, so
FramePort skips them and, on the next scan, replaces a game that was added from one with the original download.
- **AXRB export ZIPs:** unpack them first. A game's ZIP holds `base.apk` and `Android/obb/<package>/`, which FramePort
finds as is.
- **"Game data (.obb) is missing" on the game's page** although you have the file: the backup didn't include it (copy
the game's `Android/obb/<package>` folder from the Quest with SideQuest), or it sits in a folder FramePort doesn't
look in. Then use the layout in the previous answer.
- **Several backups of one game:** FramePort uses the newest. To use another, scan just that backup's folder.
- **AXRB's patched builds** (`patched/`, `*-axrb.apk`) are made for AXRB's PC runtime and lack the game's data, so
FramePort skips them.
- **AXRB export ZIPs:** unpack them first.
- **"Game data (.obb) is missing"** although you have the OBB files: the backup didn't include them (copy
`Android/obb/<package>` from the Quest with SideQuest), or the files are somewhere FramePort doesn't look. Use the layout in
the first answer.
- **The game starts and stops at once (Steam shows Resume):** add the game again from its original download or
backup, click **Update on Frame**, then report it (last answer on this page) if it still stops.
backup, click **Update on Frame**, and report the problem if the game still stops.
## Can I add .xapk, .apks, .apkm or .apkx files (app bundles)?
Yes. Add them like an APK: **Add an APK…**, drag them onto the Library, scan a folder, use an install link, or
`frameport scan <file>`.
| File | Made by | Inside |
|---|---|---|
| `.xapk` | APKPure and others | `manifest.json`, the APK (or `base.apk` + `config.*.apk` splits), `Android/obb/<package>/` |
| `.apks` | bundletool, SAI | `splits/base-master.apk` + `splits/base-arm64_v8a.apk` …, or `universal.apk`; SAI: `base.apk` + `split_*.apk` |
| `.apkm` | APKMirror | `base.apk` + `split_config.*.apk`, `info.json` |
| `.apkx` | various | the same kind of zip under another name |
| a folder of split APKs | `adb pull` of `pm path`, unpacked bundles | `base.apk` + `split_config.arm64_v8a.apk` … |
- FramePort merges the parts into one APK (the Frame installs one APK per game), in FramePort's data folder
(`bundles/`). Your file isn't changed. FramePort takes the 64-bit ARM part, else the 32-bit one. Language and screen
parts are merged with [APKEditor](https://github.com/REAndroid/APKEditor), downloaded on first use.
- OBB files in the bundle, or in an `obb/` or `<package>` folder next to the bundle, are sent like a normal game's data.
- The merged APK is signed with the game's own key, like every build. A game that checks its own signature may
refuse to run.
- **"is a protected APKMirror bundle":** older `.apkm` files are encrypted for APKMirror's installer. Download the
app again (current `.apkm` files work), or get an `.apk` or `.xapk`.
- Unpacking needs about twice the bundle's size in free space.
## Where are a game's settings in FramePort?
Every game has three places; the answers below refer to them.
- **Game settings:** right-click the game → **Game settings…**. Render resolution, refresh rate, controllers, menus
and more. Changes apply at the next start.
- **Patches:** open the game's page → **Customize** (next to **What FramePort will do**). Turn a patch on or off,
then click **Update on Frame**.
- **The game's menu:** right-click the game, or **…** on the game's page. Groups: **Update**, **On the Frame**, **Saves**,
**Name and artwork**, **Recipe** (**Reset to suggested recipe…**, **Share working recipe…**) and **Troubleshoot**
(**Report a problem…**).
- **Game settings:** right-click the game in the **Library** → **Game settings…**. Render resolution (supersampling), refresh rate,
controllers, menus and more, in plain words. Changes apply the next time the game starts.
- **Patches:** click the game in the **Library** to open its page, then **Customize** (in the **Patches** section).
Turn a patch on or off, then click **Update on Frame** at the top of the page.
- **The game's menu:** the **…** button at the top of its page (or right-click in the Library → **More actions…**).
It has **Reset to suggested recipe…** and **Report a problem…**, among others.
The sidebar has two tabs for troubleshooting: **Monitor** (the running game's frame rate, CPU and graphics) and
**Files** (each installed game's storage on the Frame, under **Game storage**). Quest games expose their Android
storage; PC VR and Linux games expose their install folders, including installations on another drive.
SD cards, USB drives and other Steam library drives appear under **Drives**, with free space when available.
Read-only drives can be browsed and downloaded from, but cannot be changed.
For troubleshooting, the sidebar has **Monitor** (frame rate, CPU, graphics) and **Files** (each game's files on the
Frame under **Game storage**; microSD cards and other drives under **Drives**).
## A game doesn't work right. Where do I start?
1. **Update FramePort** (it offers updates itself; Settings → Updates), then open the game's page: if it shows
**Update on Frame**, click it. Recipes and patches improve often.
2. **Read the game's page.** The last launch test and your last play session (**Last session**) leave their result
there, often with a suggested patch or setting and a button that applies it. Try that first.
3. **Changed patches yourself?** The game's menu → **Reset to suggested recipe…** brings back the tested ones.
4. **Find your symptom below.** Each answer says how to tell what's wrong and what to do.
5. **Still stuck?** Report it (last answer on this page).
1. **Update FramePort**, then open the game's page. If the page shows **Update on Frame**, click **Update on Frame**.
2. **Read the game's page.** The last launch test and **Last session** often suggest a patch or setting, with a
button that applies the suggestion. Try the suggestion first.
3. **Changed patches yourself?** **Recipe → Reset to suggested recipe…** brings back the tested ones.
4. **Find your symptom below.**
5. **Still stuck?** [Report the problem](#how-do-i-report-a-problem-well).
## The game doesn't start, or stops right away
How to tell: the game's page shows the last launch test's result (**Last launch test:** …) with an explanation, and
Steam returns to **Play** or **Resume** within seconds.
The game's page shows the last launch test's result with an explanation.
- **"Game data (.obb) is missing"** on the game's page: FramePort didn't find the game's data files, so they never
reached the Frame.
1. Find the game's `.obb` files (named like `main.<version>.<package>.obb`).
2. Put them in a folder named exactly the package name (shown in the file names) next to the game's APK, as in the
first answer on this page.
3. Library → **Add games** → **Scan a folder…** and pick the game's folder.
4. Open the game's page and click **Update on Frame**.
- **"Lepton didn't start the game this time"**, or it failed only on the first start after an install: click **Play**
again.
- **"This game is 32-bit only"**: the Frame can't run 32-bit apps, and no patch changes that. Look for a PC VR version
of the game.
- **"The game quit itself right after starting"**: open the game's page and click **Update on Frame** so the suggested
patches apply (many games check for a Quest at start, and a patch removes that).
- **Nothing like that, or it still fails:** report it.
- **"Game data (.obb) is missing":** the data files never reached the Frame.
1. Find the game's `.obb` files (`main.<version>.<package>.obb`).
2. Put them in a folder named exactly the package name, next to the APK ([layout](#how-should-i-lay-out-a-game-that-has-obb-files)).
3. **Add games → Scan a folder…** and pick the game's folder.
4. Click **Update on Frame**.
- **"Lepton didn't start the game this time":** click **Play** again.
- **32-bit-only APK:** the Frame has no 32-bit support. FramePort's 32-bit bridge (experimental) is suggested; if the
game still doesn't run, look for a PC VR version.
- **"The game quit itself right after starting":** click **Update on Frame** so the suggested patches apply.
- **Anything else:** report the problem.
## It hangs on a logo, a black screen or a loading screen
## The game hangs on a logo, a black screen or a loading screen
How to tell what it's doing: leave it running and open **Monitor** in FramePort's sidebar.
Leave the game running and open **Monitor**.
- **CPU or graphics busy:** it's still working, often preparing its shaders on the first start. Wait up to 10 minutes
once; the next start is quicker.
- **CPU and graphics near zero:** it's stuck waiting for something. While it still hangs, open the game's menu →
**Report a problem…** and save the diagnostics: that zip shows what each of the game's threads waits for (FramePort
1.0.1 and newer), which a zip saved after quitting can't. Attach it to the report.
- **The game starts with a video** and hangs at its logo: game page → **Customize** → turn off **Hardware video
decoding** → **Update on Frame**, and try again.
- **CPU or graphics busy:** the game is still working, often preparing shaders on the first start. Wait up to 10 minutes
once; later starts are quicker.
- **CPU and graphics near zero:** the game is stuck. **While the game still hangs**, use **Troubleshoot → Report a problem…**: the
zip then shows what the game waits for.
- **The game starts with a video** and hangs at its logo: **Customize** → turn off **Hardware video decoding** →
**Update on Frame**.
## The picture is wrong
Most picture problems have a setting or a patch. Change one thing at a time, then start the game again (patches also
need **Update on Frame**).
Change one thing at a time, then start the game again (patches also need **Update on Frame**).
| You see | Try |
|---|---|
| Something shows in one eye only, or one eye is gray (Unity games) | Game page → **Customize** → [Unity: render each eye separately](PATCHES.md#frame-unity_multipass) |
| Something shows in one eye only, or one eye is gray (Unity games) | **Customize** → [Unity: render each eye separately](PATCHES.md#frame-unity_multipass) |
| Left and right look swapped (depth feels wrong) | **Game settings…** → **Swap eyes** |
| Textures flicker, objects pop in and out | **Game settings…** → **Turn off space warp** |
| One eye shimmers while menus look steady | Game page → **Customize** → **Eye-tracked foveation (Valve)**: **Fixed**, else **Off** |
| One eye shimmers while menus look steady | **Game settings…** → **Foveated rendering (Valve)**: **Fixed**, else **Off** |
| Menus or screens upside down | **Game settings…** → **Fix upside-down menus** |
| Curved menus or video screens missing | **Game settings…** → **Show curved menus and screens** |
| Blurry | **Game settings…** → raise **Render resolution (supersampling)** (costs frame rate) |
The [patch reference](PATCHES.md) lists every patch with what it does. If nothing helps, report it with a short
description of what you see.
Every patch is in the [patch reference](PATCHES.md). Nothing helps? Report the problem and describe what you see.
## It stutters or runs slowly
## The game stutters or runs slowly
How to tell: open **Monitor** in the sidebar while you play. Smooth play needs the game's frame rate at the refresh
rate (for example 72 or 90 fps).
Open **Monitor** while you play. Smooth play needs the frame rate at the refresh rate (for example 72 or 90 fps).
- **Below the refresh rate:** right-click the game → **Game settings…** → lower **Render resolution (supersampling)** (for example to 0.8×) or
pick a lower **Refresh rate**. After a session, the game's page may suggest a value.
- **At the refresh rate but it still judders:** report it; that points to timing, not speed.
- **Below the refresh rate:** **Game settings…** → lower **Render resolution (supersampling)** (for example to 0.8×) or the
**Refresh rate**. After a session, the game's page may suggest a value.
- **At the refresh rate, but the game still judders:** report the problem.
## The game pauses or recenters by itself
The Frame briefly tells games they lost focus, for example when the headset's sensor thinks you took it off.
Right-click the game → **Game settings…**: keep **Don't pause on short interruptions** on and raise **Ignore focus
dips up to** (for example to 2.5 s).
The Frame briefly tells games they lost focus, for example when the headset's sensor thinks you took the headset off.
**Game settings…**: keep **Don't pause on short interruptions** on and raise **Ignore focus dips up to** (for example
to 2.5 s).
## Controllers don't work in a game
Right-click the game → **Game settings…**:
In **Game settings…**:
- **No controllers, or only hands:** turn on **Use controllers**.
- **Buttons work but you can't aim at menus:** adjust **Pointer angle**.
- **Can't aim at menus:** adjust **Pointer angle**.
- **Vibration too strong:** lower **Vibration strength**.
- **Buttons repeat or do the wrong thing:** **Controller buttons** → **Mirror** or **Quest pair**
([Controller buttons](CONTROLLERS.md)).
## The Frame turns off or gets hot during a game
How to tell: if it only happens on battery and never while plugged in, it's power, not FramePort. Heavy games can
draw more than the battery delivers, especially at lower charge, and the Frame then turns off without an error.
Only on battery, never plugged in? The cause is power, not FramePort: heavy games can draw more than the battery delivers.
- Play plugged in, or right-click the game → **Game settings…** → lower **Render resolution (supersampling)** to reduce the load.
- It also turns off while plugged in: report it. The diagnostics include the log of the boot that ended.
- Play plugged in, or lower **Render resolution (supersampling)** in **Game settings…**.
- The Frame also turns off while plugged in: report the problem.
## My progress isn't saved
1. Quit through the game's own menu, not Steam's **Exit game**. Exit game closes the game at once, and some games only
save when they exit normally.
2. Check whether it saves at all: **Files** in the sidebar → under **Game storage**, the game → look at the files'
dates. Play, save in the game, quit from its menu and look again.
3. Nothing changes: report it.
1. Quit through the game's own menu. Most games save when Steam's **Exit game** pauses them, but some only save when
you quit them yourself.
2. Check that the game saves at all: **Files** → **Game storage** → the game → look at the files' dates. Play, save, quit
and look again.
3. Nothing changes: report the problem.
Saves stay on the Frame when you update or reinstall a game.
## Can I back up a game's saves?
Yes. Steam doesn't sync Quest games' saves, so FramePort copies them to your PC: **Back up saves** next to Play on
the installed game's page, or **Back up all saves** on the Steam Frame page (Installed games). Close the game
first. A backup holds the game's own storage (`/data/data/<package>` and `/sdcard/Android/data/<package>`), without
caches, shader caches and large downloaded content (videos, OBBs, files over 64 MB); the job says when something was
left out. FramePort keeps the 10 newest backups per game (Settings → Data). They're in FramePort's data folder under
`save-backups/<package>/` (**Open backups folder** in the game's menu).
Yes. Steam doesn't sync Quest games' saves, so FramePort copies them to your PC. Close the game first, then use
**Back up saves** next to **Play**, or **Back up all saves** on the Steam Frame page. Caches and large downloaded
content (videos, OBBs, files over 64 MB) are left out.
In **Settings → Data**, **Choose backup folder…** selects a different folder, including a local Drive or Nextcloud
sync folder. FramePort checks that it can write there. **Use the default** resets the location. Existing backups stay
where they are and remain listed for restoring; retention only removes old backups in the currently selected folder.
FramePort writes local files; your cloud client handles uploading them.
- **Where:** `save-backups/<package>/` in FramePort's data folder (**Saves → Open backups folder**). Settings →
**Data** → **Choose backup folder…** picks another, for example a cloud-sync folder. Older backups stay restorable.
- **How many:** the 10 newest per game by default (Settings → Data).
- **Automatically:** **Back up saves when a game closes** (Settings → Data, off by default). The Frame keeps 3
snapshots per game, even while FramePort is closed; FramePort copies the snapshots over when FramePort
connects. Connect once after turning the setting on.
- **Restore:** **Saves → Restore saves…**. **Undo last restore** in the same window takes you back. Uninstalling a
game with *Also delete its saves* backs them up first.
**Back up saves when a game closes** (Settings → Data, off by default) keeps the newest three changed snapshots per
game on the Frame, even while FramePort is closed. Connect once after changing this setting to apply it to the Frame.
New snapshots are copied to your backup folder when FramePort connects or polls the connection, marked **automatic**,
and count toward the same backup limit. Unchanged saves are skipped. A snapshot captures what the game wrote to disk;
it cannot recover progress a forcibly closed game never saved.
**Restore saves…** puts one back: it replaces the game's saves on the Frame. The Frame keeps a copy of the saves it
had, so **Undo last restore** in the same window takes you back. When you uninstall a game and tick *Also delete its
saves*, FramePort backs them up first. Command line: `frameport saves backup|list|restore|undo <game>`.
Command line: `frameport saves backup|list|restore|undo <game>`.
## A game with mods crashes
How to tell: play once without mods and custom content. If it then works, a mod is the cause; the diagnostics zip
usually names the mod's library in the crash. Report it to the mod's author, and check for a mod version that matches
your game version.
Play once without mods. If the game then works, a mod is the cause (the diagnostics zip usually names the mod).
Report the crash to the mod's author, and check for a mod version that matches your game version.
## The game needs files I supply (ports, emulators, mods)
How to tell where they go: the game says it on screen or in its instructions, and its log names the path (in the
diagnostics zip, `launch.log`). It's usually `Android/data/<package>/files`.
The game's instructions say where; the place is usually `Android/data/<package>/files` (the game's `launch.log`
names the path).
1. Unpack or extract the files on your PC first: these apps read folders, not disc images (`.iso`) or archives.
2. **Files** in the sidebar → under **Game storage**, the game → open that folder → **Upload folder…** (or
**Upload files…**). The game's menu → **Add videos and files** opens the same place.
1. Unpack the files on your PC first: these apps read folders, not disc images (`.iso`) or archives.
2. **Files** → **Game storage** → the game → that folder → **Upload folder…** or **Upload files…**.
In-game "choose a file" buttons don't work on the Frame, which has no file picker.
In-game "choose a file" buttons don't work: the Frame has no file picker.
## How do I report a problem well?
1. Reproduce it once, then right away open the game's menu (**…** on its page) → **Report a problem…**. For a hang,
do it while the game still hangs.
2. Say what you saw in the headset and what you already tried.
3. **Collect and open GitHub** saves a diagnostics zip and opens a prefilled game report; attach the zip to it. The
zip holds logs, not your game files, and personal details are removed.
1. Reproduce the problem, then right away use **Troubleshoot → Report a problem…** in the game's menu. For a hang,
report while the game still hangs.
2. Say what you saw in the headset and what you tried.
3. **Collect and open GitHub** saves a diagnostics zip and opens a prefilled report: attach the zip. The zip holds logs,
not your game files, and personal details are removed.
+152 -93
View File
@@ -1,16 +1,20 @@
# Steam Frame runtime reference (SteamOS 0.3.0, build 20260922)
# Steam Frame runtime reference
## Setup from the project page (checked 2026-10-10, dev Frame, SteamOS build 20260922)
Facts about the Steam Frame's runtime that FramePort relies on, checked on the device from SteamOS 0.3.0 (build
20260922) onwards; each section says when and on which version where it matters. Symptoms and fixes per game are in
[PLAYBOOK.md](PLAYBOOK.md), longer investigations in [DEVICE_NOTES.md](DEVICE_NOTES.md).
## Setup from the project page (checked 2026-10-10, SteamOS build 20260922)
- Factory Frames have no browser; Steam's taskbar **+** offers Chromium as a Flatpak (`org.chromium.Chromium`,
flathub is configured as a system remote). It is the default handler for https links once installed.
flathub is configured as a system remote). Chromium is the default handler for https links once installed.
- Desktop Mode has `curl`, `wget`, `python3` 3.12, `konsole`, `kdialog`, `xdg-open`, `avahi-browse`, `systemd-run`;
no `wl-copy`/`xclip`.
- avahi-daemon runs, but `avahi-browse` from an SSH session fails ("Daemon not running": no system D-Bus access
there). A stdlib-Python mDNS query works when it listens on port 5353 in the 224.0.0.251 group (like avahi):
replies to a random source port are dropped by the Frame's firewall. `bootstrap/setup.sh` does exactly that.
- The Frame reaches the PC's port 8765 over the home Wi-Fi without any firewall change on the PC side here (WSL in
mirrored mode with the earlier FramePort rule state). The PC is also visible through the Frame's hotspot
- The Frame reaches the PC's port 8765 over home Wi-Fi without a firewall change on the PC (tested with WSL in
mirrored mode). The PC is also visible through the Frame's hotspot
(`wlanap`, 10.35.78.x): the script lists one PC once, by name and words.
- Dolphin's "executable scripts" setting is the default (ask); `.sh` files open with a `bash.desktop` handler that
runs them in a terminal. Not used by the setup (a pasted line is simpler and needs no download prompt).
@@ -28,10 +32,25 @@
200 keys, so ~200 launches after boot every container fails ("create keyring … Disk quota exceeded"). FramePort sets
`keyring = false` in `~/.config/containers/containers.conf`; a reboot clears already-leaked keys.
- Picks the activity with category LAUNCHER (`APP_ACTIVITY`). Game data is visible at `/sdcard/Android/{obb,data}/<pkg>`.
- Ending a game: Lepton traps TERM/INT and tears down with `podman exec … reboot -p` + `podman stop -t 0`; neither
pauses the app (no `onPause`, no Unity `OnApplicationPause(true)`). `input keyevent` HOME does nothing in the
headless container; `am start -a android.intent.action.MAIN -c android.intent.category.HOME` (Launcher3) pauses the
game at once and stops it ~0.3 s later. FramePort does that before its own stops (GitHub #480, DEVICE_NOTES
"Graceful stop").
- Steam's Exit game (`Apps.TerminateApp`) signals the game's whole process tree at once. `conmon` is re-parented to
Steam's `reaper` (a bare child subreaper), so the container's Android processes are in that tree although their
cgroup is podman's `libpod-<id>.scope`; moving processes out of Steam's `app-steam-app<appid>-<pid>.scope` doesn't
shield them. The container is gone ~0.7 s after the call; nothing in launch.sh can run before that. Every Exit
game path in Steam's UI (the in-headset frame menu, Quick Access, "exit the running game?" dialogs) is
`SteamClient.Apps.TerminateApp(gameid, false)` in SharedJSContext's code (the menus are popups without scripts of
their own; each popup window has its own `SteamClient` object, unused), and `SteamClient.Apps.TerminateApp` is a
writable property there. FramePort hooks it while a game runs (launch.sh's `_exit_hook`, DEVICE_NOTES "Graceful
stop"). CDP `Runtime.addBinding` works on Steam's CEF (calls arrive as `Runtime.bindingCalled` on the connection
that added it; the function stays on the page after that connection closes, calls then go nowhere).
- 64-bit only (no AArch32): 32-bit-only APKs run through FramePort's a32 bridge (`frame.a32_bridge`).
- `<install>/lepton-shaders/` holds Mesa's shader cache (Lepton sets `mesa.shader.cache.dir=/data/shaders`):
`foz_cache.foz` + `foz_cache_idx.foz` per graphics driver (Turnip Vulkan, zink GL), typically 4–6 files and 7–19 MB
per game, 2D apps included. It speeds up later starts; deleting it only costs one slower start.
per game, 2D apps included. The cache speeds up later starts; deleting the cache only costs one slower start.
- **2D apps** (verified 2026-10-02 with an open-source 2048 game): Lepton runs every app headless
(`lepton.headless=true`; only OpenXR output reaches the headset) unless the app folder (`STEAM_COMPAT_INSTALL_PATH`)
contains a file `lepton-show-flatscreen` (`liblepton/app_metadata.sh`): then Android (11, Waydroid) gets a Wayland
@@ -55,7 +74,7 @@
changes when the files move.
- The game's files (`lepton-app/`, `lepton-data/` = saves, `lepton-shaders/`, `settings.conf`, `launch.log`; PC VR:
`game/`, `revive/`, `compatdata/` = Proton prefix; Linux: `app/`) live in `deployment.json["base"]`: the anchor
itself on internal storage, or `<mount>/FramePort/<pkg>` on another drive (GitHub #90, agent v63).
itself on internal storage, or `<mount>/FramePort/<pkg>` on another drive (GitHub #90).
- SteamOS mounts removable drives (microSD) under `/run/media/<user>/<label or uuid>`; the agent's `drives` reads
`/proc/mounts` (plus the drives of Steam library folders) and refuses vfat/exfat/ntfs (Lepton's data and Proton
prefixes need Unix owners, permissions and symlinks) and read-only mounts. A drive that isn't mounted is an error for
@@ -68,21 +87,6 @@
## Room data
Live floor coverage (agent v104) is enabled by default for manual and game-requested scans; the capture
payload accepts `preview: false`, and `frameport frame room scan --no-preview` disables it. A world-locked,
upward-facing 6 m square at floor height follows the current raw-to-STAGE calibration and stays centred on the
scan's first recorded camera position. Every 16th depth vertex contributes to raw-space 10 cm voxels; at most once
per second, newly occupied cells redraw a 256×256 RGBA map in one colour (FramePort's secondary blue), each cell a
filled 10 cm square, only the opacity telling floor (<0.3 m, faint), raised surfaces (0.3–1.5 m) and tall surfaces
(>1.5 m, strongest, the room's outline) apart; unseen transparent. Overlays have no depth (they draw over hands and
passthrough), so the map stays faint; three colours in 3x3 blotches clashed in the owner's first run. It shows
coverage, not a validated room boundary. IVROverlay_028 slots 1/3/20/22/33/43/62 (OpenVR 2.15.6 header)
create/destroy/sort order (1, below the scan prompts' 10)/set width/set absolute STAGE transform/show/upload raw
pixels. Each upload is 256 KiB (at most 256 KiB/s); timing for the first
20 uploads and every tenth thereafter, plus failures, goes to `rooms/scans/scan-*.fprs.log` and the built room’s `scan.log`. Overlay errors or an
upload taking over 50 ms disable the preview while capture continues; headset appearance and upload cost still
need device validation.
- SteamVR on the Frame has no Meta scene/anchor extensions. Its chaperone supplies only a 2D outline raised to
2.43 m, without furniture, ceiling or a room mesh. Valve's default boundary is a 20-point circle of roughly 1 m
radius; FramePort refuses to treat it as a room.
@@ -100,20 +104,37 @@ need device validation.
`dirname($FRAMEBRIDGE_CONFIG)/room.txt`. Stale copies are removed if no room exists or emulation is disabled.
Each game's persistent anchors stay in `/sdcard/Android/data/<pkg>/files/framebridge-anchors.txt`.
- With `scene_emul`, launch.sh starts `_scan_watch <pkg>` after Lepton, closing fd 9; its exit trap stops the
watcher, which also exits if the launcher disappears. It polls once a second and logs to `<base>/scan-watch.log`.
watcher, which also exits if the launcher disappears. The watcher polls once a second and logs to `<base>/scan-watch.log`.
In `<base>/lepton-data/external/Android/data/<pkg>/files/`, FrameBridge writes `framebridge-scan-request`
(`request <id> <unix_seconds>`); the watcher clears stale request/ack/result files at startup, writes
`framebridge-scan-ack` (`ack <id>`), reuses the headset capture/build worker, stores the current room and copies
it beside settings.conf before atomically writing `framebridge-scan-result` (`ok <id>` or `failed <id> <reason>`).
A global `rooms/.scan.lock` excludes concurrent captures, including manual scans; `rooms/.lock` still protects
room publication. Launcher exit cancels the scan; a 350-second deadline leaves time for the six-minute protocol timeout.
- Frame → Room and `frameport frame room` manage rooms. `from-boundary` creates walls without furniture;
- The Room tab and `frameport frame room` manage rooms. `from-boundary` creates walls without furniture;
`import` / `export` transfer room files; `show` writes an SVG preview; `build <dump.fprs>` syncs the bundled
`fp_roomscan` by SHA-256 and runs it on the Frame with `--offline`. `scan` = agent `room_capture`: records the
RoomView meshes (`/xrservice/roomview3d/meshData` + `/meshLayout`, `IVRBlockQueue_005`) while in-headset prompts
guide the player, then builds the room from that recording.
## Save backups (Quest games, agent v101, discussion #352)
### Scan preview
Live floor coverage is shown by default for manual and game-requested scans; the capture
payload accepts `preview: false`, and `frameport frame room scan --no-preview` disables it. A world-locked,
upward-facing 6 m square at floor height follows the current raw-to-STAGE calibration and stays centred on the
scan's first recorded camera position. Every 16th depth vertex contributes to raw-space 10 cm voxels; at most once
per second, newly occupied cells redraw a 256×256 RGBA map in one colour (FramePort's secondary blue), each cell a
filled 10 cm square, only the opacity telling floor (<0.3 m, faint), raised surfaces (0.3–1.5 m) and tall surfaces
(>1.5 m, strongest, the room's outline) apart; unseen transparent. Overlays have no depth (they draw over hands and
passthrough), so the map stays faint; three colours in 3x3 blotches clashed in the first headset test. The map shows
coverage, not a validated room boundary. IVROverlay_028 slots 1/3/20/22/33/43/62 (OpenVR 2.15.6 header)
create/destroy/sort order (1, below the scan prompts' 10)/set width/set absolute STAGE transform/show/upload raw
pixels. Each upload is 256 KiB (at most 256 KiB/s); timing for the first
20 uploads and every tenth thereafter, plus failures, goes to `rooms/scans/scan-*.fprs.log` and the built room’s `scan.log`. Overlay errors or an
upload taking over 50 ms disable the preview while capture continues; headset appearance and upload cost still
need device validation.
## Save backups (Quest games, discussion #352)
- A Lepton game's saves are two folders of `<base>/lepton-data`: `internal/<pkg>` (= `/data/data/<pkg>`:
`shared_prefs/`, `files/`, `databases/`; Unreal's `files/UE4Game|UnrealGame/.../Saved` often lives here) and
`external/Android/data/<pkg>` (= `/sdcard/Android/data/<pkg>`: Unity's `files/`, Unreal's external `Saved`). On the
@@ -125,7 +146,7 @@ need device validation.
exec channel's stdout, the result JSON on stderr. Left out: folders named cache/code_cache/no_backup/il2cpp/
UnityShaderCache/(Vulkan)ProgramBinaryCache/GPUCache/… at any depth, Unreal `Saved/Logs|Crashes`, FrameBridge's
`fb_snap_*`/`framebridge*` files, `.obb/.apk/.foz/.mp4/.mkv/.webm/.mov`, symlinks and special files, and files over
64 MiB (big downloaded content; e.g. BattleTalent's `files/Video`). More than 4 GiB of saves is refused.
64 MiB (big downloaded content; for example BattleTalent's `files/Video`). More than 4 GiB of saves is refused.
- `_restore_saves '<json>'` reads the archive from stdin and treats it as untrusted: only regular files and folders
under `internal/`/`external/` (no `..`, absolute paths, links, devices, duplicates), ≤1 GiB per file / 8 GiB in all,
each file's sha256 must match the manifest, the manifest's package must be the game. Everything is staged in
@@ -154,7 +175,7 @@ need device validation.
(archive stdout, result JSON stderr). PC `install/saves.sync_snapshots` runs in the background, verifies the archive
hash and package, imports it as a normal backup marked `automatic`, and applies `saves.keep`. A local
`<data>/save-snapshots-synced.json` digest ledger prevents reimporting snapshots after pruning or deleting backups.
Failures leave snapshots on the Frame for the next poll. This path does no disk/SSH work in rendering.
Failures leave snapshots on the Frame for the next poll. The snapshot sync does no disk/SSH work in rendering.
## OpenXR runtime (as seen by games through overport's loader)
- Instance extensions present include KHR_android_create_instance (must be enabled; the adapter adds it),
@@ -169,7 +190,7 @@ need device validation.
- Layer types (checked 2026-09-30 in `/opt/steamvr/bin/androidarm64/vrclient.so`, the Android-side runtime Lepton
mounts at `/data/steamvr/runtime`): its compositors (`CSxrCompositorPrism`, `CSxrCompositorOpenVR`) only have
`ComposeLayerQuad` and `ComposeLayerProjection`; cube/cylinder/equirect/equirect2 exist only as enum names, so
there is nothing to switch on. It has a layer limit ("Exceeded the layer limit"; the value is logged by
there is nothing to switch on. The runtime has a layer limit ("Exceeded the layer limit"; the value is logged by
`layer_debug` from `maxLayerCount`). The adapter emulates cylinders (strips) and, per game, equirect layers
(`equirect_emul`, an adapter projection layer). Refused cube swapchains use GLES/Vulkan stand-ins;
`cube_quads` (default on) copies their faces into six quads for frames without a projection
@@ -200,23 +221,24 @@ and left/up/right as Touch Y; right A as Touch A and X/Y/B as Touch B. Left View
FrameBridge's `quest_buttons` preference (`0` default, `1` Mirror, `2` Quest pair) enables the extension when offered
and suggests a Frame binding translated from the game's Touch suggestions. Common pressure/trigger/thumbstick,
pose and haptic paths keep their Touch counterparts; thumbrest touch and system click are dropped, bumpers stay
unbound. The player's SteamVR custom bindings can override suggestions. This works on both bridge ABIs, without
unbound. The player's SteamVR custom bindings can override suggestions. `quest_buttons` works on both bridge ABIs, without
the arm64-only extension shim. See [Controller buttons](CONTROLLERS.md) and the [mapping contract](input/QUEST_BUTTONS.md).
Device verification of the new modes is pending integration with the native stream.
The runtime accepts the suggested bindings in headless launches; the modes haven't been tried in the headset on
many games yet.
## 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
engine in BattleTalent 4.7 rejects those otherwise valid pointers in `lua_pushlightuserdata`. The cause 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;
exactly. No game scripts or account checks are changed. The test 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,
The patch 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.
Early frame-rectangle errors and backend login errors remain in the capture; the launch 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.
@@ -227,10 +249,10 @@ Lua error text. `Unknown Platform` also appears after this crash is fixed, so it
Appending `debug` while preserving inherited flags showed `fdm_injection_CreateInstance: skipped`.
A temporary `FDM_DEBUG=enable,debug` launch created the Unity/Manta Vulkan instance and device,
detected a 54x54 two-layer density map for 1728x1728 eye buffers, and logged render passes with
`fdm 1`, `already_has_fdm=1`, and changing offsets for both eyes. This verifies the injection path,
`fdm 1`, `already_has_fdm=1`, and changing offsets for both eyes. The probe verifies the injection path,
not visual quality, gaze calibration, sustained gameplay stability, or an FPS/power improvement.
`device.foveation` mode `tracked` now explicitly loads the layer and sets `FDM_DEBUG=enable` without
debug spam. It is opt-in; do not enable it globally or assume all GLES/Zink and Vulkan renderers work.
debug spam. The mode is opt-in; do not enable it globally or assume all GLES/Zink and Vulkan renderers work.
Follow-up wearer test on 2026-10-11 FAILED in Batman: large rainbow glitches outside the gaze region
and jitter even in clear regions. Restoring the original launcher and restarting removed BOTH symptoms,
confirmed by the wearer. Do not recommend forced injection for this Batman build. Reaching foveated
@@ -254,7 +276,7 @@ Lua error text. `Unknown Platform` also appears after this crash is fixed, so it
That is the only way to reach the Vulkan side of OpenGL ES games (Zink creates the instance inside Mesa).
- GL ES: Zink (Mesa GL on Vulkan). Strict GLSL (see PLAYBOOK) and occasional `DEVICE LOST` with MSAA render-to-texture.
## Proton / Windows games (surveyed 2026-09-29; running a Rift game under it not yet verified)
## Proton / Windows games (surveyed 2026-09-29)
- Steam on the Frame registers ARM64 compat tools from the app **"Steam Frame ARM64 Compat List"** (appinfo
`extended.compat_tools`, found dynamically by the agent): `proton_11-arm64` (4628740, needs
`steamlinuxruntime_steamrt4-arm64` 4185400), `proton-experimental-arm64` (4427310), `fex` (3127680, FEX-Emu for
@@ -262,10 +284,11 @@ Lua error text. `Unknown Platform` also appears after this crash is fixed, so it
SteamLinuxRuntime_4). None are installed by default.
- `steam -ifrunning steam://install/<appid>` only opens a confirmation dialog in the headset. The agent's unattended
mode writes an appmanifest stub (StateFlags 1026, installdir from appinfo) and restarts Steam, which then downloads it.
- **x86_64 Linux apps (agent v61; verified on the device 2026-10-07: an x86_64 glibc test program installed + launch test RUNNING, "machine=x86_64 glibc=2.41"):**
- **x86_64 Linux apps (verified on the device 2026-10-07: an x86_64 glibc test program installed + launch test
RUNNING, "machine=x86_64 glibc=2.41"):**
FEX is Steam app 3127680 (`fex`, installs as `common/FEX-Emu`, ~6 MB, 21 s via the appmanifest-stub path:
`install_proton` / `proton_status` with `kind: linux_x86`). Its toolmanifest commandline is
`/fex-compat-tool %verb% --`, **no** require_tool_appid: it does not use the Steam Linux Runtime. fex-compat-tool
`install_proton` / `proton_status` with `kind: linux_x86`). FEX's toolmanifest commandline is
`/fex-compat-tool %verb% --`, **no** require_tool_appid: FEX does not use the Steam Linux Runtime. fex-compat-tool
(Python) runs `<FEX-Emu>/usr/bin/FEX` with RootFS `/usr/share/guestos/fex-mesa` (part of the SteamOS image: an
x86 Arch-style root with glibc 2.41, Mesa, graphics_provider.json for x86_64 + i386), emulates x86_64 and i386
(emulator.json), honors `STEAM_FEX_TSOENABLED`, `STEAM_FEX_MULTIBLOCK`, `STEAM_COMPAT_FEX_CONFIG`, sets
@@ -297,8 +320,12 @@ Lua error text. `Unknown Platform` also appears after this crash is fixed, so it
`XR_API_LAYER_PATH` (it keeps `XR_ENABLE_API_LAYERS`), so the agent registers the layer as an explicit layer in
`~/.local/share/openxr/1/api_layers/explicit.d/` (home is shared into the container) with an absolute library path.
- Oculus Store builds that delay-load `LibOVRPlatform64_1.dll` (Platform SDK, for example Lies Beneath) crash with
`0xc06d007e` (delay-load module not found): that DLL comes with the Oculus app, which the Frame doesn't have. (No Oculus runtime DLLs or
registry keys are needed: Revive's LibOVRRT hook works once OpenXR does.)
`0xc06d007e` (delay-load module not found): that DLL comes with the Oculus app, which the Frame doesn't have.
- Revive under Proton arm64: Revive's Detours hooks (LoadLibrary, WinVerifyTrust) don't take effect because Wine's
kernelbase is ARM64EC code. `pcvr.libovr_redirect` puts Revive's runtime where the game looks for `LibOVRRT*_1.dll`
(gets past `-3001`), but games that check the runtime's Oculus signature then stop at `-3021`
(`ovrError_LibSignCheck`), which FramePort doesn't bypass. So LibOVR games run only on the PC; OpenVR/OpenXR builds
(for example Rick and Morty, Vader Immortal) run directly under Proton. DEVICE_NOTES "Revive under Proton arm64".
- Unreal's Oculus plugin (and LibOVR's `ovr_Detect`) first checks for the Windows event `OculusHMDConnected` (created
by the Oculus service on a PC) and silently skips VR without it. Revive hooks `OpenEventW` for that — Lies Beneath
got as far as OVRPlugin, so the hook evidently works there — but it depends on Detours patching Wine's kernelbase,
@@ -310,18 +337,20 @@ Lua error text. `Unknown Platform` also appears after this crash is fixed, so it
- 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.
(GitHub #263). The agent 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`).
- Processes started from Steam (Konsole, and SSH sessions started from it) share steam.service's cgroup and end with
Steam: run such work through `systemd-run --user`.
- SSH: Developer Mode enables `sshd`; FramePort logs in with its own key (the setup adds it), so no user password is
needed.
- mDNS: avahi-daemon runs by default; hostname `frame` → `frame.local`.
- Desktop Mode menu entries (GitHub #84, agent v63): Linux apps get `~/.local/share/applications/frameport-<slug>.desktop`
- Desktop Mode menu entries (GitHub #84): Linux apps get `~/.local/share/applications/frameport-<slug>.desktop`
(+ an executable copy in `~/Desktop` when that folder exists; Plasma starts executable `.desktop` files there
without a trust prompt), `Exec=env FRAMEPORT_DESKTOP=1 "<anchor>/launch.sh"`, marked `X-FramePort-Package=<pkg>`.
Plasma's launcher exits right after starting the program, so with `FRAMEPORT_DESKTOP=1` the Linux launcher skips
its "Steam parent gone → end the app" watchdog and keeps the desktop's DISPLAY/WAYLAND_DISPLAY instead of taking
gamescope's from Steam. Not yet tried from the Frame's Desktop Mode.
- Linux apps' own icons (GitHub #99, agent v64): finalize_linux (and every refresh of the menu entries) copies the
- Linux apps' own icons (GitHub #99): finalize_linux (and every refresh of the menu entries) copies the
app's icon to `<anchor>/artwork/app-icon.{png,svg}`: an AppImage's `squashfs-root/.DirIcon` (usually a symlink),
else the `Icon=` of its top-level `.desktop` file (a folder app: the first `.desktop` within 3 levels) looked up
next to it, in `(usr/)share/icons/hicolor/*/apps/` and `(usr/)share/pixmaps/`; the biggest PNG ≥128 px, else an
@@ -363,7 +392,7 @@ over Quest games.
a utility client, live; the path is kept in `~/.config/openvr/config/appconfig.json` `manifest_paths`.
`SetApplicationAutoLaunch` is accepted (GetApplicationAutoLaunch → true; vrserver has
`CAppInfoManager::StartAutolaunchOverlays`) but on the dev Frame it never reached `steamvr.vrsettings` (no
autolaunch entry for `temporalreality.overlay` there, appconfig.json holds only manifest_paths) and the owner's
autolaunch entry for `temporalreality.overlay` there, appconfig.json holds only manifest_paths) and the
watch didn't start by itself, so FramePort doesn't rely on it (see "Autostart" below). The linuxarm64 vrserver only reads
**`binary_path_linux_arm`**: a manifest with `binary_path_linux` alone is skipped ("must specify binary_path for
launch_type binary. Skipping"; Steam's own steamapps.vrmanifest entries are skipped the same way), so an app's
@@ -371,12 +400,12 @@ over Quest games.
`LaunchApplication(key)` then starts the binary (Temporal Reality's launch.sh: process up, overlays created).
- A Steam shortcut of an overlay app and a game shortcut run at the same time (Temporal Reality, then 4XVR, both
through `steam://rungameid`: both "Game process added", both kept running).
- FramePort (agent v75): `register_vr_overlay` writes `<anchor>/frameport-overlay.vrmanifest` (the app's own key,
- FramePort: `register_vr_overlay` writes `<anchor>/frameport-overlay.vrmanifest` (the app's own key,
name and image from its bundled manifest, binary = launch.sh in `binary_path_linux_arm` + `binary_path_linux`,
absolute paths) and registers it; `unregister_vr_overlay`, uninstall and purge remove it; `ensure_host_fixes`
registers ones SteamVR missed (it wasn't running). Overlay apps skip launch tests, the Linux launcher's
Steam-parent watchdog (`FRAMEPORT_OVERLAY=1`) and don't count as a running game.
- **Autostart (agent v76)**: the deployment's `overlay.autostart` is the source of truth (SetApplicationAutoLaunch
- **Autostart**: the deployment's `overlay.autostart` is the source of truth (SetApplicationAutoLaunch
is still set for SteamVR builds that honour it). While at least one installed overlay app has it on,
`ensure_host_fixes` keeps the user service `frameport-vr-overlays.service` (`~/.config/systemd/user`, enabled for
default.target, Restart=always) = `frameport_agent.py _vr_overlay_watch`: every 5 s it checks the vrserver it knows
@@ -395,17 +424,18 @@ over Quest games.
## Video of the headset view (surveyed 2026-10-05; used by the Live view tab)
- `steamvr-v4l2cam.service` (user unit, part of gamescope-session.target, `Restart=always`) runs SteamVR's
`/opt/steamvr/bin/linuxarm64/v4l2cam --output=99`: it reads the compositor's "Headset View" (IVRHeadsetView) and
`/opt/steamvr/bin/linuxarm64/v4l2cam --output=99`: v4l2cam reads the compositor's "Headset View" (IVRHeadsetView) and
writes it to a v4l2loopback webcam named **"SteamVR"** (`/dev/video99`, 1920x1080 RGB24, advertised 30 fps, frames
arrive at the display rate). Nothing on the Frame reads it by default; idle it costs nothing, read ~0.2 core.
It shows what the wearer sees (SteamVR home, Steam's panels; a game's layers are expected but not yet seen in it).
It shows what the wearer sees: SteamVR home, Steam's panels and games (4XVR's theatre with an overlay on top,
2026-10-10).
Black and ~1 fps (one frame per ~1.0 s) while the Frame sleeps (standby): a 30 fps stream then repeats each
frame in bursts, which looks like a stall in a player. Its size follows SteamVR's headset view (v4l2cam has no size
frame in bursts, which looks like a stall in a player. The picture's size follows SteamVR's headset view (v4l2cam has no size
option), so the live view only scales down (360p/480p/720p/1080p) or sends it as is ("full").
- Sound: `pactl get-default-sink` (`alsa_loopback_device.stereo.alsa_output.platform-sound.HiFi__Speaker__sink`) and its
`.monitor` source carry what the Frame plays; the Frame's ffmpeg has the `pulse` input and `aac`. Timestamps: pulse
uses the wall clock, v4l2 CLOCK_MONOTONIC → `-ts mono2abs` on the v4l2 input. Don't force
`-use_wallclock_as_timestamps` on the pulse input: it stamped bursts of AAC packets with one time.
`-use_wallclock_as_timestamps` on the pulse input: that option stamped bursts of AAC packets with one time.
- Steam's own game recording / Remote Play / broadcast capture the **gamescope** PipeWire node (`CDesktopCapturePipeWire:
... node path: gamescope`): with gamescope's `--backend openvr` that's only the flat Steam UI, not VR. Steam's arm64
`libvideo.so` encodes with x264 (vaapi/nvenc paths don't apply). No cast/spectator feature for the Frame's own VR view
@@ -414,9 +444,9 @@ over Quest games.
`h264_v4l2m2m` hangs, gst `v4l2h264enc` not-negotiated). ffmpeg + libx264 works: FramePort's live view
(`install/livestream.py`: `fps=30` before the scale, ultrafast/zerolatency, 3 threads, nice 10, fragmented MP4 on
stdout, + AAC 128k) measured 0.34 core at 720p / 0.56 at 1080p with a quiet picture (video only); expect ~1-1.4
cores with a busy scene. That is now only the fallback.
cores with a busy scene. The x264 path is now only the fallback.
- Hardware encoding (2026-10-07): the iris encoder works when driven directly through the V4L2 stateful encoder
interface. That is FramePort's `fp_venc` (`native/venc`, spec + every measured value in `native/venc/SPEC.md`).
interface. FramePort's `fp_venc` does that (`native/venc`, spec + every measured value in `native/venc/SPEC.md`).
Facts:
- Device: `/dev/video23` (`/dev/video-enc0` links to it), driver `iris_driver`, M2M multiplanar.
- Formats: input NV12/NV21/AB24(RGBA)/QC24/Q08C; output H264/HEVC; sizes 128..8192.
@@ -454,7 +484,7 @@ over Quest games.
0–N s alongside), plus `frag_keyframe` so that a requested keyframe starts a fragment.
- With sound, ffmpeg paces its inputs against each other and pulse's audio arrives later than the wall clock
(more while something plays). The video input then counts as ahead, and ffmpeg stops reading it for up to
0.7 s, so the writer blocks. That was the cause of 1080p dropping frames: busy 1080p replayed with sound took
0.7 s, so the writer blocks. The blocked writer caused 1080p's dropped frames: busy 1080p replayed with sound took
34–47 s for 20 s of video. Video alone was fine; `nice`, `-raw_packet_size` and the input queue size didn't
matter. Shifting the video input back fixes it, but too far makes ffmpeg hold the video for interleaving and
release it in clumps: -1 s gave output gaps of up to 550 ms, a longer and stuttering delay in the browser.
@@ -464,7 +494,7 @@ over Quest games.
sound about 1 s. Chrome keeps about 0.6 s of audio ahead and stalls below that, whatever the player does:
1.1× catch-up gave 8 stalls per 30 s, no speed-up 1–2, same average lag; 50 ms fragments didn't help. So the
player doesn't speed up while sound is on.
- Quality (owner's headset test, 2026-10-07): 3 Mbit/s CBR at 720p36 showed heavy compression artifacts. The
- Quality (headset test, 2026-10-07): 3 Mbit/s CBR at 720p36 showed heavy compression artifacts. The
hardware path now uses VBR with a 1.5× peak (the encoder accepts BITRATE_MODE VBR + BITRATE_PEAK) and higher
targets: 360p 1.5, 480p 2.5, 720p 5, 1080p 8, Full 10 Mbit/s. The x264 fallback keeps its rates.
- The default sink is SUSPENDED while nothing plays; its monitor still delivers (silent) audio.
@@ -475,6 +505,12 @@ over Quest games.
and key presses (Steam's keyboard, USB/Bluetooth keyboards, `input text`) don't reach VR apps. The app folder's
`lepton-show-flatscreen` marker shows the window (VR keeps working) and gives it focus; Steam's on-screen keyboard
then opens for its text fields (FramePort patch `device.text_input_window`).
- That window is a gamescope client: gamescope (`--backend openvr`, `--virtual-connector-strategy PerAppId`) shows a
Steam app's windows as the SteamVR dashboard overlay `valve.steam.desktopgame.<appid>` with mouse input. SteamVR
puts a game's window into theater mode while the game isn't a VR scene app yet (setting
`dashboard.autoShowGameTheater`) and opens the dashboard on it when the OpenXR session starts: the controllers then
drive a mouse pointer on the Android screen. FramePort turns the setting off while such a game runs (see
DEVICE_NOTES.md "Typing window takes the controllers").
- `/dev/uinput` is writable by the steamos user (ACL for Steam Input): a uinput virtual keyboard works like a real
one everywhere (FramePort's "Type on Frame", agent `_keyboard`).
- Unity text fields close without a system keyboard; see `frame.unity_text_input` in PLAYBOOK.md.
@@ -493,7 +529,7 @@ radius pairs do **not** describe the generated shared-injection density map.
The inspected arm64 `libVkLayer_VALVE_fdm_injection.so` has these semantics (virtual addresses):
- `init_fdm_fove_level` at `0x1cdf4` calls the cached option reader, then
`debug_parse_flags_option` (`0x21d74`) with table `0x27e70`: named tokens `lo` → 1,
`med` → 2, `hi` → 3. It does not call atoi/strtol. Unset, empty, unknown and numeric
`med` → 2, `hi` → 3. The parser does not call atoi/strtol. Unset, empty, unknown and numeric
strings parse to zero and retain the current profile (initialized to 1 at `0x1ce74`).
Tokens are case-sensitive and separated by non-alphanumeric/non-underscore characters;
matching values are ORed, so combinations can alias another profile. `all` also yields 3;
@@ -512,29 +548,50 @@ The inspected arm64 `libVkLayer_VALVE_fdm_injection.so` has these semantics (vir
`disable_offsets` fixes the gaze position (`0x1b6c8`), and `yflip` flips the vertical
offset (`0x1b9b8`). These three flags do not resize the profile.
- `FDM_SWAPCHAIN_SIZE` is parsed by `atoi` (`0x1c518`), clamped to nonnegative, and
seeds a known swapchain size (`0x1ccf8`). It is a size/matching hint, not a radius.
The synthetic map is square with side `ceil(size/32)` (`0x19048–0x19088`); image
association also matches this tile size (`0x16c78`). Changing render resolution changes
the pixel extent, tile quantization and potentially the matched render target/map. This can
change the visible diagnostic area without making the ignored radius exports effective;
a matched device capture is needed to establish which path a particular game used.
seeds a known swapchain size (`0x1ccf8`). The value is a size/matching hint, not a radius.
The synthetic map (Zink/GLES dynamic-rendering path, `CmdBeginRendering`) is square with side
`ceil(size/32)`, size = the render area width (`0x19048–0x19088`); image association also matches
this tile size (`0x16c78`).
- Where the sharp area lands (device-checked 2026-10-24 with AC Nexus, GitHub #394; details in
DEVICE_NOTES.md "Foveation: sharp area centre and size"): for Vulkan games the layer's
`xrCreateSwapchain` creates a map of `ceil(w/32) x ceil(h/32)` texels per eye swapchain (one array
layer per eye) and fills the preset rectangles in map-normalized coordinates, so the sharp area is
centred on the **whole swapchain** (all presets centre at about 0.51, 0.51). The layer knows each view's
imageRect (`xrEndFrame` stores them) but doesn't use it for the map. Eye-tracked offsets
(`calc_offset`, `0x1b848`) are `gaze NDC x 0.5 x render-area size`, rounded to the offset
granularity (8), measured from that same swapchain centre; `disable_offsets` (Fixed) also leaves
every image without `VK_IMAGE_CREATE_FRAGMENT_DENSITY_MAP_OFFSET_BIT_EXT` (added at `0x16afc`
only when offsets are on). A game that submits a smaller or shifted rect (dynamic resolution,
asymmetric eyes: AC Nexus rect=0,0 / 287,0 1726x1728 in 2016x1728, Asgard's Wrath 2 0,0 / 347,0
2237x2256 in 2592x2256) gets the sharp area off its view's centre, and the eye's optical axis
(asymmetric FOV, for example -1.03/+0.88 rad) isn't the rect centre either. FramePort's
`VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h, below Valve's layer) rewrites the
per-view offsets so the area sits on each eye.
- Turnip (freedreno) applies a density map per GMEM **bin**: it reads the map on the CPU at the
bin's centre (`tu_calc_frag_area`, texel size `2^ceil(log2(fb/map))` >= 32 px) and renders the
whole bin at that density. The visible sharp area is therefore the set of bins whose centre falls
in Valve's rectangle: the area's size snaps to the bin grid (bins are ~150-200 px here and get relatively
smaller as render resolution grows), so the same preset looks bigger or smaller and changes
shape between render scales. The bin snapping is the driver's design, not a setting; FramePort can't
pick the bins.
Per-game library preferences now store `level`. Legacy radius choices are translated on read
(and written as `level` on Save): nearest old nominal .30 → lo, .25 → med, .20 → hi,
with larger values clamped by nearest choice; zero or unknown/invalid values → Default.
This preserves approximate intent, not a claim that these presets implement those radii.
The translation preserves approximate intent, not a claim that these presets implement those radii.
Explicit `level` wins. Obsolete radius exports are removed from managed launchers.
For device verification, hold game scene, render resolution and gaze constant; compare explicit
`lo`, `med`, `hi` across fresh game starts (Default should match lo absent another runtime override).
First use Fixed for a stable comparison, then Eye-tracked to check gaze following. Enable
**Darken coarse fragments (test)** and adapter `snapshot=N` to capture submitted eye images headlessly
every N seconds (GLES only, left eye at quarter size); compare the undarkened center and peripheral bands. Existing adapter
every N seconds (GLES: left eye at quarter size; Vulkan: both eyes at half size, the whole swapchain layer, with
imageRect and FOV in the log); compare the undarkened center and peripheral bands. Existing adapter
builds need diagnostic support installed. Turn the test and snapshots off before benchmarking.
Agent `FOVEATION_ENV_KEYS` must include `"FOVE_LEVEL"` for launcher updates via `set_settings`.
`device.foveation` enables Valve's shared Vulkan injection and suppresses competing native foveation through
`foveation_fix=1`. Game settings stores mode, profile and test selection as a per-game library preference, outside shared recipes. This remains
`foveation_fix=1`. Game settings stores mode, profile and test selection as a per-game library preference, outside shared recipes. Foveation remains
experimental: complete images were confirmed in RE4, Batman, Deadpool, The Climb 2 and Asgard's Wrath 2,
not all Quest games or renderers. RE4 and Batman checks include title/calibration scenes; these are not
equivalent to extended gameplay coverage. Asgard gameplay was visually confirmed, without an on/off benchmark.
@@ -542,7 +599,7 @@ Batman additionally receives the verified ordinary shared-allocation patch, guar
OVRPlugin hash; unknown Batman versions fail explicitly rather than receiving unchecked binary offsets.
The Climb 2 uses a private `libvrapi_foveation.so` bridge, installed only into that game's APK after the
standard bridge patch. It retains images during held-focus visibility dips, separates mono overlay output
standard bridge patch. The private bridge retains images during held-focus visibility dips, separates mono overlay output
storage from stereo density-map storage, and rate-limits its view-cache writes. Its process-local Valve layer
correction supplies both eye offsets and avoids the invalid subpass mask-table reads seen during Alps loading;
`yflip` corrects vertical gaze direction. The renderer SHA-256 and 1.0 resolution scale are checked before
@@ -552,7 +609,7 @@ the reason. No system library is changed. A SteamOS update requires revalidation
Both-eye gaze following was confirmed in Climb's darkening diagnostic, but long-session stability remains
experimental. Off disables injection through launcher exports; the private APK compatibility support remains installed.
**Darken coarse fragments (test)** enables the dormant Vulkan shader diagnostic. It dims RGB where the
**Darken coarse fragments (test)** enables the dormant Vulkan shader diagnostic. The diagnostic dims RGB where the
actual `FragSizeEXT` footprint exceeds one pixel; alpha is unchanged. Deadpool's shaded regions were confirmed
to follow gaze in the headset. Unsupported shader output interfaces are skipped, and multiple passes may
compound darkening, so this is a diagnostic rather than a precise composited mask. Do not benchmark with it on.
@@ -561,7 +618,26 @@ in Climb the same library also carries the compatibility correction.
High-resolution Deadpool samples crossed from about 45 to 90 FPS with injection enabled, but temperatures and
power rose as it rendered twice as many frames. Foveation does not guarantee lower temperature at uncapped FPS.
Preset export/migration has configuration coverage; these preset comparisons have not been device-tested here.
Preset export/migration has configuration coverage. Headless device check of the presets (2026-10-24, AC Nexus,
Fixed, darkening test + FrameBridge `snapshot=N`, which also reads Vulkan eye images back): DEVICE_NOTES.md
"Foveation: sharp area centre and size".
### User preferences (discussion #365)
Game settings → Picture provides Default / Eye-tracked / Fixed / Off, a runtime profile, and an advanced diagnostic switch.
`set_settings` atomically rewrites just the managed launcher exports and records them in deployment.json;
no APK rebuild, reinstall or live device intervention is needed. Changes apply at the next start. Default restores
recipe/catalog launcher defaults; explicit preferences win. One-time library migration extracts old recipe choices
and leaves internal APK compatibility support. Catalog defaults (including Climb 2 and Batman) still apply to new
entries. Shared working configs omit `device.foveation` entirely.
APK-side: adapter builds automatically prepare Batman's hash-verified allocation fix and Climb 2's
renderer-guarded private bridge for verified builds, independently of the runtime choice. Unknown versions skip
automatic compatibility edits; explicit catalog requests still fail validation rather than guessing offsets. FrameBridge
builds include the dormant diagnostic layer (activated only by `FRAMEPORT_FOVEATION_TEST=1`), so future test toggles
need only a launcher update. Existing builds need one normal update to acquire that support. Render resolution
(supersampling) changes the game's render resolution: higher is sharper but slower. Climb 2 foveation still requires
1.0 scale.
## Monitoring sources (probed 2026-10-07, SteamOS 0.4.3, kernel 6.18; used by the Monitor tab, agent `_monitor`)
@@ -570,7 +646,7 @@ All readable by the steamos user without root; the agent reads them directly (no
| Metric | Source | Notes |
|---|---|---|
| CPU | `/proc/stat` deltas; `cpufreq/policy{0,2,5,7}` | 8 cores in 4 clusters: 0-1 / 2-4 / 5-6 / 7 (max 2.27 / 3.15 / 2.96 / 3.05 GHz) |
| GPU busy | Sum of `drm-engine-gpu` (ns) deltas in `/proc/<pid>/fdinfo/<fd>` over every render-node fd (msm DRM) | Gives GPU % per process too. A process can hold several render fds: add them all. Lepton games see the node as `/dev/kgsl-3d0` (bind mount of `/dev/dri/renderD128`). `drm-total-memory` reads 0. msm can report garbage engine time (SteamVR's vrcompositor: near 2^64, rising ~1000x faster than real time; idle Frame at 100 %, 2026-10-18): a per-fd delta longer than the interval falls back to `drm-cycles-gpu` / GPU clock (agent v102 `gpu_busy_ns`) |
| GPU busy | Sum of `drm-engine-gpu` (ns) deltas in `/proc/<pid>/fdinfo/<fd>` over every render-node fd (msm DRM) | Gives GPU % per process too. A process can hold several render fds: add them all. Lepton games see the node as `/dev/kgsl-3d0` (bind mount of `/dev/dri/renderD128`). `drm-total-memory` reads 0. msm can report garbage engine time (SteamVR's vrcompositor: near 2^64, rising ~1000x faster than real time; idle Frame at 100 %, 2026-10-18): a per-fd delta longer than the interval falls back to `drm-cycles-gpu` / GPU clock (agent `gpu_busy_ns`) |
| GPU clock | `/sys/class/devfreq/3d00000.gpu/{cur,max}_freq` | 231–903 MHz in 12 steps |
| Memory / pressure | `/proc/meminfo`; `/proc/pressure/{cpu,memory,io}` (`some avg10`) | 16 GB RAM, 8 GB swap |
| Temperatures | `/sys/class/thermal/thermal_zone*/{type,temp}` | 48 zones: `cpu*`/`cpuss*`, `gpuss-*`, `ddr`, `nsph*` (NPU), `pm8550*`/`pm8010*` (power ICs), `modem*`, `camera*`, `video`, `max1720x_bat*` |
@@ -586,26 +662,9 @@ game processes without one), temperatures every 2 s, the CPU-cluster power rails
2-10 s, the stream measured ~10-13 ms CPU per 1 s tick on the device (idle clocks; idle or with 4XVR running) = about
1 % of one core, ~0.15 % of the whole CPU. The I2C sensors (power monitors, battery gauge) are the slowest reads.
### Foveation user preferences (discussion #365)
Game settings → Picture provides Default / Eye-tracked / Fixed / Off, a runtime profile, and an advanced diagnostic switch.
`set_settings` (agent 98) atomically rewrites just the managed launcher exports and records them in deployment.json;
no APK rebuild, reinstall or live device intervention is needed. Changes apply at the next start. Default restores
recipe/catalog launcher defaults; explicit preferences win. One-time library migration extracts old recipe choices
and leaves internal APK compatibility support. Catalog defaults (including Climb 2 and Batman) still apply to new
entries. Shared working configs omit `device.foveation` entirely.
APK-side: adapter builds automatically prepare Batman's hash-verified allocation fix and Climb 2's
renderer-guarded private bridge for verified builds, independently of the runtime choice. Unknown versions skip
automatic compatibility edits; explicit catalog requests still fail validation rather than guessing offsets. FrameBridge
builds include the dormant diagnostic layer (activated only by `FRAMEPORT_FOVEATION_TEST=1`), so future test toggles
need only a launcher update. Existing builds need one normal update to acquire that support. Render resolution
(supersampling) changes the game's render resolution: higher is sharper but slower. Climb 2 foveation still requires
1.0 scale.
## Steam library scheduling
Agent v99 holds automatic shortcut restarts while the local app's
The agent holds automatic shortcut restarts while the local app's
`~/.local/share/frameport-app/gui.alive` heartbeat is under ten seconds old, as well as while a game or Desktop Mode
is open. Closed/crashed GUIs expire; the existing twelve-hour hold limit still applies.
@@ -625,10 +684,10 @@ mode isn't `later` and nothing is pending, and by purge; an otherwise empty `ste
SteamOS 0.3.0 facts (checked 2026-10-16): Steam is the static user unit `/usr/lib/systemd/user/steam.service`
(Type=simple, Restart=always, TimeoutStartSec 90 s, its own `-` ExecStartPre lines; `extra_args.conf` drop-ins are
Valve's documented way to extend it), systemd 257; no user drop-ins exist by default.
This automatic application targets SteamOS's `steam.service`; if Steam is started outside that service, use
The automatic reapplication targets SteamOS's `steam.service`; if Steam is started outside that service, use
FramePort's Restart Steam now action to apply the journal safely.
Off skips new shortcuts (existing entries remain). Play in Off, and Play for a pending Later entry, starts launch.sh
in a detached user systemd unit, just as launch tests do, without adding a devkit entry. Steam/SteamVR must already
be running. This loses Steam's per-game overlay/controller setup and Exit game integration; use FramePort Monitor
to stop the game. This direct path has unit coverage; headset input/visual behaviour needs device validation.
be running. Direct starts lose Steam's per-game overlay/controller setup and Exit game integration; use FramePort Monitor
to stop the game. The direct path has unit coverage; headset input/visual behaviour needs device validation.
+15 -11
View File
@@ -5,7 +5,7 @@ setup changes on the Steam Frame, how to undo it and what your network needs.
## Changes on the Frame
The setup runs [`bootstrap/bootstrap.sh`](../bootstrap/bootstrap.sh), which FramePort sends from your PC. It changes:
The setup runs [`bootstrap/bootstrap.sh`](../bootstrap/bootstrap.sh), which FramePort sends from your PC. The script changes:
| Change | Where | How to undo |
|---|---|---|
@@ -14,14 +14,14 @@ The setup runs [`bootstrap/bootstrap.sh`](../bootstrap/bootstrap.sh), which Fram
| Stops game starts from failing after about 200 launches (a limit in podman, the tool Lepton runs games with). | `[containers]` / `keyring = false` in `~/.config/containers/containers.conf`. | Remove those lines. |
| Asks Steam to install **Lepton** (Valve's Android runtime) if it's missing. You confirm it in Steam. | Steam library | Uninstall it in Steam. |
Valve's Developer Mode helper is the same one the Settings switch uses. It turns on remote login (SSH), the service
Valve's Developer Mode helper is the same one the Settings switch uses. The helper turns on remote login (SSH), the service
that makes the Frame findable on your network, remote desktop, debugging and crash dumps; turning Developer Mode off
turns them all off again.
The script runs as your user: no root, no `sudo`, no password. Nothing else on the system changes. It also leaves two
files: `~/.cache/frameport-setup.sh` (the part that finishes on its own) and its log.
The script runs as your user: no root, no `sudo`, no password. Nothing else on the system changes. The script also leaves
two files: `~/.cache/frameport-setup.sh` (the part that finishes on its own) and its log.
If Developer Mode can't be turned on automatically, the script asks you to turn it on in Settings → System →
If Developer Mode can't be turned on automatically, the script asks you to turn Developer Mode on in Settings → System →
**Enable Developer Mode** and run the setup line again.
**Later, FramePort adds** (as your user):
@@ -29,15 +29,19 @@ If Developer Mode can't be turned on automatically, the script asks you to turn
- the games, each with a launcher and its data in `~/Applications/quest-frame/<package>/`;
- their Steam library entries and artwork (`shortcuts.vdf` + `config/grid/`);
- for PC VR games: an OpenXR layer (`~/.local/share/openxr/1/api_layers/explicit.d/XR_APILAYER_FRAMEPORT_timefix.json`)
and Valve's ARM64 Proton with its Steam Linux Runtime (Steam downloads them and restarts once).
and Valve's ARM64 Proton with its Steam Linux Runtime (Steam downloads them and restarts once); for x86_64 Linux
apps: Valve's FEX;
- for Linux apps: entries in Desktop Mode's app menu (`~/.local/share/applications/frameport-*.desktop`);
- only when used: a user service that starts SteamVR overlay apps (`frameport-vr-overlays.service`) and, with
**Steam library: Later**, a drop-in for Steam's service (`~/.config/systemd/user/steam.service.d/`).
**Settings → Uninstall FramePort → Also remove from the Frame** deletes the games, their Steam entries, the helper
folder, the OpenXR layer and the setup files. Developer Mode, the login line, the podman setting, Lepton and Proton
folder, the OpenXR layer, the menu entries, the services and the setup files. Developer Mode, the login line, the podman setting, Lepton and Proton
stay: undo them as shown above.
## Network and firewalls
The setup is the only time the Frame connects to your PC. It downloads the setup script from FramePort on TCP port
The setup is the only time the Frame connects to your PC. The Frame downloads the setup script from FramePort on TCP port
8765 (8766 or 8767 if taken), only while the setup page is open and for at most 30 minutes. Everything else goes
from your PC to the Frame.
@@ -51,15 +55,15 @@ How the setup line finds your PC:
The setup line's script is [bootstrap/setup.sh](../bootstrap/setup.sh). If the setup just says "timed out", the
setup page shows what is likely blocking it after about 45 seconds:
- **Windows:** allow FramePort when Windows asks. On a network Windows treats as **Public** it stays blocked; set
- **Windows:** allow FramePort when Windows asks. On a network Windows treats as **Public** the connection stays blocked; set
your home network to Private in Windows' network settings.
- **macOS:** with the firewall on (System Settings → Network → Firewall), allow incoming connections for FramePort
when asked.
- **Linux:** firewalld: `sudo firewall-cmd --add-port=8765/tcp` (until the next restart). ufw:
`sudo ufw allow 8765/tcp`, afterwards `sudo ufw delete allow 8765/tcp`.
- **WSL** (FramePort's Linux version on Windows): Windows blocks connections into WSL without asking. FramePort
adds a temporary firewall rule (one admin prompt) and removes it when setup is done or after 35 minutes. WSL must
adds a temporary firewall rule (one admin prompt) and removes the rule when setup is done or after 35 minutes. WSL must
share Windows' network: `networkingMode=mirrored` under `[wsl2]` in `%UserProfile%\.wslconfig`, then
`wsl --shutdown`.
- Or skip the setup line and use **Pair new host** (see [Install and first steps](INSTALL.md#connecting-the-steam-frame)):
it needs no connection into your PC.
**Pair new host** needs no connection into your PC.
+31
View File
@@ -13,10 +13,12 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Arkio | Quest | ✅ Works | |
| Asgard's Wrath 2 | Quest | ✅ Works | |
| Assassin's Creed Nexus | Quest | ✅ Works | |
| Astro VR Host | Quest | ✅ Works | |
| Attack on Titan VR: Unbreakable | Quest | ✅ Works | |
| Audica | Quest | ✅ Works | |
| AugmentedEmpire | Quest | ✅ Works | |
| BAM | Quest | ✅ Works | |
| Barbaria | Quest | ✅ Works | |
| BARTENDER VR SIMULATOR | Quest | ✅ Works | |
| Batman: Arkham Shadow | Quest | ✅ Works | |
| BattleGlide | Quest | ✅ Works | |
@@ -44,19 +46,24 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| cubism | Quest | ✅ Works | |
| cubism | Quest | ✅ Works | |
| Dance Central | Quest | ✅ Works | |
| Darksword | Quest | ✅ Works | |
| Deep Cuts | Quest | ✅ Works | |
| Deisim | Quest | ✅ Works | |
| Demeo | Quest | ✅ Works | |
| Demeter | Quest | ✅ Works | |
| Dinosaur Island | Quest | ✅ Works | |
| Doom3Quest | Quest | ✅ Works | |
| Down the Rabbit Hole | Quest | ✅ Works | |
| Drums Rock | Quest | ✅ Works | |
| Drunkn Bar Fight | Quest | ✅ Works | |
| Drunkn Bar Fight 2 - Early Access | Quest | ✅ Works | |
| Eleven Table Tennis | Quest | ✅ Works | |
| Eleven: Table Tennis VR | PC VR | ✅ Works | |
| EndoparasiticVR | Quest | ✅ Works | |
| Epic Roller Coasters | Quest | ✅ Works | |
| Espire 2 | Quest | ✅ Works | |
| Eye of the Temple | Quest | ✅ Works | |
| First Contact | Quest | ✅ Works | |
| FrenzyVR | Quest | ✅ Works | |
| Fruit Ninja | Quest | ✅ Works | |
| Fruit Salon | Quest | ✅ Works | |
@@ -68,12 +75,14 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Gunjack | Quest | ✅ Works | |
| H.U.N.T | Quest | ✅ Works | |
| Hitstream | Quest | ✅ Works | |
| Hungry Shark Evolution | Quest | ✅ Works | |
| I Am Cat | Quest | ✅ Works | |
| I Am Monkey | Quest | ✅ Works | |
| I Expect You To Die 2 | Quest | ✅ Works | |
| IEYTD3 | Quest | ✅ Works | |
| In Death: Unchained | Quest | ✅ Works | |
| Into The Radius 2 | Quest | ✅ Works | |
| Invasion | Quest | ✅ Works | |
| Job Simulator | Quest | ✅ Works | |
| Journey of the Gods | Quest | ✅ Works | |
| Jurassic World Aftermath Collection | Quest | ✅ Works | |
@@ -84,6 +93,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Lambda1VR | Quest | ✅ Works | |
| Laser Dance | Quest | ✅ Works | |
| LEGO® Bricktales | Quest | ✅ Works | |
| Little Nightmares VR | Quest | ✅ Works | |
| Lost Recipes | Quest | ✅ Works | |
| Lucky's Tale | Quest | ✅ Works | |
| Maestro | Quest | ✅ Works | |
@@ -96,6 +106,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Mobile Suit Gundam: Silver Phantom | Quest | ✅ Works | |
| Moss | Quest | ✅ Works | |
| Moss: Book II | Quest | ✅ Works | |
| Movie Deck for Plex | Quest | ✅ Works | |
| Munkr Funkr (The Legacy Days) | Quest | ✅ Works | |
| Nano | Quest | ✅ Works | |
| Neko Atsume Purrfect | Quest | ✅ Works | |
@@ -105,14 +116,19 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Ocean Rift | Quest | ✅ Works | |
| Operation Wolf Returns: First Mission VR | Quest | ✅ Works | |
| OrbusVR | Quest | ✅ Works | |
| Orcs Must Die: By The Blade | Quest | ✅ Works | |
| palazzo_santacruz | Quest | ✅ Works | |
| Path of the Warrior | Quest | ✅ Works | |
| Pistol Whip | Quest | ✅ Works | |
| Pistol Whip | PC VR | ✅ Works | |
| Please Don't Touch Anything | Quest | ✅ Works | |
| Port0r: Cop VR | Quest | ✅ Works | |
| Port0r: House of the Dead VR | Quest | ✅ Works | |
| Port0r: Rally VR | Quest | ✅ Works | |
| PowerWash Simulator VR | Quest | ✅ Works | |
| Punch Fit | Quest | ✅ Works | |
| QuestCraft | Quest | ✅ Works | |
| QuestZDoom | Quest | ✅ Works | |
| Racket: Nx | Quest | ✅ Works | |
| RacketPinball | Quest | ✅ Works | |
| RC Pilot Trainer | Quest | ✅ Works | |
@@ -129,10 +145,13 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Shoot The Zombirds VR | Quest | ✅ Works | |
| Shores of Loci | Quest | ✅ Works | |
| SilentSlayer | Quest | ✅ Works | |
| Simpsons Hit and Run VR | Quest | ✅ Works | |
| Smash Drums | Quest | ✅ Works | |
| Sniper Elite VR | Quest | ✅ Works | |
| Sniper Elite VR: Winter Warrior | Quest | ✅ Works | |
| Solero | Quest | ✅ Works | |
| Space Pirate Trainer Quest | Quest | ✅ Works | |
| SpaceControl | Quest | ✅ Works | |
| Spectre | Quest | ✅ Works | |
| Sports Scramble (Santa Cruz) | Quest | ✅ Works | |
| Star Wars Pinball VR | Quest | ✅ Works | |
@@ -145,10 +164,12 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Survival Nation | Quest | ✅ Works | |
| Surviving Mars: Pioneer | Quest | ✅ Works | |
| SynthRiders | Quest | ✅ Works | |
| SynthRiders | Quest | ✅ Works | |
| Table Troopers | Quest | ✅ Works | |
| Taskmaster | Quest | ✅ Works | |
| Tea For God | Quest | ✅ Works | |
| Test Quest Controllers | Quest | ✅ Works | |
| Test Quest MR | Quest | ✅ Works | |
| TetrisEffect | Quest | ✅ Works | |
| The Boys VR | Quest | ✅ Works | |
| The Climb | Quest | ✅ Works | |
@@ -165,6 +186,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| ToyTrains | Quest | ✅ Works | |
| TRIANGLE STRATEGY | Quest | ✅ Works | |
| Under Cover | Quest | ✅ Works | |
| UNDERDOGS | Quest | ✅ Works | |
| Vacation Simulator | Quest | ✅ Works | |
| Vader Immortal: Episode I | Quest | ✅ Works | |
| Vader Immortal: Episode II | Quest | ✅ Works | |
@@ -178,6 +200,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Wallace & Gromit in The Grand Getaway | Quest | ✅ Works | |
| Waltz of the Wizard: Extended Edition | Quest | ✅ Works | |
| Wander | Quest | ✅ Works | |
| Zero Caliber 2 | Quest | ✅ Works | |
| ZombielandVR | Quest | ✅ Works | |
| AFFECTED: The Manor | Quest | ⚠️ Works with issues | Gear VR game (32-bit only), runs through FramePort's 32-bit bridge (experimental). |
| Ancient_Dungeon | Quest | ⚠️ Works with issues | Multiplayer isn't available (offline). |
@@ -186,12 +209,15 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Does it Stack? | Quest | ⚠️ Works with issues | Mixed reality mode does not work (works on Demeo), everything else seems to be OK |
| Dungeons of Eternity | Quest | ⚠️ Works with issues | It can not connect to servers |
| End Space | Quest | ⚠️ Works with issues | Controls stop responding after the first mission loads. |
| Eternal Starlight | Quest | ⚠️ Works with issues | Voice commands do not appear to work. |
| Exploding Kittens VR | Quest | ⚠️ Works with issues | Requires online for most modes. |
| ExploreVR | Quest | ⚠️ Works with issues | The 3D world warps and shifts with head movement (wrong perspective). |
| FGO VR | Quest | ⚠️ Works with issues | Audio stuttering and popping sounds |
| Fisherman | Quest | ⚠️ Works with issues | Loading screens have corruption, but everything works otherwise. |
| GoldenEye VR | Quest | ⚠️ Works with issues | No sound; otherwise works great. |
| HardBullet | Quest | ⚠️ Works with issues | Loading screens (a cube overlay) are missing or corrupted up to FramePort 1.2.1; cube_quads (next release) shows them as six panels, not confirmed in the hea… |
| Homeworld: Vast Reaches | Quest | ⚠️ Works with issues | The game starts but after selecting to start a new game it seems to crash. |
| JKXR | Quest | ⚠️ Works with issues | Tested in the early part of Jedi Outcast. |
| Lawn Mowing Simulator VR | Quest | ⚠️ Works with issues | Left eye textures flicker only when mowing in specific areas. |
| MacacoVR | Quest | ⚠️ Works with issues | 2d loading screens are glitchy, otherwise main gameplay is fine |
| Majora's Mask VR | Quest | ⚠️ Works with issues | Audio on the file select screen sounds crunchy. |
@@ -199,6 +225,9 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
| Pinball FX VR | Quest | ⚠️ Works with issues | Plays; space warp off (fewer stutters). |
| Plezy | Quest | ⚠️ Works with issues | Some videos play with strong artifacts and wrong colours; the cause is unknown, possibly hardware video decoding. |
| Port0r: Time Crisis II VR | Quest | ⚠️ Works with issues | Gameplay runs well; cut scenes and the title and game-selection screens after inserting coins don't display properly. |
| Port0r: Time Crisis VR | Quest | ⚠️ Works with issues | Immersive mode shows a black screen (only the arm and gun); the flat and 3D screen modes work (game menu, setup button). |
| PrimedGun v2 | Quest | ⚠️ Works with issues | Vulkan via a statically linked Khronos OpenXR loader, which bypasses FrameBridge (the picture jumps). |
| Puzzling Places | Quest | ⚠️ Works with issues | Works fine, but pointers dont show well on main menu, easy to work around. |
| Quantum Void | Quest | ⚠️ Works with issues | no hands |
| Ragnarock | Quest | ⚠️ Works with issues | |
@@ -211,6 +240,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| The Light Brigade | Quest | ⚠️ Works with issues | Judder in weapons |
| The Thrill Of The Fight | Quest | ⚠️ Works with issues | Right eye doesn't draw the opponent or the referee. |
| Time Stall | Quest | ⚠️ Works with issues | Both eyes distort during movement (unresolved). |
| Umurangi Generation VR | Quest | ⚠️ Works with issues | The game freezes after taking a photo, while it saves it, which makes it hard to play. |
| Vampire: The Masquerade - Justice | Quest | ⚠️ Works with issues | Game runs fine and playable, on loading there's a random sphere in the middle of the sceen. |
| WiiCompiled VR | Quest | ⚠️ Works with issues | To add a game: in FramePort's Files tab, upload your .wcgame file to this game's storage, folder Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR… |
| Breachers | Quest | ❌ Doesn't run | Online-only multiplayer that needs Meta's online services, which aren't available on the Frame. |
@@ -218,5 +248,6 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Espire 1: VR Operative (Quest Edition) | Quest | ❌ Doesn't run | Mesa GL driver crash during texture upload. |
| GOLF+ | Quest | ❌ Doesn't run | Black screen. |
| HITMAN 3 VR: Reloaded | Quest | ❌ Doesn't run | Vulkan driver crash (freedreno), even without Valve layers. |
| PianoVision | Quest | ❌ Doesn't run | Starts, but needs camera hand tracking (Android games on the Frame only get hands built from the controllers) and USB MIDI from a piano, which the Frame's An… |
| Roblox | Quest | ❌ Doesn't run | Crashes on its first VR frame on the Frame. |
| Thief Simulator | Quest | ❌ Doesn't run | Quits at start with the game's own "installed from unknown source" message. |
+5 -5
View File
@@ -1,8 +1,8 @@
# Evaluating patch heuristics
`scripts/eval_heuristics.py` compares `engine.suggest(analysis, use_catalog=False)` with the bundled
`catalog/games/*.yaml` entries. It does not merge personal or remote catalog recipes into the benchmark.
It supports the original dumps command, or a precomputed `scripts/heuristics_snapshot.py` snapshot:
`catalog/games/*.yaml` entries. The script does not merge personal or remote catalog recipes into the benchmark.
The script supports the original dumps command, or a precomputed `scripts/heuristics_snapshot.py` snapshot:
```sh
.venv/bin/python scripts/eval_heuristics.py '<dumps folder>' --verbose
@@ -24,7 +24,7 @@ can move the cutoff. Very small strata can have no holdout. Unsupported entries
Ground truth contains catalog choices and patch registry defaults, with no inherited heuristic adapter settings.
Default expectations are determined per analysis, using each default patch's own detection in the same way as the
engine's initial heuristic pass. Thus a conditional default that is not detected (e.g. LTW on a game without LTW)
engine's initial heuristic pass. Thus a conditional default that is not detected (for example LTW on a game without LTW)
is not expected. Explicit catalog `overport_remove` and `frame_remove` entries remove expected defaults. An absent
expected default is represented as `remove:<patch id>`; enabled defaults do not inflate patch scores.
@@ -59,10 +59,10 @@ Before tuning a rule to remove an "extra", check which kind it is: a gap on an o
catalog (fit report), not a detection error. Before adding a patch to a working entry, weigh its benefit (does it
fix something the game hits?) against a rebuild prompt for everyone who installed it; adapter settings never need
adding (inherited). Results 2026-10-15: strict 5/65 exact, F1 73.8%; dated 39/65, F1 88.6% (dev 90.5%, holdout
79.5%). Remaining misses are one-game fixes found at runtime (e.g. Vader's gl_error_clear, BlazeRush's GL settings).
79.5%). Remaining misses are one-game fixes found at runtime (for example Vader's gl_error_clear, BlazeRush's GL settings).
An evaluation writes JSON even when its headline has differences, then exits 1 for compatibility with the original
script. It exits 0 when exact, or after producing a fit report.
script. The script exits 0 when exact, or after producing a fit report.
## New patch fit
+180 -193
View File
@@ -6,7 +6,7 @@ game on the Steam Frame.
[![The install tutorial](media/frameport-install.jpg)](media/frameport-install.mp4)
Download the file for your PC from the [latest release](https://github.com/spoopyghosty0/frameport/releases/latest)
and unpack it anywhere. No installer or admin rights are needed.
and unpack it anywhere. No installer or admin rights needed.
| Your PC | Download | Start |
|---|---|---|
@@ -14,193 +14,187 @@ and unpack it anywhere. No installer or admin rights are needed.
| macOS (Apple Silicon) | `FramePort-macos-arm64.zip` | `FramePort.app` |
| Linux (x64, Ubuntu 22.04 or newer) | `FramePort-linux-x64.tar.gz` | `FramePort/FramePort` |
| Linux (ARM64, Ubuntu 22.04 or newer) | `FramePort-linux-arm64.tar.gz` | `FramePort/FramePort` |
| Arch Linux (AUR, community package) | [`frameport-bin`](https://aur.archlinux.org/packages/frameport-bin) | `frameport` |
On first start FramePort downloads the tools it uses (Settings → Tools shows them).
On first start FramePort downloads the tools it uses (listed in Settings → Tools).
## 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**.
- **macOS:** right-click `FramePort.app` → **Open** → **Open** (once).
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort`.
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort`. Arch Linux: `yay -S frameport-bin`.
## Connecting the Steam Frame
The Frame and your PC must be on the same network (or connected with a [USB cable](#with-a-usb-cable)).
The Frame and your PC must be on the same network, or connected with a [USB cable](#with-a-usb-cable).
1. In FramePort open **Steam Frame** and click **Start setup**. Keep that page open.
2. On the Frame, first time only:
1. Open the **SteamVR dashboard → Launch a program → Desktop**. The Frame's desktop opens.
2. Open the app menu (bottom left) → **System → Konsole**, the Frame's terminal.
3. Type this setup line and press **Enter** (on-screen keyboard or any USB or Bluetooth keyboard):
2. On the Frame (first time only):
1. Open **SteamVR dashboard → Launch a program → Desktop**.
2. Open the app menu (bottom left) → **System → Konsole**.
3. Type this line and press **Enter**:
```
curl -fsSL https://frameport.app/s | bash
```
No keyboard? Open [the setup page](https://frameport.app/setup/) in Chromium on the Frame,
tap **Copy** and paste it into Konsole.
4. Konsole shows a 4-digit code. When FramePort shows the same code, click **Allow**. Nothing changes on the Frame
before that.
5. Steam restarts once and the desktop closes. If Steam asks to install **Lepton** (Valve's Android runtime),
confirm it.
No keyboard? Open [the setup page](https://frameport.app/setup/) in Chromium on the Frame, tap **Copy** and
paste the line into Konsole.
4. Konsole shows a 4-digit code. When FramePort shows the same code, click **Allow**. Nothing changes before that.
5. Steam restarts once. If Steam asks to install **Lepton** (Valve's Android runtime), confirm.
FramePort connects within a minute. No password is needed. The setup turns on **Developer Mode**;
[What the setup changes](FRAME_SETUP.md) lists everything.
FramePort connects within a minute, without a password, and from then on by itself. The setup turns on **Developer
Mode** (Settings → System on the Frame); keep Developer Mode on. [What the setup changes](FRAME_SETUP.md) lists everything.
Later, FramePort connects to the Frame by itself. If you turn Developer Mode off (Settings → System → **Enable
Developer Mode**), turn it on again there.
**Setup command:** if your network blocks FramePort's search, click **Use the setup command**. It shows a line with
your PC's address and a one-time code, for example `curl -fsS 192.168.1.20:8765/1a2b3c4d | bash`. Run it in Konsole instead.
**Without Konsole:** turn on Developer Mode yourself (Settings → System → **Enable Developer Mode**), then open
Settings → Developer → **Pair new host** on the Frame. FramePort finds the Frame and asks to connect; approve it in the
headset. Install Lepton from FramePort's Steam Frame page afterwards if it's missing.
- **Network blocks the search?** Click **Use the setup command** and run the line FramePort shows in Konsole
instead (the line holds your PC's address and a one-time code).
- **Without Konsole:** turn on Developer Mode, then open Settings → Developer → **Pair new host** on the Frame.
Approve FramePort's request in the headset. Install Lepton from FramePort's Steam Frame page if Lepton is missing.
### With a USB cable
A cable works on networks that block the setup, and uploads are faster (about 37 MB/s, three times typical Wi-Fi).
A cable works on networks that block the setup, and uploads are about three times faster than on typical Wi-Fi.
1. On the Frame, turn on Developer Mode (Settings → System → **Enable Developer Mode**). The cable only works in
Developer Mode.
2. Connect the Frame's USB-C port to your PC. No driver is needed.
3. In FramePort click **Steam Frame → Set up with a USB cable** and follow the steps.
1. On the Frame, turn on Developer Mode (Settings → System). The cable only works in Developer Mode.
2. Connect the Frame's USB-C port to your PC. No driver needed.
3. Click **Steam Frame → Set up with a USB cable** and follow the steps.
Uploads use the cable whenever it's plugged in. Unplug it any time: FramePort finds the Frame on Wi-Fi again.
Uploads use the cable while the cable is plugged in. You can unplug the cable any time.
### Firewalls
If the setup only says "timed out", a firewall on your PC blocks the Frame. After about 45 seconds the setup page
says which. Details: [Network and firewalls](FRAME_SETUP.md#network-and-firewalls).
says which one. Details: [Network and firewalls](FRAME_SETUP.md#network-and-firewalls).
## Running FramePort on the Frame (experimental)
FramePort can run on the Frame itself, without a PC:
1. Turn on Developer Mode (Settings → System).
2. In Desktop Mode, download `FramePort-linux-arm64.tar.gz`, unpack the file and start `FramePort/FramePort`.
1. Turn on Developer Mode (Settings → System → **Enable Developer Mode**).
2. In Desktop Mode, download `FramePort-linux-arm64.tar.gz`, unpack it (`tar xzf FramePort-linux-arm64.tar.gz`) and
start `FramePort/FramePort`.
Games appear in Steam once you close FramePort and return to Gaming Mode ([when games appear in
Steam](#when-games-appear-in-steam)).
With the default **Steam library: Now**, games appear after you close FramePort and return to Gaming Mode.
FramePort also waits while its window is open in Gaming Mode, so you can install several games in one session.
Please report anything odd with **Report a problem…**.
**Moving from a PC:**
Settings → Installing → **Steam library** controls future installs:
- **Now** (default): restart Steam after adding shortcuts, waiting while a game, Desktop Mode, or FramePort's local
window is open (up to 12 hours). Unchanged shortcuts need no restart.
- **Later**: save shortcuts without restarting Steam. Keep installing; changes are applied at the next Steam
service start, including after a reboot. **Restart Steam now** applies them immediately. It closes Desktop Mode
and FramePort running on the Frame, and asks again if a game is running.
- **Off**: skip new Steam shortcuts; existing entries stay. FramePort's **Play** runs the installed launcher directly.
Steam and SteamVR must already be running. Steam's per-game overlay, controller setup, and Exit game integration
are unavailable; stop the game from FramePort's Monitor. Pending games in Later also use this direct Play path.
CLI: `frameport frame steam-library now|later|off` saves the setting and applies it to the connected Frame.
`frameport frame restart-steam` applies pending entries now; `--yes` confirms ending a running game.
These settings concern game shortcuts; installing a missing Steam runtime such as Proton can still require a restart.
1. First copy the signing keys: the `<package>.keystore` files from `overport-workspace/signatures/` in FramePort's
data folder (Windows: `%LOCALAPPDATA%\FramePort`) to
`~/.local/share/frameport-app/overport-workspace/signatures/` on the Frame. Without them, updates can't install
over your games and progress tied to a Meta account can look gone.
2. Put the original game files on the Frame and add that folder to the Library there. The PC's library doesn't
carry over. Data files on the same drive as the installs take space only once.
3. Installed games keep working, and reinstalling them from the Frame keeps their saves. Don't manage a game from
both the PC and the Frame unless both have the same keys.
## Installing games
![Game page](images/game.png)
- **Install on Frame** on a game's page, or select several games in the Library and install them together. Installs
run in the background; **Activity** shows the current one.
- **Install on Frame** on a game's page, or select several games in the Library. Installs run in the background;
**Activity** shows the current one.
- **Update all** updates every game whose build changed, for example after a FramePort update.
- If the Frame sleeps or leaves the Wi-Fi, installs wait and continue when it's back. While installs run, the Frame
stays awake.
- Your own game files are never changed. The patched copy is deleted once the game is on the Frame (Settings →
Installing).
- In the downloaded app you can also **drag files onto the Library**: games, Linux apps, Windows programs or folders.
- If the Frame sleeps or drops off the network, installs wait and continue. The Frame stays awake while they run.
- Your own game files are never changed.
- In the downloaded app you can **drag files onto the Library**: games, Linux apps, Windows programs or folders.
- App bundles (`.xapk`, `.apks`, `.apkm`, `.apkx`, split APKs) are added like an APK:
[FAQ](FAQ.md#can-i-add-xapk-apks-apkm-or-apkx-files-app-bundles).
**Game settings…** (in the game's menu) shows the settings that matter for that game in plain words: sharpness,
refresh rate, controllers, menus, 360° video and mixed reality. Changes are used the next time the game starts.
A game's **menu** opens with a right-click in the Library or the **…** button on the game's page. The menu's groups
are
**Update**, **On the Frame**, **Saves**, **Name and artwork**, **Recipe** and **Troubleshoot**.
- **Game settings…** shows the settings that matter for that game: sharpness, refresh rate, controllers, menus,
360° video and mixed reality. Changes apply the next time the game starts.
- **Name and artwork → Rename…** renames the game in the Library and in Steam. The game's Steam entry, artwork and
saves stay.
![Game settings](images/game-settings.png)
**Rename…** (in the game's menu) changes the name in the Library and in Steam; FramePort keeps it from then on. An
installed game keeps its Steam entry, artwork and saves (Steam restarts once).
### When games appear in Steam
Steam reads new games only when Steam starts. Settings → Installing → **Steam library** sets when Steam restarts:
- **Now** (default): Steam restarts after an install. The restart waits while a game, Desktop Mode or FramePort on the Frame
is open (up to 12 hours).
- **Later**: new games appear at Steam's next start, also after a reboot. **Restart Steam now** adds them at once
(the restart closes Desktop Mode and FramePort on the Frame, and asks first if a game is running).
- **Off**: no new Steam entries; existing ones stay. FramePort's **Play** starts the game directly. Steam and SteamVR
must be running, and Steam's overlay, controller setup and Exit game don't work for the game: end the game from **Monitor**.
Games waiting for **Later** start this way too.
Command line: `frameport frame steam-library now|later|off`, `frameport frame restart-steam [--yes]`. Installing
Proton can still restart Steam.
### microSD cards and other drives
- The **Steam Frame** page's **Storage** section sets where new games go (**Install new games to**).
- To move an installed game, right-click it → **Move to…**. The game must be closed; saves and the Steam entry stay.
- **Steam Frame → Storage → Install new games to** sets where new games go.
- **On the Frame → Move to…** in a game's menu moves the game. Close the game first; saves and the Steam entry stay.
- A game on a card only starts while the card is inserted.
- Cards formatted as FAT, exFAT or NTFS can't hold games: format the card in SteamOS first.
- Cards formatted as FAT, exFAT or NTFS can't hold games. Format the card in SteamOS first.
### Why do the controller buttons repeat or do the wrong thing?
### Controller buttons
SteamVR maps Quest Touch onto the Frame's controller buttons by default: three top buttons repeat Touch Y/B.
Choose **Game settings → Controllers → Controller buttons** to use Mirror or Quest pair for one game, or
**Settings → Controllers** for the library default. **Apply to installed games now** sends the default to Quest
games without an override. Changes take effect at the next launch.
See [Controller buttons](CONTROLLERS.md) for the diagram, mappings and limits.
Buttons repeat or do the wrong thing? **Game settings → Controllers → Controller buttons** picks **Mirror** or
**Quest pair** for one game. **Settings → Controllers** sets the default; **Apply to installed games now** sends the
default to games without their own choice. Details: [Controller buttons](CONTROLLERS.md).
### Android apps and Windows programs
- Android apps without VR are installed unchanged and shown as a window in the headset. If one shows nothing (or a
VR app opens as a window), open **Customize** on the game's page, set **Show as VR or as a flat window** and click
**Update on Frame**.
- **Add games → Add a Windows program…** adds a single Windows program. The Frame runs it as a window through Proton
(Valve's tool for running Windows programs).
- Android apps without VR are installed unchanged and shown as a window. Shows nothing, or a VR app opens as a
window? On the game's page: **Customize** → **Show as VR or as a flat window** → **Update on Frame**.
- **Add games → Add a Windows program…** adds one Windows program. The Frame runs the program as a window through Proton.
### Install links ("Install with FramePort" buttons)
Some websites have an **Install with FramePort** button (a `https://frameport.app/install?…` link):
Some websites have an **Install with FramePort** button (a `https://frameport.app/install?…` link).
- **Click a button** on Windows or Linux: FramePort opens, shows what it would download and asks first. Then it adds
the game and installs it on the Frame.
- **Add games → Add from a link…** takes a button's address (right-click → Copy link) or a direct download link. Use
it on macOS, where buttons can't open FramePort yet.
- **Click the button** (Windows, Linux): FramePort opens, shows what FramePort would download and asks first.
- **Add games → Add from a link…** takes a button's address (right-click → Copy link) or a direct download link.
Use **Add from a link…** on macOS, where buttons can't open FramePort yet.
- **Settings → Install links** turns the buttons on or off.
- Only `https://` links to public servers are used. Only install from sites you trust.
- Only `https://` links to public servers work. Only install from sites you trust.
Website owners: see [Install button](INSTALL_BUTTON.md).
Website owners: [Install button](INSTALL_BUTTON.md).
## Typing on the Frame
- **Type on Frame** (in the sidebar): while this tab is open, your PC's keyboard types on the Frame. Select a text
field in the headset and type, or paste longer text into the box.
- **Steam's on-screen keyboard** works in apps shown as a window. Steam lists that window as **Gamescope**: leave it
open.
- **Unity games whose text fields close at once:** FramePort suggests the patch **Make Unity text fields work**.
Games added before this patch existed: open the game's menu → **Analyze again**, then reinstall.
- **Type on Frame** (sidebar): while the tab is open, your PC's keyboard types on the Frame. Select a text field in
the headset and type, or paste text into the box.
- **Steam's on-screen keyboard** works in apps shown as a window. Steam lists that window as **Gamescope**: leave the
window open.
- **Unity text fields close at once?** Use the patch **Make Unity text fields work**. Not offered for an older game?
**Troubleshoot → Analyze again**, then **Update on Frame**.
![Type on Frame](images/type-on-frame.png)
## Watching the Frame
- **Monitor** (in the sidebar) shows the running game's frame rate, CPU, graphics, memory, temperature, power and
battery, each with a 2-minute chart. **Show details** shows more.
- Under **Processes**, right-click a process to end it. **End game** closes the game like Steam's Exit game. A lock
marks programs Steam or the desktop needs; FramePort asks again before ending those.
- **Live view** streams what the headset shows, with sound, to your browser. It uses about one CPU core of the Frame,
so stop it when you're done.
- **Screenshots** shows the screenshots you took in the headset, sorted by game and day, to view or download.
- **Monitor** (sidebar): the running game's frame rate, CPU, graphics, memory, temperature, power and battery.
**Show details** shows more.
- Under **Processes**, right-click a process to end the process. **End game** closes the game like Steam's Exit game.
FramePort asks again before ending programs Steam or the desktop needs (marked with a lock).
- **Live view** streams what the headset shows, with sound, to your browser. Live view uses about one CPU core of the
Frame: stop Live view when you're done.
- **Screenshots** shows your headset screenshots by game and day, to view or download.
## Your room (mixed reality, experimental)
Mixed-reality games ask for your room: walls, furniture and its surfaces. **Room** (in the sidebar) keeps it:
Mixed-reality games ask for your room: walls, furniture and surfaces. The **Room** tab keeps the room.
- **Scan room in the headset**, then put the headset on with passthrough and look around for about a minute; the tab
shows how far the scan is. **Cancel scan** stops it and keeps your saved room. **Use my play area** makes walls from
the SteamVR play area instead (no furniture).
- The room shows in 3D: drag to turn it, scroll to zoom, right-drag to move it, double-click to see all of it.
**Open full 3D view** shows every detail of the scan in your browser (a page on this PC only).
- **Clean-up** tidies every scan (flat walls, no scan behind walls or where you walked, no loose bits). The
recommended settings suit most rooms; change them (**More settings** has all of them) and press **Apply** to clean
up your saved scan again. Your settings are also used for your next scans.
- **Labels** shows what the scanner's boxes became and why. A tall, thin box by a wall can be **Add as door** or
**Add as closet**.
- **Furniture & screens**: pick a box in the list or click it in the 3D view to change its type, place or size, add
or delete boxes, and add doors and windows on a wall. **Save** sends them to the Frame; they stay when you clean up
the scan again (**Back to the scan's boxes** under Labels drops them).
- The free **Test Quest MR** app (Get Test Quest MR on the tab) shows what a game receives from your scan.
1. **Scan room in the headset**, put the headset on and look around for about a minute. **Cancel scan** keeps your
saved room. **Use my play area** makes walls only, from the SteamVR play area.
2. Check the room in 3D: drag to turn, scroll to zoom, right-drag to move, double-click to see the whole room. **Open full
3D view** shows every detail in your browser.
3. Fix the room if needed:
- **Clean-up** tidies every scan. The recommended settings suit most rooms; **Apply** re-cleans your scan with
changed ones (also used for later scans).
- **Labels** shows what each scanned box became and why. A tall, thin box by a wall can be **Add as door** or
**Add as closet**.
- **Furniture & screens**: click a box to change the box's type, place or size; add or delete boxes; add doors and
windows. **Save** sends them to the Frame. They survive a new clean-up (**Back to the scan's boxes** drops them).
The free **Test Quest MR** app (**Get Test Quest MR** on the tab) shows what a game receives.
![Room](images/room.png)
@@ -208,52 +202,48 @@ Mixed-reality games ask for your room: walls, furniture and its surfaces. **Room
## Updating
FramePort checks for a new version at start and every 6 hours. When there is one, the Library shows **Update now**:
FramePort downloads it, checks it, restarts and keeps your games and settings. **Later** skips that version.
FramePort checks for a new version at start and every 6 hours. **Update now** in the Library downloads the new
version, checks it and restarts FramePort; your games and settings stay. **Later** skips that version.
- Settings → **Updates** turns the check off or turns on **Install updates automatically**.
- **Recipes** (the tested patches and settings for each game) update by themselves. A game whose recipe changed shows
**Update on Frame**.
- **Moved your games?** Scan the new folder (or **Rescan folders**): games found there whose old files are gone keep their library entry, name, artwork, tags and settings, and only their location changes. **Settings → Library folders** lists the folders FramePort scans; remove the old one there (the games stay). See the [FAQ](FAQ.md#i-moved-my-games-to-another-folder-or-drive-how-do-i-update-the-library).
- **A newer version of a game:** right-click it (or use **…** on its page) → **Update with a newer APK…** (Quest and
Android games) or **Update with a newer version…** (Linux apps: the new AppImage, archive or folder; PC VR games:
the new game folder). Its name, artwork, tags and your settings stay. Then click **Update on Frame** (or **on this
PC**).
- **Dev builds:** if you're asked to test a change before its release, use Settings → Updates → **Install the latest
dev build…**. The next release then arrives as a normal update.
- Settings → **Updates** turns the check off or on, or turns on **Install updates automatically**.
- **FramePort doesn't start after an update** (Windows; an error such as `cannot import name …`): the update didn't
finish, and the program folder holds files of two versions. Close FramePort (check Task Manager), delete the
program folder, download `FramePort-windows-x64.zip` again and unpack the zip into an empty folder. Games,
settings and signing keys are stored in `%LOCALAPPDATA%\FramePort` and stay.
- **Recipes** update by themselves. A game whose recipe changed shows **Update on Frame**.
- **Moved your games?** Scan the new folder or click **Rescan folders**. Moved games keep their name, artwork, tags
and settings. [FAQ](FAQ.md#i-moved-my-games-to-another-folder-or-drive-how-do-i-update-the-library).
- **A newer version of a game:** game menu → **Update** → **Update with a newer APK…** (Quest and Android games) or
**Update with a newer version…** (Linux apps, PC VR games). Then **Update on Frame** (or **on this PC**). Name,
artwork, tags and settings stay.
- **Dev builds** (when asked to test a change): Settings → Updates → **Install the latest dev build…**.
- **AUR install:** update with your AUR helper (`yay -Syu`). FramePort can't update itself in `/opt`.
## PC VR games
PC VR games are Windows VR games. Scan a folder of them (one folder per game) or use **Add games → Add a PC game
folder…**. FramePort asks which program starts the game if it finds more than one.
Scan a folder of Windows VR games (one folder per game), or **Add games → Add a PC game folder…**. If a game has
several programs, FramePort asks which program starts the game.
- **Play from this PC** (Windows with Steam and SteamVR): **Install on this PC** adds the game to Steam. Stream it to
the Frame with Steam Link.
- **Play on the Frame (experimental):** on the Steam Frame page, install *PC VR games (Proton)*, then click **Install
on Frame** on the game's page.
- **Oculus games** (made for Meta's Rift headset) need Revive, which FramePort downloads. Games that check their
license through the Oculus app only run on your PC.
- **Play from this PC** (Windows with Steam and SteamVR): **Install on this PC** adds the game to Steam. Stream the
game to the Frame with Steam Link.
- **Play on the Frame (experimental):** install *PC VR games (Proton)* on the Steam Frame page, then **Install on
Frame**.
- **Oculus games** (made for Meta's Rift) need Revive, which FramePort downloads, and run only on your PC. Games that
check their license through the Oculus app need the Oculus app there.
**Windows games without VR:** use **Add games → Add a PC game folder…** too. The Frame runs them as a window through
Proton, like Steam's own Windows games. Whether a game runs depends on Proton.
**Windows games without VR:** also **Add a PC game folder…**. The Frame runs them as a window through Proton.
## Linux apps
**Add games → Add a Linux app…** takes an AppImage (a single-file Linux app) or a `.zip`/`.tar.gz`/`.tar.xz`
archive; **Add a Linux app folder…** takes an unpacked app. **Install on Frame** uploads it unchanged and adds it to
the Steam library.
**Add games → Add a Linux app…** takes an AppImage or a `.zip`/`.tar.gz`/`.tar.xz` archive; **Add a Linux app
folder…** takes an unpacked app. **Install on Frame** uploads the app unchanged and adds the app to Steam.
- Apps built for **arm64** (the Frame's processor) run directly. Apps built for **x86_64** (most PCs) run through FEX,
a translator Steam installs on the Frame the first time; they run slower. The game page shows which kind you have.
- If the app needs libraries the Frame doesn't have, the game page lists them: look for a build that includes them.
- Linux apps also appear in **Desktop Mode**'s app menu. Turn this off on the game
page (**Desktop Mode**).
- **SteamVR overlay apps** (a wrist watch, a performance display: programs that draw over games instead of being
one) are registered with the Frame's SteamVR, which shows them over whatever you play, Quest and PC VR games
alike. FramePort recognises them by the `.vrmanifest` file they ship. By default they start by themselves whenever
SteamVR starts (**SteamVR overlay → Start with SteamVR** on the game page; a small FramePort service on the Frame
does this, `frameport-vr-overlays`); otherwise start the app from your library before the game. Windows overlay
apps get the patch **SteamVR overlay app (Frame)**.
- **arm64** builds run directly. **x86_64** builds run through FEX, which Steam installs the first time; they're
slower. The game page shows which you have.
- Missing libraries are listed on the game page: look for a build that includes them.
- Linux apps also appear in Desktop Mode's app menu (switch **Desktop Mode** on the game page).
- **SteamVR overlay apps** (a wrist watch, a performance display) show over any game. They start with SteamVR
(**Start with SteamVR** on the game page). Windows overlay apps use the patch **SteamVR overlay app (Frame)**.
## Files on the Frame
@@ -261,27 +251,25 @@ The **Files** tab copies files between your PC and the Frame: videos, documents,
![Files](images/files.png)
- Use **Upload files**, **Upload folder** and **New folder**; right-click an entry to download, rename or delete
it. Drag across entries to select several.
- In the downloaded app you can drag files from your PC onto the list.
- **Videos**, **Downloads** and **Documents** are shared by every Quest game (inside the game: `/sdcard/Movies`,
`/sdcard/Download`, `/sdcard/Documents`).
- **Game storage** holds each game's own files. A game's menu → **Add videos and files…** opens it.
- **Upload files**, **Upload folder**, **New folder**; right-click to download, rename or delete. Drag across
entries to select several. In the downloaded app you can also drag files onto the list.
- **Videos**, **Downloads** and **Documents** are shared by every Quest game (`/sdcard/Movies`, `/sdcard/Download`,
`/sdcard/Documents` inside the game).
- **Game storage** holds each game's own files (game menu → **On the Frame** → **Add videos and files**).
## Backing up saves
Steam doesn't sync Quest games' saves. A game's menu (**…** on its page) → **Saves** → **Back up saves** copies them
to your PC (close the game first); **Back up all saves** on the Steam Frame page does every installed Quest game.
**Restore saves…** puts a backup back on the Frame and keeps a copy of the replaced saves there (**Undo last
restore**). Backups live in FramePort's data folder under `save-backups/`; the 10 newest per game are kept (Settings →
Data). Caches and large downloaded content stay out. More in the [FAQ](FAQ.md#can-i-back-up-a-games-saves).
Steam doesn't sync Quest games' saves. **Back up saves** next to **Play** copies them to your PC (close the game
first); **Back up all saves** on the Steam Frame page does every game. Game menu → **Saves** → **Restore saves…** puts
one back. Settings → **Data** sets the folder, how many to keep and automatic backups.
[FAQ](FAQ.md#can-i-back-up-a-games-saves).
## Share a recipe or report a problem
- **Share working recipe…** (in the game's menu) opens a GitHub issue with the game's recipe filled in. Accepted
recipes become built-in for everyone. Untested games ask how they run after you install or test them.
- **Report a problem…** (in the game's menu, or Settings → Problems and feedback) saves a diagnostics zip with
personal data removed and opens a GitHub issue to attach it to.
- **Share working recipe…** (game menu → **Recipe**) opens a GitHub issue with the game's recipe filled in. Accepted
recipes become built-in for everyone.
- **Report a problem…** (game menu → **Troubleshoot**, or Settings → Problems and feedback) saves a diagnostics zip
without personal data and opens a GitHub issue to attach the zip to.
Without the app: [share a recipe](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml)
· [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml).
@@ -289,45 +277,44 @@ Without the app: [share a recipe](https://github.com/spoopyghosty0/frameport/iss
## Uninstalling
Settings → **Uninstall FramePort…** removes FramePort's data and, if you choose, its games on the Frame (saves can be
kept). It first saves your signing keys to Documents: you need them to update your games later. Then delete the
kept). The uninstaller first saves your signing keys to Documents; you need them to update your games later. Then delete the
FramePort folder.
## Verify a download
`SHA256SUMS.txt` in each release lists the files' checksums: `sha256sum -c SHA256SUMS.txt`. Each file also has a
GitHub build attestation (proof that GitHub built it from this repository):
`gh attestation verify FramePort-windows-x64.zip -R spoopyghosty0/frameport`.
- `sha256sum -c SHA256SUMS.txt` checks the files against the release's checksums.
- `gh attestation verify FramePort-windows-x64.zip -R spoopyghosty0/frameport` checks that GitHub built the file from
this repository.
- Windows: to show FramePort as the publisher, import `FramePort-selfsigned.cer` (attached to each release) into
*Trusted Root Certification Authorities* (Current User). The certificate can only sign code; remove the certificate with `certmgr.msc`.
SHA-256 fingerprint:
`4E:12:98:91:62:C0:E4:50:FB:65:1D:34:BB:73:00:09:7B:78:BE:88:5C:A7:6C:42:23:46:9B:92:A1:59:A7:6E`.
Windows: to show FramePort as the publisher, import `FramePort-selfsigned.cer` (attached to each release) into
*Trusted Root Certification Authorities* (Current User). The certificate can only sign code; remove it with
`certmgr.msc`. Its SHA-256 fingerprint:
`4E:12:98:91:62:C0:E4:50:FB:65:1D:34:BB:73:00:09:7B:78:BE:88:5C:A7:6C:42:23:46:9B:92:A1:59:A7:6E`.
## Command line
## For power users: the command line
The **command-line version** (Python 3.11 or newer) installs from the release's
`frameport-<version>-py3-none-any.whl` with `uv tool install <link>` (or pipx or pip).
Everything the app does is also a `frameport` command; `frameport --help` and `frameport <command> --help` list every
option. The main ones:
The command-line version (Python 3.11 or newer) installs from the release's `frameport-<version>-py3-none-any.whl`
with `uv tool install <link>` (or pipx or pip). `frameport --help` and `frameport <command> --help` list every
option. The main commands:
| Command | What it does |
|---|---|
| `scan <folder>` / `list` / `show <game>` | add games, list the library, show a game's patches |
| `folders [--add <folder>] [--remove <folder>]` / `rescan` | list the library folders, add or remove one (its games stay); scan them again for new and moved games |
| `rename <game> "<name>"` | rename a game (`--reset`: back to the automatic name) |
| `folders [--add <folder>] [--remove <folder>]` / `rescan` | manage library folders; scan them again |
| `add-linux <path> [--exe <program>]` | add a Linux app (AppImage, folder or archive) |
| `new-version <game> <path>` | point a game at its newer version (APK, Linux app file/folder, PC VR game folder) |
| `recipe <game> --enable/--disable <patch>` | change a game's patches (`patches` lists them all) |
| `build <game>` / `install <game>` / `test <game>` | build, install on the Frame (`--to pc` for this PC), launch test |
| `new-version <game> <path>` | point a game at its newer version |
| `recipe <game> --enable/--disable <patch>` | change a game's patches (`patches` lists them) |
| `build <game>` / `install <game>` / `test <game>` | build, install on the Frame (`--to pc`: this PC), launch test |
| `session <game>` / `settings <game> <key>=<value>` | check the last play session; change a game setting without rebuilding |
| `frame discover` / `frame connect` / `frame info` | find, connect to and describe the Frame |
| `frame send <files> --dest videos` / `frame storage` / `frame cleanup` | copy files to the Frame, list where they can go, free space |
| `frame drives` / `frame move <game> --to <drive>` / `install --dest <drive>` | list the Frame's drives, move a game, install to a drive |
| `saves backup <game>\|--all` / `saves list <game>` / `saves restore <game> [n]` / `saves undo <game>` | back up Quest games' saves to this PC, restore one, undo the last restore |
| `frame send <files> --dest videos` / `frame storage` / `frame cleanup` | copy files to the Frame, list targets, free space |
| `frame drives` / `frame move <game> --to <drive>` / `install --dest <drive>` | list drives, move a game, install to a drive |
| `saves backup <game>\|--all` / `saves list` / `saves restore` / `saves undo` | back up and restore Quest games' saves |
| `tools status` / `tools install` | the tools FramePort downloads |
| `open-link "<link>"` | install from an install button's address or a download link (`--yes`, `--no-install`) |
| `open-link "<link>"` | install from an install link (`--yes`, `--no-install`) |
| `diag report <game>` / `share-recipe <game>` | report a problem / share a working recipe |
| `update` | update FramePort (`--check` only checks: exit code 10 = update available) |
| `update` | update FramePort (`--check`: exit code 10 = update available) |
| `uninstall-app` | uninstall FramePort |
Exit codes: 0 done, 1 failed, 2 wrong usage. `FRAMEPORT_DEBUG=1` shows full error details;
`FRAMEPORT_NO_UPDATE_CHECK=1` turns update checks off.
Exit codes: 0 done, 1 failed, 2 wrong usage. `FRAMEPORT_DEBUG=1` shows full errors; `FRAMEPORT_NO_UPDATE_CHECK=1`
turns update checks off.
+37 -45
View File
@@ -1,8 +1,7 @@
# Install button
A button for web pages and READMEs that opens a game straight in FramePort. Clicking it hands FramePort an install
link; FramePort shows what it would download and asks before it does anything (see
[Install links](INSTALL.md#install-links-install-with-frameport-buttons)).
A button for web pages and READMEs that opens a game in FramePort. FramePort shows what it would download and asks
before doing anything ([Install links](INSTALL.md#install-links-install-with-frameport-buttons)).
<p>
<img src="badges/install-with-frameport-animated.svg" alt="Install with FramePort" width="216" height="60">
@@ -12,27 +11,26 @@ link; FramePort shows what it would download and asks before it does anything (s
All files are in [`docs/badges/`](badges/). Each SVG is self-contained (no external fonts or images), 216 × 60.
| File | Use it for |
| File | Use for |
|---|---|
| [`install-with-frameport.svg`](badges/install-with-frameport.svg) | Everywhere. No motion. |
| [`install-with-frameport-animated.svg`](badges/install-with-frameport-animated.svg) | GitHub READMEs and other pages that only allow images. The glow plays by itself every 6 seconds. |
| [`install-with-frameport-hover.svg`](badges/install-with-frameport-hover.svg) | Websites where you can paste HTML. It glows while the pointer is on it, but only when its SVG code is in the page itself (see below). |
| [`install-with-frameport@2x.png`](badges/install-with-frameport@2x.png) | Places without SVG support (some forums, email). 432 × 120; show it at 216 × 60. |
| [`install-with-frameport-animated.svg`](badges/install-with-frameport-animated.svg) | GitHub READMEs and other image-only pages. Glows every 6 seconds. |
| [`install-with-frameport-hover.svg`](badges/install-with-frameport-hover.svg) | Websites where you can paste HTML. Glows under the pointer, only when the SVG code is in the page ([below](#on-a-website)). |
| [`install-with-frameport@2x.png`](badges/install-with-frameport@2x.png) | Places without SVG (some forums, email). 432 × 120; show the PNG at 216 × 60. |
Both animated versions stop moving for visitors who turn on reduced motion in their system settings.
The animated versions stand still for visitors who turn on reduced motion.
## The link
The button links to FramePort's install page, which names either a manifest or one file:
The button links to FramePort's install page with a manifest or one file:
```
https://frameport.app/install?manifest=<URL-encoded https address of a manifest .json>
https://frameport.app/install?url=<URL-encoded https address of an .apk, a Linux build (.zip, .tar.*, .AppImage) or a Windows .exe>
```
The page shows what the link installs, opens FramePort with the matching `frameport://` link and, when FramePort
isn't installed, offers the download. Pages that only target people who already have FramePort can link to the
`frameport://` form directly:
The page shows what the link installs, opens FramePort, and offers the download when FramePort isn't installed. For
people who already have FramePort you can link to the `frameport://` form directly:
```
frameport://install?manifest=<URL-encoded https address of a manifest .json>
@@ -51,26 +49,24 @@ The manifest:
}
```
`name` becomes the game's title in Steam. `sha256` is optional but recommended: FramePort checks the download
against it. The `frameport` object is optional; its icon and description are shown in the
install question. Only public `https://` addresses are accepted.
`version` is optional (it can also go inside the `frameport` object) and makes updates possible: FramePort remembers
the manifest's address and version, fetches the manifest again at start (at most every 6 hours) and, when its
`version` is newer, offers to download and install the update (Library bar and the game's page; CLI
`frameport link-updates [--install]`). Keep the manifest at the same address and raise `version` with every build.
Versions compare number by number (`1.10` > `1.9`), and a suffix sorts before the plain release (`1.2.0-beta2` <
`1.2.0`). Bare file links (`?url=…`) and manifests without a version are never checked.
- `name` becomes the game's title in Steam.
- `sha256` is optional but recommended: FramePort checks the download against it.
- `frameport` is optional: the icon and description are shown in the install question.
- `version` is optional (`version` can also go inside `frameport`) and enables updates. FramePort fetches the manifest again
at most every 6 hours and offers newer versions (Library bar, game page, `frameport link-updates [--install]`).
Keep the manifest at the same address and raise `version` with every build. Versions compare number by number
(`1.10` > `1.9`); a suffix sorts before the release (`1.2.0-beta2` < `1.2.0`). Bare file links (`?url=…`) and
manifests without a version are never checked.
- Only public `https://` addresses are accepted.
To build a link from Python: `frameport.deeplink.make_link(manifest_url="https://…/my-game.json")`.
**Where to host the manifest.** The install page shows the manifest's name, description and icon only when the
manifest's host lets web pages read it (CORS); otherwise it shows "A game or app" (FramePort itself reads any public
address). On GitHub, commit the manifest to the repository and link it as a file: `https://raw.githubusercontent.com/
<owner>/<repo>/main/my-game.json`, or the `https://github.com/<owner>/<repo>/blob/main/my-game.json` page address,
which the page and FramePort turn into the raw address. A manifest attached to a GitHub release
(`…/releases/download/…`) installs fine, but web pages can't read it: the page then shows the release's name from
GitHub's API instead.
manifest's host allows web pages to read the manifest (CORS); otherwise the page shows "A game or app". FramePort itself reads any public address.
On GitHub, commit the manifest and link the raw file
(`https://raw.githubusercontent.com/<owner>/<repo>/main/my-game.json`) or the file's `…/blob/main/my-game.json` page,
which FramePort turns into the raw address. A manifest attached to a GitHub release installs fine, but the page then shows the
release's name instead.
## On a website
@@ -91,21 +87,19 @@ give the link an accessible name:
</a>
```
Host the files yourself, or load them from jsDelivr, which serves them with the right content type:
`https://cdn.jsdelivr.net/gh/spoopyghosty0/frameport@main/docs/badges/install-with-frameport.svg` (replace `main`
with a release tag such as `v1.0.0` to pin a version; jsDelivr caches `@main` for up to 12 hours).
Host the files yourself, or load them from jsDelivr:
`https://cdn.jsdelivr.net/gh/spoopyghosty0/frameport@main/docs/badges/install-with-frameport.svg`. Replace `main`
with a release tag such as `v1.0.0` to pin a version (jsDelivr caches `@main` for up to 12 hours).
## In a GitHub README
GitHub removes links that don't start with `https://` or `http://` from READMEs, so a `frameport://` link there is
not clickable. Use the `https://frameport.app/install?…` link:
GitHub removes `frameport://` links from READMEs. Use the `https://frameport.app/install?…` link with the animated or
still SVG (the hover version doesn't react on GitHub):
```markdown
[![Install with FramePort](https://cdn.jsdelivr.net/gh/spoopyghosty0/frameport@main/docs/badges/install-with-frameport-animated.svg)](https://frameport.app/install?manifest=https%3A%2F%2Fexample.com%2Fmy-game.json)
```
Use the animated or still SVG: GitHub shows SVGs as images, so the hover version doesn't react there.
## Testing your setup
The example button on [frameport.app](https://frameport.app/#button) uses a demo link:
@@ -114,20 +108,18 @@ The example button on [frameport.app](https://frameport.app/#button) uses a demo
https://frameport.app/install/?manifest=https%3A%2F%2Fframeport.app%2Fdemo%2Fcool-game.json
```
Click it to check that FramePort receives install links on your PC. FramePort recognises this one address and shows
a demo install question for a placeholder game, "Cool Game"; nothing is downloaded, added to the library or
installed (`frameport open-link` says it's the demo link and stops). Real buttons install real games.
Click the button to check that FramePort receives install links on your PC. FramePort shows a demo install question for "Cool Game";
nothing is downloaded or installed (`frameport open-link` says the link is the demo and stops).
## If a visitor doesn't have FramePort
The `https://frameport.app/install?…` link handles this: its page offers the download and a second try. A bare
`frameport://` link does nothing visible (or shows the browser's "no app for this link" message) when FramePort
isn't installed; put a line next to such a button, for example "Needs [FramePort](https://frameport.app)". On macOS, web pages can't hand links to FramePort yet: users paste the link into Add games →
Add from a link….
The `https://frameport.app/install?…` link handles a missing FramePort: the install page offers the download. A bare `frameport://` link does
nothing visible without FramePort, so add a line such as "Needs [FramePort](https://frameport.app)". On macOS, web
pages can't hand links to FramePort yet: users paste the link into **Add games → Add from a link…**.
## Usage rules
- Keep the alt text "Install with FramePort".
- Don't recolor, stretch, crop or redraw the button, and don't show it smaller than 40 px tall.
- Leave some space around it (at least 8 px) so it doesn't touch other buttons.
- It works on light and dark pages as it is; there is no light version.
- Don't recolor, stretch, crop or redraw the button, and don't show the button smaller than 40 px tall.
- Leave at least 8 px of space around the button.
- The button works on light and dark pages as it is; there is no light version.
+255 -68
View File
@@ -2,7 +2,7 @@
# Patches
Every patch and game setting FramePort knows: 191 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: 203 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,9 +11,9 @@ 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) (64)
- [Game settings](#group-adapter) (79)
- [Files and settings on the Frame](#group-device) (10)
- [Steam Frame patches](#group-frame) (71)
- [Game settings](#group-adapter) (82)
- [Files and settings on the Frame](#group-device) (12)
- [PC VR (Proton and Revive)](#group-pcvr) (13)
## Index
@@ -27,7 +27,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Copy OVRPlugin if VrApi is present](#patch_copy_ovrplugin_vrapi) | `patch_copy_ovrplugin_vrapi` | Patches from OVRPort | default |
| [Disable controller tracking offset](#patch_disable_controller_offset) | `patch_disable_controller_offset` | Patches from OVRPort | |
| [Disable Meta XR Audio telemetry](#patch_disable_meta_xr_audio_telemetry) | `patch_disable_meta_xr_audio_telemetry` | Patches from OVRPort | |
| [Disable application space warp if used](#patch_disable_space_warp) | `patch_disable_space_warp` | Patches from OVRPort | 4 |
| [Disable application space warp if used](#patch_disable_space_warp) | `patch_disable_space_warp` | Patches from OVRPort | 5 |
| [Fix minimal Android SDK](#patch_fix_min_android_sdk) | `patch_fix_min_android_sdk` | Patches from OVRPort | default |
| [Fix UE4 crash with Unity stub](#patch_fix_unreal_crash) | `patch_fix_unreal_crash` | Patches from OVRPort | default |
| [Force enable passthrough](#patch_force_passthrough) | `patch_force_passthrough` | Patches from OVRPort | 3 |
@@ -39,72 +39,79 @@ 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 | 46 |
| [Remove Unreal's ForceQuit](#patch_remove_unreal_force_quit) | `patch_remove_unreal_force_quit` | Patches from OVRPort | 50 |
| [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 |
| [Pico/YVR/Quest metadata](#patch_vr_metadata) | `patch_vr_metadata` | Patches from OVRPort | default |
| [VrApi → OpenXR adapter (OVRPort)](#patch_vrapi_openxr) | `patch_vrapi_openxr` | Patches from OVRPort | |
| [32-bit ARM bridge (Gear VR / Go games)](#frame-a32_bridge) | `frame.a32_bridge` | Steam Frame patches | 6 |
| [32-bit ARM bridge (Gear VR / Go games)](#frame-a32_bridge) | `frame.a32_bridge` | Steam Frame patches | 7 |
| [FrameBridge OpenXR adapter](#frame-adapter) | `frame.adapter` | Steam Frame patches | default |
| [Content files from the game's data](#frame-asset_files) | `frame.asset_files` | Steam Frame patches | 2 |
| [Asset-file service bypass](#frame-asset_files_stub) | `frame.asset_files_stub` | Steam Frame patches | |
| [Skip Meta avatars](#frame-avatar_stub) | `frame.avatar_stub` | Steam Frame patches | 1 |
| [bHaptics: skip the Bluetooth permission request](#frame-bhaptics_permission) | `frame.bhaptics_permission` | Steam Frame patches | 6 |
| [bHaptics: skip the Bluetooth permission request](#frame-bhaptics_permission) | `frame.bhaptics_permission` | Steam Frame patches | 12 |
| [BlazeRush: controllers without Meta avatars, own shader cache](#frame-blazerush) | `frame.blazerush` | Steam Frame patches | 1 |
| [Browser for sign-ins: open pages in Steam's browser](#frame-browser_handoff) | `frame.browser_handoff` | Steam Frame patches | |
| [Cortopia games: don't wait for Meta's org-scoped id](#frame-cortopia_org_id) | `frame.cortopia_org_id` | Steam Frame patches | 1 |
| [Epic Online Services: stop the keychain retry loop](#frame-eos_keychain_loop) | `frame.eos_keychain_loop` | Steam Frame patches | |
| [GAZZLERS: take the Quest platform path](#frame-gazzlers_quest_platform) | `frame.gazzlers_quest_platform` | Steam Frame patches | 1 |
| [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 |
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | 1 |
| [Play services: Android 11 version checks](#frame-gms_platform_version) | `frame.gms_platform_version` | Steam Frame patches | 2 |
| [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 | 35 |
| [Hardware video decoding (H.264, HEVC, VP9)](#frame-hw_video_decode) | `frame.hw_video_decode` | Steam Frame patches | 43 |
| [Just Dance VR: keep progress in the local save](#frame-justdance_local_save) | `frame.justdance_local_save` | Steam Frame patches | |
| [Keep Android's shared libraries the app uses](#frame-keep_system_libraries) | `frame.keep_system_libraries` | Steam Frame patches | default |
| [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 |
| [Unity: disable Meta depth occlusion](#frame-meta_depth_occlusion) | `frame.meta_depth_occlusion` | Steam Frame patches | 1 |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 7 |
| [Declare Meta-only permissions](#frame-meta_permissions) | `frame.meta_permissions` | Steam Frame patches | 10 |
| [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 |
| [Oculus OS telemetry class stubs](#frame-oculusos) | `frame.oculusos` | Steam Frame patches | 17 |
| [Oculus OS telemetry class stubs](#frame-oculusos) | `frame.oculusos` | Steam Frame patches | 18 |
| [Fix crash when the game prepares the microphone](#frame-ovr_microphone) | `frame.ovr_microphone` | Steam Frame patches | 1 |
| [Answer Meta's org-scoped user id request](#frame-ovr_org_scoped_id) | `frame.ovr_org_scoped_id` | Steam Frame patches | default |
| [Keep the same Meta user between starts](#frame-ovr_stable_user) | `frame.ovr_stable_user` | Steam Frame patches | default |
| [Trace Meta platform requests (diagnostics)](#frame-ovr_trace) | `frame.ovr_trace` | Steam Frame patches | |
| [Platform compat (ovrMessageType_ToString)](#frame-ovrplatformcompat) | `frame.ovrplatformcompat` | Steam Frame patches | default |
| [Stub missing Meta platform functions](#frame-ovrstubs) | `frame.ovrstubs` | Steam Frame patches | default |
| [Unpaired pointer-authentication checks](#frame-pac_hints) | `frame.pac_hints` | Steam Frame patches | 1 |
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | 1 |
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 6 |
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | 3 |
| [QuestCraft: count the Frame's network as online](#frame-pojlib_connection) | `frame.pojlib_connection` | Steam Frame patches | 1 |
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 9 |
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
| [Embedded OpenXR loader: use FrameBridge](#frame-static_openxr_loader) | `frame.static_openxr_loader` | 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 |
| [Team Beef ports: treat the Frame as a Meta headset](#frame-tbxr_vendor) | `frame.tbxr_vendor` | Steam Frame patches | 3 |
| [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 | 27 |
| [GL shim for Unity (no multisampled render-to-texture)](#frame-unity_gl_shim) | `frame.unity_gl_shim` | Steam Frame patches | 28 |
| [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 | 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 | 33 |
| [Unity: start VR without Meta's system apps](#frame-unity_oculus_check) | `frame.unity_oculus_check` | Steam Frame patches | 15 |
| [Unity: keep MSAA off at runtime (OVRManager)](#frame-unity_runtime_msaa_off) | `frame.unity_runtime_msaa_off` | Steam Frame patches | 34 |
| [Unity: skip Meta's player-in-room check](#frame-unity_scene_active_rooms) | `frame.unity_scene_active_rooms` | Steam Frame patches | 1 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 122 |
| [Make Unity text fields work](#frame-unity_text_input) | `frame.unity_text_input` | Steam Frame patches | 136 |
| [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 |
| [Unreal: match multiview depth to the eye size](#frame-unreal_multiview_depth) | `frame.unreal_multiview_depth` | Steam Frame patches | 1 |
| [Unreal: reserve memory like on Quest](#frame-unreal_noreserve) | `frame.unreal_noreserve` | Steam Frame patches | |
| [Unreal: stand-ins for OVRPlugin functions OVRPort lacks](#frame-unreal_ovrp_entrypoints) | `frame.unreal_ovrp_entrypoints` | Steam Frame patches | 3 |
| [Unreal: reserve memory like on Quest](#frame-unreal_noreserve) | `frame.unreal_noreserve` | Steam Frame patches | 1 |
| [Unreal: stand-ins for OVRPlugin functions OVRPort lacks](#frame-unreal_ovrp_entrypoints) | `frame.unreal_ovrp_entrypoints` | Steam Frame patches | 6 |
| [Unreal: Quest controller bindings (RPOC key map)](#frame-unreal_quest_keymap) | `frame.unreal_quest_keymap` | Steam Frame patches | 3 |
| [Unreal: don't wait for the Quest shader precompile](#frame-unreal_quest_precompile) | `frame.unreal_quest_precompile` | Steam Frame patches | 3 |
| [Unreal: start without the OBB check](#frame-unreal_skip_obb_check) | `frame.unreal_skip_obb_check` | Steam Frame patches | 9 |
| [Unreal: start without the OBB check](#frame-unreal_skip_obb_check) | `frame.unreal_skip_obb_check` | Steam Frame patches | 10 |
| [Unreal: skip the OBB delivery check](#frame-unreal_skip_obb_delivery) | `frame.unreal_skip_obb_delivery` | Steam Frame patches | 1 |
| [Unreal: bypass the stalled loading splash](#frame-unreal_splash_bypass) | `frame.unreal_splash_bypass` | Steam Frame patches | 1 |
| [Unreal: thumb follows the capacitive touches](#frame-unreal_thumb_touch) | `frame.unreal_thumb_touch` | Steam Frame patches | 8 |
| [USB MIDI: don't poll for USB devices](#frame-usb_midi_watcher) | `frame.usb_midi_watcher` | Steam Frame patches | |
| [Vermillion: paint canvases in the scene](#frame-vermillion_scene_canvas) | `frame.vermillion_scene_canvas` | Steam Frame patches | 1 |
| [Vivox voice chat: skip Android 12 audio routing](#frame-vivox_audio_route) | `frame.vivox_audio_route` | Steam Frame patches | 3 |
| [Vivox voice chat: skip Android 12 audio routing](#frame-vivox_audio_route) | `frame.vivox_audio_route` | Steam Frame patches | 4 |
| [Vulkan: drop invalid render-pass pointers](#frame-vk_sanitize) | `frame.vk_sanitize` | Steam Frame patches | default · 1 |
| [VrApi → OpenXR bridge](#frame-vrapi_bridge) | `frame.vrapi_bridge` | Steam Frame patches | 4 |
| [Quiet Meta's VrApi loader (OpenXR games)](#frame-vrapi_stub) | `frame.vrapi_stub` | Steam Frame patches | 2 |
@@ -115,11 +122,11 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Pointer turn](#adapter-aim_yaw) | `adapter.aim_yaw` | Game settings | |
| [Fix inside-out mixed reality](#adapter-alpha_blend_emul) | `adapter.alpha_blend_emul` | Game settings | |
| [Use controllers](#adapter-controller_fix) | `adapter.controller_fix` | Game settings | 2 |
| [Show Steam Frame controllers](#adapter-controller_models) | `adapter.controller_models` | Game settings | 7 |
| [Show Steam Frame controllers](#adapter-controller_models) | `adapter.controller_models` | Game settings | 9 |
| [Show cube loading screens](#adapter-cube_quads) | `adapter.cube_quads` | Game settings | |
| [Stand in for cube layers](#adapter-cube_standin) | `adapter.cube_standin` | Game settings | |
| [Show curved menus and screens](#adapter-cylinder_strips) | `adapter.cylinder_strips` | Game settings | |
| [Show 360° backgrounds and videos](#adapter-equirect_emul) | `adapter.equirect_emul` | Game settings | 7 |
| [Show 360° backgrounds and videos](#adapter-equirect_emul) | `adapter.equirect_emul` | Game settings | 9 |
| [360° detail](#adapter-equirect_face) | `adapter.equirect_face` | Game settings | |
| [360° picture orientation](#adapter-equirect_flip) | `adapter.equirect_flip` | Game settings | |
| [360° video frame rate limit](#adapter-equirect_fps) | `adapter.equirect_fps` | Game settings | |
@@ -127,20 +134,21 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [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 | 2 |
| [Colour space support](#adapter-fb_color_space) | `adapter.fb_color_space` | Game settings | |
| [Colour space support](#adapter-fb_color_space) | `adapter.fb_color_space` | Game settings | 1 |
| [Keep full resolution (no dynamic resolution)](#adapter-fixed_resolution) | `adapter.fixed_resolution` | Game settings | 2 |
| [Upside-down panel images (diagnostics)](#adapter-flip_dump) | `adapter.flip_dump` | Game settings | |
| [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 |
| [Ignore focus dips up to](#adapter-focus_hold_ms) | `adapter.focus_hold_ms` | Game settings | 2 |
| [Hide Quest foveation](#adapter-foveation_fix) | `adapter.foveation_fix` | Game settings | |
| [Clear the game's GL errors (GLES)](#adapter-gl_error_clear) | `adapter.gl_error_clear` | Game settings | 3 |
| [Clear the game's GL errors (GLES)](#adapter-gl_error_clear) | `adapter.gl_error_clear` | Game settings | 4 |
| [Hide fast anti-aliasing (OpenGL ES)](#adapter-gl_hide_msrtt) | `adapter.gl_hide_msrtt` | Game settings | 1 |
| [Draw each eye separately (OpenGL ES)](#adapter-gl_hide_multiview) | `adapter.gl_hide_multiview` | Game settings | 1 |
| [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 | 107 |
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 122 |
| [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,17 +166,18 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Animate fingers from touch](#adapter-proximity_emul) | `adapter.proximity_emul` | Game settings | |
| [Controller buttons](#adapter-quest_buttons) | `adapter.quest_buttons` | Game settings | |
| [Keep pictures in bounds](#adapter-rect_clamp) | `adapter.rect_clamp` | Game settings | |
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 3 |
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 6 |
| [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 | 15 |
| [Render resolution (supersampling)](#adapter-scale) | `adapter.scale` | Game settings | 18 |
| [Room depth](#adapter-scene_depth) | `adapter.scene_depth` | Game settings | |
| [Use room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 18 |
| [Use room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 21 |
| [Room height](#adapter-scene_height) | `adapter.scene_height` | Game settings | |
| [Hide play-area size](#adapter-scene_hide_bounds) | `adapter.scene_hide_bounds` | Game settings | 1 |
| [Scan when requested](#adapter-scene_scan_request) | `adapter.scene_scan_request` | Game settings | |
| [Room width](#adapter-scene_width) | `adapter.scene_width` | Game settings | |
| [Eye-image snapshots (diagnostics)](#adapter-snapshot) | `adapter.snapshot` | Game settings | |
| [Provide space warp](#adapter-space_warp_emul) | `adapter.space_warp_emul` | Game settings | 1 |
| [Keep menus in place](#adapter-stable_local) | `adapter.stable_local` | Game settings | 1 |
| [Drop layer color fades](#adapter-strip_color_bias) | `adapter.strip_color_bias` | Game settings | |
| [Strip depth layers](#adapter-strip_depth) | `adapter.strip_depth` | Game settings | |
@@ -176,6 +185,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Native video surfaces (Batman renderer)](#adapter-surface_native) | `adapter.surface_native` | Game settings | 1 |
| [Swap eyes](#adapter-swap_eyes) | `adapter.swap_eyes` | Game settings | |
| [Retry refused picture formats](#adapter-swapchain_fix) | `adapter.swapchain_fix` | Game settings | |
| [Share panel images (swapchain limit)](#adapter-swapchain_pool) | `adapter.swapchain_pool` | Game settings | 2 |
| [Steady controller input after focus](#adapter-sync_guard) | `adapter.sync_guard` | Game settings | 3 |
| [Hide Vulkan foveation](#adapter-vk_hide_fdm) | `adapter.vk_hide_fdm` | Game settings | 1 |
| [Without Steam's shader cache layer](#adapter-vk_no_fossilize) | `adapter.vk_no_fossilize` | Game settings | 1 |
@@ -191,13 +201,15 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
| [Keep the game's saved resolution in step](#device-config_sync) | `device.config_sync` | Files and settings on the Frame | 1 |
| [Show as VR or as a flat window](#device-display_mode) | `device.display_mode` | Files and settings on the Frame | |
| [Settings files for the game](#device-files) | `device.files` | Files and settings on the Frame | 3 |
| [Foveated rendering (Valve)](#device-foveation) | `device.foveation` | Files and settings on the Frame | 5 |
| [Foveated rendering (Valve)](#device-foveation) | `device.foveation` | Files and settings on the Frame | 6 |
| [Headset model](#device-headset_model) | `device.headset_model` | Files and settings on the Frame | default |
| [Hide Android's navigation bar](#device-hide_navbar) | `device.hide_navbar` | Files and settings on the Frame | |
| [Hybrid app: allow the switch into VR](#device-hybrid_app) | `device.hybrid_app` | Files and settings on the Frame | |
| [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 |
| [Share a folder with another game](#device-shared_folder) | `device.shared_folder` | Files and settings on the Frame | |
| [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 | 122 |
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 135 |
| [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) | |
@@ -306,7 +318,7 @@ Turns off Meta's frame-rate trick, which causes glitches or hangs outside Quest
- **Changes:** Changes the APK (during the OVRPort conversion).
- **Suggested (on):** Very heavy game (… GiB) using OVRPlugin: application space warp causes artifacts/hangs off-Quest.
- **Offered (off):** Enable if the game shows warping artifacts or hangs (uses OVRPlugin space warp).
- **Catalog games (4):** Asgard's Wrath 2, Batman: Arkham Shadow, Marvel's Deadpool VR, Puzzling Places.
- **Catalog games (5):** Asgard's Wrath 2, Batman: Arkham Shadow, Epic Roller Coasters, Marvel's Deadpool VR, Puzzling Places.
- **Source:** [patches/overport.py](../src/frameport/patches/overport.py)
> For heavy games that use application space warp: it causes artifacts or hangs on non-Quest runtimes (for example Asgard's Wrath 2, Batman: Arkham Shadow).
@@ -442,7 +454,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 (46):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Breathedge VR, Carve Snowboarding, Creed, Dance Central, Espire 1: VR Operative (Quest Edition), Espire 2 and 38 more.
- **Catalog games (50):** Asgard's Wrath 2, BARTENDER VR SIMULATOR, Breathedge VR, Carve Snowboarding, Creed, Dance Central, Darksword, Espire 1: VR Operative (Quest Edition) and 42 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)
@@ -517,9 +529,10 @@ Runs a 32-bit game (Gear VR, Oculus Go, early Quest) on the Frame, which only ru
- `frame.a32_bridge` · experimental · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** 32-bit-only game: the Frame can't run 32-bit ARM code, the bridge translates it.
- **Catalog games (6):** AFFECTED: The Manor, Gunjack, Journey of the Gods, Shadow Point, Sports Scramble (Santa Cruz), Spunky Dungeon.
- **Catalog games (7):** AFFECTED: The Manor, First Contact, Gunjack, Journey of the Gods, Shadow Point, Sports Scramble (Santa Cruz), Spunky Dungeon.
- **Suggested by launch-test signatures:**
- `a32-jit-overflow` (fatal): The 32-bit bridge's translator stopped the game on a long run of instructions it hands to its interpreter, for example OpenSSL's ARMv8 SHA-1 code in Pinball FX2 VR.
- `a32-video-ndk` (error): A 32-bit game on the bridge can't play its videos through Unity's video player (Ghost Giant's intro stays black).
- `no-arm64` (fatal): 32-bit-only APK (e.g. a Gear VR or Oculus Go game).
- **Source:** [patches/frame/a32_bridge.py](../src/frameport/patches/frame/a32_bridge.py)
@@ -589,7 +602,7 @@ Stops bHaptics from asking for a Bluetooth permission the Frame doesn't have (th
- `frame.bhaptics_permission` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** bHaptics asks for an Android 12 Bluetooth permission at start; the permission screen interrupts the game on the Frame.
- **Catalog games (6):** Quantum Void, Ragnarock, Smash Drums, Tea For God, Vampire: The Masquerade - Justice, Vendetta Forever.
- **Catalog games (12):** Darksword, Deisim, Epic Roller Coasters, Eternal Starlight, Quantum Void, Ragnarock, Smash Drums, SynthRiders and 4 more.
- **Suggested by launch-test signatures:**
- `bhaptics-permission-prompt` (error): The game asked for Android 12's Bluetooth permission (bHaptics' vest SDK, for example Yupitergrad).
- **Source:** [patches/frame/bhaptics_permission.py](../src/frameport/patches/frame/bhaptics_permission.py)
@@ -609,6 +622,17 @@ Makes BlazeRush's menus and controllers work without Meta's avatars, and lets it
> BlazeRush 1.0.349 only (Klownicle's guide, GitHub #57). Its menus wait for Meta's avatar, which never comes on the Frame, before they create the game's own controllers: the second menu ignored all input. Two instructions in libtargemapp.so skip that wait and the avatar mesh drawing; the game's shader cache gets a new folder name (cache/br_shader_v1), so programs compiled before the GL shim's shader fixes aren't reused. Only this exact build is changed (code fingerprint and every instruction checked); anything else is left as it is.
### <a id="frame-browser_handoff"></a>Browser for sign-ins: open pages in Steam's browser
For apps that say no browser is installed (e.g. when signing in): their web pages open in Steam's browser instead.
- `frame.browser_handoff` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Offered (off):** The app looks for a web browser (its manifest asks Android for apps that open http/https links), and Lepton has none. Turn on if it says no browser is installed, e.g. when signing in.
- **Source:** [patches/frame/browser_handoff.py](../src/frameport/patches/frame/browser_handoff.py)
> Lepton's Android has no web browser. Apps that check for one before opening a page (for example Plex's sign-in) say no browser is installed and stop. This adds a small browser entry to the app itself (an activity for http/https links in its manifest and an extra classes.dex): the app's check finds it, and the page opens in Steam's browser, where Lepton sends every link. Whether the app notices the finished sign-in depends on the app (a code or PIN it polls for works; a sign-in that returns to the app through a link doesn't).
### <a id="frame-cortopia_org_id"></a>Cortopia games: don't wait for Meta's org-scoped id
Gets Cortopia games (for example Escaping Wonderland) past their loading screen.
@@ -621,7 +645,7 @@ Gets Cortopia games (for example Escaping Wonderland) past their loading screen.
- `cortopia-org-id-wait` (error): The game waits forever on its loading screen for an answer from Meta's platform (an org-scoped user id, used as its analytics id) that OVRPort's platform library doesn't give.
- **Source:** [patches/frame/cortopia_org_id.py](../src/frameport/patches/frame/cortopia_org_id.py)
> Cortopia Studios' games (e.g. Escaping Wonderland) ask Meta's platform for an org-scoped user id at start when on Wi-Fi, only to use it as their analytics id. OVRPort's platform library doesn't answer that request ("An unknown error occurred. Request failed."), so the game waits forever on its loading screen. Makes the game take its own offline path for that one request (the analytics id stays empty); the licence check and the logged-in user before it are unchanged. Found with Cpp2IL in libil2cpp.so.
> Cortopia Studios' games (e.g. Escaping Wonderland, TMNT: Empire City) ask Meta's platform for an org-scoped user id at start when on Wi-Fi, only to use it as their analytics id. OVRPort's platform library doesn't answer that request ("An unknown error occurred. Request failed."), so the game waits forever on its loading screen. Makes the game take its own offline path for that one request (the analytics id stays empty); the licence check and the logged-in user before it are unchanged. Found with Cpp2IL in libil2cpp.so.
### <a id="frame-eos_keychain_loop"></a>Epic Online Services: stop the keychain retry loop
@@ -636,6 +660,20 @@ Stops Epic Online Services from retrying its login keychain forever (the game wo
> Epic Online Services (EOS) keeps its login keychain in the app's settings storage. When it can't write there, it resets the keychain and tries again without end (for example Beyond Sandbox: a start that never finishes, 100 % CPU, a log of gigabytes). The reset method returns at once instead (an in-place edit of classes.dex), so EOS reports the failure to the game once.
### <a id="frame-gazzlers_quest_platform"></a>GAZZLERS: take the Quest platform path
Lets GAZZLERS start on the Frame (it stopped with "Current Platform is not supported" because it didn't recognise the device).
- `frame.gazzlers_quest_platform`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Catalog games (1):** GAZZLERS.
- **Suggested by launch-test signatures:**
- `gazzlers-platform-unsupported` (fatal): The game decides whether it runs on a Meta Quest or a PICO headset from Android's device name and stops for any other name; the Frame's Android calls itself Valve Lepton.
- **Source:** [patches/frame/gazzlers_platform.py](../src/frameport/patches/frame/gazzlers_platform.py)
> GAZZLERS decides between its Meta Quest and PICO code from Android's device name and stops with "Current Platform is not supported" on the Frame, whose Android reports itself as Valve Lepton. Rewrites PlatformDeterminer.GetAndroidPlatform -&gt; Meta in libil2cpp.so (found with Cpp2IL), so the game starts as it does on a Quest.
### <a id="frame-gl_multiview_fbo"></a>OpenGL ES: multiview shaders on flat panels (HUD, menus)
Experimental: shows HUDs, menus and other flat panels that stay black on the Frame (for example Doom3Quest's HUD and PDA).
@@ -670,7 +708,7 @@ Lets games with Google Play services code start on the Frame (they'd crash at st
- `frame.gms_platform_version` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Google Play services' Android 12 checks were built in as 'always true'; on Lepton (Android 11) the game would crash at start.
- **Catalog games (1):** Table Troopers.
- **Catalog games (2):** Solero, Table Troopers.
- **Suggested by launch-test signatures:**
- `gms-android12-call` (fatal): Google Play services' code in the game called an Android 12 method (the game was built for Android 12 and newer, so its version checks were built in as "always true") and Lepton runs Android 11; the game crashes on its GoogleApiHandler thread at start (for example Titan Isles).
- **Source:** [patches/frame/gms_platform_version.py](../src/frameport/patches/frame/gms_platform_version.py)
@@ -698,11 +736,37 @@ 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 (35):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Exploding Kittens VR and 27 more.
- **Catalog games (43):** 4XVR Video Player, AugmentedEmpire, Batman: Arkham Shadow, Battlemarked, BattleTalent, Breathedge VR, Dance Central, Darksword and 35 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.
### <a id="frame-justdance_local_save"></a>Just Dance VR: keep progress in the local save
Makes Just Dance VR keep your progress between sessions (it only kept it online, after a Meta sign-in).
- `frame.justdance_local_save`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Suggested by launch-test signatures:**
- `justdance-local-save-off` (info): Just Dance VR (0.5.4 and later) opens its local save at every start but never writes it (the save request method is empty in this build; on a Quest the progress is kept by Ubisoft's online backend after a Meta sign-in, which the Frame doesn't have), so progress, unlocks and the finished tutorial are lost at every restart.
- **Source:** [patches/frame/justdance_save.py](../src/frameport/patches/frame/justdance_save.py)
> Just Dance VR (GitHub #480) loads its progress from a local save at start, but the method that writes it (LocalSaveEvents.RequestSave) is empty: on a Quest the progress is kept by Ubisoft's online backend after a Meta sign-in, which the Frame doesn't have, so progress, unlocks and the finished tutorial were lost at every restart. The method gets the body it is declared with (raise the SaveValueRequested event the game's own save system listens to), built from its neighbour SaveSystemReady and placed in a new code segment of libil2cpp.so; everything is found per build with Cpp2IL (downloaded on first use). A build that already writes its save is left unchanged. No sign-in or online data is touched.
### <a id="frame-keep_system_libraries"></a>Keep Android's shared libraries the app uses
Lets apps start that need one of Android's optional libraries (for example YouTube VR).
- `frame.keep_system_libraries` · on by default
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Suggested by launch-test signatures:**
- `system-library-missing` (fatal): The app closed at start because it can't find classes from one of Android's optional libraries (for example org.apache.http.legacy in YouTube VR).
- **Source:** [patches/frame/system_libraries.py](../src/frameport/patches/frame/system_libraries.py)
> OVRPort removes every &lt;uses-library&gt; entry from the manifest (Quest-only libraries would stop the install), including ones the Frame's Android has. An app that needs one of those then can't find its classes and closes at start (for example YouTube VR: ClassNotFoundException org.apache.http.protocol.HttpRequestHandler, from org.apache.http.legacy). This puts back the entries for libraries the Frame provides, marked optional.
### <a id="frame-langpacks"></a>Language packs from the game's files
Lets the game use language files already in its data instead of waiting for a download that never comes.
@@ -723,7 +787,7 @@ Makes the game start from the Frame's Steam library.
- **Suggested (on):** Manifest uses category INFO only; Lepton needs LAUNCHER.
- **Suggested (on):** Applied automatically if the manifest needs it.
- **Suggested by launch-test signatures:**
- `no-launcher` (fatal): Nothing starts: Lepton found no entry point in the app (an activity with category LAUNCHER).
- `no-launcher` (fatal): Nothing starts: Lepton found no entry point in the app (an activity with category LAUNCHER; one on an activity-alias alone does not count).
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
> Lepton only launches an activity with category LAUNCHER (Quest apps often use INFO). Symptom when missing: launch.log says 'APP_ACTIVITY is empty' and nothing starts.
@@ -784,7 +848,7 @@ Declares Meta's permissions some mixed-reality games ask for, so they don't stop
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Mixed-reality game that needs the room model: its 'use spatial data' permission must be granted (for example Demeter).
- **Suggested (on):** Uses Meta's room or anchor APIs: their permissions must be granted, or the game never asks for the room (for example NOPE's MR mode).
- **Catalog games (7):** Arkio, BattleTalent, Demeter, Laser Dance, Punch Fit, Real VR Fishing, Test Quest MR.
- **Catalog games (10):** Arkio, BattleTalent, Demeter, Laser Dance, Movie Deck for Plex, Punch Fit, Real VR Fishing, SynthRiders and 2 more.
- **Source:** [patches/frame/manifest_fixes.py](../src/frameport/patches/frame/manifest_fixes.py)
> Declares com.oculus.permission.* / horizonos.permission.* that the app uses, so Android grants them at install (for example the 'use spatial data' permission of mixed-reality games like Demeter).
@@ -827,7 +891,7 @@ Provides stand-ins for Quest system calls some games make at start.
- **Suggested (on):** The Unreal build of Meta XR Audio looks up com.oculus.os.AnalyticsEvent and aborts without it (for example NOPE Challenge).
- **Suggested (on):** Several Meta SDK libraries look up com.oculus.os.AnalyticsEvent (…); games like this abort without the stub (for example Nano).
- **Offered (off):** … references com.oculus.os.AnalyticsEvent; usually harmless (for example Batman: Arkham Shadow and LEGO Bricktales run without it). Enable if the game aborts with ClassNotFoundException.
- **Catalog games (17):** AgeOfJoy, AllInOneSports, Caves, Deep Cuts, Does it Stack?, Nano, Neko Atsume Purrfect, NOPE CHALLENGE and 9 more.
- **Catalog games (18):** AgeOfJoy, AllInOneSports, Caves, Deep Cuts, Does it Stack?, Drums Rock, Nano, Neko Atsume Purrfect and 10 more.
- **Suggested by launch-test signatures:**
- `oculus-os-class` (fatal): The game looks up a Quest-only system class and aborts.
- **Source:** [patches/frame/telemetry.py](../src/frameport/patches/frame/telemetry.py)
@@ -848,6 +912,20 @@ Stops a crash a few seconds after the logo in games with voice chat.
> OVRPort's Meta platform library opens the microphone only when it's started, but answers a game's question about the microphone buffer size from that not-yet-opened stream: games that ask first (Unreal's Oculus voice chat, for example The Walking Dead: Saints & Sinners Ch. 2) crash a few seconds after the logo (SIGSEGV in libaaudio.so). Adds the missing check (size 0 until the microphone is started).
### <a id="frame-ovr_org_scoped_id"></a>Answer Meta's org-scoped user id request
Answers Meta's request for your org-scoped user id, which games such as Eleven Table Tennis wait for on their loading screen.
- `frame.ovr_org_scoped_id` · on by default
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** The game's Meta start-up waits for an org-scoped user id that OVRPort's platform library never answers (stuck loading screen).
- **Suggested (on):** … waits for Meta's org-scoped user id, which OVRPort's platform library never answers (stuck loading screen).
- **Suggested by launch-test signatures:**
- `meta-org-scoped-id-wait` (error): The game waits on its loading screen for Meta's org-scoped user id, which OVRPort's platform library never answers.
- **Source:** [patches/frame/ovr_org_scoped_id.py](../src/frameport/patches/frame/ovr_org_scoped_id.py)
> OVRPort's platform library never answers Meta's org-scoped user id request (ovr_User_GetOrgScopedID returns 'no request'), so Meta's Unity wrapper logs "An unknown error occurred. Request failed." and a game that waits for the id stays on its loading screen (e.g. Eleven Table Tennis, Escaping Wonderland). Adds the small platform library FramePort also uses for language packs and answers that request with a fixed id derived from the game's user id. Every other platform call goes to OVRPort's library unchanged.
### <a id="frame-ovr_stable_user"></a>Keep the same Meta user between starts
Keeps your progress in games that file their saves under your Meta user.
@@ -917,13 +995,27 @@ Stops games with Photon Voice chat from crashing on the Frame when voice chat st
- `frame.photon_voice_audio_route` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Photon Voice calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes when voice chat starts.
- **Catalog games (1):** Table Troopers.
- **Catalog games (3):** Deisim, Table Troopers, UNDERDOGS.
- **Suggested by launch-test signatures:**
- `photon-voice-api31` (fatal): The game's Photon Voice microphone plugin calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes when voice chat starts (for example Table Troopers at the tutorial).
- **Source:** [patches/frame/photon_voice_audio_route.py](../src/frameport/patches/frame/photon_voice_audio_route.py)
> Photon Voice's microphone plugin (for example Table Troopers) picks the audio device with Android 12 methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the game crashes with a NoSuchMethodError when voice chat starts. That method returns at once instead (an in-place edit of classes.dex); the microphone keeps Android's default audio route.
### <a id="frame-pojlib_connection"></a>QuestCraft: count the Frame's network as online
Lets QuestCraft download its Java runtime: it waits for Wi-Fi, and the Frame's Android reports its network as Ethernet.
- `frame.pojlib_connection` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** QuestCraft only downloads its Java runtime on Wi-Fi; the Frame's Android reports its network as Ethernet.
- **Catalog games (1):** QuestCraft.
- **Suggested by launch-test signatures:**
- `questcraft-no-java-runtime` (fatal): QuestCraft's Java runtime isn't there.
- **Source:** [patches/frame/pojlib_connection.py](../src/frameport/patches/frame/pojlib_connection.py)
> QuestCraft downloads its Java runtime only when Android reports a Wi-Fi connection; the Frame's Android reports Ethernet, so the download is skipped and Minecraft can't start. The connection check answers yes instead (an in-place edit of classes.dex).
### <a id="frame-sdl_clipboard"></a>SDL apps: start without a clipboard service
Lets SDL and LÖVE games start on the Frame (they'd crash looking for a clipboard).
@@ -931,7 +1023,7 @@ Lets SDL and LÖVE games start on the Frame (they'd crash looking for a clipboar
- `frame.sdl_clipboard` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** SDL app: it crashes at start in Lepton, which has no clipboard service.
- **Catalog games (6):** Crisis Zone VR, Majora's Mask VR, PrimedGun v2, Time Crisis II VR (Test), Time Crisis VR (Experimental), WiiCompiled VR.
- **Catalog games (9):** Crisis Zone VR, GoldenEye VR, Majora's Mask VR, PrimedGun v2, Simpsons Hit and Run VR, Time Crisis II VR (Test), Time Crisis VR (Experimental), WiiCompiled VR and 1 more.
- **Suggested by launch-test signatures:**
- `sdl-no-clipboard` (fatal): The app crashed at start because the Frame's Android has no clipboard.
- **Source:** [patches/frame/sdl_clipboard.py](../src/frameport/patches/frame/sdl_clipboard.py)
@@ -964,6 +1056,20 @@ Opens the app straight in VR, skipping its flat launcher screen. Turn off to use
> 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".
### <a id="frame-static_openxr_loader"></a>Embedded OpenXR loader: use FrameBridge
Makes an embedded OpenXR loader use FrameBridge and your game settings.
- `frame.static_openxr_loader` · revision 2
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** The embedded Khronos OpenXR loader bypasses FrameBridge.
- **Requires:** [`frame.adapter`](#frame-adapter)
- **Suggested by launch-test signatures:**
- `static-openxr-loader-bypass` (warning): A Khronos loader loaded the system runtime without FrameBridge.
- **Source:** [patches/frame/static_openxr_loader.py](../src/frameport/patches/frame/static_openxr_loader.py)
> Routes a game's statically linked Khronos OpenXR loader through FrameBridge by rewriting its runtime manifest paths in place into its own /sdcard (including assembled paths). Disables its Android runtime broker queries so they fall back to that file. Ships a runtime shim and an unmodified inner Khronos loader, which reads the system runtime manifest. Reinstall to write the per-game manifest.
### <a id="frame-swapchain_limit"></a>Allow swapchains larger than 4096 px
Allows very large pictures (8K video, theaters) instead of quitting.
@@ -985,7 +1091,7 @@ Lets Team Beef ports (for example Lambda1VR) start on the Frame.
- `frame.tbxr_vendor` · revision 2
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Team Beef port: it picks its OpenXR setup by headset maker, which the Frame reports as "valve".
- **Catalog games (1):** Lambda1VR.
- **Catalog games (3):** JKXR, Lambda1VR, QuestZDoom.
- **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. Keeps GL4ES in charge of its desktop OpenGL draw calls so animated models don't turn into stretched triangles.
@@ -1010,7 +1116,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 (27):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 19 more.
- **Catalog games (28):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 20 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)
@@ -1065,7 +1171,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 (14):** Accounting+, Audica, BattleSisters, Cosmodread, End Space, Eye of the Temple, I Expect You To Die 2, Jurassic World Aftermath Collection and 6 more.
- **Catalog games (15):** Accounting+, Audica, BattleSisters, Cosmodread, End Space, Eye of the Temple, I Expect You To Die 2, Invasion and 7 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)
@@ -1079,7 +1185,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 (33):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 25 more.
- **Catalog games (34):** Audica, BattleSisters, BattleTalent, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 26 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.
@@ -1107,7 +1213,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 (122):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 114 more.
- **Catalog games (136):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable, Audica and 128 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.
@@ -1174,6 +1280,7 @@ Lets the game load: it reserves more memory at start than the Frame grants in on
- `frame.unreal_noreserve` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unreal's Binned3 allocator reserves more memory at start than the Frame's kernel allows in one piece; without this the game crashes while it loads.
- **Catalog games (1):** Zero Caliber 2.
- **Suggested by launch-test signatures:**
- `unreal-reserve-refused` (fatal): The game crashed while Unreal was still loading, before its first frame.
- **Source:** [patches/frame/unreal_noreserve.py](../src/frameport/patches/frame/unreal_noreserve.py)
@@ -1187,7 +1294,7 @@ Lets older Unreal games start VR on the Frame (for example Star Wars Pinball VR)
- `frame.unreal_ovrp_entrypoints`
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unreal looks up OVRPlugin function(s) OVRPort's OVRPlugin lacks: ……. Without them Unreal starts no VR.
- **Catalog games (3):** Ghostbusters: Rise of the Ghost Lord, Puzzling Places, Star Wars Pinball VR.
- **Catalog games (6):** Darksword, Ghostbusters: Rise of the Ghost Lord, Orcs Must Die: By The Blade, Puzzling Places, Solero, Star Wars Pinball VR.
- **Source:** [patches/frame/unreal_ovrp.py](../src/frameport/patches/frame/unreal_ovrp.py)
> Unreal's Oculus module looks up every OVRPlugin function it was built against and starts no VR when one is missing; OVRPort's OpenXR OVRPlugin lacks some old ones (for example ovrp_GetPTWNear, which Unreal 4.25 builds like Star Wars Pinball VR look up). The game then never starts an OpenXR session and crashes a few seconds in. Adds a generated library to libOVRPlugin.so with a stand-in for each missing function that reports failure (ovrpFailure), so Unreal's Oculus support starts. The functions OVRPort has are untouched.
@@ -1223,7 +1330,7 @@ Starts Unreal games without re-checking their data file (they'd seem to hang on
- `frame.unreal_skip_obb_check` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Unreal checks the OBB's contents before starting (bVerifyOBBOnStartUp); on the Frame a failed or slow check can't be seen and the game seems to hang.
- **Catalog games (9):** Dance Central, Kill It With Fire VR, Lost Recipes, MacacoVR, Maestro, Puzzling Places, Ragnarock, Scared By Squares and 1 more.
- **Catalog games (10):** Dance Central, Darksword, Kill It With Fire VR, Lost Recipes, MacacoVR, Maestro, Puzzling Places, Ragnarock and 2 more.
- **Suggested by launch-test signatures:**
- `unreal-obb-check-stuck` (fatal): The game stays on Unreal's OBB check screen (DownloaderActivity), which the Frame doesn't show, and never starts VR (for example Contractors, GitHub #159).
- **Source:** [patches/frame/unreal_obb_check.py](../src/frameport/patches/frame/unreal_obb_check.py)
@@ -1302,7 +1409,7 @@ Lets games with Vivox voice chat start on the Frame (they'd crash on an Android
- `frame.vivox_audio_route` · also for 2D apps
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Vivox voice chat: it calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes.
- **Catalog games (3):** Black Hole Pool, Dungeons of Eternity, Real VR Fishing.
- **Catalog games (4):** Black Hole Pool, Dungeons of Eternity, Epic Roller Coasters, Real VR Fishing.
- **Suggested by launch-test signatures:**
- `vivox-api31` (fatal): The game's Vivox voice-chat SDK calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes (for example Green Hell VR).
- **Source:** [patches/frame/vivox_audio_route.py](../src/frameport/patches/frame/vivox_audio_route.py)
@@ -1467,7 +1574,7 @@ For games that show the headset's own controllers. Takes effect after a reinstal
- **Options:**
- Value: int, default `0`.
- Game settings → Controllers → "Show Steam Frame controllers".
- **Catalog games (7):** Hitstream, Neko Atsume Purrfect, Ocean Rift, Real VR Fishing, SynthRiders, VR HOT Quest, Walkabout Mini Golf.
- **Catalog games (9):** Hitstream, Movie Deck for Plex, Neko Atsume Purrfect, Ocean Rift, Real VR Fishing, SynthRiders, UNDERDOGS, VR HOT Quest and 1 more.
- **Suggested by launch-test signatures:**
- `render-model-requested` (info): The game shows Quest controllers (or none) because the Frame doesn't provide controller models.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
@@ -1526,7 +1633,7 @@ Video players' virtual theaters and 360° videos would otherwise stay black.
- **Options:**
- Value: int, default `0`.
- Game settings → 360° pictures and video → "Show 360° backgrounds and videos".
- **Catalog games (7):** 4XVR Video Player, EndoparasiticVR, Into The Radius 2, Pinball FX VR, Puzzling Places, Riven, Tea For God.
- **Catalog games (9):** 4XVR Video Player, EndoparasiticVR, Into The Radius 2, Orcs Must Die: By The Blade, Pinball FX VR, Puzzling Places, Riven, Solero and 1 more.
- **Suggested by launch-test signatures:**
- `equirect-layers-dropped` (warning): The game's 360° pictures (for example a video player's theater or 360° videos) are missing because the Frame's runtime can't show equirect layers.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
@@ -1642,12 +1749,30 @@ For games that stop at start because the headset reports no colour space (for ex
- **Options:**
- Value: int, default `0`.
- Game settings → Troubleshooting → "Colour space support" (advanced).
- **Catalog games (1):** OutRun Quest VR.
- **Suggested by launch-test signatures:**
- `color-space-missing` (fatal): The game needs Meta's colour-space extension, which the Frame's runtime doesn't offer, and quits before it starts VR (for example OutRun Quest VR).
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Offers Meta's colour-space extension when the Frame's runtime lacks it: the game is told the headset uses Rec.709 and may choose any colour space (no conversion is done). For games that quit before their VR session with "Rec.709 color space unavailable" (for example OutRun Quest VR, GitHub #336). Only reaches games that use their own OpenXR loader; OVRPort's dispatcher can't forward the new functions.
### <a id="adapter-fixed_resolution"></a>Keep full resolution (no dynamic resolution)
Prevents dynamic resolution from moving Valve foveation off-centre or leaving a white edge line. May increase GPU load. Used at the next launch.
- `adapter.fixed_resolution`
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Options:**
- Value: int, default `0`.
- Game settings → Picture → "Keep full resolution (no dynamic resolution)" (advanced).
- **Catalog games (2):** Batman: Arkham Shadow, Zero Caliber 2.
- **Suggested by launch-test signatures:**
- `dynamic-resolution-foveation` (info): The game submits an offset projection viewport.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Keeps OpenXR layer recommendations at the full swapchain size. Prevents dynamic resolution from moving Valve foveation off-centre or leaving a coarse white edge line. May increase GPU load.
### <a id="adapter-flip_dump"></a>Upside-down panel images (diagnostics)
Saves a few UI panels before and after FrameBridge turns them upright, for testing without the headset.
@@ -1660,7 +1785,7 @@ Saves a few UI panels before and after FrameBridge turns them upright, for testi
- Game settings → Troubleshooting → "Upside-down panel images (diagnostics)" (advanced).
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Saves the first N UI panels that FrameBridge turns upright (Vulkan games whose panels are stored upside down), before and after the flip, as fb_flip_&lt;n&gt;_raw.ppm / fb_flip_&lt;n&gt;_upright.ppm in the game's files folder on the Frame. Shows what the runtime gets without anyone wearing the headset.
> Saves up to N images of the UI panels FrameBridge turns upright (Vulkan games whose panels are stored upside down), exactly as the runtime gets them, as fb_flip_&lt;n&gt;.ppm in the game's files folder on the Frame (a new file whenever a panel's content changes). Shows what is displayed without anyone wearing the headset.
### <a id="adapter-flip_emul"></a>Fix upside-down menus
@@ -1674,7 +1799,7 @@ Turns menus and text the right way up.
- Game settings → Menus and screens → "Fix upside-down menus".
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Blit quads flagged XrCompositionLayerImageLayoutFB VERTICAL_FLIP upside down (Vulkan). Fixes UI panels and text that show upside down (for example Assassin's Creed Nexus).
> Shows quads flagged XrCompositionLayerImageLayoutFB VERTICAL_FLIP as upside-down copies, refreshed every frame (Vulkan). Fixes UI panels and text that show upside down (for example Assassin's Creed Nexus).
### <a id="adapter-flip_quads"></a>Rotate panels 180°
@@ -1742,14 +1867,14 @@ For OpenGL ES games whose memory use keeps growing (e.g. Vader Immortal).
- `adapter.gl_error_clear`
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Suggested (on), value `1`:** Team Beef port: these leave OpenGL errors pending, which made the Frame's runtime leak GPU memory every frame until the game slowed to a crawl (for example QuestZDoom): they are cleared first.
- **Options:**
- Value: int, default `0`.
- Game settings → Troubleshooting → "Clear the game's GL errors (GLES)" (advanced).
- **Catalog games (3):** Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
- **Catalog games (4):** QuestZDoom, Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Clears OpenGL errors the game left pending before FrameBridge calls the Frame's runtime. The runtime's GL code fails on any pending error; after one such failure at the start it allocated GPU memory every frame without freeing it (e.g. Vader Immortal, ~20 MB a second).
> Clears OpenGL errors the game left pending before FrameBridge calls the Frame's runtime. The runtime's GL code fails on any pending error; after one such failure at the start it allocated GPU memory every frame without freeing it (e.g. Vader Immortal, ~20 MB a second; QuestZDoom slowed to a crawl after a few minutes).
### <a id="adapter-gl_hide_msrtt"></a>Hide fast anti-aliasing (OpenGL ES)
@@ -1835,7 +1960,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 (107):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Battlemarked, BattleSisters, BattleTalent and 99 more.
- **Catalog games (122):** AgeOfJoy, Ancient_Dungeon, Attack on Titan VR: Unbreakable, Audica, AugmentedEmpire, Barbaria, Battlemarked, BattleSisters and 114 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.
@@ -2101,7 +2226,9 @@ How often the picture updates. Higher is smoother if the game can keep up.
- **Options:**
- Value: float, default `0.0`.
- Game settings → Picture → "Refresh rate": Game's choice (`0`), 72 Hz (`72`), 80 Hz (`80`), 90 Hz (`90`), 96 Hz (`96`), 108 Hz (`108`), 120 Hz (`120`), 144 Hz (`144`).
- **Catalog games (3):** Does it Stack?, Majora's Mask VR, Vacation Simulator.
- **Catalog games (6):** Astro VR Host, Does it Stack?, Majora's Mask VR, Port0r: Time Crisis VR, Simpsons Hit and Run VR, Vacation Simulator.
- **Suggested by launch-test signatures:**
- `refresh-rate-refused` (error): The game asked for a refresh rate the Frame's runtime refuses and may quit on that error.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Display refresh rate for this game (72, 80, 90, 96, 108, 120 or 144; 0 = the game's choice). Applied through SteamVR before the game starts. A video's frame rate that divides the refresh rate plays smoothest (for example 30 fps at 90 Hz, 24 fps at 72 Hz).
@@ -2144,7 +2271,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 (15):** 4XVR Video Player, BlazeRush, CrazyTaxiVR, cubism, FGO VR, I Expect You To Die 2, KillerFrequency, Spectre and 7 more.
- **Catalog games (18):** 4XVR Video Player, Barbaria, BlazeRush, CrazyTaxiVR, cubism, Darksword, Drums Rock, FGO VR and 10 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.
@@ -2173,7 +2300,7 @@ Scene games get your saved room. Without one, they get a simple room from the pl
- **Options:**
- Value: int, default `0`.
- Game settings → Mixed reality → "Use room layout".
- **Catalog games (18):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, cubism, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord and 10 more.
- **Catalog games (21):** Arkio, AugmentedEmpire, Battlemarked, BattleTalent, cubism, Demeo, Demeter, Ghostbusters: Rise of the Ghost Lord and 13 more.
- **Suggested by launch-test signatures:**
- `mr-discovery-unsupported` (warning): The game cannot discover a Meta room.
- `scene-missing` (warning): The game wants Meta's room data (scene API), which the Frame lacks.
@@ -2250,7 +2377,24 @@ Saves what the game draws every few seconds, for testing without the headset.
- Game settings → Troubleshooting → "Eye-image snapshots (diagnostics)" (advanced).
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. OpenGL ES games only.
> Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. Vulkan games: both eyes at half size (fb_snap_vk_N_vV.ppm), the whole swapchain image with the submitted rect and FOV in the log.
### <a id="adapter-space_warp_emul"></a>Provide space warp
For games that stop at start because they require Meta's space warp (for example OutRun Quest VR). The game then renders every frame normally.
- `adapter.space_warp_emul`
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Options:**
- Value: int, default `0`.
- Game settings → Troubleshooting → "Provide space warp" (advanced).
- **Catalog games (1):** OutRun Quest VR.
- **Suggested by launch-test signatures:**
- `appsw-required` (fatal): The game requires Meta's space warp (Application SpaceWarp) and quits because the Frame reports no buffer size for it (for example OutRun Quest VR).
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> For games that require Meta's Application SpaceWarp and quit without it ("AppSW recommended buffers unavailable", for example OutRun Quest VR, GitHub #336). FrameBridge offers XR_FB_space_warp, reports a motion-vector size (the runtime's, else a quarter of the eye size), serves the game's motion-vector and space-warp depth images itself (OpenGL ES textures or Vulkan images, which the Frame's runtime wouldn't take) and removes the space warp info from every frame: the game renders every frame normally and its motion vectors are not used. Overrides Turn off space warp.
### <a id="adapter-stable_local"></a>Keep menus in place
@@ -2354,6 +2498,21 @@ Lets OpenGL ES games start when the Frame refuses their picture format. Leave it
> Retry rejected GLES formats/MSAA with sRGB, samples=1.
### <a id="adapter-swapchain_pool"></a>Share panel images (swapchain limit)
Keeps extra menus, panels and new eye images working when the Frame reaches its image count limit.
- `adapter.swapchain_pool`
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Options:**
- Value: int, default `0`.
- Game settings → Troubleshooting → "Share panel images (swapchain limit)" (advanced).
- **Catalog games (2):** 4XVR Video Player, Assassin's Creed Nexus.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Share panel images in runtime atlases to avoid the Frame's swapchain count limit (about 15 per session). Useful when opening extra menus closes them or leaves panels gray, or when a game stops drawing because a new set of eye images was refused. GLES and Vulkan apps; on Vulkan only sRGB panels are shared, eye images never.
### <a id="adapter-sync_guard"></a>Steady controller input after focus
For games that crash right after you return to them (for example after the Steam menu).
@@ -2599,7 +2758,7 @@ Render less detail outside your gaze; adjust the full-resolution area or darken
- `mode`: str, default empty — Catalog default: tracked, fixed or off (empty = Valve's default)
- `level`: str, default empty — Runtime profile: lo, med, hi (empty = Default)
- `test`: bool, default `False` — Darken lower-resolution regions (diagnostic)
- **Catalog games (5):** Ocean Rift, Quantum Void, Tea For God, The 7th Guest VR, Vampire: The Masquerade - Justice.
- **Catalog games (6):** Darksword, Ocean Rift, Quantum Void, Tea For God, The 7th Guest VR, Vampire: The Masquerade - Justice.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Reduces shading away from the full-resolution area, following each eye's gaze. Default keeps the runtime profile unchanged. Low has the largest full-resolution center; medium and high use smaller centers and stronger peripheral detail reduction. Larger areas retain more detail but save less GPU work. Fixed stops the area following your eyes. The optional test darkens actual coarse-shaded fragments. Runtime choices are in Game settings. Tested on RE4, Deadpool and Asgard's Wrath 2. The Climb 2 gets a version-guarded private bridge at 1.0 scale; Batman gets its hash-verified allocation fix (unsupported OVRPlugin versions are refused). Other games may need additional fixes; turn this off if artifacts appear.
@@ -2629,6 +2788,19 @@ Hides Android's back, home and recents buttons, which cover the app's own button
> Removes Android's back/home/recents buttons from a 2D app's window, where they cover the app's own controls. Turn it off for an app that needs the on-screen back button.
### <a id="device-hybrid_app"></a>Hybrid app: allow the switch into VR
Lets apps that start as a 2D panel (e.g. YouTube) switch into VR for 360° videos and full screen.
- `device.hybrid_app` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Suggested by launch-test signatures:**
- `hybrid-feature-missing` (warning): The app stays in its 2D panel because it can't find a Quest system feature it checks before switching into VR (360° videos, full screen). "Hybrid app: allow the switch into VR" declares that feature for the app.
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> Horizon OS "hybrid apps" start as a 2D panel and switch into VR for some content (e.g. YouTube's 360° videos and full screen). Before the switch the app checks that the system has the feature oculus.software.vr.app.hybrid; the Frame's Android doesn't, so the app stays in its panel ("Missing hybrid app system feature" in its log). This declares the feature in the app's container (a permissions file mounted by FramePort's Podman wrapper; the APK isn't changed). Whether the app's VR part then works on the Frame depends on the app (GitHub #484). Applied with Update on Frame.
### <a id="device-language"></a>Game language
Choose the language for games that follow the system language. The game must include it. Apply with Update on Frame.
@@ -2654,6 +2826,20 @@ Extra settings for the Android container (for testing).
> Environment variables exported by the launcher (for example VK_INSTANCE_LAYERS="" to test without Valve's layers).
### <a id="device-shared_folder"></a>Share a folder with another game
Makes a folder of this game's storage the same as another game's (e.g. a companion app that manages its game's mods), as on a Quest where all apps share one storage.
- `device.shared_folder` · also for 2D apps
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
- **Options:**
- `game`: str, default empty — The other game's package, e.g. com.drbeef.jkxr
- `folder`: str, default empty — The folder in /sdcard to share, e.g. JKXR
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
> On a Quest every app sees the same /sdcard; on the Frame each game has its own. This makes a folder of this game's /sdcard the same folder as in another game's, e.g. a companion app that manages its game's mods and start options (JKXR's companion app and /sdcard/JKXR, GitHub #440). The folder stays in the other game's storage; this game's own copy of it, if it had one, is kept beside it. Both games must be installed on the Frame. Applied with Update on Frame.
### <a id="device-steam_gamepad"></a>Controllers as a gamepad (Steam Input)
Lets the game use your controllers as a gamepad instead of a pointer, for 2D games with touch controls (for example SDL games).
@@ -2675,11 +2861,12 @@ 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):** The app opens a flat launcher that starts its VR part (…): Lepton runs VR apps headless, so the launcher is only seen in a shown Android window.
- **Suggested (on):** App with text fields: key presses only reach a shown Android window.
- **Catalog games (122):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Assassin's Creed Nexus, Attack on Titan VR: Unbreakable and 114 more.
- **Catalog games (135):** AFFECTED: The Manor, AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arcsmith, Arkio, Attack on Titan VR: Unbreakable, Audica and 127 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".
> 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). While the game runs, FramePort turns off SteamVR's automatic theater mode for game windows: it put the Android window in front of the player with the controllers as a mouse pointer, and the game got no controller input. The window stays a page of the game in the dashboard. Pairs with "Make Unity text fields work without a system keyboard". Apps that open a flat launcher before their VR part (for example Xelee or WiiCompiled, whose launchers pick the game) need the window too: headless, the launcher is never seen and the VR part never starts. For those, theater mode stays on, so the launcher is in front of you; once the VR part runs, close the dashboard with the controller's button if it opened on the window.
## <a id="group-pcvr"></a>PC VR (Proton and Revive)
+104 -71
View File
@@ -1,7 +1,9 @@
# Porting playbook
Everything here was hit while porting Quest and Rift games to the Steam Frame (September–October 2026; 38 catalog recipes). The machine-readable
version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); keep both in sync.
Symptoms, causes and fixes found while porting Quest and PC VR games to the Steam Frame since September 2026. The
machine-readable version is `catalog/triage.yaml` (used by launch tests, session triage and `frameport test`); keep
both in sync. Rows name the GitHub issue (#N) where a problem was reported; [DEVICE_NOTES.md](DEVICE_NOTES.md) has the
longer investigations.
## Fast path for a new game
1. `frameport scan <folder>` → check the suggested recipe (`frameport show <pkg>`). Direct-VrApi engines get the bridge.
@@ -13,24 +15,27 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
## Startup failures (visible in launch.log)
| Symptom | Cause | Fix |
|---|---|---|
| `RuntimeManifestFile::FindManifestFiles - using global runtime file /vendor/etc/openxr/1/active_runtime.json`, no FrameBridge lines (#254) | Embedded Khronos loader bypasses the APK loader chain | `frame.static_openxr_loader`: redirect its manifest paths and broker authorities, bundle runtime shim + inner loader + FrameBridge; reinstall to write `/sdcard/fp_openxr_runtime.json`. Device check pending |
| Valve foveation sharp centre moves to the top right or a white line appears at the right edge as Batman shrinks its projection rect (#406 / #394) | Dynamic resolution draws a corner-anchored subrect inside the full swapchain used by Valve FDM | Enable `adapter.fixed_resolution` (Game settings → Picture), rebuild/reinstall for the new adapter and OVRPlugin shim; verify `fixed_resolution: recommended ... -> ...` and full-size `projection view ... rect change` logs. Headset verification pending; more GPU load |
| Quest 3-only options missing; OVRPlugin logs `venderId 10291, systemName Oculus Quest2` (#373, Asgard's Wrath 2) | OVRPort's dispatcher overwrites FrameBridge's name and supplies a Quest 2 `XR_META_headset_id`; OVRPlugin prioritizes that UUID | `device.headset_model`: Quest 3 by default, per-game Quest 3S / Quest Pro / Quest 2; rebuild the APK. Byte-checked dispatcher name + UUID edit; Quest 2 leaves the dispatcher unchanged. See DEVICE_NOTES 'Per-game headset identity' |
| 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 |
| 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) | The game's 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` |
| Foveation full-resolution area looks identical at 10% and 45% (GitHub #394), while render resolution changes it | `FOVE_INNER`/`FOVE_OUTER` are neither passed through by Lepton nor read by Valve's layer; shared injection uses rectangular density presets | Game settings → Picture now exports named `FOVE_LEVEL=lo/med/hi`; Default preserves the runtime choice. Medium/high shrink the center and reduce peripheral density further. Legacy radius preferences map to the nearest old nominal preset. Compare Fixed + Darken coarse fragments with adapter `snapshot=N` at a fixed render resolution; see FRAME_RUNTIME.md for addresses and profile data. Agent launcher allowlist needs `FOVE_LEVEL`. |
| Valve foveation's sharp area sits off-centre in both eyes (bottom right in In Death: Unchained / The 7th Guest, towards the swapchain centre with dynamic resolution or asymmetric eye rects), Eye-tracked or Fixed (GitHub #394) | Valve's density map covers the whole swapchain with its sharp rectangle at the swapchain centre; the game's view is a smaller or shifted imageRect and the eye's optical axis isn't the rect centre (asymmetric FOV) | Rebuild (Update on Frame): the arm64 FrameBridge build's `VK_LAYER_FRAMEPORT_fdm_centre` (native/foveation/centre.h) sits below Valve's layer and moves each eye's map to its optical centre (Fixed, run on Valve's offset path: Valve's own `disable_offsets` path crashed AC Nexus) or re-bases the gaze on the eye's rect (Eye-tracked); the layer never changes images and writes every offset Turnip reads (Valve declares 1 for two views). launch.log: `FrameBridge: fdm views: …` and `fdm_centre: pass WxH map … -> x,y`. Check headlessly with Darken coarse fragments + `snapshot=N` (Vulkan: `fb_snap_vk_N_vV.ppm`). `FRAMEPORT_FDM_CENTRE=0` (launcher `LEPTON_ENV_`) turns it off for comparison. The area's size still snaps to Turnip's GMEM bins (density is chosen per bin), so the area changes somewhat with render resolution; the bin snapping isn't fixable in FramePort |
| 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`) | Game settings → Picture → "Foveated rendering (Valve)": **Fixed** (`FDM_DEBUG=disable_offsets`; verify injection remains active on the installed runtime) or **Off** (`VK_INSTANCE_LAYERS=""`); catalog field `foveation:` |
| One eye (usually the right) shimmers or jitters in the 3D view while menus and panels look steady; pacing is clean (GitHub #69, for example 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`) | Game settings → Picture → "Foveated rendering (Valve)": **Fixed** (`FDM_DEBUG=disable_offsets`; verify injection remains active on the installed runtime) or **Off** (`VK_INSTANCE_LAYERS=""`); catalog field `foveation:` |
| With `device.foveation` Eye-tracked/Fixed, the sharp area looks right but everything around it is black with scattered coloured blocks (menus on quads too); pacing fine (GitHub #303, Medieval Dynasty; Vampire Survivors reported the same) | Unity's Oculus XR plugin asks for SUBSAMPLED eye images (`XR_META_vulkan_swapchain_create_info`, `VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT`); under Valve's FDM their coarse areas stay in the compact subsampled layout, which the compositor reads as an ordinary image. `foveation_fix=1` only dropped the `XrSwapchainCreateInfoFoveationFB` nodes and passed the META node on. With `foveation_fix=0` + `foveation_storage_fix=1` the adapter logs `foveation_storage_fix: shared allocation SUBSAMPLED cleared` for the eye swapchains | `device.foveation` (tracked/fixed) now also sets `foveation_storage_fix=1`, which clears SUBSAMPLED whatever `foveation_fix` is (generic version of Batman's OVRPlugin allocation fix). Rebuild + reinstall (new adapter). Headless-checked only (72 fps, bit cleared); picture needs the headset |
| A Unity OpenXR game closes right after the OpenXR instance is created: `F VrApiLoader: vrapi_SetPropertyInt was called before vrapi_Initialize()!`, SIGABRT on UnityMain (GitHub #62, Jurassic World Aftermath) | the APK still ships Meta's VrApi loader; OVRPlugin (1.89, on OpenXR) calls it without starting VrApi and Meta's loader aborts. The VrApi bridge can't replace it (39 of the loader's 114 functions: OVRPlugin wouldn't link) | `frame.vrapi_stub` (same exports, each returns 0); triage `vrapi-before-init` |
| A VrApi game exits at once: `UnsatisfiedLinkError … dlopen failed: cannot locate symbol "vrapi_PollEvent"` (GitHub #57, BlazeRush) | the engine imports a VrApi function the VrApi bridge didn't implement | the bridge has `vrapi_PollEvent`, `RecenterPose`, `SetDisplayRefreshRate`, `GetSystemPropertyFloatArray` (rebuild the game); the static check "VrApi functions resolvable" names any other missing one; triage `vrapi-symbol-missing` |
| A Team Beef port (Lambda1VR, RTCWQuest, …) exits at once: `UnsatisfiedLinkError: dlopen failed: library "libopenxr_loader_valve.so" not found` (GitHub #59) | TBXR loads `openxr_loader_<Build.MANUFACTURER>` (Lepton: valve) and takes the Meta path only when `strstr(OPENXR_HMD, "meta")` matches, else Pico's | `frame.tbxr_vendor` (empty `libopenxr_loader_valve.so` + the "meta" literal → "alve"). Game data (`xash/`, …) goes to /sdcard: upload it with the Files tab to the game's storage. Black eyes with `TBXR: Incomplete frame buffer object: GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE`: TBXR always uses multisampled render-to-texture (even `--msaa 1`); `frame.tbxr_vendor` links the GL shim, which hands it single-sampled versions (log `GL shim: multisampled render-to-texture … drawn single-sampled`) |
| A Quest game renders, then logs `Entitlement check failed` / `ovrPlatformInitialize_NotEntitled` (often with retries) and quits, e.g. `SIG: 9` after `[Photon] Disconnected` (GitHub #322, Deadly Delivery) | The game's Meta licence check (entitlement) needs Meta's online login, which the Frame doesn't have; online-only games quit (triage `meta-entitlement`) | None: FramePort doesn't get around entitlement checks. Offline games that log it and play on are fine |
| Black screen after VR starts; `Asset status error (code 100)` in `DLC_Manager:AssetStatusCallback` (GitHub #203, e.g. PixelRipped 1995) | Meta asset-file status fails after entitlement succeeds; the log does not identify the native call or asset ID | Try `frame.asset_files_stub` (opt-in, triage-only): native/langpack answers GetList with an empty successful list and StatusById with installed status, an empty path and the requested ID. No DLC files are supplied; download requests retain their existing behavior. Disable with `FRAMEPORT_ASSET_FILES_STUB=0`. Host-tested only; whether this fixes startup needs a game/device test |
| A Unity Quest game stays on a loading screen or doesn't go on; `An unknown error occurred. Request failed.` from `Oculus.Platform.Request`1:OnComplete` (GitHub #346 Eleven, #360 Spectre, #368 Thief Simulator; triage `meta-request-failed`) | A Meta platform call the game waits for got no usable answer: OVRPort's platform library answers some calls without Meta's services (request id 0) and Meta's Unity wrapper drops the callback | Find the call in the stack (the game's own coroutine after `OnComplete`); a per-game fix that skips it when it isn't needed to play (e.g. an analytics id request). Calls the game needs online stay unsupported |
| Game aborts at start: `OVRPlatform-Loader: DisplayErrorAndExit: Failed to launch SystemActivities` after an `ActivityNotFoundException` stack (GitHub #357 Breachers; triage `meta-system-activities`) | The game ships its own renamed copy of Meta's platform loader (e.g. `libovr_custom.so`), which bypasses OVRPort's dispatcher and tries to open Meta's system activities (Horizon OS) | None in general: such games are online games built on Meta's services. Serving OVRPort's dispatcher under that name is possible but unlikely to make them playable |
| A Quest game renders, then logs `Entitlement check failed` / `ovrPlatformInitialize_NotEntitled` (often with retries) and quits, for example `SIG: 9` after `[Photon] Disconnected` (GitHub #322, Deadly Delivery) | The game's Meta licence check (entitlement) needs Meta's online login, which the Frame doesn't have; online-only games quit (triage `meta-entitlement`) | None: FramePort doesn't get around entitlement checks. Offline games that log it and play on are fine |
| Black screen after VR starts; `Asset status error (code 100)` in `DLC_Manager:AssetStatusCallback` (GitHub #203, for example PixelRipped 1995) | Meta asset-file status fails after entitlement succeeds; the log does not identify the native call or asset ID | Try `frame.asset_files_stub` (opt-in, triage-only): native/langpack answers GetList with an empty successful list and StatusById with installed status, an empty path and the requested ID. No DLC files are supplied; download requests retain their existing behavior. Disable with `FRAMEPORT_ASSET_FILES_STUB=0`. Host-tested only; whether this fixes startup needs a game/device test |
| A Unity Quest game stays on a loading screen or doesn't go on; `An unknown error occurred. Request failed.` from `Oculus.Platform.Request`1:OnComplete` (GitHub #346 Eleven, #360 Spectre, #368 Thief Simulator; triage `meta-request-failed`) | A Meta platform call the game waits for got no usable answer: OVRPort's platform library answers some calls without Meta's services (request id 0) and Meta's Unity wrapper drops the callback | Find the call in the stack (the game's own coroutine after `OnComplete`); a per-game fix that skips it when it isn't needed to play (for example an analytics id request). Calls the game needs online stay unsupported |
| Game aborts at start: `OVRPlatform-Loader: DisplayErrorAndExit: Failed to launch SystemActivities` after an `ActivityNotFoundException` stack (GitHub #357 Breachers; triage `meta-system-activities`) | The game ships its own renamed copy of Meta's platform loader (for example `libovr_custom.so`), which bypasses OVRPort's dispatcher and tries to open Meta's system activities (Horizon OS) | None in general: such games are online games built on Meta's services. Serving OVRPort's dispatcher under that name is possible but unlikely to make them playable |
| Missing sound or extra content in a game whose OBB folder has separate `*.pak` files (GitHub #61, Star Wars: Tales from the Galaxy's Edge: seasons + Wwise banks) | the game asks Meta's platform for these asset files (`ovr_AssetFile_GetList` / `DownloadById`); OVRPort's loader doesn't list them | copy every content/sound-bank PAK and use `frame.asset_files` (native/langpack lists the data's `*.pak` as installed `default` assets, IAP `free`, and answers DownloadById at once with the path; off: env FRAMEPORT_ASSET_FILES=0). Tales also needs the splash and depth-size fixes below |
| Quest saves need a cloud folder or backups after playing (discussion #352) | Steam does not sync Quest saves | Settings → Data: choose a writable local cloud-sync folder and optionally enable **Back up saves when a game closes**, then connect to apply it. The Frame keeps three changed snapshots per game; FramePort imports them on connection/poll. Manual **Back up saves** is next to Play; Restore stays in the menu. Old backup folders remain available and are never moved silently |
| A SideQuest backup has `apk/` and a sibling `obb/` with content/sound-bank PAKs, but the Frame only has the main OBB (GitHub #61, Tales from the Galaxy's Edge) | older scans treated that layout as loose expansion files and selected only the named OBBs; the asset-file patch then found no PAKs | the scanner chooses the OBB folder by version first, then keeps the whole sibling data folder when all its expansion files are selected for this APK; app updates repair affected library entries and refresh asset/language-pack detection. Reinstall to copy the missing files, or scan the backup again first. Folders with other packages or unselected OBB versions still send only the selected OBBs |
@@ -38,57 +43,68 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Upper part of the intro/environment is black or clipped, but a separate menu panel is intact (GitHub #61, Tales from the Galaxy's Edge) | `FSceneRenderTargets::AllocMobileMultiViewDepth` caches the initial Android window dimensions (1920x1016/1020), leaving the depth attachment shorter than the eye color texture (1728x1728); drawing is limited by the smaller attachment | `frame.unreal_multiview_depth`: refreshes both cached depth dimensions from the current scene `BufferSize` before allocation. Checks the exported symbol, function size and surrounding instructions, then replaces only the branch that skips refresh. Keeps multiview and multisampling. Headset test: full view and gameplay work |
| `cannot locate symbol "ovrPeerConnectionState_ToString"` (or another `ovr<Enum>_ToString`) at start (BlazeRush, GitHub #57) | OVRPort's platform loader lacks several Platform SDK enum helpers | `frame.ovrstubs` now stubs them too; `*_ToString` stubs return "" (not NULL) |
| Exits right after the OpenXR instance is created: `Failed to create XR session: -50` (`XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING`, Lambda1VR) | the game skips `xrGet*GraphicsRequirementsKHR`; Meta's runtime tolerates it, SteamVR's doesn't. Lambda1VR's TBXR doesn't even enable `XR_KHR_opengl_es_enable` (only `XR_EXT_local_floor`) | FrameBridge adds `XR_KHR_opengl_es_enable` when an app enables no graphics API extension, asks for the requirements on the game's behalf and retries xrCreateSession once (logs `added XR_KHR_opengl_es_enable …`, `the app skipped …; asked for it`; every launch logs the final extension list `xrCreateInstance with N extension(s)`); triage `graphics-requirements-missing`. Verified headless: Lambda1VR runs at 72 fps |
| Exits at start: `OVRAvatar-Loader: DisplayErrorAndExit: Failed to launch SystemActivities` after `ovrAvatar_Initialize: Failed to load AvatarSDK driver` (BlazeRush, GitHub #57) | Meta's avatar loader needs Horizon; it then tries Meta's error screen and aborts | `frame.avatar_stub` (same exports, do nothing); triage `avatar-driver-missing`. Games that ship the library but don't start avatars at launch (Lucky's Tale, BattleSisters, Arcsmith) don't need it |
| Exits at start: `OVRAvatar-Loader: DisplayErrorAndExit: Failed to launch SystemActivities` after `ovrAvatar_Initialize: Failed to load AvatarSDK driver` (BlazeRush, GitHub #57) | Meta's avatar loader needs Horizon; the loader then tries Meta's error screen and aborts | `frame.avatar_stub` (same exports, do nothing); triage `avatar-driver-missing`. Games that ship the library but don't start avatars at launch (Lucky's Tale, BattleSisters, Arcsmith) don't need it |
| BlazeRush (GitHub #57, Klownicle's guide): the first menu takes A, the next ignores all input; car bodies and controllers invisible; coloured streaks/flashes from particles; narrow square view; blurry, or displaced with black borders after a scale change | the menus wait for a Meta avatar the stub never delivers; bone indices fed with glVertexAttribPointer (Mesa converts them to floats); mediump particle maths; FOV read once before any session (bridge said 90x90); old program binaries in its shader cache; user_config.xml keeps the old resolution | catalog recipe: `frame.blazerush` (avatar wait NOP + avatar renderer RET + cache folder `cache/br_shader_v1`, exact build only), GL shim `gl_int_attribs=1` + `gl_highp_markers=in uvec2 inBoneIndices;\|uniform InstanceData\|void main()` (`gl_hide_multiview=0`, `gl_hide_msrtt=0` keep its GL as tested), the VrApi bridge's saved FOV (`framebridge-vrapi-fov.txt`; triage `vrapi-fov-before-tracking`), `scale=1.7`, `device.config_sync` (agent writes r_3dwidth/r_3dheight/r_width/r_height from the runtime's eye size × scale before each start); triage `gl-integer-attribs` |
| Frame freezes or the game is killed (OOM) on the first controller vibration (for example Lucky's Tale's save slots, BattleSisters, Sniper Elite VR's tutorial grab: memory grows ~1 GB/s until the kill) | OVRPort runtimes up to 3.4.3-23204ea read the haptic envelope's duration (ns) as seconds and allocate gigabytes of samples (GitHub #9, ovrport/app#73); also reached from Unity's legacy `ovrp_SetControllerHaptics`; perf page faults all in `libopenxr_loader.so` `xrApplyHapticFeedback` | `adapter.haptic_fix` (envelope → one plain vibration in `native/xrshim`; PCM vibrations (`XrHapticPcmVibrationFB`) are converted the same way). Fixed in runtime 3.4.3-aa54c3f: builds made with it leave the workaround out by themselves (log "not needed: adapter.haptic_fix", `frameport show` "last build left out"); rebuild an older build to switch |
| Controller vibrations much stronger than on a Quest (The Boys VR, Jurassic World, BONELAB, …) | OVRPlugin games send amplitude envelopes; `haptic_fix` turned each into one vibration at the envelope's *peak* for its whole duration (a short fading pulse became a long full-strength buzz) | `haptic_fix` now uses the envelope's RMS; Game settings → "Vibration strength" (`haptic_scale`, 0-1) scales every vibration in FrameBridge; launch.log shows the first requests (`haptic: …`). OVRPlugin stops a vibration with amplitude 0 (2 s duration): FrameBridge turns that into xrStopHapticFeedback (without it the last buzz ran its full 2 s) |
| Play or a launch test fails at once: `Game files missing at <path> (storage not mounted?)` in launch.log, or "… files are on a drive that isn't inserted" (GitHub #90) | the game was installed or moved to a microSD card (`<mount>/FramePort/<pkg>`) that isn't inserted or mounted now | insert the card; or move the game back (game menu → Move to…, `frameport frame move <pkg> --to internal`) once the card is back. `frameport frame drives` shows what the agent sees (exFAT/NTFS cards are refused: format in SteamOS) |
| A Linux app started from Desktop Mode's menu closes after ~2 s, or opens on the wrong display (GitHub #84) | its launcher predates agent v63: the "Steam parent gone" watchdog ended it when Plasma's launcher exited, and it took gamescope's DISPLAY from Steam | connect FramePort once (agent v63's `upgrade_launchers` adds the `FRAMEPORT_DESKTOP` checks; the menu entry runs `env FRAMEPORT_DESKTOP=1 <anchor>/launch.sh`). Game Mode controller problems in such apps are Steam Input's (a separate issue) |
| A Linux app (AppImage) shows no/FramePort's placeholder icon in Desktop Mode or Steam (GitHub #99) | before agent v64 only FramePort's art set's icon was used, and a lone AppImage has no store art | connect FramePort once (agent v64 copies the app's `.DirIcon` / `.desktop` `Icon=` to `<anchor>/artwork/app-icon.*` and refreshes the menu entry); reinstall to also get it into the library + Steam shortcut. Still the placeholder: check `<base>/app/squashfs-root/.DirIcon` and the `.desktop` file's `Icon=` (an icon outside the app folder or only an XPM isn't used); a chosen icon (`artwork/.icon-source` = `custom`) always wins |
| An overlay app (fpsVR, a wrist watch) installed on the Frame never appears, or SteamVR doesn't start it | not registered with the Frame's SteamVR, or registered with a manifest SteamVR skips: on the Frame vrserver reads only `binary_path_linux_arm` (vrserver.txt: "must specify binary_path for launch_type binary. Skipping") | `pcvr.vr_overlay` (Windows) / a Linux app with a bundled `.vrmanifest` (`is_dashboard_overlay`): agent v75 `register_vr_overlay` (check `vrcmd --overlays`, `~/.config/openvr/config/appconfig.json` manifest_paths); the app's own `--install` writes binary_path_linux only. fpsVR under Proton never loads OpenVR (quits after 60 s): unsupported for now |
| An overlay app with **Start with SteamVR** on doesn't start by itself after the Frame or SteamVR starts (it works when started by hand) | SteamVR's own auto-launch flag (SetApplicationAutoLaunch) didn't persist on the Frame (no autolaunch entry in `~/.config/openvr/config/steamvr.vrsettings`) | agent v76: FramePort's service `frameport-vr-overlays.service` starts it at every new vrserver. Check `systemctl --user status frameport-vr-overlays`, `~/.local/share/frameport/vr-overlays.log` (`new SteamVR (vrserver N)` → `<pkg>: started` / `running` / `couldn't start it: …`), the deployment's `overlay.autostart`, and that `~/.local/share/frameport/vr-overlays.disabled` doesn't exist; `frameport frame info` (ensure_host_fixes) installs a missing service |
| A Linux app started from Desktop Mode's menu closes after ~2 s, or opens on the wrong display (GitHub #84) | an older launcher: the "Steam parent gone" watchdog ended it when Plasma's launcher exited, and it took gamescope's DISPLAY from Steam | connect FramePort once (`upgrade_launchers` adds the `FRAMEPORT_DESKTOP` checks; the menu entry runs `env FRAMEPORT_DESKTOP=1 <anchor>/launch.sh`). Game Mode controller problems in such apps are Steam Input's (a separate issue) |
| A Linux app (AppImage) shows no/FramePort's placeholder icon in Desktop Mode or Steam (GitHub #99) | older installs used only FramePort's art set's icon, and a lone AppImage has no store art | connect FramePort once (the agent copies the app's `.DirIcon` / `.desktop` `Icon=` to `<anchor>/artwork/app-icon.*` and refreshes the menu entry); reinstall to also get it into the library + Steam shortcut. Still the placeholder: check `<base>/app/squashfs-root/.DirIcon` and the `.desktop` file's `Icon=` (an icon outside the app folder or only an XPM isn't used); a chosen icon (`artwork/.icon-source` = `custom`) always wins |
| An overlay app (fpsVR, a wrist watch) installed on the Frame never appears, or SteamVR doesn't start it | not registered with the Frame's SteamVR, or registered with a manifest SteamVR skips: on the Frame vrserver reads only `binary_path_linux_arm` (vrserver.txt: "must specify binary_path for launch_type binary. Skipping") | `pcvr.vr_overlay` (Windows) / a Linux app with a bundled `.vrmanifest` (`is_dashboard_overlay`): agent `register_vr_overlay` (check `vrcmd --overlays`, `~/.config/openvr/config/appconfig.json` manifest_paths); the app's own `--install` writes binary_path_linux only. fpsVR under Proton never loads OpenVR (quits after 60 s): unsupported for now |
| An overlay app with **Start with SteamVR** on doesn't start by itself after the Frame or SteamVR starts (it works when started by hand) | SteamVR's own auto-launch flag (SetApplicationAutoLaunch) didn't persist on the Frame (no autolaunch entry in `~/.config/openvr/config/steamvr.vrsettings`) | FramePort's service `frameport-vr-overlays.service` starts it at every new vrserver. Check `systemctl --user status frameport-vr-overlays`, `~/.local/share/frameport/vr-overlays.log` (`new SteamVR (vrserver N)` → `<pkg>: started` / `running` / `couldn't start it: …`), the deployment's `overlay.autostart`, and that `~/.local/share/frameport/vr-overlays.disabled` doesn't exist; `frameport frame info` (ensure_host_fixes) installs a missing service |
| Every game suddenly fails to start: `crun: create keyring …: Disk quota exceeded`, `is not a running context` | rootless podman leaked a kernel keyring per launch; 200-key quota exhausted | `keyring = false` in `~/.config/containers/containers.conf` (FramePort agent does it), then reboot the Frame once |
| Launch test "failed" although the game runs in the headset: frames paced, then `OVRPortVrApi: EnterVrMode: no tracked head pose before deadline`, then a crash (e.g. `zink: DEVICE LOST` + SIGSEGV in a `gdrv` thread) (GitHub #57, BlazeRush) | the VrApi bridge ends the VR session after 30 s without head tracking, which only happens without a worn headset; the game's renderer then crashes while tearing down | none needed: launch tests (`triage(..., headless=True)`) treat an exit or crash after a `headless_end` line (catalog/triage.yaml) as a pass when frames were paced before it (info finding `headless-end`); play sessions ignore it. Add new such markers to `headless_end` |
| Launch test "failed" although the game runs in the headset: frames paced, then `OVRPortVrApi: EnterVrMode: no tracked head pose before deadline`, then a crash (for example `zink: DEVICE LOST` + SIGSEGV in a `gdrv` thread) (GitHub #57, BlazeRush) | the VrApi bridge ends the VR session after 30 s without head tracking, which only happens without a worn headset; the game's renderer then crashes while tearing down | none needed: launch tests (`triage(..., headless=True)`) treat an exit or crash after a `headless_end` line (catalog/triage.yaml) as a pass when frames were paced before it (info finding `headless-end`); play sessions ignore it. Add new such markers to `headless_end` |
| 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 Cortopia game (Escaping Wonderland, GitHub #360) stays on its loading screen (frames paced); launch.log: `E Unity: An unknown error occurred. Request failed.` with `OculusPlatformExtension:ToUniTask` / `Cortopia.Scripts.Platform.Meta.<Initialize>d__13` | after the licence check and the logged-in user, the game asks for `Users.GetOrgScopedID` when on Wi-Fi (its analytics id); OVRPort's `ovr_User_GetOrgScopedID` returns request id 0, Meta's C# wrapper logs that line and never calls back, so the awaited start-up never finishes | `frame.cortopia_org_id` (suggested from the analysis, triage `cortopia-org-id-wait`): the `b.ne` after `cmp w0, #2` on `get_internetReachability` in `<Initialize>d__13.MoveNext` -> `b` (Cpp2IL), the game's own no-Wi-Fi path; the same "Request failed." line in another game = another request OVRPort doesn't implement |
| A Cortopia game (Escaping Wonderland, GitHub #360; TMNT: Empire City, #422) stays on its loading screen (frames paced); launch.log: `E Unity: An unknown error occurred. Request failed.` with `OculusPlatformExtension:ToUniTask` / `Cortopia.Scripts.Platform.Meta.<Initialize>d__13`, or `PlatformExtensions:ToUniTask` / `Cortopia.Scripts.Platform.<Login>d__17` | after the licence check and the logged-in user, the game asks for `Users.GetOrgScopedID` when on Wi-Fi (its analytics id); OVRPort's `ovr_User_GetOrgScopedID` returns request id 0, Meta's C# wrapper logs that line and never calls back, so the awaited start-up never finishes | `frame.cortopia_org_id` (suggested from the analysis, triage `cortopia-org-id-wait`): the `b.ne` after `cmp w0, #2` on `get_internetReachability` in `<Initialize>d__13.MoveNext` (or `OculusPlatform.<Login>d__N.MoveNext`) -> `b` (Cpp2IL), the game's own no-Wi-Fi path; the licence check before it is untouched (the error comes from the generic `Request`1`, the licence check uses the non-generic `Request`); the same "Request failed." line in another game = another request OVRPort doesn't implement |
| A Unity game waits on its loading screen after Meta's logged-in user; launch.log: `An unknown error occurred. Request failed.` from `Oculus.Platform.Request`1:OnComplete` called in the logged-in user's callback (GitHub #452 Eleven Table Tennis: `MetaPlatform:<BuildIdentity>b__0`; also Cortopia's games, #360/#422) | the game asks `Users.GetOrgScopedID(userId)` and waits for the answer; OVRPort's `ovr_User_GetOrgScopedID`, `ovr_Message_GetOrgScopedID` and `ovr_OrgScopedID_GetID` are `mov x0, xzr; ret` (request id 0, no message ever), so Meta's wrapper drops the callback. (`ovr_User_GetUserProof`, which Eleven asks next, is answered by OVRPort: a queued message with a nonce) | `frame.ovr_org_scoped_id` (triage `meta-org-scoped-id-wait`; suggested for known games and Cortopia's analysis flag): `libfp_langpack.so` (native/langpack, marker `@FPORGID@`, env `FRAMEPORT_ORG_SCOPED_ID=1`) answers with a fresh request id and a successful `ovrMessage_User_GetOrgScopedID` whose id is a hash of the (already stable) user id; linked alone it passes the language-pack calls through (`@FPLANGS@` 0) |
| A VR game renders but ignores the controllers; the Android screen (or the dashboard showing it) with a mouse pointer in front of you (AC Nexus, #476 PadelVRTraining, #477 AltspaceVR; all with "Show the app's Android window") | `device.text_input_window` makes the Android window a Steam game window in gamescope; SteamVR auto-theaters it before the game's OpenXR session exists and opens the dashboard on it when the session starts (`vrwebhelper_systemui.txt`: "GameFrame now alive, auto-theatering", `SetDockLocation`) | the dashboard worker turns `dashboard.autoShowGameTheater` off while such a game runs (dashboard.log: "theater mode off while the game runs"); with `FRAMEPORT_KEEP_DASHBOARD=1` (or an older FramePort): turn the window off for that game or press the dashboard button once |
| 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) |
| 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>` |
| The game starts (Process RUNNING, launch test passes) but nothing appears and no OpenXR/FrameBridge line is ever logged; logcat shows a 2D/SDL activity drawing to its window and then going quiet (#475 Xelee) | The launcher activity is a flat 2D launcher (often category `com.oculus.intent.category.2D`) that starts a separate VR activity (analysis `extra.vr_activity`); Lepton runs VR apps headless, so the launcher is never seen and waits forever | `device.text_input_window` (suggested for such apps) shows the launcher; the deployment's recipe `flat_launcher` keeps SteamVR's theater mode on for it. Once set up, `frame.start_activity` can start the VR activity directly (skips the launcher) |
| 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 | FramePort now 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: Play registers the game through Steam's devkit interface ("Devkit Game: …", `devkit_register`) and starts that; uninstall removes it. 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) | 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>` |
| Progress/settings lost after closing the game, although it saves on a Quest; every start logs a fresh save (Just Dance VR: `OfflineSaveDico_FromString - NeedLoad: False`), the session log has no `OnApplicationPause(true)` before its end (GitHub #480) | The container was killed (launch.sh `podman kill`, Lepton's teardown `reboot -p` + `podman stop -t 0`) while the activity was still resumed: Android never paused the app, so saves written in `onPause`/`OnApplicationPause` never ran | launch.sh `pause_game` (added to existing launchers by `upgrade_launchers`), `stop` and the Monitor's End game first `am start` the HOME intent in the container and wait (≤3 s) until the activity is STOPPED; result in `<base>/stop-pause.log`. A game still loading may not answer within 3 s (`still state=STOPPING`). Steam's own Exit game (Steam signals the whole process tree at once): launch.sh's `_exit_hook` worker hooks `SteamClient.Apps.TerminateApp` in Steam's UI through CEF devtools and pauses the game before Steam's call goes through (≤3 s; `<base>/exit-hook.log`; off per game with `FRAMEPORT_NO_EXIT_HOOK=1`). Not covered: Steam restarts / power-off, SteamVR's own "Exit <app>" (`QuitSceneApp`). See DEVICE_NOTES "Graceful stop" |
| Just Dance VR (0.5.4 and later): progress, unlocks and the tutorial are gone at every start although the game now pauses before it ends (`OnApplicationPause(true)` in the log); `OfflineSave_Init succeeded` and no save file is ever written (GitHub #480) | The game loads its progress from a local save (`LocalSaveEvents.RequestHasSaveData`/`RequestLoad` -> `LocalSaveManager.OnLoadValueRequested` -> `LocalSaveController.LoadData` -> `OfflineSave_GetValue`) but `LocalSaveEvents.RequestSave` is empty (one `ret` in libil2cpp.so), so `OnSaveValueRequested` -> `SaveData` -> `OfflineSave_SetValue` never run. On a Quest progress lives in Ubisoft's PlayFab after a login with the Meta user id (`PlayfabAuthenticate`, `LoginWithCustomID`); no Meta platform on the Frame = no sign-in = nothing kept. `OfflineSaveDico_FromString - NeedLoad: False` is logged after every parse with a constant false: the line doesn't mean the data was rejected (an HMAC mismatch is error 41 = file marked corrupted) | `frame.justdance_local_save`: RequestSave branches to its declared body (`SaveValueRequested?.Invoke(new LocalSaveEventArgs(key, value))`), built from `SaveSystemReady` decoded with capstone, in a new R+X segment (`elf.append_segment`); all addresses from Cpp2IL per build; a build whose RequestSave has a body is left alone. The game's OfflineSave thread then writes `files/JdNext.OfflineSave/JdOfflineSave_JdLog.OfflineSave.txt` within ~1 s. Nothing online touched. Triage `justdance-local-save-off` (`unless_patch`: not when the fix is in the build). Per-song star records (PlayerDataTracker) still don't load: its loader ends with a `yield break` |
| 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 |
| 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. The game's 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. Close and start the game again without uninstalling: the folders are now repaired (Aftermath confirmed in the headset). The launcher also 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 |
| 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 (`kind: linux_x86`; FEX also needs `STEAM_COMPAT_DATA_PATH`: "No compat data path?" in launch.log = an older 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 |
| PC VR game runs as a flat 2D window (no VR) | SteamVR was not running, so Revive/LibOVR had no runtime to bind to | FramePort now auto-starts SteamVR on Play; make sure SteamVR comes up before the game |
| PC VR game crashed launching via Steam on the OpenXR backend | Revive's newer OpenXR backend was less reliable | PC now defaults to Revive's OpenVR (SteamVR) backend; reinstall to apply |
| Oculus PC VR game: `Unable to load LibOVRRT DLL` / `-3001` on the Frame | The game can't find a VR runtime (Revive's LoadLibrary hook doesn't work under Proton-arm64) | `pcvr.libovr_redirect` (default on) symlinks Revive's runtime as `LibOVRRT{64,32}_1.dll` in the exe dir — pure substitution; a build that checks the runtime signature then hits `-3021` (not bypassed) |
| Oculus PC VR game: `Unable to load LibOVRRT DLL` / `Failed to initialize Oculus API (-3001)` on the Frame | The game can't find a VR runtime: Revive's LoadLibrary hook doesn't work under Proton-arm64 (ARM64EC kernelbase) | `pcvr.libovr_redirect` (default on with Revive) symlinks Revive's runtime as `LibOVRRT{64,32}_1.dll` in the exe dir and next to every OVRPlugin.dll: pure substitution; a build that checks the runtime signature then hits `-3021` (not bypassed) |
| Oculus Rift game won't run on the Frame | Oculus/LibOVR game: needs Revive, whose hooks don't work under Proton-arm64, and FramePort won't defeat the Oculus runtime signature check | Play it on this PC (SteamVR + Revive). Only OpenVR/OpenXR-native Rift games run on the Frame |
| Un-cracked Oculus PC VR game: "Initializing OVR session" then exits / signature check on the runtime | Revive's hooks don't install on Proton-arm64; the Oculus shim rejects the unsigned Revive runtime | Not runnable on the Frame without the repack's crack extracted (not done by FramePort); use the PC version |
| PC VR game: `Failed to initialize Oculus API (-3001)` / `Unable to load LibOVRRT DLL` | Revive's LoadLibrary hook doesn't work under Proton arm64 (ARM64EC kernelbase); the game's LibOVR shim finds no runtime DLL | Symlink `LibOVRRT64_1.dll` → Revive's DLL in the prefix's system32 (manual so far) → then -3021 (runtime signature check; open) |
| Uploads to the Frame are slow | The link, storage or transfer backend can limit throughput | Check the job's "Transfer link"; try the Frame's hotspot or USB cable. Large install files prefer system OpenSSH SFTP, retaining paired-host checks and `.part` resume; unavailable or failed native uploads fall back to Paramiko. The log names the backend. Settings → Installing → Parallel uploads accepts 1–8 (default 3; saved preferences are preserved). More is not always faster. Measure the production path with `scripts/upload_bench.py --methods app-files-3 --check-resume` (three repetitions, checksum verification) |
| Log: `OpenSSH SFTP failed (255): … Permission denied (publickey,password)`, then Paramiko | OpenSSH offered keys the Frame doesn't authorize (GitHub #242) | The log lists the keys OpenSSH offered (`Offering public key`) and its warnings. A connection that logged in with the PC's own keys/agent makes OpenSSH use them too (copies of `~/.ssh/id_*` + the agent); a refused FramePort key is retried that way once. A Frame that accepts only another key file: Settings → Installing → SSH keys (CLI `frameport frame ssh-keys --add <file>`); passphrase keys go through the SSH agent |
| Log: `OpenSSH SFTP failed (1): stat C://…/[…: No such file or directory` | sftp got glob escapes inside its quotes (GitHub #324, fixed): `[` in a local folder name | Update FramePort; the native_upload `quote()` passes only `\\` and `\"` escapes, Windows paths with `/` |
| PC VR game exits right away: `Unhandled Exception: 0xc06d007e` (after `FOnlineSubsystemOculus::InitWithWindowsPlatform`) | Delay-loaded `LibOVRPlatform64_1.dll` (Oculus Platform SDK, installed with the Oculus app) is missing | None on the Frame (entitlement check; FramePort doesn't replace it). PC mode with the Oculus app |
| Unity PC VR game (Oculus + SteamVR build: `OVRPlugin.dll` and `openvr_api.dll` in `<Name>_Data/Plugins`) opens a window and quits after ~20 s; no VR Vulkan instance after the game's DXVK device in launch.log; Unity log (`unity log … Player.log`, agent v67): `OpenVR failed initialization` / `Initialization of device … failed` | Unity's built-in VR tries its SDKs in list order (Oculus first) unless `-vrmode` picks one; a catalog recipe verified with another build of the game (for example SUPERHOT VR's OpenXR build, GitHub #105) used to drop the build's own arguments | `pcvr.launch_args` = `-vrmode OpenVR` (detected for such builds; kept when the catalog's `xr` differs from the build's). A game's Electron launcher next to it (SHVR.exe) is ranked below the game |
| Unity PC VR game (Oculus + SteamVR build: `OVRPlugin.dll` and `openvr_api.dll` in `<Name>_Data/Plugins`) opens a window and quits after ~20 s; no VR Vulkan instance after the game's DXVK device in launch.log; Unity log (`unity log … Player.log`): `OpenVR failed initialization` / `Initialization of device … failed` | Unity's built-in VR tries its SDKs in list order (Oculus first) unless `-vrmode` picks one; a catalog recipe verified with another build of the game (for example SUPERHOT VR's OpenXR build, GitHub #105) used to drop the build's own arguments | `pcvr.launch_args` = `-vrmode OpenVR` (detected for such builds; kept when the catalog's `xr` differs from the build's). A game's Electron launcher next to it (SHVR.exe) is ranked below the game |
| Unreal PC VR game opens the crash reporter instead of closing | UE starts CrashReportClient.exe on a crash | `pcvr.no_crash_reporter` (default for Unreal Rift games): `-nocrashreports` + CrashReportClient.exe renamed `.disabled` in the Frame copy |
| A backup's game was added without its data (library `data_dir` null, `data_bytes` 0) although the folder has .obb files | The OBBs sit in a layout the scan didn't know: `<game>/obb/<package>/`, `Android/obb/<package>/`, or SideQuest-style `<game>/apk/x.apk` + `<game>/obb/<package>/` | `sources/quest_dump.find_data_dir` finds a `<package>` folder with .obb files ≤3 levels below the APK's folder or ≤2 below its parent (skipping neighboring folders with APKs: other games); an `apk`/`apks` folder counts as part of its game folder. Add the folder again (Analyze again doesn't look for data) |
| "Failed to correctly load the game files" at the menu; `Error loading FMOD bank "…/Android/obb/<package>/Master.bank": ERROR` (GitHub #239, Jurassic World Aftermath Collection); launch test previously passed at 72 fps | SideQuest's `<package>/<backup time>/apk/x.apk` + `obb/Master.bank` + `obb/Master.strings.bank` backup was scanned without data: these are raw sound banks, not .obb expansion files, so the sibling-folder search missed them | `quest_dump.find_data_dir` checks the parent game folder's `obb/` (or `obbs/`) when the APK is in `apk/` or `apks/`, including raw assets with or without a package subfolder. Add the backup folder again and reinstall; triage `fmod-bank-missing` fails the launch test even when the error screen renders frames. Copy the whole data folder from the same backup as the APK |
| A game from an AXRB download starts and stops at once (Steam shows Resume), no OBB on the Frame; or a SideQuest backup adds a second game named after an APK in its `obb/` folder (reddit 2026-10-11) | AXRB keeps its own PC builds in `AXRB/patched/<package>/*-axrb.apk` (OVRPort-converted for its runtime, no OBBs); the scan added them over the original `AXRB/<app id>/<binary id>/base.apk`. SideQuest's VR4 backup has Unreal's `VR4-Android-Shipping-arm64.apk` next to the OBBs | `quest_dump._axrb_patched` skips AXRB's patched copies; a rescan replaces entries made from them; AXRB downloads send every non-APK file next to `base.apk` (DLC, assets) and are named after the package; APKs inside another game's data folder are ignored; SideQuest backups are named after the package folder, not the backup time. docs/FAQ.md has both layouts |
| Game hangs at start / never loads although its .obb files are right there: next to the APK, in an `obb` folder beside an `apk` folder inside a `<package>` folder, or a SideQuest backup (`<timestamp>_<versionCode>.apk`) (GitHub #85, #91) | No folder named after the package, so the folder-layout checks found nothing and no OBB was uploaded | `quest_dump.find_data` then looks for the files by name, `(main\|patch).<versionCode>.<package>.obb` (any case), in the APK's folder, its parent and up to 3 levels below each (folders with their own APKs are other games); prefers the folder and files of the APK's own versionCode, else the newest older, else any version. When that folder also holds other things (the APK itself, other games' OBBs), the library entry gets `data_files` and only those files are uploaded, counted and deleted with the game. Add the folder again |
| CryEngine game: `Powered by CRYENGINE`, then SIGTRAP in `libAndroidLauncher.so` during `CreateSystemInterface`, before any OpenXR instance (GitHub #238, The Climb 666.0) | Installed without its OBB: the SideQuest backup has `apk/com.crytek.climb.apk` + `obb/main.com.crytek.climb.obb`, but the scan required a versionCode in the OBB name and skipped it. The reported backtrace's fatal-error call formats `Engine initialization failed: Open Engine Paks: Unable to find "engine" pak files (%s)` | `quest_dump.find_data` accepts `(main\|patch).<package>.obb` as well as numbered names, keeping the original filename. Update on Frame finds the previously skipped data; a rescan finds it too. Analysis version 12 recognizes monolithic CryEngine through `assets/game.cryproject`, Vulkan through `assets/system.cfg` (`r_Driver = vk`), and an expected OBB through the launcher's expansion-path format strings; a missing OBB now gets the existing `missing-obb` warning/finding. This fixes the original missing-content startup failure; the rendering failures below were found once the data was installed |
| The Climb 666.0: audio with no headset picture, `OVRPortVrApi: Unsupported frame flags or swap interval`; then SIGSEGV in `vulkan.freedreno.so` during `vkCmdEndRenderPass` once frames are accepted (GitHub #238) | The game submits the obsolete Quest texture-density debug flag (`0x100`), which the bridge rejected. It also binds the bridge's full-rate texture as a fragment-density attachment even though foveation support is reported off; Turnip reads density maps on the CPU, but an ordinary sampled image has no mapped backing memory | Updated `frame.vrapi_bridge` accepts the compositor debug/volume flags and creates the compatibility texture with `VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT`. Do not gate that usage on the advertised format features (the Frame reports R8G8_UNORM without the density bit). `device.files` selects `r_vr_foveated = 0` when the engine contains that cvar; The Climb 2 keeps `r_variable_rate_shading = 0`. Triage `vrapi-frame-rejected` marks rejected frames as fatal despite pacing logs. Picture confirmed on the headset. With the corrected config, an uncapped launch stayed running for over four minutes at a median 72 fps with no native crash or GPU fault. The user confirmed working menus and gameplay on 2026-10-12 |
| Unreal/Unity game stops right after its graphics start or hangs at start although its OBBs are complete; the files are named `main.203.com.Armature.VR4-003.obb` / `patch.203.com.Armature.VR4-001.obb` or differ in case (GitHub #450, #481) | the game opens `main.<versionCode>.<package>.obb` by exact name | installs send such files under the right name (`quest_dump.obb_names`; the files on the PC keep theirs) and the game page says so (info). Still a warning, sent unchanged: another app's name (#129 `….lj369vr.obb`), a copy next to the right-named file, two copies for one name, a copy suffix without a versionCode: rename by hand, scan again, Update on Frame. docs/DEVICE_NOTES.md#expansion-file-names-450-481 |
| CryEngine game: `Powered by CRYENGINE`, then SIGTRAP in `libAndroidLauncher.so` during `CreateSystemInterface`, before any OpenXR instance (GitHub #238, The Climb 666.0) | Installed without its OBB: the SideQuest backup has `apk/com.crytek.climb.apk` + `obb/main.com.crytek.climb.obb`, but the scan required a versionCode in the OBB name and skipped it. The reported backtrace's fatal-error call formats `Engine initialization failed: Open Engine Paks: Unable to find "engine" pak files (%s)` | `quest_dump.find_data` accepts `(main\|patch).<package>.obb` as well as numbered names, keeping the original filename. Update on Frame finds the previously skipped data; a rescan finds it too. Analysis version 12 recognizes monolithic CryEngine through `assets/game.cryproject`, Vulkan through `assets/system.cfg` (`r_Driver = vk`), and an expected OBB through the launcher's expansion-path format strings; a missing OBB now gets the existing `missing-obb` warning/finding. The OBB fix solves the original missing-content startup failure; the rendering failures below were found once the data was installed |
| CryEngine game (The Climb 1/2) on FramePort 1.3.0: `Messagebox: cap: Engine initialization failed! text:CrySystem initialization failed!` right after the renderer lists its Vulkan device extensions, then SIGSEGV in `CSystem::ShutDown` (GitHub #432/#449). The Climb 1 (monolithic, no symbols): SIGSEGV fault addr 0xf0 under `CreateSystemInterface+320` in `libAndroidLauncher.so` right after `MESA: Using gralloc0 CrOS API`, or a hang at that line | 1.3.0 links the dormant foveation diagnostic `libVkLayer_fdm_darken.so` into every arm64 game's engine library; in CryEngine's `libCryRenderVulkan.so` that stops CrySystem from starting. Bisected headless on the dev Frame: the new build runs once only its renderer is the unmodified one | The layer exports `vkEnumerate{Instance,Device}{Extension,Layer}Properties` and `elf.add_needed` put it first, so a renderer that imports those from `libvulkan.so` got the layer's answers (no extensions). `foveation.diagnostic_support` now links it after the existing DT_NEEDED entries, skips `libCryRender*` and CryEngine's monolithic `libAndroidLauncher.so` (The Climb 1; Climb 2's private VrApi bridge still loads the layer); `frame.adapter` package revision 3 for Climb 2, 5 for The Climb 1 marks their builds for Update on Frame. Triage `cryengine-init-failed` |
| VrApi game: `OVRPortVrApi: xrInitializeLoaderKHR failed: OpenXR result -7`, then SIGTRAP from the engine (no `FrameBridge:` lines; static check warns `not in the APK or the system list: libVkLayer_fdm_darken.so`) (GitHub #432, The Climb 1 on a dev build) | FrameBridge (`libopenxr_loader_generic.so`) is linked against `libVkLayer_fdm_darken.so` on arm64, but the build skipped shipping the layer file together with the CryEngine link, so FrameBridge didn't load and the bridge found no loader | `diagnostic_support` always ships the layer; `frame.adapter` validate fails without it. Triage `vrapi-bridge-no-loader` |
| QuestCraft: Minecraft never starts, `libjvm.so not found`; no `files/runtimes/` folder in the game's storage; the log shows `Transports: ETHERNET` (GitHub #18, #468) | pojlib's `API.hasConnection()` only counts Wi-Fi and Lepton's Android reports Ethernet, so the Java runtime download is skipped | `frame.pojlib_connection` makes `hasConnection()` return true (dex edit; catalog + QuestCraft's package, new scans by analysis field). Workaround: unpack QuestCraft's `JRE.zip` into `files/runtimes/JRE` with the Files tab. Triage `questcraft-no-java-runtime` |
| Nothing starts, `APP_ACTIVITY is empty`, although the manifest has category LAUNCHER: it sits on an `<activity-alias>` (Flycast-based HOTD2 VR: `MainActivity` → `NativeGLActivity`, GitHub #400/#469) | Lepton only starts real activities | `frame.launcher` gives the alias's target activity its own MAIN/LAUNCHER intent-filter (the alias's categories; inserted after the activity's start element) and turns the alias's LAUNCHER into INFO (`axml.launcher_from_alias`); the validator's "Launchable in Lepton" check now requires a real activity |
| A game shows UTC instead of local time (Walkabout Mini Golf's clock, GitHub #429); in the container `getprop persist.sys.timezone` = `../usr/share/zoneinfo/<Zone>` | Lepton sets the zone from a bare `readlink /etc/localtime` minus `/usr/share/zoneinfo/`, and SteamOS' link is relative | Lepton launchers put `~/.local/share/frameport/lepton-shims` first on PATH (agent `TZ_LINE`, `upgrade_launchers` adds it); its `readlink` answers `/etc/localtime` with the resolved path. Checked on the dev Frame: `America/New_York`, `date` in EDT. A boot property line from FramePort can't override it (Lepton writes its own line later) |
| A companion app doesn't find its game's files (JKXR's companion app and `/sdcard/JKXR`, GitHub #440) | On a Quest all apps share one /sdcard; in Lepton each game has its own | `device.shared_folder` (`game`, `folder`; catalog field `shared_folder`): launch.sh links the folder to the other game's copy and adds that path to `STEAM_COMPAT_LIBRARY_PATHS`, which Lepton bind-mounts at the same path (agent `SHARE_LINES`). Checked on the dev Frame between two installed games (read + write). The companion can't start the other game: each game has its own container |
| 8K video (Batman's cutscenes) plays choppy: `Iris video/hevc busy (8192x4096)` then `initialization failed (-12); using software decoding` (GitHub #437) | Iris takes an 8K session only while no other is open; Steam's own web helper sometimes keeps `/dev/video22` (qcom-iris-decoder) open, most likely after its UI played a video. With it free the same file decodes in hardware (dev Frame, 2026-10-23) | Restart Steam (or the Frame) before playing. Hardware-video launchers log `FramePort: hardware video decoder … in use by: …` at start (agent `DECODER_USERS_LINE`); triage `video-decoder-held-by-steam`. Videos up to 640x480 decode in software so FramePort's own small players never hold Iris |
| The Climb 666.0: audio with no headset picture, `OVRPortVrApi: Unsupported frame flags or swap interval`; then SIGSEGV in `vulkan.freedreno.so` during `vkCmdEndRenderPass` once frames are accepted (GitHub #238) | The game submits the obsolete Quest texture-density debug flag (`0x100`), which the bridge rejected. The game also binds the bridge's full-rate texture as a fragment-density attachment even though foveation support is reported off; Turnip reads density maps on the CPU, but an ordinary sampled image has no mapped backing memory | Updated `frame.vrapi_bridge` accepts the compositor debug/volume flags and creates the compatibility texture with `VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT`. Do not gate that usage on the advertised format features (the Frame reports R8G8_UNORM without the density bit). `device.files` selects `r_vr_foveated = 0` when the engine contains that cvar; The Climb 2 keeps `r_variable_rate_shading = 0`. Triage `vrapi-frame-rejected` marks rejected frames as fatal despite pacing logs. Picture confirmed on the headset. With the corrected config, an uncapped launch stayed running for over four minutes at a median 72 fps with no native crash or GPU fault. The user confirmed working menus and gameplay on 2026-10-12 |
| Unreal game: `JNI_OnLoad` (OVRPlugin), then silence: no OpenXR instance, no crash, no frames (GitHub #85 TRIANGLE STRATEGY; launch-test finding `missing-obb`) | Installed without its OBB: the manifest says `com.epicgames.ue4.GameActivity.bHasOBBFiles` = true (UE5: `com.epicgames.unreal.…`) but the library has no data folder (`data_dir` null, `data_bytes` 0) | Put the .obb files in a `<package>` (or `obb/`) folder next to the APK, add the folder again, reinstall. Analysis `extra.expects_obb`; the game page, the install question and `frameport scan`/`install` warn; launch tests that sent no frames get the `missing-obb` finding from the library (the game logs nothing). Unity split-binary builds count too (`extra.unity_split`, `analysis/unity_split.py`): an XR-plugin build (Oculus XR Plugin / Unity OpenXR library) without `assets/bin/Data/UnitySubsystems/` in the APK, or BuildSettings listing more scenes than the APK has `levelN` files (globalgamemanagers loose or read from the head of data.unity3d); exact on the 48 Unity APKs of the dumps (13 split, 17 full games; OBBs of full builds are asset bundles or sound banks) |
| Unreal game: launch.log ends with `ActivityTaskManager: START u0 {… cmp=<pkg>/.DownloaderActivity}` and `Displayed <pkg>/.DownloaderActivity`, then nothing: no OpenXR instance, no crash (GitHub #159 Contractors; triage `unreal-obb-check-stuck`, also when the OBB is on the Frame) | Epic's DownloaderActivity (started by GameActivity.onResume before the engine's main init) checks every OBB's name + size compiled into the APK (OBBData); with manifest meta-data `com.epicgames.ue4.GameActivity.bVerifyOBBOnStartUp=true` it then reads the whole OBB and compares each entry's CRC32 (cached in `files/cacheFile.txt` by the OBB's mtime). Its progress and error screens are invisible in Lepton (headless VR); a CRC mismatch waits for a button forever. A missing / wrong-size OBB starts Google Play's downloader (or a "No Google Play Store Key" dialog) and also waits | `frame.unreal_skip_obb_check` (suggested by analysis `unreal_verify_obb`): sets that meta-data to false (one boolean in the manifest, `axml.set_meta_data_bool`), which skips only the CRC pass. Still stuck: the OBB doesn't match the APK version (name `main.<versionCode>.<pkg>.obb` and its size) - copy APK + OBB from the same install |
| Unreal game: launch.log ends with `ActivityTaskManager: START u0 {… cmp=<pkg>/.DownloaderActivity}` and `Displayed <pkg>/.DownloaderActivity`, then nothing: no OpenXR instance, no crash (GitHub #159 Contractors; triage `unreal-obb-check-stuck`, also when the OBB is on the Frame) | Epic's DownloaderActivity (started by GameActivity.onResume before the engine's main init) checks every OBB's name + size compiled into the APK (OBBData); with manifest meta-data `com.epicgames.ue4.GameActivity.bVerifyOBBOnStartUp=true` it then reads the whole OBB and compares each entry's CRC32 (cached in `files/cacheFile.txt` by the OBB's mtime). DownloaderActivity's progress and error screens are invisible in Lepton (headless VR); a CRC mismatch waits for a button forever. A missing / wrong-size OBB starts Google Play's downloader (or a "No Google Play Store Key" dialog) and also waits | `frame.unreal_skip_obb_check` (suggested by analysis `unreal_verify_obb`): sets that meta-data to false (one boolean in the manifest, `axml.set_meta_data_bool`), which skips only the CRC pass. Still stuck: the OBB doesn't match the APK version (name `main.<versionCode>.<pkg>.obb` and its size) - copy APK + OBB from the same install |
| Unreal game has CRC verification off but the delivery bypass is unavailable (GitHub #159, Medal of Honor: Above and Beyond) | Obfuscation renamed `expansionFilesDelivered`: MoH calls boolean predicates `a()` and `d()` from its package DownloaderActivity.onCreate, both logging `Checking for file : ` and calling Google expansion filename/delivery helpers. The second gate can show “Select OBB to use” | Analysis recognizes these predicates by activity, caller, log and helper return types; stuck-downloader triage offers `frame.unreal_skip_obb_delivery` as an experimental opt-in. CRC stays off. The inspected build checks three Java OBB entries and has native indexed-patch filename support; its backup has 22 OBBs and 34 MP4s, all selected from the package folder. Bypassing Java delivery does not establish native data completeness or headset success |
| Unreal game still stays on `DownloaderActivity` with CRC checking off (`bVerifyOBBOnStartUp=false`) and a fully uploaded matching OBB (GitHub #283, Into The Radius) | The separate Java `expansionFilesDelivered()` name and size gate can reject an OBB through Android external storage in Lepton; disabling CRC does not bypass it | Try `frame.unreal_skip_obb_delivery` (opt-in, experimental, triage-only). It returns true from known Unreal/package delivery predicates in every dex, preserving normal downloader success and main init. Analysis `unreal_obb_delivery` (version 16) controls applicability. The native engine still needs complete, matching, readable OBB data; unverified on a device |
| Unreal game still stays on `DownloaderActivity` with CRC checking off (`bVerifyOBBOnStartUp=false`) and a fully uploaded matching OBB (GitHub #283, Into The Radius) | The separate Java `expansionFilesDelivered()` name and size gate can reject an OBB through Android external storage in Lepton; disabling CRC does not bypass it | Try `frame.unreal_skip_obb_delivery` (opt-in, experimental, triage-only). The patch returns true from known Unreal/package delivery predicates in every dex, preserving normal downloader success and main init. Analysis `unreal_obb_delivery` (version 16) controls applicability. The native engine still needs complete, matching, readable OBB data; unverified on a device |
| `INSTALL_FAILED_MISSING_SPLIT`, or the game page notes "Split APK: only the base APK was found" (triage `missing-split`, analysis `extra.split_apk`) | Only the base of a split APK set (Play app bundle install) was added: `android:isSplitRequired` / `requiredSplitTypes`, code or libraries live in the splits | Add the whole bundle (.xapk/.apks/.apkm/.apkx) or the folder of split APKs (discussion #482): `sources/bundles.py` merges base + the arm64-v8a split (lib/assets in Python, resource/feature splits via APKEditor), drops the split attributes and `com.android.vending.splits.required`; the merged APK is signed with the game's key |
| `INSTALL_FAILED_NO_MATCHING_ABIS` | 32-bit-only APK (Gear VR / Go / early Quest); Frame has no AArch32 | `frame.a32_bridge` (suggested for every 32-bit-only APK, experimental): the a32 translator runs the game's 32-bit libraries inside the 64-bit app (native/a32translator/bridge) |
| a32 bridge faults at a low Thumb PC while loading a library, with the same unrelocated pointer in `.init_array` (e.g. Journey of the Gods: `0x1259e9` in OVRPlugin) | The guest loader ignored the older `DT_ANDROID_RELR` / `DT_ANDROID_RELRSZ` tags, leaving relative pointers at their link addresses | Fixed in the guest loader: both Android and standard RELR tags use the existing RELR decoder. Rebuild the a32 runtime and APK; host regression tests cover both tag families, but game startup still needs a Frame run |
| a32 bridge faults at a low Thumb PC while loading a library, with the same unrelocated pointer in `.init_array` (for example Journey of the Gods: `0x1259e9` in OVRPlugin) | The guest loader ignored the older `DT_ANDROID_RELR` / `DT_ANDROID_RELRSZ` tags, leaving relative pointers at their link addresses | Fixed in the guest loader: both Android and standard RELR tags use the existing RELR decoder. Rebuild the a32 runtime and APK; host regression tests cover both tag families, but game startup still needs a Frame run |
| 32-bit game on the bridge quits right after its loading screen with `a32: block at 0x… overflowed the code buffer` (exit 127; Pinball FX2 VR, GitHub #342: OpenSSL's `sha1_block_data_order_armv8`, ~60 SHA-1 crypto instructions in a row, reached from `RAND_poll`) | Every instruction the JIT hands to the interpreter needs an exit stub; the translator only ended a block at 96 instructions or half its code buffer, so a dense run of such instructions overflowed the 64-entry stub table and `fatal()` ended the process | Fixed in the translator (jit.c `STUB_RESERVE`: a block ends while 8 stubs are left); rebuild the game. Find the code with the logged address minus the library's load address (`a32rt: loaded …`) and `A32_BREAK=<addr>` for its caller; `tests/programs/sha1ce.c` covers it |
| 32-bit Unity game on the bridge: a video never shows (Ghost Giant's intro stays black, GitHub #439); log `AndroidMediaNDK could not load symbol AMediaFormat_new` then `GetDirectBufferAddress: buffer at 0x… is outside the guest's reach` / `AndroidVideoMedia: Error while getting codec input buffers` | Unity's video player needs libmediandk (AMediaFormat/AMediaCodec/AMediaExtractor); the bridge had none, and Unity's JNI fallback reads MediaCodec ByteBuffers in the host's heap, above the guest's 4 GiB | Bridge `host/media.c` + imports.txt: the NDK media functions (and `guest/media_keys.c`: the `AMEDIAFORMAT_KEY_*` data symbols Unity also looks up; without them it stops at `could not load symbol AMEDIAFORMAT_KEY_AAC_PROFILE`), format strings/buffers copied, codec buffers mirrored in guest memory (synced at `queueInputBuffer` / `getOutputBuffer`); triage `a32-video-ndk`; harness `fakemedia.c` + `libbtest4.c`. Headless check (2026-10-24, `OVP_HEADLESS_TEST=1` + `snapshot=3`): Ghost Giant's two intro videos play and its main menu renders |
| 32-bit game on the bridge: framerate falls over minutes, purple/garbled geometry, then `Could not allocate memory: System out of memory!` (Spunky Dungeon) | The bridge's guest copies of mapped GL buffers were tracked per buffer target: a second buffer mapped on the same target lost the first one's copy (never written back: garbage vertices; never freed: ~10 MB/s until the guest's 4 GiB were full) | Fixed in the bridge (gl.c: mappings per buffer object). Check: the guest's `[anon:scudo:*]` maps below 4 GiB in /proc/<pid>/maps must stay flat |
| 32-bit game on the bridge loads, leaves VR and its engine threads end, no crash, no finish() (Gunjack) | The game checks its own APK signature (UE4 `AndroidThunkJava_GetSignature` = Base64 of the signing certificate's SHA-1, compared in AndroidMain): every FramePort build is re-signed | `frame.a32_bridge` stores the original APK's value in `assets/fp_a32_signature.txt` (Unreal games) and the bridge answers the guest's call with it (log `AndroidThunkJava_GetSignature: original signature returned`). Find other such checks with `A32_BREAK` on the game's exit path (vrapi_LeaveVrMode logs its caller) |
| 32-bit NativeActivity game on the bridge ignores every controller (Gunjack, UE 4.10 Gear VR: no vrapi input functions, it reads the Gear VR touchpad/back button as Android input events from its AInputQueue) | The bridge sent its controller events through `Activity.dispatchKeyEvent`, which a NativeActivity never passes to native code | input.c puts synthetic touchpad (motion, touchscreen source) and back-key events in front of the game's AInputQueue: `ALooper_pollAll/pollOnce` return the queue's ident while one waits, `AInputQueue_getEvent` hands it out, the AInputEvent/AKeyEvent/AMotionEvent imports answer for it. Right trigger = hold the touchpad, A = tap, right stick = swipe (up/down = swipe forward/back along the touchpad's X axis: Gunjack's `OnFlick` takes flick angles <45°/>315° as reload, 135-225° as special weapon), B/left menu = back (log `controllers: the game reads its AInputQueue`) |
@@ -101,8 +117,10 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| missing `ovrMessageType_ToString` | same, but the game needs a real string | `frame.ovrplatformcompat` (automatic) |
| The game stays in its built-in language or waits at start although its OBB / files hold language packs (`<tag>.lang`, for example `de.lang`) | overport's platform loader answers `ovr_LanguagePack_GetCurrent/SetCurrent` with request id 0, so the game never gets the pack's path | `frame.langpacks` (opt-in, experimental, shown for games whose data has `*.lang`; `native/langpack`, build it with `python native/build.py --only langpack`). Default pack: the one the game applies, else env `FRAMEPORT_LANGPACK=<tag>`, else the only pack; search path override `FRAMEPORT_LANGPACK_DIRS`. Unreal games (Deadpool VR) gray out a pack whose asset `Metadata` differs from the game's version string: the patch writes the APK's versionName as Metadata (env `FRAMEPORT_LANGPACK_META` overrides) and reports the pack under the `/storage/emulated/0/Android/obb/<pkg>/` spelling of its path; with `/sdcard/…` the text switched but the dialogue stayed silent (verified in the headset with Deadpool VR) |
| `ClassNotFoundException com.oculus.os.AnalyticsEvent` → abort | Quest telemetry lookup in Meta XR Audio (Unreal build) or native code | `frame.metaxr_telemetry` + `frame.oculusos` (automatic) |
| App closes at start: `ClassNotFoundException: Didn't find class "org.apache.http.protocol.HttpRequestHandler"` / `NoClassDefFoundError: Failed resolution of: Lorg/apache/http/…` (GitHub #484, YouTube VR; triage `system-library-missing`, also for the other libraries below) | The original manifest asks for `<uses-library android:name="org.apache.http.legacy" android:required="false"/>`; OVRPort's `patch_remove_uses_library` (default on) drops every `<uses-library>`, so Android 9+ doesn't put the legacy Apache HTTP classes on the class path of an app targeting API 28+. Lepton's Android 11 has them (/system/etc/permissions/org.apache.http.legacy.xml) | `frame.keep_system_libraries` (automatic when the original manifest names one of `system_libraries.LEPTON_LIBRARIES`, analysis `extra.uses_libraries`): puts those entries back after OVRPort, always `required="false"`; Update on Frame |
| A 2D panel app (YouTube for Quest) stays in its panel: full screen and 360°/immersive buttons do nothing, log `Missing hybrid app system feature. Aborting launch of immersive mode.` (GitHub #484) | Horizon OS hybrid apps check for the system feature `oculus.software.vr.app.hybrid` (manifest `uses-feature … required="false"`) before they start their VR activity; the Frame's Android lacks it | opt-in patch `device.hybrid_app` (offered when the manifest names the feature; no APK change, Update on Frame): the launcher exports `FRAMEPORT_FEATURES` and puts FramePort's Podman wrapper on PATH, which mounts a permissions file at `/system/etc/permissions/frameport-features.xml`. Check: `podman exec lepton-steamlaunch-<appid> pm list features` lists the feature, launch.log has `FramePort: system features for this container`. Verified headless that Android picks the feature up; whether the app's VR activity then runs is untested |
| `JNI DETECTED ERROR`, `GetStringUTFChars … NULL` | CheckJNI is on because overport marks the app debuggable | `frame.nodebug` |
| Unreal game shows a one-time "optimizing" screen on its very first start, then closes (`System.exit` status 0 right after the system log's `ActivityTaskManager: START … LAUNCHER … pkg=<own package> … from uid 10xxx`, Lepton "Exited!"); the next start works | UE 4.21+ restarts the app after its first-run shader precompile (`r.ProgramBinaryCache.RestartAndroidAfterPrecompile`, `AndroidThunkJava_RestartApplication`); Lepton ends the container with the first process, the restarted one with it (Sports Scramble). Skipping the restart or setting the cvar to 0 (`-ini:Engine:[ConsoleVariables]:…` in UE4CommandLine.txt: read, no restart requested) left Sports Scramble on the "optimizing" level for good: the game relies on the restart | launch.sh starts Lepton once more (agent v86 `_restart_requested` matches both log lines within 5 s; `launch-before-restart.log` keeps the first run; `FRAMEPORT_NO_RESTART=1` turns it off). Works for 32- and 64-bit games |
| Unreal game shows a one-time "optimizing" screen on its very first start, then closes (`System.exit` status 0 right after the system log's `ActivityTaskManager: START … LAUNCHER … pkg=<own package> … from uid 10xxx`, Lepton "Exited!"); the next start works | UE 4.21+ restarts the app after its first-run shader precompile (`r.ProgramBinaryCache.RestartAndroidAfterPrecompile`, `AndroidThunkJava_RestartApplication`); Lepton ends the container with the first process, the restarted one with it (Sports Scramble). Skipping the restart or setting the cvar to 0 (`-ini:Engine:[ConsoleVariables]:…` in UE4CommandLine.txt: read, no restart requested) left Sports Scramble on the "optimizing" level for good: the game relies on the restart | launch.sh starts Lepton once more (`_restart_requested` matches both log lines within 5 s; `launch-before-restart.log` keeps the first run; `FRAMEPORT_NO_RESTART=1` turns it off). Works for 32- and 64-bit games |
| Unreal game quits a few seconds after start (`System.exit`) | ForceQuit after a failed Quest platform check | alternate build with `patch_remove_unreal_force_quit` (`use_alt`) |
| Unreal game crashes a few seconds after the logo with no backtrace (Unreal's own signal handler hides it: logcat-crash.log empty, Zygote `exited due to signal 11`); the crashing thread is `GameThread`, pc in `libaaudio.so`, lr `libovrplatformloader.so (ovr_Microphone_GetOutputBufferMaxSize+0x10)`; the last platform call is `ovr_Microphone_Create` (for example The Walking Dead: Saints & Sinners Ch. 2) | OVRPort's platform loader opens the microphone's AAudio stream only in `ovr_Microphone_Start`, but `GetOutputBufferMaxSize` reads that stream unchecked; Unreal's Oculus voice asks for the size right after `Create` | `frame.ovr_microphone` (NULL check rewritten in place; size 0 until the microphone starts). OVRPort runtime 3.4.3-aa54c3f has the check itself: builds made with it leave the patch out (log "not needed: frame.ovr_microphone", `patches/upstream.py`) |
| `xrCreateSwapchain` -26 / format unsupported (GLES) | Frame takes only sRGB formats, no MSAA | adapter `swapchain_fix` (default on) |
@@ -112,17 +130,18 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| SIGABRT right after start, backtrace `libopenxr_loader.so (xrCreateSwapchain+…)` | overport's dispatcher aborts on swapchains > 4096 px ("Wrong createInfo size"); video players use 7680×3840 theater textures | `frame.swapchain_limit` (default on; guard → 16384 px; triage `swapchain-size-abort`), for example 4XVR |
| Cube loading screen missing/frozen, `cube_standin: dropped a layer (type=1000006000)` (HardBullet #385, Budget Cuts #107) | Runtime refuses cube swapchains and composites only quad/projection layers | Rebuild with `cube_standin=1`, `cube_quads=1` (defaults): mip-0 cube faces become six inward quads. Pools reuse six swapchains per size/format, with array slices for distinct cubes. Cubes are conservatively skipped in any frame containing a projection, since Frame draws quads above it. Allocation/copy failure falls back to dropping with `cube_quads:` diagnostics. Host-tested; headset visuals pending |
| Video/menu panel missing, log `dropped N unusable layer(s)` with cylinder (1000017000) or equirect2 (1000091000) layers | the Frame runtime lacks XR_KHR_composition_layer_cylinder/equirect(2)/cube (its SteamVR runtime only composites quad + projection layers) | adapter shows cylinders as flat strips (setting `cylinder_strips`, default on); 360° equirect layers (theaters, 360° videos): `adapter.equirect_emul` (GLES only, per game) |
| 360° theater / 360° video black or missing (for example 4XVR) | equirect layers dropped | `adapter.equirect_emul=1`: a worker thread with a shared GLES context converts each 360° image to a cube map when it changes and draws one adapter projection layer from it for every frame with exactly that frame's views; it replaces the 360° layers in place, so the game's own projection layer (balcony, controllers, 360° videos it draws itself) stays on top. Quads can't be used: the Frame draws quad layers above every projection layer whatever the order. Log: `first 360 view ready`, `in 5 s: N 360 image update(s) … M view redraw(s)`. Upside down / mirrored / behind you: `equirect_flip` 1/2/4 (`frameport settings <pkg> equirect_flip=1`, no rebuild). Never Vulkan (a Vulkan renderer hung the Frame's GPU in AC Nexus) |
| 360° theater / 360° video black or missing (for example 4XVR) | equirect layers dropped | `adapter.equirect_emul=1`: a worker thread with a shared GLES context converts each 360° image to a cube map when it changes and draws one adapter projection layer from it for every frame with exactly that frame's views; the layer replaces the 360° layers in place, so the game's own projection layer (balcony, controllers, 360° videos it draws itself) stays on top. Quads can't be used: the Frame draws quad layers above every projection layer whatever the order. Log: `first 360 view ready`, `in 5 s: N 360 image update(s) … M view redraw(s)`. Upside down / mirrored / behind you: `equirect_flip` 1/2/4 (`frameport settings <pkg> equirect_flip=1`, no rebuild). Never Vulkan (a Vulkan renderer hung the Frame's GPU in AC Nexus) |
| Fades to black don't show: you see the level loading behind a loading screen (Batman: Arkham Shadow, GitHub #197); with `layer_debug=1` launch.log shows `color scale 0.00 0.00 0.00 …` on the projection layer | the game fades by dimming its eye image with XR_KHR_composition_layer_color_scale_bias; whether the Frame's runtime applies it isn't confirmed in the headset yet | `adapter.fade_quad=1` (opt-in, `native/adapter/fade_quad.c`): FrameBridge adds a black, alpha-blended quad right after the dimmed projection layer (VIEW space, 1 m ahead, 6×6 m; opacity = 1 − scale in 16 steps from one 8×8 swapchain refilled only when the step changes: Vulkan clear / GLES clear; one swapchain per step hit XR_ERROR_LIMIT_REACHED in Batman), log `fade_quad: drawing fades over the picture`; `fade_quad=2` also removes the layer's own scale (if both show, the fade is doubled) |
| GAZZLERS stops at start: `Exception: Current Platform is not supported` at `PlatformDeterminer.GetAndroidPlatform` (GitHub #404) | the game chooses its Meta/PICO code from `SystemInfo.deviceModel` ("pico"/Quest substrings) and throws for anything else; Lepton's Android is "Valve Lepton" (write-once `ro.product.*`, not settable through `LEPTON_GFXRECON_*` boot properties) | `frame.gazzlers_quest_platform` (catalog): `PlatformDeterminer.GetAndroidPlatform` -> 1 (Platform.Meta) in libil2cpp.so (Cpp2IL), the path it takes on a Quest |
| Vermillion: the hand and brush disappear over the canvas while painting, but still paint (GitHub #200) | the game shows a canvas being painted as its own compositor (quad) layer and turns the canvas's scene renderer off; the Frame draws quad layers above every projection layer whatever the order, so the canvas covers the hand (Vulkan game: no GL layer emulation) | `frame.vermillion_scene_canvas` (catalog): `PaintableMesh.HasCompositorLayerAvailable` / `ShouldUseCompositorOverlay` -> false in libil2cpp.so (Cpp2IL), so the game's own fallback draws the canvas in the scene (Klownicle's finding) |
| Unity game hangs right after its first frames, no `pacing:` lines; SIGQUIT dump shows UnityMain in a game library's `JNI_OnLoad` → `usleep` (for example SKYBOX: `libskybox.so`, `SignatureChecker`) | the game checks its APK signing certificate; every port is re-signed | can't be fixed without defeating anti-tamper (FramePort doesn't): mark "Can't run". Diagnose hangs: `podman exec <container> kill -3 <pid>` writes all thread stacks to launch.log |
| Video player doesn't list sent videos (for example 4XVR "Internal Storage" only shows some) | the player lists its own /sdcard folder (4XVR: /sdcard/4XPlayer), not /sdcard/Movies; Android's media index doesn't work in Lepton | game page "Add videos" / game menu "Add videos and files" (Videos + the app's own folder, hard links) or `frameport frame send <files> --app <pkg>` |
| One eye gray in a stereo video, log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED) | fewer/smaller swapchains: lower `equirect_res`; unresolved |
| One eye gray in a stereo video, extra menus close, or the game stops drawing after a new eye-image set; log `xrCreateSwapchain … result=-10` | the runtime refused a swapchain (XR_ERROR_LIMIT_REACHED): about 15 live swapchains per session | `adapter.swapchain_pool=1` shares extra panel images in atlases: GLES (#387, 4XVR) and Vulkan (#424, AC Nexus with the Quest 3 identity; Vulkan pools only sRGB format-43 panels, never eye images). Log `swapchain_pool: virtual WxH [Vulkan] (…)`. The limit is a count cap, so lowering resolution does not fix it; headset validation pending |
| VrApi game (32-bit bridge or `frame.vrapi_bridge`): holding the menu button to recenter turns the view ~90-120 degrees to the side | the bridge's app space is LOCAL + the head's yaw at the start; the Frame's recenter moves LOCAL to the head, so the old yaw was added again | fixed in the VrApi bridge (2026-10-13): on XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING (LOCAL) and on `vrapi_RecenterPose` it takes the origin from the head again (log `recentered: head yaw …`); rebuild the game |
| Menu/screen jumps to where you look (for example 4XVR) | the app re-creates LOCAL spaces (4XVR: every 2–4 s) and the Frame places them at the current head yaw, or the app recenters after the Frame's brief focus dips | turn on `adapter.layer_debug`, check `that LOCAL space sits at … deg` vs `focus_hold: hid a … ms focus dip`; patches: `adapter.stable_local` (new LOCAL spaces line up with the first) / `adapter.focus_hold` (on by default: hides dips up to `focus_hold_ms` once the game has been focused for a second; never at start-up: a global debounce broke AC Nexus) |
| Pointer misses menu items | the Frame's aim pose differs from Touch's | `adapter.layer_debug` logs `aim in grip … pitch/yaw`; correct with `aim_pitch`/`aim_yaw`/`aim_forward` (`frameport settings`, no rebuild) |
| Video stutters although fps is steady | video frame rate doesn't divide the refresh rate (30 fps at 72 Hz) | `adapter.refresh_rate` (for example 90 for 30 fps, 72 for 24 fps); `layer_debug` logs the offered rates and the app's own requests |
| A saved 90 Hz refresh setting still launches at 72 Hz (for example Jurassic World Aftermath); FrameBridge reads `refresh_rate=90` but logs result `-1000101000` | The Frame's OpenXR runtime only enumerates its current rate and rejects requests for another mode (`XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB`) | Agent v92 applies `adapter.refresh_rate` to SteamVR's per-game `preferredRefreshRate` through IVRSettings, on save/install and before Lepton starts (existing launchers upgrade on connection). `0` clears the override; unavailable SteamVR is retried at launch. Device-tested: Aftermath selected 90 Hz and sustained 90.2 fps |
| A saved 90 Hz refresh setting still launches at 72 Hz (for example Jurassic World Aftermath); FrameBridge reads `refresh_rate=90` but logs result `-1000101000` | The Frame's OpenXR runtime only enumerates its current rate and rejects requests for another mode (`XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB`) | FramePort applies `adapter.refresh_rate` to SteamVR's per-game `preferredRefreshRate` through IVRSettings, on save/install and before Lepton starts (existing launchers upgrade on connection). `0` clears the override; unavailable SteamVR is retried at launch. Device-tested: Aftermath selected 90 Hz and sustained 90.2 fps |
| PC VR game judders/stutters although the GPU keeps up; vrcompositor.txt "Timed out. N total" high or frames dropped | the game misses the headset's refresh (for example 96 Hz = 10.4 ms; Stormland's slow frames take 10.7 ms) | `pcvr.steamvr_tuning` (default on): next Play sets SteamVR per-app `preferredRefreshRate` + `motionSmoothingOverride` via `fp_vrsettings.exe` |
| Quest app keeps recentering / snapping the view when you turn your head; log shows session state 5→4→3→4→5 within a second | the Frame briefly takes focus; the app recenters on focus changes | adapter `focus_hold` (on by default; an earlier global debounce made AC Nexus stay black and was removed) |
| Video player / app finds no local videos; MediaProvider "Requested path /home/steamos/... doesn't appear under ..." | Lepton's /sdcard is a symlink to a host path, so Android's media index rejects every file | upload in the Files tab (Videos = /sdcard/Movies) and browse folders in the app |
@@ -135,8 +154,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Game (Unity, GLES) freezes, `zink: DEVICE LOST` | multisampled render-to-texture hangs the GPU | `frame.unity_no_msaa`; if it persists: unfixable → PC version |
| Unreal GLES game crashes ~3 s after start: `Fatal signal 11 … fault addr 0x10000 in tid … (RHIThread)`, `#01 … libgallium_dri.so` (GitHub #83 Star Wars Pinball VR, triage `unreal-msrtt-crash`) | Unreal's mobile MSAA renders through multisampled render-to-texture; Zink's `find_rp_state` then indexes `rendering_state_cache[6]` one past its end (sample count ≥ 32) and calls a junk hash function (0x10000). Mesa bug (no bounds check) | `frame.unreal_gl_shim` (GL shim with `gl_hide_msrtt`, multiview kept for Unreal: Unreal 4.25 only enables multiview with GL_OVR_multiview, GL_OVR_multiview2 **and** GL_OVR_multiview_multisampled_render_to_texture, so the shim keeps the last one visible for Unreal and maps glFramebufferTextureMultisampleMultiviewOVR to the single-sampled glFramebufferTextureMultiviewOVR; log `multiview multisampled render-to-texture (N samples) drawn single-sampled`) |
| Crash with `SIGILL (ILL_ILLOPN)` and `*pc=0xd50323bf` (autiasp), for example on Unreal's HttpManager thread (GitHub #83 Star Wars Pinball VR, triage `pac-unpaired`) | The engine's OpenSSL ARMv8 assembly has autiasp without a matching paciasp (libUE4.so: 38 paciasp, 40 autiasp; Poly1305 NEON). Quest CPUs treat PAC hints as NOPs, the Frame's CPU checks them | `frame.pac_hints`: in a library with unpaired counts every paciasp/autiasp becomes a NOP (in place) |
| launch.log stops at `Waiting for app …` (nothing after it, `logcat: Unexpected EOF!` or `Logcat read failure`); the game runs, but Steam's "Resume game" menu isn't closed, the launch test shows nothing and session triage has no log (Vader Immortal on Lepton 3.0.5, Under Cover on 2.8.14; a silent mirror twice on 3.0.5 on 2026-10-14) | Lepton's logcat mirror died or never wrote | `_logcat_keeper` (started by launch.sh, added to older launchers by `upgrade_launchers`; agent v91) reads the container's logcat itself after a failure line or 20 s without a logcat line (`podman exec lepton-steamlaunch-<appid> logcat -v threadtime -T <epoch>`: from the launcher's start, or from just before the mirror's last line), sets a 16 MiB log buffer and appends to launch.log; log `<base>/logcat-keeper.log` |
| A game no longer starts after a reinstall that was launch-tested right away: logcat `Zip: EOCD not found, …/base.apk is not zip` / `Failed to parse …base.apk`, no `Start proc` for the game (VR HOT, 2026-10-09) | The launch test's 45 s ran from the launcher, but the first start after an APK change spends ~1–2 min booting Lepton and installing the app: stopping the container mid-install left a broken installed copy | Agent v68: the test window counts from Lepton's `Waiting for app` (at most 240 s extra for boot + install). Repair a broken one: `touch <base>/lepton-app/game.apk` (Lepton re-installs it on the next start) |
| launch.log stops at `Waiting for app …` (nothing after it, `logcat: Unexpected EOF!` or `Logcat read failure`); the game runs, but Steam's "Resume game" menu isn't closed, the launch test shows nothing and session triage has no log (Vader Immortal on Lepton 3.0.5, Under Cover on 2.8.14; a silent mirror twice on 3.0.5 on 2026-10-14) | Lepton's logcat mirror died or never wrote | `_logcat_keeper` (started by launch.sh, added to older launchers by `upgrade_launchers`) reads the container's logcat itself after a failure line or 20 s without a logcat line (`podman exec lepton-steamlaunch-<appid> logcat -v threadtime -T <epoch>`: from the launcher's start, or from just before the mirror's last line), sets a 16 MiB log buffer and appends to launch.log; log `<base>/logcat-keeper.log` |
| A game no longer starts after a reinstall that was launch-tested right away: logcat `Zip: EOCD not found, …/base.apk is not zip` / `Failed to parse …base.apk`, no `Start proc` for the game (VR HOT, 2026-10-09) | The launch test's 45 s ran from the launcher, but the first start after an APK change spends ~1–2 min booting Lepton and installing the app: stopping the container mid-install left a broken installed copy | The test window counts from Lepton's `Waiting for app` (at most 240 s extra for boot + install). Repair a broken one: `touch <base>/lepton-app/game.apk` (Lepton re-installs it on the next start) |
| Unreal game (OVRPlugin) never starts VR: launch.log has `OVRPlugin: JNI_OnLoad` but never `CompositorOpenXR::PreInitialize`, no OpenXR session, then a crash a few seconds in (for example Star Wars Pinball VR, UE 4.25: null pointer in `FSceneRenderer::GetMultiViewSceneColor`, GitHub #83) | Unreal's Oculus module looks up every `ovrp_*` function of the OVRPlugin it was built against (`InitializeOculusPluginWrapper`, all ANDed); OVRPort's OpenXR OVRPlugin lacks a few old ones (UE 4.25 / OVRPlugin 1.44: `ovrp_GetPTWNear`), so the wrapper fails and OculusHMD is never pre-initialized | `frame.unreal_ovrp_entrypoints` (suggested when analysis `unreal_ovrp_lookups` name functions the shipped OVRPlugin lacks): generated `libfp_ovrpstubs.so` (DT_NEEDED of libOVRPlugin.so; dlsym on the plugin's handle searches its dependencies) with a stand-in per missing name returning ovrpFailure (-1000); build note lists the names |
| Only the Android home screen shows; the log has Unity's VR device as `None` (for example Accounting+, Unity 2017) | Unity 2017–2018 built-in Oculus support starts VR only when `com.oculus.systemactivities` is installed; Lepton has no Meta system apps, so Unity falls back to non-VR | `frame.unity_oculus_check`: that package name in libunity.so → `android`, plus `native/ovrpshim` (libfp_ovrp.so): Unity 2017's legacy frame loop (`ovrp_Update2`/`ovrp_BeginFrame`) never calls `ovrp_WaitToBeginFrame`, so without the shim no `xrWaitFrame` happens, `CompositorOpenXR::Update … outside of frame bounds` floods the log and the dashboard freezes. With it: log `ovrp frame loop shim: waited for frame N`, ~71 fps. Accounting+ then stops at "press any button" although its OVRInput gets clean input (`INPUT_PROBE` in the patch logs connected controllers/buttons/input focus per call) — unresolved |
| Stuck on the loading screen or quits at start; `Unable to open archive file`, `is corrupted! Remove it and launch unity again` or `Failed to read data for the AssetBundle` in launch.log (for example Batman, GitHub #155) | The game's data folder (OBB) is incomplete or from another version than the APK | Copy the whole data folder again from the same install as the APK, add the game again, reinstall (triage `unity-data-missing`) |
@@ -152,7 +171,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Godot 4 app crashes at start: `java.lang.NullPointerException: null cannot be cast to non-null type android.content.ClipboardManager` at `org.godotengine.godot.Godot.<init>` (Godot 4.3/4.4; 4.2 when the clipboard is first used, in `Godot$mClipboard$2.invoke`) (for example EndoparasiticVR, GitHub #134); triage `godot-no-clipboard` | Lepton's Android has no clipboard service and Godot's Kotlin code casts it with a non-null `as ClipboardManager` (`Intrinsics.checkNotNull` before the check-cast). Godot 4.1 (Java) and 4.5+ (`as?`) don't throw | `frame.godot_clipboard` (suggested by analysis `godot_clipboard`): that checkNotNull call → nops in classes.dex (check-cast accepts null), and Godot's getClipboard/hasClipboard/setClipboard return at once ("" / false / nothing; `Dex.return_early`), in place; matched exactly, other dex files untouched. Checked against the Maven godot-lib 4.2.2/4.3.0/4.4.1 and the 4.3/4.4.1 release export templates (D8 re-reads the result); not yet run on the device |
| Game crashes at start: `java.lang.NoSuchMethodError: No virtual method getAvailableCommunicationDevices()… in class Landroid/media/AudioManager` at `com.vivox.sdk.AudioChangeListener.checkAudioRouteAndApplyChanges` (for example Green Hell VR, GitHub #101); triage `vivox-api31` | Newer Vivox (voice chat) builds call Android 12 audio-routing methods without a version check; Lepton is Android 11. Older Vivox builds (Eleven Table Tennis, BattleSisters) don't have that code | `frame.vivox_audio_route` (suggested by analysis `vivox_api31`): every AudioChangeListener method that calls one of them returns at once (`Dex.return_early`: return-void / `const/4 v0, 0; return v0`, in place); voice chat keeps the default audio route |
| Game crashes when voice chat starts (Table Troopers: at the tutorial's start): `java.lang.NoSuchMethodError: No virtual method getAvailableCommunicationDevices()… in class Landroid/media/AudioManager` at `com.exitgames.photon.audioinaec.AudioInAEC.updateAudioDevice` ← `AudioInAEC$1.run` (GitHub #362/#325); triage `photon-voice-api31` (the same error from another library: `audio-api31`, report it) | Photon Voice's Android microphone plugin picks the audio device with Android 12 methods (getAvailableCommunicationDevices / setCommunicationDevice) and has no version check (it logs "Start: SDK >= 31, using BroadcastReceiver" on every Android); Lepton is Android 11. `frame.vivox_audio_route` only edits Vivox's class, so it can't help here | `frame.photon_voice_audio_route` (suggested by analysis `photon_voice_api31`): AudioInAEC.updateAudioDevice (void, the only caller) returns at once (`Dex.return_early`, in place); the microphone thread goes on (communication mode, echo canceller, AudioRecord) on the default route. Checked against Table Troopers' classes.dex; not yet run on the device |
| A game on a microSD card or another drive doesn't start, loses its saves or loops at start; launch.log has `ln: cannot create symbolic link from 'Frame' to '/data/data/<pkg>'` or `Couldn't create directory for SharedPreferences file /data/user/0/<pkg>/…` (GitHub #213/#214/#215/#221: a card named "Steam Frame 512g"); triage `lepton-data-link-failed` | Valve's Lepton pastes `STEAM_COMPAT_DATA_PATH` unquoted into `sh -c "ln -s …/internal/<pkg> /data/data/<pkg>"` (liblepton mounting.sh) and APK paths into unquoted substitutions, so a path with a space never gets the app's internal storage | agent v83: the Quest launcher links the game folder to `~/.local/share/frameport/lepton-paths/<pkg>` at every start and gives Lepton that path (`app_dir_lines`); `upgrade_launchers` converts existing launchers at the next connection; deployment.json keeps the real path |
| A game on a microSD card or another drive doesn't start, loses its saves or loops at start; launch.log has `ln: cannot create symbolic link from 'Frame' to '/data/data/<pkg>'` or `Couldn't create directory for SharedPreferences file /data/user/0/<pkg>/…` (GitHub #213/#214/#215/#221: a card named "Steam Frame 512g"); triage `lepton-data-link-failed` | Valve's Lepton pastes `STEAM_COMPAT_DATA_PATH` unquoted into `sh -c "ln -s …/internal/<pkg> /data/data/<pkg>"` (liblepton mounting.sh) and APK paths into unquoted substitutions, so a path with a space never gets the app's internal storage | the Quest launcher links the game folder to `~/.local/share/frameport/lepton-paths/<pkg>` at every start and gives Lepton that path (`app_dir_lines`); `upgrade_launchers` converts existing launchers at the next connection; deployment.json keeps the real path |
| Start never finishes, 100 % CPU, launch.log of gigabytes repeating `EOSSDK.createKeychain` / `resetKeychain` (Beyond Sandbox, GitHub #214); triage `eos-keychain-loop` | Epic Online Services' Java keychain can't write its SharedPreferences and resets/creates without end (seen when the app had no internal storage, row above) | fix the storage first; `frame.eos_keychain_loop` (opt-in, offered by triage; analysis `eos_keychain`) makes `EOSSDK.resetKeychain` return at once (`Dex.return_early`) |
| Game quits ~20 s after start (Titan Isles, GitHub #310); launch.log has `FATAL EXCEPTION: GoogleApiHandler` `java.lang.NoSuchMethodError: No virtual method permitUnsafeIntentLaunch()` at `com.google.android.gms.common.internal.zzp` (play-services-basement); triage `gms-android12-call` | Not the missing Play services: the APK was built for minSdk 32, so R8 folded Play services' `PlatformVersion.isAtLeastS()`/`isAtLeastSv2()` to `return true`; OVRPort lowers minSdk, the folded checks stay, and Lepton (Android 11) lacks the Android 12 method | `frame.gms_platform_version` (suggested by analysis `gms_folded_sdk_checks`): those constant checks return false again (in-place dex edit) |
| App quits a few seconds after OpenXR starts (PianoVision, GitHub #297, after `frame.unity_appui_vibrator`); launch.log has `FATAL EXCEPTION: MidiDeviceConnectionWatchThread` `NullPointerException: … UsbManager.getDeviceList()' on a null object reference` at `jp.kshoji.driver.midi.device.MidiDeviceConnectionWatcher$MidiDeviceConnectionWatchThread.checkConnectedDevices`; triage `usb-midi-watcher` | Lepton's Android has no USB service (`getSystemService("usb")` is null); kshoji's USB MIDI driver polls it | `frame.usb_midi_watcher` (suggested by analysis `usb_midi_watcher`): the poll returns at once; no USB MIDI on the Frame |
@@ -160,8 +179,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Unity game freezes on its loading screen ~10-15 s in (Yupitergrad, GitHub #235); launch.log has `GrantPermissionsActivity: Permission grant result … permission=android.permission.BLUETOOTH_CONNECT`, then OVRPlugin `SetupDisplayObjects(): Unable to find EGLConfig for configId 0` and no more frames; triage `bhaptics-permission-prompt` | bHaptics' vest SDK (`com.bhaptics.commons.PermissionUtils.requestBluetoothPermission`) asks for Android 12's BLUETOOTH_CONNECT; on Lepton's Android 11 it can't be granted at install, so Android's permission screen takes the foreground for a moment, the game pauses/resumes and OVRPlugin's GLES compositor stops drawing | `frame.bhaptics_permission` (suggested by analysis `bhaptics_permission`): the request returns at once (`Dex.return_early`) |
| Unity game aborts at start: `Abort message: 'cannot create thread specific key for __cxa_get_globals()'` (SIGABRT in a library such as `libFirebaseCppApp-*.so`, for example Wander 0.3570, GitHub #198); triage `pthread-keys-exhausted` | Bionic allows 128 pthread keys per process; Unity 6 takes ~40 (most without a destructor), large native libraries (Gecko `libxul.so`, Firebase, avatar SDKs) and the Frame's own libraries (Mesa/Zink, FrameBridge, OVRPort, Valve's Vulkan layers) take the rest | `frame.unity_tls_key_pool` (opt-in; suggested for Unity 6 with Firebase C++ or Gecko): libunity.so's `pthread_key_*`/`pthread_get/setspecific` imports are renamed to `fp_tkey_*` (`elf.rename_imports`, versions set global) and `libfp_tkeys.so` (`native/tlskeys`) is added to its DT_NEEDED; destructor-less keys become slots behind one real key (logs `fp_tkeys: pooling Unity's thread keys`). Approach by Klownicle (#198) |
| Controllers do nothing (for example stuck on a setup screen); log: `xrSuggestInteractionProfileBindings … XR_ERROR_PATH_UNSUPPORTED` | the game only suggests bindings for Meta's newer profiles (Touch Plus / Touch Pro); the Frame's Android runtime knows `oculus/touch_controller`, not those | adapter `profile_remap` (default on): the bindings are suggested again for Touch (log: `controller profile … -> oculus/touch_controller`) |
| Controllers "don't sync": virtual hands/controllers lie on the floor below you, no buttons (Unity OpenXR games that only suggest Touch Plus/Pro, e.g. Hotel Infinity #327); log with `input_diag=1`: only `meta/touch_controller_plus` (or Pro) suggested, `controller profile … -> oculus/touch_controller` | the runtime then reports `oculus/touch_controller` for each hand, a profile the game never suggested: Unity's OpenXR input creates no device for it | adapter `profile_remap` (default on) reports the game's own profile for such hands (log: `profile_remap: /user/hand/left reported as meta/touch_controller_plus`) |
| Controllers "don't sync": hands on the floor below you, no buttons, in a Unity OpenXR game with Meta's OpenXR features (e.g. Spatial Zombies #328); log: `xrCreateAction: XR_ERROR_PATH_UNSUPPORTED` (input_diag: `xrCreateAction -> XR_ERROR_PATH_UNSUPPORTED (thumbstick)`), then `xrGetCurrentInteractionProfile: XR_ERROR_ACTIONSET_NOT_ATTACHED` | Meta's "Detached … Controller" devices add `/user/detached_controller_meta/left|right` to Unity's merged actions; the Frame has no XR_META_detached_controllers, the action fails and Unity attaches no action set (OVRPlugin's input goes with it) | adapter `profile_remap` (default on) leaves those paths out of actions and bindings (log: `profile_remap: action thumbstick without 2 detached-controller path(s)`) |
| Controllers "don't sync": virtual hands/controllers lie on the floor below you, no buttons (Unity OpenXR games that only suggest Touch Plus/Pro, for example Hotel Infinity #327); log with `input_diag=1`: only `meta/touch_controller_plus` (or Pro) suggested, `controller profile … -> oculus/touch_controller` | the runtime then reports `oculus/touch_controller` for each hand, a profile the game never suggested: Unity's OpenXR input creates no device for it | adapter `profile_remap` (default on) reports the game's own profile for such hands (log: `profile_remap: /user/hand/left reported as meta/touch_controller_plus`) |
| Controllers "don't sync": hands on the floor below you, no buttons, in a Unity OpenXR game with Meta's OpenXR features (for example Spatial Zombies #328); log: `xrCreateAction: XR_ERROR_PATH_UNSUPPORTED` (input_diag: `xrCreateAction -> XR_ERROR_PATH_UNSUPPORTED (thumbstick)`), then `xrGetCurrentInteractionProfile: XR_ERROR_ACTIONSET_NOT_ATTACHED` | Meta's "Detached … Controller" devices add `/user/detached_controller_meta/left|right` to Unity's merged actions; the Frame has no XR_META_detached_controllers, the action fails and Unity attaches no action set (OVRPlugin's input goes with it) | adapter `profile_remap` (default on) leaves those paths out of actions and bindings (log: `profile_remap: action thumbstick without 2 detached-controller path(s)`) |
| Some buttons do nothing or the wrong thing (for example a Quest game's X/Y on the Frame's left controller) | the game binds only Quest Touch and SteamVR maps Touch onto the Frame controllers with its own remap (Touch left X/Y land on the d-pad, right X and Y repeat B), or the runtime rejected a profile or an input call | choose **Game settings → Controllers → Controller buttons**: Mirror (`quest_buttons=1`) or Quest pair (`2`); `0` restores SteamVR default. **Settings → Controllers** sets the global default and can apply it to installed games without overrides. Used from the next launch; [mapping and limits](CONTROLLERS.md). For rejected bindings, turn on adapter `input_diag` (`frameport settings <pkg> input_diag=1`) and inspect `input_diag:` lines: accepted profiles, rejected paths and input calls. With a mode enabled, expect `quest_buttons: frame_controller bindings (mode N, K bindings, D dropped): <XrResult>` |
| Unity game hangs or the whole Frame restarts (for example in a menu); log: `The current MSAA level is 0, but the recommended MSAA level is 4. Switching to the recommended level.` (for example Lucky's Tale) | Meta's OVRManager (`useRecommendedMSAALevel`) turns 4x MSAA on at runtime, past QualitySettings; multisampled render-to-texture on GLES/Zink hangs the GPU | `frame.unity_runtime_msaa_off` (Cpp2IL: `OVRDisplay.get_recommendedMSAALevel` → 0) |
| Unity game: one eye shows only effects or gray, the other is fine (for example I Am Cat, Oculus XR Plugin on GLES) | the game's single-pass multiview rendering goes wrong for array slice 1 (the adapter submits both slices correctly) | `frame.unity_multipass` (Cpp2IL: `OculusSettings.GetStereoRenderingMode` → MultiPass); `swap_eyes=1` tells whether the game or the adapter is at fault |
@@ -169,11 +188,19 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| VrApi bridge: never enters VR | Frame reaches FOCUSED later than Quest; tracking only when worn | bridge waits 30 s; test in the headset |
| Unreal game (ILMxLAB, for example Vader Immortal) plays its intro, then stays on its loading card (portrait + progress bar) at 72 fps and ignores input (GitHub #49) | the menu waits for `UVRUtils::GetQuestShaderPrecompilePercent()` to reach 100 %; the precompile only starts when `IsRunningOnSantaCruz()` (a Quest), the other branch returns 0.0 | `frame.unreal_quest_precompile` (non-Quest branch returns 1.0; found by Klownicle); suggested when the engine exports that function (analysis `unreal_quest_gates`) |
| Video in a game or player stutters, falls behind or drops to a few fps (4K/8K, for example a video player's 360° video, Batman's cutscenes); `OMX.google.*` / `c2.android.*` decoders in the log | Lepton's Android has only software decoders | `frame.hw_video_decode` (suggested when the APK uses MediaCodec/ExoPlayer/VLC; no APK change, reinstall): `OMX.frameport.{avc,hevc,vp9}.decoder` on the Frame's Iris hardware (H.264 High, HEVC Main, VP9 profile 0 up to 4096x2304; 8-bit; up to 8192 px), mounted into this game's container only (log `Iris hardware video/… decoder active`; launch.log `FramePort video: loading the Iris hardware codec plugin`). `Iris … unavailable` / `initialization failed (-12)` (triage `hw-video-decoder-busy`): another decoder holds the hardware, usually Steam's own hardware video decoding: turn it off in Steam, restart Steam. Video broken with it: Settings → Installing → Hardware video decoding off (all games) or `FRAMEPORT_NO_HW_VIDEO=1 %command%` (one game) |
| `Rec.709 color space unavailable`, the game quits before xrCreateSession (GitHub #336, OutRun Quest VR) | The Frame's runtime has no XR_FB_color_space | Adapter setting `fb_color_space=1` (triage `color-space-missing` suggests it): FrameBridge offers the extension (v3), reports Rec.709 and accepts any colour space without converting. Reaches games with their own OpenXR loader; OVRPort's dispatcher can't forward the new functions. Device-unverified |
| `AppSW recommended buffers unavailable`, the game quits at start with or without XR_FB_space_warp (GitHub #336, OutRun Quest VR) | The game requires Meta's Application SpaceWarp; the Frame's runtime reports no motion-vector size and its GLES swapchains take only sRGB colour formats. `hide_space_warp` can't help (the game refuses to start without the extension) | Adapter setting `space_warp_emul=1` (triage `appsw-required`): FrameBridge offers XR_FB_space_warp, reports a motion-vector size (the runtime's, else eye/4), serves the motion-vector + space-warp depth swapchains as its own GL textures / Vulkan images and strips `XrCompositionLayerSpaceWarpInfoFB` at xrEndFrame. Log `space_warp_emul:`. docs/DEVICE_NOTES.md#xr_fb_space_warp-emulation-336 |
| Log: `request display Hz: -1000101000` (XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB), then the game destroys its instance right after its first frames (GitHub #336, OutRun Quest VR) | The game asked for a refresh rate the runtime refuses and treats that as fatal | Fixed in FrameBridge: a refused or unsupported `xrRequestDisplayRefreshRateFB` keeps the current rate and returns success (log `refresh rate: app asked for …`); rebuild with Update on Frame. Older builds: Game settings → Refresh rate = 90 Hz |
Foveation mode, area size and diagnostic test are per-game user preferences (discussion #365), excluded from
shared working recipes. Saving Game settings rewrites launcher exports through `set_settings`; changes apply
at the next game start without rebuilding or reinstalling. Existing builds need one update for dormant diagnostic
support. Catalog `device.foveation` defaults and the guarded Batman/Climb 2 APK compatibility work remain supported.
## Picture problems (headset on)
| Symptom | Cause | Fix |
|---|---|---|
| Found after playing: the game page's "Last session" (or `frameport session <pkg>`) lists a finding (agent v70 `session_log`: the newest `plays.log` session's launch.log, ≤4 MB, + its crash logcat + kernel GPU lines) | launch tests never reach FOCUSED or gameplay; the same `catalog/triage.yaml` signatures (sync_guard, pac_hints, unreal_gl_shim, vk_spec_fixes, …) now also run on real sessions, plus the frame-rate and focus-dip findings below | "Try this setting" (FrameBridge settings: written on the Frame at once via `set_settings`, used from the next start) / "Rebuild with this patch" (APK patches); launch-test sessions (`test <unix>` in plays.log) are skipped |
| Found after playing: the game page's "Last session" (or `frameport session <pkg>`) lists a finding (agent `session_log`: the newest `plays.log` session's launch.log, ≤4 MB, + its crash logcat + kernel GPU lines) | launch tests never reach FOCUSED or gameplay; the same `catalog/triage.yaml` signatures (sync_guard, pac_hints, unreal_gl_shim, vk_spec_fixes, …) now also run on real sessions, plus the frame-rate and focus-dip findings below | "Try this setting" (FrameBridge settings: written on the Frame at once via `set_settings`, used from the next start) / "Rebuild with this patch" (APK patches); launch-test sessions (`test <unix>` in plays.log) are skipped |
| Textures flicker, jump or smear while moving in an OVRPlugin game that renders space-warp swapchains (`xrCreateSwapchain 376x376 format=97` / `format=129`; triage `space-warp-used`, info + question) | Application SpaceWarp: half the frames plus motion vectors; the Frame's reprojection of them misbehaves (Into The Radius 2, Metro Awakening) | adapter `hide_space_warp=1`; offered as "Did textures flicker…?" after a play session, never applied on its own |
| The game froze or closed and the kernel log has `hangcheck` / GPU fault / `msm_drm … recover` lines during the session (triage `gpu-hang`, play sessions only) | a shader or effect hangs the Frame's GPU (for example Vader Immortal's lightspeed jump) | capture the shaders (`adapter.vk_shader_dump=1` Vulkan, `adapter.zink_shader_dump=1` OpenGL ES: triage keeps the one matching the session's swapchain formats), play to the hang again and send a problem report (diagnostics carry the newest dumped modules); the maintainer adds a `vk_shader_fix` / `zink_shader_fix` catalog entry. `zink-device-lost` keeps its MSAA suggestions |
| The game keeps pausing or recentring for a moment while worn (session finding `focus-dips`: ≥3 `focus: back after N ms` lines longer than the game's focus_hold_ms, up to 5 s) | the Frame's wear sensor flickers "HMD off"; FrameBridge logs every runtime focus loss/return (always, before focus_hold) | `adapter.focus_hold_ms=<longest dip + 250 ms, in 500 ms steps, ≤5000>` (or `adapter.focus_hold=1` when off) |
@@ -182,11 +209,13 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| The hands' thumbs never move when you touch the thumbstick or buttons (Unreal `ThumbUp` stays 1, for example Vader Immortal) | OVRPlugin reports thumb proximity from XR_FB_touch_controller_proximity, which the Frame's runtime lacks; OVRPort's loader offers it anyway and the proximity actions never get a binding | adapter `proximity_emul=1` (thumb proximity from the touch inputs: thumbstick, face buttons, thumb rest; 2 = also the index finger from trigger touch); log `proximity_emul: finger proximity bound to touch`. **For UE4 Oculus games this didn't animate the thumbs** (Vader, GitHub #49): use `frame.unreal_thumb_touch` (found by Klownicle): `OculusInput::FOculusInput::SendControllerEvents` computes ThumbUp from NearTouches (masks 0x2/0x8); the patch makes it read Touches (the slot 4 bytes before) with masks 0x0f00 (X/Y/stick/thumb rest) / 0x000f (A/B/stick/thumb rest), keeping the inversion; three instructions, matched exactly (build note `OculusInput ThumbUp: from the capacitive touches`). Matches Vader Immortal Ep. I, Robo Recall, Phantom: Covert Ops, Time Stall, Star Wars: Tales from the Galaxy's Edge; newer UE4 builds (for example Asgard's Wrath 2, RE4, In Death) compile it differently (shared mask registers) and aren't matched |
| Vulkan game (Unreal / own engine with the Vulkan shim): the GPU hangs at one effect or cutscene (kernel `hangcheck detected gpu lockup`, freeze/exit; for example a VR4 cutscene, GitHub #140) | a SPIR-V shader reads a loop counter/accumulator before setting it (VR4's campaign shader, GitHub #10) | adapter `vk_shader_dump=1` (`frame.vk_sanitize`'s shim; log `vk shim: dumping SPIR-V modules to …`) writes every distinct module once to `files/fp_vk_shaders/<size>_<sha256>.spv` + `index.txt` (`<seq> <ms> <unix ms> <name> new\|known\|again`); play to the hang, collect diagnostics (`target/shaders/fp_vk_shaders/`, newest 4 MB) and take the modules first created (`new`) shortly before the kernel's hangcheck time; disassemble (`spirv-dis`), find the variable read before its first store, write a `vk_shader_fix` (`<size>:<sha256>:<byte offset>:<words>` = inserted OpStores; log `vk shim: fixed shader module`); switch the dump off again |
| OpenGL ES game: memory and GPU mappings grow every frame (~20 MB/s, `/dev/dri/renderD128` mappings ~6 per frame) until the frame rate collapses; the game's `xrclient_<package>.txt` (Steam logs) has one `SXR_GL_CHECK: error:0x502 ... glGenTextures` before the session starts (GitHub #285, Vader Immortal) | The game leaves GL errors pending on its context; the runtime's GL interop reads glGetError after its own calls and takes the stale error as its own failure. From then on `CVRCompositorSharedTextures::BeginCommandBuffer` allocates a new command buffer every frame instead of reusing one (perf page-fault stacks) | Adapter `gl_error_clear=1` (GLES only) clears pending GL errors before xrCreateSwapchain/xrBeginFrame/xrEndFrame; Vader's catalog recipes set it. Verified headless 2026-10-14: 191 GPU mappings and ~1.07 GB flat over 100 s (before: +430 mappings/s, +22 MB/s), no SXR_GL_CHECK |
| Team Beef port slows to a crawl after 5-10 minutes on any map; vrcompositor logs `WaitForPendingPresent: failed to wait for present` more and more often, until its watchdog aborts or the kernel oopses in `drm_gpuvm_validate_deferred_collect` (`msm_ioctl_gem_submit`, from the game's render thread) and the Frame restarts; `xrclient_<package>.txt` has `SXR_GL_CHECK: error:0x500 ... glGenTextures` (GitHub #237, QuestZDoom) | Same runtime GL-interop leak as above: the game leaves GL_INVALID_ENUM pending every frame. `/sys/kernel/debug/dri/0/gem` (readable as steamos) showed the game's GPU VM growing by ~105 objects a second (8,000 after 75 s headless); every GPU submit then has more objects to validate | `adapter.gl_error_clear=1`: suggested for every Team Beef port (`tbxr_libs`), set in QuestZDoom's catalog recipe. Headless 2026-10-24: the game's GPU objects stay flat (1,915 in total over 2 minutes), no SXR_GL_CHECK |
| OpenGL ES game: the GPU hangs at one effect (freeze/exit, compositor watchdog, for example Vader Immortal's lightspeed jump) | a shader (as Zink compiles it) reads loop counters/accumulators before setting them | capture the SPIR-V (adapter `zink_shader_dump=1` writes every module to `files/fp_spirv/`, or a GPU capture), then a `zink_shader_fix` entry (same format as `vk_shader_fix`) + `frame.zink_shader_fix` (Vulkan layer under Zink, activated through GraphicsEnv's debug layer list); log `shader fix layer: fixed shader module N`; triage `zink-shader-fix-mismatch` when a Frame update changed the driver's output |
| 2D Android app: Steam says it's running but nothing shows | Lepton runs apps headless unless the app folder has `lepton-show-flatscreen` | FramePort adds it for apps without VR (agent v28+); reinstall apps installed before |
| A game with a shader fix in its recipe (`vk_shader_fix` / `zink_shader_fix`, for example VR4, Vader Immortal) hangs the GPU again at the effect it was fixed for, after a game update; the log has `… shader fix(es) configured, none matched after N modules in S s (game updated?)` (triage `shader-fix-unmatched`, info) | fixes match a shader by exact size + SHA-256, so a changed shader is silently left alone; the shim/layer logs that line once per session when no configured fix matched 5 minutes after the first shader (checked only at shader creation; a match logs `fixed shader module`). Not reaching the effect yet gives the same line | capture the shaders again (`adapter.vk_shader_dump=1` / `adapter.zink_shader_dump=1`), play to the hang and send diagnostics; the maintainer updates the catalog's fix for the new shader. Never key this on the game's version string |
| 2D Android app: Steam says it's running but nothing shows | Lepton runs apps headless unless the app folder has `lepton-show-flatscreen` | FramePort adds it for apps without VR; reinstall apps installed by a very old FramePort |
| A phone/tablet app shows nothing in the headset, or a VR app opens as a flat window | FramePort's VR/2D guess (`vr_kind`) is wrong for this app | patch `device.display_mode` → **Flat window** or **VR** (Customize → "Show as VR or as a flat window"), then Update on Frame |
| An install button opens another program (or nothing) instead of FramePort | another program owns the link type, or the switch is off, or macOS (Flet can't receive links there) | Settings → Install links → **Use FramePort for these links**; else Add games → Install from a link… (paste the button's address) |
| 2D Android game ignores the controllers: they only move a pointer, an SDL game keeps its touch controls ("no controller attached"); Android's log lists only `wayland_touch`/`wayland_keyboard`/`wayland_pointer` (GitHub #162, Skate 3) | Lepton's Android gets keyboard, pointer and touch from the Wayland seat only: no gamepad device | patch `device.steam_gamepad` (2D apps; suggested when the manifest declares `android.hardware.gamepad` or `LEANBACK_LAUNCHER`; no APK change, reinstall): the launcher puts FramePort's Podman wrapper (`~/.local/share/frameport/agent/bin/podman`) first on PATH, which binds the host's `/dev/input` at `/dev/fp-input` plus an Xbox 360 key layout (Vendor_28de_Product_11ff.kl, incl. D-pad buttons), and exports `LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=1`; the launcher's `_gamepad_links` worker (agent v85) keeps `/dev/input/eventN` links to Steam Input's virtual pads (uinput, 28de, "Microsoft X-Box 360 pad N") in step while the game runs, so a pad Steam makes later (controllers woke up, reconnected) is added and a gone one removed (Android's EventHub follows through inotify). Check: `<base>/gamepad.log` (`pads: event5` / `pads: none`), launch.log `EventHub: New device … pad`, `podman exec lepton-steamlaunch-<appid> dumpsys input` lists the pad. Steam Input must be on for the shortcut; Steam activates the shortcut's own layout while the game has focus (controller.txt `Queueing activation for controller: 0 app: <shortcut appid as signed int>`), and Steam's UI layout (app 769) while the dashboard is open. Off for one start: `FRAMEPORT_NO_GAMEPAD=1 %command%` |
| 2D Android game ignores the controllers: they only move a pointer, an SDL game keeps its touch controls ("no controller attached"); Android's log lists only `wayland_touch`/`wayland_keyboard`/`wayland_pointer` (GitHub #162, Skate 3) | Lepton's Android gets keyboard, pointer and touch from the Wayland seat only: no gamepad device | patch `device.steam_gamepad` (2D apps; suggested when the manifest declares `android.hardware.gamepad` or `LEANBACK_LAUNCHER`; no APK change, reinstall): the launcher puts FramePort's Podman wrapper (`~/.local/share/frameport/agent/bin/podman`) first on PATH, which binds the host's `/dev/input` at `/dev/fp-input` plus an Xbox 360 key layout (Vendor_28de_Product_11ff.kl, incl. D-pad buttons), and exports `LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=1`; the launcher's `_gamepad_links` worker keeps `/dev/input/eventN` links to Steam Input's virtual pads (uinput, 28de, "Microsoft X-Box 360 pad N") in step while the game runs, so a pad Steam makes later (controllers woke up, reconnected) is added and a gone one removed (Android's EventHub follows through inotify). Check: `<base>/gamepad.log` (`pads: event5` / `pads: none`), launch.log `EventHub: New device … pad`, `podman exec lepton-steamlaunch-<appid> dumpsys input` lists the pad. Steam Input must be on for the shortcut; Steam activates the shortcut's own layout while the game has focus (controller.txt `Queueing activation for controller: 0 app: <shortcut appid as signed int>`), and Steam's UI layout (app 769) while the dashboard is open. Off for one start: `FRAMEPORT_NO_GAMEPAD=1 %command%` |
| A 2D app's sign-in (or another link) says no browser is installed, and nothing opens (GitHub #466, Plex) | Lepton's Android has no browser package; the app checks for one (its manifest's `<queries>` ask for http/https VIEW handlers or a Custom Tabs service) before it starts the VIEW intent that Lepton would have sent to Steam's browser (`ActivityStarter.execute`: every VIEW → `steam://openurl/<url>` into `/lepton/steam.pipe`) | patch `frame.browser_handoff` (opt-in, game page "Browser for sign-ins: open pages in Steam's browser"; then Update on Frame): a browser stand-in activity + `classesN.dex` in the app's own APK; the app's check finds it and the page opens in Steam's browser. Check: launch.log `ActivityTaskManager: Intercepted VIEW`, Steam's console_log.txt `ExecCommandLine: "steam://openurl/…"`. A sign-in that returns to the app through a link can't complete (Lepton sends every VIEW to Steam); code/PIN sign-ins work. The first patched build is signed with FramePort's key (the app was installed unchanged before) |
| Game starts in English and has no in-game language choice | Game follows Lepton's English Android locale | Choose Game language under Customize (`device.language`), then Update on Frame; the game must include the selected language |
| 2D Android app: back/home/recents buttons cover the app's own buttons | Android's navigation bar | patch `device.hide_navbar` (on by default for apps without VR) sets `qemu.hw.mainkeys=1`; reinstall to apply |
| Black screen, audio works, GLES engine with direct VrApi | Mesa rejects Quest-style GLSL | `frame.gl_shim` (logs `GLShim: SHADER COMPILE FAILED` + source lines) |
@@ -195,21 +224,21 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Own GLES engine (OpenXR, not VrApi): eye view fine, but HUD / menu / PDA panels black (for example Doom3Quest, GitHub #77; no error in the log) | every shader declares `layout(num_views=2) in;`, also the ones drawn into the engine's single-layer offscreen framebuffers (2D textures/renderbuffers); OVR_multiview makes such draws INVALID_OPERATION and Mesa drops them (`draw_validate.c`), Quest's driver doesn't | `frame.gl_multiview_fbo` (opt-in, experimental: `libfpglmv.so` interposes GLES and draws those cases with a single-view twin of the program; logs `GLMV: twin built: program N -> M`, per-5-s `single-view draws` counters; `gl_mv_debug=1` adds glGetError checks; triage `gl-multiview-twin-failed`). Alternative: the game's own fix (two-layer multiview pool, Xandrix's patch in #77) |
| Team Beef idTech 4 port (PreyVR, GitHub #306): main menu black except the mouse cursor, loading screen fine, 72 fps, no GL error; one game thread at 100 % CPU | the game clock runs 1000 / refresh ms per tic (`USERCMD_MSEC`, from xrGetDisplayRefreshRateFB). The Frame's runtime lacks XR_FB_display_refresh_rate and OVRPort's dispatcher answered 0 Hz: 0 ms tics, the async thread spins (millions of tics/s) and `com_frameTime` stays 0, so the menu GUI stays at its first animation step (everything hidden) | FrameBridge (default, no setting) answers xrGetDisplayRefreshRateFB with the measured rate (1 / predictedDisplayPeriod, else 72 Hz); log `refresh rate: runtime reported 0.0 Hz (result -7), reporting 72.0 Hz`. Found with `gl_mv_debug=2` (`frameport settings <pkg> gl_mv_debug=2`, needs `frame.gl_multiview_fbo`): only 2 draws per frame (backdrop + cursor) and `trace clocks: com_frameTime=0` |
| Upside-down image in a GL bridge game | GL images start at the bottom row | fixed in the bridge (swap angleUp/Down for GL chains) |
| UI panels upside down (AC Nexus) | XrCompositionLayerImageLayoutFB VERTICAL_FLIP unsupported | adapter `flip_emul=1` (default); rotating quads makes them vanish |
| UI panels upside down (AC Nexus) | XrCompositionLayerImageLayoutFB VERTICAL_FLIP unsupported | adapter `flip_emul=1` (default: upside-down copies in an atlas, every frame); `flip_dump=N` saves what the runtime gets; rotating quads makes them vanish |
| Passthrough black (BAM) | XR_FB_passthrough missing | adapter `passthrough_emul=1` (default) + `patch_force_passthrough` for MR-only games |
| Mixed reality inside out: the room shows on the game's objects, black where the room should be (Unreal, for example Star Wars: Beyond Victory, GitHub #329) | Unreal keeps 1 - coverage in the scene alpha and OVRPlugin chains XrCompositionLayerAlphaBlendFB (colour factors ONE_MINUS_SRC_ALPHA / SRC_ALPHA, `layer_debug` shows `n=1000041001(3,2/3,2)`) to the eye layer; the Frame's runtime ignores it | adapter `alpha_blend_emul=1` (default, Vulkan): inverts the alpha of the eye images in place while passthrough is emulated; logs `alpha_blend: inverting the alpha of swapchain=…` |
| Quest Touch controllers (or none) shown in a game that uses Meta's runtime controller models | XR_FB_render_model missing on the Frame | adapter `controller_models=1`: the agent converts the Frame's SteamVR controller models to glb at install, the adapter serves them. Games with their own controller meshes aren't affected (none of the 34 catalog games use runtime models) |
| Quest Touch controllers (or none) shown in a game that uses Meta's runtime controller models | XR_FB_render_model missing on the Frame | adapter `controller_models=1`: the agent converts the Frame's SteamVR controller models to glb at install, the adapter serves them. Games with their own controller meshes aren't affected (no catalog game uses runtime models) |
| `overportOXR: Unknown proc addr: xr…` for a function the adapter emulates | overport's dispatcher only forwards functions in its own table | route the plugin's lookups through `native/xrshim` (done for XR_FB_render_model when `controller_models=1`) |
| MR game asks for a room scan | The game requested a new room through Meta scene capture | FramePort scans in the headset and loads the new room. `scene_scan_request=0` turns it off and uses the stored room. |
| MR game stuck waiting for room data (e.g. Demeter) | No Meta scene API | `adapter.scene_emul=1` (+ `frame.meta_permissions`); Frame → Room → Use my play area, or import a scanned room |
| Unity MR game rejects every room / its "use guardian" fallback does nothing; `ovrpBoundary_Outer is not supported`, `IndexOutOfRangeException` in a guardian check (e.g. Demeter's `RemoveObjectNotInGuardian`) | OVRPlugin calls the Frame's 1x1 m STAGE rect a configured boundary but can't give its outline | `adapter.scene_hide_bounds=1` (with `scene_emul`): FrameBridge then answers the app's STAGE `xrGetReferenceSpaceBoundsRect` with `XR_SPACE_BOUNDS_UNAVAILABLE` (log `scene: STAGE bounds hidden`). Per game only: NOPE stalled without a play area. Demeter also needs a floor of at least 2.5 x 2.5 m, as on a Quest |
| MR game with `scene_emul` never asks for the room (no `scene: query`/`discovery` lines), black screen or MR mode with no room (e.g. NOPE, Pinball FX); `m_XR_META_boundary_visibility extension is not supported` every frame | Meta's scene/anchor permissions undeclared (Lepton can't grant them); boundary suppression unsupported | `frame.meta_permissions` (now with every `scene_emul` recipe of a USE_SCENE/USE_ANCHOR_API game, migration `scene_permissions`); FrameBridge emulates `XR_META_boundary_visibility` (log `scene: boundary suppressed`). `passthrough:` lines show the app's passthrough calls |
| MR game stuck waiting for room data (for example Demeter) | No Meta scene API | `adapter.scene_emul=1` (+ `frame.meta_permissions`); Room tab → Scan room in the headset or Use my play area |
| Unity MR game rejects every room / its "use guardian" fallback does nothing; `ovrpBoundary_Outer is not supported`, `IndexOutOfRangeException` in a guardian check (for example Demeter's `RemoveObjectNotInGuardian`) | OVRPlugin calls the Frame's 1x1 m STAGE rect a configured boundary but can't give its outline | `adapter.scene_hide_bounds=1` (with `scene_emul`): FrameBridge then answers the app's STAGE `xrGetReferenceSpaceBoundsRect` with `XR_SPACE_BOUNDS_UNAVAILABLE` (log `scene: STAGE bounds hidden`). Per game only: NOPE stalled without a play area. Demeter also needs a floor of at least 2.5 x 2.5 m, as on a Quest |
| MR game with `scene_emul` never asks for the room (no `scene: query`/`discovery` lines), black screen or MR mode with no room (for example NOPE, Pinball FX); `m_XR_META_boundary_visibility extension is not supported` every frame | Meta's scene/anchor permissions undeclared (Lepton can't grant them); boundary suppression unsupported | `frame.meta_permissions` (now with every `scene_emul` recipe of a USE_SCENE/USE_ANCHOR_API game, migration `scene_permissions`); FrameBridge emulates `XR_META_boundary_visibility` (log `scene: boundary suppressed`). `passthrough:` lines show the app's passthrough calls |
| Build says `Unity text fields: not fixed: Cpp2IL couldn't start on this PC` (older versions: `Cpp2IL couldn't read this game's code: at Cpp2IL.Program.Main`) | .NET aborts without libicu (fresh WSL/Linux) | FramePort runs Cpp2IL with `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1`; such failures aren't cached, so the next build retries |
| MR uses an empty/simple room; FrameBridge `scene: capture requested -> guardian box` / `using guardian room`, `room from guardian`, or fallback box dimensions (`mr-no-room`) | No saved room, so FrameBridge uses its guardian box | Save a room in Frame → Room. A real SteamVR boundary supplies walls; Valve's default circle is refused. Import a scanner-built room for furniture/mesh; headset capture is not implemented yet |
| MR uses an empty/simple room; FrameBridge `scene: capture requested -> guardian box` / `using guardian room`, `room from guardian`, or fallback box dimensions (`mr-no-room`) | No saved room, so FrameBridge uses its guardian box | Scan or save a room in the Room tab (**Scan room in the headset**, or **Use my play area**: walls only, from a real SteamVR boundary; Valve's default circle is refused) |
| `Unknown proc addr: xrDiscoverSpacesMETA` or MRUK `NoRoomsFound` (`mr-discovery-unsupported`) | Scene discovery is unavailable or no room was discovered | Enable `adapter.scene_emul`, rebuild/reinstall to get the updated extension shim, then save a room. All games declaring USE_SCENE or USE_ANCHOR_API now get this suggestion |
| MR game: virtual objects (e.g. Laser Dance's lasers) vanish when you come close to them | Meta's Depth API occlusion: the Frame has no depth sensor (XR_META_environment_depth), so the environment depth the occlusion shaders read is never valid and near objects count as hidden behind the room | `frame.meta_depth_occlusion` (Unity IL2CPP; offered, not turned on, when the game has the HARD_OCCLUSION / SOFT_OCCLUSION keywords; analysis `meta_depth_occlusion`): EnvironmentDepthManager's checks -> false and its keyword setter does nothing (Cpp2IL), the keywords renamed in the asset files that contain them; Laser Dance's known build also gets its laser materials/LODs fixed. From PR #246; not checked in the owner's headset yet |
| Unity MR game keeps asking for a room scan / `No scene model found` although FrameBridge served a room (OVRSceneManager with ActiveRoomsOnly) | OVRSceneManager loads only the room whose floor outline contains the player (IsUserInRoom); with the play-area room the player can stand outside it (FrameBridge logs the head against the floor outline: `scene: head in STAGE ... OUTSIDE the floor outline`) | Scan the room first (Frame → Room). Still stuck: `frame.unity_scene_active_rooms` (catalog or by hand, Unity IL2CPP scene games): IsUserInRoom / PointInPolygon2D -> true (Cpp2IL). From PR #246 (Laser Dance) |
| MR game with many quads (21+): frames refused with `XR_ERROR_LAYER_LIMIT_EXCEEDED` (e.g. Punch Fit) | The runtime takes at most maxLayerCount (16) layers | FrameBridge keeps the projection layers and its own fade quad, then the app's other layers in submission order up to the limit (log `xrEndFrame: N layers exceed the runtime limit 16`). From PR #246 |
| MR game: virtual objects (for example Laser Dance's lasers) vanish when you come close to them | Meta's Depth API occlusion: the Frame has no depth sensor (XR_META_environment_depth), so the environment depth the occlusion shaders read is never valid and near objects count as hidden behind the room | `frame.meta_depth_occlusion` (Unity IL2CPP; offered, not turned on, when the game has the HARD_OCCLUSION / SOFT_OCCLUSION keywords; analysis `meta_depth_occlusion`): EnvironmentDepthManager's checks -> false and its keyword setter does nothing (Cpp2IL), the keywords renamed in the asset files that contain them; Laser Dance's known build also gets its laser materials/LODs fixed. From PR #246; not checked in the headset yet |
| Unity MR game keeps asking for a room scan / `No scene model found` although FrameBridge served a room (OVRSceneManager with ActiveRoomsOnly) | OVRSceneManager loads only the room whose floor outline contains the player (IsUserInRoom); with the play-area room the player can stand outside it (FrameBridge logs the head against the floor outline: `scene: head in STAGE ... OUTSIDE the floor outline`) | Scan the room first (Room tab). Still stuck: `frame.unity_scene_active_rooms` (catalog or by hand, Unity IL2CPP scene games): IsUserInRoom / PointInPolygon2D -> true (Cpp2IL). From PR #246 (Laser Dance) |
| MR game with many quads (21+): frames refused with `XR_ERROR_LAYER_LIMIT_EXCEEDED` (for example Punch Fit) | The runtime takes at most maxLayerCount (16) layers | FrameBridge keeps the projection layers and its own fade quad, then the app's other layers in submission order up to the limit (log `xrEndFrame: N layers exceed the runtime limit 16`). From PR #246 |
| Hand-tracking game janky (Silhouette) | Frame synthesizes hands from controllers | `controller_fix=0` passes hands through; not really fixable |
| Eye distortion while moving (Arcsmith, Time Stall) | unknown (not eye swap, tracking, Valve layers, depth or pacing) | unresolved |
| The Climb 666.0: hand outlines ripple in both eyes while menus and gameplay work (GitHub #238) | Unknown. Sessions paced around 72 fps, controller tracking reported valid, and SteamVR motion smoothing was already off; these checks do not establish the cause | Unresolved after wearer comparisons on 2026-10-12: disabling Valve's Vulkan layers, setting `r_AntialiasingMode = 1`, and setting `r_AntialiasingMode = 0` with `r_MotionBlur = 0` each gave the same or worse result. These were config comparisons; runtime cvar values were not verified. Restored the playable configuration (`r_vr_foveated = 0` with the default Vulkan layers), retaining the startup/rendering crash fixes above. Do not keep the unsuccessful experimental settings as a fix |
@@ -225,6 +254,16 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| Unreal 5 game crashes in `vulkan.freedreno.so` called straight from `libUnreal.so` right after its swapchains, or a few seconds into the first frames (Metro Awakening) | Unreal turns on fragment-density-map foveation because Turnip offers VK_EXT_fragment_density_map, then records barriers (to `FRAGMENT_DENSITY_MAP_OPTIMAL`) and image views (2D array R8G8) for a density map the Frame never provides (VK_NULL_HANDLE); Turnip dereferences it | adapter `vk_hide_fdm=1` (the game doesn't see the extension) + `vk_spec_fixes=1` (leaves out null-image barriers, refuses null-image views); triage `unreal-fdm-missing`. Found by disassembling the driver at the crash pc: the struct offsets (0x58 = VkSubpassDescription2, 0x48/0x60 = image barriers, viewType/format in registers) name the call |
| Own-engine game crashes in `je_free` from its own library on the first frames (Roblox) | the game frees memory it doesn't own; not the Vulkan shim or the format fallback | unsupported |
## FramePort itself (uploads, links, store details)
| Symptom | Cause | Fix |
|---|---|---|
| Uploads to the Frame are slow | The link, storage or transfer backend can limit throughput | Check the job's "Transfer link"; try the Frame's hotspot or USB cable. Large install files prefer system OpenSSH SFTP, retaining paired-host checks and `.part` resume; unavailable or failed native uploads fall back to Paramiko. The log names the backend. Settings → Installing → Parallel uploads accepts 1–8 (default 3; saved preferences are preserved). More is not always faster. Measure the production path with `scripts/upload_bench.py --methods app-files-3 --check-resume` (three repetitions, checksum verification) |
| Log: `OpenSSH SFTP failed (255): … Permission denied (publickey,password)`, then Paramiko | OpenSSH offered keys the Frame doesn't authorize (GitHub #242) | The log lists the keys OpenSSH offered (`Offering public key`) and its warnings. A connection that logged in with the PC's own keys/agent makes OpenSSH use them too (copies of `~/.ssh/id_*` + the agent); a refused FramePort key is retried that way once. A Frame that accepts only another key file: Settings → Installing → SSH keys (CLI `frameport frame ssh-keys --add <file>`); passphrase keys go through the SSH agent |
| Build fails at once: `Input APK is not a file: …LEGO?? Bricktales/…` (a folder or file name with ®, ™, accents or CJK shows `?`) | FramePort on Linux or on the Frame started Java under a C/POSIX locale, so Java decoded the path as ASCII (GitHub #487, fixed) | Update FramePort (`toolchain.java_env` gives Java `LC_ALL=C.UTF-8` when the locale isn't UTF-8); older versions: rename the folder without those characters and rescan |
| Log: `OpenSSH SFTP failed (1): stat C://…/[…: No such file or directory` | sftp got glob escapes inside its quotes (GitHub #324, fixed): `[` in a local folder name | Update FramePort; the native_upload `quote()` passes only `\\` and `\"` escapes, Windows paths with `/` |
| An install button opens another program (or nothing) instead of FramePort | another program owns the link type, or the switch is off, or macOS (Flet can't receive links there) | Settings → Install links → **Use FramePort for these links**; else Add games → Add from a link… (paste the button's address) |
| Game page shows another game's description or screenshots, or they stay wrong after Rename (GitHub #377: "Lollipop" matched an adult Steam game) | Store details were looked up by title and kept after a rename; Steam exact-title matches had no content check | Rename/reset drops title-matched details and screenshots and fetches them again under the new name (package-matched details and custom art stay); Steam matches with sexual-content descriptors are rejected for details and artwork |
## Live view (Live view tab, `install/livestream.py`)
| Symptom | Cause | Fix |
|---|---|---|
@@ -256,7 +295,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
addresses with a full Cpp2IL diffable-cs dump (method `Offset` = address − the r-xp mapping of libil2cpp.so).
Sample memory per mapping name from `/proc/<pid>/smaps` (`[anon:libc_malloc]` = native heap).
- Hangs (every thread asleep): first ask for a diagnostics zip saved *while the game hangs*: `target/threads.txt`
(agent v79) shows each thread's wait and syscall (futex = a lock, ppoll/recvfrom + an internet socket = network). Then
shows each thread's wait and syscall (futex = a lock, ppoll/recvfrom + an internet socket = network). Then
backtraces of all threads with gdb. Root inside the Lepton container may ptrace the
game (the host's `ptrace_scope` 1 doesn't apply there) and the guest has `gdbserver64`; `debuggerd -b` fails (no
tombstoned). Copy every mapped library to a local sysroot (`podman exec <container> cat /proc/<pid>/maps`, then
@@ -275,7 +314,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
libil2cpp.so) — patch the field read in the caller (`Il2cppReturnPatch.field_loads`).
- Fixes found by users count: a reporter's shader capture (VR4) became the `vk_shader_fix` recipe data (match by size
+ SHA-256, insert words); verify on the device that the log shows the module being fixed.
- Don't install or test on the Frame while the owner is playing: adding a library entry restarts Steam (FramePort
- Don't install or test on the Frame while someone is playing on it: adding a library entry restarts Steam (FramePort
now skips the restart when the shortcut is unchanged and waits while a game runs), and a launch test starts a game.
- Catalog YAML: a `: ` inside an unquoted value breaks the file and the loader skipped it silently (the game vanished
from the list); `tests/test_catalog_files.py` now catches it.
@@ -286,14 +325,15 @@ Stream PC VR instead: native Steam version if one exists (Espire 1: 669290; HITM
or the Rift version through Revive (Journey of the Gods, Shadow Point). The catalog stores these as
`pcvr_alternative`. FramePort handles Rift games itself (next section).
## Oculus Rift (PC VR) games
Scan a folder with Rift game dumps (Windows game folders) like Quest dumps; they get ids `rift.<slug>` and the
"PC VR (Proton and Revive)" patch group. Two ways to run them:
- **This PC** (`--to pc`, GUI "Install on this PC"): a non-Steam shortcut in the local Windows Steam runs
`ReviveInjector.exe /openxr "<game.exe>"` (FramePort's portable Revive; on WSL copied to
## PC VR games (Oculus Rift and SteamVR)
Scan a folder of Windows game folders like Quest dumps; they get ids `rift.<slug>` and the "PC VR" patch group.
Repacks are usually pre-patched: by default the game is installed unchanged and its program started directly.
Revive is only suggested for games with Oculus (LibOVR) code, and those only run on the PC.
- **This PC** (`--to pc`, GUI "Install on this PC"): a non-Steam shortcut in the local Windows Steam runs the game,
or `ReviveInjector.exe "<game.exe>"` with Revive's OpenVR backend (FramePort's portable Revive; on WSL copied to
`%LOCALAPPDATA%\FramePort`). Play on the Frame by streaming from SteamVR.
- **The Frame** (GUI "Install on Frame"): game + Revive are uploaded to `~/Applications/quest-frame/rift.*`,
and launch.sh runs them with the Frame's ARM64 Proton (install it first: Frame page → PC VR games (Proton) → Install…). Experimental.
- **The Frame** (GUI "Install on Frame", experimental): the game is uploaded to `~/Applications/quest-frame/rift.*`
and launch.sh runs it with the Frame's ARM64 Proton (Steam Frame page → PC VR games (Proton) → Install…).
| Symptom | Cause | Fix |
|---|---|---|
@@ -301,14 +341,7 @@ Scan a folder with Rift game dumps (Windows game folders) like Quest dumps; they
| Game quits at once; `ovrPlatformInitialize_NotEntitled` / entitlement failed | Oculus Platform SDK entitlement check (FramePort flags these: "Uses the Oculus Platform SDK") | PC mode with the Oculus app installed and a license you own. FramePort doesn't bypass entitlement checks |
| `Failed to create process` in ReviveInjector.txt | wrong exe, or 32/64-bit mismatch | check the detected exe on the game page; rescan |
| `XR_ERROR_RUNTIME_UNAVAILABLE` / no OpenXR runtime (PC) | SteamVR not running / not the OpenXR runtime | start SteamVR, set it as OpenXR runtime; or `pcvr.revive_openvr` |
| `XR_ERROR_EXTENSION_NOT_PRESENT` / `xrConvertTimespecTimeToTimeKHR failed` (Frame) | ReviveXR needs `XR_KHR_win32_convert_performance_counter_time`; wineopenxr builds it on the host's timespec conversion. The Frame's Linux runtime supports it (2026-09-29), unlike its Android runtime | `pcvr.xr_timefix` (off by default): FramePort's timefix OpenXR layer emulates it like the Quest adapter |
| `XR_ERROR_EXTENSION_NOT_PRESENT` / `xrConvertTimespecTimeToTimeKHR failed` (Frame) | ReviveXR needs `XR_KHR_win32_convert_performance_counter_time`; wineopenxr builds it on the host's timespec conversion. The Frame's Linux runtime supports it (2026-09-29), unlike its Android runtime | `pcvr.xr_timefix` (default on): FramePort's timefix OpenXR layer emulates it like the Quest adapter |
| Proton game hangs at start in a headless/SSH launch; log `no driver could be loaded` / `explorer process failed to start` | no display session (DISPLAY/GAMESCOPE_WAYLAND_DISPLAY) | the Proton launch.sh takes them from the running Steam (built in) |
| Unreal game starts (window created) then `CrashReportClient` runs | the game crashed under Proton; often no VR runtime reached it (Revive off) | keep `pcvr.revive` on (repacks' bundled LibRevive64.dll isn't loaded by itself); read the game log + crash summary in the launch log |
| Unreal game starts (window created) then `CrashReportClient` runs | the game crashed under Proton; often no VR runtime reached it (Revive off) | LibOVR (Oculus) games need `pcvr.revive` (a repack's bundled LibRevive64.dll isn't loaded by itself) and run only on the PC; otherwise read the game log + crash summary in the launch log |
| `VK_ERROR_DEVICE_LOST` under DXVK | freedreno GPU hang | `pcvr.proton_log` to capture; PC mode |
| Game page shows another game's description or screenshots, or they stay wrong after Rename (GitHub #377: "Lollipop" matched an adult Steam game) | Store details were looked up by title and kept after a rename; Steam exact-title matches had no content check | Rename/reset drops title-matched details and screenshots and fetches them again under the new name (package-matched details and custom art stay); Steam matches with sexual-content descriptors are rejected for details and artwork |
| `Rec.709 color space unavailable`, the game quits before xrCreateSession (GitHub #336, OutRun Quest VR) | The Frame's runtime has no XR_FB_color_space | Adapter setting `fb_color_space=1` (triage `color-space-missing` suggests it): FrameBridge offers the extension (v3), reports Rec.709 and accepts any colour space without converting. Reaches games with their own OpenXR loader; OVRPort's dispatcher can't forward the new functions. Device-unverified |
Foveation mode, radius and diagnostic test are per-game user preferences (discussion #365), excluded from
shared working recipes. Saving Game settings rewrites launcher exports through `set_settings`; changes apply
at the next game start without rebuilding or reinstalling. Existing builds need one update for dormant diagnostic
support. Catalog `device.foveation` defaults and the guarded Batman/Climb 2 APK compatibility work remain supported.
+2 -2
View File
@@ -81,7 +81,7 @@ scenes:
Filmed scenes are one continuous session: each starts where the last one left off. Cards are rendered at the end
and slotted in, with crossfades between everything. The `fresh` profile starts FramePort as on a new computer: an
empty library, the welcome screen and a Frame that isn't set up. Its games wait for the pretend folder scan.
empty library, the welcome screen and a Frame that isn't set up. The demo games wait for the pretend folder scan.
## Writing steps
@@ -139,7 +139,7 @@ The `showcase` workflow (`.github/workflows/showcase.yml`):
1. **When it runs:** on pushes to `main` that touch the UI, its translations or the showcase files, and on
**Run workflow**.
2. **What it renders:** the screenshots every time. Videos when their storyboard changed (all of them when the
scripts changed), or the ones named on a manual run (`all`, or e.g. `tour install`). UI tweaks alone don't
scripts changed), or the ones named on a manual run (`all`, or for example `tour install`). UI tweaks alone don't
re-commit 10 MB videos.
3. **What happens next:** every render is uploaded as the run's `showcase` artifact. When a picture changed, the
workflow opens or updates the pull request on branch `showcase/update`, which you review and merge.
+13 -17
View File
@@ -1,27 +1,24 @@
# Themes
FramePort comes with three color themes, all dark:
FramePort has three color themes, all dark. Switch in **Settings → Appearance**; the change applies at once.
| Theme | Look |
|---|---|
| **Portal** (default) | The logo's two portals: orange for actions and the current selection, blue for secondary actions, progress and the PC side. |
| **Portal** (default) | The logo's colors: orange for actions and selection, blue for secondary actions, progress and the PC side. |
| **Portal (OLED)** | Portal on true black, for OLED screens. |
| **Original** | FramePort's first look, with a violet accent. |
Switch in **Settings → Appearance**. The change applies at once.
## Your own theme
A theme is a small JSON file. To make one:
A theme is a small JSON file.
1. In **Settings → Appearance**, pick the theme closest to what you want and click **Copy this theme as a file**.
2. Paste it into a text editor and save it as `something.json`.
3. Change the name and the colors you want different. You can delete every color you keep: missing colors come
from the `base` theme.
4. Back in **Settings → Appearance**, click **Install theme file…** and choose the file. FramePort checks it,
switches to it and keeps a copy in its data folder (`themes/`), so you can delete the original.
1. In **Settings → Appearance**, pick the closest theme and click **Copy this theme as a file**.
2. Paste the theme into a text editor and save the file as `something.json`.
3. Change the name and the colors you want. Delete the colors you keep: missing ones come from the `base` theme.
4. Click **Install theme file…** and choose the file. FramePort checks the file, switches to the theme and keeps a
copy, so you can delete the original.
To remove an installed theme, click the bin icon on its card.
The bin icon on a theme's card removes the theme.
### Format
@@ -51,11 +48,10 @@ To remove an installed theme, click the bin icon on its card.
| `SURFACE`, `SURFACE_2`, `SURFACE_3` | Cards; raised and hovered parts; inputs and chips |
| `BORDER`, `BORDER_STRONG` | Outlines |
| `TEXT`, `TEXT_2`, `TEXT_3` | Main text; secondary text; captions and disabled text |
| `ACCENT`, `ACCENT_SOFT`, `ON_ACCENT` | Main buttons and the current selection; its tint; text on it |
| `SECONDARY`, `SECONDARY_SOFT` | Secondary buttons, progress, switch tracks; its tint |
| `ACCENT`, `ACCENT_SOFT`, `ON_ACCENT` | Main buttons and the current selection; the accent's tint; text on the accent |
| `SECONDARY`, `SECONDARY_SOFT` | Secondary buttons, progress, switch tracks; the secondary color's tint |
| `OK`, `WARN`, `ERROR`, `INFO` | Status: works, needs attention, failed, information |
| `PC` | PC VR games and "on this PC" |
FramePort refuses a theme file it can't show well, and says why: a light window background (FramePort is dark only),
text that's hard to read on cards, an unknown color name or a value that isn't a color. An example is in
[`themes/example-theme.json`](themes/example-theme.json).
FramePort refuses a theme it can't show well and says why: a light background (FramePort is dark only), hard-to-read
text, an unknown color name or a value that isn't a color. Example: [`themes/example-theme.json`](themes/example-theme.json).
+20 -19
View File
@@ -1,29 +1,32 @@
# Transfer measurements and incremental installs
## Current uploader
OpenSSH SFTP is the installer backend for files of 8 MiB and more, with Paramiko as the fallback; smaller files go in
tar batches. **Parallel uploads** (Settings → Installing) is 3 by default (1–8; saved preferences are kept). The
sections after "Production OpenSSH follow-up" are the measurements that led there, kept for reference: their
statements about the "production" uploader describe the state at the time.
## Production OpenSSH follow-up
OpenSSH is now the preferred installer backend for files >=8 MiB. The new default is three workers (range 1–8),
with saved preferences preserved. Fresh files use `put`; existing partial files use `reput`. Each worker keeps
With the OpenSSH backend, the default became three workers. Fresh files use `put`; existing partial files use `reput`. Each worker keeps
one SFTP batch session. Progress is polled through a separate bounded SFTP channel. Native failures resume via
Paramiko; cancellation stops the owned SFTP/SSH process trees and does not trigger fallback.
Windows matched production comparison: eight 64 MiB files, 512 MiB total, three shuffled repetitions per backend,
three workers, same home Wi-Fi/internal storage. OpenSSH median **15.51 MB/s**, Paramiko median **11.08 MB/s**
(about 40% faster). All six checksums passed. OpenSSH trials ranged 9.5–16.8 MB/s, so link variability still
matters; it was slower than Paramiko in one pass. Native credential/progress setup took 0.12–0.13 seconds.
matters; OpenSSH was slower than Paramiko in one pass. Native credential/progress setup took 0.12–0.13 seconds.
The separate Windows 128 MiB production test also passed every checksum. Native cancellation left a partial file
of 74,752,000 bytes; resuming with OpenSSH produced the expected SHA-256. The log confirmed no fallback for the
resumed production upload. Reports:
`/mnt/c/Users/Austin/AppData/Local/Temp/frameport-upload-native-production-comparison-windows.json` and
`/mnt/c/Users/Austin/AppData/Local/Temp/frameport-upload-native-production-windows.json`.
resumed production upload.
The earlier sections below preserve measurements made before the production backend switch.
The WSL production validation (128 MiB, three workers, three repetitions) measured 14.1 / 19.1 / 15.0 MB/s,
with every checksum passing and native backend logs recorded. Production cancellation left a 72,330,240-byte
partial file; OpenSSH resumed it in 4.23 seconds with the expected SHA-256 and no Paramiko fallback. Report:
`/tmp/frameport-upload-native-production-wsl.json`.
partial file; OpenSSH resumed it in 4.23 seconds with the expected SHA-256 and no Paramiko fallback.
## Measurements (2026-10-14)
@@ -54,7 +57,7 @@ checks application cancellation and both resume paths; native SFTP cancellation
The Frame's USB and hotspot interfaces were up, but neither direct endpoint was reachable from the PC. No removable
storage was mounted. USB, hotspot and microSD performance remain **unmeasured** in this round. The Frame's router
connection used 5 GHz / 80 MHz Wi-Fi, with roughly -56 dBm signal during a read-only probe. This does not reproduce
connection used 5 GHz / 80 MHz Wi-Fi, with roughly -56 dBm signal during a read-only probe. That setup does not reproduce
the 6 GHz direct-hotspot setup in [discussion #242](https://github.com/spoopyghosty0/frameport/discussions/242), where
the reporter measured about 114 MB/s with OpenSSH versus 23 MB/s with Paramiko.
@@ -72,7 +75,7 @@ uses independent Paramiko connections.
The later comparison tested both backends sequentially on Windows and WSL, with three shuffled repetitions of
the same eight 16 MiB files per worker count. Native `sftp-files-N` balances files across N persistent OpenSSH
SFTP batch processes. Setup and completion are included; checksum verification is excluded. All 60 transfers
passed SHA-256 checks. This is benchmark code; the production uploader still uses Paramiko.
passed SHA-256 checks. `sftp-files-N` is benchmark code; the production uploader still uses Paramiko.
| Workers | Windows Paramiko MB/s | Windows OpenSSH MB/s | WSL Paramiko MB/s | WSL OpenSSH MB/s |
|---|---:|---:|---:|---:|
@@ -87,10 +90,7 @@ by 6.5% on Windows and 22.8% on WSL. Eight was faster on Windows but slower on W
default remains two because three regressed by 16.1% on Windows and 5.3% on WSL in this matched comparison.
Link variability remains substantial, and no fast hotspot/USB result is claimed.
Reports: `/mnt/c/Users/Austin/AppData/Local/Temp/frameport-upload-native-parallel-windows.json` and
`/tmp/frameport-upload-native-parallel-wsl.json`.
Settings → Installing → **Parallel uploads** accepts **1–8**, default **2**. It controls large files within one
Settings → Installing → **Parallel uploads** accepts **1–8** (default 2 at the time, 3 now). The setting controls large files within one
install; the FIFO still runs one install job at a time. A single APK is not split into chunks. Large files are
scheduled largest first, with at most one authenticated SSH connection per active worker and never more workers
than files. Small files still use tar batches after the large-file uploads. Choose 1 on a busy network; a higher
@@ -112,7 +112,7 @@ closes only its worker transports; completed files and interrupted `.part` files
On Windows, the production uploader improved from 5.6 to 10.9 MB/s with 2 workers (**1.95 times faster**), and
17.3 MB/s with 8 (**3.08 times faster**). All twelve Windows production transfers passed SHA-256 verification.
The preceding Windows prototype also tested 3 workers (12.2 MB/s median), before aggregate progress/cancellation
were integrated; that is not included as a production measurement in the table.
were integrated; that result is not included as a production measurement in the table.
WSL was noisy: four workers improved its median by about 29%, but 2 did not improve its median, and 3 was slower.
There is no evidence that a particular worker count is universally fastest. The option is retained because it
@@ -124,7 +124,8 @@ environment. Production code, including cleanup and progress checks, was used fo
uv run python scripts/upload_bench.py --mib 128 --repeat 3 --methods app-files-1,app-files-2,app-files-3,app-files-4,app-files-8 --json parallel.json
```
`install.upload_threads` persists the setting. Malformed values use 2 and numeric values are clamped to 1–8.
`install.upload_threads` persists the setting. Malformed values use the default and numeric values are clamped to
1–8.
The serial setting reuses the existing control connection; parallel workers verify that the new transport reaches
the same paired headset before sending any files.
@@ -145,7 +146,7 @@ other network traffic. A 512 MiB payload is preferred for follow-up tests on fas
above used 128 MiB.
The script creates a unique scratch directory on the selected filesystem, checks content outside the timed upload,
and removes its own temporary remote files and temporary key copy. It never modifies the pairing key's ACL. Windows
and removes its own temporary remote files and temporary key copy. The script never modifies the pairing key's ACL. Windows
OpenSSH rejects an `OWNER RIGHTS` ACL entry; the temporary copy grants only the current user, without Python's
Windows `chmod` behaviour that can introduce that entry. Native clients use the application's key and known-hosts
file with strict host verification.
@@ -169,7 +170,7 @@ when the user customized their recipe.
data manifest/digest and artwork hashes. Routine device samples carry only the digest, and updates fetch a
detailed manifest only when necessary. Live numeric settings writes update only the applied runtime configuration,
leaving APK build identity intact. Desired data manifests are cached locally for badges, without render-path I/O.
- Runtime-only updates use agent v87 `configure`: no conversion, signing, APK transfer, artwork, Steam restart or
- Runtime-only updates use the agent's `configure`: no conversion, signing, APK transfer, artwork, Steam restart or
automatic launch test. They still require the installed APK identity and an available destination, and refuse
changes while the game is running. Defaults explicitly reset overrides embedded in an older APK. Generated
configuration files are restored or removed only when they still match FramePort's last write; user edits survive.
@@ -188,7 +189,7 @@ when the user customized their recipe.
safely. Converted-copy cleanup and `build.keep_copies` remain in effect; a configuration/data update does not
require the deleted converted APK.
Install results record `stage_seconds` in the library's install history. This includes planning, conversion,
Install results record `stage_seconds` in the library's install history. The timings include planning, conversion,
validation, transfer and finalization stages reached by that operation; the GUI's subsequent optional launch test
is separate. Use these timings to distinguish build cost, network transfer and device/Steam work when reviewing a
slow install. Configuration success is recorded after writing the configuration and completing its sync; a failed
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

+27 -1
View File
@@ -4,6 +4,23 @@ FrameBridge includes automatic metadata retirement for the verified Meta XR Audi
Wwise build; see [AUDIO_METADATA.md](../docs/AUDIO_METADATA.md) for its lifetime
invariant and regression coverage. It uses the existing adapter installation.
`adapter/swapchain_pool.c` implements default-off GLES `swapchain_pool` (#387): at eight live app runtime
swapchains, eligible single-layer colour images use private GL textures packed into up to two lazy 4096² sRGB
runtime atlases. EndFrame copies each referenced source once and rewrites the final quads (including cylinder
strips), cylinders and stereo projection views after existing layer fixes. OpenXR image ownership, GL bindings,
scissor state, source fences, rectangle reuse and session cleanup are covered by `tests/test_swapchain_pool.py`
(`FRAMEPORT_NATIVE_TESTS=1`, host compiler + cached OpenXR and GLES/EGL/Vulkan headers). Host-tested;
4XVR submenu interaction, visual correctness and copy cost still need a headset check.
Vulkan sessions (#424, AC Nexus) use the same setting: format-43 (sRGB RGBA8) single-layer colour panels get private
device-local VkImages (app usage + transfer src/dst, returned as `XrSwapchainImageVulkanKHR`), moved to
COLOR_ATTACHMENT_OPTIMAL at their first acquire; xrEndFrame records one command buffer per frame (a 3-deep fence ring
on the binding's queue, submits under `flip_lock`) with vkCmdCopyImage into the waited atlas image (COLOR_ATTACHMENT ->
TRANSFER_DST -> COLOR_ATTACHMENT) before releasing it. Quads flagged VERTICAL_FLIP (`flip_emul`, also without the
pool) use the same atlases: a pooled panel's copy becomes a vkCmdBlitImage with reversed rows, a runtime swapchain's
quad gets a per-frame atlas area with an upside-down blit of its shown image; the app's images are only read. Other formats and every arraySize-2 (eye) image keep runtime swapchains. The harness
checks layouts, copy regions, the ring and AC Nexus' swapchain count with a recording Vulkan double; the real queue,
layouts on Turnip and the picture need the device.
Sources of the prebuilt binaries in `../artifacts/` (committed, with `SHA256SUMS`). End users never compile anything;
developers rebuild with `python native/build.py` (downloads NDK r27c, OpenXR headers at pinned commits, d8 into
`native/.cache/`, git-ignored). The adapter, GL shim and platform compat rebuild byte-identically; the VrApi bridge
@@ -15,15 +32,18 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
| `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`). |
| `langpack/` | `arm64-v8a/libfp_langpack.so` | Language packs for the Meta Platform SDK (patch `frame.langpacks`, opt-in). overport's platform loader is a dispatcher in front of Meta's own loader and answers `ovr_LanguagePack_GetCurrent/SetCurrent` with `return 0` (no reply ever). This library becomes the loader's first DT_NEEDED; the loader's own exports of the 31 functions it defines are made STB_LOCAL in `.dynsym` (`elf.hide_exports`, nothing moves), so symbol lookups reach the library first. It serves `<tag>.lang` files found in the game's OBB / app files (`$FRAMEPORT_LANGPACK_DIRS` overrides the search path) through `ovr_LanguagePack_*`, `ovr_AssetFile_GetList` (the loader's list + the packs) and `ovr_AssetFile_StatusById`; its own message queue is merged into `ovr_PopMessage`, and every handle that is not ours (an address-range check) goes to the loader's original function, whose address is read at run time from the loader's in-memory `.dynsym`. With patch `frame.asset_files` (marker byte `@FPASSETS@`, or env `FRAMEPORT_ASSET_FILES=1`) it also lists the data's `*.pak` content files as installed `default` asset files and answers `ovr_AssetFile_DownloadById` for them at once (a completed `DownloadUpdate`, then the result with the path). Host-tested against a stand-in loader (`tests/test_langpack.py`, plain `cc`, no NDK); the bionic/headset side is unverified. |
| `langpack/` | `arm64-v8a/libfp_langpack.so` | Language packs for the Meta Platform SDK (patch `frame.langpacks`, opt-in). overport's platform loader is a dispatcher in front of Meta's own loader and answers `ovr_LanguagePack_GetCurrent/SetCurrent` with `return 0` (no reply ever). This library becomes the loader's first DT_NEEDED; the loader's own exports of the 31 functions it defines are made STB_LOCAL in `.dynsym` (`elf.hide_exports`, nothing moves), so symbol lookups reach the library first. It serves `<tag>.lang` files found in the game's OBB / app files (`$FRAMEPORT_LANGPACK_DIRS` overrides the search path) through `ovr_LanguagePack_*`, `ovr_AssetFile_GetList` (the loader's list + the packs) and `ovr_AssetFile_StatusById`; its own message queue is merged into `ovr_PopMessage`, and every handle that is not ours (an address-range check) goes to the loader's original function, whose address is read at run time from the loader's in-memory `.dynsym`. With patch `frame.asset_files` (marker byte `@FPASSETS@`, or env `FRAMEPORT_ASSET_FILES=1`) it also lists the data's `*.pak` content files as installed `default` asset files and answers `ovr_AssetFile_DownloadById` for them at once (a completed `DownloadUpdate`, then the result with the path). With patch `frame.ovr_org_scoped_id` (marker `@FPORGID@`, or env `FRAMEPORT_ORG_SCOPED_ID=1`) it answers `ovr_User_GetOrgScopedID`, which OVRPort's loader exports as `return 0` (GitHub #452), with a successful `ovrMessage_User_GetOrgScopedID` whose `ovr_OrgScopedID_GetID` is a hash of the user id; when only that patch links the library, the language-pack calls go to the loader unchanged (marker `@FPLANGS@` = 0, env `FRAMEPORT_LANGPACK_SERVE`). Host-tested against a stand-in loader (`tests/test_langpack.py`, plain `cc`, no NDK); the bionic/headset side is unverified. |
| `glshim/` | `arm64-v8a/libglshim.so` | Mesa GLSL compatibility for GLAD engines (hooks eglGetProcAddress): comments out `#pragma` before `#extension`, enables `GL_EXT_shader_implicit_conversions`, hides GL_OVR_multiview (`gl_hide_multiview`, default 1) and logs failed shaders. `-DGLSHIM_TRACE` adds per-FBO draw/error tracing. |
| `glmv/` | `arm64-v8a/libfpglmv.so` | Single-view draws of OVR_multiview programs (patch `frame.gl_multiview_fbo`, opt-in, GitHub #77). Mesa enforces OVR_multiview's rule that a draw's program declares as many views as the draw framebuffer has; engines that compile every shader with `num_views=2` and also draw into ordinary 2D framebuffers (Doom3Quest's HUD/PDA pool) lose those draws. The engine library gets it as first DT_NEEDED (direct `gl*` imports) and its `dlopen("libGLESv3.so")` string is rewritten to `libfpglmv.so` (same length; its `qgl*` dlsym table); linked `--no-as-needed` to libGLESv3 so dlsym on its handle finds everything it doesn't wrap. It records stage sources at glLinkProgram, caches the draw framebuffer's color-attachment view count per bind, and when a multiview program draws into a 0-view framebuffer it binds a lazily built twin (`glmv_rewrite.h`: num_views layout blanked, `gl_ViewID_OVR` -> `(0u)`; attribute locations + uniform block bindings copied, default-block uniforms copied before every such draw with a shadow to skip unchanged ones), draws, rebinds the original. Logcat tag `GLMV`; `gl_mv_debug=1`. The rewriter is host-tested on Doom3Quest's 19 shaders (`tests/test_gl_multiview_fbo.py`); untested on the device. |
| `tlskeys/` | `arm64-v8a/libfp_tkeys.so` | Thread-key pool for Unity (patch `frame.unity_tls_key_pool`, opt-in, GitHub #198; approach and the first helper by Klownicle). Bionic allows 128 pthread keys per process; Unity 6 plus large native libraries (Firebase, Gecko) and the Frame's own libraries use them up (Wander: `cannot create thread specific key for __cxa_get_globals()`). libunity.so's four key imports are renamed to `fp_tkey_create/_delete/_get/_set` (`elf.rename_imports`, same `.dynstr` bytes, versions set global) and this library is added to its DT_NEEDED: keys without a destructor become slots (512, generation-tagged, `0x40000000 + slot`) in a per-thread table under one real key; keys with a destructor and all other libraries keep real keys. Lock-free get/set. Logs `fp_tkeys: pooling Unity's thread keys` once (tag FrameBridge). Host-tested (rename); not yet run on the device. |
| `foveation/` | `arm64-v8a/libVkLayer_fdm_darken.so` (+ `libvrapi_foveation.so`, The Climb 2) | Two Vulkan layers in one library, linked (dormant) into every arm64 FrameBridge build. `VK_LAYER_FRAMEPORT_fdm_darken` (above Valve's `VALVE_fdm_injection`, only with the foveation test): dims coarse fragments (`FRAMEPORT_FOVEATION_TEST=1`) or paints 1/fragment area (`=2`, developers). `VK_LAYER_FRAMEPORT_fdm_centre` (`centre.h`, inserted right **below** Valve's layer whenever `FDM_DEBUG` has `enable`, i.e. Eye-tracked/Fixed; off with `FRAMEPORT_FDM_CENTRE=0`, never for The Climb 2): Valve's density map covers the whole swapchain with its sharp rectangle at the swapchain centre; this layer rewrites the per-view density-map offsets at the end of each pass (never changing images: for Fixed it drops `disable_offsets` from `FDM_DEBUG` in-process so Valve runs its offset mode, then overrides Valve's gaze offsets; every offset Turnip reads is written, per view or per eye map layer) so the sharp area sits on each eye's optical centre inside its submitted rect, or on the gaze point re-based on that rect (GitHub #394). FrameBridge (`adapter/fdm_views.c`) reports the views via `fp_fdm_set_views`. Host harness: `tests/test_fdm_centre.py`. |
| `zinkfix/` | `arm64-v8a/libVkLayer_fp_shaderfix.so` | Shader-fix Vulkan layer `VK_LAYER_FP_shader_fix` for OpenGL ES games (patch `frame.zink_shader_fix`): sits between Zink (Mesa's GL on Vulkan) and the driver and inserts the words of `zink_shader_fix` (same format as the Vulkan shim's `vk_shader_fix`: size + SHA-256 match) into SPIR-V modules; `zink_shader_dump=1` writes every distinct module to `files/fp_spirv/`. The engine library loads it first (DT_NEEDED); its constructor puts the layer in front of Android's GraphicsEnv debug layer list (private Android 11 `getDebugLayers`/`setDebugLayers`, found through libvulkan.so's dependencies) before EGL starts, so the loader picks it up from the app's lib dir. Exports only the enumeration functions and `VK_LAYER_FP_shader_fixGet*ProcAddr` (never plain `vk*` entry points). Vader Immortal's lightspeed shaders (Klownicle, GitHub #49). |
| `xrlayer/` | `linux-arm64/libxr_frameport_timefix.so` + `XR_APILAYER_FRAMEPORT_timefix.json` | OpenXR API layer for Windows PC VR games under Proton on the Frame (glibc aarch64, built freestanding with the NDK's clang), on by default (patch `pcvr.xr_timefix`). Retries `xrCreateInstance` as an OpenXR 1.0 app when the runtime rejects 1.1 (the Frame's SteamVR runtime does, and Proton 11's VR helper asks for 1.1). Also emulates `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` if the runtime refuses them (the Frame's Android runtime does; its Linux runtime, used by Proton, supports them as of 2026-09-29 — so this part is only a fallback; Proton's wineopenxr needs them for `XR_KHR_win32_convert_performance_counter_time`, which Revive requires) with the adapter's xrWaitFrame-calibrated offset, and drops the extension from xrCreateInstance if the runtime rejects it. Enabled per game by the Proton launch.sh (`XR_API_LAYER_PATH`, `XR_ENABLE_API_LAYERS`). `tests/test_xrlayer.py` builds it for x86_64 and drives it via ctypes. |
| `oculushmd/` | `win-x64/fp_oculushmd.exe` | PC VR counterpart of overport's `patch_oculus_unreal` (patch `pcvr.oculus_unreal`, default for Unreal Rift games on the Frame). Unreal's OculusHMD and LibOVR's `ovr_Detect` only start when the Windows named event `OculusHMDConnected` exists and is signalled (the Oculus service creates it). `fp_oculushmd.exe <command line>` creates it (manual reset, signalled), runs the command (Revive's injector) in a job object and keeps the event until every process in the job — the game and its children — has exited (fallback without job support: 3 minutes); returns the command's exit code and logs `FramePort oculushmd: …` to stderr (launch.log). Freestanding Windows x64 console exe (no CRT, no Windows SDK): the NDK's clang + `lld-link /Brepro` with a kernel32 import library generated by `llvm-dlltool`, so it's byte-reproducible. Tested on Windows from WSL with a build whose event name is `-DEVENT_NAME=L"…"` (the Oculus service owns the real event on a PC with the Oculus app). |
| `xrruntime/` | `arm64-v8a/libfp_xrruntime.so` | Runtime negotiation endpoint for embedded Khronos loaders (`frame.static_openxr_loader`). Loads FrameBridge by soname, forwards Android loader initialization through its GIPA, and returns FrameBridge GIPA. The unmodified inner Khronos loader is reused from OVRPort’s generic loader (also when `overport: False`); its paths remain system paths. Built with `--only xrruntime`. |
| `xrshim/` | `arm64-v8a/libframe_xrshim.so` | FrameBridge extension shim, only in builds with `controller_models=1`. overport's dispatcher (`libopenxr_loader.so`) answers `xrGetInstanceProcAddr` from a fixed table and refuses everything else (`overportOXR: Unknown proc addr: xrLoadRenderModelFB`), so the adapter never sees XR_FB_render_model lookups. Meta's OVRPlugin gets the function with `dlopen("libopenxr_loader.so")` + `dlsym`, so FramePort rewrites that `.rodata` string in `libOVRPlugin.so` (same length, in place) to `libframe_xrshim.so`. The shim returns the adapter's emulated functions (`framebridge_extension_proc`) and forwards every other name to overport. A DT_NEEDED interposer does not work here (dlsym on a handle searches that library first); tried and dropped. |
| `venc/` | `linux-arm64-bin/fp_venc` | Hardware H.264 encoder for the live view, run on the Frame (spec: `venc/SPEC.md`). Reads SteamVR's "headset view" webcam (v4l2loopback `/dev/video99`, RGB24), box-scales and converts it to NV12 (BT.709 limited range; `convert.c`, NEON with a bit-identical scalar reference, also built for x86_64 by the host test), and feeds the SoC's V4L2 stateful encoder (`dev-encoder.rst`, Qualcomm iris `/dev/video-enc0`): CBR, no B-frames, SPS/PPS on every IDR, one frame per slot of a fixed grid (fps = a divisor of the panel refresh read through DRM KMS, default cap 45), Annex B on stdout. stdin `k` = keyframe, `q`/EOF = drain and stop; `--probe` prints the setup as JSON, `--selftest` compares NEON and scalar output. Fully static and freestanding (raw syscalls, own `_start`; only the NDK's kernel UAPI headers, staged in `.cache/uapi-arm64`), byte-reproducible. Lives in `linux-arm64-bin/` because `linux-arm64/` is uploaded whole as the OpenXR layer. |
| `java-handoff/` | `dex/browser-handoff.dex` | `org.frameport.handoff.BrowserHandoff`, the browser stand-in activity of patch `frame.browser_handoff` (GitHub #466): Lepton hands VIEW intents to Steam before resolving, so it normally never runs; started directly it writes `steam://openurl/<url>` to `/lepton/steam.pipe` on a thread and finishes. Build: `python native/build.py --only handoff` (javac + d8; Android classes from compile-only stand-ins). |
| `java-stubs/` | `dex/oculusos-stubs.dex` | No-op `com.oculus.os.AnalyticsEvent` / `UnifiedTelemetryLogger` for native code that looks them up. |
The ovr_* stub library is *not* prebuilt: `src/frameport/analysis/stubgen.py` generates it per game from the exact
@@ -31,3 +51,9 @@ missing symbols (no compiler needed).
After changing anything here: `python native/build.py`, commit artifacts + SHA256SUMS, then run `frameport parity`
(the report marks updated binaries as "expected") and a device launch test.
`adapter.fixed_resolution` (off by default, #406/#394) keeps META recommended layer
resolution at tracked full swapchain size. Both adapter GIPA and xrshim's existing
extension entry point serve it; the setting adds OVRPlugin shim routing. The extension
stays visible, avoiding dependence on a plugin fallback. No adapter revision bump for
this opt-in fix. See DEVICE_NOTES "Fixed layer resolution / Batman" for verification.
+14 -3
View File
@@ -141,6 +141,13 @@ python native/a32translator/bridge/build.py [--test]
`vrapi_GetPredictedTracking`). The runtime exports it itself (`libvrapi.so` is a system library; the game's copy is
Oculus' loader and is dropped) and translates it to FramePort's 64-bit VrApi -> OpenXR bridge (shipped as
`libfp_vrapi64.so`, with OVRPort's OpenXR dispatcher and the FrameBridge adapter). Layouts are VrApi 1.5's.
- **NDK media** (`host/media.c`, imports.txt): libmediandk's AMediaFormat/AMediaCodec/AMediaExtractor functions and
its `AMEDIAFORMAT_KEY_*` string variables (`guest/media_keys.c`, AOSP's values), as Unity's video player looks
them up (it drops NDK playback if one is missing, and its JNI fallback needs
`GetDirectBufferAddress` of codec buffers above 4 GiB). Format strings/buffers get guest copies freed with the
format; codec buffers get a guest mirror per (codec, input/output, index): input mirrors go to the codec at
`queueInputBuffer` (the queued range), output buffers come in at `getOutputBuffer` (the range
`dequeueOutputBuffer` reported). `AMediaCodecBufferInfo` has the same layout on both sides.
- **OpenSL ES** (`guest/opensles.c`): engine, output mix and PCM players with an Android buffer queue, implemented in
the guest (OpenSL objects are function tables); a guest thread per player writes to AAudio on the host.
- **Signals**: host handlers never use SA_RESTART; a blocking system call interrupted by a guest signal is
@@ -153,7 +160,9 @@ python native/a32translator/bridge/build.py [--test]
Unity only renders with window focus.
- Debugging: `A32_WATCH=<s>` logs every guest thread's pc and the return addresses on its stack as `lib+offset`;
`A32B_TRACE=1` logs native registrations and calls; `A32_STRACE`, `A32_INTERP` as for a32run. Set them through the
recipe's `device.lepton_env` (`LEPTON_ENV_<NAME>`).
recipe's `device.lepton_env` (`LEPTON_ENV_<NAME>`). `OVP_HEADLESS_TEST=1` (tests without a worn headset): the
VrApi bridge submits images although the runtime doesn't ask for them and `vrapi.c` keeps untracked default poses
instead of dropping the frame, so FrameBridge's `snapshot=N` shows what the game draws.
Learned on the way: `-static-pie` links `crtbegin_dynamic.o`, which leaves `.init_array` to a dynamic linker (so
none of libc's own constructors ran: bionic's systrace property crashed in an Android process); bionic's arm32
@@ -165,7 +174,8 @@ Tests:
with a fake JVM (`fakejni.c`) for three guest libraries: RegisterNatives and `Java_*` exports, 12-argument
natives (stack arguments on both sides), floats/doubles/bytes, strings, arrays below and above 4 GiB, every
Call*Method style, NewObject, nested calls, guest threads that attach, several host threads, exceptions, a
NativeActivity with a DT_NEEDED library, and C++ exceptions in guest libraries using ordinary REL,
NativeActivity with a DT_NEEDED library, NDK media against a fake libmediandk whose buffers lie above 4 GiB
(`fakemedia.c`, `libbtest4.c`), and C++ exceptions in guest libraries using ordinary REL,
standard RELR and Android's older RELR tags (with APS2). Runs under qemu-aarch64 or
natively (passes on the Frame with JIT and interpreter).
- `uv run python bridge/test/make_apk.py`: `org.frameport.a32test`, a NativeActivity APK without Java code whose
@@ -214,7 +224,8 @@ up to ±30 % between runs), against the same source built natively for AArch64:
2. A FramePort patch that converts an APK (libraries moved, stubs, bridge, runtime, `extractNativeLibs=true`) +
triage signature + PLAYBOOK row.
3. Bridge gaps: guest threads of host threads that exit are never freed; `Java_*` functions of libraries the guest
dlopens itself aren't registered; `GetDirectBufferAddress` of buffers above 4 GiB returns 0; signals the guest
dlopens itself aren't registered; `GetDirectBufferAddress` of buffers above 4 GiB returns 0 (Java-side writes would need syncing at JNI
calls; NDK media avoids it for Unity's video); signals the guest
handles are only delivered to guest threads.
4. JIT: VLDn single-lane/all-lanes forms, flag liveness (skip unused C/V computations), keeping the VFP cache
across a loop's back edge, and VRECPS/VRSQRTS/VCVTR natively.
+6 -4
View File
@@ -22,10 +22,10 @@ OUT = ROOT / "out" / "bridge"
TRANSLATOR = ["dispatch.c", "mem.c", "loader.c", "syscall.c", "signals.c", "threads.c", "interp.c", "interp_vfp.c",
"interp_neon.c", "a32_decode.c", "t32_decode.c", "vfp_neon_decode.c", "jit.c", "jit_vfp.c",
"jit_glue.S"]
HOST = ["bridge.c", "handles.c", "jni.c", "natives.c", "native_activity.c", "android.c", "vrapi.c", "input.c", "tramp.S", "gen/jni_gen.c",
HOST = ["bridge.c", "handles.c", "jni.c", "natives.c", "native_activity.c", "android.c", "vrapi.c", "input.c", "media.c", "tramp.S", "gen/jni_gen.c",
"gen/imports.c"]
HOST_GL = ["gl.c", "gen/gl.c"] # not in the PC harness (no GLES there)
GUEST = ["main.c", "loader.c", "compat.c", "log.c", "gl.c", "opensles.c", "hc.S", "gen/stubs.S", "gen/gl_stubs.S", "gen/exports.S"]
GUEST = ["main.c", "loader.c", "compat.c", "log.c", "gl.c", "opensles.c", "media_keys.c", "hc.S", "gen/stubs.S", "gen/gl_stubs.S", "gen/exports.S"]
WARN = ["-Wall", "-Wextra", "-Wno-unused-parameter", "-Wno-missing-field-initializers"]
@@ -71,7 +71,7 @@ def main():
src = ROOT / "src"
common = [*opt, *prefix, *WARN, "-march=armv8-a+crypto", "-std=gnu11", "-fno-strict-aliasing", "-fPIC"]
run([cc64, *common, "-shared", "-fvisibility=hidden", *[src / s for s in TRANSLATOR], *[h / s for s in HOST + HOST_GL],
"-llog", "-landroid", "-laaudio", "-lEGL", "-lGLESv3", "-ldl", "-lm", "-Wl,--no-undefined", "-Wl,-soname,liba32bridge.so", "-o", OUT / "liba32bridge.so"])
"-llog", "-landroid", "-laaudio", "-lmediandk", "-lEGL", "-lGLESv3", "-ldl", "-lm", "-Wl,--no-undefined", "-Wl,-soname,liba32bridge.so", "-o", OUT / "liba32bridge.so"])
run([cc64, *common, "-shared", "-fvisibility=hidden", h / "stub.c", "-L", OUT, "-la32bridge",
"-Wl,--no-undefined", "-o", OUT / "liba32stub.so"])
@@ -82,12 +82,14 @@ def main():
# the harness is static (runs under qemu-aarch64 or on any arm64 Linux): high image base keeps the
# low 4 GiB free, like a32run's
run([cc64, *common, "-DA32B_STDERR_LOG", "-DA32B_TEST", "-static", "-Wl,--image-base=0x7000000000", "-I", h,
t / "harness.c", t / "fakejni.c", *[src / s for s in TRANSLATOR], *[h / s for s in HOST], "-lm", "-ldl",
t / "harness.c", t / "fakejni.c", t / "fakemedia.c", *[src / s for s in TRANSLATOR], *[h / s for s in HOST], "-lm", "-ldl",
"-o", to / "harness"])
run([cc32, *opt, *prefix, *WARN, "-mthumb", "-mfpu=neon-vfpv4", "-shared", "-fPIC", t / "libbtest.c",
"-Wl,-soname,libbtest.so", "-llog", "-o", to / "libbtest.a32.so"])
run([cc32, *opt, *prefix, *WARN, "-marm", "-mfpu=neon-vfpv4", "-shared", "-fPIC", t / "libbtest2.c",
to / "libbtest.a32.so", "-landroid", "-llog", "-o", to / "libbtest2.a32.so"])
run([cc32, *opt, *prefix, *WARN, "-mthumb", "-mfpu=neon-vfpv4", "-shared", "-fPIC", "-I", t,
t / "libbtest4.c", "-ldl", "-o", to / "libbtest4.a32.so"])
run([bin_ / "armv7a-linux-androideabi30-clang++", *opt, *prefix, *WARN, "-mthumb", "-shared", "-fPIC",
"-static-libstdc++", t / "libbtest3.cpp", "-o", to / "libbtest3.a32.so"])
# Exercise both RELR tag families through the real guest loader. C++ constructors,
+5
View File
@@ -204,6 +204,8 @@ def gen_imports_host(imports, out):
"#pragma clang diagnostic ignored \"-Wdeprecated-declarations\"",
"#include <android/asset_manager_jni.h>", "#include <android/configuration.h>", "#include <android/input.h>", "#include <android/looper.h>",
"#include <android/native_window_jni.h>", "#include <android/sensor.h>",
"#include <media/NdkMediaCodec.h>", "#include <media/NdkMediaExtractor.h>",
"#include <media/NdkMediaFormat.h>",
'#include "../imports_internal.h"', ""]
for name, ret, args, manual in imports:
if manual:
@@ -286,6 +288,9 @@ def gen_guest(ndk, imports, gdir, extra):
names[n] = n
for n, *_ in imports:
names[n] = n
# libmediandk's data symbols (const char * variables), defined in guest/media_keys.c
for n in re.findall(r"^KEY\((\w+),", (HERE / "guest" / "media_keys.c").read_text(), re.M):
names[f"AMEDIAFORMAT_KEY_{n}"] = f"AMEDIAFORMAT_KEY_{n}"
for n in extra:
names.setdefault(n, OVERRIDES.get(n, n))
missing = sorted(want - have - set(OVERRIDES) - SKIP - set(COMPAT))
@@ -0,0 +1,31 @@
/* libmediandk's data symbols: the AMEDIAFORMAT_KEY_* string pointers. Unity's video player looks them up with dlsym
* like its functions and drops NDK playback if one is missing. The host's libmediandk only compares key names by
* content, so guest strings with AOSP's values (frameworks/av media/ndk/NdkMediaFormat.cpp) work with its formats.
* gen_stubs.py exports every AMEDIAFORMAT_KEY_ defined here. The set: what Unity 2017.4's libunity.so references. */
#define KEY(name, value) const char *AMEDIAFORMAT_KEY_##name = value
KEY(AAC_PROFILE, "aac-profile");
KEY(BIT_RATE, "bitrate");
KEY(CHANNEL_COUNT, "channel-count");
KEY(CHANNEL_MASK, "channel-mask");
KEY(COLOR_FORMAT, "color-format");
KEY(DURATION, "durationUs");
KEY(FLAC_COMPRESSION_LEVEL, "flac-compression-level");
KEY(FRAME_RATE, "frame-rate");
KEY(HEIGHT, "height");
KEY(I_FRAME_INTERVAL, "i-frame-interval");
KEY(IS_ADTS, "is-adts");
KEY(IS_AUTOSELECT, "is-autoselect");
KEY(IS_DEFAULT, "is-default");
KEY(IS_FORCED_SUBTITLE, "is-forced-subtitle");
KEY(LANGUAGE, "language");
KEY(MAX_HEIGHT, "max-height");
KEY(MAX_INPUT_SIZE, "max-input-size");
KEY(MAX_WIDTH, "max-width");
KEY(MIME, "mime");
KEY(PUSH_BLANK_BUFFERS_ON_STOP, "push-blank-buffers-on-shutdown");
KEY(REPEAT_PREVIOUS_FRAME_AFTER, "repeat-previous-frame-after");
KEY(ROTATION, "rotation-degrees");
KEY(SAMPLE_RATE, "sample-rate");
KEY(STRIDE, "stride");
KEY(WIDTH, "width");
+311
View File
@@ -0,0 +1,311 @@
/* NDK media (libmediandk) functions that gen_stubs.py can't generate (imports.txt "!" lines): host memory the guest
* must read or write.
*
* - Strings and buffers an AMediaFormat owns (getString, getBuffer, toString) get guest copies, freed with the format
* (not guest_cstr's cache: a deleted format's memory comes back for another string).
* - Codec buffers (getInputBuffer/getOutputBuffer) lie in the host's heap, above 4 GiB. The guest gets a mirror per
* (codec, input/output, index) in its own memory: an input mirror goes to the codec's buffer at queueInputBuffer
* (the range queued), an output buffer comes into its mirror at getOutputBuffer (the range dequeueOutputBuffer
* reported, else all of it). Unity's video player fills input buffers with AMediaExtractor_readSampleData, which
* then writes straight into the mirror (guest memory is host memory). Mirrors go with AMediaCodec_delete. */
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaExtractor.h>
#include <media/NdkMediaFormat.h>
#include <stddef.h>
#include <stdlib.h>
#include "imports_internal.h"
#define W(f) extern __typeof__(f) f __attribute__((weak))
W(AMediaFormat_delete); W(AMediaFormat_toString); W(AMediaFormat_getString); W(AMediaFormat_getBuffer);
W(AMediaCodec_delete); W(AMediaCodec_getInputBuffer); W(AMediaCodec_queueInputBuffer);
W(AMediaCodec_dequeueOutputBuffer); W(AMediaCodec_getOutputBuffer);
#undef W
/* the guest's AMediaCodecBufferInfo (ARM EABI: int64_t 8-byte aligned) has the host's layout: passed through */
_Static_assert(sizeof(AMediaCodecBufferInfo) == 24 && offsetof(AMediaCodecBufferInfo, presentationTimeUs) == 8 &&
offsetof(AMediaCodecBufferInfo, flags) == 16, "AMediaCodecBufferInfo layout");
/* ---------------------------------------------------------------- AMediaFormat */
typedef struct fcopy {
struct fcopy *next;
AMediaFormat *format;
const void *host; /* the format's own memory this copies */
uint32_t size;
uint32_t g;
} fcopy;
static pthread_mutex_t fcopy_lock = PTHREAD_MUTEX_INITIALIZER;
static fcopy *fcopies;
/* a guest copy of n bytes of format f's memory p (the earlier copy if it still matches) */
static uint32_t format_copy(AMediaFormat *f, const void *p, size_t n)
{
if (in_guest_reach(p, n)) return p2g(p);
pthread_mutex_lock(&fcopy_lock);
for (fcopy *k = fcopies; k; k = k->next)
if (k->format == f && k->host == p && k->size == n && !memcmp(g2p(k->g), p, n)) {
uint32_t g = k->g;
pthread_mutex_unlock(&fcopy_lock);
return g;
}
pthread_mutex_unlock(&fcopy_lock);
uint32_t g = guest_malloc(n ? (uint32_t)n : 1);
fcopy *k = malloc(sizeof *k);
if (!g || !k) {
free(k);
return g;
}
memcpy(g2p(g), p, n);
k->format = f, k->host = p, k->size = (uint32_t)n, k->g = g;
pthread_mutex_lock(&fcopy_lock);
k->next = fcopies;
fcopies = k;
pthread_mutex_unlock(&fcopy_lock);
return g;
}
static void format_forget(AMediaFormat *f)
{
fcopy *gone = NULL;
pthread_mutex_lock(&fcopy_lock);
for (fcopy **pp = &fcopies; *pp;) {
fcopy *k = *pp;
if (k->format == f) {
*pp = k->next;
k->next = gone;
gone = k;
} else {
pp = &k->next;
}
}
pthread_mutex_unlock(&fcopy_lock);
while (gone) {
fcopy *k = gone;
gone = k->next;
guest_free(k->g);
free(k);
}
}
void imp_AMediaFormat_delete(a32_cpu *c, argreader *R)
{
AMediaFormat *f = h2ptr(rd32(R));
if (!f || !AMediaFormat_delete) {
ret32(c, (uint32_t)AMEDIA_ERROR_INVALID_OBJECT);
return;
}
format_forget(f);
ret32(c, (uint32_t)AMediaFormat_delete(f));
}
void imp_AMediaFormat_toString(a32_cpu *c, argreader *R)
{
AMediaFormat *f = h2ptr(rd32(R));
const char *s = f && AMediaFormat_toString ? AMediaFormat_toString(f) : NULL;
ret32(c, s ? format_copy(f, s, strlen(s) + 1) : 0);
}
void imp_AMediaFormat_getString(a32_cpu *c, argreader *R)
{
AMediaFormat *f = h2ptr(rd32(R));
const char *name = g2p(rd32(R));
uint32_t out = rd32(R);
const char *s = NULL;
bool ok = f && AMediaFormat_getString && AMediaFormat_getString(f, name, &s) && s;
if (ok && out) st32(out, format_copy(f, s, strlen(s) + 1));
ret32(c, ok);
}
void imp_AMediaFormat_getBuffer(a32_cpu *c, argreader *R)
{
AMediaFormat *f = h2ptr(rd32(R));
const char *name = g2p(rd32(R));
uint32_t out_data = rd32(R), out_size = rd32(R);
void *d = NULL;
size_t n = 0;
bool ok = f && AMediaFormat_getBuffer && AMediaFormat_getBuffer(f, name, &d, &n);
if (ok) {
if (out_data) st32(out_data, d ? format_copy(f, d, n) : 0);
if (out_size) st32(out_size, (uint32_t)n);
}
ret32(c, ok);
}
/* ---------------------------------------------------------------- AMediaCodec buffers */
typedef struct mirror {
struct mirror *next;
AMediaCodec *codec;
int output;
size_t index;
uint8_t *host; /* the codec's buffer as last handed out */
size_t cap; /* its size */
uint32_t g, gcap; /* the guest's copy and its size (0: none yet) */
int32_t off, size; /* output: the range dequeueOutputBuffer reported (size -1: unknown) */
} mirror;
static pthread_mutex_t mirror_lock = PTHREAD_MUTEX_INITIALIZER;
static mirror *mirrors;
/* the entry for (codec, output, index), created if missing; mirror_lock held */
static mirror *mirror_find(AMediaCodec *codec, int output, size_t index, int create)
{
for (mirror *m = mirrors; m; m = m->next)
if (m->codec == codec && m->output == output && m->index == index) return m;
if (!create) return NULL;
mirror *m = calloc(1, sizeof *m);
if (!m) return NULL;
m->codec = codec, m->output = output, m->index = index, m->size = -1;
m->next = mirrors;
mirrors = m;
return m;
}
/* the guest's view of codec buffer p (cap bytes): p itself when the guest can reach it, else its mirror (made, or
* remade when the codec's buffer grew); *off, *size: the output range dequeueOutputBuffer reported (size -1: none) */
static uint32_t mirror_of(AMediaCodec *codec, int output, size_t index, uint8_t *p, size_t cap, int32_t *off,
int32_t *size)
{
*off = 0, *size = -1;
int reach = in_guest_reach(p, cap);
if (!reach && cap > 0xffffffffu) return 0;
uint32_t old = 0, g = 0;
pthread_mutex_lock(&mirror_lock);
mirror *m = mirror_find(codec, output, index, !reach);
if (m) {
m->host = p, m->cap = cap;
*off = m->off, *size = m->size;
if (reach) { /* a buffer in reach needs no mirror (queueInputBuffer sees that from host) */
} else if (m->g && m->gcap >= cap) {
g = m->g;
} else {
old = m->g;
m->g = 0, m->gcap = 0;
}
}
pthread_mutex_unlock(&mirror_lock);
if (reach) return p2g(p);
if (!m) return 0;
if (old) guest_free(old); /* guest calls: outside the lock */
if (g) return g;
g = guest_malloc(cap ? (uint32_t)cap : 1);
if (!g) return 0;
pthread_mutex_lock(&mirror_lock);
m = mirror_find(codec, output, index, 1);
uint32_t extra = 0;
if (m && !m->g) { /* (another thread could have made one meanwhile: then that one stays) */
m->g = g, m->gcap = (uint32_t)(cap ? cap : 1);
} else if (m) {
extra = g;
g = m->g;
}
pthread_mutex_unlock(&mirror_lock);
if (extra) guest_free(extra);
return m ? g : 0;
}
void imp_AMediaCodec_getInputBuffer(a32_cpu *c, argreader *R)
{
AMediaCodec *codec = h2ptr(rd32(R));
size_t index = rd32(R);
uint32_t out_size = rd32(R);
size_t cap = 0;
uint8_t *p = codec && AMediaCodec_getInputBuffer ? AMediaCodec_getInputBuffer(codec, index, &cap) : NULL;
if (out_size) st32(out_size, p ? (uint32_t)cap : 0);
int32_t off, size;
ret32(c, p ? mirror_of(codec, 0, index, p, cap, &off, &size) : 0);
}
void imp_AMediaCodec_queueInputBuffer(a32_cpu *c, argreader *R)
{
AMediaCodec *codec = h2ptr(rd32(R));
size_t index = rd32(R);
uint32_t off = rd32(R), size = rd32(R);
uint64_t time = rd64(R);
uint32_t flags = rd32(R);
if (!codec || !AMediaCodec_queueInputBuffer) {
ret32(c, (uint32_t)AMEDIA_ERROR_INVALID_OBJECT);
return;
}
pthread_mutex_lock(&mirror_lock);
mirror *m = mirror_find(codec, 0, index, 0);
if (m && m->g && m->host && !in_guest_reach(m->host, m->cap) && size && (uint64_t)off + size <= m->cap)
memcpy(m->host + off, (uint8_t *)g2p(m->g) + off, size);
else if (m && m->g && size && (uint64_t)off + size > m->cap)
bridge_log(ANDROID_LOG_WARN, "AMediaCodec_queueInputBuffer: %u bytes at %u past buffer %zu (%zu bytes)", size,
off, index, m->cap);
pthread_mutex_unlock(&mirror_lock);
ret32(c, (uint32_t)AMediaCodec_queueInputBuffer(codec, index, (off_t)off, size, time, flags));
}
void imp_AMediaCodec_dequeueOutputBuffer(a32_cpu *c, argreader *R)
{
AMediaCodec *codec = h2ptr(rd32(R));
AMediaCodecBufferInfo *info = g2p(rd32(R));
int64_t timeout = (int64_t)rd64(R);
AMediaCodecBufferInfo tmp;
if (!codec || !AMediaCodec_dequeueOutputBuffer) {
ret32(c, (uint32_t)AMEDIACODEC_INFO_TRY_AGAIN_LATER);
return;
}
ssize_t r = AMediaCodec_dequeueOutputBuffer(codec, info ? info : &tmp, timeout);
if (r >= 0) {
AMediaCodecBufferInfo *i = info ? info : &tmp;
pthread_mutex_lock(&mirror_lock);
mirror *m = mirror_find(codec, 1, (size_t)r, 1);
if (m) m->off = i->offset, m->size = i->size;
pthread_mutex_unlock(&mirror_lock);
}
ret32(c, (uint32_t)r);
}
void imp_AMediaCodec_getOutputBuffer(a32_cpu *c, argreader *R)
{
AMediaCodec *codec = h2ptr(rd32(R));
size_t index = rd32(R);
uint32_t out_size = rd32(R);
size_t cap = 0;
uint8_t *p = codec && AMediaCodec_getOutputBuffer ? AMediaCodec_getOutputBuffer(codec, index, &cap) : NULL;
if (out_size) st32(out_size, p ? (uint32_t)cap : 0);
if (!p) {
ret32(c, 0);
return;
}
int32_t off, size;
uint32_t g = mirror_of(codec, 1, index, p, cap, &off, &size);
if (g && g != p2g(p)) {
if (size < 0 || off < 0 || (uint64_t)off + (uint32_t)size > cap) off = 0, size = (int32_t)cap;
memcpy((uint8_t *)g2p(g) + off, p + off, (size_t)size);
}
ret32(c, g);
}
void imp_AMediaCodec_delete(a32_cpu *c, argreader *R)
{
AMediaCodec *codec = h2ptr(rd32(R));
if (!codec || !AMediaCodec_delete) {
ret32(c, (uint32_t)AMEDIA_ERROR_INVALID_OBJECT);
return;
}
mirror *gone = NULL;
pthread_mutex_lock(&mirror_lock);
for (mirror **pp = &mirrors; *pp;) {
mirror *m = *pp;
if (m->codec == codec) {
*pp = m->next;
m->next = gone;
gone = m;
} else {
pp = &m->next;
}
}
pthread_mutex_unlock(&mirror_lock);
while (gone) {
mirror *m = gone;
gone = m->next;
guest_free(m->g);
free(m);
}
ret32(c, (uint32_t)AMediaCodec_delete(codec));
}
+6 -1
View File
@@ -508,8 +508,13 @@ static void submit_diag(void *m, const submit2 *d, const pose64 *pose, int incom
/* Repair only a default without matching tracked provenance on a world projection, never an arbitrary render pose,
* head-locked layer or cylinder placement. If no tracked answer exists, end an empty frame
* rather than reproject an image around the origin. No additional WaitFrame/time advance. */
/* OVP_HEADLESS_TEST=1 (automated tests without a worn headset, as for the VrApi bridge): no head is ever tracked, so
* submit_prepare would drop every frame; it keeps the game's own pose instead, so FrameBridge's snapshots show what the
* game draws (-1: not read yet) */
static int headless_test = -1;
static void submit_prepare(void *m, submit2 *d)
{
if (headless_test < 0) headless_test = getenv("OVP_HEADLESS_TEST") != NULL;
pose64 original = {0};
int have_pose = 0, incoming_match = 1, had_default = 0, repaired = 0, dropped = 0;
/* Inspect all incoming world projections before any locate adds a cache entry. */
@@ -543,7 +548,7 @@ static void submit_prepare(void *m, submit2 *d)
if ((tr.Status & 15) != 15) tr = tracking_of(m, 0);
if ((tr.Status & 15) == 15) {
p->HeadPose = tr.HeadPose; repaired = 1;
} else { dropped = 1; break; }
} else if (!headless_test) { dropped = 1; break; }
}
/* Record the incoming pose, not the repaired pose. */
submit_diag(m, d, have_pose ? &original : NULL, incoming_match, had_default, repaired, dropped);
+47
View File
@@ -181,3 +181,50 @@ J AAsset_getRemainingLength64(H)
! I AAsset_openFileDescriptor(HPP)
! I AAsset_openFileDescriptor64(HPP)
I AAsset_isAllocated(H)
# NDK media (libmediandk), as Unity's video player uses it: Unity needs every one of these, else it falls back to
# its JNI path, whose MediaCodec ByteBuffers lie outside the guest's reach. "!" ones are in host/media.c: strings and
# buffers the host owns get guest copies (codec buffers: mirrors synchronised at queueInputBuffer/getOutputBuffer).
H AMediaFormat_new()
! I AMediaFormat_delete(H)
! S AMediaFormat_toString(H)
I AMediaFormat_getInt32(HPP)
I AMediaFormat_getInt64(HPP)
I AMediaFormat_getFloat(HPP)
! I AMediaFormat_getString(HPP)
! I AMediaFormat_getBuffer(HPPP)
v AMediaFormat_setInt32(HPI)
v AMediaFormat_setInt64(HPJ)
v AMediaFormat_setFloat(HPf)
v AMediaFormat_setString(HPP)
v AMediaFormat_setBuffer(HPPI)
H AMediaCodec_createCodecByName(P)
H AMediaCodec_createDecoderByType(P)
H AMediaCodec_createEncoderByType(P)
! I AMediaCodec_delete(H)
I AMediaCodec_configure(HHHHI)
I AMediaCodec_start(H)
I AMediaCodec_stop(H)
I AMediaCodec_flush(H)
I AMediaCodec_dequeueInputBuffer(HJ)
! P AMediaCodec_getInputBuffer(HIP)
! I AMediaCodec_queueInputBuffer(HIIIJI)
! I AMediaCodec_dequeueOutputBuffer(HPJ)
! P AMediaCodec_getOutputBuffer(HIP)
H AMediaCodec_getOutputFormat(H)
I AMediaCodec_releaseOutputBuffer(HII)
I AMediaCodec_releaseOutputBufferAtTime(HIJ)
H AMediaExtractor_new()
I AMediaExtractor_delete(H)
I AMediaExtractor_setDataSource(HP)
I AMediaExtractor_setDataSourceFd(HIJJ)
I AMediaExtractor_getTrackCount(H)
H AMediaExtractor_getTrackFormat(HI)
I AMediaExtractor_selectTrack(HI)
I AMediaExtractor_unselectTrack(HI)
I AMediaExtractor_readSampleData(HPI)
I AMediaExtractor_getSampleFlags(H)
I AMediaExtractor_getSampleTrackIndex(H)
J AMediaExtractor_getSampleTime(H)
I AMediaExtractor_advance(H)
I AMediaExtractor_seekTo(HJI)
@@ -0,0 +1,290 @@
/* A fake libmediandk for the harness: formats, a "decoder" that XORs its input into its output buffers and an
* extractor that writes a known pattern. Everything it hands out lies above 4 GiB, like a real codec's buffers in
* the host's heap, so the guest only ever sees the bridge's mirrors and copies. */
#define _GNU_SOURCE
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaExtractor.h>
#include <media/NdkMediaFormat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include "fakemedia.h"
struct fake_media_state fake_media;
/* bump allocator in a mapping well above 4 GiB */
static void *hi_alloc(size_t n)
{
static uint8_t *base, *cur, *end;
if (!base) {
base = mmap((void *)0x6000000000ull, 16 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED) abort();
cur = base, end = base + (16 << 20);
}
n = (n + 15) & ~(size_t)15;
if (cur + n > end) abort();
void *p = cur;
cur += n;
memset(p, 0, n);
return p;
}
/* ---------------------------------------------------------------- AMediaFormat */
enum { T_I32, T_I64, T_FLOAT, T_STR, T_BUF };
struct AMediaFormat {
struct {
char name[32];
int type;
int64_t i;
float f;
char *s;
void *b;
size_t n;
} e[16];
int n;
char *text;
};
static AMediaFormat *fmt_new(void) { return hi_alloc(sizeof(AMediaFormat)); }
static int fmt_slot(AMediaFormat *f, const char *name, int type, int create)
{
for (int k = 0; k < f->n; k++)
if (!strcmp(f->e[k].name, name)) return f->e[k].type == type || create ? k : -1;
if (!create || f->n == 16) return -1;
snprintf(f->e[f->n].name, sizeof f->e[0].name, "%s", name);
f->e[f->n].type = type;
return f->n++;
}
AMediaFormat *AMediaFormat_new(void) { return fmt_new(); }
media_status_t AMediaFormat_delete(AMediaFormat *f)
{
fake_media.formats_deleted++;
return AMEDIA_OK;
}
const char *AMediaFormat_toString(AMediaFormat *f)
{
f->text = hi_alloc(512);
int o = 0;
for (int k = 0; k < f->n && o < 400; k++)
o += snprintf(f->text + o, 512 - (size_t)o, "%s%s", k ? ", " : "",
f->e[k].type == T_STR ? f->e[k].s : f->e[k].name);
return f->text;
}
void AMediaFormat_setInt32(AMediaFormat *f, const char *name, int32_t v)
{
int k = fmt_slot(f, name, T_I32, 1);
if (k >= 0) f->e[k].type = T_I32, f->e[k].i = v;
}
void AMediaFormat_setInt64(AMediaFormat *f, const char *name, int64_t v)
{
int k = fmt_slot(f, name, T_I64, 1);
if (k >= 0) f->e[k].type = T_I64, f->e[k].i = v;
}
void AMediaFormat_setFloat(AMediaFormat *f, const char *name, float v)
{
int k = fmt_slot(f, name, T_FLOAT, 1);
if (k >= 0) f->e[k].type = T_FLOAT, f->e[k].f = v;
}
void AMediaFormat_setString(AMediaFormat *f, const char *name, const char *v)
{
int k = fmt_slot(f, name, T_STR, 1);
if (k < 0) return;
f->e[k].type = T_STR;
f->e[k].s = hi_alloc(strlen(v) + 1);
strcpy(f->e[k].s, v);
}
void AMediaFormat_setBuffer(AMediaFormat *f, const char *name, const void *data, size_t n)
{
int k = fmt_slot(f, name, T_BUF, 1);
if (k < 0) return;
f->e[k].type = T_BUF;
f->e[k].b = hi_alloc(n);
memcpy(f->e[k].b, data, n);
f->e[k].n = n;
}
bool AMediaFormat_getInt32(AMediaFormat *f, const char *name, int32_t *out)
{
int k = fmt_slot(f, name, T_I32, 0);
if (k >= 0) *out = (int32_t)f->e[k].i;
return k >= 0;
}
bool AMediaFormat_getInt64(AMediaFormat *f, const char *name, int64_t *out)
{
int k = fmt_slot(f, name, T_I64, 0);
if (k >= 0) *out = f->e[k].i;
return k >= 0;
}
bool AMediaFormat_getFloat(AMediaFormat *f, const char *name, float *out)
{
int k = fmt_slot(f, name, T_FLOAT, 0);
if (k >= 0) *out = f->e[k].f;
return k >= 0;
}
bool AMediaFormat_getString(AMediaFormat *f, const char *name, const char **out)
{
int k = fmt_slot(f, name, T_STR, 0);
if (k >= 0) *out = f->e[k].s;
return k >= 0;
}
bool AMediaFormat_getBuffer(AMediaFormat *f, const char *name, void **data, size_t *n)
{
int k = fmt_slot(f, name, T_BUF, 0);
if (k >= 0) *data = f->e[k].b, *n = f->e[k].n;
return k >= 0;
}
/* ---------------------------------------------------------------- AMediaCodec */
struct AMediaCodec {
uint8_t *in[FAKE_NBUF], *out[FAKE_NBUF];
int in_busy[FAKE_NBUF];
struct {
int pending;
AMediaCodecBufferInfo info;
} outq[FAKE_NBUF];
};
AMediaCodec *AMediaCodec_createDecoderByType(const char *mime)
{
if (strcmp(mime, "video/avc")) return NULL;
AMediaCodec *c = hi_alloc(sizeof *c);
for (int k = 0; k < FAKE_NBUF; k++) {
c->in[k] = hi_alloc(FAKE_BUFSZ);
c->out[k] = hi_alloc(FAKE_BUFSZ);
}
fake_media.buffers_high = (uintptr_t)c->in[0] >= 0x100000000ull;
return c;
}
media_status_t AMediaCodec_configure(AMediaCodec *c, const AMediaFormat *f, ANativeWindow *w, AMediaCrypto *crypto,
uint32_t flags)
{
int32_t width = 0;
fake_media.configured = f && AMediaFormat_getInt32((AMediaFormat *)f, "width", &width) && width == 640 && !w &&
!crypto;
return AMEDIA_OK;
}
media_status_t AMediaCodec_start(AMediaCodec *c) { return AMEDIA_OK; }
media_status_t AMediaCodec_delete(AMediaCodec *c)
{
fake_media.codecs_deleted++;
return AMEDIA_OK;
}
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec *c, int64_t timeout)
{
fake_media.last_timeout = timeout;
for (int k = 0; k < FAKE_NBUF; k++)
if (!c->in_busy[k]) {
c->in_busy[k] = 1;
return k;
}
return AMEDIACODEC_INFO_TRY_AGAIN_LATER;
}
uint8_t *AMediaCodec_getInputBuffer(AMediaCodec *c, size_t idx, size_t *size)
{
if (idx >= FAKE_NBUF) return NULL;
*size = FAKE_BUFSZ;
return c->in[idx];
}
/* the queued bytes must be what the guest wrote: pattern 1 (the guest's own) or 2 (the fake extractor's) */
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec *c, size_t idx, off_t off, size_t size, uint64_t time,
uint32_t flags)
{
if (idx >= FAKE_NBUF || off < 0 || (size_t)off + size > FAKE_BUFSZ) return AMEDIA_ERROR_INVALID_PARAMETER;
int p1 = 1, p2 = 1;
for (size_t k = 0; k < size; k++) {
p1 &= c->in[idx][off + k] == FAKE_PATTERN1(k);
p2 &= c->in[idx][off + k] == FAKE_PATTERN2(k);
}
if (size && p1) fake_media.queued_guest_data++;
if (size && p2) fake_media.queued_extractor_data++;
c->in_busy[idx] = 0;
for (size_t k = 0; k < size; k++) c->out[idx][8 + k] = c->in[idx][off + k] ^ 0x5a;
c->outq[idx].pending = 1;
c->outq[idx].info.offset = 8;
c->outq[idx].info.size = (int32_t)size;
c->outq[idx].info.presentationTimeUs = (int64_t)time;
c->outq[idx].info.flags = flags;
return AMEDIA_OK;
}
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec *c, AMediaCodecBufferInfo *info, int64_t timeout)
{
for (int k = 0; k < FAKE_NBUF; k++)
if (c->outq[k].pending) {
c->outq[k].pending = 0;
*info = c->outq[k].info;
return k;
}
return AMEDIACODEC_INFO_TRY_AGAIN_LATER;
}
uint8_t *AMediaCodec_getOutputBuffer(AMediaCodec *c, size_t idx, size_t *size)
{
if (idx >= FAKE_NBUF) return NULL;
*size = FAKE_BUFSZ;
return c->out[idx];
}
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec *c, size_t idx, bool render)
{
return idx < FAKE_NBUF ? AMEDIA_OK : AMEDIA_ERROR_INVALID_PARAMETER;
}
AMediaFormat *AMediaCodec_getOutputFormat(AMediaCodec *c)
{
AMediaFormat *f = fmt_new();
AMediaFormat_setInt32(f, "width", 1920);
return f;
}
/* ---------------------------------------------------------------- AMediaExtractor */
struct AMediaExtractor {
int unused;
};
AMediaExtractor *AMediaExtractor_new(void) { return hi_alloc(sizeof(AMediaExtractor)); }
media_status_t AMediaExtractor_delete(AMediaExtractor *e) { return AMEDIA_OK; }
media_status_t AMediaExtractor_setDataSourceFd(AMediaExtractor *e, int fd, off64_t offset, off64_t length)
{
fake_media.fd = fd, fake_media.fd_offset = offset, fake_media.fd_length = length;
return AMEDIA_OK;
}
media_status_t AMediaExtractor_seekTo(AMediaExtractor *e, int64_t pos, SeekMode mode)
{
fake_media.seek_pos = pos, fake_media.seek_mode = (int)mode;
return AMEDIA_OK;
}
int64_t AMediaExtractor_getSampleTime(AMediaExtractor *e) { return 0x123456789ll; }
ssize_t AMediaExtractor_readSampleData(AMediaExtractor *e, uint8_t *buf, size_t cap)
{
if (cap < 100) return -1;
for (int k = 0; k < 100; k++) buf[k] = FAKE_PATTERN2(k);
return 100;
}
@@ -0,0 +1,25 @@
/* The harness's fake libmediandk (fakemedia.c) and the guest test (libbtest4.c) share these. */
#ifndef FAKEMEDIA_H
#define FAKEMEDIA_H
#include <stdint.h>
#define FAKE_NBUF 4
#define FAKE_BUFSZ 8192
#define FAKE_PATTERN1(k) ((uint8_t)((k) * 13 + 1))
#define FAKE_PATTERN2(k) ((uint8_t)((k) * 7 + 3))
struct fake_media_state {
int buffers_high; /* the codec's buffers lie above 4 GiB */
int configured; /* configure got the guest's format (width 640) and no window/crypto */
int queued_guest_data; /* queueInputBuffer calls that carried the guest's bytes */
int queued_extractor_data; /* ... the extractor's bytes (readSampleData into the guest's buffer) */
int formats_deleted, codecs_deleted;
int64_t last_timeout;
int fd;
int64_t fd_offset, fd_length, seek_pos;
int seek_mode;
};
extern struct fake_media_state fake_media;
#endif
@@ -13,6 +13,8 @@
#include <sys/mman.h>
#include "a32bridge.h"
#include "fakejni.h"
#include "fakemedia.h"
#include <media/NdkMediaExtractor.h>
static int failures;
@@ -274,6 +276,29 @@ static void test_cxx(const char *dir, const char *name)
CHECK(r == 303, "%s: C++ exceptions and static constructors in a guest library (%d)", name, r);
}
static void test_media(const char *dir)
{
char stub[512];
snprintf(stub, sizeof stub, "%s/libbtest4.so", dir);
uint32_t h = a32b_load(stub);
uint32_t fn = h ? a32b_sym(h, "media_test") : 0;
int r = fn ? (int)a32b_call(fn, NULL, 0) : -1;
CHECK(r >= 0 && (r & 1), "libmediandk: every function Unity's video player looks up resolves (0x%x)", r);
CHECK(r >= 0 && (r & 64), "libmediandk data symbols: every AMEDIAFORMAT_KEY_* Unity looks up, with AOSP's value");
CHECK(r >= 0 && (r & 2), "AMediaFormat: values both ways, host-owned string, buffer and toString copied in");
CHECK(fake_media.buffers_high, "the fake codec's buffers lie above 4 GiB");
CHECK(r >= 0 && (r & 4) && fake_media.configured && fake_media.queued_guest_data == 1,
"AMediaCodec input buffer: the guest's writes reach the codec at queueInputBuffer");
CHECK(r >= 0 && (r & 8), "AMediaCodec output buffer: buffer info and the codec's bytes reach the guest");
CHECK(r >= 0 && (r & 16) && fake_media.queued_extractor_data == 1 && fake_media.last_timeout == -1,
"AMediaExtractor_readSampleData into a mirrored input buffer, queued again (same buffer reused)");
CHECK(fake_media.fd == 5 && fake_media.fd_offset == 1ll << 33 && fake_media.fd_length == 77 &&
fake_media.seek_pos == 5000000000ll && fake_media.seek_mode == AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC,
"64-bit arguments: setDataSourceFd, seekTo");
CHECK(r >= 0 && (r & 32) && fake_media.formats_deleted == 2 && fake_media.codecs_deleted == 1,
"getOutputFormat, AMediaFormat_delete, AMediaCodec_delete");
}
int main(int argc, char **argv)
{
if (argc < 2) {
@@ -290,6 +315,7 @@ int main(int argc, char **argv)
test_cxx(argv[1], "libbtest3");
test_cxx(argv[1], "libbtest3_android");
test_cxx(argv[1], "libbtest3_standard");
test_media(argv[1]);
CHECK(mprotect(taken, 4 << 20, PROT_NONE) == 0, "the host's low mapping is still there");
if (failures) {
printf("%d FAILED\n", failures);
@@ -0,0 +1,135 @@
/* Guest test library (armv7): NDK media the way Unity's video player uses it (dlopen libmediandk.so, dlsym every
* function, extractor -> decoder input buffers, output buffers read back). The host side is test/fakemedia.c. */
#include <dlfcn.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaExtractor.h>
#include <media/NdkMediaFormat.h>
#include <string.h>
#include "fakemedia.h"
/* the functions Unity 2017.4's libunity.so looks up: all must resolve, or Unity drops NDK video playback */
static const char *const unity_names[] = {
"AMediaCodec_configure", "AMediaCodec_createCodecByName", "AMediaCodec_createDecoderByType",
"AMediaCodec_createEncoderByType", "AMediaCodec_delete", "AMediaCodec_dequeueInputBuffer",
"AMediaCodec_dequeueOutputBuffer", "AMediaCodec_flush", "AMediaCodec_getInputBuffer",
"AMediaCodec_getOutputBuffer", "AMediaCodec_getOutputFormat", "AMediaCodec_queueInputBuffer",
"AMediaCodec_releaseOutputBuffer", "AMediaCodec_releaseOutputBufferAtTime", "AMediaCodec_start",
"AMediaCodec_stop", "AMediaExtractor_advance", "AMediaExtractor_delete", "AMediaExtractor_getSampleFlags",
"AMediaExtractor_getSampleTime", "AMediaExtractor_getSampleTrackIndex", "AMediaExtractor_getTrackCount",
"AMediaExtractor_getTrackFormat", "AMediaExtractor_new", "AMediaExtractor_readSampleData",
"AMediaExtractor_seekTo", "AMediaExtractor_selectTrack", "AMediaExtractor_setDataSource",
"AMediaExtractor_setDataSourceFd", "AMediaExtractor_unselectTrack", "AMediaFormat_delete",
"AMediaFormat_getBuffer", "AMediaFormat_getFloat", "AMediaFormat_getInt32", "AMediaFormat_getInt64",
"AMediaFormat_getString", "AMediaFormat_new", "AMediaFormat_setBuffer", "AMediaFormat_setFloat",
"AMediaFormat_setInt32", "AMediaFormat_setInt64", "AMediaFormat_setString", "AMediaFormat_toString",
};
/* ... and the data symbols (const char *AMEDIAFORMAT_KEY_*) it looks up the same way */
static const char *const unity_keys[][2] = {
{"AMEDIAFORMAT_KEY_AAC_PROFILE", "aac-profile"}, {"AMEDIAFORMAT_KEY_BIT_RATE", "bitrate"},
{"AMEDIAFORMAT_KEY_CHANNEL_COUNT", "channel-count"}, {"AMEDIAFORMAT_KEY_CHANNEL_MASK", "channel-mask"},
{"AMEDIAFORMAT_KEY_COLOR_FORMAT", "color-format"}, {"AMEDIAFORMAT_KEY_DURATION", "durationUs"},
{"AMEDIAFORMAT_KEY_FLAC_COMPRESSION_LEVEL", "flac-compression-level"},
{"AMEDIAFORMAT_KEY_FRAME_RATE", "frame-rate"}, {"AMEDIAFORMAT_KEY_HEIGHT", "height"},
{"AMEDIAFORMAT_KEY_I_FRAME_INTERVAL", "i-frame-interval"}, {"AMEDIAFORMAT_KEY_IS_ADTS", "is-adts"},
{"AMEDIAFORMAT_KEY_IS_AUTOSELECT", "is-autoselect"}, {"AMEDIAFORMAT_KEY_IS_DEFAULT", "is-default"},
{"AMEDIAFORMAT_KEY_IS_FORCED_SUBTITLE", "is-forced-subtitle"}, {"AMEDIAFORMAT_KEY_LANGUAGE", "language"},
{"AMEDIAFORMAT_KEY_MAX_HEIGHT", "max-height"}, {"AMEDIAFORMAT_KEY_MAX_INPUT_SIZE", "max-input-size"},
{"AMEDIAFORMAT_KEY_MAX_WIDTH", "max-width"}, {"AMEDIAFORMAT_KEY_MIME", "mime"},
{"AMEDIAFORMAT_KEY_PUSH_BLANK_BUFFERS_ON_STOP", "push-blank-buffers-on-shutdown"},
{"AMEDIAFORMAT_KEY_REPEAT_PREVIOUS_FRAME_AFTER", "repeat-previous-frame-after"},
{"AMEDIAFORMAT_KEY_ROTATION", "rotation-degrees"}, {"AMEDIAFORMAT_KEY_SAMPLE_RATE", "sample-rate"},
{"AMEDIAFORMAT_KEY_STRIDE", "stride"}, {"AMEDIAFORMAT_KEY_WIDTH", "width"},
};
static void *lib;
/* the key variable's string ("" if the symbol is missing) */
static const char *key(const char *sym)
{
const char *const *p = dlsym(lib, sym);
return p && *p ? *p : "";
}
#define FN(name) ((__typeof__(&name))dlsym(lib, #name))
int media_test(void)
{
int bits = 0;
lib = dlopen("libmediandk.so", RTLD_NOW);
if (!lib) return -1;
int all = 1;
for (unsigned k = 0; k < sizeof unity_names / sizeof *unity_names; k++) all &= dlsym(lib, unity_names[k]) != NULL;
bits |= all;
int keys = 1;
for (unsigned k = 0; k < sizeof unity_keys / sizeof *unity_keys; k++)
keys &= !strcmp(key(unity_keys[k][0]), unity_keys[k][1]);
if (keys) bits |= 64;
/* formats: values both ways, a string and a buffer the host owns */
AMediaFormat *fmt = FN(AMediaFormat_new)();
static const uint8_t csd[5] = {0, 0, 0, 1, 0x67};
FN(AMediaFormat_setString)(fmt, key("AMEDIAFORMAT_KEY_MIME"), "video/avc"); /* names from the key symbols, as Unity does */
FN(AMediaFormat_setInt32)(fmt, key("AMEDIAFORMAT_KEY_WIDTH"), 640);
FN(AMediaFormat_setInt64)(fmt, "durationUs", 1ll << 40);
FN(AMediaFormat_setFloat)(fmt, "frame-rate", 29.5f);
FN(AMediaFormat_setBuffer)(fmt, "csd-0", csd, sizeof csd);
const char *mime = NULL;
void *data = NULL;
size_t n = 0;
int32_t w = 0;
int64_t d = 0;
float fr = 0;
const char *text = FN(AMediaFormat_toString)(fmt);
if (fmt && FN(AMediaFormat_getString)(fmt, "mime", &mime) && mime && !strcmp(mime, "video/avc") &&
FN(AMediaFormat_getBuffer)(fmt, "csd-0", &data, &n) && n == sizeof csd && !memcmp(data, csd, n) &&
FN(AMediaFormat_getInt32)(fmt, "width", &w) && w == 640 && FN(AMediaFormat_getInt64)(fmt, "durationUs", &d) &&
d == 1ll << 40 && FN(AMediaFormat_getFloat)(fmt, "frame-rate", &fr) && fr == 29.5f && text &&
strstr(text, "video/avc") && !FN(AMediaFormat_getString)(fmt, "missing", &mime))
bits |= 2;
/* decoder input: the guest writes its buffer, the codec gets the bytes */
AMediaCodec *codec = FN(AMediaCodec_createDecoderByType)("video/avc");
size_t cap = 0;
if (codec && FN(AMediaCodec_configure)(codec, fmt, NULL, NULL, 0) == AMEDIA_OK &&
FN(AMediaCodec_start)(codec) == AMEDIA_OK) {
ssize_t i = FN(AMediaCodec_dequeueInputBuffer)(codec, 10000);
uint8_t *buf = i >= 0 ? FN(AMediaCodec_getInputBuffer)(codec, (size_t)i, &cap) : NULL;
if (buf && cap == FAKE_BUFSZ) {
for (int k = 0; k < 1000; k++) buf[16 + k] = FAKE_PATTERN1(k);
if (FN(AMediaCodec_queueInputBuffer)(codec, (size_t)i, 16, 1000, 0x100000001ull, 4) == AMEDIA_OK)
bits |= 4;
}
}
/* decoder output: the buffer info and the bytes the codec wrote */
AMediaCodecBufferInfo info;
memset(&info, 0, sizeof info);
ssize_t o = codec ? FN(AMediaCodec_dequeueOutputBuffer)(codec, &info, 10000) : -1;
uint8_t *out = o >= 0 ? FN(AMediaCodec_getOutputBuffer)(codec, (size_t)o, &cap) : NULL;
if (out && cap == FAKE_BUFSZ && info.offset == 8 && info.size == 1000 && info.presentationTimeUs == 0x100000001ll &&
info.flags == 4) {
int same = 1;
for (int k = 0; k < 1000; k++) same &= out[8 + k] == (FAKE_PATTERN1(k) ^ 0x5a);
if (same && FN(AMediaCodec_releaseOutputBuffer)(codec, (size_t)o, 0) == AMEDIA_OK) bits |= 8;
}
/* extractor: 64-bit arguments, readSampleData straight into an input buffer, queued again */
AMediaExtractor *ex = FN(AMediaExtractor_new)();
if (ex && FN(AMediaExtractor_setDataSourceFd)(ex, 5, 1ll << 33, 77) == AMEDIA_OK &&
FN(AMediaExtractor_seekTo)(ex, 5000000000ll, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC) == AMEDIA_OK &&
FN(AMediaExtractor_getSampleTime)(ex) == 0x123456789ll && codec) {
ssize_t i = FN(AMediaCodec_dequeueInputBuffer)(codec, -1);
uint8_t *buf = i >= 0 ? FN(AMediaCodec_getInputBuffer)(codec, (size_t)i, &cap) : NULL;
ssize_t got = buf ? FN(AMediaExtractor_readSampleData)(ex, buf, cap) : -1;
if (got == 100 && FN(AMediaCodec_queueInputBuffer)(codec, (size_t)i, 0, (size_t)got, 0, 0) == AMEDIA_OK)
bits |= 16;
FN(AMediaExtractor_delete)(ex);
}
/* formats the codec makes, deletes */
AMediaFormat *of = codec ? FN(AMediaCodec_getOutputFormat)(codec) : NULL;
if (of && FN(AMediaFormat_getInt32)(of, "width", &w) && w == 1920 && FN(AMediaFormat_delete)(of) == AMEDIA_OK &&
FN(AMediaFormat_delete)(fmt) == AMEDIA_OK && FN(AMediaCodec_delete)(codec) == AMEDIA_OK)
bits |= 32;
return bits;
}
@@ -141,6 +141,10 @@ int a32_vrapi_selftest(void)
tracking_reset(); test_tracked = 0;
projection.Header.Flags = 4; submit_prepare(m, &desc); VT(desc.LayerCount == 1);
projection.Header.Flags = 0; submit_prepare(m, &desc); VT(desc.LayerCount == 0);
/* OVP_HEADLESS_TEST: an untracked default pose is submitted as it is (headless snapshots) */
headless_test = 1; desc.LayerCount = 1; submit_prepare(m, &desc);
VT(desc.LayerCount == 1 && !memcmp(&projection.HeadPose, &identity.HeadPose, sizeof(pose64)));
headless_test = 0;
/* Check the real SubmitFrame2 thunk ends an empty frame with no tracked fallback. */
memset(buf, 0, 4096);
st32(g+32,1); st32(g+36,array); st32(array,layer); st32(layer,1);
+1 -1
View File
@@ -221,7 +221,7 @@ static int ab_invert_view(const XrSwapchainSubImage *sub) {
int ok = 0;
pthread_mutex_lock(&flip_lock);
tracked_swapchain *t = find_tracked(sub->swapchain, 0);
uint32_t index = t ? t->last_acquired : 0, layer = sub->imageArrayIndex;
uint32_t index = t ? shown_image(t) : 0, layer = sub->imageArrayIndex;
XrRect2Di rect = sub->imageRect;
if (!t || !t->image_count || index >= t->image_count || layer >= 32 || rect.extent.width <= 0 ||
rect.extent.height <= 0 || t->info.sampleCount > 1 ||
+3 -3
View File
@@ -220,11 +220,11 @@ static int layer_uses_standin(const XrCompositionLayerBaseHeader *layer) {
static PFN_xrUpdateSwapchainFB real_update_swapchain;
static PFN_xrGetSwapchainStateFB real_get_swapchain_state;
static XRAPI_ATTR XrResult XRAPI_CALL standin_update_swapchain(XrSwapchain swapchain, const XrSwapchainStateBaseHeaderFB *state) {
if (is_standin(swapchain)) return XR_SUCCESS;
if (is_standin(swapchain) || sw_is_fake(swapchain)) return XR_SUCCESS;
return real_update_swapchain ? real_update_swapchain(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
static XRAPI_ATTR XrResult XRAPI_CALL standin_get_swapchain_state(XrSwapchain swapchain, XrSwapchainStateBaseHeaderFB *state) {
if (is_standin(swapchain)) return XR_ERROR_VALIDATION_FAILURE; // no state to report (apps treat it as "unsupported")
if (is_standin(swapchain) || sw_is_fake(swapchain)) return XR_ERROR_VALIDATION_FAILURE; // no state to report (apps treat it as "unsupported")
return real_get_swapchain_state ? real_get_swapchain_state(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
}
@@ -316,7 +316,7 @@ static int cube_copy_face(const standin_swapchain *s, int face, uint32_t index,
PFN_vkQueueWaitIdle idle = (PFN_vkQueueWaitIdle)vk.GetDeviceProcAddr(vk.device, "vkQueueWaitIdle");
VkCommandBufferBeginInfo begin = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, NULL};
if (idle && vk.ResetCommandBuffer(vk.cmd, 0) == VK_SUCCESS && vk.BeginCommandBuffer(vk.cmd, &begin) == VK_SUCCESS) {
if (idle && vk_cmd_idle() && vk.ResetCommandBuffer(vk.cmd, 0) == VK_SUCCESS && vk.BeginCommandBuffer(vk.cmd, &begin) == VK_SUCCESS) {
VkImage dst = images[index].image;
barrier(s->image, face, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
+3 -1
View File
@@ -42,7 +42,9 @@ static XRAPI_ATTR XrResult XRAPI_CALL eye_hook_xrReleaseSwapchainImage(XrSwapcha
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (is_standin(swapchain)) return XR_SUCCESS;
if (release_wait_active()) eye_wait_for_gpu();
XrResult result = fn(swapchain, info);
int pooled = pool_find(swapchain);
XrResult result = pooled >= 0 ? pool_cycle(pooled, 2, NULL) : fn(swapchain, info);
if (XR_SUCCEEDED(result)) flip_on_release(swapchain);
if (eye_debug) {
int64_t now = monotonic_ns();
pthread_mutex_lock(&eye_lock);
+2 -5
View File
@@ -64,6 +64,7 @@ static int fade_fill_vulkan(VkImage image, float alpha) {
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, NULL};
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkClearColorValue black = {.float32 = {0.0f, 0.0f, 0.0f, alpha}};
if (!vk_cmd_idle()) return 0;
vk.ResetCommandBuffer(vk.cmd, 0);
vk.BeginCommandBuffer(vk.cmd, &begin);
barrier(image, 0, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
@@ -71,11 +72,7 @@ static int fade_fill_vulkan(VkImage image, float alpha) {
barrier(image, 0, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
vk.EndCommandBuffer(vk.cmd);
VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO, NULL, 0, NULL, NULL, 1, &vk.cmd, 0, NULL};
int ok = vk.QueueSubmit(vk.queue, 1, &submit, vk.fence) == VK_SUCCESS &&
vk.WaitForFences(vk.device, 1, &vk.fence, VK_TRUE, 100000000ull) == VK_SUCCESS;
vk.ResetFences(vk.device, 1, &vk.fence);
return ok;
return vk_cmd_submit(100000000ull) != 0;
}
static int fade_fill_gles(GLuint texture, float alpha) {
+60
View File
@@ -0,0 +1,60 @@
// FramePort FrameBridge: tells FramePort's foveation centring layer (native/foveation/centre.h, in
// libVkLayer_fdm_darken.so, which this library links on arm64) where each eye's view is (GitHub #394). Every
// xrEndFrame: the first projection layer's views as swapchain size, array layer, imageRect and the eye's optical
// centre (the straight-ahead direction from the view's FOV angles; not the rect's centre when the FOV is
// asymmetric), in the rows of the Vulkan image the density map covers (GLES through Zink: GL's bottom row is the
// image's first row). Sent only when something changed.
#include <stddef.h>
typedef struct {
uint32_t width, height, layer;
int32_t x, y, w, h;
float cx, cy;
} fdm_view_info;
// One eye's view; 0 when the swapchain or the FOV is unusable.
static int fdm_view_of(const XrCompositionLayerProjectionView *pv, int gles, fdm_view_info *o) {
uint32_t w = 0, h = 0;
if (!swapchain_size(pv->subImage.swapchain, &w, &h)) return 0;
const XrRect2Di *r = &pv->subImage.imageRect;
float l = tanf(pv->fov.angleLeft), rt = tanf(pv->fov.angleRight);
float up = tanf(pv->fov.angleUp), dn = tanf(pv->fov.angleDown);
if (!(rt - l > 1e-4f) || !(up - dn > 1e-4f) || r->extent.width <= 0 || r->extent.height <= 0) return 0;
o->width = w, o->height = h, o->layer = pv->subImage.imageArrayIndex;
o->x = r->offset.x, o->y = r->offset.y, o->w = r->extent.width, o->h = r->extent.height;
o->cx = (float)r->offset.x + (float)r->extent.width * (-l / (rt - l));
o->cy = (float)r->offset.y + (float)r->extent.height * ((gles ? -dn : up) / (up - dn));
return 1;
}
// In libVkLayer_fdm_darken.so (a DT_NEEDED of the arm64 build); NULL without it (32-bit builds).
extern void fp_fdm_set_views(const fdm_view_info *views, uint32_t count) __attribute__((weak));
static void fdm_views_end_frame(const XrFrameEndInfo *info) {
void (*set_views)(const fdm_view_info *, uint32_t) = fp_fdm_set_views;
if (!set_views || !info) return;
const XrCompositionLayerProjection *proj = NULL;
for (uint32_t i = 0; i < info->layerCount && !proj; ++i)
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION)
proj = (const XrCompositionLayerProjection *)info->layers[i];
if (!proj) return; // keep the last views (a frame of quads only doesn't move the eyes)
fdm_view_info views[4];
uint32_t n = 0;
for (uint32_t v = 0; v < proj->viewCount && n < 4; ++v)
n += (uint32_t)fdm_view_of(&proj->views[v], !vk.device, &views[n]);
static fdm_view_info last[4];
static uint32_t last_n;
// the FOV moves a little every frame: resend when a centre moved by a pixel or more
int changed = n != last_n;
for (uint32_t i = 0; i < n && !changed; ++i)
changed = memcmp(&views[i], &last[i], offsetof(fdm_view_info, cx)) || fabsf(views[i].cx - last[i].cx) >= 1.0f ||
fabsf(views[i].cy - last[i].cy) >= 1.0f;
if (!changed) return;
memcpy(last, views, sizeof(views));
last_n = n;
set_views(views, n);
static int logged;
for (uint32_t i = 0; i < n && logged < 8; ++i, ++logged)
LOG("fdm views: %ux%u layer %u rect=%d,%d %dx%d optical centre %.0f,%.0f", views[i].width, views[i].height,
views[i].layer, views[i].x, views[i].y, views[i].w, views[i].h, views[i].cx, views[i].cy);
}
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-only
// swapchain_size is the adapter's mutex-protected table, shared with rect_clamp.
static int fixed_layer_size(const XrCompositionLayerBaseHeader *layer, XrExtent2Di *full) {
if (!layer) return 0;
uint32_t w = 0, h = 0;
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)layer;
if (!p->viewCount || !p->views) return 0;
for (uint32_t v = 0; v < p->viewCount; ++v) {
uint32_t vw, vh;
if (!swapchain_size(p->views[v].subImage.swapchain, &vw, &vh)) return 0;
if (v && (vw != w || vh != h)) return 0; // no single full size for this layer
w = vw; h = vh;
}
} else if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)layer;
if (!swapchain_size(q->subImage.swapchain, &w, &h)) return 0;
} else return 0; // preserve runtime behavior for other layer types
if (!w || !h || w > INT32_MAX || h > INT32_MAX) return 0;
full->width = (int32_t)w; full->height = (int32_t)h;
return 1;
}
Loaded 100 of 282 files, more files were not shown because too many files have changed in this diff. Show more