Compare commits

..
Author SHA1 Message Date
spoopyghosty0 8c9391e826 ui_smoke: a warm-up screen while the web fonts load (the first shot measured text with a fallback font: wrapped and overlapping labels) 2026-10-07 17:40:32 -04:00
spoopyghosty0 2a81330f38 Selected sidebar tab: white icon on the portal fade in dual themes (an orange icon clashed with it) 2026-10-07 17:20:17 -04:00
spoopyghosty0 cf4a8c3752 Wordmark with a portal between Frame and Port; glyphs match Material icons in menus
- Dual themes: "Frame" (accent), a small tilted portal (rim from the accent into SECONDARY around a dark hole,
  generated in the active theme's colours, also for installed themes: glyphs.portal_mark), "Port" (SECONDARY)
- Glyphs: stroke 2 (was 1.75, lighter than Material's icons); menu items tint them TEXT like Material's own icons
- ui_smoke: waits until the web server listens (a fixed 6 s failed with a second smoke run on the machine)
2026-10-07 16:59:10 -04:00
spoopyghosty0 42fb5f7fa2 Portal (OLED) and Original themes, installable theme files, both portals in the Portal theme
- Built-ins: Portal (default), Portal (OLED) (true black) and Original (the former violet; "classic" still maps to it)
- Portal now uses both of the logo's portals: orange stays the main action and selection; the new SECONDARY token
  (blue) colours outlined buttons, progress bars and switch tracks; "dual" themes add the blue-to-orange sidebar
  edge, the two-colour wordmark and the selected tab's fade. Original looks exactly as before
- Theme files: <data>/themes/*.json (name, base, dual, any subset of the colour tokens), checked on install (dark
  background, readable text, known colours; T.ThemeError says why). Settings -> Appearance: "Install theme file...",
  "Copy this theme as a file" (a template), a bin icon removes an installed theme
- docs/THEMES.md + docs/themes/example-theme.json (kept valid by a test); ui_smoke --themes includes installed themes
2026-10-07 16:43:34 -04:00
spoopyghosty0 3c7efc1c15 Portal theme, FramePort logo and glyphs, live theme switching in Settings
- Logo (a monitor that goes through a tilted portal and comes out as the Steam Frame) in the sidebar and welcome
  screen; src/assets/icon.png is the bundles' app icon (flet build; PyInstaller fallback via --icon)
- 24 px line glyphs (ui/icons/glyphs, ui/glyphs.py) replace Material icons for FramePort's own concepts: Frame,
  launch test, PC, live view, screenshots, Type on Frame, verified recipe, rebuild; shown as tinted images through
  C.as_icon and the button/menu helpers (same size as the Material icons next to them)
- Colour themes, dark only: "portal" (default; orange accent, blue PC side, status colours chosen so warnings never
  read as the accent) and "classic" (the previous violet). Settings -> Appearance theme cards switch live:
  app.restyle() rebuilds the shell and drops cached views (setting ui.theme)
- Theme colours are read when building: default arguments no longer capture tokens, module-level colour maps refill
  through T.on_change; tests/test_theme.py checks both
- ui_smoke.py --themes renders the picker and every theme after a live switch
2026-10-07 16:17:19 -04:00
944 changed files with 4172 additions and 147069 deletions

No files matched your search

+12 -11
View File
@@ -1,30 +1,31 @@
name: Game report
description: 'A game doesn''t run right on the Frame, or FramePort itself misbehaves (FramePort fills this in: "Report a problem…").'
title: "[Game report] "
labels: ["game-compatibility"]
name: Problem report
description: A game or FramePort doesn't work (FramePort → "Report a problem…" fills this in and saves a diagnostics zip).
title: "[Problem] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please attach the **diagnostics zip** FramePort saved: drag it into the "Diagnostics" box below. It holds logs
and device details with personal data removed, and no game files.
Please attach the **diagnostics zip** FramePort saved (drag it into the "Diagnostics" box below). It contains
logs, the recipe and device details with personal data removed — no game files. Without the app, it can be
read with `frameport diag inspect <zip>`.
- type: input
id: game
attributes:
label: Game
description: Title and package name; leave empty for problems with FramePort itself
description: Title — package id (version, engine / XR API); empty for app problems
- type: textarea
id: description
attributes:
label: What happens?
description: What you did, what you expected and what happened
description: What you did, what you expected, what you saw (in the headset, if it started)
validations:
required: true
- type: textarea
id: findings
attributes:
label: Launch test
description: Filled in by FramePort
label: Launch test / triage
description: Filled in by FramePort from the last launch test
- type: textarea
id: recipe
attributes:
@@ -38,6 +39,6 @@ body:
id: logs
attributes:
label: Diagnostics
description: "Drag the FramePort-diag-*.zip here (max 25 MB). If the box won't take it, submit the report and attach the zip in a comment."
description: Drag the FramePort-diag-*.zip here (max 25 MB)
validations:
required: true
-7
View File
@@ -1,8 +1 @@
blank_issues_enabled: true
contact_links:
- name: Questions and ideas
url: https://github.com/spoopyghosty0/frameport/discussions
about: Setup help, how-to questions and feature ideas go to Discussions.
- name: Docs and FAQ
url: https://frameport.app/docs/
about: Install steps, game folder layouts and common questions.
+11 -11
View File
@@ -1,32 +1,32 @@
name: Working recipe
description: 'Share a recipe that works on the Steam Frame (FramePort fills this in: game menu → "Share working recipe…").'
title: "[Working recipe] "
name: Working configuration
description: Submit a recipe that works on the Steam Frame (FramePort → game menu → "Share working config…" fills this in).
title: "[Working config] "
labels: ["working-config"]
body:
- type: markdown
attributes:
value: |
Thanks! Once a maintainer accepts the recipe, it becomes built-in for everyone. Please don't share game files
or links to them.
Thanks! A maintainer reviews the recipe; once it's labelled `catalog-accepted`, a pull request adding it to
`catalog/games/` is opened automatically. Please don't paste game files or links to them.
- type: input
id: game
attributes:
label: Game
description: Title and package name (e.g. Lucky's Tale — com.playful.LuckysTale)
description: Title — package id (platform, version, engine / XR API)
validations:
required: true
- type: input
id: result
attributes:
label: Result
description: works or issues
description: works or issues (+ the last headless launch test)
validations:
required: true
- type: textarea
id: recipe
attributes:
label: Recipe
description: Filled in by FramePort. Don't edit the package line.
description: The catalog entry (catalog/games/<package>.yaml). Don't edit the package line.
render: yaml
validations:
required: true
@@ -34,16 +34,16 @@ body:
id: environment
attributes:
label: Environment
description: FramePort and SteamOS versions
description: FramePort, tool and SteamOS versions
- type: textarea
id: notes
attributes:
label: Notes
description: What you checked in the headset and any known issues
description: What you checked in the headset, known issues, anything unusual
- type: checkboxes
id: confirm
attributes:
label: Confirmation
options:
- label: I played the game in the headset with this recipe.
- label: I played the game in the headset with this recipe (headless launch tests can't check the picture).
required: true
-18
View File
@@ -150,7 +150,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write # starts the pages workflow
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.2.0
@@ -160,12 +159,6 @@ jobs:
merge-multiple: true
- name: Wheel (CLI install via uv tool / pipx / pip, and its self-update)
run: uv build --wheel -o out && ls out
- name: Videos (docs/media, rendered by the showcase workflow; frameport-<name>.mp4 → FramePort-<name>.mp4)
run: |
for f in docs/media/frameport-*.mp4; do
[ -f "$f" ] || continue
n=$(basename "$f" .mp4); cp "$f" "out/FramePort-${n#frameport-}.mp4"
done
- name: Checksums
run: cd out && sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
- name: Publish release
@@ -186,11 +179,6 @@ jobs:
echo; cat packaging/release-footer.md; } > "$notes" # short: the update dialog shows these notes
gh release delete "$tag" --yes 2>/dev/null || true
gh release create "$tag" out/* packaging/FramePort-selfsigned.cer --title "FramePort ${tag}" --notes-file "$notes"
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run pages.yml --ref main
dev-release:
# "Run workflow" with dev ticked: replaces the rolling `dev` pre-release (never shown by the automatic update check;
@@ -200,7 +188,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write # starts the pages workflow
steps:
- uses: actions/checkout@v7
with:
@@ -232,8 +219,3 @@ jobs:
gh release delete dev --yes --cleanup-tag 2>/dev/null || true
gh release create dev out/* packaging/FramePort-selfsigned.cer --prerelease --target "$GITHUB_SHA" \
--title "FramePort dev build ${VERSION}" --notes-file "$notes"
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run pages.yml --ref main
+2 -3
View File
@@ -1,5 +1,5 @@
name: catalog-from-issue
# A maintainer adds the label `catalog-accepted` to a "Working recipe" issue → this opens a PR adding the
# A maintainer adds the label `catalog-accepted` to a "Working configuration" issue → this opens a PR adding the
# recipe to catalog/games/. Never runs for unlabelled issues; the issue text is only read by the validating script
# (never interpolated into shell commands). Needs Settings → Actions → "Allow GitHub Actions to create pull requests".
on:
@@ -40,8 +40,7 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
uv run python scripts/patch_docs.py # docs/PATCHES.md lists the catalog games per patch
git add "catalog/games/$PKG.yaml" docs/PATCHES.md site/src/data/patches.json
git add "catalog/games/$PKG.yaml"
git commit -m "catalog: $PKG (from #$ISSUE)"
git push --force origin "$branch"
if ! gh pr view "$branch" >/dev/null 2>&1; then
-72
View File
@@ -1,72 +0,0 @@
# The project page (site/, Astro) on GitHub Pages: https://frameport.app/
# Rebuilt when the site, the docs it renders, the catalog or the showcase renders change on main, when a release is
# published, and once a day (contributors, releases and the games board stay fresh). The games board also refreshes
# itself from the catalog on main in the visitor's browser, so a catalog change shows before the next build.
# One-time setup: Settings → Pages → Source: GitHub Actions; custom domain frameport.app (kept in the settings, not a
# CNAME file: GitHub ignores that file for Actions deployments).
name: pages
on:
push:
branches: [main]
paths:
- 'site/**'
- 'docs/*.md'
- 'catalog/games/**'
- 'bootstrap/setup.sh'
- 'docs/images/**'
- 'docs/media/**'
- 'docs/badges/**'
- 'src/frameport/ui/icons/**'
- '.github/workflows/pages.yml'
release: # the changelog page shows every release (CI's own releases start this through workflow_dispatch)
types: [published]
schedule:
- cron: '17 5 * * *'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: site/package-lock.json
- uses: actions/configure-pages@v6
- run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg # the video tiles' short previews
- run: npm ci
- run: npm test
- run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the contributors list (higher API limit)
# analytics (Settings → Secrets and variables → Actions → Variables); unset = none on the site
PUBLIC_CF_ANALYTICS_TOKEN: ${{ vars.CF_ANALYTICS_TOKEN }}
PUBLIC_GA_ID: ${{ vars.GA_MEASUREMENT_ID }}
- uses: actions/upload-pages-artifact@v5
with:
path: site/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
-107
View File
@@ -1,107 +0,0 @@
name: showcase
# Docs screenshots and the videos (the demo tour, the install tutorial, …), rendered from the demo library
# (docs/showcase/, scripts/showcase/; see docs/SHOWCASE.md). Opens or updates one pull request (branch
# showcase/update) when a picture really changed; the renders are also kept as a workflow artifact either way.
on:
workflow_dispatch:
inputs:
videos:
description: "Videos to record too: names from docs/showcase/videos (e.g. tour install), all, or empty"
type: string
default: ""
push:
branches: [main]
paths:
- "src/frameport/ui/**"
- "src/frameport/locales/**"
- "docs/showcase/**"
- "scripts/showcase/**"
concurrency:
group: showcase-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
render:
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
PYTHONUTF8: "1"
PYTHONIOENCODING: utf-8
TZ: UTC
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the push's changed files decide whether the video is recorded again
- uses: astral-sh/setup-uv@v10.2.0
- name: ffmpeg and fonts
run: sudo apt-get update && sudo apt-get install -y ffmpeg fonts-roboto fonts-noto-color-emoji
- run: uv sync --extra dev --python 3.12 # the Python the renders are made with locally
- name: Chromium for Playwright
run: uv run playwright install --with-deps chromium
- name: Store art cache (fetched once from the stores the app uses)
uses: actions/cache@v6.1.0
with:
path: ~/.cache/frameport-showcase
key: showcase-art-${{ hashFiles('docs/showcase/demo-library.yaml') }}
restore-keys: showcase-art-
- name: Docs screenshots
run: uv run python scripts/showcase/render_docs.py --keep showcase-out/screenshots
- name: Videos
# a push records the videos whose storyboard (docs/showcase/videos/<name>.yaml) changed, or all of them when
# the recorder changed (UI tweaks alone don't re-record ~10 MB files); Run workflow records what it's asked
env:
INPUT_VIDEOS: ${{ inputs.videos }}
BEFORE: ${{ github.event.before }}
run: |
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
read -ra names <<< "$INPUT_VIDEOS"
if [ ${#names[@]} -eq 0 ]; then echo "no videos asked for"; exit 0; fi
[ "${names[0]}" = all ] && names=()
uv run python scripts/showcase/record_video.py "${names[@]}" --work showcase-out/videos
else
uv run python scripts/showcase/record_video.py --changed-since "$BEFORE" --work showcase-out/videos
fi
- name: Keep the renders
if: always()
uses: actions/upload-artifact@v7
with:
name: showcase
path: |
showcase-out/screenshots
showcase-out/videos/*/video.json
docs/media
docs/images/tour-teaser.webp
if-no-files-found: ignore
retention-days: 14
- name: What changed
id: diff
run: |
git status --short -- docs/images docs/media
if [ -n "$(git status --porcelain -- docs/images docs/media)" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"; fi
- name: Pull request
if: steps.diff.outputs.changed == 'true' && github.ref == 'refs/heads/main'
uses: peter-evans/create-pull-request@v8.1.1
with:
branch: showcase/update
delete-branch: true
add-paths: |
docs/images/*
docs/media/*
commit-message: "Docs: update screenshots and videos"
title: "Update docs screenshots and videos"
body: |
Rendered by the `showcase` workflow from the demo library (docs/showcase/demo-library.yaml) with a
pretend Frame: only pictures that visibly changed are included. Look them over before merging (the
run's `showcase` artifact has every render).
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+1 -18
View File
@@ -9,7 +9,7 @@ parity-report.md
*.keystore
*.apk
.pytest_cache/
native/.cache
native/.cache/
native/**/build/
# per-machine Claude Code settings (contain home paths)
.claude/settings.local.json
@@ -19,21 +19,4 @@ CLAUDE.local.md
# outputs of `frameport report` / `parity` / ruff
REPORT.md
.ruff_cache/
.eval-home/
.eval-results/
.claude/worktrees/
# project page (site/)
site/node_modules/
site/dist/
site/.astro/
site/public/media/
site/src/assets/media/
site/media-hq/
site/public/s
site/public/setup.sh
# Impeccable design-tool files (kept local, not in the repo)
/PRODUCT.md
/DESIGN.md
/.impeccable/
+287 -344
View File
@@ -6,7 +6,6 @@ FramePort ports Meta Quest standalone APKs to the **Valve Steam Frame** (SteamOS
install over SSH (agent) → Steam shortcut → headless launch test + log triage`.
Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRAME_RUNTIME.md` for runtime facts.
Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debugging a game).
## Layout
- `src/frameport/` — Python package. `pipeline.py` is the API the CLI (`cli.py`) and GUI (`ui/`, Flet 1.0) share.
@@ -24,7 +23,7 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
Frame, and 24 px line glyphs `fp:frame|port|patch|container|test|pc|sync|live|shot|keys|recipe|link`, copied into
the assets dir and shown as tinted `ft.Image`s: pass `G.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 `.page` of an unmounted control
status_row, art_fill, confirm, `update()` = safe update: in Flet 1.0 reading `.page` of 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
@@ -39,27 +38,15 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
launch.sh logs `start/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
pkg)`); 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; stdin `k` = keyframe, EOF = stop; `--probe` / `--selftest`;
artifact `linux-arm64-bin/`, synced to `~/.local/share/frameport/bin` by 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; stderr `live: 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 page
output's pulse monitor (sound) → ffmpeg/x264 + AAC (qualities scale down only; "full" = SteamVR's size) →
fragmented MP4 on an SSH exec channel's stdout (the script stops ffmpeg on stdin EOF) → relay on 127.0.0.1 (keeps
init + fragments since the last keyframe of the *video* track for late viewers) → player page
`install/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 outside `flet build` bundles; 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 with `C.Sparkline` (Flet canvas, no charts
extension), details, a pooled process table (Game / Steam & SteamVR / All; right-click: end / force kill / end
game; MON_CRITICAL needs force, MON_NEVER is refused)); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
stops on disconnect/window close); settings; welcome; activity panel). Files, Screenshots and the Library share right-click menus
(one `ft.ContextMenu` per view, filled on right-click; on one of several selected items they act on the whole
selection, `C.menu_targets`) and click-and-drag multi-select (`C.DragSelect`: pan start/end on the area + item
hover events, which Flutter also sends with the button held; Flet can't report item positions, so no rubber band).
@@ -67,40 +54,7 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
await a FilePicker) must be coroutine functions or go through `page.run_task`: Flet doesn't await a lambda's
coroutine (the Files row Download button silently did nothing). `ui_smoke.py --fake-frame --gestures` drives real
mouse drags/right-clicks.
Live Frame data comes from one app-owned `app.monitor_hub` (`frame/monitor_hub.py`): subscribers name modules +
interval, the hub runs one agent `_monitor` stream (union of modules, fastest interval; per-module collection
needs agent v73), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries 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 flagged `overlay` and listed last; newest first. The
UI takes `frame/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.py` helpers: 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 setting
`ui.live_frame_card` (Settings → Appearance, default on) is on; else the battery comes from the 30 s poll).
ui_smoke injects `FakeMonitorSession` as the hub's `session_factory`.
User tags live in library entries (`tags`), filters in library setting `ui.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.overlay` at 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_TODAY` pretends 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: settings `fun.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). Setting `ui.easter_eggs` (no UI) turns them
off; the showcase demo library has them off (steps hook `easter_eggs` for 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;
@@ -108,47 +62,16 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
**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,
radial `scrim`, accent-tinted shadow) and `C.CoverButton` (frosted round button, accent under the pointer; Play =
blue then orange ring ripple + "Starting on Frame…" pill for STARTING_S; its `.status` layer 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 `-solid` variant
(`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 and `C.feature_row` says 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 shows `keyboard_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.body` holds an
`ft.AnimatedSwitcher` (`app._show_view`): a new route gets a new wrapper (150 ms fade), a same-route redraw swaps
the wrapper's content (no animation; persistent views just remount); the wordmark's portal (`wordmark().data`)
pulses once when frame_state turns "connected"; both off with setting `ui.reduce_motion` (Settings → Appearance
"Reduce motion", `app.reduce_motion`). Settings has a fixed index column (`settings.SECTIONS` order: most users'
needs first (Updates, which also holds game recipe + install-link updates, Installing, Library folders,
Appearance), niche ones later (Install links, Tools, This PC), Remove FramePort last): entries scroll the section column via `scroll_to(scroll_key=…)` (sections wrapped
in Containers with `ft.ScrollKey`), the header stays; `app.settings_view.show_section(key)` (ui_smoke
`settings-index`).
Help hints: wording for non-obvious terms lives in `ui/help.py` (`HELP`); show it with `C.help_icon(key)` or the
`help=` argument of `section`/`kv` (`C.Check` help key), tooltips via `C.tip()` (wraps). Game actions for the Library
right-click menu (one `ft.ContextMenu` around the grid, filled on right-click; short: `menus.quick_menu`, key actions
+ "More actions…" = the game page with its full "…" menu opened from code, `app.open_game_menu`) and the game
page's "…" menu (`menus.menu_sections` quick=False, sectioned) come from `app.game_actions()`. Picking art (`sources.apply_choice`) downloads into a staging dir and keeps the old
`help=` argument of `section`/`status_row`/`kv`, tooltips via `C.tip()` (wraps). Game actions for the Library
right-click menu (one `ft.ContextMenu` around the grid, filled on right-click) and the game page's "…" menu come
from `app.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. …".
- 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 `.part` append, small files
streamed in tar batches; the agent counts files already in `incoming/`). Multi-select in the Library queues
installs after asking every needed question up front. Failures end in one pop-up (Resume / Uninstall / log).
- Library folders (GitHub discussion #351): `core/folders.py` (no Flet) owns setting `scan.roots` (remember/add/remove/status/rescan_roots; the origin-based fallback only while the setting was never set, so removing every folder doesn't guess them back), Settings → Library folders (`settings.library_folders`, status read via run_bg), CLI `frameport folders [--add/--remove/--json]` + `frameport rescan`. Scans relink moved games (`pipeline.ScanReport`: a library game found elsewhere whose own files are gone and the copy is the same build (Quest: source_sha256, else versionName; Rift: same program path + size from the fingerprint, matched by game id or, renamed, by program) → only paths change (`relink_quest`/`_relink_rift`), user fields/builds/installs stay; another version → Scan a folder… takes it via `replace_apk(found=)`/`replace_rift_game`, Rescan only reports it; the old copy still there → kept). `add_game` no longer overwrites a user recipe (refreshes `suggested` only). Linux apps aren't scanned. PC installs of a moved Rift game (Steam shortcut path) untested.
- Other drives (GitHub #90, agent v63): anchors stay in ~/Applications/quest-frame, a game's files may live in
`<mount>/FramePort/<pkg>` (`deployment.json` base). Agent `drives` (/proc/mounts, /run/media + Steam library
drives; vfat/exfat/ntfs/read-only refused), prepare*/finalize_linux `dest` (unmounted = error, never a fallback;
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's
`launcher` field, else text swap), list_installed `drive`/`drive_missing` (install_state keeps such games
"installed"). PC: `install/drives.py`, library setting `install.drive` (Frame page → Storage), game menu
"Move to…" (job kind tool-frame), CLI `frame 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_timefix` for the Frame OpenXR-1.1→1.0 fix, `pcvr.no_crash_reporter` for Unreal).
@@ -161,11 +84,7 @@ Detailed device findings per game/issue: docs/DEVICE_NOTES.md (read before debug
doesn't do). `VD.bat` is Virtual Desktop's launcher: ignore it except as an exe-location hint. Library migration
`rift_run_direct` resets 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; triage `unreal-crash`. Agent v67 adds Unity's logs (LocalLow/<Company>/<Product>/Player(-prev).log via `<Name>_Data/app.info`,
`output_log.txt`, Temp/…/Crashes/*/error.log; `unity_logs`) to launch tests and diagnostics; triage `unity-vr-init` /
`unity-crash`. A catalog recipe verified with another build (catalog `xr` ≠ the build's) keeps the build's own
`pcvr.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
game log + crash summaries from the Proton prefix; triage `unreal-crash`. 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.py` walks once,
@@ -204,35 +123,12 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
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; needs `STEAM_COMPAT_DATA_PATH`, see docs/FRAME_RUNTIME.md):
agent v61 `linux_x86_tools`/`pick_tool`, `install_proton`/`proton_status` `kind: 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…" /
- Linux apps (GitHub #31, library kind `linux`, `linux.<slug>`): GUI = Add games → "Add a Linux app (arm64)…" /
"…folder…" (`app.add_linux` job → `pipeline.add_linux_app`); game page `linux_summary` (program + Change…, AppImage,
OpenXR, source) instead of recipe/patches, `C.missing_libraries` callout (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_catalog` skips them; local files of a lone AppImage = the file only.
Desktop Mode entries (GitHub #84, agent v63): finalize_linux writes `frameport-<slug>.desktop` to
~/.local/share/applications (+ ~/Desktop if it exists; `X-FramePort-Package` marks ours), launch.sh with
`FRAMEPORT_DESKTOP=1` skips the Steam-parent watchdog and Steam's display; ensure_host_fixes refreshes entries
(older installs, stale ones removed), uninstall/purge remove them. Per app: library entry field `desktop_entry`
(default on; patches don't apply to Linux apps) → game page switch → agent `desktop_entry`. Untested on device.
- Scanning: SideQuest backups (`<package>/<time>/apk|obb|data`) and AXRB downloads (`AXRB/<app id>/<binary id>/base.apk`
+ OBBs/assets; its `patched/`/`*-axrb.apk` PC builds are skipped) are recognised (`sources/quest_dump.py`, FAQ).
- Quest/Rift twins stay separate entries, shown and named in Steam "Title (Quest)"/"(Rift)" (`core/titles.py`).
- Rename… (game menu "Name and artwork", Library right-click; CLI `frameport rename <pkg> "<name>"|--reset`):
`pipeline.rename_game` sets `title` + `title_locked` (+ `auto_title` = the automatic name for reset; rescans, Rift
store matches and links then only update `auto_title`; a locked name gets no twin suffix); `sync_title` → Frame
agent v77 `rename` (deployment.json title, same shortcut appid: upsert matches by Exe) + art + one Steam restart, PC
`PcReviveTarget.rename`; an unreachable Frame → `steam_name_stale` (game page "Update name on Frame", next install).
- Newer versions (GitHub #161): game menu "Update with a newer APK…" (`pipeline.replace_apk`, outdated via
`build.source_version`) and, for Linux apps / PC VR games, "Update with a newer version…" (`replace_linux_app` /
`replace_rift_game` via `add_linux_app(replace=)` / `add_rift_game(replace=)`, CLI `frameport new-version`): the
entry keeps package, name, art, tags, switches, user recipe (a confirmed exe if the new folder has it); a new
`source_id` goes into the deployment's `recipe` dict at install (no agent change) → `C.source_replaced` = outdated
(Frame + `pc_outdated`); such an install re-sends same-size files changed after the last install (mtime/ctime,
`installer.changed_since`); the old Rift folder is in `replaced_dirs` (rescans skip it). Untested on the device.
- `Recipe.as_is` = install unchanged (pre-patched libraries): `pipeline.prepare_as_is`; auto for APKs that already
contain FrameBridge (`frame_patched`). For Rift it changes nothing (the dump is never modified; the Frame copy
still gets launch fixes like the crash-reporter rename — `-nocrashreports` alone doesn't stop UE 4.23) — it does
@@ -244,21 +140,9 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
flask icon (SCIENCE_OUTLINED) so they aren't confused with Play.
- `uninstall.py` + `frameport uninstall-app` + Settings: removes the data dir (after an optional key backup zip),
PC Steam shortcuts, the WSL Revive copy, and via agent `purge` FramePort's games/files on the Frame.
- Save backups (discussion #352, agent v95; docs/FRAME_RUNTIME.md "Save backups"): agent `_backup_saves`/
`_restore_saves '<json>'` stream a tar.gz of `lepton-data/internal/<pkg>` + `external/Android/data/<pkg>` (caches,
IL2CPP/shader caches, FrameBridge files, videos/OBBs, files >64 MiB left out; manifest `frameport-saves.json` last)
over the exec channel, file work in `podman unshare` (`_saves_ns`), launch lock held, refused while running;
restore = untrusted input (staged + checked first), rollback copy `<base>/saves-before-restore.tar.gz` (undo =
`rollback: true`). PC `install/saves.py` (`<data>/save-backups/<pkg>/<ts>.tar.gz` + `.json`, keep `saves.keep`
= 10), GUI `ui/views/saves_dialog.py` (game menu section "Saves", Frame page "Back up all saves", uninstall's
"back up before deleting"), CLI `frameport saves backup|list|restore|undo`.
- `patches/` — **the unit of modularity**. `base.py` (Patch interface, registry), `overport.py` (overport CLI patch ids,
discovered dynamically via `overport patches`), `frame/*.py` (one module per Frame fix), `settings.py` (FrameBridge
adapter keys + device files as patches). Add a patch = add a module that calls `register(...)`.
`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`) and `ovrport.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).
@@ -278,22 +162,9 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
- `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 v21
`collect_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_issue` copies 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.yml` issue → maintainer label `catalog-accepted` (a plain issue needs `working-config` added by
hand) → `catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
chars). "Share working config" → `working-config.yml` issue → maintainer label `catalog-accepted` →
`catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
`core/applog.py` (`<data>/logs/app.log`, finished GUI jobs in `<data>/logs/jobs/`).
- Announcements (2026-10-15, announcements/README.md; app only, the owner dropped the site pop-up): source of
truth `announcements/current.md` on `main` (front matter id/title/date + optional image/image_alt/button+url/
expires/active, Markdown body; ships `active: false`). `announcements.py` (no Flet; raw GitHub via
`cache.cached_text`, 6 h) + `ui/announcer.py` (pop-up 20 s after start / every 6 h when no dialog is open and not
on welcome; `C.dialog` with the portal edge, image downloaded + Pillow-checked into `<data>/announcements/`, shown
by asset URL). Shown only if the user was around before `date` (setting `announce.since`, set once at the GUI's
first start: 0 when `USED_BEFORE` settings exist, else now), once: any close (X, outside click, Escape, button)
stores the id (`announce.dismissed`, last 50). New announcement = new id; same id = typo fix.
`FRAMEPORT_NO_ANNOUNCEMENTS`; `ui_smoke.py --announcement` renders it.
- Self-update (docs/ARCHITECTURE.md "Self-update", docs/INSTALL.md "Updating"): `_version.py` = the only version
(`frameport.__version__`; pyproject reads it via hatch `dynamic`; app log, diagnostics, User-Agent, Settings use
it — never `importlib.metadata`, bundles have no dist-info). `updates.py` (no Flet): `check()` = GitHub
@@ -306,15 +177,9 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
signed, `ready.json`); `apply()` writes + starts detached `apply.ps1`/`apply.sh` (wait for pid → Windows: copy over,
replaced files kept in `updates/<ver>/previous` because the zip has no top folder; macOS/Linux: mv to `.old` + swap,
rollback, `xattr -dr` quarantine → relaunch; `<data>/logs/update.log`). `ui/updater.py`: background check (10 s,
then 6 h), sidebar card (hidden control, toggled), Library bar (`library_bar`), changelog dialog, job
then 6 h), sidebar card (hidden control, toggled), Library bar (`library_bar`), notes dialog (`ft.Markdown`), job
`app-update` that restarts only when no other job is active, `apply_pending_at_start()` in `main()` 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 update
`pending_news` (setting `update.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)]
"Install updates automatically"; Settings → Updates. 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.
@@ -323,44 +188,12 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
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. Bump `AGENT_VERSION` when 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 by
`site/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`) → `/wait` hands 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`, setting `frame.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.
- `bootstrap/bootstrap.sh` — one-time Frame setup served by the pairing server (sshd, app key, avahi service, Lepton).
- `catalog/games/<package>.yaml` (installed apps also fetch these from GitHub `main`, 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 in `artifacts/` (adapter, VrApi bridge patches, GL shim, stubs).
`native/build.py` rebuilds them with NDK r27c (downloaded on demand into `native/.cache`, git-ignored; uses
`-ffile-prefix-map` so 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.py` converts 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: patch `frame.a32_bridge` (`patches/frame/a32_bridge.py`: `convert()` is shared with the dev script
`bridge/convert.py`; artifacts `liba32bridge.so`/`liba32rt.so`/`liba32stub.so` from `native/build.py --only a32`)
is suggested for every 32-bit-only APK; the recipe then has `overport: 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, and `device.text_input_window` for Unity (`engine.a32_recipe`,
also run by `pipeline.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 unless `a32 = True`, adapter settings in
`settings.XRSHIM_SETTINGS` off); `build.check_a32` refuses 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 migration
`a32_bridge_on` turned the bridge on once in every 32-bit recipe, user ones too, unless the catalog's frame_remove
names it).
Analysis `extra.headset` (ANALYSIS_VERSION 15): gearvr / go / quest → "Gear VR" / "Oculus Go" platform + Steam tag.
Catalog: `headset:` field; com.fallen.affected, net.mobilemischief.spunkydungeon, com.ccpgames.gunjack (works, 2026-10-12). Per-game a32 findings (AFFECTED, Spunky Dungeon, Gunjack's signature check `fp_a32_signature.txt`, synthetic Gear VR input, Quest builds `fp_a32_headset.txt` / `quest_touch`, recenter, debugging aids `A32_BREAK` / `A32_STRACE` / `A32_PERFMAP`, translator profiling): docs/DEVICE_NOTES.md#a32-bridge-games-lessons-debugging-aids-profiling.
- `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).
@@ -376,33 +209,16 @@ Games tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>); on-device chec
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_JAR` override
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/frameport` on 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`: venv `FramePort-dev\venv` via `UV_PROJECT_ENVIRONMENT`,
`uv sync --frozen`, log `FramePort\logs\dev-gui.log`). **WSL runs on that data need `FRAMEPORT_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.json `java-<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).
managed tools. Real app data (WSL): `~/.local/share/frameport` (tools, overport workspace + game keystores, library,
artwork, frames.json, the app's SSH key which the dev Frame authorizes).
- Device checks: `frameport test <pkg>` / `frameport parity-device --results <parity.json> --baseline <launch.txt>
[--test-only]`; the pre-FramePort baseline is `PATCHED/_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.py`
renders `docs/showcase/shots.yaml` into `docs/images/` (every shot twice, kept when both agree; only visibly changed
files are rewritten; `--check` for CI) and `scripts/showcase/record_video.py [names] [--draft]` records the
storyboards `docs/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) into
`docs/media/frameport-<name>.mp4`. Both build the demo library first (`demo_home.py`, profile `demo` or `fresh`:
catalog games from `demo-library.yaml` + `demo-analyses.json`, art/details cached in ~/.cache/frameport-showcase, a
pretend Frame from `showcase-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. The `showcase` workflow renders on UI pushes to main (+ the videos whose
storyboard changed, or the ones named on dispatch) and opens PR `showcase/update`; releases attach every video as
`FramePort-<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
- Docs screenshots (`docs/images/`): `python scripts/scrub_library.py ~/.local/share/frameport <dir> --status works,issues` (copies
library + artwork only; titles replace folder names, local paths → `D:/Games/...`, sort by size) then
`FRAMEPORT_HOME=<dir> python scripts/ui_smoke.py --out <shots> --docs --fake-frame --game <pkg>` (set
FRAMEPORT_JAVA/_OVERPORT_JAR/_APKSIGNER_JAR so no tool download toast appears; `--viewport 1280x2600` + crop for the
patch list). Check every PNG for paths, IPs, user names and repack/scene names before committing.
README rules (owner): states the project is a proof of concept, 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`, then
`FRAMEPORT_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
@@ -414,11 +230,6 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
`T.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 2560x1440` renders it. Flet draws a grey box for invalid layouts (e.g.
an `expand` child in a `wrap=True` Row).
- 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 (no `gui.alive`) and older than 30 s is a leftover FramePort process and is ended. `main()` ends the process
(`os._exit`) once `ft.run` returns: 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 -f` patterns match your own shell — use `pgrep -f "[b]in/frameport-gui|[f]let-desktop-light"`.
## Round-2 polish (2026-10-02)
@@ -442,15 +253,12 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
## 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).
- No AArch32: 32-bit-only APKs fail with `INSTALL_FAILED_NO_MATCHING_ABIS`. Unfixable; point to Rift + Revive.
- 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`/`xrConvertTimeToTimespecTimeKHR` return FUNCTION_UNSUPPORTED → adapter emulates
(offset = predictedDisplayTime − period − CLOCK_MONOTONIC, sampled in xrWaitFrame).
- No XR_FB_display_refresh_rate (`xrGetDisplayRefreshRateFB` -7), yet OVRPort's dispatcher offers it and answered 0 Hz
→ FrameBridge always answers with the measured rate (1/predictedDisplayPeriod, else 72). PreyVR (#306, idTech 4:
`USERCMD_MSEC = 1000 / GetRefresh()`) had 0 ms tics: async thread spinning, `com_frameTime` stuck at 0, menu black.
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m).
- GL goes through **Zink** (Mesa GL on Vulkan). Mesa GLSL is strict: `#pragma` before `#extension` fails, implicit
int/float conversions fail (enable `GL_EXT_shader_implicit_conversions`), num_views=2 shaders on single-view FBOs
@@ -475,10 +283,7 @@ The Frame runs firewalld (22 and 32000 open). PC side: the setup server needs in
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.
scan redraw ran never closed: "dropped a patch for unknown control").
**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
@@ -520,14 +325,28 @@ Launch tests ignore log files older than the launch (stale Proton/game logs used
(`\\wsl$` is unreliable), so PC mode copies Revive to `%LOCALAPPDATA%\FramePort`.
FramePort never bypasses Oculus entitlement checks (Platform SDK games: flagged, PC mode with the Oculus app only).
**Upload speed:** home Wi-Fi caps uploads at ~11-18 MB/s whatever the SSH method; the Frame's hotspot (`wlanap`) ~83-97 MB/s; `Frame.fast_link()` (installer `transfer_link`, uploads ≥64 MiB) prefers usb0 > wlanap when the host key matches. → docs/DEVICE_NOTES.md#upload-speed
**Upload speed (measured 2026-09-29):** via the home router (PC Wi-Fi → router → Frame wlan0) only 15-18 MB/s, the
same for paramiko, OpenSSH and 4 parallel streams, so the network path is the limit, not the SSH code. Direct to the
Frame's hotspot (`wlanap` 10.35.78.1; the owner's PC has Valve's USB Wi-Fi dongle on it) 83 MB/s OpenSSH / 97 MB/s
paramiko; reading dumps from D: via WSL drvfs is then the cap (~82 MB/s). `Frame.fast_link()` (installer
`transfer_link`, uploads ≥64 MiB) uses usb0 > wlanap when reachable **and** the host key matches the paired Frame.
**Platform SDK detection** must include delay imports (`PEInfo.delay_imports`), modular Unreal's
`*-OnlineSubsystemOculus-*.dll` and Ready At Dawn's `pnsovr.dll`: Robo Recall, Lies Beneath and Lone Echo I/II all use
it; without the Oculus app they crash with 0xc06d007e (triage `delayload-missing`) — not fixable legitimately.
GUI: Frame → Installed → folder icon = file browser (agent `list_files`, flags files missing vs the install
manifest); Settings → About shows the bundled agent version and the Frame's.
**Revive under Proton arm64:** Revive's Detours hooks don't take effect (Wine's kernelbase is ARM64EC); a system32 `LibOVRRT64_1.dll` symlink gets past -3001 to -3021 = ovrError_LibSignCheck (Oculus-signed runtime required; not bypassed). → docs/DEVICE_NOTES.md#revive-under-proton-arm64-lone-echo
**Revive under Proton arm64 (Lone Echo, 2026-09-29):** Revive's Detours hooks (LoadLibraryW/ExW, OpenEventW, the
signature check) don't take effect: Wine's kernelbase is ARM64EC. The LibOVR shim in the game then does a plain
`LoadLibrary("LibOVRRT64_1.dll")` search (exe dir, cwd, system32, windows, PATH — no registry, `OculusBase` or
`LIBOVR_DLL_DIR` used by this build) → `Failed to initialize Oculus API (-3001)` (= Lies Beneath's "Unable to load
LibOVRRT DLL"). A symlink `pfx/drive_c/windows/system32/LibOVRRT64_1.dll -> <base>/revive/LibReviveXR64.dll` (Wine
maps a symlink to the already-loaded module) gets past it to **-3021 = ovrError_LibSignCheck**: the shim only accepts
an Oculus-signed runtime. Next step (not done): make the runtime check pass without Detours, e.g. an injected helper
that patches the game's import table (IAT) for LoadLibrary*/WinVerifyTrust, or a prefix-level override. This is
runtime interop (what Revive does), not the Platform SDK licence check (which FramePort never touches).
Uninstall (Quest, saves kept) used to leave `deployment.json` → still "installed"; fixed (agent v16).
**Discovery/network:** Developer-Mode SteamOS devices announce `_steamos-devkit._tcp` (TXT `login=steamos`) — use it;
they don't publish `_ssh._tcp`. The Frame has several links: `wlan0` (home Wi-Fi), `wlanap` = its own hotspot at
10.35.78.1/24 (a PC can join it directly), `usb0` = USB gadget network 10.86.200.233/29 (up when cabled to a PC).
@@ -541,7 +360,23 @@ stopping Steam kills them → always run that work via `systemd-run --user` (the
**overport:** always `--version=latest`; `--workspace` holds runtimes and **per-package keystores (password
"password", alias "key") — never lose them**: updates must be signed with the same key or saves are lost on reinstall.
Output is deterministic (same input + runtime → same bytes), which is what makes parity testing possible.
**OVRPort 1.2.5 (2026-10-01):** `patch` rejects unknown/duplicate args, unknown ids and an empty `--patches=`; it adds `libovrplatformcompat.so` itself (`frame.ovrplatformcompat` then skips). New patches `patch_ac_nexus_no_appsw_72/_90` (AC Nexus build 207706 only; `_90` = catalog default; STRICT patches only from a catalog recipe where `applies()` holds), `patch_disable_meta_xr_audio_telemetry`, `patch_vrapi_openxr` (OVRPort's VrApi translator fails on the Frame → keep `frame.vrapi_bridge`). The owner prefers OVRPort's fixes over ours where they work as well. `frameport install <pkg> --apk <file>` installs a test build. → docs/DEVICE_NOTES.md#ovrport-125
**OVRPort 1.2.5 (2026-10-01, the fork's first release; CLI-only):** same commands (`patches [--json]`, `patch`, `help`,
`install`), but `patch` rejects unknown/duplicate args, unknown patch ids and an empty `--patches=`
(`tools/overport.patch` refuses empty lists). After patching it adds `libovrplatformcompat.so` itself when the platform
loader lacks `ovrMessageType_ToString` (our `frame.ovrplatformcompat` then skips: same library). New patches (all off;
`patches/overport.py`): `patch_ac_nexus_no_appsw_72/_90` (AC Nexus build 207706 only, exclusive),
`patch_disable_meta_xr_audio_telemetry` (x86_64 emulators: hidden), `patch_vrapi_openxr` = OVRPort's VrApi adapter =
the **unpatched** upstream of our `frame.vrapi_bridge` (`native/vrapi` unchanged since our 5e7df52), only usable with an
experimental CLI built with `-PwithVrApi=true` (stable jars list it but fail). The owner prefers OVRPort's fixes over
ours where they work as well (less to maintain): compare in the headset before switching a default.
`frameport install <pkg> --apk <file>` installs a specific (test) build signed with the game's key.
Headset results (2026-10-02, `PATCHED/_test-ovrport-1.2.5/TESTING.md`): OVRPort's own VrApi translator fails on the
Frame (Climb 2: requests VkFormat 37 → crash; POTW: no GLES path, missing `vrapi_GetTextureSwapChainHandle`) → keep
`frame.vrapi_bridge`; those two changes are upstream candidates. 1.2.5 builds (its platform compat, new permissions)
work. AC Nexus: `patch_ac_nexus_no_appsw_90` is the catalog default (owner preferred 90 Hz); `STRICT` patches are only
taken from a catalog recipe where `applies()` holds (the AC Nexus ones need build MAIN.450412.207706.final, else the
whole overport run fails).
**Patching gotchas:**
- UnityPy re-serialization breaks scene loading → patch QualitySettings ints in place.
- LIEF's DT_NEEDED injection shifts segments; our `elf.add_needed` appends a new PT_LOAD (reuses PT_NOTE, else moves the
@@ -553,65 +388,243 @@ Output is deterministic (same input + runtime → same bytes), which is what mak
- VrApi-direct engines (CryEngine Climb 2, POTW) need the VrApi bridge; the bridge drops whole frames on unknown layer
types (→ black screen with audio).
**Steam Frame controller models:** adapter setting `controller_models` (`native/adapter/render_model.c`, emulates XR_FB_render_model) + agent `install_controller_models` (Frame's SteamVR models → glb at finalize); no catalog game uses runtime models; Valve's models are never committed or copied off the Frame. → docs/DEVICE_NOTES.md#steam-frame-controller-models
**Steam Frame controller models (2026-09-30, not yet seen in a game):** adapter setting `controller_models`
(`native/adapter/render_model.c`) emulates XR_FB_render_model and serves `files/framebridge/controller_{left,right}.glb`;
agent v20 (`install_controller_models`, command `controller_models`) converts the Frame's SteamVR render models
(folder name matching "frame" + left/right, OBJ+PNG, `openxr_grip` component → grip space) at finalize/set_settings.
Valve's models are never committed or copied off the Frame. Only games that use Meta's runtime controller models
benefit (manifest `RENDER_MODEL` permission/feature → suggested); **none of the 34 catalog games do** (they ship their
own meshes: that would need per-game asset replacement). Model discovery and conversion
checked on the device 2026-09-30: `/opt/steamvr/drivers/frame_controller/resources/rendermodels/frame_controller_{left,right}`
(component OBJs in model space = the whole `<name>.obj`, one `_color.png` 2048² near-black, `openxr_grip` rotates about
X only; hidden-by-default components like `status` are skipped) → ~2 MB glb each, ~1 s. Not yet seen in a game (none
of the installed builds has the new adapter, and none requests runtime models), so whether Meta's SDK attaches runtime models at the grip pose is unverified.
**overport's dispatcher (`libopenxr_loader.so`) only forwards functions in its own table** (`overportOXR: Unknown
proc addr: …`), so adapter emulations of functions it doesn't know are unreachable. For XR_FB_render_model,
`native/xrshim` fills the gap: OVRPlugin's `dlopen("libopenxr_loader.so")` string is rewritten to the shim (see
native/README). Verified on the device with Toy Master (2026-09-30): `extension shim: xrLoadRenderModelFB -> FrameBridge`.
Toy Master doesn't request a model after that (it uses its own), so the glb loading path is only unit-tested.
**Crash backtraces:** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (launch tests return `crash_log`). Fossilize loads into every app (userdebug guest) → `frame.vk_sanitize` (`native/vkshim`); overport's dispatcher aborts on swapchains > 4096 px → `frame.swapchain_limit`; cylinder layers → quad strips (`cylinder_strips`). Drawing equirect layers ourselves and a focus debounce were tried and removed: don't retry without a new approach. → docs/DEVICE_NOTES.md#crash-backtraces-fossilize-swapchain-limit-cylinder-and-equirect-layers
**360° layers / 4XVR:** the Frame's Android runtime composites only quad + projection layers (quads always above projection layers). Per-game adapter settings (off by default): `equirect_emul` (GLES), `equirect_flip/face/res/fps/stereo`, `stable_local`, `focus_hold` (default on), `aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug`; agent `link_media` / `app_media_dirs` put sent files into a player's own folder. → docs/DEVICE_NOTES.md#360-layers--4xvr
**Companion apps / intents:** a second APK can run inside a game's Lepton container, but Lepton turns every `android.intent.action.VIEW` with data into `steam://openurl` → "open in external player" hand-offs can't work; Lepton installs exactly one `*.apk` per app folder. → docs/DEVICE_NOTES.md#companion-apps--intents-stremio--4xvr
**Text input:** Lepton's Android has no IME and VR apps run headless (no focused window); `lepton-show-flatscreen` keeps VR and gives focus + Steam's keyboard (`device.text_input_window`); `frame.unity_text_input` patches Unity's TMP_InputField/InputField at Cpp2IL offsets; agent `_keyboard` = uinput "Type on Frame". `pm install` of a second APK corrupts the main app (`cmd_real package install` skips the hook). → docs/DEVICE_NOTES.md#text-input-stremio-vr
**User reports #4-#10:** shortcuts go to the signed-in account (agent `library_users`), `launch` errors `NOT_IN_LIBRARY`; shared Cpp2IL run (`unity_text_input.Il2cppReturnPatch`) for `frame.unity_runtime_msaa_off` and `frame.unity_multipass`; VR4 → catalog `use_alt`. **Catalog changes reach existing games**: recipes store `catalog_rev`, `library._follow_catalog` re-derives non-user recipes. → docs/DEVICE_NOTES.md#user-reports-2026-10-04-issues-4-10
**Round 2 of reports / headset rounds 2-3 (2026-10-04):** vkshim `vk_shader_fix` (VR4); `Il2cppReturnPatch.field_loads` (I Am Cat); builds record `recipe_fp` (changed recipe/revised patch → "Update on Frame"); `Frame.relocate`; adapter `profile_remap` (Touch Plus/Pro); `sync_guard` (Myst); `focus_hold` tuning; ovrpshim for Unity 2017 (Accounting+); Steam library to every account, `shortcuts_lost` / `add_devkit_entry`; no clipboard service → `frame.sdl_clipboard`, `frame.godot_clipboard`; 2D apps keep non-VR patches (`needs_vr = False`); `frame/local.py` (FramePort on the Frame); after every app update `library._follow_catalog` re-derives non-user recipes once (`recipes.app_version`, `library.REFRESH_ON_UPDATE`). Analysis fields added later: **bump `analysis/detect.ANALYSIS_VERSION`** (re-analysis at GUI start + `pipeline.refresh_analyses`; `analysis_failed`). Roblox and BONELAB crash (unsupported). → docs/DEVICE_NOTES.md#round-2-of-reports-headset-rounds-2-3-2026-10-04
**Round 4:** XR_KHR_android_surface_swapchain is listed but unsupported → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`); save folders without u/g+rwx → launch.sh `fix_perms`; Lucky's Tale OOM (no fix); `perf` works as steamos (gdb blocked); QuestCraft JRE, WiiCompiled import by hand (Lepton has no document picker). → docs/DEVICE_NOTES.md#round-4-2026-10-04-surface-swapchains-save-folder-permissions
**Steam library failures (GitHub #21/#30/#41/#42):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (Play → AppError_9) → fallback: register through Steam's devkit interface (`devkit-1 … create-shortcut`, `~/devkit-game/<id>/`; `devkit_appid`, `devkit_appid_from_log`, `devkit_unregister`, `copy_grid_art`; ids: letter first, letters/digits/_ only); `collect_diag` adds `steam_library`. Cause still unknown. → docs/DEVICE_NOTES.md#steam-library-failures-and-the-devkit-fallback-github-21304142
**Exit game (GitHub #36):** `Apps.TerminateApp` (Steam CEF devtools on 127.0.0.1:8080) closes games; launch.sh ends the game when its parent reaper is gone (`upgrade_launchers` adds it). → docs/DEVICE_NOTES.md#exit-game-github-36
**SteamVR dashboard at game start:** launch.sh's `_dashboard_worker` hides the dashboard via `SteamClient.OpenVR.VROverlay` (agent `steam_js`) from the first FrameBridge `new layer:` line (120 s, ≤10 times, stops after `toggle_dashboard_action`); log `<base>/dashboard.log`, opt out `FRAMEPORT_KEEP_DASHBOARD=1`. → docs/DEVICE_NOTES.md#steamvr-dashboard-at-game-start
**Space warp / ITR2:** OVRPort's dispatcher offers XR_FB_space_warp itself (`patch_disable_space_warp` doesn't reach UE5) → FrameBridge `hide_space_warp` (per game, off; suggested for UE5 + OVRPlugin). `equirect_emul` switches itself off on Vulkan. → docs/DEVICE_NOTES.md#space-warp--itr2
**PR #34 (Lucas-Mathieu):** `native/adapter/audio_metadata.h` keeps Meta XR Audio Wwise metadata alive (Batman: Arkham Shadow 1.4.1 smoke-bomb crash). Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally. → docs/DEVICE_NOTES.md#pr-34-meta-xr-audio-metadata-batman
**Heuristics eval (2026-10-04):** 19/44 recipes exact (superseded by the 2026-10-15 numbers under Heuristics). → docs/DEVICE_NOTES.md#heuristics-eval-2026-10-04
**Crash backtraces (2026-09-30):** Lepton writes tombstones only to `lepton-logcats/steamlaunch-<appid>/logcat-crash.log` (after "Dumping logcat"), not launch.log; agent v23 launch tests wait for it and return `crash_log`, triage gets it via `triage(..., crash=)` (not pid-filtered: tombstones come from crash_dump's pid). The guest is `userdebug` (`ro.debuggable=1`), so Lepton's Fossilize layer loads into every app whatever the APK's debuggable flag; it has no off switch. Fossilize crashed Deadpool VR on uninitialized attachment-reference pNexts (several, in FVulkanRenderPass and the render-target layout) → `frame.vk_sanitize` = `native/vkshim` (engine's `dlopen("libvulkan.so")` string → `libfp_vk.so`, wraps vkCreateRenderPass2/KHR, drops unreadable/wrong-sType pNexts; verified: game runs). overport's dispatcher aborts on swapchains > 4096 px (`cmp wN,#4096` in xrCreateSwapchain) → `frame.swapchain_limit` (verified with 4XVR, 7680×3840 accepted by the runtime). Both are default-on (parity classifies their rewrites as
"expected"; library migration `quest_binary_fixes_v2`). The adapter shows cylinder layers (4XVR's movie screen) as ~15° quad strips (`cylinder_strips`, setting
`cylinder_strips`); the runtime composites them at full resolution. Equirect (360°) layers are dropped: drawing
them ourselves (GLES renderer in xrEndFrame: stutter, never showed 4XVR's VR video; Vulkan renderer: hung the
Frame's GPU in AC Nexus; reading the newest *released* image broke 4XVR's theatre) was removed again on the owner's
request — don't retry without a new approach. Converted layers must count as "swapped" or the original layer list
is submitted (runtime returns -2 for every frame). A focus debounce (hiding the Frame's brief
FOCUSED→VISIBLE→SYNCHRONIZED dips, which make 4XVR recenter) was also removed: AC Nexus stayed black with it (it
hid 521 ms focus changes at start). The new approach (below) is per-game, off the app's render thread, GLES-only.
**360° layers / 4XVR (2026-09-30):** the Frame's Android SteamVR runtime (`/opt/steamvr/bin/androidarm64/vrclient.so`,
mounted in Lepton as `/data/steamvr/runtime`) only composites quad + projection layers (cube/cylinder/equirect(2)
are enum names only) — nothing to unlock. Per-game adapter settings (off by default, hooks installed only when on):
`equirect_emul` (GLES only: a worker thread with a shared EGL context converts each 360° image to a cube map when it
changes and draws one adapter projection layer from it for every frame with exactly that frame's views (the app's own
projection views, else xrLocateViews at displayTime); xrEndFrame waits ≤6 ms for the worker's CPU submit, never the
GPU. Learned in the headset: quads can't be a background (the Frame draws quad layers above all projection layers
whatever the order: hid 4XVR's balcony/controllers), and the Frame doesn't reproject a projection layer from its own
pose (images drawn only after head turns, or ahead from xrWaitFrame and sometimes late, wobbled);
`equirect_flip/face/res/fps/stereo`), `stable_local`, `focus_hold` (only after 3 s FOCUSED, dips <600 ms),
`aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `layer_debug` (diagnostics); focus_hold is default-on since 2026-10-05. 4XVR re-creates LOCAL spaces every 2–4 s
(menu recentring suspect); its theatres are baked 7680×3840 equirect2 images (`assets/100.png` …). Test 360° videos
(NASA, public domain) are in the Frame's ~/Videos; copies in `~/Downloads/frameport-360`. 4XVR's "Internal Storage"
lists its own `/sdcard/4XPlayer`, not Movies: agent v25 `link_media` hard-links sent files into an app's own top-level
folder (`app_media_dirs`), `frameport frame send --app <pkg>`, GUI game page "Add videos" (players) / "Add videos &
files…". A 4XVR webm stereo swapchain was refused with XR_ERROR_LIMIT_REACHED (-10) → right eye grey; our projection
swapchain was halved (1536/eye) in case memory is the limit (unverified). Frame data snapshot
(SteamVR runtimes, Lepton scripts, logs; never commit Valve binaries): `~/frameport-research/frame-data-2026-09-30/`.
Not yet verified in the headset.
**Companion apps / intents (2026-10-03, tested with Stremio + 4XVR):** a second APK can be installed into a running
Lepton container (`podman exec -i lepton-steamlaunch-<appid> pm install -g -S <size> < apk`) and runs there, VR
included (4XVR in Stremio's flatscreen container: FrameBridge 72 fps, settings via LEPTON_ENV_FRAMEBRIDGE_CONFIG). But
Lepton's services.jar (`ActivityStarter.execute`) intercepts **every** `android.intent.action.VIEW` with data (any
scheme, explicit component or not, no property to disable): it writes `steam://openurl/<uri>` to `/lepton/steam.pipe`
(= the host Steam client's `~/.steam/steam.pipe`) and starts nothing. So "open in external player" hand-offs (Stremio
→ 4XVR) can't work inside Lepton; don't retry without Valve changing it. Lepton installs exactly one `*.apk` per app
folder (two break `get_apk_path`).
**Text input (2026-10-03, verified in the headset with Stremio VR):** Lepton's Android has no IME (`ime list` empty)
and runs VR apps headless (`lepton.headless=true`): no Android window has input focus (`dumpsys input` FocusedWindows
empty), so neither `input text`, a USB keyboard nor Steam's keyboard reach the app. `lepton-show-flatscreen` on a VR
app keeps VR working (FrameBridge 72 fps) and gives the window focus; Steam's on-screen keyboard then opens for text
fields. Unity's TMP_InputField on non-Quest Android waits for the system keyboard (`TouchScreenKeyboardShouldBeUsed`)
and deselects a frame later unless `isKeyboardUsingEvents` (Android: `InPlaceEditing() && m_HideSoftKeyboard`);
uGUI InputField's LateUpdate keeps the field when `InPlaceEditing()`. Patch `frame.unity_text_input` rewrites them
(`mov w0,#0|#1; ret`) at Cpp2IL's **Offset** (= file offset; RVA differs by 0x4000 in Stremio's lib), Cpp2IL
2022.1 pre-release (Unity 6 / metadata v31; Il2CppDumper can't), cached per libil2cpp sha. `/dev/uinput` has an ACL
for steamos (Steam Input) → agent v33 `_keyboard` uinput keyboard ("Type on Frame") reaches everything with focus.
Installing a second APK into a container with `pm install` re-runs Lepton's post-install hook on the **main** app
(`lepton.active_app_id`; moves its files to /data/steam_app) and corrupts it on the next start (fix: touch the APK →
re-bake); `cmd_real package install` skips the hook.
**User reports (2026-10-04, issues #4-#10):** Steam shortcuts go to the signed-in account (`loginusers.vdf`
MostRecent) else every account (agent v35 `library_users`; it used to refuse with >1 account → Play gave Steam's "Game
configuration unavailable"); `launch` uses the shortcut's own appid and errors `NOT_IN_LIBRARY`, the app then adds it
and plays. Unity IL2CPP fixes share one Cpp2IL run (`unity_text_input.Il2cppReturnPatch`, `ALL_TARGETS`, cache keyed by
global-metadata.dat): `frame.unity_runtime_msaa_off` (OVRManager raises MSAA to 4x at runtime: "Switching to the
recommended level" → Lucky's Tale restarted the headset; `OVRDisplay.get_recommendedMSAALevel` → 0) and
`frame.unity_multipass` (Oculus XR Plugin multiview → MultiPass via `OculusSettings.GetStereoRenderingMode`; I Am Cat's
right eye grey). Both checked against Toy Master's Cpp2IL output, not yet in a headset. VR4 quits itself (System.exit
after the intro movie) → catalog `use_alt`. **Catalog changes reach existing games** without hardcoded migrations:
recipes store `catalog_rev` (= `CatalogEntry.rev()`), `library._follow_catalog` re-derives non-user recipes whose entry
changed; a maintained entry beats the user's shared copy when its `updated`/verified date is later (`catalog._newer`).
**Round 2 of reports (2026-10-04):** VR4's campaign hang = one fragment shader reading an uninitialized loop counter
(reporter's capture): vkshim `vk_shader_fix` setting (`<size>:<sha256>:<offset>:<words>`, from the recipe's `adapter:`;
matched by size + SHA-256, words inserted into a copy) — on the device the module is fixed at load ("fixed shader
module (6488 -> 6512 bytes)"); campaign itself not yet seen in a headset by us. Text settings (kind "str") stay out of
the Game settings dialog (saving it dropped them). I Am Cat: `GetStereoRenderingMode` is inlined (no call sites) →
`Il2cppReturnPatch.field_loads` rewrites the field read in `OculusLoader.Initialize` (Cpp2IL gives the field offset,
`field:<name>`); verified with Toy Master: eye swapchains array=1 ×2 instead of array=2. Builds record `recipe_fp`
(`patches/base.recipe_fingerprint`, `Patch.revision`): a changed recipe or revised patch shows "Update on Frame".
Remembered Frames are found again after an address change (`Frame.relocate`: scan, same SSH host key, before
login). Packaged apps show artwork by file path (`thumbs.use_file_paths`, #16). Diagnostics: this boot's kernel log.
Device tests of requested games (2026-10-04, headless): I Am Cat, Myst 3.3.0, BattleGlide, Blade & Sorcery: Nomad
start (RUNNING, frames). Roblox: SIGSEGV in je_free from libroblox.so ~3 s in (likely its anti-tamper vs the re-signed
APK). BONELAB (Unity 2021.3 Vulkan, OVRPlugin 1.94): SIGSEGV with pc == fault addr in vkCreateInstance, called from
libSLZQuestNative.so's Vulkan hooks into an unmapped (unloaded) library right after OVRPlugin's pre-init
xrDestroyInstance; pinning openxr/vrclient libraries (RTLD_NODELETE) did NOT help — find the unloaded library with the
linker's dlopen/dlclose logging next. Accounting+ (Unity 2017.4 built-in Oculus, libOVRPlugin + libvrapi): Unity
never starts VR on the Frame (no OVRPlugin/OpenXR lines; runs as a 2D app), which is why its "controller setup"
screen can't be passed — Unity 2017's Oculus device check, not the controller profiles. Cpp2IL can't read Unity 2017.
The Frame's Android runtime (vrclient.so) knows oculus/touch_controller but not Meta's Touch Plus/Pro profiles
(OVRPlugin suggests those too, routinely) → adapter `profile_remap`.
Headset round 2 (2026-10-04): the Frame drops focus often (ms to 7 s; only some are presence/standby) → games pause
or recentre (B&S Player.OnVRPresence → Teleport, BattleGlide/Unreal pauses); focus_hold now 1 s focused / 1 s max dip
(longer must still pause). Myst crash = vrclient xrSyncActions race after FOCUSED → `sync_guard` (confirmed); its
object glitches: app space warp suspected (FB_space_warp is advertised and used) → recipe `patch_disable_space_warp`.
I Am Cat multipass: poses/times consistent, but every frame is submitted after the next xrWaitFrame (one period late;
Zink + doubled draws) and the game clamps its physics step to 10 ms → judder while still; testing scale 0.8. Roblox's
newest crash is on Fossilize's recording thread → vk_sanitize extended to own-engine libs (engine "Other").
Accounting+: unity_oculus_check + native/ovrpshim (libfp_ovrp.so waits via ovrp_WaitToBeginFrame before Unity
2017's ovrp_Update2) → VR in the headset at ~72 fps (2026-10-04), but stuck at "press any button" although input
reaches its OVRInput cleanly (probe: both Touch connected 0x63, individual buttons, input focus 1) → works with issues.
"outside of frame bounds" warnings stay (~2/frame) and are harmless.
Steam library (GitHub #4/#21/#27, agent v38): shortcuts go to every Steam account (signed-in first; MostRecent isn't
always the account on the Frame); stop_steam also waits for Steam's helpers; after the restart the agent re-reads
shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lost`), else reports it. 0.6.3's Play
auto-repair (`_add_then_play`) crashed (its job got a Job, not a reporter). Uploads resume on a transient OSError.
Lepton's Android 11 has no clipboard service (134 services; checked with `podman exec … service check clipboard`):
SDL/LÖVE apps crashed at start → `frame.sdl_clipboard` (`apk/dex.py`: in-place dex edit, nops one invoke, fixes
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). 2D apps (vr_kind none)
keep every suggested patch that isn't about VR (`needs_vr = False`), not a fixed list. FramePort on the Frame:
`frame/local.py` (127.0.0.1, own key authorized; "This Frame (experimental)"; app data in
~/.local/share/frameport-app because ~/.local/share/frameport is the agent's) and Steam library changes wait while
Desktop Mode is open. Verified on the device 2026-10-04 (install + launch test from the Frame). Library updates on the
Frame run one at a time (agent v40 flock): two quick uninstalls used to bring a removed shortcut back (Roblox).
Headset round 3 (owner's verdicts, 2026-10-04): Blade & Sorcery works — its freezes were the headset's wear sensor
flickering off while worn (vrserver.txt "HMD off/on" 0.5-2 s) → per-game `focus_hold_ms=2500` (only this game reacts;
not a default). BattleGlide works (focus_hold). Myst works with issues (object glitches; space warp ruled out, its
recipe keeps it on). I Am Cat works with issues (judder; scale 0.8 no help). Roblox unsupported (je_free in
libroblox, also with the Vulkan shim). Installs skip the Steam restart when the shortcut is unchanged and wait while a
game runs (agent v37). New default fixes in FramePort's code reach games already in a library: after every app update
`library._follow_catalog` re-derives each non-user recipe once (setting `recipes.app_version`; tests switch it off via
`library.REFRESH_ON_UPDATE`); analysis fields added later still need a re-analysis. Troubleshooting techniques: docs/PLAYBOOK.md "Debugging techniques". Unity `boot.config` "vulkan" substring mislabels GLES games as Vulkan
(I Am Cat ran GLES); OVRPlugin's "Unavailable OpenXR extension: XR_FB_scene" is routine (no longer triaged).
**Round 4 (2026-10-04):** XR_KHR_android_surface_swapchain is listed by the Frame's runtime but returns
FUNCTION_UNSUPPORTED → adapter `surface_emul` (default on, `native/adapter/surface_swapchain.c`): an ordinary runtime
swapchain is returned, the game gets a Surface from a SurfaceTexture on a worker thread (own EGL context, JavaVM from
XrInstanceCreateInfoAndroidKHR), frames are read back (glReadPixels) and uploaded in xrEndFrame (Vulkan: own command
buffer + fence per panel, never waits; GLES: glTexSubImage2D). I Am Monkey's intro works (owner-confirmed). SUPERHOT
(Quest) quit at start: its cloud save folder was mode 1700 and Lepton's app writes through the folder's group →
launch.sh `fix_perms` adds u+rwx,g+rwx (agent v41). Lucky's Tale: Unity's Loading.PreloadManager thread grows the
native heap ~1 GB/s until the OOM killer (Frame freezes); no fix. `perf` works as steamos (paranoid 2; ptrace_scope 1
blocks gdb/eu-stack/debuggerd) — see PLAYBOOK "Debugging techniques". QuestCraft downloads its JRE at first run into
`files/runtimes/JRE` (Pojlib Installer); on the Frame it was missing (unpacked by hand, unverified). WiiCompiled: its launcher shows
(lepton-show-flatscreen) but imports .wcgame files through Android's document picker (OPEN_DOCUMENT), which Lepton
lacks (ActivityNotFoundException) → imported by hand: libmain.so + game.json → internal files/game/<profile>,
DATA/ → external files/WiiCompiledOpenXRVR/DATA, MOD/ → .../RetroRewind6 (decompiled with jadx). Lepton has no
picker at all: a FramePort fix would be an injected picker activity. Generated cover/banner art used to suppress the
Steam placeholder set (no Steam art at all) → fixed in artwork/steam.py.
**Steam library failures (GitHub #21/#30):** on some Frames Steam never loads FramePort's shortcuts.vdf entries (same SteamOS build and arm64 beta client as the dev Frame; IDs correct; file written before Steam starts): Play → console_log `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` (= "Game configuration unavailable", Steam treats the id as a store app). Cause unknown; agent v42/43 diagnostics: `launch` returns `steam` (started / error + code / silent from console_log), `collect_diag` adds `steam_library` (real Steam dir, beta, Steam start vs vdf write time, entries per account, devkit games) + `steam-console.txt` (incl. Steam's `logs/shortcuts.previous.txt` if present; steamclient.so has "LoadShortcuts: rejecting attempt to load shortcuts: invalid account ID"). **Fallback (agent v43):** when Play gets AppError_9 the agent registers the game through Steam's devkit interface (what Valve's Devkit Management Tool does, MIT: `devkit-1 steam://devkit-1/<~/.steam/steam.token>/create-shortcut?response=<file>&gameid=<id>&directory=~/devkit-game` written to ~/.steam/steam.pipe; answer file / `.error`; `~/devkit-game/<id>/launch.sh` = link to the game's launch.sh, `<id>-argv.json` ["launch.sh"] (relative to the folder), `<id>-settings.json` {steam_play 0, compat_tool ""}). Steam adds it live (no restart), picks its own appid (read back from shortcuts.vdf by DevkitGameID), shows it as a normal non-Steam game named "Devkit Game: <id>" (OpenVR 0, but VR works: owner-verified 2026-10-04), art copied to grid/<appid>*. Ids: letter first, then letters/digits/_ only (spaces, '-', leading digit → "missing/invalid arguments"). Later Plays use the devkit entry; uninstall (`devkit_unregister`, `delete-shortcut` live) and purge remove it. Verified on the dev Frame: register/launch/unregister; the AppError_9 trigger itself only in unit tests. GitHub #42 (two Steam accounts, neither MostRecent): Steam rewrote the active account's shortcuts.vdf without FramePort's entries right after starting, and added the devkit entry live without saving it, so `devkit_appid` (vdf only) failed ("Steam added the devkit entry but didn't save it"); agent v55 `devkit_appid_from_log` reads Steam's `sanitize shortcut app id "~/devkit-game/<id>/launch.sh": replacing 0 with N` console line. GitHub #41 (one account): same Steam behaviour, and every install restarts Steam, which forgets the never-saved devkit entry. Agent v56: the log fallback reads only the current session's console_log.txt; Play re-registers a devkit entry that gets AppError_9; art updates also write the devkit entry's grid art (`copy_grid_art`). Open: why some Frames' Steam never loads or saves shortcuts.vdf (single account too).
**Exit game (GitHub #36, 2026-10-04):** `Apps.TerminateApp` (what Exit game calls; reachable for tests through Steam's CEF devtools on 127.0.0.1:8080, SharedJSContext, `Runtime.evaluate` — a stdlib websocket client is enough) closed both a normal shortcut and a devkit entry on the dev Frame within 1-3 s. SIGTERM to Steam's `reaper` alone left launch.sh + Lepton (setsid) + the container running (an earlier headset launch ran on for 87 min); SIGTERM to launch.sh cleans up in ~4 s. Agent v44: launch.sh's 2 s loop ends the game when its parent (the reaper) is gone; `upgrade_launchers` (from ensure_host_fixes) adds that to existing launchers in place. Which in-headset exit path fails is still unknown. Screenshots on the Frame: all under SteamVR's appid 250820 (`760/remote/250820/screenshots`, screenshots.vdf has creation time, no game).
**SteamVR dashboard at game start (2026-10-04):** the dashboard (Resume game / controller / VR options) was open whenever a FramePort game started (also over Lepton's 2D launcher). Steam's UI exposes `SteamClient.OpenVR.VROverlay.{IsDashboardVisible,HideDashboard,ShowDashboard}` (CEF devtools, `steam_js` in the agent). Agent v44: launch.sh starts `_dashboard_worker` (waits for FrameBridge's first `pacing:` line in launch.log, then hides a visible dashboard for 25 s, at most 3 times; log `<base>/dashboard.log`; opt out with env FRAMEPORT_KEEP_DASHBOARD=1). Verified on the dev Frame (dashboard shown → 4XVR launched via Steam → hidden ~5 s after the first frames); not yet seen in the headset. **Headset (ITR2, 2026-10-05): too late** — Steam shows its frame menu (`valve.steam.gamepadui.frame.menu`, vrwebhelper_systemui.txt `[PooledPopups] Showing`) ~0.3 s after the game's first submitted frame (FrameBridge `new layer:`), but the first `pacing:` summary comes ~8 s later; the owner had pressed Resume (`[HideDashboard] return_to_game`) before the worker looked. Agent v52: watches from the first `new layer:` line (incremental log reads, 0.5 s polls, 30 s window). Agent v59: watches 120 s and hides up to 10 times (the menu came back after 30 s in the owner's sessions). Agent v53: stops for good once the player opens the dashboard with the controller (`toggle_dashboard_action` in vrwebhelper_systemui.txt since the worker started): v52 closed it 60 ms after each press, and the game, paused for it, stayed paused.
**Space warp / ITR2 (2026-10-04):** Into The Radius 2 (UE5, `libUnreal.so` + OVRPlugin, Vulkan) uses Application SpaceWarp (extra swapchains `376x376 format=97` motion vectors + `format=129` depth); #35 reports flickering textures. OVRPort's `patch_disable_space_warp` has no effect on UE5, and hiding XR_FB_space_warp in FrameBridge's enumerate doesn't reach the game either: **OVRPort's dispatcher offers/enables XR_FB_space_warp itself**. FrameBridge `hide_space_warp` (per game, off) hides it and strips `XrCompositionLayerSpaceWarpInfoFB` (1000171000) from the projection views (log `hide_space_warp: removed space warp info`); the game still renders its MV swapchains. Test build installed on the dev Frame; flicker not yet checked in a headset. Heuristic: UE5 + OVRPlugin shows it as an option (off). `equirect_emul` is suggested for Unreal games whose graphics API isn't detected ("GLES or unknown"); on Vulkan it switches itself off (Myst/Riven/ITR2): harmless, but Unreal Vulkan detection is a known gap.
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). Verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
**Heuristics eval (2026-10-04):** `scripts/eval_heuristics.py "<VR CyberDeck Portable>"` → 19/44 recipes exact, 322/352 fields (most diffs: `frame.unity_text_input` suggested where older catalog recipes lack it; the rest are runtime-only findings: use_alt, vk_shader_fix, focus_hold, sync_guard, multipass). `PATCHED/` copies aren't scored (already converted).
**Power (0.9, agent v47):** sidebar bar below the Frame card: Sleep / Restart / Shut down, each confirmed (`app.frame_power`, a running game asks again). logind answers CanSuspend/CanReboot/CanPowerOff = "challenge" for an SSH session but "yes" inside a user unit, so agent `power` runs `systemctl suspend|reboot|poweroff` from a transient `systemd-run --user --on-active=3` timer (the command answers first). Restart verified on the device (back in ~84 s, new boot id).
**Proton default:** the newest **stable** ARM64 Proton (`pick_proton`; owner's headset A/B); `pcvr.proton_tool` dropdown (Experimental = alias `proton-experimental`), `ensure_proton`, `proton_alternative_worth_trying`. → docs/DEVICE_NOTES.md#proton-default-stable-vs-experimental
**USB cable link:** `usb0` is a USB NCM gadget (Developer Mode only); Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py`); ~37 MB/s; `scripts/usb_autotest.py`, `frameport frame usb-check`; discovery prefers USB > hotspot > network (`discovery.dedupe`). → docs/DEVICE_NOTES.md#usb-cable-link
**Proton default (agent v51, owner's choice 2026-10-05):** the newest **stable** ARM64 Proton is the default for PC VR games (`pick_proton`: asked-for name/alias, else newest installed stable, else any stable). Agent v49-50 defaulted to Experimental; the owner's headset A/B with Rick and Morty (Experimental 69 % reprojected frames, stable felt much smoother) reverted it. The game page's Customize shows `pcvr.proton_tool` as a dropdown (Stable (default) / Experimental = alias `proton-experimental`); `ensure_proton` installs a chosen one (it asks once more for the runtime Steam only names after Proton is installed; Experimental installed unattended on the device in 53 s). A PC VR launch test that fails on stable suggests Experimental (`pipeline.PROTON_TOOL`, `proton_alternative_worth_trying`, button "Try Proton Experimental and reinstall"). Remaining Rick and Morty stutter on head turns = the game rendering below the refresh rate (reprojection), not Proton.
**USB cable link (verified 2026-10-05, cabled to the dev PC):** `usb0` is a USB **NCM** gadget (configfs g1: `ncm.usb0` + `ffs.adb`, Valve 28de:2460) set up by `usb-ncm-gadget@usb0.service`, which only runs with `ConditionPathExists=|/etc/systemd/system/adbd.service.d/steamos-devkit-enabled` = **Developer Mode only**. `usb-ncm-dnsmasq@usb0` serves DHCP with fixed `dhcp-host` entries: Frame 10.86.200.233, PC **10.86.200.234** (`frame/usb.py` FRAME_USB_IP/PC_USB_IP). Windows 11 binds its built-in "UsbNcm Host Device" (no driver); WSL mirrored sees eth 10.86.200.234. USB is high-speed (USB 2.0): upload 36.9 MB/s vs 11.3 MB/s home Wi-Fi. Frame→PC over the cable worked without a firewall change. `scripts/usb_autotest.py` (Windows via powershell.exe from WSL), `frameport frame usb-check`. Wizard "Set up with a USB cable" (`PairingServer(host=PC_USB_IP)`; the real bootstrap ran end to end over USB). Discovery dedupes by SSH host key and prefers USB > Frame hotspot > network (`discovery.dedupe`); a lost address is re-found by key (`Frame.relocate`, tested with a dead saved address).
**Catalog updates without a release (2026-10-05):** apps fetch `catalog/games/*.yaml` from GitHub `main` (`recommend/catalog.refresh_remote`: one API call `git/trees/main?recursive=1` + raw.githubusercontent downloads of changed blobs, cached as `catalog-gh-<sha>.yaml`, every 6 h; GUI at start + Settings → Data "Check now"; CLI background thread + `frameport catalog-update`). `catalog.load()` only reads the cache. A remote entry is skipped (`unusable_reason`) when it names a patch/adapter key this app doesn't register, has a field `CatalogEntry` doesn't know, or `min_app` > this version: add `min_app` (or rely on new patch ids) when a config needs an unreleased FramePort. Remote beats bundled unless the bundled entry has a later updated/verified date; the user's own entries still win as before. Off: setting `catalog.auto_update`, env FRAMEPORT_NO_CATALOG_UPDATE; FRAMEPORT_CATALOG_URL still overrides the source. So: **pushing a catalog change to main reaches users within 6 h** (their recipes follow via catalog_rev → Update on Frame).
**Start activity / QuestCraft / ITR2 pop-in:** `frame.start_activity` (analysis `extra.vr_activity`, `axml.set_start_activity`; OVRPort gives every MAIN activity LAUNCHER, so take the VR activity from the original APK); Unreal reads user config from **internal** storage; Lepton passes `LEPTON_ENV_<NAME>` to the app as `<NAME>` (QuestCraft `LIBGL_NOERROR`); vkshim `vk_query_slots` left off. → docs/DEVICE_NOTES.md#start-activity--questcraft--itr2-pop-in
**Vulkan validation / ITR2 solved:** bundle Khronos' `libVkLayer_khronos_validation.so` into a test APK + vkshim `vk_validation=1` (logcat tag VALIDATION); `vk_spec_fixes=1` drops ITR2's invalid depth resolve; VALVE_rpo removed its fog (catalog `lepton_env: VK_INSTANCE_LAYERS`). Agent `not_started` waits 30 s for "Boot complete!". → docs/DEVICE_NOTES.md#vulkan-validation--itr2-solved
**GitHub #38/#39:** SteamVR rejects image rects past their swapchain (-25) → FrameBridge `rect_clamp` (default on), triage `swapchain-rect-invalid`; The Room VR MSAA crash in libgallium. → docs/DEVICE_NOTES.md#github-3839-powerwash-simulator-the-room-vr
**Headset round 2026-10-05:** FrameBridge `pose_consistency` (I Am Cat judder), xrshim `haptic_fix` (Lucky's Tale), **`focus_hold` on by default with 5 s** (wear-sensor dips, `logs/eyetracking.txt`), `unity_no_overlay_copy` (The Room VR), ovrpshim frame wait for Unity 2019 (`UnityOculusCheck.legacy_loop`), `frame.ovr_trace` (opt-in, `native/ovrtrace`, logcat `fp_ovrtrace`). → docs/DEVICE_NOTES.md#headset-round-2026-10-05-evening
**Unity built-in VR input:** probe builds `FRAMEPORT_INPUT_PROBE=1`; ovrpshim adds a `GetMouseButtonDown` icall click (Accounting+); libunity 2019 only polls Touch when the device model starts with "Oculus" → `unity_oculus_check` revision 4 `oculus_model_checks` (BattleSisters); `haptic_fix` suggested for every game with libOVRPlugin.so. → docs/DEVICE_NOTES.md#unity-built-in-vr-input-accounting-battlesisters
**Vader Immortal (GitHub #49):** stuck on its loading card and leaked GPU mappings (solved below: `gl_error_clear`); diagnostics FrameBridge `layer_debug`, `strip_color_bias`, `frame_balance`, `snapshot=N` (`snapshot_gl.c`: saves the submitted left-eye image as `files/fb_snap_*.ppm`). → docs/DEVICE_NOTES.md#vader-immortal-headless-investigation
**SteamVR overlay apps:** host OpenVR overlays composite over Lepton games; detection `analysis/vroverlay.py`, patch `pcvr.vr_overlay`, Linux fields `vr_overlay`/`vr_overlay_autostart`, agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay`; SteamVR on the Frame reads only `binary_path_linux_arm`; FramePort's own autostart (`ensure_overlay_service`, `frameport-vr-overlays.service`). See also docs/FRAME_RUNTIME.md "Overlay apps". → docs/DEVICE_NOTES.md#steamvr-overlay-apps
**Vader's leak solved / AC Nexus flips:** stale GL errors broke SteamVR's GL interop → adapter `gl_error_clear` (GLES, catalog for Vader ep1-3); read `xrclient_<pkg>.txt` in Steam's logs for runtime errors; the runtime caps swapchains per session (~15) → flip emulation in place; adapter `flip_dump`. → docs/DEVICE_NOTES.md#vaders-leak-solved--ac-nexus-flips
**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).
**Double launch (agent v57, 2026-10-06):** a second Play while Lepton still boots (~10 s with nothing to see) made
the second Lepton stop the first one's container ("Waiting for steamlaunch-<appid> (PID …) to exit", exit 137
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
`flock` on `<base>/.launch.lock` (fd 9, inherited by Lepton; the watchdog and dashboard helpers close it with
`9>&-`); a second launch exits 0 and logs to `<base>/launch-dup.log`. `upgrade_launchers` adds it to existing
launchers. Verified on the device (second launch during boot: ignored, the first kept running). Vader on its loading
card ignores input too (owner pressed/held every button: presses reach the runtime, the card never changes).
**Issue triage round (2026-10-06, owner's headset):** vibration in OVRPlugin games = 2 s vibrations updated per frame and
stopped with amplitude 0; FrameBridge turns amplitude 0 into xrStopHapticFeedback (else each buzz ran 2 s), haptic_fix
uses the envelope RMS (was its peak), per-game `haptic_scale` → Creed/The Boys/Jurassic World fine. Lambda1VR (TBXR):
loader named by Build.MANUFACTURER (`frame.tbxr_vendor`), no graphics extension enabled (FrameBridge adds
XR_KHR_opengl_es_enable + asks for the requirements on -50), always multisampled render-to-texture (GL shim gives
single-sampled stand-ins when GL_EXT_multisampled_render_to_texture is hidden) + its xash/ data → works. Jurassic World:
`frame.vrapi_stub`. Metro Awakening: contributor's vkshim fixes + hide_space_warp + no VALVE_rpo (jumping polygons like
ITR2). Pinball FX VR: hide_space_warp (stutters). Eleven: a Meta online request fails after platform init → needs Meta
services. Star Wars Tales: top half black + freeze after loading; ruled out: asset-file paks (never requested), Valve
foveation (off: no change), GL errors (MESA_DEBUG=1: none), EGL_BAD_ACCESS once in OVRPlugin init (harmless); the game's
eye image reads back black. BlazeRush: VrApi bridge exports + ovr*_ToString stubs + avatar stub get it to the menu room
at 72 fps, full input reaches it (diagnostics `input 5s`: Touch type only, sticks 1.0, buttons, poses 0x8f), but the room
draws no controllers and ignores input; avatar loader forced to its "Failed" path (no logged-in user) changed nothing.
`frame.ovr_trace` can't trace Unity games (P/Invoke dlsym; nothing imports ovr_*). Headless: the VrApi bridge's 30 s
head-pose deadline ends VR mode without a worn headset (not a game bug).
**Lepton storage (2026-09-30):** each app's /sdcard (= /storage/emulated/0 → `<base>/lepton-data/external`) has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (liblepton/mounting.sh, only if they exist at start); agent v24 `storage_targets` reads that mapping. Android's MediaProvider canonicalises paths to /home/steamos/... and rejects every file ("doesn't appear under [/system/media...]"), `sm list-volumes` is empty: the media index never works, apps must browse folders. Lepton installs with `adb install -g` (runtime permissions granted, MANAGE_EXTERNAL_STORAGE too). Files: `install/files.py`, `frameport frame send|storage`, GUI Files tab (formerly Frame → Send files).
**SteamVR per-app settings (2026-09-30):** editing steamvr.vrsettings while SteamVR runs is lost; the web API (127.0.0.1:27062 /app/setsettings) needs `x-steamvr-secret`. `native/vrsettings` = `fp_vrsettings.exe` (freestanding, OpenVR `FnTable:IVRSettings_003` as a Utility app, loads SteamVR's bin/win64/openvr_api.dll) sets them live and SteamVR persists them: section `steam.app.<shortcut appid>`, keys `preferredRefreshRate` (float) and `motionSmoothingOverride` (0 global, 1 on, 2 off, 3 always). Steam Link (vrlink) lists the Frame's rates 72/80/90/96/108/120/144 in vrserver.txt and follows the per-app preference ("host preferred N Hz"; whether the key is honoured is unverified in-headset yet). Judder metric: vrcompositor.txt session summary dropped + "Timed out. N total" (Stormland: 0 dropped but 313 timeouts in 2 min); fpsVR (`%LOCALAPPDATA%\fpsVR\*.json`, 0.1 ms histograms) gives p99 CPU/GPU ms. `pcvr.steamvr_tuning` (default on, PC only) applies on Play: highest rate whose budget ≥ p99×1.05, at least one step down, smoothing on.
**Language packs (merged from PR #20, 2026-10-04):** overport's `libovrplatformloader.so` is a dispatcher that `dlopen`s Meta's own loader (`libovrplatformloader_meta.so` / `_meta_q1.so`, also `libpxrplatformloader.so`) and keeps its own message queue; `ovr_LanguagePack_GetCurrent/SetCurrent` are 8-byte `return 0` stubs in it, `ovr_AssetFile_GetList` forwards to Meta's loader. `frame.langpacks` (opt-in, `native/langpack`) serves `<tag>.lang` files from the game's data; `elf.hide_exports` marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's `is_symbol_global_and_defined` is from memory). `libfp_langpack.so` is built here (`python native/build.py --only langpack`) and committed. Owner-verified 2026-10-04: Deadpool VR with only `en.lang` and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get `lang_packs` filled after an app update (`library._refresh_data_fields`). The library logs to logcat under the tag `fp_langpack` (dirs looked in, packs found, every language-pack call), so it shows up in the game's `launch.log`; an `ovr_AssetFile_GetList` the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its `Metadata` equals the game's own version string (`ULanguagePacksSubsystem` compares it with `%s.%s.%s.%s.%s` built from the build info, e.g. `1.0.40.356975.Quest` = versionName; found by disassembling `libUE4.so`): the patch writes the APK's versionName into the library (`@FPMETA@` slot, `with_metadata`), `FRAMEPORT_LANGPACK_META` overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled `/sdcard/Android/obb/<pkg>/x.lang`; with the `/storage/emulated/0/Android/obb/<pkg>/x.lang` spelling (Unreal's own, now listed first in `scan_dirs`) German dialogue plays. `FRAMEPORT_LANGPACK_SKIP=<tags>` or a file `fp_langpack_skip` in the obb folder leaves packs out of the list (experiments).
**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`)
@@ -662,56 +675,6 @@ Installed apps find the release themselves (self-update), so the notes are what
- **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)` with `comments{replies}`).
- **Issue templates & labels** (`.github/ISSUE_TEMPLATE/`): `[Game report]` → `game-compatibility` (a game that
doesn't work right); `[Working recipe]` (`working-config.yml`) → `working-config` (a headset-verified config to add);
`[App]` → `bug` (FramePort itself). Mislabel/relabel to match.
- **Working-recipe submissions → the catalog.** The body's `### Recipe` YAML 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`
(writes `catalog/games/<pkg>.yaml`; `--out <dir>` to dry-run into a scratch dir first). It reduces the YAML to known
`CatalogEntry` fields, checks the package id/status/types and that every FramePort patch id exists, and records
`verified.issue: N`. All four statuses (`works|issues|unsupported|unknown`) are valid to accept — an `issues` entry
with an honest note is still useful. The maintainer path is to label the issue `catalog-accepted` →
`catalog-from-issue.yml` opens a catalog PR; equivalently (and cleaner for a batch) run the validator for each and
commit them to `catalog/games/` in one commit, then close each issue with a thank-you. Re-run `scripts/patch_docs.py`
(catalog entries bump each patch's usage count in `docs/PATCHES.md`) and `pytest -k catalog` before committing.
- **Diagnostics zips** (`frameport diag`, agent `collect_diag`): the game page saves `FramePort-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), and `games/<pkg>/`
(`recipe.yaml`, `target/deployment.json` + `target/launch.log`, `logs/*-session-*.log`, `package/AndroidManifest.xml`
+ `package/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 `.zip` false-matches on an empty report. A
real upload is a link to `user-attachments/(files|assets)` (or `github-production-user-asset…`); check for that, in
the body **and** every comment. A `github.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.yaml` from `main` (`catalog.refresh_remote`), recipes follow via `catalog_rev` →
"Update on Frame". So accepting a recipe or adding a triage signature is "live" once pushed.
- *Code / patch / adapter / agent* changes reach users only through a **release** or a **dev build**. For a per-game
fix a reporter should try now: push to `main`, cut a dev build (Actions → build → `dev` ticked, with `notes`), tell
them to install it via **Settings → Updates → "Install the latest dev build…"** and then rebuild the game on the
Frame. Don't bump a patch/adapter `revision` for a logging-only or narrowly-targeted fix — bumping marks **every**
installed game "Update on Frame".
- **Verifying a device fix without the headset:** many runtime failures (e.g. an `xrEndFrame` layer 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 isolated `FRAMEPORT_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:
@@ -719,15 +682,9 @@ MR-only (PASSTHROUGH required + BOUNDARYLESS_APP) → force_passthrough, + USE_S
hand tracking required → controller_fix=0; OVRPlugin + ≥20 GiB → disable_space_warp; Unreal ≤4.21 or Unreal Meta XR
Audio → nodebug; Oculus-OS class referenced by the Unreal audio build or ≥2 Meta libs → oculusos; legacy-VrApi Unity
GLES with MSAA → unity_no_msaa; CryEngine → user.cfg r_variable_rate_shading=0; direct VrApi → bridge (+GL shim for
GLAD/GLES); Unreal → alternate no-ForceQuit build. Scoring (docs/HEURISTICS_EVAL.md, 2026-10-15): analyse the dumps
once (`scripts/heuristics_snapshot.py <out.json> "<VR CyberDeck Portable>" "<PATCHED>"`, ~30 min, keep out of git),
then `scripts/eval_heuristics.py --snapshot <out.json> --split dev|holdout|all [--fit-report]` (catalog-only truth,
per-patch precision/recall, a fixed 20% holdout to catch rules fitted to one game). Read the **dated** score: an
"extra" on an entry verified before the patch existed is a catalog gap, and adapter/device picks are inherited by
catalog recipes anyway. 2026-10-15: strict 5/65 exact, F1 73.8%; dated 39/65, F1 88.6%; remaining misses are
one-game runtime fixes. Don't key a rule on one game's code fingerprint (an Unreal splash/depth "detect" matching
only Star Wars Tales + FNAF HW was rejected: it would also have bumped ANALYSIS_VERSION for every user). Add a rule →
re-run dev + holdout + `pytest -m games`.
GLAD/GLES); Unreal → alternate no-ForceQuit build. Score changes with
`python scripts/eval_heuristics.py "<dumps>"` (catalog off vs verified recipes; currently 33/34 exact — Phantom's
"use the no-ForceQuit build" is only detectable at runtime via triage). Add a rule → re-run the eval + `pytest -m games`.
## Frame operations
- Find/connect: `frameport frame discover` / `frame info` (the remembered Frame is in `<user data>/frames.json`).
@@ -757,19 +714,5 @@ the unresolved eye distortion, and testing the release bundles on real Windows/m
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 as `steamos@<frame>`.
Steam library scheduling (#305, agent v99): `install.steam_library` = now/later/off; agent `library_mode` persists it.
Automatic restarts also hold on a fresh local `frameport-app/gui.alive` (10 s; existing 12 h cap).
Later journals changes and installs steam.service ExecStartPre/ExecStartPost hooks to reapply after Steam's exit
writes and verify before clearing; preserve this ordering and avoid locking across systemctl's startup hooks.
Off Play (also pending Later Play) uses detached systemd-run launch.sh, with no shortcut/devkit registration;
Steam's per-game controller/overlay/Exit game integration is lost. Restart Steam now explicitly bypasses holds,
asks before ending a running game, and closes local FramePort/Desktop Mode. Purge removes the startup hook.
Hook hardening: both lines `-` + `/usr/bin/timeout -k 5 20`; ExecStartPost only `systemd-run --no-block`s the
verify (unit frameport-library-verify) so Steam's start never waits; drop-in removed once mode != later and nothing
pending (`drop_library_hooks_if_idle`). Steam = static user unit steam.service (Type=simple, Restart=always, 90 s).
+85 -96
View File
@@ -7,127 +7,117 @@
![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-blue)
![Steam Frame](https://img.shields.io/badge/Steam%20Frame-supported-1b2838?logo=steam&logoColor=white)
Install Quest games, Android apps, Linux apps and PC VR games on the **Valve Steam Frame**. FramePort sets up the
Frame, patches each game so it runs there, uploads it and adds it to your Steam library with artwork.
[![FramePort: library, one-click install, play, monitor](docs/images/tour-teaser.webp)](docs/media/frameport-tour.mp4)
▶ [Watch the full tour](docs/media/frameport-tour.mp4) · New to FramePort? [Watch the install tutorial](docs/media/frameport-install.mp4)
(about 90 seconds each)
> **Notice:** FramePort doesn't download, share or unlock games. Use games you got legally, for example from
> [SideQuest](https://sidequestvr.com/). FramePort downloads published open-source tools (see [Built on](#built-on))
> and adds its own patches so games run on the Frame.
>
> Not every game runs on the Frame: recipes are tested by the community, and some games need Meta's services or
> hardware the Frame lacks.
## Features
Install games that target the Meta Quest, Android, or general PCVR onto your **Valve Steam Frame**. FramePort handles everything from uploading game files, setting up your Frame, injecting compatibility patches, and adding shortcuts to your Steam library. FramePort aims to be as simple as possible by taking advantage of the fact that the Steam Frame runs on Linux.
![Library](docs/images/library.png)
- **Easy setup:** one line in the Frame's terminal. No root, no password. [What it changes](docs/FRAME_SETUP.md).
- **One click per game:** patch, upload, add to Steam with artwork and test that it starts.
- **Recipes:** tested patches and settings for 120+ games; other games get suggested patches, each explained in
plain words.
- **Game settings:** sharpness, refresh rate, controllers, 360° video and mixed reality as simple switches.
- **More than Quest games:** Android apps in a window, Linux apps, PC VR games and Windows programs.
- **Install links:** "Install with FramePort" buttons on websites ([frameport.app](https://frameport.app) links) open
in FramePort.
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
- **Updates itself**, and reports problems without personal data.
> **Notice:** FramePort explicitly does NOT download, share, or unlock games. You must provide legally obtained game
> executables. Core features of FramePort simply download and wrap other published tools (see [Built on](#built-on))
> with patches provided by FramePort adding a hardware compatibility layer. This enables users to use games/apps legally
> purchased on sites like [SideQuest](https://sidequestvr.com/).
Details for each: [Install and first steps](docs/INSTALL.md).
## Features
![Monitor](docs/images/monitor.png)
![Screenshots](docs/images/screenshots.png)
- **Painless setup:** one short command on the Frame. No root, no `sudo`, no password.
[What it changes](docs/FRAME_SETUP.md).
- **Type on Frame:** use your computer's keyboard on the Frame: in VR apps, Android apps, Steam and the desktop.
[More](#type-on-frame).
- **One click per game:** convert, patch, sign, upload, add to Steam with artwork, launch test. Artwork that can't be found automatically can be picked from the stores or replaced with your own images.
- **Per-game recipes:** a tested catalog plus detection rules; every patch explained in plain words.
- **FrameBridge:** FramePort's OpenXR adapter emulates what the Frame natively lacks (passthrough, room, controller models,
curved and 360° layers); game settings as simple switches.
- **Beyond Quest:** Android apps as windows, PC VR via Proton or Revive, Windows (non-VR) games via Proton, a Files
tab with drag and drop.
- **Linux apps:** install arm64 Linux apps (AppImage, a folder, or a zip/tar archive) on the Frame with a Steam
shortcut; they run natively on SteamOS.
- **Screenshots tab:** the screenshots you took in the headset, sorted by game (matched by play time) and day;
view them and download them to your computer.
- **Live view tab:** watch what the headset shows, with sound, in a browser window on your computer.
- **Self-updating** releases, redacted diagnostics, one-click problem reports and working-config sharing.
## Quick start
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
Silicon) or Linux, unpack it and start FramePort.
2. In FramePort click **Steam Frame → Start setup**. The Frame and your PC must be on the same Wi-Fi (or use a USB
cable).
3. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**, then the app menu → **System →
Konsole**, and run:
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) and unzip the build for Windows, macOS
(Apple Silicon) or Linux, then start FramePort.
2. **Connect the Frame** (once):
1. In FramePort click **Steam Frame → Show setup command**. Keep FramePort open; the Frame and your computer must
be on the same Wi-Fi.
2. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**: the Linux desktop opens on a virtual
screen.
3. Open the app menu (bottom-left corner of that desktop) → **System → Konsole** (or search for Konsole).
4. Type the command FramePort shows exactly as shown (on-screen keyboard or any USB/Bluetooth keyboard) and press
**Enter**. This will run the following [bash setup script](bootstrap/bootstrap.sh).
5. After a few seconds the desktop closes by itself (Steam restarts once); that's expected. If
Steam asks to install **Lepton**, confirm it. FramePort shows the Frame as connected within a minute. No
password needed.
3. **Add games → Scan a folder** with your game backups (APK + OBB, or PC VR game folders).
4. Open a game → **Install on Frame**, then play it from the Frame's Steam library.
```
curl -fsSL https://frameport.app/s | bash
```
Full guide, firewalls and troubleshooting: [docs/INSTALL.md](docs/INSTALL.md). Questions (e.g. how to lay out games
with OBB files): [docs/FAQ.md](docs/FAQ.md).
4. Click **Allow** in FramePort when it shows the same 4-digit code as the Frame. Steam restarts once; if it asks to
install **Lepton** (Valve's Android runtime), confirm it.
5. **Add games → Scan a folder…** with your games, open one and click **Install on Frame**. Play it from the Frame's
Steam library.
## Type on Frame
Full guide, firewalls and troubleshooting: [Install and first steps](docs/INSTALL.md). How to lay out game folders:
[FAQ](docs/FAQ.md).
Typing in VR is painful, so FramePort turns your computer's keyboard into a keyboard for the Frame. Open **Type on
Frame** (its own tab in the sidebar), select a text field in the headset and type: searches, logins, chat, in any
app, in Steam or on the desktop. Paste longer text to type it in one go. Nothing to install: FramePort adds a virtual
keyboard on the Frame while the tab is open, without root.
![Type on Frame](docs/images/type-on-frame.png)
Unity apps whose text fields close the moment you select them on the Frame (no system keyboard there) get a per-game
fix, so Steam's on-screen keyboard and Type on Frame work in them too.
[Details](docs/INSTALL.md#typing-on-the-frame).
## Screenshots
Screenshots you take in the headset show up in FramePort's **Screenshots** tab, grouped by day and matched to the game
you were playing. Open one full size, step through them, and download single shots, a selection or all of them to
your computer.
![Screenshots](docs/images/screenshots.png)
![Screenshot viewer](docs/images/screenshot-viewer.png)
## Live view
The **Live view** tab streams what the headset shows, with its sound, to your computer: click **Start live view** and
it opens in your default web browser (full screen with a double-click; click **Sound on** to hear it, as browsers start
videos muted). Pick 360p to 1080p, or the headset view's full size. The picture comes from SteamVR's built-in headset
view on the Frame and is encoded there while you watch (about one CPU core), so stop it when you're done. It's black
while the headset sleeps.
## Compatibility
Tested games use their recipe (the patches and settings that work for them) automatically; for other games FramePort
suggests patches. See the [list of tested games](docs/GAMES.md). Got a game working? Share its recipe from the game's
**…** menu: [how](docs/INSTALL.md#share-a-recipe-or-report-a-problem).
If a game has already been tested with FramePort, it will automatically use the optimal game config. Otherwise, FramePort
will attempt to guess key patches. If you find a new config that works for an app you are testing, please consider submitting it to the community!
## Compared with other tools
**[List of tested games](docs/GAMES.md)**
FrameDrop and Valve's own tools install an app as it is. FramePort differs in four ways:
- **Free and open source** (GPL-3.0). FrameDrop is free (donationware) without published source.
- **Quest games that don't run on the Frame as they are** get converted and patched, with a tested recipe for 120+
games. The others install the game unchanged.
- **Windows, macOS and Linux.** FrameDrop is for Windows.
- **Wi-Fi or a USB cable**, and no **Pair new host** step.
<details>
<summary>All differences</summary>
| | **FramePort** | **FrameDrop** | **By hand with Valve's tools** |
|---|---|---|---|
| Cost and source code | Free, open source (GPL-3.0) | Free (donationware), source not published | Free, from Valve |
| Runs on | Windows, macOS, Linux | Windows | Depends on the tool |
| First connection | One line in the Frame's terminal; it turns on Developer Mode itself. No password | Turn on Developer Mode, then **Pair new host** | Turn on Developer Mode and pair, or use Android's debug tool (adb) |
| Wireless or cable | Wi-Fi, the Frame's hotspot or a USB cable | Same Wi-Fi network | Wi-Fi, or adb |
| Quest games that don't run as they are | Converted and patched for the Frame | Installed as they are | Installed as they are |
| Knows which patches a game needs | Tested recipes for 120+ games, updated without an app update | Not stated | No |
| In the Steam library | Yes, with artwork and tags | As a "Devkit Game" shortcut | As "Devkit Game: &lt;title&gt;"; not with adb |
| Android apps, Linux apps, Windows programs | All three; Proton (runs Windows programs) is installed for you | All three; install Proton first | Yes; 2D Android apps need an extra file |
| PC VR games | On your PC; some also on the Frame | Not supported | Not supported |
| "Install with …" buttons on websites | Yes ([frameport.app](https://frameport.app) links) | Its own | No |
| A game doesn't start | A launch test reads the logs and suggests a patch | A log viewer | No help |
| Use the Frame from your PC | Live view, Monitor, Files, Screenshots, Type on Frame | Not stated | No |
</details>
Other tools as described on their own pages, checked 2026-10-09:
[FrameDrop about](https://framedropvr.com/about) · [how-to](https://framedropvr.com/how-to) ·
[Valve: loading games on Steam Frame](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames).
Out of date? [Report a problem](https://github.com/spoopyghosty0/frameport/issues/new).
**Tested something? Share it.** In FramePort open the game → **…** → **Share working config…** (it fills in the
recipe for you) or **Report a problem…** (attaches a diagnostics zip with personal data removed). Without the app:
[share a working config](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml) · [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml). Shared configs become built-in recipes for everyone.
## Built on
Most of the work is done by these projects:
[OVRPort](https://github.com/Android-XR-Bridge/OVRPort) (Quest → OpenXR, originally
[ovrport/app](https://github.com/ovrport/app)) · Valve Lepton, Proton and SteamVR ·
[Revive](https://github.com/LibreVR/Revive) · Mesa (Zink) · [Khronos OpenXR SDK](https://github.com/KhronosGroup/OpenXR-SDK)
· Eclipse Temurin, Android apksigner and NDK · [Flet](https://flet.dev) · OculusDB and Steam store data.
What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
What FramePort adds itself: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
## Documentation
| | |
|---|---|
| [Install and first steps](docs/INSTALL.md) | Install, connect, update, PC VR games, files, problem reports |
| [What the setup changes](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
| [FAQ](docs/FAQ.md) | Game folders and common questions |
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
| [Contributing](CONTRIBUTING.md) | Code, recipes, translations |
| [INSTALL.md](docs/INSTALL.md) | Install, connect, update, PC VR, files, problem reports |
| [FRAME_SETUP.md](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [COMPATIBILITY.md](docs/COMPATIBILITY.md) | What runs and how well |
| [GAMES.md](docs/GAMES.md) | Tested games and how well they run |
| [PLAYBOOK.md](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [FRAME_RUNTIME.md](docs/FRAME_RUNTIME.md) | Steam Frame runtime facts |
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How the code is organised |
| [CONTRIBUTING.md](CONTRIBUTING.md) | Code, recipes, translations |
## Development
@@ -138,9 +128,8 @@ uv run frameport --help # command line
uv run pytest # tests
```
## AI usage
I don't have much free time for this project, so I use AI tools to debug compatibility problems faster.
## AI Usage Notice
While I would like to program everything manually, I no longer have much free time for personal projects. As a result I make use of AI tools to make it significantly quicker to debug compatibility issues.
## License
-3
View File
@@ -5,15 +5,12 @@ FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the followin
## Included in this repository / the release bundles
| Component | Where | License |
|---|---|---|
| LuaJIT (Lua engine for verified xLua pointer compatibility, github.com/LuaJIT/LuaJIT) | `artifacts/arm64-v8a/libfp_luajit.so`; pinned build in `native/luajit/build.py` | MIT (`native/luajit/COPYRIGHT`, included in bundles as `artifacts/licenses/LuaJIT.txt`) |
| OVRPort VrApi → OpenXR adapter (`native/vrapi`, github.com/Android-XR-Bridge/OVRPort), with FramePort's changes | `native/vrapi-bridge/`, `artifacts/arm64-v8a/libvrapi.so` | GPL-3.0 (`native/vrapi-bridge/LICENSE.upstream`) |
| OVRPort platform compatibility library (`native/platform`) | `native/platformcompat/`, `artifacts/arm64-v8a/libovrplatformcompat.so` | GPL-3.0; Meta Platform SDK headers under the Meta Platform Technologies SDK License (`native/platformcompat/licenses/OCULUS-PLATFORM-SDK.txt`) |
| Khronos OpenXR SDK headers | used to build the native layers | Apache-2.0 / MIT (`native/vrapi-bridge/licenses/OPENXR-SDK.txt`) |
| Android NDK runtime (statically linked libc++) | native Android libraries | Apache-2.0 with LLVM exception, plus legacy notices (`native/vrapi-bridge/licenses/ANDROID-NDK.txt`) |
| Flet and Flutter (desktop app runtime) | release bundles | Apache-2.0 / BSD-3-Clause |
| flet-dropzone / desktop_drop (drag-and-drop) | release bundles | Apache-2.0 / MIT |
| FFmpeg 7.1.1 hardware HEVC wrapper (LGPL configuration, without GPL/nonfree components) | `artifacts/hevc/libstagefrighthw.so`; source/rebuild instructions in `native/hevc/build.py` and `native/hevc/README.md` | LGPL-2.1-or-later (`artifacts/hevc/COPYING.FFmpeg`); unmodified source: https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz |
| AOSP Android 11 native media/utility headers | `native/hevc/platform/` | Apache-2.0; copyright/license notices retained in the headers |
| Python packages (paramiko, zeroconf, psutil, pyelftools, capstone, UnityPy, PyYAML, requests, typer, pyaxmlparser, Pillow, cryptography, …) | release bundles | their own licenses (see each package's metadata) |
FramePort's own native code (the FrameBridge adapter, GL/Vulkan/OpenXR shims, the Windows helpers) is GPL-3.0-only.
+98 -5236
View File
File diff suppressed because it is too large. Load diff
-40
View File
@@ -1,40 +0,0 @@
# Announcements
`current.md` is the one announcement FramePort shows as a pop-up in the app. Edit it on `main`: the app reads it
from GitHub `main` (raw.githubusercontent.com) at most every 6 hours, so a push reaches everyone without a release.
## Who sees it
- **Only people who already used FramePort before the announcement's `date`.** A new install never shows one, and
nor does an announcement published before FramePort was installed.
- **Once.** Closing it in any way (the X, a click outside it, Escape, or its button) dismisses it for good. The app
remembers dismissals in its settings (`announce.dismissed`).
- **A new announcement = a new `id`.** Fixing a typo with the same `id` doesn't show it again to anyone who closed it.
A new `id` shows to everyone who used FramePort before its `date`, whether they closed the earlier one or not.
## The file
```markdown
---
active: true # false = nothing is shown (the fields are still checked)
id: meetup-2026-11 # letters, digits, . _ - — change it for every new announcement
title: Steam Frame meetup on Discord # required
date: 2026-11-02T18:00:00Z # required: when it was published (UTC; a bare date = midnight UTC)
image: announcements/meetup.png # optional: a file in this folder, or an https URL (PNG, JPEG, WebP, GIF; ≤ 5 MiB)
image_alt: The meetup poster # optional: what the picture shows (screen readers)
button: Join the meetup # optional: an action button, with url
url: https://example.com/meetup
expires: 2026-11-30 # optional: not shown after this day (UTC, inclusive)
---
The body, in Markdown: **bold**, *italic*, `code`, [links](https://frameport.app/), lists, headings.
```
The body is Markdown (no HTML). A `date` in the future works as a schedule: nobody sees it before then.
Images look best at about 2:1 (e.g. 1200×600). The pop-up is 600 px wide and shows between 140 and 320 px of the
picture's height.
`pytest tests/test_announcements.py` checks this file, so a broken announcement fails CI instead of silently showing
nothing.
To turn announcements off for testing: `FRAMEPORT_NO_ANNOUNCEMENTS=1`.
-16
View File
@@ -1,16 +0,0 @@
---
# The announcement FramePort shows once (announcements/README.md). Switched off: set active: true.
active: false
id: example-2026-10
title: Example announcement
date: 2026-10-15T12:00:00Z
# image: announcements/example.png
# image_alt: What the picture shows
# button: Read more
# url: https://frameport.app/changelog/
# expires: 2026-11-30
---
This is where the text goes, in **Markdown**: *emphasis*, `code`, [links](https://frameport.app/) and lists.
- Keep it short: one or two paragraphs.
- Change `id` for every new announcement.
+8 -24
View File
@@ -1,32 +1,16 @@
1a4dce5c4a6d0dfc5f20e55ac29e70c73cc2cc124c21ca6a7e1b316e6afb1c79 ./arm64-v8a/libVkLayer_fdm_darken.so
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
222727ddabf7eaee08c92e86c64f02edc6639667e9041ab2629032b885c9ae3a ./arm64-v8a/liba32bridge.so
c9c3da5cac224c38e2210b781bd12b7f97e02bd6b0ebb0ef85543e4ea4b76671 ./arm64-v8a/liba32rt.so
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/libfp_langpack.so
69102b128e00fa1809489a7308e09c1505f563bf7091bbdd3fe5a58e4cc96c2c ./arm64-v8a/libfp_luajit.so
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
98a99cefe93079268d9158b23fa8ad5c7ade8e54c0d31526892635625eb3ef04 ./arm64-v8a/libfp_ovrp.so
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
468b97ee3c36481d939a87425b15880c7a801d16732350e1e2832b385d63f20a ./arm64-v8a/libfp_vk.so
7522a7cfb236e48d3442d418c4820cd37760a9627c6bdef80fb0df82df5fd826 ./arm64-v8a/libfp_vk.so
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
2d1726aaa8ee0ce788df1b8727bd55ab72efae749e7652f38f67e5e317b74b2f ./arm64-v8a/libopenxr_loader_generic.so
06c8a17b37cb1513fdfeba87ece457a02097bb22dd0a3a3a8719fb8fea16b52a ./arm64-v8a/libframe_xrshim.so
9f5ba793d1804728394a8fe9e83028e13f1bc939b558892e7beb5c3662317259 ./arm64-v8a/libglshim.so
cc5b493cadefe76ad13fd096d3f8494a140ba593e0e1692db12d284bc548c1cf ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
eba4e9ed8563fa0de00d88bb85f960e79f13a6f60eddd437b203c8d3b7d308af ./armeabi-v7a/libopenxr_loader_generic.so
aa4dc0020c77e12d41ef6ce80b23ad90c9cb7e882338eddf2261efe8645cf38d ./arm64-v8a/libvrapi.so
c10412dd76a65a1d587b7ed226367d1a6f2bec238c733c97ee67eb8870c868e8 ./armeabi-v7a/libopenxr_loader_generic.so
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
c2c70f4af1a5c3e6089dff1130161dd417f8044f99ec639eeecf1083930aab16 ./win-x64/fp_oculushmd.exe
60ebc5fea05b8945082e2ca8b9f2eb74616012a26eb50133af273dff0fcddc42 ./win-x64/fp_vrsettings.exe
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable → Regular
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
-502
View File
@@ -1,502 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
Binary file not shown.
-19
View File
@@ -1,19 +0,0 @@
{
"revision": 8,
"runtime_sha256": "456e912c75cd389abcf6a63bc80e2a53bdc334371d00b200c93680388ae955e2",
"files": {
"libstagefrighthw.so": "141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c",
"podman.py": "529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca",
"media_codecs_frameport.xml": "46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405",
"COPYING.FFmpeg": "b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe"
},
"codecs": [
"video/hevc",
"video/avc",
"video/x-vnd.on2.vp9"
],
"build": {
"ndk_revision": "27.2.12479018",
"ffmpeg_source_sha256": "733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1"
}
}
-32
View File
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<MediaCodecs>
<Decoders>
<MediaCodec name="OMX.frameport.hevc.decoder" type="video/hevc" rank="64">
<Limit name="size" min="128x128" max="8192x8192" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-138240" />
<Limit name="blocks-per-second" range="1-7864320" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
<MediaCodec name="OMX.frameport.avc.decoder" type="video/avc" rank="64">
<Limit name="size" min="128x128" max="8192x8192" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-138240" />
<Limit name="blocks-per-second" range="1-7864320" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
<MediaCodec name="OMX.frameport.vp9.decoder" type="video/x-vnd.on2.vp9" rank="64">
<Limit name="size" min="128x128" max="4096x2304" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="1-36864" />
<Limit name="blocks-per-second" range="1-2211840" />
<Limit name="bitrate" range="1-245000000" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
</Decoders>
</MediaCodecs>
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/python3
"""Add shared codecs to Lepton containers without editing their shared rootfs."""
import hashlib
import json
import os
import re
import shutil
import sys
from pathlib import Path
from xml.etree import ElementTree as ET
def mounts(directory, config, args):
if not args or args[0] != "run":
return []
name = None
for i, arg in enumerate(args):
if arg == "--name" and i + 1 < len(args):
name = args[i + 1]
elif arg.startswith("--name="):
name = arg.partition("=")[2]
if config.get("scope") == "shared":
# The common launcher supplies these values. Do not intercept arbitrary
# Podman containers or depend on a game's package/modified APK.
appid = os.environ.get("SteamAppId", "")
if not re.fullmatch(r"[0-9]+", appid) or name != f"lepton-steamlaunch-{appid}":
return []
app = Path(os.environ.get("STEAM_COMPAT_INSTALL_PATH", ""))
if app.name != "lepton-app" or not app.is_dir():
return []
root_arg = None
for i, arg in enumerate(args):
if arg == "--rootfs" and i + 1 < len(args):
root_arg = args[i + 1]
elif arg.startswith("--rootfs="):
root_arg = arg.partition("=")[2]
if not root_arg or not root_arg.endswith(":O"):
return []
root = Path(root_arg[:-2]).resolve()
else: # old per-game deployments remain compatible during migration
if name != f"lepton-steamlaunch-{config['appid']}":
return []
root = Path(config["lepton"]).resolve().parent / "images" / "rootfs"
expected_root = str(root) + ":O"
if not any(arg in (expected_root, "--rootfs=" + expected_root) for arg in args):
return []
device = Path("/dev/video-dec0")
runtime = root / "vendor/lib64/libstagefright_softomx.so"
upstream_plugin = (root / "vendor/lib64/libstagefrighthw.so").exists()
for i, arg in enumerate(args):
if arg == "--mount" and i + 1 < len(args):
fields = dict(item.split("=", 1) for item in args[i + 1].split(",") if "=" in item)
target = fields.get("destination", fields.get("target"))
if target == "/vendor/lib64/libstagefrighthw.so":
upstream_plugin = True
if target == "/vendor/lib64/libstagefright_softomx.so" and fields.get("source"):
runtime = Path(fields["source"])
if upstream_plugin:
print("FramePort video: using the runtime's hardware codec plugin", file=sys.stderr)
return []
if not device.exists() or hashlib.sha256(runtime.read_bytes()).hexdigest() != config["runtime_sha256"]:
print("FramePort video: device or runtime ABI differs; retaining the stock codecs", file=sys.stderr)
return []
xml = ET.parse(root / "vendor/etc/media_codecs.xml")
if not any(node.get("href") == "media_codecs_frameport.xml" for node in xml.getroot().findall("Include")):
ET.SubElement(xml.getroot(), "Include", href="media_codecs_frameport.xml")
# Immutable plugin versions can serve simultaneous app launches and
# different Lepton installations. Never share a temporary XML filename.
# The version directory stays as the agent verified it: the merged list goes to the user's runtime dir.
root_key = hashlib.sha256(str(root).encode()).hexdigest()[:16]
merged = merged_dir() / f"media_codecs.{root_key}.xml"
temporary = merged.with_suffix(f".{os.getpid()}.tmp")
xml.write(temporary, encoding="utf-8", xml_declaration=True)
temporary.replace(merged)
# Lepton supplies its own /dev tmpfs. A Podman --device node disappears
# beneath it; a bind mount matches Lepton's existing GPU/sound device setup.
result = ["--mount", f"type=bind,source={device},destination=/dev/video-dec0,rw"]
for path, target in (
(directory / "libstagefrighthw.so", "/vendor/lib64/libstagefrighthw.so"),
(merged, "/vendor/etc/media_codecs.xml"),
(directory / "media_codecs_frameport.xml", "/vendor/etc/media_codecs_frameport.xml"),
):
if not path.is_file():
raise FileNotFoundError(f"missing video codec mount: {path}")
result += ["--mount", f"type=bind,source={path},destination={target},ro"]
print("FramePort video: loading the Iris hardware codec plugin for this container", file=sys.stderr)
return result
def merged_dir():
runtime = os.environ.get("XDG_RUNTIME_DIR", "")
base = Path(runtime) if runtime and Path(runtime).is_dir() else Path.home() / ".cache"
path = base / "frameport-video"
path.mkdir(mode=0o700, parents=True, exist_ok=True)
return path
def switched_off(directory):
"""FRAMEPORT_NO_HW_VIDEO=1 (e.g. in a game's Steam launch options) or the Frame-wide switch (FramePort's
setting; the agent writes video-codec/disabled) leave every container with Android's stock codecs."""
if os.environ.get("FRAMEPORT_NO_HW_VIDEO", "") not in ("", "0"):
return True
return directory.parent.name == "versions" and (directory.parent.parent / "disabled").exists()
def real_podman(directory):
"""Find Podman independently of deployment.json, without recursing into this wrapper."""
own_bin = (directory / "bin").resolve()
own_script = Path(__file__).resolve()
# Lepton runs its `podman exec` calls (boot wait, app pid, logcat mirror) with the Android guest's PATH
# (/product/bin:/system/bin:...), which has no host Podman: the system folders come after PATH. Failing there
# broke Lepton's logcat mirror and app-pid checks, and the container was stopped early.
entries = os.environ.get("PATH", os.defpath).split(os.pathsep) + ["/usr/local/bin", "/usr/bin", "/bin"]
for entry in entries:
folder = Path(entry or os.curdir).resolve()
if folder == own_bin:
continue
found = shutil.which("podman", path=str(folder))
if found:
executable = Path(found).resolve()
if executable.parent != own_bin and executable != own_script:
return str(executable)
raise RuntimeError("no real Podman executable found outside the codec wrapper directory")
def main():
directory = Path(__file__).resolve().parent.parent
args = sys.argv[1:]
fallback = real_podman(directory)
if switched_off(directory):
os.execv(fallback, [fallback, *args])
try:
config = json.loads((directory / "deployment.json").read_text())
podman = Path(config.get("podman", fallback)).resolve()
if podman.parent == (directory / "bin").resolve() or podman == Path(__file__).resolve():
raise ValueError("configured Podman points to the codec wrapper")
extra = mounts(directory, config, args)
launch_args = [args[0], *extra, *args[1:]] if extra else args
os.execv(str(podman), [str(podman), *launch_args])
except Exception as exc: # a codec/configuration failure must never prevent the stock container from starting
print(f"FramePort video: retaining stock codecs: {exc}", file=sys.stderr)
os.execv(fallback, [fallback, *args])
if __name__ == "__main__":
main()
-56
View File
@@ -1,56 +0,0 @@
===============================================================================
LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
Copyright (C) 2005-2026 Mike Pall. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
===============================================================================
[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
Copyright (C) 1994-2012 Lua.org, PUC-Rio.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================================================
[ LuaJIT includes code from dlmalloc, which has this license statement: ]
This is a version (aka dlmalloc) of malloc/free/realloc written by
Doug Lea and released to the public domain, as explained at
https://creativecommons.org/licenses/publicdomain
===============================================================================
Binary file not shown.
+9 -12
View File
@@ -30,17 +30,13 @@ user_systemd() {
say "FramePort setup for $(hostname) ($(. /etc/os-release; echo "$NAME $VERSION_ID"))"
say "Letting FramePort log in"
say "Authorizing the FramePort app's key"
key=$(curl -fsS "$PC_URL/key?code=$PAIR_CODE")
[[ "$key" == ssh-ed25519\ * ]] || { echo "Couldn't reach FramePort at $PC_URL. Is it still open?"; exit 1; }
[[ "$key" == ssh-ed25519\ * ]] || { echo "Could not fetch the app's key from $PC_URL (is the app still open?)"; exit 1; }
mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
# a file without a final newline would glue our key onto its last line, where sshd never matches it (GitHub #242)
grep -qxF "$key" ~/.ssh/authorized_keys || {
[ -n "$(tail -c1 ~/.ssh/authorized_keys)" ] && echo >> ~/.ssh/authorized_keys
echo "$key" >> ~/.ssh/authorized_keys
}
grep -qxF "$key" ~/.ssh/authorized_keys || echo "$key" >> ~/.ssh/authorized_keys
say "Setting up Lepton's containers"
say "Configuring podman for Lepton"
# rootless podman leaks one kernel keyring per container start; ~200 game launches would exhaust the quota
mkdir -p ~/.config/containers
grep -qs '^ *keyring *=' ~/.config/containers/containers.conf || printf '[containers]\nkeyring = false\n' >> ~/.config/containers/containers.conf
@@ -121,7 +117,7 @@ JOB
if [[ -f /etc/steamos-devkit-enabled ]]; then
say "Developer Mode is on"
bash "$JOB" finish "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER" 2>&1 | tee "$LOG"
say "Done. FramePort on your PC shows this Frame as connected."
say "Done. Return to FramePort on your PC: this Frame should now appear as connected."
exit 0
fi
@@ -130,8 +126,9 @@ if [[ ! -x "$DEVKIT_HELPER" || ! -f "$STEAM_CONFIG" ]] || \
! user_systemd systemd-run --user --collect --quiet --unit="frameport-setup-$$" \
bash -c 'bash "$0" "$@" >"$HOME/.cache/frameport-setup.log" 2>&1' \
"$JOB" devmode "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER"; then
echo "Couldn't turn it on. Turn it on in Settings → System → Enable Developer Mode, then run this again."
echo "Couldn't turn it on automatically. Turn it on in Settings > System > Developer, then run this command again."
exit 1
fi
echo "Steam restarts and the desktop closes. Confirm Lepton if asked."
echo "Then check FramePort on your PC."
echo "Steam restarts to turn it on. That closes the desktop in a few seconds and the Frame returns to its"
echo "normal view; setup finishes on its own. If Steam asks to install Lepton, confirm it."
echo "Then return to FramePort on your PC: the Frame appears as connected within a minute."
-234
View File
@@ -1,234 +0,0 @@
#!/usr/bin/env bash
# FramePort setup from the project page: the same for every Frame and every PC, nothing to copy from the PC. Run in
# the Frame's desktop terminal (SteamVR dashboard -> Launch a program -> Desktop, then System -> Konsole):
# curl -fsSL https://frameport.app/s | bash
# with FramePort open on your PC at Steam Frame -> Connect. Options: --pc <address[:port]> (skip the search).
#
# What it does:
# 1. finds FramePort on your network (it announces itself over mDNS while that page is open; else the USB cable's
# address and a quick scan of this network)
# 2. asks it to set up this Frame: FramePort shows the same 4 digits as this terminal, you click Allow there
# 3. runs FramePort's setup script from your PC, the one the typed line `curl -fsS <pc>/<code> | bash` runs (it
# authorizes the app's key, configures podman, turns on Developer Mode, asks for Lepton)
# Nothing changes on the Frame before you allow it on the PC.
set -euo pipefail
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
PORTS="8765 8766 8767"
USB_PC=10.86.200.234 # the PC's fixed address on the Frame's USB cable network (Developer Mode only)
PC=""
while [[ $# -gt 0 ]]; do
case $1 in
--pc) PC=${2:-}; shift 2 ;;
--pc=*) PC=${1#--pc=}; shift ;;
-h|--help) sed -n '2,13p' "$0" 2>/dev/null || true; exit 0 ;;
*) shift ;;
esac
done
for tool in curl python3; do
command -v "$tool" >/dev/null || { echo "This needs $tool, which SteamOS normally has. Run the setup command FramePort shows instead."; exit 1; }
done
# FramePort PCs, one per line: name<TAB>address:port<TAB>words. Stdlib Python: avahi isn't always answering on the
# Frame, and its firewall only lets mDNS in on port 5353, so this listens there in the group, like avahi does.
find_pcs() {
python3 - "$PORTS" "$USB_PC" <<'PY'
import json, socket, struct, sys, time, urllib.request
from concurrent.futures import ThreadPoolExecutor
PORTS = [int(p) for p in sys.argv[1].split()]
USB_PC = sys.argv[2]
SERVICE = "_frameport-pair._tcp.local"
def qname(name):
return b"".join(bytes([len(p)]) + p.encode() for p in name.split(".")) + b"\0"
def read_name(d, off):
labels, jumped, end = [], False, off
while True:
n = d[off]
if n == 0:
off += 1
break
if n & 0xC0 == 0xC0:
ptr = struct.unpack("!H", d[off:off + 2])[0] & 0x3FFF
if not jumped:
end = off + 2
jumped, off = True, ptr
continue
labels.append(d[off + 1:off + 1 + n].decode(errors="replace"))
off += 1 + n
return ".".join(labels), (end if jumped else off)
def mdns(seconds=3.0):
found, srv, txt, addr = {}, {}, {}, {}
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
try:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except OSError:
pass
s.bind(("", 5353))
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton("224.0.0.251") + socket.inet_aton("0.0.0.0"))
query = struct.pack("!6H", 0, 0, 1, 0, 0, 0) + qname(SERVICE) + struct.pack("!HH", 12, 1)
s.sendto(query, ("224.0.0.251", 5353))
except OSError:
return []
end, again = time.time() + seconds, True
while time.time() < end:
if again and time.time() > end - seconds / 2:
s.sendto(query, ("224.0.0.251", 5353))
again = False
s.settimeout(max(0.05, min(0.5, end - time.time())))
try:
d, src = s.recvfrom(9000)
except socket.timeout:
continue
try:
_, _, qd, an, ns, ar = struct.unpack("!6H", d[:12])
off = 12
for _ in range(qd):
_, off = read_name(d, off)
off += 4
for _ in range(an + ns + ar):
name, off = read_name(d, off)
rtype, _, _, rdlen = struct.unpack("!HHIH", d[off:off + 10])
off += 10
if rtype == 12 and name.lower() == SERVICE:
found.setdefault(read_name(d, off)[0], src[0])
elif rtype == 33:
srv[name] = (read_name(d, off + 6)[0], struct.unpack("!H", d[off + 4:off + 6])[0])
elif rtype == 16:
items, p = {}, off
while p < off + rdlen:
n = d[p]
k, _, v = d[p + 1:p + 1 + n].decode(errors="replace").partition("=")
items[k] = v
p += 1 + n
txt[name] = items
elif rtype == 1 and rdlen == 4:
addr[name] = socket.inet_ntoa(d[off:off + 4])
off += rdlen
except (IndexError, struct.error, UnicodeError):
continue
out = []
for inst, src in found.items():
target, port = srv.get(inst, ("", PORTS[0]))
props = txt.get(inst, {})
out.append((props.get("pc") or inst.split(".")[0], f"{addr.get(target) or src}:{port}", props.get("words", "")))
return out
def ping(host_port):
try:
with urllib.request.urlopen(f"http://{host_port}/ping", timeout=1.5) as r:
info = json.load(r)
return (info.get("pc", host_port), host_port, info.get("words", ""))
except Exception:
return None
def scan():
"""This network's /24 (and the USB cable's PC address) for a FramePort setup port."""
hosts = [f"{USB_PC}:{p}" for p in PORTS]
try:
out = __import__("subprocess").run(["ip", "-4", "-o", "addr"], capture_output=True, text=True).stdout
except OSError:
out = ""
for line in out.splitlines():
parts = line.split()
if len(parts) > 3 and parts[1] not in ("lo",) and "/" in parts[3]:
mine = parts[3].split("/")[0]
base = mine.rsplit(".", 1)[0]
hosts += [f"{base}.{i}:{p}" for i in range(1, 255) if f"{base}.{i}" != mine for p in PORTS[:1]]
def open_port(hp):
h, p = hp.rsplit(":", 1)
try:
with socket.create_connection((h, int(p)), timeout=0.4):
return hp
except OSError:
return None
with ThreadPoolExecutor(64) as ex:
live = [hp for hp in ex.map(open_port, hosts) if hp]
return [r for r in map(ping, live) if r]
pcs = mdns()
if not pcs:
pcs = scan()
seen = set()
for name, hp, words in pcs:
key = (name, words) if words else hp # one PC on several links (home Wi-Fi and the Frame's hotspot): once
if key not in seen:
seen.add(key)
print(f"{name}\t{hp}\t{words}")
PY
}
say "FramePort setup for $(hostname)"
if [[ -n "$PC" ]]; then
[[ "$PC" == *:* ]] || PC="$PC:${PORTS%% *}"
WORDS=""
else
echo "Looking for FramePort on your network (in FramePort on your PC: Steam Frame → Start setup)..."
mapfile -t pcs < <(find_pcs)
if [[ ${#pcs[@]} -eq 0 ]]; then
echo
echo "FramePort wasn't found. Check that:"
echo " - FramePort is open on your PC, at Steam Frame → Start setup"
echo " - the Frame and the PC are on the same Wi-Fi (or connected with a USB cable)"
echo "Or run the setup command FramePort shows there (Use the setup command)."
exit 1
fi
pick=0
if [[ ${#pcs[@]} -gt 1 ]]; then
echo "More than one FramePort is open on this network:"
for i in "${!pcs[@]}"; do
IFS=$'\t' read -r name hp words <<<"${pcs[$i]}"
echo " $((i + 1))) $name ($hp) $words"
done
# stdin is this script itself (curl | bash): ask on the terminal; without one, take the first
if { exec 3</dev/tty; } 2>/dev/null; then
read -r -u 3 -p "Which one? [1-${#pcs[@]}] " n
exec 3<&-
[[ "$n" =~ ^[0-9]+$ && $n -ge 1 && $n -le ${#pcs[@]} ]] || { echo "No such number."; exit 1; }
pick=$((n - 1))
else
echo "No terminal to ask in: using the first. (Choose with --pc <address:port>.)"
fi
fi
IFS=$'\t' read -r name PC WORDS <<<"${pcs[$pick]}"
echo "Found FramePort on $name ($PC)${WORDS:+, PC words: $WORDS}"
fi
nonce=$(python3 -c 'import secrets; print(secrets.token_hex(12))')
digits=$(python3 -c 'import hashlib, sys; print(f"{int(hashlib.sha256(sys.argv[1].encode()).hexdigest()[:8], 16) % 10000:04d}")' "$nonce")
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
status=$(curl -sS -o "$tmp" -w '%{http_code}' -G --data-urlencode "host=$(hostname)" --data-urlencode "nonce=$nonce" \
"http://$PC/hello") || true
case $status in
200) reply=$(cat "$tmp") ;;
429) echo "FramePort at $PC already has several setup requests waiting. Allow or deny them there (requests from"
echo "setups that ended go away by themselves within a few minutes), then run this again."; exit 1 ;;
000|"") echo "FramePort at $PC didn't answer. Is it still open at Steam Frame → Start setup?"; exit 1 ;;
*) echo "FramePort at $PC refused the request (HTTP $status). Run this again, or run the setup command FramePort shows."
exit 1 ;;
esac
id=$(python3 -c 'import json, sys; print(json.loads(sys.argv[1])["id"])' "$reply")
WORDS=$(python3 -c 'import json, sys; print(json.loads(sys.argv[1]).get("words", ""))' "$reply")
say "FramePort on your PC asks to set up this Frame"
printf ' Click Allow there if it shows \033[1;33m%s\033[0m (PC: %s)\n' "$digits" "${WORDS:-?}"
code=""
for _ in $(seq 12); do # up to about 20 minutes
status=$(curl -sS -o "$tmp" -w '%{http_code}' -m 115 "http://$PC/wait?id=$id" || echo 000)
case $status in
200) code=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["code"])' "$tmp"); break ;;
202) continue ;;
403) echo "Not allowed on the PC. Nothing was changed on this Frame."; exit 1 ;;
*) echo "Lost FramePort at $PC (HTTP $status). Run this again, or run the setup command FramePort shows."; exit 1 ;;
esac
done
[[ -n "$code" ]] || { echo "Not allowed in time. Run this again when you're at your PC."; exit 1; }
say "Allowed. Running the setup from $PC"
curl -fsS "http://$PC/$code" | bash
-19
View File
@@ -1,19 +0,0 @@
package: ScaredBySquares.Game
title: Scared By Squares
status: unknown
notes: 'Showed black and dropped back to the home environment while the game kept running (sound only, GitHub 323).
Fixed in FramePort 1.2.1 and checked on the Frame without the headset (the game runs at 72 fps and SteamVR keeps it
as the running game); not yet confirmed in the headset. Rebuild and reinstall the game after updating FramePort.'
details: 'The game (Unreal 4.27, OpenXR) creates two controller actions named "turn" (an action mapping and an axis
mapping called Turn). The Frame''s runtime accepts the duplicate, then fails to load the input manifest it builds
from the actions ("SetXrInputConfig failed. Failed to load manifest") and treats the game as closed. FrameBridge''s
action_dedup (on by default) creates the second one as "turn_2".'
tested_version: '2'
engine: Unreal
xr: OpenXR
frame:
- frame.hw_video_decode
- frame.unreal_skip_obb_check
updated: '2026-10-16'
min_app: 1.2.1
source_hint: Scared By Squares
@@ -6,14 +6,10 @@ notes: 'Native OpenXR GLES video player. 360 theatres/videos (equirect2 layers,
at 72 fps (the runtime asks for 4536 px per eye). Confirmed in the headset by the owner (2026-10-01). Its "Internal
Storage" list is /sdcard/4XPlayer: send videos with the game page''s "Add videos".'
details: 'Settings: equirect_emul (GLES worker draws 360 layers into a projection layer), stable_local, focus_hold,
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains. Its hardware decoding mode (Java MediaCodec,
Vr4pMediaPlayer; its own FFmpeg is the software path) gets the Frame''s video hardware with frame.hw_video_decode
(added 2026-10-09, not yet checked in the headset).'
scale 0.75; frame.swapchain_limit for its 7680x3840 theatre swapchains.'
tested_version: 2.0.22
engine: Other
xr: OpenXR
frame:
- frame.hw_video_decode
adapter:
equirect_emul: 1
stable_local: 1
@@ -21,6 +17,4 @@ adapter:
scale: 0.75
verified:
date: '2026-10-01'
updated: '2026-10-10'
min_app: 0.12.1
source_hint: 4XVR Video Player
+7 -9
View File
@@ -10,14 +10,12 @@ frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-11'
known_good_sha256: 0e55a80ed420395b7eaf8075230fad1fae288f6c25d2ce8cc6aa49899e9b7596
app: 1.0.4
date: '2026-10-05'
known_good_sha256: 522cfebf954e1cab6054798479b0b32c5ba9e38cca83dd98db2ac0f7e3c0802a
app: 0.9.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 211
source_hint: Waltz
frame_build: '20260922.6101926'
agent: 48
issue: 44
source_hint: WotW
+9 -14
View File
@@ -4,19 +4,14 @@ tested_version: '1.0'
source_hint: Sports Scramble
engine: Unreal
xr: VrApi
status: works
notes: 32-bit only (armeabi-v7a), runs through FramePort's 32-bit bridge (experimental). Both controllers work as
Touch controllers. The first start shows a one-time "optimizing" screen (a bit jittery) and then starts the game
again by itself.
details: 'Unreal Engine 4 NativeActivity with OVRPlugin + VrApi, GLES, OBB. The bridge needed AAsset_getBuffer
(copied into guest memory), AKeyEvent_getFlags and the remaining AConfiguration getters. A Santa Cruz (early
Quest) build: it requires 6DoF head tracking, so the bridge passes both Frame controllers through as Touch
controllers (assets/fp_a32_headset.txt) instead of the Gear VR remote. Unreal restarts the app after its
first-run shader precompile; the launcher starts Lepton again for it (agent 86). Plays in the headset (owner,
2026-10-13).'
frame:
- frame.a32_bridge
status: unsupported
notes: 32-bit only; the Frame has no AArch32.
details: 32-bit (armeabi-v7a) only; the Frame has no AArch32.
alt_overport:
- patch_remove_unreal_force_quit
pcvr_alternative: Rift only; not on the Revive compatibility list.
verified:
date: '2026-10-13'
min_app: 0.12.1
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: e2e6788da99e2a38ea8f1e83e997ab44fed5492ce117d99ee3d376e6faeb0de4
+6 -6
View File
@@ -1,9 +1,9 @@
package: com.Armature.VR4
title: VR4
status: works
notes: Installs the no-ForceQuit build and fixes two campaign shaders that hung the GPU at cutscenes.
details: Entering the campaign gave a black screen, slowing audio and a crash (Mercenaries loaded). Two permutations of one fragment shader
read a loop counter before setting it, which hangs the GPU; the Vulkan shim zeroes it (vk_shader_fix). The
notes: Installs the no-ForceQuit build and fixes a campaign shader that hung the GPU after the opening cutscene.
details: Entering the campaign gave a black screen, slowing audio and a crash (Mercenaries loaded). One fragment shader
reads a loop counter before setting it, which hangs the GPU; the Vulkan shim zeroes it (vk_shader_fix). The
no-ForceQuit build is kept. Diagnosed and confirmed on a Frame by the reporter of GitHub issue 10.
tested_version: '2.3'
engine: Unreal
@@ -14,8 +14,8 @@ use_alt: true
adapter:
# one campaign shader reads an uninitialized loop counter and hangs the GPU (black screen, slowing audio, crash);
# the Vulkan shim inserts two OpStores that zero it and its accumulator (from the reporter's capture, GitHub
# issue 10); a second permutation of the same shader (6604 bytes) hung the GPU at a later cutscene (GitHub #140)
vk_shader_fix: 6488:5dcd842db8d21e3fdd13ec916d2e34dd0cfdf12a92fffaf8a27115072ef55c59:2624:0x0003003e,138,18,0x0003003e,150,42;6604:a0e6edd8a8e969acbefe1a193f0bc535d8c0c18d004def4c361840a9f2601f13:2956:0x0003003e,122,18,0x0003003e,135,42
# issue 10)
vk_shader_fix: 6488:5dcd842db8d21e3fdd13ec916d2e34dd0cfdf12a92fffaf8a27115072ef55c59:2624:0x0003003e,138,18,0x0003003e,150,42
verified:
date: '2026-10-02'
known_good_sha256: 40ff74e37264b28ae8826923b69d0731d9620094c76462be34159643d6ae91b3
@@ -24,5 +24,5 @@ verified:
frame_build: '20260930.6234839'
agent: 26
issue: 1
updated: '2026-10-11'
updated: '2026-10-04'
source_hint: Resident Evil 4
-6
View File
@@ -7,14 +7,8 @@ xr: OpenXR
status: works
notes: Uses Meta XR Audio via FMOD (not covered by patch_meta_xr_audio); if audio is missing, that is the likely
cause.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 63fff470777f2753d55ad50f8355573ced8fa48bcaee0f5e6a0c10369f951a38
updated: '2026-10-15'
@@ -6,15 +6,9 @@ engine: Unity
xr: VrApi
status: issues
notes: Right eye distorts during movement (unresolved; swap, tracking, Valve layers, depth and pacing ruled out).
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
details: Legacy VrApi via OVRPlugin; overport swaps in an OpenXR OVRPlugin. Do not remove VrApi.
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 192ff250f5fbec38b9c4e898f0b8408edc25901f46bb4b227f7ca60bbd9f0bba
updated: '2026-10-15'
@@ -6,15 +6,9 @@ engine: Unity
xr: OpenXR
status: works
notes: Needed the GLES/sRGB swapchain fallback.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
details: Also available as a native PC SteamVR title.
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 158f7c4f5e8558029dc8cc5e8dcb4e67e846b261bf34e3e448fee7f657c74597
updated: '2026-10-15'
@@ -1,23 +0,0 @@
package: com.CortopiaStudios.DTRH
title: Down the Rabbit Hole
status: works
tested_version: 01.04.434
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-10'
known_good_sha256: ce9e02e9b8fc64dc37bec2bd869d2ed1c4a99f28adffd6b5ec38c87b3cb639f7
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 145
source_hint: '8430807483680247'
@@ -1,19 +0,0 @@
package: com.CortopiaStudios.Graves
title: GORN2
status: works
tested_version: 1.10.1
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-07'
known_good_sha256: d8a7949edc90c19902eb89f888ad6a350475387d630991767b3097667ad16d4c
app: 0.12.0
overport_cli: 1.2.5
issue: 88
source_hint: GORN 2
-20
View File
@@ -1,20 +0,0 @@
package: com.CreaTeam.Sclerosis
title: Sclerosis
status: works
notes: Copy the "Amnesia The Dark Descent" folder from a Windows/Proton install to "FramePort/com.CreaTeam.Sclerosis/lepton-data/external".
Default hand positions are rather askew and should be amended via VR Controller Mappings in Steam itself. FOV
warping effects are EXTREMELY disorienting in VR, beware. Otherwise seems to work without issue.
tested_version: 1.10.7
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-10-16'
known_good_sha256: e17421ad9894f5e57fb9b1b8110544715166756ef924b5a86831f6f20e70a6d5
app: 1.2.0
overport_cli: 1.2.5
issue: 339
source_hint: Sclerosis_v1.10.7
@@ -1,24 +0,0 @@
package: com.Curve.LawnMowingSimulatorVR
title: Lawn Mowing Simulator VR
status: issues
notes: Left eye textures flicker only when mowing in specific areas.
tested_version: 1.0.50
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
pose_consistency: 1
hide_space_warp: 1
verified:
date: '2026-10-16'
known_good_sha256: 2653e9b1dfbd11d607b5dd17b9c07ab19d9e98e1004ef9b14cb52867b5f959dc
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 91
issue: 344
source_hint: com.Curve.LawnMowingSimulatorVR
@@ -1,29 +0,0 @@
package: com.CyDream.BattleTalent
title: BattleTalent
status: issues
notes: "Plays normally. The mixed-reality mode doesn't appear in the start menu (also with an APK from a Quest 3); MR
progress in #355. Multiplayer not tested yet."
tested_version: '4.7'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.hw_video_decode
- frame.luajit_pointers
- frame.unity_runtime_msaa_off
- frame.meta_permissions
- frame.unity_multipass
device:
- device.text_input_window
adapter:
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-17'
known_good_sha256: 17bbf8d7fc0f24d61986a0c5e1e9175cfe1239de6f8bcb2f588a57f80d83a1d3
app: 1.2.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 96
issue: 381
source_hint: com.CyDream.BattleTalent
@@ -1,24 +0,0 @@
package: com.CyberneticWalrus.DoesitStackMeta
title: Does it Stack?
status: issues
notes: Mixed reality mode does not work (works on Demeo), everything else seems to be OK
tested_version: 1.02 (3683)
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
haptic_fix: 1
refresh_rate: 90.0
verified:
date: '2026-10-09'
known_good_sha256: e9fce6ac812f1bb317a7a6959be89c5ff54e87f56af9ad4c42dff5fecad4efe8
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261006.6173745'
agent: 59
issue: 112
source_hint: Does It Stack
@@ -1,22 +0,0 @@
package: com.FireproofStudios.GhostTown
title: Ghost Town
status: works
tested_version: '1.01'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-14'
known_good_sha256: 248b59ade696bdd4808fc05c2117aa9e8debe133d3d0874a276cc8cb67439f58
app: 1.1.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 88
issue: 299
source_hint: Ghost Town
@@ -1,10 +0,0 @@
package: com.FlatHeadStudio.DeadlyDelivery
title: Deadly Delivery
status: unsupported
notes: "Quits after Meta's entitlement check fails: it needs Meta's online services."
tested_version: 1.0.30
engine: Unity
xr: OpenXR
verified:
date: '2026-10-15'
issue: 322
-20
View File
@@ -1,20 +0,0 @@
package: com.ForwardXP.nuke
title: Please Don't Touch Anything
status: works
notes: Seems functional
tested_version: '2.2'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-10-08'
known_good_sha256: 17460c8691b9ff82f6c7ca5597afd0aacf1a94aa30ebb6a6108802991b60f51b
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261006.6173745'
agent: 59
issue: 106
source_hint: apks
@@ -1,21 +0,0 @@
package: com.FourPlayersStudio.Retronika
title: Retronika
status: works
tested_version: 3.2.601
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-07'
known_good_sha256: 290bfda3969547d5f1ff621ab5a8be9dd322fc9eee6b243c863576d4ff5b4b81
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261006.6173745'
agent: 59
issue: 97
source_hint: Retronika
@@ -1,21 +0,0 @@
package: com.FunktronicLabs.TheLightBrigade
title: The Light Brigade
status: issues
notes: Judder in weapons
tested_version: '746'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-09'
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 114
source_hint: The Light Brigade
@@ -6,14 +6,8 @@ engine: Unity
xr: OpenXR
status: works
notes: Recommended pilot title.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 2300f628ce04c1da60b52fed18335d6a31b88cc88313fd6d2c0e1a8e7785126f
updated: '2026-10-15'
-21
View File
@@ -1,21 +0,0 @@
package: com.GenderGames.Ladder
title: Ladder
status: works
tested_version: Release
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-12'
known_good_sha256: 7d23692149d40fe469dbf2e13446ceeb94a8a32807e09ddad7731158de9be0a2
app: 1.0.4
overport_cli: 1.2.5
issue: 243
source_hint: Ladder
@@ -1,23 +0,0 @@
package: com.HarmonixMusic.DCVRQuest
title: Dance Central
status: works
tested_version: 1.2.2
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.hw_video_decode
adapter:
haptic_fix: 1
pose_time_fix: 1
verified:
date: '2026-10-12'
known_good_sha256: cbaf2eb594cb25d6a345b50612f9e7ca3e2b2f05abaa072632e8fc0d91277c61
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 228
source_hint: com.HarmonixMusic.DCVRQuest
@@ -1,22 +0,0 @@
package: com.HypetrainDigitalLtd.Breathedge
title: Breathedge VR
status: works
notes: Loading fade is broken and only shows a small square but the game works!
tested_version: 1.0.7
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.hw_video_decode
adapter:
haptic_fix: 1
verified:
date: '2026-10-16'
known_good_sha256: 4975a32fbf36b4d417142a944bf7d771c18a184dc0320b5937ac2c67bd013575
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 91
issue: 347
source_hint: Breathadge
@@ -1,38 +1,20 @@
package: com.ILMxLAB.VaderImmortal.ep1
title: 'Vader Immortal: Episode I'
status: works
notes: 'Plays (owner''s headset test): loading card, grip/trigger, hands and the lightspeed sequence fixed. Thumbs
follow the touches with frame.unreal_thumb_touch (Klownicle''s engine fix).'
details: Unreal Engine 4 (GLES). Found and verified in gameplay on a Frame by Klownicle (GitHub issue 49); FramePort
reimplements them as patches that match the game's code exactly and change nothing when it differs. The engine's
Quest checks (IsRunningOnSantaCruz) answer "not a Quest" on the Frame - the menu waited for a Quest-only shader
precompile that never starts (frame.unreal_quest_precompile makes the non-Quest branch report 100 %), and the
game's key map picked the empty Gear VR controls (frame.unreal_quest_keymap keeps the Quest set). Two lightspeed
shaders, as the Frame's GL driver (Zink) compiles them, read loop counters and accumulators before setting them
and hang the GPU; a Vulkan layer under Zink inserts the stores that zero them (frame.zink_shader_fix, zink_shader_fix;
the captured modules are from Lepton 2.8.14 and stop matching if a Frame update changes the driver's output -
the layer logs that). OVRPlugin asks for poses at its own clock (pose_time_fix). Unreal's Oculus input animates the
thumbs from near-touch, which the Frame never reports (frame.unreal_thumb_touch reads the touches instead; binding
the proximity to the touch inputs in FrameBridge didn't reach the game). The game leaves GL errors
pending; the runtime's GL check took one as its own failure and then allocated a command buffer every frame
(~20 MB a second until the frame rate collapsed). gl_error_clear clears them first (GitHub #285).
status: issues
notes: Starts in VR and plays the intro, then stays on the loading card (Vader's portrait with a progress bar).
details: Unreal Engine 4 (GLES). After the Lucasfilm intro the game shows its loading card and never loads the next
scene; it keeps rendering at 72 fps and ignores input. Ruled out so far - Meta platform requests (all answered),
video playback (the intro is a video and plays), the no-ForceQuit build, frame and swapchain handling. The Frame's
SteamVR runtime also leaks about 20 MB of memory a second while the game runs, which slows it down after a few
minutes. Next steps - find what the loading code waits for in the game's engine library (no symbols) and report
the runtime leak to Valve. GitHub issue 49.
tested_version: 1.1.1+667256.cl.387770
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_quest_precompile
- frame.unreal_quest_keymap
- frame.unreal_thumb_touch
- frame.zink_shader_fix
adapter:
pose_time_fix: 1
gl_error_clear: 1
zink_shader_fix: 12016:b2919629761ad268e0b21c72ac8520f81b76e72c3adc26897f97eae9a3e70a66:3092:0x0003003e,166,82,0x0003003e,177,82,0x0003003e,183,82,0x0003003e,189,82,0x0003003e,256,82,0x0003003e,266,82,0x0003003e,272,82,0x0003003e,278,82,0x0003003e,318,82,0x0003003e,328,82,0x0003003e,334,82,0x0003003e,340,82;11436:6f18be49f2aaa2452f962eb16f7af744c5c22a7b25f37ce681e0d6399e35a869:2932:0x0003003e,164,80,0x0003003e,175,80,0x0003003e,181,80,0x0003003e,187,80,0x0003003e,254,80,0x0003003e,264,80,0x0003003e,270,80,0x0003003e,276,80,0x0003003e,316,80,0x0003003e,326,80,0x0003003e,332,80,0x0003003e,338,80
verified:
date: '2026-10-09'
date: '2026-10-06'
issue: 49
updated: '2026-10-14'
min_app: 0.12.1
updated: '2026-10-06'
source_hint: Vader Immortal- Episode I
@@ -1,26 +0,0 @@
package: com.ILMxLAB.VaderImmortal.ep2
title: 'Vader Immortal: Episode II'
status: works
notes: Plays (owner's headset test, 2026-10-09) with Episode I's fixes, which FramePort finds in this episode's
code too.
details: 'Unreal Engine 4 (GLES), the same engine build as Episode I: the Quest-only shader precompile and key map
(frame.unreal_quest_precompile, frame.unreal_quest_keymap), thumbs from the capacitive touches (frame.unreal_thumb_touch)
and poses at OVRPlugin''s clock (pose_time_fix). Episode I''s lightspeed shader fix doesn''t apply. The campaign
intro once stayed black with its dialogue playing; it rendered on the next run.'
tested_version: 2.0.3+667261.cl.387778
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_quest_precompile
- frame.unreal_quest_keymap
- frame.unreal_thumb_touch
adapter:
pose_time_fix: 1
gl_error_clear: 1
verified:
date: '2026-10-09'
updated: '2026-10-14'
min_app: 0.12.1
source_hint: Vader Immortal- Episode II
@@ -1,26 +0,0 @@
package: com.ILMxLAB.VaderImmortal.ep3
title: 'Vader Immortal: Episode III'
status: works
notes: Plays (owner's headset test, 2026-10-09) with Episode I's fixes, which FramePort finds in this episode's
code too.
details: 'Unreal Engine 4 (GLES), the same engine build as Episode I: the Quest-only shader precompile and key map
(frame.unreal_quest_precompile, frame.unreal_quest_keymap), thumbs from the capacitive touches (frame.unreal_thumb_touch)
and poses at OVRPlugin''s clock (pose_time_fix). Episode I''s lightspeed shader fix doesn''t apply. The campaign
intro starts on a black screen with only dialogue: that is the game, not a bug.'
tested_version: 3.0.3+667263.cl.387932
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_quest_precompile
- frame.unreal_quest_keymap
- frame.unreal_thumb_touch
adapter:
pose_time_fix: 1
gl_error_clear: 1
verified:
date: '2026-10-09'
updated: '2026-10-14'
min_app: 0.12.1
source_hint: Vader Immortal- Episode III
@@ -1,20 +0,0 @@
package: com.InnoverseLtd.FrenzyVR
title: FrenzyVR
status: works
notes: Works, runs good
tested_version: '107'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-10-11'
known_good_sha256: 0d87e78dcbe1c705af0d01cda8eabbea514849f7403e5904f0ef49b5cf0653fb
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 216
source_hint: Frenzy VR
@@ -4,25 +4,14 @@ tested_version: '1.03'
source_hint: Sniper Elite VR
engine: Unity
xr: VrApi
status: works
notes: Plays with controllers that follow the hands (owner's headset test, Quest 1 build 35713).
details: 'Unity 2019.4 on its built-in Oculus VR (no Oculus XR Plugin). Its legacy frame loop never waited for frames
(GPU hang, zink: DEVICE LOST) and Unity reported only Go controllers: frame.unity_oculus_check. Its frame wait then
deadlocked at the Init scene (Unity skipped beginning a frame) and the physics-step pose update located the
controllers in the past (hands lagged): both fixed in that patch (revision 5), switched on for this game by ovrp_begin_gate and ovrp_hold_physics. Grabbing an object in the tutorial
vibrates the controller; OVRPort''s envelope conversion then allocated ~1 GB/s until the memory cap ended the game:
haptic_fix. The Quest 1 build has no arraysources_assets.hd, which the game loads on the Frame (it detects a Quest
2) and then skips the .sd bundle: textures are missing unless the .sd file is also present under the .hd name in the
game''s obb folder.'
status: unsupported
notes: 'GPU hang (zink: DEVICE LOST) even with MSAA off.'
details: Legacy VrApi via OVRPlugin.
frame:
- frame.unity_no_msaa
- frame.unity_oculus_check
adapter:
haptic_fix: 1
ovrp_begin_gate: 1
ovrp_hold_physics: 1
pcvr_alternative: Sniper Elite VR (Steam PC VR). Winter Warrior (Quest) works on the Frame.
verified:
date: '2026-10-07'
updated: '2026-10-07'
min_app: 0.12.1
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: e80883eb6945e267a375edf0ffd789268686440819add9d4dee9fdbdf466cbe4
@@ -5,14 +5,9 @@ source_hint: 'Sniper Elite VR- Winter Warrior'
engine: Unity
xr: OpenXR
status: works
notes: frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
frame:
- frame.unity_text_input
device:
- device.text_input_window
notes: ''
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 6b8170da5826b9a35326a20967a0e5a242d46fb569a0a3c669e1cbd368102dd3
updated: '2026-10-15'
@@ -1,22 +0,0 @@
package: com.LocoMotionDevs.Superfly
title: Superfly
status: works
notes: Works, take a bit to launch but works
tested_version: 1.3.3
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-11'
known_good_sha256: e180b6c58b80c1c2d0cc49e75d645a41d3d4bc63ed7ffe1e6f91eac51e486a17
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 217
source_hint: Superfly
-24
View File
@@ -1,24 +0,0 @@
package: com.MeatSpace.Nakatomi
title: Vendetta Forever
status: works
tested_version: 1.0.4.0
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-15'
known_good_sha256: 72885ad42f1b730a1bd7fad271b87665914e3cf14fde60409395868458ac7b06
app: 1.1.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 88
issue: 321
source_hint: com.MeatSpace.Nakatomi
@@ -1,24 +0,0 @@
package: com.MightyCoconut.WalkaboutMiniGolf
title: Walkabout Mini Golf
status: works
tested_version: '6.7'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
controller_models: 1
haptic_fix: 1
scale: 1.5
verified:
date: '2026-10-08'
known_good_sha256: cf3b5cbe131af8a2baaa3f5c665b1c7001c8b299473abfcf98d90e3c02b08a2f
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 111
source_hint: Walkabout Mini Golf
@@ -1,28 +0,0 @@
package: com.Miragesoft.RealVrFishingQ
title: Real VR Fishing
status: issues
notes: Seems to be almost 100% functional with meta permissions enabled. In the new Mixed Reality mode, the room
scan doesn't seem to do anything and I can't seem to manually place anything either.
tested_version: '3.06096'
engine: Unity
xr: OpenXR
frame:
- frame.vivox_audio_route
- frame.unity_text_input
- frame.oculusos
- frame.meta_permissions
device:
- device.text_input_window
adapter:
haptic_fix: 1
scene_emul: 1
controller_models: 1
verified:
date: '2026-10-16'
known_good_sha256: 8b17dd26ef0486618ec006f9d948a60dabea4d7763bbe6061db07ee8aad27c76
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 91
issue: 333
source_hint: Real VR Fishing
@@ -1,19 +0,0 @@
package: com.NiVision.RacketPinball
title: RacketPinball
status: works
tested_version: '2.06'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-13'
known_good_sha256: 80e850f81c24112e8c35a560a333846163a3e783da391290db7663b9015ca58f
app: 1.0.4
overport_cli: 1.2.5
issue: 262
source_hint: RacketPinball
-5
View File
@@ -6,14 +6,11 @@ engine: Unity
xr: OpenXR
status: works
notes: Passthrough + Meta scene/room emulated from the Steam guardian (scene_emul=1).
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
details: Mixed-reality-only; builds its level from the room model, which the adapter emulates from the Steam guardian.
overport_extra:
- patch_force_passthrough
adapter:
scene_emul: 1
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
@@ -21,5 +18,3 @@ verified:
known_good_sha256: 9f900fc86fbb2663abe83c82ab79cc72bbf1f93fea54f7583fd274dd07e3d09c
frame:
- frame.meta_permissions
- frame.unity_text_input
updated: '2026-10-15'
@@ -1,22 +0,0 @@
package: com.PeanutButton.Retropolis
title: Retropolis
status: works
tested_version: '0.87'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-10'
known_good_sha256: 4656b980fccd812d8edac15805dcd86c2f44029abdab98c3187699d1fc878f88
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 153
source_hint: com.PeanutButton.Retropolis
+9 -12
View File
@@ -1,14 +1,12 @@
package: com.PixelToys.BattleSisters
title: BattleSisters
status: works
notes: Plays; controller buttons and vibration work, hands follow the controllers (pose_time_fix).
details: Unity 2019.4 with Unity's built-in Oculus support. frame.unity_oculus_check starts VR (Meta's system-app
check), adds the frame wait its legacy loop never makes (without it the GPU hung, black screen) and lets Unity's
Oculus input accept Lepton's device model (it only reported controllers on a device named "Oculus ...", so buttons
were dead). haptic_fix stops the first controller vibration from freezing the Frame (OVRPort's loader read its
duration in nanoseconds as seconds and the game ran out of memory). With pose_time_fix the hands follow the controllers
- OVRPlugin asked for hand poses at Android's monotonic "now", which on SteamOS 0.4.5 is 2.56 s behind the runtime's
XrTime, so the hands lagged far behind.
notes: Starts in VR; controller buttons and vibration work.
details: Unity 2019.4 with Unity's built-in Oculus support. frame.unity_oculus_check starts VR (Meta's system-app check),
adds the frame wait its legacy loop never makes (without it the GPU hung, black screen) and lets Unity's Oculus
input accept Lepton's device model (it only reported controllers on a device named "Oculus ...", so buttons were
dead). haptic_fix stops the first controller vibration from freezing the Frame (OVRPort's loader read its duration
in nanoseconds as seconds and the game ran out of memory).
tested_version: 1.2.4
engine: Unity
xr: VrApi
@@ -20,10 +18,9 @@ frame:
- frame.unity_gl_shim
adapter:
haptic_fix: 1
pose_time_fix: 1
verified:
date: '2026-10-08'
date: '2026-10-06'
issue: 48
updated: '2026-10-08'
min_app: 0.12.1
updated: '2026-10-06'
min_app: 0.11.1
source_hint: Battle Sister
@@ -1,23 +0,0 @@
package: com.PotamWorks.SmashDrums
title: Smash Drums
status: works
tested_version: 5.2.1.4
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
scene_emul: 1
verified:
date: '2026-10-15'
known_good_sha256: 67d36cdcadaf14c2f744849ae25af4f92bf8ea14ff935318562f40d7f2ddc88f
app: 1.1.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 88
issue: 320
source_hint: com.PotamWorks.SmashDrums
-20
View File
@@ -1,20 +0,0 @@
package: com.PubTest.PubTest
title: Drunkn Bar Fight 2 - Early Access
status: works
notes: Game launches and plays fine
tested_version: '0.515'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-10-11'
known_good_sha256: f340b5ba70ee9368ead6bfe2c287eda6c5686280ddb8e12b8c9a8d720326de36
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 218
source_hint: Drunkn Bar Fight 2 - Early Access
@@ -1,25 +0,0 @@
package: com.RealitiesIO.puzzlingPlaces
title: Puzzling Places
status: issues
notes: Works fine, but pointers dont show well on main menu, easy to work around.
tested_version: '2.14'
engine: Unreal
xr: OpenXR
overport_extra:
- patch_disable_space_warp
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.unreal_ovrp_entrypoints
- frame.hw_video_decode
adapter:
equirect_emul: 1
haptic_fix: 1
verified:
date: '2026-10-16'
known_good_sha256: 8d9bb64b0af4c34269fffb64c5ef7515d6ba4715f5234f95517975b8e2b56c00
app: 1.2.0
overport_cli: 1.2.5
issue: 359
source_hint: Puzzling Places
@@ -1,24 +0,0 @@
package: com.RuneSkovboJohansen.EyeOfTheTemple
title: Eye of the Temple
status: works
tested_version: '1.528'
engine: Unity
xr: VrApi
frame:
- frame.unity_no_msaa
- frame.unity_oculus_check
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.hw_video_decode
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-15'
known_good_sha256: 78ad96eaf4383b02666e0a1af99c5985f718f97982466e5dca9ebb636974af00
app: 1.1.1
overport_cli: 1.2.5
issue: 302
source_hint: Eye of the Temple
-22
View File
@@ -1,22 +0,0 @@
package: com.SDI.TWD
title: 'The Walking Dead: Saints & Sinners'
status: works
tested_version: Shipping / 2024.01.19 / build 400355 - STAGECH1
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.ovr_microphone
- frame.hw_video_decode
adapter:
haptic_fix: 1
verified:
date: '2026-10-17'
known_good_sha256: 4b172a5abba0901c75278fb55b867f344120c658c26ae07c8558d90e0c38cec3
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 91
issue: 376
source_hint: The Walking Dead- Saints & Sinners
@@ -1,19 +0,0 @@
package: com.SchellGames.DieScreaming
title: SilentSlayer
status: works
tested_version: 1.1.2+323863s
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
adapter:
haptic_fix: 1
verified:
date: '2026-10-11'
known_good_sha256: 5cb085909f9a54488543102242f19c5b2f176a1439659a29a9cca04362601f68
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 176
source_hint: Silent Slayer - Vault of the Vampire
@@ -1,22 +0,0 @@
package: com.SchellGames.LostRecipes
title: Lost Recipes
status: works
tested_version: 1.0.1+273175
engine: Unreal
xr: VrApi
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.hw_video_decode
adapter:
haptic_fix: 1
verified:
date: '2026-10-16'
known_good_sha256: f071a1deea3e3b6dce0c3a923127f540a48b88532106536c1838f496e9d3e6c9
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 91
issue: 340
source_hint: Lost Recipes
@@ -1,22 +0,0 @@
package: com.ScytheDevTeam.ContainmentProtocol
title: Deep Cuts
status: works
tested_version: 1.1.3271
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-10'
known_good_sha256: d8d4bfb0954059c5ed1bcaa0edfb8b18f154ff710049f4808c42cfd8d88ee5a2
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 144
source_hint: com.ScytheDevTeam.ContainmentProtocol
@@ -1,21 +0,0 @@
package: com.StatusPro.NFLProEra2
title: NFL PRO ERA II
status: works
tested_version: 2.4.0
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.unity_runtime_msaa_off
- frame.unity_gl_shim
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-11'
known_good_sha256: caf2e8f148ca4929ba5e73112fa7e0958c00d7ee0c5f28926c3f39714db6c923
app: 1.0.4
overport_cli: 1.2.3
issue: 226
source_hint: com.StatusPro.NFLProEra2
+9 -14
View File
@@ -1,25 +1,20 @@
package: com.StressLevelZero.BONELAB
title: BONELAB
status: works
notes: Builds 1.2068 and 1.2974 play (owner's headset tests). One recipe serves both builds - 1.2068 needs frame.unity_user_presence (head tracking and controls); 1.2974 also needs
frame.slz_vulkan_hooks (72 fps after about 35 s of shader prewarming), which changes nothing on 1.2068.
details: Both builds - the game runs (72 fps), but its player body stayed frozen - no head tracking, the controllers stuck to the
status: issues
notes: Build 1.2068 plays (head tracking and controls work with frame.unity_user_presence); the newer build
1.2974 crashes at start (Vulkan), no fix yet.
details: The game runs (72 fps), but its player body stayed frozen - no head tracking, the controllers stuck to the
model, no buttons - although OpenXR input and OVRPlugin's poses were fine. A few seconds after start (when Unity
switches XR loaders) OVRPlugin reports the worn headset as not worn (ovrp_GetUserPresent2 -> 0), and BONELAB's
Marrow rig only follows the player while Unity's HMD device reports UserPresence (OpenControllerRig checks
XRHMD.IsUserPresent every frame). frame.unity_user_presence reports the headset as worn. Build 1.2974 only - it adds Stress
Level Zero's graphics plugin (libSLZQuestNative.so), which hooks Unity's Vulkan start-up (vkCreateInstance/
vkCreateDevice) and vkCreateSampler; on the Frame its vkCreateInstance wrapper calls an invalid pointer right after
OVRPlugin's pre-init instance is destroyed (a jump to 0, into an unloaded library, or a hang). frame.slz_vulkan_hooks
switches its two registrations off, so Unity starts Vulkan itself; the plugin's pipeline cache isn't used.
tested_version: 1.2974.57485
XRHMD.IsUserPresent every frame). frame.unity_user_presence reports the headset as worn. Build 1.2974 crashed
earlier in vkCreateInstance from the game's own Vulkan hooks (libSLZQuestNative.so); not retested.
tested_version: 1.2068.34555
engine: Unity
xr: OpenXR
frame:
- frame.unity_user_presence
- frame.slz_vulkan_hooks
verified:
date: '2026-10-08'
updated: '2026-10-08'
date: '2026-10-06'
updated: '2026-10-06'
source_hint: BONELAB
min_app: 0.12.1
@@ -1,25 +0,0 @@
package: com.TacticalNounours.QuantumVoid
title: Quantum Void
status: issues
notes: no hands
tested_version: '1.11'
engine: Unity
xr: OpenXR
frame:
- frame.bhaptics_permission
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
pose_time_fix: 1
foveation: tracked
verified:
date: '2026-10-14'
known_good_sha256: 1c7afccbd627b9bd2342884679aeca0efc2429df700f3c73ec162498db320098
app: 1.1.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 88
issue: 307
source_hint: Quantum Void
@@ -6,18 +6,12 @@ engine: Unity
xr: OpenXR
status: issues
notes: Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
details: Hand-tracking-only game; Steam Frame has no optical hand tracking. Built with controller_fix=0 so hand
data passes through if the runtime ever provides it.
adapter:
controller_fix: 0
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: a19b80db0c300d422cbba35a4924ab59211b39bcb6485e200d310e3e1372ddb7
updated: '2026-10-15'
@@ -1,23 +0,0 @@
package: com.TinyBuild.killitwithfirevr
title: Kill It With Fire VR
status: works
notes: Works
tested_version: 1.0.5.4208
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.hw_video_decode
adapter:
haptic_fix: 1
verified:
date: '2026-10-11'
known_good_sha256: fcadbdaf3255e2909edf763d0c61fb55808ba448d4f277e91ba37b0a0ff9f497
app: 1.0.4
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 79
issue: 219
source_hint: Kill It With Fire VR
-22
View File
@@ -1,22 +0,0 @@
package: com.ToastVR.Matilda
title: Max Mustard
status: works
tested_version: 1.1.0
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-08'
known_good_sha256: 9a67459fa36c3b9ac0d035e6c43b1809b39772699333e8cfd7d2a4d7f17926a7
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 59
issue: 110
updated: '2026-10-09'
source_hint: com.ToastVR.Matilda
@@ -1,22 +0,0 @@
package: com.ToastVR.RichiesPlankExperience
title: Richie's Plank Experience
status: works
tested_version: 2.3.585
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-07'
known_good_sha256: 7eb1366c2398e554624a5d66aefb0c731c64b1b6944bc1726d9187f123c37fb7
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20260922.6101926'
agent: 59
issue: 95
source_hint: apks
@@ -1,11 +0,0 @@
package: com.TriangleFactory.Breachers
title: Breachers
status: unsupported
notes: Online-only multiplayer that needs Meta's online services, which aren't available on the Frame. The game's own
copy of Meta's platform library (libovr_custom.so) quits at start ("Failed to launch SystemActivities").
tested_version: 40.0.23
engine: Unity
xr: OpenXR
verified:
date: '2026-10-17'
issue: 357
-24
View File
@@ -1,24 +0,0 @@
package: com.UNIVRS.Freedom
title: 'Attack on Titan VR: Unbreakable'
status: works
notes: Keep the extra files that come with the APK and OBB next to the OBB (they hold the audio). Cutscenes are
detached from the head but still viewable.
tested_version: 1.0.2950
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.asset_files
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-16'
known_good_sha256: 1b3ced1a24b3b708f66aba6aa879863b667c0e502ef153d8a8cfa03e529d21fd
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261015.6141207'
agent: 91
issue: 337
source_hint: Aot
+3 -11
View File
@@ -4,24 +4,16 @@ tested_version: MAIN.450412.207706.final
source_hint: Assassins Creed Nexus VR
engine: Unity
xr: OpenXR
status: works
notes: FrameBridge turns its upside-down UI panels upright (flip emulation, in place since FramePort 1.2.0). Ubisoft
login may not work.
frame.unity_text_input and device.text_input_window added from detection (not headset-verified).
status: issues
notes: Some launch warning text is still upside down; the rest of the UI is fixed by flip emulation. Ubisoft login
may not work.
details: Ubisoft online login may not work. Data folder holds raw assets (start0..2, .climb, bundles), not .obb
files; install to obb as-is.
overport_extra:
- patch_ac_nexus_no_appsw_90
frame:
- frame.unity_text_input
device:
- device.text_input_window
verified:
date: '2026-09-28'
overport_cli: 1.2.3
overport_runtime: 3.4.3-23204ea
known_good_sha256: 89644a12bf8df956fa3d3a9a40540ba9e1fb62f63494714a05c9f05984383ac8
no_appsw_90: '2026-10-02, OVRPort 1.2.5 (owner: 90 Hz felt better than 72 Hz; same known issues)'
flip_in_place: '2026-10-14, owner in the headset: launch warnings and tutorial pop-ups upright (GitHub #138)'
updated: '2026-10-15'
min_app: 1.2.0
@@ -1,23 +0,0 @@
package: com.VR_HOT.VR_HOT_Quest
title: VR HOT Quest
status: works
tested_version: 0.8.8.1
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.oculusos
device:
- device.text_input_window
adapter:
controller_models: 1
haptic_fix: 1
verified:
date: '2026-10-08'
known_good_sha256: c321606c536eba1673572a9f44fd9e20d2cb5bd9609d536189e6ad7057b9cd67
app: 0.12.0
overport_cli: 1.2.5
frame_build: '20260922.6101926'
agent: 59
issue: 98
source_hint: VR-HOT-Quest-0.8.8.1
@@ -1,27 +0,0 @@
package: com.VertigoGames.The7thGuestVR
title: The 7th Guest VR
status: issues
notes: Runs and is playable. Foveated rendering (fixed and eye-tracked) sits offset toward the bottom right, and
changing the foveated region size has no visible effect.
tested_version: 1.1.3.12072
engine: Unreal
xr: OpenXR
alt_overport:
- patch_remove_unreal_force_quit
frame:
- frame.unreal_skip_obb_check
- frame.hw_video_decode
adapter:
haptic_fix: 1
scale: 2.0
foveation: fixed
foveation_radius: 0.1
verified:
date: '2026-10-16'
known_good_sha256: 11a1208888adedf4aa48a672b2af5535f0e8f11e5bb1ce0a28bafe5d272bb672
app: 1.2.0
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 91
issue: 363
source_hint: Guest
@@ -1,22 +0,0 @@
package: com.Virtuallyz.OperationWolf
title: 'Operation Wolf Returns: First Mission VR'
status: works
tested_version: '0.28'
engine: Unity
xr: OpenXR
frame:
- frame.unity_text_input
- frame.hw_video_decode
device:
- device.text_input_window
adapter:
haptic_fix: 1
verified:
date: '2026-10-15'
known_good_sha256: aff311f10d3e938e1eeef22263da7fe84ca38f25786ca4483543596de335c702
app: 1.1.1
overport_cli: 1.2.5
frame_build: '20261007.6125817'
agent: 88
issue: 318
source_hint: com.Virtuallyz.OperationWolf
-19
View File
@@ -1,19 +0,0 @@
package: com.Virtuos.R6DLM
title: 'Medal of Honor: Above and Beyond'
status: unknown
notes: 'Not yet tested on the Frame (the game is ~41 GB, too large to install on the dev unit). Its first-run
Downloader screen is invisible in Lepton, so it stayed stuck there; frame.unreal_skip_obb_delivery skips the check.
The OBB files still have to be present. Please report back whether it reaches the game (GitHub 159).'
details: 'Medal of Honor packs its data in OBBs behind Epic''s DownloaderActivity, whose name/size check is
obfuscated (its methods are renamed), so FramePort didn''t recognise it and the "skip the OBB delivery check"
option never appeared. Detection now recognises the obfuscated predicate and frame.unreal_skip_obb_delivery
rewrites it to pass. frame.hw_video_decode gives the game''s videos the Frame''s hardware decoder.'
tested_version: '0.518'
engine: Unreal
xr: VrApi
frame:
- frame.hw_video_decode
- frame.unreal_skip_obb_delivery
updated: '2026-10-15'
min_app: 1.2.0
source_hint: Medal of Honor
Loaded 100 of 944 files, more files were not shown because too many files have changed in this diff. Show more