mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-11 18:01:25 +02:00
Compare commits
23
Commits
showcase/update
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2912d637b5 | ||
|
|
5c070ccf23 | ||
|
|
b979067215 | ||
|
|
4440b459e5 | ||
|
|
09b8046337 | ||
|
|
0d67335775 | ||
|
|
867b9977d5 | ||
|
|
80092098e0 | ||
|
|
feab03e049 | ||
|
|
9713da222a | ||
|
|
82e617741e | ||
|
|
9de0707070 | ||
|
|
1343930ae0 | ||
|
|
5424e429e4 | ||
|
|
9149eaecd7 | ||
|
|
64278e9e2c | ||
|
|
1531acb575 | ||
|
|
6796e006d9 | ||
|
|
45f05dd3c6 | ||
|
|
5a2cd45fb8 | ||
|
|
757671b389 | ||
|
|
b2688dd368 | ||
|
|
13de114f39 |
No files matched your search
@@ -40,7 +40,8 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$branch"
|
||||
git add "catalog/games/$PKG.yaml"
|
||||
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 commit -m "catalog: $PKG (from #$ISSUE)"
|
||||
git push --force origin "$branch"
|
||||
if ! gh pr view "$branch" >/dev/null 2>&1; then
|
||||
|
||||
@@ -967,5 +967,7 @@ 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>`.
|
||||
@@ -28,7 +28,7 @@ Frame, patches each game so it runs there, uploads it and adds it to your Steam
|
||||
|
||||
- **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 90+ games; other games get suggested patches, each explained in
|
||||
- **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.
|
||||
@@ -76,7 +76,7 @@ suggests patches. See the [list of tested games](docs/GAMES.md). Got a game work
|
||||
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 90+
|
||||
- **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.
|
||||
@@ -91,7 +91,7 @@ FrameDrop and Valve's own tools install an app as it is. FramePort differs in fo
|
||||
| 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 90+ games, updated without an app update | Not stated | No |
|
||||
| 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: <title>"; 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 |
|
||||
|
||||
@@ -44,7 +44,7 @@ try:
|
||||
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
|
||||
fcntl = None
|
||||
|
||||
AGENT_VERSION = 78
|
||||
AGENT_VERSION = 79
|
||||
HOME = os.path.expanduser("~")
|
||||
STEAM = os.path.join(HOME, ".local/share/Steam")
|
||||
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
|
||||
@@ -5936,6 +5936,96 @@ def shader_dumps(files_dir, max_bytes=SHADER_DUMP_BYTES, session_bytes=SHADER_SE
|
||||
return out
|
||||
|
||||
|
||||
# arm64 syscall numbers seen in stuck threads (/proc/<tid>/syscall gives the number)
|
||||
ARM64_SYSCALLS = {22: "epoll_pwait", 29: "ioctl", 56: "openat", 63: "read", 64: "write", 73: "ppoll",
|
||||
98: "futex", 101: "nanosleep", 115: "clock_nanosleep", 203: "connect", 206: "sendto",
|
||||
207: "recvfrom", 212: "recvmsg", 260: "wait4"}
|
||||
TCP_STATES = {"01": "ESTABLISHED", "02": "SYN_SENT", "03": "SYN_RECV", "04": "FIN_WAIT1", "05": "FIN_WAIT2",
|
||||
"06": "TIME_WAIT", "07": "CLOSE", "08": "CLOSE_WAIT", "09": "LAST_ACK", "0A": "LISTEN", "0B": "CLOSING"}
|
||||
|
||||
|
||||
def _remote_kind(hexaddr):
|
||||
"""/proc/net/tcp(6) remote address → loopback / local / internet (never the address itself: it would need
|
||||
redacting, and what matters is whether the game waits on the internet)."""
|
||||
import ipaddress
|
||||
try:
|
||||
raw = bytes.fromhex(hexaddr)
|
||||
if len(raw) == 4:
|
||||
ip = ipaddress.IPv4Address(raw[::-1])
|
||||
else: # four little-endian 32-bit words
|
||||
ip = ipaddress.IPv6Address(b"".join(raw[i:i + 4][::-1] for i in range(0, 16, 4)))
|
||||
ip = ip.ipv4_mapped or ip
|
||||
except ValueError:
|
||||
return "?"
|
||||
if ip.is_unspecified:
|
||||
return "none"
|
||||
if ip.is_loopback:
|
||||
return "loopback"
|
||||
return "local" if ip.is_private or ip.is_link_local else "internet"
|
||||
|
||||
|
||||
def game_threads(pkg, proc=None, samples=3, gap=1.0, sleep=time.sleep):
|
||||
"""What a running (e.g. stuck) Android game's threads are doing (GitHub #157): for every process of `pkg` (its
|
||||
main process and `pkg:<name>` ones), each thread's state, CPU time used over the sampled seconds, kernel wait
|
||||
channel and current syscall, plus its TCP sockets. Readable as steamos: the container's user namespace belongs to
|
||||
steamos. None when the game isn't running."""
|
||||
proc = proc or PROC
|
||||
pids = []
|
||||
for d in os.listdir(proc):
|
||||
if not d.isdigit():
|
||||
continue
|
||||
cmd = (_rd(os.path.join(proc, d, "cmdline")) or "").split("\0")[0]
|
||||
if cmd == pkg or cmd.startswith(pkg + ":"):
|
||||
pids.append((int(d), cmd))
|
||||
if not pids:
|
||||
return None
|
||||
|
||||
def cpu(pid):
|
||||
out = {}
|
||||
for t in os.listdir(os.path.join(proc, str(pid), "task")) if os.path.isdir(os.path.join(proc, str(pid),
|
||||
"task")) else []:
|
||||
st = _rd(os.path.join(proc, str(pid), "task", t, "stat")) or ""
|
||||
f = st[st.rfind(")") + 2:].split()
|
||||
if len(f) > 12:
|
||||
out[t] = (f[0], int(f[11]) + int(f[12]))
|
||||
return out
|
||||
|
||||
first = {pid: cpu(pid) for pid, _ in sorted(pids)}
|
||||
for _ in range(max(0, samples - 1)):
|
||||
sleep(gap)
|
||||
span = max(0, samples - 1) * gap
|
||||
lines = [f"# threads of the running game {pkg}; cpu_ms = CPU time used in {span:g} s, wchan = kernel wait, "
|
||||
"syscall = what the thread is blocked in"]
|
||||
for pid, cmd in sorted(pids):
|
||||
last = cpu(pid)
|
||||
lines.append(f"\n## process {cmd} (pid {pid}, {len(last)} threads)")
|
||||
lines.append(f"{'tid':>7} st {'cpu_ms':>7} {'comm':<16} {'wchan':<24} syscall")
|
||||
rows = []
|
||||
for tid, (state, ticks) in last.items():
|
||||
used = (ticks - first.get(pid, {}).get(tid, (state, ticks))[1]) * 1000 // CLK_TCK
|
||||
tdir = os.path.join(proc, str(pid), "task", tid)
|
||||
comm = (_rd(os.path.join(tdir, "comm")) or "?").strip()
|
||||
wchan = (_rd(os.path.join(tdir, "wchan")) or "?").strip() or "-"
|
||||
sc = (_rd(os.path.join(tdir, "syscall")) or "?").split()
|
||||
call = sc[0] if sc else "?"
|
||||
if call.isdigit():
|
||||
call = ARM64_SYSCALLS.get(int(call), "syscall " + call)
|
||||
rows.append((-used, int(tid), f"{tid:>7} {state:>2} {used:>7} {comm:<16} {wchan:<24} {call}"))
|
||||
lines += [r[2] for r in sorted(rows)]
|
||||
socks = []
|
||||
for name in ("tcp", "tcp6"):
|
||||
for ln in (_rd(os.path.join(proc, str(pid), "net", name)) or "").splitlines()[1:]:
|
||||
f = ln.split()
|
||||
if len(f) > 3 and ":" in f[2]:
|
||||
addr, port = f[2].rsplit(":", 1)
|
||||
socks.append(f"{name:<4} {TCP_STATES.get(f[3], f[3]):<12} remote={_remote_kind(addr)}"
|
||||
f":{int(port, 16)}")
|
||||
if socks:
|
||||
lines.append("# TCP sockets (remote = loopback / local network / internet, and port)")
|
||||
lines += sorted(set(socks))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def cmd_collect_diag(args):
|
||||
"""Everything useful for debugging without the game or the PC app: host runtime facts and, with a package, the
|
||||
game's launcher, settings, deployment, logs (launch, Lepton logcat, Proton/Revive/Unreal) and its file listing.
|
||||
@@ -6016,6 +6106,13 @@ def cmd_collect_diag(args):
|
||||
for p in sorted(cands, key=lambda p: next((i for i, k in enumerate(order) if k in os.path.basename(p)), 9)):
|
||||
name = os.path.basename(p)
|
||||
files[name if name.startswith("logcat") else "logcat-" + name] = _tail(p, max_bytes)
|
||||
if dep.get("kind", "quest") == "quest":
|
||||
try:
|
||||
threads = game_threads(pkg, samples=int(args.get("thread_samples", 3)))
|
||||
except OSError:
|
||||
threads = None
|
||||
if threads:
|
||||
files["threads.txt"] = threads
|
||||
shaders = shader_dumps(data_files_dir(base, pkg), int(args.get("shader_bytes", SHADER_DUMP_BYTES)),
|
||||
int(args.get("shader_session_bytes", SHADER_SESSION_BYTES)))
|
||||
if shaders:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
package: com.WhiteDoorGames.Cosmodread
|
||||
title: Cosmodread
|
||||
status: works
|
||||
notes: First run wouldn't allow you to interact with the "New Game" button. Restarting it fixed it.
|
||||
tested_version: 1.0.0.28
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: 44911a4607d0de2770705eefb6339a4c7353cb5dfb49f389d6731edbb9cbea92
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 174
|
||||
source_hint: Cosmodread
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.innerspacevr.afishermanstale
|
||||
title: Fisherman
|
||||
status: issues
|
||||
notes: Loading screens have corruption, but everything works otherwise.
|
||||
tested_version: '1.087'
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-11'
|
||||
known_good_sha256: c645ca6321845bd460b4ff1b8ac871e941adb26d280cccba0285e79e4a96a231
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 175
|
||||
source_hint: A Fisherman's Tale
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.schellgames.ieytd3
|
||||
title: IEYTD3
|
||||
status: works
|
||||
tested_version: 1.0.1.211419 (ReleaseCandidate_1.0.0_QuestStore)
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- 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: 06f901ae22f7c07a79e11fc1af6b3941a51bbc99624336d6a0939f9593f3bfa0
|
||||
app: 0.12.1.dev263
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 78
|
||||
issue: 181
|
||||
source_hint: 2026-10-08T23-57-41.310Z_211419
|
||||
@@ -1,9 +1,9 @@
|
||||
package: ru.targem.blazerush
|
||||
title: BlazeRush
|
||||
status: works
|
||||
notes: From Klownicle's guide (GitHub issue 57), verified in a Steam Frame headset with this build; FramePort's
|
||||
version of the fixes still needs a headset check. Menus and racing controls, car bodies and controllers, particle
|
||||
effects, the full field of view and a sharp, correctly placed picture.
|
||||
notes: From Klownicle's guide (GitHub issue 57); FramePort's version verified in a Steam Frame headset (2026-10-11).
|
||||
Menus and racing controls, car bodies and controllers, particle effects, the full field of view and a sharp,
|
||||
correctly placed picture.
|
||||
details: 'Seven fixes from the guide. frame.blazerush (this exact build only) skips the menus'' wait for a Meta avatar,
|
||||
which never comes on the Frame, so the game creates its own controllers, and gives its shader cache a new folder
|
||||
(cache/br_shader_v1) so programs compiled before the shader fixes aren''t reused. The GL shim feeds integer vertex
|
||||
@@ -34,8 +34,8 @@ config_sync:
|
||||
r_width: h*16/9
|
||||
r_height: h
|
||||
verified:
|
||||
date: '2026-10-10'
|
||||
date: '2026-10-11'
|
||||
issue: 57
|
||||
updated: '2026-10-10'
|
||||
updated: '2026-10-11'
|
||||
min_app: 1.0.0
|
||||
source_hint: BlazeRush
|
||||
+9
-2
@@ -74,7 +74,8 @@ signatures:
|
||||
diagnosis: Android's debug checks stopped the game. CheckJNI is on because the converted app is debuggable, and it aborts on sloppy JNI use.
|
||||
suggest: [frame.nodebug]
|
||||
- id: force-quit
|
||||
pattern: 'System\.exit|ForceQuit|FAndroidMisc::RequestExit'
|
||||
# not Android's own WebView helper, which calls System.exit at every container boot (GitHub #179)
|
||||
pattern: '(?m)^(?!.*WebViewLoader).*System\.exit|ForceQuit|FAndroidMisc::RequestExit'
|
||||
severity: error
|
||||
diagnosis: The game quit itself right after starting. Usually Unreal's ForceQuit after a failed Quest platform check; the alternate build leaves it out.
|
||||
suggest: [patch_remove_unreal_force_quit]
|
||||
@@ -340,6 +341,12 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: 'Games can''t start until the Frame restarts: it ran out of kernel keys. Rootless podman leaks one session keyring per container start; the FramePort agent sets keyring=false in ~/.config/containers/containers.conf, and a reboot frees the leaked keys.'
|
||||
suggest: []
|
||||
- id: app-not-started
|
||||
pattern: 'Target app \(.*\) did not start'
|
||||
severity: error
|
||||
diagnosis: Lepton didn't start the game this time, which happens now and then on the first start after an install. Start it again; if it never starts, attach diagnostics.
|
||||
suggest: []
|
||||
supersedes: [force-quit]
|
||||
- id: container-not-started
|
||||
pattern: "is not a running context|OCI runtime error"
|
||||
unless: 'Boot complete!' # Lepton 3.x prints the error while its container is still starting, then boots fine
|
||||
@@ -482,7 +489,7 @@ milestones: # progress markers, in order; the furthest one reached is reported
|
||||
- {id: pcvr-openxr, kind: pcvr, pattern: 'wineopenxr: .*[Ss]ession|LibReviveXR|Revive.*OpenXR|xrCreateSession', label: OpenXR reached}
|
||||
- {id: pcvr-session, kind: pcvr, pattern: 'xrCreateSession|XR_SESSION_STATE_(READY|SYNCHRONIZED)', label: OpenXR session created}
|
||||
- {id: container, pattern: 'Boot complete!', label: Lepton container started}
|
||||
- {id: process, pattern: 'Start proc \d+:', label: App process started}
|
||||
- {id: process, pattern: 'Start proc \d+:(?!WebViewLoader|com\.android\.|android\.)', label: App process started}
|
||||
- {id: adapter, pattern: 'FrameBridge: scale=', label: FrameBridge adapter loaded}
|
||||
- {id: instance, pattern: 'xrCreateInstance result=0', label: OpenXR instance created}
|
||||
- {id: session, pattern: 'xrCreateSession|session state=1|Created OpenXR session', label: OpenXR session created}
|
||||
|
||||
@@ -40,6 +40,9 @@ games/<pkg>/target/ from the Frame (or the PC): launch.sh, settings.con
|
||||
ReviveInjector.txt / steam-<appid>.log / game-log-N.txt (PC VR), files.json (+ missing)
|
||||
(game-log-N.txt: Unreal Saved/Logs + crash summaries, Unity Player.log / Player-prev.log /
|
||||
output_log.txt + crash error.log from the Proton prefix, agent v67)
|
||||
threads.txt (agent v79): saved while a Quest game runs (e.g. hangs), each thread's
|
||||
state, CPU ms over 2 s, kernel wait and syscall, plus its TCP sockets (remote = loopback /
|
||||
local / internet + port, no addresses); read once from /proc, the game isn't paused
|
||||
games/<pkg>/target/shaders/ shader dumps (agent v72), when the game wrote any:
|
||||
fp_vk_shaders/ adapter vk_shader_dump=1 (Vulkan shim): <size>_<sha256>.spv + index.txt
|
||||
fp_spirv/ adapter zink_shader_dump=1 (OpenGL ES, shader-fix layer under Zink): <size>_<sha256>.spv
|
||||
|
||||
+144
-4
@@ -55,10 +55,150 @@ AXRB/ ← AXRB's download folder (Downloads/AX
|
||||
- **Several backups of one game:** FramePort uses the newest one. To use another, scan just that backup's folder.
|
||||
- **AXRB's patched builds** (`patched/`, `*-axrb.apk`) are converted for AXRB's PC runtime and lack the game's data, so
|
||||
FramePort skips them and, on the next scan, replaces a game that was added from one with the original download.
|
||||
- **AXRB export ZIPs:** unzip first. A game's ZIP holds `base.apk` and `Android/obb/<package>/`, which FramePort finds
|
||||
as is.
|
||||
- **The game page says the data file (.obb) is missing** although you have one: the backup didn't include it (copy
|
||||
- **AXRB export ZIPs:** unpack them first. A game's ZIP holds `base.apk` and `Android/obb/<package>/`, which FramePort
|
||||
finds as is.
|
||||
- **"Game data (.obb) is missing" on the game's page** although you have the file: the backup didn't include it (copy
|
||||
the game's `Android/obb/<package>` folder from the Quest with SideQuest), or it sits in a folder FramePort doesn't
|
||||
look in. Then use the layout in the previous answer.
|
||||
- **The game starts and stops at once (Steam shows Resume):** add the game again from its original download or
|
||||
backup, click **Update on Frame**, then **Report a problem…** on its page if it still stops.
|
||||
backup, click **Update on Frame**, then report it (last answer on this page) if it still stops.
|
||||
|
||||
## Where are a game's settings in FramePort?
|
||||
|
||||
Every game has three places; the answers below refer to them.
|
||||
|
||||
- **Game settings:** right-click the game in the **Library** → **Game settings…**. Sharpness, refresh rate,
|
||||
controllers, menus and more, in plain words. Changes apply the next time the game starts.
|
||||
- **Patches:** click the game in the **Library** to open its page, then **Customize** (in the **Patches** section).
|
||||
Turn a patch on or off, then click **Update on Frame** at the top of the page.
|
||||
- **The game's menu:** the **…** button at the top of its page (or right-click in the Library → **More actions…**).
|
||||
It has **Reset to suggested recipe…** and **Report a problem…**, among others.
|
||||
|
||||
The sidebar has two tabs for troubleshooting: **Monitor** (the running game's frame rate, CPU and graphics) and
|
||||
**Files** (each installed game's storage on the Frame, under **Game storage**).
|
||||
|
||||
## A game doesn't work right. Where do I start?
|
||||
|
||||
1. **Update FramePort** (it offers updates itself; Settings → Updates), then open the game's page: if it shows
|
||||
**Update on Frame**, click it. Recipes and patches improve often.
|
||||
2. **Read the game's page.** The last launch test and your last play session (**Last session**) leave their result
|
||||
there, often with a suggested patch or setting and a button that applies it. Try that first.
|
||||
3. **Changed patches yourself?** The game's menu → **Reset to suggested recipe…** brings back the tested ones.
|
||||
4. **Find your symptom below.** Each answer says how to tell what's wrong and what to do.
|
||||
5. **Still stuck?** Report it (last answer on this page).
|
||||
|
||||
## The game doesn't start, or stops right away
|
||||
|
||||
How to tell: the game's page shows the last launch test's result (**Last launch test:** …) with an explanation, and
|
||||
Steam returns to **Play** or **Resume** within seconds.
|
||||
|
||||
- **"Game data (.obb) is missing"** on the game's page: FramePort didn't find the game's data files, so they never
|
||||
reached the Frame.
|
||||
1. Find the game's `.obb` files (named like `main.<version>.<package>.obb`).
|
||||
2. Put them in a folder named exactly the package name (shown in the file names) next to the game's APK, as in the
|
||||
first answer on this page.
|
||||
3. Library → **Add games** → **Scan a folder…** and pick the game's folder.
|
||||
4. Open the game's page and click **Update on Frame**.
|
||||
- **"Lepton didn't start the game this time"**, or it failed only on the first start after an install: click **Play**
|
||||
again.
|
||||
- **"This game is 32-bit only"**: the Frame can't run 32-bit apps, and no patch changes that. Look for a PC VR version
|
||||
of the game.
|
||||
- **"The game quit itself right after starting"**: open the game's page and click **Update on Frame** so the suggested
|
||||
patches apply (many games check for a Quest at start, and a patch removes that).
|
||||
- **Nothing like that, or it still fails:** report it.
|
||||
|
||||
## It hangs on a logo, a black screen or a loading screen
|
||||
|
||||
How to tell what it's doing: leave it running and open **Monitor** in FramePort's sidebar.
|
||||
|
||||
- **CPU or graphics busy:** it's still working, often preparing its shaders on the first start. Wait up to 10 minutes
|
||||
once; the next start is quicker.
|
||||
- **CPU and graphics near zero:** it's stuck waiting for something. While it still hangs, open the game's menu →
|
||||
**Report a problem…** and save the diagnostics: that zip shows what each of the game's threads waits for (FramePort
|
||||
1.0.1 and newer), which a zip saved after quitting can't. Attach it to the report.
|
||||
- **The game starts with a video** and hangs at its logo: game page → **Customize** → turn off **Hardware video
|
||||
decoding** → **Update on Frame**, and try again.
|
||||
|
||||
## The picture is wrong
|
||||
|
||||
Most picture problems have a setting or a patch. Change one thing at a time, then start the game again (patches also
|
||||
need **Update on Frame**).
|
||||
|
||||
| You see | Try |
|
||||
|---|---|
|
||||
| Something shows in one eye only, or one eye is gray (Unity games) | Game page → **Customize** → [Unity: render each eye separately](PATCHES.md#frame-unity_multipass) |
|
||||
| Left and right look swapped (depth feels wrong) | **Game settings…** → **Swap eyes** |
|
||||
| Textures flicker, objects pop in and out | **Game settings…** → **Turn off space warp** |
|
||||
| One eye shimmers while menus look steady | Game page → **Customize** → **Eye-tracked foveation (Valve)**: **Fixed**, else **Off** |
|
||||
| Menus or screens upside down | **Game settings…** → **Fix upside-down menus** |
|
||||
| Curved menus or video screens missing | **Game settings…** → **Show curved menus and screens** |
|
||||
| Blurry | **Game settings…** → raise **Sharpness** (costs frame rate) |
|
||||
|
||||
The [patch reference](PATCHES.md) lists every patch with what it does. If nothing helps, report it with a short
|
||||
description of what you see.
|
||||
|
||||
## It stutters or runs slowly
|
||||
|
||||
How to tell: open **Monitor** in the sidebar while you play. Smooth play needs the game's frame rate at the refresh
|
||||
rate (for example 72 or 90 fps).
|
||||
|
||||
- **Below the refresh rate:** right-click the game → **Game settings…** → lower **Sharpness** (for example to 0.8×) or
|
||||
pick a lower **Refresh rate**. After a session, the game's page may suggest a value.
|
||||
- **At the refresh rate but it still judders:** report it; that points to timing, not speed.
|
||||
|
||||
## The game pauses or recenters by itself
|
||||
|
||||
The Frame briefly tells games they lost focus, for example when the headset's sensor thinks you took it off.
|
||||
Right-click the game → **Game settings…**: keep **Don't pause on short interruptions** on and raise **Ignore focus
|
||||
dips up to** (for example to 2.5 s).
|
||||
|
||||
## Controllers don't work in a game
|
||||
|
||||
Right-click the game → **Game settings…**:
|
||||
|
||||
- **No controllers, or only hands:** turn on **Use controllers**.
|
||||
- **Buttons work but you can't aim at menus:** adjust **Pointer angle**.
|
||||
- **Vibration too strong:** lower **Vibration strength**.
|
||||
|
||||
## The Frame turns off or gets hot during a game
|
||||
|
||||
How to tell: if it only happens on battery and never while plugged in, it's power, not FramePort. Heavy games can
|
||||
draw more than the battery delivers, especially at lower charge, and the Frame then turns off without an error.
|
||||
|
||||
- Play plugged in, or right-click the game → **Game settings…** → lower **Sharpness** to reduce the load.
|
||||
- It also turns off while plugged in: report it. The diagnostics include the log of the boot that ended.
|
||||
|
||||
## My progress isn't saved
|
||||
|
||||
1. Quit through the game's own menu, not Steam's **Exit game**. Exit game closes the game at once, and some games only
|
||||
save when they exit normally.
|
||||
2. Check whether it saves at all: **Files** in the sidebar → under **Game storage**, the game → look at the files'
|
||||
dates. Play, save in the game, quit from its menu and look again.
|
||||
3. Nothing changes: report it.
|
||||
|
||||
Saves stay on the Frame when you update or reinstall a game.
|
||||
|
||||
## A game with mods crashes
|
||||
|
||||
How to tell: play once without mods and custom content. If it then works, a mod is the cause; the diagnostics zip
|
||||
usually names the mod's library in the crash. Report it to the mod's author, and check for a mod version that matches
|
||||
your game version.
|
||||
|
||||
## The game needs files I supply (ports, emulators, mods)
|
||||
|
||||
How to tell where they go: the game says it on screen or in its instructions, and its log names the path (in the
|
||||
diagnostics zip, `launch.log`). It's usually `Android/data/<package>/files`.
|
||||
|
||||
1. Unpack or extract the files on your PC first: these apps read folders, not disc images (`.iso`) or archives.
|
||||
2. **Files** in the sidebar → under **Game storage**, the game → open that folder → **Upload folder…** (or
|
||||
**Upload files…**). The game's menu → **Add videos and files** opens the same place.
|
||||
|
||||
In-game "choose a file" buttons don't work on the Frame, which has no file picker.
|
||||
|
||||
## How do I report a problem well?
|
||||
|
||||
1. Reproduce it once, then right away open the game's menu (**…** on its page) → **Report a problem…**. For a hang,
|
||||
do it while the game still hangs.
|
||||
2. Say what you saw in the headset and what you already tried.
|
||||
3. **Collect and open GitHub** saves a diagnostics zip and opens a prefilled game report; attach the zip to it. The
|
||||
zip holds logs, not your game files, and personal details are removed.
|
||||
+2314
File diff suppressed because it is too large.
Load diff
+4
-1
@@ -31,6 +31,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| An overlay app (fpsVR, a wrist watch) installed on the Frame never appears, or SteamVR doesn't start it | not registered with the Frame's SteamVR, or registered with a manifest SteamVR skips: on the Frame vrserver reads only `binary_path_linux_arm` (vrserver.txt: "must specify binary_path for launch_type binary. Skipping") | `pcvr.vr_overlay` (Windows) / a Linux app with a bundled `.vrmanifest` (`is_dashboard_overlay`): agent v75 `register_vr_overlay` (check `vrcmd --overlays`, `~/.config/openvr/config/appconfig.json` manifest_paths); the app's own `--install` writes binary_path_linux only. fpsVR under Proton never loads OpenVR (quits after 60 s): unsupported for now |
|
||||
| An overlay app with **Start with SteamVR** on doesn't start by itself after the Frame or SteamVR starts (it works when started by hand) | SteamVR's own auto-launch flag (SetApplicationAutoLaunch) didn't persist on the Frame (no autolaunch entry in `~/.config/openvr/config/steamvr.vrsettings`) | agent v76: FramePort's service `frameport-vr-overlays.service` starts it at every new vrserver. Check `systemctl --user status frameport-vr-overlays`, `~/.local/share/frameport/vr-overlays.log` (`new SteamVR (vrserver N)` → `<pkg>: started` / `running` / `couldn't start it: …`), the deployment's `overlay.autostart`, and that `~/.local/share/frameport/vr-overlays.disabled` doesn't exist; `frameport frame info` (ensure_host_fixes) installs a missing service |
|
||||
| Every game suddenly fails to start: `crun: create keyring …: Disk quota exceeded`, `is not a running context` | rootless podman leaked a kernel keyring per launch; 200-key quota exhausted | `keyring = false` in `~/.config/containers/containers.conf` (FramePort agent does it), then reboot the Frame once |
|
||||
| Launch test EXITED, launch.log has `Target app (<pkg>) did not start` and no `Start proc …:<pkg>` (GitHub #179) | Lepton occasionally doesn't start the app on the first start after an APK change; Android's `WebViewLoader-: System.exit` at boot is not the game (triage `app-not-started`) | start it again |
|
||||
| A Unity app's text field shows a caret for a moment and loses focus; no keyboard appears (for example Stremio VR login) | Unity's TMP_InputField/InputField wait for Android's on-screen keyboard and close themselves without one (Lepton has none); headless VR apps also have no focused Android window, so no key press reaches them | `frame.unity_text_input` (Cpp2IL finds `TouchScreenKeyboardShouldBeUsed`/`isKeyboardUsingEvents`, rewritten to false/true) + `device.text_input_window` (lepton-show-flatscreen: Steam's keyboard and Type on Frame work) |
|
||||
| Play gives Steam's "Game configuration unavailable"; Steam's console_log: `GameAction [AppID <id>] … RequestingLicense → UpdatingAppInfo → LaunchApp failed with AppError_9` | this Frame's Steam never loaded FramePort's shortcuts.vdf entry (it treats the id as a store app); cause unknown (GitHub #21/#30) | automatic since agent v43: Play registers the game through Steam's devkit interface ("Devkit Game: …", `devkit_register`) and starts that; uninstall removes it. Agent v58: a devkit entry Steam forgot after a restart is added again live on Play (no NOT_IN_LIBRARY → no Steam restart), and installs/art updates of devkit games only copy art (no Steam restart, GitHub #41) |
|
||||
| Steam's Exit game leaves the game running (container `lepton-steamlaunch-<appid>` still up) | Steam stopped only its `reaper`; launch.sh and Lepton (setsid) never got a signal (GitHub #36) | agent v44: launch.sh's 2 s loop ends the game when its parent is gone (`upgrade_launchers` adds it to existing launchers); by hand: `podman kill lepton-steamlaunch-<appid>` |
|
||||
@@ -182,7 +183,9 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
touching new memory (`perf script -F comm,tid,ip,dso`); `-t <tid> -F 499` samples one thread. Map IL2CPP
|
||||
addresses with a full Cpp2IL diffable-cs dump (method `Offset` = address − the r-xp mapping of libil2cpp.so).
|
||||
Sample memory per mapping name from `/proc/<pid>/smaps` (`[anon:libc_malloc]` = native heap).
|
||||
- Hangs (every thread asleep): backtraces of all threads with gdb. Root inside the Lepton container may ptrace the
|
||||
- Hangs (every thread asleep): first ask for a diagnostics zip saved *while the game hangs*: `target/threads.txt`
|
||||
(agent v79) shows each thread's wait and syscall (futex = a lock, ppoll/recvfrom + an internet socket = network). Then
|
||||
backtraces of all threads with gdb. Root inside the Lepton container may ptrace the
|
||||
game (the host's `ptrace_scope` 1 doesn't apply there) and the guest has `gdbserver64`; `debuggerd -b` fails (no
|
||||
tombstoned). Copy every mapped library to a local sysroot (`podman exec <container> cat /proc/<pid>/maps`, then
|
||||
`tar -chf - -T <list>` through `podman exec -i`; the app's own libs from the installed `lepton-app/game.apk` at their
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
"""Generate docs/PATCHES.md (every patch and game setting) and site/src/data/patches.json (id -> anchor) from the
|
||||
live patch registry, the bundled catalog and catalog/triage.yaml.
|
||||
|
||||
python scripts/patch_docs.py # rewrite both files
|
||||
python scripts/patch_docs.py --check # exit 1 if either file is out of date (CI / tests)
|
||||
|
||||
Anchors: every entry starts with `<a id="<anchor>"></a>`, where the anchor is the patch id with "." replaced by "-"
|
||||
(`frame.unity_text_input` -> `#frame-unity_text_input`, `patch_copy_libraries` stays as it is). GitHub resolves such
|
||||
ids (it prefixes them with "user-content-" and follows plain `#anchor` links), and the site's docs renderer uses them
|
||||
as the heading id. Nothing here runs a patch's detection: "suggested when" is read from the reasons in the source.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
import yaml # noqa: E402
|
||||
|
||||
from frameport.patches import base, settings # noqa: E402
|
||||
|
||||
DOC = ROOT / "docs" / "PATCHES.md"
|
||||
SITE_MAP = ROOT / "site" / "src" / "data" / "patches.json"
|
||||
HEADER = "<!-- generated by scripts/patch_docs.py — don't edit by hand -->"
|
||||
MAX_GAMES = 8
|
||||
|
||||
# the game page's group names (ui/views/game.py CATEGORY_TITLES) plus what each group is
|
||||
CATEGORIES = {
|
||||
"overport": ("Patches from OVRPort",
|
||||
"OVRPort converts a Quest game's Meta VR code to standard OpenXR; FramePort picks which of its "
|
||||
"patches run. The list comes from the OVRPort CLI at runtime (new OVRPort patches appear in the app "
|
||||
"on their own); this page shows FramePort's bundled list (`patches/overport.py`)."),
|
||||
"frame": ("Steam Frame patches",
|
||||
"FramePort's own changes to a converted Quest game, for what the Steam Frame lacks or does differently."),
|
||||
"adapter": ("Game settings",
|
||||
"Settings of FrameBridge, FramePort's OpenXR layer inside every converted Quest game. Most can be "
|
||||
"changed on the Frame later without a rebuild (game page → Game settings)."),
|
||||
"device": ("Files and settings on the Frame",
|
||||
"Files, environment and window options written on the Frame when a Quest game or Android app is "
|
||||
"installed. They don't change the APK."),
|
||||
"pcvr": ("PC VR (Proton and Revive)",
|
||||
"Options for PC VR games, which run through Proton on the Frame (or Revive on this PC)."),
|
||||
}
|
||||
|
||||
|
||||
def anchor(pid: str) -> str:
|
||||
return pid.replace(".", "-")
|
||||
|
||||
|
||||
def one_line(text: str) -> str:
|
||||
return " ".join(str(text).split())
|
||||
|
||||
|
||||
def first_sentence(text: str) -> str:
|
||||
text = one_line(text)
|
||||
m = re.search(r"(?<=[.!?])\s+(?=[A-Z(])", text)
|
||||
return text[:m.start()] if m else text
|
||||
|
||||
|
||||
def md_escape(text: str) -> str:
|
||||
"""Text for a markdown table cell or a plain line (no accidental HTML or cell breaks)."""
|
||||
return one_line(text).replace("|", "\\|").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ "suggested when", read from detect()'s source
|
||||
|
||||
def _string(node: ast.AST) -> str | None:
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||
return node.value
|
||||
if isinstance(node, ast.JoinedStr):
|
||||
parts = []
|
||||
for v in node.values:
|
||||
parts.append(v.value if isinstance(v, ast.Constant) else "…")
|
||||
return "".join(parts)
|
||||
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
||||
a, b = _string(node.left), _string(node.right)
|
||||
return a + b if a is not None and b is not None else None
|
||||
return None
|
||||
|
||||
|
||||
def _scope(test: ast.AST, patch: base.Patch) -> bool | None:
|
||||
"""Whether an `if` test that names this kind of patch (self.key == "x", self.id == "x", self.default_on) holds for
|
||||
`patch`; None when the test is about the game, not the patch."""
|
||||
tests = test.values if isinstance(test, ast.BoolOp) and isinstance(test.op, ast.And) else [test]
|
||||
for t in tests:
|
||||
if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) and t.value.id == "self" \
|
||||
and t.attr == "default_on":
|
||||
return bool(patch.default_on)
|
||||
if isinstance(t, ast.Compare) and len(t.ops) == 1 and isinstance(t.left, ast.Attribute) \
|
||||
and isinstance(t.left.value, ast.Name) and t.left.value.id == "self" and t.left.attr in ("key", "id"):
|
||||
own = getattr(patch, t.left.attr, None)
|
||||
right = t.comparators[0]
|
||||
try:
|
||||
value = ast.literal_eval(right)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(t.ops[0], ast.Eq):
|
||||
return own == value
|
||||
if isinstance(t.ops[0], ast.In):
|
||||
return own in value
|
||||
return None
|
||||
|
||||
|
||||
def suggestions(patch: base.Patch) -> list[tuple[bool, str, dict]]:
|
||||
fn = type(patch).detect
|
||||
if fn is base.Patch.detect:
|
||||
return [(True, "On for every game.", {})] if patch.default_on else []
|
||||
try:
|
||||
tree = ast.parse(textwrap.dedent(inspect.getsource(fn)))
|
||||
except (OSError, TypeError, SyntaxError):
|
||||
return []
|
||||
found: list[tuple[bool, str, dict]] = []
|
||||
|
||||
def visit(node: ast.AST) -> None:
|
||||
if isinstance(node, ast.If):
|
||||
ok = _scope(node.test, patch)
|
||||
if ok is not False:
|
||||
for n in node.body:
|
||||
visit(n)
|
||||
for n in node.orelse:
|
||||
visit(n)
|
||||
return
|
||||
if isinstance(node, ast.Call) and getattr(node.func, "id", None) == "Suggestion" and len(node.args) >= 2:
|
||||
reason = _string(node.args[1])
|
||||
if reason:
|
||||
rec = node.args[0]
|
||||
recommended = rec.value if isinstance(rec, ast.Constant) else True
|
||||
params = {}
|
||||
if len(node.args) > 2:
|
||||
try:
|
||||
params = ast.literal_eval(node.args[2])
|
||||
except ValueError:
|
||||
params = {}
|
||||
found.append((bool(recommended), reason, params if isinstance(params, dict) else {}))
|
||||
for child in ast.iter_child_nodes(node):
|
||||
visit(child)
|
||||
|
||||
visit(tree)
|
||||
seen, out = set(), []
|
||||
for s in found:
|
||||
if s[1] not in seen:
|
||||
seen.add(s[1])
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ catalog and triage
|
||||
|
||||
CATALOG_FIELDS = {"overport_extra": None, "alt_overport": None, "frame": None, "device": None, "pcvr": None}
|
||||
CATALOG_MAPS = {"device_files": "device.files", "config_sync": "device.config_sync", "lepton_env": "device.lepton_env",
|
||||
"foveation": "device.foveation", "proton_env": "pcvr.proton_env", "proton_tool": "pcvr.proton_tool"}
|
||||
|
||||
|
||||
def catalog_uses() -> dict[str, list[str]]:
|
||||
"""patch id -> titles of bundled catalog games whose recipe names it."""
|
||||
uses: dict[str, set[str]] = {}
|
||||
for f in sorted((ROOT / "catalog" / "games").glob("*.yaml")):
|
||||
d = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
||||
title = one_line(d.get("title") or d.get("package") or f.stem)
|
||||
if d.get("kind") == "rift":
|
||||
title += " (PC VR)"
|
||||
ids: set[str] = set()
|
||||
for field in CATALOG_FIELDS:
|
||||
ids.update(str(x) for x in d.get(field) or [])
|
||||
for key in d.get("adapter") or {}:
|
||||
ids.add(f"adapter.{key}")
|
||||
for field, pid in CATALOG_MAPS.items():
|
||||
if d.get(field):
|
||||
ids.add(pid)
|
||||
for pid in ids:
|
||||
uses.setdefault(pid, set()).add(title)
|
||||
return {k: sorted(v, key=str.casefold) for k, v in uses.items()}
|
||||
|
||||
|
||||
def triage_uses() -> dict[str, list[tuple[str, str, str]]]:
|
||||
"""patch id -> (signature id, severity, diagnosis) of the triage signatures that suggest it."""
|
||||
data = yaml.safe_load((ROOT / "catalog" / "triage.yaml").read_text(encoding="utf-8")) or {}
|
||||
out: dict[str, list[tuple[str, str, str]]] = {}
|
||||
for sig in data.get("signatures") or []:
|
||||
for item in sig.get("suggest") or []:
|
||||
pid = str(item).split("=", 1)[0].strip()
|
||||
diagnosis = first_sentence(sig.get("diagnosis", ""))
|
||||
out.setdefault(pid, []).append((sig["id"], sig.get("severity", ""), diagnosis))
|
||||
return {k: sorted(set(v)) for k, v in out.items()}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ one entry
|
||||
|
||||
def changes(p: base.Patch) -> str:
|
||||
if p.stage == "overport":
|
||||
return "Changes the APK (during the OVRPort conversion)."
|
||||
if p.stage == "apk":
|
||||
return "Changes the APK (a rebuild and reinstall apply it)."
|
||||
if p.category == "adapter":
|
||||
text = (p.description + " " + settings.UI.get(getattr(p, "key", ""), {}).get("help", "")).lower()
|
||||
if "rebuild" in text:
|
||||
return "Game setting, stored in the build: needs a rebuild."
|
||||
return "Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild."
|
||||
if p.category == "pcvr":
|
||||
return "Applied when the PC VR game is installed or started; no rebuild."
|
||||
return "Install only: written on the Frame when the game is installed; the APK isn't changed."
|
||||
|
||||
|
||||
def _value(v) -> str:
|
||||
if v == "" or v is None:
|
||||
return "empty"
|
||||
if isinstance(v, dict):
|
||||
return "none" if not v else json.dumps(v, sort_keys=True)
|
||||
return f"`{v}`"
|
||||
|
||||
|
||||
def options(p: base.Patch) -> list[str]:
|
||||
lines = []
|
||||
if p.category == "adapter":
|
||||
key = p.key
|
||||
spec = next(s for s in settings.SETTINGS if s[0] == key)
|
||||
kind, default = spec[1], spec[2]
|
||||
line = f"Value: {kind}, default {_value(default)}"
|
||||
if key in settings.BASE_SETTINGS:
|
||||
line += " (always written)"
|
||||
lines.append(line + ".")
|
||||
ui = settings.UI.get(key)
|
||||
if ui and kind in ("int", "float") and key not in settings.HIDDEN:
|
||||
group = dict(settings.GROUPS).get(ui["group"], ui["group"])
|
||||
control = ui["control"]
|
||||
where = f"Game settings → {group} → \"{ui['label']}\""
|
||||
if ui.get("level") == "advanced":
|
||||
where += " (advanced)"
|
||||
if control[0] == "slider":
|
||||
where += f": slider {control[1]:g}–{control[2]:g} in steps of {control[3]:g}"
|
||||
elif control[0] == "choice":
|
||||
where += ": " + ", ".join(f"{label} (`{v:g}`)" if isinstance(v, float) else f"{label} (`{v}`)"
|
||||
for v, label in control[1])
|
||||
if ui.get("depends"):
|
||||
where += f"; shown while `adapter.{ui['depends']}` is on"
|
||||
lines.append(where + ".")
|
||||
else:
|
||||
lines.append("Not in the Game settings dialog (set by a recipe or the CLI).")
|
||||
return lines
|
||||
for prm in p.params:
|
||||
text = f"`{prm.key}`: {prm.kind}, default {_value(prm.default)}"
|
||||
if prm.minimum is not None or prm.maximum is not None:
|
||||
low = prm.minimum if prm.minimum is not None else "…"
|
||||
high = prm.maximum if prm.maximum is not None else "…"
|
||||
text += f", {low}–{high}"
|
||||
if prm.help:
|
||||
text += f" — {md_escape(prm.help)}"
|
||||
lines.append(text)
|
||||
return lines
|
||||
|
||||
|
||||
def source_link(p: base.Patch) -> str:
|
||||
try:
|
||||
path = Path(inspect.getsourcefile(type(p))).resolve().relative_to(ROOT)
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
return f"[{path.relative_to('src/frameport').as_posix()}](../{path.as_posix()})"
|
||||
|
||||
|
||||
def entry(p: base.Patch, games: list[str], sigs: list[tuple[str, str, str]]) -> str:
|
||||
out = [f"### <a id=\"{anchor(p.id)}\"></a>{md_escape(p.title)}", ""]
|
||||
if p.summary:
|
||||
out += [md_escape(p.summary), ""]
|
||||
facts = [f"`{p.id}`"]
|
||||
if p.default_on:
|
||||
facts.append("on by default")
|
||||
if p.experimental:
|
||||
facts.append("experimental")
|
||||
if not p.needs_vr:
|
||||
facts.append("also for 2D apps")
|
||||
rev = p.revision
|
||||
if rev > 1:
|
||||
facts.append(f"revision {rev}")
|
||||
out.append("- " + " · ".join(facts))
|
||||
out.append(f"- **Changes:** {changes(p)}")
|
||||
sug = suggestions(p)
|
||||
for recommended, reason, params in sug:
|
||||
label = "Suggested (on)" if recommended else "Offered (off)"
|
||||
if params and p.category == "adapter" and "value" in params:
|
||||
label += f", value `{params['value']}`"
|
||||
out.append(f"- **{label}:** {md_escape(reason)}")
|
||||
if not sug:
|
||||
out.append("- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a "
|
||||
"launch test.")
|
||||
if getattr(p, "strict", False):
|
||||
out.append("- **Only for its game's build:** outside it the whole OVRPort run fails, so recipes only use it "
|
||||
"where it applies.")
|
||||
if p.conflicts:
|
||||
out.append("- **Conflicts with:** " + ", ".join(f"[`{c}`](#{anchor(c)})" for c in sorted(p.conflicts)))
|
||||
if p.requires:
|
||||
out.append("- **Requires:** " + ", ".join(f"[`{c}`](#{anchor(c)})" for c in sorted(p.requires)))
|
||||
opts = options(p)
|
||||
if len(opts) == 1:
|
||||
out.append(f"- **Options:** {opts[0]}")
|
||||
elif opts:
|
||||
out.append("- **Options:**")
|
||||
out += [f" - {o}" for o in opts]
|
||||
if games:
|
||||
shown = ", ".join(md_escape(g) for g in games[:MAX_GAMES])
|
||||
more = f" and {len(games) - MAX_GAMES} more" if len(games) > MAX_GAMES else ""
|
||||
out.append(f"- **Catalog games ({len(games)}):** {shown}{more}.")
|
||||
if sigs:
|
||||
out.append("- **Suggested by launch-test signatures:**")
|
||||
out += [f" - `{sid}` ({sev}): {md_escape(diag)}" for sid, sev, diag in sigs]
|
||||
src = source_link(p)
|
||||
if src:
|
||||
out.append(f"- **Source:** {src}")
|
||||
desc = md_escape(p.description)
|
||||
if desc and desc != md_escape(p.summary):
|
||||
out += ["", f"> {desc}"]
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ the page
|
||||
|
||||
def build() -> tuple[str, str]:
|
||||
patches = base.all_patches()
|
||||
ids = [p.id for p in patches]
|
||||
anchors = [anchor(i) for i in ids]
|
||||
if len(set(anchors)) != len(anchors):
|
||||
raise SystemExit("two patches share an anchor")
|
||||
uses, sigs = catalog_uses(), triage_uses()
|
||||
cats = sorted({p.category for p in patches}, key=lambda c: (base.CATEGORIES.index(c), c))
|
||||
by_cat = {c: sorted((p for p in patches if p.category == c), key=lambda p: p.id) for c in cats}
|
||||
counts = {c: len(by_cat[c]) for c in cats}
|
||||
title = lambda c: CATEGORIES.get(c, (c.capitalize(), "")) # noqa: E731
|
||||
|
||||
lines = [HEADER, "", "# Patches", "",
|
||||
f"Every patch and game setting FramePort knows: {len(patches)} entries in {len(cats)} groups. A game's "
|
||||
"recipe is its chosen patches and settings; tested games get theirs from the catalog "
|
||||
"([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You "
|
||||
"change them on the game page under Customize.", "",
|
||||
"Each entry says what the patch does, whether it changes the APK or only the install, when FramePort "
|
||||
"suggests it, its options, the catalog games whose recipe names it and the launch-test signatures "
|
||||
"that suggest it. Patches that are on by default are on for every game, so the catalog rarely names "
|
||||
"them.", "",
|
||||
"Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_text_input` for "
|
||||
"`frame.unity_text_input`.", "",
|
||||
"## Groups", ""]
|
||||
for c in cats:
|
||||
lines.append(f"- [{title(c)[0]}](#{anchor('group-' + c)}) ({counts[c]})")
|
||||
lines += ["", "## Index", "", "| Patch | Id | Group | Catalog games |", "| --- | --- | --- | --- |"]
|
||||
for c in cats:
|
||||
for p in by_cat[c]:
|
||||
n = len(uses.get(p.id, []))
|
||||
games = ("default" + (f" · {n}" if n else "")) if p.default_on else (str(n) if n else "")
|
||||
lines.append(f"| [{md_escape(p.title)}](#{anchor(p.id)}) | `{p.id}` | {title(c)[0]} | {games} |")
|
||||
lines.append("")
|
||||
for c in cats:
|
||||
name, intro = title(c)
|
||||
lines += [f"## <a id=\"{anchor('group-' + c)}\"></a>{name}", ""]
|
||||
if intro:
|
||||
lines += [intro, ""]
|
||||
for p in by_cat[c]:
|
||||
lines.append(entry(p, uses.get(p.id, []), sigs.get(p.id, [])))
|
||||
unknown = sorted(set(sigs) - set(ids))
|
||||
if unknown:
|
||||
raise SystemExit(f"catalog/triage.yaml suggests unknown patches: {', '.join(unknown)}")
|
||||
doc = "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
site = {p.id: {"anchor": anchor(p.id), "title": one_line(p.title), "group": title(p.category)[0]}
|
||||
for p in patches}
|
||||
site_json = json.dumps(dict(sorted(site.items())), indent=1, ensure_ascii=False) + "\n"
|
||||
return doc, site_json
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||||
ap.add_argument("--check", action="store_true", help="only check that the files are up to date")
|
||||
args = ap.parse_args()
|
||||
doc, site_json = build()
|
||||
targets = [(DOC, doc), (SITE_MAP, site_json)]
|
||||
if args.check:
|
||||
stale = [p for p, text in targets if not p.exists() or p.read_text(encoding="utf-8") != text]
|
||||
for p in stale:
|
||||
print(f"{p.relative_to(ROOT)} is out of date: run python scripts/patch_docs.py", file=sys.stderr)
|
||||
return 1 if stale else 0
|
||||
for p, text in targets:
|
||||
p.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(f"wrote {p.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -556,6 +556,12 @@ const ICON: Record<string, string> = {
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
@keyframes halo { 50% { box-shadow: 0 0 0 2px var(--accent), 0 0 0 11px rgba(255, 138, 31, .12); } }
|
||||
|
||||
/* medium (tablets): the title keeps its own width; the buttons' "auto" column squeezed it to "Libr" and one word
|
||||
per line. The buttons wrap in what's left */
|
||||
@container (min-width: 701px) and (max-width: 960px) {
|
||||
.view :global(.lib-h) { grid-template-columns: max-content minmax(0, 1fr); }
|
||||
}
|
||||
|
||||
/* narrow: the sidebar becomes an icon rail */
|
||||
@container (max-width: 700px) {
|
||||
.body { grid-template-columns: 56px minmax(0, 1fr); }
|
||||
@@ -567,5 +573,12 @@ const ICON: Record<string, string> = {
|
||||
.view :global(.mon-game) { grid-template-columns: 56px minmax(0, 1fr); }
|
||||
.view :global(.mon-game .big-spark) { grid-column: 1 / -1; }
|
||||
.view :global(.grid) { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
|
||||
/* Library header: title and count on their own line, the search full width, the buttons wrapping below */
|
||||
.view :global(.lib-h) { grid-template-columns: minmax(0, 1fr); }
|
||||
.view :global(.lib-h .vh-act) { justify-content: flex-start; }
|
||||
.view :global(.lib-h .search-w) { flex: 1 1 100%; }
|
||||
.view :global(.lib-h .search-w .search) { flex: 1; min-width: 0; width: auto; }
|
||||
.view :global(.add-w) { grid-column: 1; justify-self: start; }
|
||||
.view :global(.add-w .menu) { right: auto; left: 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -166,6 +166,9 @@ const ICON = {
|
||||
.heads { grid-template-columns: minmax(0, 1fr); }
|
||||
.hcard.foss { grid-template-columns: minmax(0, 1fr); }
|
||||
.hcard.foss .hicon { grid-row: auto; }
|
||||
/* one column: the title's "2 / -1" from the wide layout would add a second, implicit column and squeeze
|
||||
FramePort's text into the first */
|
||||
.hcard.foss h3 { grid-column: auto; }
|
||||
.hcard.foss .other { border-left: 0; padding-left: 0; border-top: 1px dashed var(--border-strong); padding-top: 8px; }
|
||||
th, td { padding: 10px 12px; }
|
||||
.rowh { width: 38%; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// date in the browser from GitHub's main branch (scripts/catalog-live.ts), the way the app follows the catalog.
|
||||
import { loadSnapshot } from '../data/catalog';
|
||||
import { STATUS, counts, engines, latestCheck, rowsHtml, sortGames, type Game, type Status } from '../scripts/catalog';
|
||||
import { patchHref } from '../scripts/patchlinks';
|
||||
import { SHARE_CONFIG, REPORT_PROBLEM, blob } from '../data/site';
|
||||
|
||||
const snapshot = loadSnapshot();
|
||||
@@ -58,7 +59,7 @@ const latest = latestCheck(games);
|
||||
<span role="columnheader">Status</span>
|
||||
<span role="columnheader" class="c-date">Checked</span>
|
||||
</div>
|
||||
<ul class="rows" role="rowgroup" data-rows set:html={rowsHtml(games)} />
|
||||
<ul class="rows" role="rowgroup" data-rows set:html={rowsHtml(games, patchHref)} />
|
||||
<p class="empty" data-empty hidden>No tested game matches. It may still work: FramePort suggests patches.</p>
|
||||
</div>
|
||||
<div class="board-foot">
|
||||
@@ -79,6 +80,7 @@ const latest = latestCheck(games);
|
||||
|
||||
<script>
|
||||
import { counts, engines, latestCheck, rowsHtml, sortGames, type Snapshot } from '../scripts/catalog';
|
||||
import { patchHref } from '../scripts/patchlinks';
|
||||
import { refreshCatalog } from '../scripts/catalog-live';
|
||||
|
||||
const board = document.querySelector<HTMLElement>('[data-board]');
|
||||
@@ -151,7 +153,7 @@ const latest = latestCheck(games);
|
||||
src.classList.add('live');
|
||||
if (!live.changed) return;
|
||||
const games = sortGames(live.games);
|
||||
list.innerHTML = rowsHtml(games);
|
||||
list.innerHTML = rowsHtml(games, patchHref);
|
||||
const n = counts(games);
|
||||
for (const s of ['works', 'issues', 'unsupported'] as const) set(`[data-n="${s}"]`, String(n[s]));
|
||||
set('[data-total]', String(games.length));
|
||||
@@ -221,6 +223,9 @@ const latest = latestCheck(games);
|
||||
.rows :global(.pt) { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.rows :global(.pt span) { font: 600 10.5px/1 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--text-2); margin-right: 4px; }
|
||||
.rows :global(.pt code) { font-size: 12px; padding: 3px 8px; border-radius: 6px; background: rgba(58, 168, 255, .1); color: var(--secondary); }
|
||||
.rows :global(.pt a) { text-decoration: none; border-radius: 6px; }
|
||||
.rows :global(.pt a code) { transition: background 160ms; }
|
||||
.rows :global(.pt a:hover code), .rows :global(.pt a:focus-visible code) { background: rgba(58, 168, 255, .22); text-decoration: underline; }
|
||||
.rows :global(.fx) { font: 400 12px/1.4 var(--font-mono); color: var(--text-3); }
|
||||
@keyframes open { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
|
||||
.rows :global(.row[hidden]) { display: none; }
|
||||
|
||||
@@ -3,8 +3,10 @@ import Wordmark from './Wordmark.astro';
|
||||
import Contributors from './Contributors.astro';
|
||||
import { REPO, REPORT_PROBLEM, url } from '../data/site';
|
||||
|
||||
interface Props { credits?: boolean }
|
||||
const { credits = true } = Astro.props;
|
||||
// "all" = the "Built on" section with the projects and the contributors (the homepage), "contributors" = only the
|
||||
// contributors (What's new), none by default (docs, download, privacy, 404 and the other utility pages)
|
||||
interface Props { credits?: 'all' | 'contributors' | false }
|
||||
const { credits = false } = Astro.props;
|
||||
// each project named links to its own page (Lepton has none of its own: it links to Waydroid, which it is built on)
|
||||
type Part = [name: string, href?: string];
|
||||
const builtOn: { parts: Part[]; what: string }[] = [
|
||||
@@ -23,7 +25,12 @@ const builtOn: { parts: Part[]; what: string }[] = [
|
||||
/** "a", "a and b", "a, b and c" */
|
||||
const joiner = (i: number, n: number) => (i === 0 ? '' : i === n - 1 ? ' and ' : ', ');
|
||||
---
|
||||
{credits && (
|
||||
{credits === 'contributors' && (
|
||||
<section class="section credits" id="contributors">
|
||||
<div class="wrap"><Contributors /></div>
|
||||
</section>
|
||||
)}
|
||||
{credits === 'all' && (
|
||||
<section class="section credits" id="credits">
|
||||
<div class="wrap">
|
||||
<div class="head reveal">
|
||||
|
||||
@@ -26,7 +26,8 @@ const logo = readFileSync(join(process.cwd(), '..', 'src/frameport/ui/icons/logo
|
||||
b.style.setProperty('--mx', `${((e.clientX - r.left) / r.width) * 100}%`);
|
||||
b.style.setProperty('--my', `${((e.clientY - r.top) / r.height) * 100}%`);
|
||||
});
|
||||
b.addEventListener('pointerdown', () => { b.classList.remove('pressed'); void b.offsetWidth; b.classList.add('pressed'); });
|
||||
// the press ripple is for the mouse: on a touch screen it fired on every scroll that started on the badge
|
||||
b.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch') return; b.classList.remove('pressed'); void b.offsetWidth; b.classList.add('pressed'); });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Glyph from './Glyph.astro';
|
||||
import PcvrHow from './PcvrHow.astro';
|
||||
import { patchLabel, stageDetails, type LogGame } from '../scripts/logdetails';
|
||||
import { PATCH_DOC, patchHref, patchInfo } from '../scripts/patchlinks';
|
||||
|
||||
interface Props { example: LogGame; others: LogGame[] }
|
||||
const { example, others } = Astro.props;
|
||||
@@ -79,7 +80,10 @@ const frame: Stage[] = [
|
||||
<p class="foot muted reveal">
|
||||
<span class="recipe-box" aria-live="polite">
|
||||
<span class="rb-k">Recipe</span>
|
||||
<span class="rb-p" data-lg-patches>{example.patches.length ? example.patches.map((x) => <code>{patchLabel(x)}</code>) : <code>Default patches</code>}</span>
|
||||
<span class="rb-p" data-lg-patches>{example.patches.length
|
||||
? example.patches.map((x) => { const href = patchHref(x); return href
|
||||
? <a class="chip" href={href} title={patchInfo(x)!.title}><code>{patchLabel(x)}</code></a> : <code>{patchLabel(x)}</code>; })
|
||||
: <a class="chip" href={PATCH_DOC} title="Every patch FramePort knows"><code>Default patches</code></a>}</span>
|
||||
<span class="rb-n" data-lg-note>{example.note}</span>
|
||||
</span>
|
||||
<span class="foot-t"><span data-lg="foot">{example.title}</span>'s recipe from the catalog. Untested games get suggested patches.</span>
|
||||
@@ -93,6 +97,7 @@ const frame: Stage[] = [
|
||||
|
||||
<script>
|
||||
import { patchLabel, stageDetails, type LogGame } from '../scripts/logdetails';
|
||||
import { PATCH_DOC, patchHref, patchInfo } from '../scripts/patchlinks';
|
||||
// another tested game, its real engine, VR API and recipe; the log plays again
|
||||
const log = document.querySelector<HTMLElement>('[data-log]');
|
||||
const games = JSON.parse(document.querySelector('[data-log-games]')?.textContent || '[]') as
|
||||
@@ -112,7 +117,15 @@ const frame: Stage[] = [
|
||||
chips.replaceChildren(...(g.patches.length ? g.patches : ['Default patches']).map((x) => {
|
||||
const c = document.createElement('code');
|
||||
c.textContent = g.patches.length ? patchLabel(x) : x;
|
||||
return c;
|
||||
// each chip links to its entry in the patch reference (the default chip to the whole reference)
|
||||
const href = g.patches.length ? patchHref(x) : PATCH_DOC;
|
||||
if (!href) return c;
|
||||
const a = document.createElement('a');
|
||||
a.className = 'chip';
|
||||
a.href = href;
|
||||
a.title = g.patches.length ? patchInfo(x)!.title : 'Every patch FramePort knows';
|
||||
a.append(c);
|
||||
return a;
|
||||
}));
|
||||
}
|
||||
const note = document.querySelector('[data-lg-note]');
|
||||
@@ -178,7 +191,12 @@ const frame: Stage[] = [
|
||||
padding: 14px 16px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); }
|
||||
.rb-k { font: 600 11px/1 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
|
||||
.rb-p { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.rb-p code { font-size: 12px; padding: 3px 8px; border-radius: 6px; background: rgba(58, 168, 255, .1); color: var(--secondary);
|
||||
/* :global: "Try another game" builds the chips in the browser, without Astro's scoping attribute */
|
||||
/* chips are links without the running-text underline (global.css .foot a): the chip's fill brightens instead */
|
||||
.foot .rb-p :global(a.chip), .foot .rb-p :global(a.chip:hover) { text-decoration: none; background: none; padding: 0; border-radius: 6px; }
|
||||
.rb-p :global(a code) { transition: background 160ms; }
|
||||
.rb-p :global(a:hover code), .rb-p :global(a:focus-visible code) { background: rgba(58, 168, 255, .22); text-decoration: underline; }
|
||||
.rb-p :global(code) { font-size: 12px; padding: 3px 8px; border-radius: 6px; background: rgba(58, 168, 255, .1); color: var(--secondary);
|
||||
animation: chip .35s var(--ease-out) both; }
|
||||
.rb-n { grid-column: 2; color: var(--text-2); font-size: 14px; }
|
||||
.rb-n:empty { display: none; }
|
||||
|
||||
@@ -17,7 +17,7 @@ export const HEADLINES: Headline[] = [
|
||||
frameport: 'GPL-3.0. Read it, build it, change it, share it.',
|
||||
others: ['Free (donationware). Source not published.', 'Free, from Valve.'] },
|
||||
{ icon: 'game', title: "Quest games that don't run as they are",
|
||||
frameport: 'Patches them for the Frame, with a tested recipe for 90+ games.',
|
||||
frameport: 'Patches them for the Frame, with a tested recipe for 120+ games.',
|
||||
others: ['Installs the APK as it is.', 'Installs the APK as it is.'] },
|
||||
{ icon: 'pc', title: 'Your PC',
|
||||
frameport: 'Windows, macOS and Linux.',
|
||||
@@ -45,7 +45,7 @@ export const ROWS: Row[] = [
|
||||
y('Converted and patched for the Frame'),
|
||||
n('Installs the APK as it is'),
|
||||
n('Installs the APK as it is')] },
|
||||
{ label: 'Does it know which patches a game needs?', cells: [y('A tested recipe for 90+ games, updated without an app update'), u(), n('No')] },
|
||||
{ label: 'Does it know which patches a game needs?', cells: [y('A tested recipe for 120+ games, updated without an app update'), u(), n('No')] },
|
||||
{ label: 'Will the game be in my Steam library?', cells: [
|
||||
y('Yes, with artwork and tags'), p('As a "Devkit Game" shortcut'), p('As "Devkit Game: <title>"; not with adb')] },
|
||||
{ label: 'Android apps, Linux apps, Windows programs?', cells: [
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
{
|
||||
"updated": "2026-10-10",
|
||||
"updated": "2026-10-11",
|
||||
"people": [
|
||||
{
|
||||
"login": "spoopyghosty0",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/336754034?v=4",
|
||||
"url": "https://github.com/spoopyghosty0",
|
||||
"commits": 462,
|
||||
"commits": 523,
|
||||
"issues": 4,
|
||||
"prs": 2,
|
||||
"total": 484
|
||||
"prs": 3,
|
||||
"total": 550
|
||||
},
|
||||
{
|
||||
"login": "StevenPartridge",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/22158507?v=4",
|
||||
"url": "https://github.com/StevenPartridge",
|
||||
"commits": 0,
|
||||
"issues": 15,
|
||||
"prs": 0,
|
||||
"total": 45
|
||||
},
|
||||
{
|
||||
"login": "tkp206093",
|
||||
@@ -19,15 +28,6 @@
|
||||
"prs": 0,
|
||||
"total": 45
|
||||
},
|
||||
{
|
||||
"login": "StevenPartridge",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/22158507?v=4",
|
||||
"url": "https://github.com/StevenPartridge",
|
||||
"commits": 0,
|
||||
"issues": 14,
|
||||
"prs": 0,
|
||||
"total": 42
|
||||
},
|
||||
{
|
||||
"login": "Klownicle",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/6896081?v=4",
|
||||
@@ -100,6 +100,24 @@
|
||||
"prs": 0,
|
||||
"total": 15
|
||||
},
|
||||
{
|
||||
"login": "eepykith",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/338898488?v=4",
|
||||
"url": "https://github.com/eepykith",
|
||||
"commits": 0,
|
||||
"issues": 4,
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "nickwilde77555",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/180547421?v=4",
|
||||
"url": "https://github.com/nickwilde77555",
|
||||
"commits": 0,
|
||||
"issues": 4,
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "Rizsraketa",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/64963535?v=4",
|
||||
@@ -127,6 +145,15 @@
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "trankillity",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/53357041?v=4",
|
||||
"url": "https://github.com/trankillity",
|
||||
"commits": 0,
|
||||
"issues": 4,
|
||||
"prs": 0,
|
||||
"total": 12
|
||||
},
|
||||
{
|
||||
"login": "xhuytox",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/20061642?v=4",
|
||||
@@ -163,15 +190,6 @@
|
||||
"prs": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"login": "eepykith",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/338898488?v=4",
|
||||
"url": "https://github.com/eepykith",
|
||||
"commits": 0,
|
||||
"issues": 3,
|
||||
"prs": 0,
|
||||
"total": 9
|
||||
},
|
||||
{
|
||||
"login": "m4ddh7jm7c-bit",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/338352097?v=4",
|
||||
@@ -227,18 +245,18 @@
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "nickwilde77555",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/180547421?v=4",
|
||||
"url": "https://github.com/nickwilde77555",
|
||||
"login": "puppet-head",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/8073775?v=4",
|
||||
"url": "https://github.com/puppet-head",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "puppet-head",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/8073775?v=4",
|
||||
"url": "https://github.com/puppet-head",
|
||||
"login": "Sidilium",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/44266441?v=4",
|
||||
"url": "https://github.com/Sidilium",
|
||||
"commits": 0,
|
||||
"issues": 2,
|
||||
"prs": 0,
|
||||
@@ -262,6 +280,15 @@
|
||||
"prs": 1,
|
||||
"total": 6
|
||||
},
|
||||
{
|
||||
"login": "AlexanderCowell",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/225416878?v=4",
|
||||
"url": "https://github.com/AlexanderCowell",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "bcain2005-gif",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/258048498?v=4",
|
||||
@@ -334,6 +361,15 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "gnargle",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/9372593?v=4",
|
||||
"url": "https://github.com/gnargle",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "InsideCat02",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/111202797?v=4",
|
||||
@@ -389,9 +425,9 @@
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "Sidilium",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/44266441?v=4",
|
||||
"url": "https://github.com/Sidilium",
|
||||
"login": "s1lenceSRG",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/340965322?v=4",
|
||||
"url": "https://github.com/s1lenceSRG",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
@@ -415,15 +451,6 @@
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "trankillity",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/53357041?v=4",
|
||||
"url": "https://github.com/trankillity",
|
||||
"commits": 0,
|
||||
"issues": 1,
|
||||
"prs": 0,
|
||||
"total": 3
|
||||
},
|
||||
{
|
||||
"login": "TylerJaacks",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/1505374?v=4",
|
||||
|
||||
+27
-6
@@ -5,15 +5,20 @@ import { join } from 'node:path';
|
||||
import { Marked, type Tokens } from 'marked';
|
||||
import { REPO, url } from './site';
|
||||
import { fileTree } from '../scripts/filetree';
|
||||
import { patchHref } from '../scripts/patchlinks';
|
||||
|
||||
export interface DocInfo { file: string; slug: string; name: string; group: string; blurb: string; summary: string }
|
||||
export interface DocInfo {
|
||||
file: string; slug: string; name: string; group: string; blurb: string; summary: string;
|
||||
/** deepest heading level in the page's table of contents (default 3) */
|
||||
tocDepth?: number;
|
||||
}
|
||||
export const DOCS: DocInfo[] = [
|
||||
{ file: 'INSTALL.md', slug: 'install', name: 'Install and first steps', group: 'Get started', blurb: 'Download, connect the Frame, install games.',
|
||||
summary: 'Install FramePort on Windows, macOS or Linux, connect your Steam Frame once, then install and play Quest and PC VR games.' },
|
||||
{ file: 'FRAME_SETUP.md', slug: 'frame-setup', name: 'What the setup changes', group: 'Get started', blurb: 'Changes on the Frame, firewalls, undoing it.',
|
||||
summary: "What FramePort's one-time setup changes on the Steam Frame, how to undo it, and what your network and firewall need." },
|
||||
{ file: 'FAQ.md', slug: 'faq', name: 'FAQ', group: 'Get started', blurb: 'Game folders and common questions.',
|
||||
summary: "Common questions about FramePort: how to lay out game folders with APK and OBB files, and what to do when a game doesn't run." },
|
||||
{ file: 'FAQ.md', slug: 'faq', name: 'FAQ', group: 'Get started', blurb: 'Game folders and fixing problems.',
|
||||
summary: "Common questions about FramePort: how to lay out game folders with APK and OBB files, where a game's settings are, and how to fix games that don't start, hang, look wrong or stutter." },
|
||||
{ file: 'COMPATIBILITY.md', slug: 'compatibility', name: 'Compatibility', group: 'Using FramePort', blurb: 'What runs on the Frame.',
|
||||
summary: 'Which Quest games, PC VR games and Android apps run on the Steam Frame with FramePort, and how to share a recipe that works.' },
|
||||
{ file: 'THEMES.md', slug: 'themes', name: 'Themes', group: 'Using FramePort', blurb: 'Color themes and making your own.',
|
||||
@@ -22,6 +27,9 @@ export const DOCS: DocInfo[] = [
|
||||
summary: 'Add an Install with FramePort button to your download page or README, so one click installs your game on a Steam Frame.' },
|
||||
{ file: 'DIAGNOSTICS.md', slug: 'diagnostics', name: 'Diagnostics and problem reports', group: 'For developers', blurb: 'What a problem report contains.',
|
||||
summary: "What FramePort's problem reports and shared recipes contain, how personal data is removed, and how recipes reach the catalog." },
|
||||
{ file: 'PATCHES.md', slug: 'patches', name: 'Patch reference', group: 'For developers', blurb: 'Every patch and game setting.',
|
||||
summary: 'Every FramePort patch and game setting: what it does, whether it changes the APK, when it is suggested, its options and the games that use it.',
|
||||
tocDepth: 2 },
|
||||
{ file: 'ARCHITECTURE.md', slug: 'architecture', name: 'Architecture', group: 'For developers', blurb: 'How FramePort is built and what it adds.',
|
||||
summary: 'How FramePort is built: the projects it wraps, the pipeline from scan to launch test, and what it adds for the Steam Frame.' },
|
||||
{ file: 'PLAYBOOK.md', slug: 'playbook', name: 'Porting playbook', group: 'For developers', blurb: 'Symptoms and fixes from real games.',
|
||||
@@ -72,6 +80,8 @@ export function loadDoc(info: DocInfo): Doc {
|
||||
if (h1) md = md.replace(h1[0], '');
|
||||
const slug = slugger();
|
||||
const toc: Doc['toc'] = [];
|
||||
const tocDepth = info.tocDepth ?? 3;
|
||||
const linked = new WeakSet<object>(); // code spans inside a link: never a second link
|
||||
const marked = new Marked({
|
||||
gfm: true,
|
||||
walkTokens(token) {
|
||||
@@ -84,6 +94,7 @@ export function loadDoc(info: DocInfo): Doc {
|
||||
link.tokens = [{ type: 'text', raw: named.name, text: named.name }];
|
||||
}
|
||||
link.href = rewrite(link.href);
|
||||
for (const t of link.tokens ?? []) if (t.type === 'codespan') linked.add(t);
|
||||
}
|
||||
if (token.type === 'html') {
|
||||
(token as Tokens.HTML).text = (token as Tokens.HTML).text.replace(/(src|href)="([^"]+)"/g, (_, attr, v) => `${attr}="${rewrite(v)}"`);
|
||||
@@ -101,10 +112,20 @@ export function loadDoc(info: DocInfo): Doc {
|
||||
return `<figure class="diagram"><div class="diagram-scroll"><img src="${href}" alt="${alt}" loading="lazy" /></div>`
|
||||
+ '<figcaption>Scroll sideways or tap to enlarge.</figcaption></figure>';
|
||||
},
|
||||
// a heading that starts with its own anchor (<a id="…"></a>, as in PATCHES.md) keeps that id, so GitHub and the
|
||||
// site share one link target
|
||||
heading({ tokens, depth, text }) {
|
||||
const id = slug(text);
|
||||
if (depth <= 3) toc.push({ id, text: text.replace(/[`*_]/g, ''), depth });
|
||||
return `<h${depth} id="${id}"><a class="anchor" href="#${id}" aria-label="Link to this section">#</a>${this.parser.parseInline(tokens)}</h${depth}>\n`;
|
||||
const own = text.match(/^<a id="([^"]+)"><\/a>\s*/);
|
||||
const label = own ? text.slice(own[0].length) : text;
|
||||
const id = own ? own[1] : slug(text);
|
||||
if (depth <= tocDepth) toc.push({ id, text: label.replace(/[`*_]/g, ''), depth });
|
||||
const inner = this.parser.parseInline(tokens).replace(/^<a id="[^"]+"><\/a>\s*/, '');
|
||||
return `<h${depth} id="${id}"><a class="anchor" href="#${id}" aria-label="Link to this section">#</a>${inner}</h${depth}>\n`;
|
||||
},
|
||||
// a patch id in a code span (`frame.unity_text_input`) links to its entry in the patch reference
|
||||
codespan(token) {
|
||||
const href = linked.has(token) || !/^[\w.]+$/.test(token.text) ? null : patchHref(token.text);
|
||||
return href ? `<a class="patch-ref" href="${href}"><code>${token.text}</code></a>` : false;
|
||||
},
|
||||
table(token) {
|
||||
const head = token.header.map((c) => `<th>${this.parser.parseInline(c.tokens)}</th>`).join('');
|
||||
|
||||
@@ -0,0 +1,777 @@
|
||||
{
|
||||
"adapter.aim_forward": {
|
||||
"anchor": "adapter-aim_forward",
|
||||
"title": "Pointer start",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.aim_pitch": {
|
||||
"anchor": "adapter-aim_pitch",
|
||||
"title": "Pointer angle",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.aim_yaw": {
|
||||
"anchor": "adapter-aim_yaw",
|
||||
"title": "Pointer turn",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.controller_fix": {
|
||||
"anchor": "adapter-controller_fix",
|
||||
"title": "Use controllers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.controller_models": {
|
||||
"anchor": "adapter-controller_models",
|
||||
"title": "Show Steam Frame controllers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.cube_standin": {
|
||||
"anchor": "adapter-cube_standin",
|
||||
"title": "Stand in for cube layers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.cylinder_strips": {
|
||||
"anchor": "adapter-cylinder_strips",
|
||||
"title": "Show curved menus and screens",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_emul": {
|
||||
"anchor": "adapter-equirect_emul",
|
||||
"title": "Show 360° backgrounds and videos",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_face": {
|
||||
"anchor": "adapter-equirect_face",
|
||||
"title": "360° detail",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_flip": {
|
||||
"anchor": "adapter-equirect_flip",
|
||||
"title": "360° picture orientation",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_fps": {
|
||||
"anchor": "adapter-equirect_fps",
|
||||
"title": "360° video frame rate limit",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_res": {
|
||||
"anchor": "adapter-equirect_res",
|
||||
"title": "360° sharpness",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.equirect_stereo": {
|
||||
"anchor": "adapter-equirect_stereo",
|
||||
"title": "360° 3D",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.eye_debug": {
|
||||
"anchor": "adapter-eye_debug",
|
||||
"title": "Per-eye diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.flip_emul": {
|
||||
"anchor": "adapter-flip_emul",
|
||||
"title": "Fix upside-down menus",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.flip_quads": {
|
||||
"anchor": "adapter-flip_quads",
|
||||
"title": "Rotate panels 180°",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.focus_hold": {
|
||||
"anchor": "adapter-focus_hold",
|
||||
"title": "Don't pause on short interruptions",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.focus_hold_ms": {
|
||||
"anchor": "adapter-focus_hold_ms",
|
||||
"title": "Ignore focus dips up to",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.foveation_fix": {
|
||||
"anchor": "adapter-foveation_fix",
|
||||
"title": "Hide Quest foveation",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.gl_hide_msrtt": {
|
||||
"anchor": "adapter-gl_hide_msrtt",
|
||||
"title": "Hide fast anti-aliasing (OpenGL ES)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.gl_hide_multiview": {
|
||||
"anchor": "adapter-gl_hide_multiview",
|
||||
"title": "Draw each eye separately (OpenGL ES)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.gl_highp_markers": {
|
||||
"anchor": "adapter-gl_highp_markers",
|
||||
"title": "GL shim: high-precision vertex shaders",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.gl_int_attribs": {
|
||||
"anchor": "adapter-gl_int_attribs",
|
||||
"title": "GL shim: integer vertex inputs",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.gl_mv_debug": {
|
||||
"anchor": "adapter-gl_mv_debug",
|
||||
"title": "Flat panel diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.haptic_fix": {
|
||||
"anchor": "adapter-haptic_fix",
|
||||
"title": "Fix freezes when controllers vibrate",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.haptic_scale": {
|
||||
"anchor": "adapter-haptic_scale",
|
||||
"title": "Vibration strength",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.hide_space_warp": {
|
||||
"anchor": "adapter-hide_space_warp",
|
||||
"title": "Turn off space warp",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.input_diag": {
|
||||
"anchor": "adapter-input_diag",
|
||||
"title": "Controller input diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.layer_debug": {
|
||||
"anchor": "adapter-layer_debug",
|
||||
"title": "Extra diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.layer_fix": {
|
||||
"anchor": "adapter-layer_fix",
|
||||
"title": "Drop broken layers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.mutable_fix": {
|
||||
"anchor": "adapter-mutable_fix",
|
||||
"title": "Vulkan format workaround",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.ovrp_begin_gate": {
|
||||
"anchor": "adapter-ovrp_begin_gate",
|
||||
"title": "Unity frame loop: wait for skipped frames",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.ovrp_hold_physics": {
|
||||
"anchor": "adapter-ovrp_hold_physics",
|
||||
"title": "Steadier hands (older Unity)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.passthrough_emul": {
|
||||
"anchor": "adapter-passthrough_emul",
|
||||
"title": "See your room",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.pose_consistency": {
|
||||
"anchor": "adapter-pose_consistency",
|
||||
"title": "Steadier head tracking",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.pose_debug": {
|
||||
"anchor": "adapter-pose_debug",
|
||||
"title": "Pose timing diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.pose_time_fix": {
|
||||
"anchor": "adapter-pose_time_fix",
|
||||
"title": "Fix hands lagging behind controllers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.profile_remap": {
|
||||
"anchor": "adapter-profile_remap",
|
||||
"title": "Treat newer Quest controllers as Touch",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.proximity_emul": {
|
||||
"anchor": "adapter-proximity_emul",
|
||||
"title": "Animate fingers from touch",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.rect_clamp": {
|
||||
"anchor": "adapter-rect_clamp",
|
||||
"title": "Keep pictures in bounds",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.refresh_rate": {
|
||||
"anchor": "adapter-refresh_rate",
|
||||
"title": "Refresh rate",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.release_wait": {
|
||||
"anchor": "adapter-release_wait",
|
||||
"title": "Wait for the game's GPU",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.respace_kick": {
|
||||
"anchor": "adapter-respace_kick",
|
||||
"title": "Re-create play space",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scale": {
|
||||
"anchor": "adapter-scale",
|
||||
"title": "Sharpness",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scene_depth": {
|
||||
"anchor": "adapter-scene_depth",
|
||||
"title": "Room depth",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scene_emul": {
|
||||
"anchor": "adapter-scene_emul",
|
||||
"title": "Pretend room layout",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scene_height": {
|
||||
"anchor": "adapter-scene_height",
|
||||
"title": "Room height",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scene_width": {
|
||||
"anchor": "adapter-scene_width",
|
||||
"title": "Room width",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.snapshot": {
|
||||
"anchor": "adapter-snapshot",
|
||||
"title": "Eye-image snapshots (diagnostics)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.stable_local": {
|
||||
"anchor": "adapter-stable_local",
|
||||
"title": "Keep menus in place",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.strip_color_bias": {
|
||||
"anchor": "adapter-strip_color_bias",
|
||||
"title": "Drop layer color fades",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.strip_depth": {
|
||||
"anchor": "adapter-strip_depth",
|
||||
"title": "Strip depth layers",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.surface_emul": {
|
||||
"anchor": "adapter-surface_emul",
|
||||
"title": "Show video panels",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.surface_native": {
|
||||
"anchor": "adapter-surface_native",
|
||||
"title": "Native video surfaces (Batman renderer)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.swap_eyes": {
|
||||
"anchor": "adapter-swap_eyes",
|
||||
"title": "Swap eyes",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.swapchain_fix": {
|
||||
"anchor": "adapter-swapchain_fix",
|
||||
"title": "Retry refused picture formats",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.sync_guard": {
|
||||
"anchor": "adapter-sync_guard",
|
||||
"title": "Steady controller input after focus",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_hide_fdm": {
|
||||
"anchor": "adapter-vk_hide_fdm",
|
||||
"title": "Hide Vulkan foveation",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_query_slots": {
|
||||
"anchor": "adapter-vk_query_slots",
|
||||
"title": "Stop objects popping in and out",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_shader_dump": {
|
||||
"anchor": "adapter-vk_shader_dump",
|
||||
"title": "Vulkan shader dump (diagnostics)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_shader_fix": {
|
||||
"anchor": "adapter-vk_shader_fix",
|
||||
"title": "Vulkan shader fixes",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_spec_fixes": {
|
||||
"anchor": "adapter-vk_spec_fixes",
|
||||
"title": "Fix depth flicker (Unreal Vulkan)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.vk_validation": {
|
||||
"anchor": "adapter-vk_validation",
|
||||
"title": "Vulkan error checks (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.zink_shader_dump": {
|
||||
"anchor": "adapter-zink_shader_dump",
|
||||
"title": "OpenGL ES shader dump (diagnostics)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.zink_shader_fix": {
|
||||
"anchor": "adapter-zink_shader_fix",
|
||||
"title": "OpenGL ES shader fixes",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"device.config_sync": {
|
||||
"anchor": "device-config_sync",
|
||||
"title": "Keep the game's saved resolution in step",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.display_mode": {
|
||||
"anchor": "device-display_mode",
|
||||
"title": "Show as VR or as a flat window",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.files": {
|
||||
"anchor": "device-files",
|
||||
"title": "Settings files for the game",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.foveation": {
|
||||
"anchor": "device-foveation",
|
||||
"title": "Eye-tracked foveation (Valve)",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.hide_navbar": {
|
||||
"anchor": "device-hide_navbar",
|
||||
"title": "Hide Android's navigation bar",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.lepton_env": {
|
||||
"anchor": "device-lepton_env",
|
||||
"title": "Extra Lepton settings",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.steam_gamepad": {
|
||||
"anchor": "device-steam_gamepad",
|
||||
"title": "Controllers as a gamepad (Steam Input)",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"device.text_input_window": {
|
||||
"anchor": "device-text_input_window",
|
||||
"title": "Show the app's Android window (for typing)",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"frame.adapter": {
|
||||
"anchor": "frame-adapter",
|
||||
"title": "FrameBridge OpenXR adapter",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.asset_files": {
|
||||
"anchor": "frame-asset_files",
|
||||
"title": "Content files from the game's data",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.avatar_stub": {
|
||||
"anchor": "frame-avatar_stub",
|
||||
"title": "Skip Meta avatars",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.blazerush": {
|
||||
"anchor": "frame-blazerush",
|
||||
"title": "BlazeRush: controllers without Meta avatars, own shader cache",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.gl_multiview_fbo": {
|
||||
"anchor": "frame-gl_multiview_fbo",
|
||||
"title": "OpenGL ES: multiview shaders on flat panels (HUD, menus)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.gl_shim": {
|
||||
"anchor": "frame-gl_shim",
|
||||
"title": "GL shim (Mesa GLSL compatibility)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.godot_clipboard": {
|
||||
"anchor": "frame-godot_clipboard",
|
||||
"title": "Godot apps: start without a clipboard service",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.hw_video_decode": {
|
||||
"anchor": "frame-hw_video_decode",
|
||||
"title": "Hardware video decoding (H.264, HEVC, VP9)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.langpacks": {
|
||||
"anchor": "frame-langpacks",
|
||||
"title": "Language packs from the game's files",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.launcher": {
|
||||
"anchor": "frame-launcher",
|
||||
"title": "Launcher category (INFO → LAUNCHER)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.ltw_depth": {
|
||||
"anchor": "frame-ltw_depth",
|
||||
"title": "GL wrapper: OpenGL ES depth formats (Minecraft)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.meta_permissions": {
|
||||
"anchor": "frame-meta_permissions",
|
||||
"title": "Declare Meta-only permissions",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.metaxr_telemetry": {
|
||||
"anchor": "frame-metaxr_telemetry",
|
||||
"title": "Meta XR Audio: skip Quest telemetry class",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.nodebug": {
|
||||
"anchor": "frame-nodebug",
|
||||
"title": "Clear android:debuggable",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.oculusos": {
|
||||
"anchor": "frame-oculusos",
|
||||
"title": "Oculus OS telemetry class stubs",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.ovr_microphone": {
|
||||
"anchor": "frame-ovr_microphone",
|
||||
"title": "Fix crash when the game prepares the microphone",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.ovr_trace": {
|
||||
"anchor": "frame-ovr_trace",
|
||||
"title": "Trace Meta platform requests (diagnostics)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.ovrplatformcompat": {
|
||||
"anchor": "frame-ovrplatformcompat",
|
||||
"title": "Platform compat (ovrMessageType_ToString)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.ovrstubs": {
|
||||
"anchor": "frame-ovrstubs",
|
||||
"title": "Stub missing Meta platform functions",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.pac_hints": {
|
||||
"anchor": "frame-pac_hints",
|
||||
"title": "Unpaired pointer-authentication checks",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.sdl_clipboard": {
|
||||
"anchor": "frame-sdl_clipboard",
|
||||
"title": "SDL apps: start without a clipboard service",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.slz_vulkan_hooks": {
|
||||
"anchor": "frame-slz_vulkan_hooks",
|
||||
"title": "Stress Level Zero: let Unity start Vulkan itself",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.start_activity": {
|
||||
"anchor": "frame-start_activity",
|
||||
"title": "Start straight in VR (skip the 2D launcher)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.swapchain_limit": {
|
||||
"anchor": "frame-swapchain_limit",
|
||||
"title": "Allow swapchains larger than 4096 px",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.tbxr_vendor": {
|
||||
"anchor": "frame-tbxr_vendor",
|
||||
"title": "Team Beef ports: treat the Frame as a Meta headset",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_gl_shim": {
|
||||
"anchor": "frame-unity_gl_shim",
|
||||
"title": "GL shim for Unity (no multisampled render-to-texture)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_multipass": {
|
||||
"anchor": "frame-unity_multipass",
|
||||
"title": "Unity: render each eye separately (no multiview)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_no_msaa": {
|
||||
"anchor": "frame-unity_no_msaa",
|
||||
"title": "Unity: disable MSAA",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_no_overlay_copy": {
|
||||
"anchor": "frame-unity_no_overlay_copy",
|
||||
"title": "Unity: skip OVROverlay layers (fades, splash screens)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_oculus_check": {
|
||||
"anchor": "frame-unity_oculus_check",
|
||||
"title": "Unity: start VR without Meta's system apps",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_runtime_msaa_off": {
|
||||
"anchor": "frame-unity_runtime_msaa_off",
|
||||
"title": "Unity: keep MSAA off at runtime (OVRManager)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_text_input": {
|
||||
"anchor": "frame-unity_text_input",
|
||||
"title": "Make Unity text fields work",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unity_user_presence": {
|
||||
"anchor": "frame-unity_user_presence",
|
||||
"title": "Unity: headset always counts as worn",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_gl_shim": {
|
||||
"anchor": "frame-unreal_gl_shim",
|
||||
"title": "GL shim for Unreal (no multisampled render-to-texture)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_ovrp_entrypoints": {
|
||||
"anchor": "frame-unreal_ovrp_entrypoints",
|
||||
"title": "Unreal: stand-ins for OVRPlugin functions OVRPort lacks",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_quest_keymap": {
|
||||
"anchor": "frame-unreal_quest_keymap",
|
||||
"title": "Unreal: Quest controller bindings (RPOC key map)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_quest_precompile": {
|
||||
"anchor": "frame-unreal_quest_precompile",
|
||||
"title": "Unreal: don't wait for the Quest shader precompile",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_skip_obb_check": {
|
||||
"anchor": "frame-unreal_skip_obb_check",
|
||||
"title": "Unreal: start without the OBB check",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_thumb_touch": {
|
||||
"anchor": "frame-unreal_thumb_touch",
|
||||
"title": "Unreal: thumb follows the capacitive touches",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vivox_audio_route": {
|
||||
"anchor": "frame-vivox_audio_route",
|
||||
"title": "Vivox voice chat: skip Android 12 audio routing",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vk_sanitize": {
|
||||
"anchor": "frame-vk_sanitize",
|
||||
"title": "Vulkan: drop invalid render-pass pointers",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vrapi_bridge": {
|
||||
"anchor": "frame-vrapi_bridge",
|
||||
"title": "VrApi → OpenXR bridge",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vrapi_stub": {
|
||||
"anchor": "frame-vrapi_stub",
|
||||
"title": "Quiet Meta's VrApi loader (OpenXR games)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.zink_shader_fix": {
|
||||
"anchor": "frame-zink_shader_fix",
|
||||
"title": "OpenGL ES: shader fixes (Vulkan layer under Zink)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"patch_ac_nexus_no_appsw_72": {
|
||||
"anchor": "patch_ac_nexus_no_appsw_72",
|
||||
"title": "AC Nexus: no AppSW at 72 Hz",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_ac_nexus_no_appsw_90": {
|
||||
"anchor": "patch_ac_nexus_no_appsw_90",
|
||||
"title": "AC Nexus: no AppSW at 90 Hz",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_clean_up_frida": {
|
||||
"anchor": "patch_clean_up_frida",
|
||||
"title": "Clean up Frida leftovers in smali",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_copy_libraries": {
|
||||
"anchor": "patch_copy_libraries",
|
||||
"title": "Copy OVRPort libraries",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_copy_ovrplugin_vrapi": {
|
||||
"anchor": "patch_copy_ovrplugin_vrapi",
|
||||
"title": "Copy OVRPlugin if VrApi is present",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_disable_controller_offset": {
|
||||
"anchor": "patch_disable_controller_offset",
|
||||
"title": "Disable controller tracking offset",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_disable_meta_xr_audio_telemetry": {
|
||||
"anchor": "patch_disable_meta_xr_audio_telemetry",
|
||||
"title": "Disable Meta XR Audio telemetry",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_disable_space_warp": {
|
||||
"anchor": "patch_disable_space_warp",
|
||||
"title": "Disable application space warp if used",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_fix_min_android_sdk": {
|
||||
"anchor": "patch_fix_min_android_sdk",
|
||||
"title": "Fix minimal Android SDK",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_fix_unreal_crash": {
|
||||
"anchor": "patch_fix_unreal_crash",
|
||||
"title": "Fix UE4 crash with Unity stub",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_force_passthrough": {
|
||||
"anchor": "patch_force_passthrough",
|
||||
"title": "Force enable passthrough",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_generate_config": {
|
||||
"anchor": "patch_generate_config",
|
||||
"title": "Generate OVRPort config",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_launcher_entry": {
|
||||
"anchor": "patch_launcher_entry",
|
||||
"title": "Fix launcher icon entry",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_mark_allow_backup": {
|
||||
"anchor": "patch_mark_allow_backup",
|
||||
"title": "Mark application to allow backup",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_mark_as_debuggable": {
|
||||
"anchor": "patch_mark_as_debuggable",
|
||||
"title": "Mark application as debuggable",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_meta_xr_audio": {
|
||||
"anchor": "patch_meta_xr_audio",
|
||||
"title": "Patch Meta XR Audio",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_oculus_unity": {
|
||||
"anchor": "patch_oculus_unity",
|
||||
"title": "Patch Oculus detection for Unity",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_oculus_unreal": {
|
||||
"anchor": "patch_oculus_unreal",
|
||||
"title": "Patch Oculus detection for Unreal",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_remove_localized_names": {
|
||||
"anchor": "patch_remove_localized_names",
|
||||
"title": "Remove localized app names",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_remove_unreal_force_quit": {
|
||||
"anchor": "patch_remove_unreal_force_quit",
|
||||
"title": "Remove Unreal's ForceQuit",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_remove_uses_library": {
|
||||
"anchor": "patch_remove_uses_library",
|
||||
"title": "Remove uses-library",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_remove_vrapi": {
|
||||
"anchor": "patch_remove_vrapi",
|
||||
"title": "Remove VrApi library",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_replace_icon_label": {
|
||||
"anchor": "patch_replace_icon_label",
|
||||
"title": "Replace application label and icon",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_vr_metadata": {
|
||||
"anchor": "patch_vr_metadata",
|
||||
"title": "Pico/YVR/Quest metadata",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"patch_vrapi_openxr": {
|
||||
"anchor": "patch_vrapi_openxr",
|
||||
"title": "VrApi → OpenXR adapter (OVRPort)",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"pcvr.launch_args": {
|
||||
"anchor": "pcvr-launch_args",
|
||||
"title": "Launch options (SteamVR or OpenXR)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.libovr_redirect": {
|
||||
"anchor": "pcvr-libovr_redirect",
|
||||
"title": "Provide the Oculus runtime from Revive (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.no_crash_reporter": {
|
||||
"anchor": "pcvr-no_crash_reporter",
|
||||
"title": "No Unreal crash reporter",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.oculus_unreal": {
|
||||
"anchor": "pcvr-oculus_unreal",
|
||||
"title": "Patch Oculus detection for Unreal (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.proton_env": {
|
||||
"anchor": "pcvr-proton_env",
|
||||
"title": "Extra Proton settings (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.proton_log": {
|
||||
"anchor": "pcvr-proton_log",
|
||||
"title": "Proton debug log (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.proton_tool": {
|
||||
"anchor": "pcvr-proton_tool",
|
||||
"title": "Proton version (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.repack_launcher": {
|
||||
"anchor": "pcvr-repack_launcher",
|
||||
"title": "Use the repack's launcher (bundled Revive)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.revive": {
|
||||
"anchor": "pcvr-revive",
|
||||
"title": "Revive (Oculus LibOVR → OpenXR)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.revive_openvr": {
|
||||
"anchor": "pcvr-revive_openvr",
|
||||
"title": "Revive: SteamVR (OpenVR) backend on PC",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.steamvr_tuning": {
|
||||
"anchor": "pcvr-steamvr_tuning",
|
||||
"title": "Automatic SteamVR performance settings (this PC)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.vr_overlay": {
|
||||
"anchor": "pcvr-vr_overlay",
|
||||
"title": "SteamVR overlay app (Frame)",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
},
|
||||
"pcvr.xr_timefix": {
|
||||
"anchor": "pcvr-xr_timefix",
|
||||
"title": "Frame OpenXR compatibility layer",
|
||||
"group": "PC VR (Proton and Revive)"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,29 @@
|
||||
{
|
||||
"updated": "2026-10-10",
|
||||
"updated": "2026-10-11",
|
||||
"releases": [
|
||||
{
|
||||
"tag": "v1.0.1",
|
||||
"name": "FramePort v1.0.1",
|
||||
"date": "2026-10-11",
|
||||
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/v1.0.1",
|
||||
"dev": false,
|
||||
"notes": "FramePort v1.0.1: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n- **Diagnostics for games that hang:** save diagnostics while the game is stuck, and the zip shows what the game is waiting for.\n- **Clearer launch tests:** when Lepton doesn't start a game, the test says so instead of reporting that the game quit itself.\n- A reference of every patch and game setting is now in the docs.\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n## Install\n\nDownload the file for your PC, unpack it and start FramePort. Guide:\n[Install and first steps](https://frameport.app/docs/install/).\n"
|
||||
},
|
||||
{
|
||||
"tag": "v1.0.0",
|
||||
"name": "FramePort v1.0.0",
|
||||
"date": "2026-10-11",
|
||||
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/v1.0.0",
|
||||
"dev": false,
|
||||
"notes": "FramePort v1.0.0: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n- **A new look:** Portal and Original themes, an \"On your Frame\" shelf in the Library, and install progress shown as the game passing through a portal.\n- **Easier setup:** run `curl -fsSL https://frameport.app/s | bash` on the Frame and allow it on the PC. A Frame in Developer Mode pairs without a click.\n- **SteamVR overlay apps** (for example Temporal Reality) run on the Frame over your games and can start with SteamVR.\n- **More folders just work:** SideQuest backups and AXRB downloads import with their data files.\n- **Rename any game**, and the name in Steam follows.\n- **Hardware video decoding** for games that play videos (opt-in per game), a Steam Input gamepad for flat Android apps, and Godot games start.\n- **BlazeRush works** (fixes from a tester's guide), and Unreal games no longer hang on their OBB check.\n- **Play sessions are checked too:** after you play, FramePort reads the session's log and suggests fixes on the game's page.\n- **126 game recipes** (76 in 0.12.0), and fixes for Vader Immortal, VR4, Doom3Quest, Star Wars Pinball, Green Hell VR, Budget Cuts and more.\n- The update dialog lists every change since your version. Install buttons now use frameport.app links, and game reports are titled \"[Game report]\".\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n## Install\n\nDownload the file for your PC, unpack it and start FramePort. Guide:\n[Install and first steps](https://frameport.app/docs/install/).\n"
|
||||
},
|
||||
{
|
||||
"tag": "dev",
|
||||
"name": "FramePort dev build 0.12.1.dev253",
|
||||
"date": "2026-10-10",
|
||||
"name": "FramePort dev build 0.12.1.dev263",
|
||||
"date": "2026-10-11",
|
||||
"url": "https://github.com/spoopyghosty0/frameport/releases/tag/dev",
|
||||
"dev": true,
|
||||
"notes": "Dev build **0.12.1.dev253** from commit ca14fad (2026-10-10). For testing fixes before\nthe next release; you get that release as a normal update.\n\n## Please test\n\nControllers as a gamepad for flat Android apps (#162, opt-in); Vulkan shader capture for GPU hangs (vk_shader_dump, #140); Update on Frame after adding a newer APK (#161); 15 new catalog entries.\n\n## Changes since v0.12.0\n\n- Steam Input gamepad for 2D Android apps (device.steam_gamepad, agent v72, GitHub #162)\n- Vulkan shim: vk_shader_dump writes the game's SPIR-V modules with a creation index (GitHub #140)\n- Triage: unity-data-missing fails a launch test when Unity can't read the game's data files (incomplete OBB or one from another version; Batman passed while st\n- Game page shows Update on Frame after a newer APK of an installed game was added (builds record the APK version they were made from; GitHub #161); triage only\n- Catalog: Eleven (Quest) and GOLF+ unsupported (Meta online services / the game's login refuse the platform), Beat Saber DLC isn't detected, Doom3Quest works (\n- Catalog: 13 shared working configs (Freedom, Zombieland VR, Caves, Ancient Dungeon, Containment Protocol, Down the Rabbit Hole, End Space, ExploreVR, Racket: \n- Shared video decoder (codec revision 8): hidden VP9 frames no longer end the EOS drain early\n- Shared video decoder: remove a game's old per-game codec folder once its launcher is converted, also when the recipe (not deployment.json) asks for hardware d\n- Shared video decoder: per game (frame.hw_video_decode) with a global off switch; 8K retry at start and VP9 fixes from the hw-video-decode branch\n- Catalog: Audica works (shared working config)\n- Uninstall dialog: option to also delete the game's saves and everything it stored on the Frame (mods, downloads); they survived every uninstall before (GitHub\n- Install on this PC works on Windows again: the agent (loaded for its VDF code) imported the POSIX-only fcntl at the top (ModuleNotFoundError for every PC VR i\n- Deploy shared hardware video decoding for compatible Lepton apps\n- Codec wrapper (PR #96): find the host's podman when Lepton calls it with the Android guest's PATH (its boot-wait, app-pid and logcat-mirror podman exec calls \n- Catalog: Doom3Quest uses frame.gl_multiview_fbo (HUD/PDA shown, no frame-rate drop: Xandrix1987's headset test, GitHub #77)\n- Catalog: cubism works (shared working config)\n- SUPERHOT VR (Quest): create its cloud save folder at install, so the launcher gives it group write permission before the very first start (the game creates an\n- Catalog: Vader Immortal III's campaign intro starts black by design (owner)\n- Catalog: Vader Immortal Episodes II and III work (owner's headset test) with Episode I's Unreal fixes (quest precompile, key map, thumb touch, pose_time_fix),\n- Docs: session triage (PLAYBOOK rows for the session findings, CLAUDE.md note incl. the pac_hints survey)\n- Triage real play sessions: the connection refresh fetches a finished session's log (agent v70) and triages it (launch-test signatures + new space-warp-used qu\n- Agent v70: session_log (the newest play session's launch.log sliced to 4 MB + its crash logcat + kernel GPU hang lines), last_play in list_installed, launch t\n- FrameBridge: always log the runtime's focus losses and returns with their length (focus: lost / back after N ms), before focus_hold hides a dip; for session t\n- Heuristics: suggest pose_time_fix for Unreal 4 games with Vader Immortal's Oculus input (the unreal_thumb_touch code match). Catalog: Phantom, Robo Recall and\n- Harden codec deployment and isolate native video hooks\n- Catalog: Clockwork (Quest), Beat Saber, Eleven: Table Tennis VR and Pistol Whip (PC VR, run directly without Revive) from shared working configs\n- Heuristics: suggest scene_emul for every game that declares Meta's USE_SCENE permission, not only mixed-reality-only ones (VR HOT's room setup retried forever\n- Agent v68: launch tests count their window from Lepton's 'Waiting for app' (up to 240 s more for boot + install). The first start after an APK change installs\n- frame.unreal_thumb_touch: UE4 OculusInput's ThumbUp from the capacitive touches instead of near-touch, which the Frame never reports (thumbs always pointed up\n- pose_time_fix: never move a recent predicted display time; monotonic times only told apart when the clocks are more than 4 display periods apart and the time \n- Lint: extraneous parentheses (ruff UP034)\n- Game page: a patch the build left out because the OVRPort runtime already has the fix (build.superseded, e.g. haptic_fix with runtime 3.4.3-aa54c3f) no longer\n- Triage: Lepton's own short lines survive the game-process filter. With the package known, game_lines dropped every line of 3 words or fewer, so Lepton's 'Boot\n- Catalog: Max Mustard is listed by its name, not its package name Matilda (GitHub #110)\n- frame.gl_multiview_fbo: single-view twins for multiview programs drawn into flat framebuffers Line truncated
|
||||
"notes": "Dev build **0.12.1.dev263** from commit dccdcaa (2026-10-11). For testing fixes before\nthe next release; you get that release as a normal update.\n\n## Please test\n\n1.0 release candidate. New since dev261: SteamVR overlay apps (for example Temporal Reality's Linux arm64 build) installed with Add a Linux app run on the Frame over your games, with a Start with SteamVR switch.\n\n## Changes since v0.12.0\n\n- SteamVR overlay apps on the Frame (agent v75)\n- Sharing: fall back to a plain issue when the form opens empty (#167)\n- Monitor: per-module collection needs agent 73 (main's 72 lacks it); correct agent versions in comments\n- serialize_flet_updates: tolerate Flet versions without Session.patch_control or Prop (log, keep the rest)\n- Issue forms: quote the descriptions so the templates are valid YAML\n- Auto pairing scan: skip container/VM/VPN interfaces and CGNAT, back off after empty scans\n- pyproject: Development Status 5 - Production/Stable\n- Catalog: Vacation Simulator starts; controllers aren't tracked yet (shared working config)\n- Catalog: The Thrill Of The Fight runs with Unity's overlay copy skipped; the right eye misses the opponent and referee (shared working config)\n- Catalog: Fruit Salon works (shared working config)\n- Auto pairing: hold off only while a setup request is open (not for allowed, denied or expired ones)\n- Docs: diagrams instead of ASCII art\n- Install links: FramePort's https://frameport.app/install?… page is the documented button link (pasted links parse too); README, docs, site and in-app texts \n- Pairing: setup requests expire when the Frame stops polling; setup.sh explains HTTP 429\n- Diagnostics: send every shader module of the newest session (#140)\n- CLI frame pair: don't announce the pairing server over mDNS (it can't answer setup-URL requests)\n- Issue reports: a game's report is titled \"[Game report] <game>\" with the game-compatibility label; reports about FramePort itself \"[App] FramePort\" with bug (\n- Setup line: curl -fsSL https://frameport.app/s (fail on HTTP errors, explicit https)\n- Site: Cloudflare and Google analytics, with a cookie choice\n- Docs: update screenshots and videos\n- Site: SEO for frameport.app\n- Site: What's new page and section from GitHub releases\n- Site: shorter, consistent copy, served at frameport.app\n- Docs: shorter, consistent copy\n- App: shorter, consistent interface text\n- Developer docs, CLI and launch-test diagnoses: consistent terms\n- Steam Input gamepad for 2D Android apps (device.steam_gamepad, agent v72, GitHub #162)\n- Wording: style word list and length rules\n- Vulkan shim: vk_shader_dump writes the game's SPIR-V modules with a creation index (GitHub #140)\n- Triage: unity-data-missing fails a launch test when Unity can't read the game's data files (incomplete OBB or one from another version; Batman passed while st\n- Game page shows Update on Frame after a newer APK of an installed game was added (builds record the APK version they were made from; GitHub #161); triage only\n- Catalog: Eleven (Quest) and GOLF+ unsupported (Meta online services / the game's login refuse the platform), Beat Saber DLC isn't detected, Doom3Quest works (\n- Catalog: 13 shared working configs (Freedom, Zombieland VR, Caves, Ancient Dungeon, Containment Protocol, Down the Rabbit Hole, End Space, ExploreVR, Racket: \n- Site: two neon worlds, a portal-first download page, one-eye live view\n- Game page: open the last launch test's log\n- Site: interactive docs, a download page, two more worlds, solid rocks\n- Site: three hero worlds, a 3D live view, a closer app trial, real screenshots\n- Install walkthrough: the setup line and Allow, in the video and on the site\n- Pair Frames in Developer Mode without a click on the PC\n- Shared video decoder (codec revision 8): hidden VP9 frames no longer end the EOS drain early\n"
|
||||
},
|
||||
{
|
||||
"tag": "v0.12.0",
|
||||
|
||||
@@ -104,7 +104,7 @@ const count = (n: number) => (n === 1 ? '1 change' : `${n} changes`);
|
||||
{RELEASES.length === 0 && <p class="lead">No releases yet. See <a href={`${REPO}/releases`}>GitHub</a>.</p>}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<Footer credits="contributors" />
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -45,6 +45,8 @@ const prev = DOCS[i - 1], next = DOCS[i + 1];
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* long paths and identifiers (the patch reference has many) wrap instead of widening the page on phones */
|
||||
.prose :global(:is(p, li, blockquote)) { overflow-wrap: break-word; }
|
||||
.crumb { font-size: 13px; color: var(--text-2); margin-bottom: 10px; }
|
||||
.crumb a { color: var(--text-2); }
|
||||
.crumb span { margin: 0 4px; color: var(--text-3); }
|
||||
|
||||
@@ -60,5 +60,5 @@ const logGames = games.filter((g) => g.status !== 'unsupported' && g.platform ==
|
||||
<Install />
|
||||
<WhatsNew />
|
||||
</main>
|
||||
<Footer />
|
||||
<Footer credits="all" />
|
||||
</Layout>
|
||||
@@ -32,3 +32,12 @@ test('rows escape what comes from GitHub', () => {
|
||||
assert.ok(html.includes('<img'));
|
||||
assert.ok(html.includes('data-engine=""x"'));
|
||||
});
|
||||
|
||||
test('recipe chips link to the patch reference when a link is given', () => {
|
||||
const g = toGame({ title: 'A', status: 'works', frame: ['frame.x', 'frame.unknown'] });
|
||||
const link = (id) => (id === 'frame.x' ? '/docs/patches/#frame-x' : null);
|
||||
const html = rowHtml(g, false, link);
|
||||
assert.match(html, /<a href="\/docs\/patches\/#frame-x" [^>]*><code>frame\.x<\/code><\/a>/);
|
||||
assert.match(html, /<code>frame\.unknown<\/code>/);
|
||||
assert.doesNotMatch(rowHtml(g), /<a /);
|
||||
});
|
||||
@@ -71,13 +71,21 @@ export function sortGames(games: Game[]): Game[] {
|
||||
|
||||
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
||||
|
||||
export function rowHtml(g: Game, showPackage = false): string {
|
||||
/** Where a recipe chip links to (its entry in the patch reference), or null for none. */
|
||||
export type PatchLink = (id: string) => string | null;
|
||||
|
||||
const chip = (p: string, link?: PatchLink) => {
|
||||
const href = link?.(p);
|
||||
return href ? `<a href="${esc(href)}" title="What this patch does"><code>${esc(p)}</code></a>` : `<code>${esc(p)}</code>`;
|
||||
};
|
||||
|
||||
export function rowHtml(g: Game, showPackage = false, link?: PatchLink): string {
|
||||
const s = STATUS[g.status];
|
||||
const facts = [g.version && `tested with version ${g.version}`, g.app && `checked with FramePort ${g.app}`, g.checked]
|
||||
.filter(Boolean).join(' · ');
|
||||
const more = `<div class="more" role="cell" hidden>`
|
||||
+ (g.details ? `<p>${esc(g.details)}</p>` : '<p>Runs with the default patches; nothing special needed.</p>')
|
||||
+ (g.patches.length ? `<p class="pt"><span>Recipe</span>${g.patches.map((p) => `<code>${esc(p)}</code>`).join('')}</p>` : '')
|
||||
+ (g.patches.length ? `<p class="pt"><span>Recipe</span>${g.patches.map((p) => chip(p, link)).join('')}</p>` : '')
|
||||
+ (facts ? `<p class="fx">${esc(facts)}</p>` : '') + '</div>';
|
||||
return `<li class="row" role="row" tabindex="0" aria-expanded="false" data-status="${g.status}" data-engine="${esc(g.engine)}" `
|
||||
+ `data-text="${esc(`${g.title} ${g.engine} ${g.xr} ${g.platform} ${g.patches.join(' ')}`.toLowerCase())}">`
|
||||
@@ -92,11 +100,11 @@ export function rowHtml(g: Game, showPackage = false): string {
|
||||
}
|
||||
|
||||
/** All rows; games that share a title and platform (e.g. two builds) show their package to tell them apart. */
|
||||
export function rowsHtml(games: Game[]): string {
|
||||
export function rowsHtml(games: Game[], link?: PatchLink): string {
|
||||
const seen = new Map<string, number>();
|
||||
const key = (g: Game) => `${g.title.toLowerCase()}|${g.platform}`;
|
||||
for (const g of games) seen.set(key(g), (seen.get(key(g)) ?? 0) + 1);
|
||||
return games.map((g) => rowHtml(g, seen.get(key(g))! > 1)).join('');
|
||||
return games.map((g) => rowHtml(g, seen.get(key(g))! > 1, link)).join('');
|
||||
}
|
||||
|
||||
export function counts(games: Game[]): Record<Status, number> {
|
||||
|
||||
@@ -25,10 +25,12 @@ function cursorLight() {
|
||||
document.addEventListener('pointerleave', () => light.classList.remove('on'));
|
||||
}
|
||||
|
||||
/** Clicking empty space: the app's Play ripple, a blue ring then an orange one. */
|
||||
/** Clicking empty space: the app's Play ripple, a blue ring then an orange one. Mouse (and pen) only: on a touch
|
||||
* screen every tap or scroll start would draw it. */
|
||||
function clickRipples() {
|
||||
if (reduce) return;
|
||||
addEventListener('pointerdown', (e) => {
|
||||
if (e.pointerType === 'touch') return;
|
||||
const t = e.target as Element;
|
||||
if (t.closest('a, button, input, select, textarea, label, summary, [data-mock], [data-crossing], canvas, [role="button"], .row, [tabindex]')) return;
|
||||
for (const [i, cls] of ['b', 'o'].entries()) {
|
||||
@@ -129,17 +131,35 @@ export function countUp(root: ParentNode = document) {
|
||||
root.querySelectorAll<HTMLElement>('[data-count]').forEach((el) => io.observe(el));
|
||||
}
|
||||
|
||||
/** Scrolling away from the top flies the hero toward its portal; the hero's own scene leans in. */
|
||||
/** Scrolling away flies the hero toward its portal; the hero's own scene leans in (--sp, 0..1, from the page's
|
||||
* scroll over the hero's height). Stacked (phones, narrow windows) the scene sits below the text: measured that way it
|
||||
* was already blurred and enlarged when it came into view and unreadable in the middle of the screen, so there it
|
||||
* follows its own position: 0 until its middle rises above 40 % of the screen's height, 1 once it has left at the top. */
|
||||
function heroScroll() {
|
||||
const hero = document.querySelector<HTMLElement>('.hero');
|
||||
if (!hero || reduce) return;
|
||||
const copy = hero?.querySelector<HTMLElement>('.copy');
|
||||
const scene = hero?.querySelector<HTMLElement>('.scene');
|
||||
if (!hero || !copy || !scene || reduce) return;
|
||||
const stacked = matchMedia('(max-width: 900px)'); // Hero.astro's one-column breakpoint
|
||||
let raf = 0;
|
||||
const update = () => {
|
||||
raf = 0;
|
||||
const p = Math.min(1, Math.max(0, scrollY / Math.max(1, hero.offsetHeight)));
|
||||
hero.style.setProperty('--sp', p.toFixed(3));
|
||||
const page = Math.min(1, Math.max(0, scrollY / Math.max(1, hero.offsetHeight)));
|
||||
copy.style.setProperty('--sp', page.toFixed(3));
|
||||
let p = page;
|
||||
if (stacked.matches) {
|
||||
// the layout box, not the transformed one (the transform is what --sp drives)
|
||||
const parent = (scene.offsetParent as HTMLElement | null) ?? document.body;
|
||||
const centre = parent.getBoundingClientRect().top + scene.offsetTop + scene.offsetHeight / 2;
|
||||
// counting starts a little above the middle of the screen (fully sharp around the middle), or where the scene
|
||||
// sits at the top of the page if that's higher, so it never starts out blurred
|
||||
const start = Math.min(innerHeight * 0.4, centre + scrollY);
|
||||
p = Math.min(1, Math.max(0, (start - centre) / (start + scene.offsetHeight / 2)));
|
||||
}
|
||||
scene.style.setProperty('--sp', p.toFixed(3));
|
||||
};
|
||||
addEventListener('scroll', () => { if (!raf) raf = requestAnimationFrame(update); }, { passive: true });
|
||||
addEventListener('resize', update);
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Patch ids → their entry in the patch reference (docs/PATCHES.md → /docs/patches/). The map is written by
|
||||
// scripts/patch_docs.py next to the markdown, so the site needs no Python to build.
|
||||
import map from '../data/patches.json';
|
||||
import { url } from '../data/site';
|
||||
|
||||
export interface PatchInfo { anchor: string; title: string; group: string }
|
||||
const PATCHES = map as Record<string, PatchInfo>;
|
||||
/** The patch reference page itself. */
|
||||
export const PATCH_DOC = url('docs/patches/');
|
||||
|
||||
/** The registry id behind a recipe reference: "overport.patch_x" (the board's OVRPort prefix) → "patch_x", lepton_env
|
||||
* keys ("env.X") → device.lepton_env, "adapter.scale=0.85" → "adapter.scale". */
|
||||
export function patchId(ref: string): string {
|
||||
const id = ref.trim().split('=')[0];
|
||||
if (id.startsWith('overport.')) return id.slice('overport.'.length);
|
||||
if (id.startsWith('env.')) return 'device.lepton_env';
|
||||
return id;
|
||||
}
|
||||
|
||||
export function patchInfo(ref: string): PatchInfo | undefined {
|
||||
return PATCHES[patchId(ref)];
|
||||
}
|
||||
|
||||
/** The site link to a patch's entry, or null for an id the reference doesn't know. */
|
||||
export function patchHref(ref: string): string | null {
|
||||
const info = patchInfo(ref);
|
||||
return info ? `${PATCH_DOC}#${info.anchor}` : null;
|
||||
}
|
||||
@@ -213,6 +213,7 @@ code, kbd, pre { font-family: var(--font-mono); }
|
||||
.click-ring { position: fixed; z-index: 95; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; pointer-events: none;
|
||||
border: 1px solid var(--secondary); opacity: 0; animation: click-ring .55s var(--ease-out) both; }
|
||||
.click-ring.o { border-color: var(--accent); }
|
||||
@media (hover: none) { .click-ring { display: none; } }
|
||||
@keyframes click-ring { 0% { opacity: .45; transform: scale(1); } 100% { opacity: 0; transform: scale(2.6); } }
|
||||
|
||||
/* headings: words rise in, the pointer lights them in the portal's colours */
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.0.1"
|
||||
@@ -1710,3 +1710,39 @@ def test_two_revive_games_keep_their_own_shortcuts(monkeypatch, tmp_path):
|
||||
assert again == one
|
||||
names = sorted(v["appname"] for v in a.vdf_decode(vdf.read_bytes())["shortcuts"].values())
|
||||
assert names == ["Game One", "Game Two"]
|
||||
|
||||
|
||||
def test_game_threads_reports_stuck_threads(monkeypatch, tmp_path):
|
||||
"""GitHub #157: diagnostics saved while a game hangs show what each thread waits in."""
|
||||
a = load_agent(monkeypatch, tmp_path)
|
||||
proc = tmp_path / "proc"
|
||||
|
||||
def task(pid, tid, comm, ticks, wchan, syscall):
|
||||
t = proc / str(pid) / "task" / str(tid)
|
||||
t.mkdir(parents=True)
|
||||
(t / "stat").write_text(f"{tid} ({comm}) S 1 1 1 0 -1 0 0 0 0 0 {ticks} 0 0 0 20 0 1 0")
|
||||
(t / "comm").write_text(comm + "\n")
|
||||
(t / "wchan").write_text(wchan)
|
||||
(t / "syscall").write_text(syscall)
|
||||
|
||||
(proc / "200").mkdir(parents=True)
|
||||
(proc / "200" / "cmdline").write_text("com.example.game\0")
|
||||
task(200, 200, "GameThread", 50, "futex_wait_queue", "98 0x1 0x2")
|
||||
task(200, 201, "RenderThread", 10, "do_epoll_wait", "22 0x1")
|
||||
(proc / "200" / "net").mkdir()
|
||||
(proc / "200" / "net" / "tcp").write_text(
|
||||
" sl local_address rem_address st\n"
|
||||
" 0: 0100007F:1F90 0B01A8C0:01BB 02 00000000:00000000\n")
|
||||
(proc / "300").mkdir()
|
||||
(proc / "300" / "cmdline").write_text("com.other\0")
|
||||
|
||||
def sleep(_): # GameThread uses 1 s of CPU between the two samples
|
||||
(proc / "200" / "task" / "200" / "stat").write_text(
|
||||
"200 (GameThread) R 1 1 1 0 -1 0 0 0 0 0 150 0 0 0 20 0 1 0")
|
||||
|
||||
out = a.game_threads("com.example.game", proc=str(proc), samples=2, gap=1, sleep=sleep)
|
||||
assert "com.example.game (pid 200, 2 threads)" in out
|
||||
game = next(ln for ln in out.splitlines() if "GameThread" in ln)
|
||||
assert "futex" in game and "futex_wait_queue" in game and f"{100 * 1000 // a.CLK_TCK}" in game
|
||||
assert "epoll_pwait" in out and "SYN_SENT" in out and "remote=local:443" in out
|
||||
assert "192.168" not in out and a.game_threads("com.missing", proc=str(proc)) is None
|
||||
@@ -0,0 +1,43 @@
|
||||
"""docs/PATCHES.md and the site's patch map are generated from the registry: they must be current."""
|
||||
import importlib.util
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _module():
|
||||
spec = importlib.util.spec_from_file_location("patch_docs", ROOT / "scripts" / "patch_docs.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def test_patch_docs_are_up_to_date():
|
||||
p = subprocess.run([sys.executable, str(ROOT / "scripts" / "patch_docs.py"), "--check"], capture_output=True,
|
||||
text=True)
|
||||
assert p.returncode == 0, p.stderr
|
||||
|
||||
|
||||
def test_every_patch_has_one_anchor_and_links_resolve():
|
||||
from frameport.patches import base
|
||||
|
||||
doc = (ROOT / "docs" / "PATCHES.md").read_text(encoding="utf-8")
|
||||
anchors = re.findall(r'<a id="([^"]+)"></a>', doc)
|
||||
assert len(anchors) == len(set(anchors))
|
||||
for p in base.all_patches():
|
||||
assert p.id.replace(".", "-") in anchors, p.id
|
||||
for target in re.findall(r"\]\(#([^)]+)\)", doc):
|
||||
assert target in anchors, target
|
||||
|
||||
|
||||
def test_suggestions_are_read_per_setting():
|
||||
from frameport.patches import base
|
||||
|
||||
mod = _module()
|
||||
reasons = [r for _, r, _ in mod.suggestions(base.get("adapter.controller_fix"))]
|
||||
assert reasons and all("Hand tracking" in r for r in reasons)
|
||||
assert mod.suggestions(base.get("adapter.swap_eyes")) == []
|
||||
assert mod.anchor("frame.unity_text_input") == "frame-unity_text_input"
|
||||
@@ -363,10 +363,12 @@ def test_fake_pairing_server_has_what_the_gui_uses():
|
||||
broke when open_asks() was added to the real one)."""
|
||||
from showcase import fakes
|
||||
|
||||
from frameport.frame.pairing import PairingServer
|
||||
|
||||
real = {n for n in dir(PairingServer) if not n.startswith("_")}
|
||||
used = set()
|
||||
for f in (REPO / "src/frameport/ui").rglob("*.py"):
|
||||
used |= set(re.findall(r"\bpairing\.([a-z_]+)\b", f.read_text(encoding="utf-8")))
|
||||
real = {n for n in dir(__import__("frameport.frame.pairing", fromlist=["x"]).PairingServer) if not n.startswith("_")}
|
||||
for f in (REPO / "src/frameport/ui").rglob("*.py"): # any `.name` of a PairingServer method in the GUI
|
||||
used |= set(re.findall(r"\.([a-z_]+)\b", f.read_text(encoding="utf-8"))) & real
|
||||
fake = fakes.FakePairingServer()
|
||||
missing = sorted(n for n in used & real if not hasattr(fake, n))
|
||||
assert not missing, missing
|
||||
@@ -312,3 +312,21 @@ def test_unity_data_missing_fails_the_launch_test():
|
||||
"10-10 00:51:20.000 1 2 I FrameBridge: pacing: 72.0 fps\n")
|
||||
r = triage.triage(log, "RUNNING")
|
||||
assert "unity-data-missing" in [f.id for f in r.findings] and r.verdict == "fail"
|
||||
|
||||
|
||||
def test_webview_helper_exit_is_not_a_force_quit():
|
||||
"""GitHub #179: Android's WebView helper exits at every container boot; the game itself never started."""
|
||||
log = """
|
||||
10-11 05:09:26.499 220 249 I ActivityManager: Start proc 367:WebViewLoader-arm64-v8a/1037
|
||||
10-11 05:09:26.546 220 249 I ActivityManager: Start proc 397:com.android.networkstack.process/1073 for service
|
||||
10-11 05:09:26.579 367 367 I WebViewLoader-: System.exit called, status: 0
|
||||
LeptonProcessObserver: Target app (com.example.game) did not start.
|
||||
"""
|
||||
res = triage(log, "EXITED", package="com.example.game")
|
||||
ids = [f.id for f in res.findings]
|
||||
assert "force-quit" not in ids and "app-not-started" in ids
|
||||
assert "App process started" not in res.milestones
|
||||
# a real System.exit from the game still counts
|
||||
real = "09-28 17:39:01.000 1000 1000 I ActivityManager: Start proc 1150:com.example.game/u0a55 for activity\n" \
|
||||
"09-28 17:39:05.000 1150 1150 I GameActivity: System.exit called, status: 0\n"
|
||||
assert "force-quit" in [f.id for f in triage(real, "EXITED", package="com.example.game").findings]
|
||||
Reference in new issue
Block a user