136 KiB
FramePort — notes for Claude
FramePort ports Meta Quest standalone APKs to the Valve Steam Frame (SteamOS, aarch64). Games run in Valve's Lepton
(Waydroid-based Android container), one container per game, launched from a Steam library shortcut. Pipeline:
scan → analyze → suggest recipe (catalog/heuristics) → user confirms → overport → Frame fixes → sign → static checks → install over SSH (agent) → Steam shortcut → headless launch test + log triage.
Read docs/PLAYBOOK.md (symptom → fix) before debugging a game, and docs/FRAME_RUNTIME.md for runtime facts.
Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debugging a game).
Layout
src/frameport/— Python package.pipeline.pyis the API the CLI (cli.py) and GUI (ui/, Flet 1.0) share.ui/—app.pyshell (sidebar with Frame connection + activity cards, routing, actions, 30 s connection poll),theme.py(design tokens; change colours/spacing only there. Dark-only colour themes (owner: no light mode): built-insportal(default),portal_oled(true black) andoriginal(the old violet; aliasclassic). Portal = the logo's dichotomy: orange ACCENT = main action + selection, blue SECONDARY = secondary (outlined) buttons, progress, switch tracks, PC;dualthemes add the blue→orange sidebar edge, two-colour wordmark (C.wordmark) and selected-tab fade (C.portal_gradient). Installable theme files<data>/themes/*.json(docs/THEMES.md: name, base, dual, any subset ofT.TOKENS; refused if light/unreadable,T.ThemeError), Settings → Appearance: cards switch live viaapp.restyle()(rebuilds the shell, drops cached views; settingui.theme), "Install theme file…", "Copy this theme as a file", bin icon removes. Read tokens asT.NAMEwhen building — never as a default argument or module-level constant (they keep the import-time theme; module maps refill throughT.on_change,tests/test_theme.pychecks defaults)),glyphs.py+icons/(the logologo.svg/logo-solid.svg= a monitor that comes out of a tilted portal as the Frame, and 24 px line glyphsfp:frame|port|patch|container|test|pc|sync|live|shot|keys|recipe|link, copied into the assets dir and shown as tintedft.Images: passG.FRAME… wherever an icon goes,C.as_icon/ the button helpers handle both kinds;src/assets/icon.png= the bundles' app icon),components.py(pill, card, callout, checklist, art_fill, confirm,update()= safe update: in Flet 1.0 reading.pageof an unmounted control raises),jobs.py(background FIFO job queue, one at a time, cancel via Reporter; no Flet),views/(library: search/filters/tags/sort as pure tested helpers; game: hero + one-click install, patches under "Customize"; frame: device + readiness + installed, or connect wizard; files: the Frame's file manager (persistent like the library; locations = Videos/Downloads/Documents from agentstorage_targets, each installed Quest game's storage, the home folder; SFTP viainstall/files.pylist_dir/upload/download/make_dir/rename/delete, all confined to the location byfiles.inside; Lepton's links in a game's storage can't be renamed/deleted; replaced the "Send files" dialog, game menu → "Add videos & files…" =go("files", pkg); multi-select bar (download/ delete); drag-and-drop from the OS via theflet-dropzoneextension (Apache-2.0, Flutterdesktop_drop), which only aflet buildbundle contains:files.dropzone_available()keeps it out of source runs and the PyInstaller fallback, which would show an unknown control); screenshots (Steam screenshots on the Frame,install/screenshots.py- agent v46
list_screenshots/delete_screenshots: the Frame files every headset shot under SteamVR 250820, so launch.sh logsstart/end <unix>to<anchor>/plays.log(upgrade_launchers adds it; Proton launchers exec → start only) and shots are matched by time; thumbnails cached in<data>/screenshots-cache/<frame>/, shown by asset URL; delete leaves screenshots.vdf alone (Steam rewrites it at exit); game menu → "Screenshots" =go("screenshots", pkg); Copy image (card hover button, right-click, viewer) =core/clipboard.copy_image(Windows/WSL: PowerShell bitmap + file via %TEMP%, macOS osascript PNG, Linux wl-copy/xclip; Flet's set_image only in web mode)); live view (views/live.py+install/livestream.py: the Frame's built-in SteamVR "headset view" webcam (steamvr-v4l2cam.service→ v4l2loopback "SteamVR" /dev/video99, see docs/FRAME_RUNTIME.md) + the default output's pulse monitor (sound) →fp_venc(native/venc, own clean-room V4L2 driver of the Frame's iris hardware encoder: RGB24→NV12 box downscale with NEON, H.264 CBR, one frame per slot of an even fraction of the panel rate read from DRM, e.g. 96 Hz → 32 fps; stdink= keyframe, EOF = stop;--probe/--selftest; artifactlinux-arm64-bin/, synced to~/.local/share/frameport/binby sha256 at stream start, no agent change) piped into ffmpeg (-c:v copy+ AAC), else the old ffmpeg/x264 30 fps path (qualities scale down only; "full" = SteamVR's size; stderrlive: encoder=… fps=…→ status line) → fragmented MP4 on an SSH exec channel's stdout (stdin EOF stops it) → relay on 127.0.0.1 (a new viewer asks the hardware encoder for a keyframe and waits for it, else gets init + fragments since the last keyframe of the video track) → player pageinstall/live_player.py(MSE; starts muted as browsers require, "Sound on" button; 0.3 s cushion, catches up at 1.1x, seeks only when >2 s behind: seeking to the very edge starved it) opened in the user's default browser: Flet can't show video outsideflet buildbundles; the stream outlives the tab and stops on disconnect/window close); monitor (views/monitor.py+frame/monitor.py+ agent v62_monitor: one JSON sample per tick over an SSH exec channel while the tab is shown (stopped in go/disconnect/on_close, the agent ends at EOF); sources and costs in docs/FRAME_RUNTIME.md "Monitoring sources"; game card (fps from FrameBridge pacing; End game = Steam's Exit game, then cmd_stop), tiles withC.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)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus (oneft.ContextMenuper view, filled on right-click; on one of several selected items they act on the whole selection,C.menu_targets) and click-and-drag multi-select (C.DragSelect: pan start/end on the area + item hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band). Selection bars sit below the list: above it, their appearing shifted the items mid-drag. Async handlers (they await a FilePicker) must be coroutine functions or go throughpage.run_task: Flet doesn't await a lambda's coroutine (the Files row Download button silently did nothing).ui_smoke.py --fake-frame --gesturesdrives real mouse drags/right-clicks. Live Frame data comes from one app-ownedapp.monitor_hub(frame/monitor_hub.py): subscribers name modules + interval, the hub runs one agent_monitorstream (union of modules, fastest interval; per-module collection needs agent v73), reconnects after a lost stream and stops it when nobody subscribes;_pollretries an "error". Games in a sample (agent v81): FramePort's own (launch.sh / container / Proton folder), any other game Steam's reaper runs (steam:<appid>, title from the appmanifest, e.g. Job Simulator from Steam's store; not SteamVR, not devkit entries; End game = Steam's Exit game), overlay apps flaggedoverlayand listed last; newest first. The UI takesframe/monitor.main_game(sample)(never an overlay app alone). The agent's own workers never count. Subscribers: "monitor" (the Monitor tab, everything, only while shown) and "card" = the sidebar's live Frame card (ui/frame_card.pyhelpers: battery ring, "now playing" row with fps + 2-min sparkline → click opens Monitor; games + battery every 5 s, paused while a job's stage is "Upload…", only while connected and settingui.live_frame_card(Settings → Appearance, default on) is on; else the battery comes from the 30 s poll). ui_smoke injectsFakeMonitorSessionas the hub'ssession_factory. User tags live in library entries (tags), filters in library settingui.library. Easter eggs (ui/easter.py, owner's wish; never mentioned in the UI or docs): seven quick clicks on the sidebar logo (the wordmark's portal glows + rings, a cover is tossed out of it and falls across the window on a random arc:toss_path,page.overlayat the computed window position_wordmark_portal; the logo's portal in themes without one), the Monitor's fps number has a mood tooltip (above the number, clear of the pointer) and 2.5 minutes within 0.5 fps of target (PERFECT_SECONDS) make the game card glow and pulse blue/orange for 5 s, the number grow in the portal gradient and the sparkline take it; the "Perfect pacing" pill + line stay until the rate slips (PerfectPacing.done,_show_perfect,Sparkline.portal), holiday badges on the logo that dance on hover and shower the window on click (HOLIDAY_MOTION;SHOWERS/shower_plan: New Year confetti, winter snow, Valentine hearts, Pi digits, clovers, hopping eggs, bouncing pumpkins;holiday: Valentine's heart, Pi Day, St Patrick's clover, Easter Sat-Mon egg (computus), April 1 = the logo upside down until hovered, Halloween pumpkin, Dec 31-Jan 1 party popper, else Dec 20-Jan 2 snowflake; on those days the logo's box grows to hold the badge: Flutter only hit-tests inside a control's bounds;FRAMEPORT_TODAYpretends a date), Library search "frameport" (logo spins, "That's me!"), Type on Frame "hello" (keys still go to the Frame; a headset peeks up from the window's corner and waves), the battery ring (only the ring, with a glow behind it; not the percentage) empties at once and spins while its bar refills, then pulses, on reaching 100 % plugged (charged_now,ring_pulse,ring_glow), a long upload (>2.5 min,HOP_AFTER) makes the transit cover hop, the Live view tab shows a blinking ON AIR sign while streaming (app.sync_on_air), install milestones (confetti + message at the 1st/10th/100th different game: settingsfun.installed= game ids,fun.milestones= shown; each milestone shows once ever, recorded before it's shown; updates/reinstalls never count; the first use seeds from games already on a Frame and marks passed milestones shown). Settingui.easter_eggs(no UI) turns them off; the showcase demo library has them off (steps hookeaster_eggsfor egg demos). Reduce motion: no animation. Performance rules (the app froze before): never put image bytes in controls — artwork is served by URL from the GUI assets dir (= user data dir;ft.run(assets_dir=…)), as thumbnails (artwork/thumbs.py, Pillow); the library view is persistent, streams cards in batches from a background thread and filters by toggling visibility; job/connection events callapp.refresh_view()(targeted), notrender(); no I/O in render paths. Never recreate clickable controls on progress ticks (sidebar, activity tiles): update their properties — replacing them 5×/s swallowed clicks (couldn't leave the Library during an upload). Labelled switches:C.switch(label, …)(Material's default label colour is dark on our dark theme). Cover cards (Library grid + shelf):library.card_hover/hover_motion(lift, art zoom inside the clipped frame, radialscrim, accent-tinted shadow) andC.CoverButton(frosted round button, accent under the pointer; Play = blue then orange ring ripple + "Starting on Frame…" pill for STARTING_S; its.statuslayer must sit below the button in the Stack, else it takes the clicks); the game page's Play turns into "Starting on …" with a spinner (GameView._starting). Reduce motion: no lift/zoom/rings. Filled buttons use a glyph's-solidvariant (glyphs.solid, e.g.frame-solid.svg: the outline glyph faded into the orange). Empty states (C.empty_state(..., features=[(icon, heading, sentence)])): with features the portal is bigger, a soft portal light (C.portal_glow) fills the screen andC.feature_rowsays what the screen will show (the not-connected Files/Screenshots/Live/Type/Monitor tabs and the empty Library); the idle Live view is a framed "screen" with its own Start button +live_features(), Type on Frame showskeyboard_tips()under the box. Setup checklists (Frame → Ready to play, Settings → Tools / This PC) =C.checklist([C.Check(ok, title, detail, help, fix=(label, icon, handler), extra=control)]): ok True/False/"warn"/None (= spinner); the fix button shows inline only while an item isn't ready — wire existing actions only. Motion:app.bodyholds anft.AnimatedSwitcher(app._show_view): a new route gets a new wrapper (150 ms fade), a same-route redraw swaps the wrapper's content (no animation; persistent views just remount); the wordmark's portal (wordmark().data) pulses once when frame_state turns "connected"; both off with settingui.reduce_motion(Settings → Appearance "Reduce motion",app.reduce_motion). Settings has a fixed index column (settings.SECTIONSorder: most users' needs first (Updates, which also holds game recipe + install-link updates, Installing, Library folders, Appearance), niche ones later (Install links, Tools, This PC), Remove FramePort last): entries scroll the section column viascroll_to(scroll_key=…)(sections wrapped in Containers withft.ScrollKey), the header stays;app.settings_view.show_section(key)(ui_smokesettings-index). Help hints: wording for non-obvious terms lives inui/help.py(HELP); show it withC.help_icon(key)or thehelp=argument ofsection/kv(C.Checkhelp key), tooltips viaC.tip()(wraps). Game actions for the Library right-click menu (oneft.ContextMenuaround 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_sectionsquick=False, sectioned) come fromapp.game_actions(). 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. …".
- agent v46
- Installs: queued/cancelled/failed ones are remembered (library setting
ui.installs) → Library "Resume" bar; uploads are interruptible (Cancel checked per MiB) and resumable (big files via SFTP.partappend, small files streamed in tar batches; the agent counts files already inincoming/). Multi-select in the Library queues installs after asking every needed question up front. Failures end in one pop-up (Resume / Uninstall / log). - Library folders (GitHub discussion #351):
core/folders.py(no Flet) owns settingscan.roots(remember/add/remove/status/rescan_roots; the origin-based fallback only while the setting was never set, so removing every folder doesn't guess them back), Settings → Library folders (settings.library_folders, status read via run_bg), CLIframeport folders [--add/--remove/--json]+frameport rescan. Scans relink moved games (pipeline.ScanReport: a library game found elsewhere whose own files are gone and the copy is the same build (Quest: source_sha256, else versionName; Rift: same program path + size from the fingerprint, matched by game id or, renamed, by program) → only paths change (relink_quest/_relink_rift), user fields/builds/installs stay; another version → Scan a folder… takes it viareplace_apk(found=)/replace_rift_game, Rescan only reports it; the old copy still there → kept).add_gameno longer overwrites a user recipe (refreshessuggestedonly). Linux apps aren't scanned. PC installs of a moved Rift game (Steam shortcut path) untested. - Other drives (GitHub #90, agent v63): anchors stay in ~/Applications/quest-frame, a game's files may live in
<mount>/FramePort/<pkg>(deployment.jsonbase). Agentdrives(/proc/mounts, /run/media + Steam library drives; vfat/exfat/ntfs/read-only refused), prepare*/finalize_linuxdest(unmounted = error, never a fallback; installed games keep their base),move(detached systemd-run +move_status; cp -a under podman unshare, count+bytes check, symlink retarget, launch.sh: Quest app_dir line / Linux+PC VR rewritten from the record'slauncherfield, else text swap), list_installeddrive/drive_missing(install_state keeps such games "installed"). PC:install/drives.py, library settinginstall.drive(Frame page → Storage), game menu "Move to…" (job kind tool-frame), CLIframe drives/frame move/install --dest. Untested on the device. - PC VR repacks are pre-patched to run directly (proven: Rick and Morty, Vader Immortal run when the exe is
launched directly; Revive breaks them). So Rift games default to
as_is= install the copy unchanged and launch the exe directly (pcvr.xr_timefixfor the Frame OpenXR-1.1→1.0 fix,pcvr.no_crash_reporterfor Unreal). Revive is only suggested for games with Oculus (LibOVR) code (pcvr.revive, andpcvr.oculus_unreal; repacks with a bundled Revive and SteamVR/OpenXR games run directly): needed by an un-cracked Oculus game that fails at "Initializing OVR session". Those (Lone Echo, Robo Recall, Lies Beneath: crack .7z not extracted / Platform SDK) hit Revive's Oculus-runtime signature check under Proton-arm64 — Revive's LoadLibrary/WinVerifyTrust hooks don't install (ARM64EC), and the game's Oculus SDK shim rejects the unsigned Revive runtime (wintrust + crypt32 signer "Oculus VR") — so they don't run on the Frame without extracting the repack's crack (which FramePort doesn't do).VD.batis Virtual Desktop's launcher: ignore it except as an exe-location hint. Library migrationrift_run_directresets existing recipes. Auto launch-test is skipped on PC installs (it would start the game on the user's desktop). Launch tests collect the Unreal game log + crash summaries from the Proton prefix; triageunreal-crash. Agent v67 adds Unity's logs (LocalLow///Player(-prev).log via<Name>_Data/app.info,output_log.txt, Temp/…/Crashes/*/error.log;unity_logs) to launch tests and diagnostics; triageunity-vr-init/unity-crash. A catalog recipe verified with another build (catalogxr≠ the build's) keeps the build's ownpcvr.launch_args(engine._other_build; GitHub #105: SUPERHOT VR's Oculus+OpenVR build needs-vrmode OpenVR, the catalog's is the OpenXR build); Electron launchers next to a game rank −30 (rift.is_electron). Lies Beneath via Proton without Revive crashed (UE 4.23 "Unhandled exception"). - Rift scanning:
sources/rift_dump.scan= the scanned folder's subfolders are games (one per folder; a folder is a game if all candidate exes sit under one child), recursing into collections;analysis/rift.pywalks once, filters helpers + non-GUI PEs,rank_exes(Unreal *-Shipping beats its launcher, Steam builds −10, ambiguous →exe_confirmed=False→ GUI exe dialog),clean_title, fingerprint (unchanged folders aren't re-analyzed), modular-Unreal Oculus plugin DLLs + UTF-16 markers count as LibOVR,revive_bundled→ as-is. - Rift VR-API routing (
analysis/rift.py):openvr/openxr/libovrdetected from imports + bundled DLLs (NOT the universal-in-UnrealIVRSystem/VR_InitInternalstrings).frame_native = (openxr or openvr) and not libovr= confidently runs on the Frame via wineopenxr (SteamVR), no Revive. Any LibOVR game →needs_revive→ PC only on the Frame (Revive's ARM64EC hooks don't work under Proton-arm64, and FramePort does not defeat the Oculus runtime Authenticode signature check). Patchpcvr.libovr_redirect(Frame, default on for Revive games, agent v19set_libovr_redirect) symlinks Revive's runtime asLibOVRRT{64,32}_1.dllin the game's exe dir and next to every OVRPlugin.dll (Unreal's OVR shim searches its own module dir, not the exe dir) — the LoadLibrary redirect (pure runtime substitution), which does NOT touch the game's runtime signature check (a checking build still fails at-3021; only non-checking builds run). PoC verified on-device (2026-09-30): with the redirect, Lone Echo's Oculus SDK now loads Revive's runtime and reaches Oculus API init (-3021) instead of failing to load a runtime at all — i.e. the substitution works;-3021is the downstream signature/runtime-init stage the redirect doesn't touch. (OVRPlugin/Unreal builds use a more restrictive LibOVRRT search; the next-to-OVRPlugin placement covers the common case.) Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive), not via static detection. PC installs default to Revive's OpenVR backend (pcvr.revive_openvron) and auto-start SteamVR on Play (winhost.start_steamvr); the Frame launcher always uses/openxr. UI (game.pywhere()) states per-game where it runs; installing an Oculus game on the Frame shows a warning. Migrationrift_frame_nativere-analyzes + re-derives existing entries. - Own artwork (0.8.0): game menu → "Use your own artwork…" (also from the Find artwork dialog) =
views/art_dialog.show_custom_art_dialog: one slot per kind (portrait/landscape/hero/logo/icon), FilePicker →sources.apply_custom(Pillow check, ≥64 px, ≤40 MB, scaled to ≤3840 px, PNG if alpha/logo/icon else JPEG, replaces only that kind + its thumbnails, drops the generated cover/banner, writes.picked= "custom") /remove_custom; Steam shapes are still composed from what exists (steam.pyPREFER). - Art for Rift games:
artwork/sources.py— Quest version package (OculusDB packageName, exact name or + "Unplugged"-type suffix, never sequels) → Meta art; OculusDB square cover; Steam (exact names only); exe icon. - Store details (
artwork/details.py, entrydetails): OculusDB (description, genres, publisher, website; by Quest package or Rift match) + Steam appdetails (exact title: developer, release date, up to 6 screenshots → artworkshot_N.jpg). Meta store pages reject scraping, so Oculus exclusives have no screenshots. Genres become automatic tags. Steam shortcuts get a complete composed art set (artwork/steam.py: 600×900 / 920×430 / 1920×620 / logo / 256 icon, blurred-backdrop compositing for square-only covers; no store art (at most the APK icon) → a placeholder set: the name on a colour from the title hash + the APK icon,steam_set_for; 2D Android apps get no store lookups) and tags: how it runs, the original platform (Meta Quest / Oculus Rift), genres, user tags — merged with tags set in Steam (non-Steam shortcuts can't hold a description). - Linux apps (GitHub #31, library kind
linux,linux.<slug>; x86_64 builds run through FEX (app 3127680, no SLR: RootFS /usr/share/guestos/fex-mesa from the OS image; needsSTEAM_COMPAT_DATA_PATH, see docs/FRAME_RUNTIME.md): agent v61linux_x86_tools/pick_tool,install_proton/proton_statuskind: linux_x86,finalize_linux x86_64,installer.ensure_proton(kind=); an arm64 program wins over an x86_64 one): GUI = Add games → "Add a Linux app…" / "…folder…" (app.add_linuxjob →pipeline.add_linux_app); game pagelinux_summary(program + Change…, AppImage, OpenXR, source) instead of recipe/patches,C.missing_librariescallout (Frame deployment, else last install); no Analyze/Rebuild/recipe/share/Game settings actions, Frame only; platform "Linux" badge + library filter; Steam tags "Linux app on Frame"/"Linux";_follow_catalogskips them; local files of a lone AppImage = the file only. Desktop Mode entries (GitHub #84, agent v63): finalize_linux writesframeport-<slug>.desktopto ~/.local/share/applications (+ ~/Desktop if it exists;X-FramePort-Packagemarks ours), launch.sh withFRAMEPORT_DESKTOP=1skips 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 fielddesktop_entry(default on; patches don't apply to Linux apps) → game page switch → agentdesktop_entry. Untested on device. - Scanning: SideQuest backups (
<package>/<time>/apk|obb|data) and AXRB downloads (AXRB/<app id>/<binary id>/base.apk- OBBs/assets; its
patched//*-axrb.apkPC builds are skipped) are recognised (sources/quest_dump.py, FAQ).
- OBBs/assets; its
- Quest/Rift twins stay separate entries, shown and named in Steam "Title (Quest)"/"(Rift)" (
core/titles.py). - Rename… (game menu "Name and artwork", Library right-click; CLI
frameport rename <pkg> "<name>"|--reset):pipeline.rename_gamesetstitle+title_locked(+auto_title= the automatic name for reset; rescans, Rift store matches and links then only updateauto_title; a locked name gets no twin suffix);sync_title→ Frame agent v77rename(deployment.json title, same shortcut appid: upsert matches by Exe) + art + one Steam restart, PCPcReviveTarget.rename; an unreachable Frame →steam_name_stale(game page "Update name on Frame", next install). - Newer versions (GitHub #161): game menu "Update with a newer APK…" (
pipeline.replace_apk, outdated viabuild.source_version) and, for Linux apps / PC VR games, "Update with a newer version…" (replace_linux_app/replace_rift_gameviaadd_linux_app(replace=)/add_rift_game(replace=), CLIframeport new-version): the entry keeps package, name, art, tags, switches, user recipe (a confirmed exe if the new folder has it); a newsource_idgoes into the deployment'srecipedict at install (no agent change) →C.source_replaced= outdated (Frame +pc_outdated); such an install re-sends same-size files changed after the last install (mtime/ctime,installer.changed_since); the old Rift folder is inreplaced_dirs(rescans skip it). Untested on the device. Recipe.as_is= install unchanged (pre-patched libraries):pipeline.prepare_as_is; auto for APKs that already contain FrameBridge (frame_patched). For Rift it changes nothing (the dump is never modified; the Frame copy still gets launch fixes like the crash-reporter rename —-nocrashreportsalone doesn't stop UE 4.23) — it does not turn Revive off: a repack's patches (cracks,VD.bat= Virtual Desktop's Oculus runtime) give a LibOVR game no Oculus runtime on the Frame; Revive is that runtime (Lone Echo without it: EXITED in seconds). SteamVR builds (Rick and Morty) turn Revive off via their recipe (pcvr_remove, catalogas_is).- Play: Library hover button / right-click / game page / Frame rows → agent
launch=steam://rungameid/<appid<<32 | 0x02000000>through the Frame's Steam (in-headset session); PC: Windows Steam. Launch tests use a flask icon (SCIENCE_OUTLINED) so they aren't confused with Play. uninstall.py+frameport uninstall-app+ Settings: removes the data dir (after an optional key backup zip), PC Steam shortcuts, the WSL Revive copy, and via agentpurgeFramePort's games/files on the Frame.- Save backups (discussion #352, agent v95; docs/FRAME_RUNTIME.md "Save backups"): agent
_backup_saves/_restore_saves '<json>'stream a tar.gz oflepton-data/internal/<pkg>+external/Android/data/<pkg>(caches, IL2CPP/shader caches, FrameBridge files, videos/OBBs, files >64 MiB left out; manifestframeport-saves.jsonlast) over the exec channel, file work inpodman unshare(_saves_ns), launch lock held, refused while running; restore = untrusted input (staged + checked first), rollback copy<base>/saves-before-restore.tar.gz(undo =rollback: true). PCinstall/saves.py(<data>/save-backups/<pkg>/<ts>.tar.gz+.json, keepsaves.keep= 10), GUIui/views/saves_dialog.py(game menu section "Saves", Frame page "Back up all saves", uninstall's "back up before deleting"), CLIframeport saves backup|list|restore|undo. Agent v101: PC foldersaves.folder(Settings → Data; old folders stay listed insaves.previous_folders), automatic snapshotssaves.auto(default off; flag~/.local/share/frameport/save-snapshots/enabled, Quest launchers run_snapshot_closein the EXIT trap under the launch lock, newest 3 per game on the Frame;install/saves.sync_snapshotsimports them as "automatic" once, ledger<data>/save-snapshots-synced.json). Device-checked 2026-10-17 with BlazeRush. patches/— the unit of modularity.base.py(Patch interface, registry),overport.py(overport CLI patch ids, discovered dynamically viaoverport patches),frame/*.py(one module per Frame fix),settings.py(FrameBridge adapter keys + device files as patches). Add a patch = add a module that callsregister(...).upstream.py: upstream fixes that replace a workaround per build (a probe finds the fix in OVRPort's output → the build leaves the workaround out,build.superseded; recipes unchanged). Registered:ovrport.haptic_envelope(→adapter.haptic_fix,frame/haptic_envelope.py) andovrport.microphone_stream(→frame.ovr_microphone), both fixed in OVRPort runtime 3.4.3-aa54c3f (ovrport/app#73; haptics owner-verified with Lucky's Tale 2026-10-07).analysis/— APK/ELF inspection (detect.py),elf.py(pyelftools reads; own DT_NEEDED writer),stubgen.py(generates the ovr_* stub .so without a compiler).apk/—axml.py(binary manifest editor),workspace.py(staged zip edits),sign.py(apksigner; it aligns too).recommend/—catalog.py(known-good recipes: user > remoteFRAMEPORT_CATALOG_URL> bundled),engine.py.tools/— portable toolchain (Temurin JRE, overport jar, apksigner) downloaded dynamically into the user data dir. The overport CLI comes from the downstream fork Android-XR-Bridge/OVRPort (stablevX.Y.Zreleases,OVRPort-<ver>-stable-cli.jar; fallback ovrport/appcli-jar.zip), see "overport" below.frame/— SSH (paramiko), mDNS discovery, pairing server;install/installer.py;validate/(static, device, triage).targets/—Targetinterface;frame_lepton.py(Quest via Lepton + Rift via Proton),pc_revive.py(Rift games on this Windows/WSL PC via Revive + local Steam shortcut;core/winhost.py= Windows/WSL helpers).- Oculus Rift (PC VR):
sources/rift_dump.py,analysis/rift.py(own PE reader),patches/pcvr.py(categorypcvr, shown as patches;base.for_gameseparates Quest/PC VR patches),tools/revive.py(Revive: FRAMEPORT_REVIVE_DIR > the user's installed Revive (C:\Program Files\Revive, else registry HKLM/HKCU\Software\Revive; version = GitHub release matching the DLL build date, since Revive's version resources are stale) > portable copy unpacked from ReviveInstaller.exe in pure Python; never replaces the user's install). Library idsrift.<slug>, entries havekind: rift. parity.py— rebuild catalog games from dumps and classify every APK entry difference vs known-good builds.diag/— user feedback without tokens (docs/DIAGNOSTICS.md):redact.py(every file/issue text: IPs, hosts, home dirs, Steam ids, dump folders → placeholders),bundle.py(redacted diagnostics zip; agent v21collect_diag;frameport diag collect|inspect|report),issue.py(prefilled GitHub issue-form links, ≤7.5k chars;*_links=IssueLinks(form, plain, body): body = the Markdown the form writes (### <label>, recipe in a ```yaml block; labels checked against the templates), plain =?title=&body=link. GitHub #167: some (Flatpak) browsers opened the form with empty fields →app.open_issuecopies the body to the clipboard and offers "Form empty? Open a plain issue"; keep the templates valid YAML, quote:in descriptions). "Share working config" →working-config.ymlissue → maintainer labelcatalog-accepted(a plain issue needsworking-configadded by hand) →catalog-from-issue.ymlworkflow (scripts/catalog_from_issue.pyvalidates) opens a catalog PR. App log:core/applog.py(<data>/logs/app.log, finished GUI jobs in<data>/logs/jobs/).- Announcements (2026-10-15, announcements/README.md; app only, the owner dropped the site pop-up): source of
truth
announcements/current.mdonmain(front matter id/title/date + optional image/image_alt/button+url/ expires/active, Markdown body; shipsactive: false).announcements.py(no Flet; raw GitHub viacache.cached_text, 6 h) +ui/announcer.py(pop-up 20 s after start / every 6 h when no dialog is open and not on welcome;C.dialogwith the portal edge, image downloaded + Pillow-checked into<data>/announcements/, shown by asset URL). Shown only if the user was around beforedate(settingannounce.since, set once at the GUI's first start: 0 whenUSED_BEFOREsettings exist, else now), once: any close (X, outside click, Escape, button) stores the id (announce.dismissed, last 50). New announcement = new id; same id = typo fix.FRAMEPORT_NO_ANNOUNCEMENTS;ui_smoke.py --announcementrenders it. - Self-update (docs/ARCHITECTURE.md "Self-update", docs/INSTALL.md "Updating"):
_version.py= the only version (frameport.__version__; pyproject reads it via hatchdynamic; app log, diagnostics, User-Agent, Settings use it — neverimportlib.metadata, bundles have no dist-info).updates.py(no Flet):check()= GitHub releases/latest viacached_json("app-release.json"), 6 h, skips drafts/prereleases and the user's skipped version (settingsupdate.last_check/skipped/auto_check/auto_install/cli_hint; envFRAMEPORT_NO_UPDATE_CHECK);install_kind()=bundle(psutil exe path → FramePort.exe's folder / FramePort.app / FramePort/FramePort),source(.gitcheckout:git pull --ff-only+uv sync, refused on a dirty tree),wheel(uv tool/pipx/pip 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 detachedapply.ps1/apply.sh(wait for pid → Windows: copy over, replaced files kept inupdates/<ver>/previousbecause the zip has no top folder; macOS/Linux: mv to.old+ swap, rollback,xattr -drquarantine → 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, jobapp-updatethat restarts only when no other job is active,apply_pending_at_start()inmain()for "Install updates automatically"; Settings → Updates. Changelog:updates.fetch_changelog(releases list,app-releases.json, same 6 h cache, offline → cached / []) →ChangelogEntry(whats_new()keeps only the tag message's "What's new" part: CI's intro, update hint and release-footer.md stripped);between/changelog_for(installed < v ≤ offered, dev entry only when on/choosing dev, skipped versions included),recent_history; the update/dev dialogs show one collapsible section per version (_changes_column, newest open); after an updatepending_news(settingupdate.last_seen_version; first start only records it; offline → retried next start) → Library "was updated" bar → "See what's new…"; Settings → Updates "What's new…" = recent history. CLI:--version,frameport update [--check (exit 10)] [--yes], once-a-day stderr hint read from the cache only (refresh_cache()in a daemon thread writes no settings, to avoid read-modify-write races).scripts/update_smoke.py <archive>runs the real extract + swap script (no relaunch) — CI runs it on all three OS with the archive it just built. Verified end to end (2026-10-02): real 0.3.3 bundles on Windows and Linux (Ubuntu 22.04/WSLg) found, staged and installed 0.3.4 ("Install updates automatically" path) and relaunched as 0.3.4 with settings kept. Not yet clicked by hand: the "Update now" button path (same apply(), called from the running app). macOS: CI smoke only.
agent/frameport_agent.py— runs on the Frame (python3 stdlib only), JSON over SSH. Owns the install layout, launch.sh template, Steam shortcuts (binary VDF), launch tests. BumpAGENT_VERSIONwhen changing it.bootstrap/bootstrap.sh— one-time Frame setup served by the pairing server (app key, podman fix, Developer Mode, Lepton).bootstrap/setup.sh— the setup URL (curl -fsSL https://frameport.app/s | bash, copied to the site bysite/scripts/sync-media.mjs): finds the PC (stdlib mDNS on 5353 for_frameport-pair._tcp, which the pairing server announces while open; else the USB address and a /24 scan of/ping),/hello→ the user clicks Allow (4 digits on both sides,ask_digits) →/waithands over the code → runs bootstrap.sh exactly like the typed line. Checked on the dev Frame 2026-10-10 (mDNS and scan paths) with a stub bootstrap; tests/test_setup_url.py. Auto-pairing (frame/autopair.py,app._start_auto_pair, settingframe.auto_pair, default on; off with FRAMEPORT_HOME/FRAMEPORT_NO_AUTO_PAIR): while no Frame is connected, Frames in Developer Mode (devkit mDNS, else a 30 s scan of the /24s for :32000/login-name, deduped by host key) are connected if FramePort's key works, else offered the key via devkit.register every 4 s (refused at once until Pair new host is open; 45 s back-off after an unanswered request). This WSL dev PC never hears the Frame's mDNS (Hyper-V firewall): the scan finds it.catalog/games/<package>.yaml(installed apps also fetch these from GitHubmain, see "Catalog updates") — 38 recipes (34 verified 2026-09-28; Deadpool VR, 4XVR, NEX Player and AC Nexus's 90 Hz default confirmed later by the owner);catalog/triage.yaml— log signatures → fixes.native/— sources of the prebuilt binaries inartifacts/(adapter, VrApi bridge patches, GL shim, stubs).native/build.pyrebuilds them with NDK r27c (downloaded on demand intonative/.cache, git-ignored; uses-ffile-prefix-mapso no local paths get embedded; zip symlinks are restored as copies). Users never need the NDK.native/a32translator/(not an artifact, not shipped) =a32run, an AArch32 → AArch64 user-mode translator (interpreter + JIT, ARM/Thumb-2/VFP/NEON) for running 32-bit ARM Linux programs on the Frame; its README has the design, the qemu-arm differential fuzzer and program tests.native/a32translator/bridge/= the in-APK Android bridge (32-bit JNI/NativeActivity libraries in a 64-bit app: stub + liba32bridge.so + guest runtime liba32rt.so; harness with a fake JVM; test APK org.frameport.a32test passes in Lepton; GLES/EGL, OpenSL ES via AAudio, VrApi 1.x -> the 64-bit VrApi bridge, controller input as Gear VR touchpad/back events;bridge/convert.pyconverts an APK). First game: AFFECTED The Manor (Gear VR, Unity 5.6 + Mono) runs in the headset with picture and sound (2026-10-10); Spunky Dungeon (Unity 2019.2, OVRPlugin 1.46 on VrApi 1.31, Platform SDK) plays at 72 fps. In FramePort: patchframe.a32_bridge(patches/frame/a32_bridge.py:convert()is shared with the dev scriptbridge/convert.py; artifactsliba32bridge.so/liba32rt.so/liba32stub.sofromnative/build.py --only a32) is suggested for every 32-bit-only APK; the recipe then hasoverport: False(OVRPort is 64-bit only; the patch ships OVRPort's loader chain + its 32-bit platform dispatcher itself,overport.ensure_runtime_lib), only frame.a32_bridge/adapter/launcher of the frame patches, anddevice.text_input_windowfor Unity (engine.a32_recipe, also run bypipeline.set_recipe+ builds, so a bridge turned on by hand drops OVRPort & the 64-bit fixes; which patches work there =base.works_on_a32(Patch.a32; overport/frame off unlessa32 = True, adapter settings insettings.XRSHIM_SETTINGSoff);build.check_a32refuses anything else,engine.warnings(recipe, analysis)says so; a catalog entry for a 32-bit game that neither names nor removes the bridge predates it: bridge kept, status unknown; parity gives bridge builds without a bridge known-good "new build (no baseline)"; library migrationa32_bridge_onturned the bridge on once in every 32-bit recipe, user ones too, unless the catalog's frame_remove names it). Analysisextra.headset(ANALYSIS_VERSION 15): gearvr / go / quest → "Gear VR" / "Oculus Go" platform + Steam tag. Catalog:headset:field; com.fallen.affected, net.mobilemischief.spunkydungeon, com.ccpgames.gunjack (works, 2026-10-12). Per-game a32 findings (AFFECTED, Spunky Dungeon, Gunjack's signature checkfp_a32_signature.txt, synthetic Gear VR input, Quest buildsfp_a32_headset.txt/quest_touch, recenter, debugging aidsA32_BREAK/A32_STRACE/A32_PERFMAP, translator profiling): docs/DEVICE_NOTES.md#a32-bridge-games-lessons-debugging-aids-profiling.scripts/—package.py(flet build/pack),eval_heuristics.py(score heuristics),ui_smoke.py(GUI screenshots).docs/— PLAYBOOK, FRAME_RUNTIME, ARCHITECTURE, INSTALL (end users), parity reports (offline + device).
Dev commands
# the dev venv is .venv in the repo root (git-ignored); on the NTFS drive uv needs UV_LINK_MODE=copy
UV_LINK_MODE=copy uv sync --extra dev # creates/updates ./.venv
uv run pytest # unit tests (no device, no game files)
uv run frameport --help # CLI; uv run frameport-gui for the GUI
uv run frameport parity --known-good <PATCHED/_known-good-*> --sources "<folder with the game dumps>"
Games tests: pytest -m games (FRAMEPORT_GAMES=); on-device checks are CLI commands (below);
native layer test: FRAMEPORT_NATIVE_TESTS=1 pytest -m native (compiles with the NDK, ~2 min on NTFS).
Repo is on an NTFS drive (core.fileMode=false); line endings are LF (.gitattributes).
FRAMEPORT_HOME=<dir>isolates all app data (tests use it);FRAMEPORT_JAVA/_OVERPORT_JAR/_APKSIGNER_JARoverride managed tools. The owner's real app data is the Windows copy's:%LOCALAPPDATA%\FramePort(=/mnt/c/Users/ <user>/AppData/Local/FramePort; tools, overport workspace + game keystores, library, artwork, frames.json, the app's SSH key which the dev Frame authorizes; moved from WSL's~/.local/share/frameporton 2026-10-11, which is now only a stale backup). The desktop shortcut "FramePort (dev)" runs the source checkout natively on Windows (%LOCALAPPDATA%\FramePort-dev\launch-frameport-dev.vbs: venvFramePort-dev\venvviaUV_PROJECT_ENVIRONMENT,uv sync --frozen, logFramePort\logs\dev-gui.log). WSL runs on that data needFRAMEPORT_WINDOWS_DATA=1(set in.claude/settings.local.json;paths.shares_windows_data): the data folder becomes the Windows one, saved paths from the other side are translated on load (paths.localize: /mnt/d/X <-> D:\X in library.json, tools.json, PC deployment records), Java/Cpp2IL keep one copy per OS (tools.jsonjava-<os>-<arch>), and the link handler isn't registered (it would point the Windows copy's links at WSL). Don't run a WSL GUI and the Windows GUI at once (the gui.lock flock/msvcrt locks don't see each other; only the gui.alive heartbeat does).- Device checks:
frameport test <pkg>/frameport parity-device --results <parity.json> --baseline <launch.txt> [--test-only]; the pre-FramePort baseline isPATCHED/_known-good-2026-09-28/_frame-state/baseline-launch.txt. - Docs screenshots + videos (docs/SHOWCASE.md): never by hand.
uv run python scripts/showcase/render_docs.pyrendersdocs/showcase/shots.yamlintodocs/images/(every shot twice, kept when both agree; only visibly changed files are rewritten;--checkfor CI) andscripts/showcase/record_video.py [names] [--draft]records the storyboardsdocs/showcase/videos/<name>.yaml(tour = FramePort in use + README teaser; install = the install tutorial: instruction cards for the download / the command on the Frame, the rest filmed from a first start) intodocs/media/frameport-<name>.mp4. Both build the demo library first (demo_home.py, profiledemoorfresh: catalog games fromdemo-library.yaml+demo-analyses.json, art/details cached in ~/.cache/frameport-showcase, a pretend Frame fromshowcase-frame.json) in a temp FRAMEPORT_HOME: no personal data, refuses the real data dir and placeholder art; discovery is off and the setup command shows placeholders for the address and code. Steps find elements by name through Flutter's semantics tree (scripts/showcase/web.py: text, tooltip; the innermost match wins) — give new icon-only controls a tooltip. Theshowcaseworkflow renders on UI pushes to main (+ the videos whose storyboard changed, or the ones named on dispatch) and opens PRshowcase/update; releases attach every video asFramePort-<name>.mp4.scripts/scrub_library.py(the owner's own library) is only for private screenshots now. README rules (owner): says not every game runs (no longer "proof of concept" since 1.0), provides no piracy tools, credits the wrapped projects (most functionality is theirs); neutral technical wording; no Quest2Frame mentions anywhere. - GUI smoke test:
uv pip install flet-web playwright && playwright install chromium, thenFRAMEPORT_HOME=<test dir> python scripts/ui_smoke.py --out <dir> [--game <pkg>] [--frame steamos@<host>] [--update](--update= fake release: banner, dialog, Settings → Updates) and look at the PNGs. Flet 1.0 notes:ft.runmust own the main thread; background work viapage.run_thread; FilePicker is awaited (await ft.FilePicker().get_directory_path()); dialogs viapage.show_dialog/pop_dialog; running from source needsflet-desktop(declared) and web mode needsflet-web. - GUI scale:
theme.set_scale()(library settingui.scale, "auto" = halfway to Windows' AppliedDPI/96 under WSL (150 % → 125 %; full was too large), where the Linux window gets no Windows scaling; Settings → Appearance; applied at start). Sizes go through tokens orT.px(n), Material defaults through the theme's text_theme; never hardcode a bare pixel number in ui/.scripts/ui_smoke.py --scale 1.5 --viewport 2560x1440renders it. Flet draws a grey box for invalid layouts (e.g. anexpandchild in awrap=TrueRow). - One GUI per data folder (
core/instance.py, 2026-10-11):<data>/gui.lock(flock/msvcrt, pid inside); a second start asks the open window to come to the front (gui.focus, read by_watch_links) and exits; a lock held with no window (nogui.alive) and older than 30 s is a leftover FramePort process and is ended.main()ends the process (os._exit) onceft.runreturns: background threads (re-analysis, session triage) used to keep a windowless process running. Session triage shows one pop-up for all games of a burst (_announce_sessions). - Stopping the GUI:
pkill -fpatterns match your own shell — usepgrep -f "[b]in/frameport-gui|[f]let-desktop-light".
Round-2 polish (2026-10-02)
errors.py:explain(exc)(plain sentence for GUI + CLI),is_connection_error. Install/test jobs for the Frame (Job.needs_frame) that lose the connection go back to the queue front and the queue pauses (jobs.paused);app._pollretries every 10 s and resumes (owner-verified on the device 2026-10-02: the queue resumed after the connection was lost). Wake lock: agent v30keep_awake(systemd-inhibit idle:sleep, idle-only fallback because polkitinhibit-block-sleepis auth_admin for non-local sessions), held while Frame jobs exist.- Already converted inputs (
analysis.is_overport_output, e.g. the owner's library points atPATCHED/copies) are not converted again: a second OVRPort run replaced libovrplatformloader.so and dropped its DT_NEEDED on libovrstubs.so (Wallace & Gromit / Espire 2 crashed: cannot locate symbol ovr_…). Alt builds come from the saved<pkg>.alt-noforcequit.apk, else one OVRPort run (frame.ovrstubs relinks).missing_ovr_symbolsonly counts linked stub/compat libs. Converted copies inoutput/are removed after a Frame install (build.keep_copies). - Art:
.pickedmarker = user's pick, automatic fetches never overwrite it and only fill missing kinds; a Meta result's picture is the OculusDB image shown in the picker (the ovrp image service served "dogfooding" placeholder covers for some packages, e.g. Asgard's Wrath 2).steam_art_stale→ game page reminder. No store art → generatedcover.jpg/banner.jpg(APK icon viafetch.apk_icon). - Game settings dialog (
ui/views/adapter_dialog.py, metadatapatches/settings.UI); plain patch summaries (patches/summaries.py,Patch.summary, "Show technical details" = settingui.patch_details); share nudge (views/game.should_ask_to_share); saving a recipe keeps its status (was always "works").
Hard-won facts (don't re-learn these)
Frame runtime (SteamOS 0.3.0, build 20260922):
- No AArch32: 32-bit-only APKs fail with
INSTALL_FAILED_NO_MATCHING_ABIS→frame.a32_bridge(the a32 translator inside the app, see native/a32translator below). - GLES swapchains: only
GL_SRGB8_ALPHA8/GL_SRGB8(35907/35905), no MSAA. Vulkan: format 43 (sRGB) but not 37 (UNORM). - Missing: XR_FB_passthrough (emulate via ALPHA_BLEND), XR_FB_scene/spatial entities (emulated room from STAGE bounds), XR_KHR_composition_layer_equirect2/cylinder, XR_FB_composition_layer_image_layout (flip emulated by Vulkan blit).
xrConvertTimespecTimeToTimeKHR/xrConvertTimeToTimespecTimeKHRreturn FUNCTION_UNSUPPORTED → adapter emulates (offset = predictedDisplayTime − period − CLOCK_MONOTONIC, sampled in xrWaitFrame).- No XR_FB_display_refresh_rate (
xrGetDisplayRefreshRateFB-7), yet OVRPort's dispatcher offers it and answered 0 Hz → FrameBridge always answers with the measured rate (1/predictedDisplayPeriod, else 72). PreyVR (#306, idTech 4:USERCMD_MSEC = 1000 / GetRefresh()) had 0 ms tics: async thread spinning,com_frameTimestuck at 0, menu black. - Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m).
- GL goes through Zink (Mesa GL on Vulkan). Mesa GLSL is strict:
#pragmabefore#extensionfails, implicit int/float conversions fail (enableGL_EXT_shader_implicit_conversions), num_views=2 shaders on single-view FBOs give GL_INVALID_OPERATION. Some GLES games hitzink: DEVICE LOST(Unity MSAA RTT; Sniper Elite VR even without). - Valve injects
VALVE_rpo/VALVE_fdm_injectionVulkan layers (via VK_INSTANCE_LAYERS). - Tracking only works with the headset worn. SSH/headless launches never reach VISIBLE/FOCUSED and poses have flags 0x3. So automated tests prove startup (process alive, instance/session created, frames paced), never visuals.
Setup / pairing (2026-10-02, verified on the device): Developer Mode = "DevModeEnabled" in
~/.local/share/Steam/config/config.vdf (InstallConfigStore/developer), applied by Valve's
/usr/bin/steamos-polkit-helpers/steamos-devkit-mode --enable|--disable (polkit allow_any: no password; enables/
disables sshd, xrdp, steamos-devkit-service, debug port forwards; sentinel /etc/steamos-devkit-enabled). Steam
re-asserts the config value at every start and rewrites config.vdf on exit → stop Steam, edit, helper, start.
Desktop Mode is a nested Plasma inside steam.service (own XDG_RUNTIME_DIR /run/user/1000/nested_plasma + private
D-Bus): systemd-run --user from Konsole fails ("Failed to connect to user scope bus") unless XDG_RUNTIME_DIR/
DBUS_SESSION_BUS_ADDRESS point at /run/user/$UID, and stopping Steam ends the desktop and everything started in
it → bootstrap.sh runs the Dev Mode job + Lepton request + /paired as a user unit (log ~/.cache/frameport-setup.log);
no sudo/password anywhere (verified: naive Frame → connected, password never set). Valve's devkit pairing (fallback,
PC → Frame only): POST :32000/register with an ssh-rsa key (connection.devkit_key, frame/devkit.py) works only
while Steam is in pairing mode (Settings → Developer → Pair new host), else 403 at once; approve hook waits 30 s.
The Frame runs firewalld (22 and 32000 open). PC side: the setup server needs inbound TCP 8765–8767 — WSL's Hyper-V
firewall blocks it silently (DefaultInboundAction Block): pairing.ensure_reachable adds a temporary rule via one
UAC prompt, removed when the server stops (flag file in %TEMP%, max 35 min); hints per OS after 45 s without a request
(pairing.firewall_hint). Flet 1.0 patches aren't thread-safe → app.serialize_flet_updates() (a dialog shown while a
scan redraw ran never closed: "dropped a patch for unknown control"); it also makes property writes (Flet's
Prop.__set__) take the same app.FLET_LOCK: a property set on a background thread while the loop packed a patch
changed the control's _values mid-iteration ("dictionary changed size during iteration", a sidebar click's page
change died while the live Frame card took a sample). Anything that serves the app (ui_smoke, showcase) calls it too.
Lepton: needs an activity with category LAUNCHER (Quest apps often only have INFO → "APP_ACTIVITY is empty").
2D apps: Lepton runs every app headless (lepton.headless=true, only OpenXR output reaches the headset) unless the app folder (<base>/lepton-app/) has a lepton-show-flatscreen file (liblepton/app_metadata.sh) → Waydroid window on gamescope; agent v28 set_flatscreen at finalize for vr_kind == "none". Android 11's navbar covered the
app's controls → patch device.hide_navbar (default on for vr_kind none, migration flat_hide_navbar) exports
qemu.hw.mainkeys=1 as a second line of LEPTON_GFXRECON_FP_PROPS (runtime alternatives failed, see
docs/FRAME_RUNTIME.md). Verified in the headset 2026-10-02 (2048).
Lepton = Steam app 3029110 (+ "Lepton Development" 3056000, needs Developer Mode). Per-game env: STEAM_COMPAT_INSTALL_PATH
/DATA_PATH/SHADER_PATH, SteamAppId. Logs: <base>/launch.log and ~/.local/share/Steam/logs/lepton-logcats/steamlaunch-<appid>.
Containers are podman lepton-steamlaunch-<appid>. Some Unreal games create save dirs without u+rwx → launcher repairs every 2 s.
Rootless podman leaks one kernel session keyring per container start (200-key quota per user): after ~200 launches
since boot every game fails with crun: create keyring …: Disk quota exceeded / is not a running context. Fix:
keyring = false in ~/.config/containers/containers.conf (agent ensure_host_fixes, bootstrap); leaked keys only
go away with a reboot. Check usage: grep "^ *1000:" /proc/key-users (agent info → kernel_keys).
Proton on the Frame (2026-09-29): Steam registers ARM64 tools from app "Steam Frame ARM64 Compat List"
(proton_11-arm64 4628740 needs SLR4-arm64 4185400; proton-experimental-arm64 4427310; fex 3127680) — not installed
by default; steam://install/<id> only opens a dialog in the headset (agent install_proton mode unattended =
appmanifest stubs + Steam restart; untested on device). Proton only sets up VR/wineopenxr when SteamGameId is set.
Host OpenXR = SteamVR (~/.config/openxr/1/active_runtime.json). Steam VR games on the Frame (Pistol Whip etc.) are
APKs via Lepton. Verified on the device: the app installs Proton itself (installer.ensure_proton: appmanifest stubs +
Steam restart, ~90 s); Proton runs x86 code; wineopenxr gets registered; headless launches need DISPLAY/
GAMESCOPE_WAYLAND_DISPLAY from Steam's environment (launch.sh imports them). The Linux SteamVR runtime supports
XR_KHR_convert_timespec_time (only the Android runtime lacks it). But it only accepts OpenXR 1.0 apps: apiVersion
1.1 → XR_ERROR_API_VERSION_UNSUPPORTED, and Proton 11's vrhelper asks for 1.1 → no VR at all (flat window; Revive:
"Unable to load LibOVRRT DLL"). FramePort's layer (native/xrlayer, artifacts/linux-arm64, patch pcvr.xr_timefix,
default on + one-time library migration in core/library._migrate) retries as 1.0. Proton's container (pressure-vessel)
drops XR_API_LAYER_PATH, so the agent registers the layer as an explicit layer in
~/.local/share/openxr/1/api_layers/explicit.d/ (library: shared copy in ~/.local/share/frameport/xrlayer/); it
only loads when XR_ENABLE_API_LAYERS names it. Unreal PC VR games get
pcvr.no_crash_reporter by default (-nocrashreports + CrashReportClient.exe renamed .disabled in the Frame copy)
and pcvr.oculus_unreal (PC VR counterpart of overport's patch_oculus_unreal: UE's OculusHMD needs the Windows event
OculusHMDConnected; launch.sh wraps the injector in helpers/fp_oculushmd.exe = native/oculushmd,
artifacts/win-x64, which provides it until the game's job is empty; untested on the device yet).
Rift games tried on the Frame: Rick and Morty (SteamVR build, no Revive: "OpenVR initialized!" with the layer; flat
before — confirm in the headset), Lies Beneath (Oculus Store UE 4.23 build: delay-loads LibOVRPlatform64_1.dll =
Platform SDK → crash 0xc06d007e; needs the Oculus app, FramePort doesn't replace it; also OVRPlugin's LibOVR shim
reports "Unable to load LibOVRRT DLL" before any OpenXR call — likely needs a real Oculus runtime install, unverified).
Launch tests ignore log files older than the launch (stale Proton/game logs used to be triaged). Revive injector CLI: ReviveInjector.exe [/openxr] <exe path>
(joins args; logs to %LOCALAPPDATA%\Revive\ReviveInjector.txt). On WSL, run Windows programs from Windows paths
(\\wsl$ is unreliable), so PC mode copies Revive to %LOCALAPPDATA%\FramePort.
FramePort never bypasses Oculus entitlement checks (Platform SDK games: flagged, PC mode with the Oculus app only).
Upload speed: home Wi-Fi caps uploads at ~11-18 MB/s whatever the SSH method; the Frame's hotspot (wlanap) ~83-97 MB/s; Frame.fast_link() (installer transfer_link, uploads ≥64 MiB) prefers usb0 > wlanap when the host key matches. → docs/DEVICE_NOTES.md#upload-speed
Platform SDK detection must include delay imports (PEInfo.delay_imports), modular Unreal's
*-OnlineSubsystemOculus-*.dll and Ready At Dawn's pnsovr.dll: Robo Recall, Lies Beneath and Lone Echo I/II all use
it; without the Oculus app they crash with 0xc06d007e (triage delayload-missing) — not fixable legitimately.
GUI: Frame → Installed → folder icon = file browser (agent list_files, flags files missing vs the install
manifest); Settings → About shows the bundled agent version and the Frame's.
Revive under Proton arm64: Revive's Detours hooks don't take effect (Wine's kernelbase is ARM64EC); a system32 LibOVRRT64_1.dll symlink gets past -3001 to -3021 = ovrError_LibSignCheck (Oculus-signed runtime required; not bypassed). → docs/DEVICE_NOTES.md#revive-under-proton-arm64-lone-echo
Discovery/network: Developer-Mode SteamOS devices announce _steamos-devkit._tcp (TXT login=steamos) — use it;
they don't publish _ssh._tcp. The Frame has several links: wlan0 (home Wi-Fi), wlanap = its own hotspot at
10.35.78.1/24 (a PC can join it directly), usb0 = USB gadget network 10.86.200.233/29 (up when cabled to a PC).
The dev PC runs WSL2 in mirrored networking mode (mDNS works). Beware pkill -f <pattern> killing your own shell.
Steam: shortcut appid = crc32('"/launch.sh"' + title) | 0x80000000; shortcuts.vdf is only read at Steam
start, so Steam must be stopped while writing it. Terminals/SSH started from Steam live in steam.service's cgroup —
stopping Steam kills them → always run that work via systemd-run --user (the agent does). Artwork goes to
userdata/<id>/config/grid/<appid>{p,,_hero,_logo}.<ext>.
overport: always --version=latest; --workspace holds runtimes and per-package keystores (password
"password", alias "key") — never lose them: updates must be signed with the same key or saves are lost on reinstall.
Output is deterministic (same input + runtime → same bytes), which is what makes parity testing possible.
OVRPort 1.2.5 (2026-10-01): patch rejects unknown/duplicate args, unknown ids and an empty --patches=; it adds libovrplatformcompat.so itself (frame.ovrplatformcompat then skips). New patches patch_ac_nexus_no_appsw_72/_90 (AC Nexus build 207706 only; _90 = catalog default; STRICT patches only from a catalog recipe where applies() holds), patch_disable_meta_xr_audio_telemetry, patch_vrapi_openxr (OVRPort's VrApi translator fails on the Frame → keep frame.vrapi_bridge). The owner prefers OVRPort's fixes over ours where they work as well. frameport install <pkg> --apk <file> installs a test build. → docs/DEVICE_NOTES.md#ovrport-125
Patching gotchas:
- UnityPy re-serialization breaks scene loading → patch QualitySettings ints in place.
- LIEF's DT_NEEDED injection shifts segments; our
elf.add_neededappends a new PT_LOAD (reuses PT_NOTE, else moves the phdr table + adds PT_PHDR) and leaves existing bytes untouched. Bionic requires section headers and matching .dynamic. - apksigner 37 aligns (4 B / .so 16 KiB) itself — no zipalign needed.
- Titles from aapt with apostrophes got truncated once; we read labels with pyaxmlparser and store titles from the
overport image API (
https://ovrp.crx.moe/images/by_package?package=), which also serves Steam artwork. - GLAD engines fetch all GL via eglGetProcAddress → wrap it (GL shim) to fix/trace shaders; that's how POTW was solved.
- VrApi-direct engines (CryEngine Climb 2, POTW) need the VrApi bridge; the bridge drops whole frames on unknown layer types (→ black screen with audio).
Quest controller mode: adapter quest_buttons (0/1/2, per game + library input.quest_buttons) suggests Frame Controller bindings translated from the game's Touch set (suggest_frame, mapping docs/input/quest_buttons.json = single source for C table, tests, diagram scripts/controller_diagram.py); runtime accepts them headless. → docs/DEVICE_NOTES.md#quest-controller-mode-quest_buttons-2026-10-17
Steam Frame controller models: adapter setting controller_models (native/adapter/render_model.c, emulates XR_FB_render_model) + agent install_controller_models (Frame's SteamVR models → glb at finalize); no catalog game uses runtime models; Valve's models are never committed or copied off the Frame. → docs/DEVICE_NOTES.md#steam-frame-controller-models
overport's dispatcher (libopenxr_loader.so) only forwards functions in its own table (overportOXR: Unknown proc addr: …), so adapter emulations of functions it doesn't know are unreachable. For XR_FB_render_model,
native/xrshim fills the gap: OVRPlugin's dlopen("libopenxr_loader.so") string is rewritten to the shim (see
native/README). Verified on the device with Toy Master (2026-09-30): extension shim: xrLoadRenderModelFB -> FrameBridge.
Toy Master doesn't request a model after that (it uses its own), so the glb loading path is only unit-tested.
Crash backtraces: Lepton writes tombstones only to lepton-logcats/steamlaunch-<appid>/logcat-crash.log (launch tests return crash_log). Fossilize loads into every app (userdebug guest) → frame.vk_sanitize (native/vkshim); overport's dispatcher aborts on swapchains > 4096 px → frame.swapchain_limit; cylinder layers → quad strips (cylinder_strips). Drawing equirect layers ourselves and a focus debounce were tried and removed: don't retry without a new approach. → docs/DEVICE_NOTES.md#crash-backtraces-fossilize-swapchain-limit-cylinder-and-equirect-layers
360° layers / 4XVR: the Frame's Android runtime composites only quad + projection layers (quads always above projection layers). Per-game adapter settings (off by default): equirect_emul (GLES), equirect_flip/face/res/fps/stereo, stable_local, focus_hold (default on), aim_pitch/aim_yaw/aim_forward, refresh_rate, layer_debug; agent link_media / app_media_dirs put sent files into a player's own folder. → docs/DEVICE_NOTES.md#360-layers--4xvr
Companion apps / intents: a second APK can run inside a game's Lepton container, but Lepton turns every android.intent.action.VIEW with data into steam://openurl → "open in external player" hand-offs can't work; Lepton installs exactly one *.apk per app folder. → docs/DEVICE_NOTES.md#companion-apps--intents-stremio--4xvr
Text input: Lepton's Android has no IME and VR apps run headless (no focused window); lepton-show-flatscreen keeps VR and gives focus + Steam's keyboard (device.text_input_window); frame.unity_text_input patches Unity's TMP_InputField/InputField at Cpp2IL offsets; agent _keyboard = uinput "Type on Frame". pm install of a second APK corrupts the main app (cmd_real package install skips the hook). → docs/DEVICE_NOTES.md#text-input-stremio-vr
User reports #4-#10: shortcuts go to the signed-in account (agent library_users), launch errors NOT_IN_LIBRARY; shared Cpp2IL run (unity_text_input.Il2cppReturnPatch) for frame.unity_runtime_msaa_off and frame.unity_multipass; VR4 → catalog use_alt. Catalog changes reach existing games: recipes store catalog_rev, library._follow_catalog re-derives non-user recipes. → docs/DEVICE_NOTES.md#user-reports-2026-10-04-issues-4-10
Round 2 of reports / headset rounds 2-3 (2026-10-04): vkshim vk_shader_fix (VR4); Il2cppReturnPatch.field_loads (I Am Cat); builds record recipe_fp (changed recipe/revised patch → "Update on Frame"); Frame.relocate; adapter profile_remap (Touch Plus/Pro); sync_guard (Myst); focus_hold tuning; ovrpshim for Unity 2017 (Accounting+); Steam library to every account, shortcuts_lost / add_devkit_entry; no clipboard service → frame.sdl_clipboard, frame.godot_clipboard; 2D apps keep non-VR patches (needs_vr = False); frame/local.py (FramePort on the Frame); after every app update library._follow_catalog re-derives non-user recipes once (recipes.app_version, library.REFRESH_ON_UPDATE). Analysis fields added later: bump analysis/detect.ANALYSIS_VERSION (re-analysis at GUI start + pipeline.refresh_analyses; analysis_failed). Roblox and BONELAB crash (unsupported). → docs/DEVICE_NOTES.md#round-2-of-reports-headset-rounds-2-3-2026-10-04
Round 4: XR_KHR_android_surface_swapchain is listed but unsupported → adapter surface_emul (default on, native/adapter/surface_swapchain.c); save folders without u/g+rwx → launch.sh fix_perms; Lucky's Tale OOM (no fix); perf works as steamos (gdb blocked); QuestCraft JRE, WiiCompiled import by hand (Lepton has no document picker). → docs/DEVICE_NOTES.md#round-4-2026-10-04-surface-swapchains-save-folder-permissions
Steam library failures (GitHub #21/#30/#41/#42): on some Frames Steam never loads FramePort's shortcuts.vdf entries (Play → AppError_9) → fallback: register through Steam's devkit interface (devkit-1 … create-shortcut, ~/devkit-game/<id>/; devkit_appid, devkit_appid_from_log, devkit_unregister, copy_grid_art; ids: letter first, letters/digits/_ only); collect_diag adds steam_library. Cause still unknown. → docs/DEVICE_NOTES.md#steam-library-failures-and-the-devkit-fallback-github-21304142
Exit game (GitHub #36): Apps.TerminateApp (Steam CEF devtools on 127.0.0.1:8080) closes games; launch.sh ends the game when its parent reaper is gone (upgrade_launchers adds it). → docs/DEVICE_NOTES.md#exit-game-github-36
SteamVR dashboard at game start: launch.sh's _dashboard_worker hides the dashboard via SteamClient.OpenVR.VROverlay (agent steam_js) from the first FrameBridge new layer: line (120 s, ≤10 times, stops after toggle_dashboard_action); log <base>/dashboard.log, opt out FRAMEPORT_KEEP_DASHBOARD=1. → docs/DEVICE_NOTES.md#steamvr-dashboard-at-game-start
Space warp / ITR2: OVRPort's dispatcher offers XR_FB_space_warp itself (patch_disable_space_warp doesn't reach UE5) → FrameBridge hide_space_warp (per game, off; suggested for UE5 + OVRPlugin). equirect_emul switches itself off on Vulkan. → docs/DEVICE_NOTES.md#space-warp--itr2
PR #34 (Lucas-Mathieu): native/adapter/audio_metadata.h keeps Meta XR Audio Wwise metadata alive (Batman: Arkham Shadow 1.4.1 smoke-bomb crash). Outside PRs: review source, rebuild artifacts/ ourselves, merge locally. → docs/DEVICE_NOTES.md#pr-34-meta-xr-audio-metadata-batman
Heuristics eval (2026-10-04): 19/44 recipes exact (superseded by the 2026-10-15 numbers under Heuristics). → docs/DEVICE_NOTES.md#heuristics-eval-2026-10-04
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).
Start activity / QuestCraft / ITR2 pop-in: frame.start_activity (analysis extra.vr_activity, axml.set_start_activity; OVRPort gives every MAIN activity LAUNCHER, so take the VR activity from the original APK); Unreal reads user config from internal storage; Lepton passes LEPTON_ENV_<NAME> to the app as <NAME> (QuestCraft LIBGL_NOERROR); vkshim vk_query_slots left off. → docs/DEVICE_NOTES.md#start-activity--questcraft--itr2-pop-in
Vulkan validation / ITR2 solved: bundle Khronos' libVkLayer_khronos_validation.so into a test APK + vkshim vk_validation=1 (logcat tag VALIDATION); vk_spec_fixes=1 drops ITR2's invalid depth resolve; VALVE_rpo removed its fog (catalog lepton_env: VK_INSTANCE_LAYERS). Agent not_started waits 30 s for "Boot complete!". → docs/DEVICE_NOTES.md#vulkan-validation--itr2-solved
GitHub #38/#39: SteamVR rejects image rects past their swapchain (-25) → FrameBridge rect_clamp (default on), triage swapchain-rect-invalid; The Room VR MSAA crash in libgallium. → docs/DEVICE_NOTES.md#github-3839-powerwash-simulator-the-room-vr
Headset round 2026-10-05: FrameBridge pose_consistency (I Am Cat judder), xrshim haptic_fix (Lucky's Tale), focus_hold on by default with 5 s (wear-sensor dips, logs/eyetracking.txt), unity_no_overlay_copy (The Room VR), ovrpshim frame wait for Unity 2019 (UnityOculusCheck.legacy_loop), frame.ovr_trace (opt-in, native/ovrtrace, logcat fp_ovrtrace). → docs/DEVICE_NOTES.md#headset-round-2026-10-05-evening
Unity built-in VR input: probe builds FRAMEPORT_INPUT_PROBE=1; ovrpshim adds a GetMouseButtonDown icall click (Accounting+); libunity 2019 only polls Touch when the device model starts with "Oculus" → unity_oculus_check revision 4 oculus_model_checks (BattleSisters); haptic_fix suggested for every game with libOVRPlugin.so. → docs/DEVICE_NOTES.md#unity-built-in-vr-input-accounting-battlesisters
Vader Immortal (GitHub #49): stuck on its loading card and leaked GPU mappings (solved below: gl_error_clear); diagnostics FrameBridge layer_debug, strip_color_bias, frame_balance, snapshot=N (snapshot_gl.c: saves the submitted left-eye image as files/fb_snap_*.ppm). → docs/DEVICE_NOTES.md#vader-immortal-headless-investigation
SteamVR overlay apps: host OpenVR overlays composite over Lepton games; detection analysis/vroverlay.py, patch pcvr.vr_overlay, Linux fields vr_overlay/vr_overlay_autostart, agent register_vr_overlay/unregister_vr_overlay/launch_vr_overlay; SteamVR on the Frame reads only binary_path_linux_arm; FramePort's own autostart (ensure_overlay_service, frameport-vr-overlays.service). See also docs/FRAME_RUNTIME.md "Overlay apps". → docs/DEVICE_NOTES.md#steamvr-overlay-apps
Vader's leak solved / AC Nexus flips: stale GL errors broke SteamVR's GL interop → adapter gl_error_clear (GLES, catalog for Vader ep1-3); read xrclient_<pkg>.txt in Steam's logs for runtime errors; the runtime caps swapchains per session (~15) → flip emulation in place; adapter flip_dump. → docs/DEVICE_NOTES.md#vaders-leak-solved--ac-nexus-flips
Unreal's self-restart: UE 4.21+ restarts itself after the first shader precompile and Lepton ends the container → launch.sh RESTART_BLOCK starts Lepton once more (upgrade_launchers; FRAMEPORT_NO_RESTART=1 off). → docs/DEVICE_NOTES.md#unreals-self-restart
xrWaitFrame runs ahead: the runtime sometimes returns a display time <1 refresh after the last submit and rejects the second submission (VRCompositorError_AlreadySubmitted) → the VrApi bridge re-waits (pacing 2s: counters). → docs/DEVICE_NOTES.md#xrwaitframe-runs-ahead-journey-of-the-gods
Lepton's logcat mirror dies (EOF, read failure, or silent) → launch.sh _logcat_keeper reads podman exec <container> logcat itself into launch.log (log <base>/logcat-keeper.log). → docs/DEVICE_NOTES.md#leptons-logcat-mirror-dies
Double launch: launch.sh takes flock on <base>/.launch.lock (fd 9); a second launch exits 0 and logs to <base>/launch-dup.log (upgrade_launchers adds it). → docs/DEVICE_NOTES.md#double-launch
Issue triage round 2026-10-06: FrameBridge turns haptic amplitude 0 into xrStopHapticFeedback, per-game haptic_scale; Lambda1VR → frame.tbxr_vendor; Jurassic World → frame.vrapi_stub; Metro Awakening / Pinball FX → hide_space_warp; frame.ovr_trace can't trace Unity games; the VrApi bridge's 30 s head-pose deadline ends headless VR mode. → docs/DEVICE_NOTES.md#issue-triage-round-2026-10-06
Lepton storage: each app's /sdcard has Movies/Download/Documents symlinked to the Frame's ~/Videos/~/Downloads/~/Documents (agent storage_targets); the media index never works; Lepton grants all runtime permissions. → docs/DEVICE_NOTES.md#lepton-storage
SteamVR per-app settings: native/vrsettings = fp_vrsettings.exe sets steam.app.<appid> preferredRefreshRate / motionSmoothingOverride live; pcvr.steamvr_tuning (default on, PC only). → docs/DEVICE_NOTES.md#steamvr-per-app-settings
Language packs: frame.langpacks (opt-in, native/langpack, elf.hide_exports, logcat tag fp_langpack) serves <tag>.lang files; Unreal needs the pack's Metadata = the game's version (with_metadata, FRAMEPORT_LANGPACK_META) and the /storage/emulated/0/... path spelling; FRAMEPORT_LANGPACK_SKIP. → docs/DEVICE_NOTES.md#language-packs
Cube swapchains: the runtime refuses faceCount=6 → FrameBridge cube_standin (default on, GLES only, native/adapter/cube_standin.c). → docs/DEVICE_NOTES.md#cube-swapchains
Hardware video decoding: native/hevc OMX plugin (OMX.frameport.{avc,hevc,vp9}.decoder), installed once per Frame (ensure_video_codec, agent video_codec_status/install_video_codec/video_codec_switch); bump revision in native/hevc/build.py with every artifact change; per game frame.hw_video_decode (analysis media_codec), Settings video.hw_decode, FRAMEPORT_NO_HW_VIDEO=1; Batman migration batman_video_patches + surface_native. → docs/DEVICE_NOTES.md#hardware-video-decoding
Vulkan shader dump: adapter vk_shader_dump=1 (native/vkshim/shader_dump.h) writes SPIR-V modules + index.txt to files/fp_vk_shaders/; collect_diag returns the newest session's modules (shader_dumps); triage gpu-hang, triage.graphics_api. → docs/DEVICE_NOTES.md#vulkan-shader-dump
Unreal OBB check (#159): frame.unreal_skip_obb_check (analysis unreal_verify_obb) turns off Epic's OBB CRC pass; triage unreal-obb-check-stuck. → docs/DEVICE_NOTES.md#unreal-obb-check
Issue round 2026-10-12: Lepton breaks on unquoted paths with spaces → launchers give Lepton a ~/.local/share/frameport/lepton-paths/<pkg> link (app_dir_lines/set_app_dir/launcher_base); frame.eos_keychain_loop, frame.unity_tls_key_pool (libfp_tkeys.so, elf.rename_imports), frame.vermillion_scene_canvas; tools/cpp2il.py falls back to Cpp2IL's dev build for metadata v107; Linux desktop_icon; scripts/upload_bench.py. → docs/DEVICE_NOTES.md#issue-round-2026-10-12
OpenSSH uploads (#324/#242): native_upload.quote escapes only backslash and ", Win32 local paths go as C:/…; Frame.auth records the login that worked (app_key / user_key from ssh.extra_keys, frame ssh-keys, agent, password); strict IdentitiesOnly first, relaxed retry. → docs/DEVICE_NOTES.md#openssh-uploads
Meta user id (#182): OVRPort's platform loader makes up a new user id every start → frame.ovr_stable_user (default on) returns a stable per-game, per-user id. → docs/DEVICE_NOTES.md#meta-user-id
Vivox API 31 (#101): newer Vivox calls Android 12 AudioManager methods → frame.vivox_audio_route (analysis vivox_api31, Dex.return_early). → docs/DEVICE_NOTES.md#vivox-api-31
Steam Input gamepad for 2D apps (#162): opt-in device.steam_gamepad (deployment steam_gamepad → launcher GAMEPAD_LINE → PODMAN_WRAPPER / podman_run_args bind-mount Steam's virtual pads). → docs/DEVICE_NOTES.md#steam-input-gamepad-for-2d-apps
Multiview programs on flat framebuffers (#77): Mesa drops num_views=2 draws on single-view FBOs → frame.gl_multiview_fbo (native/glmv = libfpglmv.so, analysis gl_multiview_libs); headset-verified with Doom3Quest. → docs/DEVICE_NOTES.md#multiview-programs-on-flat-framebuffers-doom3quest
BlazeRush (#57): frame.blazerush (1.0.349 only), GL shim gl_int_attribs / gl_highp_markers, saved VrApi FOV (files/framebridge-vrapi-fov.txt), device.config_sync (catalog config_sync); launch tests treat an end after the headless deadline as a pass (triage headless_end). → docs/DEVICE_NOTES.md#blazerush
Session triage: real play sessions are triaged too: agent last_play / session_log (<anchor>/plays.log), pipeline.sessions_due / triage_session = validate/session.analyze (incl. space-warp-used, gpu-hang, slow-frames, focus-dips) → library last_session → game page "Last session"; apply_suggestions_live; CLI frameport session <pkg> [--apply]; pac_hints stays triage-only. → docs/DEVICE_NOTES.md#session-triage
Start activity / QuestCraft / ITR2 pop-in (2026-10-05): WiiCompiled's VR part (QuestActivity, SDL, process :game) stays behind Lepton's flat window when the launcher is shown (device.text_input_window). frame.start_activity (opt-in; analysis extra.vr_activity = the original APK's activity with Meta's VR category when it isn't the launcher) gives that activity's filter LAUNCHER and every other LAUNCHER becomes INFO, per element (axml.set_start_activity); Lepton's own apk-info-extractor --print-activity-name (on the Frame under Lepton/liblepton/apk_extractor) then answers QuestActivity. OVRPort gives every MAIN activity LAUNCHER + VR categories, so the VR activity must come from the original APK's analysis, not the converted manifest. Lepton has no activity override (APP_ACTIVITY is reset when app_metadata.sh is sourced). QuestCraft reaches Minecraft 1.21.5 (Fabric, Vivecraft, Sodium; LTW "Large Thin Wrapper" GL 3.0 on the Frame's Zink) and stops at OpenGL error 1282 in WindowFramebuffer createTexture (Minecraft's crash report in files/instances/1.21.5/crash-reports/, Pojlib's files/latestlog.txt); the vrclient SIGSEGV / "pthread_mutex_lock called on a destroyed mutex" afterwards is only the shutdown (Unity's render thread still in vrclient while the instance is destroyed). ITR2 "models popping in and out" in game: Unreal reads user config from internal storage (lepton-data/internal/<pkg>/files/UnrealGame/<Project>/<Project>/Saved/Config/Android/Engine.ini, not external); r.AllowOcclusionQueries=0 there broke its menus (removed again). Vulkan shim vk_query_slots (two slots per occlusion query) never saw ITR2 create an occlusion query pool, and the run with it crashed (Unreal RenderThread SIGSEGV, caught by sentry-native: no tombstone; the backtrace is in cache/sentry/*/.sentry-native/*.run/*.envelope with module offsets) - left off. Next suspect: Unreal 5 mobile HZB occlusion reading depth stored with Valve's FDM injection layer; test = launcher VK_INSTANCE_LAYERS='' (Lepton then loads neither VALVE_rpo nor VALVE_fdm_injection, Fossilize still). QuestCraft: LTW overrides glGetError and returns 0 with LIBGL_NOERROR set; Lepton passes LEPTON_ENV_<NAME> to the app as <NAME>, so catalog lepton_env: {LEPTON_ENV_LIBGL_NOERROR: '1'} gets Minecraft past the GL error 1282; then black picture (see the catalog entry). FrameBridge logs new layer once per layer type per process and projection views only for the first frames, so a second session in the same process (Vivecraft after Unity) needs layer_debug=1.
Vulkan validation / ITR2 solved (2026-10-05): Khronos' validation layer can't come from Lepton (its layer dir is in the read-only guest image; the Linux copies on the Frame are glibc) and a global debug.vulkan.layers property (via the LEPTON_GFXRECON_FP_PROPS trick) kills the container at boot. Working recipe: bundle libVkLayer_khronos_validation.so (Khronos android-binaries release, arm64-v8a, stored uncompressed) into a test APK re-signed with the game's key (apk.sign.sign), install with frameport install <pkg> --apk, and set the Vulkan shim's vk_validation=1 (it adds the layer at vkCreateInstance; findings under the logcat tag VALIDATION in launch.log, each message ID capped at 10). ITR2's flicker + windows behind models = Unreal's Qualcomm shader-resolve subpasses with a depth resolve of a single-sampled depth attachment (VUID-04908/-03179); vk_spec_fixes=1 drops that resolve (its TRANSFER_DST half never applies to ITR2: the main depth is transient). Valve's VALVE_rpo layer removed ITR2's fog (catalog lepton_env: VK_INSTANCE_LAYERS: VK_LAYER_VALVE_fdm_injection). r.ViewDistanceScale=2 in the internal Saved/Config/Android/Engine.ini only exists on the owner's Frame (the catalog can't write internal storage yet). Launch tests: Lepton prints a transient "is not a running context" on the first start after an APK change (agent v54 not_started waits 30 s for "Boot complete!").
GitHub #38/#39 (2026-10-05): PowerWash Simulator stuck at "Waiting.." on a reporter's Frame = Unity sized the right-eye image rect past its swapchain (rect 268+1656 on a 1920-wide swapchain; the runtime's recommended size differs per Frame) and SteamVR rejected every frame (xrEndFrame failed -25, XR_ERROR_SWAPCHAIN_RECT_INVALID), after which the game stopped calling xrEndFrame. FrameBridge rect_clamp (default on; sizes noted in xrCreateSwapchain; clamped layers count as swapped) keeps projection/quad rects inside their swapchain; triage swapchain-rect-invalid. Not reproducible on the dev Frame with scale (Unity's eye sizing isn't just the recommended width); no regression there (72 fps). The adapter's revision was NOT bumped (that would mark every installed game outdated): affected users rebuild. The Room VR (#38) = Unity's render thread SIGSEGV in libgallium_dri.so ~30 ms after the game recreates its eye swapchain with samples=4 (FrameBridge retries samples=1); the game's own code turns MSAA on (not OVRManager); candidate fix: hide GL_EXT_multisampled_render_to_texture (GL shim) - needs the APK to test. The launch test passed both (render thread dead / no frames): triage gap.
Headset round 2026-10-05 (evening): I Am Cat's judder = repeated xrLocateViews of one display time returning
slightly different poses → FrameBridge pose_consistency (owner-verified, works). Lucky's Tale freeze on touching a
save slot = OVRPort's OpenXR dispatcher converting XR_FB_haptic_amplitude_envelope with ns read as s (GitHub #9) →
xrshim haptic_fix (owner-verified); its remaining short pauses were the wear sensor ("HMD off/on" 0.5-1 s in
logs/eyetracking.txt, not vrserver.txt) → focus_hold is on by default with 5 s (owner's choice; C defaults
too). The Room VR works (unity_no_overlay_copy). BattleSisters (Unity 2019.4 built-in VR, no libOculusXRPlugin.so)
flooded "outside of frame bounds" ~2400/s and hung the GPU (kernel hangcheck, zink DEVICE LOST) → the ovrpshim frame
wait now also applies to Unity 2019 without the Oculus XR Plugin (UnityOculusCheck.legacy_loop). Vader Immortal
(UE4, stuck on the loading image after the intro, GitHub #49): VRP repacks carry a Frida gadget (libfrda.so,
config hijack_responses), but OVRPort's patch_clean_up_frida removes its loadLibrary call, so it never runs on
the Frame; frame.ovr_trace (opt-in, native/ovrtrace: 1138 exported ovr_* stubs → real loader functions, logcat
tag fp_ovrtrace: calls, PopMessage answers, unanswered requests every 10 s) is the next diagnostic.
Unity built-in VR input (2026-10-05): probe builds (FRAMEPORT_INPUT_PROBE=1 frameport build …; it logs
trigger/grip crossings too) showed OVRPlugin returns the full Touch state for every controller mask (also Go masks);
presses reach the games' OVRInput. Accounting+ (Il2CppDumper v6.7.46 reads its Unity 2017 metadata v24, run on Windows)
passes stance selection (NewtonVR grip/trigger) but its motion warning waits for Input.GetMouseButtonDown(0/1),
which Lepton never delivers (no focused Android window) → ovrpshim registers its own
UnityEngine.Input::GetMouseButtonDown(System.Int32) icall (il2cpp_add_internal_call, after Unity's: resolve first)
that adds a click in the frame a Touch trigger or A/B/X/Y is newly pressed (unity_oculus_check revision 3).
BattleSisters (Unity 2019 InputSystem/XR InputDevices via VrHandInput): libunity's Oculus module (OVRPlugin
function table: a global pointer at 0x16b57c0 in this build, slots filled by name, e.g. +0x160 GetControllerState,
+0x168 State2, +0xe8 GetNodePresent) only reports controllers when strncmp(deviceModel, "Oculus", 6) == 0 (next to
the Go check deviceModel == "Oculus Pacific"); Lepton's model is "Valve Lepton" → Go/unknown → only Go masks polled,
buttons dead. unity_oculus_check revision 4 (oculus_model_checks, Unity 2019 only) turns the 17 compares' length
into 0 (orr w2, wzr, #6 → mov w2, #0 after the adrp/add of the "Oculus" literal; the string stays: it is also
Unity's VR device name). Device result: libunity now polls ovrp_GetControllerState(0x3) (Touch); owner-confirmed
2026-10-06: buttons work. Its first controller vibration then hit OVRPort's haptic-envelope bug (11 GB, OOM kill) →
haptic_fix is now suggested for every game with libOVRPlugin.so (settings detect), BattleSisters works. Ruled out before: ProductName, GetNodePresent, the device-model string itself, exports.
Accounting+ works (owner, 2026-10-06).
Vader Immortal (UE4, GitHub #49, 2026-10-06, headless): stuck after the intro on an in-game image (the splash
quad ends ~6 s in; then the game's own projection frames, 72 fps, balanced xrBeginFrame/xrEndFrame). Not the Platform
SDK (frame.ovr_trace: only user + entitlement, both answered) and not the repack's Frida gadget (OVRPort's
patch_clean_up_frida removes its loadLibrary). It leaks ~430 GPU mappings (/dev/dri/renderD128) and ~20 MB a
second (6.5 GB + swap after 5 min, then 26 fps): page-fault stacks (perf -e page-faults, offsets resolved with the
process maps + vrclient.so's own symbols; its text segment is at file offset + 0x4000) end in SteamVR's runtime:
xrBeginFrame → CSxrCompositorOpenVR::BeginFrame → SubmitExplicitTimingData → CVRCompositorSharedTextures:: BeginGPUTimingCommandBuffer and xrEndFrame → CVRCompositorClient::SubmitWithArrayIndexAndTime. Ruled out: the
layer color scale/bias + image layout structs (FrameBridge strip_color_bias 1/2, diagnostic), Valve's Vulkan
layers (VK_INSTANCE_LAYERS=""), array swapchains in general (Lucky's Tale/I Am Cat flat). Kernel tracepoints aren't
allowed for steamos. FrameBridge: layer_debug logs xrDestroySwapchain and per-5 s xrBeginFrame/xrEndFrame counts;
frame_balance (ends an open frame before the next begin) exists but Vader never leaves one open. Next (built,
not yet run: the Frame slept): layer_debug also counts xrAcquire/Wait/ReleaseSwapchainImage per 5 s (hooked only
with layer_debug, after the other conditional hooks) to see whether Vader skips a wait or release: it doesn't (361/361/361 per 5 s).
FrameBridge snapshot=N (snapshot_gl.c, GLES): every N s the left-eye image the game submits is read back on its
own context and saved as files/fb_snap_0-7.ppm (quarter size) — headless launches show a black headset view, this
shows what the game draws. Vader: Lucasfilm logo (an OBB mp4: video works), then its loading card (portrait + segmented
bar) that never advances; the async loader thread sleeps and OBB reads stop (~168 MB of a 2.7 GB pak).
SteamVR overlay apps (agent v75, 2026-10-10): host OpenVR overlays (VRApplication_Overlay) are composited over Lepton (Quest) games on the Frame (probe checker seen over 4XVR in the headset view, native/vroverlay_probe); Windows ones register through Proton's vrclient too (Temporal Reality's Windows build created its overlays; fpsVR never reaches OpenVR under Proton). Detection analysis/vroverlay.py (bundled .vrmanifest with is_dashboard_overlay, else OpenVR + IVROverlay + self-registration markers, engine Other; Rift fingerprint RIFT_ANALYSIS), patch pcvr.vr_overlay (param autostart), Linux entry fields vr_overlay/vr_overlay_autostart (game page switch); agent register_vr_overlay/unregister_vr_overlay/launch_vr_overlay (IVRApplications via ctypes, live; manifest <anchor>/frameport-overlay.vrmanifest, the app's own key, binary = launch.sh). SteamVR on the Frame reads only binary_path_linux_arm (binary_path_linux alone is skipped). Overlay apps: no launch test, no Linux Steam-parent watchdog (FRAMEPORT_OVERLAY), not a "running game". A Steam overlay-app shortcut and a game shortcut run together. SteamVR's SetApplicationAutoLaunch didn't persist on the dev Frame (the watch never started by itself) → agent v76: FramePort's own autostart: the deployment's overlay.autostart is the source of truth; ensure_host_fixes (ensure_overlay_service) keeps the user service frameport-vr-overlays.service (_vr_overlay_watch, 5 s polls of the known vrserver's /proc stat) only while an overlay app has autostart on (register/unregister, uninstall, purge and the kill switch ~/.local/share/frameport/vr-overlays.disabled / FRAMEPORT_NO_OVERLAY_AUTOSTART update it); a new vrserver (pid + start time, kept in ~/.cache/frameport-vr-overlays.json across agent updates) → child _vr_overlay_round: wait for IVRApplications, 15 s grace, launch_vr_overlay for apps not running (folder process or GetApplicationProcessId); log ~/.local/share/frameport/vr-overlays.log. Dev Frame: adopted the running vrserver, skipped the already running watch; a start after a real SteamVR start/boot is not yet seen. Details: docs/FRAME_RUNTIME.md "Overlay apps".
Vader's leak solved / AC Nexus flips (2026-10-14): Vader Immortal leaves GL errors pending on its context; SteamVR's GL interop (openxr_graphics_gl_openvr.cpp, xrclient_<pkg>.txt in Steam's logs: SXR_GL_CHECK: error:0x502 ... glGenTextures) takes the stale error as its own failure once, and from then on CVRCompositorSharedTextures::BeginCommandBuffer allocates a command buffer every frame (perf page-fault stacks; ~430 renderD128 mappings + 22 MB/s). Adapter gl_error_clear (GLES, catalog on for Vader ep1-3) clears them before xrCreateSwapchain/xrBeginFrame/xrEndFrame: flat memory, no SXR_GL_CHECK (headless). The per-game xrclient_<pkg>.txt logs are worth reading for runtime errors. The Frame's runtime caps swapchains per session (XR_ERROR_LIMIT_REACHED from ~15; AC Nexus asks for more): flip emulation now turns quads upright in place (scratch VkImage, once per acquire; transfer usage added to single-layer colour swapchains) instead of a shadow swapchain per quad, which ran into the cap (#138). Headless sessions don't draw AC Nexus's panels (adapter flip_dump saves before/after images of flipped quads, for headset runs).
Mixed reality: scanned rooms (agent v92, 2026-10-16, branch mr-scene; docs/mr/): Quest MR games read the room
through Meta's scene APIs; the Frame offers none, and Valve's chaperone is no room (2D outline raised to 2.43 m, often
the default 1 m circle -> the Android runtime's STAGE rect falls back to 1x1 m; XRService's plane detection is
horizontal-only, its persistent map sparse ORB, its XRIPC single-client; cameras are XRService's alone,
IVRTrackedCamera gives no frames). The passthrough's RoomView 3D depth mesh is readable (docs/mr/PHASE0.md:
IVRBlockQueue_005 /xrservice/roomview3d/meshData|meshLayout, metadata through the C++ IVRPaths_002 on the block
handle, meshIsUpToDate on the queue handle; 132x128 fan, ~30/s; raw->standing moves during a session -> convert
with the final one), so FramePort scans rooms itself: agent room_capture (Background/Overlay OpenVR app, prompts =
artifacts/roomscan/.png via IVROverlay_028 slots 1/3/20/22/35/43/44/63, scripts/mr/render_prompts.py) -> .fprs
dump -> fp_roomscan (native/roomscan, freestanding, offline) -> room.txt + room-mesh.bin
(docs/mr/ROOM_FORMAT.md; outline = positive shoelace in x-z, wall inward normal (-dz, dx)) in
~/.local/share/frameport/rooms/<universe>/; launch.sh copies it next to settings.conf (_room_sync, games with
scene_emul); FrameBridge (scene_room.c/scene_meta.c/scene_anchors.c) serves N walls, volumes (BOUNDED_3D),
planes, GLOBAL_MESH (XR_META_spatial_entity_mesh), XR_META discovery/persistence (newer Meta SDKs and MRUK), anchors
saved in files/framebridge-anchors.txt; all through xrshim (OVRPort's dispatcher drops unknown META calls).
scene_emul is now suggested for every USE_SCENE/USE_ANCHOR_API game (migration scene_emul_all; NOPE and Pinball
FX had none). PC: Frame page "Room" card, frameport frame room scan|status|from-boundary|import|export|build|show.
Real data (owner's room, 2026-10-16): floor at 0, table surfaces, orthogonal walls found; the ceiling needs "look up".
Unreal's self-restart (agent v90, 2026-10-13): UE 4.21+ restarts the app once after its first-run shader
precompile ("optimizing" screen; r.ProgramBinaryCache.RestartAndroidAfterPrecompile): it starts its own LAUNCHER
activity and calls System.exit(0); Lepton ends the container with the first process (the restarted one with it).
Skipping AndroidThunkJava_RestartApplication or the cvar = 0 (accepted via UE4CommandLine.txt -ini:) left Sports
Scramble on that level for good. launch.sh (RESTART_BLOCK, upgrade_launchers adds it) starts Lepton once more when
_restart_requested sees the app's own LAUNCHER start (system log, uid >= 10000) <= 5 s before its exit; first run
kept as launch-before-restart.log; FRAMEPORT_NO_RESTART=1 off. Owner-verified with Sports Scramble's first start (2026-10-13).
xrWaitFrame runs ahead (Journey of the Gods, 2026-10-14, owner-verified): with real frames (headset on) the
Frame's runtime sometimes returns from xrWaitFrame at once with a display time less than one refresh after the frame
just submitted, and then rejects the second submission for that refresh (VRCompositorError_AlreadySubmitted in
Steam's logs/xrclient_<pkg>.txt; FrameBridge pacing above the display rate). VrApi games that render faster than
the display ran ahead (~120 submits/s at 90 Hz) and the scene slid and snapped back. The VrApi bridge re-waits when
the new display time is under half a period after the last submitted one (pacing 2s: counters). Headless runs only
submit empty frames and never show it. Lepton's logcat mirror also missed the whole verifying session (launch.log =
Lepton's own lines only): the keeper now catches a silent mirror too (agent v91).
Lepton's logcat mirror dies (agent v67, 2026-10-09; v91 2026-10-14): Lepton's mirror (podman exec -it <container> logcat -v threadtime,color) fails three ways: logcat: Unexpected EOF! right after the game starts, Logcat read failure, or running and writing nothing at all (Lepton 3.0.5: seen twice on 2026-10-14, once in the owner's session, once headless). launch.sh's _logcat_keeper reads podman exec <container> logcat -v threadtime -T <epoch> itself into launch.log after a failure line or 20 s without a logcat line (from the launcher's start when the mirror never wrote, else from just before its last line), restarts it from where it ended (≤5), stops it when Lepton's coloured lines come back, sets the container's log buffer to 16 MiB (logcat -G), and logs every decision to <base>/logcat-keeper.log. Checked on the dev Frame: a mirror killed mid-game (hand-over without a gap) and a silent mirror (the whole session recovered).
Double launch (agent v57, 2026-10-06): a second Play while Lepton still boots (~10 s with nothing to see) made
the second Lepton stop the first one's container ("Waiting for steamlaunch- (PID …) to exit", exit 137
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
flock on <base>/.launch.lock (fd 9, inherited by Lepton; the watchdog and dashboard helpers close it with
9>&-); a second launch exits 0 and logs to <base>/launch-dup.log. upgrade_launchers adds it to existing
launchers. Verified on the device (second launch during boot: ignored, the first kept running). Vader on its loading
card ignores input too (owner pressed/held every button: presses reach the runtime, the card never changes).
Issue triage round (2026-10-06, owner's headset): vibration in OVRPlugin games = 2 s vibrations updated per frame and
stopped with amplitude 0; FrameBridge turns amplitude 0 into xrStopHapticFeedback (else each buzz ran 2 s), haptic_fix
uses the envelope RMS (was its peak), per-game haptic_scale → Creed/The Boys/Jurassic World fine. Lambda1VR (TBXR):
loader named by Build.MANUFACTURER (frame.tbxr_vendor), no graphics extension enabled (FrameBridge adds
XR_KHR_opengl_es_enable + asks for the requirements on -50), always multisampled render-to-texture (GL shim gives
single-sampled stand-ins when GL_EXT_multisampled_render_to_texture is hidden) + its xash/ data → works. Jurassic World:
frame.vrapi_stub. Metro Awakening: contributor's vkshim fixes + hide_space_warp + no VALVE_rpo (jumping polygons like
ITR2). Pinball FX VR: hide_space_warp (stutters). Eleven: a Meta online request fails after platform init → needs Meta
services. Star Wars Tales: top half black + freeze after loading; ruled out: asset-file paks (never requested), Valve
foveation (off: no change), GL errors (MESA_DEBUG=1: none), EGL_BAD_ACCESS once in OVRPlugin init (harmless); the game's
eye image reads back black. BlazeRush: VrApi bridge exports + ovrToString stubs + avatar stub get it to the menu room
at 72 fps, full input reaches it (diagnostics input 5s: Touch type only, sticks 1.0, buttons, poses 0x8f), but the room
draws no controllers and ignores input; avatar loader forced to its "Failed" path (no logged-in user) changed nothing.
frame.ovr_trace can't trace Unity games (P/Invoke dlsym; nothing imports ovr*). Headless: the VrApi bridge's 30 s
head-pose deadline ends VR mode without a worn headset (not a game bug).
Lepton storage (2026-09-30): each app's /sdcard (= /storage/emulated/0 → <base>/lepton-data/external) has Movies/Download/Documents symlinked to the Frame's ~/Videos/~/Downloads/~/Documents (liblepton/mounting.sh, only if they exist at start); agent v24 storage_targets reads that mapping. Android's MediaProvider canonicalises paths to /home/steamos/... and rejects every file ("doesn't appear under [/system/media...]"), sm list-volumes is empty: the media index never works, apps must browse folders. Lepton installs with adb install -g (runtime permissions granted, MANAGE_EXTERNAL_STORAGE too). Files: install/files.py, frameport frame send|storage, GUI Files tab (formerly Frame → Send files).
SteamVR per-app settings (2026-09-30): editing steamvr.vrsettings while SteamVR runs is lost; the web API (127.0.0.1:27062 /app/setsettings) needs x-steamvr-secret. native/vrsettings = fp_vrsettings.exe (freestanding, OpenVR FnTable:IVRSettings_003 as a Utility app, loads SteamVR's bin/win64/openvr_api.dll) sets them live and SteamVR persists them: section steam.app.<shortcut appid>, keys preferredRefreshRate (float) and motionSmoothingOverride (0 global, 1 on, 2 off, 3 always). Steam Link (vrlink) lists the Frame's rates 72/80/90/96/108/120/144 in vrserver.txt and follows the per-app preference ("host preferred N Hz"; whether the key is honoured is unverified in-headset yet). Judder metric: vrcompositor.txt session summary dropped + "Timed out. N total" (Stormland: 0 dropped but 313 timeouts in 2 min); fpsVR (%LOCALAPPDATA%\fpsVR\*.json, 0.1 ms histograms) gives p99 CPU/GPU ms. pcvr.steamvr_tuning (default on, PC only) applies on Play: highest rate whose budget ≥ p99×1.05, at least one step down, smoothing on.
Language packs (merged from PR #20, 2026-10-04): overport's libovrplatformloader.so is a dispatcher that dlopens Meta's own loader (libovrplatformloader_meta.so / _meta_q1.so, also libpxrplatformloader.so) and keeps its own message queue; ovr_LanguagePack_GetCurrent/SetCurrent are 8-byte return 0 stubs in it, ovr_AssetFile_GetList forwards to Meta's loader. frame.langpacks (opt-in, native/langpack) serves <tag>.lang files from the game's data; elf.hide_exports marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's is_symbol_global_and_defined is from memory). libfp_langpack.so is built here (python native/build.py --only langpack) and committed. Owner-verified 2026-10-04: Deadpool VR with only en.lang and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get lang_packs filled after an app update (library._refresh_data_fields). The library logs to logcat under the tag fp_langpack (dirs looked in, packs found, every language-pack call), so it shows up in the game's launch.log; an ovr_AssetFile_GetList the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its Metadata equals the game's own version string (ULanguagePacksSubsystem compares it with %s.%s.%s.%s.%s built from the build info, e.g. 1.0.40.356975.Quest = versionName; found by disassembling libUE4.so): the patch writes the APK's versionName into the library (@FPMETA@ slot, with_metadata), FRAMEPORT_LANGPACK_META overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled /sdcard/Android/obb/<pkg>/x.lang; with the /storage/emulated/0/Android/obb/<pkg>/x.lang spelling (Unreal's own, now listed first in scan_dirs) German dialogue plays. FRAMEPORT_LANGPACK_SKIP=<tags> or a file fp_langpack_skip in the obb folder leaves packs out of the list (experiments).
Cube swapchains (GitHub #107, 2026-10-09): the Frame's runtime refuses faceCount=6 swapchains (-2,
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
ovrp_EndFrame4 (memset). FrameBridge cube_standin (default on, native/adapter/cube_standin.c) serves a refused cube
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
Hardware video decoding (PR #96, PR #128 by Lucas-Mathieu, adapted 2026-10-10, agent v71): native/hevc = one
OMX plugin OMX.frameport.{avc,hevc,vp9}.decoder (FFmpeg v4l2m2m on Iris /dev/video-dec0, FFmpeg software decoders as
fallback inside the component, Vulkan copy for big native surfaces). The connection installs it once per Frame
(ensure_video_codec → agent video_codec_status/install_video_codec: ~/.local/share/frameport/video-codec/versions/ <manifest sha> + current symlink). A Frame keeps the same or a newer revision (no flip-flop between PCs) → bump
revision in native/hevc/build.py with every artifact change; a failed install isn't retried during the connection.
Per game: only launchers of games whose recipe has frame.hw_video_decode (install stage, no APK change; suggested
from analysis media_codec, ANALYSIS_VERSION 8; deployment.json hw_video_decode) get the codec line → the Podman
wrapper (native/hevc/podman.py) mounts the plugin into that container (merged media_codecs.xml in
$XDG_RUNTIME_DIR/frameport-video). Off for every game: Settings → Installing (library video.hw_decode → agent
video_codec_switch = video-codec/disabled); one game: FRAMEPORT_NO_HW_VIDEO=1 %command%. upgrade_launchers
converts agent ≤70 launchers (Batman's <base>/frameport-codec → keeps the line, folder then removed). Batman: catalog
- migration
batman_video_patches(frame.hw_video_decode+ hidden adapter settingsurface_native). Iris: 8K refused (ENOMEM) while any other decoder session is open; SteamVR's vrlinkrunthread holds one ~12 s at game start → refused opens retry until 20 s after the plugin loaded (else 2 s), then software; VP9 7680x3840 never returned a picture → VP9 ≤4096x2304; hidden VP9 frames come back as empty capture buffers (bytesused 0, no LAST): FFmpeg's wrapper ended the EOS drain at the first one (two-pass VP9 lost its last ~25 frames) → build.py requeues them (codec revision 8; drain ends at LAST, or 1 s after a skipped empty buffer; dev Frame 2026-10-10: 600/600 = OMX.google.vp9 Y hashes). Rebuilds are deterministic (ext4 + NTFS path with spaces, two NDK copies → same; rev 8 141de01f…; PR #128's own sources → its 27a2d749…). Not yet run on the device. Vulkan shader dump (GitHub #140, 2026-10-10): adaptervk_shader_dump=1(Vulkan shim,native/vkshim/shader_dump.h; applies where vk_sanitize can: Unreal/Other arm64) writes each distinct SPIR-V module once tofiles/fp_vk_shaders/<size>_<sha256>.spv(tmp + rename) and oneindex.txtline per vkCreateShaderModule (<seq> <ms> <unix ms> <name> new|known|again|failed, O_APPEND) to find the module behind a GPU hang. Agent v72collect_diagreturnsshaders(newest modules of fp_vk_shaders + fp_spirv, ≤4 MB each, base64) → bundlegames/<pkg>/target/shaders/. Triagegpu-hangsuggests both dumps;triage.graphics_api(FrameBridge's xrCreateSwapchain formats: <0x1000 Vulkan, else GL) keeps only the session's API's (API_ONLY). Host-tested only. Agent v74: a reporter's hang came ~10 s after the last new module, so the culprit wasn't among the newest written → with an index naming modules,shader_dumpssends every module of the index's newest session (after the last# start), deduplicated, last used first, each ≤4 MB, ≤30 MB in all (session= how many it names) + that session's index lines; no index (fp_spirv) = the old newest-by-mtime 4 MB. One JSON line (~40 MB base64) over the SSH channel is read whole byFrame.agent(fine at that size);_Writer.fitdrops the least recently used modules last, after cutting logs, to stay under the 24 MB zip limit. Unreal OBB check (GitHub #159, 2026-10-10): Epic's DownloaderActivity checks OBB name+size (OBBData), then withbVerifyOBBOnStartUp=trueCRCs the whole OBB; its screens are invisible in Lepton (stuck atDisplayed …/.DownloaderActivity, triageunreal-obb-check-stuck) →frame.unreal_skip_obb_check(analysisunreal_verify_obb, ANALYSIS_VERSION 11) flips the manifest boolean (axml.set_meta_data_bool); skips only the CRC pass. Host-tested only. Issue round 2026-10-12 (agent v83): Lepton pastes STEAM_COMPAT_DATA_PATH unquoted intosh -c "ln -s …/internal/<pkg> /data/data/<pkg>"(mounting.sh): a game on a card named "Steam Frame 512g" got no internal storage (no saves/prefs; Beyond Sandbox's EOS then loopedcreateKeychain→resetKeychainforever, 1.85 GB log). Quest launchers with an unsafe path nowln -sfn <base> ~/.local/share/frameport/lepton-paths/<pkg>at every start and give Lepton that path (app_dir_lines/set_app_dir/launcher_base; upgrade_launchers converts; deployment.json keeps the real base). Verified on the device with a spaced copy of Stremio (old: thelnerror; new: /data/data link, shared_prefs writable).frame.eos_keychain_loop(opt-in, triageeos-keychain-loop) returns from EOSSDK.resetKeychain. Wander 0.3570 (Unity 6.5, Firebase, Gecko) ran out of bionic's 128 pthread keys →frame.unity_tls_key_pool(Klownicle's approach, #198: libunity's 4 key imports renamedfp_tkey_*viaelf.rename_imports,libfp_tkeys.sopools destructor-less keys behind one real key); device: past Firestore, 72 fps. Cpp2IL releases can't read metadata v107 (Unity 6.5) →tools/cpp2il.pyfalls back to the development CI build (nightly.link) on that error. Vermillion's canvas is an OVROverlay quad (drawn above the hand) →frame.vermillion_scene_canvas(PaintableMesh getters → false; installed build checked, 72 fps headless; headset not yet). Linux apps:desktop_iconswitch, a deleted desktop copy stays deleted (#212). Findings: OLAR (#201) RHIThread null deref in libUnreal (ldr x8,[x19,#792], x19=0) not fixed by hide_space_warp / vk_hide_fdm / no Valve layers; FNAF HW (#135) "freeze" = first-start GL program-binary-cache compile on Zink (threads busy in zink/disk cache), not a hang; VR4 (#199) reporter's patch.203 OBB truncated. Upload benchmark (scripts/upload_bench.py, #242): home Wi-Fi caps every method at ~11 MB/s; paramiko uses ~3x OpenSSH's CPU; Windows OpenSSH refuses FramePort's key file (ACL "bad permissions") until its permissions are tightened. OpenSSH uploads (2026-10-15, #324/#242): sftp escapes* ? [inside double quotes itself (and undoes it for put's destination), sonative_upload.quoteescapes only\\/"; Win32 sftp turns every local backslash into/(escapes too) → local paths go asC:/…. Logins:Frame.auth= the attempt that worked (app_key / user_key = a key file from Settings → Installing → SSH keys, libraryssh.extra_keys, CLIframe ssh-keys/ agent / password). OpenSSH strict (FramePort + Settings keys, IdentitiesOnly) unless auth was "agent"; a refused strict login retries relaxed (+ private copies of ~/.ssh/id_{rsa,dsa,ecdsa,ed25519}: OpenSSH skips its defaults once any -i is given, + the agent);-vkey lines go into the error. FramePort doesn't add its key to the Frame for this. Windows-tested. Meta user id (GitHub #182, 2026-10-11): OVRPort's platform loader (runtimes 3.4.3-23204ea and -aa54c3f) answersovr_GetLoggedInUserID(and two other callers) without a Meta account with a made-up id from a helper (stp; mov x29, sp; bl random; …; mov/movk x9 = 0x53444835ec580000 (6e18); add x0, x8, x9; ldp; ret), kept only in memory → a new id every start; Walkabout Mini Golf files saves underProfiles/Oculus/<id>/and lost all progress.frame.ovr_stable_user(default_on, detect = the game has libovrplatformloader.so) rewrites the helper tomov x0, <6e18 + 32 bits of sha256(package, keystore)>; ret: stable per game and per user (keystores are personal), so online games never see two FramePort users as one. Found in every PATCHED build checked. Host-tested only; upstream candidate. Vivox API 31 (GitHub #101, 2026-10-09): newer Vivox builds (Green Hell VR) call Android 12 AudioManager communication-device methods fromcom.vivox.sdk.AudioChangeListenerwith no SDK check → NoSuchMethodError on Lepton's Android 11.frame.vivox_audio_route(analysisvivox_api31, ANALYSIS_VERSION 4) makes every such method return at once (Dex.return_early: return-void /const/4 v0,0; return v0; nopping the invoke would leave a move-result the verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified headless: Vivox initialises, 150 s at ~65-70 fps. Steam Input gamepad for 2D apps (GitHub #162, agent v72, 2026-10-10): Lepton's Android only has the Wayland seat'swayland_touch/keyboard/pointer. Steam makes its virtual pad (uinput,/devices/virtual/input, 28de:11ff, "Microsoft X-Box 360 pad N", ACL for steamos) only while the Frame's controllers are on (controller.txt "Steam Controller reserving XInput slot 0"; gone when they sleep), also with no game running; steamos-manager's 28de:0000 keys device isn't a pad (no BTN_SOUTH). Lepton's container root (system_server too) = steamos (keep-id:uid=0), so a bind-mounted/dev/input/eventNopens. Opt-indevice.steam_gamepad(vr_kind none; suggested forandroid.hardware.gamepad/LEANBACK_LAUNCHER, analysisgamepad, ANALYSIS_VERSION 9): deployment.jsonsteam_gamepad→ launcherGAMEPAD_LINE(after the codec line: exports FRAMEPORT_GAMEPAD + LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD, putsagent/binfirst on PATH) →PODMAN_WRAPPER(written by ensure_host_fixes; non-runcalls go straight on;runimports the agent'spodman_run_args; hands on to the next Podman after its own PATH entry with itself removed from PATH, so it chains to the codec wrapper either way round) adds the pads +Vendor_28de_Product_<pid>.kl(Xbox 360 layout). Verified headless with a uinput stand-in pad (Stremio's container: EventHubclasses=0x80000141, our key layout, KeyEvent BUTTON_A dispatched); with Steam's real pad and a game: not yet. Pads made after the start aren't seen until the next one. Multiview programs on flat framebuffers (GitHub #77, Doom3Quest, 2026-10-09, prototype): Mesa enforces OVR_multiview's "program num_views == draw framebuffer views" rule (draw_validate.c, the draw is dropped silently); Qualcomm doesn't. Doom3Quest compiles every VS withlayout(num_views=2) in;and draws its HUD/PDA into 2D-texture FBOs → black. Opt-inframe.gl_multiview_fbo(analysisgl_multiview_libs, ANALYSIS_VERSION 6; own-engine GLES only):native/glmv=libfpglmv.so(12 chars = "libGLESv3.so": libdoom3.so's one.rodatadlopen string is rewritten in place, its qgl* table comes from dlsym on that handle; also first DT_NEEDED for its direct gl*/egl* imports). Such draws use a lazily built single-view twin (view 0, uniforms copied per draw); eglMakeCurrent resets the per-thread cache. Host-tested only (rewriter on all 19 Doom3Quest shaders + glmv.c against a stand-in GL,tests/test_gl_multiview_fbo.py); the host's Mesa 23.2 llvmpipe has no OVR_multiview. Headset-verified by Xandrix1987 (2026-10-09: HUD/PDA shown, no fps drop) -> in the Doom3Quest catalog recipe. BlazeRush (GitHub #57, Klownicle's verified guide, 2026-10-10; FramePort's version headless-tested only):frame.blazerush(1.0.349 only: exec-segment SHA + every word checked, offsets from PT_LOAD) NOPs the avatar-readiness wait (0x4b2dbc), RETs the avatar renderer (0x4b2adc), renamescache/shader_cache→cache/br_shader_v1. GL shim (now also exports glShaderSource/glLinkProgram/glProgramBinary/glDeleteProgram/glDraw*: pass-through unless a setting is on)gl_int_attribs(integer inputs fed via glVertexAttribPointer re-set with glVertexAttribIPointer per draw, reflection cached per EGL context + program, dropped on link/binary/delete) andgl_highp_markers(vertex shaders with every '|' marker: mediump→highp except on in/out/uniform lines); host testtests/test_blazerush.pywith a stand-in GL. FOV: the game reads VrApi props 7/8 right after vrapi_Initialize (no session; the Frame has no XR_EPIC_view_configuration_fov) → the bridge saves the live FOV (also located before tracking, headless: 118.87×120.04 on the dev Frame) infiles/framebridge-vrapi-fov.txtand answers with it at the next start (logFOV properties: … (saved by an earlier session)).device.config_sync(catalog fieldconfig_sync, agent v78): user_config.xml r_3dwidth/height, r_width/height from the runtime eye size (FrameBridge logsruntime recommended eye size) × scale, written at finalize, set_settings and by launch.sh before Lepton starts (_config_sync). Headless runs at scale 1.7 end inzink: DEVICE LOSTright after the bridge's 30 s no-tracking deadline tears the session down (gpu fault at the teardown; not at scale 1.0): a headless-only artifact, the headset never hits that deadline. Launch tests count an exit/crash after such a line as a pass (triage.yamlheadless_end, findingheadless-end; GitHub #57 "Failed to launch" on install). Session triage (agent v70, 2026-10-09): launch tests never reach FOCUSED, so real play sessions are triaged too. list_installed gives each gamelast_play{start, end, test} from<anchor>/plays.log(launch tests write atest <unix>line first → test sessions are only marked);session_logreturns the newest session's launch.log (<base>/session.log, ≤4 MB: head + tail + FrameBridge/crash lines between) + that session's logcat-crash.log +journalctl -kGPU lines ("kernel: …"). The GUI's connection refresh (app._check_sessions→pipeline.sessions_due, background thread; sessions >7 days old are skipped) runspipeline.triage_session=validate/session.analyze(triage.yaml signatures incl.space-warp-used(info,question:, never auto-applied),gpu-hang(report: true) - computed
slow-frames(pacing windows vs the nearest refresh rate) andfocus-dips(FrameBridge always logsfocus: lost/focus: back after N ms, session_fixes.c)) → librarylast_session/last_session_checked→ game page "Last session" callout (Try this fix / Rebuild with this fix / Yes-No question / Report / Dismiss). Suggestions may be values (adapter.scale=0.85,triage.split_suggestion); adapter-only fixes are pushed live (apply_suggestions_live→ agent set_settings, like the Game settings dialog). CLIframeport session <pkg> [--apply]. PC VR (Proton) launchers log no session end, so they aren't triaged yet. pac_hints stays triage-only: a survey of all 68 dump APKs found unpaired PAC hints in 21 libraries of 18 games (OpenSSL's 38/40 in most UE4 libUE4.so and libEOSSDK.so, UE5 libUnreal.so ~400 vs +2-3, libass, libopencv, …), most of them games that work, so default-on would rewrite many working builds. Unresolved (as of 2026-09-28): Arcsmith (right-eye distortion) and Time Stall (both eyes) — swap, tracking, Valve layers, depth, pacing ruled out. Sniper Elite VR (DEVICE LOST), Espire 1 (Mesa GL upload crash), HITMAN 3 (freedreno crash): use PC versions.
Install links / FrameDrop button protocol: deeplink.py parses framedrop://, frameport:// and https install links (strict URL rules, manifest ≤256 KiB, downloads into <data>/downloads/), pipeline.add_from_link, views/link_dialog.py, CLI frameport open-link; urlhandler.py registers a script (bundles can't take URL arguments), <data>/links/*.link + gui.alive heartbeat; Settings → Install links (links.framedrop/links.frameport); ui/dropped.py, pipeline.add_windows_exe, device.display_mode; demo link deeplink.DEMO_MANIFEST. → docs/DEVICE_NOTES.md#install-links--framedrop-button-protocol
Install link updates: manifest version → library link.version; pipeline.check_link_updates (GUI start, 6 h, setting links.check_updates = Settings → Updates → Games) marks link_update → Library bar / game page → link_dialog.update_from_link; CLI frameport link-updates; the demo link is never checked. → docs/DEVICE_NOTES.md#install-links--framedrop-button-protocol
Releases, CI, GitHub
Maintainer-only notes (accounts, credentials, key locations) live in the git-ignored CLAUDE.local.md.
Public repo github.com/spoopyghosty0/frameport (branch main). Push a v* tag → CI (.github/workflows/build.yml)
tests, builds Windows x64 / macOS arm64 / Linux x64 bundles, signs, attests and publishes a GitHub Release
(FramePort-*.zip/.tar.gz, the CLI wheel frameport-<ver>-py3-none-any.whl, SHA256SUMS.txt,
FramePort-selfsigned.cer; notes = "What's new" from the annotated tag message + the short packaging/release-footer.md; owner: keep release
notes short — a few "What's new" bullets, nothing long after them).
Installed apps find the release themselves (self-update), so the notes are what users see in the update dialog.
- Dev builds (for testers, no release): Actions → build → Run workflow (main), "dev" ticked, optional "notes"
(what to test). The bundles get version
<next patch>.dev<run number>(scripts/dev_version.py, not committed) and replace the rollingdevpre-release (dev-releasejob; same assets + SHA256SUMS). Automatic update checks ignore pre-releases; testers use Settings → Updates → "Install the latest dev build…" (updates.check_dev,Updater.install_dev).parse_versionsorts 0.9.0 < 0.9.1.devN < 0.9.1, so testers get the next release normally. - Upstream trackers: issues labelled
upstream(#73 microphone, #74 haptics, #75 VrApi bridge) list our workarounds for OVRPort bugs and how to drop them; check them against OVRPort's latest release before each release. - Release checklist: bump
src/frameport/_version.py(the only version;scripts/package.pyfails a tag build whose tag ≠v<_version>), commit,git tag -a vX.Y.Z -m "FramePort X.Y.Z" -m "<What's new, Markdown bullets>", push the commit and the tag. Never publish a release without itsSHA256SUMS.txt(the updater refuses it) and keep the asset names (updates.ASSETS) — renaming them breaks updating for every installed copy. - Signing is free/self-signed by the owner's choice (no paid certs, no SignPath): Windows binaries are signed with
a self-signed "FramePort (self-signed)" code-signing cert (RSA 3072, valid to 2031, SHA-256
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); secretsWINDOWS_CODESIGN_PFX(base64) +WINDOWS_CODESIGN_PASSWORD. The private key is never committed (its location is inCLAUDE.local.md). macOS is ad-hoc signed only (Gatekeeper needs right-click → Open; notarization would need the paid Apple program). Users still see SmartScreen unless they import the .cer into Trusted Root. - CI gotchas:
flet buildneeds--yes --no-rich-output(it prompts to install Flutter; rich output crashes the Windows console) plus PYTHONUTF8; macOS builds need--python-version 3.12 --arch arm64(cryptography has no wheels for flet's default Python / x86_64 cross-build), with a PyInstaller fallback step;astral-sh/setup-uvhas no floating major tags after v7 → pin the exact version; force-moving a tag starts duplicate runs (cancel one). jni(2026-10-06): jni_flutter 1.0.4/1.0.4+1 generate bindings that require jni ^1.1.0 while Flet's build template pins jni 1.0.0 → everyflet buildbundle failed (JniVersionCheck, "generated bindings expect package:jni ^1.1.0"). pyproject[tool.flet.flutter.pubspec.dependency_overrides] jni = "1.1.0"fixes it (pinning jni_flutter 1.0.4 does not); drop it once Flet's template moves to jni 1.1. A failed tag build publishes nothing: delete and re-push the tag on the fixed commit.- macOS runner (2026-10-02):
macos-latestjobs went unassigned (cancelled after 15 min, no steps);macos-15's Xcode 16.4 fails a Flutter plugin (NWPath has no member);macos-26(Xcode 26) builds withflet build. The PyInstaller fallback (package.py --pyinstaller) passes--yesso a failedflet build's folder doesn't stop it at a prompt. - Windows PowerShell calls from Python (
updates._powershell()): use%SystemRoot%\System32\WindowsPowerShell\v1.0\ powershell.exewithPSModulePathremoved from the environment — started under PowerShell 7 (CI's default shell, or a user's pwsh terminal) it couldn't run Get-AuthenticodeSignature (v0.3.0's Windows update smoke failed on it).v0.3.0is a tag without a release (that failed build); the updater shipped first in v0.3.1. - Never start PowerShell with
DETACHED_PROCESSfrom the packaged app: it exits 0 without running anything (found in the 0.3.1→0.3.2 end-to-end test: the app quit, nothing updated).updates.spawn_hidden= CREATE_NEW_CONSOLE + hidden window;apply()waits until the script has logged that it runs and raises otherwise (the app then stays open). Windows installs of 0.3.1/0.3.2 can't update themselves: they need one manual download. - The Linux bundle is built on ubuntu-22.04: a 24.04-built Flutter bundle needs GLib 2.80 (
undefined symbol: g_once_init_enter_pointeron 22.04). - The repo is public: never commit personal data — the Frame's IP address, the Steam user id, the owner's email,
local home paths (native builds use
-ffile-prefix-map). History was rewritten once to remove them.
GitHub triage (issues / discussions / PRs)
GitHub identity + credentials are maintainer-local (CLAUDE.local.md): check gh api user --jq .login before any
GitHub write. The owner approves before posting, labelling or closing anything. Reply style: "I"/neutral, never
we/our/us; never mention agent version numbers or internal tool versions; show the owner every draft in full with a
short recap of the thread so far; read all comments on a thread first (a later one often has the zip or the answer).
- Every triage also sweeps for unanswered comments, not just new threads: open AND recently closed issues, PRs
(incl. merged/closed ones) and discussions (top-level comments and their replies) whose latest comment isn't the
maintainer's, plus earlier user comments a later maintainer reply skipped. Users often ask follow-ups after a fix or
after closing (#294: two questions after the reporter closed it; #249: a Q&A discussion with no reply for days;
#161/#110: requests posted after the close). Quick check:
gh issue list --state all --search "updated:>=<date>" --json number,comments --jq '.[] | select(.comments[-1].author.login != "spoopyghosty0")'; discussions via GraphQL (discussions(orderBy:UPDATED_AT)withcomments{replies}). - Issue templates & labels (
.github/ISSUE_TEMPLATE/):[Game report]→game-compatibility(a game that doesn't work right);[Working recipe](working-config.yml) →working-config(a headset-verified config to add);[App]→bug(FramePort itself). Mislabel/relabel to match. - Working-recipe submissions → the catalog. The body's
### RecipeYAML is the contribution. Validate it with the real validator:gh issue view N --json body -q .body | python scripts/catalog_from_issue.py --issue N(writescatalog/games/<pkg>.yaml;--out <dir>to dry-run into a scratch dir first). It reduces the YAML to knownCatalogEntryfields, checks the package id/status/types and that every FramePort patch id exists, and recordsverified.issue: N. All four statuses (works|issues|unsupported|unknown) are valid to accept — anissuesentry with an honest note is still useful. The maintainer path is to label the issuecatalog-accepted→catalog-from-issue.ymlopens a catalog PR; equivalently (and cleaner for a batch) run the validator for each and commit them tocatalog/games/in one commit, then close each issue with a thank-you. Re-runscripts/patch_docs.py(catalog entries bump each patch's usage count indocs/PATCHES.md) andpytest -k catalogbefore committing. - Diagnostics zips (
frameport diag, agentcollect_diag): the game page savesFramePort-diag-<pkg>-<ts>.zip. Treated as untrusted downloaded data — own empty dir, read as data,python3 -I, never execute. Layout:app/(app.log, jobs/, settings.json),frame/(host/info/steam-console, boot logs), andgames/<pkg>/(recipe.yaml,target/deployment.json+target/launch.log,logs/*-session-*.log,package/AndroidManifest.xmlpackage/elf.json). The redactor replaces IPs/hosts/home dirs/Steam ids.
- "Did they attach a zip?" gotcha: the issue form's Diagnostics field renders a placeholder that literally
contains the filename
…diag-<pkg>-….zip into this box, so grepping for.zipfalse-matches on an empty report. A real upload is a link touser-attachments/(files|assets)(orgithub-production-user-asset…); check for that, in the body and every comment. Agithub.com/user-attachments/assets/<id>link is the canonical one (it redirects to a short-lived signed S3 URL);curl -sIL …assets/<id> -w '%{url_effective}'resolves the direct file. - 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.yamlfrommain(catalog.refresh_remote), recipes follow viacatalog_rev→ "Update on Frame". So accepting a recipe or adding a triage signature is "live" once pushed. - 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 →devticked, withnotes), tell them to install it via Settings → Updates → "Install the latest dev build…" and then rebuild the game on the Frame. Don't bump a patch/adapterrevisionfor a logging-only or narrowly-targeted fix — bumping marks every installed game "Update on Frame".
- Catalog / recipe / triage changes reach installed apps within ~6 h with no build — apps fetch
- Verifying a device fix without the headset: many runtime failures (e.g. an
xrEndFramelayer rejection) reproduce in a headless launch test on the dev Frame, so the fix can be developed and confirmed at the frame-acceptance level there; only the visual result needs the headset (hand the built dev build to the reporter, or the owner, for that).frameport settings <pkg> <key>=<value>toggles adapter settings (e.g.layer_debug=1) live, no rebuild. To reproduce a reporter's exact build, grab their APK from its upstream source (e.g. the linked GitHub release), scan → build → install → test in an isolatedFRAMEPORT_HOME(see "Dev commands" / the Frame-busy rules).
Heuristics (games not in the catalog)
Each patch's detect() suggests itself from the Analysis; applies() says whether it can matter at all (the UI/CLI
hide non-applicable patches; enabled ones are always shown). Rules learned from the 34 games:
MR-only (PASSTHROUGH required + BOUNDARYLESS_APP) → force_passthrough, + USE_SCENE → scene_emul + meta_permissions;
hand tracking required → controller_fix=0; OVRPlugin + ≥20 GiB → disable_space_warp; Unreal ≤4.21 or Unreal Meta XR
Audio → nodebug; Oculus-OS class referenced by the Unreal audio build or ≥2 Meta libs → oculusos; legacy-VrApi Unity
GLES with MSAA → unity_no_msaa; CryEngine → user.cfg r_variable_rate_shading=0; direct VrApi → bridge (+GL shim for
GLAD/GLES); Unreal → alternate no-ForceQuit build. Scoring (docs/HEURISTICS_EVAL.md, 2026-10-15): analyse the dumps
once (scripts/heuristics_snapshot.py <out.json> "<VR CyberDeck Portable>" "<PATCHED>", ~30 min, keep out of git),
then scripts/eval_heuristics.py --snapshot <out.json> --split dev|holdout|all [--fit-report] (catalog-only truth,
per-patch precision/recall, a fixed 20% holdout to catch rules fitted to one game). Read the dated score: an
"extra" on an entry verified before the patch existed is a catalog gap, and adapter/device picks are inherited by
catalog recipes anyway. 2026-10-15: strict 5/65 exact, F1 73.8%; dated 39/65, F1 88.6%; remaining misses are
one-game runtime fixes. Don't key a rule on one game's code fingerprint (an Unreal splash/depth "detect" matching
only Star Wars Tales + FNAF HW was rejected: it would also have bumped ANALYSIS_VERSION for every user). Add a rule →
re-run dev + holdout + pytest -m games.
Frame operations
- Find/connect:
frameport frame discover/frame info(the remembered Frame is in<user data>/frames.json). - Power off over SSH: plain
systemctl poweroffis refused by polkit for remote sessions; this works:ssh steamos@<frame> 'systemd-run --user --wait --pipe --quiet systemctl poweroff'.sudoneeds the user's password (not known to Claude). A reboot clears leaked kernel keyrings. - Reinstalls keep one rollback copy (
<base>/previous-game.apk,settings.conf.previous);frameport frame cleanupremoves them (and--path ~/Xextra folders under home).
Project status (2026-10-02)
Self-update, the Files tab, OVRPort 1.2.5, non-Quest Android apps (vr_kind) and GUI localisation (tr(), 0.3.x) are
in; see the sections above. Earlier state (2026-09-29):
34 Quest games ported; the owner confirmed in the headset that all FramePort-rebuilt games work: 23 work, 5 work with
issues (Arcsmith/Time Stall eye distortion, AC Nexus some flipped launch text, Phantom DLC button, Silhouette hands),
6 can't run (Sniper Elite VR, Espire 1, HITMAN, and the 32-bit Journey of the Gods / Shadow Point / Sports Scramble).
Parity: all 34 rebuilt from the dumps match the known-good builds (docs/parity-report.md, generated locally and
git-ignored; 34/34 again with OVRPort 1.2.5 on 2026-10-02) and were reinstalled +
launch-tested with 0 regressions (docs/parity-device-report.md). PATCHED/ holds exactly the installed builds.
Owner preferences: manual installs (no third-party installer apps), Python + Flet, dynamic data over hardcoding,
free tooling only, public repo scrubbed of personal data, keep the known-good backups.
Rift/PC VR support (2026-09-29): implemented + unit-tested, not yet tried with a real Rift game on the PC or Frame
(needs a Rift dump, and Proton installed on the Frame). Open ideas: exe-icon artwork for Rift games, macOS x86_64 bundle, USB-cable connection (Frame usb0, untested),
the unresolved eye distortion, and testing the release bundles on real Windows/macOS machines (never launched yet).
Conventions
- Dynamic first: fetch live data (tool versions, overport patch list/titles, artwork, catalog) with cache + bundled
fallback (
core/cache.py). Don't hardcode what can be discovered (e.g. Lepton path via appmanifests). - Keep APK edits minimal and byte-stable (parity depends on it). Every new fix: a patch module + a triage signature + a PLAYBOOK row + a unit test.
- New or changed patch, game setting, catalog recipe or triage signature → run
python scripts/patch_docs.py(regenerates docs/PATCHES.md + site/src/data/patches.json, the site's id → anchor map; a test runs--check). - Known-good backups of every working APK and the signing keys:
PATCHED/_known-good-2026-09-28/(don't delete). - Your development Frame:
frameport frame info(remembered in<user data>/frames.json); SSH assteamos@<frame>.
Steam library scheduling (#305, agent v99): install.steam_library = now/later/off; agent library_mode persists it.
Automatic restarts also hold on a fresh local frameport-app/gui.alive (10 s; existing 12 h cap).
Later journals changes and installs steam.service ExecStartPre/ExecStartPost hooks to reapply after Steam's exit
writes and verify before clearing; preserve this ordering and avoid locking across systemctl's startup hooks.
Off Play (also pending Later Play) uses detached systemd-run launch.sh, with no shortcut/devkit registration;
Steam's per-game controller/overlay/Exit game integration is lost. Restart Steam now explicitly bypasses holds,
asks before ending a running game, and closes local FramePort/Desktop Mode. Purge removes the startup hook.
Hook hardening: both lines - + /usr/bin/timeout -k 5 20; ExecStartPost only systemd-run --no-blocks 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).