mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-17 18:03:05 +02:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a71734552 | ||
|
|
3441f3ee39 | ||
|
|
b9a9757a64 | ||
|
|
f9f3a63e0c | ||
|
|
95fd2e5b0b | ||
|
|
98f91abe97 | ||
|
|
5eb6c8389d | ||
|
|
c811dbabe5 | ||
|
|
14a58443d3 | ||
|
|
e27dce210f | ||
|
|
5e38e4b46e | ||
|
|
387c0eb28c | ||
|
|
7dcb601792 | ||
|
|
3733a38e4e | ||
|
|
e1c16a4bf2 | ||
|
|
61d728fd21 | ||
|
|
122845308f | ||
|
|
451b4d6e5b | ||
|
|
c21126ec7d | ||
|
|
879bcbf9b0 | ||
|
|
351a1af31d | ||
|
|
5739646ce1 | ||
|
|
6b002d0799 | ||
|
|
6b4e50cf7c | ||
|
|
30b1c78b99 | ||
|
|
45563c5047 | ||
|
|
58489eeb75 | ||
|
|
0ce121f2dc | ||
|
|
06b289f7fc | ||
|
|
25ac577365 | ||
|
|
c9240670d4 | ||
|
|
ebda4d644a | ||
|
|
9e96fb8fb4 | ||
|
|
98786ab110 | ||
|
|
18f45702de | ||
|
|
3b71e5a560 | ||
|
|
c023ea28f7 | ||
|
|
8b1e2cdf43 | ||
|
|
633fcde50d | ||
|
|
d063bad128 | ||
|
|
90cb7b575e | ||
|
|
e1bc723f71 | ||
|
|
73839b2dda | ||
|
|
e4f9ca6bd3 | ||
|
|
aa9c605ccc | ||
|
|
607b0946da | ||
|
|
772d08a1a1 | ||
|
|
d32c49d4f5 |
No files matched your search
@@ -146,23 +146,6 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
`launcher` field, else text swap), list_installed `drive`/`drive_missing` (install_state keeps such games
|
||||
"installed"). PC: `install/drives.py`, library setting `install.drive` (Frame page → Storage), game menu
|
||||
"Move to…" (job kind tool-frame), CLI `frame drives`/`frame move`/`install --dest`. Untested on the device.
|
||||
- **Quest games on this PC via AXRB** (experimental, branch `worktree-pc-android-axrb`, docs/PC_ANDROID.md): the
|
||||
"This PC" target (`PcReviveTarget`) installs APKs through `targets/pc_android.py` + `tools/axrb.py`. AXRB
|
||||
(Android-XR-Bridge/AXRB, Windows: x86_64 Android 16 emulator + libndk_translation + its own OpenXR runtime →
|
||||
`axrb-host-bridge.exe` → SteamVR) is downloaded only on first use (`AXRB-Setup-<v>.exe /S`, SHA256SUMS, per user
|
||||
→ `%LOCALAPPDATA%\Programs\axrb-launcher`), never bundled/modified (host code: AXRB Source-Available License).
|
||||
Its runtime setup lives only in AXRB's Electron JS, so FramePort repeats it (pins read from the installed
|
||||
`app.asar` `core/components.json`, AVD `axrb-managed-api36` port 5584, adb `-P 5038`, receipts `ready.json`/
|
||||
`license-acceptance.json`, so AXRB's own launcher shares it). Spike 2026-10-10 (headless): **unconverted APKs
|
||||
abort** (Meta platform loader) → PC build = OVRPort patches + `Patch.on_pc` APK fixes (`base.pc_selection`,
|
||||
`build(pc=True)`, library `build_pc`, `pc_recipe_fingerprint`); Lucky's Tale ran a full OpenXR session in AXRB's
|
||||
runtime; Pinball FX VR crashed in gfxstream vkAllocateMemory (AXRB's layer). SteamVR can't stand in for a headset
|
||||
(null driver: no D3D11 adapter; vrlink without a Frame: no instance) → frames need the owner. Play = Steam
|
||||
shortcut `powershell.exe -File %LOCALAPPDATA%\FramePort\axrb\fp-axrb-run.ps1 -Package …` → AXRB's
|
||||
`run_windows_game.ps1`; all such shortcuts share Exe, so agent v71 `upsert/remove_shortcut(options_key=
|
||||
"-Package <pkg> ")`. AXRB scripts start through WMI (`axrb.spawn_detached`): QEMU/adb inherit any handle, and
|
||||
even /dev/null through WSL interop kept the call hanging; PowerShell 5.1 `*>>` logs are UTF-16 (`read_log`). Triage
|
||||
`kind: axrb`. Full uninstall in AXRB deletes OBB + saves; a signature change needs one.
|
||||
- **PC VR repacks are pre-patched to run directly** (proven: Rick and Morty, Vader Immortal run when the exe is
|
||||
launched directly; Revive breaks them). So Rift games default to `as_is` = install the copy unchanged and launch
|
||||
the exe directly (`pcvr.xr_timefix` for the Frame OpenXR-1.1→1.0 fix, `pcvr.no_crash_reporter` for Unreal).
|
||||
@@ -565,6 +548,61 @@ maps a symlink to the already-loaded module) gets past it to **-3021 = ovrError_
|
||||
an Oculus-signed runtime. Next step (not done): make the runtime check pass without Detours, e.g. an injected helper
|
||||
that patches the game's import table (IAT) for LoadLibrary*/WinVerifyTrust, or a prefix-level override. This is
|
||||
runtime interop (what Revive does), not the Platform SDK licence check (which FramePort never touches).
|
||||
**Rift via x86_64 Wine under FEX (research, 2026-10-08, headless on the dev Frame, SteamOS 0.4.5; no product code):**
|
||||
why Revive failed is confirmed upstream: x64 Detours returns ERROR_NOT_SUPPORTED on ARM64EC targets (microsoft/Detours
|
||||
PR #388; only an ARM64EC build of Detours can hook them). A fully x86_64 Wine avoids it: GE-Proton11-7 **x86_64** runs
|
||||
under the FEX tool (`fex-compat-tool run -- <x86 binary>`; RootFS /usr/share/guestos/fex-mesa, no FreeType there)
|
||||
— prefix boot 35 s, kernelbase.dll is x86-64. Proton's script needs help without the x86_64 SLR (app 4183110, not
|
||||
installed): its Python runs natively (the RootFS has none), so `files/bin/wine{,server}` and the two direct
|
||||
`wine-preloader` argv calls must go through a FEX wrapper that also unsets `FEX_APP_CONFIG(_LOCATION)` (Proton sets
|
||||
them on aarch64 for its own ARM64EC FEX → "RootFS path set to ''"); GE's game drive needs STEAM_COMPAT_INSTALL_PATH
|
||||
unset. SteamVR now ships `bin/linux64/vrclient.so` + `bin/vrclient.so` (i386) + `vrclient_x64.dll` (Oct 6 build; not
|
||||
there on 09-30): Proton's x86_64 vrclient bridge reaches the arm64 vrserver. **Rick and Morty ran as the scene app**
|
||||
("OpenVR initialized!", controller tracking, 9176 presents / 6 dropped, CPU 11.6 / GPU 17.9 ms, target 72).
|
||||
**ReviveInjector: "Succesfully injected!"**, LibRevive64.dll loaded next to OVRPlugin.dll in Lies Beneath, which then
|
||||
died on 0xc06d007e = delay-loaded `libovrplatform64_1.dll` (Platform SDK) before connecting to SteamVR — so Revive's
|
||||
signature hook and Revive→SteamVR are still unproven. Every Rift game in the library except Rick and Morty uses the
|
||||
Platform SDK → the Meta (Link) app in the prefix, logged in with the user's own account, is the real blocker; the
|
||||
only public attempt (github.com/michauMiau/oculus-wine-linux, 2026-10) never got OculusSetup.exe (32-bit .NET)
|
||||
past its HTTPS config fetch. Scratch files on the Frame: `~/frameport-rifttest/` (scripts t1-t4.sh).
|
||||
Meta runtime in the same Wine (2026-10-09): Platform SDK games reach OVRServer's IPC on the Frame. The apparent
|
||||
intermittent handshake hang was **OVRLibrarian.exe** (.NET, Wine Mono): under FEX it finishes its work but never exits,
|
||||
its AppTracker entry stays, and the runtime then tracks no later client (Meta app, game), so the game waits forever
|
||||
for the reply event the runtime only sets after identifying it (found with wineserver `+server` traces PC vs Frame).
|
||||
Ending OVRLibrarian before the game → game tracked, "Missing entitlement" exactly as on the PC (needs a login).
|
||||
Logged in (2026-10-09, owner's own account, browser sign-in; the `oculus://` answer had to be forwarded into the Wine
|
||||
app's `\\.\pipe\oculus`), the runtime also needed a WinRT `Windows.Devices.WiFi.WiFiAdapter` stand-in (Air Link code
|
||||
fail-fasts without it), Wine's DeviceWatcher add/remove_Updated/Removed implemented, and the C: volume name
|
||||
(`\\?\Volume{...0043}`) linked in dosdevices. Copying the logged-in `sessions/` + CoreData to the Frame prefix kept the
|
||||
login. **Oculus First Contact then ran in VR on the Frame** (Revive in OpenVR mode; `/openxr` failed in wineopenxr):
|
||||
scene app at 72 Hz target, but 92 % of frames "timed out" (game CPU-bound under FEX) → stutter/flicker/lag.
|
||||
Performance (2026-10-10, headless with patched Revive visibility + SteamVR `pauseCompositorOnStandby=false`, numbers from
|
||||
`IVRCompositor::GetFrameTimings`): the frame loop waited on sync round trips through the FEX-run x86_64 wineserver.
|
||||
`native/fexwine/`: a **native aarch64 wineserver** built from GE's exact source (protocol 938) + **ntsync** (works once
|
||||
OVRLibrarian is ended) took hitches 17 → 0-4 per 20 s. The rest were Unreal's large-block allocator committing,
|
||||
filling (one `rep stos`) and freeing 514 MB every 1-8 s: ~130k 4 KB page faults each under FEX. `fp_mem.so`
|
||||
(`FP_BIGCACHE_MB=256`, guest preload via `FEX_ENV=LD_PRELOAD=…`: fex-compat-tool deletes LD_PRELOAD) keeps that block
|
||||
→ 0 hitches, steady 36 fps + motion smoothing, ~5.5 % timed out. 72 fps would need ~30 % less GPU (game ~55 % busy
|
||||
at 36 fps); THP (fragmentation), MSAA off, Turnip gmem don't help. Also needed: Unreal's
|
||||
`Slate.DeferWindowsMessageProcessing` = 0 (exe patch; the default deadlocks Wine's IME window ~1/3 of starts).
|
||||
Headset (owner, 2026-10-10): with motion smoothing off for the game (SteamVR per-app `motionSmoothingOverride=2`:
|
||||
smoothing cost ~15 % GPU) "much much better", 95 % of frames new at 72 Hz; MSAA off (DXVK `d3d11.disableMsaa`, -25 %
|
||||
GPU) helped but the owner misses smooth edges → the game's MSAA CVar is `r.MobileMSAA` (Oculus' clustered-forward
|
||||
branch; no r.MSAACount in the exe), 2x untested. Headless tests need the headset worn once per boot (else "IMU
|
||||
fallback" and the game waits at startup forever).
|
||||
**Experimental mode on branch `rift-on-frame` (the owner's single linkable branch for this work):** CLI only,
|
||||
`FRAMEPORT_EXPERIMENTAL_FEX=1 frameport fex setup | import-login <signed-in PC prefix> [--app <meta name>] | install
|
||||
[<meta name>] [--msaa N] [--force-unverified] | status` (hidden group). Generic per game: exe + arguments from Meta's
|
||||
manifest (`Software/Manifests/<app>.json` launchFile/launchParameters), the Unreal IME-deadlock fix found in the binaries
|
||||
(`find_ime_patch`: the CVar's UTF-16 name → its `lea` xref → `lea r8` member/global → `mov [reg+disp],1` / int 1;
|
||||
also modular ApplicationCore/Slate DLLs), `static_checks` (engine, imported graphics DLLs: monolithic Unreal imports
|
||||
every RHI, so it can't tell which runs). `agent/fexrift.py` `GAMES` = verified games + per-game opt-ins only (fp_mem's
|
||||
unzeroed big-block cache, `msaa_ini`); others need `--force-unverified`. PC `src/frameport/fexrift.py` uploads `agent/fexrift.py` + `artifacts/fexrift`
|
||||
(sources native/fexwine) + Revive 3.2.0 with 6 byte patches (refused for other builds); the Frame helper downloads
|
||||
GE-Proton11-7 x86_64 (SHA-512), x86 GnuTLS (Arch archive), installs Meta's runtime (Meta's signature checks kept) and
|
||||
writes the game's launcher. Agent v79 `fex_prepare/status/setup/import_login/install`, kind `frame_fex` (uninstall
|
||||
keeps the shared prefix; launch tests refused). Everything under `~/.local/share/frameport/fexrift` (purge removes it,
|
||||
login tokens included). Not yet run end to end on the device; the login still comes from a PC prefix signed in by hand.
|
||||
Uninstall (Quest, saves kept) used to leave `deployment.json` → still "installed"; fixed (agent v16).
|
||||
|
||||
**Discovery/network:** Developer-Mode SteamOS devices announce `_steamos-devkit._tcp` (TXT `login=steamos`) — use it;
|
||||
|
||||
@@ -111,8 +111,7 @@ Out of date? [Report a problem](https://github.com/spoopyghosty0/frameport/issue
|
||||
Most of the work is done by these projects:
|
||||
[OVRPort](https://github.com/Android-XR-Bridge/OVRPort) (Quest → OpenXR, originally
|
||||
[ovrport/app](https://github.com/ovrport/app)) · Valve Lepton, Proton and SteamVR ·
|
||||
[Revive](https://github.com/LibreVR/Revive) · [AXRB](https://github.com/Android-XR-Bridge/AXRB) (Quest games on a PC,
|
||||
experimental) · Mesa (Zink) · [Khronos OpenXR SDK](https://github.com/KhronosGroup/OpenXR-SDK)
|
||||
[Revive](https://github.com/LibreVR/Revive) · Mesa (Zink) · [Khronos OpenXR SDK](https://github.com/KhronosGroup/OpenXR-SDK)
|
||||
· Eclipse Temurin, Android apksigner and NDK · [Flet](https://flet.dev) · OculusDB and Steam store data.
|
||||
What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ FramePort's own native code (the FrameBridge adapter, GL/Vulkan/OpenXR shims, th
|
||||
| Eclipse Temurin JRE | api.adoptium.net | GPL-2.0 with Classpath Exception |
|
||||
| apksigner (Android build-tools) | dl.google.com Android repository | Apache-2.0 |
|
||||
| Revive (portable copy, only if Revive isn't installed) | github.com/LibreVR/Revive releases | GPL-3.0 |
|
||||
| AXRB / Android XR Bridge (official installer, only when a Quest game is installed on this PC; run unmodified) | github.com/Android-XR-Bridge/AXRB releases | mixed: launcher GPL-3.0-or-later, host/clock code AXRB Source-Available License 1.0, other parts MIT (AXRB's `LICENSE`) |
|
||||
| Android Emulator, platform-tools, build-tools, Android 16 system image (pinned by AXRB) | dl.google.com Android repository | Android SDK License |
|
||||
|
||||
Valve's Lepton, Proton and SteamVR, and Meta's software, are used on your devices as installed by Steam / Meta;
|
||||
FramePort doesn't distribute them.
|
||||
+1308
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,133 @@
|
||||
"""Data-driven FEX game compatibility. No entitlement/runtime-signature changes; stdlib only on the Frame."""
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import struct
|
||||
|
||||
STATUSES = {"verified", "likely", "untested", "blocked"}
|
||||
GRAPHICS = {"d3d11", "d3d9", "d3d12", "opengl", "vulkan"}
|
||||
|
||||
|
||||
def exe_facts(path):
|
||||
"""Read just the PE headers. A 32-bit client still needs GE's WoW64 and a working 32-bit VR/graphics bridge."""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
head = f.read(64)
|
||||
if head[:2] != b"MZ" or len(head) < 64:
|
||||
return {}
|
||||
f.seek(struct.unpack_from("<I", head, 0x3c)[0])
|
||||
pe = f.read(26)
|
||||
if len(pe) < 26 or pe[:4] != b"PE\0\0":
|
||||
return {}
|
||||
machine = struct.unpack_from("<H", pe, 4)[0]
|
||||
return {"exe_32bit": machine == 0x14c, "exe_machine": hex(machine)}
|
||||
except (OSError, struct.error):
|
||||
return {}
|
||||
|
||||
|
||||
def sha256(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def match_profile(games, app, path):
|
||||
"""Exact canonical name OR SHA-256 OR (exe basename AND byte size). No fuzzy title matches: different store
|
||||
builds can need different routes. Multiple matches are refused rather than silently picking one."""
|
||||
hits, digest = [], None
|
||||
for key, profile in games.items():
|
||||
match = profile.get("match", {"canonical_names": [key]})
|
||||
hit = app in match.get("canonical_names", [])
|
||||
if not hit and path and os.path.isfile(path):
|
||||
hit = any(os.path.basename(path).lower() == e["name"].lower() and os.path.getsize(path) == e["size"]
|
||||
for e in match.get("executables", []))
|
||||
if not hit and match.get("sha256"):
|
||||
digest = digest or sha256(path)
|
||||
hit = digest in match["sha256"]
|
||||
if hit:
|
||||
hits.append((key, profile))
|
||||
if len(hits) > 1:
|
||||
raise ValueError("ambiguous FEX compatibility profiles: " + ", ".join(k for k, _ in hits))
|
||||
return hits[0] if hits else (None, None)
|
||||
|
||||
|
||||
def graphics_lines(route):
|
||||
if route not in GRAPHICS:
|
||||
raise ValueError(f"unknown FEX graphics route: {route}")
|
||||
# Proton owns DLL selection/copying: DXVK for D3D9/11, vkd3d-proton for D3D12. Do not override DLLs or copy
|
||||
# anything into the game. Clearing inherited flags also keeps a prior game's wined3d choice from leaking in.
|
||||
lines = ["unset PROTON_USE_WINED3D PROTON_NO_D3D12 PROTON_NO_D3D11"]
|
||||
if route == "opengl":
|
||||
lines.append("export PROTON_USE_WINED3D=1")
|
||||
lines.append(f"echo 'FramePort FEX: graphics={route}'")
|
||||
return lines
|
||||
|
||||
|
||||
def register(core):
|
||||
original_find = core.find_ime_target
|
||||
|
||||
def find_ime_target(bins):
|
||||
# Before ApplicationCore was split out, FWindowsApplication (including its deferral CVar) lived in Core.
|
||||
# Robo Recall's modular UE4 build has the SAME CVar and constructor instruction there. Search that module
|
||||
# with the existing anchored finder; never infer a member offset from bAllowedToDeferMessageProcessing.
|
||||
extra = []
|
||||
for folder in sorted({os.path.dirname(b) for b in bins}):
|
||||
extra += [os.path.join(folder, n) for n in sorted(os.listdir(folder))
|
||||
if re.search(r"(?:^|-)core(?:-win(?:32|64)-shipping)?\.dll$", n, re.I)]
|
||||
return original_find(list(dict.fromkeys(list(bins) + extra)))
|
||||
|
||||
def game_info(app, info):
|
||||
path = os.path.join(info.get("game_dir") or os.path.join(core.META_DIR, "Software", "Software", app),
|
||||
info.get("exe", ""))
|
||||
key, profile = match_profile(core.GAMES, app, path)
|
||||
if profile is None:
|
||||
return
|
||||
status = profile.get("status", "verified") # backward compatible with First Contact's original data
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"invalid FEX profile status: {status}")
|
||||
info.update({k: v for k, v in profile.items() if k != "match"})
|
||||
info.update(profile=key, status=status)
|
||||
|
||||
def launcher_env(app, info):
|
||||
lines = graphics_lines(info.get("graphics", "d3d11"))
|
||||
if info.get("fp_mem") == "enabled":
|
||||
if info.get("status") != "verified":
|
||||
raise ValueError("fp_mem requires a verified per-game opt-in")
|
||||
lines.append("export FP_MEM=1")
|
||||
lines.append("export FP_BIGCACHE_MB=" + str(int(info.get("bigcache_mb", 256))))
|
||||
# candidate is informational: never enable the unzeroed cache merely because the engine uses Binned2.
|
||||
for k, v in info.get("env", {}).items():
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z_0-9]*", k) or not isinstance(v, str):
|
||||
raise ValueError("invalid FEX profile environment")
|
||||
# with the perf extension loaded, perf_prepare writes the game's one dxvk.conf (profile lines + its settings)
|
||||
if info.get("dxvk") and "perf_prepare" not in core.COMMANDS:
|
||||
lines += ["printf '%s\\n' " + " ".join(shlex.quote(line) for line in info["dxvk"]) + ' > "$A/dxvk.conf"',
|
||||
'export DXVK_CONFIG_FILE="$A/dxvk.conf"']
|
||||
return lines
|
||||
|
||||
def checks(app, info, result):
|
||||
facts = exe_facts(result["exe"])
|
||||
facts.update(profile=info.get("profile"), profile_status=info.get("status", "untested"),
|
||||
graphics=info.get("graphics", "d3d11"), fp_mem=info.get("fp_mem", "off"))
|
||||
# frameport_agent forwards checks to the PC and deployment.json; top-level helper warnings aren't forwarded.
|
||||
if result.get("warnings"):
|
||||
facts["profile_warnings"] = result["warnings"]
|
||||
if facts.get("exe_32bit"):
|
||||
facts["wow64"] = "untested: requires GE WoW64, 32-bit graphics and SteamVR vrclient bridge under FEX"
|
||||
if facts["graphics"] == "opengl":
|
||||
facts["opengl"] = "untested: no FEX GL thunk configured; rootfs GL may use software rendering or Zink"
|
||||
if facts["graphics"] == "d3d12":
|
||||
# GE versions have used both split lib/lib64 and architecture-qualified Wine directories. Report what
|
||||
# is actually installed; the Proton script remains responsible for selecting its own layout.
|
||||
paths = [os.path.join(core.GE, "files", lib, "wine", "vkd3d-proton", tail, "d3d12.dll")
|
||||
for lib in ("lib", "lib64") for tail in ("", "x86_64-windows", "i386-windows")]
|
||||
facts["vkd3d_dlls"] = [os.path.relpath(p, core.GE) for p in paths if os.path.isfile(p)]
|
||||
return facts
|
||||
|
||||
core.GAME_INFO_HOOKS.append(game_info)
|
||||
core.find_ime_target = find_ime_target
|
||||
core.LAUNCHER_ENV_HOOKS.append(launcher_env)
|
||||
core.INSTALL_CHECK_HOOKS.append(checks)
|
||||
@@ -0,0 +1,396 @@
|
||||
{
|
||||
"_comment": [
|
||||
"FEX compatibility profiles, loaded by fexrift.py and matched by fexrift_compat.py. No entitlement changes.",
|
||||
"match: OR of canonical_names (exact Meta names), sha256 (list of exe hashes), executables (list of {name,size}, both required). Without match the key is the exact canonical name. Ambiguous matches are refused.",
|
||||
"status: verified (headset checked), likely (static evidence; install with warning), untested (force_unverified required), blocked (refuse even force). graphics: d3d11|d3d9 (DXVK), d3d12 (vkd3d-proton), opengl (PROTON_USE_WINED3D), vulkan.",
|
||||
"env: string-valued environment; args: optional launch arguments override (absent keeps manifest). dxvk: dxvk.conf lines written only to the launcher anchor. notes: evidence and first-launch checks. revive: false for direct OpenVR games, otherwise true.",
|
||||
"ime_patch: optional fallback {rva,old,new}, RVA may be hex; generic finder always wins, including old modular Core.dll. Null means no fallback, never an exemption from IME checks.",
|
||||
"fp_mem: off|candidate|enabled; candidate is informational and NEVER enables the unzeroed cache. enabled requires verified status. bigcache_mb: cache size when enabled, default 256. env FP_MEM is an explicit opt-in retained for First Contact.",
|
||||
"exe is the verified First Contact fallback only; other profiles keep the Meta manifest launch path. msaa_ini allows --msaa for that verified game only."
|
||||
],
|
||||
"oculus-first-contact": {
|
||||
"title": "Oculus First Contact",
|
||||
"exe": "TouchNUX/Binaries/Win64/TouchNUX-Win64-Shipping.exe",
|
||||
"args": [
|
||||
"-gamemode=\"experienceonly\""
|
||||
],
|
||||
"ime_patch": {
|
||||
"rva": "0x22a605",
|
||||
"old": "c7862001000001000000",
|
||||
"new": "c7862001000000000000"
|
||||
},
|
||||
"_env": "Unreal's large-block allocator gives 0.5-1.6 GB blocks back every few seconds and this game overwrites them completely: fp_mem keeps them (native/fexwine/fp_mem.c)",
|
||||
"env": {
|
||||
"FP_MEM": "1",
|
||||
"FP_BIGCACHE_MB": "256"
|
||||
},
|
||||
"msaa_ini": true,
|
||||
"status": "verified",
|
||||
"graphics": "d3d11",
|
||||
"fp_mem": "enabled",
|
||||
"bigcache_mb": 256,
|
||||
"notes": "Headset verified; r.MobileMSAA=2 prevented rendering. Other games remain unverified."
|
||||
},
|
||||
"arktika-1": {
|
||||
"title": "Arktika 1",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "arktika1.exe",
|
||||
"size": 20181504
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "graphics API unclear (?); custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; d3d11 is a trial default, not a detected API"
|
||||
},
|
||||
"asgards-wrath": {
|
||||
"title": "Asgards Wrath",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WrathGame-Win64-Shipping.exe",
|
||||
"size": 63707648
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"brass-tactics": {
|
||||
"title": "Brass Tactics",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Brass-Win64-Shipping.exe",
|
||||
"size": 52790272
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"chronos": {
|
||||
"title": "Chronos",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Chronos-Win64-Shipping.exe",
|
||||
"size": 40722432
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": "off",
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "IME fix OFF: the generic finder matched member +0x100 in this UE 4.10 build and the patched game never submitted a frame; unpatched it renders (Frame headless 2026-10-16: 202 frames / 105 s, 1.9 fps while loading, headset not yet checked). Media Foundation videos need a playback test"
|
||||
},
|
||||
"defector": {
|
||||
"title": "Defector",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "DFCTR.exe",
|
||||
"size": 16204288
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"hubris": {
|
||||
"title": "Hubris",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "HubrisVR-Win64-Shipping.exe",
|
||||
"size": 85706240
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenXR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"lies-beneath": {
|
||||
"title": "Lies Beneath",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Kodiak-Win64-Shipping.exe",
|
||||
"size": 63537664
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"lone-echo": {
|
||||
"title": "Lone Echo",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "loneecho.exe",
|
||||
"size": 22336512
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"lone-echo-2": {
|
||||
"title": "Lone Echo 2",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "loneecho2.exe",
|
||||
"size": 33436160
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d12",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "D3D12: needs vkd3d-proton on Turnip under FEX (untested); custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
},
|
||||
"rick-and-morty-virtual-rick-ality": {
|
||||
"title": "Rick and Morty - Virtual Rick-ality",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "RickAndMortyVR.exe",
|
||||
"size": 21284352
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "opengl",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "OpenGL: no FEX GL thunk configured; rootfs GL renderer unverified; OpenVR directly, no Platform SDK; guest GL and speech WinRT need a device check",
|
||||
"revive": false
|
||||
},
|
||||
"robo-recall": {
|
||||
"title": "Robo Recall",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "RoboRecall-Win64-Shipping.exe",
|
||||
"size": 198144
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR)"
|
||||
},
|
||||
"skydances-behemoth": {
|
||||
"title": "Skydances BEHEMOTH",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "BHM-Win64-Shipping.exe",
|
||||
"size": 144449024
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "candidate",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenXR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"stormland": {
|
||||
"title": "Stormland",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Stormland.exe",
|
||||
"size": 85195776
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; Media Foundation videos need a playback test"
|
||||
},
|
||||
"the-climb": {
|
||||
"title": "The Climb",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Climb.exe",
|
||||
"size": 95534080
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d9",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "D3D9: DXVK under FEX (untested); CryEngine engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; D3D9 preferred over imported GL; confirm the actual selected renderer"
|
||||
},
|
||||
"the-unspoken": {
|
||||
"title": "The Unspoken",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "Unspoken.exe",
|
||||
"size": 69496320
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown; Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-i": {
|
||||
"title": "Vader Immortal - Episode I",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 58100736
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-ii": {
|
||||
"title": "Vader Immortal - Episode II",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 59464192
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"vader-immortal-episode-iii": {
|
||||
"title": "Vader Immortal - Episode III",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WKND-Win64-Shipping.exe",
|
||||
"size": 59814912
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "likely",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "same profile as First Contact (x64, D3D11, LibOVR/OpenVR); Media Foundation videos need a playback test"
|
||||
},
|
||||
"wilsons-heart": {
|
||||
"title": "Wilsons Heart",
|
||||
"match": {
|
||||
"executables": [
|
||||
{
|
||||
"name": "WHVR.exe",
|
||||
"size": 15379968
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "untested",
|
||||
"graphics": "d3d11",
|
||||
"env": {},
|
||||
"ime_patch": null,
|
||||
"fp_mem": "off",
|
||||
"bigcache_mb": 256,
|
||||
"dxvk": [],
|
||||
"notes": "custom engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Local Rift copies in Meta libraries. Runs on the Frame, stdlib only; never touches entitlement or login data."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
import zlib
|
||||
|
||||
SEGMENT_SIZE = 10000000 # Meta's manifest format uses decimal 10 MB, not MiB
|
||||
|
||||
|
||||
def canonical(value):
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", value):
|
||||
raise ValueError("invalid Meta canonical name")
|
||||
return value
|
||||
|
||||
|
||||
def relative(value):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("invalid relative game path")
|
||||
value = value.replace("\\", "/")
|
||||
if (not value or value.startswith("/") or any(p in ("", ".", "..") for p in value.split("/"))
|
||||
or any(c in value for c in ':\x00\r\n')):
|
||||
raise ValueError("game path escapes its folder")
|
||||
return value
|
||||
|
||||
|
||||
def skipped(name):
|
||||
name = name.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
||||
return (name.endswith((".7z", ".ag")) or bool(re.search(r"\.7z\.0", name))
|
||||
or name in ("data.bin", "agrepackinstaller.exe", "vd.bat"))
|
||||
|
||||
|
||||
def confined(path, root):
|
||||
"""Reject symlink components too: an upload or delete must never follow a game-controlled link."""
|
||||
root, path = os.path.abspath(root), os.path.abspath(path)
|
||||
if os.path.commonpath([path, root]) != root:
|
||||
raise ValueError("path escapes the library")
|
||||
cursor = path
|
||||
while True:
|
||||
if os.path.islink(cursor):
|
||||
raise ValueError("symlink in local game path")
|
||||
if cursor == root:
|
||||
break
|
||||
cursor = os.path.dirname(cursor)
|
||||
if os.path.realpath(root) != root:
|
||||
raise ValueError("symlink in library root")
|
||||
return path
|
||||
|
||||
|
||||
def hash_file(path):
|
||||
total, segments = hashlib.sha256(), []
|
||||
size = 0
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(SEGMENT_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
total.update(chunk)
|
||||
# The fixture's last element is the zlib-compressed length, not the uncompressed chunk size. The first
|
||||
# element is an opaque Meta segment identifier: 0 explicitly means unknown for a local-only copy. These
|
||||
# records register existing bytes; they are NOT a CDN download/repair recipe (device validation needed).
|
||||
segments.append([0, hashlib.sha256(chunk).hexdigest(), len(zlib.compress(chunk))])
|
||||
return {"sha256": total.hexdigest(), "size": size, "segmentSize": SEGMENT_SIZE, "segments": segments}
|
||||
|
||||
|
||||
def register(core):
|
||||
def record_path(app):
|
||||
return os.path.join(core.ROOT, "local-games", canonical(app) + ".json")
|
||||
|
||||
def read_record(app):
|
||||
return core.read_json(record_path(app)) or {}
|
||||
|
||||
def save_record(app, record):
|
||||
os.makedirs(os.path.dirname(record_path(app)), exist_ok=True)
|
||||
path = record_path(app)
|
||||
with open(path + ".tmp", "w") as f:
|
||||
json.dump(record, f, indent=1)
|
||||
os.replace(path + ".tmp", path)
|
||||
|
||||
def layout(app, dest):
|
||||
app = canonical(app)
|
||||
if dest:
|
||||
# The main agent validates this against list_drives / resolve_dest on EVERY command. No fallback when a
|
||||
# drive disappears. Keep the physical game path simple while Meta sees its required Software/ layout.
|
||||
if not os.path.isabs(dest) or not os.path.isdir(dest):
|
||||
raise ValueError("destination drive is not mounted")
|
||||
base = confined(os.path.join(dest, "FramePort", "fexrift-games"), dest)
|
||||
library = confined(os.path.join(dest, "FramePort", "fexrift-library"), dest)
|
||||
guid = str(uuid.uuid5(uuid.NAMESPACE_URL, "frameport-fex-library:" + dest))
|
||||
else:
|
||||
library = os.path.join(core.META_DIR, "Software")
|
||||
base, guid = os.path.join(library, "Software"), core.LIBRARY_GUID
|
||||
return {"game_dir": confined(os.path.join(base, app), base), "library": library,
|
||||
"library_guid": guid, "dest": dest}
|
||||
|
||||
def validated_record(app):
|
||||
rec = read_record(app)
|
||||
if not rec:
|
||||
raise ValueError("no local-copy upload reservation for this game")
|
||||
expected = layout(app, rec.get("dest"))
|
||||
if any(rec.get(k) != v for k, v in expected.items()):
|
||||
raise ValueError("local-copy reservation has an invalid storage path")
|
||||
return rec
|
||||
|
||||
def prepare(args):
|
||||
app = canonical(args["app"])
|
||||
package = args["package"]
|
||||
files, seen = {}, set()
|
||||
for name, st in args["files"].items():
|
||||
rel = relative(name)
|
||||
if skipped(rel) or rel.casefold() in seen:
|
||||
raise ValueError("skipped or duplicate file in upload inventory")
|
||||
if not isinstance(st["size"], int) or st["size"] < 0 or not isinstance(st["mtime_ns"], int):
|
||||
raise ValueError("invalid source file stat")
|
||||
seen.add(rel.casefold())
|
||||
files[rel] = st
|
||||
if not files:
|
||||
raise ValueError("no game files to upload")
|
||||
old = read_record(app)
|
||||
dest = args.get("dest") or None
|
||||
loc = layout(app, dest)
|
||||
if old and (old.get("dest") != dest or old.get("package") != package):
|
||||
raise ValueError("local copy already has another package or drive; uninstall it before changing either")
|
||||
if not old and (os.path.exists(os.path.join(loc["library"], "Manifests", app + ".json"))
|
||||
or os.path.exists(loc["game_dir"])):
|
||||
raise ValueError("game already exists outside FramePort's local-copy installer")
|
||||
existing = {}
|
||||
for rel, st in files.items():
|
||||
path = confined(os.path.join(loc["game_dir"], rel), loc["game_dir"])
|
||||
confined(path + ".part", loc["game_dir"])
|
||||
if old.get("files", {}).get(rel) != st:
|
||||
# Delete completed bytes too: if this new attempt is cancelled before uploading them, the next
|
||||
# prepare must not mistake the old same-size file for a completed transfer of the new source.
|
||||
for stale in (path, path + ".part"):
|
||||
if os.path.isfile(stale):
|
||||
os.unlink(stale)
|
||||
if (old.get("files", {}).get(rel) == st and os.path.isfile(path)
|
||||
and os.path.getsize(path) == st["size"]):
|
||||
existing[rel] = st["size"]
|
||||
if old and set(old.get("files", {})) - set(files):
|
||||
raise ValueError("source removed files since the last upload; uninstall the local copy before replacing it")
|
||||
os.makedirs(loc["game_dir"], exist_ok=True)
|
||||
st = os.statvfs(loc["game_dir"])
|
||||
save_record(app, dict(old, **loc, app=app, package=package, files=files))
|
||||
return dict(loc, existing=existing, free_bytes=st.f_bavail * st.f_frsize)
|
||||
|
||||
def registry_library(rec, remove=False):
|
||||
guid, dest = rec["library_guid"], rec.get("dest")
|
||||
key = rf"HKEY_CURRENT_USER\Software\Oculus VR, LLC\Oculus\Libraries\{guid}"
|
||||
if remove:
|
||||
text = f"Windows Registry Editor Version 5.00\r\n\r\n[-{key}]\r\n"
|
||||
else:
|
||||
if dest:
|
||||
devices = os.path.join(core.PFX, "dosdevices")
|
||||
os.makedirs(devices, exist_ok=True)
|
||||
volume = os.path.join(devices, "volume{" + guid + "}")
|
||||
if os.path.lexists(volume):
|
||||
if not os.path.islink(volume) or os.path.realpath(volume) != dest:
|
||||
raise ValueError("Wine volume mapping is already in use")
|
||||
else:
|
||||
os.symlink(dest, volume)
|
||||
# Do not appropriate a user's drive letter. Reuse our mount mapping, else pick the first unused one.
|
||||
letter = next((c for c in "defghijklmnopqrstuvwxy"
|
||||
if os.path.islink(os.path.join(devices, c + ":"))
|
||||
and os.path.realpath(os.path.join(devices, c + ":")) == dest), None)
|
||||
if not letter:
|
||||
letter = next((c for c in "defghijklmnopqrstuvwxy"
|
||||
if not os.path.lexists(os.path.join(devices, c + ":"))), None)
|
||||
if not letter:
|
||||
raise ValueError("no free Wine drive letter")
|
||||
os.symlink(dest, os.path.join(devices, letter + ":"))
|
||||
rec["drive_letter"] = letter
|
||||
original = letter.upper() + ":\\FramePort\\fexrift-library"
|
||||
path = "\\\\?\\Volume{" + guid + "}\\FramePort\\fexrift-library"
|
||||
else:
|
||||
original = "C:\\Program Files\\Meta Horizon\\Software"
|
||||
path = "\\\\?\\Volume{00000000-0000-0000-0000-000000000043}\\Program Files\\Meta Horizon\\Software"
|
||||
text = "Windows Registry Editor Version 5.00\r\n\r\n"
|
||||
if not dest:
|
||||
text += (r"[HKEY_CURRENT_USER\Software\Oculus VR, LLC\Oculus\Libraries]" + "\r\n"
|
||||
+ '"DefaultLibrary"=' + core.meta_reg_str(guid) + "\r\n\r\n")
|
||||
text += (f"[{key}]\r\n" + '"OriginalPath"=' + core.meta_reg_str(original) + "\r\n"
|
||||
+ '"Path"=' + core.meta_reg_str(path) + "\r\n")
|
||||
reg = os.path.join(core.ROOT, "local-library.reg")
|
||||
core.put_text(reg, text)
|
||||
core.wine("regedit", "/S", core.winpath(reg), timeout=90)
|
||||
|
||||
def register_local(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app)
|
||||
if args.get("package") != rec["package"]:
|
||||
raise ValueError("upload belongs to another package")
|
||||
exe = relative(args["exe"])
|
||||
if exe not in rec["files"]:
|
||||
raise ValueError("launch program is not an uploaded file")
|
||||
app_id = args["app_id"]
|
||||
if not isinstance(app_id, str) or not re.fullmatch(r"[1-9][0-9]*", app_id):
|
||||
raise ValueError("invalid Meta app ID")
|
||||
files = {}
|
||||
for rel, st in rec["files"].items():
|
||||
path = confined(os.path.join(rec["game_dir"], rel), rec["game_dir"])
|
||||
if not os.path.isfile(path) or os.path.getsize(path) != st["size"]:
|
||||
raise ValueError(f"upload incomplete: {rel}")
|
||||
files[rel] = hash_file(path)
|
||||
library = rec["library"]
|
||||
os.makedirs(library, exist_ok=True)
|
||||
if rec.get("dest"):
|
||||
software = os.path.join(library, "Software")
|
||||
target = os.path.dirname(rec["game_dir"])
|
||||
if os.path.lexists(software):
|
||||
if not os.path.islink(software) or os.path.realpath(software) != target:
|
||||
raise ValueError("external Meta library Software path is already in use")
|
||||
else:
|
||||
os.symlink(target, software)
|
||||
args_list = args.get("args") or []
|
||||
if not isinstance(args_list, list) or any(not isinstance(a, str) or "\x00" in a for a in args_list):
|
||||
raise ValueError("launch arguments must be strings")
|
||||
# list2cmdline follows Windows argv quoting; Meta's own launchFile uses forward slashes. The local launcher
|
||||
# keeps the original argv list in the reservation, avoiding POSIX reinterpretation of Windows quotes.
|
||||
manifest = core.dawn_manifest({"packageType": "APP", "isCore": False, "appId": app_id,
|
||||
"canonicalName": app, "launchFile": exe,
|
||||
"launchParameters": subprocess.list2cmdline(args_list),
|
||||
"launchParameters2D": "", "version": str(args.get("version") or "0.0.0"),
|
||||
"versionCode": 1, "files": files})
|
||||
# App fixtures serialize the manifest's derived properties before the package base properties. Match that
|
||||
# order (core's runtime package writer uses base-first); both use the same defaults, CRLF and indentation.
|
||||
first = ("parentCanonicalName", "files", "dlcs", "assetFiles")
|
||||
manifest = {**{k: manifest[k] for k in first}, **{k: v for k, v in manifest.items() if k not in first}}
|
||||
manifests = os.path.join(library, "Manifests")
|
||||
os.makedirs(manifests, exist_ok=True)
|
||||
for suffix, content in ((".json", manifest), (".json.mini", dict(manifest, files={
|
||||
k: v for k, v in manifest["files"].items() if k.lower().endswith(".exe")}))):
|
||||
path = os.path.join(manifests, app + suffix)
|
||||
core.write_manifest(path + ".tmp", content)
|
||||
os.replace(path + ".tmp", path)
|
||||
registry_library(rec)
|
||||
rec.update({k: args.get(k) for k in ("app_id", "app_id_source", "store_app_id", "skipped", "title")})
|
||||
rec.update(source="local_copy", exe=exe, args=args_list, registered=True)
|
||||
save_record(app, rec)
|
||||
return local_info({"app": app})
|
||||
|
||||
def local_info(args):
|
||||
rec = validated_record(args["app"])
|
||||
return {k: rec.get(k) for k in ("source", "app_id", "app_id_source", "store_app_id", "skipped", "dest",
|
||||
"package", "game_dir", "library_guid", "registered")}
|
||||
|
||||
def unregister_local(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app)
|
||||
if args.get("package") != rec["package"]:
|
||||
raise ValueError("local copy belongs to another package")
|
||||
# Shared/default library stays. An external library is removed from the registry only after its last local
|
||||
# game goes; other games, the Wine prefix, sign-in and save directories remain intact.
|
||||
peers = [core.read_json(os.path.join(core.ROOT, "local-games", f)) or {}
|
||||
for f in os.listdir(os.path.dirname(record_path(app))) if f.endswith(".json") and f != app + ".json"]
|
||||
if rec.get("dest") and not any(p.get("library_guid") == rec["library_guid"] for p in peers):
|
||||
registry_library(rec, remove=True)
|
||||
if os.path.isdir(rec["game_dir"]):
|
||||
shutil.rmtree(rec["game_dir"])
|
||||
for suffix in (".json", ".json.mini"):
|
||||
path = os.path.join(rec["library"], "Manifests", app + suffix)
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
os.unlink(record_path(app))
|
||||
return {"removed": True}
|
||||
|
||||
def runtime_state(args):
|
||||
app = canonical(args["app"])
|
||||
rec = validated_record(app) if read_record(app) else None
|
||||
# Read only newly appended runtime log text. Never return log lines: runtime logs can contain account data.
|
||||
logdir = os.path.join(core.USER_DIR, "AppData", "Local", "Oculus")
|
||||
def logs():
|
||||
return [os.path.join(logdir, n) for n in os.listdir(logdir)
|
||||
if n.lower().endswith((".txt", ".log")) and any(
|
||||
s in n.lower() for s in ("server", "service"))] if os.path.isdir(logdir) else []
|
||||
offsets = {p: os.path.getsize(p) for p in logs()}
|
||||
librarian = os.path.join(logdir, "OVRLibrarian.log")
|
||||
lib_offset = os.path.getsize(librarian) if os.path.isfile(librarian) else 0
|
||||
probe = subprocess.run(["pgrep", "-f", "[O]VRServer_x64.exe"], capture_output=True, timeout=5)
|
||||
started, proc = probe.returncode != 0, None
|
||||
end = time.monotonic() + 95 # leave room for wineserver's bounded cleanup, total <=120 s
|
||||
state = "timeout"
|
||||
try:
|
||||
if started:
|
||||
runtime = os.path.join(core.META_DIR, "Support", "oculus-runtime")
|
||||
proc = subprocess.Popen([os.path.join(core.GEF, "files/bin/wine"), "./OVRServer_x64.exe"],
|
||||
cwd=runtime, env=core.wine_env(), stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
while time.monotonic() < end:
|
||||
if started and os.path.isfile(librarian):
|
||||
with open(librarian, errors="replace") as f:
|
||||
if os.path.getsize(librarian) < lib_offset:
|
||||
lib_offset = 0
|
||||
f.seek(lib_offset)
|
||||
done = "invocation completed" in f.read(65536).lower()
|
||||
lib_offset = f.tell()
|
||||
if done:
|
||||
subprocess.run(["pkill", "-9", "-f", "[O]VRLibrarian.exe"], capture_output=True, timeout=3)
|
||||
for path in logs():
|
||||
with open(path, errors="replace") as f:
|
||||
off = offsets.get(path, 0)
|
||||
f.seek(off if os.path.getsize(path) >= off else 0)
|
||||
text = f.read(1 << 20)
|
||||
offsets[path] = f.tell()
|
||||
for line in text.splitlines():
|
||||
if app not in line and (not rec or rec.get("app_id", "_") not in line):
|
||||
continue
|
||||
match = re.search(r"\bstate\s*:\s*(ready|install_available)\b", line, re.I)
|
||||
if match:
|
||||
state = match[1].lower()
|
||||
# Meta's PackageManager lists every registered package at start ("Currently installed
|
||||
# package: <name>, version code: N"); the "state:" line only comes later (app tracker / sign-in)
|
||||
elif state == "timeout" and re.search(r"Currently installed package:\s*" + re.escape(app) + r"\s*,",
|
||||
line):
|
||||
state = "installed"
|
||||
if state != "timeout" or (proc is not None and proc.poll() is not None):
|
||||
break
|
||||
time.sleep(1)
|
||||
finally:
|
||||
if started:
|
||||
subprocess.run([os.path.join(core.GEF, "files/bin/wineserver"), "-k"], env=core.wine_env(),
|
||||
capture_output=True, timeout=10)
|
||||
if proc is not None:
|
||||
try:
|
||||
proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return {"runtime_state": state, "reused_server": not started}
|
||||
|
||||
original_manifest = core.manifest_launch
|
||||
original_info = core.game_info
|
||||
|
||||
def manifest_launch(app):
|
||||
rec = read_record(app)
|
||||
if not rec.get("registered"):
|
||||
return original_manifest(app)
|
||||
rec = validated_record(app)
|
||||
manifest = core.read_json(os.path.join(rec["library"], "Manifests", app + ".json")) or {}
|
||||
if not manifest.get("launchFile"):
|
||||
raise ValueError("local-copy Meta manifest is missing; register the copy again")
|
||||
return {"exe": relative(manifest["launchFile"]), "args": rec["args"], "title": rec["title"],
|
||||
"game_dir": rec["game_dir"], "args_argv": True}
|
||||
|
||||
def game_info(app):
|
||||
rec = read_record(app)
|
||||
if not rec.get("registered"):
|
||||
return original_info(app)
|
||||
info = dict(core.GAMES.get(app) or {})
|
||||
info.update(manifest_launch(app))
|
||||
return info, app in core.GAMES
|
||||
|
||||
core.manifest_launch = manifest_launch
|
||||
core.game_info = game_info
|
||||
core.COMMANDS.update(prepare_local=prepare, register_local=register_local, unregister_local=unregister_local,
|
||||
local_info=local_info, runtime_state=runtime_state)
|
||||
core.INSTALL_CHECK_HOOKS.append(lambda app, info, result: {"local_copy": True} if read_record(app) else {})
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Meta's own browser sign-in, in the Frame's shared prefix. No credential files are read.
|
||||
|
||||
The broker is a separate, owner-only Unix socket process: native_sso URLs live only in its memory, never in a
|
||||
status file, launcher, journal or argv sent over SSH. Helpers and Wine subprocesses have their output discarded.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
CORE = None
|
||||
DESKTOP = "frameport-oculus.desktop"
|
||||
REGKEY = r"HKCU\Software\Wine\WineBrowser"
|
||||
MAX_URL = 65536
|
||||
|
||||
|
||||
def folder():
|
||||
return Path(CORE.ROOT) / "login"
|
||||
|
||||
|
||||
def request(message):
|
||||
with socket.socket(socket.AF_UNIX) as s:
|
||||
s.settimeout(90)
|
||||
s.connect(str(folder() / "broker.sock"))
|
||||
s.sendall(json.dumps(message).encode())
|
||||
s.shutdown(socket.SHUT_WR)
|
||||
return json.loads(receive(s))
|
||||
|
||||
|
||||
def receive(s):
|
||||
data = bytearray()
|
||||
while True:
|
||||
part = s.recv(8192)
|
||||
if not part:
|
||||
return data.decode()
|
||||
data.extend(part)
|
||||
if len(data) > MAX_URL * 2:
|
||||
raise ValueError("sign-in message too long")
|
||||
|
||||
|
||||
def valid_url(url, callback=False):
|
||||
if not isinstance(url, str) or len(url) > MAX_URL or any(ord(c) < 32 for c in url):
|
||||
return False
|
||||
try:
|
||||
u = urllib.parse.urlsplit(url)
|
||||
return (u.scheme, u.netloc, u.path) == (("oculus", "link", "/frl_login") if callback else
|
||||
("https", "auth.meta.com", "/native_sso/confirm"))
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def host_env():
|
||||
env = dict(os.environ)
|
||||
env.update(CORE.steam_display_env())
|
||||
env.update(XDG_RUNTIME_DIR=f"/run/user/{os.getuid()}",
|
||||
DBUS_SESSION_BUS_ADDRESS=f"unix:path=/run/user/{os.getuid()}/bus")
|
||||
env.pop("LD_PRELOAD", None)
|
||||
return env
|
||||
|
||||
|
||||
def quiet(argv, **kw):
|
||||
return subprocess.run(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60, **kw)
|
||||
|
||||
|
||||
def wine_env():
|
||||
env = CORE.wine_env({"ENABLE_GAMESCOPE_WSI": "0", "PROTON_NO_ZENITY": "1", "PROTONFIXES_DISABLE": "1",
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": CORE.STEAM})
|
||||
env.pop("LD_PRELOAD", None)
|
||||
env.pop("STEAM_COMPAT_INSTALL_PATH", None)
|
||||
return env
|
||||
|
||||
|
||||
def snapshot(paths):
|
||||
return {str(p): base64.b64encode(p.read_bytes()).decode() if p.exists() else None for p in paths}
|
||||
|
||||
|
||||
def restore_files(saved):
|
||||
for name, data in saved.items():
|
||||
p = Path(name)
|
||||
if data is None:
|
||||
p.unlink(missing_ok=True)
|
||||
else:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(base64.b64decode(data))
|
||||
|
||||
|
||||
def desktop_paths():
|
||||
config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
|
||||
apps = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share") / "applications"
|
||||
paths = {apps / DESKTOP, config / "mimeapps.list", apps / "mimeapps.list", apps / "defaults.list"}
|
||||
for base in (config, apps):
|
||||
paths.update(base.glob("*-mimeapps.list"))
|
||||
for desktop in os.environ.get("XDG_CURRENT_DESKTOP", "").lower().split(":"):
|
||||
if desktop and re.fullmatch(r"[a-z0-9_-]+", desktop):
|
||||
paths.add(base / f"{desktop}-mimeapps.list")
|
||||
return apps, paths
|
||||
|
||||
|
||||
def desktop_entry(helper):
|
||||
# Desktop Exec has its own quoting (single shell quotes are not valid here); %% is a literal percent.
|
||||
escaped = str(helper).replace("%", "%%")
|
||||
for c in ("\\", '"', "`", "$"):
|
||||
escaped = escaped.replace(c, "\\" + c)
|
||||
escaped = re.sub(r"[\r\n\t]", " ", escaped.replace("\\", "\\\\")) # Desktop string-value layer
|
||||
return ('[Desktop Entry]\nType=Application\nName=Meta sign-in (FramePort)\n'
|
||||
f'Exec="{escaped}" %u\nNoDisplay=true\nTerminal=false\nMimeType=x-scheme-handler/oculus;\n')
|
||||
|
||||
|
||||
def helper_script(op):
|
||||
# This tiny script never imports the core or prints exceptions (which could contain the supplied URL).
|
||||
return ("#!/usr/bin/python3 -I\nimport json,socket,sys\ntry:\n"
|
||||
" with socket.socket(socket.AF_UNIX) as s:\n"
|
||||
" s.settimeout(90)\n"
|
||||
f" s.connect({str(folder() / 'broker.sock')!r})\n"
|
||||
f" s.sendall(json.dumps({{'op': {op!r}, 'url': sys.argv[1]}}).encode())\n"
|
||||
" s.shutdown(socket.SHUT_WR)\n s.recv(4096)\n"
|
||||
"except Exception:\n sys.exit(1)\n")
|
||||
|
||||
|
||||
def launcher():
|
||||
q = shlex.quote
|
||||
env = wine_env()
|
||||
keys = ("STEAM_COMPAT_DATA_PATH", "WINEPREFIX", "WINEDLLOVERRIDES", "WINEDEBUG", "FP_NATIVE_WSERVER",
|
||||
"PROTON_NO_ZENITY", "PROTONFIXES_DISABLE", "STEAM_COMPAT_CLIENT_INSTALL_PATH", "XDG_RUNTIME_DIR",
|
||||
"DBUS_SESSION_BUS_ADDRESS", "ENABLE_GAMESCOPE_WSI")
|
||||
exports = "".join(f"export {k}={q(env[k])}\n" for k in keys)
|
||||
display = "".join(f"export {k}={q(v)}\n" for k, v in CORE.steam_display_env().items())
|
||||
return f'''#!/usr/bin/env bash
|
||||
# FramePort Meta sign-in: discard Electron output, which can contain native_sso URLs.
|
||||
exec >/dev/null 2>&1
|
||||
exec 9>{q(str(folder() / '.launch.lock'))}; flock -n 9 || exit 0
|
||||
{exports}{display}unset LD_PRELOAD STEAM_COMPAT_INSTALL_PATH STEAM_COMPAT_LIBRARY_PATHS PROTON_LOG PROTON_LOG_DIR
|
||||
python3 -I {q(str(Path(CORE.HERE) / 'fexrift.py'))} login_ping '{{}}' || exit 0
|
||||
G={q(CORE.GEF)}
|
||||
L="$WINEPREFIX/drive_c/users/steamuser/AppData/Local/Oculus"
|
||||
cleanup() {{
|
||||
kill $LK 2>/dev/null
|
||||
"$G/files/bin/wineserver" -k
|
||||
rm -f {q(str(folder() / 'launch.pid'))}
|
||||
}}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 0' TERM INT HUP
|
||||
# The start tick prevents --stop from signalling a reused PID after an interrupted launcher.
|
||||
printf '%s %s\\n' "$$" "$(awk '{{print $22}}' /proc/$$/stat)" > {q(str(folder() / 'launch.pid'))}
|
||||
cd {q(str(Path(CORE.META_DIR) / 'Support/oculus-runtime'))}
|
||||
"$G/files/bin/wine" ./OVRServer_x64.exe </dev/null &
|
||||
for i in $(seq 1 45); do
|
||||
sleep 2
|
||||
tail -n 3 "$L/OVRLibrarian.log" 2>/dev/null | grep -qi "invocation completed" && break
|
||||
done
|
||||
sleep 3; pkill -9 -f "[O]VRLibrarian.exe"; sleep 10
|
||||
( while sleep 5; do pkill -9 -f "[O]VRLibrarian.exe"; done ) & LK=$!
|
||||
cd {q(str(Path(CORE.META_DIR) / 'Support/oculus-client'))}
|
||||
"$G/proton" run ./Client.exe
|
||||
'''
|
||||
|
||||
|
||||
def signed_in():
|
||||
sessions = Path(CORE.PFX) / "drive_c/users/steamuser/AppData/Roaming/Oculus/sessions"
|
||||
return CORE.signed_in(str(sessions)) # sessions/<numeric user id>/, not Meta's _oaf framework folder
|
||||
|
||||
|
||||
def running(name):
|
||||
# /proc, rather than pgrep -f, avoids matching the command requesting status. Scope to this prefix.
|
||||
for p in Path("/proc").glob("[0-9]*/environ"):
|
||||
try:
|
||||
if (f"WINEPREFIX={CORE.PFX}".encode() in p.read_bytes().split(b"\0") and
|
||||
name.encode().lower() in p.with_name("cmdline").read_bytes().lower()):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def status(args):
|
||||
st = CORE.read_json(str(folder() / "state.json"), {}) or {}
|
||||
out = {"runtime_running": running("OVRServer_x64.exe"), "client_running": running("Client.exe"),
|
||||
"page_opened_at": st.get("page_opened_at"), "callback_received_at": st.get("callback_received_at"),
|
||||
"signed_in": signed_in()}
|
||||
if args.get("take_url"):
|
||||
try:
|
||||
out.update(request({"op": "take_url"}))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def deliver(url):
|
||||
if not valid_url(url, callback=True):
|
||||
return {"delivered": False}
|
||||
try:
|
||||
# Never put the callback in argv, even between two local processes. The PE reads UTF-8 on stdin.
|
||||
p = quiet([str(Path(CORE.GEF) / "proton"), "run", str(Path(CORE.HERE) / "fp_pipesend.exe")],
|
||||
input=url.encode(), env=wine_env())
|
||||
return {"delivered": p.returncode == 0}
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return {"delivered": False}
|
||||
|
||||
|
||||
class Broker:
|
||||
def __init__(self, via_pc):
|
||||
self.via_pc = via_pc
|
||||
self.pending = None
|
||||
self.stopped = False
|
||||
|
||||
def handle(self, msg):
|
||||
op = msg.get("op")
|
||||
if op in ("page", "callback"):
|
||||
url = msg.get("url")
|
||||
if not valid_url(url, callback=op == "callback"):
|
||||
return {"accepted": False}
|
||||
st = CORE.read_json(str(folder() / "state.json"), {}) or {}
|
||||
st["page_opened_at" if op == "page" else "callback_received_at"] = time.time()
|
||||
if op == "page":
|
||||
st["page_url"] = "https://auth.meta.com/native_sso/confirm"
|
||||
CORE.write_json(str(folder() / "state.json"), st)
|
||||
if op == "callback":
|
||||
return deliver(url)
|
||||
if self.via_pc:
|
||||
self.pending = url
|
||||
else:
|
||||
quiet(["xdg-open", url], env=host_env())
|
||||
return {"accepted": True}
|
||||
if op == "take_url":
|
||||
url, self.pending = self.pending, None
|
||||
return {"open_url": url} if url else {}
|
||||
if op == "stop":
|
||||
self.pending = None
|
||||
self.stopped = True
|
||||
return {"ready": True}
|
||||
|
||||
|
||||
def broker(args):
|
||||
path = folder() / "broker.sock"
|
||||
service = Broker(bool(args.get("via_pc")))
|
||||
try:
|
||||
with socket.socket(socket.AF_UNIX) as s:
|
||||
s.bind(str(path))
|
||||
os.chmod(path, 0o600)
|
||||
s.listen(4)
|
||||
s.settimeout(1)
|
||||
end = time.monotonic() + int(args.get("timeout", 900))
|
||||
while not service.stopped and time.monotonic() < end:
|
||||
try:
|
||||
conn, _ = s.accept()
|
||||
except TimeoutError:
|
||||
continue
|
||||
with conn:
|
||||
conn.settimeout(5)
|
||||
try:
|
||||
reply = service.handle(json.loads(receive(conn)))
|
||||
conn.sendall(json.dumps(reply).encode())
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass # do not serialize an exception that might contain a URL
|
||||
finally:
|
||||
service.pending = None
|
||||
restore()
|
||||
path.unlink(missing_ok=True)
|
||||
return {"stopped": True}
|
||||
|
||||
|
||||
def restore():
|
||||
path = folder() / "restore.json"
|
||||
saved = CORE.read_json(str(path), None)
|
||||
if saved is None:
|
||||
return
|
||||
# Keep the restore record until every step succeeds: --stop can repair an interrupted cleanup.
|
||||
launcher_pid = stop_launcher()
|
||||
CORE.wineserver_kill()
|
||||
if launcher_pid:
|
||||
for _ in range(100):
|
||||
if process_tick(launcher_pid[0]) != launcher_pid[1]:
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in launcher is still stopping; retry fex login --stop")
|
||||
(folder() / "launch.pid").unlink(missing_ok=True)
|
||||
old = saved.get("browser")
|
||||
if old:
|
||||
CORE.wine("reg", "add", REGKEY, "/v", "Browsers", "/t", old[0], "/d", old[1], "/f")
|
||||
else:
|
||||
CORE.wine("reg", "delete", REGKEY, "/v", "Browsers", "/f", check=False)
|
||||
restore_files(saved["files"])
|
||||
path.unlink()
|
||||
|
||||
|
||||
def stop_launcher():
|
||||
# Killing Wine alone while launch.sh is waiting for OVRLibrarian lets it start Client.exe *after* cleanup.
|
||||
# Signal the owning shell too; its pending TERM exits the startup loop as soon as the current sleep ends.
|
||||
path = folder() / "launch.pid"
|
||||
try:
|
||||
pid, tick = path.read_text().split()
|
||||
pid = int(pid)
|
||||
if pid <= 1:
|
||||
return
|
||||
if process_tick(pid) == tick:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
return pid, tick
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
|
||||
|
||||
def process_tick(pid):
|
||||
try:
|
||||
return Path(f"/proc/{pid}/stat").read_text().rpartition(")")[2].split()[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def stop(args):
|
||||
try:
|
||||
request({"op": "stop"})
|
||||
for _ in range(900):
|
||||
if not (folder() / "broker.sock").exists():
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in is still stopping; retry fex login --stop")
|
||||
except (OSError, ValueError):
|
||||
restore()
|
||||
(folder() / "broker.sock").unlink(missing_ok=True)
|
||||
return {"stopped": True, "signed_in": signed_in()}
|
||||
|
||||
|
||||
def start(args):
|
||||
timeout = int(args.get("timeout") or 900)
|
||||
if not 1 <= timeout <= 86400:
|
||||
raise CORE.FexError("sign-in timeout must be between 1 and 86400 seconds")
|
||||
if not (Path(CORE.HERE) / "fp_pipesend.exe").is_file():
|
||||
raise CORE.FexError("fp_pipesend.exe missing: build native/fexwine/pipesend.c and upload it with fex setup")
|
||||
if not (Path(CORE.META_DIR) / "Support/oculus-client/Client.exe").is_file():
|
||||
raise CORE.FexError("Meta's PC app isn't installed yet; run fex setup first")
|
||||
if running("Client.exe") or running("OVRServer_x64.exe"):
|
||||
raise CORE.FexError("close the Rift game or Meta app before starting sign-in (the prefix is shared)")
|
||||
try:
|
||||
request({"op": "ping"})
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
raise CORE.FexError("Meta sign-in already active; use fex login --stop first")
|
||||
stop({}) # repair a previous interrupted run before snapshotting its handler
|
||||
folder().mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
folder().chmod(0o700)
|
||||
apps, paths = desktop_paths()
|
||||
previous = subprocess.run(["xdg-mime", "query", "default", "x-scheme-handler/oculus"],
|
||||
capture_output=True, text=True, timeout=10, env=host_env()).stdout.strip()
|
||||
reg = CORE.wine("reg", "query", REGKEY, "/v", "Browsers", check=False)
|
||||
match = re.search(r"Browsers\s+(REG_\w+)\s+(.*)", reg.stdout)
|
||||
CORE.write_json(str(folder() / "restore.json"),
|
||||
{"browser": list(match.groups()) if match else None, "handler": previous,
|
||||
"files": snapshot(paths)})
|
||||
try:
|
||||
for name, text in (("fp-open-url", helper_script("page")),
|
||||
("fp-oculus-callback", helper_script("callback")), ("launch.sh", launcher())):
|
||||
CORE.put_text(str(folder() / name), text, 0o700)
|
||||
CORE.write_json(str(folder() / "state.json"), {})
|
||||
apps.mkdir(parents=True, exist_ok=True)
|
||||
(apps / DESKTOP).write_text(desktop_entry(folder() / "fp-oculus-callback"))
|
||||
if quiet(["xdg-mime", "default", DESKTOP, "x-scheme-handler/oculus"], env=host_env()).returncode:
|
||||
raise CORE.FexError("couldn't register the Frame's temporary oculus link handler")
|
||||
CORE.wine("reg", "add", REGKEY, "/v", "Browsers", "/t", "REG_SZ", "/d",
|
||||
str(folder() / "fp-open-url"), "/f")
|
||||
subprocess.Popen([sys.executable, "-I", str(Path(CORE.HERE) / "fexrift.py"), "login_broker",
|
||||
json.dumps({"via_pc": bool(args.get("via_pc")), "timeout": timeout + 180})],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
for _ in range(100):
|
||||
try:
|
||||
request({"op": "ping"})
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
time.sleep(.05)
|
||||
else:
|
||||
raise CORE.FexError("couldn't start Meta sign-in's private URL broker")
|
||||
if args.get("start_now"):
|
||||
if quiet(["systemd-run", "--user", "--collect", "--quiet", "--unit=frameport-fexlogin",
|
||||
"--property=StandardOutput=null", "--property=StandardError=null",
|
||||
str(folder() / "launch.sh")], env=host_env()).returncode:
|
||||
raise CORE.FexError("couldn't start Meta's app; retry without --start-now and use Steam")
|
||||
return {"launcher": str(folder() / "launch.sh"), "started": bool(args.get("start_now")),
|
||||
"signed_in": signed_in()}
|
||||
except Exception:
|
||||
stop({})
|
||||
raise
|
||||
|
||||
|
||||
def register(core):
|
||||
global CORE
|
||||
CORE = core
|
||||
core.COMMANDS.update(login_start=start, login_status=status, login_stop=stop, login_broker=broker,
|
||||
login_ping=lambda args: request({"op": "ping"}),
|
||||
login_callback=lambda args: request({"op": "callback", "url": args.get("url")}))
|
||||
@@ -0,0 +1,575 @@
|
||||
"""Launch-time performance experiments. Frame side, stdlib only; no account data is read here.
|
||||
|
||||
Settings contain numbers/booleans and a small environment whitelist, never arbitrary shell. A launch prepares its
|
||||
own config and exports it before Proton starts. Bench owns the otherwise-idle shared prefix for at most 120 s.
|
||||
"""
|
||||
import collections
|
||||
import ctypes as C
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
CORE = None
|
||||
FEX_KEYS = {"FEX_TSOENABLED", "FEX_VECTORTSOENABLED", "FEX_MEMCPYSETTSOENABLED", "FEX_MULTIBLOCK",
|
||||
"FEX_X87REDUCEDPRECISION"}
|
||||
DEFAULTS = {"dxvk_disable_msaa": False, "dxgi_sync_interval": -1, "dxgi_max_frame_latency": 0,
|
||||
"eye_scale": 1.5, "motion_smoothing": 2, "cores": "4-7", "ntsync": True,
|
||||
"fp_mem": False, "bigcache_mb": 256, "fp_mem_zero": False, "fex": {},
|
||||
"wine_large_address_aware": False}
|
||||
BOOL_KEYS = {"dxvk_disable_msaa", "ntsync", "fp_mem", "fp_mem_zero", "wine_large_address_aware"}
|
||||
INT_KEYS = {"dxgi_sync_interval": (-1, 4), "dxgi_max_frame_latency": (0, 16), "motion_smoothing": (0, 2),
|
||||
"bigcache_mb": (1, 6144)}
|
||||
|
||||
|
||||
def app_name(app):
|
||||
if not isinstance(app, str) or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}", app):
|
||||
raise ValueError("invalid Meta app name")
|
||||
return app
|
||||
|
||||
|
||||
def validate(values):
|
||||
if not isinstance(values, dict):
|
||||
raise ValueError("settings must be an object")
|
||||
out = {}
|
||||
for key, value in values.items():
|
||||
if key in BOOL_KEYS:
|
||||
if type(value) is not bool:
|
||||
raise ValueError(f"{key} must be true or false")
|
||||
elif key in INT_KEYS:
|
||||
lo, hi = INT_KEYS[key]
|
||||
if type(value) is not int or not lo <= value <= hi:
|
||||
raise ValueError(f"{key} must be an integer in {lo}..{hi}")
|
||||
elif key == "eye_scale":
|
||||
if type(value) not in (float, int) or value not in (1.0, 1.25, 1.5):
|
||||
raise ValueError("eye_scale must be 1.0, 1.25 or 1.5 (a divisor)")
|
||||
elif key == "cores":
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[0-7](?:-[0-7])?(?:,[0-7](?:-[0-7])?)*", value):
|
||||
raise ValueError("cores must be a taskset CPU list within 0..7")
|
||||
if any(len(p) == 3 and p[0] > p[2] for p in value.split(",")):
|
||||
raise ValueError("cores range is reversed")
|
||||
elif key == "fex":
|
||||
if not isinstance(value, dict) or any(k not in FEX_KEYS or type(v) not in (str, int) or
|
||||
str(v) not in ("0", "1") for k, v in value.items()):
|
||||
raise ValueError("fex permits only boolean TSO, MULTIBLOCK and X87REDUCEDPRECISION keys")
|
||||
value = {k: str(v) for k, v in value.items()}
|
||||
else:
|
||||
raise ValueError(f"unknown performance setting: {key}")
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def settings_path(app):
|
||||
return os.path.join(CORE.ROOT, "settings", app_name(app) + ".json")
|
||||
|
||||
|
||||
def settings_get(args):
|
||||
app = app_name(args["app"])
|
||||
info = CORE.GAMES.get(app, {})
|
||||
env = info.get("env", {})
|
||||
defaults = dict(DEFAULTS, fp_mem=env.get("FP_MEM", "") not in ("", "0"),
|
||||
bigcache_mb=int(env.get("FP_BIGCACHE_MB", 256)))
|
||||
defaults.update(validate(info.get("perf", {})))
|
||||
overrides = validate(CORE.read_json(settings_path(app), {}) or {})
|
||||
return {"app": app, "settings": dict(defaults, **overrides), "overrides": overrides}
|
||||
|
||||
|
||||
def settings_set(args):
|
||||
app = app_name(args["app"])
|
||||
# Validate the whole request before writing. Null removes an override (the catalog/default becomes active).
|
||||
updates = {k: v for k, v in args.items() if k != "app"}
|
||||
if any(k not in DEFAULTS for k in updates):
|
||||
raise ValueError("unknown performance setting")
|
||||
values = settings_get({"app": app})["overrides"]
|
||||
values.update(validate({k: v for k, v in updates.items() if v is not None}))
|
||||
for k, v in updates.items():
|
||||
if v is None:
|
||||
values.pop(k, None)
|
||||
CORE.write_json(settings_path(app), values)
|
||||
return settings_get({"app": app})
|
||||
|
||||
|
||||
def scale_folder(scale):
|
||||
return os.path.join(CORE.ROOT, "revive-scales", format(float(scale), "g"))
|
||||
|
||||
|
||||
def eye_variant(data, scale):
|
||||
"""Only the research Revive 3.2.0 eye-size site, with PE RVA translation. Never touch Oculus binaries.
|
||||
|
||||
Accept the stock eye-size load or the research divisor load. Refuse other instructions rather than guessing
|
||||
at a new Revive build. Use a dedicated padding slot, leaving shared constants in the data section unchanged.
|
||||
"""
|
||||
if data[:2] != b"MZ":
|
||||
raise ValueError("Revive eye scale: invalid PE")
|
||||
pe = struct.unpack_from("<I", data, 0x3c)[0]
|
||||
n, opt = struct.unpack_from("<H", data, pe + 6)[0], struct.unpack_from("<H", data, pe + 20)[0]
|
||||
|
||||
def offset(rva):
|
||||
for i in range(n):
|
||||
size, va, rawsize, raw = struct.unpack_from("<IIII", data, pe + 24 + opt + i * 40 + 8)
|
||||
if va <= rva < va + min(size, rawsize):
|
||||
return raw + rva - va
|
||||
raise ValueError("Revive eye scale: RVA outside sections")
|
||||
ins, slot = offset(0x12eab), offset(0x12e9c)
|
||||
# Original movss xmm4,[1.0], or the research patch's load of the existing 1.5 at RVA 0x4eaa4.
|
||||
# Put our divisor in verified function-alignment padding: changing a shared rdata constant affects other code.
|
||||
old = data[ins:ins + 8]
|
||||
original = bytes.fromhex("f30f102515bb0400")
|
||||
research = b"\xf3\x0f\x10\x25" + struct.pack("<i", 0x4eaa4 - 0x12eb3)
|
||||
if old not in (original, research) or data[offset(0x12e91)] != 0xc3 or data[slot:slot + 4] != b"\xcc" * 4:
|
||||
raise ValueError("Revive eye scale: unexpected 3.2.0 eye-size instruction/padding")
|
||||
out = bytearray(data)
|
||||
struct.pack_into("<f", out, slot, scale)
|
||||
out[ins:ins + 8] = b"\xf3\x0f\x10\x25" + struct.pack("<i", 0x12e9c - 0x12eb3)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def prepare_scales():
|
||||
src = Path(CORE.REVIVE)
|
||||
data = (src / "LibRevive64.dll").read_bytes()
|
||||
# PC setup validates stock 3.2.0 and applies REVIVE_PATCHES (src/frameport/fexrift.py): the visibility fixes and
|
||||
# the eye-size redirect to the 1.5 constant. Accept only that upload, or the same without the eye-size redirect.
|
||||
uploaded = {"eda2a449d230832c95734a03e8c83eeb531668ce676693f636e19c315765af4e", # all six patches
|
||||
"0a3a6733f5decec74abb16b85c1f72783a6f421160753f600ddae4e8cb6482c6"} # visibility fixes only
|
||||
if hashlib.sha256(data).hexdigest() not in uploaded:
|
||||
raise ValueError("Revive eye scale: needs FramePort's patched Revive 3.2.0 upload")
|
||||
for scale in (1.0, 1.25, 1.5):
|
||||
patched = eye_variant(data, scale) # validate before replacing any directory
|
||||
dest = Path(scale_folder(scale))
|
||||
tmp = dest.with_name(dest.name + ".new")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
shutil.copytree(src, tmp, copy_function=os.link)
|
||||
# Break the hard link before changing the DLL: shared base and other variants stay intact.
|
||||
dll = tmp / "LibRevive64.dll"
|
||||
dll.unlink()
|
||||
dll.write_bytes(patched)
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
os.replace(tmp, dest)
|
||||
|
||||
|
||||
def perf_prepare(args):
|
||||
app = app_name(args["app"])
|
||||
s = settings_get({"app": app})["settings"]
|
||||
config = os.path.join(CORE.ROOT, "settings", app + ".dxvk.conf")
|
||||
os.makedirs(os.path.dirname(config), exist_ok=True) # first launch of a game nobody changed settings for
|
||||
managed = ("d3d11.disableMsaa", "dxgi.syncInterval", "dxgi.maxFrameLatency") # these settings win
|
||||
profile = [str(line) for line in args.get("dxvk", []) if "\n" not in str(line)
|
||||
and str(line).split("=", 1)[0].strip() not in managed]
|
||||
lines = profile + [f"{k} = {s[key]}" for k, key, default in (
|
||||
("d3d11.disableMsaa", "dxvk_disable_msaa", False), ("dxgi.syncInterval", "dxgi_sync_interval", -1),
|
||||
("dxgi.maxFrameLatency", "dxgi_max_frame_latency", 0)) if s[key] != default]
|
||||
# Defaults = exactly the launch verified in the headset (First Contact, 2026-10-10): no dxvk.conf, the uploaded
|
||||
# Revive (its eye-size patch is 1.5), no large-address-aware flag; only a changed setting adds something.
|
||||
if lines:
|
||||
CORE.put_text(config, "\n".join(lines + [""]))
|
||||
elif os.path.lexists(config):
|
||||
os.remove(config)
|
||||
if s["eye_scale"] == 1.5:
|
||||
revive = CORE.REVIVE
|
||||
else:
|
||||
revive = scale_folder(s["eye_scale"])
|
||||
if not os.path.isfile(os.path.join(revive, "LibRevive64.dll")):
|
||||
prepare_scales()
|
||||
exports = {"FP_CORES": s["cores"], "FP_REVIVE_DIR": revive,
|
||||
"FP_MEM": "1" if s["fp_mem"] else "", "FP_BIGCACHE_MB": str(s["bigcache_mb"]),
|
||||
"FP_MEM_ZERO": "1" if s["fp_mem_zero"] else "0", "PROTON_NO_NTSYNC": "0" if s["ntsync"] else "1",
|
||||
"FP_MOTION_SMOOTHING": json.dumps({"section": f"steam.app.{CORE.GAME_ID}",
|
||||
"key": "motionSmoothingOverride", "kind": "i",
|
||||
"value": s["motion_smoothing"]})}
|
||||
exports.update({k: str(s["fex"].get(k, "0" if "TSO" in k else "1" if k == "FEX_MULTIBLOCK" else "0"))
|
||||
for k in FEX_KEYS})
|
||||
if lines:
|
||||
exports["DXVK_CONFIG_FILE"] = config
|
||||
if s["wine_large_address_aware"]:
|
||||
exports["WINE_LARGE_ADDRESS_AWARE"] = "1"
|
||||
shell = "\n".join(f"export {k}={shlex.quote(v)}" for k, v in exports.items())
|
||||
if s["ntsync"]:
|
||||
shell += "\nunset PROTON_NO_NTSYNC"
|
||||
return {"shell": shell, "settings": s}
|
||||
|
||||
|
||||
def launcher_env(app, info):
|
||||
# stdout is an isolated helper's JSON, then a tiny decoder emits only validated exports. Failure aborts launch:
|
||||
# continuing with an old config would give misleading experimental results (and could enable stale fp_mem).
|
||||
# the compat profile's dxvk.conf lines (stream C) go into the same per-game config as these settings
|
||||
dxvk = [str(line) for line in info.get("dxvk", []) if "\n" not in str(line) and "\r" not in str(line)]
|
||||
arg = shlex.quote(json.dumps({"app": app_name(app), "dxvk": dxvk}))
|
||||
decode = 'import json,sys; print(json.load(sys.stdin)["result"]["shell"])'
|
||||
return [f'FP_PERF=$(python3 -I "$R/artifacts/fexrift.py" perf_prepare {arg}) || exit 1',
|
||||
f'eval "$(printf \'%s\' "$FP_PERF" | python3 -I -c {shlex.quote(decode)})"']
|
||||
|
||||
|
||||
# OpenVR IVRCompositor_022 Compositor_FrameTiming, including its 80-byte TrackedDevicePose_t. Keeping the tail
|
||||
# prevents GetFrameTimings overwriting a truncated ctypes structure. Versions are pinned, never probed by index.
|
||||
class FrameTiming(C.Structure):
|
||||
_fields_ = [(n, C.c_uint32) for n in ("size", "index", "presents", "mis_presents", "dropped", "reprojection")]
|
||||
_fields_ += [("system_time", C.c_double)]
|
||||
_fields_ += [(n, C.c_float) for n in ("pre_gpu", "post_gpu", "gpu_ms", "compositor_gpu", "compositor_cpu",
|
||||
"compositor_idle", "client_interval", "present_call", "wait_present",
|
||||
"submit", "poses_called", "poses_ready", "frame_ready", "update_start",
|
||||
"update_end", "render_start")]
|
||||
_fields_ += [("pose", C.c_byte * 80), ("vsyncs_ready", C.c_uint32), ("vsyncs_first", C.c_uint32)]
|
||||
|
||||
|
||||
class VR:
|
||||
def __init__(self):
|
||||
self.core, self.method, self.settings = CORE.vr_settings()
|
||||
try:
|
||||
self.compositor = self.interface(b"IVRCompositor_022")
|
||||
self.system = self.interface(b"IVRSystem_022")
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def interface(self, name):
|
||||
err = C.c_int()
|
||||
obj = self.method(self.core, 3, C.c_void_p, C.c_char_p, C.POINTER(C.c_int))(name, C.byref(err))
|
||||
if not obj or err.value:
|
||||
raise ValueError("benchmark needs " + name.decode())
|
||||
return obj
|
||||
|
||||
def activity(self):
|
||||
# IVRSystem_022::GetTrackedDeviceActivityLevel(HMD=0). UserInteraction (1) and its timeout (2) refuse.
|
||||
return self.method(self.system, 15, C.c_int, C.c_uint32)(0)
|
||||
|
||||
def screen_timeout(self):
|
||||
err = C.c_int()
|
||||
value = self.method(self.settings, 7, C.c_float, C.c_char_p, C.c_char_p, C.POINTER(C.c_int))(
|
||||
b"power", b"turnOffScreensTimeout", C.byref(err))
|
||||
if err.value or not math.isfinite(value) or value <= 0:
|
||||
raise ValueError("cannot read screen timeout; benchmark refused")
|
||||
return value
|
||||
|
||||
def timings(self):
|
||||
rows = (FrameTiming * 128)()
|
||||
for row in rows:
|
||||
row.size = C.sizeof(FrameTiming)
|
||||
n = self.method(self.compositor, 9, C.c_uint32, C.POINTER(FrameTiming), C.c_uint32)(rows, 128)
|
||||
return [{"index": r.index, "presents": r.presents, "gpu_ms": r.gpu_ms,
|
||||
"cpu_ms": r.client_interval, "system_time": r.system_time} for r in rows[:min(n, 128)]]
|
||||
|
||||
def scene_pid(self):
|
||||
return self.method(self.compositor, 22, C.c_uint32)() # GetCurrentSceneFocusProcess
|
||||
|
||||
def close(self):
|
||||
self.method(self.core, 1, None)()
|
||||
|
||||
|
||||
def drm_engines(text):
|
||||
"""DRM fdinfo nanoseconds, deduplicated by drm-client-id by the caller (dup fds share one client)."""
|
||||
out = {}
|
||||
for line in text.splitlines():
|
||||
m = re.fullmatch(r"drm-engine-([\w-]+):\s*(\d+)\s+ns", line.strip())
|
||||
if m:
|
||||
out[m[1]] = int(m[2])
|
||||
return out
|
||||
|
||||
|
||||
def process_sample(pids, proc="/proc"):
|
||||
engines, waits, seen = {}, collections.Counter(), set()
|
||||
for pid in pids:
|
||||
base = Path(proc) / str(pid)
|
||||
for fd in (base / "fdinfo").glob("*"):
|
||||
try:
|
||||
text = fd.read_text()
|
||||
except OSError:
|
||||
continue
|
||||
client = re.search(r"^drm-client-id:\s*(\d+)", text, re.M)
|
||||
device = re.search(r"^drm-pdev:\s*(.+)", text, re.M)
|
||||
if not client:
|
||||
continue
|
||||
identity = (device[1] if device else "", client[1])
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
for engine, ns in drm_engines(text).items():
|
||||
engines[(identity, engine)] = ns
|
||||
for task in (base / "task").glob("*"):
|
||||
try:
|
||||
waits[task.joinpath("wchan").read_text().strip()] += 1
|
||||
except OSError:
|
||||
pass
|
||||
return engines, waits
|
||||
|
||||
|
||||
def gpu_delta(before, after):
|
||||
# Clients that open during the interval have no baseline; do not attribute their lifetime counters to one tick.
|
||||
return sum(max(0, v - before[k]) for k, v in after.items() if k in before)
|
||||
|
||||
|
||||
def processes(proc="/proc"):
|
||||
out = {}
|
||||
for p in Path(proc).glob("[0-9]*"):
|
||||
try:
|
||||
if p.stat().st_uid != os.getuid():
|
||||
continue
|
||||
cmd = p.joinpath("cmdline").read_bytes().replace(b"\0", b" ").decode(errors="replace")
|
||||
comm = p.joinpath("comm").read_text().strip()
|
||||
out[int(p.name)] = (comm, cmd)
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def busy_processes(rows):
|
||||
# Do not print cmdlines: another stream may be handling an Oculus browser sign-in.
|
||||
return [pid for pid, (comm, cmd) in rows.items() if
|
||||
(re.search(r"(?i)(\.exe(?: |$)|(?:^|/)(?:proton|wine(?:64)?(?:-preloader)?)(?: |$)|lepton-steamlaunch-)",
|
||||
cmd)
|
||||
or (comm == "reaper" and not re.search(r"\bAppId=250820\b", cmd))
|
||||
or comm.startswith("wineserver") or comm in ("FEXInterpreter", "FEXLoader", "pressure-vessel"))]
|
||||
|
||||
|
||||
def recent_steam_input(steam, now=None):
|
||||
"""Steam input log is a supplementary guard, HMD activity is the authoritative one. Never read login logs."""
|
||||
now = time.time() if now is None else now
|
||||
path = Path(steam) / "logs/controller.txt"
|
||||
try:
|
||||
# A recent controller log write can be a device reconnection, so conservatively refuse it too.
|
||||
return now - path.stat().st_mtime < 60
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def vr_set(section, key, kind, value):
|
||||
# In its own process: cmd_vrsetting in the process that holds VR()'s SteamVR connection segfaulted on the
|
||||
# second call (reproduced on the Frame 2026-10-16) after the first had turned the display's standby pause off.
|
||||
result = vrsetting_subprocess(section, key, kind, value)
|
||||
if result.get("error"):
|
||||
raise ValueError(f"SteamVR refused {section}/{key}")
|
||||
|
||||
|
||||
def stop_launcher(child):
|
||||
# The shared prefix was verified idle before launch, so -k cannot interrupt an earlier game or login.
|
||||
try:
|
||||
subprocess.run([os.path.join(CORE.GEF, "files/bin/wineserver"), "-k"], env=CORE.wine_env(),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
finally:
|
||||
if child is not None:
|
||||
try:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
child.wait(timeout=5)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
child.wait(timeout=5)
|
||||
|
||||
|
||||
START_WAIT = 180 # seconds the bench waits for the game's process after starting its launcher
|
||||
|
||||
|
||||
def bench_run(args, launcher, vr):
|
||||
seconds = args.get("seconds", 120)
|
||||
if type(seconds) is not int or not 1 <= seconds <= 120:
|
||||
raise ValueError("seconds must be an integer in 1..120")
|
||||
if busy_processes(processes()) or recent_steam_input(CORE.STEAM) or vr.activity() != 3:
|
||||
raise ValueError("benchmark refused: game, Wine, container, HMD activity or recent Steam input")
|
||||
screen = vr.screen_timeout() # no setting is changed until both safety checks and this read succeed
|
||||
vrserver_log = os.path.join(CORE.STEAM, "logs", "vrserver.txt")
|
||||
log_start = os.path.getsize(vrserver_log) if os.path.isfile(vrserver_log) else 0
|
||||
guard_write(screen=screen) # before any change: the parent restores from it if this process dies
|
||||
child, old_handler = None, None
|
||||
settings = settings_get(args)["settings"]
|
||||
frames, waits, gpu, gpu_seconds = {}, collections.Counter(), 0, 0.0
|
||||
started = time.monotonic()
|
||||
|
||||
def interrupted(signum, frame):
|
||||
raise ValueError("benchmark interrupted")
|
||||
try:
|
||||
old_handler = signal.signal(signal.SIGTERM, interrupted)
|
||||
vr_set("power", "pauseCompositorOnStandby", "b", False)
|
||||
vr_set("power", "turnOffScreensTimeout", "f", float(seconds + START_WAIT + 10))
|
||||
# Save the initial frame index so stale scene frames in the compositor ring are excluded.
|
||||
initial = {r["index"] for r in vr.timings()}
|
||||
with open(os.devnull, "wb") as sink:
|
||||
child = subprocess.Popen([launcher], stdin=subprocess.DEVNULL, stdout=sink, stderr=sink,
|
||||
start_new_session=True, close_fds=True)
|
||||
guard_write(screen=screen, pgid=getattr(child, "pid", None))
|
||||
# The launcher starts Meta's runtime and waits for it (60-100 s under FEX) before the game: the window
|
||||
# starts when the game's process appears, not at the launch.
|
||||
wait_end = time.monotonic() + START_WAIT
|
||||
while not [q for q, (_, cmd) in processes().items() if args["exe"].lower() in cmd.lower()]:
|
||||
if time.monotonic() > wait_end:
|
||||
raise ValueError(f"benchmark: the game didn't start within {START_WAIT} s; inspect launch.log")
|
||||
if child.poll() is not None:
|
||||
raise ValueError("benchmark launcher exited before the game started; inspect launch.log")
|
||||
level = vr.activity()
|
||||
if level == 1: # controller.txt isn't a signal here: the starting game loads its input bindings
|
||||
raise ValueError(f"benchmark stopped: owner interaction (activity {level})")
|
||||
time.sleep(1)
|
||||
before, previous = {}, time.monotonic()
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
# Before the launch the headset must be in standby (3); the game itself then wakes the compositor
|
||||
# (activity leaves standby), so during the run only real use stops it: 1 = user interaction (worn or
|
||||
# moved), or new controller input.
|
||||
level = vr.activity()
|
||||
if level == 1: # controller.txt isn't a signal here: the starting game loads its input bindings
|
||||
raise ValueError(f"benchmark stopped: owner interaction (activity {level})")
|
||||
if child.poll() is not None:
|
||||
raise ValueError("benchmark launcher exited before the sampling window ended; inspect launch.log")
|
||||
rows = processes()
|
||||
# Only the actual game's exe (or FEX's argv containing it), not the runtime/compositor GPU client.
|
||||
pids = [p for p, (_, cmd) in rows.items() if args["exe"].lower() in cmd.lower()]
|
||||
after, w = process_sample(pids)
|
||||
now = time.monotonic()
|
||||
if before.keys() & after.keys():
|
||||
gpu += gpu_delta(before, after)
|
||||
gpu_seconds += now - previous
|
||||
before, previous = after, now
|
||||
waits.update(w)
|
||||
focus = vr.scene_pid()
|
||||
for r in vr.timings():
|
||||
if focus in pids and r["index"] not in initial:
|
||||
frames[r["index"]] = r # ring revisits update presents, not another frame
|
||||
time.sleep(min(0.5, max(0, deadline - time.monotonic())))
|
||||
finally:
|
||||
# Each cleanup is attempted even when the preceding one fails. Display safety outranks benchmark success.
|
||||
try:
|
||||
stop_launcher(child)
|
||||
finally:
|
||||
try:
|
||||
vr_set("power", "pauseCompositorOnStandby", "b", True)
|
||||
finally:
|
||||
try:
|
||||
vr_set("power", "turnOffScreensTimeout", "f", screen)
|
||||
finally:
|
||||
if old_handler is not None:
|
||||
signal.signal(signal.SIGTERM, old_handler)
|
||||
records = list(frames.values())
|
||||
# Without a tracking universe (IMU fallback, e.g. after a boot without the headset worn) Rift games wait at
|
||||
# startup forever: SteamVR's own frames are all a run then counts (seen 2026-10-16, ~1.9/s, game GPU ~0 %).
|
||||
imu_fallback = False
|
||||
try:
|
||||
with open(vrserver_log, errors="replace") as f:
|
||||
f.seek(log_start if os.path.getsize(vrserver_log) >= log_start else 0)
|
||||
imu_fallback = "IMU fallback is active" in f.read(8 << 20)
|
||||
except OSError:
|
||||
pass
|
||||
duration = max(0.001, time.monotonic() - started)
|
||||
times = [r["system_time"] for r in records]
|
||||
span = max(times) - min(times) if len(times) > 1 else 0
|
||||
return {"app": args["app"], "seconds": seconds, "elapsed_s": round(duration, 3), "frames": len(records),
|
||||
"imu_fallback": imu_fallback, # true: the result says nothing about the game (put the headset on once)
|
||||
"valid": len(records) >= 2 and span > 0 and not imu_fallback, "sample_span_s": round(span, 3),
|
||||
"fps": round((len(records) - 1) / span, 2) if span > 0 else None,
|
||||
"hitches": sum(r["presents"] >= 3 for r in records),
|
||||
"hitches_per_20s": round(20 * sum(r["presents"] >= 3 for r in records) / span, 2) if span > 0 else None,
|
||||
"game_gpu_busy_pct": round(100 * gpu / (gpu_seconds * 1e9), 2) if gpu_seconds else None,
|
||||
"render_gpu_ms": round(sum(r["gpu_ms"] for r in records) / len(records), 3) if records else None,
|
||||
"client_interval_ms": round(sum(r["cpu_ms"] for r in records) / len(records), 3) if records else None,
|
||||
"thread_wchan_samples": dict(waits), "settings": settings}
|
||||
|
||||
|
||||
def bench(args):
|
||||
app = app_name(args["app"])
|
||||
# Launcher path is supplied by the owning agent's deployment record, not an arbitrary remote shell command.
|
||||
launcher = args["launcher"]
|
||||
if not os.path.isfile(launcher) or os.path.basename(launcher) != "launch.sh":
|
||||
raise ValueError("install the game before benchmarking")
|
||||
info, _ = CORE.game_info(app)
|
||||
exe = os.path.basename(info["exe"].replace("\\", "/"))
|
||||
os.makedirs(CORE.ROOT, exist_ok=True)
|
||||
with open(os.path.join(CORE.ROOT, ".bench.lock"), "w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return bench_guarded(dict(args, app=app, exe=exe), launcher)
|
||||
|
||||
|
||||
# The measurement talks to SteamVR through ctypes (vrclient.so). A native crash there kills the process without
|
||||
# running bench_run's finally, which once left pauseCompositorOnStandby=false (display kept on) on the Frame
|
||||
# (2026-10-16). So it runs in a forked child, and the parent restores the display settings and stops the game from
|
||||
# a guard file when the child returns no result, each restore in its own process (another crash can't skip it).
|
||||
def guard_path():
|
||||
return os.path.join(CORE.ROOT, ".bench.guard.json")
|
||||
|
||||
|
||||
def guard_write(**kw):
|
||||
CORE.write_json(guard_path(), kw)
|
||||
|
||||
|
||||
def vrsetting_subprocess(section, key, kind, value):
|
||||
"""fexrift.py vrsetting in a fresh process: {"error": code} from SteamVR, or {"error": "..."} if it failed."""
|
||||
arg = json.dumps({"section": section, "key": key, "kind": kind, "value": value})
|
||||
p = subprocess.run([sys.executable, "-I", os.path.join(CORE.HERE, "fexrift.py"), "vrsetting", arg],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
try:
|
||||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||||
except (ValueError, IndexError):
|
||||
return {"error": f"vrsetting exited {p.returncode}"}
|
||||
return out.get("result", {}) if out.get("ok") else {"error": out.get("error", "failed")}
|
||||
|
||||
|
||||
def crash_cleanup():
|
||||
guard = CORE.read_json(guard_path(), {}) or {}
|
||||
try:
|
||||
if guard.get("pgid"):
|
||||
try:
|
||||
os.killpg(int(guard["pgid"]), signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
pass
|
||||
subprocess.run([os.path.join(CORE.GEF, "files/bin/wineserver"), "-k"], env=CORE.wine_env(),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
|
||||
finally:
|
||||
vrsetting_subprocess("power", "pauseCompositorOnStandby", "b", True)
|
||||
if guard.get("screen") is not None:
|
||||
vrsetting_subprocess("power", "turnOffScreensTimeout", "f", float(guard["screen"]))
|
||||
|
||||
|
||||
def bench_guarded(args, launcher):
|
||||
read_fd, write_fd = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0: # child: measure, report through the pipe
|
||||
os.close(read_fd)
|
||||
code = 0
|
||||
try:
|
||||
vr = VR()
|
||||
try:
|
||||
out = {"ok": True, "result": bench_run(args, launcher, vr)}
|
||||
finally:
|
||||
vr.close()
|
||||
except Exception as e: # noqa: BLE001 - reported to the parent as the command's error
|
||||
out, code = {"ok": False, "error": f"{type(e).__name__}: {e}"}, 1
|
||||
with os.fdopen(write_fd, "w") as w:
|
||||
w.write(json.dumps(out))
|
||||
os._exit(code)
|
||||
os.close(write_fd)
|
||||
try:
|
||||
with os.fdopen(read_fd) as r:
|
||||
raw = r.read()
|
||||
_, status = os.waitpid(pid, 0)
|
||||
out = json.loads(raw) if raw.strip() else None
|
||||
if out is None: # crashed or killed: bench_run's own cleanup didn't run
|
||||
crash_cleanup()
|
||||
raise ValueError(f"benchmark process died ({'signal ' + str(-os.waitstatus_to_exitcode(status)) if os.WIFSIGNALED(status) else 'no result'}); display settings restored, game stopped")
|
||||
if not out["ok"]:
|
||||
raise ValueError(out["error"])
|
||||
return out["result"]
|
||||
finally:
|
||||
try:
|
||||
os.remove(guard_path())
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def register(core):
|
||||
global CORE
|
||||
CORE = core
|
||||
core.COMMANDS.update(settings_get=settings_get, settings_set=settings_set, perf_prepare=perf_prepare, bench=bench)
|
||||
core.LAUNCHER_ENV_HOOKS.append(launcher_env)
|
||||
# Use the existing setup step so --steps revive prepares the variants too, without another download.
|
||||
def revive():
|
||||
result = core.step_revive()
|
||||
prepare_scales()
|
||||
return result
|
||||
core.STEPS = [(name, revive if name == "revive" else fn) for name, fn in core.STEPS]
|
||||
+248
-14
@@ -44,7 +44,7 @@ try:
|
||||
except ImportError: # Windows: pc_revive loads this file for its VDF code only (GitHub #131)
|
||||
fcntl = None
|
||||
|
||||
AGENT_VERSION = 92 # v92: upsert/remove_shortcut options_key (the PC's Quest games share powershell.exe as Exe)
|
||||
AGENT_VERSION = 94
|
||||
HOME = os.path.expanduser("~")
|
||||
STEAM = os.path.join(HOME, ".local/share/Steam")
|
||||
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
|
||||
@@ -982,14 +982,8 @@ def vdf_encode(obj):
|
||||
return bytes(out) + bytes([TYPE_END])
|
||||
|
||||
|
||||
def _shortcut_matches(v, exe, options_key=None):
|
||||
"""A shortcut is the game's when its Exe matches; with options_key (games sharing one Exe, e.g. Quest games on a
|
||||
PC all start powershell.exe) its LaunchOptions must also contain that key."""
|
||||
return isinstance(v, dict) and v.get("Exe") == exe and (not options_key or options_key in v.get("LaunchOptions", ""))
|
||||
|
||||
|
||||
def upsert_shortcut(vdf_path, exe, title, start_dir, icon="", tag="Quest on Frame", launch_options="", tags=None,
|
||||
openvr=True, write=True, options_key=None):
|
||||
openvr=True, write=True):
|
||||
"""Add/update a non-Steam shortcut (matched by Exe, so its appid never changes). `tags` (genres, the user's tags)
|
||||
are merged with tags already on the shortcut, so ones set in Steam are kept. write=False: change nothing, return
|
||||
(appid, whether shortcuts.vdf would change)."""
|
||||
@@ -1003,7 +997,7 @@ def upsert_shortcut(vdf_path, exe, title, start_dir, icon="", tag="Quest on Fram
|
||||
def game_of(options): # the game's program in the injector's arguments (its flags may change)
|
||||
found = re.findall(r'"([^"]+\.exe)"|(\S+\.exe)', options or "", re.IGNORECASE)
|
||||
return (found[-1][0] or found[-1][1]).lower() if found else (options or "")
|
||||
entry = next((v for v in shortcuts.values() if _shortcut_matches(v, exe, options_key)
|
||||
entry = next((v for v in shortcuts.values() if isinstance(v, dict) and v.get("Exe") == exe
|
||||
and (not shared or game_of(v.get("LaunchOptions", "")) == game_of(launch_options))), None)
|
||||
ident = entry["appid"] if entry else shortcut_appid(exe, title)
|
||||
if entry is None:
|
||||
@@ -1098,12 +1092,12 @@ def grid_files(grid, appid):
|
||||
return []
|
||||
|
||||
|
||||
def remove_shortcut(vdf_path, exe, options_key=None):
|
||||
def remove_shortcut(vdf_path, exe):
|
||||
if not os.path.exists(vdf_path):
|
||||
return False
|
||||
root = vdf_decode(open(vdf_path, "rb").read())
|
||||
sc = root.get("shortcuts", {})
|
||||
keep = [v for v in sc.values() if not _shortcut_matches(v, exe, options_key)]
|
||||
keep = [v for v in sc.values() if not (isinstance(v, dict) and v.get("Exe") == exe)]
|
||||
if len(keep) == len(sc):
|
||||
return False
|
||||
root["shortcuts"] = {str(i): v for i, v in enumerate(keep)}
|
||||
@@ -1148,7 +1142,7 @@ def shortcut_args(pkg):
|
||||
icon = app_icon_for(dep, anchor, steam=True)[0] # a Linux app's own icon unless the user chose one
|
||||
flat = dep.get("vr") is False
|
||||
kind = dep.get("kind")
|
||||
tag = ("Windows game on Frame" if flat else "PC VR on Frame") if kind == "pcvr" else \
|
||||
tag = ("Windows game on Frame" if flat else "PC VR on Frame") if kind in ("pcvr", "frame_fex") else \
|
||||
"Linux app on Frame" if kind == "linux" else "Quest on Frame"
|
||||
return f'"{anchor}/launch.sh"', dep["title"], anchor, icon, tag, dep.get("tags") or [], not flat
|
||||
|
||||
@@ -1897,7 +1891,7 @@ def app_media_dirs(ext):
|
||||
|
||||
def lepton_external(pkg):
|
||||
dep = deployment(pkg)
|
||||
if not dep or dep.get("kind") == "pcvr":
|
||||
if not dep or dep.get("kind") in ("pcvr", "frame_fex"):
|
||||
raise AgentError(f"{pkg} is not an installed Quest (Lepton) game")
|
||||
return os.path.join(dep["base"], "lepton-data", "external")
|
||||
|
||||
@@ -5466,8 +5460,11 @@ def cmd_uninstall(args):
|
||||
base = dep["base"]
|
||||
pcvr = dep.get("kind") == "pcvr"
|
||||
linux = dep.get("kind") == "linux"
|
||||
if (pcvr_pids(base) if pcvr or linux else container_running(dep["appid"])):
|
||||
fex = dep.get("kind") == "frame_fex"
|
||||
if (pcvr_pids(base) if pcvr or linux or fex else container_running(dep["appid"])):
|
||||
raise AgentError("the game is running")
|
||||
if fex: # the game lives in the shared Meta runtime prefix (with the user's login): only the launcher goes
|
||||
return fex_uninstall(pkg, dep, args)
|
||||
if is_overlay(dep) or os.path.exists(os.path.join(ANCHORS, pkg, OVERLAY_MANIFEST)):
|
||||
unregister_overlay(pkg) # SteamVR forgets the overlay app (before its manifest's folder goes)
|
||||
keep_data = args.get("keep_data", True)
|
||||
@@ -5818,6 +5815,8 @@ def cmd_stop(args):
|
||||
run(["systemctl", "--user", "stop", f"frameport-test-{dep['appid']}"])
|
||||
if dep.get("kind") == "pcvr":
|
||||
stop_pcvr(dep)
|
||||
elif dep.get("kind") == "frame_fex": # the launcher's EXIT trap stops Wine (wineserver -k)
|
||||
run(["pkill", "-TERM", "-f", re.escape(os.path.join(ANCHORS, dep["package"], "launch.sh"))])
|
||||
elif dep.get("kind") == "linux":
|
||||
for pid in pcvr_pids(dep["base"]):
|
||||
run(["kill", "-TERM", pid])
|
||||
@@ -5930,6 +5929,8 @@ def cmd_launch_test(args):
|
||||
if is_overlay(dep): # nothing to see without a game; register_vr_overlay's SteamVR check is the test
|
||||
return {"state": "SKIPPED", "skipped": "overlay app", "elapsed": 0, "log": None, "log_size": 0,
|
||||
"crash_log": None, "kind": dep.get("kind")}
|
||||
if dep.get("kind") == "frame_fex":
|
||||
raise AgentError("launch tests aren't available for Rift games with Meta's runtime (experimental) yet")
|
||||
if dep.get("kind") == "pcvr":
|
||||
return launch_test_pcvr(dep, anchor, log, seconds)
|
||||
if dep.get("kind") == "linux":
|
||||
@@ -7912,6 +7913,239 @@ def monitor_session(stdin, stdout, monitor=None, sleep=time.sleep, max_ticks=Non
|
||||
return 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ Rift via FEX (experimental)
|
||||
# Oculus Rift games that need Meta's PC runtime, on the Frame alone: x86_64 GE-Proton under FEX + Meta's runtime +
|
||||
# Revive in one shared Wine prefix. The work is done by fexrift.py (uploaded with its binaries by the PC, see
|
||||
# src/frameport/fexrift.py and native/fexwine/README.md); these commands only start it and track games (kind
|
||||
# "frame_fex"; the game's files stay inside the prefix where Meta's app put them).
|
||||
FEX_ROOT = os.path.join(HOME, ".local/share/frameport/fexrift")
|
||||
FEX_ARTIFACTS = os.path.join(FEX_ROOT, "artifacts")
|
||||
FEX_HELPER = os.path.join(FEX_ARTIFACTS, "fexrift.py")
|
||||
|
||||
|
||||
def fex_helper(command, args=None, timeout=900):
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
raise AgentError("the files for Rift games on the Frame aren't here yet (run the setup first)")
|
||||
# Game inventories can exceed Linux's per-argument limit; the helper already accepts JSON on stdin.
|
||||
p = run([sys.executable, "-I", FEX_HELPER, command], input=json.dumps(args or {}), timeout=timeout)
|
||||
try:
|
||||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||||
except (IndexError, ValueError):
|
||||
raise AgentError(f"fexrift {command} failed: {(p.stderr or p.stdout)[-400:]}") from None
|
||||
if not out.get("ok"):
|
||||
raise AgentError(out.get("error") or f"fexrift {command} failed")
|
||||
return out["result"]
|
||||
|
||||
|
||||
def cmd_fex_prepare(args):
|
||||
"""Where the PC uploads fexrift.py + its binaries (artifacts/) and the login archive (incoming/), and what's
|
||||
already there (unchanged files aren't re-sent)."""
|
||||
incoming = os.path.join(FEX_ROOT, "incoming")
|
||||
for d in (FEX_ARTIFACTS, incoming):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
os.chmod(incoming, 0o700)
|
||||
st = os.statvfs(FEX_ROOT)
|
||||
return {"root": FEX_ROOT, "artifacts": FEX_ARTIFACTS, "incoming": incoming,
|
||||
"existing": tree_manifest(FEX_ARTIFACTS), "free_bytes": st.f_bavail * st.f_frsize,
|
||||
"fex": os.path.isdir(os.path.join(STEAM, "steamapps/common/FEX-Emu"))}
|
||||
|
||||
|
||||
def cmd_fex_status(args):
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
return {"helper": False}
|
||||
res = fex_helper("status", timeout=60)
|
||||
res["helper"] = True
|
||||
return res
|
||||
|
||||
|
||||
def cmd_fex_setup(args):
|
||||
"""Start the setup (GE-Proton download, Meta's runtime from Meta's CDN, the prefix): ~10-20 min, detached;
|
||||
progress via fex_status (setup.state / step / error)."""
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
raise AgentError("upload the files first (fex_prepare)")
|
||||
steps = [s for s in args.get("steps") or [] if re.fullmatch(r"[a-z]+", str(s))]
|
||||
unit = f"frameport-fexsetup-{int(time.time())}"
|
||||
p = run(["systemd-run", "--user", "--collect", "--quiet", f"--unit={unit}", "--setenv=HOME=" + HOME,
|
||||
sys.executable, "-I", FEX_HELPER, "setup", json.dumps({"steps": steps})])
|
||||
if p.returncode != 0:
|
||||
raise AgentError(f"couldn't start the setup: {p.stderr.strip()[-300:]}")
|
||||
return {"started": True, "unit": unit}
|
||||
|
||||
|
||||
def cmd_fex_import_login(args):
|
||||
"""The login archive the PC uploaded to incoming/login.tar (fexrift.py deletes it afterwards)."""
|
||||
return fex_helper("import_login", {"tar": os.path.join(FEX_ROOT, "incoming", "login.tar")})
|
||||
|
||||
|
||||
# Local-copy extension commands: validate removable storage here (the helper has no drive discovery). Never let a
|
||||
# missing mount silently turn an SD-card install into an upload to the root filesystem.
|
||||
def fex_local_dest(dest):
|
||||
if not dest or dest == "internal":
|
||||
return None
|
||||
for d in cmd_drives({})["drives"]:
|
||||
if not d["internal"] and d["path"] == dest:
|
||||
resolve_dest(dest) # existing filesystem / writable-mount checks
|
||||
return dest
|
||||
raise AgentError("--dest must name an available external drive's mount point (see frame drives)")
|
||||
|
||||
|
||||
def cmd_fex_prepare_local(args):
|
||||
pkg = check_pkg(args["package"])
|
||||
dep = deployment(pkg)
|
||||
if dep and (dep.get("kind") != "frame_fex" or dep.get("source") != "local_copy"
|
||||
or dep.get("app") != args.get("app")):
|
||||
raise AgentError(f"{pkg} is already installed in another way; uninstall it first")
|
||||
return fex_helper("prepare_local", dict(args, package=pkg, dest=fex_local_dest(args.get("dest"))))
|
||||
|
||||
|
||||
def cmd_fex_register_local(args):
|
||||
info = fex_helper("local_info", {"app": args["app"]})
|
||||
fex_local_dest(info.get("dest"))
|
||||
return fex_helper("register_local", args, timeout=7200) # hashes large copies on the Frame, not on NTFS
|
||||
|
||||
|
||||
def cmd_fex_runtime_state(args):
|
||||
return fex_helper("runtime_state", {"app": args["app"]}, timeout=120)
|
||||
|
||||
|
||||
# Browser sign-in: this block deliberately uses stdin, not fex_helper's JSON argv. A callback carries credentials.
|
||||
FEX_LOGIN_PACKAGE = "linux.frameport_meta_signin"
|
||||
|
||||
|
||||
def fex_login_call(command, args=None):
|
||||
if not os.path.isfile(FEX_HELPER):
|
||||
raise AgentError("run fex setup before signing in")
|
||||
p = subprocess.run([sys.executable, "-I", FEX_HELPER, command], input=json.dumps(args or {}),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
try:
|
||||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||||
except (ValueError, IndexError):
|
||||
raise AgentError("Meta sign-in helper did not answer") from None
|
||||
if not out.get("ok"):
|
||||
# No raw subprocess output in errors for the commands handling URLs.
|
||||
raise AgentError(out.get("error", "Meta sign-in failed") if command == "login_start" else
|
||||
"Meta sign-in helper failed; retry fex login --stop")
|
||||
return out["result"]
|
||||
|
||||
|
||||
def cmd_fex_login(args):
|
||||
result = fex_login_call("login_start", {"start_now": bool(args.get("start_now")),
|
||||
"via_pc": bool(args.get("via_pc")),
|
||||
"timeout": int(args.get("login_timeout") or 900)})
|
||||
if not args.get("start_now"):
|
||||
anchor = os.path.join(ANCHORS, FEX_LOGIN_PACKAGE)
|
||||
os.makedirs(anchor, exist_ok=True)
|
||||
launcher = os.path.join(anchor, "launch.sh")
|
||||
if os.path.lexists(launcher):
|
||||
os.remove(launcher)
|
||||
os.symlink(result["launcher"], launcher)
|
||||
with open(os.path.join(anchor, "deployment.json"), "w") as f:
|
||||
json.dump({"package": FEX_LOGIN_PACKAGE, "kind": "linux", "title": "Meta sign-in (FramePort)",
|
||||
"base": os.path.join(FEX_ROOT, "login"), "vr": False, "desktop_entry": False,
|
||||
"appid": shortcut_appid(f'"{launcher}"', "Meta sign-in (FramePort)")}, f)
|
||||
result["package"] = FEX_LOGIN_PACKAGE
|
||||
return result
|
||||
|
||||
|
||||
def cmd_fex_login_status(args):
|
||||
return fex_login_call("login_status", {"take_url": bool(args.get("take_url"))})
|
||||
|
||||
|
||||
def cmd_fex_login_callback(args):
|
||||
return fex_login_call("login_callback", {"url": args.get("url")})
|
||||
|
||||
|
||||
def cmd_fex_login_cancel(args):
|
||||
result = fex_login_call("login_stop")
|
||||
anchor = os.path.join(ANCHORS, FEX_LOGIN_PACKAGE)
|
||||
if os.path.exists(os.path.join(anchor, "deployment.json")):
|
||||
cmd_shortcuts({"remove": [{"exe": f'"{anchor}/launch.sh"'}], "restart": True})
|
||||
devkit_unregister(FEX_LOGIN_PACKAGE, steam_running=run(["pgrep", "-x", "steam"]).returncode == 0)
|
||||
remove_tree(anchor)
|
||||
return result
|
||||
|
||||
|
||||
def cmd_fex_install(args):
|
||||
"""A game Meta's app downloaded (now in the prefix) as a Steam game: per-game fixes + launch.sh +
|
||||
deployment.json (kind frame_fex). The Steam shortcut is added like for other games (shortcuts)."""
|
||||
pkg = check_pkg(args["package"])
|
||||
app = str(args.get("app") or "")
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", app):
|
||||
raise AgentError(f"bad app name {app!r}")
|
||||
anchor = os.path.join(ANCHORS, pkg)
|
||||
dep = deployment(pkg)
|
||||
if dep and dep.get("kind") != "frame_fex":
|
||||
raise AgentError(f"{pkg} is already installed in another way")
|
||||
local = None
|
||||
if args.get("source") == "local_copy" or (dep or {}).get("source") == "local_copy":
|
||||
local = fex_helper("local_info", {"app": app})
|
||||
if local.get("package") != pkg or not local.get("registered"):
|
||||
raise AgentError("local copy must be registered for this package first")
|
||||
fex_local_dest(local.get("dest"))
|
||||
msaa = args.get("msaa")
|
||||
res = fex_helper("install", {"app": app, "anchor": anchor, "msaa": msaa if msaa in (1, 2, 4) else None,
|
||||
"force_unverified": bool(args.get("force_unverified"))})
|
||||
title = str(args.get("title") or res.get("title") or pkg).replace("\n", " ")
|
||||
appid = dep["appid"] if dep else shortcut_appid(f'"{anchor}/launch.sh"', title)
|
||||
dep = {"package": pkg, "kind": "frame_fex", "app": app, "appid": int(appid), "title": title,
|
||||
"base": res["game_dir"], "exe": res["exe"], "tags": args.get("tags") or [], "vr": True,
|
||||
"fixes": {k: res.get(k) for k in ("ime_patch", "msaa")}, "verified": bool(res.get("verified")),
|
||||
"checks": res.get("checks") or {},
|
||||
"agent_version": AGENT_VERSION, "installed": int(time.time())}
|
||||
if local:
|
||||
dep.update({k: local.get(k) for k in ("source", "app_id", "app_id_source", "store_app_id", "skipped", "dest")})
|
||||
path = os.path.join(anchor, "deployment.json")
|
||||
with open(path + ".tmp", "w") as f:
|
||||
json.dump(dep, f, indent=1)
|
||||
os.replace(path + ".tmp", path)
|
||||
return {"package": pkg, "appid": dep["appid"], "title": title, "launcher": res["launcher"], "fixes": dep["fixes"],
|
||||
"verified": dep["verified"], "checks": dep["checks"]}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- FEX launch-time settings / bounded bench
|
||||
# Kept separate from install: settings are read at launch and never require changing the game's files.
|
||||
def cmd_fex_settings(args):
|
||||
values = args.get("values")
|
||||
if values is not None and (not isinstance(values, dict) or "app" in values):
|
||||
raise AgentError("values must be a settings object without app")
|
||||
return fex_helper("settings_set" if values is not None else "settings_get",
|
||||
dict(values or {}, app=args["app"]), timeout=60)
|
||||
|
||||
|
||||
def cmd_fex_bench(args):
|
||||
seconds = args.get("seconds", 120)
|
||||
if type(seconds) is not int or not 1 <= seconds <= 120:
|
||||
raise AgentError("seconds must be an integer in 1..120")
|
||||
if game_running(): # includes Lepton containers even when their processes are in another PID namespace
|
||||
raise AgentError("benchmark refused: a game or container is running")
|
||||
app = args["app"]
|
||||
for pkg in sorted(os.listdir(ANCHORS)) if os.path.isdir(ANCHORS) else []:
|
||||
dep = deployment(pkg)
|
||||
if dep and dep.get("kind") == "frame_fex" and dep.get("app") == app:
|
||||
return fex_helper("bench", {"app": app, "seconds": seconds,
|
||||
"launcher": os.path.join(ANCHORS, pkg, "launch.sh")},
|
||||
timeout=seconds + 240) # + the wait for the game to start (START_WAIT)
|
||||
raise AgentError("install this FEX game before benchmarking")
|
||||
|
||||
|
||||
def fex_uninstall(pkg, dep, args):
|
||||
"""Meta-owned games stay; FramePort local copies lose their game folder and manifests, preserving the prefix."""
|
||||
anchor = os.path.join(ANCHORS, pkg)
|
||||
if dep.get("source") == "local_copy":
|
||||
fex_local_dest(dep.get("dest"))
|
||||
fex_helper("unregister_local", {"app": dep["app"], "package": pkg}, timeout=180)
|
||||
removed_sc = False
|
||||
if args.get("remove_shortcut") and steam_users():
|
||||
removed_sc = cmd_shortcuts({"remove": [{"exe": f'"{anchor}/launch.sh"', "appid": dep.get("appid")}]})["started"]
|
||||
if args.get("remove_shortcut"):
|
||||
try:
|
||||
devkit_unregister(pkg, steam_running=run(["pgrep", "-x", "steam"]).returncode == 0)
|
||||
except Exception: # noqa: BLE001 (the files go anyway at the next purge)
|
||||
pass
|
||||
remove_tree(anchor)
|
||||
return {"removed": True, "kept_saves": True, "shortcut_removed": removed_sc}
|
||||
|
||||
|
||||
COMMANDS = {n[4:]: f for n, f in globals().items() if n.startswith("cmd_")}
|
||||
|
||||
|
||||
|
||||
@@ -5,20 +5,19 @@ c9c3da5cac224c38e2210b781bd12b7f97e02bd6b0ebb0ef85543e4ea4b76671 ./arm64-v8a/li
|
||||
18d8d917e83a6d87008f41908eb478dff635e798844e125abd1e8a5cf31805e2 ./arm64-v8a/liba32stub.so
|
||||
d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/libfp_langpack.so
|
||||
69102b128e00fa1809489a7308e09c1505f563bf7091bbdd3fe5a58e4cc96c2c ./arm64-v8a/libfp_luajit.so
|
||||
e2465e8960b4dd591d303e30c9d80b8a7c7c7eeff53736419c7105a697725732 ./arm64-v8a/libfp_ovrp.so
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
7fa29014eed2881ee0d7ac486434de9c5fe46710e640b15f697030ad052acfc4 ./arm64-v8a/libfp_vkext.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
227626fcd96ea31a5c4fbd006e971b40f9b5a1e10d1bb30456f0a2792fa2c9e2 ./arm64-v8a/libglshim.so
|
||||
34ae4e6757adf83942d179a098ae91b4da187fead17e02a3a65dd6507daf2073 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
0f058921e9c1f5b9ef5847cc9e6b80a23e75028c76c1d3bb02d1610b535f81c9 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
f7f8349b0d3e9a29305b4d2aa1eb3ec6b394feff4d3e2ea07b8a65f6df1d15f9 ./arm64-v8a/libvrapi.so
|
||||
71b3df219f331bcb42a483550e35371475b1aa886762ddcf5d0d739d74c51ead ./arm64-v8a/libvrapi_foveation.so
|
||||
c7dea9e3435f36e281341121a9e43102f008cc08bba08191c21bac14912e6b87 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
9e8a7ce599b62a7c607b0d3637e4ec1b1ea1974c3373a7af3f5920ad2adc5e00 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
3e9099b5015e8f486c00bcea9d111ee721faba355a89bcf1df69561e3dc6325c ./digicert-assured-id-root-ca.der
|
||||
e04db8044185c1c77941252e61794e84af7156c449fedcda4a826d7a9fc91989 ./dlls/crypt32.dll
|
||||
ec1387fad1f9b65503077e5aa24e3a3b60b3b04b85fd9412e0f2c451772f9b25 ./dlls/dnsapi.dll
|
||||
00ebfab0f3a066d9c85820332e0ec91cf4040797e580516dcafa048dfe758aa7 ./dlls/sechost.dll
|
||||
41584c41db41991f20a0b55011efa94618e0817ab351579f793ca01dbb488a0e ./dlls/windows.devices.enumeration.dll
|
||||
420b21045ddc4f166733bf6d2d7227f0131e20f161c4568accb2b9c15a6d8e86 ./fp_mem.so
|
||||
f8d6aaaa123337c47837d0be5852f7f6687bb9c4bfaaa108aaab3f04af1ad5b9 ./fp_pipesend.exe
|
||||
13d455b700b08f40eb3e3452ab264bf535cb3d176c92ec9a3324a635ce4f8588 ./windows.devices.wifi.dll
|
||||
941a65274007db76b1c40880117fe7d881db32c5394ad09f80ccc44a08213a75 ./wineserver.arm64
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
-133
@@ -6,6 +6,50 @@
|
||||
# with diagnostics. Launch tests and real play sessions (agent session_log, `frameport session`) are both matched.
|
||||
# Sources: every failure we hit porting 34 games to the Steam Frame (see docs/PLAYBOOK.md).
|
||||
signatures:
|
||||
# FEX Rift compatibility (stream C): PC runtime/renderer failures; never suggest entitlement workarounds.
|
||||
- id: fex-missing-entitlement
|
||||
kind: pcvr
|
||||
pattern: '(?i:Missing entitlement)'
|
||||
severity: info
|
||||
diagnosis: Meta reports that the signed-in account does not own this game. This is not a FramePort bug. Use an account that owns it; entitlement and Platform SDK checks remain intact.
|
||||
suggest: []
|
||||
- id: fex-registration-missing
|
||||
kind: pcvr
|
||||
pattern: '(?i:OVRServer[^\r\n]*install_available|install_available[^\r\n]*OVRServer)'
|
||||
severity: error
|
||||
diagnosis: Meta's runtime sees the app as available to install rather than registered as installed. Reinstall or register the game through the FEX install flow with its matching Meta manifest, then retry.
|
||||
suggest: []
|
||||
- id: fex-imu-fallback
|
||||
kind: pcvr
|
||||
pattern: 'Rejecting new committed universe since IMU fallback is active'
|
||||
severity: warning
|
||||
diagnosis: SteamVR has not acquired headset tracking since boot. Put the headset on once, wait for tracking, then retry the FEX game. A headless launch before that can wait forever.
|
||||
suggest: []
|
||||
- id: fex-librarian-stuck
|
||||
kind: pcvr
|
||||
pattern: '(?s)\A(?=.*(?i:Service invocation completed))(?=.*OVRLibrarian[^\r\n]*(?i:still running|did not exit))'
|
||||
severity: warning
|
||||
diagnosis: OVRLibrarian completed its work but remains alive under FEX and blocks later client registration. End only OVRLibrarian.exe after completion, then retry; the FEX launcher normally performs this cleanup.
|
||||
suggest: []
|
||||
- id: fex-vkd3d-init
|
||||
kind: pcvr
|
||||
pattern: '(?i:vkd3d[^\r\n]*(?:failed to (?:create|initialize)|could not (?:create|initialize)|no (?:suitable|compatible).*(?:device|adapter)))'
|
||||
severity: error
|
||||
diagnosis: The D3D12 route could not initialize vkd3d-proton or a Vulkan device. Check GE's shipped vkd3d-proton DLLs, clear PROTON_NO_D3D12 and PROTON_USE_WINED3D, and verify the FEX Vulkan thunk reaches Turnip. This route still needs a device test.
|
||||
suggest: []
|
||||
- id: fex-wined3d-init
|
||||
kind: pcvr
|
||||
pattern: '(?i:wined3d[^\r\n]*(?:failed to (?:create|initialize|find)|could not (?:create|initialize)|OpenGL support is disabled))'
|
||||
severity: error
|
||||
diagnosis: Wine's OpenGL renderer could not initialize. The FEX route has no configured GL thunk; verify the guest rootfs GL driver and display access. A Vulkan thunk alone does not provide host OpenGL. Prefer the game's D3D9 or D3D11 route when available.
|
||||
suggest: []
|
||||
- id: fex-revive-injector
|
||||
kind: pcvr
|
||||
pattern: '(?i:ReviveInjector[^\r\n]*(?:failed|unable|error (?:code|inject)))'
|
||||
severity: error
|
||||
diagnosis: ReviveInjector could not start or inject the game. Check the game executable and x86_64 Revive files in the shared GE-Proton prefix, and use OpenVR mode (without /openxr). An OpenVR-only game can launch directly. Do not change entitlement or Platform SDK checks.
|
||||
suggest: []
|
||||
# End FEX Rift compatibility (stream C).
|
||||
- id: meta-asset-status-error
|
||||
pattern: 'Asset status error\s+\(code 100\)'
|
||||
severity: error
|
||||
@@ -570,132 +614,6 @@ signatures:
|
||||
the video was decoded in software instead, which is slow for 4K/8K. Turn off hardware video decoding in Steam's
|
||||
settings, restart Steam, then start the game again.
|
||||
suggest: []
|
||||
# ---- Quest games on this PC (AXRB, docs/PC_ANDROID.md): AXRB's host.err/session.json + guest log
|
||||
- id: axrb-no-headset
|
||||
kind: axrb
|
||||
pattern: 'failed to find D3D11 adapter requested by OpenXR runtime|xrCreateInstance failed: XR_ERROR_RUNTIME_FAILURE'
|
||||
severity: fatal
|
||||
diagnosis: The PC's OpenXR runtime (SteamVR) has no headset to show the game on, so AXRB's bridge couldn't open its VR session. Connect the headset (Steam Link / cable) and let SteamVR show it before starting the game.
|
||||
suggest: []
|
||||
- id: axrb-host-not-ready
|
||||
kind: axrb
|
||||
pattern: 'Host pose server did not become ready|Host failed to start'
|
||||
severity: fatal
|
||||
diagnosis: AXRB's Windows bridge (axrb-host-bridge.exe) didn't start, so the game was never launched. Its reason is in host.err (diagnostics); a missing headset is the usual one.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-gfxstream-crash
|
||||
kind: axrb
|
||||
pattern: 'vulkan\.ranchu\.so \(gfxstream::vk::'
|
||||
severity: fatal
|
||||
diagnosis: The game crashed inside the Android emulator's Vulkan driver (gfxstream), reached through AXRB's runtime layer (e.g. Pinball FX VR at its first Vulkan allocations). That's AXRB's GPU path, not FramePort's build - report it to AXRB with the diagnostics.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-translation-crash
|
||||
kind: axrb
|
||||
pattern: 'libndk_translation\.so \(berberis::|DEBUG\s*:.*#00 pc \S+\s+/system/lib64/libndk_translation\.so'
|
||||
severity: fatal
|
||||
diagnosis: The game crashed in Android's ARM translation (libndk_translation), which runs Quest games' arm64 code on the PC's x86 CPU (known for some Unity games, AXRB issue #9). Nothing to patch in the game; AXRB plans another translator.
|
||||
suggest: []
|
||||
# AXRB's direct mode (axrb-direct.exe, no emulator): direct.log lines are "<s> <tid> E/<tag>: ..." from the runner
|
||||
# and "E/<tag>(<tid>): ..." from the game's Android log; direct.out has AppProcess's AXRB_AOSP_* lines
|
||||
- id: axrb-direct-incomplete
|
||||
kind: axrb
|
||||
pattern: 'Direct mode installation is incomplete|Direct mode is missing |AXRB Direct is incomplete|Preparing the AOSP application failed'
|
||||
severity: fatal
|
||||
diagnosis: AXRB's direct mode is missing part of itself or of its Android system root, so the game was never started. Reinstall AXRB-Emulatorless (or rebuild the local build FRAMEPORT_AXRB_DIRECT_DIR points at), then `frameport pc install-axrb --backend direct` sets the system root up again.
|
||||
suggest: []
|
||||
- id: axrb-direct-no-vulkan
|
||||
kind: axrb
|
||||
pattern: 'vulkan-1\.dll is not installed|E/vulkan: \S+ is not available on the host'
|
||||
severity: fatal
|
||||
diagnosis: AXRB's direct mode found no usable Vulkan driver on this PC (or the driver lacks a function the game calls). Install the graphics card's current driver.
|
||||
suggest: []
|
||||
- id: axrb-direct-no-gles
|
||||
kind: axrb
|
||||
pattern: 'eglInitialize\b.*(?:fail|0x3008)|Failed to initiali[sz]e EGL|Unable to initiali[sz]e (?:EGL|the OpenGL)|EGL_BAD_DISPLAY|Unable to retrieve the default EGL display'
|
||||
# Unreal Vulkan builds probe EGL at start too (eglInitialize -> EGL_BAD_DISPLAY in direct mode), then create
|
||||
# their Vulkan device: only a session without one is a GLES game (Deadpool VR, 2026-10-15)
|
||||
# ... and one that died first on something AXRB's direct mode lacks (a dialog window, a framework call) never
|
||||
# got that far: that is the blocker, not EGL (Of Lies and Rain, NOPE CHALLENGE, 2026-10-16)
|
||||
unless: '(?m)I/vulkan: vkCreateDevice\(.*\) = 0$|UnsupportedOperationException: addToDisplayAsUser|^AXRB_SERVICE_UNIMPLEMENTED '
|
||||
severity: fatal
|
||||
diagnosis: The game draws with OpenGL ES, and AXRB's direct mode only has a Vulkan renderer. Install it for AXRB's emulator instead (the "AXRB mode on this PC" option, pc.axrb_backend backend=emulator, or `frameport install <game> --to pc --axrb-backend emulator`).
|
||||
suggest: []
|
||||
- id: axrb-direct-no-native-window
|
||||
kind: axrb
|
||||
pattern: '(?m)^AXRB_AOSP_HOST_SURFACES 0'
|
||||
unless: 'APP_CMD_INIT_WINDOW|AXRB_AOSP_TAKEN_SURFACE|AXRB\.Space|session state='
|
||||
severity: warning
|
||||
diagnosis: The game's activity got no window in AXRB's direct mode. NativeActivity games (CryEngine, other own engines) take their surface with Window.takeSurface, which AXRB's direct mode doesn't serve yet (it only hands SurfaceViews one), so they wait for APP_CMD_INIT_WINDOW forever and never start VR (The Climb 2 stopped after "Loading plug-in CryHTTP"). That's AXRB's side; AXRB's emulator may run the game.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-vulkan-extensions
|
||||
kind: axrb
|
||||
pattern: 'Renderer initialization[\s\S]{0,600}?CrySystem initialization failed'
|
||||
unless: 'fp_vkext: listing'
|
||||
severity: fatal
|
||||
diagnosis: CryEngine found its Vulkan device without extensions every Quest has (VK_EXT_fragment_density_map, Android's external memory) and gave up before creating it; the PC's graphics driver doesn't have them. "Vulkan - list Android's external-memory extensions (this PC)" lists them for the game in builds for this PC, and AXRB leaves them out again when the device is created.
|
||||
suggest: [frame.vk_android_extensions]
|
||||
- id: axrb-direct-unhandled-instruction
|
||||
kind: axrb
|
||||
pattern: 'fatal signal 4 \(SIGILL\)|unhandled synchronous signal 4 '
|
||||
severity: fatal
|
||||
diagnosis: AXRB's direct mode met an arm64 instruction its translator can't run (Dynarmic, or Digitalis when chosen) and the game stopped. That's AXRB's side - report it to AXRB with the diagnostics (direct.log has the instruction's address); the other translator (pc.axrb_backend translator=digitalis / dynarmic) or AXRB's emulator may get further.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-guest-crash
|
||||
kind: axrb
|
||||
pattern: 'E/cpu: thread \d+ \(.*\): fatal signal (?:5|6|7|11) '
|
||||
severity: fatal
|
||||
diagnosis: The game's own arm64 code crashed in AXRB's direct mode (a SIGSEGV/SIGBUS/SIGABRT, or the SIGTRAP Unity raises on a fatal error; direct.log has the backtrace by library). Often a missing Android service or a translation bug rather than the game; try the other translator (pc.axrb_backend translator=digitalis) or AXRB's emulator, and report it to AXRB with the diagnostics.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-host-fault
|
||||
kind: axrb
|
||||
pattern: 'E/host-fault: |E/fatal: '
|
||||
severity: fatal
|
||||
diagnosis: axrb-direct.exe itself crashed (its translator or Linux/Binder layer, not the game). That's AXRB's side - report it to AXRB with the diagnostics.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-dialog-window
|
||||
kind: axrb
|
||||
pattern: 'UnsupportedOperationException: addToDisplayAsUser'
|
||||
severity: fatal
|
||||
diagnosis: The game opened a second Android window (a Dialog, for example Unreal's message box or a Unity plugin's dialog) and AXRB's direct mode can only show the game's own activity, so the game stopped ("Adding window failed"). Seen with Medieval Dynasty, Under Cover and Of Lies and Rain. That's AXRB's side; AXRB's emulator shows dialogs, so `--axrb-backend emulator` may get further (Vulkan games there go through gfxstream).
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-unimplemented-service
|
||||
kind: axrb
|
||||
pattern: '(?m)^AXRB_SERVICE_UNIMPLEMENTED \S+'
|
||||
severity: error
|
||||
diagnosis: The game (or a library in it, often Google Play services) called an Android framework service function AXRB's direct mode doesn't implement yet (the line names it, e.g. IActivityManager.getMyMemoryState for NOPE CHALLENGE); the call throws, and an uncaught one ends the game. That's AXRB's side - report it to AXRB with the diagnostics; AXRB's emulator has the full framework.
|
||||
suggest: []
|
||||
report: true
|
||||
- id: axrb-direct-java-crash
|
||||
kind: axrb
|
||||
pattern: 'E/app: the application crashed|FATAL EXCEPTION: |Runtime aborting|JNI DETECTED ERROR IN APPLICATION'
|
||||
severity: fatal
|
||||
diagnosis: The game's Java side (or Android's ART runtime under it) ended with an uncaught exception or an abort in AXRB's direct mode. direct.log / direct.err have the stack; a framework service AXRB's direct mode doesn't provide yet (Meta/Play services, account, DRM) is the usual cause.
|
||||
suggest: []
|
||||
- id: axrb-direct-missing-function
|
||||
kind: axrb
|
||||
pattern: 'guest called unknown host function|unimplemented JNI function|call to unregistered host function'
|
||||
severity: error
|
||||
diagnosis: The game called an Android/JNI function AXRB's direct mode doesn't implement yet; it may fail right after. Report it to AXRB with the diagnostics.
|
||||
suggest: []
|
||||
- id: axrb-direct-unsupported-syscall
|
||||
kind: axrb
|
||||
pattern: 'W/syscall: unimplemented syscall|W/binder: unsupported (?:command|ioctl|object type)|execve\(.*\) refused: one process image only'
|
||||
severity: warning
|
||||
diagnosis: The game asked for a Linux system call or Binder feature (or a second process) AXRB's direct mode doesn't provide. Often harmless; when the game then fails, it's the first thing to report to AXRB.
|
||||
suggest: []
|
||||
- id: axrb-direct-slow-stop
|
||||
kind: axrb
|
||||
pattern: 'the game did not close within 25 seconds'
|
||||
severity: info
|
||||
diagnosis: The game didn't finish its activity shutdown when the session ended, so AXRB stopped it; anything it hadn't saved yet may be lost.
|
||||
suggest: []
|
||||
# Launch tests run without a worn headset. Some parts give up on tracking after a while and end the VR session; what
|
||||
# the game does after that (often a crash in the GPU driver while its renderer tears down) never happens in the
|
||||
# headset. In a launch test that already had paced frames before such a line, a later exit or crash still passes,
|
||||
@@ -704,13 +622,6 @@ headless_end:
|
||||
- label: the VrApi bridge's 30 s tracking deadline
|
||||
pattern: 'EnterVrMode: no tracked head pose before deadline'
|
||||
milestones: # progress markers, in order; the furthest one reached is reported
|
||||
- {id: axrb-host, kind: axrb, pattern: 'AXRB TCP: listening', label: AXRB bridge started}
|
||||
- {id: axrb-direct-art, kind: axrb, pattern: '(?m)^AXRB_AOSP_BOOTSTRAP', label: Android runtime (ART) started (direct mode)}
|
||||
- {id: axrb-direct-activity, kind: axrb, pattern: '(?m)^AXRB_AOSP_ACTIVITY_CREATED ', label: Game activity created (direct mode)}
|
||||
- {id: axrb-runtime, kind: axrb, pattern: 'AXRB\.PoseClient|Got runtime: package: com\.axrb\.openxrruntime', label: AXRB's OpenXR runtime loaded}
|
||||
- {id: axrb-instance, kind: axrb, pattern: 'xrCreateInstance\(\) succeeded|LoaderInstance::CreateInstance succeeded', label: OpenXR instance created}
|
||||
- {id: axrb-direct-vulkan, kind: axrb, pattern: '(?m)I/vulkan: vkCreateDevice\(.*\) = 0$', label: Vulkan device created on the PC's GPU (direct mode)}
|
||||
- {id: axrb-frames, kind: axrb, pattern: 'AXRB\.Perf end-frame', label: Submitting frames}
|
||||
- {id: pcvr-launch, kind: pcvr, pattern: 'FramePort: launching|Launched injector with', label: Launcher started}
|
||||
- {id: pcvr-oculus-hmd, kind: pcvr, pattern: 'FramePort oculushmd: OculusHMDConnected event ready', label: Oculus headset event provided}
|
||||
- {id: pcvr-inject, kind: pcvr, pattern: 'Succesfully injected|Successfully injected', label: Revive injected into the game}
|
||||
|
||||
+95
-173
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 174 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
Every patch and game setting FramePort knows: 169 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
|
||||
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
|
||||
|
||||
@@ -11,9 +11,9 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
## Groups
|
||||
|
||||
- [Patches from OVRPort](#group-overport) (25)
|
||||
- [Steam Frame patches](#group-frame) (56)
|
||||
- [Game settings](#group-adapter) (71)
|
||||
- [Files and settings on the Frame](#group-device) (9)
|
||||
- [Steam Frame patches](#group-frame) (55)
|
||||
- [Game settings](#group-adapter) (68)
|
||||
- [Files and settings on the Frame](#group-device) (8)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
|
||||
## Index
|
||||
@@ -96,7 +96,6 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Unreal: thumb follows the capacitive touches](#frame-unreal_thumb_touch) | `frame.unreal_thumb_touch` | Steam Frame patches | 7 |
|
||||
| [Vermillion: paint canvases in the scene](#frame-vermillion_scene_canvas) | `frame.vermillion_scene_canvas` | Steam Frame patches | 1 |
|
||||
| [Vivox voice chat: skip Android 12 audio routing](#frame-vivox_audio_route) | `frame.vivox_audio_route` | Steam Frame patches | 1 |
|
||||
| [Vulkan: list Android's external-memory extensions (this PC)](#frame-vk_android_extensions) | `frame.vk_android_extensions` | Steam Frame patches | |
|
||||
| [Vulkan: drop invalid render-pass pointers](#frame-vk_sanitize) | `frame.vk_sanitize` | Steam Frame patches | default · 1 |
|
||||
| [VrApi → OpenXR bridge](#frame-vrapi_bridge) | `frame.vrapi_bridge` | Steam Frame patches | 3 |
|
||||
| [Quiet Meta's VrApi loader (OpenXR games)](#frame-vrapi_stub) | `frame.vrapi_stub` | Steam Frame patches | 2 |
|
||||
@@ -137,20 +136,17 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Vulkan format workaround](#adapter-mutable_fix) | `adapter.mutable_fix` | Game settings | |
|
||||
| [Unity frame loop: wait for skipped frames](#adapter-ovrp_begin_gate) | `adapter.ovrp_begin_gate` | Game settings | 1 |
|
||||
| [Steadier hands (older Unity)](#adapter-ovrp_hold_physics) | `adapter.ovrp_hold_physics` | Game settings | 1 |
|
||||
| [Unity frame loop diagnostics (log)](#adapter-ovrp_trace) | `adapter.ovrp_trace` | Game settings | |
|
||||
| [See your room](#adapter-passthrough_emul) | `adapter.passthrough_emul` | Game settings | |
|
||||
| [Steadier head tracking](#adapter-pose_consistency) | `adapter.pose_consistency` | Game settings | 1 |
|
||||
| [Pose timing diagnostics (log)](#adapter-pose_debug) | `adapter.pose_debug` | Game settings | |
|
||||
| [Fix hands lagging behind controllers](#adapter-pose_time_fix) | `adapter.pose_time_fix` | Game settings | 14 |
|
||||
| [Treat newer Quest controllers as Touch](#adapter-profile_remap) | `adapter.profile_remap` | Game settings | |
|
||||
| [Main picture only](#adapter-projection_only) | `adapter.projection_only` | Game settings | |
|
||||
| [Animate fingers from touch](#adapter-proximity_emul) | `adapter.proximity_emul` | Game settings | |
|
||||
| [Keep pictures in bounds](#adapter-rect_clamp) | `adapter.rect_clamp` | Game settings | |
|
||||
| [Refresh rate](#adapter-refresh_rate) | `adapter.refresh_rate` | Game settings | 3 |
|
||||
| [Wait for the game's GPU](#adapter-release_wait) | `adapter.release_wait` | Game settings | |
|
||||
| [Re-create play space](#adapter-respace_kick) | `adapter.respace_kick` | Game settings | |
|
||||
| [Sharpness](#adapter-scale) | `adapter.scale` | Game settings | 4 |
|
||||
| [Cap the eye size](#adapter-scale_max) | `adapter.scale_max` | Game settings | |
|
||||
| [Room depth](#adapter-scene_depth) | `adapter.scene_depth` | Game settings | |
|
||||
| [Pretend room layout](#adapter-scene_emul) | `adapter.scene_emul` | Game settings | 9 |
|
||||
| [Room height](#adapter-scene_height) | `adapter.scene_height` | Game settings | |
|
||||
@@ -180,7 +176,6 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Extra Lepton settings](#device-lepton_env) | `device.lepton_env` | Files and settings on the Frame | 3 |
|
||||
| [Controllers as a gamepad (Steam Input)](#device-steam_gamepad) | `device.steam_gamepad` | Files and settings on the Frame | |
|
||||
| [Show the app's Android window (for typing)](#device-text_input_window) | `device.text_input_window` | Files and settings on the Frame | 88 |
|
||||
| [AXRB mode on this PC](#pc-axrb_backend) | `pc.axrb_backend` | Files and settings on the Frame | |
|
||||
| [Launch options (SteamVR or OpenXR)](#pcvr-launch_args) | `pcvr.launch_args` | PC VR (Proton and Revive) | 2 |
|
||||
| [Provide the Oculus runtime from Revive (Frame)](#pcvr-libovr_redirect) | `pcvr.libovr_redirect` | PC VR (Proton and Revive) | |
|
||||
| [No Unreal crash reporter](#pcvr-no_crash_reporter) | `pcvr.no_crash_reporter` | PC VR (Proton and Revive) | |
|
||||
@@ -274,12 +269,12 @@ Removes a controller position adjustment, for games whose hands look misplaced.
|
||||
|
||||
Stops Meta's audio library from reporting to Meta (only matters on PC emulators).
|
||||
|
||||
- `patch_disable_meta_xr_audio_telemetry` · builds for this PC (AXRB) add it when needed
|
||||
- `patch_disable_meta_xr_audio_telemetry`
|
||||
- **Changes:** Changes the APK (during the OVRPort conversion).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Source:** [patches/overport.py](../src/frameport/patches/overport.py)
|
||||
|
||||
> Skips Meta XR Audio's telemetry under x86_64 ARM translation (emulators). Not needed on the Frame, which runs games natively; added by itself to builds for this PC (AXRB) of games with Meta XR Audio.
|
||||
> Skips Meta XR Audio's telemetry under x86_64 ARM translation (emulators). Not needed on the Frame, which runs games natively.
|
||||
|
||||
### <a id="patch_disable_space_warp"></a>Disable application space warp if used
|
||||
|
||||
@@ -511,7 +506,7 @@ Runs a 32-bit game (Gear VR, Oculus Go, early Quest) on the Frame, which only ru
|
||||
|
||||
FramePort's layer between the game and the Steam Frame: fills in what the Frame lacks (controllers, room, passthrough). Needed for every converted game.
|
||||
|
||||
- `frame.adapter` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.adapter` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Required for every OpenXR/overport build on the Frame.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -528,7 +523,7 @@ FramePort's layer between the game and the Steam Frame: fills in what the Frame
|
||||
|
||||
Lets the game find extra content it keeps next to its data (for example Star Wars Tales' seasons).
|
||||
|
||||
- `frame.asset_files` · also in builds for this PC (AXRB)
|
||||
- `frame.asset_files`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** The game asks Meta's platform for asset files and its data has … content file(s) (*.pak) that OVRPort's loader doesn't report.
|
||||
- **Catalog games (1):** Star Wars: Tales from the Galaxy's Edge.
|
||||
@@ -540,7 +535,7 @@ Lets the game find extra content it keeps next to its data (for example Star War
|
||||
|
||||
Experimental bypass for a failed Meta asset-file check at startup. Reports an empty asset list and installed status without a file path; does not provide DLC. Experimental.
|
||||
|
||||
- `frame.asset_files_stub` · experimental · also in builds for this PC (AXRB)
|
||||
- `frame.asset_files_stub` · experimental
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -553,7 +548,7 @@ Experimental bypass for a failed Meta asset-file check at startup. Reports an em
|
||||
|
||||
Lets games start that would stop at Meta's avatars (for example BlazeRush). No Meta avatars.
|
||||
|
||||
- `frame.avatar_stub` · also in builds for this PC (AXRB)
|
||||
- `frame.avatar_stub`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (1):** BlazeRush.
|
||||
@@ -581,7 +576,7 @@ Stops bHaptics from asking for a Bluetooth permission the Frame doesn't have (th
|
||||
|
||||
Makes BlazeRush's menus and controllers work without Meta's avatars, and lets its shader fixes take effect.
|
||||
|
||||
- `frame.blazerush` · also in builds for this PC (AXRB)
|
||||
- `frame.blazerush`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Only for its game's build:** outside it the whole OVRPort run fails, so recipes only use it where it applies.
|
||||
@@ -594,7 +589,7 @@ Makes BlazeRush's menus and controllers work without Meta's avatars, and lets it
|
||||
|
||||
Stops Epic Online Services from retrying its login keychain forever (the game would never finish starting).
|
||||
|
||||
- `frame.eos_keychain_loop` · also for 2D apps · also in builds for this PC (AXRB, experimental)
|
||||
- `frame.eos_keychain_loop` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -619,7 +614,7 @@ Experimental: shows HUDs, menus and other flat panels that stay black on the Fra
|
||||
|
||||
Corrects graphics code the Frame's drivers reject (black screen with sound).
|
||||
|
||||
- `frame.gl_shim` · experimental · also in builds for this PC (AXRB, experimental)
|
||||
- `frame.gl_shim` · experimental
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** GLES engine resolving GL through eglGetProcAddress (GLAD) on the VrApi bridge.
|
||||
- **Requires:** [`frame.vrapi_bridge`](#frame-vrapi_bridge)
|
||||
@@ -659,7 +654,7 @@ Plays the game's videos on the Frame's video hardware, so 4K and 8K video runs s
|
||||
|
||||
Lets the game use language files already in its data instead of waiting for a download that never comes.
|
||||
|
||||
- `frame.langpacks` · experimental · also in builds for this PC (AXRB)
|
||||
- `frame.langpacks` · experimental
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Source:** [patches/frame/langpack.py](../src/frameport/patches/frame/langpack.py)
|
||||
@@ -670,7 +665,7 @@ Lets the game use language files already in its data instead of waiting for a do
|
||||
|
||||
Makes the game start from the Frame's Steam library.
|
||||
|
||||
- `frame.launcher` · on by default · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.launcher` · on by default · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Manifest uses category INFO only; Lepton needs LAUNCHER.
|
||||
- **Suggested (on):** Applied automatically if the manifest needs it.
|
||||
@@ -708,7 +703,7 @@ Lets games with a verified older Lua engine get past a startup crash on the Fram
|
||||
|
||||
Declares Meta's permissions some mixed-reality games ask for, so they don't stop.
|
||||
|
||||
- `frame.meta_permissions` · also in builds for this PC (AXRB)
|
||||
- `frame.meta_permissions`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Mixed-reality game that needs the room model: its 'use spatial data' permission must be granted (for example Demeter).
|
||||
- **Catalog games (2):** Arkio, Demeter.
|
||||
@@ -720,7 +715,7 @@ Declares Meta's permissions some mixed-reality games ask for, so they don't stop
|
||||
|
||||
Skips a Quest-only reporting step in Meta's audio library that crashes some games.
|
||||
|
||||
- `frame.metaxr_telemetry` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.metaxr_telemetry` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Meta XR Audio present; patched automatically if it has the telemetry lookup.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -733,7 +728,7 @@ Skips a Quest-only reporting step in Meta's audio library that crashes some game
|
||||
|
||||
Stops strict debugging checks that make some games quit.
|
||||
|
||||
- `frame.nodebug` · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.nodebug` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unreal Engine ….…: older UE4 makes JNI calls CheckJNI rejects (for example Time Stall aborted on GetStringUTFChars(NULL)).
|
||||
- **Suggested (on):** Unreal build of Meta XR Audio: its telemetry lookup leaves a pending JNI exception that CheckJNI turns into an abort (for example NOPE Challenge).
|
||||
@@ -749,7 +744,7 @@ Stops strict debugging checks that make some games quit.
|
||||
|
||||
Provides stand-ins for Quest system calls some games make at start.
|
||||
|
||||
- `frame.oculusos` · also in builds for this PC (AXRB)
|
||||
- `frame.oculusos`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** The Unreal build of Meta XR Audio looks up com.oculus.os.AnalyticsEvent and aborts without it (for example NOPE Challenge).
|
||||
- **Suggested (on):** Several Meta SDK libraries look up com.oculus.os.AnalyticsEvent (…); games like this abort without the stub (for example Nano).
|
||||
@@ -765,7 +760,7 @@ Provides stand-ins for Quest system calls some games make at start.
|
||||
|
||||
Stops a crash a few seconds after the logo in games with voice chat.
|
||||
|
||||
- `frame.ovr_microphone` · also in builds for this PC (AXRB)
|
||||
- `frame.ovr_microphone`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** The game uses Meta's microphone API (ovr_Microphone_GetOutputBufferMaxSize), which crashes in OVRPort's library before the microphone is started.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -778,7 +773,7 @@ Stops a crash a few seconds after the logo in games with voice chat.
|
||||
|
||||
Keeps your progress in games that file their saves under your Meta user.
|
||||
|
||||
- `frame.ovr_stable_user` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.ovr_stable_user` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** The game uses Meta's platform library: OVRPort's version gives it a new user id at every start, which loses saves filed under that id.
|
||||
- **Source:** [patches/frame/ovr_stable_user.py](../src/frameport/patches/frame/ovr_stable_user.py)
|
||||
@@ -789,7 +784,7 @@ Keeps your progress in games that file their saves under your Meta user.
|
||||
|
||||
For troubleshooting: records the game's requests to Meta's online services.
|
||||
|
||||
- `frame.ovr_trace` · experimental · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.ovr_trace` · experimental · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Source:** [patches/frame/ovr_trace.py](../src/frameport/patches/frame/ovr_trace.py)
|
||||
@@ -800,7 +795,7 @@ For troubleshooting: records the game's requests to Meta's online services.
|
||||
|
||||
Adds a small piece of Meta's platform library that some games need to start.
|
||||
|
||||
- `frame.ovrplatformcompat` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.ovrplatformcompat` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Applied automatically when the OVRPort build needs it.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -813,7 +808,7 @@ Adds a small piece of Meta's platform library that some games need to start.
|
||||
|
||||
Provides stand-ins for Meta store functions the game expects, so it doesn't crash at start.
|
||||
|
||||
- `frame.ovrstubs` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.ovrstubs` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Applied automatically when the OVRPort build needs it.
|
||||
- **Suggested by launch-test signatures:**
|
||||
@@ -880,7 +875,7 @@ Opens the app straight in VR, skipping its flat launcher screen. Turn off to use
|
||||
|
||||
Allows very large pictures (8K video, theaters) instead of quitting.
|
||||
|
||||
- `frame.swapchain_limit` · on by default · also in builds for this PC (AXRB)
|
||||
- `frame.swapchain_limit` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Video player (…): creates swapchains larger than 4096 px (8K video, theater textures), which OVRPort's dispatcher aborts on.
|
||||
- **Suggested (on):** Recommended for every game: OVRPort aborts on swapchains larger than 4096 px; the Frame's runtime handles them.
|
||||
@@ -920,7 +915,7 @@ Stops a gray or frozen screen in Unity games that turn on anti-aliasing while ru
|
||||
|
||||
Draws each eye separately. Try it when one eye shows a gray or broken picture.
|
||||
|
||||
- `frame.unity_multipass` · also in builds for this PC (AXRB, experimental) · revision 2
|
||||
- `frame.unity_multipass` · revision 2
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (1):** I Am Cat.
|
||||
@@ -961,7 +956,7 @@ Stops a gray screen crash in some Unity games by skipping their overlay layers (
|
||||
|
||||
Lets older Unity games start VR without Meta's system apps (they'd stay on a 2D screen).
|
||||
|
||||
- `frame.unity_oculus_check` · also in builds for this PC (AXRB) · revision 5
|
||||
- `frame.unity_oculus_check` · revision 5
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity with built-in Oculus support: it checks for Meta's system apps before starting VR, else it runs as a 2D app (for example Accounting+, BattleSisters).
|
||||
- **Catalog games (12):** Accounting+, Audica, BattleSisters, Cosmodread, End Space, Eye of the Temple, Jurassic World Aftermath Collection, Mini Motor Racing X and 4 more.
|
||||
@@ -991,7 +986,7 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
|
||||
|
||||
Lets you type into this app's text fields on the Frame (they'd close at once).
|
||||
|
||||
- `frame.unity_text_input` · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.unity_text_input` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity app with text fields (…): they close at once on the Frame because it has no system keyboard.
|
||||
- **Catalog games (85):** AgeOfJoy, AllInOneSports, Ancient_Dungeon, Arkio, Audica, AugmentedEmpire, Battlemarked, BattleSisters and 77 more.
|
||||
@@ -1003,7 +998,7 @@ Lets you type into this app's text fields on the Frame (they'd close at once).
|
||||
|
||||
Lets Unity 6 games with many native libraries start when they run out of thread keys (they'd abort with "cannot create thread specific key"), for example Wander.
|
||||
|
||||
- `frame.unity_tls_key_pool` · also for 2D apps · also in builds for this PC (AXRB, experimental)
|
||||
- `frame.unity_tls_key_pool` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Offered (off):** Unity 6 with …: turn on if the game aborts at start with "cannot create thread specific key" (for example Wander).
|
||||
- **Catalog games (1):** Wander.
|
||||
@@ -1057,7 +1052,7 @@ Shows the full environment when its upper part is clipped but menu panels look c
|
||||
|
||||
Lets older Unreal games start VR on the Frame (for example Star Wars Pinball VR).
|
||||
|
||||
- `frame.unreal_ovrp_entrypoints` · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_ovrp_entrypoints`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unreal looks up OVRPlugin function(s) OVRPort's OVRPlugin lacks: ……. Without them Unreal starts no VR.
|
||||
- **Catalog games (2):** Ghostbusters: Rise of the Ghost Lord, Star Wars Pinball VR.
|
||||
@@ -1069,7 +1064,7 @@ Lets older Unreal games start VR on the Frame (for example Star Wars Pinball VR)
|
||||
|
||||
Makes grip, trigger and touch controls reach the game's hands (it picked the empty Gear VR controls on the Frame, for example Vader Immortal).
|
||||
|
||||
- `frame.unreal_quest_keymap` · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_quest_keymap`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (3):** Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
|
||||
@@ -1081,7 +1076,7 @@ Makes grip, trigger and touch controls reach the game's hands (it picked the emp
|
||||
|
||||
Lets the game leave its loading screen: it waited for a Quest-only step that never starts on the Frame (for example Vader Immortal).
|
||||
|
||||
- `frame.unreal_quest_precompile` · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_quest_precompile`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (3):** Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
|
||||
@@ -1093,7 +1088,7 @@ Lets the game leave its loading screen: it waited for a Quest-only step that nev
|
||||
|
||||
Starts Unreal games without re-checking their data file (they'd seem to hang on an invisible check screen).
|
||||
|
||||
- `frame.unreal_skip_obb_check` · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_skip_obb_check` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unreal checks the OBB's contents before starting (bVerifyOBBOnStartUp); on the Frame a failed or slow check can't be seen and the game seems to hang.
|
||||
- **Catalog games (5):** Dance Central, Kill It With Fire VR, MacacoVR, Maestro, Ragnarock.
|
||||
@@ -1107,7 +1102,7 @@ Starts Unreal games without re-checking their data file (they'd seem to hang on
|
||||
|
||||
Skips a separate data-file check when an Unreal game still hangs at startup with the content check off. Experimental; the game still needs its data.
|
||||
|
||||
- `frame.unreal_skip_obb_delivery` · experimental · also for 2D apps · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_skip_obb_delivery` · experimental · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (1):** Medal of Honor: Above and Beyond.
|
||||
@@ -1135,7 +1130,7 @@ Lets the game finish loading when its separate loading splash stalls the render
|
||||
|
||||
Makes the hands' thumbs follow what your thumb touches (they always pointed up: the Frame doesn't sense a thumb hovering, for example Vader Immortal).
|
||||
|
||||
- `frame.unreal_thumb_touch` · also in builds for this PC (AXRB)
|
||||
- `frame.unreal_thumb_touch`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Catalog games (7):** Phantom: Covert Ops, Robo Recall, Star Wars: Tales from the Galaxy's Edge, Time Stall, Vader Immortal: Episode I, Vader Immortal: Episode II, Vader Immortal: Episode III.
|
||||
@@ -1169,24 +1164,11 @@ Lets games with Vivox voice chat start on the Frame (they'd crash on an Android
|
||||
|
||||
> Vivox's voice-chat SDK (for example Green Hell VR) checks the audio route with Android 12 methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the game crashes at start with a NoSuchMethodError. Those Vivox methods return at once instead (an in-place edit of classes.dex); voice chat keeps the default audio route.
|
||||
|
||||
### <a id="frame-vk_android_extensions"></a>Vulkan: list Android's external-memory extensions (this PC)
|
||||
|
||||
On this PC only: lets games that draw with Vulkan themselves (for example The Climb 2) start in AXRB's direct mode.
|
||||
|
||||
- `frame.vk_android_extensions` · also in builds for this PC (AXRB)
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `axrb-direct-vulkan-extensions` (fatal): CryEngine found its Vulkan device without extensions every Quest has (VK_EXT_fragment_density_map, Android's external memory) and gave up before creating it; the PC's graphics driver doesn't have them. "Vulkan - list Android's external-memory extensions (this PC)" lists them for the game in builds for this PC, and AXRB leaves them out again when the device is created.
|
||||
- **Source:** [patches/frame/vk_android_extensions.py](../src/frameport/patches/frame/vk_android_extensions.py)
|
||||
|
||||
> Only in builds for this PC (AXRB's direct mode). Direct mode gives the game the Windows graphics driver's Vulkan device, which lacks extensions every Quest has: Android's external memory (VK_KHR_external_memory_fd, VK_ANDROID_external_memory_android_hardware_buffer, VK_EXT_queue_family_foreign) and VK_EXT_fragment_density_map. Engines that call VrApi with Vulkan themselves, such as CryEngine in The Climb 2, refuse to create their device without them ("CrySystem initialization failed" right after "Renderer initialization"). The game's renderer gets them listed (libfp_vkext.so); AXRB leaves them out again when the device is created.
|
||||
|
||||
### <a id="frame-vk_sanitize"></a>Vulkan: drop invalid render-pass pointers
|
||||
|
||||
Cleans up graphics data that crashes some Unreal games on the Frame.
|
||||
|
||||
- `frame.vk_sanitize` · on by default · also in builds for this PC (AXRB, experimental)
|
||||
- `frame.vk_sanitize` · on by default
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Own-engine game: applied when one of its libraries loads Vulkan by name (for example Roblox); keeps Lepton's Fossilize layer from crashing on uninitialized pointers.
|
||||
- **Suggested (on):** Unreal game: applied automatically when the engine loads Vulkan by name; keeps Lepton's Fossilize layer from crashing on uninitialized pointers (for example Deadpool VR).
|
||||
@@ -1202,7 +1184,7 @@ Cleans up graphics data that crashes some Unreal games on the Frame.
|
||||
|
||||
Lets games built on Meta's oldest VR interface run on the Frame.
|
||||
|
||||
- `frame.vrapi_bridge` · experimental · also in builds for this PC (AXRB)
|
||||
- `frame.vrapi_bridge` · experimental
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** The engine calls libvrapi.so directly; OVRPort can't translate it.
|
||||
- **Conflicts with:** [`frame.vrapi_stub`](#frame-vrapi_stub)
|
||||
@@ -1218,7 +1200,7 @@ Lets games built on Meta's oldest VR interface run on the Frame.
|
||||
|
||||
Stops Meta's old VR library from closing the game at start (for example Jurassic World Aftermath).
|
||||
|
||||
- `frame.vrapi_stub` · also in builds for this PC (AXRB)
|
||||
- `frame.vrapi_stub`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Conflicts with:** [`frame.vrapi_bridge`](#frame-vrapi_bridge)
|
||||
@@ -1249,7 +1231,7 @@ Settings of FrameBridge, FramePort's OpenXR layer inside every converted Quest g
|
||||
|
||||
Moves where the pointer starts, forward or back.
|
||||
|
||||
- `adapter.aim_forward` · also in builds for this PC (AXRB)
|
||||
- `adapter.aim_forward`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1263,7 +1245,7 @@ Moves where the pointer starts, forward or back.
|
||||
|
||||
If the pointer doesn't hit what you aim at, tilt it up or down.
|
||||
|
||||
- `adapter.aim_pitch` · also in builds for this PC (AXRB)
|
||||
- `adapter.aim_pitch`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1277,7 +1259,7 @@ If the pointer doesn't hit what you aim at, tilt it up or down.
|
||||
|
||||
Turns the pointer left or right.
|
||||
|
||||
- `adapter.aim_yaw` · also in builds for this PC (AXRB)
|
||||
- `adapter.aim_yaw`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1291,7 +1273,7 @@ Turns the pointer left or right.
|
||||
|
||||
Turn off only for games you play with your hands instead of controllers.
|
||||
|
||||
- `adapter.controller_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.controller_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested (on), value `0`:** Hand tracking is required by the game: pass hands through instead of reporting Touch controllers (for example Silhouette).
|
||||
- **Options:**
|
||||
@@ -1323,7 +1305,7 @@ For games that show the headset's own controllers. Takes effect after a reinstal
|
||||
|
||||
Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.
|
||||
|
||||
- `adapter.cube_standin` · also in builds for this PC (AXRB)
|
||||
- `adapter.cube_standin`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1337,7 +1319,7 @@ Keeps games running that use a 360° cube layer the Frame lacks (for example Bud
|
||||
|
||||
The Frame can't show curved panels; this shows them as gently bent strips.
|
||||
|
||||
- `adapter.cylinder_strips` · also in builds for this PC (AXRB)
|
||||
- `adapter.cylinder_strips`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1351,7 +1333,7 @@ The Frame can't show curved panels; this shows them as gently bent strips.
|
||||
|
||||
Video players' virtual theaters and 360° videos would otherwise stay black.
|
||||
|
||||
- `adapter.equirect_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested (on), value `1`:** The game draws 360° layers (for example a video player's theater or 360° videos), which the Frame's runtime can't show: show them as panels around you.
|
||||
- **Options:**
|
||||
@@ -1368,7 +1350,7 @@ Video players' virtual theaters and 360° videos would otherwise stay black.
|
||||
|
||||
Higher keeps more detail but uses more memory.
|
||||
|
||||
- `adapter.equirect_face` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_face`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1382,7 +1364,7 @@ Higher keeps more detail but uses more memory.
|
||||
|
||||
Only if 360° pictures show wrongly.
|
||||
|
||||
- `adapter.equirect_flip` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_flip`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1396,7 +1378,7 @@ Only if 360° pictures show wrongly.
|
||||
|
||||
Must be at least the video's own frame rate.
|
||||
|
||||
- `adapter.equirect_fps` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_fps`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1410,7 +1392,7 @@ Must be at least the video's own frame rate.
|
||||
|
||||
Higher is sharper but needs more power.
|
||||
|
||||
- `adapter.equirect_res` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_res`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1424,7 +1406,7 @@ Higher is sharper but needs more power.
|
||||
|
||||
Shows 3D 360° pictures flat if they look doubled.
|
||||
|
||||
- `adapter.equirect_stereo` · also in builds for this PC (AXRB)
|
||||
- `adapter.equirect_stereo`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1438,7 +1420,7 @@ Shows 3D 360° pictures flat if they look doubled.
|
||||
|
||||
Logs details when one eye looks wrong. No effect on the game.
|
||||
|
||||
- `adapter.eye_debug` · also in builds for this PC (AXRB)
|
||||
- `adapter.eye_debug`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1452,7 +1434,7 @@ Logs details when one eye looks wrong. No effect on the game.
|
||||
|
||||
For games whose fades to black don't show, so you see the level loading (for example Batman: Arkham Shadow).
|
||||
|
||||
- `adapter.fade_quad` · also in builds for this PC (AXRB)
|
||||
- `adapter.fade_quad`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1467,7 +1449,7 @@ For games whose fades to black don't show, so you see the level loading (for exa
|
||||
|
||||
Saves a few UI panels before and after FrameBridge turns them upright, for testing without the headset.
|
||||
|
||||
- `adapter.flip_dump` · also in builds for this PC (AXRB)
|
||||
- `adapter.flip_dump`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1481,7 +1463,7 @@ Saves a few UI panels before and after FrameBridge turns them upright, for testi
|
||||
|
||||
Turns menus and text the right way up.
|
||||
|
||||
- `adapter.flip_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.flip_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1495,7 +1477,7 @@ Turns menus and text the right way up.
|
||||
|
||||
Older way to turn upside-down panels; prefer Fix upside-down menus.
|
||||
|
||||
- `adapter.flip_quads` · also in builds for this PC (AXRB)
|
||||
- `adapter.flip_quads`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1541,7 +1523,7 @@ Longer for headsets that briefly think they're off your head.
|
||||
|
||||
Hides a Quest-only way of saving GPU power that the Frame lacks. Leave it on.
|
||||
|
||||
- `adapter.foveation_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.foveation_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1555,7 +1537,7 @@ Hides a Quest-only way of saving GPU power that the Frame lacks. Leave it on.
|
||||
|
||||
For OpenGL ES games whose memory use keeps growing (e.g. Vader Immortal).
|
||||
|
||||
- `adapter.gl_error_clear` · also in builds for this PC (AXRB)
|
||||
- `adapter.gl_error_clear`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1644,7 +1626,7 @@ Logs details when flat panels stay black. No effect on the game.
|
||||
|
||||
For games that freeze the Frame when a controller vibrates (for example Lucky's Tale). Needs a rebuild.
|
||||
|
||||
- `adapter.haptic_fix` · also in builds for this PC (AXRB) · revision 2
|
||||
- `adapter.haptic_fix` · revision 2
|
||||
- **Changes:** Game setting, stored in the build: needs a rebuild.
|
||||
- **Suggested (on), value `1`:** Meta's OVRPlugin vibrates controllers through OVRPort's loader, which turns some vibrations into gigabyte allocations that freeze the Frame (for example Lucky's Tale, BattleSisters): vibrations are converted before they reach it.
|
||||
- **Options:**
|
||||
@@ -1659,7 +1641,7 @@ For games that freeze the Frame when a controller vibrates (for example Lucky's
|
||||
|
||||
Turn it down if controller vibrations are too strong.
|
||||
|
||||
- `adapter.haptic_scale` · also in builds for this PC (AXRB)
|
||||
- `adapter.haptic_scale`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1673,7 +1655,7 @@ Turn it down if controller vibrations are too strong.
|
||||
|
||||
For games whose picture flickers or smears (for example Into The Radius 2).
|
||||
|
||||
- `adapter.hide_space_warp` · also in builds for this PC (AXRB)
|
||||
- `adapter.hide_space_warp`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Offered (off), value `1`:** Unreal Engine 5 game with Meta's space warp: turn on if the picture flickers or smears (for example Into The Radius 2).
|
||||
- **Options:**
|
||||
@@ -1690,7 +1672,7 @@ For games whose picture flickers or smears (for example Into The Radius 2).
|
||||
|
||||
Logs details when buttons do nothing or the wrong thing. No effect on the game.
|
||||
|
||||
- `adapter.input_diag` · also in builds for this PC (AXRB)
|
||||
- `adapter.input_diag`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1704,7 +1686,7 @@ Logs details when buttons do nothing or the wrong thing. No effect on the game.
|
||||
|
||||
Logs details for troubleshooting. No effect on the game.
|
||||
|
||||
- `adapter.layer_debug` · also in builds for this PC (AXRB)
|
||||
- `adapter.layer_debug`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1718,7 +1700,7 @@ Logs details for troubleshooting. No effect on the game.
|
||||
|
||||
Skips picture layers the Frame would refuse. Leave it on.
|
||||
|
||||
- `adapter.layer_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.layer_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1732,7 +1714,7 @@ Skips picture layers the Frame would refuse. Leave it on.
|
||||
|
||||
Experimental. Leave it off unless asked to test it.
|
||||
|
||||
- `adapter.mutable_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.mutable_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1746,7 +1728,7 @@ Experimental. Leave it off unless asked to test it.
|
||||
|
||||
For older Unity games that freeze at the first scene change (for example Sniper Elite VR). Can make loading screens stutter.
|
||||
|
||||
- `adapter.ovrp_begin_gate` · also in builds for this PC (AXRB)
|
||||
- `adapter.ovrp_begin_gate`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1761,7 +1743,7 @@ For older Unity games that freeze at the first scene change (for example Sniper
|
||||
|
||||
For older Unity games whose hands trail the controllers (for example Sniper Elite VR). In others the hands lag.
|
||||
|
||||
- `adapter.ovrp_hold_physics` · also in builds for this PC (AXRB)
|
||||
- `adapter.ovrp_hold_physics`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1772,25 +1754,11 @@ For older Unity games whose hands trail the controllers (for example Sniper Elit
|
||||
|
||||
> Unity's built-in Oculus frame loop (frame.unity_oculus_check): don't pass Unity's physics-step pose update on, so the hands use the frame's display-time poses. Fixes hands trailing the controllers in some games (for example Sniper Elite VR); in others it makes the hands lag (for example BattleSisters).
|
||||
|
||||
### <a id="adapter-ovrp_trace"></a>Unity frame loop diagnostics (log)
|
||||
|
||||
Logs where an older Unity game's frame loop waits. No effect on the game.
|
||||
|
||||
- `adapter.ovrp_trace` · also in builds for this PC (AXRB)
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Unity frame loop diagnostics (log)" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Unity's built-in Oculus frame loop (frame.unity_oculus_check): every 5 s, logs how many frame updates, waits, begins and ends went through and which one is in progress (thread, frame, for how long), to see where a game that stops after a few frames waits. No effect on the game.
|
||||
|
||||
### <a id="adapter-passthrough_emul"></a>See your room
|
||||
|
||||
Shows the Frame's cameras where the game expects passthrough.
|
||||
|
||||
- `adapter.passthrough_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.passthrough_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1806,7 +1774,7 @@ Shows the Frame's cameras where the game expects passthrough.
|
||||
|
||||
For games that judder or shimmer while you hold your head still (for example I Am Cat).
|
||||
|
||||
- `adapter.pose_consistency` · also in builds for this PC (AXRB) · revision 2
|
||||
- `adapter.pose_consistency` · revision 2
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1821,7 +1789,7 @@ For games that judder or shimmer while you hold your head still (for example I A
|
||||
|
||||
Logs how game poses line up with the Frame's clock. No effect on the game.
|
||||
|
||||
- `adapter.pose_debug` · also in builds for this PC (AXRB)
|
||||
- `adapter.pose_debug`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1835,7 +1803,7 @@ Logs how game poses line up with the Frame's clock. No effect on the game.
|
||||
|
||||
For games whose hands trail the controllers when you move them (for example BattleSisters).
|
||||
|
||||
- `adapter.pose_time_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.pose_time_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested (on), value `1`:** Unity's built-in Oculus support reads controller poses through OVRPlugin at its monotonic "now", seconds in the past on the Frame: they are located at the right time (for example BattleSisters, Sniper Elite VR).
|
||||
- **Suggested (on), value `1`:** Unreal 4's Oculus input of this OVRPlugin generation asks for controller poses at OVRPlugin's own clock, which is behind the Frame's: they are located at the right time (for example Vader Immortal).
|
||||
@@ -1851,7 +1819,7 @@ For games whose hands trail the controllers when you move them (for example Batt
|
||||
|
||||
For apps that only know Quest 3/Pro controllers: without it they get no buttons.
|
||||
|
||||
- `adapter.profile_remap` · also in builds for this PC (AXRB)
|
||||
- `adapter.profile_remap`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1861,25 +1829,11 @@ For apps that only know Quest 3/Pro controllers: without it they get no buttons.
|
||||
|
||||
> Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead (components Touch lacks are dropped). Without it such apps get no controller input.
|
||||
|
||||
### <a id="adapter-projection_only"></a>Main picture only
|
||||
|
||||
Shows only the game's main picture, without menus or overlays drawn as extra layers.
|
||||
|
||||
- `adapter.projection_only` · also in builds for this PC (AXRB)
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Main picture only" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Frames that carry more than the game's main picture (menus or overlays as extra layers) submit the main picture alone. For runtimes that refuse such frames: AXRB's emulator on this PC rejects every multi-layer OpenGL ES frame. The extra layers don't show; 2 (OpenGL ES games, the default on this PC) draws the flat panels that come after the main picture into it first, so menus and messages drawn as panels still show.
|
||||
|
||||
### <a id="adapter-proximity_emul"></a>Animate fingers from touch
|
||||
|
||||
For games whose hands' thumbs never move when you touch the buttons or thumbstick (for example Vader Immortal).
|
||||
|
||||
- `adapter.proximity_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.proximity_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1893,7 +1847,7 @@ For games whose hands' thumbs never move when you touch the buttons or thumbstic
|
||||
|
||||
Stops games from hanging when their eye picture is a little too big (for example PowerWash Simulator). Leave it on.
|
||||
|
||||
- `adapter.rect_clamp` · also in builds for this PC (AXRB)
|
||||
- `adapter.rect_clamp`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1907,7 +1861,7 @@ Stops games from hanging when their eye picture is a little too big (for example
|
||||
|
||||
How often the picture updates. Higher is smoother if the game can keep up.
|
||||
|
||||
- `adapter.refresh_rate` · also in builds for this PC (AXRB)
|
||||
- `adapter.refresh_rate`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1922,7 +1876,7 @@ How often the picture updates. Higher is smoother if the game can keep up.
|
||||
|
||||
For games where one eye flickers or jitters. Costs some speed.
|
||||
|
||||
- `adapter.release_wait` · also in builds for this PC (AXRB)
|
||||
- `adapter.release_wait`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1936,7 +1890,7 @@ For games where one eye flickers or jitters. Costs some speed.
|
||||
|
||||
Re-creates the game's play space after the first frames. For testing.
|
||||
|
||||
- `adapter.respace_kick` · also in builds for this PC (AXRB)
|
||||
- `adapter.respace_kick`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1950,7 +1904,7 @@ Re-creates the game's play space after the first frames. For testing.
|
||||
|
||||
Higher looks sharper but needs more power; lower runs smoother. 1.0× is the game's own.
|
||||
|
||||
- `adapter.scale` · also in builds for this PC (AXRB)
|
||||
- `adapter.scale`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1961,25 +1915,11 @@ Higher looks sharper but needs more power; lower runs smoother. 1.0× is the gam
|
||||
|
||||
> Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels.
|
||||
|
||||
### <a id="adapter-scale_max"></a>Cap the eye size
|
||||
|
||||
Keeps games from rendering larger than the resolution scale allows.
|
||||
|
||||
- `adapter.scale_max` · also in builds for this PC (AXRB)
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Cap the eye size" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> With a resolution scale below 1, the largest eye size the game is told about is the scaled one too, for games that size their eye pictures from the maximum (for example Unity 2017 games). On by default on this PC for OpenGL ES games, whose pictures AXRB's emulator copies pixel by pixel.
|
||||
|
||||
### <a id="adapter-scene_depth"></a>Room depth
|
||||
|
||||
0 = the size of your play area.
|
||||
|
||||
- `adapter.scene_depth` · also in builds for this PC (AXRB)
|
||||
- `adapter.scene_depth`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -1993,7 +1933,7 @@ Keeps games from rendering larger than the resolution scale allows.
|
||||
|
||||
Mixed-reality games that build their level from your room get a simple room with walls.
|
||||
|
||||
- `adapter.scene_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.scene_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested (on), value `1`:** Mixed-reality game that builds its level from the room model: emulate a guardian-sized room (for example Demeter).
|
||||
- **Suggested (on), value `1`:** The game asks Meta's system for your room layout (scene), which the Frame doesn't have: emulate a room from the play area (for example VR HOT's room setup).
|
||||
@@ -2011,7 +1951,7 @@ Mixed-reality games that build their level from your room get a simple room with
|
||||
|
||||
Ceiling height of the pretend room.
|
||||
|
||||
- `adapter.scene_height` · also in builds for this PC (AXRB)
|
||||
- `adapter.scene_height`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2025,7 +1965,7 @@ Ceiling height of the pretend room.
|
||||
|
||||
0 = the size of your play area.
|
||||
|
||||
- `adapter.scene_width` · also in builds for this PC (AXRB)
|
||||
- `adapter.scene_width`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2039,7 +1979,7 @@ Ceiling height of the pretend room.
|
||||
|
||||
Saves what the game draws every few seconds, for testing without the headset.
|
||||
|
||||
- `adapter.snapshot` · also in builds for this PC (AXRB)
|
||||
- `adapter.snapshot`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2053,7 +1993,7 @@ Saves what the game draws every few seconds, for testing without the headset.
|
||||
|
||||
For games whose menus or screens jump to wherever you look.
|
||||
|
||||
- `adapter.stable_local` · also in builds for this PC (AXRB)
|
||||
- `adapter.stable_local`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2068,7 +2008,7 @@ For games whose menus or screens jump to wherever you look.
|
||||
|
||||
Diagnostics: removes layer color fades. Some fades no longer show.
|
||||
|
||||
- `adapter.strip_color_bias` · also in builds for this PC (AXRB)
|
||||
- `adapter.strip_color_bias`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2082,7 +2022,7 @@ Diagnostics: removes layer color fades. Some fades no longer show.
|
||||
|
||||
Removes depth information from the game's layers. For testing.
|
||||
|
||||
- `adapter.strip_depth` · also in builds for this PC (AXRB)
|
||||
- `adapter.strip_depth`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2096,7 +2036,7 @@ Removes depth information from the game's layers. For testing.
|
||||
|
||||
Shows videos that games play on a flat panel (for example an intro video).
|
||||
|
||||
- `adapter.surface_emul` · also in builds for this PC (AXRB)
|
||||
- `adapter.surface_emul`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2110,7 +2050,7 @@ Shows videos that games play on a flat panel (for example an intro video).
|
||||
|
||||
Shows Batman: Arkham Shadow's cutscenes; set by that game's recipe.
|
||||
|
||||
- `adapter.surface_native` · also in builds for this PC (AXRB) · revision 4
|
||||
- `adapter.surface_native` · revision 4
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2125,7 +2065,7 @@ Shows Batman: Arkham Shadow's cutscenes; set by that game's recipe.
|
||||
|
||||
Shows each eye's picture to the other eye. For testing.
|
||||
|
||||
- `adapter.swap_eyes` · also in builds for this PC (AXRB)
|
||||
- `adapter.swap_eyes`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2139,7 +2079,7 @@ Shows each eye's picture to the other eye. For testing.
|
||||
|
||||
Lets OpenGL ES games start when the Frame refuses their picture format. Leave it on.
|
||||
|
||||
- `adapter.swapchain_fix` · also in builds for this PC (AXRB)
|
||||
- `adapter.swapchain_fix`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2172,7 +2112,7 @@ For games that crash right after you return to them (for example after the Steam
|
||||
|
||||
For Unreal Engine 5 games that crash on the Frame (for example Metro Awakening).
|
||||
|
||||
- `adapter.vk_hide_fdm` · also in builds for this PC (AXRB)
|
||||
- `adapter.vk_hide_fdm`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2190,7 +2130,7 @@ For Unreal Engine 5 games that crash on the Frame (for example Metro Awakening).
|
||||
|
||||
For Vulkan games whose models flicker or vanish while you look at them (for example Into The Radius 2). Needs a rebuild.
|
||||
|
||||
- `adapter.vk_query_slots` · also in builds for this PC (AXRB) · revision 3
|
||||
- `adapter.vk_query_slots` · revision 3
|
||||
- **Changes:** Game setting, stored in the build: needs a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2204,7 +2144,7 @@ For Vulkan games whose models flicker or vanish while you look at them (for exam
|
||||
|
||||
Saves the game's shaders to find one that hangs the GPU. No effect on the game.
|
||||
|
||||
- `adapter.vk_shader_dump` · also in builds for this PC (AXRB)
|
||||
- `adapter.vk_shader_dump`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2220,7 +2160,7 @@ Saves the game's shaders to find one that hangs the GPU. No effect on the game.
|
||||
|
||||
Corrects specific shaders that hang the game on the Frame. Comes from a game's recipe.
|
||||
|
||||
- `adapter.vk_shader_fix` · also in builds for this PC (AXRB) · revision 2
|
||||
- `adapter.vk_shader_fix` · revision 2
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2235,7 +2175,7 @@ Corrects specific shaders that hang the game on the Frame. Comes from a game's r
|
||||
|
||||
For Unreal games whose models flicker or show through walls and windows on the Frame (for example Into The Radius 2). Needs a rebuild.
|
||||
|
||||
- `adapter.vk_spec_fixes` · also in builds for this PC (AXRB) · revision 2
|
||||
- `adapter.vk_spec_fixes` · revision 2
|
||||
- **Changes:** Game setting, stored in the build: needs a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2252,7 +2192,7 @@ For Unreal games whose models flicker or show through walls and windows on the F
|
||||
|
||||
Logs Vulkan mistakes the game makes. Needs a special test build.
|
||||
|
||||
- `adapter.vk_validation` · also in builds for this PC (AXRB)
|
||||
- `adapter.vk_validation`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
@@ -2399,24 +2339,6 @@ Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard
|
||||
|
||||
> Lepton runs VR apps headless: their Android window is never shown, so it never gets keyboard focus and no key press (Steam's on-screen keyboard, a USB/Bluetooth keyboard, Type on Frame) reaches the app's text fields. With Lepton's lepton-show-flatscreen marker the window is shown (behind the VR view, which keeps working) and Steam's keyboard opens for a selected text field. Steam lists the shown window as "Gamescope" (the Frame's compositor). Pairs with "Make Unity text fields work without a system keyboard".
|
||||
|
||||
### <a id="pc-axrb_backend"></a>AXRB mode on this PC
|
||||
|
||||
For this PC only: run the game in AXRB's direct mode (no Android emulator) or in AXRB's Android emulator. Automatic uses direct mode for Vulkan games.
|
||||
|
||||
- `pc.axrb_backend` · also in builds for this PC (AXRB)
|
||||
- **Changes:** Install only: written on the Frame when the game is installed; the APK isn't changed.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- `backend`: str, default empty — direct or emulator (empty = automatic)
|
||||
- `translator`: str, default empty — dynarmic or digitalis (direct mode; empty = AXRB's default)
|
||||
- `precompose`: bool, default `False` — AXRB composes projection layers itself
|
||||
- `legacy_oculus_driver`: bool, default `False` — direct mode: provide com.oculus.systemdriver
|
||||
- `quest_identity`: bool, default `False` — direct mode: report a Quest 3 device
|
||||
- `controllers_only`: bool, default `False` — direct mode: hide hand tracking (needs legacy_oculus_driver)
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Only for installs on this PC (AXRB): which AXRB runs the game. Automatic picks AXRB's direct mode (AXRB-Emulatorless: the game's arm64 code runs in one Windows process, no Android emulator) for Vulkan games and the Android emulator for OpenGL ES games, since direct mode has no OpenGL ES renderer yet. The other parameters are direct mode's per-game options (AXRB's run_direct_game.ps1): translator (dynarmic, AXRB's default, or digitalis), precompose (AXRB composes the game's projection layers itself; also used by the emulator), and legacy_oculus_driver / quest_identity / controllers_only for unconverted APKs with older Oculus loaders (controllers_only needs legacy_oculus_driver). The Steam Frame ignores this.
|
||||
|
||||
## <a id="group-pcvr"></a>PC VR (Proton and Revive)
|
||||
|
||||
Options for PC VR games, which run through Proton on the Frame (or Revive on this PC).
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
# Quest games on this PC (AXRB)
|
||||
|
||||
FramePort's PC target ("This PC") already runs Oculus Rift games through Revive. For Quest/Android games it uses
|
||||
**AXRB** (Android XR Bridge, github.com/Android-XR-Bridge/AXRB): an Android 16 emulator (x86_64 Google APIs image,
|
||||
WHPX) that runs arm64 games through Android's ARM translation (`libndk_translation`), with AXRB's own OpenXR runtime
|
||||
inside Android and a Windows host process (`axrb-host-bridge.exe`) that opens a real OpenXR session on the PC's
|
||||
runtime (SteamVR, Virtual Desktop, ...). FramePort only drives it: it downloads AXRB's official release when a user
|
||||
first installs a Quest game on the PC, never bundles or modifies it (AXRB's host/clock code is under the AXRB
|
||||
Source-Available License 1.0, which allows running it and interoperating through its public interfaces).
|
||||
|
||||
Status: **experimental**. AXRB is young (v1.0.4, 2026-10-06) and many games still fail in it; Steam Frame
|
||||
controllers don't work in AXRB yet (AXRB issue #20).
|
||||
|
||||
## Two backends: the emulator (1.0.4) and direct mode (2.x, "AXRB-Emulatorless")
|
||||
|
||||
AXRB's main branch (2026-10-15, launcher 2.0.0) became **AXRB-Emulatorless**: a separate program (own installer,
|
||||
app id `org.axrb.emulatorless`, `%LOCALAPPDATA%\Programs\axrb-emulatorless-launcher`, runtime
|
||||
`%LOCALAPPDATA%\AXRB-Emulatorless Runtime`, profile `%APPDATA%\AXRB-Emulatorless`) whose **direct mode** runs the
|
||||
game without an emulator: `axrb-direct.exe` translates the game's arm64 code (Dynarmic JIT; Digitalis optional) and
|
||||
runs the real AOSP ART + framework from Google's Android 16 arm64 system image, with its own Linux syscall/Binder
|
||||
layer; Vulkan goes to the Windows driver, frames and tracking through the same `axrb-host-bridge.exe`. **Vulkan only:**
|
||||
its EGL is a stub, there is no GLES renderer. Both programs can be installed side by side; FramePort keeps one
|
||||
`axrb.Install` record per backend (`backend`, `version`, app folder, AXRB's root = `resources\runtime`, runtime
|
||||
folder) and picks per game:
|
||||
|
||||
- **Choice** (`targets/pc_android.choose_backend`): `--axrb-backend direct|emulator` on `frameport install --to pc`,
|
||||
else the recipe's `pc.axrb_backend` (device category, `on_pc`: part of the PC build's selection, no-op on the
|
||||
Frame), else **direct for `graphics` Vulkan, emulator for GLES / unknown**. Unreal games never declare
|
||||
`android.hardware.vulkan`; since analysis version 20 their manifest meta-data
|
||||
`com.epicgames.{ue4,unreal}.GameActivity.bSupportsVulkan` = true makes them "Vulkan (Unreal bSupportsVulkan)"
|
||||
(UE 4.25+ Quest builds, e.g. Deadpool VR, Pinball FX VR, Myst; false or missing = GLES: Espire 1, Vader Immortal). `deployment.json` records `backend`
|
||||
and the launcher `flags`; installs from before have none (= emulator). Switching leaves the other backend's copy
|
||||
and saves alone.
|
||||
- **Per-game options** (`pc.axrb_backend` params → `run_direct_game.ps1` switches): `translator` (`dynarmic` =
|
||||
AXRB's default, `digitalis`), `precompose` (`-PrecomposeProjectionLayers`, also for the emulator's
|
||||
`run_windows_game.ps1`; also taken from AXRB's own `core/game-compatibility.json` profile for the game, e.g. The
|
||||
Climb 2), `legacy_oculus_driver` / `quest_identity` / `controllers_only` (for unconverted store APKs with older
|
||||
Oculus loaders; off by default and never taken from AXRB's profiles: FramePort always hands AXRB an OVRPort build;
|
||||
`controllers_only` only works with the legacy driver, as `prepare_art_session.py` requires).
|
||||
- **Install/upgrade** (`tools/axrb.install`): `releases/latest` when it has the backend's installer
|
||||
(`AXRB-Setup-*.exe` / `AXRB-Emulatorless-Setup-*.exe`, SHA256SUMS checked, `/S`), else the known-good release
|
||||
(`KNOWN_GOOD = {"emulator": "1.0.4"}`, `releases/tags/v1.0.4`). An install older than the known-good one is
|
||||
upgraded on use; `frameport pc install-axrb --backend … --update` upgrades to the release on offer. No 2.x release
|
||||
is published yet: **`FRAMEPORT_AXRB_DIRECT_DIR`** names a local build (AXRB's source tree after
|
||||
`scripts\build\build_direct.ps1` + the host build, its `out\direct`, or a folder laid out like the installed
|
||||
`resources\runtime`): it needs the files of AXRB's `core/direct-runtime-files.cjs` (`out\direct\axrb-direct.exe`,
|
||||
`art\axrb-bootstrap.dex`, `art\lib64\*`, `guest\lib64\*`, `guest\axrb-systemdriver.apk`) plus
|
||||
`scripts\run\run_direct_game.ps1`, `scripts\run\stop_game.ps1`, `scripts\paths.ps1`,
|
||||
`direct\tools\make_sysroot.py`, `direct\tools\prepare_art_session.py`, `out\host\bin\Release\axrb-host-bridge.exe`
|
||||
(+ `openxr_loader.dll`, `out\gpu\Release` for the GPU-share layer) and ideally `tools\python` (else Python from
|
||||
PATH). A local build is set up but never replaced. `FRAMEPORT_AXRB_DIRECT_RUNTIME` overrides the runtime folder
|
||||
(default: the launcher's `library.json` `settings.managedDirectory`, else `%LOCALAPPDATA%\AXRB-Emulatorless Runtime`).
|
||||
- **Direct setup** (`axrb.setup_direct`, what `core/direct.mjs ensureDirectSystem` does): the pinned image
|
||||
(`arm64-v8a-36_r02.zip`, 810,736,863 bytes, SHA-1 `62ad6714…`; values read from the installed `core/direct.mjs`,
|
||||
only `dl.google.com`) into its own empty folder, AXRB's `make_sysroot.py <zip> <runtime>\direct\sysroot.new --art`
|
||||
with AXRB's embedded Python, receipt `.axrb-sysroot.json` `{"digest","format"}`, rename to `sysroot`, archive
|
||||
deleted; `license-acceptance.json` like AXRB's DirectSetup. Ready = receipt matches + the key files exist and
|
||||
aren't empty + AXRB's direct files complete. Requirements: ≥ 6 GB RAM, `vulkan-1.dll` (no hypervisor, no GPU
|
||||
vendor gate).
|
||||
- **Direct install** (no adb): the PC build is copied to `%LOCALAPPDATA%\FramePort\axrb\games\<pkg>\<pkg>.apk`
|
||||
(FramePort removes its converted copies after an install; `FRAMEPORT_AXRB_LAUNCHER_DIR` moves FramePort's `axrb`
|
||||
folder, for isolated `FRAMEPORT_HOME` runs), the launch activity comes from the APK's manifest
|
||||
(`axml.launcher_activity`, `pkg/fully.qualified.Class`), the data files become AXRB's content list `assets.txt`
|
||||
(`Android/obb/<pkg>/<file>\t<Windows path>`, read-only mounts of the files where they are; WSL-only folders, which
|
||||
Windows reads only through `\\wsl$`, are copied next to the APK). AXRB's `prepare_art_session.py` turns every
|
||||
content file into one `--mount-ro` argument of `axrb-direct.exe` (with the path resolved: junctions and 8.3 names
|
||||
don't shorten it), and Batman's 90 files from a long dump folder made a 32,876-character command line (Windows'
|
||||
limit is 32,767: "The filename or extension is too long"). A list over 25,000 characters therefore goes through
|
||||
hard links in `games\<pkg>\content` (no extra space; only on the drive FramePort's folder is on), else the install
|
||||
stops with a clear message. App data and saves: `<runtime>\direct\data\data\<pkg>` and
|
||||
`direct\sdcard\Android\data\<pkg>`.
|
||||
- **Play**: Steam shortcut → `%LOCALAPPDATA%\FramePort\axrb\fp-axrb-direct.ps1 -Package … -Activity … [flags]` →
|
||||
sets `AXRB_DATA_HOME=<runtime>\output` (as AXRB's launcher does) → AXRB's `run_direct_game.ps1 -Apk -Package
|
||||
-Activity -GameName -Executable <root>\out\direct\axrb-direct.exe -DataDirectory <runtime>\direct -Sysroot
|
||||
<runtime>\direct\sysroot [-AssetList] -Translator …`. Exit 4 = host lost.
|
||||
- **Launch test**: watches `direct.out` for `AXRB_AOSP_ACTIVITY_CREATED` / `AXRB_AOSP_LIFECYCLE` (5 min for ART's
|
||||
interpreted first start), runs, then stops through AXRB's `stop_game.ps1 -ParentPid` (closes the bridge window;
|
||||
the run script lets the game pause/stop/save). Logs `<runtime>\output\logs\game\{direct.out, direct.err,
|
||||
direct.log, host.err, host.log, session.json}` (diagnostics: `axrb/direct/*`); triage signatures
|
||||
`axrb-direct-*` and milestones ART started → activity created → OpenXR → Vulkan device (`catalog/triage.yaml`).
|
||||
|
||||
**Tested end to end with a local build** (2026-10-15, AXRB `windows-android16` @ 59626ac built on the dev PC,
|
||||
`FRAMEPORT_AXRB_DIRECT_DIR` = its staged `runtime\`, the staged sysroot reached through a junction
|
||||
`<FRAMEPORT_AXRB_DIRECT_RUNTIME>\direct\sysroot`; Monado's simulated HMD, see below): scan → PC build → direct
|
||||
install → `frameport test --to pc --sim --axrb-backend direct` → launcher → AXRB's run script → bridge + runner →
|
||||
milestones → AXRB's Stop. Still untested: the 2.x NSIS installer (no release yet), `setup_direct`'s download +
|
||||
`make_sysroot.py` path, the upgrade of an installed 1.0.x, AXRB's launcher seeing FramePort-set-up runtimes.
|
||||
|
||||
### Direct-mode results (2026-10-15, Monado simulated HMD at 72 Hz, Dynarmic, no headset)
|
||||
|
||||
PC build = the catalog recipe's PC selection (OVRPort's patches + `on_pc` fixes + FrameBridge with `focus_hold=0`),
|
||||
no `pc.axrb_backend` options. "fps" = AXRB.Perf `end-frame` (game) / `host-fresh-submit` (bridge → Monado), the
|
||||
median of the last summaries. What is drawn was judged from Monado's window and AXRB's layer captures.
|
||||
|
||||
| Game (engine, graphics) | Reached | fps | Picture | Blocker / notes |
|
||||
|---|---|---|---|---|
|
||||
| Batman: Arkham Shadow 1.4.1-350961 (Unity, Vulkan; 90 content files, 27 GB) | menu (SELECT REGION) in ~25 s | 63.7 / 62.9 | stereo menu over the Batcave scene, both eyes correct | none; needed the content-list fix (command line overflow) and hard links; stop clean (activities stopped) |
|
||||
| PowerWash Simulator VR 2.5.0 (Unity 2020.3, Vulkan) | in game: the garage hub (~3-4 min; at 90 s still its loading logo) | 70.5 / 69.8 | garage in both eyes, both hands drawn at the simulated controllers | none; Meta multiplayer callbacks throw NullReferenceExceptions (no Meta services, harmless so far) |
|
||||
| LEGO Bricktales 2.1 (Unity, Vulkan, URP) | scene `Diorama_Hub_Startup` loaded (~3 min) | 72 / 72 | intro text panel ("You want to free the simulation? Follow the white rabbit.") on black, both eyes | stop slow: the game didn't finish its activity shutdown in 25 s (`axrb-direct-slow-stop`); Meta achievements fail (no Meta services, harmless) |
|
||||
| Toy Master 1.1.2 (Unity 2022.3, **GLES**) | activity created, then crash in 18 s | - | - | not a Vulkan game: "Vulkan (Unity boot.config)" came from the Oculus plugin's `xr-vulkan-*` keys; Unity: "Unable to retrieve the default EGL display!" + SIGTRAP. Automatic choice now sends such games to the emulator |
|
||||
| Marvel's Deadpool VR 1.0.40 (UE 4.27, Vulkan; `frame.vk_sanitize` in the build) | first-run LANGUAGE menu (~3 min; at 90 s still loading at 1-2 fps) | 64 (FrameBridge pacing) | game-drawn stereo menu, both eyes | none; AXRB transcodes ASTC to BC on the GPU; stop slow (`axrb-direct-slow-stop`) |
|
||||
| Pinball FX VR 1.9 (UE 5.3, Vulkan, 7 GB OBB) | 3D lobby with the Terms & Conditions panel (~4 min) | ~60 (FrameBridge pacing) | arcade hall + panel in both eyes | none of the emulator's blockers (gfxstream `vkAllocateMemory` SIGSEGV, and Refract's FRSQRTE SIGILL / ASTC layout probe / promoted extension names) showed up in direct mode (no SIGILL under Dynarmic, no ASTC or device-loss failure; AXRB stores ASTC as BC); OVRPlugin's xrPerfSettingsSetPerformanceLevelEXT unsupported (harmless) |
|
||||
| Myst 3.3.0 (UE 5.3, Vulkan) | loading screen (book), then its starfield intro | 72 | stereo, both eyes | a white splash quad at first (texture not shown yet), then normal |
|
||||
| Metro Awakening 1.0-916944 (UE 5.2, Vulkan) | splash art, then the USER DATA COLLECTION consent screen with the laser pointer | 72 | stereo, both eyes | stop slow (`axrb-direct-slow-stop`) |
|
||||
|
||||
Unreal Vulkan games: before analysis version 20 every Unreal game was "GLES or unknown" (Unreal never declares
|
||||
`android.hardware.vulkan`), so the automatic choice sent them to the emulator; now `GameActivity.bSupportsVulkan`
|
||||
picks direct mode (Myst and Metro above were installed without `--axrb-backend`). Unreal probes EGL before it
|
||||
creates its Vulkan device, which tripped `axrb-direct-no-gles`; the signature now needs no Vulkan device. None of
|
||||
the four needed a recipe change for the PC.
|
||||
|
||||
Integration bugs found and fixed on the way: AXRB's trap message never reached FramePort's log; the content list
|
||||
overflowed the command line (Batman); Unity's EGL failure and SIGTRAP weren't triaged; long sessions lost their
|
||||
start-up milestones; the boot.config Vulkan label picked direct mode for GLES games. Open, AXRB's side: the content
|
||||
list should reach `axrb-direct.exe` as a file (FramePort's hard links only work when the data is on the drive of
|
||||
FramePort's folder; drafted as an upstream issue). Needs a headset: picture and latency through SteamVR, sound,
|
||||
controllers (the simulated ones hold still), Batman beyond its region menu.
|
||||
|
||||
### VrApi-direct games (2026-10-16, direct mode, Monado simulated HMD, Dynarmic, no headset)
|
||||
|
||||
Games whose engine calls VrApi itself (no OVRPlugin), so OVRPort can't translate them: they need a `libvrapi.so`
|
||||
that translates VrApi to OpenXR. Two candidates: FramePort's `frame.vrapi_bridge` (OVRPort's translator + FramePort's
|
||||
changes) and OVRPort's own `patch_vrapi_openxr` (the unpatched translator; only in experimental CLI builds made with
|
||||
`-PwithVrApi=true`, e.g. the `OVRPort-CLI-1.2.5-experimental-5e7df52.jar` AXRB's v1.0.0/1.0.1 releases attached;
|
||||
AXRB's own profile for The Climb 2 uses it with `-PrecomposeProjectionLayers`). Only converted copies of both games
|
||||
were at hand, so the comparison swapped the one file (OVRPort's = the v1.2.5 build from the 2026-10-02 Frame tests).
|
||||
|
||||
| Game (engine, graphics) | VrApi route | Reached | fps | Picture | Blocker / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| The Climb 2 2.2 (CryEngine, Vulkan VrApi) | FramePort's bridge | first-start menu (gender/skin selection) in ~60 s | 69.8 / 69.8 | menu, both hands and controllers, both eyes (AXRB layer captures + Monado's window) | needs AXRB's HostWindow fix (below, AXRB's side) + `frame.vk_android_extensions`; crash only during AXRB's stop (forgiven by triage); no `-PrecomposeProjectionLayers` needed |
|
||||
| The Climb 2 2.2 | OVRPort's translator | the same menu | 70.2 / 70.2 | identical | same blockers; passes AXRB's AHB device-extension request through unchanged (worked only because `libfp_vkext.so` lists AHB) |
|
||||
| Path of the Warrior (own engine, **GLES** VrApi) | - | not run | - | - | GLES: direct mode can't run it; the emulator (1.0.4) had a copy signed with another key installed by another test, so it wasn't replaced. OVRPort's translator can't run it anywhere (no GLES path: `cannot locate symbol "vrapi_GetTextureSwapChainHandle"`, Frame test 2026-10-02); FramePort's bridge has one |
|
||||
|
||||
**Default for PC builds: FramePort's bridge** (`frame.vrapi_bridge`, now `on_pc = True`; recipes keep it as they
|
||||
are, nothing to switch). Both translators give the same picture and frame rate in direct mode, but OVRPort's needs
|
||||
an unpublished experimental CLI (stable jars list `patch_vrapi_openxr` but fail), has no GLES path (POTW, every GLES
|
||||
VrApi game on the emulator) and lacks the device-extension filter; FramePort's is the same code the Frame builds
|
||||
use, so one recipe serves both.
|
||||
|
||||
Blockers found on the way (in order):
|
||||
1. **No window for NativeActivity** (AXRB's side, drafted upstream): CryEngine's `EngineActivity` takes its surface
|
||||
with `Window.takeSurface`; direct mode's `HostWindow.attach` only bridges SurfaceViews (`AXRB_AOSP_HOST_SURFACES
|
||||
0`), so `APP_CMD_INIT_WINDOW` never comes and the game waits after `Loading plug-in CryHTTP` (triage
|
||||
`axrb-direct-no-native-window`). A ~40-line HostWindow patch (`mTakeSurfaceCallback` → `registerSurface` +
|
||||
`surfaceCreated/Changed`, bootstrap dex rebuilt with AXRB's `build_art_bootstrap.py` into a test copy of the
|
||||
local build) fixed it. Likely the same for every NativeActivity engine.
|
||||
2. **Quest Vulkan device extensions**: CryEngine checks its list before vkCreateDevice and gave up
|
||||
(`CrySystem initialization failed!`): AXRB's runtime asks for `VK_ANDROID_external_memory_android_hardware_buffer`
|
||||
also in direct mode, and the game requires `VK_EXT_fragment_density_map` (NVIDIA's Windows driver has neither).
|
||||
The bridge now leaves runtime extensions no device offers out; `frame.vk_android_extensions` lists the missing
|
||||
Quest ones to the renderer (AXRB drops them at vkCreateDevice). Triage `axrb-direct-vulkan-extensions`.
|
||||
3. **Simulator only**: Monado's head lacks POSITION_TRACKED, so AXRB's host never made its LOCAL origin and the
|
||||
bridge never got a tracked head (empty frames, `initial recenter: head flags=0x0`) → `--sim` sets AXRB's
|
||||
`AXRB_TEST_SYNTHETIC_HMD=2`.
|
||||
4. The analysis called The Climb 2 "GLES or unknown" (no manifest declaration) → analysis 20: engines importing
|
||||
`vrapi_CreateSystemVulkan` are "Vulkan (VrApi Vulkan system)", so it goes to direct mode automatically.
|
||||
|
||||
Debugging aid: `FRAMEPORT_AXRB_TEST_ENV="AXRB_DIRECT_TRACE=syscall,vulkan;AXRB_CAPTURE_FIRST_SEQUENCE=4500"` passes
|
||||
variables to a launch test's AXRB processes (`AXRB_DIRECT_DUMP` only works in AXRB's own VM mode, not with ART).
|
||||
Needs a headset: the picture through SteamVR, controllers past the first menu (the simulated ones hold still),
|
||||
climbing, sound; and a released AXRB-Emulatorless with the window fix.
|
||||
|
||||
### Library sweep 2026-10-16
|
||||
|
||||
Every other Quest game of the owner's library that `pipeline.pc_installable` accepts and whose APK still exists,
|
||||
installed with `frameport install <pkg> --to pc --no-library` (automatic backend, isolated `FRAMEPORT_HOME`, catalog
|
||||
recipe or heuristics, no per-game changes) and tested with `frameport test <pkg> --to pc --sim --seconds 150` (240 for
|
||||
Unreal) under Monado's simulated HMD at 72 Hz. Direct mode = the local AXRB build of 59626ac **with the HostWindow
|
||||
patch** (above; upstream had no newer commit or 2.x release on 2026-10-16); emulator = AXRB 1.0.4 with this branch's
|
||||
`adb install -g`, `projection_only=2` and qwerty-head fixes. "fps" = game submits / bridge to Monado (AXRB.Perf
|
||||
medians); "Picture" = Monado's window (both eyes) at the end of the run, plus AXRB's layer captures. Not run: the
|
||||
three 32-bit-only games (Sports Scramble, Shadow Point, Journey of the Gods: AXRB has no AArch32, and the PC build
|
||||
drops the 32-bit bridge), and the emulator games after NEX Player (disk, see the end).
|
||||
|
||||
| Game (engine) | Backend | Reached | fps | Picture | Blocker / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Arcsmith 1.128 (Unity) | direct | frames | 59 / 51.7 | in game: the workshop, both hands with tools, both eyes | none |
|
||||
| QuestCraft 6.0.0-RC4 (Unity) | direct | frames | 65.1 / 63.6 | launcher room, QuestCraft EULA panel, laser pointers | none (Minecraft itself is behind its Continue button) |
|
||||
| BattleGlide 0.4.1 (UE) | direct | frames | 62.2 / 54.8 | title screen (START), both hands | none |
|
||||
| In Death: Unchained 1.8.0 (UE) | direct | frames | 64.1 / 56.9 | sky + PRIVACY UPDATE panel | none |
|
||||
| The Boys VR 1.1.0 (Unity) | direct | frames | 62.4 / 59.9 | ANALYTICS DISCLAIMER panel over a lit floor, laser | slow stop |
|
||||
| Creed: Rise to Glory CE (UE) | direct | frames | 60.6 / 55.5 | IMPORTANT SAFETY INFORMATION panel, both hands, laser | none |
|
||||
| Wallace & Gromit: The Grand Getaway 1.0.34 (UE) | direct | frames | 61.5 / 53.5 | space scene (nebula, planets), both gloved hands | none |
|
||||
| VR4 2.3 (UE) | direct | frames | 71.9 / 72 | title scene: church interior, "resident evil 4" logo, both hands | none (slow stop) |
|
||||
| Demeter 1.1.0 (Unity, MR) | direct | frames, FrameBridge scene emulation active | 62.3 / 57.3 | "PERMISSION REQUIRED ... access to your spatial data" panel, controllers | AXRB's direct mode grants only 8 allow-listed permissions (`HostPackageManager.permission`): `com.oculus.permission.USE_SCENE` stays denied although the build declares it (`frame.meta_permissions`). AXRB's side |
|
||||
| Green Hell VR 2.5.2 (Unity) | direct | frames | 11.8 / 11.7 | loading screen at "50%" after 180 s | slow load |
|
||||
| TRIANGLE STRATEGY 1.0 (UE) | direct | frames | 44.6 / 40.2 | both controllers with a "Control Guide" label, rest black at 280 s | still loading? |
|
||||
| Riven 1.9.0 (UE) | direct | frames | 29.8 / 25.2 | white splash quad (AXRB captures), black window at 274 s | still loading (as Myst's white splash) |
|
||||
| Espire 2 1.7.13 (UE) | direct | frames | 72 / 71.8 | white splash quad low in both eyes at 289 s | splash texture never shown |
|
||||
| Carve Snowboarding 1.09 (UE) | direct | frames | 64.5 / 59.3 | both hands, rest black at 270 s | scene never appears; not diagnosed |
|
||||
| Asgard's Wrath 2 12.1 (UE, 33 GB) | direct | frames | 30.6 / 21.9 | black at 287 s | still loading |
|
||||
| Assassin's Creed Nexus 207706 (Unity) | direct | frames | 42.6 / 42.6 | black at 180 s (captures too) | loading or no visible layer yet |
|
||||
| BARTENDER VR SIMULATOR 1.9.2.7 (UE) | direct | frames (in a second process) | 62.7 / 53.6 | black for 260 s (window + captures all 0) | not diagnosed (no Unreal log lines in direct.log) |
|
||||
| Vermillion 1.7.2 (Unity) | direct | frames, OVRManager focused | 63.8 / 60.3 | black with its brush loading icon | stays there (Vuplex 3D WebView starts, then nothing) |
|
||||
| H.U.N.T 0.1.2 (Unity OpenXR) | direct | session created, no frame in 200 s | - | bridge splash | never submits a frame; not diagnosed |
|
||||
| WiiCompiled VR 0.4.0 | direct | 2D launcher activity, no OpenXR | - | bridge splash | its launcher imports games through a document picker; the VR part is a second activity |
|
||||
| Medieval Dynasty New Settlement 2.4 (Unity) | direct | OpenXR FOCUSED, exit(1) at 30 s | - | - | **opens an Android Dialog** (right after "initOculus Java!"); direct mode can't add a second window (`UnsupportedOperationException: addToDisplayAsUser`, "Adding window failed"). Triage `axrb-direct-dialog-window` |
|
||||
| Under Cover 1.1.3r2 (Unity) | direct | Vulkan device, exit(1) at 33 s | - | - | the same Dialog |
|
||||
| Of Lies and Rain 1.1.0 (UE) | direct | vkCreateInstance, exit(1) at 32 s | - | - | Unreal's `MessageBox01` (an error box before its Vulkan device; its text is lost with the window) |
|
||||
| NOPE CHALLENGE 1.0.964 (UE) | direct | ART, crash at 15 s | - | - | Google Play services call `IActivityManager.getMyMemoryState`: `AXRB_SERVICE_UNIMPLEMENTED` -> uncaught exception. Triage `axrb-direct-unimplemented-service` |
|
||||
| BAM 1.2 (Unity) | direct (manifest declares Vulkan) | activity, crash at 33 s | - | - | runs GLES: "Unable to retrieve the default EGL display!"; the manifest's Vulkan feature doesn't say which API Unity picks first. Emulator run not reached |
|
||||
| Mobile Suit Gundam: Silver Phantom 1.1.58 (Unity) | direct | install refused | - | - | 257 content files with ~80-character names don't fit axrb-direct.exe's command line even through hard links (the guest paths alone are too long); needs AXRB to read the list from a file or mount the folder. Emulator run not reached |
|
||||
| Silhouette 1.3.0 (Unity) | emulator | frames | 30.2 / 30.1 | title menu (Play / Options / Quit), both eyes | none headless |
|
||||
| Yupitergrad 2.6.1 (Unity) | emulator | frames | 13.8 / 14 | splash ("Loading...") in both eyes | still loading at 174 s |
|
||||
| 4XVR Video Player 2.0.22 | emulator | frames | 34.9 / 32.2 | black | gfxstream rejects its GL every frame: `glEnable(GL_FRAMEBUFFER_SRGB_EXT)`, a `glTexParameteri` value, vertex attribute index >= `CODEC_MAX_VERTEX_ATTRIBUTES` |
|
||||
| NEX Player 1.3.1 (Unity) | emulator | frames | 9.3 / 9.3 | black | not diagnosed |
|
||||
| Lambda1VR 1.7.3 (Team Beef) | emulator | Java crash at start | - | - | `libopenxr_loader_google.so not found`: TBXR ports load `openxr_loader_<Build.MANUFACTURER>`; `frame.tbxr_vendor` handles "valve" and isn't in PC builds; AXRB's Android says "google" (a PC variant: add that library and match "google" in the maker checks) |
|
||||
|
||||
Summary (31 games run): **8 reach a menu or the game in both eyes** in direct mode (Arcsmith in game; QuestCraft,
|
||||
BattleGlide, In Death, The Boys, Creed, Wallace & Gromit, VR4 at their first menu/panel) and 1 in the emulator
|
||||
(Silhouette); Demeter shows its own "permission required" panel; 12 submit frames but were still loading or black at
|
||||
the end of the run (big Unreal games at 30-45 fps while loading under Dynarmic); 9 never submit a frame. The most
|
||||
common blockers, all on AXRB's side:
|
||||
1. **No second window in direct mode** (3 games: Medieval Dynasty, Under Cover, Of Lies and Rain): any Android
|
||||
Dialog ends the game. Upstream candidate together with the permission allow-list (Demeter's `USE_SCENE`) and the
|
||||
unimplemented framework calls (Play services' `getMyMemoryState`, NOPE).
|
||||
2. **Long or loading-heavy starts**: under Dynarmic the large Unreal games need more than 4-5 minutes to their first
|
||||
picture (Riven, Asgard's Wrath 2, TRIANGLE STRATEGY, Espire 2's white splash); a longer `--seconds` would show more.
|
||||
3. **The content list on the command line** (Gundam): a list file or folder mounts in `axrb-direct.exe` would fix it
|
||||
for good (drafted upstream).
|
||||
4. GLES-in-direct-mode mislabels: a Unity manifest that declares Vulkan can still run GLES (BAM).
|
||||
|
||||
FramePort changes from the sweep: triage `axrb-direct-dialog-window` and `axrb-direct-unimplemented-service`
|
||||
(`axrb-direct-no-gles` no longer blames Unreal's EGL probe when one of those ended the game first); the
|
||||
command-line message suggests the emulator; and **direct-mode uninstalls and reinstalls remove the libraries AXRB's
|
||||
`prepare_art_session.py` extracts per APK** into `<runtime>\direct\installed\<identity>`, which AXRB never deletes
|
||||
(5 GB after 25 games; `pc_android.staged_libraries` computes AXRB's identity: the APK's Windows path + every
|
||||
`lib/arm64-v8a` entry's name, CRC and size; unit-tested, not yet seen on a real uninstall).
|
||||
|
||||
The emulator half stopped after NEX Player: the WSL disk and C: filled up (other jobs; C: 14 GB free, below the 60
|
||||
GB the sweep keeps), so the remaining emulator games were not run: The Room VR, Pixel Ripped 1995, Time Stall, Five
|
||||
Nights at Freddy's: HW, Star Wars Pinball VR, I Am Cat, Vader Immortal 1-3, Genotype, Phantom: Covert Ops, Sniper Elite
|
||||
VR (+ Winter Warrior), Robo Recall, Budget Cuts Ultimate, Espire 1, BodyCombat, Star Wars: Tales from the Galaxy's
|
||||
Edge, HITMAN 3, Jurassic World Aftermath, Nano, Wander, Stremio, Eleven Table Tennis, BlazeRush, plus BAM and Gundam
|
||||
in the emulator.
|
||||
|
||||
## Spike results (2026-10-10, Windows 11 26200, i7-14700K, RTX 4080 SUPER, 64 GB; headless)
|
||||
|
||||
| Step | Result |
|
||||
|---|---|
|
||||
| AXRB install | `AXRB-Setup-<v>.exe /S` (electron-builder one-click NSIS, per user, no UAC) → `%LOCALAPPDATA%\Programs\axrb-launcher`, 462 MB. Release assets: Setup exe + `SHA256SUMS-<v>-setup.txt` + source zip (no portable zip since 1.0.4). |
|
||||
| Runtime setup | Not scriptable through AXRB (it lives in the Electron launcher's JS), so FramePort reproduces it: 4 pinned `dl.google.com` zips from AXRB's `core/components.json` (emulator 36.5.11, platform-tools r37.0.1, build-tools 36.1, `sys-img/google_apis/x86_64-36_r07`), hash-checked, into `%LOCALAPPDATA%\AXRB Runtime\sdk`, AVD `axrb-managed-api36` written as AXRB writes it, `ready.json` / `license-acceptance.json` receipts so AXRB's own launcher sees the same runtime as set up. 2.4 GB download, ~3 min on a fast line; 26 GB on disk after one 7 GB game. |
|
||||
| Emulator boot | AXRB's `scripts\emulator\windows_android_emulator.ps1 -Action Start` (headless QEMU, AXRB clock adapter + GPU layer). First cold boot ~6 min (TSC-corrected clock cold-boots every time). ABIs `x86_64,arm64-v8a`. QEMU (`Start-Process`) and the adb server inherit the caller's handles: a pipe, a file through WSL interop, even `/dev/null` through interop (its relay stays open) all left the FramePort call hanging after the script had ended → FramePort starts AXRB scripts through WMI (`Win32_Process.Create`, hidden, nothing inherited), polls the PID and reads an `FP_EXIT <code>` line from the log. Windows PowerShell 5.1's `*>>` writes UTF-16. |
|
||||
| AXRB runtime | `adb install --no-incremental --force-queryable -r axrb-openxr-runtime-debug.apk` (0.3 s); found by the OpenXR loader through the system runtime broker. |
|
||||
| Data | `adb push` of OBB/pak files: 25-35 MB/s (7.2 GB Pinball FX VR in 5.5 min). A full uninstall (or a signature change: `INSTALL_FAILED_UPDATE_INCOMPATIBLE`) deletes the OBB folder and the saves → FramePort's stable per-game keys matter here too. |
|
||||
| **Original APK** (Pinball FX VR) | No launcher activity (Meta's VR category only), started explicitly: **SIGABRT 0.3 s after loading Meta's `libovrplatformloader.so`**. Unconverted Quest APKs don't run. |
|
||||
| **OVRPort-only build** (Lucky's Tale, Unity 2019 GLES) | OVRPort's loader → AXRB runtime → instance + session, frames submitted (2 s each without a host: the image send times out), no crash in 40 s. |
|
||||
| **OVRPort-only build** (Pinball FX VR, UE5 Vulkan) | OVRPlugin pre-init OK (AXRB reports "Oculus 67.522.0"), then exits (ForceQuit) / with the no-ForceQuit build SIGSEGV in `gfxstream vkAllocateMemory` called from AXRB's runtime Vulkan layer. Not decided headless (no host attached, see below). |
|
||||
| Host bridge | Needs an OpenXR runtime with a D3D11 adapter. Headless options all failed: SteamVR's null driver (`failed to find D3D11 adapter requested by OpenXR runtime`), `vrlink` without a headset (`xrCreateInstance` -2), Meta XR Simulator (binary download needs a Meta developer login). → frames-to-SteamVR need a real headset. |
|
||||
|
||||
**Build decision: FramePort hands AXRB an OVRPort-only build** (the recipe's OVRPort patches, no Steam Frame fixes,
|
||||
signed with the game's FramePort key). The original APK crashes; FramePort's Frame fixes (FrameBridge adapter, Lepton
|
||||
and Frame-runtime workarounds) target the Frame. The recipe's alternate (no-ForceQuit) build is used when the recipe
|
||||
says so. (Since 2026-10-15 the PC build also carries the runtime-independent APK fixes and FrameBridge, see below.)
|
||||
|
||||
## Which patches reach the build for this PC
|
||||
|
||||
One recipe serves both builds; `patches/base.pc_selection(patches, analysis)` picks the PC part (used by
|
||||
`build.build(pc=True)`, `pipeline.pc_recipe_fingerprint`, the PC deployment record and `ui/components.pc_outdated`).
|
||||
`docs/PATCHES.md` marks each kept patch "also in builds for this PC (AXRB)".
|
||||
|
||||
- **All of OVRPort's patches** (category `overport`).
|
||||
- **APK fixes that don't depend on Lepton or the Frame's runtime** (`Patch.on_pc = True`): they fix the game's own
|
||||
code or its Meta libraries, which misbehave the same way in AXRB's Android (no Meta services, not a Quest device
|
||||
model, no Horizon app): `frame.ovrplatformcompat`, `frame.ovrstubs`, `frame.swapchain_limit` (OVRPort's
|
||||
dispatcher), `frame.ovr_stable_user`, `frame.unreal_ovrp_entrypoints`, `frame.avatar_stub`, `frame.vrapi_stub`,
|
||||
`frame.metaxr_telemetry`, `frame.oculusos`, `frame.ovr_microphone`, `frame.langpacks`, `frame.asset_files`,
|
||||
`frame.asset_files_stub`, `frame.ovr_trace`, `frame.blazerush`, `frame.unity_oculus_check` (with its ovrpshim),
|
||||
`frame.unity_text_input`, `frame.unreal_quest_precompile`, `frame.unreal_quest_keymap`,
|
||||
`frame.unreal_thumb_touch`, `frame.unreal_skip_obb_check`, `frame.unreal_skip_obb_delivery`, `frame.nodebug`,
|
||||
`frame.meta_permissions`, `frame.launcher`, and `frame.vrapi_bridge` (VrApi → OpenXR through OVRPort's loader,
|
||||
so AXRB's runtime; seen working in direct mode with The Climb 2, see "VrApi-direct games" below; the bridge
|
||||
doesn't need the GL shim, but GLES games keep `frame.gl_shim` for gfxstream, see "GLES games in the emulator").
|
||||
- **Experimental on the PC, for GLES games in the emulator**: `frame.gl_shim` (only with a GLES analysis,
|
||||
`Patch.pc_keeps`; Vulkan VrApi games never get it), `frame.unity_multipass` (also added by `pc_detect`), and the
|
||||
GLES FrameBridge defaults `scale` ≤ 0.5, `scale_max=1`, `projection_only=1`.
|
||||
- **Experimental on the PC** (`on_pc = "experimental"`, kept but not yet seen working in AXRB): `frame.vk_sanitize`
|
||||
(the Vulkan shim; gfxstream / the host driver instead of Lepton's Fossilize), `frame.unity_tls_key_pool`,
|
||||
`frame.eos_keychain_loop`.
|
||||
- **PC-only APK fix** `frame.vk_android_extensions` (`pc_detect`: direct-VrApi games whose engine creates VrApi's
|
||||
Vulkan system, analysis `graphics` "Vulkan (VrApi Vulkan system)"): the renderer's
|
||||
`vkEnumerateDeviceExtensionProperties` import goes to `libfp_vkext.so`, which also lists the Quest device
|
||||
extensions the PC's driver lacks (VK_EXT_fragment_density_map, Android external memory); AXRB's vkCreateDevice
|
||||
leaves them out again.
|
||||
- **FrameBridge** (`frame.adapter`) with its settings in `lib/<abi>/libframe_settings.so`: it wraps OVRPort's loader
|
||||
the same way in AXRB (scale, haptic_fix, pose_time_fix, rect_clamp, profile_remap, ...). Left out
|
||||
(`settings.PC_DENY`): `sync_guard` (a race in the Frame's vrclient.so), `focus_hold` / `focus_hold_ms` (the
|
||||
Frame's flickering wear sensor; FrameBridge's built-in default is on, so the PC build writes `focus_hold=0`,
|
||||
`settings.PC_VALUES`), `controller_models` (models the agent converts from the Frame's SteamVR at install) and the
|
||||
settings only the GL shim / Zink shader layer read (`gl_*`, `zink_*`). `controller_fix` stays: it is the game's
|
||||
choice (0 = hand tracking) and its Touch mapping of `/valve/` and generic profiles is runtime-independent.
|
||||
FrameBridge finds `libframe_settings.so` next to its own path (dladdr); on the Frame the agent also passes the
|
||||
settings as files, in AXRB the library is the only source, so the PC build turns `extractNativeLibs` on
|
||||
(`build.extract_native_libs`; apps with `extractNativeLibs="false"` would map their libraries from base.apk).
|
||||
Without OVRPort (a 32-bit recipe) the PC build drops FrameBridge: there is no loader to wrap.
|
||||
- **PC-only:** OVRPort's `patch_disable_meta_xr_audio_telemetry` (for x86_64 ARM translation, which AXRB is) is
|
||||
never in a Steam Frame recipe (`applies()` is False); `pc_selection` adds it, given the analysis, to games with
|
||||
Meta XR Audio (`Patch.pc_detect`; AXRB's own profile turns it on for North Star).
|
||||
- **Off on the PC:** Lepton/Android 11 workarounds (clipboard, Vivox audio route, start activity, ...), the Zink/Mesa
|
||||
GL shims and shader layer, pac_hints/luajit, hardware video decoding, every `device.*` option, the 32-bit bridge.
|
||||
- TODO: `device.files` (files written into the game's storage at install) could be reproduced with `adb push` in
|
||||
`targets/pc_android.py`.
|
||||
|
||||
### FramePort end to end (same day, headless, this branch)
|
||||
|
||||
`frameport install com.playful.LuckysTale --to pc --no-library` (isolated `FRAMEPORT_HOME`): PC build (19 patches:
|
||||
OVRPort's + `frame.ovrplatformcompat`/`frame.ovrstubs`/`frame.swapchain_limit`; static checks pass without the
|
||||
FrameBridge check) → AXRB found, requirements met → emulator → AXRB runtime → `adb install` → 3.7 GB OBB in ~95 s
|
||||
(39 MB/s) → `deployment.json` + launcher script. `frameport test com.playful.LuckysTale --to pc`: launcher → AXRB run
|
||||
script → host bridge → triage `axrb-no-headset` (expected without a headset). The Steam shortcut write (needs a Steam
|
||||
restart) is unit-tested only.
|
||||
|
||||
## How FramePort drives AXRB
|
||||
|
||||
- `tools/axrb.py`: find (env `FRAMEPORT_AXRB_DIR` > installed AXRB) / install (Setup exe, SHA256SUMS checked, `/S`) /
|
||||
runtime setup (above) / requirements (WHPX via AXRB's `check_windows.ps1`, ≥ 12 GB RAM, x64).
|
||||
- Environment for every AXRB script (the installed build sets none of it itself): `AXRB_DATA_HOME=<Runtime>\output`,
|
||||
`ANDROID_AVD_HOME=<Runtime>\avd`, `ANDROID_USER_HOME=<Runtime>\android`, `ANDROID_HOME`/`ANDROID_SDK_ROOT=<Runtime>\sdk`,
|
||||
`ANDROID_ADB_SERVER_PORT=5038`, `ADB_USB_LEGACY=1`, `ADB_LOCAL_TRANSPORT_MAX_PORT=5683`, AXRB's embedded Python
|
||||
(`resources\runtime\tools\python`) first on PATH; arguments `-Sdk <Runtime>\sdk -Avd axrb-managed-api36 -Port 5584`.
|
||||
- Install: start the emulator if needed (and stop it again if FramePort started it), install the AXRB runtime APK,
|
||||
`adb install -r` the build, push OBB/data to `/sdcard/Android/obb|data/<pkg>/`, record `<data>/pc/<pkg>/deployment.json`
|
||||
(`kind: "android"`).
|
||||
- Play: a Steam shortcut runs FramePort's small launcher script (`%LOCALAPPDATA%\FramePort\axrb\fp-axrb-run.ps1`),
|
||||
which sets the environment above and calls AXRB's `scripts\run\run_windows_game.ps1` (boots the emulator when
|
||||
needed and then owns it, registers a SteamVR app identity, starts the host bridge, `am start`, waits; exit codes
|
||||
0 ok, 1 error, 3 game lost, 4 host lost; `logs\game\session.json`, `host.err`).
|
||||
- Logs for diagnostics: `<Runtime>\output\logs\game\{host.log,host.err,session.json,guest.log}`,
|
||||
`logs\emulator\emulator.std{out,err}.log`.
|
||||
|
||||
## Headset results (2026-10-10, Steam Frame streaming to the PC through Steam Link / vrlink, 96 Hz)
|
||||
|
||||
AXRB's bridge opened a SteamVR session on the Frame each time (session state up to FOCUSED, both hands active), started
|
||||
through FramePort's launcher with the emulator booted by AXRB's run script (~1 min), and shut down cleanly.
|
||||
|
||||
- **Lucky's Tale (Unity, GLES): black.** GLES games go through AXRB's raw image path (eye readback → TCP), sized
|
||||
by SteamVR's recommended resolution: 3412×3412 per eye at the owner's 250 % supersampling = 93 MB per frame,
|
||||
~10 s per frame; at a lowered resolution 2644×2644 = 56 MB, 6.6 s per frame (~8.5 MB/s). AXRB's zero-copy GPU
|
||||
path covers Vulkan games only. The ~8.5 MB/s was the transport, not the readback: see "GLES games in the
|
||||
emulator" below (adb reverse: ~100 MB/s, half-size eyes, multipass: Lucky's Tale draws at 30 fps).
|
||||
- **Pinball FX VR (UE5, Vulkan): crash 4 s after start**, the same gfxstream `vkAllocateMemory` SIGSEGV from AXRB's
|
||||
runtime layer as headless (so not a headless artefact). AXRB's side.
|
||||
|
||||
Not reached: sound, controllers. Next candidates: a Vulkan game AXRB reports playable (Deadpool VR, Batman: Arkham
|
||||
Shadow); an AXRB issue for the Pinball crash.
|
||||
|
||||
## GLES games in the emulator (2026-10-16, headless under Monado's simulated HMD)
|
||||
|
||||
AXRB 1.0.4's emulator is the only backend with GLES (direct mode's EGL is a stub). A GLES frame is read back with
|
||||
`glReadPixels` on the game's thread inside `xrEndFrame` (`runtime/src/frame_transport.cpp`), sent as raw RGBA to the
|
||||
host (two eyes per frame) and uploaded there; `xrEndFrame` returns only after the send. Measured with
|
||||
`AXRB.Perf eye-readback` / `image-send` (guest log) and `host-image-arrival` (host.err):
|
||||
|
||||
| Eye size (per eye) | Bytes/frame | Route | Send | Frame rate |
|
||||
|---|---|---|---|---|
|
||||
| 896×1007 (Monado's recommendation) | 7.2 MB | default: TCP to 10.0.2.2 (QEMU user-mode network) | 850-950 ms | 1.1 fps |
|
||||
| 448×504 (`scale=0.5`) | 1.8 MB | default | 200-230 ms | 4.4 fps |
|
||||
| 896×1008 | 7.2 MB | **adb reverse** (`debug.axrb.image_transport=adb`) | 87 ms (~83 MB/s) | 9.4 fps |
|
||||
| 640×720 (`scale=0.7`) | 3.7 MB | adb reverse | 38 ms (~96 MB/s) | 17.6 fps |
|
||||
| 448×512 (`scale=0.5`) | 1.8 MB | adb reverse | 14.5 ms (~125 MB/s) | 30.5-30.8 fps |
|
||||
|
||||
The readback itself is 1-3 ms. The default route caps at ~8 MB/s (the same rate as in the headset run above); AXRB's
|
||||
adb reverse route (AXRB sets up `adb reverse tcp:38491` anyway) is 10-15x faster. For SteamVR's resolutions the math
|
||||
stays hard: 2016² per eye = 32.5 MB/frame (~3 fps), at half size 8.1 MB (~10 fps); the owner's 2644² at half size
|
||||
14 MB (~6 fps). Lowering SteamVR's resolution for the game is what helps further (AXRB has a minimum-extent setting,
|
||||
`AXRB_MIN_EYE_EXTENT`, but no maximum).
|
||||
|
||||
What FramePort does for GLES games on this PC (all in the PC build / launcher, nothing on the Frame changes):
|
||||
- FramePort's launcher (`fp-axrb-run.ps1`) starts Android itself when it isn't running (AXRB's Start action, as the run
|
||||
script would; the run script then gets `-OwnsEmulator` and shuts it down), sets `debug.axrb.image_transport=adb`
|
||||
(`FRAMEPORT_AXRB_IMAGE_TRANSPORT=default` leaves AXRB's choice) and `debug.wm.disable_deprecated_target_sdk_dialog=1`.
|
||||
- `pc_selection` with a GLES analysis: FrameBridge `scale` at most 0.5 (FrameBridge's minimum), `scale_max=1` (the
|
||||
maximum eye size reported is the scaled one too), `projection_only=2`: AXRB's emulator refuses every GLES frame with
|
||||
more than one layer (`xrEndFrame` -2), so FrameBridge submits the projection layer alone after drawing the quad
|
||||
layers that follow it into its eye images (`native/adapter/quad_compose_gl.c`, on the game's GL context, per eye at
|
||||
the eye's view and FOV, blending by the quad's flags); `projection_only=1` only drops them.
|
||||
- Game installs use `adb install -g` (runtime permissions granted, as Lepton does on the Frame): Android's permission
|
||||
dialog over the game's activity pauses Unity for good (BattleSisters asks for the microphone in its menu).
|
||||
- The VrApi bridge copies the game's eye textures into the OpenXR images with `glBlitFramebuffer` when the context
|
||||
has no `glCopyImageSubData` (the emulator's GLES translator is "OpenGL ES 3.1" without `GL_EXT_copy_image`; its stub
|
||||
only logs `libEGL: called unimplemented OpenGL ES API`, so every eye reached the runtime black).
|
||||
- `frame.unity_multipass` (experimental on the PC, `pc_detect` for Unity GLES + Oculus XR Plugin): gfxstream lists
|
||||
`GL_OVR_multiview2` but its encoder finds a multiview framebuffer incomplete and drops every draw
|
||||
(`GL2Encoder.cpp:s_glDrawElements … GL error 0x506 checkFramebufferCompleteness`): both eyes black.
|
||||
- `frame.gl_shim` (experimental on the PC, GLES only, `Patch.pc_keeps`): gfxstream has no
|
||||
`GL_EXT_disjoint_timer_query`; the shim lists it and hands out stand-ins ("available, 0 ns") so GLAD engines that
|
||||
call the timer queries unconditionally don't jump to NULL. Its multiview hiding also suits gfxstream.
|
||||
- FrameBridge settings without a rebuild: `frameport settings <pkg> --to pc key=value` writes
|
||||
`/sdcard/Android/data/<pkg>/files/framebridge.conf` (emulator: adb push, Android started for it if needed; direct
|
||||
mode: the file in AXRB's runtime folder); an empty value removes a key.
|
||||
|
||||
Per game (AXRB 1.0.4 emulator, Monado simulated HMD at 72 Hz, FramePort's PC build; Windows 11, RTX 4080 SUPER):
|
||||
|
||||
| Game | Build / settings | Furthest | Frame rate / time | Blocker |
|
||||
|---|---|---|---|---|
|
||||
| Lucky's Tale 1.0.55 (Unity 2019.4 + Oculus XR Plugin, GLES) | PC build + `frame.unity_multipass`, `scale=0.5`, adb route | **title scene drawn in both eyes** (Monado window captured) | 30.5-30.8 fps, image-send 14.5 ms | none headless; needs the headset (picture through SteamVR at its resolution, controllers) |
|
||||
| Accounting+ 1.24.35 (Unity 2017.4 built-in Oculus, GLES) | PC build (`frame.unity_oculus_check`), `projection_only`, old-SDK dialog off | **stance selection screen drawn in both eyes** | 10 fps, image-send 82 ms: its OVRPlugin makes 896×1024 eye buffers whatever FrameBridge's scale/max say | its quads (OVROverlay) were dropped by `projection_only=1` (AXRB rejects every multi-layer GLES frame with -2); rebuilt with `projection_only=2` (2026-10-16, qwerty head): same screen at 9.9 fps, no regression (its controller pointers are missing there because qwerty's controllers are inactive); eye size only shrinks with the PC runtime's resolution |
|
||||
| Path of the Warrior 21.11.10 (VrApi-direct GLES, own engine; from the `PATCHED/` copy) | VrApi bridge (blit copies) + GL shim (timer-query stand-ins), `scale=0.5`, `projection_only=2`, `--sim` with Monado's qwerty head | **its first screen drawn in both eyes**: the "Please ensure that your Oculus Touch Controllers are properly connected" panel (a VrApi cylinder → quad, drawn into the projection) | 38 fps, image-send ~15 ms | the simulator's controllers: Monado's qwerty builder (needed for a tracked head here, see `--sim` notes) has WMR controllers, which AXRB's bindings don't cover. Found on the way: (1) the simulated head: no tracked pose, `EnterVrMode` gave up after 30 s; (2) every frame -2 (projection + the cylinder quad); (3) black eyes: no `glCopyImageSubData` in the emulator's GLES. Before the stand-ins: SIGSEGV at pc 0 in `EgHalGrTimestampQueryGL` (NULL `glad_glGenQueriesEXT`). Needs a headset: controllers past this screen, gameplay |
|
||||
| BattleSisters 1.2.4 (Unity 2019.4 built-in VR, GLES) | PC build (ovrpshim frame loop), `scale=0.5`, installed with `-g`, `--sim` with Monado's qwerty head | **main menu drawn in both eyes** (Continue / New game / Level select hub / Last Bastion / Settings) | 31 fps for 90 s, no stall | none headless. The "hang after 3 frames" was Android's permission dialog: a few seconds into its menu the game asks for a runtime permission (microphone), `GrantPermissionsActivity` became the resumed activity over the game, Unity paused (AAudio pause, no Update2/WaitToBeginFrame/BeginFrame/EndFrame in progress per `ovrp_trace`) and nobody can answer it; not the ovrpshim frame wait and not AXRB's pacing. Fixed by installing with `adb install -g` (as Lepton does). Needs a headset: controllers, gameplay |
|
||||
|
||||
`--sim` notes: AXRB 1.0.4's emulator host only reads `AXRB_TEST_SYNTHETIC_HMD=1` (direct mode's `=2` is newer) and
|
||||
applies it only to a head *without* valid flags; Monado's simulated head is valid but not tracked, so the host never
|
||||
sets its LOCAL origin and VrApi games never get a tracked head. For games on the emulator backend `frameport test
|
||||
--sim` therefore runs Monado's **qwerty** builder (`SIMULATED_ENABLE=0`, `QWERTY_ENABLE=1`: its head reports
|
||||
POSITION_TRACKED; host.err `AXRB OpenXR: LOCAL origin in tracking world=(0.000 1.600 0.000)`). Its controllers are
|
||||
WMR ones, which AXRB's suggested bindings (Touch/Index/Vive) don't cover: no hands. `FRAMEPORT_PC_XR_SIM_HEAD=simulated`
|
||||
keeps the simulated head (and its ml2 controllers), `=tracked` forces qwerty. Monado's `remote` builder (tracked head +
|
||||
Index controllers) would cover both but listens on 0.0.0.0:4242 (a firewall prompt), so it isn't used. FrameBridge's
|
||||
`ovrp_trace=1` (ovrpshim, Unity 2017-2019 legacy frame loop) logs every 5 s how many Update2/WaitToBeginFrame/
|
||||
BeginFrame/EndFrame calls went through and which one is in progress (thread, frame, how long): where a stalled game
|
||||
waits. AXRB's opt-in host capture (`AXRB_CAPTURE_PREFIX`) only starts at sequence 2000 in 1.0.4 (hard-coded;
|
||||
`AXRB_CAPTURE_FIRST_SEQUENCE` isn't read), which GLES games at 10-30 fps don't reach in a test: use the Monado
|
||||
window captures and FrameBridge `snapshot=N`. AXRB's guest log only keeps warnings plus AXRB's own tags: read
|
||||
`adb logcat -d` for FrameBridge/OVRPlugin lines. `debuggerd -b <pid>` works from `adb shell` (stacks).
|
||||
|
||||
## Open (needs the owner + a headset)
|
||||
|
||||
- A game in the headset through SteamVR (picture, audio, controllers; Steam Frame controllers: AXRB #20).
|
||||
- Pinball FX VR's Vulkan crash with the host attached happens in the emulator only; in direct mode it reaches its lobby
|
||||
headless. Needs the headset: picture/latency through SteamVR, controllers, a table played.
|
||||
- Play from the PC's Steam library (shortcut → launcher script → AXRB).
|
||||
|
||||
## Testing without a headset (Monado's simulated HMD, 2026-10-15)
|
||||
|
||||
AXRB's host bridge needs an OpenXR session with a D3D11 binding, which SteamVR doesn't give without a headset. For
|
||||
unattended tests FramePort can run the bridge against **Monado for Windows** with Monado's simulated HMD instead,
|
||||
without changing the system's OpenXR runtime or touching SteamVR:
|
||||
|
||||
- **Monado build:** an official CI artifact of `monado/monado` (gitlab.freedesktop.org, project 2685), job `windows`
|
||||
of a `main` pipeline (there is no Windows release asset; the job's `install/` folder is the portable runtime:
|
||||
`bin\monado-service.exe`, `bin\openxr_monado.dll`, `openxr_monado.json`, SDL2/Vulkan loader DLLs). Tested build:
|
||||
pipeline 1766968, job 112793105, commit 8b259763f (`v25.1.0-917-g8b259763f`). Unpacked by hand into
|
||||
`%LOCALAPPDATA%\FramePort-dev\monado\install` (or `FRAMEPORT_MONADO_DIR`): no installer, no UAC, no registry. Its
|
||||
`share\steamvr-monado` (a SteamVR driver) is never registered.
|
||||
- **Selection per process only:** the OpenXR loader's `XR_RUNTIME_JSON=<monado>\openxr_monado.json`. HKLM's
|
||||
`ActiveRuntime` stays SteamVR. AXRB's run script registers an app identity with SteamVR whenever HKLM names SteamVR
|
||||
(an OpenVR utility client, which starts SteamVR), so under the simulator FramePort's launcher call answers that one
|
||||
`python steamvr_app_identity.py` call "failed" (the script only warns).
|
||||
- **Service settings** (`tools/xr_sim.SERVICE_ENV`): `SIMULATED_ENABLE=1` (the simulated builder; the Windows build
|
||||
only has the qwerty/remote/simulated builders, so no real device is probed), `SIMULATED_LEFT/RIGHT=ml2` (the only
|
||||
simulated controllers whose bindings cover the Vive wand profile: AXRB suggests Touch/Index/Vive, so with `simple`
|
||||
or `wmr` its hands stay inactive), `XRT_COMPOSITOR_DEFAULT_FRAMERATE=72`. Headless variant: `XRT_COMPOSITOR_NULL=1`
|
||||
+ `XRT_COMPOSITOR_NULL_FPS=72` (no window, nothing to capture).
|
||||
- **D3D11 + adapter:** Monado's D3D11 client compositor works (timeline semaphores); it renders on the discrete GPU
|
||||
(Vulkan device 0, RTX 4080 SUPER here; `XRT_COMPOSITOR_FORCE_GPU_INDEX` would change it) and asks D3D11 clients for
|
||||
that adapter's LUID, which DXGI finds.
|
||||
- **Results:** a minimal D3D11 client (`native/xrsimprobe/xr_sim_probe.cpp`, built with VS Build Tools against the
|
||||
Khronos `openxr_loader_windows` 1.1.63 release) went IDLE → READY → SYNCHRONIZED → VISIBLE → FOCUSED in 0.01 s
|
||||
and submitted 72 frames/s until it asked to exit. AXRB's host bridge (`--serve-openxr 38490 0 <title>`, no
|
||||
emulator) reached `D3D11 graphics binding ready`, session state 5 (FOCUSED), `AXRB TCP: listening on
|
||||
0.0.0.0:38490`, both controllers active with valid poses, `host-end-frame: rate=72.0/s`, its splash in both eyes.
|
||||
No game was run yet.
|
||||
- **Seeing what is drawn:** the windowed compositor ("Monado (Windowed)", 1264×681, both eyes side by side without
|
||||
distortion) can be saved with `native/xrsimprobe/xr_sim_capture.cpp` (`PrintWindow`, works behind other windows;
|
||||
`xr_sim.capture(path)`), and `--sim` also sets AXRB's own opt-in `AXRB_CAPTURE_PREFIX` (4 PPM readbacks of the
|
||||
submitted layers, 5 s apart, from frame 300) into `<data>/logs/xr-sim/`. A launch test that reached RUNNING saves
|
||||
Monado's window there too, just before it stops the game (`<time>-<pkg>-headset.bmp`, `pc_android.sim_capture`),
|
||||
and every AXRB launch test reports the frame rate from AXRB's `AXRB.Perf` summaries (game submits / bridge). Monado itself has no screenshot function;
|
||||
`XRT_WINDOW_PEEK` and `XRT_DEBUG_GUI` exist but only show windows.
|
||||
- **Use:** `frameport test <pkg> --to pc --sim` (or `FRAMEPORT_PC_XR_SIM=1`): starts `monado-service.exe` hidden
|
||||
(WMI, like AXRB's scripts; ready when its pipe `\\.\pipe\<%TEMP%>\monado_comp_ipc` exists), passes the environment to
|
||||
the AXRB launch (`pc_android.launch_test(extra_env=…)`, default `xr_sim.active_env()`), stops the service afterwards
|
||||
(an already running Monado is used and left running). Service log: `<data>/logs/xr-sim-service.log`.
|
||||
- **Head per backend:** when every game tested is on AXRB's emulator backend, the service runs Monado's qwerty builder
|
||||
instead of the simulated one (a tracked head; AXRB 1.0.4's emulator host has no synthetic head for Monado's
|
||||
untracked one, see the GLES section's `--sim` notes); its controllers stay inactive in AXRB.
|
||||
`FRAMEPORT_PC_XR_SIM_HEAD=tracked|simulated` overrides the choice. An already running Monado is used as it is.
|
||||
- **Limits:** the simulated head wobbles slowly and the controllers hold still with no buttons pressed, so it proves
|
||||
startup, frame submission and what is drawn, not gameplay; the windowed compositor opens a visible window on the
|
||||
desktop; frame pacing is Monado's, not a headset's.
|
||||
+12
-21
@@ -268,27 +268,18 @@ Scan a folder with Rift game dumps (Windows game folders) like Quest dumps; they
|
||||
| Unreal game starts (window created) then `CrashReportClient` runs | the game crashed under Proton; often no VR runtime reached it (Revive off) | keep `pcvr.revive` on (repacks' bundled LibRevive64.dll isn't loaded by itself); read the game log + crash summary in the launch log |
|
||||
| `VK_ERROR_DEVICE_LOST` under DXVK | freedreno GPU hang | `pcvr.proton_log` to capture; PC mode |
|
||||
|
||||
## Quest games on this PC (AXRB, `docs/PC_ANDROID.md`)
|
||||
## Experimental Rift through FEX
|
||||
|
||||
These signatures apply to PC VR logs (`kind: pcvr`). Compatibility profiles describe static evidence until a
|
||||
headset test verifies them. Meta's entitlement and runtime signature checks remain intact.
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Unconverted Quest APK in AXRB: no launcher activity, SIGABRT ~0.3 s after `libovrplatformloader.so` loads | Meta's platform loader aborts outside a Quest | FramePort always gives AXRB its OVRPort build (the PC build: OVRPort + `on_pc` APK fixes, no Steam Frame fixes) |
|
||||
| `axrb-no-headset`: host.err `failed to find D3D11 adapter requested by OpenXR runtime` / `xrCreateInstance failed: XR_ERROR_RUNTIME_FAILURE` | SteamVR has no headset (null driver: no D3D11 adapter; vrlink with no Frame connected: no instance) | connect the headset and let SteamVR show it first; for unattended tests use Monado's simulated headset: `frameport test <pkg> --to pc --sim` (docs/PC_ANDROID.md "Testing without a headset") |
|
||||
| Testing a PC (AXRB) game without a headset: need a D3D11 OpenXR session, but SteamVR must stay untouched | SteamVR without a headset gives no D3D11 adapter / no instance | `--sim` (or `FRAMEPORT_PC_XR_SIM=1`): portable Monado for Windows, simulated HMD + `ml2` controllers, selected per process with `XR_RUNTIME_JSON` (the system runtime stays SteamVR); AXRB's SteamVR app registration is skipped; frames in `<data>/logs/xr-sim/` (AXRB_CAPTURE_PREFIX), Monado's window via `xr_sim.capture()`. Service log `<data>/logs/xr-sim-service.log` |
|
||||
| `axrb-gfxstream-crash`: SIGSEGV in `vulkan.ranchu.so (gfxstream::vk::ResourceTracker::on_vkAllocateMemory)` from `libVkLayer_AXRB_runtime.so` (Pinball FX VR, UE5 Vulkan, headless) | the emulator's Vulkan path through AXRB's runtime layer | report to AXRB with diagnostics (`axrb/*` logs); recheck with the host attached |
|
||||
| `axrb-translation-crash`: crash in `libndk_translation.so (berberis::…)` | Android's ARM translation (AXRB issue #9, some Unity games) | nothing to patch; wait for AXRB's other translator |
|
||||
| Reinstall fails `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, or the game data must be copied again | a copy signed with another key was installed in AXRB (e.g. by AXRB's own launcher); a full uninstall deletes the OBB folder and saves | uninstall it in AXRB first; FramePort keeps one key per game, so its own updates keep data |
|
||||
| A FramePort call that runs an AXRB script never returns although the script ended (emulator already up) | QEMU (`Start-Process`) and the adb server inherit the caller's handles; through WSL interop even a file or /dev/null keeps the relay open | `axrb.spawn_detached` starts the script through WMI (`Win32_Process.Create`, nothing inherited) and polls its PID; exit code from the log's `FP_EXIT` line |
|
||||
| Which AXRB runs a game? (`deployment.json` `backend`) | AXRB 2.x (AXRB-Emulatorless) has a direct mode (`axrb-direct.exe`, no emulator) that only renders Vulkan; 1.0.4 is the emulator | automatic: direct for `graphics` Vulkan, emulator for GLES/unknown; `pc.axrb_backend` (backend / translator / precompose / legacy-driver options) or `frameport install <pkg> --to pc --axrb-backend direct\|emulator`; a switch leaves the other backend's copy and saves where they are |
|
||||
| `axrb-direct-no-gles`: direct.log `eglInitialize … EGL_BAD_DISPLAY` / `Unable to initialize EGL` / Unity's `Unable to retrieve the default EGL display!` | the game draws with OpenGL ES; direct mode's EGL is a stub (no GLES renderer). Not reported once a Vulkan device exists (Unreal Vulkan builds probe EGL first) | install it for the emulator (`pc.axrb_backend` backend=emulator); automatic choice already does this when the analysis says GLES |
|
||||
| `axrb-direct-incomplete`: `Direct mode installation is incomplete` / `Direct mode is missing …` / `Preparing the AOSP application failed` | a direct-mode file of AXRB is missing, or the sysroot (`<runtime>\direct\sysroot`, receipt `.axrb-sysroot.json` digest + format) isn't complete | reinstall AXRB-Emulatorless or rebuild the local build (`FRAMEPORT_AXRB_DIRECT_DIR`), then `frameport pc install-axrb --backend direct` (re-runs AXRB's `make_sysroot.py --art`) |
|
||||
| `axrb-direct-no-vulkan`: `vulkan-1.dll is not installed: the guest has no GPU` | no Windows Vulkan loader/driver | install the GPU's current driver |
|
||||
| `axrb-direct-unhandled-instruction`: `fatal signal 4 (SIGILL)` in direct.log | Dynarmic (or Digitalis) can't execute an arm64 instruction | report to AXRB with diagnostics (`axrb/direct/*`); try `pc.axrb_backend` translator=digitalis (or dynarmic) or the emulator |
|
||||
| `axrb-direct-guest-crash` (`E/cpu: thread … fatal signal 11/7/6/5`), `axrb-direct-host-fault` (`E/host-fault:` / `E/fatal:` = axrb-direct.exe itself), `axrb-direct-java-crash` (`FATAL EXCEPTION`, `Runtime aborting`) | a crash under direct mode's translator/ART; usually a missing Android service (Meta/Play/account/DRM), a translation bug or an unsupported syscall/Binder call (`axrb-direct-unsupported-syscall`, warning) | the other translator or the emulator; report to AXRB with the backtrace in direct.log |
|
||||
| `axrb-direct-no-native-window`: `AXRB_AOSP_HOST_SURFACES 0`, then nothing (The Climb 2: last line `Loading plug-in CryHTTP`, the main thread busy-polls its looper) | NativeActivity games take their surface with `Window.takeSurface`; AXRB's direct mode (`HostWindow.attach`) only bridges SurfaceViews, so `APP_CMD_INIT_WINDOW` never comes | AXRB's side (drafted upstream issue + a ~40-line HostWindow patch, tested); FramePort can't fix it in the APK. With the patched bootstrap the game gets its window (`AXRB_AOSP_TAKEN_SURFACE`) |
|
||||
| `axrb-direct-vulkan-extensions`: CryEngine `Renderer initialization` → `CrySystem initialization failed!` with no vkCreateDevice (The Climb 2, direct mode) | the PC's driver lacks Quest-only device extensions (VK_EXT_fragment_density_map is the one Climb 2 requires; Android external memory too); AXRB's runtime also asks for AHB in direct mode | `frame.vk_android_extensions` (PC builds only, added automatically for direct-VrApi Vulkan games): the renderer's `vkEnumerateDeviceExtensionProperties` import → `libfp_vkext.so`, which lists them; AXRB drops them again at vkCreateDevice. The VrApi bridge leaves runtime extensions no device has out of `vrapi_GetDeviceExtensionsVulkan` |
|
||||
| AXRB under Monado's simulator: VrApi games only submit empty frames; bridge `initial recenter: head flags=0x0`, `EnterVrMode: no tracked head pose before deadline` | Monado's simulated head lacks POSITION_TRACKED, so AXRB's host never sets up its LOCAL origin (`local_origin_flags` 0) and every pose in LOCAL has flags 0 | `--sim` sets AXRB's test-only `AXRB_TEST_SYNTHETIC_HMD=2` (fixed standing head + valid LOCAL; direct mode's host only: AXRB 1.0.4's emulator host only knows `=1`, which it ignores for a valid head) and, for games on the emulator backend, runs Monado's qwerty head instead (POSITION_TRACKED, the host sets LOCAL: `AXRB OpenXR: LOCAL origin in tracking world=(0.000 1.600 0.000)`; its WMR controllers stay inactive in AXRB, `FRAMEPORT_PC_XR_SIM_HEAD=simulated` keeps the simulated head); real headsets need neither |
|
||||
| AXRB emulator, GLES VrApi game (Path of the Warrior): frames submitted but both eyes pure black (FrameBridge `snapshot` of the eye images: all 0), guest log `libEGL: called unimplemented OpenGL ES API` about once a second | the emulator's GLES translator ("OpenGL ES 3.1") has no `glCopyImageSubData`: the VrApi bridge's copy of the game's eye texture into the OpenXR image did nothing | the bridge copies with `glBlitFramebuffer` when the context lacks ES 3.2 / `GL_EXT_copy_image` (log `GLES eye copies: glBlitFramebuffer, no glCopyImageSubData (OpenGL ES 3.1 …)`) |
|
||||
| AXRB emulator, GLES game with menus/messages as quad layers (VrApi cylinders, OVROverlay): `xrEndFrame failed: -2` on every frame, or with `projection_only=1` the menus missing | AXRB 1.0.4's emulator refuses GLES frames with more than one layer | FrameBridge `projection_only=2` (the PC default for GLES): the quads after the projection layer are drawn into its eye images on the game's GL context, then the projection layer goes alone (log `projection_only=2: 1 quad layer(s) drawn into the main picture`) |
|
||||
| A direct-mode launch test ran its full time but ends in `axrb-direct-guest-crash` | the game crashed during AXRB's own stop (after `AXRB_AOSP_ACTIVITIES_STOPPED`; The Climb 2 after `ovr_Shutdown finished`) | triage turns it into the info finding `axrb-direct-crash-at-stop`; the verdict passes |
|
||||
| Direct-mode launch test: "NEVER_STARTED" although AXRB's bridge ran | direct.out never printed `AXRB_AOSP_ACTIVITY_CREATED`/`AXRB_AOSP_LIFECYCLE` (ART's first start is interpreted, `-Xint`: slow); the run script stops when the bridge loses its headset | check host.err (`axrb-no-headset`), direct.out/err; the test waits 5 min for the activity and stops through AXRB's `stop_game.ps1` (closes the bridge window, the game pauses and saves) |
|
||||
| Meta runtime says `Missing entitlement` | The signed-in Meta account does not own the game | Informational, not a FramePort bug; use an account that owns it. No bypass or patch is offered (`fex-missing-entitlement`) |
|
||||
| `OVRServer` reports `install_available` | The matching app is not registered as installed | Reinstall/register through the FEX install flow with the matching Meta manifest (`fex-registration-missing`) |
|
||||
| `Rejecting new committed universe since IMU fallback is active` | SteamVR has not acquired headset tracking since boot | Wear the headset once and wait for tracking before retrying (`fex-imu-fallback`) |
|
||||
| `Service invocation completed`, followed by confirmation `OVRLibrarian still running` | Wine Mono's OVRLibrarian never exits under FEX and blocks later clients | End only OVRLibrarian.exe after completion; check that the FEX launcher's cleanup runs. Completion alone is healthy (`fex-librarian-stuck`) |
|
||||
| `vkd3d` fails to create/initialize a device | D3D12 renderer initialization failed | Check GE's bundled vkd3d-proton, clear inherited wined3d/no-D3D12 flags, verify FEX Vulkan thunk to Turnip (`fex-vkd3d-init`) |
|
||||
| `wined3d` fails to initialize OpenGL | No working guest GL/display route; the Vulkan thunk does not provide a GL thunk | Inspect guest Mesa renderer and display access; prefer the game's D3D9/11 route if available (`fex-wined3d-init`) |
|
||||
| `ReviveInjector` reports failure to start/inject | Wrong exe, missing x64 Revive files or injection failure | Check executable and shared GE prefix; use OpenVR mode, without `/openxr`. OpenVR-only games can run directly (`fex-revive-injector`) |
|
||||
| Robo Recall has no IME CVar in ApplicationCore or Slate | Older modular Unreal keeps FWindowsApplication in Core | Search `*-Core-Win64-Shipping.dll` with the same anchored CVar finder; this surveyed build has the constructor default at RVA `0x15ffe5`. Never patch an unanchored boolean based on an assumed member offset |
|
||||
@@ -0,0 +1,276 @@
|
||||
# Rift through FEX: compatibility stream C
|
||||
|
||||
Compatibility is static evidence until a headset test verifies a build. First Contact remains the only verified
|
||||
profile. No game entitlement, Platform SDK or Oculus signature checks are changed. A Meta account without ownership
|
||||
must receive **Missing entitlement**; this is an informational result, not a bug to work around.
|
||||
|
||||
## Implementation and profile schema
|
||||
|
||||
`agent/fexrift_compat.py` registers a `GAME_INFO_HOOKS` handler, launcher environment handler and install-check
|
||||
handler. `game_info()` obtains the exe and launch arguments from Meta's manifest, then applies a matching profile.
|
||||
The helper reports `verified: false` for likely/untested builds. Likely profiles install with a warning (also in
|
||||
`checks.profile_warnings`, which the parent agent forwards); untested builds need `force_unverified`; blocked
|
||||
profiles are refused even with force. `checks` records the selected profile, status, route, cache eligibility and
|
||||
32-bit warning. No agent version bump is included.
|
||||
|
||||
`agent/fexrift_games.json` is data, not executable configuration. Its `_comment` describes these fields:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `match.canonical_names` | List of exact Meta canonical app names; no fuzzy title matching |
|
||||
| `match.sha256` | List of exact executable SHA-256 strings |
|
||||
| `match.executables` | List of `{name, size}` objects: case-insensitive basename **and** exact byte size must match |
|
||||
| `status` | `verified`, `likely`, `untested`, `blocked` |
|
||||
| `graphics` | `d3d11` (default), `d3d9`, `d3d12`, `opengl`, `vulkan` |
|
||||
| `env` | String environment values |
|
||||
| `ime_patch` | Optional fallback `{rva, old, new}`, with integer/hex RVA; null means no fallback |
|
||||
| `fp_mem` | `off`, `candidate` (informational, no preload), `enabled` (verified per-game opt-in) |
|
||||
| `bigcache_mb` | Cache size when enabled, default 256 |
|
||||
| `dxvk` | List of dxvk.conf lines; launcher writes `$A/dxvk.conf` and exports `DXVK_CONFIG_FILE` |
|
||||
| `args` | Optional argument list override; omit to preserve manifest arguments |
|
||||
| `notes` | Evidence and first-launch cautions |
|
||||
| `revive` | Optional, defaults true; false launches an OpenVR-only game directly |
|
||||
|
||||
Match alternatives are OR, while basename and size within an executable match are AND. Multiple matching profiles
|
||||
are refused. With no `match`, the profile key is the exact canonical name (legacy First Contact compatibility).
|
||||
The surveyed profiles use basename+size because the owner's folders do not establish authoritative Meta canonical
|
||||
names; their keys identify profiles, **not** presumed Meta app IDs. They preserve the manifest's exe and arguments.
|
||||
A different store build/size needs a new survey or an explicit untested install; do not guess its identity.
|
||||
|
||||
A SHA-only identity should be rechecked against the original executable after any game-byte patch: patching changes
|
||||
its hash. Prefer canonical names or basename+size for profiles that need the IME fix. Size matches choose launch
|
||||
settings only; the anchored finder independently verifies the bytes before patching.
|
||||
|
||||
`fp_mem: candidate` is set only when the surveyed lower bound of the UE version is >=4.18 (Binned2 big-block path).
|
||||
It never enables the unzeroed cache. Every other game needs evidence that it fully overwrites reused memory before
|
||||
an opt-in. First Contact's existing explicit environment/cache settings remain enabled.
|
||||
|
||||
## Older Unreal: Robo Recall
|
||||
|
||||
The premise that Robo Recall lacks `Slate.DeferWindowsMessageProcessing` was a search-location error. Its modular
|
||||
`RoboRecall-Core-Win64-Shipping.dll` contains the UTF-16 CVar at file offset `0x1e1bb0`; the existing anchored finder
|
||||
returns RVA `0x15ffe5`, old `c7860002000001000000`, new `c7860002000000000000`. This changes the constructor's
|
||||
`mov dword ptr [rsi+0x200],1` immediate to zero, with the existing expected-byte guard and `.orig` backup.
|
||||
|
||||
Older Unreal placed `FWindowsApplication` in Runtime/Core; newer trees split it into Runtime/ApplicationCore.
|
||||
`bAllowedToDeferMessageProcessing` and `GetDeferMessages` control the same Windows message deferral path, so an old
|
||||
engine is not exempt from the IME deadlock merely because ApplicationCore/Slate lack the marker. The extension
|
||||
extends `find_ime_target()` to search `*-Core-Win64-Shipping.dll` (and Win32/Core.dll forms), using the unchanged
|
||||
CVar/xref/storage finder. It never patches a guessed member offset. Synthetic tests cover successful Core discovery,
|
||||
repeat installs, and a CVar name without an anchored reference. No Unreal source checkout was available offline;
|
||||
the observed binary is the evidence for this build, rather than an inferred engine-version exemption.
|
||||
|
||||
## Graphics and remaining device questions
|
||||
|
||||
DXVK handles D3D9/11; D3D12 uses GE's own vkd3d-proton selection. The launcher clears inherited
|
||||
`PROTON_USE_WINED3D`, `PROTON_NO_D3D12` and `PROTON_NO_D3D11`, keeps `FP_VKTHUNK=1`, and lets Proton select/copy its
|
||||
shipped DLLs. It makes no renderer DLL edits in a game. `checks.vkd3d_dlls` reports installed d3d12 DLLs under
|
||||
`files/{lib,lib64}/wine/vkd3d-proton/{,x86_64-windows/,i386-windows/}`. These are discovery candidates, not a hardcoded
|
||||
installation prerequisite. GE-Proton's script/archive was not available locally, and network/device access was
|
||||
prohibited: verify the exact 11-7 layout and its script logic on the device before claiming D3D12 support.
|
||||
|
||||
The `opengl` profile exports `PROTON_USE_WINED3D=1`. A native OpenGL game still calls OpenGL directly; this flag
|
||||
selects Wine's GL renderer for any D3D calls. There is **no configured FEX GL thunk**. Host acceleration would need
|
||||
FEX's matching guest libGL/libEGL thunk libraries, host counterparts and enabled GL thunk configuration, with a
|
||||
working display/GLX/EGL bridge. Guest rootfs Mesa might instead use software rendering or Zink through Vulkan;
|
||||
inspect the actual renderer before attributing a game failure to Wine or promising hardware acceleration.
|
||||
The Climb prefers its imported D3D9 route; imports alone do not prove which renderer its config selects.
|
||||
|
||||
A 32-bit exe is flagged in the survey and install checks. GE WoW64 under FEX still needs working 32-bit graphics
|
||||
and SteamVR's 32-bit vrclient bridge; it is not promoted to likely based on engine alone. XAudio2 and Bink are facts
|
||||
to watch; Media Foundation is flagged for a real movie-playback check. Anti-cheat detected by filenames/imports
|
||||
makes a generated profile blocked. Absence of a static marker is not proof that runtime-loaded middleware is absent.
|
||||
|
||||
## Reproduce the offline survey
|
||||
|
||||
Use an isolated `FRAMEPORT_HOME`; never set `FRAMEPORT_WINDOWS_DATA`. No program in a dump is executed, no archive
|
||||
is extracted, and no file in a game folder is written. The report contains local paths and must stay uncommitted.
|
||||
|
||||
```bash
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
uv run python - <<'PY'
|
||||
import subprocess, sys
|
||||
from pathlib import Path
|
||||
root = Path('/mnt/d/Backup Data/Misc/emus/VR_META_QUEST/ARMGDDN_PCVR_OCULUS')
|
||||
cmd = [sys.executable, 'scripts/rift_fex_survey.py', '--out', '.home/survey']
|
||||
for folder in sorted(root.iterdir()):
|
||||
if folder.is_dir():
|
||||
cmd += ['--folder', str(folder)]
|
||||
subprocess.run(cmd, check=True)
|
||||
PY
|
||||
```
|
||||
|
||||
Outputs: `.home/survey/survey.json`, `survey.md`, and `profiles.json` (ready to paste, but review argument overrides
|
||||
and direct OpenVR routing). The Unreal pak/ini reader is deliberately left in place for stream B's extraction.
|
||||
|
||||
## Owner's library: static results
|
||||
|
||||
All 19 selected game exes are x86_64; no selected 32-bit client was found. Eighteen use the Platform SDK; Rick and
|
||||
Morty is the exception. The table contains **10** likely titles (including Robo Recall and all three Vader episodes),
|
||||
plus nine untested titles. Profile IDs below are data keys matched by exe basename+size, not canonical app names.
|
||||
`candidate` means cache eligibility only; it is off at launch. No surveyed game is promoted to verified.
|
||||
|
||||
| Game / profile ID | Route | Status | Platform SDK | fp_mem | Watch at first launch |
|
||||
|---|---|---|---|---|---|
|
||||
| Arktika 1 / `arktika-1` | d3d11 trial; API undetected | untested | yes | off | Establish actual renderer and custom-engine startup; do not infer D3D11 from this default |
|
||||
| Asgard's Wrath / `asgards-wrath` | d3d11 | likely | yes | candidate | Large-block stalls/memory, Media Foundation movies, entitlement |
|
||||
| Brass Tactics / `brass-tactics` | d3d11 | likely | yes | off | Media Foundation movies, tracking and IME startup |
|
||||
| Chronos / `chronos` | d3d11 | likely | yes | off | Older UE D3D11 path, movies and controller input |
|
||||
| Defector / `defector` | d3d11 | untested | yes | off | Custom-engine startup and Bink cutscenes |
|
||||
| Hubris / `hubris` | d3d11 | likely | yes | candidate | XAudio2 sound, Media Foundation movies, shader/GPU cost |
|
||||
| Lies Beneath / `lies-beneath` | d3d11 | likely | yes | candidate | Platform SDK delay load/entitlement; IME fix and movies |
|
||||
| Lone Echo / `lone-echo` | d3d11 (also imports d3d9) | untested | yes | off | Runtime tracks client, entitlement; custom-engine startup and allocator stalls |
|
||||
| Lone Echo II / `lone-echo-2` | d3d12 | untested | yes | off | vkd3d-proton DLL layout and Vulkan device creation on Turnip |
|
||||
| Rick and Morty / `rick-and-morty-virtual-rick-ality` | opengl; direct OpenVR | untested | **no** | off | Actual GL renderer/acceleration; SteamVR bridge; speech WinRT; never inject Revive |
|
||||
| Robo Recall / `robo-recall` | d3d11 | likely | yes | off | Core DLL IME fix at `0x15ffe5`; Meta entitlement and tracking |
|
||||
| Skydance's Behemoth / `skydances-behemoth` | d3d11 | likely | yes | candidate | XAudio2 sound, Media Foundation movies and heavy GPU/memory use |
|
||||
| Stormland / `stormland` | d3d11 | untested | yes | off | Custom engine, Media Foundation and Bink cutscenes |
|
||||
| The Climb / `the-climb` | d3d9 preferred; also imports GL | untested | yes | off | Confirm CryEngine selects D3D9 and DXVK; GL import alone is not a selected renderer |
|
||||
| The Unspoken / `the-unspoken` | d3d11 | untested | yes | off | Custom engine, Media Foundation and Bink cutscenes |
|
||||
| Vader Immortal I / `vader-immortal-episode-i` | d3d11 | likely | yes | off (UE range starts at 4.16) | Movies, controller input, startup IME; leave MSAA at its game default |
|
||||
| Vader Immortal II / `vader-immortal-episode-ii` | d3d11 | likely | yes | off (UE range starts at 4.16) | Movies, controller input, startup IME; leave MSAA at its game default |
|
||||
| Vader Immortal III / `vader-immortal-episode-iii` | d3d11 | likely | yes | off (UE range starts at 4.16) | Movies, controller input, startup IME; leave MSAA at its game default |
|
||||
| Wilson's Heart / `wilsons-heart` | d3d11 | untested | yes | off | Custom-engine startup and Bink cutscenes |
|
||||
|
||||
## Exact maintainer device checks (not executed here)
|
||||
|
||||
Only run one FEX game at a time: they share the Wine prefix and wineserver. The following uses the existing CLI;
|
||||
stream A/B may add a direct login/install path at integration. Set `FRAMEPORT_HOME` to the development data folder
|
||||
with the paired Frame configuration, then keep using that folder. The example `.home` is isolated from normal app
|
||||
data and needs a paired Frame first. Never set `FRAMEPORT_WINDOWS_DATA`.
|
||||
|
||||
1. On the development PC, upload the integrated helper/extensions/profiles and verify setup:
|
||||
|
||||
```bash
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
export FRAMEPORT_EXPERIMENTAL_FEX=1
|
||||
uv run frameport fex setup
|
||||
uv run frameport fex status
|
||||
```
|
||||
|
||||
Wait for setup to finish. Expect `proton`, `prefix` and `fex` true; `apps` lists Meta canonical names. No token or
|
||||
sign-in URL is needed in the output. If importing from an already signed-in PC Wine prefix, use the exact app
|
||||
name from its Meta manifest filename (replace both example values):
|
||||
|
||||
```bash
|
||||
app='actual-meta-canonical-name'
|
||||
prefix='/path/to/signed-in/wine-prefix'
|
||||
uv run frameport fex import-login "$prefix" --app "$app"
|
||||
uv run frameport fex install "$app"
|
||||
```
|
||||
|
||||
For a matching likely profile, expect `verified: false`, `checks.profile_status: likely`, a profile ID and
|
||||
`checks.profile_warnings`; no `--force-unverified` should be needed. If the downloaded build differs from the
|
||||
surveyed basename+size, expect refusal and review its own survey before forcing. An explicitly blocked profile
|
||||
must still refuse with `--force-unverified`. An untested game needs that switch. The PC reporter also warns
|
||||
that the game has not been verified on the Frame.
|
||||
|
||||
2. On the Frame, wear the headset once after boot and wait for tracking. Launch from the Steam shortcut. For these
|
||||
checks in a Frame terminal, replace `app` with the same canonical name:
|
||||
|
||||
```bash
|
||||
app='actual-meta-canonical-name'
|
||||
pkg="rift.${app//-/_}"
|
||||
anchor="$HOME/Applications/quest-frame/$pkg"
|
||||
python3 - "$anchor/deployment.json" <<'PY'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
for key in ('profile', 'profile_status', 'profile_warnings', 'graphics', 'fp_mem', 'exe_32bit', 'wow64', 'vkd3d_dlls'):
|
||||
if key in d.get('checks', {}):
|
||||
print(key, d['checks'][key])
|
||||
PY
|
||||
rg 'FramePort FEX: graphics=' "$anchor/launch.log"
|
||||
rg 'PROTON_USE_WINED3D|PROTON_NO_D3D12|DXVK_CONFIG_FILE|export FP_MEM' "$anchor/launch.sh"
|
||||
```
|
||||
|
||||
Expect the profile route. Candidate caches must have no `export FP_MEM=1`. For Rick and Morty also run
|
||||
`rg 'taskset.*proton' "$anchor/launch.sh"`: the command must invoke the game directly with no ReviveInjector.
|
||||
A graphics route marker proves selection of launcher settings, not successful rendering. Check both eyes,
|
||||
tracking/controllers, sound and a movie in the headset before changing a profile to verified.
|
||||
|
||||
3. For Robo Recall, `checks.ime.file` should name `RoboRecall-Core-Win64-Shipping.dll` and `fixes.ime_patch` should be
|
||||
`patched` on first install and `already` on repeat. On the Frame, verify the original backup exists and the
|
||||
constructor immediate is zero (only the installed copy is read):
|
||||
|
||||
```bash
|
||||
python3 - "$anchor/deployment.json" <<'PY'
|
||||
import importlib.util, json, os, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
path = os.path.join(os.path.dirname(d['exe']), d['checks']['ime']['file'])
|
||||
helper = os.path.expanduser('~/.local/share/frameport/fexrift/artifacts/fexrift.py')
|
||||
spec = importlib.util.spec_from_file_location('fx', helper)
|
||||
fx = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(fx)
|
||||
b = open(path, 'rb').read()
|
||||
p, reason = fx.find_ime_patch(b)
|
||||
assert p and b[fx.pe_offset(b, p['rva']):fx.pe_offset(b, p['rva']) + len(bytes.fromhex(p['new']))] == bytes.fromhex(p['new']), reason
|
||||
assert os.path.isfile(path + '.orig')
|
||||
print('Core IME default zero; original backup present')
|
||||
PY
|
||||
```
|
||||
|
||||
4. For Lone Echo II (untested), install with `--force-unverified`. In a Frame terminal inspect only GE's files and
|
||||
script; no network fetch is needed:
|
||||
|
||||
```bash
|
||||
r="$HOME/.local/share/frameport/fexrift"
|
||||
rg --files "$r/ge/files" | rg '/vkd3d-proton/.*/?d3d12(core)?\.dll$|/vkd3d-proton/d3d12(core)?\.dll$'
|
||||
rg -n 'PROTON_USE_WINED3D|PROTON_NO_D3D12|vkd3d-proton|dxvk' "$r/ge/proton"
|
||||
```
|
||||
|
||||
Expect bundled vkd3d-proton and script selection when wined3d/no-D3D12 are unset. `checks.vkd3d_dlls` should name
|
||||
the installed layout. A vkd3d initialization failure is triaged `fex-vkd3d-init`; a successful Vulkan device plus
|
||||
frames in both eyes is the first actual evidence of this route. If the file layout differs, update discovery
|
||||
candidates without copying DLLs into the game. For GL, inspect the active **guest** renderer; an installed host
|
||||
Mesa library or the Vulkan thunk does not establish guest GL acceleration.
|
||||
|
||||
5. Exercise each triage signature offline on the PC with
|
||||
`uv run pytest -q tests/test_fexrift_compat.py -k triage`. On-device expected strings are `Missing entitlement`,
|
||||
`OVRServer ... install_available`, SteamVR's IMU-fallback message, vkd3d/wined3d initialization failure, or a
|
||||
ReviveInjector failure. Do not dump entire Meta log files: they may carry sign-in data. A Librarian completion
|
||||
line alone must not diagnose a hang. After completion, confirm persistence using `pgrep -f '[O]VRLibrarian.exe'`;
|
||||
only if it remains alive, record the safe diagnostic `OVRLibrarian still running` alongside the completion
|
||||
marker. The launcher now emits the fixed combined marker itself if Librarian persists after cleanup and
|
||||
completion is present; it never mirrors the log. The combined trace matches `fex-librarian-stuck`. Stop only that process after completion with
|
||||
`pkill -9 -f '[O]VRLibrarian.exe'`; the normal launcher already does this. For an unowned title, accept Meta's
|
||||
Missing entitlement result and stop that game test.
|
||||
|
||||
Undo: exit the game in Steam before touching files. Remove the selected profile from the device copy of
|
||||
`artifacts/fexrift_games.json` and rerun install with `--force-unverified` to regenerate the default D3D11/Revive
|
||||
launcher. For an OpenVR-only game, retain `revive: false` in an untested profile when comparing graphics routes.
|
||||
Remove the anchor's `dxvk.conf` when no longer used. To undo the IME fix, restore **only the installed binary**
|
||||
from its `.orig` copy after stopping the game; for example, on the Frame:
|
||||
|
||||
```bash
|
||||
python3 - "$anchor/deployment.json" <<'PY'
|
||||
import json, os, shutil, sys
|
||||
record = json.load(open(sys.argv[1]))
|
||||
path = os.path.join(os.path.dirname(record['exe']), record['checks']['ime']['file'])
|
||||
shutil.copy2(path + '.orig', path)
|
||||
print('Installed IME binary restored')
|
||||
PY
|
||||
```
|
||||
|
||||
Do not delete the shared prefix to undo a profile: it holds Meta's runtime and the owner's login/saves. Game dumps
|
||||
are never modified. Re-running install reapplies the anchored IME fix; to compare launches, restore after install
|
||||
and before Play.
|
||||
|
||||
## Validation in this worktree
|
||||
|
||||
- Offline survey completed for all 19 folders with no per-game errors. Every surveyed executable matched exactly
|
||||
its intended committed profile through the compatibility hook. Ten profiles are likely, nine untested; the
|
||||
separate First Contact profile stays verified. No selected or alternate candidate exe was 32-bit. No static
|
||||
anti-cheat marker was found; this does not rule out runtime-loaded middleware.
|
||||
- Focused FEX/survey/triage tests: **96 passed, 1 skipped**. Ruff passed for the new extension, survey and their tests.
|
||||
- `uv run python scripts/patch_docs.py` was run; `--check` passes and generated patch docs needed no committed change.
|
||||
- Final whole-suite run: **1,646 passed, 43 skipped, 11 failed, 16 setup errors**. All 27 unsuccessful cases failed
|
||||
at local `socket()` creation with `PermissionError: [Errno 1] Operation not permitted`. They are existing local
|
||||
HTTP/pairing/stream tests in `test_deeplink`, `test_devkit`, `test_install_links_extra`, `test_livestream`,
|
||||
`test_setup_url`, and `test_ui_state`. They need a runner allowing loopback sockets; no application or test code
|
||||
was changed to hide this restriction. Re-run the full suite there before integration.
|
||||
- The prebuilt virtual environment was used with `UV_CACHE_DIR=/tmp/frameport-rift-uv UV_NO_SYNC=1` because the default
|
||||
uv cache is read-only and offline metadata resolution cannot install hatchling. For example:
|
||||
`UV_CACHE_DIR=/tmp/frameport-rift-uv UV_NO_SYNC=1 uv run pytest -q`.
|
||||
- No device or external network access was attempted. D3D12, guest OpenGL acceleration, WoW64 and gameplay remain
|
||||
device questions; the steps above specify the evidence needed. The GE script and Unreal source-history checkout
|
||||
were unavailable locally; Robo Recall's actual Core DLL supplies direct evidence for the IME fix instead.
|
||||
@@ -0,0 +1,228 @@
|
||||
# Stream B: unchanged local Rift copies and Meta library registration
|
||||
|
||||
Implemented on `rift/install`. Device validation is pending: this workspace cannot reach the Frame or create local
|
||||
network sockets. No entitlement, Platform SDK, runtime signature check, sign-in data or repack crack is changed by
|
||||
this work. A Meta **Missing entitlement** response remains a failure to report to the owner.
|
||||
|
||||
## Implementation
|
||||
|
||||
`frameport fex install --from-library rift.<slug>` takes the Rift entry's `game_dir`, selected `exe`, title and
|
||||
`launch_args` (also supported in `analysis.extra`). `--folder DIR [--title T]` uses the existing Rift analyzer to pick
|
||||
the exe; ambiguous choices must first be resolved in the library. Optional positional `APP` overrides the generated
|
||||
canonical name. Otherwise use the analysis's `canonical_name`, then a lowercase title slug. Local unverified games
|
||||
require `--force-unverified` **before** any upload. Existing prefix-game installs still work without either option.
|
||||
|
||||
`src/frameport/fexinstall.py` stats the folder and reuses `installer.transfer_link()` and `upload_files()` unchanged:
|
||||
fast link, SFTP `.part` append, tar batches, progress and Reporter cancellation all follow the existing install path.
|
||||
Finished files are skipped by source size/mtime plus remote size; changed sources discard old completed/partial
|
||||
bytes before retry. The source is statted again after transfer. This avoids hashing all source files over NTFS;
|
||||
it cannot detect an edit that deliberately preserves both size and mtime. Do not edit the source during an install.
|
||||
The free-space check reserves 1 GiB in addition to all pending bytes (conservative when `.part` files already exist).
|
||||
|
||||
Skipped, logged and recorded: case-insensitive `*.7z`, `*.7z.0*` split volumes (including backup suffixes), `Data.bin`,
|
||||
`AGRepackInstaller.exe`, `VD.bat`, and `*.AG`. Nothing is extracted or executed on the PC. Every included file's bytes
|
||||
are uploaded as supplied. Existing install-time generic fixes, including the Unreal IME fix, still run on the Frame
|
||||
after registration; they may change the **Frame** executable. The source folder stays unchanged. Symlinks, path
|
||||
escapes, duplicate Windows filenames and collisions with Meta-managed copies are refused.
|
||||
|
||||
App ID precedence:
|
||||
|
||||
1. `[OnlineSubsystemOculus]` in unencrypted pak `Config/DefaultEngine.ini`, then loose `DefaultEngine.ini`,
|
||||
`Oculus.ini` or `OculusConfig.ini`; keys `RiftAppId`, `OculusAppId`, `AppId` in that order. Also checks only the
|
||||
known JSON names `OculusConfig.json` / `oculus-platform-config.json` for `RiftAppId`, `OculusAppId`, `AppId`,
|
||||
`app_id`. IDs must be positive numbers. No binary-string guessing or archive decryption.
|
||||
2. Already cached OculusDB data: `details.store_url` (`fetch_details()` retains the ID in this URL, not an explicit
|
||||
Rift-ID field). No lookup/network request is introduced. A Quest-linked details record can name its Quest store
|
||||
product: check this fallback manually before testing a game with no config ID.
|
||||
3. `--app-id ID`, used only if no earlier source supplies one.
|
||||
|
||||
`app_id_source` records the selected origin. `store_app_id` independently retains the cached store ID when present,
|
||||
so a game ID such as First Contact's `1184903171584429` is not lost to store product `1217155751659625`. Manifest
|
||||
`appId` uses the selected ID; device tests must establish whether Meta instead requires the store product ID for
|
||||
particular builds. The version comes from a version-like folder name, then the exe's PE `.rsrc` FileVersion, else
|
||||
`0.0.0`; versionCode is 1. The shared reader moved to `analysis/unreal_pak.py`; the survey re-exports it via imports
|
||||
so its existing callers and tests keep working. Stream C should keep these imports when merging its survey edits.
|
||||
|
||||
`agent/fexrift_install.py` registers `prepare_local`, `register_local`, `local_info`, `unregister_local` and
|
||||
`runtime_state`. Reservations/metadata live in `fexrift/local-games/<app>.json`, with no login information. They also
|
||||
allow interrupted pre-registration uploads to resume. Registration hashes each file **on the Frame**, writes
|
||||
`<library>/Manifests/<app>.json` and `.json.mini`, and imports the library registry entry through Wine. The extension
|
||||
supplies the library manifest to `manifest_launch` / `game_info`; the existing static checks, IME patch and launcher
|
||||
then run normally. The install check hook marks `local_copy: true`.
|
||||
|
||||
Manifest JSON matches the supplied real First Contact app fixtures: property order (derived properties first),
|
||||
types, CRLF, two-space indentation, backslash `files` keys, forward-slash `launchFile`, and mini containing exe
|
||||
records only. App packageType `APP`, isCore false, launchFile2D null, launchParameters2D empty string,
|
||||
redistributables empty; other defaults come from `MANIFEST_DEFAULTS`. Launch parameters use Windows argv quoting;
|
||||
the launcher preserves the original argv list (a small shared launcher hook avoids splitting local-copy arguments
|
||||
while retaining the existing profiles' shell-fragment parsing).
|
||||
|
||||
Each file has its actual size and whole-file SHA-256, `segmentSize: 10000000`, and one segment per **decimal 10 MB**
|
||||
chunk. Each triple is `[0, chunk_sha256, len(zlib.compress(chunk))]`. The fixture's third number is compressed length,
|
||||
not uncompressed size. We do not know Meta's opaque first value and use 0 as an explicit placeholder; no fabricated
|
||||
CDN identifiers or download URLs are written. Empty files have zero segments. **These manifests describe local
|
||||
bytes, not a working Meta CDN repair recipe. Whether Meta accepts the placeholder without scheduling repair is an
|
||||
open device question.** Do not initiate an update/repair from Meta's app to validate this.
|
||||
|
||||
Internal games: `<PFX>/drive_c/Program Files/Meta Horizon/Software/Software/<app>`, using Meta's existing default
|
||||
library registration. `--dest <mount>` must exactly match an available external mount returned by the existing
|
||||
`drives` command; existing `resolve_dest()` rejects unusable filesystems. No fallback on a missing card.
|
||||
|
||||
External games: `<mount>/FramePort/fexrift-games/<app>`. Their Meta library root is the sibling
|
||||
`<mount>/FramePort/fexrift-library`, containing `Manifests/` and a `Software` symlink to `fexrift-games/`. A stable
|
||||
UUID5 from the mount path identifies the registry library. A prefix `dosdevices/volume{GUID}` links to the mount,
|
||||
and the first unused D:–Y: letter links there too (an existing letter for the same mount is reused). Registry
|
||||
`Path` uses `\\?\Volume{GUID}\FramePort\fexrift-library`, `OriginalPath` uses the letter. The default library stays
|
||||
default. Mount-path changes require uninstall/reinstall; moving these games through the general Move action has
|
||||
not been implemented by this stream. Wine volume discovery and external-library scans need device validation.
|
||||
|
||||
Deployment kind stays `frame_fex`, with `source: local_copy`, `app_id`, `app_id_source`, `store_app_id`, `skipped`,
|
||||
`dest`, and the existing launcher/check fields. Local-copy uninstall removes its files and both manifests. The last
|
||||
game on an external library also removes that library's registry key; the shared default key, shared prefix, saves,
|
||||
login and reusable drive/volume links remain. Imported Meta-managed games still keep their files on uninstall.
|
||||
There is no agent-version bump; integration must bump it so updated agent commands reach existing devices.
|
||||
|
||||
The only other shared-agent helper change sends helper JSON on stdin, avoiding Linux's single-argument size limit
|
||||
for large inventories. The extension files are already included by `artifact_files()` and need no packaging change.
|
||||
|
||||
## Runtime probe
|
||||
|
||||
`--runtime-check` calls the separate bounded `fex_runtime_state` command after install (helper `runtime_state`). It
|
||||
reuses an existing OVRServer or starts it with the shared prefix's Wine environment. It examines only newly appended
|
||||
Service/OVRServer log text, returning `runtime_state: ready`, `install_available` or `timeout` plus `reused_server`.
|
||||
It never returns raw runtime log lines. If it started the server, it kills OVRLibrarian after a fresh invocation
|
||||
completion and stops its Wine server at exit; a reused runtime is left running. The polling budget is 95 seconds,
|
||||
with bounded process start/probe/cleanup, and the agent imposes a 120-second limit. A reused idle runtime may emit no
|
||||
new state line and return `timeout`; that result is inconclusive. The probe does not start the game or prove account
|
||||
ownership. The actual log naming/state-line format remains a device question.
|
||||
|
||||
## Offline validation
|
||||
|
||||
Use a writable UV cache in this sandbox (the default cache is read-only):
|
||||
|
||||
```sh
|
||||
UV_CACHE_DIR=/tmp/frameport-uv-cache uv run --offline --no-sync pytest -q
|
||||
UV_CACHE_DIR=/tmp/frameport-uv-cache uv run --offline --no-sync pytest -q \
|
||||
tests/test_fex_install.py tests/test_fexrift.py tests/test_fexrift_ext.py tests/test_rift_fex_survey.py
|
||||
```
|
||||
|
||||
The full run reached 1,654 passed, 43 skipped; 11 failures + 16 setup errors all tried to create network sockets
|
||||
for existing localhost HTTP/streaming/pairing tests, which this sandbox forbids (`PermissionError: Operation not
|
||||
permitted`). Rerun the whole suite without that sandbox restriction at integration. No socket tests were changed
|
||||
to conceal this limitation. Triage/patch catalog files were not touched, so `patch_docs.py --check` is not required.
|
||||
|
||||
Final follow-up: 1,658 passed, 43 skipped, 27 deselected when excluding precisely those socket-dependent tests.
|
||||
The final install/FEX/survey run (including an additional split-volume suffix case) passed 68 tests, 1 skipped.
|
||||
Ruff passed for the new modules/tests and the survey; `git diff --check` passed.
|
||||
|
||||
## Exact maintainer device test
|
||||
|
||||
Run these only on the maintainer's connected PC. Keep `FRAMEPORT_HOME` pointing to this worktree's `.home`, with
|
||||
its paired Frame target and Rift library entries. Do not point at the Windows FramePort data directory. Ensure the
|
||||
maintainer's integration agent version includes this branch. Wear the headset once after boot; stop any running
|
||||
FEX game before the runtime probe (all use one prefix).
|
||||
|
||||
```sh
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
export FRAMEPORT_EXPERIMENTAL_FEX=1
|
||||
uv run frameport fex setup
|
||||
uv run frameport fex status
|
||||
uv run frameport list
|
||||
```
|
||||
|
||||
Setup uploads this extension and binaries; it may fetch the signed Meta runtime on the device. Use the login stream
|
||||
to sign in with the owner's account first. `status` should show prefix/meta installed and `login: true` without
|
||||
printing any session content. Do not copy session files, CoreData or sign-in URL queries into a report.
|
||||
|
||||
Pick a library ID the account owns; example (substitute the exact ID from `frameport list`):
|
||||
|
||||
```sh
|
||||
uv run frameport fex install --from-library rift.robo_recall --force-unverified --runtime-check
|
||||
```
|
||||
|
||||
Expect `Skipped repack leftover: ...` entries, `Meta app ID ... (config:...)` or the cached/CLI fallback, upload
|
||||
progress, `Hash and register the game on the Frame`, then generic checks/Steam registration. Final JSON should
|
||||
have `checks.local_copy: true`, with `checks.runtime_state: ready` if Meta emitted the state line. Check
|
||||
`app_id_source` and both IDs in deployment with the filtered command below. `install_available` means registration
|
||||
did not take; stop and report the IDs, canonical name and manifest shape. `timeout` requires inspecting the log
|
||||
format **locally on the device**, keeping any account data private.
|
||||
|
||||
A folder install uses the same flow:
|
||||
|
||||
```sh
|
||||
uv run frameport fex install --folder '/path/to/a/game copy' --title 'My Rift Game' \
|
||||
--app-id 1234567890123456 --force-unverified --runtime-check
|
||||
```
|
||||
|
||||
Replace the example ID with the game's actual ID; the option is a fallback and does not override a config ID.
|
||||
Do not try registering over First Contact already imported by the login tar: the collision refusal is deliberate.
|
||||
|
||||
Inspect deployment fields and launch using the paired connection (edit the package below once):
|
||||
|
||||
```sh
|
||||
uv run python - <<'PY'
|
||||
import json
|
||||
from frameport.cli import _fex_target
|
||||
frame = _fex_target(None).frame
|
||||
pkg = 'rift.robo_recall'
|
||||
try:
|
||||
games = frame.agent('list_installed', package=pkg, details=True)['games']
|
||||
for game in games:
|
||||
print(json.dumps({k: game.get(k) for k in
|
||||
('package', 'kind', 'source', 'app_id', 'app_id_source', 'store_app_id', 'skipped', 'dest')}, indent=1))
|
||||
print(frame.agent('launch', package=pkg))
|
||||
finally:
|
||||
frame.close()
|
||||
PY
|
||||
```
|
||||
|
||||
Expect `kind: frame_fex`, `source: local_copy`, IDs/provenance, and a Steam launch. In the headset check both eyes,
|
||||
controllers, audio and reaching a scene. Look locally for runtime `state: ready`, launcher `FramePort: starting
|
||||
Meta's runtime` / `FramePort: starting <title>`, and the game log. **Missing entitlement** is a valid ownership
|
||||
failure to report, never a reason to modify the copy or checks. Robo Recall may lack the generic IME patch site;
|
||||
report a hang without assuming the registration failed.
|
||||
|
||||
Resume: interrupt the upload with Ctrl-C, then repeat the identical install command. Expect completed files to be
|
||||
skipped and big `.part` files to continue. Compare a few **non-patched** files' SHA-256 locally on source/Frame;
|
||||
manifests should list actual sizes/hashes. No archive, installer, `.AG` file or `VD.bat` should have been uploaded.
|
||||
|
||||
External drive: first uninstall the test copy using the rollback below, then:
|
||||
|
||||
```sh
|
||||
uv run frameport frame drives
|
||||
uv run frameport fex install --from-library rift.robo_recall --force-unverified \
|
||||
--dest /run/media/steamos/REPLACE_WITH_MOUNT --runtime-check
|
||||
```
|
||||
|
||||
Use an actual `usable: true` mount. Expect physical files in `FramePort/fexrift-games/<canonical>`, sibling
|
||||
`fexrift-library/Manifests`, a `Software` symlink, a separate registry library, and `runtime_state: ready`. With the
|
||||
card removed, reinstall/uninstall must fail explicitly rather than writing/deleting internal storage. Reinsert at
|
||||
the same mount path before rollback. Repeat with two games to confirm uninstalling one leaves the second library
|
||||
registered; removing the last should remove its registry key.
|
||||
|
||||
## Undo
|
||||
|
||||
For an installed local copy, end the game in Steam first, then run (substitute its package):
|
||||
|
||||
```sh
|
||||
uv run python - <<'PY'
|
||||
from frameport.cli import _fex_target
|
||||
frame = _fex_target(None).frame
|
||||
try:
|
||||
print(frame.agent('uninstall', package='rift.robo_recall', remove_shortcut=True, keep_data=True))
|
||||
finally:
|
||||
frame.close()
|
||||
PY
|
||||
```
|
||||
|
||||
Expect `removed: true`; game folder, both manifests, deployment and shortcut disappear. Shared prefix, login, saves
|
||||
and unrelated games remain. For a cancelled upload that never produced a deployment, remove only its reservation
|
||||
and uploaded copy through the helper on the Frame (replace canonical/package; stop shared-prefix games first):
|
||||
|
||||
```sh
|
||||
python3 -I ~/.local/share/frameport/fexrift/artifacts/fexrift.py unregister_local \
|
||||
'{"app":"robo-recall","package":"rift.robo_recall"}'
|
||||
```
|
||||
|
||||
Reusable drive links and the empty external library scaffolding can remain for later installs; they contain no
|
||||
game/login data. Do not delete the shared prefix to undo one local installation.
|
||||
@@ -0,0 +1,178 @@
|
||||
# Stream A: Meta sign-in on the Frame
|
||||
|
||||
Implemented on `rift/login`. No Frame or network access was used. The PE helper has **not** been compiled here
|
||||
(llvm-mingw is unavailable); the browser portal and pipe framing remain device acceptance checks.
|
||||
|
||||
## What was built
|
||||
|
||||
`frameport fex login [--start-now] [--timeout 900] [--via-pc]` signs in through Meta's own Client.exe, running in the
|
||||
existing shared FEX prefix. The default creates a temporary, flat Steam entry named **Meta sign-in (FramePort)**
|
||||
using the existing shortcut installer. `--start-now` instead starts a detached user unit without restarting Steam.
|
||||
`fex login --stop` repairs interrupted cleanup. All commands require `FRAMEPORT_EXPERIMENTAL_FEX=1`.
|
||||
|
||||
`agent/fexrift_login.py` registers `login_start`, `login_status`, `login_stop` and private broker/callback commands.
|
||||
`agent/frameport_agent.py` has one separate `fex_login*` block; it passes login messages to the extension over stdin,
|
||||
never JSON argv. No agent version bump or changes to other streams' hook points were needed.
|
||||
|
||||
The launch script imports Steam's display variables, disables gamescope WSI and preloads, starts the existing Meta
|
||||
runtime, waits for OVRLibrarian's completion and kills its stuck process, then starts Client.exe with Proton. Its
|
||||
stdout/stderr and Proton logging are disabled because Electron can print authentication URLs. A lock prevents a
|
||||
second Client.exe launcher. A private broker must be alive before the launcher can run. Cancellation signals the
|
||||
launcher (PID plus kernel start tick, to reject a reused PID), stops Wine, then restores the handler settings. This
|
||||
also prevents an interrupted runtime warm-up from starting Client.exe after cleanup.
|
||||
|
||||
The prefix's WineBrowser `Browsers` setting temporarily points to `login/fp-open-url`. That helper sends the URL to
|
||||
an owner-only Unix socket broker. The broker records only `page_opened_at`, `callback_received_at` and the fixed
|
||||
query-free page URL in `login/state.json`. It opens the real URL with host xdg-open, with output discarded. The
|
||||
temporary host `frameport-oculus.desktop` invokes `fp-oculus-callback`, which sends the callback to the same broker.
|
||||
The broker passes it to `proton run fp_pipesend.exe` **through stdin**. Neither agent forwarding nor Wine invocation
|
||||
puts the callback into argv. The desktop protocol invocation itself necessarily supplies `%u` to its short-lived
|
||||
handler; do not collect process-command-line diagnostics during sign-in.
|
||||
|
||||
`native/fexwine/pipesend.c` sends a NUL-terminated UTF-8 callback to `\\.\pipe\oculus`. It retries a missing or busy pipe
|
||||
for about 20 seconds and prints nothing. Exit codes: 0 written, 2 invalid input, 3 pipe unavailable, 4 write failed.
|
||||
Only Meta's running client receives the link; a second Client.exe is never started to deliver it.
|
||||
|
||||
The extension snapshots the previous WineBrowser value/type, previous host scheme owner, desktop file and mimeapps
|
||||
files before registration. `login/restore.json` contains **registration settings only**. A failed restoration leaves
|
||||
this record for `--stop` to retry. Normal success, timeout, errors and Ctrl-C all clean up; a broker deadline also
|
||||
restores the Frame settings if the PC disconnects (requested timeout plus 180 seconds for Steam shortcut setup).
|
||||
An OS kill/reboot can interrupt this cleanup; rerun `--stop` after reconnecting. The temporary shortcut is removed
|
||||
by the PC's cancel request, asynchronously through the existing shortcut worker. A disconnected PC cannot remove
|
||||
that shortcut until it reconnects, but the launcher refuses to run without its broker.
|
||||
|
||||
## PC browser fallback
|
||||
|
||||
`--via-pc` keeps the full auth URL **only in broker memory**, draining it once through the agent's SSH status reply
|
||||
when the PC explicitly requests `take_url`. Ordinary `fex_login_status` replies never contain the URL. The PC opens
|
||||
the page in its default browser and temporarily borrows the `oculus` scheme. The handler POSTs directly to a loopback
|
||||
server with a random endpoint; it never uses the normal install-link `links/*.link` inbox. HTTP logging is disabled.
|
||||
WSL registers a Windows handler which invokes the WSL Python callback, and opens the sign-in page in Windows.
|
||||
|
||||
Windows/WSL restoration exports and restores the **entire** HKCU `Software\Classes\oculus` tree, including arbitrary
|
||||
subkeys and value types, or removes it if absent before login. The non-secret export is deleted after restoration;
|
||||
if importing it fails, a recovery copy is retained as `<FRAMEPORT_HOME>/oculus-handler-restore.reg` and the error
|
||||
names it. Linux restores the exact previous desktop/mimeapps bytes, including an absent default. macOS fallback
|
||||
is explicitly unsupported. These temporary registrations are separate from the permanent FramePort install-link
|
||||
settings. The PC context restores registration even when sign-in, SSH forwarding or Frame cleanup raises.
|
||||
|
||||
Saved login state in `sessions/` and CoreData stays in the prefix. Status only checks whether a non-empty session
|
||||
file exists; it does not read its contents, prove the session is unexpired, or imply entitlement to any game. This
|
||||
change does not replace Platform SDK DLLs, alter entitlement checks, or access repack archives. A game reporting
|
||||
**Missing entitlement** must remain an entitlement failure.
|
||||
|
||||
## Offline verification
|
||||
|
||||
Run from this worktree, keeping app data isolated in its existing `FRAMEPORT_HOME`:
|
||||
|
||||
```sh
|
||||
UV_CACHE_DIR=/tmp/frameport-uv-cache uv run --offline pytest -q tests/test_fex_login.py tests/test_fexrift.py tests/test_fexrift_ext.py
|
||||
UV_CACHE_DIR=/tmp/frameport-uv-cache uv run --offline ruff check agent/fexrift_login.py src/frameport/fexlogin.py src/frameport/urlhandler.py src/frameport/cli.py tests/test_fex_login.py
|
||||
UV_CACHE_DIR=/tmp/frameport-uv-cache uv run --offline pytest -q
|
||||
```
|
||||
|
||||
The focused suite passed (56 passed, 1 skipped); lint passed. The full suite rerun completed: **1,651 passed, 43 skipped,
|
||||
11 failed, 16 errors**. Every failure/error was `PermissionError: [Errno 1] Operation not permitted` from existing
|
||||
local HTTP/pairing/live-stream socket tests: this sandbox prohibits socket creation/binding. No other stream's files
|
||||
or tests were changed to hide those failures. Run the whole suite on a host permitting loopback sockets before
|
||||
integration. No triage/patch definitions were changed, so patch-doc regeneration is not required.
|
||||
|
||||
## Exact maintainer device acceptance steps
|
||||
|
||||
Use the maintainer's paired Frame configuration; do not copy credentials or configuration into this branch.
|
||||
Close any Rift game first: sign-in uses the same Wine prefix and stopping it ends that prefix's processes.
|
||||
|
||||
1. Build the PE with llvm-mingw on an x86_64 host (or run the existing full `build_pe_dlls.sh` build):
|
||||
|
||||
```sh
|
||||
x86_64-w64-mingw32-clang -O2 -s -Wl,--no-insert-timestamp -o artifacts/fexrift/fp_pipesend.exe native/fexwine/pipesend.c
|
||||
uv run python - <<'PY'
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
root = Path("artifacts/fexrift")
|
||||
sums = root / "SHA256SUMS"
|
||||
lines = [s for s in sums.read_text().splitlines() if s.strip() and s.split()[-1].lstrip("*") != "fp_pipesend.exe"]
|
||||
lines.append(hashlib.sha256((root / "fp_pipesend.exe").read_bytes()).hexdigest() + " fp_pipesend.exe")
|
||||
sums.write_text("\n".join(lines) + "\n")
|
||||
PY
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex setup
|
||||
```
|
||||
|
||||
Setup uploads the extension automatically alongside the existing FEX artifacts. Without the exe, login must fail
|
||||
with `fp_pipesend.exe missing: build native/fexwine/pipesend.c and upload it with fex setup`.
|
||||
|
||||
2. Record the Frame's current scheme owner (substitute its paired SSH destination for `FRAME`):
|
||||
|
||||
```sh
|
||||
ssh steamos@FRAME 'xdg-mime query default x-scheme-handler/oculus'
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex login --timeout 900
|
||||
```
|
||||
|
||||
Expect `Put on the headset: open 'Meta sign-in (FramePort)' in your Steam library.` Open that entry. Allow about
|
||||
two minutes for the runtime warm-up. Confirm Meta's app is a flat window, not another VR game.
|
||||
|
||||
3. Choose Meta's sign-in action. Confirm Chromium opens **on the Frame**. The PC should print `Sign-in page opened
|
||||
on the Frame.` Complete the real Meta sign-in. When Chromium asks to open an external app, accept it. This is the
|
||||
unproven Flatpak boundary: OpenURI through xdg-desktop-portal must resolve the host's temporary `oculus` handler.
|
||||
Expect `Sign-in response reached Meta's app on the Frame.` followed by `Signed in.` The CLI closes the app/runtime,
|
||||
restores handlers and requests removal of the Steam entry. Check the Steam entry disappears after the shortcut
|
||||
worker finishes; Steam may restart. The saved login must remain visible as `"login": true` in:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex status
|
||||
ssh steamos@FRAME 'xdg-mime query default x-scheme-handler/oculus'
|
||||
```
|
||||
|
||||
The second command must return the original owner (including no owner, if originally unset). Do not dump Meta
|
||||
logs, sessions, CoreData, process argv or the full browser URL for debugging.
|
||||
|
||||
4. On an unsigned-in test prefix, or before completing the first sign-in, test the alternate launch route:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex login --start-now --timeout 900
|
||||
```
|
||||
|
||||
Expect `Meta's app is starting on the Frame.` There should be no Steam restart and no new shortcut. After a
|
||||
successful login already exists, this command can immediately report `Signed in.` and clean up; that does not
|
||||
exercise a fresh browser exchange. Do not delete the owner's session files just to force this test.
|
||||
|
||||
5. If the portal never delivers the callback, cancel with Ctrl-C and use:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex login --start-now --via-pc --timeout 900
|
||||
```
|
||||
|
||||
Click sign-in in the Frame's Meta window; the page must open in the PC browser. Expect `Sign-in page opened in
|
||||
the PC browser.`, then the response/success messages above. On Windows/WSL, verify Meta's previous
|
||||
`HKCU\Software\Classes\oculus` registration is restored; on Linux verify the original xdg default. This also
|
||||
needs a real Windows default-protocol check: OS UserChoice policy can override a Classes registration.
|
||||
|
||||
6. To inspect progress without exposing URLs, use the ordinary agent status command from a second terminal:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run python - <<'PY'
|
||||
import json
|
||||
from frameport.cli import _fex_target
|
||||
frame = _fex_target(None).frame
|
||||
print(json.dumps(frame.agent("fex_login_status"), indent=2))
|
||||
PY
|
||||
```
|
||||
|
||||
Expected keys: `runtime_running`, `client_running`, `page_opened_at`, `callback_received_at`, `signed_in`. Runtime
|
||||
should become true before client; timestamps should appear in that order; signed_in should become true only
|
||||
after a non-empty session file exists. If callback_received_at appears but signed_in never becomes true on a
|
||||
fresh sign-in, the **pipe framing remains unverified**: this implementation uses UTF-8 plus a terminating NUL.
|
||||
Compare against the maintainer's successful spike sender's bytes without printing the URL. Do not retry by
|
||||
starting a second Client.exe, altering entitlement checks, or exposing tokens in diagnostics.
|
||||
|
||||
7. Test cancellation during runtime warm-up, timeout with `--timeout 10`, and repeatable repair/undo:
|
||||
|
||||
```sh
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex login --stop
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run frameport fex login --stop
|
||||
```
|
||||
|
||||
Expect `Meta sign-in stopped. Your saved login stays on the Frame.` both times. No delayed Client.exe should
|
||||
appear after waiting two minutes. Status should show client/runtime false, the previous protocol owner should
|
||||
be restored, and the session files should remain. If SSH was lost, the broker cleans up the Frame by its deadline;
|
||||
reconnect and run `--stop` to remove a leftover shortcut or retry an interrupted restoration.
|
||||
@@ -0,0 +1,276 @@
|
||||
# Rift on Frame: performance stream
|
||||
|
||||
Implemented offline on `rift/perf`, from `98786ab`. No Frame measurements were made. Existing research measurements
|
||||
are in `native/fexwine/README.md`; the commands below are for the maintainer to run on the paired Frame.
|
||||
|
||||
## What changed
|
||||
|
||||
`agent/fexrift_perf.py` registers `settings_get`, `settings_set`, `perf_prepare` and `bench`. Setup's existing `revive`
|
||||
step also builds three Revive folders. The PC's existing artifact discovery uploads the extension automatically.
|
||||
The only launcher template changes select the core list, Revive folder and motion-smoothing JSON from variables.
|
||||
The agent and CLI have separate settings/bench blocks; the agent version is unchanged.
|
||||
|
||||
Settings live in `<ROOT>/settings/<Meta-app>.json`, separately from the game catalog. Defaults come from the game's
|
||||
`perf` object, with the existing `env.FP_MEM` / `env.FP_BIGCACHE_MB` as backward-compatible defaults. Overrides take
|
||||
precedence. `null` removes an override. Preparation runs on every launch, writes `<app>.dxvk.conf`, and exports
|
||||
validated values before Wine starts. Updating a setting requires no reinstall after the new launcher is installed.
|
||||
|
||||
| Setting | Default | Accepted values / effect |
|
||||
| --- | --- | --- |
|
||||
| `dxvk_disable_msaa` | `false` | Boolean; `d3d11.disableMsaa` |
|
||||
| `dxgi_sync_interval` | `-1` | Integer -1..4; `dxgi.syncInterval` |
|
||||
| `dxgi_max_frame_latency` | `0` | Integer 0..16; `dxgi.maxFrameLatency` |
|
||||
| `eye_scale` | `1.5` | Divisor 1.0 / 1.25 / 1.5 (100 / 80 / 67 percent nominal eye size) |
|
||||
| `motion_smoothing` | `2` | 0 = default, 1 = force on, 2 = force off; SteamVR `motionSmoothingOverride` |
|
||||
| `cores` | `4-7` | taskset CPU list within 0..7; e.g. `4-7` or `0-3,7` |
|
||||
| `ntsync` | `true` | Off sets `PROTON_NO_NTSYNC=1`; on unsets it |
|
||||
| `fp_mem` | per-game opt-in | Boolean; guest preload enabled only when true |
|
||||
| `bigcache_mb` | `256` | Cache minimum block size, 1..6144 MiB (not the total cache budget) |
|
||||
| `fp_mem_zero` | `false` | Boolean; `FP_MEM_ZERO=1` opts into lazy zero pages |
|
||||
| `wine_large_address_aware` | `true` | Boolean; `WINE_LARGE_ADDRESS_AWARE=1/0` |
|
||||
| `fex` | `{}` | JSON map of whitelisted keys with 0/1 values |
|
||||
|
||||
FEX keys: `FEX_TSOENABLED`, `FEX_VECTORTSOENABLED`, `FEX_MEMCPYSETTSOENABLED`, `FEX_MULTIBLOCK`,
|
||||
`FEX_X87REDUCEDPRECISION`. The first three retain the launcher's default 0, MULTIBLOCK defaults to 1, X87 to 0.
|
||||
Unknown keys, arbitrary environment variables, shell fragments and invalid paths are refused. The commands do not
|
||||
inspect login files, print process command lines, or change Platform SDK/entitlement checks.
|
||||
|
||||
### Revive variants
|
||||
|
||||
The checked-in PC `REVIVE_PATCHES` contains the six visibility patches but **does not contain the research eye-size
|
||||
redirect described in the brief**. Verified locally against the installed DLL's pinned upstream hash:
|
||||
|
||||
- Stock `LibRevive64.dll`: SHA-256 `ef9ffd5342bb4afad9b1c0f4fee1ca3bf32f9d3159d49dd841cc9e22f02663e4`.
|
||||
- After the six PC patches: `eda2a449d230832c95734a03e8c83eeb531668ce676693f636e19c315765af4e`.
|
||||
- RVA `0x12eab` (file `0x122ab`) is `movss xmm4,[rip+0x4bb15]`, loading a shared 1.0 constant.
|
||||
- The research redirect loads the existing 1.5 at **RVA** `0x4eaa4` (file `0x4d4a4`). It is not file offset `0x4eaa4`.
|
||||
|
||||
The extension accepts the pinned six-patch upload, optionally with exactly that research redirect. It redirects
|
||||
this one load into verified alignment padding at RVA `0x12e9c`, after the preceding function's `ret`. Each copy
|
||||
gets its own divisor there. It leaves shared constants untouched. `revive-scales/{1,1.25,1.5}/` hard-links all other
|
||||
files to `revive/`; the DLL is unlinked before writing, so variants and the base never modify one another. The
|
||||
injector is launched from the selected folder, which is where it finds `LibRevive64.dll`.
|
||||
Unexpected hashes, instructions or occupied padding fail explicitly. No Oculus binaries are patched here.
|
||||
|
||||
### fp_mem v3
|
||||
|
||||
`FP_MEM_ZERO=1` calls `madvise(MADV_DONTNEED)` on exactly the reused part before returning it: the requested prefix
|
||||
for short/equal reuse, the cached prefix for a longer allocation. The fresh tail needs no advice. Failed advice
|
||||
evicts the cache entry and falls through to real mmap, rather than returning stale contents. Without ZERO, v2's
|
||||
unzeroed behavior remains. Debug output counts advice calls, discarded bytes and failures.
|
||||
|
||||
The fake-syscall host harness in `tests/test_fex_perf.py` checks equal, short and long reuse, advice failure,
|
||||
unzeroed compatibility, and `FP_MEM_ZERO=0`. It skips only the C test when no host C compiler is installed.
|
||||
The guest artifact has **not** been rebuilt in this stream. On an x86_64 glibc 2.34-compatible build host, from the
|
||||
repository root, the maintainer must run:
|
||||
|
||||
```sh
|
||||
gcc -O2 -shared -fPIC -o artifacts/fexrift/fp_mem.so native/fexwine/fp_mem.c -ldl -lpthread
|
||||
python3 - <<'PY'
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
root = Path('artifacts/fexrift')
|
||||
manifest = root / 'SHA256SUMS'
|
||||
lines = manifest.read_text().splitlines()
|
||||
hash_ = hashlib.sha256((root / 'fp_mem.so').read_bytes()).hexdigest()
|
||||
lines = [f'{hash_} ./fp_mem.so' if line.endswith(' ./fp_mem.so') else line for line in lines]
|
||||
manifest.write_text('\n'.join(lines) + '\n')
|
||||
PY
|
||||
```
|
||||
|
||||
This is an **x86_64 guest** shared library, even though wineserver is native aarch64. Commit the rebuilt artifact
|
||||
and checksum at integration. ZERO experiments with the old artifact would silently measure v2 instead.
|
||||
|
||||
## Benchmark behavior and limits
|
||||
|
||||
`frameport fex bench APP --seconds N` and `scripts/rift_fex_bench.py` each request exactly one run. N is an integer
|
||||
1..120; it includes launcher/runtime startup, not just game sampling. The Frame enforces the deadline independently
|
||||
of the PC connection. A bench lock excludes another bench. The agent first rejects active Lepton/FramePort games;
|
||||
the helper rejects Wine/Proton/FEX processes, other Steam reapers, and Lepton container supervisors. It excludes
|
||||
SteamVR's own AppId 250820 reaper. An open Meta login session also blocks the benchmark.
|
||||
|
||||
The helper requires `IVRSystem_022::GetTrackedDeviceActivityLevel(HMD)` to report **Standby (3)**. All other values,
|
||||
including unknown, idle, recently interacting and interacting, are refused. A write to Steam's `logs/controller.txt`
|
||||
within the last 60 seconds also blocks it (conservative: reconnection is enough). The same owner-activity checks run
|
||||
while sampling; interaction stops the run. This does not claim to detect every desktop keyboard event; do not use
|
||||
this headless tool while someone is using the Frame. A native non-Steam app outside Steam's reaper may also evade
|
||||
the process guard. Do not run simultaneous Play/login/install operations against the shared prefix.
|
||||
|
||||
Before launch, read a positive finite `power/turnOffScreensTimeout`, set `power/pauseCompositorOnStandby=false`
|
||||
through the existing `vrsetting` helper, and temporarily set the screen timeout to N+10. Start `launch.sh` in its
|
||||
own process group. Finally, stop the shared-prefix wineserver (5-second limit), terminate/kill the launcher group,
|
||||
set `pauseCompositorOnStandby=true`, and restore the original screen timeout. Each restoration is attempted even
|
||||
when a preceding cleanup fails. SIGTERM and ordinary errors run this cleanup; SIGKILL, power loss or a dead SteamVR
|
||||
service cannot execute Python's finally. The temporary screen timeout is finite even in those cases.
|
||||
|
||||
Every 0.5 seconds, read up to 128 compositor records via pinned `IVRCompositor_022::GetFrameTimings`. Deduplicate by
|
||||
frame index, retain updated present counts, exclude the initial ring, and count only while scene focus belongs to
|
||||
the game's PID. A hitch is one unique frame presented at least three times. GPU activity uses the actual game
|
||||
process's DRM fdinfo engine-nanosecond deltas, deduplicating shared DRM client IDs; no debugfs permissions needed.
|
||||
Thread wait states are histogrammed from the game's `/proc/<pid>/task/*/wchan` files. No shell commands or logs
|
||||
containing account data enter the summary.
|
||||
|
||||
Successful stdout is one JSON line: effective settings, elapsed time, unique frames, `valid`, frame span, fps,
|
||||
hitches (also per 20 seconds), average render GPU time and client frame interval, game GPU busy percent, and sampled
|
||||
thread wait states. GPU busy is the sum of available engines and may exceed 100 percent on devices with independent
|
||||
engines. `null` means unavailable, not zero. Client frame interval is **not CPU ApplicationTime**. `valid=false`,
|
||||
zero frames or a short sample span means the launch produced insufficient data; do not treat it as a fast run.
|
||||
Use equivalent spans and a stable scene when comparing results. A launcher exiting early is an error.
|
||||
|
||||
## Exact device checks
|
||||
|
||||
The maintainer runs these commands; this stream did not access the network or the Frame. Use the account owner's
|
||||
normal sign-in workflow from the login stream. A missing entitlement is a failed run to report, never bypass.
|
||||
|
||||
1. Build/checksum the guest artifact above. From this worktree on the PC:
|
||||
|
||||
```sh
|
||||
export FRAMEPORT_HOME="$PWD/.home"
|
||||
export FRAMEPORT_EXPERIMENTAL_FEX=1
|
||||
uv run frameport fex setup
|
||||
uv run frameport fex install oculus-first-contact
|
||||
uv run frameport fex settings oculus-first-contact
|
||||
```
|
||||
|
||||
Setup should complete the `revive` step and create all three scale folders. Install once to get the new launcher.
|
||||
Settings should show fp_mem on, threshold 256, eye divisor 1.5, motion smoothing 2, cores 4-7 and ntsync on.
|
||||
Run the Frame helper with `python3 -I` as usual; no additional pip packages are needed there.
|
||||
|
||||
2. Wear the headset once after boot, then remove it and let it reach standby. End every game and the Meta client;
|
||||
wait at least 60 seconds after controller input. A worn headset or a running game must make bench refuse **before
|
||||
changing display settings**. Do not work around a refusal by disabling the checks.
|
||||
|
||||
3. Establish a baseline, then run exactly one bounded experiment per command pair below. Each line is independent;
|
||||
restore the previous changed key with `null` before moving to the next experiment.
|
||||
|
||||
```sh
|
||||
uv run frameport fex settings oculus-first-contact dxvk_disable_msaa=false dxgi_sync_interval=-1 dxgi_max_frame_latency=0 eye_scale=1.5 motion_smoothing=2 cores=4-7 ntsync=true fp_mem=true bigcache_mb=256 fp_mem_zero=false 'fex={}' wine_large_address_aware=true
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
```
|
||||
|
||||
Expected: one summary JSON, `valid=true`, nonzero frames, finite fps/GPU timings; DRM busy may be null if fdinfo
|
||||
is unavailable. After it returns, the game/runtime should be gone and the headset should sleep normally.
|
||||
`launch.log` in the game's anchor should contain `FramePort: starting Meta's runtime` and
|
||||
`FramePort: starting Oculus First Contact`. No rendering with `r.MobileMSAA=2` was proven; do not use that knob.
|
||||
|
||||
4. Run these candidate experiments separately, returning to the baseline between them:
|
||||
|
||||
```sh
|
||||
uv run frameport fex settings oculus-first-contact dxvk_disable_msaa=true
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact dxvk_disable_msaa=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact dxgi_max_frame_latency=1
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact dxgi_max_frame_latency=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact eye_scale=1.25
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact eye_scale=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact 'fex={"FEX_X87REDUCEDPRECISION":1}'
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact fex=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact wine_large_address_aware=false
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact wine_large_address_aware=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact ntsync=false
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact ntsync=null
|
||||
|
||||
uv run frameport fex settings oculus-first-contact fp_mem_zero=true
|
||||
uv run frameport fex bench oculus-first-contact --seconds 120
|
||||
uv run frameport fex settings oculus-first-contact fp_mem_zero=null
|
||||
```
|
||||
|
||||
The baseline covers eye divisor 1.5, ntsync on, and ZERO off. MSAA off previously improved GPU load but reduced
|
||||
edge quality. Compare hitches, frame span, GPU time and thread wait distributions; this stream makes no claim
|
||||
that X87 or large-address-awareness helps. GE may ignore the latter for a 64-bit game. Test image quality later
|
||||
in an ordinary worn-headset Play session; the headless bench intentionally refuses that session.
|
||||
|
||||
Script alternative for a single run (do not run this in addition unless another run is intended):
|
||||
|
||||
```sh
|
||||
uv run python scripts/rift_fex_bench.py oculus-first-contact --seconds 120
|
||||
```
|
||||
|
||||
5. Interrupt a run by sending SIGTERM to the `python3 -I .../fexrift.py bench` helper **on the Frame**, after checking
|
||||
its PID with `pgrep -af '[f]exrift.py bench'`. Use `kill -TERM <that-pid>` (never a broad pkill). Expected: interrupted
|
||||
result, game stopped, compositor standby enabled and original screen timeout restored. Also check refusal while
|
||||
the headset is worn and a deliberately invalid setting, e.g. `eye_scale=1.4`, without launching anything.
|
||||
On the Frame, this reads the two settings without printing any account data:
|
||||
|
||||
```sh
|
||||
python3 -I - <<'PY'
|
||||
import ctypes as C, importlib.util, os
|
||||
path = os.path.expanduser('~/.local/share/frameport/fexrift/artifacts/fexrift.py')
|
||||
spec = importlib.util.spec_from_file_location('fx', path)
|
||||
fx = importlib.util.module_from_spec(spec); spec.loader.exec_module(fx)
|
||||
core, method, settings = fx.vr_settings()
|
||||
try:
|
||||
for key, idx, typ in [('pauseCompositorOnStandby', 5, C.c_bool), ('turnOffScreensTimeout', 7, C.c_float)]:
|
||||
err = C.c_int()
|
||||
value = method(settings, idx, typ, C.c_char_p, C.c_char_p, C.POINTER(C.c_int))(
|
||||
b'power', key.encode(), C.byref(err))
|
||||
print(key, value, 'error', err.value)
|
||||
finally:
|
||||
method(core, 1, None)()
|
||||
PY
|
||||
```
|
||||
|
||||
Record these values before the test. Afterwards expect `pauseCompositorOnStandby True error 0` and the original
|
||||
positive timeout with error 0. Confirm the physical display sleeps. With guest `FP_MEM_DEBUG=1`, ZERO reuse logs
|
||||
should show `fp_mem: zero calls ..., bytes ..., failures 0`. This debug variable can be set manually in a test
|
||||
launcher for one run; remove that line afterwards (it is deliberately outside the user environment whitelist).
|
||||
|
||||
## Undo and remaining device questions
|
||||
|
||||
Reset every override (no login or game files removed):
|
||||
|
||||
```sh
|
||||
uv run frameport fex settings oculus-first-contact dxvk_disable_msaa=null dxgi_sync_interval=null dxgi_max_frame_latency=null eye_scale=null motion_smoothing=null cores=null ntsync=null fp_mem=null bigcache_mb=null fp_mem_zero=null fex=null wine_large_address_aware=null
|
||||
```
|
||||
|
||||
To remove this experiment entirely, restore the integration's previous launcher/module and guest artifact, then
|
||||
remove only `settings/<app>.json`, `settings/<app>.dxvk.conf` and `revive-scales/` after stopping all FEX games.
|
||||
Do not remove `compat/pfx`; it holds the owner's login and installed games. For emergency display restoration on the
|
||||
Frame (replace 300 with the previously recorded timeout):
|
||||
|
||||
```sh
|
||||
R="$HOME/.local/share/frameport/fexrift"
|
||||
python3 -I "$R/artifacts/fexrift.py" vrsetting '{"section":"power","key":"pauseCompositorOnStandby","kind":"b","value":true}'
|
||||
python3 -I "$R/artifacts/fexrift.py" vrsetting '{"section":"power","key":"turnOffScreensTimeout","kind":"f","value":300}'
|
||||
```
|
||||
|
||||
Unverified: SteamVR's availability of the pinned historical compositor/system interfaces; whether scene focus PID
|
||||
matches the FEX game's host PID; whether the HMD reports Standby while compositor rendering is forced; how many
|
||||
usable game frames remain after runtime startup within 120 seconds; fdinfo visibility on this OS; the cost of ZERO
|
||||
on the device; all visual/entitlement behavior for other games. Failures must stay visible, not be replaced by fake
|
||||
performance numbers. If startup consumes the window, report `valid=false` and adjust startup in the owning stream
|
||||
before drawing conclusions; do not silently extend the bounded run.
|
||||
|
||||
## Offline validation and integration
|
||||
|
||||
Run `uv run pytest -q` in a normal environment and `uv run python scripts/patch_docs.py --check`. In this sandbox,
|
||||
use a writable UV cache and the already installed environment:
|
||||
|
||||
```sh
|
||||
UV_CACHE_DIR=/tmp/frameport-perf-uv uv run --offline --no-sync pytest -q
|
||||
UV_CACHE_DIR=/tmp/frameport-perf-uv uv run --offline --no-sync pytest -q tests/test_fex_perf.py tests/test_fexrift.py tests/test_fexrift_ext.py
|
||||
```
|
||||
|
||||
The original worktree's Git common directory is mounted read-only. Commits are therefore in the local, writable
|
||||
repository `/tmp/frameport-rift-perf.git` (same base and `rift/perf` ref) and exported as
|
||||
`/tmp/frameport-rift-perf.bundle`. The worktree contains the complete edits. A maintainer with writable original
|
||||
Git metadata can fetch that bundle and fast-forward/cherry-pick the commits; do not discard the worktree edits
|
||||
until integration is confirmed. Local-only instruction files and `.home` are excluded from every commit.
|
||||
|
||||
Validation result in this environment: 80 focused tests passed, 1 skipped; Ruff passed for the three new Python
|
||||
files; `scripts/patch_docs.py --check` passed; `git diff --check` passed. The full suite ran: **1676 passed,
|
||||
43 skipped, 11 failed, 16 errors**. Every remaining failure/error raised `PermissionError: [Errno 1] Operation not
|
||||
permitted` when existing pairing/download/live-stream tests attempted to create sockets. This sandbox forbids
|
||||
sockets, including loopback; these tests were not disabled or modified. A clean full-suite pass remains an
|
||||
integration check in a normal environment. The complete short traceback log is
|
||||
`/tmp/frameport-rift-perf-pytest.log`.
|
||||
+1
-2
@@ -13,13 +13,12 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
|
||||
|---|---|---|
|
||||
| `luajit/` | `arm64-v8a/libfp_luajit.so` | Pinned upstream LuaJIT with full-width lightuserdata (`frame.luajit_pointers`). Only the verified old Lua engine's shared public functions become imports; the original xLua API 105 bindings, LuaSocket helpers and `luaopen_perflib` remain in the game. No game library is bundled. Build: `python native/build.py --only luajit`; source revision + archive SHA-256 in `luajit/build.py`, MIT notices in `luajit/COPYRIGHT` and `artifacts/licenses/LuaJIT.txt`. `runtime.c` checks GC64 and the retained `xlua_gl` global-reference offset. `smoke.c` compares a real low-address allocation with the original library's own native static pointer; the unmodified library fails only the high-address case, the patched one round-trips both exactly. Startup verified on Frame; gameplay unverified. |
|
||||
| `adapter/` | `{arm64-v8a,armeabi-v7a}/libopenxr_loader_generic.so` | **FrameBridge**: replaces overport's generic loader (which is renamed `libopenxr_loader_original.so`) and fixes Steam Frame runtime gaps. Hooks xrCreateInstance, swapchains, xrEndFrame, spaces, xrPollEvent, … everything else is forwarded by `gen_forwarders.py`-generated tail calls. `scene_emu.c` = Meta scene/spatial-entity emulation; `flip_vk.c` = Vulkan blit for VERTICAL_FLIP quads. `session_fixes.c` = per-game session fixes, all off by default and hooked only when on (`layer_debug` diagnostics, `stable_local`, `focus_hold`, aim correction `aim_pitch/aim_yaw/aim_forward`, `refresh_rate`, `proximity_emul` = finger proximity (XR_FB_touch_controller_proximity, which the Frame lacks) from the capacitive touch inputs: OVRPlugin's `hand_thumb_proximity`/`hand_trigger_proximity` actions get extra Touch bindings); `input_diag.c` = controller-input diagnostics (setting `input_diag`, off by default and hooked only when on; logs suggested interaction profiles and the runtime's answers, each hand's current profile, functions the runtime lacks and failing input/haptics/perf calls, each line once; tested on the host by `tests/test_input_diag.py`); `layer_emul_gl.c` = `equirect_emul` (GLES sessions only): 360° equirect/equirect2 layers become one adapter projection layer, drawn by a worker thread with its own EGL context shared with the app's: each 360° image is converted to a cube map only when it changes, and the view is redrawn only after a 4° head turn (GL/EGL resolved with dlopen, so Vulkan games never load them); `layer_math.h` = their pose/face math, tested on the host by `tests/test_layer_emul.py`. `render_model.c` = XR_FB_render_model emulation (setting `controller_models`, off by default): serves `files/framebridge/controller_{left,right}.glb`, which the agent converts on the Frame from SteamVR's own Frame controller render models (never shipped by FramePort); tested on the host by `tests/test_render_model.py`. Settings: `libframe_settings.so` in the APK + `settings.conf`/`framebridge.conf` on the Frame. |
|
||||
| `vrapi-bridge/` | `arm64-v8a/libvrapi.so` | VrApi → OpenXR bridge from [Android-XR-Bridge/OVRPort](https://github.com/Android-XR-Bridge/OVRPort) `native/vrapi` at 5e7df52 (GPL-3.0, `LICENSE.upstream`; unchanged upstream as of OVRPort v1.2.5, which ships the unpatched code as its opt-in `patch_vrapi_openxr` in experimental CLI builds), with our changes in `upstream-patches/`: GLES sessions + GL texture swapchains, cylinder→quad layers, GL vertical flip, UNORM↔sRGB format twins, 30 s VR-mode deadline, VALID-only recenter, loading-icon layers skipped, runtime device extensions no Vulkan device offers left out of `vrapi_GetDeviceExtensionsVulkan` (AXRB's runtime asks for AHB also in its direct mode; not in `upstream-patches/` yet), `vrapi_PollEvent`/`RecenterPose`/`SetDisplayRefreshRate`/`GetSystemPropertyFloatArray` (BlazeRush imports them, GitHub #57), legacy compositor debug/volume frame flags accepted, full-rate Vulkan foveation images allocated as density attachments (The Climb, GitHub #238), diagnostics behind `-DOVP_GL_DIAG`. `upstream-patches/` reproduces these sources from the fork's `native/vrapi` (checked 2026-10-06 against its latest commit); upstream PRs Android-XR-Bridge/OVRPort#3-#8, tracked in GitHub #75. |
|
||||
| `vrapi-bridge/` | `arm64-v8a/libvrapi.so` | VrApi → OpenXR bridge from [Android-XR-Bridge/OVRPort](https://github.com/Android-XR-Bridge/OVRPort) `native/vrapi` at 5e7df52 (GPL-3.0, `LICENSE.upstream`; unchanged upstream as of OVRPort v1.2.5, which ships the unpatched code as its opt-in `patch_vrapi_openxr` in experimental CLI builds), with our changes in `upstream-patches/`: GLES sessions + GL texture swapchains, cylinder→quad layers, GL vertical flip, UNORM↔sRGB format twins, 30 s VR-mode deadline, VALID-only recenter, loading-icon layers skipped, `vrapi_PollEvent`/`RecenterPose`/`SetDisplayRefreshRate`/`GetSystemPropertyFloatArray` (BlazeRush imports them, GitHub #57), legacy compositor debug/volume frame flags accepted, full-rate Vulkan foveation images allocated as density attachments (The Climb, GitHub #238), diagnostics behind `-DOVP_GL_DIAG`. `upstream-patches/` reproduces these sources from the fork's `native/vrapi` (checked 2026-10-06 against its latest commit); upstream PRs Android-XR-Bridge/OVRPort#3-#8, tracked in GitHub #75. |
|
||||
| `platformcompat/` | `arm64-v8a/libovrplatformcompat.so` | Real `ovrMessageType_ToString` (same fork, `native/platform`). |
|
||||
| `langpack/` | `arm64-v8a/libfp_langpack.so` | Language packs for the Meta Platform SDK (patch `frame.langpacks`, opt-in). overport's platform loader is a dispatcher in front of Meta's own loader and answers `ovr_LanguagePack_GetCurrent/SetCurrent` with `return 0` (no reply ever). This library becomes the loader's first DT_NEEDED; the loader's own exports of the 31 functions it defines are made STB_LOCAL in `.dynsym` (`elf.hide_exports`, nothing moves), so symbol lookups reach the library first. It serves `<tag>.lang` files found in the game's OBB / app files (`$FRAMEPORT_LANGPACK_DIRS` overrides the search path) through `ovr_LanguagePack_*`, `ovr_AssetFile_GetList` (the loader's list + the packs) and `ovr_AssetFile_StatusById`; its own message queue is merged into `ovr_PopMessage`, and every handle that is not ours (an address-range check) goes to the loader's original function, whose address is read at run time from the loader's in-memory `.dynsym`. With patch `frame.asset_files` (marker byte `@FPASSETS@`, or env `FRAMEPORT_ASSET_FILES=1`) it also lists the data's `*.pak` content files as installed `default` asset files and answers `ovr_AssetFile_DownloadById` for them at once (a completed `DownloadUpdate`, then the result with the path). Host-tested against a stand-in loader (`tests/test_langpack.py`, plain `cc`, no NDK); the bionic/headset side is unverified. |
|
||||
| `glshim/` | `arm64-v8a/libglshim.so` | Mesa GLSL compatibility for GLAD engines (hooks eglGetProcAddress): comments out `#pragma` before `#extension`, enables `GL_EXT_shader_implicit_conversions`, hides GL_OVR_multiview (`gl_hide_multiview`, default 1) and logs failed shaders. `-DGLSHIM_TRACE` adds per-FBO draw/error tracing. |
|
||||
| `glmv/` | `arm64-v8a/libfpglmv.so` | Single-view draws of OVR_multiview programs (patch `frame.gl_multiview_fbo`, opt-in, GitHub #77). Mesa enforces OVR_multiview's rule that a draw's program declares as many views as the draw framebuffer has; engines that compile every shader with `num_views=2` and also draw into ordinary 2D framebuffers (Doom3Quest's HUD/PDA pool) lose those draws. The engine library gets it as first DT_NEEDED (direct `gl*` imports) and its `dlopen("libGLESv3.so")` string is rewritten to `libfpglmv.so` (same length; its `qgl*` dlsym table); linked `--no-as-needed` to libGLESv3 so dlsym on its handle finds everything it doesn't wrap. It records stage sources at glLinkProgram, caches the draw framebuffer's color-attachment view count per bind, and when a multiview program draws into a 0-view framebuffer it binds a lazily built twin (`glmv_rewrite.h`: num_views layout blanked, `gl_ViewID_OVR` -> `(0u)`; attribute locations + uniform block bindings copied, default-block uniforms copied before every such draw with a shadow to skip unchanged ones), draws, rebinds the original. Logcat tag `GLMV`; `gl_mv_debug=1`. The rewriter is host-tested on Doom3Quest's 19 shaders (`tests/test_gl_multiview_fbo.py`); untested on the device. |
|
||||
| `tlskeys/` | `arm64-v8a/libfp_tkeys.so` | Thread-key pool for Unity (patch `frame.unity_tls_key_pool`, opt-in, GitHub #198; approach and the first helper by Klownicle). Bionic allows 128 pthread keys per process; Unity 6 plus large native libraries (Firebase, Gecko) and the Frame's own libraries use them up (Wander: `cannot create thread specific key for __cxa_get_globals()`). libunity.so's four key imports are renamed to `fp_tkey_create/_delete/_get/_set` (`elf.rename_imports`, same `.dynstr` bytes, versions set global) and this library is added to its DT_NEEDED: keys without a destructor become slots (512, generation-tagged, `0x40000000 + slot`) in a per-thread table under one real key; keys with a destructor and all other libraries keep real keys. Lock-free get/set. Logs `fp_tkeys: pooling Unity's thread keys` once (tag FrameBridge). Host-tested (rename); not yet run on the device. |
|
||||
| `vkext/` | `arm64-v8a/libfp_vkext.so` | Quest Vulkan device extensions for builds for this PC (patch `frame.vk_android_extensions`, never on the Frame). AXRB's direct mode gives the game the Windows driver's device, which lacks `VK_EXT_fragment_density_map` and Android's external memory; CryEngine (The Climb 2) refuses to create its device without them. The renderer's `vkEnumerateDeviceExtensionProperties` import is renamed to `fpEnumerateDeviceExtensionProperties` (same length, `elf.rename_imports`) and this library added to its DT_NEEDED: it lists the real extensions plus the missing ones; AXRB's vkCreateDevice leaves those out again. Logs `fp_vkext: listing …` (tag FrameBridge). Seen working in direct mode (2026-10-16, headless). |
|
||||
| `zinkfix/` | `arm64-v8a/libVkLayer_fp_shaderfix.so` | Shader-fix Vulkan layer `VK_LAYER_FP_shader_fix` for OpenGL ES games (patch `frame.zink_shader_fix`): sits between Zink (Mesa's GL on Vulkan) and the driver and inserts the words of `zink_shader_fix` (same format as the Vulkan shim's `vk_shader_fix`: size + SHA-256 match) into SPIR-V modules; `zink_shader_dump=1` writes every distinct module to `files/fp_spirv/`. The engine library loads it first (DT_NEEDED); its constructor puts the layer in front of Android's GraphicsEnv debug layer list (private Android 11 `getDebugLayers`/`setDebugLayers`, found through libvulkan.so's dependencies) before EGL starts, so the loader picks it up from the app's lib dir. Exports only the enumeration functions and `VK_LAYER_FP_shader_fixGet*ProcAddr` (never plain `vk*` entry points). Vader Immortal's lightspeed shaders (Klownicle, GitHub #49). |
|
||||
| `xrlayer/` | `linux-arm64/libxr_frameport_timefix.so` + `XR_APILAYER_FRAMEPORT_timefix.json` | OpenXR API layer for Windows PC VR games under Proton on the Frame (glibc aarch64, built freestanding with the NDK's clang), on by default (patch `pcvr.xr_timefix`). Retries `xrCreateInstance` as an OpenXR 1.0 app when the runtime rejects 1.1 (the Frame's SteamVR runtime does, and Proton 11's VR helper asks for 1.1). Also emulates `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` if the runtime refuses them (the Frame's Android runtime does; its Linux runtime, used by Proton, supports them as of 2026-09-29 — so this part is only a fallback; Proton's wineopenxr needs them for `XR_KHR_win32_convert_performance_counter_time`, which Revive requires) with the adapter's xrWaitFrame-calibrated offset, and drops the extension from xrCreateInstance if the runtime rejects it. Enabled per game by the Proton launch.sh (`XR_API_LAYER_PATH`, `XR_ENABLE_API_LAYERS`). `tests/test_xrlayer.py` builds it for x86_64 and drives it via ctypes. |
|
||||
| `oculushmd/` | `win-x64/fp_oculushmd.exe` | PC VR counterpart of overport's `patch_oculus_unreal` (patch `pcvr.oculus_unreal`, default for Unreal Rift games on the Frame). Unreal's OculusHMD and LibOVR's `ovr_Detect` only start when the Windows named event `OculusHMDConnected` exists and is signalled (the Oculus service creates it). `fp_oculushmd.exe <command line>` creates it (manual reset, signalled), runs the command (Revive's injector) in a job object and keeps the event until every process in the job — the game and its children — has exited (fallback without job support: 3 minutes); returns the command's exit code and logs `FramePort oculushmd: …` to stderr (launch.log). Freestanding Windows x64 console exe (no CRT, no Windows SDK): the NDK's clang + `lld-link /Brepro` with a kernel32 import library generated by `llvm-dlltool`, so it's byte-reproducible. Tested on Windows from WSL with a build whose event name is `-DEVENT_NAME=L"…"` (the Oculus service owns the real event on a PC with the Oculus app). |
|
||||
|
||||
@@ -98,15 +98,6 @@ static int foveation_eye_enabled, foveation_eye_supported;
|
||||
// hide XR_FB_space_warp: games then render every frame themselves (UE5 Application SpaceWarp flickered on the Frame,
|
||||
// e.g. Into The Radius 2; OVRPort's patch_disable_space_warp doesn't reach UE5's OpenXR plugin)
|
||||
static int hide_space_warp = 0;
|
||||
// projection_only: frames with a projection layer and more layers submit the projection layer alone (AXRB 1.0.4's
|
||||
// emulator rejects every multi-layer GLES frame with -2: its GLES path has no layer batches; Accounting+'s OVROverlay
|
||||
// quads made almost every frame fail). The other layers (menus, overlays drawn as quads) are not shown; with
|
||||
// projection_only=2 (GLES) the quads after the projection layer are drawn into its eye images first (quad_compose_gl.c).
|
||||
static int projection_only = 0;
|
||||
// scale_max: the maximum eye size reported is the scaled recommended one too (games that size their eye buffers from
|
||||
// the maximum or scale the recommendation up: Accounting+'s Unity 2017 OVRPlugin made 896x1024 from a 448x504
|
||||
// recommendation). For AXRB's emulator, where every eye pixel is copied.
|
||||
static int scale_max = 0;
|
||||
static int strip_color_bias = 0;
|
||||
// fade_quad.c: draw colour-scale fades (fade to black) as a black quad over the picture; 2 = also remove the scale
|
||||
static int fade_quad = 0;
|
||||
@@ -184,8 +175,6 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "foveation_storage_fix=%f", &value) == 1) foveation_storage_fix = value != 0;
|
||||
if (sscanf(line, "foveation_eye=%f", &value) == 1) foveation_eye = value != 0;
|
||||
if (sscanf(line, "hide_space_warp=%f", &value) == 1) hide_space_warp = value != 0;
|
||||
if (sscanf(line, "projection_only=%f", &value) == 1) projection_only = value >= 2 ? 2 : value != 0;
|
||||
if (sscanf(line, "scale_max=%f", &value) == 1) scale_max = value != 0;
|
||||
if (sscanf(line, "strip_color_bias=%f", &value) == 1) strip_color_bias = (int)value;
|
||||
if (sscanf(line, "fade_quad=%f", &value) == 1) fade_quad = value > 0 ? (int)value : 0;
|
||||
if (sscanf(line, "frame_balance=%f", &value) == 1) frame_balance = value != 0;
|
||||
@@ -284,9 +273,6 @@ static void initialize(void) {
|
||||
if (proximity_emul) LOG("per-game: proximity_emul=%d (finger proximity from capacitive touch: %s)", proximity_emul,
|
||||
proximity_emul > 1 ? "thumb + index" : "thumb");
|
||||
if (hide_space_warp) LOG("per-game: hide_space_warp=1 (XR_FB_space_warp hidden, space warp info removed)");
|
||||
if (projection_only)
|
||||
LOG("per-game: projection_only=%d (frames with more layers submit the projection layer alone%s)", projection_only,
|
||||
projection_only == 2 ? ", quads drawn into it" : "");
|
||||
if (strip_color_bias) LOG("per-game: strip_color_bias=%d (layer color scale/bias removed)", strip_color_bias);
|
||||
if (fade_quad) LOG("per-game: fade_quad=%d (colour-scale fades drawn as a black quad%s)", fade_quad,
|
||||
fade_quad > 1 ? "; the layer's own scale removed" : "");
|
||||
@@ -473,12 +459,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateViewConfigurationViews(XrInstance inst
|
||||
if (h > views[i].maxImageRectHeight) h = views[i].maxImageRectHeight;
|
||||
views[i].recommendedImageRectWidth = w ? w : 1;
|
||||
views[i].recommendedImageRectHeight = h ? h : 1;
|
||||
if (scale_max && scale < 1.0f) {
|
||||
views[i].maxImageRectWidth = views[i].recommendedImageRectWidth;
|
||||
views[i].maxImageRectHeight = views[i].recommendedImageRectHeight;
|
||||
}
|
||||
LOG("view %u recommended %ux%u%s", i, views[i].recommendedImageRectWidth, views[i].recommendedImageRectHeight,
|
||||
scale_max && scale < 1.0f ? " (also the maximum)" : "");
|
||||
LOG("view %u recommended %ux%u", i, views[i].recommendedImageRectWidth, views[i].recommendedImageRectHeight);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -996,7 +977,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
|
||||
#include "layer_emul_gl.c"
|
||||
#include "cube_standin.c"
|
||||
#include "snapshot_gl.c"
|
||||
#include "quad_compose_gl.c"
|
||||
#include "surface_swapchain.c"
|
||||
#include "fade_quad.c"
|
||||
|
||||
@@ -1627,21 +1607,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
frames = 0; drift_sum = drift_max = 0;
|
||||
}
|
||||
}
|
||||
XrFrameEndInfo single;
|
||||
if (projection_only && info && info->layerCount > 1 && info->layers) {
|
||||
for (uint32_t i = 0; i < info->layerCount; ++i) {
|
||||
const XrCompositionLayerBaseHeader *l = info->layers[i];
|
||||
if (!l || l->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION) continue;
|
||||
static int logged;
|
||||
if (!logged++) LOG("projection_only: %u layers -> the projection layer alone", info->layerCount);
|
||||
qc_compose(info, i);
|
||||
single = *info;
|
||||
single.layerCount = 1;
|
||||
single.layers = &info->layers[i];
|
||||
info = &single;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((!layer_fix && !swap_eyes && !emulate_passthrough && !flip_quads && !flip_emul && !strip_depth && !fade_quad) || !info || !info->layerCount ||
|
||||
info->layerCount > 64)
|
||||
return fn(session, info);
|
||||
|
||||
@@ -126,23 +126,4 @@ static inline int lm_face_visible(int face, XrVector3f forward, float cos_limit)
|
||||
// Cosine of the angle between the face's centre direction and `forward` (unit): sort key, larger = closer to view.
|
||||
static inline float lm_face_score(int face, XrVector3f forward) { return lm_dot(lm_face_dir(face, 0, 0), forward); }
|
||||
|
||||
// Clip-space corners of a quad layer seen from one eye (projection_only=2 draws quads into the projection image):
|
||||
// `quad` = the quad's pose in the projection layer's space, `size` its extent in metres, `eye` + `fov` the view's
|
||||
// pose and angles in that space. out[0..3] = bottom-left, bottom-right, top-left, top-right (a triangle strip),
|
||||
// each x, y, z, w with w = depth in front of the eye (GL then interpolates the texture coordinates
|
||||
// perspective-correctly and clips what lies behind the eye); z = 0 (no depth test).
|
||||
static inline void lm_quad_clip(XrPosef quad, XrExtent2Df size, XrPosef eye, XrFovf fov, float out[4][4]) {
|
||||
const float tl = tanf(fov.angleLeft), tr = tanf(fov.angleRight), tu = tanf(fov.angleUp), td = tanf(fov.angleDown);
|
||||
const XrPosef to_eye = lm_pose_mul(lm_pose_inv(eye), quad);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
XrVector3f c = {(i & 1 ? 0.5f : -0.5f) * size.width, (i & 2 ? 0.5f : -0.5f) * size.height, 0};
|
||||
XrVector3f r = lm_rotate(to_eye.orientation, c);
|
||||
float x = r.x + to_eye.position.x, y = r.y + to_eye.position.y, z = r.z + to_eye.position.z;
|
||||
out[i][0] = (2 * x + (tr + tl) * z) / (tr - tl);
|
||||
out[i][1] = (2 * y + (tu + td) * z) / (tu - td);
|
||||
out[i][2] = 0;
|
||||
out[i][3] = -z;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,252 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Included by frame_adapter.c after snapshot_gl.c (whose swapchain image tracking it uses). projection_only=2 (GLES
|
||||
// sessions only): before a frame with a projection layer and more layers is reduced to the projection layer alone,
|
||||
// the quad layers submitted after the projection layer are drawn into its eye images, so menus and overlays drawn as
|
||||
// quads still show. For runtimes that refuse multi-layer GLES frames (AXRB 1.0.4's emulator: -2 for every one;
|
||||
// Path of the Warrior's menus are a VrApi cylinder the VrApi bridge turns into a quad, Accounting+ uses OVROverlay
|
||||
// quads).
|
||||
// * Drawn on the game's thread, in xrEndFrame, on its current GL context (the one the runtime reads the images back
|
||||
// from), with the eye image the game just released as the target: one textured quad per eye and quad layer, at
|
||||
// the eye's view and FOV (lm_quad_clip). No depth test: a quad is drawn over the scene (as a compositor does).
|
||||
// * Blending follows the quad's flags: XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT blends (premultiplied, or
|
||||
// straight alpha with UNPREMULTIPLIED_ALPHA_BIT), else the quad is opaque. Array quad swapchains are skipped.
|
||||
// * Every GL state it touches is restored and its errors are cleared. A failure (shader, framebuffer) is logged once
|
||||
// and turns the drawing off for the process; the frame still goes out as the projection layer alone.
|
||||
#define QC_GL_FUNCS(X) X(glCreateShader) X(glShaderSource) X(glCompileShader) X(glGetShaderiv) X(glGetShaderInfoLog) \
|
||||
X(glCreateProgram) X(glAttachShader) X(glLinkProgram) X(glGetProgramiv) X(glDeleteShader) X(glUseProgram) \
|
||||
X(glGetUniformLocation) X(glUniform4fv) X(glUniform1i) X(glUniform1f) X(glGenVertexArrays) X(glBindVertexArray) \
|
||||
X(glGenFramebuffers) X(glBindFramebuffer) X(glFramebufferTexture2D) X(glFramebufferTextureLayer) \
|
||||
X(glCheckFramebufferStatus) X(glViewport) X(glGetIntegerv) X(glGetBooleanv) X(glIsEnabled) X(glEnable) \
|
||||
X(glDisable) X(glBlendFuncSeparate) X(glBlendEquationSeparate) X(glColorMask) X(glActiveTexture) \
|
||||
X(glBindTexture) X(glGenSamplers) X(glSamplerParameteri) X(glBindSampler) X(glDrawArrays) X(glGetError)
|
||||
#define QC_DECLARE(fn) static __typeof__(&fn) q_##fn;
|
||||
QC_GL_FUNCS(QC_DECLARE)
|
||||
#undef QC_DECLARE
|
||||
|
||||
static int qc_state; // 0 untried, 1 ok, -1 off for this process
|
||||
static EGLContext qc_context; // the context the objects below belong to (VAOs and FBOs aren't shared)
|
||||
static GLuint qc_program, qc_vao, qc_fb, qc_sampler;
|
||||
static GLint qc_u_v, qc_u_tex, qc_u_opaque;
|
||||
|
||||
static const char *QC_VS =
|
||||
"#version 300 es\n"
|
||||
"uniform vec4 u_v[8];\n" // 0-3 clip-space corners (BL, BR, TL, TR), 4-7 their texture coordinates
|
||||
"out vec2 v_uv;\n"
|
||||
"void main() { gl_Position = u_v[gl_VertexID]; v_uv = u_v[4 + gl_VertexID].xy; }\n";
|
||||
static const char *QC_FS =
|
||||
"#version 300 es\n"
|
||||
"precision mediump float;\n"
|
||||
"uniform sampler2D u_tex;\n"
|
||||
"uniform float u_opaque;\n"
|
||||
"in vec2 v_uv;\n"
|
||||
"out vec4 o_color;\n"
|
||||
"void main() { vec4 c = texture(u_tex, v_uv); o_color = vec4(c.rgb, max(c.a, u_opaque)); }\n";
|
||||
|
||||
static void qc_off(const char *why) {
|
||||
qc_state = -1;
|
||||
LOG("projection_only=2: %s, quad layers are dropped instead of drawn into the main picture", why);
|
||||
}
|
||||
|
||||
static GLuint qc_shader(GLenum type, const char *src) {
|
||||
GLuint s = q_glCreateShader(type);
|
||||
q_glShaderSource(s, 1, &src, NULL);
|
||||
q_glCompileShader(s);
|
||||
GLint ok = 0;
|
||||
q_glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[256] = {0};
|
||||
q_glGetShaderInfoLog(s, sizeof(log) - 1, NULL, log);
|
||||
LOG("projection_only=2: shader: %s", log);
|
||||
q_glDeleteShader(s);
|
||||
return 0;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// GL entry points + this context's objects. 1 = ready.
|
||||
static int qc_setup(void) {
|
||||
if (qc_state < 0) return 0;
|
||||
if (!qc_state) {
|
||||
if (!snap_load_gl()) { qc_off("OpenGL ES not available"); return 0; }
|
||||
void *gles = dlopen("libGLESv3.so", RTLD_NOW | RTLD_LOCAL);
|
||||
int ok = gles != NULL;
|
||||
#define QC_RESOLVE(fn) ok &= gles && (q_##fn = (__typeof__(q_##fn))dlsym(gles, #fn)) != NULL;
|
||||
QC_GL_FUNCS(QC_RESOLVE)
|
||||
#undef QC_RESOLVE
|
||||
if (!ok) { qc_off("GL functions missing"); return 0; }
|
||||
qc_state = 1;
|
||||
}
|
||||
EGLContext ctx = s_eglGetCurrentContext();
|
||||
if (!ctx) return 0;
|
||||
if (ctx == qc_context && qc_program) return 1;
|
||||
// a new context (or the first): build the program and objects there (the old ones belonged to another context)
|
||||
GLint old_program = 0;
|
||||
q_glGetIntegerv(GL_CURRENT_PROGRAM, &old_program);
|
||||
GLuint vs = qc_shader(GL_VERTEX_SHADER, QC_VS), fs = qc_shader(GL_FRAGMENT_SHADER, QC_FS);
|
||||
GLuint program = 0;
|
||||
if (vs && fs) {
|
||||
program = q_glCreateProgram();
|
||||
q_glAttachShader(program, vs);
|
||||
q_glAttachShader(program, fs);
|
||||
q_glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
q_glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) program = 0;
|
||||
}
|
||||
if (vs) q_glDeleteShader(vs);
|
||||
if (fs) q_glDeleteShader(fs);
|
||||
if (!program) { while (q_glGetError() != GL_NO_ERROR) {} qc_off("shader program failed"); return 0; }
|
||||
qc_program = program;
|
||||
qc_u_v = q_glGetUniformLocation(program, "u_v");
|
||||
qc_u_tex = q_glGetUniformLocation(program, "u_tex");
|
||||
qc_u_opaque = q_glGetUniformLocation(program, "u_opaque");
|
||||
q_glGenVertexArrays(1, &qc_vao);
|
||||
q_glGenFramebuffers(1, &qc_fb);
|
||||
q_glGenSamplers(1, &qc_sampler);
|
||||
q_glSamplerParameteri(qc_sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // no mipmaps needed (and none required)
|
||||
q_glSamplerParameteri(qc_sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
q_glSamplerParameteri(qc_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
q_glSamplerParameteri(qc_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
q_glUseProgram((GLuint)old_program);
|
||||
qc_context = ctx;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The GL texture (and size) of the image the game last acquired (= just released) from a swapchain; 0 if unknown.
|
||||
static GLuint qc_image(XrSwapchain handle, uint32_t *w, uint32_t *h, uint32_t *array_size) {
|
||||
GLuint tex = 0;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 0);
|
||||
if (i >= 0 && snap_chains[i].count && snap_chains[i].last_acquired < snap_chains[i].count) {
|
||||
tex = snap_chains[i].images[snap_chains[i].last_acquired];
|
||||
*w = snap_chains[i].width, *h = snap_chains[i].height, *array_size = snap_chains[i].array_size;
|
||||
}
|
||||
pthread_mutex_unlock(&snap_lock);
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void qc_compose(const XrFrameEndInfo *info, uint32_t proj_index) {
|
||||
if (projection_only != 2 || !info || !info->layers) return;
|
||||
const XrCompositionLayerProjection *proj = (const XrCompositionLayerProjection *)info->layers[proj_index];
|
||||
if (!proj || proj->viewCount < 2 || !proj->views) return;
|
||||
uint32_t quads = 0;
|
||||
for (uint32_t i = proj_index + 1; i < info->layerCount; ++i)
|
||||
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_QUAD) ++quads;
|
||||
if (!quads || !qc_setup()) return;
|
||||
|
||||
// the game's state we change
|
||||
GLint draw_fb, viewport[4], program, vao, active_tex, tex2d, sampler, src_rgb, dst_rgb, src_a, dst_a, eq_rgb, eq_a;
|
||||
GLboolean color_mask[4];
|
||||
const GLenum caps[] = {GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_SCISSOR_TEST, GL_CULL_FACE,
|
||||
GL_SAMPLE_ALPHA_TO_COVERAGE, GL_RASTERIZER_DISCARD};
|
||||
GLboolean cap_on[sizeof(caps) / sizeof(caps[0])];
|
||||
q_glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_fb);
|
||||
q_glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
q_glGetIntegerv(GL_CURRENT_PROGRAM, &program);
|
||||
q_glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao);
|
||||
q_glGetIntegerv(GL_ACTIVE_TEXTURE, &active_tex);
|
||||
q_glActiveTexture(GL_TEXTURE0);
|
||||
q_glGetIntegerv(GL_TEXTURE_BINDING_2D, &tex2d);
|
||||
q_glGetIntegerv(GL_SAMPLER_BINDING, &sampler);
|
||||
q_glGetIntegerv(GL_BLEND_SRC_RGB, &src_rgb);
|
||||
q_glGetIntegerv(GL_BLEND_DST_RGB, &dst_rgb);
|
||||
q_glGetIntegerv(GL_BLEND_SRC_ALPHA, &src_a);
|
||||
q_glGetIntegerv(GL_BLEND_DST_ALPHA, &dst_a);
|
||||
q_glGetIntegerv(GL_BLEND_EQUATION_RGB, &eq_rgb);
|
||||
q_glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &eq_a);
|
||||
q_glGetBooleanv(GL_COLOR_WRITEMASK, color_mask);
|
||||
for (size_t c = 0; c < sizeof(caps) / sizeof(caps[0]); ++c) {
|
||||
cap_on[c] = q_glIsEnabled(caps[c]);
|
||||
q_glDisable(caps[c]);
|
||||
}
|
||||
q_glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
q_glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
|
||||
q_glUseProgram(qc_program);
|
||||
q_glBindVertexArray(qc_vao);
|
||||
q_glBindSampler(0, qc_sampler);
|
||||
q_glUniform1i(qc_u_tex, 0);
|
||||
q_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, qc_fb);
|
||||
|
||||
PFN_xrLocateSpace locate = (PFN_xrLocateSpace)lookup(active_instance, "xrLocateSpace");
|
||||
int drawn = 0;
|
||||
for (uint32_t eye = 0; eye < 2; ++eye) {
|
||||
const XrCompositionLayerProjectionView *view = &proj->views[eye];
|
||||
uint32_t tw = 0, th = 0, tarray = 1;
|
||||
GLuint target = qc_image(view->subImage.swapchain, &tw, &th, &tarray);
|
||||
if (!target) continue;
|
||||
if (tarray > 1)
|
||||
q_glFramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0,
|
||||
(GLint)view->subImage.imageArrayIndex);
|
||||
else
|
||||
q_glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0);
|
||||
if (q_glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
qc_off("eye image not drawable (framebuffer incomplete)");
|
||||
break;
|
||||
}
|
||||
const XrRect2Di *r = &view->subImage.imageRect;
|
||||
q_glViewport(r->offset.x, r->offset.y, r->extent.width, r->extent.height);
|
||||
for (uint32_t i = proj_index + 1; i < info->layerCount; ++i) {
|
||||
const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad *)info->layers[i];
|
||||
if (!q || q->type != XR_TYPE_COMPOSITION_LAYER_QUAD) continue;
|
||||
if (q->eyeVisibility == XR_EYE_VISIBILITY_LEFT && eye != 0) continue;
|
||||
if (q->eyeVisibility == XR_EYE_VISIBILITY_RIGHT && eye != 1) continue;
|
||||
uint32_t sw = 0, sh = 0, sarray = 1;
|
||||
GLuint source = qc_image(q->subImage.swapchain, &sw, &sh, &sarray);
|
||||
if (!source || sarray > 1 || !sw || !sh) continue;
|
||||
XrPosef pose = q->pose; // the quad in the projection layer's space
|
||||
if (q->space != proj->space) {
|
||||
XrSpaceLocation loc = {XR_TYPE_SPACE_LOCATION};
|
||||
if (!locate || XR_FAILED(locate(q->space, proj->space, info->displayTime, &loc))) continue;
|
||||
pose = lm_pose_mul(loc.pose, q->pose);
|
||||
}
|
||||
float v[8][4];
|
||||
lm_quad_clip(pose, q->size, view->pose, view->fov, v);
|
||||
const XrRect2Di *s = &q->subImage.imageRect;
|
||||
float u0 = (float)s->offset.x / sw, u1 = (float)(s->offset.x + s->extent.width) / sw;
|
||||
float v0 = (float)s->offset.y / sh, v1 = (float)(s->offset.y + s->extent.height) / sh;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
v[4 + k][0] = k & 1 ? u1 : u0;
|
||||
v[4 + k][1] = k & 2 ? v1 : v0; // GL textures: v grows upwards, the quad's top is +Y
|
||||
v[4 + k][2] = v[4 + k][3] = 0;
|
||||
}
|
||||
q_glUniform4fv(qc_u_v, 8, &v[0][0]);
|
||||
const int blend = (q->layerFlags & XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT) != 0;
|
||||
q_glUniform1f(qc_u_opaque, blend ? 0.0f : 1.0f);
|
||||
if (blend) {
|
||||
q_glEnable(GL_BLEND);
|
||||
if (q->layerFlags & XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT)
|
||||
q_glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
else
|
||||
q_glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
q_glDisable(GL_BLEND);
|
||||
}
|
||||
q_glBindTexture(GL_TEXTURE_2D, source);
|
||||
q_glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
++drawn;
|
||||
}
|
||||
}
|
||||
|
||||
// the game's state back
|
||||
q_glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
q_glBindFramebuffer(GL_DRAW_FRAMEBUFFER, (GLuint)draw_fb);
|
||||
q_glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
q_glBindTexture(GL_TEXTURE_2D, (GLuint)tex2d);
|
||||
q_glBindSampler(0, (GLuint)sampler);
|
||||
q_glActiveTexture((GLenum)active_tex);
|
||||
q_glBindVertexArray((GLuint)vao);
|
||||
q_glUseProgram((GLuint)program);
|
||||
q_glBlendFuncSeparate((GLenum)src_rgb, (GLenum)dst_rgb, (GLenum)src_a, (GLenum)dst_a);
|
||||
q_glBlendEquationSeparate((GLenum)eq_rgb, (GLenum)eq_a);
|
||||
q_glColorMask(color_mask[0], color_mask[1], color_mask[2], color_mask[3]);
|
||||
for (size_t c = 0; c < sizeof(caps) / sizeof(caps[0]); ++c)
|
||||
if (cap_on[c]) q_glEnable(caps[c]); else q_glDisable(caps[c]);
|
||||
GLenum err, first = GL_NO_ERROR;
|
||||
while ((err = q_glGetError()) != GL_NO_ERROR) if (first == GL_NO_ERROR) first = err;
|
||||
static int logged;
|
||||
if (drawn && !logged++) LOG("projection_only=2: %u quad layer(s) drawn into the main picture", quads);
|
||||
if (first != GL_NO_ERROR) {
|
||||
static int errors;
|
||||
if (errors++ < 3) LOG("projection_only=2: GL error 0x%x while drawing quads", first);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,6 @@ SNAP_GL_FUNCS(SNAP_DECLARE)
|
||||
#undef SNAP_DECLARE
|
||||
static EGLContext (*s_eglGetCurrentContext)(void);
|
||||
|
||||
// The swapchain image tracking also serves projection_only=2 (quad_compose_gl.c).
|
||||
#define snap_tracking() (snapshot || projection_only == 2)
|
||||
|
||||
static struct {
|
||||
XrSwapchain handle;
|
||||
uint32_t width, height, array_size, count, last_acquired;
|
||||
@@ -51,7 +48,7 @@ static int snap_find(XrSwapchain handle, int create) {
|
||||
}
|
||||
|
||||
static void snap_on_create(XrSwapchain handle, const XrSwapchainCreateInfo *info) {
|
||||
if (!snap_tracking() || !info) return;
|
||||
if (!snapshot || !info) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 1);
|
||||
if (i >= 0) {
|
||||
@@ -63,7 +60,7 @@ static void snap_on_create(XrSwapchain handle, const XrSwapchainCreateInfo *info
|
||||
}
|
||||
|
||||
static void snap_on_destroy(XrSwapchain handle) {
|
||||
if (!snap_tracking()) return;
|
||||
if (!snapshot) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 0);
|
||||
if (i >= 0) snap_chains[i].handle = XR_NULL_HANDLE;
|
||||
@@ -71,7 +68,7 @@ static void snap_on_destroy(XrSwapchain handle) {
|
||||
}
|
||||
|
||||
static void snap_on_enumerate(XrSwapchain handle, uint32_t count, const XrSwapchainImageBaseHeader *images) {
|
||||
if (!snap_tracking() || !images || !count || images->type != (XrStructureType)1000024002) return; // OPENGL_ES_KHR
|
||||
if (!snapshot || !images || !count || images->type != (XrStructureType)1000024002) return; // OPENGL_ES_KHR
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 1);
|
||||
if (i >= 0) {
|
||||
@@ -84,7 +81,7 @@ static void snap_on_enumerate(XrSwapchain handle, uint32_t count, const XrSwapch
|
||||
}
|
||||
|
||||
static void snap_on_acquire(XrSwapchain handle, uint32_t index) {
|
||||
if (!snap_tracking()) return;
|
||||
if (!snapshot) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 0);
|
||||
if (i >= 0) snap_chains[i].last_acquired = index;
|
||||
|
||||
+3
-12
@@ -380,15 +380,6 @@ def build_tlskeys(tc: Path):
|
||||
"-Wl,-z,max-page-size=16384", "tlskeys.c", "-llog", "-o", ART / "arm64-v8a/libfp_tkeys.so"], cwd=src)
|
||||
|
||||
|
||||
def build_vkext(tc: Path):
|
||||
"""Android-only Vulkan device extensions listed for builds for this PC (see vkext/vkext.c, patch
|
||||
frame.vk_android_extensions)."""
|
||||
src = HERE / "vkext"
|
||||
run([exe(tc, "aarch64-linux-android29-clang"), "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
"-fvisibility=hidden", f"-ffile-prefix-map={HERE}=native", "-Wl,-soname,libfp_vkext.so",
|
||||
"-Wl,-z,max-page-size=16384", "vkext.c", "-lvulkan", "-llog", "-o", ART / "arm64-v8a/libfp_vkext.so"], cwd=src)
|
||||
|
||||
|
||||
def build_luajit(tc: Path):
|
||||
run([sys.executable, HERE / "luajit/build.py", "--clang-dir", tc / "bin",
|
||||
"--out", ART / "arm64-v8a/libfp_luajit.so"])
|
||||
@@ -447,7 +438,7 @@ def write_sums():
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,vkext,luajit,vrsettings,venc,a32")
|
||||
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,glmv,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,foveation,ovrpshim,tlskeys,luajit,vrsettings,venc,a32")
|
||||
ap.add_argument("--ndk")
|
||||
args = ap.parse_args()
|
||||
parts = set(args.only.split(","))
|
||||
@@ -460,11 +451,11 @@ def main():
|
||||
"oculushmd": lambda: build_oculushmd(tc), "vkshim": lambda: build_vkshim(tc),
|
||||
"zinkfix": lambda: build_zinkfix(tc),
|
||||
"foveation": lambda: build_foveation(tc),
|
||||
"ovrpshim": lambda: build_ovrpshim(tc), "tlskeys": lambda: build_tlskeys(tc), "vkext": lambda: build_vkext(tc),
|
||||
"ovrpshim": lambda: build_ovrpshim(tc), "tlskeys": lambda: build_tlskeys(tc),
|
||||
"luajit": lambda: build_luajit(tc),
|
||||
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc),
|
||||
"vroverlay_probe": lambda: build_vroverlay_probe(tc), "a32": lambda: build_a32(tc)}
|
||||
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "vkext", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
|
||||
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "glmv", "ovrtrace", "xrshim", "vkshim", "zinkfix", "foveation", "ovrpshim", "tlskeys", "luajit", "dex", "xrlayer", "oculushmd", "vrsettings", "venc",
|
||||
"vroverlay_probe", "a32"):
|
||||
if name in parts:
|
||||
log(f"build {name}")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# x86_64 Wine under FEX: Rift games with Meta's runtime on the Frame (experimental)
|
||||
|
||||
Sources of `artifacts/fexrift/` (prebuilt, with `SHA256SUMS`) for the experimental mode `frameport fex ...`
|
||||
(FRAMEPORT_EXPERIMENTAL_FEX=1; PC side `src/frameport/fexrift.py`, Frame side `agent/fexrift.py`). Research notes:
|
||||
CLAUDE.md, "Rift via x86_64 Wine under FEX". Rebuild order: `prep_ge_wine.sh` (GE's exact Wine source) ->
|
||||
`build_pe_dlls.sh` (x86_64 host, llvm-mingw) + `build_wineserver.sh` (aarch64 host, e.g. the Frame; then `strip`) +
|
||||
fp_mem.so (command below).
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `pipesend.c` | `fp_pipesend.exe`, built by `build_pe_dlls.sh` with llvm-mingw. Sends a browser's `oculus://link/frl_login` callback into the running Meta app's `\\.\pipe\oculus`; reads UTF-8 from stdin so SSH forwarding never puts credentials in argv. Retries a busy/missing pipe for 20 seconds. Does not print the link. Add the rebuilt exe to `artifacts/fexrift/SHA256SUMS`. Pipe framing still needs the device check in `docs/rift-fex/login.md`. |
|
||||
| `prep_ge_wine.sh` | Recreates the exact Wine source of a GE-Proton release (its Wine and wine-staging submodules plus GE's patches) by running only the WINE section of GE's `protonprep-valve-staging.sh`. |
|
||||
| `build_wineserver.sh` | Builds a **native aarch64 wineserver** from that tree on an aarch64 host (the Frame has gcc, make, flex and bison). It speaks the same protocol as GE's x86_64 server (GE-Proton11-7: protocol 938, 321 requests; plain Valve Wine at the same commit lacks 15 of GE's requests). `-DFP_EMULATED_X86_64` gives it the x86_64 machine model, because its clients are x86_64 processes under FEX. Use it by having the `wineserver` wrapper exec it instead of the FEX-run x86_64 server. |
|
||||
| `build_pe_dlls.sh` + `patches/` | The four Wine DLLs FramePort replaces in GE-Proton, built from GE's tree + only these patches: `crypt32-chain-newest-issuer` (Windows' newest-issuer rule between equal chains: Meta's OculusAppFramework signer check needs the 4-certificate chain), `sechost-realtime-trace-consumer` (OpenTraceW real-time sessions: the runtime's ETW USB tracing), `dnsapi-dnsservice-functions` (DnsService* from Wine 11.19: Air Link mDNS, OVRServer quits without them), `devenum-devicewatcher-handlers` (DeviceWatcher Updated/Removed handler lists). Exports equal GE's (+8 DnsService*). Also builds `windows.devices.wifi.dll` from `wifi_stub.c`: a Windows.Devices.WiFi.WiFiAdapter class (GetDeviceSelector matches nothing) that the logged-in runtime needs. |
|
||||
| `fp_mem.c` | x86_64 `LD_PRELOAD` for the guest. `FP_BIGCACHE_MB=<n>` keeps released anonymous regions of at least n MB (up to 12, 6 GB in total) and returns them when the same start address is mapped again; a shorter request releases the rest, a longer one maps only the extra part fresh. In the headset First Contact allocated 0.5-1.6 GB blocks several times at once, which the first, single-block version missed. `FP_MEM_ZERO=1` (v3) lazily zeroes only the reused range with `MADV_DONTNEED` before returning it; unset keeps v2 behavior. Debug counters report advice calls/bytes/failures. `FP_THP=1` sets `MADV_HUGEPAGE` on large writable regions. From the repo root on the build host, build with `gcc -O2 -shared -fPIC -o artifacts/fexrift/fp_mem.so native/fexwine/fp_mem.c -ldl -lpthread` on an x86_64 host (needs glibc 2.34). **Under FEX, pass it as `FEX_ENV=LD_PRELOAD=<path>`**: Valve's `fex-compat-tool` deletes `LD_PRELOAD`. |
|
||||
|
||||
## Measurements: Oculus First Contact, headless, dev Frame, 2026-10-10
|
||||
|
||||
SteamVR compositor frame timings were read through `IVRCompositor::GetFrameTimings`.
|
||||
|
||||
| Setup | Hitches per 20 s | Frames timed out | Game CPU per frame |
|
||||
|---|---|---|---|
|
||||
| FEX-run x86_64 wineserver, server sync | 17 | 40 % | 10.4 ms |
|
||||
| Native wineserver, server sync | 9 | 32 % | 10.4 ms |
|
||||
| Native wineserver + ntsync | 0-4 | 9-10 % | 6.2 ms |
|
||||
| Native wineserver + ntsync + `FP_BIGCACHE_MB=256` | **0** (3 of 3 runs) | 5.4-5.9 % | 5.3 ms |
|
||||
|
||||
Each remaining hitch (one app frame shown about 9 times) was Unreal's large-block allocator:
|
||||
1. It calls VirtualAlloc for 514 MB (First Contact's exe RVA 0x1a188e) every 1-8 s.
|
||||
2. A game task fills the block with a single `rep stos`/`rep movs` while the game thread waits for it.
|
||||
3. Another thread then frees the block.
|
||||
|
||||
Under FEX every 4 KiB page of it faults in and gets zeroed and charged by the kernel: about 130k faults and 110-140 ms per hitch.
|
||||
|
||||
Huge pages work in isolation (514 MB in 22 ms instead of 212 ms), but fail in practice. On a Frame that has been up for a while, memory is fragmented, so allocations fall back to 4 KiB pages (`thp_fault_fallback` far above `thp_fault_alloc`). The kernel's `defrag=madvise` setting also lets those attempts stall in compaction.
|
||||
|
||||
The block cache returns the region with stale contents instead of zeroed pages. That is only safe for games that overwrite the whole block, so it has to stay a per-game setting.
|
||||
|
||||
|
||||
## Launch-time experiments and bounded measurements
|
||||
|
||||
The First Contact numbers above are research measurements, not new results from the perf extension. Subsequent
|
||||
owner tests found the remaining bottleneck to be GPU work: about 55 percent game GPU busy at 36 fps, about
|
||||
20 percent for the compositor, and 5–10 ms CPU ApplicationTime. With per-app motion smoothing off, it reached
|
||||
72 fps with occasional misses. DXVK `d3d11.disableMsaa=True` reduced measured game GPU busy from 91 to 69 percent;
|
||||
the owner reported much better performance and rougher edges. `r.MobileMSAA=2` prevented rendering.
|
||||
|
||||
`agent/fexrift_perf.py` adds validated per-game settings read at every launch, including MSAA, frame latency,
|
||||
Revive eye-size divisors, core affinity, ntsync, guest cache options and a small FEX environment whitelist.
|
||||
`FP_MEM_ZERO=1` restores zero-page semantics for cache reuse, with the cost of page faults on later touches.
|
||||
When advice fails the region is evicted and mapped fresh. ZERO is opt-in; the default cache remains unzeroed.
|
||||
The host test uses fake mmap/munmap/madvise to check full, short and long reuse and advice-failure fallback.
|
||||
The real x86_64 guest artifact and its checksum must be rebuilt by the maintainer before ZERO experiments.
|
||||
|
||||
`frameport fex bench APP --seconds 120` requests one headless run with a total launch/sampling window of at most
|
||||
120 seconds, then stops Wine and the launcher and restores compositor standby and the screen timeout. It refuses
|
||||
an active game/login or a headset outside standby. It reports deduplicated compositor frame timings, hitches
|
||||
(frames presented at least three times), game DRM fdinfo GPU deltas and sampled thread wait states. These new
|
||||
paths have only offline tests; the pinned OpenVR interfaces and process attribution need device validation.
|
||||
See [the perf stream notes](../../docs/rift-fex/perf.md) for exact build, baseline, experiment and undo commands.
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Build the four patched Wine PE DLLs FramePort adds to GE-Proton for the Meta runtime (see README.md), from GE's
|
||||
# exact Wine source (prep_ge_wine.sh) + patches/*.patch, on an x86_64 Linux host with llvm-mingw on PATH.
|
||||
# Only these DLLs are built: generated headers they don't use (Vulkan thunks) are empty stand-ins, the syscall
|
||||
# tables come from Wine's own tools/make_specfiles.
|
||||
# usage: build_pe_dlls.sh <patched GE wine dir (prep_ge_wine.sh)> <work dir> <out dir>
|
||||
set -e
|
||||
SRC=$(cd "$1" && pwd); W=$2; OUT=$3
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
[ -n "$OUT" ] || { echo "usage: $0 <wine dir> <work dir> <out dir>"; exit 2; }
|
||||
command -v x86_64-w64-mingw32-clang >/dev/null || { echo "llvm-mingw (x86_64-w64-mingw32-clang) must be on PATH"; exit 2; }
|
||||
mkdir -p "$W" "$OUT"; W=$(cd "$W" && pwd); OUT=$(cd "$OUT" && pwd)
|
||||
if [ ! -d "$W/wine" ]; then
|
||||
cp -a "$SRC" "$W/wine"
|
||||
for p in "$HERE"/patches/*.patch; do patch -d "$W/wine" -p1 -s < "$p"; echo "applied $(basename "$p")"; done
|
||||
(cd "$W/wine" && perl tools/make_specfiles >/dev/null)
|
||||
for f in include/wine/vulkan.h dlls/winevulkan/loader_thunks.c dlls/winevulkan/loader_thunks.h \
|
||||
dlls/winevulkan/vulkan_thunks.c dlls/winevulkan/vulkan_thunks.h dlls/winevulkan/winevulkan.json; do
|
||||
[ -e "$W/wine/$f" ] || : > "$W/wine/$f"
|
||||
done
|
||||
fi
|
||||
mkdir -p "$W/obj"
|
||||
if [ ! -f "$W/obj/Makefile" ]; then
|
||||
# no debug info and relative source paths: the DLLs are committed (no local paths may end up in them)
|
||||
(cd "$W/obj" && CROSSCFLAGS="-O2 -ffile-prefix-map=$W/wine=wine -ffile-prefix-map=$W/obj=obj" \
|
||||
"$W/wine/configure" --enable-archs=x86_64 --disable-tests --without-x --without-freetype \
|
||||
--without-wayland --without-gstreamer --without-pulse --without-alsa --without-oss --without-cups --without-dbus \
|
||||
--without-gnutls --without-krb5 --without-netapi --without-opencl --without-pcap --without-sane --without-udev \
|
||||
--without-usb --without-v4l2 --without-vulkan --without-sdl --without-capi --without-gphoto --without-fontconfig \
|
||||
--without-coreaudio --without-opengl --without-inotify --without-unwind --without-ffmpeg > configure.log 2>&1)
|
||||
fi
|
||||
DLLS="crypt32 sechost dnsapi windows.devices.enumeration"
|
||||
cd "$W/obj"
|
||||
make -j"$(nproc)" $(for d in $DLLS; do echo "dlls/$d/x86_64-windows/$d.dll"; done) > make.log 2>&1 || { tail -20 make.log; exit 1; }
|
||||
for d in $DLLS; do cp "dlls/$d/x86_64-windows/$d.dll" "$OUT/"; done
|
||||
# Wi-Fi WinRT stand-in (not a Wine DLL: goes into the prefix's system32 + its activation class registry key)
|
||||
x86_64-w64-mingw32-clang -O2 -shared -s -Wl,--no-insert-timestamp -o "$OUT/windows.devices.wifi.dll" \
|
||||
"$HERE/wifi_stub.c" "$HERE/wifi_stub.def" -lole32 -lwindowsapp
|
||||
# Meta's single-instance pipe, used by browser sign-in (no entitlement or Platform SDK changes).
|
||||
x86_64-w64-mingw32-clang -O2 -s -Wl,--no-insert-timestamp -o "$OUT/fp_pipesend.exe" "$HERE/pipesend.c"
|
||||
ls -la "$OUT"
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Build a native aarch64 wineserver from a patched GE Wine tree (prep_ge_wine.sh), for x86_64 Wine under FEX.
|
||||
# Run it ON an aarch64 Linux host (e.g. the Frame: gcc, make, flex, bison, autoconf are in SteamOS).
|
||||
# - Wine's configure insists on a PE cross compiler for aarch64 and on generated headers this tree lacks; the server
|
||||
# needs neither, so configure only produces include/config.h here and the server sources are compiled directly.
|
||||
# - -DFP_EMULATED_X86_64: the server takes the x86_64 branch of init_supported_machines (native machine AMD64), as
|
||||
# the clients are x86_64 processes under FEX; otherwise it would advertise ARM64 and refuse/misplace x86_64 images.
|
||||
# The result talks the same protocol as GE's x86_64 wineserver (check: SERVER_PROTOCOL_VERSION and the request list).
|
||||
# usage: build_wineserver.sh <patched wine dir> <build dir> → <build dir>/ws/wineserver
|
||||
set -e
|
||||
SRC=$(cd "$1" && pwd); B=$2
|
||||
[ -n "$B" ] || { echo "usage: $0 <wine dir> <build dir>"; exit 2; }
|
||||
[ "$(uname -m)" = aarch64 ] || { echo "run this on aarch64"; exit 2; }
|
||||
mkdir -p "$B/obj" "$B/ws"; B=$(cd "$B" && pwd)
|
||||
grep -q FP_EMULATED_X86_64 "$SRC/server/registry.c" || \
|
||||
sed -i 's/^#elif defined(__x86_64__)$/#elif defined(__x86_64__) || (defined(__aarch64__) \&\& defined(FP_EMULATED_X86_64))/' "$SRC/server/registry.c"
|
||||
grep -q FP_EMULATED_X86_64 "$SRC/server/registry.c" || { echo "registry.c layout changed"; exit 1; }
|
||||
if [ ! -f "$B/obj/include/config.h" ]; then
|
||||
cp "$SRC/configure" "$B/configure.fp"
|
||||
sed -i 's/as_fn_error $? "PE cross-compilation is required/echo "(ignored) PE cross-compilation is required/' "$B/configure.fp"
|
||||
# configure.fp must run from the source dir's point of view: copy it in, run, remove
|
||||
cp "$B/configure.fp" "$SRC/configure.fp"
|
||||
(cd "$B/obj" && "$SRC/configure.fp" --srcdir="$SRC" --without-mingw --without-x --without-wayland --without-freetype \
|
||||
--without-gstreamer --without-vulkan --without-opengl --without-pulse --without-alsa --without-sdl --without-udev \
|
||||
--without-usb --without-gnutls --without-cups --without-dbus --without-krb5 --without-netapi --without-pcap \
|
||||
--without-sane --without-v4l2 --without-gphoto --without-unwind --without-ffmpeg --disable-tests > configure.log 2>&1 || true)
|
||||
rm -f "$SRC/configure.fp"
|
||||
[ -f "$B/obj/include/config.h" ] || { echo "configure did not produce include/config.h (see $B/obj/configure.log)"; exit 1; }
|
||||
fi
|
||||
cd "$B/ws"
|
||||
# -ffile-prefix-map: the artifact is committed to a public repo, keep the build folders out of its assert paths
|
||||
CF="-c -O2 -g -pipe -fno-strict-aliasing -D__WINESRC__ -D_GNU_SOURCE -DFP_EMULATED_X86_64 -ffile-prefix-map=$SRC=wine -ffile-prefix-map=$B=build -I. -I$B/obj/include -I$SRC/include -I$SRC/server"
|
||||
for s in $(grep -oE "^\s+[a-z_0-9]+\.c" "$SRC/server/Makefile.in" | tr -d "\t "); do
|
||||
extra=()
|
||||
case $s in
|
||||
unicode.c) extra=('-DBINDIR="/usr/bin"' '-DDATADIR="/usr/share"') ;; # nls dir = <bin>/../share/wine/nls
|
||||
user.c) extra=(-include stdarg.h) ;;
|
||||
esac
|
||||
gcc $CF "${extra[@]}" -o "${s%.c}.o" "$SRC/server/$s"
|
||||
done
|
||||
gcc -o wineserver ./*.o -lrt -lm
|
||||
echo "built $B/ws/wineserver (protocol $(grep -oE "SERVER_PROTOCOL_VERSION [0-9]+" "$SRC/include/wine/server_protocol.h" | awk '{print $2}'))"
|
||||
@@ -0,0 +1,209 @@
|
||||
/* fp_mem.so: x86_64 LD_PRELOAD for Wine under FEX (give it to the guest with FEX_ENV=LD_PRELOAD=..., the FEX
|
||||
* compat tool strips LD_PRELOAD).
|
||||
*
|
||||
* FP_BIGCACHE_MB=<n>: keep the pages of released anonymous regions of >= n MB (up to FP_BIGCACHE_SLOTS regions,
|
||||
* default 12, and FP_BIGCACHE_MAX_MB in total, default 6144) and hand them back when Wine maps memory at the
|
||||
* same start address again. Unreal's large-block allocator gives blocks above its cache limit straight back to
|
||||
* the OS, and some games (Oculus First Contact: 0.5-1.4 GB every few seconds while playing) fill such blocks
|
||||
* completely each time: under FEX every 4 KB page then faults and is zeroed by the kernel (~130k faults, ~110 ms)
|
||||
* and releasing them costs about as much again, on a game task the game thread waits for.
|
||||
* Same start, other size: shorter = the rest is released; longer = only the extra part is mapped fresh.
|
||||
* Reused memory is NOT zeroed (Windows guarantees zeroed commits), so this is only for games that overwrite
|
||||
* the whole block; any other use of a kept range drops it first (fresh zero pages).
|
||||
* Kept regions keep their protection (they are not touched while kept), so keeping and reusing is nearly free.
|
||||
* FP_MEM_ZERO=1: MADV_DONTNEED the reused part before returning it (lazy kernel zero-fill). Unlike the default
|
||||
* unzeroed cache this preserves anonymous zero-page semantics; it trades the saved page faults for correctness.
|
||||
* FP_THP=1: MADV_HUGEPAGE on large writable anonymous regions (only helps while 2 MB pages are free).
|
||||
* FP_MEM_DEBUG=1: log to stderr. */
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define MAX_SLOTS 16
|
||||
static size_t cache_min, cache_max = (size_t)6144 << 20, thp_min = (size_t)32 << 20;
|
||||
static int debug, thp, zero, slots = 12;
|
||||
static int (*real_mprotect)(void *, size_t, int);
|
||||
static int (*real_munmap)(void *, size_t);
|
||||
static void *(*real_mmap)(void *, size_t, int, int, int, off_t);
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
struct entry { char *addr; size_t len; int prot, via_munmap, used; };
|
||||
static struct entry cache[MAX_SLOTS];
|
||||
static size_t kept_total;
|
||||
static unsigned long hits, partial, stores, drops, zero_calls, zero_failures;
|
||||
static size_t zero_bytes;
|
||||
|
||||
static void resolve(void)
|
||||
{
|
||||
if (!real_mmap) real_mmap = dlsym(RTLD_NEXT, "mmap");
|
||||
if (!real_munmap) real_munmap = dlsym(RTLD_NEXT, "munmap");
|
||||
if (!real_mprotect) real_mprotect = dlsym(RTLD_NEXT, "mprotect");
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void fp_mem_init(void)
|
||||
{
|
||||
const char *e;
|
||||
resolve();
|
||||
debug = getenv("FP_MEM_DEBUG") != NULL;
|
||||
thp = getenv("FP_THP") != NULL;
|
||||
zero = (e = getenv("FP_MEM_ZERO")) && !strcmp(e, "1");
|
||||
if ((e = getenv("FP_BIGCACHE_MB")) && atol(e) > 0) cache_min = (size_t)atol(e) << 20;
|
||||
if ((e = getenv("FP_BIGCACHE_MAX_MB")) && atol(e) > 0) cache_max = (size_t)atol(e) << 20;
|
||||
if ((e = getenv("FP_BIGCACHE_SLOTS")) && atoi(e) > 0) slots = atoi(e) > MAX_SLOTS ? MAX_SLOTS : atoi(e);
|
||||
}
|
||||
|
||||
static void advise(void *addr, size_t len)
|
||||
{
|
||||
uintptr_t s = ((uintptr_t)addr + 0x1fffff) & ~(uintptr_t)0x1fffff;
|
||||
uintptr_t e = ((uintptr_t)addr + len) & ~(uintptr_t)0x1fffff;
|
||||
if (e > s) madvise((void *)s, e - s, MADV_HUGEPAGE);
|
||||
}
|
||||
|
||||
/* lock held: give part of a kept range up the way the program asked for it originally */
|
||||
static void release_range(const struct entry *c, char *addr, size_t len)
|
||||
{
|
||||
if (c->via_munmap) real_munmap(addr, len);
|
||||
else real_mmap(addr, len, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_NORESERVE, -1, 0);
|
||||
}
|
||||
|
||||
static void drop(struct entry *c, const char *why)
|
||||
{
|
||||
if (debug) fprintf(stderr, "fp_mem: drop %p+%zx (%s)\n", c->addr, c->len, why);
|
||||
release_range(c, c->addr, c->len);
|
||||
kept_total -= c->len; c->used = 0; drops++;
|
||||
}
|
||||
|
||||
/* lock held: drop every kept range overlapping [addr, len) */
|
||||
static void drop_overlaps(const void *addr, size_t len, const char *why)
|
||||
{
|
||||
for (int i = 0; i < slots; i++)
|
||||
if (cache[i].used && (const char *)addr < cache[i].addr + cache[i].len && cache[i].addr < (const char *)addr + len)
|
||||
drop(&cache[i], why);
|
||||
}
|
||||
|
||||
static int any_kept(void)
|
||||
{
|
||||
for (int i = 0; i < slots; i++) if (cache[i].used) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* lock held: keep [addr, len) instead of releasing it (its pages and protection stay as they are) */
|
||||
static int store(void *addr, size_t len, int via_munmap, int prot)
|
||||
{
|
||||
int free_slot = -1;
|
||||
if (kept_total + len > cache_max) return 0;
|
||||
for (int i = 0; i < slots; i++) {
|
||||
if (!cache[i].used) { if (free_slot < 0) free_slot = i; continue; }
|
||||
if ((char *)addr < cache[i].addr + cache[i].len && cache[i].addr < (char *)addr + len) return 0;
|
||||
}
|
||||
if (free_slot < 0) return 0;
|
||||
cache[free_slot] = (struct entry){ addr, len, prot, via_munmap, 1 };
|
||||
kept_total += len; stores++;
|
||||
if (debug) fprintf(stderr, "fp_mem: keep %p+%zx (%s, kept %zu MB in total; stores %lu, hits %lu, partial %lu, drops %lu; "
|
||||
"zero calls %lu, bytes %zu, failures %lu)\n",
|
||||
addr, len, via_munmap ? "munmap" : "release", kept_total >> 20, stores, hits, partial, drops,
|
||||
zero_calls, zero_bytes, zero_failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* lock held: discard only the part about to be reused, including a short request's kept prefix. On failure the
|
||||
* caller falls through to overlap eviction + a real mmap: ZERO must never hand stale contents to the guest. */
|
||||
static int zero_reused(void *addr, size_t len)
|
||||
{
|
||||
if (!zero) return 0;
|
||||
zero_calls++;
|
||||
int r = madvise(addr, len, MADV_DONTNEED);
|
||||
if (r) zero_failures++;
|
||||
else zero_bytes += len;
|
||||
if (debug) fprintf(stderr, "fp_mem: zero calls %lu, bytes %zu, failures %lu\n",
|
||||
zero_calls, zero_bytes, zero_failures);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* protection of a region before it is released: unknown here, so assume read/write (Wine's committed views) */
|
||||
#define KEPT_PROT (PROT_READ | PROT_WRITE)
|
||||
|
||||
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off)
|
||||
{
|
||||
resolve();
|
||||
if (cache_min && len >= cache_min / 4 && any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
for (int i = 0; i < slots; i++) {
|
||||
struct entry *c = &cache[i];
|
||||
if (!c->used || (char *)addr != c->addr || fd != -1 || !(flags & MAP_ANONYMOUS) || !(prot & PROT_WRITE)) continue;
|
||||
if (len <= c->len) {
|
||||
if (prot != c->prot && real_mprotect(addr, len, prot)) break;
|
||||
if (zero_reused(addr, len)) break;
|
||||
if (len < c->len) { release_range(c, c->addr + len, c->len - len); partial++; }
|
||||
kept_total -= c->len; c->used = 0; hits++;
|
||||
if (debug) fprintf(stderr, "fp_mem: reuse %p+%zx of %zx\n", addr, len, c->len);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return addr;
|
||||
}
|
||||
/* longer: map the extra part fresh behind the kept one */
|
||||
void *t = real_mmap(c->addr + c->len, len - c->len, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (t == (void *)(c->addr + c->len)) {
|
||||
int protect_failed = prot != c->prot && real_mprotect(c->addr, c->len, prot);
|
||||
if ((zero && protect_failed) || zero_reused(c->addr, c->len)) {
|
||||
real_munmap(t, len - c->len);
|
||||
break;
|
||||
}
|
||||
kept_total -= c->len; c->used = 0; hits++; partial++;
|
||||
if (debug) fprintf(stderr, "fp_mem: reuse %p+%zx of %zx (+ fresh tail)\n", addr, len, c->len);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (thp && len - c->len >= thp_min) advise(t, len - c->len);
|
||||
return addr;
|
||||
}
|
||||
if (t != MAP_FAILED) real_munmap(t, len - c->len);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
if (cache_min && len >= cache_min && (flags & MAP_ANONYMOUS) && (flags & MAP_FIXED) && prot == PROT_NONE && fd == -1) {
|
||||
/* Wine releasing (part of) a view inside its reserved areas */
|
||||
pthread_mutex_lock(&lock);
|
||||
int ok = store(addr, len, 0, KEPT_PROT);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (ok) return addr;
|
||||
}
|
||||
if (any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "mmap overlap");
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
void *r = real_mmap(addr, len, prot, flags, fd, off);
|
||||
if (thp && r != MAP_FAILED && (flags & MAP_ANONYMOUS) && (prot & PROT_WRITE) && len >= thp_min) advise(r, len);
|
||||
return r;
|
||||
}
|
||||
|
||||
void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off_t off) __attribute__((alias("mmap")));
|
||||
|
||||
int munmap(void *addr, size_t len)
|
||||
{
|
||||
resolve();
|
||||
if (cache_min && (len >= cache_min || any_kept())) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "munmap overlap");
|
||||
if (len >= cache_min && store(addr, len, 1, KEPT_PROT)) { pthread_mutex_unlock(&lock); return 0; }
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
return real_munmap(addr, len);
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot)
|
||||
{
|
||||
resolve();
|
||||
if (any_kept()) {
|
||||
pthread_mutex_lock(&lock);
|
||||
drop_overlaps(addr, len, "mprotect overlap");
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
int r = real_mprotect(addr, len, prot);
|
||||
if (thp && !r && (prot & PROT_WRITE) && len >= thp_min) advise(addr, len);
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
diff --git a/dlls/crypt32/chain.c b/dlls/crypt32/chain.c
|
||||
--- a/dlls/crypt32/chain.c
|
||||
+++ b/dlls/crypt32/chain.c
|
||||
@@ -2578,6 +2578,36 @@
|
||||
return quality;
|
||||
}
|
||||
|
||||
+/* Between two chains of equal quality, Windows prefers the one whose issuer is
|
||||
+ * the most recent certificate (latest NotBefore) where the chains first
|
||||
+ * differ, e.g. a cross-signed root shipped with a signature (2022) over an
|
||||
+ * older self-signed copy of the same root (2013). Returns TRUE if alternate
|
||||
+ * should be preferred over chain.
|
||||
+ */
|
||||
+static BOOL CRYPT_AlternateHasNewerIssuer(const CertificateChain *chain,
|
||||
+ const CertificateChain *alternate)
|
||||
+{
|
||||
+ DWORD i, j;
|
||||
+
|
||||
+ for (i = 0; i < chain->context.cChain && i < alternate->context.cChain; i++)
|
||||
+ {
|
||||
+ const CERT_SIMPLE_CHAIN *a = chain->context.rgpChain[i];
|
||||
+ const CERT_SIMPLE_CHAIN *b = alternate->context.rgpChain[i];
|
||||
+
|
||||
+ for (j = 0; j < a->cElement && j < b->cElement; j++)
|
||||
+ {
|
||||
+ PCCERT_CONTEXT ca = a->rgpElement[j]->pCertContext;
|
||||
+ PCCERT_CONTEXT cb = b->rgpElement[j]->pCertContext;
|
||||
+
|
||||
+ if (!CertCompareCertificate(X509_ASN_ENCODING, ca->pCertInfo,
|
||||
+ cb->pCertInfo))
|
||||
+ return CompareFileTime(&cb->pCertInfo->NotBefore,
|
||||
+ &ca->pCertInfo->NotBefore) > 0;
|
||||
+ }
|
||||
+ }
|
||||
+ return FALSE;
|
||||
+}
|
||||
+
|
||||
/* Chooses the highest quality chain among chain and its "lower quality"
|
||||
* alternate chains. Returns the highest quality chain, with all other
|
||||
* chains as lower quality chains of it.
|
||||
@@ -2598,7 +2628,12 @@
|
||||
CertificateChain *alternate =
|
||||
(CertificateChain*)chain->context.rgpLowerQualityChainContext[i];
|
||||
|
||||
- if (CRYPT_ChainQuality(alternate) > CRYPT_ChainQuality(chain))
|
||||
+ DWORD alternateQuality = CRYPT_ChainQuality(alternate);
|
||||
+ DWORD chainQuality = CRYPT_ChainQuality(chain);
|
||||
+
|
||||
+ if (alternateQuality > chainQuality ||
|
||||
+ (alternateQuality == chainQuality &&
|
||||
+ CRYPT_AlternateHasNewerIssuer(chain, alternate)))
|
||||
{
|
||||
alternate->context.cLowerQualityChainContext =
|
||||
chain->context.cLowerQualityChainContext;
|
||||
@@ -0,0 +1,70 @@
|
||||
diff --git a/dlls/windows.devices.enumeration/main.c b/dlls/windows.devices.enumeration/main.c
|
||||
--- a/dlls/windows.devices.enumeration/main.c
|
||||
+++ b/dlls/windows.devices.enumeration/main.c
|
||||
@@ -118,6 +118,8 @@
|
||||
struct weak_reference_source weak_reference_source;
|
||||
|
||||
struct list added_handlers;
|
||||
+ struct list updated_handlers;
|
||||
+ struct list removed_handlers;
|
||||
struct list enumerated_handlers;
|
||||
struct list stopped_handlers;
|
||||
IUnknown *query_params;
|
||||
@@ -177,6 +179,8 @@
|
||||
if (!ref)
|
||||
{
|
||||
typed_event_handlers_clear( &impl->added_handlers );
|
||||
+ typed_event_handlers_clear( &impl->updated_handlers );
|
||||
+ typed_event_handlers_clear( &impl->removed_handlers );
|
||||
typed_event_handlers_clear( &impl->enumerated_handlers );
|
||||
typed_event_handlers_clear( &impl->stopped_handlers );
|
||||
IUnknown_Release( impl->query_params );
|
||||
@@ -225,27 +229,31 @@
|
||||
static HRESULT WINAPI device_watcher_add_Updated( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_DeviceInformationUpdate *handler,
|
||||
EventRegistrationToken *token )
|
||||
{
|
||||
- FIXME( "iface %p, handler %p, token %p stub!\n", iface, handler, token );
|
||||
- return S_OK;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, handler %p, token %p\n", iface, handler, token );
|
||||
+ return typed_event_handlers_append( &impl->updated_handlers, (ITypedEventHandler_IInspectable_IInspectable *)handler, token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_remove_Updated( IDeviceWatcher *iface, EventRegistrationToken token )
|
||||
{
|
||||
- FIXME( "iface %p, token %#I64x stub!\n", iface, token.value );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, token %#I64x.\n", iface, token.value );
|
||||
+ return typed_event_handlers_remove( &impl->updated_handlers, &token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_add_Removed( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_DeviceInformationUpdate *handler,
|
||||
EventRegistrationToken *token )
|
||||
{
|
||||
- FIXME( "iface %p, handler %p, token %p stub!\n", iface, handler, token );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, handler %p, token %p\n", iface, handler, token );
|
||||
+ return typed_event_handlers_append( &impl->removed_handlers, (ITypedEventHandler_IInspectable_IInspectable *)handler, token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_remove_Removed( IDeviceWatcher *iface, EventRegistrationToken token )
|
||||
{
|
||||
- FIXME( "iface %p, token %#I64x stub!\n", iface, token.value );
|
||||
- return E_NOTIMPL;
|
||||
+ struct device_watcher *impl = impl_from_IDeviceWatcher( iface );
|
||||
+ TRACE( "iface %p, token %#I64x.\n", iface, token.value );
|
||||
+ return typed_event_handlers_remove( &impl->removed_handlers, &token );
|
||||
}
|
||||
|
||||
static HRESULT WINAPI device_watcher_add_EnumerationCompleted( IDeviceWatcher *iface, ITypedEventHandler_DeviceWatcher_IInspectable *handler,
|
||||
@@ -592,6 +600,8 @@
|
||||
if (FAILED(hr = devquery_params_create( type, expr, prop_keys, prop_keys_len, &impl->query_params ))) goto failed;
|
||||
|
||||
list_init( &impl->added_handlers );
|
||||
+ list_init( &impl->updated_handlers );
|
||||
+ list_init( &impl->removed_handlers );
|
||||
list_init( &impl->enumerated_handlers );
|
||||
list_init( &impl->stopped_handlers );
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
diff --git a/dlls/dnsapi/dnsapi.spec b/dlls/dnsapi/dnsapi.spec
|
||||
--- a/dlls/dnsapi/dnsapi.spec
|
||||
+++ b/dlls/dnsapi/dnsapi.spec
|
||||
@@ -104,12 +104,20 @@
|
||||
@ stdcall DnsReplaceRecordSetW(ptr long ptr ptr ptr)
|
||||
@ stub DnsReplaceRecordSet_W
|
||||
@ stdcall DnsServiceBrowse(ptr ptr)
|
||||
+@ stdcall DnsServiceBrowseCancel(ptr)
|
||||
+@ stdcall DnsServiceConstructInstance(wstr wstr ptr ptr long long long long ptr ptr)
|
||||
+@ stdcall DnsServiceDeRegister(ptr ptr)
|
||||
+@ stdcall DnsServiceFreeInstance(ptr)
|
||||
@ stub DnsServiceNotificationDeregister_A
|
||||
@ stub DnsServiceNotificationDeregister_UTF8
|
||||
@ stub DnsServiceNotificationDeregister_W
|
||||
@ stub DnsServiceNotificationRegister_A
|
||||
@ stub DnsServiceNotificationRegister_UTF8
|
||||
@ stub DnsServiceNotificationRegister_W
|
||||
+@ stdcall DnsServiceRegister(ptr ptr)
|
||||
+@ stdcall DnsServiceRegisterCancel(ptr)
|
||||
+@ stdcall DnsServiceResolve(ptr ptr)
|
||||
+@ stdcall DnsServiceResolveCancel(ptr)
|
||||
@ stub DnsSetMaxNumberOfAddressesToRegister
|
||||
@ stub DnsStatusString
|
||||
@ stub DnsStringCopyAllocateEx
|
||||
diff --git a/dlls/dnsapi/main.c b/dlls/dnsapi/main.c
|
||||
--- a/dlls/dnsapi/main.c
|
||||
+++ b/dlls/dnsapi/main.c
|
||||
@@ -243,3 +243,135 @@
|
||||
FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceConstructInstance [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_SERVICE_INSTANCE * WINAPI DnsServiceConstructInstance( const WCHAR *name, const WCHAR *host,
|
||||
+ IP4_ADDRESS *ip4, IP6_ADDRESS *ip6, WORD port, WORD priority, WORD weight,
|
||||
+ DWORD count, const WCHAR **keys, const WCHAR **values )
|
||||
+{
|
||||
+ DNS_SERVICE_INSTANCE *instance;
|
||||
+ DWORD i;
|
||||
+
|
||||
+ TRACE( "(%s, %s, %p, %p, %u, %u, %u, %lu, %p, %p)\n", debugstr_w(name), debugstr_w(host),
|
||||
+ ip4, ip6, port, priority, weight, count, keys, values );
|
||||
+
|
||||
+ if (!(instance = calloc( 1, sizeof(*instance) ))) return NULL;
|
||||
+
|
||||
+ if (name) instance->pszInstanceName = wcsdup( name );
|
||||
+ if (host) instance->pszHostName = wcsdup( host );
|
||||
+ if (ip4 && (instance->ip4Address = malloc( sizeof(*ip4) ))) *instance->ip4Address = *ip4;
|
||||
+ if (ip6 && (instance->ip6Address = malloc( sizeof(*ip6) ))) *instance->ip6Address = *ip6;
|
||||
+ instance->wPort = port;
|
||||
+ instance->wPriority = priority;
|
||||
+ instance->wWeight = weight;
|
||||
+ instance->dwPropertyCount = count;
|
||||
+
|
||||
+ if (count && (instance->keys = calloc( count, sizeof(*keys) )))
|
||||
+ for (i = 0; i < count; i++) instance->keys[i] = wcsdup( keys[i] );
|
||||
+ if (count && (instance->values = calloc( count, sizeof(*values) )))
|
||||
+ for (i = 0; i < count; i++) instance->values[i] = wcsdup( values[i] );
|
||||
+
|
||||
+ return instance;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceFreeInstance [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+void WINAPI DnsServiceFreeInstance( DNS_SERVICE_INSTANCE *instance )
|
||||
+{
|
||||
+ DWORD i;
|
||||
+
|
||||
+ TRACE( "(%p)\n", instance );
|
||||
+
|
||||
+ if (!instance) return;
|
||||
+
|
||||
+ free( instance->pszInstanceName );
|
||||
+ free( instance->pszHostName );
|
||||
+ free( instance->ip4Address );
|
||||
+ free( instance->ip6Address );
|
||||
+ for (i = 0; i < instance->dwPropertyCount; i++)
|
||||
+ {
|
||||
+ if (instance->keys) free( instance->keys[i] );
|
||||
+ if (instance->values) free( instance->values[i] );
|
||||
+ }
|
||||
+ free( instance->keys );
|
||||
+ free( instance->values );
|
||||
+ free( instance );
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceRegister [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceRegister( DNS_SERVICE_REGISTER_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ /* Registration needs mDNS support, which is not implemented. Fail
|
||||
+ * synchronously: the completion callback must only run when this returns
|
||||
+ * DNS_REQUEST_PENDING, and calling it inline would re-enter the caller
|
||||
+ * while it still believes the operation is outstanding. */
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceDeRegister [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceDeRegister( DNS_SERVICE_REGISTER_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceRegisterCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceRegisterCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceResolve [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceResolve( DNS_SERVICE_RESOLVE_REQUEST *request, DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p, %p) stub\n", request, cancel );
|
||||
+
|
||||
+ if (!request) return ERROR_INVALID_PARAMETER;
|
||||
+
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceResolveCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceResolveCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
+
|
||||
+/******************************************************************************
|
||||
+ * DnsServiceBrowseCancel [DNSAPI.@]
|
||||
+ *
|
||||
+ */
|
||||
+DNS_STATUS WINAPI DnsServiceBrowseCancel( DNS_SERVICE_CANCEL *cancel )
|
||||
+{
|
||||
+ FIXME( "(%p) stub\n", cancel );
|
||||
+ return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
+}
|
||||
diff --git a/include/windns.h b/include/windns.h
|
||||
--- a/include/windns.h
|
||||
+++ b/include/windns.h
|
||||
@@ -106,7 +106,8 @@
|
||||
#define DNS_QUERY_APPEND_MULTILABEL 0x00800000
|
||||
#define DNS_QUERY_DNSSEC_OK 0x01000000
|
||||
#define DNS_QUERY_DNSSEC_CHECKING_DISABLED 0x02000000
|
||||
-#define DNS_QUERY_RESERVED 0xff000000
|
||||
+#define DNS_QUERY_DNSSEC_REQUIRED 0x04000000
|
||||
+#define DNS_QUERY_RESERVED 0xf0000000
|
||||
|
||||
#define INLINE_WORD_FLIP(out, in) { WORD _in = (in); (out) = (_in << 8) | (_in >> 8); }
|
||||
#define INLINE_HTONS(out, in) INLINE_WORD_FLIP(out, in)
|
||||
@@ -781,6 +782,73 @@
|
||||
void *reserved;
|
||||
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
|
||||
|
||||
+typedef struct _DNS_SERVICE_INSTANCE
|
||||
+{
|
||||
+ LPWSTR pszInstanceName;
|
||||
+ LPWSTR pszHostName;
|
||||
+ IP4_ADDRESS *ip4Address;
|
||||
+ IP6_ADDRESS *ip6Address;
|
||||
+ WORD wPort;
|
||||
+ WORD wPriority;
|
||||
+ WORD wWeight;
|
||||
+ DWORD dwPropertyCount;
|
||||
+ PWSTR *keys;
|
||||
+ PWSTR *values;
|
||||
+ DWORD dwInterfaceIndex;
|
||||
+} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
|
||||
+
|
||||
+typedef void WINAPI DNS_SERVICE_REGISTER_COMPLETE(DWORD, void *, PDNS_SERVICE_INSTANCE);
|
||||
+typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
|
||||
+
|
||||
+typedef struct _DNS_SERVICE_REGISTER_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ PDNS_SERVICE_INSTANCE pServiceInstance;
|
||||
+ PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+ HANDLE hCredentials;
|
||||
+ BOOL unicastEnabled;
|
||||
+} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
|
||||
+
|
||||
+typedef void WINAPI DNS_SERVICE_RESOLVE_COMPLETE(DWORD, void *, PDNS_SERVICE_INSTANCE);
|
||||
+typedef DNS_SERVICE_RESOLVE_COMPLETE *PDNS_SERVICE_RESOLVE_COMPLETE;
|
||||
+
|
||||
+typedef struct _DNS_SERVICE_RESOLVE_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ LPWSTR QueryName;
|
||||
+ PDNS_SERVICE_RESOLVE_COMPLETE pResolveCompletionCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+} DNS_SERVICE_RESOLVE_REQUEST, *PDNS_SERVICE_RESOLVE_REQUEST;
|
||||
+
|
||||
+typedef struct _MDNS_QUERY_HANDLE
|
||||
+{
|
||||
+ WCHAR nameBuf[DNS_MAX_NAME_BUFFER_LENGTH];
|
||||
+ WORD wType;
|
||||
+ PVOID pSubscription;
|
||||
+ PVOID pWnfCallbackParams;
|
||||
+ ULONG stateNameData[2];
|
||||
+} MDNS_QUERY_HANDLE, *PMDNS_QUERY_HANDLE;
|
||||
+
|
||||
+typedef void WINAPI MDNS_QUERY_CALLBACK(PVOID,PMDNS_QUERY_HANDLE,PDNS_QUERY_RESULT);
|
||||
+typedef MDNS_QUERY_CALLBACK *PMDNS_QUERY_CALLBACK;
|
||||
+
|
||||
+typedef struct _MDNS_QUERY_REQUEST
|
||||
+{
|
||||
+ ULONG Version;
|
||||
+ ULONG ulRefCount;
|
||||
+ PCWSTR Query;
|
||||
+ WORD QueryType;
|
||||
+ ULONG64 QueryOptions;
|
||||
+ ULONG InterfaceIndex;
|
||||
+ PMDNS_QUERY_CALLBACK pQueryCallback;
|
||||
+ PVOID pQueryContext;
|
||||
+ BOOL fAnswerReceived;
|
||||
+ ULONG ulResendCount;
|
||||
+} MDNS_QUERY_REQUEST, *PMDNS_QUERY_REQUEST;
|
||||
+
|
||||
DNS_STATUS WINAPI DnsAcquireContextHandle_A(DWORD,PVOID,PHANDLE);
|
||||
DNS_STATUS WINAPI DnsAcquireContextHandle_W(DWORD,PVOID,PHANDLE);
|
||||
#define DnsAcquireContextHandle WINELIB_NAME_AW(DnsAcquireContextHandle_)
|
||||
@@ -800,6 +868,7 @@
|
||||
#define DnsQuery WINELIB_NAME_AW(DnsQuery_)
|
||||
DNS_STATUS WINAPI DnsQueryEx(DNS_QUERY_REQUEST*,DNS_QUERY_RESULT*,DNS_QUERY_CANCEL*);
|
||||
DNS_STATUS WINAPI DnsCancelQuery(DNS_QUERY_CANCEL*);
|
||||
+BOOL WINAPI DnsGetCacheDataTable(PDNS_CACHE_ENTRY*);
|
||||
DNS_STATUS WINAPI DnsQueryConfig(DNS_CONFIG_TYPE,DWORD,PCWSTR,PVOID,PVOID,PDWORD);
|
||||
BOOL WINAPI DnsRecordCompare(PDNS_RECORD,PDNS_RECORD);
|
||||
PDNS_RECORD WINAPI DnsRecordCopyEx(PDNS_RECORD,DNS_CHARSET,DNS_CHARSET);
|
||||
@@ -813,13 +882,22 @@
|
||||
DNS_STATUS WINAPI DnsReplaceRecordSetUTF8(PDNS_RECORDA,DWORD,HANDLE,PVOID,PVOID);
|
||||
#define DnsReplaceRecordSet WINELIB_NAME_AW(DnsReplaceRecordSet)
|
||||
DNS_STATUS WINAPI DnsServiceBrowse(PDNS_SERVICE_BROWSE_REQUEST, PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceBrowseCancel(PDNS_SERVICE_CANCEL);
|
||||
+PDNS_SERVICE_INSTANCE WINAPI DnsServiceConstructInstance(PCWSTR,PCWSTR,PIP4_ADDRESS,PIP6_ADDRESS,WORD,WORD,WORD,DWORD,PCWSTR*,PCWSTR*);
|
||||
+DNS_STATUS WINAPI DnsServiceDeRegister(PDNS_SERVICE_REGISTER_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+VOID WINAPI DnsServiceFreeInstance(PDNS_SERVICE_INSTANCE);
|
||||
+DNS_STATUS WINAPI DnsServiceRegister(PDNS_SERVICE_REGISTER_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceRegisterCancel(PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceResolve(PDNS_SERVICE_RESOLVE_REQUEST,PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsServiceResolveCancel(PDNS_SERVICE_CANCEL);
|
||||
+DNS_STATUS WINAPI DnsStartMulticastQuery(PMDNS_QUERY_REQUEST,PMDNS_QUERY_HANDLE);
|
||||
+DNS_STATUS WINAPI DnsStopMulticastQuery(PMDNS_QUERY_HANDLE);
|
||||
DNS_STATUS WINAPI DnsValidateName_A(PCSTR,DNS_NAME_FORMAT);
|
||||
DNS_STATUS WINAPI DnsValidateName_W(PCWSTR, DNS_NAME_FORMAT);
|
||||
DNS_STATUS WINAPI DnsValidateName_UTF8(PCSTR,DNS_NAME_FORMAT);
|
||||
#define DnsValidateName WINELIB_NAME_AW(DnsValidateName_)
|
||||
BOOL WINAPI DnsWriteQuestionToBuffer_W(PDNS_MESSAGE_BUFFER,PDWORD,PCWSTR,WORD,WORD,BOOL);
|
||||
BOOL WINAPI DnsWriteQuestionToBuffer_UTF8(PDNS_MESSAGE_BUFFER,PDWORD,PCSTR,WORD,WORD,BOOL);
|
||||
-BOOL WINAPI DnsGetCacheDataTable(PDNS_CACHE_ENTRY*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
diff --git a/dlls/sechost/trace.c b/dlls/sechost/trace.c
|
||||
--- a/dlls/sechost/trace.c
|
||||
+++ b/dlls/sechost/trace.c
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "evntrace.h"
|
||||
|
||||
#include "wine/debug.h"
|
||||
+#include "wine/exception.h"
|
||||
|
||||
WINE_DEFAULT_DEBUG_CHANNEL(eventlog);
|
||||
|
||||
@@ -116,14 +117,63 @@
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
+/* A real-time consumer of a session. No ETW providers exist under Wine, so the
|
||||
+ * consumer never receives events: ProcessTrace() waits until CloseTrace(). */
|
||||
+#define REALTIME_CONSUMER_MAGIC 0x52544345 /* "ECTR" */
|
||||
+#ifndef PROCESS_TRACE_MODE_REAL_TIME
|
||||
+#define PROCESS_TRACE_MODE_REAL_TIME 0x00000100 /* shares LogFileMode in Wine's header */
|
||||
+#endif
|
||||
+
|
||||
+struct realtime_consumer
|
||||
+{
|
||||
+ DWORD magic;
|
||||
+ HANDLE closed;
|
||||
+};
|
||||
+
|
||||
+static struct realtime_consumer *get_realtime_consumer( TRACEHANDLE handle )
|
||||
+{
|
||||
+ struct realtime_consumer *consumer = (struct realtime_consumer *)(ULONG_PTR)handle;
|
||||
+
|
||||
+ if (!handle || handle == INVALID_PROCESSTRACE_HANDLE) return NULL;
|
||||
+ __TRY
|
||||
+ {
|
||||
+ if (consumer->magic != REALTIME_CONSUMER_MAGIC) consumer = NULL;
|
||||
+ }
|
||||
+ __EXCEPT_PAGE_FAULT
|
||||
+ {
|
||||
+ consumer = NULL;
|
||||
+ }
|
||||
+ __ENDTRY
|
||||
+ return consumer;
|
||||
+}
|
||||
+
|
||||
/******************************************************************************
|
||||
* OpenTraceW (sechost.@)
|
||||
*/
|
||||
TRACEHANDLE WINAPI OpenTraceW( EVENT_TRACE_LOGFILEW *logfile )
|
||||
{
|
||||
static int once;
|
||||
+ struct realtime_consumer *consumer;
|
||||
|
||||
- if (!once++) FIXME("%p: stub\n", logfile);
|
||||
+ /* a session name without a log file can only be consumed in real time */
|
||||
+ if (logfile && logfile->LoggerName &&
|
||||
+ ((logfile->LogFileMode & PROCESS_TRACE_MODE_REAL_TIME) || !logfile->LogFileName))
|
||||
+ {
|
||||
+ FIXME("%p (%s): real-time consumer without events\n", logfile, debugstr_w(logfile->LoggerName));
|
||||
+ if (!(consumer = HeapAlloc( GetProcessHeap(), 0, sizeof(*consumer) )))
|
||||
+ {
|
||||
+ SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
+ return INVALID_PROCESSTRACE_HANDLE;
|
||||
+ }
|
||||
+ consumer->magic = REALTIME_CONSUMER_MAGIC;
|
||||
+ consumer->closed = CreateEventW( NULL, TRUE, FALSE, NULL );
|
||||
+ return (TRACEHANDLE)(ULONG_PTR)consumer;
|
||||
+ }
|
||||
+
|
||||
+ if (!once++) FIXME("%p: stub (logfile %s, logger %s, mode %#lx)\n", logfile,
|
||||
+ logfile ? debugstr_w(logfile->LogFileName) : "-",
|
||||
+ logfile ? debugstr_w(logfile->LoggerName) : "-",
|
||||
+ logfile ? logfile->LogFileMode : 0);
|
||||
SetLastError(ERROR_ACCESS_DENIED);
|
||||
return INVALID_PROCESSTRACE_HANDLE;
|
||||
}
|
||||
@@ -133,6 +183,25 @@
|
||||
*/
|
||||
ULONG WINAPI ProcessTrace( TRACEHANDLE *handles, ULONG count, FILETIME *start_time, FILETIME *end_time )
|
||||
{
|
||||
+ HANDLE events[MAXIMUM_WAIT_OBJECTS];
|
||||
+ ULONG i;
|
||||
+
|
||||
+ if (handles && count && count <= MAXIMUM_WAIT_OBJECTS)
|
||||
+ {
|
||||
+ for (i = 0; i < count; i++)
|
||||
+ {
|
||||
+ struct realtime_consumer *consumer = get_realtime_consumer( handles[i] );
|
||||
+ if (!consumer) break;
|
||||
+ events[i] = consumer->closed;
|
||||
+ }
|
||||
+ if (i == count)
|
||||
+ {
|
||||
+ TRACE("%p %lu: waiting for CloseTrace\n", handles, count);
|
||||
+ WaitForMultipleObjects( count, events, TRUE, INFINITE );
|
||||
+ return ERROR_SUCCESS;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
FIXME("%p %lu %p %p: stub\n", handles, count, start_time, end_time);
|
||||
return ERROR_CALL_NOT_IMPLEMENTED;
|
||||
}
|
||||
@@ -142,6 +211,15 @@
|
||||
*/
|
||||
ULONG WINAPI CloseTrace( TRACEHANDLE handle )
|
||||
{
|
||||
+ struct realtime_consumer *consumer = get_realtime_consumer( handle );
|
||||
+
|
||||
+ if (consumer)
|
||||
+ {
|
||||
+ /* the handle stays allocated: ProcessTrace() may still be waiting on it */
|
||||
+ SetEvent( consumer->closed );
|
||||
+ return ERROR_SUCCESS;
|
||||
+ }
|
||||
+
|
||||
FIXME("%s: stub\n", wine_dbgstr_longlong(handle));
|
||||
return ERROR_INVALID_HANDLE;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Deliver a protocol callback to the RUNNING Meta client; never start a second Client.exe.
|
||||
* UTF-8 stdin is preferred (no credentials in process argv). argv[1] is useful for non-secret device probes.
|
||||
* Uses a NUL-terminated UTF-8 link. Device verification of the Oculus pipe's framing is still required.
|
||||
*/
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char link[65537];
|
||||
size_t len;
|
||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||
DWORD written;
|
||||
if (argc == 2) {
|
||||
len = strlen(argv[1]);
|
||||
if (len >= sizeof(link)) return 2;
|
||||
memcpy(link, argv[1], len);
|
||||
} else if (argc == 1) {
|
||||
len = fread(link, 1, sizeof(link), stdin);
|
||||
if (ferror(stdin) || len >= sizeof(link)) return 2;
|
||||
} else return 2;
|
||||
if (len < 24 || strncmp(link, "oculus://link/frl_login?", 24) || memchr(link, 0, len)) return 2;
|
||||
link[len] = 0;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
pipe = CreateFileW(L"\\\\.\\pipe\\oculus", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (pipe != INVALID_HANDLE_VALUE) break;
|
||||
DWORD err = GetLastError();
|
||||
if (err != ERROR_PIPE_BUSY && err != ERROR_FILE_NOT_FOUND) return 3;
|
||||
if (err == ERROR_PIPE_BUSY) WaitNamedPipeW(L"\\\\.\\pipe\\oculus", 200);
|
||||
else Sleep(200);
|
||||
}
|
||||
if (pipe == INVALID_HANDLE_VALUE) return 3;
|
||||
BOOL ok = WriteFile(pipe, link, (DWORD)len + 1, &written, NULL);
|
||||
CloseHandle(pipe);
|
||||
SecureZeroMemory(link, sizeof(link));
|
||||
return ok && written == len + 1 ? 0 : 4;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Reproduce the exact Wine source of a GE-Proton release (Wine submodule + wine-staging + GE's own patches): the WINE
|
||||
# section of GE's patches/protonprep-valve-staging.sh, without the DXVK/vkd3d/protonfixes steps.
|
||||
# usage: prep_ge_wine.sh <work dir> [tag=GE-Proton11-7] → <work dir>/src/wine
|
||||
# Needs git, patch, python3, autoconf/autoreconf (the WINE section ends with autoreconf + tools/make_requests).
|
||||
set -e
|
||||
W=$1; TAG=${2:-GE-Proton11-7}
|
||||
[ -n "$W" ] || { echo "usage: $0 <work dir> [tag]"; exit 2; }
|
||||
mkdir -p "$W" && cd "$W"
|
||||
[ -d src ] || git clone -q --depth 1 --branch "$TAG" https://github.com/GloriousEggroll/proton-ge-custom src
|
||||
cd src
|
||||
git submodule update --init --depth 1 wine wine-staging
|
||||
P=patches/protonprep-valve-staging.sh
|
||||
start=$(grep -n "^### (2) WINE PATCHING ###" $P | cut -d: -f1)
|
||||
end=$(grep -n "^### END WINE PATCHING ###" $P | cut -d: -f1)
|
||||
fn_end=$(grep -n "^### (1) PREP SECTION ###" $P | cut -d: -f1)
|
||||
[ -n "$start" ] && [ -n "$end" ] && [ -n "$fn_end" ] || { echo "unexpected layout of $P"; exit 1; }
|
||||
# the WINE section reverts commits from the Wine history: fetch them into the shallow clone first
|
||||
for c in $(sed -n "${start},${end}p" $P | grep -oE "git revert [^#]*" | grep -oE "[0-9a-f]{40}"); do
|
||||
git -C wine fetch -q --depth 2 origin "$c"
|
||||
done
|
||||
{ sed -n "1,$((fn_end - 1))p" $P; sed -n "${start},${end}p" $P; } > ../wine_only.sh
|
||||
bash ../wine_only.sh > ../prep.log 2>&1
|
||||
grep -n "SERVER_PROTOCOL_VERSION" wine/include/wine/server_protocol.h
|
||||
if find wine -name "*.rej" | grep -q .; then echo "rejected hunks, see ../prep.log"; exit 1; fi
|
||||
echo "patched Wine tree: $W/src/wine"
|
||||
@@ -0,0 +1,79 @@
|
||||
/* windows.devices.wifi.dll stand-in for Wine: activation factory of Windows.Devices.WiFi.WiFiAdapter with the
|
||||
* IWiFiAdapterStatics methods. Wine has no Wi-Fi WinRT class; Meta's runtime (OculusAppFramework, Air Link "Ari"
|
||||
* dongle code) calls WiFiAdapter.GetDeviceSelector() after login, enumerates devices with it and only calls
|
||||
* FromIdAsync for devices found. Without the class, C++/WinRT throws and the runtime fail-fasts (c0000409).
|
||||
* GetDeviceSelector returns a selector that matches no device; the async methods report E_NOTIMPL (never reached
|
||||
* when nothing is found). Unknown interface requests are logged to stderr.
|
||||
*/
|
||||
#define COBJMACROS
|
||||
#include <windows.h>
|
||||
#include <winstring.h>
|
||||
#include <stdio.h>
|
||||
|
||||
typedef struct IInspectableVtbl_ { void *s[6]; } dummy_t;
|
||||
|
||||
static const GUID IID_IUnknown_ = {0x00000000,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
|
||||
static const GUID IID_IInspectable_ = {0xaf86e2e0,0xb12d,0x4c6a,{0x9c,0x5a,0xd7,0xaa,0x65,0x10,0x1e,0x90}};
|
||||
static const GUID IID_IAgileObject_ = {0x94ea2b94,0xe9cc,0x49e0,{0xc0,0xff,0xee,0x64,0xca,0x8f,0x5b,0x90}};
|
||||
static const GUID IID_IActivationFactory_ = {0x00000035,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
|
||||
/* Windows.Devices.WiFi.IWiFiAdapterStatics */
|
||||
static const GUID IID_IWiFiAdapterStatics = {0xda25fddd,0xd24c,0x43e3,{0xaa,0xbd,0xc4,0x65,0x9f,0x73,0x0f,0x99}};
|
||||
|
||||
typedef struct statics { const void **vtbl; } statics;
|
||||
|
||||
static HRESULT WINAPI f_QueryInterface(statics *This, REFIID iid, void **out)
|
||||
{
|
||||
if (IsEqualGUID(iid, &IID_IUnknown_) || IsEqualGUID(iid, &IID_IInspectable_) || IsEqualGUID(iid, &IID_IAgileObject_)
|
||||
|| IsEqualGUID(iid, &IID_IActivationFactory_) || IsEqualGUID(iid, &IID_IWiFiAdapterStatics))
|
||||
{
|
||||
*out = This;
|
||||
return S_OK;
|
||||
}
|
||||
fprintf(stderr, "wifi_stub: unknown interface {%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}\n", iid->Data1,
|
||||
iid->Data2, iid->Data3, iid->Data4[0], iid->Data4[1], iid->Data4[2], iid->Data4[3], iid->Data4[4],
|
||||
iid->Data4[5], iid->Data4[6], iid->Data4[7]);
|
||||
*out = NULL;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
static ULONG WINAPI f_AddRef(statics *This) { return 2; }
|
||||
static ULONG WINAPI f_Release(statics *This) { return 1; }
|
||||
static HRESULT WINAPI f_GetIids(statics *This, ULONG *count, IID **iids)
|
||||
{
|
||||
IID *p = CoTaskMemAlloc(sizeof(IID));
|
||||
if (!p) return E_OUTOFMEMORY;
|
||||
*p = IID_IWiFiAdapterStatics; *count = 1; *iids = p;
|
||||
return S_OK;
|
||||
}
|
||||
static HRESULT WINAPI f_GetRuntimeClassName(statics *This, HSTRING *name)
|
||||
{
|
||||
static const WCHAR n[] = L"Windows.Devices.WiFi.WiFiAdapter";
|
||||
return WindowsCreateString(n, ARRAYSIZE(n) - 1, name);
|
||||
}
|
||||
static HRESULT WINAPI f_GetTrustLevel(statics *This, int *level) { *level = 0; return S_OK; }
|
||||
/* slot 6: IWiFiAdapterStatics::FindAllAdaptersAsync (and IActivationFactory::ActivateInstance: not activatable) */
|
||||
static HRESULT WINAPI f_FindAllAdaptersAsync(statics *This, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
static HRESULT WINAPI f_GetDeviceSelector(statics *This, HSTRING *value)
|
||||
{
|
||||
/* an interface class no device has: device enumeration with it finds nothing */
|
||||
static const WCHAR sel[] = L"System.Devices.InterfaceClassGuid:=\"{f0e2c0a5-7b3e-4f61-9b47-3d1c0f0e2a11}\" AND "
|
||||
L"System.Devices.InterfaceEnabled:=System.StructuredQueryType.Boolean#True";
|
||||
fprintf(stderr, "wifi_stub: GetDeviceSelector\n");
|
||||
return WindowsCreateString(sel, ARRAYSIZE(sel) - 1, value);
|
||||
}
|
||||
static HRESULT WINAPI f_FromIdAsync(statics *This, HSTRING id, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
static HRESULT WINAPI f_RequestAccessAsync(statics *This, void **op) { if (op) *op = NULL; return E_NOTIMPL; }
|
||||
|
||||
static const void *vtbl[] = { f_QueryInterface, f_AddRef, f_Release, f_GetIids, f_GetRuntimeClassName,
|
||||
f_GetTrustLevel, f_FindAllAdaptersAsync, f_GetDeviceSelector, f_FromIdAsync,
|
||||
f_RequestAccessAsync };
|
||||
static statics factory = { vtbl };
|
||||
|
||||
HRESULT WINAPI DllGetActivationFactory(HSTRING classid, void **out)
|
||||
{
|
||||
const WCHAR *s = WindowsGetStringRawBuffer(classid, NULL);
|
||||
fprintf(stderr, "wifi_stub: DllGetActivationFactory %ls\n", s ? s : L"(null)");
|
||||
if (!s || wcscmp(s, L"Windows.Devices.WiFi.WiFiAdapter")) { *out = NULL; return CLASS_E_CLASSNOTAVAILABLE; }
|
||||
*out = &factory;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WINAPI DllCanUnloadNow(void) { return S_FALSE; }
|
||||
@@ -0,0 +1,4 @@
|
||||
LIBRARY windows.devices.wifi.dll
|
||||
EXPORTS
|
||||
DllGetActivationFactory
|
||||
DllCanUnloadNow PRIVATE
|
||||
+5
-72
@@ -35,7 +35,6 @@ static const GLubyte *(*real_glGetStringi)(GLenum, GLuint);
|
||||
static void (*real_glGetIntegerv)(GLenum, GLint *);
|
||||
static __eglMustCastToProperFunctionPointerType (*real_eglGetProcAddress)(const char *);
|
||||
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
||||
static int tq_used; // GPU timer-query stand-ins handed out (below): GL_GPU_DISJOINT_EXT reads answer 0
|
||||
|
||||
static int hide_multiview = -1; // -1: not set (Unity and Unreal keep multiview, everything else hides it)
|
||||
static int hide_msrtt = 1;
|
||||
@@ -629,21 +628,6 @@ static int is_hidden(const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// GL_EXT_disjoint_timer_query listed although the driver lacks it: GLAD loaders only look its functions up when the
|
||||
// extension is listed, and some engines call them unconditionally (Path of the Warrior on AXRB's emulator, whose
|
||||
// gfxstream eglGetProcAddress still returns pointers for the names); the lookups then get the stand-ins below
|
||||
// eglGetProcAddress. Decided once a context answers GL_NUM_EXTENSIONS, by whether the driver lists the extension.
|
||||
static const char TIMER_QUERY_EXT[] = "GL_EXT_disjoint_timer_query";
|
||||
static int added_timer_query = -1;
|
||||
static int add_timer_query(void) {
|
||||
if (added_timer_query < 0 && real_glGetIntegerv && real_glGetStringi) {
|
||||
GLint total = 0;
|
||||
real_glGetIntegerv(GL_NUM_EXTENSIONS, &total);
|
||||
if (total > 0) added_timer_query = !ext_supported(TIMER_QUERY_EXT);
|
||||
}
|
||||
return added_timer_query > 0;
|
||||
}
|
||||
|
||||
// Filtered index table for glGetStringi (built lazily, per process).
|
||||
static GLuint *visible;
|
||||
static GLint visible_count = -1;
|
||||
@@ -657,17 +641,12 @@ static void build_table(void) {
|
||||
const char *name = (const char *)real_glGetStringi(GL_EXTENSIONS, (GLuint)i);
|
||||
if (name && !is_hidden(name)) visible[visible_count++] = (GLuint)i;
|
||||
}
|
||||
LOG("extensions: %d of %d visible to the game%s", visible_count, total,
|
||||
add_timer_query() ? " (+ GL_EXT_disjoint_timer_query, stand-ins)" : "");
|
||||
LOG("extensions: %d of %d visible to the game", visible_count, total);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default"))) void glGetIntegerv(GLenum pname, GLint *data) {
|
||||
pthread_once(&once, init);
|
||||
if (pname == GL_NUM_EXTENSIONS && data) {
|
||||
build_table();
|
||||
if (visible_count >= 0) { *data = visible_count + add_timer_query(); return; }
|
||||
}
|
||||
if (pname == 0x8FBB /* GL_GPU_DISJOINT_EXT */ && tq_used && data) { *data = 0; return; }
|
||||
if (pname == GL_NUM_EXTENSIONS && data) { build_table(); if (visible_count >= 0) { *data = visible_count; return; } }
|
||||
if (real_glGetIntegerv) real_glGetIntegerv(pname, data);
|
||||
}
|
||||
|
||||
@@ -675,10 +654,7 @@ __attribute__((visibility("default"))) const GLubyte *glGetStringi(GLenum name,
|
||||
pthread_once(&once, init);
|
||||
if (name == GL_EXTENSIONS) {
|
||||
build_table();
|
||||
if (visible_count >= 0) {
|
||||
if (index < (GLuint)visible_count) return real_glGetStringi(name, visible[index]);
|
||||
return index == (GLuint)visible_count && add_timer_query() ? (const GLubyte *)TIMER_QUERY_EXT : NULL;
|
||||
}
|
||||
if (visible_count >= 0) return index < (GLuint)visible_count ? real_glGetStringi(name, visible[index]) : NULL;
|
||||
}
|
||||
return real_glGetStringi ? real_glGetStringi(name, index) : NULL;
|
||||
}
|
||||
@@ -689,12 +665,11 @@ __attribute__((visibility("default"))) const GLubyte *glGetString(GLenum name) {
|
||||
if (name != GL_EXTENSIONS || !s) return s;
|
||||
static char *filtered;
|
||||
if (!filtered) {
|
||||
filtered = calloc(strlen((const char *)s) + sizeof(TIMER_QUERY_EXT) + 2, 1);
|
||||
filtered = calloc(strlen((const char *)s) + 1, 1);
|
||||
char *copy = strdup((const char *)s), *save = NULL;
|
||||
for (char *tok = strtok_r(copy, " ", &save); tok; tok = strtok_r(NULL, " ", &save))
|
||||
if (!is_hidden(tok)) { strcat(filtered, tok); strcat(filtered, " "); }
|
||||
free(copy);
|
||||
if (add_timer_query()) { strcat(filtered, TIMER_QUERY_EXT); strcat(filtered, " "); }
|
||||
}
|
||||
return (const GLubyte *)filtered;
|
||||
}
|
||||
@@ -819,55 +794,13 @@ EXPORT void glDrawRangeElements(GLenum m, GLuint s, GLuint e, GLsizei c, GLenum
|
||||
if (d.DrawRangeElements) d.DrawRangeElements(m, s, e, c, t, i);
|
||||
}
|
||||
|
||||
// GPU timer queries (GL_EXT_disjoint_timer_query) the driver doesn't have: GLAD engines call the function pointers
|
||||
// whether or not the extension is listed (Path of the Warrior: glad_glGenQueriesEXT NULL -> SIGSEGV at pc 0 in
|
||||
// EgHalGrTimestampQueryGL on AXRB's emulator, whose gfxstream GLES has no timer queries; Quest and the Frame have
|
||||
// them, so the stand-ins are only handed out when the real lookup fails). Every query is at once available and
|
||||
// measured 0 ns.
|
||||
static GLuint tq_next = 0x7f000000;
|
||||
static void tq_gen(GLsizei n, GLuint *ids) { for (GLsizei i = 0; ids && i < n; i++) ids[i] = __atomic_add_fetch(&tq_next, 1, __ATOMIC_RELAXED); }
|
||||
static void tq_delete(GLsizei n, const GLuint *ids) { (void)n; (void)ids; }
|
||||
static GLboolean tq_is(GLuint id) { return id > 0x7f000000; }
|
||||
static void tq_begin(GLenum target, GLuint id) { (void)target; (void)id; }
|
||||
static void tq_end(GLenum target) { (void)target; }
|
||||
static void tq_counter(GLuint id, GLenum target) { (void)id; (void)target; }
|
||||
static void tq_getiv(GLenum target, GLenum pname, GLint *v) { (void)target; (void)pname; if (v) *v = 0; }
|
||||
#define TQ_RESULT(v, pname) if (v) *(v) = (pname) == 0x8867 /* GL_QUERY_RESULT_AVAILABLE */ ? 1 : 0
|
||||
static void tq_objiv(GLuint id, GLenum pname, GLint *v) { (void)id; TQ_RESULT(v, pname); }
|
||||
static void tq_objuiv(GLuint id, GLenum pname, GLuint *v) { (void)id; TQ_RESULT(v, pname); }
|
||||
static void tq_obji64v(GLuint id, GLenum pname, GLint64 *v) { (void)id; TQ_RESULT(v, pname); }
|
||||
static void tq_objui64v(GLuint id, GLenum pname, GLuint64 *v) { (void)id; TQ_RESULT(v, pname); }
|
||||
static void tq_int64v(GLenum pname, GLint64 *v) { (void)pname; if (v) *v = 0; }
|
||||
static const struct { const char *name; void *fn; } timer_query_standins[] = {
|
||||
{"glGenQueriesEXT", (void *)tq_gen}, {"glDeleteQueriesEXT", (void *)tq_delete}, {"glIsQueryEXT", (void *)tq_is},
|
||||
{"glBeginQueryEXT", (void *)tq_begin}, {"glEndQueryEXT", (void *)tq_end},
|
||||
{"glQueryCounterEXT", (void *)tq_counter}, {"glGetQueryivEXT", (void *)tq_getiv},
|
||||
{"glGetQueryObjectivEXT", (void *)tq_objiv}, {"glGetQueryObjectuivEXT", (void *)tq_objuiv},
|
||||
{"glGetQueryObjecti64vEXT", (void *)tq_obji64v}, {"glGetQueryObjectui64vEXT", (void *)tq_objui64v},
|
||||
{"glGetInteger64vEXT", (void *)tq_int64v},
|
||||
};
|
||||
|
||||
static __eglMustCastToProperFunctionPointerType timer_query_standin(const char *name) {
|
||||
for (size_t i = 0; i < sizeof(timer_query_standins) / sizeof(timer_query_standins[0]); i++)
|
||||
if (!strcmp(name, timer_query_standins[i].name)) {
|
||||
if (!tq_used++) LOG("no GPU timer queries in this driver: %s and the other EXT query functions answer "
|
||||
"'available, 0 ns'", name);
|
||||
return (__eglMustCastToProperFunctionPointerType)timer_query_standins[i].fn;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
__attribute__((visibility("default"))) __eglMustCastToProperFunctionPointerType eglGetProcAddress(const char *name) {
|
||||
pthread_once(&once, init);
|
||||
if (name && !strcmp(name, "glGetStringi")) return (__eglMustCastToProperFunctionPointerType)glGetStringi;
|
||||
if (name && !strcmp(name, "glGetString")) return (__eglMustCastToProperFunctionPointerType)glGetString;
|
||||
if (name && !strcmp(name, "glGetIntegerv")) return (__eglMustCastToProperFunctionPointerType)glGetIntegerv;
|
||||
__eglMustCastToProperFunctionPointerType fn = real_eglGetProcAddress ? real_eglGetProcAddress(name) : NULL;
|
||||
if (name && (!fn || added_timer_query > 0)) {
|
||||
__eglMustCastToProperFunctionPointerType standin = timer_query_standin(name);
|
||||
if (standin || !fn) return standin;
|
||||
}
|
||||
if (!name) return fn;
|
||||
if (!fn || !name) return fn;
|
||||
#define WRAP(sym, real, w) if (!strcmp(name, sym)) { real = (void *)fn; return (__eglMustCastToProperFunctionPointerType)w; }
|
||||
if (hide_msrtt && (!strcmp(name, "glFramebufferTexture2DMultisampleEXT") ||
|
||||
!strcmp(name, "glRenderbufferStorageMultisampleEXT"))) {
|
||||
|
||||
@@ -34,9 +34,6 @@ static pthread_once_t once = PTHREAD_ONCE_INIT;
|
||||
// physics update back made the hands lag and the gate made loading screens stutter, so both are off unless the recipe
|
||||
// turns them on (ovrp_begin_gate=1, ovrp_hold_physics=1).
|
||||
static int begin_gate, hold_physics;
|
||||
// ovrp_trace=1 (diagnostics): every 5 s, how many frame-loop calls went through and which are in progress (thread,
|
||||
// frame index, for how long): shows where the main and render threads wait when a game stops after a few frames.
|
||||
static int trace;
|
||||
|
||||
static void read_conf_file(const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
@@ -45,7 +42,6 @@ static void read_conf_file(const char *path) {
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (!strncmp(line, "ovrp_begin_gate=", 16)) begin_gate = atoi(line + 16);
|
||||
if (!strncmp(line, "ovrp_hold_physics=", 18)) hold_physics = atoi(line + 18);
|
||||
if (!strncmp(line, "ovrp_trace=", 11)) trace = atoi(line + 11);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
@@ -77,56 +73,8 @@ static void read_conf(void) {
|
||||
if (env && *env) read_conf_file(env);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- ovrp_trace
|
||||
#include <unistd.h>
|
||||
static long long now_ns(void);
|
||||
enum { T_PHYSICS, T_RENDER, T_WAIT, T_BEGIN, T_END, T_COUNT };
|
||||
static const char *const trace_names[T_COUNT] = {"Update2(physics)", "Update2(render)", "WaitToBeginFrame",
|
||||
"BeginFrame", "EndFrame"};
|
||||
static struct { long long since; int frame, tid, last_frame, last_result; long calls; } traced[T_COUNT];
|
||||
|
||||
static void trace_enter(int k, int frame) {
|
||||
if (!trace) return;
|
||||
traced[k].frame = frame;
|
||||
traced[k].tid = gettid();
|
||||
__atomic_store_n(&traced[k].since, now_ns(), __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static void trace_exit(int k, int result) {
|
||||
if (!trace) return;
|
||||
__atomic_store_n(&traced[k].since, 0, __ATOMIC_RELEASE);
|
||||
traced[k].last_frame = traced[k].frame;
|
||||
traced[k].last_result = result;
|
||||
__atomic_add_fetch(&traced[k].calls, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
static void *trace_watch(void *arg) {
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
sleep(5);
|
||||
char line[700];
|
||||
size_t used = 0;
|
||||
long long now = now_ns();
|
||||
for (int k = 0; k < T_COUNT && used < sizeof(line) - 120; ++k) {
|
||||
long long since = __atomic_load_n(&traced[k].since, __ATOMIC_ACQUIRE);
|
||||
used += (size_t)snprintf(line + used, sizeof(line) - used, " %s %ld (last frame %d: %d)", trace_names[k],
|
||||
__atomic_exchange_n(&traced[k].calls, 0, __ATOMIC_RELAXED), traced[k].last_frame,
|
||||
traced[k].last_result);
|
||||
if (since)
|
||||
used += (size_t)snprintf(line + used, sizeof(line) - used, " [IN frame %d, tid %d, %lld ms]",
|
||||
traced[k].frame, traced[k].tid, (now - since) / 1000000);
|
||||
}
|
||||
LOG("ovrp trace 5s:%s", line);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void init(void) {
|
||||
read_conf();
|
||||
if (trace) {
|
||||
pthread_t watcher;
|
||||
if (!pthread_create(&watcher, NULL, trace_watch, NULL)) pthread_detach(watcher);
|
||||
}
|
||||
void *ovrp = dlopen("libOVRPlugin.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (!ovrp) ovrp = dlopen("libOVRPlugin.so", RTLD_NOW);
|
||||
if (ovrp) {
|
||||
@@ -186,9 +134,7 @@ EXPORT int fpov_Update2(int step, int frame_index, double prediction_seconds) {
|
||||
if (step == STEP_RENDER && real_wait && frame_index != last_waited) {
|
||||
last_waited = frame_index;
|
||||
if (last_wait_begun(frame_index)) {
|
||||
trace_enter(T_WAIT, frame_index);
|
||||
int r = real_wait(frame_index);
|
||||
trace_exit(T_WAIT, r);
|
||||
if (logged++ < 3) LOG("ovrp frame loop shim: waited for frame %d: %d", frame_index, r);
|
||||
} else if (skipped++ < 20 || skipped % 1000 == 0) {
|
||||
LOG("ovrp frame loop shim: frame %d: the last waited frame wasn't begun within %d ms, not waiting "
|
||||
@@ -197,10 +143,7 @@ EXPORT int fpov_Update2(int step, int frame_index, double prediction_seconds) {
|
||||
mouse_click_frame();
|
||||
}
|
||||
if (hold_physics && step == STEP_PHYSICS && rendered) return last_result; // see above: keeps the display-time poses
|
||||
const int traced_step = step == STEP_RENDER ? T_RENDER : T_PHYSICS;
|
||||
trace_enter(traced_step, frame_index);
|
||||
int r = real_update2 ? real_update2(step, frame_index, prediction_seconds) : -1000; // ovrpFailure
|
||||
trace_exit(traced_step, r);
|
||||
if (step == STEP_RENDER) rendered = 1, last_result = r;
|
||||
return r;
|
||||
}
|
||||
@@ -230,9 +173,7 @@ EXPORT int fpov_BeginFrame(long frame, long b, long c, long d) {
|
||||
remap_from = remap_to = -1;
|
||||
}
|
||||
pthread_mutex_unlock(&begin_lock);
|
||||
trace_enter(T_BEGIN, (int)index);
|
||||
int r = real ? real(index, b, c, d) : -1000;
|
||||
trace_exit(T_BEGIN, r);
|
||||
if (index != frame && logged++ < 20)
|
||||
LOG("ovrp frame loop shim: begin frame %ld as waited frame %ld: %d", frame, index, r);
|
||||
pthread_mutex_lock(&begin_lock);
|
||||
@@ -245,15 +186,11 @@ EXPORT int fpov_BeginFrame(long frame, long b, long c, long d) {
|
||||
|
||||
EXPORT int fpov_EndFrame(long frame, long b, long c, long d) {
|
||||
static PFN_FrameCall real;
|
||||
pthread_once(&once, init);
|
||||
if (!real) real = ovrp_fn("ovrp_EndFrame");
|
||||
pthread_mutex_lock(&begin_lock);
|
||||
if ((int)frame == remap_from) frame = remap_to;
|
||||
pthread_mutex_unlock(&begin_lock);
|
||||
trace_enter(T_END, (int)frame);
|
||||
int r = real ? real(frame, b, c, d) : -1000;
|
||||
trace_exit(T_END, r);
|
||||
return r;
|
||||
return real ? real(frame, b, c, d) : -1000;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- input diagnostics (pass-through)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// libfp_vkext.so (patch frame.vk_android_extensions, builds for this PC only): Android-only Vulkan device
|
||||
// extensions for engines that refuse to start without them. AXRB's direct mode hands the guest the Windows driver's
|
||||
// device extensions, which lack Android's external-memory ones and the fragment density map Quest GPUs have;
|
||||
// CryEngine (The Climb 2) then fails "Renderer initialization" before it creates a device. The engine's import of
|
||||
// vkEnumerateDeviceExtensionProperties is renamed to fpEnumerateDeviceExtensionProperties (same length,
|
||||
// elf.rename_imports), which lists the real extensions plus the missing ones of EXTRA. AXRB's vkCreateDevice leaves out requested extensions the host GPU doesn't have, so nothing
|
||||
// else changes; on a device that has them (the Frame, where the patch is never applied) the list is the same.
|
||||
#include <android/log.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
static const char* const EXTRA[] = {
|
||||
"VK_KHR_external_memory_fd",
|
||||
"VK_ANDROID_external_memory_android_hardware_buffer",
|
||||
"VK_EXT_queue_family_foreign",
|
||||
// Quest GPUs have it, desktop NVIDIA drivers don't: The Climb 2 requires it (no device without it; with it listed
|
||||
// and left out again by AXRB, the VrApi bridge's full-rate foveation stand-ins take over)
|
||||
"VK_EXT_fragment_density_map",
|
||||
};
|
||||
#define N_EXTRA (sizeof(EXTRA) / sizeof(EXTRA[0]))
|
||||
|
||||
__attribute__((visibility("default"))) VkResult fpEnumerateDeviceExtensionProperties(
|
||||
VkPhysicalDevice device, const char* layer, uint32_t* count, VkExtensionProperties* props) {
|
||||
if (layer || !count) return vkEnumerateDeviceExtensionProperties(device, layer, count, props);
|
||||
uint32_t n = 0;
|
||||
VkResult r = vkEnumerateDeviceExtensionProperties(device, NULL, &n, NULL);
|
||||
if (r != VK_SUCCESS) return r;
|
||||
VkExtensionProperties* all = calloc(n + N_EXTRA, sizeof(*all));
|
||||
if (!all) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
r = vkEnumerateDeviceExtensionProperties(device, NULL, &n, all);
|
||||
if (r < 0) {
|
||||
free(all);
|
||||
return r;
|
||||
}
|
||||
uint32_t total = n;
|
||||
static int logged;
|
||||
for (unsigned e = 0; e < N_EXTRA; ++e) {
|
||||
int have = 0;
|
||||
for (uint32_t i = 0; i < n && !have; ++i) have = !strcmp(all[i].extensionName, EXTRA[e]);
|
||||
if (have) continue;
|
||||
strncpy(all[total].extensionName, EXTRA[e], VK_MAX_EXTENSION_NAME_SIZE - 1);
|
||||
all[total].specVersion = 1;
|
||||
if (!logged) __android_log_print(ANDROID_LOG_INFO, "FrameBridge", "fp_vkext: listing %s (not on this GPU)", EXTRA[e]);
|
||||
++total;
|
||||
}
|
||||
logged = 1;
|
||||
if (!props) {
|
||||
*count = total;
|
||||
free(all);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
const uint32_t c = *count < total ? *count : total;
|
||||
memcpy(props, all, c * sizeof(*props));
|
||||
*count = c;
|
||||
free(all);
|
||||
return c < total ? VK_INCOMPLETE : VK_SUCCESS;
|
||||
}
|
||||
@@ -8,9 +8,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
|
||||
@@ -754,59 +752,6 @@ bool acquireJobs(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// glCopyImageSubData is OpenGL ES 3.2 (or GL_EXT/OES_copy_image). The Android emulator's GLES translator (AXRB's
|
||||
// emulator on a PC: "OpenGL ES 3.0"/"3.1" without either extension) only has a stub that logs "called unimplemented
|
||||
// OpenGL ES API" and copies nothing: every eye image reached the runtime black (Path of the Warrior). Without it the
|
||||
// copy is a glBlitFramebuffer (ES 3.0) per array layer: the same texels for the same formats (the twin RGBA8/sRGB
|
||||
// formats of ensureOutputGl would be converted by a blit; that combination only arises when the runtime lacks the
|
||||
// app's format).
|
||||
bool glCopyImageAvailable() {
|
||||
static int state = -1; // one driver per process
|
||||
if (state < 0) {
|
||||
const char* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
|
||||
const char* ext = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
int major = 0, minor = 0;
|
||||
if (version) std::sscanf(version, "OpenGL ES %d.%d", &major, &minor);
|
||||
state = major > 3 || (major == 3 && minor >= 2) ||
|
||||
(ext && (std::strstr(ext, "GL_EXT_copy_image") || std::strstr(ext, "GL_OES_copy_image")));
|
||||
OVP_LOG("GLES eye copies: %s (%s)", state ? "glCopyImageSubData" : "glBlitFramebuffer, no glCopyImageSubData",
|
||||
version ? version : "no GL version");
|
||||
}
|
||||
return state == 1;
|
||||
}
|
||||
|
||||
void blitCopyGl(GLuint source, GLuint destination, GLenum target, GLsizei width, GLsizei height, GLsizei layers) {
|
||||
GLint readFbo = 0, drawFbo = 0;
|
||||
GLboolean mask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFbo);
|
||||
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo);
|
||||
glGetBooleanv(GL_COLOR_WRITEMASK, mask);
|
||||
const GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST), discard = glIsEnabled(GL_RASTERIZER_DISCARD);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
GLuint fbo[2] = {0, 0}; // per call: framebuffers aren't shared between the app's contexts
|
||||
glGenFramebuffers(2, fbo);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
|
||||
for (GLsizei layer = 0; layer < std::max<GLsizei>(layers, 1); ++layer) {
|
||||
if (target == GL_TEXTURE_2D_ARRAY) {
|
||||
glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, source, 0, layer);
|
||||
glFramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, destination, 0, layer);
|
||||
} else {
|
||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source, 0);
|
||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
|
||||
}
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readFbo));
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawFbo));
|
||||
glDeleteFramebuffers(2, fbo);
|
||||
glColorMask(mask[0], mask[1], mask[2], mask[3]);
|
||||
if (scissor) glEnable(GL_SCISSOR_TEST);
|
||||
if (discard) glEnable(GL_RASTERIZER_DISCARD);
|
||||
}
|
||||
|
||||
bool submitCopiesGl(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
||||
ovp::Runtime& s = ovp::runtime();
|
||||
#ifdef OVP_GL_DIAG // build with -DOVP_GL_DIAG for app GL state + eye image readback probes
|
||||
@@ -846,17 +791,12 @@ bool submitCopiesGl(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
||||
const GLuint destination = chain->glOutputImages[jobs[i].outputIndex].image;
|
||||
const GLenum target = chain->arrayLayers > 1 ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D;
|
||||
while (glGetError() != GL_NO_ERROR) {} // don't blame the copy for errors the app left behind
|
||||
if (glCopyImageAvailable()) {
|
||||
glCopyImageSubData(source, target, 0, 0, 0, 0, destination, target, 0, 0, 0, 0,
|
||||
static_cast<GLsizei>(chain->width), static_cast<GLsizei>(chain->height),
|
||||
static_cast<GLsizei>(chain->arrayLayers));
|
||||
} else {
|
||||
blitCopyGl(source, destination, target, static_cast<GLsizei>(chain->width),
|
||||
static_cast<GLsizei>(chain->height), static_cast<GLsizei>(chain->arrayLayers));
|
||||
}
|
||||
glCopyImageSubData(source, target, 0, 0, 0, 0, destination, target, 0, 0, 0, 0,
|
||||
static_cast<GLsizei>(chain->width), static_cast<GLsizei>(chain->height),
|
||||
static_cast<GLsizei>(chain->arrayLayers));
|
||||
const GLenum error = glGetError();
|
||||
static int errors;
|
||||
if (error != GL_NO_ERROR && errors++ < 5) OVP_ERROR("GL eye image copy failed: 0x%x", error);
|
||||
if (error != GL_NO_ERROR && errors++ < 5) OVP_ERROR("glCopyImageSubData failed: 0x%x", error);
|
||||
#ifdef OVP_GL_DIAG
|
||||
static int probes;
|
||||
if (probes < 4000 && (probes++ % 360) == 0) {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ovp {
|
||||
@@ -748,68 +747,12 @@ VRAPI ovrResult vrapi_GetInstanceExtensionsVulkan(char* names, uint32_t* size) {
|
||||
uint32_t capacity = names ? *size : 0;
|
||||
return xrOk(s.xr.xrGetVulkanInstanceExtensionsKHR(s.instance, s.system, capacity, size, names), "xrGetVulkanInstanceExtensionsKHR") ? Success : InvalidOperation;
|
||||
}
|
||||
// The runtime's device extensions that no Vulkan device here offers are left out (FramePort): engines like
|
||||
// CryEngine refuse to create a device when a requested extension is missing, and AXRB's runtime asks for its
|
||||
// emulator's VK_ANDROID_external_memory_android_hardware_buffer + VK_EXT_queue_family_foreign also in its direct mode,
|
||||
// where the Windows driver has neither ("CrySystem initialization failed"). Where every device has them (the Frame)
|
||||
// the list is unchanged.
|
||||
static std::string supportedDeviceExtensions(const std::string& wanted) {
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.apiVersion = VK_API_VERSION_1_0;
|
||||
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
info.pApplicationInfo = &app;
|
||||
VkInstance probe = VK_NULL_HANDLE;
|
||||
if (vkCreateInstance(&info, nullptr, &probe) != VK_SUCCESS || !probe) return wanted;
|
||||
std::vector<std::string> available;
|
||||
uint32_t devices = 0;
|
||||
vkEnumeratePhysicalDevices(probe, &devices, nullptr);
|
||||
std::vector<VkPhysicalDevice> list(devices);
|
||||
if (devices && vkEnumeratePhysicalDevices(probe, &devices, list.data()) >= VK_SUCCESS) {
|
||||
for (uint32_t d = 0; d < devices; ++d) {
|
||||
uint32_t count = 0;
|
||||
vkEnumerateDeviceExtensionProperties(list[d], nullptr, &count, nullptr);
|
||||
std::vector<VkExtensionProperties> props(count);
|
||||
if (count && vkEnumerateDeviceExtensionProperties(list[d], nullptr, &count, props.data()) >= VK_SUCCESS)
|
||||
for (uint32_t i = 0; i < count; ++i) available.emplace_back(props[i].extensionName);
|
||||
}
|
||||
}
|
||||
vkDestroyInstance(probe, nullptr);
|
||||
if (available.empty()) return wanted; // nothing to judge by: keep the runtime's list
|
||||
std::string out, dropped;
|
||||
size_t start = 0;
|
||||
while (start < wanted.size()) {
|
||||
size_t end = wanted.find(' ', start);
|
||||
if (end == std::string::npos) end = wanted.size();
|
||||
std::string name = wanted.substr(start, end - start);
|
||||
if (!name.empty()) {
|
||||
std::string& to = std::find(available.begin(), available.end(), name) != available.end() ? out : dropped;
|
||||
to += (to.empty() ? "" : " ") + name;
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
if (!dropped.empty()) OVP_LOG("Vulkan device extensions the runtime asks for but no device offers, left out: %s", dropped.c_str());
|
||||
return out;
|
||||
}
|
||||
VRAPI ovrResult vrapi_GetDeviceExtensionsVulkan(char* names, uint32_t* size) {
|
||||
LOCK_STATE;
|
||||
if (!s.instance) return NotInitialized;
|
||||
if (!size) return InvalidParameter;
|
||||
static std::string filtered;
|
||||
static bool haveFiltered = false;
|
||||
if (!haveFiltered) {
|
||||
uint32_t needed = 0;
|
||||
if (!xrOk(s.xr.xrGetVulkanDeviceExtensionsKHR(s.instance, s.system, 0, &needed, nullptr), "xrGetVulkanDeviceExtensionsKHR")) return InvalidOperation;
|
||||
std::vector<char> raw(needed + 1, '\0');
|
||||
if (needed && !xrOk(s.xr.xrGetVulkanDeviceExtensionsKHR(s.instance, s.system, needed, &needed, raw.data()), "xrGetVulkanDeviceExtensionsKHR")) return InvalidOperation;
|
||||
filtered = supportedDeviceExtensions(std::string(raw.data()));
|
||||
haveFiltered = true;
|
||||
}
|
||||
const uint32_t needed = static_cast<uint32_t>(filtered.size() + 1);
|
||||
if (!names) { *size = needed; return Success; }
|
||||
if (*size < needed) { *size = needed; return InvalidParameter; }
|
||||
std::memcpy(names, filtered.c_str(), needed);
|
||||
*size = needed;
|
||||
return Success;
|
||||
uint32_t capacity = names ? *size : 0;
|
||||
return xrOk(s.xr.xrGetVulkanDeviceExtensionsKHR(s.instance, s.system, capacity, size, names), "xrGetVulkanDeviceExtensionsKHR") ? Success : InvalidOperation;
|
||||
}
|
||||
VRAPI ovrResult vrapi_CreateSystemVulkan(ovrSystemCreateInfoVulkan* info) {
|
||||
LOCK_STATE;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
// xr_sim_capture: save what Monado's compositor window shows (the simulated HMD's display = both eyes as
|
||||
// the headset would get them) to a BMP, so a headset-free test can see what a game draws.
|
||||
//
|
||||
// xr_sim_capture.exe <out.bmp> [window title, default "Monado"]
|
||||
//
|
||||
// Finds the first top-level window whose title starts with the given text (Monado's own window is
|
||||
// "Monado (Windowed)", its peek window "Monado peek") and copies it with PrintWindow(PW_RENDERFULLCONTENT),
|
||||
// which also works for a window behind others. Prints "capture: <w>x<h> <file>" or "capture: fail ...".
|
||||
//
|
||||
// Build: cl /nologo /O2 xr_sim_capture.cpp /link user32.lib gdi32.lib
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <vector>
|
||||
|
||||
#ifndef PW_RENDERFULLCONTENT
|
||||
#define PW_RENDERFULLCONTENT 0x00000002
|
||||
#endif
|
||||
|
||||
struct Find {
|
||||
const wchar_t *prefix;
|
||||
HWND hwnd;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK enum_cb(HWND h, LPARAM lp)
|
||||
{
|
||||
Find *f = (Find *)lp;
|
||||
wchar_t title[256];
|
||||
if (!IsWindowVisible(h) || GetWindowTextW(h, title, 256) <= 0)
|
||||
return TRUE;
|
||||
if (std::wcsncmp(title, f->prefix, std::wcslen(f->prefix)) == 0) {
|
||||
f->hwnd = h;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::printf("usage: xr_sim_capture <out.bmp> [window title prefix]\n");
|
||||
return 1;
|
||||
}
|
||||
// Not DPI aware on purpose: Monado's window isn't either, so its client size in this process's
|
||||
// (scaled) coordinates is the size of its swapchain, which PrintWindow copies 1:1.
|
||||
Find f{argc > 2 ? argv[2] : L"Monado", nullptr};
|
||||
EnumWindows(enum_cb, (LPARAM)&f);
|
||||
if (!f.hwnd) {
|
||||
std::printf("capture: fail no window\n");
|
||||
return 2;
|
||||
}
|
||||
RECT r;
|
||||
GetClientRect(f.hwnd, &r);
|
||||
int w = r.right - r.left, h = r.bottom - r.top;
|
||||
if (w <= 0 || h <= 0) {
|
||||
std::printf("capture: fail empty window (minimised?)\n");
|
||||
return 2;
|
||||
}
|
||||
HDC wdc = GetDC(f.hwnd);
|
||||
HDC mdc = CreateCompatibleDC(wdc);
|
||||
HBITMAP bmp = CreateCompatibleBitmap(wdc, w, h);
|
||||
HGDIOBJ old = SelectObject(mdc, bmp);
|
||||
if (!PrintWindow(f.hwnd, mdc, PW_CLIENTONLY | PW_RENDERFULLCONTENT))
|
||||
BitBlt(mdc, 0, 0, w, h, wdc, 0, 0, SRCCOPY);
|
||||
SelectObject(mdc, old);
|
||||
|
||||
BITMAPINFOHEADER bi{sizeof(bi)};
|
||||
bi.biWidth = w;
|
||||
bi.biHeight = h; // bottom-up, as BMP files store it
|
||||
bi.biPlanes = 1;
|
||||
bi.biBitCount = 24;
|
||||
bi.biCompression = BI_RGB;
|
||||
int row = (w * 3 + 3) & ~3;
|
||||
std::vector<unsigned char> px((size_t)row * h);
|
||||
GetDIBits(mdc, bmp, 0, h, px.data(), (BITMAPINFO *)&bi, DIB_RGB_COLORS);
|
||||
DeleteObject(bmp);
|
||||
DeleteDC(mdc);
|
||||
ReleaseDC(f.hwnd, wdc);
|
||||
|
||||
FILE *out = _wfopen(argv[1], L"wb");
|
||||
if (!out) {
|
||||
std::printf("capture: fail cannot write\n");
|
||||
return 2;
|
||||
}
|
||||
BITMAPFILEHEADER fh{};
|
||||
fh.bfType = 0x4D42;
|
||||
fh.bfOffBits = sizeof(fh) + sizeof(bi);
|
||||
fh.bfSize = fh.bfOffBits + (DWORD)px.size();
|
||||
std::fwrite(&fh, sizeof(fh), 1, out);
|
||||
std::fwrite(&bi, sizeof(bi), 1, out);
|
||||
std::fwrite(px.data(), 1, px.size(), out);
|
||||
std::fclose(out);
|
||||
std::printf("capture: %dx%d %ls\n", w, h, argv[1]);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
// xr_sim_probe: the smallest D3D11 OpenXR client FramePort uses to check a headset-free OpenXR runtime
|
||||
// (Monado's simulated HMD, docs/PC_ANDROID.md "Testing without a headset") on Windows.
|
||||
//
|
||||
// Creates an instance with XR_KHR_D3D11_enable, picks the D3D11 adapter by the LUID the runtime asks for,
|
||||
// runs a session (begins it on READY), submits a projection layer every frame (each eye cleared to a
|
||||
// colour that cycles), and after <seconds> asks for the exit. Every step prints one "probe: ..." line;
|
||||
// the last line is "probe: result ok frames=N focused=0|1" or "probe: result fail ...".
|
||||
// Optional: --dump <file.bmp> saves the left eye image of frame 60 (what the client drew).
|
||||
//
|
||||
// Build (VS Build Tools x64 prompt), with the official OpenXR loader release (openxr_loader_windows):
|
||||
// cl /nologo /EHsc /O2 /I<loader>\include xr_sim_probe.cpp /link <loader>\x64\lib\openxr_loader.lib d3d11.lib dxgi.lib
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <d3d11.h>
|
||||
#include <dxgi.h>
|
||||
#define XR_USE_PLATFORM_WIN32
|
||||
#define XR_USE_GRAPHICS_API_D3D11
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#define CHECK(call) \
|
||||
do { \
|
||||
XrResult r_ = (call); \
|
||||
if (XR_FAILED(r_)) { \
|
||||
std::printf("probe: result fail %s = %d\n", #call, (int)r_); \
|
||||
std::fflush(stdout); \
|
||||
std::exit(2); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static const char *state_name(XrSessionState s)
|
||||
{
|
||||
switch (s) {
|
||||
case XR_SESSION_STATE_IDLE: return "IDLE";
|
||||
case XR_SESSION_STATE_READY: return "READY";
|
||||
case XR_SESSION_STATE_SYNCHRONIZED: return "SYNCHRONIZED";
|
||||
case XR_SESSION_STATE_VISIBLE: return "VISIBLE";
|
||||
case XR_SESSION_STATE_FOCUSED: return "FOCUSED";
|
||||
case XR_SESSION_STATE_STOPPING: return "STOPPING";
|
||||
case XR_SESSION_STATE_LOSS_PENDING: return "LOSS_PENDING";
|
||||
case XR_SESSION_STATE_EXITING: return "EXITING";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void save_bmp(ID3D11Device *dev, ID3D11DeviceContext *ctx, ID3D11Texture2D *tex, const char *path)
|
||||
{
|
||||
D3D11_TEXTURE2D_DESC d;
|
||||
tex->GetDesc(&d);
|
||||
d.Usage = D3D11_USAGE_STAGING;
|
||||
d.BindFlags = 0;
|
||||
d.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
d.MiscFlags = 0;
|
||||
d.ArraySize = 1;
|
||||
d.MipLevels = 1;
|
||||
d.SampleDesc.Count = 1;
|
||||
ID3D11Texture2D *staging = nullptr;
|
||||
if (FAILED(dev->CreateTexture2D(&d, nullptr, &staging))) {
|
||||
std::printf("probe: dump failed (staging)\n");
|
||||
return;
|
||||
}
|
||||
ctx->CopySubresourceRegion(staging, 0, 0, 0, 0, tex, 0, nullptr);
|
||||
D3D11_MAPPED_SUBRESOURCE m;
|
||||
if (FAILED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
|
||||
staging->Release();
|
||||
return;
|
||||
}
|
||||
FILE *f = std::fopen(path, "wb");
|
||||
if (f) {
|
||||
uint32_t w = d.Width, h = d.Height, row = w * 3, pad = (4 - row % 4) % 4;
|
||||
uint32_t size = 54 + (row + pad) * h;
|
||||
uint8_t hdr[54] = {'B', 'M'};
|
||||
std::memcpy(hdr + 2, &size, 4);
|
||||
hdr[10] = 54;
|
||||
hdr[14] = 40;
|
||||
std::memcpy(hdr + 18, &w, 4);
|
||||
std::memcpy(hdr + 22, &h, 4);
|
||||
hdr[26] = 1;
|
||||
hdr[28] = 24;
|
||||
std::fwrite(hdr, 1, 54, f);
|
||||
std::vector<uint8_t> line(row + pad, 0);
|
||||
for (int y = (int)h - 1; y >= 0; y--) {
|
||||
const uint8_t *src = (const uint8_t *)m.pData + (size_t)y * m.RowPitch;
|
||||
for (uint32_t x = 0; x < w; x++) { // RGBA/BGRA 8-bit formats only: good enough for a probe
|
||||
bool bgra = d.Format == DXGI_FORMAT_B8G8R8A8_UNORM || d.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
|
||||
line[x * 3 + 0] = src[x * 4 + (bgra ? 0 : 2)];
|
||||
line[x * 3 + 1] = src[x * 4 + 1];
|
||||
line[x * 3 + 2] = src[x * 4 + (bgra ? 2 : 0)];
|
||||
}
|
||||
std::fwrite(line.data(), 1, line.size(), f);
|
||||
}
|
||||
std::fclose(f);
|
||||
std::printf("probe: dumped left eye %ux%u to %s\n", w, h, path);
|
||||
}
|
||||
ctx->Unmap(staging, 0);
|
||||
staging->Release();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
double seconds = 10.0;
|
||||
const char *dump = nullptr;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!std::strcmp(argv[i], "--dump") && i + 1 < argc)
|
||||
dump = argv[++i];
|
||||
else
|
||||
seconds = std::atof(argv[i]);
|
||||
}
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
|
||||
const char *exts[] = {XR_KHR_D3D11_ENABLE_EXTENSION_NAME};
|
||||
XrInstanceCreateInfo ici{XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
std::strcpy(ici.applicationInfo.applicationName, "xr_sim_probe");
|
||||
ici.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
ici.enabledExtensionCount = 1;
|
||||
ici.enabledExtensionNames = exts;
|
||||
XrInstance inst = XR_NULL_HANDLE;
|
||||
CHECK(xrCreateInstance(&ici, &inst));
|
||||
XrInstanceProperties ip{XR_TYPE_INSTANCE_PROPERTIES};
|
||||
CHECK(xrGetInstanceProperties(inst, &ip));
|
||||
std::printf("probe: runtime %s %u.%u.%u\n", ip.runtimeName, XR_VERSION_MAJOR(ip.runtimeVersion),
|
||||
XR_VERSION_MINOR(ip.runtimeVersion), XR_VERSION_PATCH(ip.runtimeVersion));
|
||||
|
||||
XrSystemGetInfo sgi{XR_TYPE_SYSTEM_GET_INFO};
|
||||
sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
XrSystemId sys;
|
||||
CHECK(xrGetSystem(inst, &sgi, &sys));
|
||||
XrSystemProperties sp{XR_TYPE_SYSTEM_PROPERTIES};
|
||||
CHECK(xrGetSystemProperties(inst, sys, &sp));
|
||||
std::printf("probe: system %s\n", sp.systemName);
|
||||
|
||||
PFN_xrGetD3D11GraphicsRequirementsKHR getReq = nullptr;
|
||||
CHECK(xrGetInstanceProcAddr(inst, "xrGetD3D11GraphicsRequirementsKHR", (PFN_xrVoidFunction *)&getReq));
|
||||
XrGraphicsRequirementsD3D11KHR req{XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR};
|
||||
CHECK(getReq(inst, sys, &req));
|
||||
|
||||
IDXGIFactory1 *factory = nullptr;
|
||||
CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void **)&factory);
|
||||
IDXGIAdapter1 *adapter = nullptr;
|
||||
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; i++) {
|
||||
DXGI_ADAPTER_DESC1 ad;
|
||||
adapter->GetDesc1(&ad);
|
||||
if (std::memcmp(&ad.AdapterLuid, &req.adapterLuid, sizeof(LUID)) == 0) {
|
||||
std::printf("probe: adapter %ls (luid %08lx:%08lx)\n", ad.Description, ad.AdapterLuid.HighPart,
|
||||
ad.AdapterLuid.LowPart);
|
||||
break;
|
||||
}
|
||||
adapter->Release();
|
||||
adapter = nullptr;
|
||||
}
|
||||
if (!adapter) {
|
||||
std::printf("probe: result fail no DXGI adapter with the runtime's LUID\n");
|
||||
return 2;
|
||||
}
|
||||
ID3D11Device *dev = nullptr;
|
||||
ID3D11DeviceContext *ctx = nullptr;
|
||||
D3D_FEATURE_LEVEL fl = D3D_FEATURE_LEVEL_11_0;
|
||||
if (FAILED(D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, &fl, 1, D3D11_SDK_VERSION, &dev,
|
||||
nullptr, &ctx))) {
|
||||
std::printf("probe: result fail D3D11CreateDevice\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
XrGraphicsBindingD3D11KHR gb{XR_TYPE_GRAPHICS_BINDING_D3D11_KHR};
|
||||
gb.device = dev;
|
||||
XrSessionCreateInfo sci{XR_TYPE_SESSION_CREATE_INFO};
|
||||
sci.next = &gb;
|
||||
sci.systemId = sys;
|
||||
XrSession session;
|
||||
CHECK(xrCreateSession(inst, &sci, &session));
|
||||
std::printf("probe: session created\n");
|
||||
|
||||
XrReferenceSpaceCreateInfo rsci{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
rsci.poseInReferenceSpace.orientation.w = 1;
|
||||
XrSpace space;
|
||||
CHECK(xrCreateReferenceSpace(session, &rsci, &space));
|
||||
|
||||
uint32_t nviews = 0;
|
||||
CHECK(xrEnumerateViewConfigurationViews(inst, sys, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &nviews, nullptr));
|
||||
std::vector<XrViewConfigurationView> vcv(nviews, {XR_TYPE_VIEW_CONFIGURATION_VIEW});
|
||||
CHECK(xrEnumerateViewConfigurationViews(inst, sys, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, nviews, &nviews,
|
||||
vcv.data()));
|
||||
|
||||
uint32_t nfmt = 0;
|
||||
CHECK(xrEnumerateSwapchainFormats(session, 0, &nfmt, nullptr));
|
||||
std::vector<int64_t> fmts(nfmt);
|
||||
CHECK(xrEnumerateSwapchainFormats(session, nfmt, &nfmt, fmts.data()));
|
||||
int64_t fmt = fmts[0];
|
||||
for (int64_t f : fmts)
|
||||
if (f == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB || f == DXGI_FORMAT_R8G8B8A8_UNORM) {
|
||||
fmt = f;
|
||||
break;
|
||||
}
|
||||
|
||||
struct Eye {
|
||||
XrSwapchain sc;
|
||||
std::vector<XrSwapchainImageD3D11KHR> imgs;
|
||||
std::vector<ID3D11RenderTargetView *> rtv;
|
||||
uint32_t w, h;
|
||||
};
|
||||
std::vector<Eye> eyes(nviews);
|
||||
for (uint32_t i = 0; i < nviews; i++) {
|
||||
Eye &e = eyes[i];
|
||||
e.w = vcv[i].recommendedImageRectWidth;
|
||||
e.h = vcv[i].recommendedImageRectHeight;
|
||||
XrSwapchainCreateInfo sc{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
sc.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT;
|
||||
sc.format = fmt;
|
||||
sc.sampleCount = 1;
|
||||
sc.width = e.w;
|
||||
sc.height = e.h;
|
||||
sc.faceCount = 1;
|
||||
sc.arraySize = 1;
|
||||
sc.mipCount = 1;
|
||||
CHECK(xrCreateSwapchain(session, &sc, &e.sc));
|
||||
uint32_t n = 0;
|
||||
CHECK(xrEnumerateSwapchainImages(e.sc, 0, &n, nullptr));
|
||||
e.imgs.assign(n, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR});
|
||||
CHECK(xrEnumerateSwapchainImages(e.sc, n, &n, (XrSwapchainImageBaseHeader *)e.imgs.data()));
|
||||
for (auto &img : e.imgs) {
|
||||
D3D11_RENDER_TARGET_VIEW_DESC rd{};
|
||||
rd.Format = (DXGI_FORMAT)fmt;
|
||||
rd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
|
||||
ID3D11RenderTargetView *v = nullptr;
|
||||
dev->CreateRenderTargetView(img.texture, &rd, &v);
|
||||
e.rtv.push_back(v);
|
||||
}
|
||||
}
|
||||
std::printf("probe: %u views %ux%u format %lld\n", nviews, eyes[0].w, eyes[0].h, (long long)fmt);
|
||||
|
||||
bool running = false, quit = false, focused = false, exit_requested = false;
|
||||
uint64_t frames = 0, rendered = 0;
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
while (!quit) {
|
||||
XrEventDataBuffer ev{XR_TYPE_EVENT_DATA_BUFFER};
|
||||
while (xrPollEvent(inst, &ev) == XR_SUCCESS) {
|
||||
if (ev.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
|
||||
auto *s = (XrEventDataSessionStateChanged *)&ev;
|
||||
double t = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
std::printf("probe: state %s at %.2f s (frames %llu)\n", state_name(s->state), t,
|
||||
(unsigned long long)frames);
|
||||
if (s->state == XR_SESSION_STATE_FOCUSED)
|
||||
focused = true;
|
||||
if (s->state == XR_SESSION_STATE_READY) {
|
||||
XrSessionBeginInfo bi{XR_TYPE_SESSION_BEGIN_INFO};
|
||||
bi.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
CHECK(xrBeginSession(session, &bi));
|
||||
running = true;
|
||||
} else if (s->state == XR_SESSION_STATE_STOPPING) {
|
||||
CHECK(xrEndSession(session));
|
||||
running = false;
|
||||
} else if (s->state == XR_SESSION_STATE_EXITING || s->state == XR_SESSION_STATE_LOSS_PENDING) {
|
||||
quit = true;
|
||||
}
|
||||
} else if (ev.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
|
||||
quit = true;
|
||||
}
|
||||
ev = {XR_TYPE_EVENT_DATA_BUFFER};
|
||||
}
|
||||
double elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
if (!running) {
|
||||
if (elapsed > seconds + 20) { // never got READY, or never left
|
||||
std::printf("probe: result fail timeout (frames %llu)\n", (unsigned long long)frames);
|
||||
return 3;
|
||||
}
|
||||
Sleep(10);
|
||||
continue;
|
||||
}
|
||||
if (elapsed > seconds && !exit_requested) {
|
||||
std::printf("probe: requesting exit after %.2f s, frames %llu\n", elapsed, (unsigned long long)frames);
|
||||
xrRequestExitSession(session);
|
||||
exit_requested = true;
|
||||
}
|
||||
|
||||
XrFrameState fs{XR_TYPE_FRAME_STATE};
|
||||
CHECK(xrWaitFrame(session, nullptr, &fs));
|
||||
CHECK(xrBeginFrame(session, nullptr));
|
||||
std::vector<XrCompositionLayerProjectionView> pv(nviews, {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW});
|
||||
XrCompositionLayerProjection layer{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
std::vector<XrCompositionLayerBaseHeader *> layers;
|
||||
if (fs.shouldRender) {
|
||||
std::vector<XrView> views(nviews, {XR_TYPE_VIEW});
|
||||
XrViewLocateInfo li{XR_TYPE_VIEW_LOCATE_INFO};
|
||||
li.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
li.displayTime = fs.predictedDisplayTime;
|
||||
li.space = space;
|
||||
XrViewState vs{XR_TYPE_VIEW_STATE};
|
||||
uint32_t got = 0;
|
||||
CHECK(xrLocateViews(session, &li, &vs, nviews, &got, views.data()));
|
||||
for (uint32_t i = 0; i < nviews; i++) {
|
||||
Eye &e = eyes[i];
|
||||
uint32_t idx;
|
||||
CHECK(xrAcquireSwapchainImage(e.sc, nullptr, &idx));
|
||||
XrSwapchainImageWaitInfo wi{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
wi.timeout = XR_INFINITE_DURATION;
|
||||
CHECK(xrWaitSwapchainImage(e.sc, &wi));
|
||||
float phase = (float)((frames % 120) / 120.0);
|
||||
float col[4] = {i == 0 ? phase : 0.1f, 0.2f, i == 1 ? phase : 0.1f, 1.0f};
|
||||
ctx->ClearRenderTargetView(e.rtv[idx], col);
|
||||
if (dump && i == 0 && rendered == 60)
|
||||
save_bmp(dev, ctx, e.imgs[idx].texture, dump);
|
||||
CHECK(xrReleaseSwapchainImage(e.sc, nullptr));
|
||||
pv[i].pose = views[i].pose;
|
||||
pv[i].fov = views[i].fov;
|
||||
pv[i].subImage.swapchain = e.sc;
|
||||
pv[i].subImage.imageRect = {{0, 0}, {(int32_t)e.w, (int32_t)e.h}};
|
||||
}
|
||||
layer.space = space;
|
||||
layer.viewCount = nviews;
|
||||
layer.views = pv.data();
|
||||
layers.push_back((XrCompositionLayerBaseHeader *)&layer);
|
||||
rendered++;
|
||||
}
|
||||
XrFrameEndInfo fe{XR_TYPE_FRAME_END_INFO};
|
||||
fe.displayTime = fs.predictedDisplayTime;
|
||||
fe.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
fe.layerCount = (uint32_t)layers.size();
|
||||
fe.layers = layers.data();
|
||||
CHECK(xrEndFrame(session, &fe));
|
||||
frames++;
|
||||
if (frames % 120 == 0)
|
||||
std::printf("probe: frames %llu rendered %llu at %.2f s\n", (unsigned long long)frames,
|
||||
(unsigned long long)rendered, elapsed);
|
||||
}
|
||||
for (auto &e : eyes)
|
||||
xrDestroySwapchain(e.sc);
|
||||
xrDestroySpace(space);
|
||||
xrDestroySession(session);
|
||||
xrDestroyInstance(inst);
|
||||
std::printf("probe: result ok frames=%llu rendered=%llu focused=%d\n", (unsigned long long)frames,
|
||||
(unsigned long long)rendered, focused ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
@@ -42,6 +42,8 @@ def stage_data():
|
||||
shutil.copytree(ROOT / name, DATA / name)
|
||||
# the Frame agent is uploaded as source; `flet build` turns every .py into .pyc (issue #2): keep a non-.py copy
|
||||
shutil.copy2(ROOT / "agent/frameport_agent.py", DATA / "agent/frameport_agent.py.txt")
|
||||
for helper in (ROOT / "agent").glob("fexrift*.py"): # experimental Rift-on-Frame helper + its extensions
|
||||
shutil.copy2(helper, DATA / "agent" / f"{helper.name}.txt")
|
||||
print(f"staged data in {DATA}")
|
||||
|
||||
|
||||
|
||||
@@ -276,10 +276,6 @@ def entry(p: base.Patch, games: list[str], sigs: list[tuple[str, str, str]]) ->
|
||||
facts.append("experimental")
|
||||
if not p.needs_vr:
|
||||
facts.append("also for 2D apps")
|
||||
if p.on_pc and p.category != "overport": # OVRPort's patches are all in builds for this PC
|
||||
facts.append("also in builds for this PC (AXRB" + (", experimental)" if p.on_pc == "experimental" else ")"))
|
||||
elif getattr(p, "pc_added", False):
|
||||
facts.append("builds for this PC (AXRB) add it when needed")
|
||||
rev = p.revision
|
||||
if rev > 1:
|
||||
facts.append(f"revision {rev}")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""One bounded Rift-on-Frame run. Uses the paired Frame and prints exactly one JSON summary on success.
|
||||
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 uv run python scripts/rift_fex_bench.py oculus-first-contact --seconds 120
|
||||
|
||||
Settings are changed separately with `frameport fex settings APP key=value`; this script never starts a sweep.
|
||||
The Frame owns the timeout/cleanup, so closing this PC process does not leave an unbounded game running.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("app", nargs="?", default="oculus-first-contact")
|
||||
p.add_argument("--seconds", type=int, default=120, choices=range(1, 121), metavar="1..120")
|
||||
p.add_argument("--frame", help="paired Frame as user@host")
|
||||
args = p.parse_args(argv)
|
||||
# Reuse the CLI's feature gate, paired-host verification and JSON output. No second SSH implementation.
|
||||
from frameport.cli import fex_bench
|
||||
fex_bench(args.app, args.seconds, args.frame)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,378 @@
|
||||
"""Static compatibility survey of Rift games for the experimental Rift-on-Frame mode (x86_64 GE-Proton under FEX +
|
||||
Meta's runtime + Revive, see native/fexwine/README.md). Read-only: looks at the game folders' PE files and the Unreal
|
||||
config inside paks; never runs anything, never touches a repack's own Oculus Platform files (only notes them).
|
||||
|
||||
Engine, VR APIs and the Platform SDK come from FramePort's own Rift analysis (analysis/rift.py); on top of that:
|
||||
the Unreal version (branch string, else the pak format's range), the RHI the game's config selects, the generic
|
||||
Unreal IME fix site (agent/fexrift.py find_ime_patch), anti-tamper sections and other runtime needs.
|
||||
|
||||
usage: uv run python scripts/rift_fex_survey.py [--folder DIR ...] [--out DIR]
|
||||
default input: the library's rift.* games; output: <user data>/fexrift/survey.{md,json} (holds local paths and folder
|
||||
names: keep it out of the repo)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from frameport.analysis import rift # noqa: E402
|
||||
from frameport.core import library # noqa: E402
|
||||
from frameport.core.paths import user_data_dir # noqa: E402
|
||||
|
||||
|
||||
def load_fexrift():
|
||||
spec = importlib.util.spec_from_file_location("fexrift_helper", ROOT / "agent" / "fexrift.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
mod.load_extensions()
|
||||
return mod
|
||||
|
||||
|
||||
FX = load_fexrift()
|
||||
MAX_PE = 400 << 20
|
||||
|
||||
DRM_SECTIONS = {".arch": "Denuvo/Arxan-style section", ".vmp0": "VMProtect", ".vmp1": "VMProtect", ".bind": "Steam DRM",
|
||||
".themida": "Themida", ".enigma1": "Enigma"}
|
||||
ANTICHEAT_FILES = ("easyanticheat", "battleye", "beclient", "eac_launcher")
|
||||
|
||||
# Shared pak/ini reader (also used by the local-copy installer).
|
||||
from frameport.analysis.unreal_pak import ( # noqa: E402, F401
|
||||
CONFIG_KEYS,
|
||||
PAK_MAGIC,
|
||||
PAK_UE,
|
||||
Reader,
|
||||
encoded_entry,
|
||||
entry_header_size,
|
||||
legacy_entry,
|
||||
pak_entries,
|
||||
pak_read,
|
||||
parse_engine_ini,
|
||||
unreal_config,
|
||||
)
|
||||
|
||||
RHI_NAMES = {"DefaultGraphicsRHI_DX11": "d3d11", "DefaultGraphicsRHI_DX12": "d3d12",
|
||||
"DefaultGraphicsRHI_Vulkan": "vulkan"}
|
||||
|
||||
|
||||
def unreal_rhi(cfg: dict) -> tuple[str, str]:
|
||||
"""(RHI the Windows build starts with, where that came from). Unreal 4 defaults to D3D11."""
|
||||
if cfg.get("DefaultGraphicsRHI") in RHI_NAMES:
|
||||
return RHI_NAMES[cfg["DefaultGraphicsRHI"]], "config"
|
||||
rhis = cfg.get("TargetedRHIs") or []
|
||||
if rhis and not any(r.startswith("PCD3D") for r in rhis):
|
||||
if any("VULKAN" in r for r in rhis):
|
||||
return "vulkan", "config (no D3D shaders)"
|
||||
if any(r.startswith("GLSL") for r in rhis):
|
||||
return "opengl", "config (no D3D shaders)"
|
||||
if rhis and all(r == "PCD3D_SM6" for r in rhis if r.startswith("PCD3D")):
|
||||
return "d3d12", "config (SM6 shaders only)"
|
||||
return "d3d11", "config" if rhis else "Unreal default"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ PE checks
|
||||
def pe_files(game_dir: Path, limit: int = 600):
|
||||
out = []
|
||||
for p in game_dir.rglob("*"):
|
||||
if p.suffix.lower() in (".exe", ".dll") and p.is_file():
|
||||
out.append(p)
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
_BRANCH = re.compile(r"\+\+[A-Za-z0-9_+.\-]{3,60}")
|
||||
_VER = re.compile(r"(?<![\d.])([45])\.(\d{1,2})(?![\d])")
|
||||
|
||||
|
||||
def unreal_version(data: bytes) -> str | None:
|
||||
""""4.13" etc. from Unreal's branch name (++UE4+Release-4.13, ++unreal+4.10+UE4-Studio, ++UE4+Studio-4.27; ASCII
|
||||
or UTF-16). Branches without a number (++depot+Game) give None."""
|
||||
texts = [m.group(0).decode() for m in re.finditer(_BRANCH.pattern.encode(), data)]
|
||||
key = "++".encode("utf-16-le")
|
||||
i = data.find(key)
|
||||
while i >= 0 and len(texts) < 2000:
|
||||
m = _BRANCH.match(data[i:i + 128].decode("utf-16-le", "replace"))
|
||||
if m:
|
||||
texts.append(m.group(0))
|
||||
i = data.find(key, i + 4)
|
||||
for t in texts:
|
||||
letters = sum(c.isalpha() for c in t)
|
||||
if letters < 4 or not re.search(r"ue[45]|unreal|release", t, re.I):
|
||||
continue
|
||||
m = _VER.search(t)
|
||||
if m:
|
||||
return f"{m.group(1)}.{m.group(2)}"
|
||||
return None
|
||||
|
||||
|
||||
def sections(data: bytes) -> list[str]:
|
||||
try:
|
||||
return [s[0] for s in FX.pe_sections(data)]
|
||||
except struct.error:
|
||||
return []
|
||||
|
||||
|
||||
def imports_of(p: Path) -> set[str]:
|
||||
try:
|
||||
info = rift.read_pe(p, limit=80 << 20)[0]
|
||||
return set(info.imports) | set(info.delay_imports)
|
||||
except (rift.PEError, OSError, struct.error, ValueError):
|
||||
return set()
|
||||
|
||||
|
||||
GRAPHICS = {"d3d11.dll": "d3d11", "d3d12.dll": "d3d12", "opengl32.dll": "opengl", "vulkan-1.dll": "vulkan",
|
||||
"d3d9.dll": "d3d9"}
|
||||
|
||||
|
||||
def survey_game(title: str, game_dir: Path, exe_rel: str | None) -> dict:
|
||||
res = {"title": title, "folder": str(game_dir)}
|
||||
if not game_dir.is_dir():
|
||||
res["verdict"] = ("missing", "folder not found")
|
||||
return res
|
||||
exe = game_dir / exe_rel if exe_rel else None
|
||||
if not exe or not exe.is_file():
|
||||
res["verdict"] = ("unknown", "main exe not known")
|
||||
return res
|
||||
a = rift.analyze(game_dir, exe_rel)
|
||||
files = pe_files(game_dir)
|
||||
names = {p.name.lower(): p for p in files}
|
||||
info, data = rift.read_pe(exe, limit=MAX_PE)
|
||||
data = bytes(data)
|
||||
res["exe"] = exe_rel
|
||||
res["arch"] = info.machine
|
||||
res["exe_32bit"] = info.machine == "x86"
|
||||
res["exe_size"] = exe.stat().st_size
|
||||
res["vr"] = [x for x in a.xr.split("+") if x != "?"] or ["?"]
|
||||
res["platform_sdk"] = bool(a.extra.get("platform_sdk"))
|
||||
res["own_platform_dll"] = any(n.startswith("libovrplatform") for n in names) # noted only, never used
|
||||
imports = set(info.imports) | set(info.delay_imports)
|
||||
# engine
|
||||
if a.engine == "Unreal":
|
||||
cores = [p for n, p in names.items() if re.search(r"-core(-win64-shipping)?\.dll$", n)]
|
||||
ue = unreal_version(data) or next((v for v in map(lambda p: unreal_version(p.read_bytes()), cores) if v), None)
|
||||
cfg = unreal_config(game_dir)
|
||||
res.update(cfg)
|
||||
pak_v = int(cfg["pak"][1:]) if re.fullmatch(r"v\d+", cfg.get("pak", "")) else None
|
||||
res["ue_version"] = ue or (PAK_UE.get(pak_v) and f"{PAK_UE[pak_v]} (pak v{pak_v})") or "?"
|
||||
res["engine"] = f"Unreal {res['ue_version']}"
|
||||
rhi, src = unreal_rhi(cfg.get("config") or {})
|
||||
res["graphics"] = [rhi]
|
||||
res["graphics_from"] = src
|
||||
# Unreal IME deadlock fix: exe (monolithic), else modular ApplicationCore/Slate (Core before that split).
|
||||
patch, why = FX.find_ime_patch(data)
|
||||
if not patch:
|
||||
for n, p in sorted(names.items()):
|
||||
if "applicationcore" in n or re.search(r"-(slate|core)(-win64-shipping)?\.dll$", n):
|
||||
p2, why2 = FX.find_ime_patch(p.read_bytes())
|
||||
if p2:
|
||||
patch, why = dict(p2, module=n), None
|
||||
break
|
||||
if not why2.startswith("no Slate"):
|
||||
why = why2
|
||||
res["ime_patch"] = {"rva": hex(patch["rva"]), "module": patch.get("module", "exe")} if patch else why
|
||||
lo = (res["ue_version"].split(" ")[0].split("-")[0]).split(".")
|
||||
try:
|
||||
major_minor = (int(lo[0]), int(lo[1]))
|
||||
res["large_block_alloc"] = "FMallocBinned2 (UE>=4.18): fp_mem candidate" if major_minor >= (4, 18) \
|
||||
else "FMallocBinned (older): fp_mem likely unneeded"
|
||||
except (ValueError, IndexError):
|
||||
res["large_block_alloc"] = "?"
|
||||
else:
|
||||
for p in files: # graphics imports of the game's own DLLs (Unity players, custom engines)
|
||||
if p.parent == exe.parent and p.stat().st_size <= 80 << 20:
|
||||
imports |= imports_of(p)
|
||||
res["graphics"] = sorted({v for k, v in GRAPHICS.items() if k in imports}) or ["?"]
|
||||
res["graphics_from"] = "imports"
|
||||
if a.engine == "Unity":
|
||||
src = names.get("unityplayer.dll")
|
||||
blob = rift.read_pe(src, limit=MAX_PE)[1] if src else data
|
||||
uv = re.search(rb"(20\d\d\.\d+\.\d+[fp]\d+|5\.\d\.\d+[fp]\d+)", blob)
|
||||
res["engine"] = f"Unity {uv.group(1).decode() if uv else '?'} " + \
|
||||
("IL2CPP" if "gameassembly.dll" in names else "Mono")
|
||||
elif any(n.startswith("crysystem") for n in names) or b"CryEngine" in data or b"CRYENGINE" in data:
|
||||
res["engine"] = "CryEngine"
|
||||
else:
|
||||
res["engine"] = "custom"
|
||||
# anti-tamper / anti-cheat
|
||||
drm = sorted({DRM_SECTIONS[s] for s in sections(data) if s in DRM_SECTIONS})
|
||||
if any(any(x in n for x in ANTICHEAT_FILES) for n in names):
|
||||
drm.append("anti-cheat files")
|
||||
res["drm"] = drm
|
||||
# other runtime needs
|
||||
# Imports in modular engine/audio/video DLLs matter too. Reuse this for each candidate exe below; companion
|
||||
# middleware is a folder-level hint, not proof that a particular movie or audio route will be exercised.
|
||||
runtime_imports = set(imports)
|
||||
for p in files:
|
||||
if p.suffix.lower() == ".dll" and p.parent == exe.parent and p.stat().st_size <= 80 << 20:
|
||||
runtime_imports |= imports_of(p)
|
||||
res["runtime"] = runtime_facts(runtime_imports, names)
|
||||
res["executables"] = []
|
||||
for candidate in a.extra.get("exe_candidates", []):
|
||||
rel = candidate["path"]
|
||||
p = game_dir / rel
|
||||
try:
|
||||
pi = info if p == exe else rift.read_pe(p, limit=80 << 20)[0]
|
||||
facts = runtime_facts(set(pi.imports) | set(pi.delay_imports), names)
|
||||
g = sorted({GRAPHICS[i] for i in set(pi.imports) | set(pi.delay_imports) if i in GRAPHICS})
|
||||
res["executables"].append(dict(exe=rel, arch=pi.machine, exe_32bit=pi.machine == "x86",
|
||||
graphics_imports=g, **facts))
|
||||
except (OSError, rift.PEError, struct.error, ValueError):
|
||||
res["executables"].append({"exe": rel, "error": "PE unreadable"})
|
||||
other = []
|
||||
if "mscoree.dll" in imports:
|
||||
other.append(".NET")
|
||||
if res["runtime"]["xaudio2"]:
|
||||
other.append("XAudio2")
|
||||
if res["runtime"]["media_foundation"]:
|
||||
other.append("Media Foundation: video playback is a Proton compatibility risk")
|
||||
if res["runtime"]["bink"]:
|
||||
other.append("Bink video")
|
||||
winrt_re = rb"W\x00i\x00n\x00d\x00o\x00w\x00s\x00\.\x00(?:[A-Z]\x00[a-zA-Z\x00\.]{4,80})"
|
||||
winrt = sorted({w.decode("utf-16-le") for w in re.findall(winrt_re, data)})[:5]
|
||||
if winrt:
|
||||
other.append("WinRT: " + ", ".join(winrt))
|
||||
res["other"] = other
|
||||
res["size_gib"] = round(sum(p.stat().st_size for p in game_dir.rglob("*") if p.is_file()) / 2**30, 1)
|
||||
res["verdict"] = verdict(res)
|
||||
res["suggested_graphics"] = graphics_route(res)
|
||||
res["profile"] = suggested_profile(res)
|
||||
return res
|
||||
|
||||
|
||||
def runtime_facts(imports: set[str], names) -> dict:
|
||||
imports = {i.lower() for i in imports}
|
||||
names = {n.lower() for n in names}
|
||||
return {"xaudio2": any(i.startswith("xaudio2") for i in imports),
|
||||
"media_foundation": bool(imports & {"mf.dll", "mfplat.dll", "mfreadwrite.dll", "evr.dll"}),
|
||||
"bink": any(n.startswith("bink") for n in names | imports),
|
||||
"anti_cheat": any(any(s in n for s in ANTICHEAT_FILES) for n in names | imports)}
|
||||
|
||||
|
||||
def graphics_route(r: dict) -> str:
|
||||
g = r.get("graphics", [])
|
||||
# Unreal's config already reduced its imports to the selected RHI. For custom multi-API engines prefer a
|
||||
# DXVK route over GL; imports prove availability, not that the game will select that renderer at runtime.
|
||||
return next((api for api in ("d3d11", "d3d12", "d3d9", "opengl", "vulkan") if api in g), "d3d11")
|
||||
|
||||
|
||||
def suggested_profile(r: dict) -> dict:
|
||||
status = {"likely OK": "likely", "blocked": "blocked"}.get(r["verdict"][0], "untested")
|
||||
profile = {"title": r["title"],
|
||||
"match": {"executables": [{"name": Path(r["exe"]).name, "size": r["exe_size"]}]},
|
||||
"status": status, "graphics": graphics_route(r), "env": {}, "ime_patch": None,
|
||||
"fp_mem": "candidate" if "candidate" in r.get("large_block_alloc", "") else "off",
|
||||
"bigcache_mb": 256, "dxvk": [],
|
||||
"notes": r["verdict"][1] + ("; Media Foundation videos need a playback test" if
|
||||
r.get("runtime", {}).get("media_foundation") else "")}
|
||||
if r.get("vr") == ["OpenVR"]:
|
||||
profile["revive"] = False
|
||||
if r.get("folder"):
|
||||
info = {"exe": r["exe"], "game_dir": r["folder"]}
|
||||
for hook in FX.GAME_INFO_HOOKS:
|
||||
hook("", info)
|
||||
# Keep an existing build-matched direct-OpenVR choice (e.g. Unity that also contains inert LibOVR strings)
|
||||
# when generating pasteable data. Do not propagate guesses from the folder/title or a bundled crack.
|
||||
if info.get("revive") is False:
|
||||
profile["revive"] = False
|
||||
return profile
|
||||
|
||||
|
||||
def verdict(r: dict) -> tuple[str, str]:
|
||||
"""(likely OK | needs work | blocked, why). "likely OK" = the profile First Contact has on the Frame: x64,
|
||||
D3D11, Unreal with the IME fix site found (or an engine without that deadlock), a VR API Revive/SteamVR serve."""
|
||||
blockers, work = [], []
|
||||
if r.get("arch") not in ("x86_64", "amd64", "AMD64"):
|
||||
work.append(f"{r.get('arch')} exe: needs GE's 32-bit (wow64) path under FEX (untested)")
|
||||
g = r.get("graphics", [])
|
||||
if "d3d11" not in g:
|
||||
if "d3d12" in g:
|
||||
work.append("D3D12: needs vkd3d-proton on Turnip under FEX (untested)")
|
||||
elif "d3d9" in g:
|
||||
work.append("D3D9: DXVK under FEX (untested)")
|
||||
elif "opengl" in g:
|
||||
work.append("OpenGL: no FEX GL thunk configured; rootfs GL renderer unverified")
|
||||
elif "vulkan" in g:
|
||||
work.append("Vulkan: Turnip through FEX's Vulkan thunk (untested with a game)")
|
||||
else:
|
||||
work.append(f"graphics API unclear ({', '.join(g)})")
|
||||
if "anti-cheat files" in r.get("drm", []) or r.get("runtime", {}).get("anti_cheat"):
|
||||
blockers.append("anti-cheat")
|
||||
elif r.get("drm"):
|
||||
work.append("anti-tamper section (" + ", ".join(r["drm"]) + "): heavier under FEX, may break")
|
||||
if r.get("vr") == ["?"]:
|
||||
work.append("VR API not detected")
|
||||
if r.get("engine", "").startswith("Unreal") and isinstance(r.get("ime_patch"), str):
|
||||
work.append(f"IME fix site not found ({r['ime_patch']})")
|
||||
if r.get("engine") in ("custom", "CryEngine"):
|
||||
work.append(f"{r['engine']} engine: no Unreal/Unity fixes apply; startup/allocator behaviour unknown")
|
||||
if blockers:
|
||||
return "blocked", "; ".join(blockers + work)
|
||||
if work:
|
||||
return "needs work", "; ".join(work)
|
||||
return "likely OK", "same profile as First Contact (x64, D3D11, " + "/".join(r.get("vr", [])) + ")"
|
||||
|
||||
|
||||
def library_games():
|
||||
for g in library.games():
|
||||
if not str(g.get("package", "")).startswith("rift."):
|
||||
continue
|
||||
extra = (g.get("analysis") or {}).get("extra") or {}
|
||||
gd = g.get("game_dir") or extra.get("folder") or ""
|
||||
yield g.get("title") or g["package"], Path(gd), g.get("exe") or extra.get("exe")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--folder", action="append", default=[])
|
||||
ap.add_argument("--out", default=str(user_data_dir() / "fexrift"))
|
||||
args = ap.parse_args()
|
||||
if args.folder:
|
||||
games = []
|
||||
for f in args.folder:
|
||||
a = rift.analyze(Path(f))
|
||||
games.append((a.label, Path(f), a.extra.get("exe")))
|
||||
else:
|
||||
games = list(library_games())
|
||||
results = []
|
||||
for title, d, exe in sorted(games, key=lambda t: t[0].lower()):
|
||||
try:
|
||||
r = survey_game(title, d, exe)
|
||||
except Exception as e: # noqa: BLE001 - one broken folder shouldn't stop the survey
|
||||
r = {"title": title, "folder": str(d), "verdict": ("error", f"{type(e).__name__}: {e}")}
|
||||
results.append(r)
|
||||
print(f"{r['verdict'][0]:11s} {title}", flush=True)
|
||||
out = Path(args.out)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / "survey.json").write_text(json.dumps(results, indent=1))
|
||||
profiles = {re.sub(r"[^a-z0-9]+", "-", r["title"].lower()).strip("-"): r["profile"]
|
||||
for r in results if r.get("profile")}
|
||||
(out / "profiles.json").write_text(json.dumps(profiles, indent=2) + "\n")
|
||||
lines = ["# Rift-on-Frame static survey", "",
|
||||
"| Game | Verdict | Engine | Graphics | VR | Platform SDK | IME fix | AA config | Notes |",
|
||||
"|---|---|---|---|---|---|---|---|---|"]
|
||||
for r in results:
|
||||
cfg = r.get("config") or {}
|
||||
aa = ", ".join(f"{k}={v}" for k, v in cfg.items() if "MSAA" in k or "AntiAliasing" in k) or r.get("pak", "")
|
||||
ime = r.get("ime_patch")
|
||||
if isinstance(ime, dict):
|
||||
ime = ime["rva"] + ("" if ime.get("module") == "exe" else f" ({ime['module']})")
|
||||
else:
|
||||
ime = str(ime)[:40] if r.get("engine", "").startswith("Unreal") else "n/a"
|
||||
graphics = ", ".join(r.get("graphics", [])) + (f" ({r['graphics_from']})" if r.get("graphics_from") else "")
|
||||
lines.append(f"| {r['title']} | {r['verdict'][0]} | {r.get('engine', '')} | {graphics} | "
|
||||
f"{', '.join(r.get('vr', []))} | {'yes' if r.get('platform_sdk') else 'no'} | {ime} | {aa} | "
|
||||
f"{r['verdict'][1]} |")
|
||||
(out / "survey.md").write_text("\n".join(lines) + "\n")
|
||||
print(f"report: {out / 'survey.md'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -179,11 +179,6 @@
|
||||
"title": "Steadier hands (older Unity)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.ovrp_trace": {
|
||||
"anchor": "adapter-ovrp_trace",
|
||||
"title": "Unity frame loop diagnostics (log)",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.passthrough_emul": {
|
||||
"anchor": "adapter-passthrough_emul",
|
||||
"title": "See your room",
|
||||
@@ -209,11 +204,6 @@
|
||||
"title": "Treat newer Quest controllers as Touch",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.projection_only": {
|
||||
"anchor": "adapter-projection_only",
|
||||
"title": "Main picture only",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.proximity_emul": {
|
||||
"anchor": "adapter-proximity_emul",
|
||||
"title": "Animate fingers from touch",
|
||||
@@ -244,11 +234,6 @@
|
||||
"title": "Sharpness",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scale_max": {
|
||||
"anchor": "adapter-scale_max",
|
||||
"title": "Cap the eye size",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.scene_depth": {
|
||||
"anchor": "adapter-scene_depth",
|
||||
"title": "Room depth",
|
||||
@@ -649,11 +634,6 @@
|
||||
"title": "Vivox voice chat: skip Android 12 audio routing",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vk_android_extensions": {
|
||||
"anchor": "frame-vk_android_extensions",
|
||||
"title": "Vulkan: list Android's external-memory extensions (this PC)",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.vk_sanitize": {
|
||||
"anchor": "frame-vk_sanitize",
|
||||
"title": "Vulkan: drop invalid render-pass pointers",
|
||||
@@ -799,11 +779,6 @@
|
||||
"title": "VrApi → OpenXR adapter (OVRPort)",
|
||||
"group": "Patches from OVRPort"
|
||||
},
|
||||
"pc.axrb_backend": {
|
||||
"anchor": "pc-axrb_backend",
|
||||
"title": "AXRB mode on this PC",
|
||||
"group": "Files and settings on the Frame"
|
||||
},
|
||||
"pcvr.launch_args": {
|
||||
"anchor": "pcvr-launch_args",
|
||||
"title": "Launch options (SteamVR or OpenXR)",
|
||||
|
||||
@@ -43,11 +43,7 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
|
||||
# 17: headset (the Meta headset a VR app was made for: Gear VR / Go games run through frame.a32_bridge) (2026-10)
|
||||
# 18: headset "quest" also for 32-bit apps that require 6DoF head tracking (Santa Cruz builds, e.g. Sports
|
||||
# Scramble: Touch controllers on the a32 bridge) (2026-10)
|
||||
# 19: unreal_obb_delivery also from obfuscated DownloaderActivity predicates (#159 Medal of Honor) (2026-10)
|
||||
# 20: Unreal's Vulkan RHI from the manifest meta-data GameActivity.bSupportsVulkan (Unreal games never declare
|
||||
# android.hardware.vulkan, so every Unreal Vulkan game was "GLES or unknown"; AXRB direct mode is Vulkan only),
|
||||
# and "Vulkan (VrApi Vulkan system)" for direct-VrApi engines that import vrapi_CreateSystemVulkan (The Climb 2)
|
||||
ANALYSIS_VERSION = 20
|
||||
ANALYSIS_VERSION = 19
|
||||
|
||||
|
||||
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
|
||||
@@ -91,18 +87,6 @@ def cryengine_project(data: bytes) -> bool:
|
||||
UE_VERIFY_OBB_KEY = ".GameActivity.bVerifyOBBOnStartUp"
|
||||
|
||||
|
||||
UE_SUPPORTS_VULKAN_KEY = ".GameActivity.bSupportsVulkan"
|
||||
|
||||
|
||||
def unreal_vulkan(meta: dict) -> bool:
|
||||
"""Unreal packaged the game with its Vulkan RHI (Project Settings > Android > Support Vulkan): UnrealBuildTool
|
||||
writes `com.epicgames.{ue4,unreal}.GameActivity.bSupportsVulkan` into the manifest, and on a Vulkan device (every
|
||||
Quest) GameActivity starts the Vulkan RHI, GLES only as a fallback. GLES-only builds write false (UE 4.21+) or
|
||||
nothing (older). Only a real boolean true counts, as GameActivity reads it with Bundle.getBoolean."""
|
||||
return any(k.startswith("com.epicgames.") and k.endswith(UE_SUPPORTS_VULKAN_KEY) and v is True
|
||||
for k, v in meta.items())
|
||||
|
||||
|
||||
def unreal_verify_obb(meta: dict) -> bool:
|
||||
"""Unreal's DownloaderActivity checks every OBB entry's CRC at the first start (a full read; on a mismatch it waits
|
||||
on an error screen). Only a real boolean counts: GameActivity reads it with Bundle.getBoolean."""
|
||||
@@ -303,17 +287,12 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
direct_vrapi = any("libvrapi.so" in elf.needed(b) for n, b in lib_bytes.items()
|
||||
if elf.is_elf(b) and n not in ("libOVRPlugin.so", "libvrapi.so"))
|
||||
vulkan_declared = b"android.hardware.vulkan" in manifest or _uses_feature(manifest, "android.hardware.vulkan")
|
||||
meta = axml.meta_data(manifest)
|
||||
if vulkan_declared:
|
||||
graphics = "Vulkan (declared in manifest)"
|
||||
elif engine == "Unreal" and unreal_vulkan(meta):
|
||||
graphics = "Vulkan (Unreal bSupportsVulkan)"
|
||||
elif engine == "Unity" and "vulkan" in boot.lower():
|
||||
graphics = "Vulkan (Unity boot.config)"
|
||||
elif engine == "CryEngine" and re.search(r'(?im)^\s*r_Driver\s*=\s*"?vk"?\s*(?:;.*)?$', system_cfg):
|
||||
graphics = "Vulkan (CryEngine system.cfg)"
|
||||
elif direct_vrapi and vrapi_vulkan(lib_bytes):
|
||||
graphics = "Vulkan (VrApi Vulkan system)"
|
||||
else:
|
||||
graphics = "GLES or unknown (no Vulkan declaration)"
|
||||
|
||||
@@ -344,6 +323,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
manifest_strings = axml.Axml(manifest).strings()
|
||||
features = _features(manifest)
|
||||
used_perms, _ = axml.used_and_declared_permissions(manifest)
|
||||
meta = axml.meta_data(manifest)
|
||||
return Analysis(
|
||||
package=package,
|
||||
version=version,
|
||||
@@ -539,14 +519,6 @@ def _uses_feature(manifest: bytes, feature: str) -> bool:
|
||||
OVR_TO_STRING = re.compile(r"^ovr[A-Z][A-Za-z0-9]*_ToString$")
|
||||
|
||||
|
||||
def vrapi_vulkan(lib_bytes: dict[str, bytes]) -> bool:
|
||||
"""An engine library that calls VrApi directly creates VrApi's Vulkan system (vrapi_CreateSystemVulkan): the game
|
||||
renders with Vulkan even without a manifest declaration (The Climb 2's CryEngine). GLES VrApi games (Path of the
|
||||
Warrior) never import it. Only the game's own libraries count, not libvrapi.so or OVRPlugin."""
|
||||
return any(elf.is_elf(b) and "vrapi_CreateSystemVulkan" in elf.dyn_symbols(b, False)
|
||||
for n, b in lib_bytes.items() if n not in ("libvrapi.so", "libOVRPlugin.so"))
|
||||
|
||||
|
||||
def missing_ovr_symbols(lib_bytes: dict[str, bytes]) -> set[str]:
|
||||
"""ovr_* / ovrMessageType_* / ovr<Enum>_ToString functions the game imports that the platform loader (+compat/stub
|
||||
libs) lacks."""
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Read unencrypted Unreal paks and DefaultEngine.ini without extracting game files (stdlib only)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
# ------------------------------------------------------------------------------------------ Unreal paks
|
||||
PAK_MAGIC = 0x5A6F12E1
|
||||
# pak format version -> the Unreal versions that write it (when the exe doesn't name its version)
|
||||
PAK_UE = {1: "4.0-4.2", 2: "4.0-4.2", 3: "4.3-4.15", 4: "4.16-4.19", 5: "4.20", 6: "4.20", 7: "4.21",
|
||||
8: "4.22-4.24", 9: "4.25", 10: "4.26", 11: "4.26-4.27"}
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, buf: bytes, pos: int = 0):
|
||||
self.buf, self.p = buf, pos
|
||||
|
||||
def take(self, fmt: str):
|
||||
v = struct.unpack_from(fmt, self.buf, self.p)
|
||||
self.p += struct.calcsize(fmt)
|
||||
return v if len(v) > 1 else v[0]
|
||||
|
||||
def fstring(self) -> str:
|
||||
n = self.take("<i")
|
||||
if n < 0:
|
||||
s = self.buf[self.p:self.p - 2 * n].decode("utf-16-le", "replace")
|
||||
self.p += -2 * n
|
||||
else:
|
||||
s = self.buf[self.p:self.p + n].decode("latin-1")
|
||||
self.p += n
|
||||
return s.rstrip("\0")
|
||||
|
||||
|
||||
def entry_header_size(ver: int, compressed: bool, nblocks: int) -> int:
|
||||
"""Size of the FPakEntry copy in front of a file's data (offset, sizes, method, hash, blocks, flags, block size)."""
|
||||
return 8 + 8 + 8 + 4 + 20 + (4 + 16 * nblocks if compressed else 0) + (1 + 4 if ver >= 3 else 0)
|
||||
|
||||
|
||||
def legacy_entry(r: Reader, ver: int):
|
||||
"""A full FPakEntry: (offset, compressed size, size, method, blocks, encrypted)."""
|
||||
off, csize, usize = r.take("<qqq")
|
||||
method = r.take("<i")
|
||||
r.p += 20
|
||||
blocks, enc = [], 0
|
||||
if ver >= 3:
|
||||
if method:
|
||||
nb = r.take("<i")
|
||||
blocks = [r.take("<qq") for _ in range(nb)]
|
||||
enc = r.buf[r.p]
|
||||
r.p += 1 + 4
|
||||
return off, csize, usize, method, blocks, enc
|
||||
|
||||
|
||||
def encoded_entry(buf: bytes, pos: int, ver: int):
|
||||
"""A v10+ bit-packed entry (FPakFile::DecodePakEntry); blocks relative to the entry like v7+ full entries."""
|
||||
r = Reader(buf, pos)
|
||||
v = r.take("<I")
|
||||
if v & 0x3f == 0x3f:
|
||||
r.take("<I") # compression block size (not needed to read)
|
||||
method = (v >> 23) & 0x3f
|
||||
off = r.take("<I" if v & (1 << 31) else "<Q")
|
||||
usize = r.take("<I" if v & (1 << 30) else "<Q")
|
||||
csize = r.take("<I" if v & (1 << 29) else "<Q") if method else usize
|
||||
enc = bool(v & (1 << 22))
|
||||
nb = (v >> 6) & 0xffff
|
||||
base = entry_header_size(ver, bool(method), nb)
|
||||
blocks = []
|
||||
if nb == 1 and not enc:
|
||||
blocks = [(base, base + csize)]
|
||||
elif nb:
|
||||
o = base
|
||||
for _ in range(nb):
|
||||
s = r.take("<I")
|
||||
blocks.append((o, o + s))
|
||||
o += (s + 15) & ~15 if enc else s
|
||||
return off, csize, usize, method, blocks, enc
|
||||
|
||||
|
||||
def pak_entries(path: Path):
|
||||
"""(version, mount point, {name: entry}, compression names) of an unencrypted UE4 pak, v1-v11; ValueError if the
|
||||
index can't be read (encrypted, unknown footer)."""
|
||||
with open(path, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(max(0, size - 512))
|
||||
tail = f.read()
|
||||
i = tail.rfind(struct.pack("<I", PAK_MAGIC), 0, len(tail) - 40)
|
||||
while i >= 0 and len(tail) - i not in (44, 44 + 4 * 32, 44 + 5 * 32): # magic..end, 0/4/5 method names
|
||||
i = tail.rfind(struct.pack("<I", PAK_MAGIC), 0, i)
|
||||
if i < 0:
|
||||
raise ValueError("no pak footer")
|
||||
_, ver, ioff, isize = struct.unpack_from("<IIqq", tail, i)
|
||||
if ver >= 4 and i >= 1 and tail[i - 1] == 1:
|
||||
raise ValueError(f"pak v{ver}: index encrypted")
|
||||
names = [tail[j:j + 32].split(b"\0")[0].decode("latin-1") for j in range(i + 44, len(tail) - 31, 32)]
|
||||
if not 0 <= ioff < size or not 0 < isize <= size - ioff:
|
||||
raise ValueError(f"pak v{ver}: index outside the file")
|
||||
f.seek(ioff)
|
||||
r = Reader(f.read(isize))
|
||||
mount = r.fstring()
|
||||
count = r.take("<i")
|
||||
entries = {}
|
||||
if ver < 10:
|
||||
for _ in range(count):
|
||||
name = r.fstring()
|
||||
entries[name] = legacy_entry(r, ver)
|
||||
return ver, mount, entries, names
|
||||
r.take("<Q") # path hash seed
|
||||
if r.take("<i"):
|
||||
r.take("<qq")
|
||||
r.p += 20 # path hash index: hashes only, no names
|
||||
if not r.take("<i"):
|
||||
raise ValueError(f"pak v{ver}: no full directory index (names hashed only)")
|
||||
fdi_off, fdi_size = r.take("<qq")
|
||||
r.p += 20
|
||||
enc_size = r.take("<i")
|
||||
encoded = r.buf[r.p:r.p + enc_size]
|
||||
r.p += enc_size
|
||||
files = [legacy_entry(r, ver) for _ in range(r.take("<i"))]
|
||||
f.seek(fdi_off)
|
||||
d = Reader(f.read(fdi_size))
|
||||
for _ in range(d.take("<i")):
|
||||
folder = d.fstring()
|
||||
for _ in range(d.take("<i")):
|
||||
name, loc = d.fstring(), d.take("<i")
|
||||
entries[(folder + name).lstrip("/")] = encoded_entry(encoded, loc, ver) if loc >= 0 else files[-loc - 1]
|
||||
return ver, mount, entries, names
|
||||
|
||||
|
||||
def pak_read(path: Path, ver: int, entry, names: list[str] | None = None) -> bytes:
|
||||
off, csize, usize, method, blocks, enc = entry
|
||||
if enc:
|
||||
raise ValueError("entry encrypted")
|
||||
with open(path, "rb") as f:
|
||||
if not method:
|
||||
f.seek(off + entry_header_size(ver, False, 0))
|
||||
return f.read(usize)
|
||||
kind = (names[method - 1] if ver >= 8 and names and 0 < method <= len(names) else "zlib").lower()
|
||||
if kind not in ("zlib", "gzip"):
|
||||
raise ValueError(f"{kind}-compressed")
|
||||
out = b""
|
||||
for s, e in blocks:
|
||||
if ver >= 7:
|
||||
s, e = s + off, e + off
|
||||
f.seek(s)
|
||||
out += zlib.decompress(f.read(e - s), 31 if kind == "gzip" else 15)
|
||||
return out
|
||||
|
||||
|
||||
CONFIG_KEYS = ("r.MobileMSAA", "r.MSAACount", "r.DefaultFeature.AntiAliasing", "r.ForwardShading",
|
||||
"r.useclusteredforward", "vr.InstancedStereo", "r.MobileHDR", "DefaultGraphicsRHI", "TargetedRHIs",
|
||||
"bUseVulkan")
|
||||
|
||||
|
||||
def parse_engine_ini(text: str) -> dict:
|
||||
"""The keys survey cares about; repeated keys (+TargetedRHIs=…) become lists."""
|
||||
want = {k.lower(): k for k in CONFIG_KEYS}
|
||||
vals: dict = {}
|
||||
for line in text.splitlines():
|
||||
k, _, v = line.strip().partition("=")
|
||||
key = want.get(k.lstrip("+-.!").lower())
|
||||
if not key:
|
||||
continue
|
||||
if key == "TargetedRHIs":
|
||||
vals.setdefault(key, [])
|
||||
if k.startswith("-"):
|
||||
vals[key] = [x for x in vals[key] if x != v]
|
||||
elif v not in vals[key]:
|
||||
vals[key].append(v)
|
||||
else:
|
||||
vals[key] = v
|
||||
return vals
|
||||
|
||||
|
||||
def unreal_config(game_dir: Path) -> dict:
|
||||
"""Pak format + rendering/AA settings from the project's DefaultEngine.ini (paks, else a loose file)."""
|
||||
paks = sorted(game_dir.rglob("*.pak"), key=lambda p: -p.stat().st_size)
|
||||
notes, ver_seen = [], None
|
||||
for pak in paks[:6]:
|
||||
try:
|
||||
ver, _, entries, names = pak_entries(pak)
|
||||
except (ValueError, OSError, struct.error, IndexError) as e:
|
||||
notes.append(f"{pak.name}: {e}")
|
||||
continue
|
||||
ver_seen = ver_seen or ver
|
||||
name = next((n for n in entries if n.endswith("Config/DefaultEngine.ini")), None)
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
text = pak_read(pak, ver, entries[name], names).decode("utf-8-sig", "replace")
|
||||
except (ValueError, zlib.error, OSError) as e:
|
||||
notes.append(f"{pak.name}: DefaultEngine.ini {e}")
|
||||
continue
|
||||
return {"pak": f"v{ver}", "config": parse_engine_ini(text), "pak_notes": notes}
|
||||
loose = next((p for p in game_dir.rglob("DefaultEngine.ini") if p.parent.name == "Config"), None)
|
||||
if loose:
|
||||
text = loose.read_text("utf-8", "replace")
|
||||
return {"pak": f"v{ver_seen}" if ver_seen else "", "config": parse_engine_ini(text),
|
||||
"pak_notes": notes + ["config from a loose DefaultEngine.ini"]}
|
||||
return {"pak": f"v{ver_seen}" if ver_seen else "unreadable", "pak_notes": notes or ["no DefaultEngine.ini found"]}
|
||||
|
||||
@@ -361,25 +361,6 @@ def component_filters(manifest: bytes) -> list[tuple[str, str, list[tuple[str, i
|
||||
return out
|
||||
|
||||
|
||||
def launcher_activity(manifest: bytes) -> str | None:
|
||||
""""<package>/<fully qualified class>" of the activity Android's launcher starts (MAIN + LAUNCHER; an alias
|
||||
names its targetActivity), what `cmd package resolve-activity --brief` answers; None when there is none."""
|
||||
x = Axml(manifest)
|
||||
names = x.strings()
|
||||
package = next((x.attr_str(el, "package") for el in x.elements() if el.name == "manifest"), None)
|
||||
targets = {x.attr_str(el, "name"): x.attr_str(el, "targetActivity")
|
||||
for el in x.elements() if el.name == "activity-alias"}
|
||||
if not package:
|
||||
return None
|
||||
for el, name, items in component_filters(manifest):
|
||||
found = {names[i] for _, _, i in items if i < len(names)}
|
||||
if el in ("activity", "activity-alias") and name and MAIN in found and LAUNCHER in found:
|
||||
cls = (targets.get(name) or name) if el == "activity-alias" else name
|
||||
cls = package + cls if cls.startswith(".") else cls if "." in cls else f"{package}.{cls}"
|
||||
return f"{package}/{cls}"
|
||||
return None
|
||||
|
||||
|
||||
def vr_activity(manifest: bytes) -> str | None:
|
||||
"""The activity with Meta's VR category when it isn't the launcher activity (a 2D launcher that starts the VR
|
||||
part, e.g. WiiCompiled's LauncherActivity -> QuestActivity)."""
|
||||
|
||||
+7
-37
@@ -1,12 +1,11 @@
|
||||
"""Build stage: OVRPort → Frame fixes (apk-stage patches) → sign → static validation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .apk import axml, sign
|
||||
from .apk import sign
|
||||
from .apk.workspace import ApkWorkspace
|
||||
from .core.events import Reporter
|
||||
from .core.models import Analysis, BuildResult, Recipe, SourceGame
|
||||
@@ -43,9 +42,9 @@ def apk_patches(selection: dict) -> list[base.Patch]:
|
||||
|
||||
|
||||
def apply_frame_fixes(apk_in: Path, apk_out_unsigned: Path, analysis: Analysis, recipe: Recipe,
|
||||
reporter: Reporter, pc: bool = False) -> tuple[list[str], list[dict], dict[str, str]]:
|
||||
reporter: Reporter) -> tuple[list[str], list[dict], dict[str, str]]:
|
||||
"""Apply the recipe's apk-stage patches; returns (applied ids, checks, workarounds left out because OVRPort's
|
||||
output already has the upstream fix (patches/upstream.py)). pc: the build for this PC (extract_native_libs)."""
|
||||
output already has the upstream fix (patches/upstream.py))."""
|
||||
applied, checks = [], []
|
||||
with ApkWorkspace(apk_in) as ws:
|
||||
selection = upstream.resolve(ws, recipe.patches, reporter)
|
||||
@@ -57,26 +56,10 @@ def apply_frame_fixes(apk_in: Path, apk_out_unsigned: Path, analysis: Analysis,
|
||||
reporter.log(f"applied {patch.id}" + (f" ({'; '.join(ctx.notes)})" if ctx.notes else ""))
|
||||
for name, ok, detail in patch.validate(ctx):
|
||||
checks.append({"name": name, "ok": ok, "detail": detail})
|
||||
if pc and "frame.adapter" in selection.patches and extract_native_libs(ws):
|
||||
reporter.log("AndroidManifest.xml: extractNativeLibs on (FrameBridge reads its settings next to itself)")
|
||||
ws.write(apk_out_unsigned)
|
||||
return applied, checks, selection.superseded
|
||||
|
||||
|
||||
def extract_native_libs(ws: ApkWorkspace) -> bool:
|
||||
"""Builds for this PC: have Android unpack the native libraries. FrameBridge reads lib/<abi>/libframe_settings.so
|
||||
with fopen next to its own path (dladdr); an app with extractNativeLibs="false" maps its libraries straight from
|
||||
base.apk ("base.apk!/lib/..."), where fopen finds nothing. On the Frame the agent also hands the adapter the
|
||||
settings (settings.conf via FRAMEBRIDGE_CONFIG, framebridge.conf in the game's storage), so the Frame build stays
|
||||
unchanged; in AXRB the library is the only source."""
|
||||
manifest = ws.read("AndroidManifest.xml")
|
||||
changed = axml.set_bool_attr(manifest, "application", "extractNativeLibs", True)
|
||||
if not changed or changed == manifest:
|
||||
return False
|
||||
ws.put("AndroidManifest.xml", changed)
|
||||
return True
|
||||
|
||||
|
||||
def check_a32(analysis: Analysis, recipe: Recipe) -> None:
|
||||
"""A 32-bit-only game builds only through the 32-bit bridge, and without what the bridge replaces: OVRPort and
|
||||
the 64-bit fixes would fail deep inside a tool or give an APK the Frame refuses (INSTALL_FAILED_NO_MATCHING_ABIS).
|
||||
@@ -94,17 +77,9 @@ def check_a32(analysis: Analysis, recipe: Recipe) -> None:
|
||||
|
||||
|
||||
def build(source: SourceGame, analysis: Analysis, recipe: Recipe, outdir: Path, reporter: Reporter,
|
||||
keep_work: bool = False, pc: bool = False) -> BuildResult:
|
||||
"""pc: the build for this PC (AXRB): only OVRPort's patches and the on_pc APK fixes (base.pc_selection)."""
|
||||
keep_work: bool = False) -> BuildResult:
|
||||
pkg = analysis.package
|
||||
if pc:
|
||||
patches = base.pc_selection(recipe.patches, analysis)
|
||||
if not recipe.overport: # FrameBridge wraps OVRPort's loader: nothing to wrap without OVRPort
|
||||
patches = {pid: p for pid, p in patches.items()
|
||||
if pid != "frame.adapter" and not pid.startswith("adapter.")}
|
||||
recipe = dataclasses.replace(recipe, patches=patches)
|
||||
else:
|
||||
check_a32(analysis, recipe)
|
||||
check_a32(analysis, recipe)
|
||||
work = work_dir() / pkg
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
work.mkdir(parents=True)
|
||||
@@ -115,9 +90,6 @@ def build(source: SourceGame, analysis: Analysis, recipe: Recipe, outdir: Path,
|
||||
# Frame patches are applied on top. Its alternate build is the copy saved next to it, or (no copy) one more
|
||||
# OVRPort run for the alternate patches; the stand-ins patch relinks what that drops.
|
||||
converted = recipe.overport and analysis.is_overport_output
|
||||
if pc and converted:
|
||||
reporter.check("Original APK", None, "this copy was already converted for the Steam Frame and keeps its Frame "
|
||||
"fixes; add the game's original APK for a clean build for this PC")
|
||||
variants = [("primary", False)] + ([("alt", True)] if recipe.alt_patches and recipe.overport else [])
|
||||
alt_copy = Path(source.apk).with_name(f"{pkg}.alt-noforcequit.apk")
|
||||
results = {}
|
||||
@@ -136,8 +108,7 @@ def build(source: SourceGame, analysis: Analysis, recipe: Recipe, outdir: Path,
|
||||
shutil.copyfile(input_apk, patched)
|
||||
reporter.stage(f"Frame fixes ({variant})")
|
||||
unsigned = work / f"{pkg}.{variant}.unsigned.apk"
|
||||
applied, checks, left_out = apply_frame_fixes(patched, unsigned, analysis, recipe, reporter,
|
||||
**({"pc": True} if pc else {}))
|
||||
applied, checks, left_out = apply_frame_fixes(patched, unsigned, analysis, recipe, reporter)
|
||||
if variant == "primary":
|
||||
superseded = left_out
|
||||
patched.unlink(missing_ok=True)
|
||||
@@ -146,8 +117,7 @@ def build(source: SourceGame, analysis: Analysis, recipe: Recipe, outdir: Path,
|
||||
sign.sign(unsigned, final, pkg)
|
||||
unsigned.unlink(missing_ok=True)
|
||||
reporter.stage(f"validate ({variant})")
|
||||
checks += check_apk(final, pkg, expect_adapter=recipe.overport and "frame.adapter" in recipe.patches
|
||||
if pc else recipe.overport)
|
||||
checks += check_apk(final, pkg, expect_adapter=recipe.overport)
|
||||
for c in checks:
|
||||
reporter.check(c["name"], c["ok"], c["detail"])
|
||||
results[variant] = final
|
||||
|
||||
+147
-77
@@ -54,6 +54,9 @@ app.add_typer(tools_app, name="tools")
|
||||
app.add_typer(frame_app, name="frame")
|
||||
app.add_typer(pc_app, name="pc")
|
||||
app.add_typer(diag_app, name="diag")
|
||||
fex_app = typer.Typer(help="Experimental (FRAMEPORT_EXPERIMENTAL_FEX=1): Oculus Rift games with Meta's PC runtime on "
|
||||
"the Frame alone (x86_64 GE-Proton under FEX). Verified game: Oculus First Contact.")
|
||||
app.add_typer(fex_app, name="fex", hidden=True)
|
||||
|
||||
|
||||
def _show_version(value: bool):
|
||||
@@ -460,18 +463,6 @@ def build(package: Optional[str] = typer.Argument(None), all_: bool = typer.Opti
|
||||
raise typer.Exit(1 if failed else 0)
|
||||
|
||||
|
||||
def _set_axrb_backend(target, backend: Optional[str]) -> None:
|
||||
if backend is None:
|
||||
return
|
||||
from .tools import axrb
|
||||
|
||||
if backend not in axrb.BACKENDS:
|
||||
raise typer.BadParameter(f"--axrb-backend: {' or '.join(axrb.BACKENDS)}")
|
||||
if getattr(target, "kind", "") != "pc":
|
||||
raise typer.BadParameter("--axrb-backend needs --to pc")
|
||||
target.axrb_backend = backend
|
||||
|
||||
|
||||
@app.command()
|
||||
def install(package: Optional[str] = typer.Argument(None), all_: bool = typer.Option(False, "--all", help=ALL_HELP),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
|
||||
@@ -479,19 +470,14 @@ def install(package: Optional[str] = typer.Argument(None), all_: bool = typer.Op
|
||||
apk_only: bool = typer.Option(False, help="reuse game data already on the Frame"),
|
||||
force: bool = typer.Option(False, help="rebuild and reinstall even if the Frame is already current"),
|
||||
no_library: bool = typer.Option(False, help="don't add to the Steam library now"),
|
||||
to: str = typer.Option("frame", help="frame, or pc (install on this PC: PC VR games through Revive, "
|
||||
"Quest games through AXRB, experimental)"),
|
||||
to: str = typer.Option("frame", help="frame, or pc (PC VR games only: install on this PC)"),
|
||||
apk: Optional[Path] = typer.Option(None, help="install this APK instead of the last build (one game; "
|
||||
"for example a test build signed with the game's key)"),
|
||||
dest: Optional[str] = typer.Option(None, help="drive for new installs: 'internal' or a drive's path from "
|
||||
"'frameport frame drives' (default: the app's setting); "
|
||||
"installed games stay where they are"),
|
||||
axrb_backend: Optional[str] = typer.Option(None, help="with --to pc, Quest games: AXRB's 'direct' mode "
|
||||
"(no emulator, Vulkan games) or its Android 'emulator' "
|
||||
"(default: the recipe's choice, else by the game's graphics)")):
|
||||
"""Install games on the Frame (or on this PC) and add them to the Steam library."""
|
||||
"installed games stay where they are")):
|
||||
"""Install games on the Frame (or PC VR games on this PC) and add them to the Steam library."""
|
||||
target = _target(frame, password, to)
|
||||
_set_axrb_backend(target, axrb_backend)
|
||||
if dest is not None and hasattr(target, "dest"):
|
||||
target.dest = "" if dest == "internal" else dest
|
||||
pkgs = _pkgs(package, all_)
|
||||
@@ -509,46 +495,21 @@ def install(package: Optional[str] = typer.Argument(None), all_: bool = typer.Op
|
||||
target.add_to_library(shortcuts, printing_reporter(False))
|
||||
|
||||
|
||||
def _sim_wants_tracked_head(deployments: list[dict]) -> bool:
|
||||
"""--sim: Monado's tracked (qwerty) head when every game tested is an Android one on AXRB's emulator backend.
|
||||
Its host bridge (1.0.4) has no synthetic head for Monado's valid-but-untracked simulated one (direct mode has:
|
||||
AXRB_TEST_SYNTHETIC_HMD=2), so the guest's LOCAL space never gets an origin and VrApi games never see a tracked
|
||||
head (tools/xr_sim.TRACKED_HEAD_ENV)."""
|
||||
from .targets import pc_android
|
||||
|
||||
android = [d for d in deployments if d.get("kind") == pc_android.KIND]
|
||||
return bool(android) and len(android) == len(deployments) and all(
|
||||
pc_android.dep_backend(d) != pc_android.axrb.DIRECT for d in android)
|
||||
|
||||
|
||||
@app.command()
|
||||
def test(package: Optional[str] = typer.Argument(None), all_: bool = typer.Option(False, "--all", help=ALL_HELP),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
|
||||
seconds: int = typer.Option(45, help="how long the game runs before it's stopped"),
|
||||
to: str = typer.Option("frame", help="frame, or pc (games installed on this PC)"),
|
||||
axrb_backend: Optional[str] = typer.Option(None, help="with --to pc: check that Quest games are installed "
|
||||
"for this AXRB backend (direct or emulator)"),
|
||||
sim: bool = typer.Option(False, "--sim", help="with --to pc: use Monado's simulated headset instead of the "
|
||||
"PC's OpenXR runtime (no headset needed; also env FRAMEPORT_PC_XR_SIM=1)")):
|
||||
to: str = typer.Option("frame", help="frame, or pc (PC VR games installed on this PC)")):
|
||||
"""Headless launch test on the Frame (or this PC) and log triage."""
|
||||
from .tools import xr_sim
|
||||
|
||||
if sim and to != "pc":
|
||||
raise typer.BadParameter("--sim only works with --to pc")
|
||||
target = _target(frame, to=to)
|
||||
_set_axrb_backend(target, axrb_backend)
|
||||
games = {g["package"]: g for g in target.installed()}
|
||||
installed = set(games)
|
||||
pkgs = _pkgs(package, all_)
|
||||
with xr_sim.running(to == "pc" and xr_sim.enabled(sim), reporter=printing_reporter(False),
|
||||
tracked=xr_sim.tracked_head(_sim_wants_tracked_head([games[p] for p in pkgs if p in games]))):
|
||||
for pkg in pkgs:
|
||||
if pkg not in installed:
|
||||
typer.echo(f"{pkg}: not installed on {target.label}", err=True)
|
||||
continue
|
||||
s = pipeline.test_game(pkg, target, printing_reporter(False), seconds)
|
||||
typer.echo(f"{pkg}: {s['state']} ({s['verdict']}) furthest: {s['milestone']}"
|
||||
+ (f"; suggestions: {', '.join(s['suggestions'])}" if s["suggestions"] else ""))
|
||||
installed = {g["package"] for g in target.installed()}
|
||||
for pkg in _pkgs(package, all_):
|
||||
if pkg not in installed:
|
||||
typer.echo(f"{pkg}: not installed on {target.label}", err=True)
|
||||
continue
|
||||
s = pipeline.test_game(pkg, target, printing_reporter(False), seconds)
|
||||
typer.echo(f"{pkg}: {s['state']} ({s['verdict']}) furthest: {s['milestone']}"
|
||||
+ (f"; suggestions: {', '.join(s['suggestions'])}" if s["suggestions"] else ""))
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -599,11 +560,9 @@ def triage(logfile: Path = typer.Argument(..., help="a launch.log"),
|
||||
|
||||
@app.command()
|
||||
def settings(package: str, values: list[str] = typer.Argument(..., help="key=value pairs"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
|
||||
to: str = typer.Option("frame", help="frame, or pc (a Quest game in AXRB on this PC; an empty value "
|
||||
"removes a setting)")):
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Change FrameBridge settings of an installed game (no rebuild), for example scale=1.2."""
|
||||
target = _target(frame, to=to)
|
||||
target = _target(frame)
|
||||
kv = dict(v.split("=", 1) for v in values)
|
||||
typer.echo(json.dumps(target.set_settings(package, kv), indent=1))
|
||||
|
||||
@@ -803,28 +762,10 @@ def frame_proton(frame: Optional[str] = typer.Option(None, help=FRAME_HELP),
|
||||
# ------------------------------------------------------------------------------------------ pc (Revive)
|
||||
@pc_app.command("info")
|
||||
def pc_info():
|
||||
"""Steam, SteamVR, Revive and AXRB (its emulator and its direct mode) on this PC (JSON)."""
|
||||
"""Steam, SteamVR and Revive on this PC (JSON)."""
|
||||
typer.echo(json.dumps(_target(None, to="pc").describe(), indent=1, default=str))
|
||||
|
||||
|
||||
@pc_app.command("install-axrb")
|
||||
def pc_install_axrb(backend: str = typer.Option("emulator", help="'emulator' (AXRB's Android emulator: Google's "
|
||||
"emulator pieces, about 2.5 GB) or 'direct' (AXRB-Emulatorless: "
|
||||
"Google's Android 16 arm64 system image, about 0.8 GB)"),
|
||||
update: bool = typer.Option(False, "--update", help="also update an installed AXRB to the "
|
||||
"newest release with an installer for this backend")):
|
||||
"""Install AXRB (Android XR Bridge, for Quest games on this PC) and set up its Android runtime. Normally done by
|
||||
the first `frameport install <quest game> --to pc`. Downloads AXRB's official installer plus Google's Android
|
||||
files. A local build (FRAMEPORT_AXRB_DIRECT_DIR / FRAMEPORT_AXRB_DIR) is set up but never replaced."""
|
||||
from .targets import pc_android
|
||||
from .tools import axrb
|
||||
|
||||
if backend not in axrb.BACKENDS:
|
||||
raise typer.BadParameter(f"--backend: {' or '.join(axrb.BACKENDS)}")
|
||||
pc_android.ensure_axrb(printing_reporter(False), backend, update=update)
|
||||
typer.echo(json.dumps(axrb.status(), indent=1, default=str))
|
||||
|
||||
|
||||
@pc_app.command("install-revive")
|
||||
def pc_install_revive():
|
||||
"""Download Revive's latest release and unpack it into FramePort's tools (no installer, no admin rights)."""
|
||||
@@ -1029,5 +970,134 @@ def _describe(exc: BaseException) -> str:
|
||||
return explain(exc)
|
||||
|
||||
|
||||
def _fex_target(frame: Optional[str]):
|
||||
from . import fexrift
|
||||
|
||||
try:
|
||||
fexrift.require_enabled()
|
||||
except RuntimeError as e:
|
||||
typer.echo(str(e), err=True)
|
||||
raise typer.Exit(2) from None
|
||||
return _target(frame)
|
||||
|
||||
|
||||
@fex_app.command("status")
|
||||
def fex_status(frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""What's set up on the Frame: GE-Proton, Meta's runtime, the login, the games in the prefix."""
|
||||
t = _fex_target(frame)
|
||||
typer.echo(json.dumps(t.frame.agent("fex_status", timeout=90), indent=1))
|
||||
|
||||
|
||||
@fex_app.command("setup")
|
||||
def fex_setup(frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Upload FramePort's files, then the Frame downloads GE-Proton (x86_64) and Meta's runtime and builds the prefix
|
||||
(10-20 minutes, about 12 GiB)."""
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
fexrift.setup(t.frame, printing_reporter(False))
|
||||
typer.echo("setup done")
|
||||
|
||||
|
||||
@fex_app.command("import-login")
|
||||
def fex_import_login(prefix: str = typer.Argument(..., help="a Wine prefix on this PC where you signed in to Meta's "
|
||||
"app (and downloaded the game with it)"),
|
||||
app: Optional[list[str]] = typer.Option(None, "--app", help="Meta's name of a game to copy "
|
||||
"(repeatable; default: the verified games)"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Copy your Meta login and the games Meta's app downloaded into the Frame's prefix (sign-in tokens: sent over
|
||||
SSH, deleted on both sides afterwards)."""
|
||||
from pathlib import Path
|
||||
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
res = fexrift.import_login(t.frame, Path(prefix).expanduser(), printing_reporter(False), apps=app or None)
|
||||
typer.echo(json.dumps(res.get("imported"), indent=1))
|
||||
|
||||
|
||||
@fex_app.command("login")
|
||||
def fex_login(start_now: bool = typer.Option(False, "--start-now", help="start Meta's app now instead of using Steam"),
|
||||
timeout: int = typer.Option(900, min=1, max=86400, help="seconds to wait for sign-in"),
|
||||
via_pc: bool = typer.Option(False, "--via-pc", help="use the PC browser and a temporary Oculus handler"),
|
||||
stop: bool = typer.Option(False, "--stop", help="end sign-in and restore link handlers; keep the login"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Sign in with your Meta account in the Frame's shared Wine prefix."""
|
||||
from . import fexlogin
|
||||
|
||||
t = _fex_target(frame)
|
||||
fexlogin.login(t.frame, start_now=start_now, timeout=timeout, via_pc=via_pc, stop=stop, emit=typer.echo)
|
||||
|
||||
|
||||
@fex_app.command("install")
|
||||
def fex_install(app_name: Optional[str] = typer.Argument(None, metavar="APP",
|
||||
help="Meta's name for the game (verified: oculus-first-contact)"),
|
||||
from_library: Optional[str] = typer.Option(None, "--from-library", help="Rift library ID: rift.<slug>"),
|
||||
folder: Optional[Path] = typer.Option(None, "--folder", help="upload an existing game folder unchanged"),
|
||||
title: Optional[str] = typer.Option(None, "--title", help="title for a local game copy"),
|
||||
app_id: Optional[str] = typer.Option(None, "--app-id", help="Meta app ID if no config/cached ID exists"),
|
||||
dest: Optional[str] = typer.Option(None, "--dest", help="external drive mount point on the Frame"),
|
||||
runtime_check: bool = typer.Option(False, "--runtime-check", help="probe Meta's library state (<=120 s)"),
|
||||
msaa: Optional[int] = typer.Option(None, help="MSAA samples for a verified game that allows it (1, 2 "
|
||||
"or 4; game default 4). UNVERIFIED: with 2 the game never "
|
||||
"rendered in two launches"),
|
||||
no_steam: bool = typer.Option(False, "--no-steam", help="don't add it to the Steam library"),
|
||||
force_unverified: bool = typer.Option(False, "--force-unverified",
|
||||
help="install a game that isn't verified with this mode "
|
||||
"(only the generic fixes; may not start)"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Install a prefix game or upload a local Rift copy (fixes, launcher, shortcut). Without
|
||||
--force-unverified an unverified game is refused with what the Frame's static checks found."""
|
||||
from . import fexrift
|
||||
|
||||
t = _fex_target(frame)
|
||||
if from_library or folder:
|
||||
from . import fexinstall
|
||||
res = fexinstall.install(t.frame, printing_reporter(False), from_library=from_library, folder=folder,
|
||||
title=title, app=app_name, app_id=app_id, dest=dest, runtime_check=runtime_check,
|
||||
msaa=msaa, add_to_steam=not no_steam, force_unverified=force_unverified)
|
||||
else:
|
||||
if title or app_id or dest:
|
||||
raise typer.BadParameter("--title, --app-id and --dest require --from-library or --folder")
|
||||
res = fexrift.install(t.frame, printing_reporter(False), app_name or "oculus-first-contact", msaa=msaa,
|
||||
add_to_steam=not no_steam, force_unverified=force_unverified)
|
||||
if runtime_check:
|
||||
res["checks"] = dict(res.get("checks") or {}, **t.frame.agent(
|
||||
"fex_runtime_state", app=app_name or "oculus-first-contact", timeout=125))
|
||||
typer.echo(json.dumps(res, indent=1))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- FEX launch-time settings / bounded bench
|
||||
@fex_app.command("settings")
|
||||
def fex_settings(app_name: str = typer.Argument(..., metavar="APP"),
|
||||
values: Optional[list[str]] = typer.Argument(None, metavar="KEY=VALUE"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""Read per-game performance settings, or override them for the next launch (JSON values; null resets a key)."""
|
||||
updates = {}
|
||||
for item in values or []:
|
||||
key, sep, raw = item.partition("=")
|
||||
if not sep or not key:
|
||||
raise typer.BadParameter("use key=value (true/false, numbers, null, or JSON for fex)")
|
||||
try:
|
||||
updates[key] = raw if key == "cores" and raw != "null" else json.loads(raw)
|
||||
except ValueError:
|
||||
updates[key] = raw # taskset's CPU list, e.g. cores=4-7
|
||||
t = _fex_target(frame)
|
||||
args = {"app": app_name}
|
||||
if values:
|
||||
args["values"] = updates
|
||||
typer.echo(json.dumps(t.frame.agent("fex_settings", **args), indent=1))
|
||||
|
||||
|
||||
@fex_app.command("bench")
|
||||
def fex_bench(app_name: str = typer.Argument("oculus-first-contact", metavar="APP"),
|
||||
seconds: int = typer.Option(120, min=1, max=120, help="total launch and sampling window (seconds)"),
|
||||
frame: Optional[str] = typer.Option(None, help=FRAME_HELP)):
|
||||
"""One bounded headless performance run on an idle Frame; stops the game and restores display power settings."""
|
||||
t = _fex_target(frame)
|
||||
typer.echo(json.dumps(t.frame.agent("fex_bench", app=app_name, seconds=seconds, timeout=seconds + 250),
|
||||
separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -289,6 +289,40 @@ def oculus_platform_dir() -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def meta_libraries() -> list[Path]:
|
||||
"""This PC's Meta Horizon / Oculus app libraries (folders holding Software/ and Manifests/): the default one under
|
||||
Program Files and the extra ones the app registered (HKCU\\Software\\Oculus VR, LLC\\Oculus\\Libraries\\{id}
|
||||
OriginalPath)."""
|
||||
if not available():
|
||||
return []
|
||||
out: list[Path] = []
|
||||
roots = [env_path("ProgramFiles"), env_path("ProgramW6432")] + ([Path("/mnt/c/Program Files")] if is_wsl() else [])
|
||||
for root in [r for r in roots if r]:
|
||||
for sub in ("Meta Horizon/Software", "Oculus/Software"):
|
||||
out.append(root / sub)
|
||||
key = r"HKCU\Software\Oculus VR, LLC\Oculus\Libraries"
|
||||
try:
|
||||
if is_windows():
|
||||
import winreg
|
||||
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key.partition("\\")[2]) as k:
|
||||
for i in range(winreg.QueryInfoKey(k)[0]):
|
||||
with winreg.OpenKey(k, winreg.EnumKey(k, i)) as sub:
|
||||
out.append(Path(str(winreg.QueryValueEx(sub, "OriginalPath")[0])))
|
||||
else:
|
||||
r = run_win([system32("reg.exe"), "query", key, "/s", "/v", "OriginalPath"])
|
||||
for m in re.finditer(r"^\s*OriginalPath\s+REG_\w+\s+(.+?)\s*$", r.stdout, re.M):
|
||||
out.append(to_local(m[1]))
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass
|
||||
seen, result = set(), []
|
||||
for p in out:
|
||||
if p not in seen and (p / "Manifests").is_dir():
|
||||
seen.add(p)
|
||||
result.append(p)
|
||||
return result
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------ firewall (pairing)
|
||||
WSL_VM_CREATOR = "{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}" # Hyper-V firewall id of WSL's VM
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Upload an existing Rift copy and register it with Meta's real runtime; no extraction or licence changes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from . import fexrift
|
||||
from .analysis import rift, unreal_pak
|
||||
from .core.events import Reporter
|
||||
|
||||
|
||||
def skipped_file(name: str) -> bool:
|
||||
name = name.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
||||
return (name.endswith((".7z", ".ag")) or bool(re.search(r"\.7z\.0", name))
|
||||
or name in ("data.bin", "agrepackinstaller.exe", "vd.bat"))
|
||||
|
||||
|
||||
def relative_file(name: str) -> str:
|
||||
name = name.replace("\\", "/")
|
||||
if (not name or name.startswith("/") or any(p in ("", ".", "..") for p in name.split("/"))
|
||||
or any(c in name for c in ':\x00\r\n')):
|
||||
raise ValueError(f"path must stay inside the game folder: {name!r}")
|
||||
return name
|
||||
|
||||
|
||||
def inventory(folder: Path) -> tuple[dict, list[str]]:
|
||||
"""Stat only: hashing happens on the Frame, avoiding a second full read from slow source drives."""
|
||||
files, skipped, seen = {}, [], set()
|
||||
for root, dirs, names in os.walk(folder):
|
||||
for name in dirs:
|
||||
if (Path(root) / name).is_symlink():
|
||||
raise ValueError("game folders must not contain directory symlinks")
|
||||
for name in sorted(names):
|
||||
path = Path(root) / name
|
||||
rel = relative_file(path.relative_to(folder).as_posix())
|
||||
if skipped_file(rel):
|
||||
skipped.append(rel)
|
||||
continue
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"symlink in game folder: {rel}")
|
||||
if not path.is_file():
|
||||
raise ValueError(f"not a regular game file: {rel}")
|
||||
if rel.casefold() in seen:
|
||||
raise ValueError(f"duplicate Windows filename: {rel}")
|
||||
seen.add(rel.casefold())
|
||||
st = path.stat()
|
||||
files[rel] = {"size": st.st_size, "mtime_ns": st.st_mtime_ns}
|
||||
return files, sorted(skipped)
|
||||
|
||||
|
||||
def ini_app_id(text: str) -> str | None:
|
||||
section = ""
|
||||
values = {}
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
section = line[1:-1].lower()
|
||||
elif section == "onlinesubsystemoculus" and not line.startswith((";", "#")):
|
||||
key, _, value = line.partition("=")
|
||||
value = value.split(";", 1)[0].strip().strip('"\'')
|
||||
if value.isdecimal() and int(value) > 0:
|
||||
values[key.strip().lower()] = value
|
||||
return next((values[k] for k in ("riftappid", "oculusappid", "appid") if k in values), None)
|
||||
|
||||
|
||||
def config_app_id(folder: Path, files: dict) -> tuple[str | None, str | None]:
|
||||
# Only unencrypted config entries are read. No archive extraction, guessing from binary strings or traversal
|
||||
# into repack installers. Loose configs also cover Unity/custom engines using Oculus' common config names.
|
||||
for rel in sorted(files):
|
||||
if not rel.lower().endswith(".pak"):
|
||||
continue
|
||||
try:
|
||||
ver, _, entries, names = unreal_pak.pak_entries(folder / rel)
|
||||
for name, ent in entries.items():
|
||||
if not name.lower().endswith("config/defaultengine.ini") or ent[2] > 4 << 20:
|
||||
continue
|
||||
value = ini_app_id(unreal_pak.pak_read(folder / rel, ver, ent, names).decode("utf-8-sig", "replace"))
|
||||
if value:
|
||||
return value, f"config:{rel}:{name}"
|
||||
except (ValueError, OSError, struct.error, IndexError, zlib.error):
|
||||
continue
|
||||
for rel in sorted(files):
|
||||
path = folder / rel
|
||||
name = path.name.lower()
|
||||
if files[rel]["size"] > 4 << 20:
|
||||
continue
|
||||
if name in ("defaultengine.ini", "oculus.ini", "oculusconfig.ini"):
|
||||
value = ini_app_id(path.read_text("utf-8-sig", errors="replace"))
|
||||
elif name in ("oculusconfig.json", "oculus-platform-config.json"):
|
||||
try:
|
||||
data = json.loads(path.read_text("utf-8-sig"))
|
||||
value = next((str(data[k]) for k in ("RiftAppId", "OculusAppId", "AppId", "app_id")
|
||||
if str(data.get(k, "")).isdecimal() and int(data[k]) > 0), None)
|
||||
except (ValueError, TypeError, KeyError):
|
||||
value = None
|
||||
else:
|
||||
continue
|
||||
if value:
|
||||
return value, f"config:{rel}"
|
||||
return None, None
|
||||
|
||||
|
||||
def exe_version(path: Path) -> str | None:
|
||||
"""VS_FIXEDFILEINFO from the PE resource section, without reading a large executable a second time."""
|
||||
try:
|
||||
with path.open("rb") as f:
|
||||
head = f.read(4096)
|
||||
pe = struct.unpack_from("<I", head, 0x3c)[0]
|
||||
if head[:2] != b"MZ" or head[pe:pe + 4] != b"PE\0\0":
|
||||
return None
|
||||
count = struct.unpack_from("<H", head, pe + 6)[0]
|
||||
opt_size = struct.unpack_from("<H", head, pe + 20)[0]
|
||||
f.seek(pe + 24 + opt_size)
|
||||
sections = f.read(min(count, 96) * 40)
|
||||
for off in range(0, len(sections), 40):
|
||||
if sections[off:off + 8].rstrip(b"\0") != b".rsrc":
|
||||
continue
|
||||
size, raw = struct.unpack_from("<II", sections, off + 16)
|
||||
f.seek(raw)
|
||||
data = f.read(min(size, 32 << 20))
|
||||
pos = data.find(struct.pack("<II", 0xfeef04bd, 0x10000))
|
||||
if pos >= 0:
|
||||
ms, ls = struct.unpack_from("<II", data, pos + 8)
|
||||
return ".".join(str(v) for v in (ms >> 16, ms & 0xffff, ls >> 16, ls & 0xffff))
|
||||
except (OSError, ValueError, struct.error, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def meta_manifest(folder: Path) -> dict | None:
|
||||
"""Meta's own manifest when the folder is in a Meta app library (<library>/Software/<name> next to
|
||||
<library>/Manifests/<name>.json, e.g. C:\\Program Files\\Meta Horizon\\Software): its exe, launch arguments
|
||||
(Unreal games need -vr), app ID and version beat anything guessed from the files."""
|
||||
path = folder.parent.parent / "Manifests" / f"{folder.name}.json"
|
||||
if folder.parent.name.lower() != "software" or not path.is_file():
|
||||
return None
|
||||
try:
|
||||
m = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if m.get("canonicalName") != folder.name or not m.get("launchFile") or not str(m.get("appId", "")).isdigit():
|
||||
return None
|
||||
return m
|
||||
|
||||
|
||||
def library_manifest(app_id: str | None, canonical: str | None, exe: str) -> dict | None:
|
||||
"""The same game's manifest in one of this PC's Meta app libraries (installed with the user's own account), for its
|
||||
launch arguments: only when Meta's exe has the same file name as the one being installed (same game build)."""
|
||||
from .core import winhost
|
||||
|
||||
for lib in winhost.meta_libraries():
|
||||
for path in sorted((lib / "Manifests").glob("*.json")):
|
||||
try:
|
||||
m = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if not (str(m.get("appId")) == str(app_id) or (canonical and m.get("canonicalName") == canonical)):
|
||||
continue
|
||||
if Path(str(m.get("launchFile", "")).replace("\\", "/")).name.lower() == Path(exe).name.lower():
|
||||
return m
|
||||
return None
|
||||
|
||||
|
||||
def source_info(folder: Path, entry: dict | None = None, title: str | None = None,
|
||||
app: str | None = None, app_id: str | None = None) -> dict:
|
||||
folder = folder.expanduser().resolve()
|
||||
if not folder.is_dir():
|
||||
raise ValueError(f"game folder not found: {folder}")
|
||||
files, skipped = inventory(folder)
|
||||
entry = entry or {}
|
||||
extra = (entry.get("analysis") or {}).get("extra") or {}
|
||||
meta = None if entry else meta_manifest(folder)
|
||||
exe = entry.get("exe") or extra.get("exe") or (meta and meta["launchFile"].replace("\\", "/"))
|
||||
if not exe:
|
||||
analysis = rift.analyze(folder)
|
||||
if not analysis.extra.get("exe_confirmed"):
|
||||
raise ValueError("several game programs found; choose one in the FramePort library first")
|
||||
extra = analysis.extra
|
||||
exe = extra["exe"]
|
||||
exe = relative_file(str(exe))
|
||||
if exe not in files:
|
||||
raise ValueError("the selected game program is missing or is a skipped repack file")
|
||||
title = title or entry.get("title") or folder.name
|
||||
canonical = (app or extra.get("canonical_name") or (meta and meta["canonicalName"])
|
||||
or re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-"))
|
||||
package = fexrift.package_for(canonical) # validate before any remote mutation
|
||||
store_url = (entry.get("details") or {}).get("store_url") or ""
|
||||
match = re.fullmatch(r"https://(?:www\.)?meta\.com/experiences/(\d+)/?", store_url)
|
||||
store_id = match[1] if match else None
|
||||
found, source = (str(meta["appId"]), "meta-manifest") if meta else config_app_id(folder, files)
|
||||
if not found and store_id:
|
||||
found, source = store_id, "oculusdb:details.store_url"
|
||||
if not found and app_id:
|
||||
if not re.fullmatch(r"[1-9][0-9]*", app_id):
|
||||
raise ValueError("--app-id must be a positive numeric Meta app ID")
|
||||
found, source = app_id, "cli:--app-id"
|
||||
if not found:
|
||||
raise ValueError("no Meta app ID in the game's config or cached store details; supply --app-id")
|
||||
args = entry.get("launch_args", extra.get("launch_args", [])) or (shlex.split(meta.get("launchParameters") or "")
|
||||
if meta else [])
|
||||
if isinstance(args, str):
|
||||
args = shlex.split(args)
|
||||
if not args and not meta:
|
||||
# Meta launches many Rift games with arguments (Unreal: -vr); a copy installed elsewhere has none of them
|
||||
lib = library_manifest(found, canonical, exe)
|
||||
if lib:
|
||||
args = shlex.split(lib.get("launchParameters") or "")
|
||||
version = re.search(r"(?i)(?<![a-z0-9])(?:v|version[ _-]*)?(\d+(?:\.\d+)+)", folder.name)
|
||||
return {"folder": folder, "app": canonical, "package": entry.get("package") or package, "title": title,
|
||||
"exe": exe, "args": args, "app_id": found, "app_id_source": source, "store_app_id": store_id,
|
||||
"version": (meta and meta.get("version")) or (version[1] if version else exe_version(folder / exe))
|
||||
or "0.0.0",
|
||||
"files": files, "skipped": skipped}
|
||||
|
||||
|
||||
def install(frame, reporter: Reporter, *, from_library: str | None = None, folder: Path | None = None,
|
||||
title: str | None = None, app: str | None = None, app_id: str | None = None, dest: str | None = None,
|
||||
msaa: int | None = None, add_to_steam: bool = True, force_unverified: bool = False,
|
||||
runtime_check: bool = False) -> dict:
|
||||
from .core import library
|
||||
from .install import installer
|
||||
|
||||
fexrift.require_enabled()
|
||||
if bool(from_library) == bool(folder):
|
||||
raise ValueError("choose exactly one of --from-library or --folder")
|
||||
entry = None
|
||||
if from_library:
|
||||
entry = library.load().get("games", {}).get(from_library)
|
||||
if not entry or entry.get("kind") != "rift":
|
||||
raise ValueError("--from-library needs a Rift library entry")
|
||||
folder = Path(entry["game_dir"])
|
||||
reporter.stage("Inspect local game files")
|
||||
info = source_info(folder, entry, title, app, app_id)
|
||||
if info["app"] not in fexrift.GAMES and not force_unverified:
|
||||
raise ValueError("this game is unverified; use --force-unverified to upload and install it")
|
||||
for rel in info["skipped"]:
|
||||
reporter.log(f"Skipped repack leftover: {rel}")
|
||||
reporter.check("Meta app ID", True, f"{info['app_id']} ({info['app_id_source']})")
|
||||
reporter.check_cancel()
|
||||
fexrift.sync_helpers(frame, reporter)
|
||||
prep = frame.agent("fex_prepare_local", app=info["app"], package=info["package"], dest=dest,
|
||||
files=info["files"])
|
||||
items = [(info["folder"] / rel, rel, st["size"]) for rel, st in info["files"].items()
|
||||
if prep["existing"].get(rel) != st["size"]]
|
||||
need = sum(size for _, _, size in items)
|
||||
if need > prep["free_bytes"] - (1 << 30):
|
||||
raise RuntimeError("not enough space for the game + 1 GiB headroom")
|
||||
reporter.stage(f"Upload {info['title']} ({need / 2**30:.1f} GiB remaining)")
|
||||
with installer.transfer_link(frame, reporter, need) as xfer:
|
||||
if items:
|
||||
installer.upload_files(xfer, items, prep["game_dir"], reporter, max(need, 1))
|
||||
reporter.check_cancel()
|
||||
if inventory(info["folder"])[0] != info["files"]:
|
||||
raise RuntimeError("source files changed during upload; retry with a stable game folder")
|
||||
reporter.stage("Hash and register the game on the Frame")
|
||||
metadata = {k: v for k, v in info.items() if k not in ("folder", "files")}
|
||||
frame.agent("fex_register_local", timeout=7200, **metadata)
|
||||
reporter.stage("Install launcher and generic fixes")
|
||||
result = frame.agent("fex_install", package=info["package"], app=info["app"], title=info["title"],
|
||||
source="local_copy", msaa=msaa, force_unverified=force_unverified)
|
||||
if add_to_steam:
|
||||
installer.add_to_steam(frame, [info["package"]], reporter)
|
||||
if runtime_check:
|
||||
result["checks"] = dict(result.get("checks") or {},
|
||||
**frame.agent("fex_runtime_state", app=info["app"], timeout=125))
|
||||
reporter.check("Meta library state", result["checks"].get("runtime_state") in ("ready", "installed"),
|
||||
result["checks"].get("runtime_state", "unknown"))
|
||||
return result
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Interactive Meta sign-in on the Frame. Credential URLs travel in memory, over SSH stdin / a private callback.
|
||||
|
||||
There is intentionally no Reporter or app log in this module: external programs' exception strings and Meta's
|
||||
Electron output can contain login URLs. Only fixed, friendly progress text reaches the CLI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import queue
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from . import fexrift, urlhandler
|
||||
from .core import winhost
|
||||
from .core.paths import user_data_dir
|
||||
|
||||
TITLE = "Meta sign-in (FramePort)"
|
||||
|
||||
|
||||
def valid_callback(url):
|
||||
if not isinstance(url, str) or len(url) > 65536 or any(ord(c) < 32 for c in url):
|
||||
return False
|
||||
try:
|
||||
u = urllib.parse.urlsplit(url)
|
||||
return (u.scheme, u.netloc, u.path) == ("oculus", "link", "/frl_login")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def open_page(url):
|
||||
try:
|
||||
u = urllib.parse.urlsplit(url)
|
||||
if (u.scheme, u.netloc, u.path) != ("https", "auth.meta.com", "/native_sso/confirm"):
|
||||
raise ValueError
|
||||
if winhost.is_wsl():
|
||||
# Start-Process on Windows, without interpolating the sign-in URL into PowerShell code or argv.
|
||||
subprocess.run([winhost.system32("WindowsPowerShell/v1.0/powershell.exe"), "-NoProfile",
|
||||
"-NonInteractive", "-Command", "Start-Process ([Console]::In.ReadToEnd())"],
|
||||
input=url, text=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
timeout=15, check=True)
|
||||
elif not webbrowser.open(url):
|
||||
raise ValueError
|
||||
except Exception:
|
||||
raise RuntimeError("couldn't open Meta's sign-in page in the PC browser") from None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pc_callback():
|
||||
"""The temporary scheme script POSTs straight into this loopback server, never to links/*.link.
|
||||
|
||||
A random endpoint and no CORS prevent arbitrary websites from posting callbacks. Handler errors and HTTP
|
||||
request lines are deliberately never logged. Both the script and registration are removed in finally blocks.
|
||||
"""
|
||||
inbox = queue.Queue(maxsize=4)
|
||||
endpoint = "/" + secrets.token_hex(24)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
n = int(self.headers.get("Content-Length", "0"))
|
||||
if self.path != endpoint or not 0 < n <= 65536:
|
||||
self.send_error(400)
|
||||
return
|
||||
url = self.rfile.read(n).decode("utf-8")
|
||||
if not valid_callback(url):
|
||||
self.send_error(400)
|
||||
return
|
||||
inbox.put_nowait(url)
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
except (ValueError, OSError, queue.Full):
|
||||
self.send_error(400)
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
def handle_error(self, *args):
|
||||
pass
|
||||
|
||||
server = Server(("127.0.0.1", 0), Handler)
|
||||
server.daemon_threads = True
|
||||
worker = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
worker.start()
|
||||
address = f"http://127.0.0.1:{server.server_address[1]}{endpoint}"
|
||||
user_data_dir().mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="fex-login-", dir=user_data_dir()) as tmp:
|
||||
kind = urlhandler.platform_kind()
|
||||
if kind == "windows":
|
||||
script = Path(tmp) / "callback.ps1"
|
||||
script.write_text("param([string]$Link)\n$ErrorActionPreference='SilentlyContinue'\n"
|
||||
f"try {{ Invoke-WebRequest -UseBasicParsing -Uri '{address}' -Method POST "
|
||||
"-Body ([Text.Encoding]::UTF8.GetBytes($Link)) | Out-Null } catch {}\n")
|
||||
ps = (Path(os.environ.get("SystemRoot", r"C:\Windows")) /
|
||||
"System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
command = f'"{ps}" -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "{script}" "%1"'
|
||||
else:
|
||||
script = Path(tmp) / "callback.py"
|
||||
script.write_text("import sys,urllib.request\ntry:\n"
|
||||
f" r=urllib.request.Request({address!r},data=sys.argv[1].encode(),method='POST')\n"
|
||||
" urllib.request.urlopen(r,timeout=10).close()\nexcept Exception:\n pass\n")
|
||||
if kind == "wsl":
|
||||
distro = os.environ.get("WSL_DISTRO_NAME", "")
|
||||
if not distro:
|
||||
raise RuntimeError("WSL_DISTRO_NAME is needed for --via-pc")
|
||||
command = (f'"C:\\Windows\\System32\\wsl.exe" -d "{distro}" -e '
|
||||
f'python3 -I "{script}" "%1"')
|
||||
else:
|
||||
# Desktop Exec syntax uses double quotes, with literal percent signs escaped.
|
||||
def dq(s):
|
||||
s = str(s).replace("%", "%%")
|
||||
for c in ("\\", '"', "`", "$"):
|
||||
s = s.replace(c, "\\" + c)
|
||||
return '"' + s.replace("\\", "\\\\") + '"' # Desktop string-value layer
|
||||
command = f"{dq(sys.executable)} -I {dq(script)} %u"
|
||||
with urlhandler.temporary_oculus_handler(command):
|
||||
yield inbox
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
worker.join(timeout=2)
|
||||
|
||||
|
||||
def login(frame, *, start_now=False, timeout=900, via_pc=False, stop=False, emit=print):
|
||||
fexrift.require_enabled()
|
||||
if stop:
|
||||
frame.agent("fex_login_cancel")
|
||||
emit("Meta sign-in stopped. Your saved login stays on the Frame.")
|
||||
return {"stopped": True}
|
||||
if not 1 <= timeout <= 86400:
|
||||
raise ValueError("sign-in timeout must be between 1 and 86400 seconds")
|
||||
with pc_callback() if via_pc else nullcontext(None) as inbox:
|
||||
try:
|
||||
started = frame.agent("fex_login", start_now=start_now, via_pc=via_pc, login_timeout=timeout)
|
||||
if started.get("package"):
|
||||
from .core.events import Reporter
|
||||
from .install import installer
|
||||
|
||||
result = installer.add_to_steam(frame, [started["package"]], Reporter())
|
||||
if result.get("state") != "done" or result.get("errors"):
|
||||
raise RuntimeError("Steam hasn't confirmed Meta sign-in; return to Gaming Mode and retry")
|
||||
emit("Meta's app is starting on the Frame." if start_now else
|
||||
f"Put on the headset: open '{TITLE}' in your Steam library.")
|
||||
if via_pc:
|
||||
emit("Complete the Meta sign-in page in this PC's browser.")
|
||||
deadline = time.monotonic() + timeout
|
||||
page_seen = callback_seen = False
|
||||
while time.monotonic() < deadline:
|
||||
st = frame.agent("fex_login_status", take_url=via_pc)
|
||||
# Pop immediately, so even the local status variable never retains a login URL after opening it.
|
||||
url = st.pop("open_url", None)
|
||||
if url:
|
||||
open_page(url)
|
||||
url = None
|
||||
if st.get("page_opened_at") and not page_seen:
|
||||
emit("Sign-in page opened in the PC browser." if via_pc else "Sign-in page opened on the Frame.")
|
||||
page_seen = True
|
||||
if inbox is not None:
|
||||
try:
|
||||
link = inbox.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
reply = frame.agent("fex_login_callback", url=link)
|
||||
if not reply.get("delivered"):
|
||||
raise RuntimeError("Meta's running app didn't accept the callback; retry sign-in")
|
||||
finally:
|
||||
link = None
|
||||
if st.get("callback_received_at") and not callback_seen:
|
||||
emit("Sign-in response reached Meta's app on the Frame.")
|
||||
callback_seen = True
|
||||
if st.get("signed_in"):
|
||||
emit("Signed in.")
|
||||
return {"signed_in": True}
|
||||
time.sleep(3)
|
||||
raise RuntimeError("Meta sign-in timed out; if the callback didn't reach the Frame, retry with --via-pc")
|
||||
finally:
|
||||
# Runs on success, Ctrl-C, timeout and partial setup errors; the broker also expires if SSH is lost.
|
||||
frame.agent("fex_login_cancel")
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Experimental: Oculus Rift games that need Meta's PC runtime, on the Steam Frame alone.
|
||||
|
||||
x86_64 GE-Proton under FEX runs Meta's runtime, Revive and the game in one Wine prefix on the Frame (research and
|
||||
measurements: native/fexwine/README.md, CLAUDE.md "Rift via x86_64 Wine under FEX"). CLI only, behind
|
||||
FRAMEPORT_EXPERIMENTAL_FEX=1 (`frameport fex ...`); verified game: Oculus First Contact.
|
||||
|
||||
Steps: setup (uploads agent/fexrift.py + artifacts/fexrift + a patched Revive 3.2.0, then the Frame downloads
|
||||
GE-Proton and Meta's runtime itself) -> import-login (the Meta login and the games Meta's app downloaded, from a
|
||||
signed-in Wine prefix on this PC) -> install (per-game fixes, launcher, Steam shortcut).
|
||||
The login archive holds sign-in tokens: it is written owner-only, sent over SSH and deleted on both sides.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .core.events import Reporter
|
||||
from .core.paths import agent_dir, artifacts_dir, user_data_dir
|
||||
|
||||
EXPERIMENT_ENV = "FRAMEPORT_EXPERIMENTAL_FEX"
|
||||
|
||||
# Revive 3.2.0's LibRevive64.dll: the visibility fixes (IsVisible / HmdMounted / HasInputFocus always true: no pauses
|
||||
# on the Frame's brief focus dips) and eye textures at 67 % of the recommended size (ovr_GetFovTextureSize divides by
|
||||
# 1.5: the Frame's GPU can't fill the full size at 72 Hz). (file offset, original bytes, new bytes)
|
||||
REVIVE_VERSION = "3.2.0"
|
||||
REVIVE_DLL_SHA256 = "ef9ffd5342bb4afad9b1c0f4fee1ca3bf32f9d3159d49dd841cc9e22f02663e4"
|
||||
REVIVE_PATCHES = [
|
||||
(0x1008c, "740d", "9090"),
|
||||
(0x122af, "15", "f1"),
|
||||
(0x122b1, "04", "03"),
|
||||
(0x151f8, "ff9030010000", "b00190909090"),
|
||||
(0x1563a, "0f94c0", "b00190"),
|
||||
(0x15807, "ff9230010000", "b00190909090"),
|
||||
]
|
||||
|
||||
# games verified with this mode: Meta app name (folder under Software/Software) -> FramePort package + title
|
||||
GAMES = {"oculus-first-contact": {"package": "rift.oculus_first_contact", "title": "Oculus First Contact"}}
|
||||
|
||||
# what a signed-in prefix contributes (relative to drive_c), tar top-level name -> path
|
||||
META = "Program Files/Meta Horizon"
|
||||
|
||||
|
||||
def enabled() -> bool:
|
||||
return os.environ.get(EXPERIMENT_ENV) == "1"
|
||||
|
||||
|
||||
def require_enabled() -> None:
|
||||
if not enabled():
|
||||
raise RuntimeError(f"Rift games with Meta's runtime on the Frame are experimental: set {EXPERIMENT_ENV}=1")
|
||||
|
||||
|
||||
def artifact_files() -> list[tuple[Path, str]]:
|
||||
"""(local file, path under the Frame's artifacts/) for everything fexrift.py needs, except Revive."""
|
||||
root = artifacts_dir() / "fexrift"
|
||||
out = [(p, p.relative_to(root).as_posix()) for p in sorted(root.rglob("*")) if p.is_file()]
|
||||
# fexrift.py, its extension modules (fexrift_<area>.py) and per-game data; release bundles carry .txt copies of
|
||||
# the .py files (their .py files are compiled away)
|
||||
names = {p.name.removesuffix(".txt") for p in agent_dir().glob("fexrift*.py*") if p.suffix in (".py", ".txt")}
|
||||
names |= {p.name for p in agent_dir().glob("fexrift*.json")}
|
||||
for name in sorted(names):
|
||||
src = agent_dir() / name
|
||||
out.append((src if src.exists() else agent_dir() / f"{name}.txt", name))
|
||||
return out
|
||||
|
||||
|
||||
def patch_revive(src: Path, dest: Path) -> Path:
|
||||
"""Copy Revive's runtime files to dest with REVIVE_PATCHES applied to LibRevive64.dll (Revive 3.2.0 only)."""
|
||||
from .tools import revive as revive_tool
|
||||
|
||||
dll = src / "LibRevive64.dll"
|
||||
if not dll.is_file() or hashlib.sha256(dll.read_bytes()).hexdigest() != REVIVE_DLL_SHA256:
|
||||
raise RuntimeError(f"this mode needs Revive {REVIVE_VERSION} (its LibRevive64.dll is patched byte by byte); "
|
||||
f"found another build in {src}")
|
||||
tmp = dest.with_name(dest.name + ".tmp")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
for f in revive_tool.runtime_files(src):
|
||||
target = tmp / f.relative_to(src)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(f, target) # content only: a system Revive's files are read-only
|
||||
data = bytearray((tmp / "LibRevive64.dll").read_bytes())
|
||||
for off, old, new in REVIVE_PATCHES:
|
||||
o, n = bytes.fromhex(old), bytes.fromhex(new)
|
||||
if bytes(data[off:off + len(o)]) != o:
|
||||
raise RuntimeError(f"Revive {REVIVE_VERSION}: unexpected bytes at {off:#x}")
|
||||
data[off:off + len(n)] = n
|
||||
(tmp / "LibRevive64.dll").write_bytes(bytes(data))
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
tmp.replace(dest)
|
||||
return dest
|
||||
|
||||
|
||||
def sync_helpers(frame, reporter: Reporter) -> None:
|
||||
"""Re-upload fexrift.py, its extension modules and per-game data (small) before work that runs them: only setup
|
||||
uploaded them, so a fixed helper on the PC never reached a Frame set up earlier."""
|
||||
from .install import installer
|
||||
|
||||
items = [(p, rel, p.stat().st_size) for p, rel in artifact_files() if rel.startswith("fexrift")]
|
||||
installer.upload_files(frame, items, frame.agent("fex_prepare")["artifacts"], reporter,
|
||||
max(sum(i[2] for i in items), 1))
|
||||
|
||||
|
||||
def setup(frame, reporter: Reporter, wait: float = 3600) -> dict:
|
||||
"""Upload fexrift.py, its binaries and the patched Revive, then run the setup on the Frame and follow it."""
|
||||
from .install import installer
|
||||
from .tools import revive as revive_tool
|
||||
|
||||
require_enabled()
|
||||
reporter.stage("Prepare the Frame")
|
||||
prep = frame.agent("fex_prepare")
|
||||
if not prep.get("fex"):
|
||||
raise RuntimeError("FEX isn't installed on the Frame (Steam app 3127680, installed with Proton for x86 games)")
|
||||
if prep["free_bytes"] < 12 << 30:
|
||||
raise RuntimeError(f"needs about 12 GiB free on the Frame, have {prep['free_bytes'] / 2**30:.1f} GiB")
|
||||
src = revive_tool.revive_dir() or revive_tool.install()
|
||||
revive = patch_revive(src, user_data_dir() / "fexrift" / "revive")
|
||||
items = [(p, rel, p.stat().st_size) for p, rel in artifact_files()]
|
||||
items += [(p, "revive/" + p.relative_to(revive).as_posix(), p.stat().st_size)
|
||||
for p in sorted(revive.rglob("*")) if p.is_file()]
|
||||
total = sum(i[2] for i in items)
|
||||
reporter.stage(f"Upload FramePort's files ({len(items)} files, {total / 2**20:.0f} MiB)")
|
||||
installer.upload_files(frame, items, prep["artifacts"], reporter, max(total, 1))
|
||||
reporter.stage("Set up GE-Proton, FEX and Meta's runtime on the Frame (10-20 minutes)")
|
||||
frame.agent("fex_setup")
|
||||
end = time.time() + wait
|
||||
last = None
|
||||
while time.time() < end:
|
||||
time.sleep(10)
|
||||
try:
|
||||
st = frame.agent("fex_status", timeout=90)
|
||||
except Exception: # noqa: BLE001 - SSH hiccups while the Frame works hard
|
||||
continue
|
||||
setup_st = st.get("setup") or {}
|
||||
if setup_st.get("step") != last and setup_st.get("step"):
|
||||
last = setup_st["step"]
|
||||
reporter.log(f"setup step: {last}")
|
||||
if setup_st.get("state") == "failed":
|
||||
raise RuntimeError(f"setup failed: {setup_st.get('error')}")
|
||||
if setup_st.get("state") == "done":
|
||||
for d in setup_st.get("done", []):
|
||||
reporter.check(f"setup: {d['step']}", True, str(d.get("result")))
|
||||
return st
|
||||
raise RuntimeError("the setup on the Frame didn't finish in time; check again with `frameport fex status`")
|
||||
|
||||
|
||||
def find_user_dir(prefix: Path) -> Path:
|
||||
users = prefix / "drive_c" / "users"
|
||||
for u in sorted(users.iterdir()) if users.is_dir() else []:
|
||||
sessions = u / "AppData" / "Roaming" / "Oculus" / "sessions"
|
||||
# signed in = sessions/<numeric Meta user id>-<n>/ (Meta's app creates sessions/_oaf without a sign-in)
|
||||
if sessions.is_dir() and any(p.is_dir() and re.fullmatch(r"\d+(-\d+)*", p.name) for p in sessions.iterdir()):
|
||||
return u
|
||||
raise RuntimeError(f"no Meta login in {prefix} (sign in with Meta's app in that prefix first)")
|
||||
|
||||
|
||||
def login_archive(prefix: Path, apps: list[str], out: Path) -> dict:
|
||||
"""tar of the login (sessions/, CoreData/), the app manifests and the given apps, from a signed-in prefix."""
|
||||
user = find_user_dir(prefix)
|
||||
meta = prefix / "drive_c" / META
|
||||
trees = {"sessions": user / "AppData" / "Roaming" / "Oculus" / "sessions", "CoreData": meta / "CoreData"}
|
||||
for name, path in trees.items():
|
||||
if not path.is_dir() or not any(path.iterdir()):
|
||||
raise RuntimeError(f"{name} missing in {prefix}: sign in with Meta's app there first")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
old_umask = os.umask(0o077)
|
||||
apps_found = []
|
||||
try:
|
||||
with tarfile.open(out, "w") as t:
|
||||
for name, path in trees.items():
|
||||
t.add(path, arcname=name)
|
||||
manifests = meta / "Software" / "Manifests"
|
||||
for app in apps:
|
||||
for f in sorted(manifests.glob(f"{app}.json*")) if manifests.is_dir() else []:
|
||||
t.add(f, arcname=f"Manifests/{f.name}")
|
||||
game = meta / "Software" / "Software" / app
|
||||
if game.is_dir():
|
||||
t.add(game, arcname=f"Software/{app}")
|
||||
apps_found.append(app)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
return {"archive": out, "apps": apps_found, "bytes": out.stat().st_size}
|
||||
|
||||
|
||||
def import_login(frame, prefix: Path, reporter: Reporter, apps: list[str] | None = None) -> dict:
|
||||
require_enabled()
|
||||
apps = apps or list(GAMES)
|
||||
for app in apps:
|
||||
package_for(app) # Meta's names only: they are globbed and become tar paths
|
||||
tmp = user_data_dir() / "fexrift" / "login.tar"
|
||||
try:
|
||||
reporter.stage("Pack the Meta login")
|
||||
info = login_archive(prefix, apps, tmp)
|
||||
missing = [a for a in apps if a not in info["apps"]]
|
||||
if missing:
|
||||
reporter.check("Games from the prefix", None, f"not downloaded there: {', '.join(missing)}")
|
||||
prep = frame.agent("fex_prepare")
|
||||
reporter.stage(f"Send the login ({info['bytes'] / 2**20:.0f} MiB)")
|
||||
frame.put(tmp, prep["incoming"] + "/login.tar", None)
|
||||
res = frame.agent("fex_import_login", timeout=1200)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
reporter.check("Meta login on the Frame", bool((res.get("status") or {}).get("login")))
|
||||
return res
|
||||
|
||||
|
||||
def package_for(app: str) -> str:
|
||||
"""FramePort package of a Meta app name (verified games keep theirs)."""
|
||||
if app in GAMES:
|
||||
return GAMES[app]["package"]
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]+", app):
|
||||
raise ValueError(f"bad Meta app name {app!r}")
|
||||
return "rift." + app.replace("-", "_")
|
||||
|
||||
|
||||
def install(frame, reporter: Reporter, app: str = "oculus-first-contact", msaa: int | None = None,
|
||||
add_to_steam: bool = True, force_unverified: bool = False) -> dict:
|
||||
"""A game from the Frame's prefix as a Steam game. Games that aren't verified (GAMES) need force_unverified:
|
||||
they get only the generic fixes the Frame finds in their binaries (the agent refuses otherwise and reports
|
||||
its static checks)."""
|
||||
from .install import installer
|
||||
|
||||
require_enabled()
|
||||
pkg = package_for(app)
|
||||
title = GAMES.get(app, {}).get("title")
|
||||
reporter.stage(f"Install {title or app}")
|
||||
sync_helpers(frame, reporter)
|
||||
res = frame.agent("fex_install", package=pkg, app=app, title=title, msaa=msaa,
|
||||
force_unverified=force_unverified)
|
||||
for k, v in (res.get("fixes") or {}).items():
|
||||
if v is not None:
|
||||
reporter.check(f"fix: {k}", v not in ("unknown build",), str(v))
|
||||
checks = res.get("checks") or {}
|
||||
if not res.get("verified"):
|
||||
reporter.check("Verified on the Frame", None, f"not yet: {app} gets only the generic fixes")
|
||||
if checks.get("engine") == "unreal" and isinstance(checks.get("ime"), str):
|
||||
reporter.check("Unreal IME fix", None, checks["ime"])
|
||||
apis = checks.get("graphics_imports") or []
|
||||
if apis and not {"d3d11", "vulkan"} & set(apis):
|
||||
reporter.check("Graphics API", None, f"imports only {', '.join(apis)}: untested with this mode")
|
||||
if add_to_steam:
|
||||
installer.add_to_steam(frame, [pkg], reporter)
|
||||
return res
|
||||
@@ -162,7 +162,6 @@
|
||||
"Android app": "",
|
||||
"Android app (APK)": "",
|
||||
"Android app without VR: installed unchanged and shown as a flat window": "",
|
||||
"Android emulator": "",
|
||||
"Android gives a process 128 thread keys (pthread keys, per-thread storage slots). Unity 6 takes about 40, and on the Frame the graphics and OpenXR libraries take more than on a Quest, so games with many other native libraries run out: Wander aborted at start (\"cannot create thread specific key\" in Firebase). Unity's keys without a clean-up function become slots behind one real key (libfp_tkeys.so); every other key and every other library keep real ones. Approach by Klownicle (GitHub #198).": "",
|
||||
"Android layer started": "",
|
||||
"Android VR": "",
|
||||
@@ -192,9 +191,6 @@
|
||||
"Automatic": "",
|
||||
"Automatic ({auto:.0%})": "",
|
||||
"Automatic SteamVR performance settings (this PC)": "",
|
||||
"AXRB mode on this PC": "",
|
||||
"AXRB {value} · Android emulator": "",
|
||||
"AXRB {value} · direct mode (no emulator)": "",
|
||||
"Back to library": "",
|
||||
"Back up the signing keys ({len}) to {shown_dir} first": "",
|
||||
"Banner": "",
|
||||
@@ -224,7 +220,6 @@
|
||||
"Cancel all": "",
|
||||
"canceled": "",
|
||||
"Canceled": "",
|
||||
"Cap the eye size": "",
|
||||
"Catalog": "",
|
||||
"Ceiling height for scene_emul.": "",
|
||||
"Ceiling height of the pretend room.": "",
|
||||
@@ -385,7 +380,6 @@
|
||||
"Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log).": "",
|
||||
"Diagnostics: removes layer color fades. Some fades no longer show.": "",
|
||||
"Diagnostics: the Vulkan shim (frame.vk_sanitize) adds Khronos' validation layer to the game's instance; its findings go to launch.log. The layer library (libVkLayer_khronos_validation.so) must be in the APK.": "",
|
||||
"Direct mode (no emulator)": "",
|
||||
"Disable application space warp if used": "",
|
||||
"Disable controller tracking offset": "",
|
||||
"Disable Meta XR Audio telemetry": "",
|
||||
@@ -467,7 +461,6 @@
|
||||
"Experimental bypass for a failed Meta asset-file check at startup. Reports an empty asset list and installed status without a file path; does not provide DLC. Experimental.": "",
|
||||
"Experimental Vulkan mutable-format workaround.": "",
|
||||
"Experimental. Leave it off unless asked to test it.": "",
|
||||
"Experimental: runs the game on this PC in AXRB (Android XR Bridge), an Android emulator that shows Android VR games through SteamVR. The first install downloads AXRB and its Android (about 2.5 GB) and needs Windows Hypervisor Platform, an NVIDIA or AMD graphics card and 12 GB of memory. Many games don't work in AXRB yet.": "",
|
||||
"Experimental: shows HUDs, menus and other flat panels that stay black on the Frame (for example Doom3Quest's HUD and PDA).": "",
|
||||
"Extra diagnostics (log)": "",
|
||||
"Extra Lepton settings": "",
|
||||
@@ -535,7 +528,6 @@
|
||||
"For one eye looking wrong (for example a jittering right eye): logs each eye's submitted pose against a fresh one, the eye-to-eye relation and when each eye's image was released, also into framebridge.log in the game's storage (some games stop Android's log early). No effect on the game.": "",
|
||||
"For OpenGL ES games whose memory use keeps growing (e.g. Vader Immortal).": "",
|
||||
"For overlay apps (fpsVR, wrist watches and other SteamVR add-ons that draw over games instead of being a game): FramePort registers the app with the Frame's SteamVR, which draws its overlays over whatever you play there, Quest and PC VR games alike. With 'start with SteamVR' on, SteamVR starts it by itself (also after a restart); otherwise start it from the library. It isn't launch-tested (it shows nothing without a game). Ignored on this PC.": "",
|
||||
"For this PC only: run the game in AXRB's direct mode (no Android emulator) or in AXRB's Android emulator. Automatic uses direct mode for Vulkan games.": "",
|
||||
"For troubleshooting: records the game's requests to Meta's online services.": "",
|
||||
"For Unity games that crash when they turn on anti-aliasing (for example The Room VR).": "",
|
||||
"For Unreal Engine 5 games that crash on the Frame (for example Metro Awakening).": "",
|
||||
@@ -570,7 +562,6 @@
|
||||
"FramePort checks GitHub for a new version every few hours. Update now downloads and checks it, then restarts FramePort; your games and settings stay.": "",
|
||||
"FramePort downloads it and restarts as the new version. Your games and settings stay.": "",
|
||||
"FramePort found more than one program in this app. Pick the one that starts it, not a helper like a crash reporter or updater.": "",
|
||||
"FramePort installs AXRB from its official release (per user, no administrator rights) and Google's Android emulator files. AXRB is a separate program with its own licence; you can remove it later from Windows' app list.": "",
|
||||
"FramePort installs games on the Frame over your network": "",
|
||||
"FramePort installs games on the Frame over your network.": "",
|
||||
"FramePort installs the new build of {title} and leaves its game data (OBB files) on the Frame as it is, for example data you changed there. If the new version needs new game data, use a normal update instead.": "",
|
||||
@@ -603,7 +594,6 @@
|
||||
"FramePort's tools, library, artwork and your games' signing keys. Back it up: an update needs the same key, or the game must be reinstalled and loses its saves.": "",
|
||||
"frames per second": "",
|
||||
"Frames that are set up show up here by themselves.": "",
|
||||
"Frames that carry more than the game's main picture (menus or overlays as extra layers) submit the main picture alone. For runtimes that refuse such frames: AXRB's emulator on this PC rejects every multi-layer OpenGL ES frame. The extra layers don't show; 2 (OpenGL ES games, the default on this PC) draws the flat panels that come after the main picture into it first, so menus and messages drawn as panels still show.": "",
|
||||
"Free up space": "",
|
||||
"Free up space on the Frame": "",
|
||||
"Free up space?": "",
|
||||
@@ -719,7 +709,6 @@
|
||||
"in your browser": "",
|
||||
"In your Steam library": "",
|
||||
"In your Steam library · launch settings changed — update it": "",
|
||||
"In your Steam library · patches changed — update it": "",
|
||||
"Install": "",
|
||||
"Install anyway": "",
|
||||
"Install anyway…": "",
|
||||
@@ -793,7 +782,6 @@
|
||||
"Keep the signing keys: without them, updating a game loses its saves.": "",
|
||||
"Keeps a smoothing setting off that the game turns on itself and that can freeze the Frame.": "",
|
||||
"Keeps every submitted image rect inside its swapchain. Unity can size the eye area a few pixels past the image on some Frames; SteamVR then rejects every frame (xrEndFrame -25, XR_ERROR_SWAPCHAIN_RECT_INVALID) and the game stops drawing (for example PowerWash Simulator stuck at \"Waiting\").": "",
|
||||
"Keeps games from rendering larger than the resolution scale allows.": "",
|
||||
"Keeps games running that use a 360° cube layer the Frame lacks (for example Budget Cuts Ultimate). Leave it on.": "",
|
||||
"Keeps one label so the title is stable.": "",
|
||||
"Keeps one name for the game, so it shows the same everywhere.": "",
|
||||
@@ -883,7 +871,6 @@
|
||||
"Logs every Meta Platform SDK request the game makes (user, entitlement, cloud saves, ...) and the messages that answer them, and every 10 s the requests that never got an answer (logcat tag fp_ovrtrace, in launch.log). For games that wait forever on a loading screen (for example Vader Immortal after its intro). Changes nothing: every call goes to the real function.": "",
|
||||
"Logs how game poses line up with the Frame's clock. No effect on the game.": "",
|
||||
"Logs Vulkan mistakes the game makes. Needs a special test build.": "",
|
||||
"Logs where an older Unity game's frame loop waits. No effect on the game.": "",
|
||||
"Longer for headsets that briefly think they're off your head.": "",
|
||||
"Look for the cable": "",
|
||||
"Looking at the app": "",
|
||||
@@ -895,7 +882,6 @@
|
||||
"Looking for your Frame…": "",
|
||||
"Looking up the store description and screenshots…": "",
|
||||
"Low": "",
|
||||
"Main picture only": "",
|
||||
"Make Unity text fields work": "",
|
||||
"Makes BlazeRush's menus and controllers work without Meta's avatars, and lets its shader fixes take effect.": "",
|
||||
"Makes grip, trigger and touch controls reach the game's hands (it picked the empty Gear VR controls on the Frame, for example Vader Immortal).": "",
|
||||
@@ -1035,16 +1021,13 @@
|
||||
"On the Frame, open Steam's Settings (Steam button → Settings).": "",
|
||||
"On the Frame, place Revive's runtime (FramePort's Revive, or the repack's bundled LibRevive) where the game's Oculus SDK looks for LibOVRRT so it can load a VR runtime (pure runtime substitution — what Revive's LoadLibrary hook does on Windows; that hook doesn't work under Proton on the Frame). It does NOT bypass the Oculus runtime signature check: a game that verifies the runtime's signature will still refuse it. Ignored on this PC (Revive's hook handles it there).": "",
|
||||
"On this PC": "",
|
||||
"On this PC only: lets games that draw with Vulkan themselves (for example The Climb 2) start in AXRB's direct mode.": "",
|
||||
"On this PC, launch the game through Revive's OpenVR backend, which talks straight to SteamVR — the original, most reliable Revive path. On by default for PC installs. Turn it off to use Revive's newer OpenXR backend instead. Ignored on the Frame (which always uses OpenXR).": "",
|
||||
"On this PC, runs Revive through SteamVR (works with more headsets).": "",
|
||||
"On your Frame": "",
|
||||
"on: {names}": "",
|
||||
"One PC game folder": "",
|
||||
"Only for installs on this PC (AXRB): which AXRB runs the game. Automatic picks AXRB's direct mode (AXRB-Emulatorless: the game's arm64 code runs in one Windows process, no Android emulator) for Vulkan games and the Android emulator for OpenGL ES games, since direct mode has no OpenGL ES renderer yet. The other parameters are direct mode's per-game options (AXRB's run_direct_game.ps1): translator (dynarmic, AXRB's default, or digitalis), precompose (AXRB composes the game's projection layers itself; also used by the emulator), and legacy_oculus_driver / quest_identity / controllers_only for unconverted APKs with older Oculus loaders (controllers_only needs legacy_oculus_driver). The Steam Frame ignores this.": "",
|
||||
"Only if 360° pictures show wrongly.": "",
|
||||
"Only if 360° pictures show wrongly: 1 = upside down, 2 = mirrored, 4 = turned around (add values to combine).": "",
|
||||
"Only in builds for this PC (AXRB's direct mode). Direct mode gives the game the Windows graphics driver's Vulkan device, which lacks extensions every Quest has: Android's external memory (VK_KHR_external_memory_fd, VK_ANDROID_external_memory_android_hardware_buffer, VK_EXT_queue_family_foreign) and VK_EXT_fragment_density_map. Engines that call VrApi with Vulkan themselves, such as CryEngine in The Climb 2, refuse to create their device without them (\"CrySystem initialization failed\" right after \"Renderer initialization\"). The game's renderer gets them listed (libfp_vkext.so); AXRB leaves them out again when the device is created.": "",
|
||||
"Only install software from sites you trust. FramePort downloads it from {host} and installs it on your Frame.": "",
|
||||
"Only internal storage": "",
|
||||
"Open": "",
|
||||
@@ -1221,7 +1204,6 @@
|
||||
"Remove": "",
|
||||
"Remove converted copies now": "",
|
||||
"Remove from library…": "",
|
||||
"Remove from this PC": "",
|
||||
"Remove localized app names": "",
|
||||
"Remove tag": "",
|
||||
"Remove this theme": "",
|
||||
@@ -1353,7 +1335,6 @@
|
||||
"Select games to install": "",
|
||||
"Set by you.": "",
|
||||
"Set up for FramePort": "",
|
||||
"Set up Quest games on this PC?": "",
|
||||
"Set up with a USB cable": "",
|
||||
"Set up your Frame": "",
|
||||
"Sets Unity QualitySettings antiAliasing to 0. Unity's multisampled render-to-texture path can hang the Frame's GL driver ('zink: DEVICE LOST', for example Sniper Elite VR). Try it for GLES Unity games that freeze or crash the GPU.": "",
|
||||
@@ -1392,7 +1373,6 @@
|
||||
"Shows Batman: Arkham Shadow's cutscenes; set by that game's recipe.": "",
|
||||
"Shows each eye's picture to the other eye. For testing.": "",
|
||||
"Shows models whose shapes come from bone data the Frame's graphics driver misreads (for example BlazeRush's cars). Comes from a game's recipe.": "",
|
||||
"Shows only the game's main picture, without menus or overlays drawn as extra layers.": "",
|
||||
"Shows the Frame's cameras where the game expects passthrough.": "",
|
||||
"Shows the full environment when its upper part is clipped but menu panels look correct.": "",
|
||||
"Shows the headset's view with sound, whatever is running. It plays here where possible, else in mpv or your browser. Stop it when you're done: it costs the game some speed.": "",
|
||||
@@ -1409,7 +1389,7 @@
|
||||
"Skip this version": "",
|
||||
"Skips a Quest-only reporting step in Meta's audio library that crashes some games.": "",
|
||||
"Skips a separate data-file check when an Unreal game still hangs at startup with the content check off. Experimental; the game still needs its data.": "",
|
||||
"Skips Meta XR Audio's telemetry under x86_64 ARM translation (emulators). Not needed on the Frame, which runs games natively; added by itself to builds for this PC (AXRB) of games with Meta XR Audio.": "",
|
||||
"Skips Meta XR Audio's telemetry under x86_64 ARM translation (emulators). Not needed on the Frame, which runs games natively.": "",
|
||||
"Skips picture layers the Frame would refuse. Leave it on.": "",
|
||||
"Sleep": "",
|
||||
"Smooth as butter": "",
|
||||
@@ -1453,7 +1433,6 @@
|
||||
"Starts a video stream on the Frame and opens it in an mpv window.": "",
|
||||
"Starts a video stream on the Frame and opens it in your browser.": "",
|
||||
"Starts a video stream on the Frame and shows it here.": "",
|
||||
"Starts the game in SteamVR on this PC, in AXRB's Android emulator.": "",
|
||||
"Starts the game in SteamVR on this PC.": "",
|
||||
"Starts the game on the Frame while nobody wears the headset and reads the log: did it start and draw frames. It can't check the picture.": "",
|
||||
"Starts the game on the Frame — put the headset on.": "",
|
||||
@@ -1617,13 +1596,11 @@
|
||||
"This Frame isn't set up for FramePort yet. Run the setup once.": "",
|
||||
"This Frame's SSH key changed since FramePort last connected (SteamOS was reset, or another device now has its address). If it's your Frame, choose Trust the new key on the Steam Frame page.": "",
|
||||
"This game can't run on the Steam Frame.": "",
|
||||
"This game draws with OpenGL ES. AXRB's emulator copies such pictures to the PC frame by frame, so FramePort renders them at half size: expect about 30 frames per second at best, less with a high SteamVR resolution. Vulkan games don't have this limit.": "",
|
||||
"This game is marked \"Can't run\" on the Steam Frame:": "",
|
||||
"This is a website in an app: it opens in Meta's browser, which the Frame doesn't have. If nothing opens, visit the website in a browser.": "",
|
||||
"This is the example button from frameport.app. Real buttons install real games.": "",
|
||||
"this PC": "",
|
||||
"This PC": "",
|
||||
"This PC (AXRB, experimental)": "",
|
||||
"This PC (for PC VR games)": "",
|
||||
"This PC (Steam and Revive)": "",
|
||||
"This PC's disk is full. Free some space and try again.": "",
|
||||
@@ -1683,11 +1660,9 @@
|
||||
"Uninstall. ": "",
|
||||
"Uninstalled {title}": "",
|
||||
"Uninstalled {title} and deleted its files on this PC ({size})": "",
|
||||
"Unity frame loop diagnostics (log)": "",
|
||||
"Unity frame loop: wait for skipped frames": "",
|
||||
"Unity's built-in Oculus frame loop (frame.unity_oculus_check): before each frame wait, wait up to 50 ms for the last waited frame to begin and skip the wait if it doesn't. Fixes a freeze at the first scene switch (for example Sniper Elite VR); in other games it can make loading screens stutter.": "",
|
||||
"Unity's built-in Oculus frame loop (frame.unity_oculus_check): don't pass Unity's physics-step pose update on, so the hands use the frame's display-time poses. Fixes hands trailing the controllers in some games (for example Sniper Elite VR); in others it makes the hands lag (for example BattleSisters).": "",
|
||||
"Unity's built-in Oculus frame loop (frame.unity_oculus_check): every 5 s, logs how many frame updates, waits, begins and ends went through and which one is in progress (thread, frame, for how long), to see where a game that stops after a few frames waits. No effect on the game.": "",
|
||||
"Unity's built-in Oculus support (Unity 2017–2019) only starts VR when Android has Meta's com.oculus.systemactivities package; without it the game runs as a 2D app (the Android home screen or a black window, for example Accounting+, BattleSisters). Points that package name in libunity.so at \"android\", which always exists. Games on Unity's built-in VR (2017–2018, and 2019 without the Oculus XR Plugin) also get the frame wait their legacy frame loop never makes (libfp_ovrp.so calls ovrp_WaitToBeginFrame before ovrp_Update2; without it no frame starts, the dashboard freezes or the GPU hangs). Per game, the settings ovrp_begin_gate (a freeze at the first scene switch) and ovrp_hold_physics (hands trailing the controllers) switch on two more frame-loop fixes (for example Sniper Elite VR). The shim also counts a newly pressed trigger or A/B/X/Y as a mouse click (Input.GetMouseButtonDown), which Go-era screens wait for (for example Accounting+'s motion warning) and which Lepton never delivers.": "",
|
||||
"Unity's Oculus XR Plugin asks OVRPlugin whether the headset is worn; on the Frame the answer turns to \"not worn\" a few seconds after start. Games that only move the player while the headset is worn then show a frozen body with no head tracking and no controls (for example BONELAB). Reports the headset as worn; taking it off still pauses the game through the session's focus.": "",
|
||||
"Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does this on a Quest), so a selected text field only flashes a caret. Rewrites TouchScreenKeyboardShouldBeUsed -> false and isKeyboardUsingEvents (TMP) / InPlaceEditing (uGUI) -> true in libil2cpp.so, found per game with Cpp2IL (downloaded on first use), so fields stay selected and take key presses: Steam's keyboard (with \"Show the app window\") or Type on Frame from the PC.": "",
|
||||
@@ -1802,7 +1777,6 @@
|
||||
"Vulkan shim (frame.vk_sanitize): the game doesn't see VK_EXT_fragment_density_map(2). Unreal Engine 5 turns on fragment-density-map foveation when the driver offers it and expects the density map from the headset, which the Frame doesn't provide: image views and barriers for a missing image, then a crash in the Frame's Vulkan driver (for example Metro Awakening). Valve's own foveation layer isn't affected.": "",
|
||||
"Vulkan shim (frame.vk_sanitize): writes every distinct SPIR-V module the game creates to Android/data/<package>/files/fp_vk_shaders/<size>_<sha256>.spv, with index.txt listing every creation in order with its time, to find the shader behind a GPU hang and capture it for a new vk_shader_fix. Diagnostics include the newest modules. No effect on the game.": "",
|
||||
"Vulkan: drop invalid render-pass pointers": "",
|
||||
"Vulkan: list Android's external-memory extensions (this PC)": "",
|
||||
"v{remote} (updates on the next action)": "",
|
||||
"Wait for the game's GPU": "",
|
||||
"Waiting": "",
|
||||
@@ -1843,7 +1817,6 @@
|
||||
"Windows' firewall blocks the Frame from reaching FramePort. Start the setup again and allow the change when Windows asks.": "",
|
||||
"Windows' firewall blocks the Frame on public networks. Set this network to Private in Windows' network settings.": "",
|
||||
"Windows' firewall for WSL blocks the Frame. Show the setup command again and allow the change when Windows asks.": "",
|
||||
"With a resolution scale below 1, the largest eye size the game is told about is the scaled one too, for games that size their eye pictures from the maximum (for example Unity 2017 games). On by default on this PC for OpenGL ES games, whose pictures AXRB's emulator copies pixel by pixel.": "",
|
||||
"Without a Meta account, OVRPort's platform library makes up a new user id at every start (ovr_GetLoggedInUserID). Games that file saves under the user id (for example Walkabout Mini Golf) then start with an empty profile each time and seem to lose all progress. Gives the game a fixed id instead, different for each game and each FramePort user.": "",
|
||||
"Working": "",
|
||||
"Working…": "",
|
||||
|
||||
@@ -78,9 +78,6 @@ class Patch:
|
||||
needs_vr: bool = True # only matters for VR apps (hidden for Android apps without VR)
|
||||
# works on a 32-bit game through the a32 bridge (frame.a32_bridge); None = by category (works_on_a32)
|
||||
a32: bool | None = None
|
||||
# also in the build for this PC (AXRB): a fix of the APK itself, not of the Frame/Lepton; "experimental" = kept
|
||||
# there too but not yet seen working in AXRB (pc_selection treats both alike)
|
||||
on_pc: bool | str = False
|
||||
revision: int = 1 # bump when apply() writes something different: installed builds then show "Update on Frame"
|
||||
package_revisions: dict[str, int] = {} # narrower updates to otherwise shared patches
|
||||
|
||||
@@ -104,16 +101,6 @@ class Patch:
|
||||
|
||||
not_applicable_reason: str = ""
|
||||
|
||||
def pc_detect(self, analysis: Analysis) -> bool:
|
||||
"""True when the build for this PC (AXRB) needs the patch although the Steam Frame build doesn't (so it isn't
|
||||
in the recipe, which both builds share): pc_selection adds it."""
|
||||
return False
|
||||
|
||||
def pc_keeps(self, analysis: Analysis) -> bool:
|
||||
"""For an on_pc patch in the recipe: False when the build for this PC leaves it out for this game anyway
|
||||
(pc_selection, given the analysis)."""
|
||||
return True
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool: # apk stage; returns True when something changed
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -165,40 +152,6 @@ def works_on_a32(patch: Patch) -> bool:
|
||||
return patch.category not in ("overport", "frame")
|
||||
|
||||
|
||||
def pc_selection(patches: dict, analysis: Analysis | None = None) -> dict:
|
||||
"""The part of a Quest recipe that goes into the build for this PC (AXRB): OVRPort's patches, the APK fixes
|
||||
marked on_pc, FrameBridge (frame.adapter) with its settings except the Frame-only ones (settings.PC_DENY), the
|
||||
PC values of settings whose Frame default doesn't fit there (settings.PC_VALUES) and, given the analysis, the
|
||||
PC-only patches the game needs (Patch.pc_detect). The Steam Frame runtime's and Lepton's workarounds stay out;
|
||||
AXRB has its own Android and OpenXR runtime. Idempotent: a PC selection selects itself."""
|
||||
load_all()
|
||||
out = {pid: params for pid, params in patches.items()
|
||||
if pid in REGISTRY and (REGISTRY[pid].category == "overport" or REGISTRY[pid].on_pc)}
|
||||
if analysis is not None:
|
||||
out = {pid: params for pid, params in out.items() if REGISTRY[pid].pc_keeps(analysis)}
|
||||
for patch in all_patches():
|
||||
if patch.id not in out and patch.pc_detect(analysis):
|
||||
out[patch.id] = {}
|
||||
if "frame.adapter" in out:
|
||||
from .settings import PC_VALUES
|
||||
|
||||
for key, value in PC_VALUES.items():
|
||||
out[f"adapter.{key}"] = {"value": value}
|
||||
if analysis is not None and "GLES" in (analysis.graphics or ""):
|
||||
# AXRB's emulator sends GLES eye images as pixels (adb reverse, ~100 MB/s, synchronous in xrEndFrame):
|
||||
# Lucky's Tale at Monado's 896x1008 per eye 9 fps, at half size (FrameBridge's minimum) 30 fps
|
||||
scale = (out.get("adapter.scale") or {}).get("value")
|
||||
out["adapter.scale"] = {"value": min(float(scale), GLES_PC_SCALE) if scale else GLES_PC_SCALE}
|
||||
# ... and rejects every GLES frame with more than one layer (no layer batches without Vulkan): FrameBridge
|
||||
# submits the projection layer alone, with the quads after it drawn into it (projection_only=2)
|
||||
out["adapter.projection_only"] = {"value": 2}
|
||||
out["adapter.scale_max"] = {"value": 1} # also for games that size their eyes from the maximum
|
||||
return out
|
||||
|
||||
|
||||
GLES_PC_SCALE = 0.5 # eye-size scale for GLES games in the build for this PC (FrameBridge accepts 0.5-2.0)
|
||||
|
||||
|
||||
def all_patches() -> list[Patch]:
|
||||
load_all()
|
||||
return sorted(REGISTRY.values(), key=lambda p: (CATEGORIES.index(p.category), p.order, p.id))
|
||||
|
||||
@@ -21,7 +21,6 @@ def settings_text(recipe_patches: dict) -> bytes:
|
||||
|
||||
class FrameBridgeAdapter(Patch):
|
||||
id = "frame.adapter"
|
||||
on_pc = True
|
||||
a32 = True # also for 32-bit games on the bridge
|
||||
title = "FrameBridge OpenXR adapter"
|
||||
description = (
|
||||
|
||||
@@ -15,7 +15,6 @@ AVATAR = "libovravatarloader.so"
|
||||
|
||||
class AvatarStub(Patch):
|
||||
id = "frame.avatar_stub"
|
||||
on_pc = True
|
||||
title = "Skip Meta avatars"
|
||||
description = ("Meta's avatar library needs Meta's Horizon app, which the Frame doesn't have; it then stops the "
|
||||
"game at start (\"Failed to launch SystemActivities\", for example BlazeRush). Replaces it with a "
|
||||
|
||||
@@ -125,7 +125,6 @@ def patch_library(data: bytes) -> tuple[bytes, dict | None]:
|
||||
|
||||
class BlazeRush(Patch):
|
||||
id = "frame.blazerush"
|
||||
on_pc = True
|
||||
title = "BlazeRush: controllers without Meta avatars, own shader cache"
|
||||
description = ("BlazeRush 1.0.349 only (Klownicle's guide, GitHub #57). Its menus wait for Meta's avatar, which "
|
||||
"never comes on the Frame, before they create the game's own controllers: the second menu ignored "
|
||||
|
||||
@@ -26,7 +26,6 @@ def patch_dex(dex: Dex) -> list[str]:
|
||||
|
||||
class EosKeychainLoop(Patch):
|
||||
id = "frame.eos_keychain_loop"
|
||||
on_pc = "experimental"
|
||||
title = "Epic Online Services: stop the keychain retry loop"
|
||||
description = ("Epic Online Services (EOS) keeps its login keychain in the app's settings storage. When it can't "
|
||||
"write there, it resets the keychain and tries again without end (for example Beyond Sandbox: a "
|
||||
|
||||
@@ -68,7 +68,6 @@ EXPORTS = (
|
||||
|
||||
class LanguagePacks(Patch):
|
||||
id = "frame.langpacks"
|
||||
on_pc = True
|
||||
title = "Language packs from the game's files"
|
||||
description = ("OVRPort's platform loader answers the Meta Platform SDK's language-pack calls "
|
||||
"(ovr_LanguagePack_GetCurrent/SetCurrent) with 'no request', so a game that asks for its language "
|
||||
@@ -117,7 +116,6 @@ class LanguagePacks(Patch):
|
||||
|
||||
class AssetFiles(Patch):
|
||||
id = "frame.asset_files"
|
||||
on_pc = True
|
||||
title = "Content files from the game's data"
|
||||
description = ("Some games keep content in separate files next to their OBB (for example Star Wars: Tales from the "
|
||||
"Galaxy's Edge: its seasons and sound banks) and ask Meta's platform for them as asset files. "
|
||||
|
||||
@@ -9,7 +9,6 @@ MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
class Launcher(Patch):
|
||||
id = "frame.launcher"
|
||||
on_pc = True
|
||||
a32 = True # also for 32-bit games on the bridge
|
||||
needs_vr = False
|
||||
title = "Launcher category (INFO → LAUNCHER)"
|
||||
@@ -36,7 +35,6 @@ class Launcher(Patch):
|
||||
|
||||
class NoDebuggable(Patch):
|
||||
id = "frame.nodebug"
|
||||
on_pc = True
|
||||
needs_vr = False
|
||||
title = "Clear android:debuggable"
|
||||
description = ("OVRPort marks apps debuggable; that turns on CheckJNI, which aborts some Unreal games on sloppy "
|
||||
@@ -70,7 +68,6 @@ class NoDebuggable(Patch):
|
||||
|
||||
class MetaPermissions(Patch):
|
||||
id = "frame.meta_permissions"
|
||||
on_pc = True
|
||||
title = "Declare Meta-only permissions"
|
||||
description = ("Declares com.oculus.permission.* / horizonos.permission.* that the app uses, so Android grants "
|
||||
"them at install (for example the 'use spatial data' permission of mixed-reality games like "
|
||||
|
||||
@@ -84,7 +84,6 @@ def guard_microphone(data: bytes) -> bytes | None:
|
||||
|
||||
class OvrMicrophone(Patch):
|
||||
id = "frame.ovr_microphone"
|
||||
on_pc = True
|
||||
title = "Fix crash when the game prepares the microphone"
|
||||
description = ("OVRPort's Meta platform library opens the microphone only when it's started, but answers a "
|
||||
"game's question about the microphone buffer size from that not-yet-opened stream: games that ask "
|
||||
|
||||
@@ -105,7 +105,6 @@ def _keystore(package: str) -> bytes:
|
||||
|
||||
class OvrStableUser(Patch):
|
||||
id = "frame.ovr_stable_user"
|
||||
on_pc = True
|
||||
title = "Keep the same Meta user between starts"
|
||||
description = ("Without a Meta account, OVRPort's platform library makes up a new user id at every start "
|
||||
"(ovr_GetLoggedInUserID). Games that file saves under the user id (for example Walkabout Mini "
|
||||
|
||||
@@ -23,7 +23,6 @@ def ovr_importers(ws) -> list[str]:
|
||||
|
||||
class OvrTrace(Patch):
|
||||
id = "frame.ovr_trace"
|
||||
on_pc = True
|
||||
title = "Trace Meta platform requests (diagnostics)"
|
||||
description = ("Logs every Meta Platform SDK request the game makes (user, entitlement, cloud saves, ...) and the "
|
||||
"messages that answer them, and every 10 s the requests that never got an answer (logcat tag "
|
||||
|
||||
@@ -29,7 +29,6 @@ class PlatformCompat(Patch):
|
||||
"is skipped.")
|
||||
order = 30
|
||||
default_on = True
|
||||
on_pc = True
|
||||
|
||||
def detect(self, a):
|
||||
return Suggestion(True, "Applied automatically when the OVRPort build needs it.")
|
||||
@@ -53,7 +52,6 @@ class OvrStubs(Patch):
|
||||
"'cannot locate symbol ovr_...'. Online/store features stay unavailable.")
|
||||
order = 31
|
||||
default_on = True
|
||||
on_pc = True # missing Meta platform symbols crash the game in any Android, not only on the Frame
|
||||
|
||||
def detect(self, a):
|
||||
return Suggestion(True, "Applied automatically when the OVRPort build needs it.")
|
||||
|
||||
@@ -45,7 +45,6 @@ class SwapchainLimit(Patch):
|
||||
"16384 px so the Frame's runtime (8192 px max) decides instead.")
|
||||
order = 45
|
||||
default_on = True
|
||||
on_pc = True # the guard is in OVRPort's dispatcher, which every OVRPort build carries
|
||||
|
||||
def detect(self, a):
|
||||
video = sorted(lib for lib in a.libs if lib.lower().startswith(VIDEO_LIBS))
|
||||
|
||||
@@ -51,7 +51,6 @@ def patch_metaxr_telemetry(data: bytes) -> bytes | None:
|
||||
|
||||
class MetaXrTelemetry(Patch):
|
||||
id = "frame.metaxr_telemetry"
|
||||
on_pc = True
|
||||
title = "Meta XR Audio: skip Quest telemetry class"
|
||||
description = ("Unreal builds of Meta XR Audio abort ('ClassNotFoundException com.oculus.os.AnalyticsEvent') on "
|
||||
"non-Quest devices. Patches that one JNI FindClass call so the library continues (for example NOPE "
|
||||
@@ -80,7 +79,6 @@ class MetaXrTelemetry(Patch):
|
||||
|
||||
class OculusOsStubs(Patch):
|
||||
id = "frame.oculusos"
|
||||
on_pc = True
|
||||
title = "Oculus OS telemetry class stubs"
|
||||
description = ("Adds no-op com.oculus.os.AnalyticsEvent / UnifiedTelemetryLogger classes for native code that "
|
||||
"looks them up through the app class loader (for example Nano, NOPE Challenge).")
|
||||
|
||||
@@ -73,7 +73,6 @@ def oculus_model_checks(data: bytes) -> tuple[bytes, int]:
|
||||
|
||||
class UnityOculusCheck(Patch):
|
||||
id = "frame.unity_oculus_check"
|
||||
on_pc = True
|
||||
title = "Unity: start VR without Meta's system apps"
|
||||
description = ("Unity's built-in Oculus support (Unity 2017–2019) only starts VR when Android has Meta's "
|
||||
"com.oculus.systemactivities package; without it the game runs as a 2D app (the Android home "
|
||||
|
||||
@@ -71,14 +71,6 @@ class UnityMultiPass(Il2cppReturnPatch):
|
||||
def detect(self, a):
|
||||
return None # only for a game that shows the symptom (catalog recipe or the user's choice)
|
||||
|
||||
# AXRB's emulator (gfxstream GLES) lists GL_OVR_multiview2, but its encoder finds a multiview framebuffer
|
||||
# incomplete and drops every draw into it (logcat: "GL2Encoder.cpp:s_glDrawElements ... GL error 0x506
|
||||
# checkFramebufferCompleteness"; Lucky's Tale, 2026-10-15: both eyes black). Each eye in its own pass draws.
|
||||
on_pc = "experimental"
|
||||
|
||||
def pc_detect(self, a):
|
||||
return self.applies(a) and "GLES" in a.graphics and bool((a.extra or {}).get("oculus_xr_plugin"))
|
||||
|
||||
|
||||
class UnityNoOverlayCopy(Il2cppReturnPatch):
|
||||
id = "frame.unity_no_overlay_copy"
|
||||
|
||||
@@ -147,7 +147,6 @@ class Il2cppReturnPatch(Patch):
|
||||
|
||||
class UnityTextInput(Il2cppReturnPatch):
|
||||
id = "frame.unity_text_input"
|
||||
on_pc = True
|
||||
title = "Make Unity text fields work"
|
||||
description = ("Unity's TMP_InputField / InputField wait for Android's on-screen keyboard and close themselves a "
|
||||
"frame later when there is none (Lepton has no on-screen keyboard; Meta's system keyboard does "
|
||||
|
||||
@@ -24,7 +24,6 @@ def heavy_native_libs(a) -> list[str]:
|
||||
|
||||
class UnityTlsKeyPool(Patch):
|
||||
id = "frame.unity_tls_key_pool"
|
||||
on_pc = "experimental"
|
||||
title = "Unity: share thread keys (games that run out of them)"
|
||||
description = (
|
||||
"Android gives a process 128 thread keys (pthread keys, per-thread storage slots). Unity 6 takes about 40, "
|
||||
|
||||
@@ -60,7 +60,6 @@ def patch_dex(dex: Dex, package: str) -> list[str]:
|
||||
|
||||
class UnrealSkipObbCheck(Patch):
|
||||
id = "frame.unreal_skip_obb_check"
|
||||
on_pc = True
|
||||
title = "Unreal: start without the OBB check"
|
||||
description = ("Unreal games can check their whole data file (.obb) before starting (bVerifyOBBOnStartUp). On "
|
||||
"the Frame that check's progress and error screens are invisible (Lepton runs VR games without "
|
||||
@@ -97,7 +96,6 @@ register(UnrealSkipObbCheck)
|
||||
|
||||
class UnrealSkipObbDelivery(Patch):
|
||||
id = "frame.unreal_skip_obb_delivery"
|
||||
on_pc = True
|
||||
title = "Unreal: skip the OBB delivery check"
|
||||
description = ("Unreal's Java OBB delivery check can leave a game on an invisible DownloaderActivity screen "
|
||||
"even with the manifest CRC check off (e.g. Into The Radius). Makes known Unreal "
|
||||
|
||||
@@ -49,7 +49,6 @@ def missing_lookups(lookups, plugin_exports) -> list[str]:
|
||||
|
||||
class UnrealOvrpEntrypoints(Patch):
|
||||
id = "frame.unreal_ovrp_entrypoints"
|
||||
on_pc = True
|
||||
title = "Unreal: stand-ins for OVRPlugin functions OVRPort lacks"
|
||||
description = ("Unreal's Oculus module looks up every OVRPlugin function it was built against and starts no VR "
|
||||
"when one is missing; OVRPort's OpenXR OVRPlugin lacks some old ones (for example ovrp_GetPTWNear, "
|
||||
|
||||
@@ -182,7 +182,6 @@ class _SantaCruzGate(Patch):
|
||||
|
||||
class QuestPrecompileDone(_SantaCruzGate):
|
||||
id = "frame.unreal_quest_precompile"
|
||||
on_pc = True
|
||||
title = "Unreal: don't wait for the Quest shader precompile"
|
||||
description = ("ILMxLAB's Unreal games (for example Vader Immortal) wait on their loading card until "
|
||||
"UVRUtils::GetQuestShaderPrecompilePercent reaches 100 %. The precompile only starts on a Quest "
|
||||
@@ -202,7 +201,6 @@ class QuestPrecompileDone(_SantaCruzGate):
|
||||
|
||||
class QuestKeyMap(_SantaCruzGate):
|
||||
id = "frame.unreal_quest_keymap"
|
||||
on_pc = True
|
||||
title = "Unreal: Quest controller bindings (RPOC key map)"
|
||||
description = ("ILMxLAB's Unreal games (for example Vader Immortal) bind their controls through URPOCKeyMapManager"
|
||||
"Component, which for an Oculus headset picks the Quest (\"SantaCruz\") or the Gear VR key set by "
|
||||
|
||||
@@ -138,7 +138,6 @@ def _engine(ws) -> str | None:
|
||||
|
||||
class UnrealThumbTouch(Patch):
|
||||
id = "frame.unreal_thumb_touch"
|
||||
on_pc = True
|
||||
title = "Unreal: thumb follows the capacitive touches"
|
||||
description = ("Unreal 4's Oculus input sets the ThumbUp axis (games animate the hand's thumb with it) from "
|
||||
"Meta's near-touch sensing, which the Frame never reports: the thumb always points up. Uses the "
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Android-only Vulkan device extensions for builds for this PC (native/vkext): AXRB's direct mode, The Climb 2."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...analysis import elf
|
||||
from ..base import ApkContext, Patch, register
|
||||
from . import artifact
|
||||
|
||||
HELPER = "libfp_vkext.so"
|
||||
RENAMES = {"vkEnumerateDeviceExtensionProperties": "fpEnumerateDeviceExtensionProperties"}
|
||||
# not the game's renderer: XR/Meta/FramePort libraries that may import the function too
|
||||
NOT_ENGINE = ("libopenxr", "libOVR", "libovr", "libvrapi", "libfp_", "libframe", "libVkLayer", "libc++")
|
||||
|
||||
|
||||
class VulkanAndroidExtensions(Patch):
|
||||
id = "frame.vk_android_extensions"
|
||||
on_pc = True
|
||||
title = "Vulkan: list Android's external-memory extensions (this PC)"
|
||||
description = (
|
||||
"Only in builds for this PC (AXRB's direct mode). Direct mode gives the game the Windows graphics driver's "
|
||||
"Vulkan device, which lacks extensions every Quest has: Android's external memory "
|
||||
"(VK_KHR_external_memory_fd, VK_ANDROID_external_memory_android_hardware_buffer, "
|
||||
"VK_EXT_queue_family_foreign) and VK_EXT_fragment_density_map. Engines that call VrApi with Vulkan "
|
||||
"themselves, such as CryEngine in The Climb 2, refuse to create their device without them (\"CrySystem "
|
||||
"initialization failed\" right after \"Renderer initialization\"). The game's renderer gets them listed "
|
||||
"(libfp_vkext.so); AXRB leaves them out again when the device is created.")
|
||||
order = 73
|
||||
|
||||
def applies(self, a): # never in a Steam Frame recipe: its driver has these extensions
|
||||
return False
|
||||
|
||||
def pc_detect(self, a) -> bool:
|
||||
return a.direct_vrapi and a.graphics.startswith("Vulkan") and "arm64-v8a" in a.abis
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
ws = ctx.ws
|
||||
if ws.abi != "arm64-v8a":
|
||||
return False
|
||||
changed = []
|
||||
for lib in ws.libs():
|
||||
if lib.startswith(NOT_ENGINE):
|
||||
continue
|
||||
data = ws.read_lib(lib)
|
||||
if not elf.is_elf(data) or not set(RENAMES) & elf.dyn_symbols(data, defined=False):
|
||||
continue
|
||||
patched, renamed = elf.rename_imports(data, RENAMES)
|
||||
if renamed:
|
||||
ws.put(ws.lib(lib), elf.add_needed(patched, HELPER))
|
||||
changed.append(lib)
|
||||
if not changed:
|
||||
return False
|
||||
ws.put(ws.lib(HELPER), artifact(ws.abi, HELPER))
|
||||
ctx.notes.append(f"{', '.join(changed)}: Vulkan device extensions through {HELPER}")
|
||||
return True
|
||||
|
||||
def validate(self, ctx: ApkContext):
|
||||
ws = ctx.ws
|
||||
if ws.abi != "arm64-v8a" or not ws.has(ws.lib(HELPER)):
|
||||
return []
|
||||
return [("Android Vulkan extensions listed", True, HELPER)]
|
||||
|
||||
|
||||
register(VulkanAndroidExtensions)
|
||||
@@ -30,7 +30,6 @@ def engine_libs(ws, analysis) -> list[str]:
|
||||
|
||||
class VulkanSanitize(Patch):
|
||||
id = "frame.vk_sanitize"
|
||||
on_pc = "experimental"
|
||||
title = "Vulkan: drop invalid render-pass pointers"
|
||||
description = ("Some engines leave the pNext of unused Vulkan attachment references uninitialized. The Frame's "
|
||||
"driver never reads it, but Lepton always loads Steam's Fossilize shader-cache layer, which follows "
|
||||
|
||||
@@ -10,7 +10,6 @@ SHIM = "libglshim.so"
|
||||
|
||||
class VrApiBridge(Patch):
|
||||
id = "frame.vrapi_bridge"
|
||||
on_pc = True # The Climb 2 renders in AXRB's direct mode (2026-10-16, headless)
|
||||
title = "VrApi → OpenXR bridge"
|
||||
description = (
|
||||
"Replaces libvrapi.so with a VrApi→OpenXR bridge (Android-XR-Bridge/OVRPort fork, GPL-3.0, patched for the "
|
||||
@@ -54,13 +53,6 @@ class GlShim(Patch):
|
||||
order = 61
|
||||
requires = ("frame.vrapi_bridge",)
|
||||
experimental = True
|
||||
# also in builds for this PC: its eglGetProcAddress wrapper hands out GPU timer-query stand-ins when the driver
|
||||
# has none (AXRB's emulator: Path of the Warrior crashed on a NULL glGenQueriesEXT), and hiding multiview helps
|
||||
# there too (gfxstream drops draws into multiview framebuffers); its GLSL fixes are additive
|
||||
on_pc = "experimental"
|
||||
|
||||
def pc_keeps(self, a):
|
||||
return "GLES" in a.graphics # Vulkan VrApi games (The Climb 2) go to AXRB's direct mode without it
|
||||
|
||||
def detect(self, a):
|
||||
if a.direct_vrapi and a.uses_glad_gl and "GLES" in a.graphics and "arm64-v8a" in a.abis:
|
||||
@@ -94,7 +86,6 @@ class GlShim(Patch):
|
||||
|
||||
|
||||
class UnityGlShim(GlShim):
|
||||
on_pc = False # its MSRTT stand-ins work around Zink; not tried in AXRB
|
||||
id = "frame.unity_gl_shim"
|
||||
title = "GL shim for Unity (no multisampled render-to-texture)"
|
||||
description = (
|
||||
@@ -118,7 +109,6 @@ class UnityGlShim(GlShim):
|
||||
|
||||
|
||||
class UnrealGlShim(GlShim):
|
||||
on_pc = False
|
||||
id = "frame.unreal_gl_shim"
|
||||
title = "GL shim for Unreal (no multisampled render-to-texture)"
|
||||
description = (
|
||||
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user