mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 21:00:42 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919d059add | ||
|
|
674c26f2d9 | ||
|
|
a6daa5cccb | ||
|
|
6f89123017 | ||
|
|
518c94d79f | ||
|
|
edf9f494e8 | ||
|
|
0db053c31f | ||
|
|
7439f082db | ||
|
|
f12f4aec4c | ||
|
|
8d95592f53 | ||
|
|
44d23f778d |
No files matched your search
@@ -554,11 +554,41 @@ passes stance selection (NewtonVR grip/trigger) but its motion warning waits for
|
||||
which Lepton never delivers (no focused Android window) → ovrpshim registers its own
|
||||
`UnityEngine.Input::GetMouseButtonDown(System.Int32)` icall (il2cpp_add_internal_call, after Unity's: resolve first)
|
||||
that adds a click in the frame a Touch trigger or A/B/X/Y is newly pressed (`unity_oculus_check` revision 3).
|
||||
BattleSisters (Unity 2019 InputSystem/XR InputDevices; libunity polls only Go masks 0x8000000/0x1000000/0x2000000;
|
||||
device model "Oculus Quest" vs Lepton's "Valve Lepton", ovrp_GetSystemProductName (already "Oculus Quest"),
|
||||
ovrp_GetNodePresent (hands present after ~2 s, logged by the shim) and missing ovrp_GetControllerState exports were
|
||||
all ruled out in the headset): buttons still dead; next = find what makes libunity's Oculus input poll only Go
|
||||
controllers. Accounting+ works (owner, 2026-10-06).
|
||||
BattleSisters (Unity 2019 InputSystem/XR InputDevices via `VrHandInput`): libunity's Oculus module (OVRPlugin
|
||||
function table: a global pointer at 0x16b57c0 in this build, slots filled by name, e.g. +0x160 GetControllerState,
|
||||
+0x168 State2, +0xe8 GetNodePresent) only reports controllers when `strncmp(deviceModel, "Oculus", 6) == 0` (next to
|
||||
the Go check `deviceModel == "Oculus Pacific"`); Lepton's model is "Valve Lepton" → Go/unknown → only Go masks polled,
|
||||
buttons dead. `unity_oculus_check` revision 4 (`oculus_model_checks`, Unity 2019 only) turns the 17 compares' length
|
||||
into 0 (`orr w2, wzr, #6` → `mov w2, #0` after the adrp/add of the "Oculus" literal; the string stays: it is also
|
||||
Unity's VR device name). Device result: libunity now polls `ovrp_GetControllerState(0x3)` (Touch); owner-confirmed
|
||||
2026-10-06: buttons work. Its first controller vibration then hit OVRPort's haptic-envelope bug (11 GB, OOM kill) →
|
||||
`haptic_fix` is now suggested for every game with libOVRPlugin.so (settings detect), BattleSisters works. Ruled out before: ProductName, GetNodePresent, the device-model string itself, exports.
|
||||
Accounting+ works (owner, 2026-10-06).
|
||||
**Vader Immortal (UE4, GitHub #49, 2026-10-06, headless):** stuck after the intro on an in-game image (the splash
|
||||
quad ends ~6 s in; then the game's own projection frames, 72 fps, balanced xrBeginFrame/xrEndFrame). Not the Platform
|
||||
SDK (`frame.ovr_trace`: only user + entitlement, both answered) and not the repack's Frida gadget (OVRPort's
|
||||
`patch_clean_up_frida` removes its loadLibrary). It **leaks ~430 GPU mappings (/dev/dri/renderD128) and ~20 MB a
|
||||
second** (6.5 GB + swap after 5 min, then 26 fps): page-fault stacks (perf -e page-faults, offsets resolved with the
|
||||
process maps + vrclient.so's own symbols; its text segment is at file offset + 0x4000) end in SteamVR's runtime:
|
||||
`xrBeginFrame → CSxrCompositorOpenVR::BeginFrame → SubmitExplicitTimingData → CVRCompositorSharedTextures::
|
||||
BeginGPUTimingCommandBuffer` and `xrEndFrame → CVRCompositorClient::SubmitWithArrayIndexAndTime`. Ruled out: the
|
||||
layer color scale/bias + image layout structs (FrameBridge `strip_color_bias` 1/2, diagnostic), Valve's Vulkan
|
||||
layers (VK_INSTANCE_LAYERS=""), array swapchains in general (Lucky's Tale/I Am Cat flat). Kernel tracepoints aren't
|
||||
allowed for steamos. FrameBridge: `layer_debug` logs xrDestroySwapchain and per-5 s xrBeginFrame/xrEndFrame counts;
|
||||
`frame_balance` (ends an open frame before the next begin) exists but Vader never leaves one open. Next (built,
|
||||
not yet run: the Frame slept): `layer_debug` also counts xrAcquire/Wait/ReleaseSwapchainImage per 5 s (hooked only
|
||||
with layer_debug, after the other conditional hooks) to see whether Vader skips a wait or release: it doesn't (361/361/361 per 5 s).
|
||||
FrameBridge `snapshot=N` (`snapshot_gl.c`, GLES): every N s the left-eye image the game submits is read back on its
|
||||
own context and saved as `files/fb_snap_0-7.ppm` (quarter size) — headless launches show a black headset view, this
|
||||
shows what the game draws. Vader: Lucasfilm logo (an OBB mp4: video works), then its loading card (portrait + segmented
|
||||
bar) that never advances; the async loader thread sleeps and OBB reads stop (~168 MB of a 2.7 GB pak).
|
||||
**Double launch (agent v57, 2026-10-06):** a second Play while Lepton still boots (~10 s with nothing to see) made
|
||||
the second Lepton stop the first one's container ("Waiting for steamlaunch-<appid> (PID …) to exit", exit 137
|
||||
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
|
||||
`flock` on `<base>/.launch.lock` (fd 9, inherited by Lepton; the watchdog and dashboard helpers close it with
|
||||
`9>&-`); a second launch exits 0 and logs to `<base>/launch-dup.log`. `upgrade_launchers` adds it to existing
|
||||
launchers. Verified on the device (second launch during boot: ignored, the first kept running). Vader on its loading
|
||||
card ignores input too (owner pressed/held every button: presses reach the runtime, the card never changes).
|
||||
**Lepton storage (2026-09-30):** each app's /sdcard (= /storage/emulated/0 → `<base>/lepton-data/external`) has `Movies`/`Download`/`Documents` symlinked to the Frame's `~/Videos`/`~/Downloads`/`~/Documents` (liblepton/mounting.sh, only if they exist at start); agent v24 `storage_targets` reads that mapping. Android's MediaProvider canonicalises paths to /home/steamos/... and rejects every file ("doesn't appear under [/system/media...]"), `sm list-volumes` is empty: the media index never works, apps must browse folders. Lepton installs with `adb install -g` (runtime permissions granted, MANAGE_EXTERNAL_STORAGE too). Files: `install/files.py`, `frameport frame send|storage`, GUI Files tab (formerly Frame → Send files).
|
||||
**SteamVR per-app settings (2026-09-30):** editing steamvr.vrsettings while SteamVR runs is lost; the web API (127.0.0.1:27062 /app/setsettings) needs `x-steamvr-secret`. `native/vrsettings` = `fp_vrsettings.exe` (freestanding, OpenVR `FnTable:IVRSettings_003` as a Utility app, loads SteamVR's bin/win64/openvr_api.dll) sets them live and SteamVR persists them: section `steam.app.<shortcut appid>`, keys `preferredRefreshRate` (float) and `motionSmoothingOverride` (0 global, 1 on, 2 off, 3 always). Steam Link (vrlink) lists the Frame's rates 72/80/90/96/108/120/144 in vrserver.txt and follows the per-app preference ("host preferred N Hz"; whether the key is honoured is unverified in-headset yet). Judder metric: vrcompositor.txt session summary dropped + "Timed out. N total" (Stormland: 0 dropped but 313 timeouts in 2 min); fpsVR (`%LOCALAPPDATA%\fpsVR\*.json`, 0.1 ms histograms) gives p99 CPU/GPU ms. `pcvr.steamvr_tuning` (default on, PC only) applies on Play: highest rate whose budget ≥ p99×1.05, at least one step down, smoothing on.
|
||||
|
||||
@@ -596,6 +626,11 @@ Installed apps find the release themselves (self-update), so the notes are what
|
||||
Windows console) plus PYTHONUTF8; macOS builds need `--python-version 3.12 --arch arm64` (cryptography has no wheels
|
||||
for flet's default Python / x86_64 cross-build), with a PyInstaller fallback step; `astral-sh/setup-uv` has no
|
||||
floating major tags after v7 → pin the exact version; force-moving a tag starts duplicate runs (cancel one).
|
||||
- `jni` (2026-10-06): jni_flutter 1.0.4/1.0.4+1 generate bindings that require jni ^1.1.0 while Flet's build template
|
||||
pins jni 1.0.0 → every `flet build` bundle failed (`JniVersionCheck`, "generated bindings expect package:jni
|
||||
^1.1.0"). pyproject `[tool.flet.flutter.pubspec.dependency_overrides] jni = "1.1.0"` fixes it (pinning
|
||||
jni_flutter 1.0.4 does not); drop it once Flet's template moves to jni 1.1. A failed tag build publishes nothing:
|
||||
delete and re-push the tag on the fixed commit.
|
||||
- macOS runner (2026-10-02): `macos-latest` jobs went unassigned (cancelled after 15 min, no steps); `macos-15`'s Xcode
|
||||
16.4 fails a Flutter plugin (`NWPath has no member`); `macos-26` (Xcode 26) builds with `flet build`. The PyInstaller
|
||||
fallback (`package.py --pyinstaller`) passes `--yes` so a failed `flet build`'s folder doesn't stop it at a prompt.
|
||||
|
||||
@@ -36,7 +36,7 @@ import time
|
||||
import zlib
|
||||
from types import SimpleNamespace
|
||||
|
||||
AGENT_VERSION = 56
|
||||
AGENT_VERSION = 57
|
||||
HOME = os.path.expanduser("~")
|
||||
STEAM = os.path.join(HOME, ".local/share/Steam")
|
||||
ANCHORS = os.path.join(HOME, "Applications/quest-frame")
|
||||
@@ -1889,6 +1889,7 @@ LAUNCH_SH = (r"""#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
app_dir={base_q}
|
||||
[[ -d "$app_dir/lepton-app" ]] || {{ echo "Game files missing at $app_dir (storage not mounted?)" >&2; exit 1; }}
|
||||
{single}
|
||||
# Some games (Unreal cloud saves, SUPERHOT's cloud/data: mode 1700) create folders without write/search permission
|
||||
# for the app inside Lepton (it writes through the folder's group), which breaks saving or makes the game quit.
|
||||
# Repair them before and during every launch.
|
||||
@@ -1939,10 +1940,19 @@ OLD_WATCHDOG = "( while sleep 2 && kill -0 $$ 2>/dev/null; do fix_perms; done )
|
||||
# by hand (also when Lepton shows its Android launcher first). Once the game's first VR frames are logged, the agent
|
||||
# closes it through Steam's UI (_dashboard_worker); opt out per game with FRAMEPORT_KEEP_DASHBOARD=1.
|
||||
DASHBOARD_LINE = ('[[ -n "${{FRAMEPORT_KEEP_DASHBOARD:-}}" ]] || python3 {agent_q} _dashboard_worker '
|
||||
'"$app_dir/launch.log" $$ >"$app_dir/dashboard.log" 2>&1 &')
|
||||
'"$app_dir/launch.log" $$ >"$app_dir/dashboard.log" 2>&1 9>&- &')
|
||||
WATCHDOG = ("parent=$PPID\n"
|
||||
"( while sleep 2 && kill -0 $$ 2>/dev/null; do fix_perms;"
|
||||
" if [[ $parent -gt 1 ]] && ! kill -0 $parent 2>/dev/null; then kill -TERM $$; fi; done ) & permfix=$!")
|
||||
" if [[ $parent -gt 1 ]] && ! kill -0 $parent 2>/dev/null; then kill -TERM $$; fi; done )"
|
||||
" 9>&- & permfix=$!")
|
||||
|
||||
|
||||
# One launcher per game: Play pressed again while Lepton still boots (~10 s with nothing to see) made the second Lepton
|
||||
# stop the first one's container, and both died (Vader Immortal, BattleSisters). The lock is held by Lepton's process
|
||||
# (inherited fd), so it is released when the game ends.
|
||||
SINGLE_LINE = ('exec 9>"$app_dir/.launch.lock"; flock -n 9 || '
|
||||
'{ echo "$(date +%s) already starting or running: second launch ignored" >>"$app_dir/launch-dup.log"; '
|
||||
'exit 0; }')
|
||||
|
||||
|
||||
def dashboard_line():
|
||||
@@ -1971,6 +1981,13 @@ def upgrade_launchers():
|
||||
new = new.replace(OLD_WATCHDOG, WATCHDOG, 1)
|
||||
if "_dashboard_worker" not in new and 'child=$!\nwait "$child"' in new:
|
||||
new = new.replace('child=$!\nwait "$child"', 'child=$!\n' + dashboard_line() + '\nwait "$child"', 1)
|
||||
guard = '[[ -d "$app_dir/lepton-app" ]] ||'
|
||||
if ".launch.lock" not in new and guard in new:
|
||||
i = new.index("\n", new.index(guard)) + 1
|
||||
new = new[:i] + SINGLE_LINE + "\n" + new[i:]
|
||||
# the lock belongs to Lepton only: helpers that outlive the game must not keep it
|
||||
new = new.replace("; fi; done ) & permfix=$!", "; fi; done ) 9>&- & permfix=$!", 1)
|
||||
new = new.replace('>"$app_dir/dashboard.log" 2>&1 &', '>"$app_dir/dashboard.log" 2>&1 9>&- &', 1)
|
||||
if PLAYS_LOG not in new:
|
||||
start, end = plays_lines(os.path.dirname(path))
|
||||
if "\nsetsid " in new and "\n trap - EXIT INT TERM\n" in new: # Lepton launcher
|
||||
@@ -1994,7 +2011,7 @@ def write_launcher(anchor, base, pkg, title, appid, lepton, env):
|
||||
if re.fullmatch(r"[A-Z_][A-Z0-9_]*", k))
|
||||
text = LAUNCH_SH.format(title=title.replace("\n", " "), pkg=pkg, base_q=shlex.quote(base), appid=appid,
|
||||
lepton_q=shlex.quote(lepton), extra_env=extra, watchdog=WATCHDOG,
|
||||
dashboard=dashboard_line(), plays_start=plays_lines(anchor)[0],
|
||||
dashboard=dashboard_line(), single=SINGLE_LINE, plays_start=plays_lines(anchor)[0],
|
||||
plays_end=plays_lines(anchor)[1])
|
||||
path = os.path.join(anchor, "launch.sh")
|
||||
with open(path + ".tmp", "w") as f:
|
||||
|
||||
@@ -5,10 +5,10 @@ b5949e9e90e4c51e2733b9f6cd3c857a447552ac7291aec340407c27b034b600 ./arm64-v8a/li
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
424d8e5cb15608ad627f9415df4c4665efd9358fed3447a5d3765279dc652f6b ./arm64-v8a/libframe_xrshim.so
|
||||
3bfd1261688ed766c0adb9172f44dd7d41cc6459f26205ce3069f536a52e4ab9 ./arm64-v8a/libglshim.so
|
||||
899b76382e6d10fe2b594b40fed369d03808a1f3ec7a18ccc05f420c11056aeb ./arm64-v8a/libopenxr_loader_generic.so
|
||||
a93928879af7b930f19c8c0a037a52711e194d2aacafea0d6d1de9809de76228 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
32525cd0a9ee3d9993d8871cd6905bdcf22cde4f4ba28f5db6434ce7d6554758 ./arm64-v8a/libvrapi.so
|
||||
79955f386b4e8d43b36b052f9f838e85310c4234403c31e1b2b3f2e914aa4627 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
49513211094cdaaa02f81f0101fa497da38979499cc8533e3f4bd0b71f6a3f85 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
|
||||
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
package: com.ILMxLAB.VaderImmortal.ep1
|
||||
title: 'Vader Immortal: Episode I'
|
||||
status: issues
|
||||
notes: Starts in VR and plays the intro, then stays on the loading card (Vader's portrait with a progress bar).
|
||||
details: Unreal Engine 4 (GLES). After the Lucasfilm intro the game shows its loading card and never loads the next
|
||||
scene; it keeps rendering at 72 fps and ignores input. Ruled out so far - Meta platform requests (all answered),
|
||||
video playback (the intro is a video and plays), the no-ForceQuit build, frame and swapchain handling. The Frame's
|
||||
SteamVR runtime also leaks about 20 MB of memory a second while the game runs, which slows it down after a few
|
||||
minutes. Next steps - find what the loading code waits for in the game's engine library (no symbols) and report
|
||||
the runtime leak to Valve. GitHub issue 49.
|
||||
tested_version: 1.1.1+667256.cl.387770
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
verified:
|
||||
date: '2026-10-06'
|
||||
issue: 49
|
||||
updated: '2026-10-06'
|
||||
source_hint: Vader Immortal- Episode I
|
||||
@@ -0,0 +1,26 @@
|
||||
package: com.PixelToys.BattleSisters
|
||||
title: BattleSisters
|
||||
status: works
|
||||
notes: Starts in VR; controller buttons and vibration work.
|
||||
details: Unity 2019.4 with Unity's built-in Oculus support. frame.unity_oculus_check starts VR (Meta's system-app check),
|
||||
adds the frame wait its legacy loop never makes (without it the GPU hung, black screen) and lets Unity's Oculus
|
||||
input accept Lepton's device model (it only reported controllers on a device named "Oculus ...", so buttons were
|
||||
dead). haptic_fix stops the first controller vibration from freezing the Frame (OVRPort's loader read its duration
|
||||
in nanoseconds as seconds and the game ran out of memory).
|
||||
tested_version: 1.2.4
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
frame:
|
||||
- frame.unity_no_msaa
|
||||
- frame.unity_oculus_check
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-06'
|
||||
issue: 48
|
||||
updated: '2026-10-06'
|
||||
min_app: 0.11.1
|
||||
source_hint: Battle Sister
|
||||
@@ -14,6 +14,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| BARTENDER VR SIMULATOR | Quest | ✅ Works | |
|
||||
| Batman: Arkham Shadow | Quest | ✅ Works | |
|
||||
| BattleGlide | Quest | ✅ Works | |
|
||||
| BattleSisters | Quest | ✅ Works | |
|
||||
| Beat Saber | Quest | ✅ Works | |
|
||||
| Beat Saber (co-existence build) | Quest | ✅ Works | |
|
||||
| Blade & Sorcery: Nomad | Quest | ✅ Works | |
|
||||
@@ -66,6 +67,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
|
||||
| Silhouette | Quest | ⚠️ Works with issues | Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky. |
|
||||
| Time Stall | Quest | ⚠️ Works with issues | Both eyes distort during movement (unresolved). |
|
||||
| Vader Immortal: Episode I | Quest | ⚠️ Works with issues | Starts in VR and plays the intro, then stays on the loading card (Vader's portrait with a progress bar). |
|
||||
| WiiCompiled VR | Quest | ⚠️ Works with issues | To add a game: in FramePort's Files tab, upload your .wcgame file to this game's storage, folder Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR… |
|
||||
| BONELAB | Quest | ❌ Doesn't run | Crashes while starting its graphics (Vulkan); no fix yet. |
|
||||
| Espire 1: VR Operative (Quest Edition) | Quest | ❌ Doesn't run | Mesa GL driver crash during texture upload. |
|
||||
|
||||
@@ -28,21 +28,6 @@ xrAttachSessionActionSets:
|
||||
.globl fwd_xrAttachSessionActionSets
|
||||
.hidden fwd_xrAttachSessionActionSets
|
||||
fwd_xrAttachSessionActionSets:
|
||||
.quad frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrBeginFrame
|
||||
.type xrBeginFrame, %function
|
||||
xrBeginFrame:
|
||||
adrp x16, fwd_xrBeginFrame
|
||||
ldr x16, [x16, :lo12:fwd_xrBeginFrame]
|
||||
br x16
|
||||
.size xrBeginFrame, .-xrBeginFrame
|
||||
.data
|
||||
.p2align 3
|
||||
.globl fwd_xrBeginFrame
|
||||
.hidden fwd_xrBeginFrame
|
||||
fwd_xrBeginFrame:
|
||||
.quad frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
@@ -403,21 +388,6 @@ xrPathToString:
|
||||
.globl fwd_xrPathToString
|
||||
.hidden fwd_xrPathToString
|
||||
fwd_xrPathToString:
|
||||
.quad frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrReleaseSwapchainImage
|
||||
.type xrReleaseSwapchainImage, %function
|
||||
xrReleaseSwapchainImage:
|
||||
adrp x16, fwd_xrReleaseSwapchainImage
|
||||
ldr x16, [x16, :lo12:fwd_xrReleaseSwapchainImage]
|
||||
br x16
|
||||
.size xrReleaseSwapchainImage, .-xrReleaseSwapchainImage
|
||||
.data
|
||||
.p2align 3
|
||||
.globl fwd_xrReleaseSwapchainImage
|
||||
.hidden fwd_xrReleaseSwapchainImage
|
||||
fwd_xrReleaseSwapchainImage:
|
||||
.quad frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
@@ -523,20 +493,5 @@ xrSyncActions:
|
||||
.globl fwd_xrSyncActions
|
||||
.hidden fwd_xrSyncActions
|
||||
fwd_xrSyncActions:
|
||||
.quad frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrWaitSwapchainImage
|
||||
.type xrWaitSwapchainImage, %function
|
||||
xrWaitSwapchainImage:
|
||||
adrp x16, fwd_xrWaitSwapchainImage
|
||||
ldr x16, [x16, :lo12:fwd_xrWaitSwapchainImage]
|
||||
br x16
|
||||
.size xrWaitSwapchainImage, .-xrWaitSwapchainImage
|
||||
.data
|
||||
.p2align 3
|
||||
.globl fwd_xrWaitSwapchainImage
|
||||
.hidden fwd_xrWaitSwapchainImage
|
||||
fwd_xrWaitSwapchainImage:
|
||||
.quad frame_unsupported
|
||||
.section .note.GNU-stack,"",%progbits
|
||||
@@ -1,7 +1,6 @@
|
||||
// Generated from overport 3.4.3 libopenxr_loader_generic.so exports minus hooked functions.
|
||||
FORWARD(xrApplyHapticFeedback)
|
||||
FORWARD(xrAttachSessionActionSets)
|
||||
FORWARD(xrBeginFrame)
|
||||
FORWARD(xrBeginSession)
|
||||
FORWARD(xrCreateAction)
|
||||
FORWARD(xrCreateActionSet)
|
||||
@@ -26,7 +25,6 @@ FORWARD(xrGetReferenceSpaceBoundsRect)
|
||||
FORWARD(xrGetSystem)
|
||||
FORWARD(xrGetViewConfigurationProperties)
|
||||
FORWARD(xrPathToString)
|
||||
FORWARD(xrReleaseSwapchainImage)
|
||||
FORWARD(xrRequestExitSession)
|
||||
FORWARD(xrResultToString)
|
||||
FORWARD(xrStopHapticFeedback)
|
||||
@@ -34,4 +32,3 @@ FORWARD(xrStringToPath)
|
||||
FORWARD(xrStructureTypeToString)
|
||||
FORWARD(xrSuggestInteractionProfileBindings)
|
||||
FORWARD(xrSyncActions)
|
||||
FORWARD(xrWaitSwapchainImage)
|
||||
@@ -34,23 +34,6 @@ xrAttachSessionActionSets:
|
||||
.globl fwd_xrAttachSessionActionSets
|
||||
.hidden fwd_xrAttachSessionActionSets
|
||||
fwd_xrAttachSessionActionSets:
|
||||
.word frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrBeginFrame
|
||||
.type xrBeginFrame, %function
|
||||
xrBeginFrame:
|
||||
ldr ip, 1f
|
||||
2: add ip, pc, ip
|
||||
ldr ip, [ip]
|
||||
bx ip
|
||||
1: .word fwd_xrBeginFrame - (2b + 8)
|
||||
.size xrBeginFrame, .-xrBeginFrame
|
||||
.data
|
||||
.p2align 2
|
||||
.globl fwd_xrBeginFrame
|
||||
.hidden fwd_xrBeginFrame
|
||||
fwd_xrBeginFrame:
|
||||
.word frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
@@ -459,23 +442,6 @@ xrPathToString:
|
||||
.globl fwd_xrPathToString
|
||||
.hidden fwd_xrPathToString
|
||||
fwd_xrPathToString:
|
||||
.word frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrReleaseSwapchainImage
|
||||
.type xrReleaseSwapchainImage, %function
|
||||
xrReleaseSwapchainImage:
|
||||
ldr ip, 1f
|
||||
2: add ip, pc, ip
|
||||
ldr ip, [ip]
|
||||
bx ip
|
||||
1: .word fwd_xrReleaseSwapchainImage - (2b + 8)
|
||||
.size xrReleaseSwapchainImage, .-xrReleaseSwapchainImage
|
||||
.data
|
||||
.p2align 2
|
||||
.globl fwd_xrReleaseSwapchainImage
|
||||
.hidden fwd_xrReleaseSwapchainImage
|
||||
fwd_xrReleaseSwapchainImage:
|
||||
.word frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
@@ -595,22 +561,5 @@ xrSyncActions:
|
||||
.globl fwd_xrSyncActions
|
||||
.hidden fwd_xrSyncActions
|
||||
fwd_xrSyncActions:
|
||||
.word frame_unsupported
|
||||
.text
|
||||
.p2align 2
|
||||
.globl xrWaitSwapchainImage
|
||||
.type xrWaitSwapchainImage, %function
|
||||
xrWaitSwapchainImage:
|
||||
ldr ip, 1f
|
||||
2: add ip, pc, ip
|
||||
ldr ip, [ip]
|
||||
bx ip
|
||||
1: .word fwd_xrWaitSwapchainImage - (2b + 8)
|
||||
.size xrWaitSwapchainImage, .-xrWaitSwapchainImage
|
||||
.data
|
||||
.p2align 2
|
||||
.globl fwd_xrWaitSwapchainImage
|
||||
.hidden fwd_xrWaitSwapchainImage
|
||||
fwd_xrWaitSwapchainImage:
|
||||
.word frame_unsupported
|
||||
.section .note.GNU-stack,"",%progbits
|
||||
@@ -86,6 +86,18 @@ static int foveation_fix = 1;
|
||||
// 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;
|
||||
static int strip_color_bias = 0;
|
||||
static int snapshot = 0; // seconds between eye-image snapshots (snapshot_gl.c; 0 = off)
|
||||
static char snap_dir[256];
|
||||
static void snap_on_create(XrSwapchain handle, const XrSwapchainCreateInfo *info);
|
||||
static void snap_on_destroy(XrSwapchain handle);
|
||||
static void snap_on_enumerate(XrSwapchain handle, uint32_t count, const XrSwapchainImageBaseHeader *images);
|
||||
static void snap_on_acquire(XrSwapchain handle, uint32_t index);
|
||||
static void snap_end_frame(const XrFrameEndInfo *info);
|
||||
static int frame_balance = 0; // end a still-open frame before the next xrBeginFrame
|
||||
static int frame_begins, frame_discarded, frame_ends, frame_balanced; // per pacing period (layer_debug)
|
||||
static int frame_open;
|
||||
static int sc_acquires, sc_waits, sc_releases, sc_wait_fails; // per pacing period (layer_debug) // drop XrCompositionLayerColorScaleBiasKHR (the runtime's color pass)
|
||||
static int controller_fix = 1;
|
||||
static int swapchain_fix = 1;
|
||||
static int rect_clamp = 1;
|
||||
@@ -136,6 +148,9 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "scale=%f", &value) == 1 && value >= 0.5f && value <= 2.0f) scale = value;
|
||||
if (sscanf(line, "foveation_fix=%f", &value) == 1) foveation_fix = value != 0;
|
||||
if (sscanf(line, "hide_space_warp=%f", &value) == 1) hide_space_warp = value != 0;
|
||||
if (sscanf(line, "strip_color_bias=%f", &value) == 1) strip_color_bias = (int)value;
|
||||
if (sscanf(line, "frame_balance=%f", &value) == 1) frame_balance = value != 0;
|
||||
if (sscanf(line, "snapshot=%f", &value) == 1) snapshot = value > 0 ? (int)value : 0;
|
||||
if (sscanf(line, "controller_fix=%f", &value) == 1) controller_fix = value != 0;
|
||||
if (sscanf(line, "swapchain_fix=%f", &value) == 1) swapchain_fix = value != 0;
|
||||
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
|
||||
@@ -209,11 +224,17 @@ static void initialize(void) {
|
||||
}
|
||||
const char *env = getenv("FRAMEBRIDGE_CONFIG");
|
||||
if (env && *env) read_settings(env);
|
||||
if (snapshot && *process && !strchr(process, '/')) {
|
||||
snprintf(snap_dir, sizeof(snap_dir), "/sdcard/Android/data/%s/files", process);
|
||||
LOG("per-game: snapshot every %d s to %s/fb_snap_N.ppm", snapshot, snap_dir);
|
||||
}
|
||||
if (eye_debug && *process && !strchr(process, '/')) {
|
||||
snprintf(path, sizeof(path), "/sdcard/Android/data/%s/files/framebridge.log", process);
|
||||
log_file = fopen(path, "a");
|
||||
}
|
||||
if (hide_space_warp) LOG("per-game: hide_space_warp=1 (XR_FB_space_warp hidden, space warp info removed)");
|
||||
if (strip_color_bias) LOG("per-game: strip_color_bias=%d (layer color scale/bias removed)", strip_color_bias);
|
||||
if (frame_balance) LOG("per-game: frame_balance=1 (an open frame is ended before the next one begins)");
|
||||
if (eye_debug || release_wait) LOG("per-game: eye_debug=%d release_wait=%d log_file=%s", eye_debug, release_wait,
|
||||
log_file ? path : "none");
|
||||
if (controller_models && *process && !strchr(process, '/')) render_model_init(process);
|
||||
@@ -474,6 +495,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
if (equirect_emul && swapchain && emul_virtual_create(session, &fixed, swapchain)) {
|
||||
remember_swapchain(*swapchain);
|
||||
emul_on_create_swapchain(*swapchain, &fixed);
|
||||
snap_on_create(*swapchain, &fixed);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult result = fn(session, &fixed, swapchain);
|
||||
@@ -485,6 +507,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
note_swapchain_size(*swapchain, fixed.width, fixed.height);
|
||||
flip_on_create_swapchain(*swapchain, &fixed);
|
||||
emul_on_create_swapchain(*swapchain, &fixed);
|
||||
snap_on_create(*swapchain, &fixed);
|
||||
return result;
|
||||
}
|
||||
if (!swapchain_fix) return result;
|
||||
@@ -508,6 +531,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
note_swapchain_size(*swapchain, fixed.width, fixed.height);
|
||||
flip_on_create_swapchain(*swapchain, &fixed);
|
||||
emul_on_create_swapchain(*swapchain, &fixed);
|
||||
snap_on_create(*swapchain, &fixed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -515,10 +539,15 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
|
||||
PFN_xrDestroySwapchain fn = (PFN_xrDestroySwapchain)lookup(active_instance, "xrDestroySwapchain");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (layer_debug) {
|
||||
static int logged;
|
||||
if (logged++ < 200) LOG("layer_debug: xrDestroySwapchain %p", (void *)swapchain);
|
||||
}
|
||||
forget_swapchain(swapchain);
|
||||
surf_on_destroy(swapchain);
|
||||
flip_on_destroy(swapchain);
|
||||
emul_on_destroy_swapchain(swapchain);
|
||||
snap_on_destroy(swapchain);
|
||||
if (equirect_emul && emul_is_virtual(swapchain)) { emul_virtual_destroy(swapchain); return XR_SUCCESS; }
|
||||
return fn(swapchain);
|
||||
}
|
||||
@@ -770,6 +799,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
|
||||
|
||||
#include "session_fixes.c"
|
||||
#include "layer_emul_gl.c"
|
||||
#include "snapshot_gl.c"
|
||||
#include "surface_swapchain.c"
|
||||
#include "eye_debug.c"
|
||||
|
||||
@@ -796,10 +826,28 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
|
||||
if (XR_SUCCEEDED(result) && images && capacity && count) {
|
||||
flip_on_enumerate_images(swapchain, *count, images);
|
||||
emul_on_enumerate(swapchain, *count < capacity ? *count : capacity, images);
|
||||
snap_on_enumerate(swapchain, *count < capacity ? *count : capacity, images);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// counted for layer_debug (an app that skips the wait or release keeps runtime resources alive)
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
|
||||
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrResult result = fn(swapchain, info);
|
||||
__atomic_add_fetch(&sc_waits, 1, __ATOMIC_RELAXED);
|
||||
if (result != XR_SUCCESS) __atomic_add_fetch(&sc_wait_fails, 1, __ATOMIC_RELAXED);
|
||||
return result;
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
|
||||
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
__atomic_add_fetch(&sc_releases, 1, __ATOMIC_RELAXED);
|
||||
return fn(swapchain, info);
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageAcquireInfo *info,
|
||||
uint32_t *index) {
|
||||
PFN_xrAcquireSwapchainImage fn = (PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
|
||||
@@ -811,7 +859,12 @@ XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, co
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult result = fn(swapchain, info, index);
|
||||
if (XR_SUCCEEDED(result) && index) { flip_on_acquire(swapchain, *index); emul_on_acquire(swapchain, *index); }
|
||||
__atomic_add_fetch(&sc_acquires, 1, __ATOMIC_RELAXED);
|
||||
if (XR_SUCCEEDED(result) && index) {
|
||||
flip_on_acquire(swapchain, *index);
|
||||
emul_on_acquire(swapchain, *index);
|
||||
snap_on_acquire(swapchain, *index);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1019,9 +1072,40 @@ XRAPI_ATTR XrResult XRAPI_CALL xrLocateViews(XrSession session, const XrViewLoca
|
||||
return result;
|
||||
}
|
||||
|
||||
// xrBeginFrame: counted for layer_debug. A game that begins a new frame while the previous one is still open
|
||||
// (never ended) makes the runtime discard it; the Frame's runtime then never recycles that frame's GPU timing
|
||||
// command buffer (Vader Immortal: ~430 GPU mappings / 20 MB a second). frame_balance ends the open frame first,
|
||||
// with no layers, as the runtime expects.
|
||||
static XrTime last_begin_display_time;
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrBeginFrame(XrSession session, const XrFrameBeginInfo *info) {
|
||||
PFN_xrBeginFrame fn = (PFN_xrBeginFrame)lookup(active_instance, "xrBeginFrame");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (frame_open && frame_balance) {
|
||||
PFN_xrEndFrame end = (PFN_xrEndFrame)lookup(active_instance, "xrEndFrame");
|
||||
XrFrameEndInfo empty = {XR_TYPE_FRAME_END_INFO, NULL, last_begin_display_time ? last_begin_display_time
|
||||
: last_predicted_time,
|
||||
XR_ENVIRONMENT_BLEND_MODE_OPAQUE, 0, NULL};
|
||||
XrResult r = end ? end(session, &empty) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
static int logged;
|
||||
if (logged++ < 3) LOG("frame_balance: ended an open frame before xrBeginFrame (result %d)", r);
|
||||
__atomic_add_fetch(&frame_balanced, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
XrResult result = fn(session, info);
|
||||
__atomic_add_fetch(&frame_begins, 1, __ATOMIC_RELAXED);
|
||||
if (result == XR_FRAME_DISCARDED) __atomic_add_fetch(&frame_discarded, 1, __ATOMIC_RELAXED);
|
||||
if (XR_SUCCEEDED(result)) {
|
||||
frame_open = 1;
|
||||
last_begin_display_time = last_predicted_time;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInfo *info) {
|
||||
PFN_xrEndFrame fn = (PFN_xrEndFrame)lookup(active_instance, "xrEndFrame");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
frame_open = 0;
|
||||
__atomic_add_fetch(&frame_ends, 1, __ATOMIC_RELAXED);
|
||||
snap_end_frame(info);
|
||||
if (eye_debug) eye_debug_end_frame(session, info);
|
||||
{ // frame pacing statistics every ~5 s: fps and submitted-vs-predicted display time
|
||||
static struct timespec start;
|
||||
@@ -1038,6 +1122,18 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
if (elapsed >= 5.0) {
|
||||
LOG("pacing: %.1f fps, displayTime vs predicted: avg %.2f ms, max %.2f ms", frames / elapsed,
|
||||
drift_sum / (double)frames / 1e6, drift_max / 1e6);
|
||||
if (layer_debug)
|
||||
LOG("layer_debug: swapchain images: %d acquired, %d waited (%d not ok), %d released",
|
||||
__atomic_exchange_n(&sc_acquires, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&sc_waits, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&sc_wait_fails, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&sc_releases, 0, __ATOMIC_RELAXED));
|
||||
if (layer_debug)
|
||||
LOG("layer_debug: frames: %d xrBeginFrame (%d discarded), %d xrEndFrame, %d balanced",
|
||||
__atomic_exchange_n(&frame_begins, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&frame_discarded, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&frame_ends, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&frame_balanced, 0, __ATOMIC_RELAXED));
|
||||
if (layer_debug)
|
||||
LOG("layer_debug: input: %d xrSyncActions (%d ok, last %d), %d bool reads, %d pressed",
|
||||
__atomic_exchange_n(&input_syncs, 0, __ATOMIC_RELAXED),
|
||||
@@ -1171,6 +1267,48 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
if (!logged++) LOG("strip_depth: removed depth info from projection layer");
|
||||
++swapped;
|
||||
}
|
||||
{
|
||||
// XR_KHR_composition_layer_color_scale_bias (1000034000) on the layer itself: logged when its values
|
||||
// change (layer_debug); with strip_color_bias the runtime never gets it (Vader Immortal: the Frame's
|
||||
// runtime allocates GPU memory for it every frame and never frees it)
|
||||
const XrBaseInStructure *head = (const XrBaseInStructure *)layer->next;
|
||||
const XrCompositionLayerColorScaleBiasKHR *cb = NULL;
|
||||
for (const XrBaseInStructure *n = head; n; n = n->next)
|
||||
if (n->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) cb = (const XrCompositionLayerColorScaleBiasKHR *)n;
|
||||
if (cb && layer_debug) {
|
||||
static float last[8] = {-1};
|
||||
float now[8] = {cb->colorScale.r, cb->colorScale.g, cb->colorScale.b, cb->colorScale.a,
|
||||
cb->colorBias.r, cb->colorBias.g, cb->colorBias.b, cb->colorBias.a};
|
||||
static int logged;
|
||||
if (memcmp(now, last, sizeof(now)) && logged++ < 100) {
|
||||
LOG("layer_debug: layer type=%d color scale %.2f %.2f %.2f %.2f bias %.2f %.2f %.2f %.2f", layer->type,
|
||||
now[0], now[1], now[2], now[3], now[4], now[5], now[6], now[7]);
|
||||
memcpy(last, now, sizeof(now));
|
||||
}
|
||||
}
|
||||
if (cb && strip_color_bias && (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION ||
|
||||
layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD)) {
|
||||
const XrBaseInStructure *rest = head; // skip leading color structs; one further down drops the chain
|
||||
while (rest && rest->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) rest = rest->next;
|
||||
for (const XrBaseInStructure *n = rest; n; n = n->next)
|
||||
if (n->type == XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR) rest = NULL;
|
||||
if (strip_color_bias > 1) rest = NULL; // 2: the layer's whole extension chain (diagnostics)
|
||||
if (layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
|
||||
if (layer != (const XrCompositionLayerBaseHeader *)&projections[count])
|
||||
projections[count] = *(const XrCompositionLayerProjection *)layer;
|
||||
projections[count].next = rest;
|
||||
layer = (const XrCompositionLayerBaseHeader *)&projections[count];
|
||||
} else {
|
||||
if (layer != (const XrCompositionLayerBaseHeader *)&quads[count])
|
||||
quads[count] = *(const XrCompositionLayerQuad *)layer;
|
||||
quads[count].next = rest;
|
||||
layer = (const XrCompositionLayerBaseHeader *)&quads[count];
|
||||
}
|
||||
static int logged;
|
||||
if (!logged++) LOG("strip_color_bias: removed the color scale/bias from a layer");
|
||||
++swapped;
|
||||
}
|
||||
}
|
||||
if (hide_space_warp && layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
|
||||
((const XrCompositionLayerProjection *)layer)->viewCount == 2) {
|
||||
// The game may still attach XrCompositionLayerSpaceWarpInfoFB (1000171000) to its views: OVRPort's
|
||||
@@ -1464,6 +1602,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
HOOK(xrCreateSwapchain)
|
||||
HOOK(xrDestroySwapchain)
|
||||
HOOK(xrEndFrame)
|
||||
HOOK(xrBeginFrame)
|
||||
HOOK(xrLocateHandJointsEXT)
|
||||
HOOK(xrGetCurrentInteractionProfile)
|
||||
HOOK(xrEnumerateInstanceExtensionProperties)
|
||||
@@ -1480,6 +1619,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
}
|
||||
if (surface_emul) HOOK_AS(xrCreateSwapchainAndroidSurfaceKHR, hook_xrCreateSwapchainAndroidSurfaceKHR)
|
||||
if (eye_debug || release_wait) HOOK_AS(xrReleaseSwapchainImage, eye_hook_xrReleaseSwapchainImage)
|
||||
if (layer_debug) { // counters only; every other wait/release hook above takes precedence
|
||||
HOOK(xrWaitSwapchainImage)
|
||||
HOOK(xrReleaseSwapchainImage)
|
||||
}
|
||||
if (sync_guard || layer_debug) HOOK_AS(xrSyncActions, hook_xrSyncActions)
|
||||
if (layer_debug) HOOK_AS(xrGetActionStateBoolean, hook_xrGetActionStateBoolean)
|
||||
if (layer_debug || aim_correction_on() || profile_remap)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// FramePort FrameBridge: eye-image snapshots for headless diagnostics (setting snapshot=<seconds>, GLES games only).
|
||||
// Every <seconds>, at xrEndFrame, the left-eye image the game just submitted (projection layer, view 0: its swapchain
|
||||
// image and array layer) is read back on the game's own GL context and written at a quarter of its size as
|
||||
// /sdcard/Android/data/<package>/files/fb_snap_<n>.ppm (n = 0-7, the oldest is overwritten). Nobody wears the headset
|
||||
// in a launch test, so the headset view stays black: this shows what the game itself draws (e.g. which screen it is
|
||||
// stuck on). The game's framebuffer, pack-buffer and pack-alignment bindings are restored afterwards.
|
||||
#define SNAP_MAX_SWAPCHAINS 16
|
||||
#define SNAP_MAX_IMAGES 8
|
||||
#define SNAP_SCALE 4
|
||||
|
||||
|
||||
#define SNAP_GL_FUNCS(X) X(glGenFramebuffers) X(glDeleteFramebuffers) X(glBindFramebuffer) X(glFramebufferTexture2D) \
|
||||
X(glFramebufferTextureLayer) X(glCheckFramebufferStatus) X(glReadPixels) X(glGetIntegerv) X(glPixelStorei) \
|
||||
X(glBindBuffer) X(glGetError)
|
||||
#define SNAP_DECLARE(fn) static __typeof__(&fn) s_##fn;
|
||||
SNAP_GL_FUNCS(SNAP_DECLARE)
|
||||
#undef SNAP_DECLARE
|
||||
static EGLContext (*s_eglGetCurrentContext)(void);
|
||||
|
||||
static struct {
|
||||
XrSwapchain handle;
|
||||
uint32_t width, height, array_size, count, last_acquired;
|
||||
GLuint images[SNAP_MAX_IMAGES];
|
||||
} snap_chains[SNAP_MAX_SWAPCHAINS];
|
||||
static pthread_mutex_t snap_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int snap_load_gl(void) {
|
||||
static int state; // 0 untried, 1 ok, -1 failed
|
||||
if (state) return state > 0;
|
||||
void *egl = dlopen("libEGL.so", RTLD_NOW | RTLD_LOCAL), *gles = dlopen("libGLESv3.so", RTLD_NOW | RTLD_LOCAL);
|
||||
int ok = egl && gles;
|
||||
if (ok) s_eglGetCurrentContext = (EGLContext (*)(void))dlsym(egl, "eglGetCurrentContext");
|
||||
#define SNAP_RESOLVE(fn) ok &= gles && (s_##fn = (__typeof__(s_##fn))dlsym(gles, #fn)) != NULL;
|
||||
SNAP_GL_FUNCS(SNAP_RESOLVE)
|
||||
#undef SNAP_RESOLVE
|
||||
state = ok && s_eglGetCurrentContext ? 1 : -1;
|
||||
if (state < 0) LOG("snapshot: OpenGL ES not available, no snapshots");
|
||||
return state > 0;
|
||||
}
|
||||
|
||||
static int snap_find(XrSwapchain handle, int create) {
|
||||
for (int i = 0; i < SNAP_MAX_SWAPCHAINS; ++i)
|
||||
if (snap_chains[i].handle == handle) return i;
|
||||
if (!create) return -1;
|
||||
for (int i = 0; i < SNAP_MAX_SWAPCHAINS; ++i)
|
||||
if (!snap_chains[i].handle) { snap_chains[i].handle = handle; snap_chains[i].count = 0; return i; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void snap_on_create(XrSwapchain handle, const XrSwapchainCreateInfo *info) {
|
||||
if (!snapshot || !info) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 1);
|
||||
if (i >= 0) {
|
||||
snap_chains[i].width = info->width;
|
||||
snap_chains[i].height = info->height;
|
||||
snap_chains[i].array_size = info->arraySize ? info->arraySize : 1;
|
||||
}
|
||||
pthread_mutex_unlock(&snap_lock);
|
||||
}
|
||||
|
||||
static void snap_on_destroy(XrSwapchain handle) {
|
||||
if (!snapshot) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 0);
|
||||
if (i >= 0) snap_chains[i].handle = XR_NULL_HANDLE;
|
||||
pthread_mutex_unlock(&snap_lock);
|
||||
}
|
||||
|
||||
static void snap_on_enumerate(XrSwapchain handle, uint32_t count, const XrSwapchainImageBaseHeader *images) {
|
||||
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) {
|
||||
uint32_t n = count < SNAP_MAX_IMAGES ? count : SNAP_MAX_IMAGES;
|
||||
const emul_gles_image *gl = (const emul_gles_image *)images;
|
||||
for (uint32_t k = 0; k < n; ++k) snap_chains[i].images[k] = gl[k].image;
|
||||
snap_chains[i].count = n;
|
||||
}
|
||||
pthread_mutex_unlock(&snap_lock);
|
||||
}
|
||||
|
||||
static void snap_on_acquire(XrSwapchain handle, uint32_t index) {
|
||||
if (!snapshot) return;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(handle, 0);
|
||||
if (i >= 0) snap_chains[i].last_acquired = index;
|
||||
pthread_mutex_unlock(&snap_lock);
|
||||
}
|
||||
|
||||
static void snap_write(const unsigned char *rgba, uint32_t w, uint32_t h, int n) {
|
||||
char path[320];
|
||||
snprintf(path, sizeof(path), "%s/fb_snap_%d.ppm", snap_dir, n);
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { LOG("snapshot: can't write %s", path); return; }
|
||||
uint32_t ow = w / SNAP_SCALE, oh = h / SNAP_SCALE;
|
||||
fprintf(f, "P6\n%u %u\n255\n", ow, oh);
|
||||
unsigned char *row = malloc((size_t)ow * 3);
|
||||
for (uint32_t y = 0; row && y < oh; ++y) {
|
||||
const unsigned char *src = rgba + (size_t)(h - 1 - y * SNAP_SCALE) * w * 4; // GL rows are bottom-up
|
||||
for (uint32_t x = 0; x < ow; ++x) {
|
||||
const unsigned char *p = src + (size_t)x * SNAP_SCALE * 4;
|
||||
row[x * 3] = p[0], row[x * 3 + 1] = p[1], row[x * 3 + 2] = p[2];
|
||||
}
|
||||
fwrite(row, 1, (size_t)ow * 3, f);
|
||||
}
|
||||
free(row);
|
||||
fclose(f);
|
||||
LOG("snapshot: %s (%ux%u)", path, ow, oh);
|
||||
}
|
||||
|
||||
static void snap_end_frame(const XrFrameEndInfo *info) {
|
||||
if (!snapshot || !*snap_dir || !info) return;
|
||||
static int64_t next_ns;
|
||||
static int taken;
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
int64_t t = (int64_t)now.tv_sec * 1000000000ll + now.tv_nsec;
|
||||
if (t < next_ns) return;
|
||||
next_ns = t + (int64_t)snapshot * 1000000000ll;
|
||||
const XrCompositionLayerProjection *proj = NULL;
|
||||
for (uint32_t i = 0; i < info->layerCount && !proj; ++i)
|
||||
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
|
||||
((const XrCompositionLayerProjection *)info->layers[i])->viewCount)
|
||||
proj = (const XrCompositionLayerProjection *)info->layers[i];
|
||||
if (!proj || !snap_load_gl()) return;
|
||||
if (!s_eglGetCurrentContext()) {
|
||||
static int logged;
|
||||
if (!logged++) LOG("snapshot: no GL context current in xrEndFrame, no snapshot");
|
||||
return;
|
||||
}
|
||||
const XrSwapchainSubImage *sub = &proj->views[0].subImage;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(sub->swapchain, 0);
|
||||
GLuint tex = 0;
|
||||
uint32_t w = 0, h = 0, array_size = 1;
|
||||
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);
|
||||
if (!tex || !w || !h) return;
|
||||
GLint read_fb = 0, pack_buffer = 0, pack_alignment = 4;
|
||||
s_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read_fb);
|
||||
s_glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &pack_buffer);
|
||||
s_glGetIntegerv(GL_PACK_ALIGNMENT, &pack_alignment);
|
||||
GLuint fb = 0;
|
||||
s_glGenFramebuffers(1, &fb);
|
||||
s_glBindFramebuffer(GL_READ_FRAMEBUFFER, fb);
|
||||
if (array_size > 1)
|
||||
s_glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, (GLint)sub->imageArrayIndex);
|
||||
else
|
||||
s_glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||
unsigned char *rgba = NULL;
|
||||
if (s_glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE &&
|
||||
(rgba = malloc((size_t)w * h * 4))) {
|
||||
s_glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
s_glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
s_glReadPixels(0, 0, (GLsizei)w, (GLsizei)h, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
|
||||
} else {
|
||||
static int logged;
|
||||
if (logged++ < 3) LOG("snapshot: eye image %u not readable (framebuffer incomplete)", tex);
|
||||
}
|
||||
s_glPixelStorei(GL_PACK_ALIGNMENT, pack_alignment);
|
||||
s_glBindBuffer(GL_PIXEL_PACK_BUFFER, (GLuint)pack_buffer);
|
||||
s_glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)read_fb);
|
||||
s_glDeleteFramebuffers(1, &fb);
|
||||
while (s_glGetError() != GL_NO_ERROR) {} // our calls must not leave errors for the game to find
|
||||
if (rgba) {
|
||||
snap_write(rgba, w, h, taken++ % 8);
|
||||
free(rgba);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,12 @@ company = "FramePort"
|
||||
path = "src"
|
||||
module = "main"
|
||||
|
||||
# jni_flutter (1.0.4 and 1.0.4+1, 2026-10) generates bindings that require jni ^1.1.0, while Flet's build template pins
|
||||
# jni 1.0.0: every bundle failed ("generated bindings expect package:jni ^1.1.0"). jni 1.1 also satisfies bindings
|
||||
# made for 1.0 (the check is "same major, at least this minor"). Remove once Flet's template moves to jni 1.1.
|
||||
[tool.flet.flutter.pubspec.dependency_overrides]
|
||||
jni = "1.1.0"
|
||||
|
||||
[tool.flet.macos]
|
||||
entitlement."com.apple.security.files.user-selected.read-write" = true
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
|
||||
__version__ = "0.11.0"
|
||||
__version__ = "0.11.1"
|
||||
@@ -306,6 +306,7 @@
|
||||
"Download…": "",
|
||||
"Draws each eye separately; try it when one eye shows a grey or broken picture.": "",
|
||||
"Drop invalid layers": "",
|
||||
"Drop layer color fades": "",
|
||||
"Drop layers whose swapchain failed or whose extension isn't enabled.": "",
|
||||
"Drop to upload": "",
|
||||
"Drops Quest-only shared library requirements.": "",
|
||||
@@ -317,6 +318,7 @@
|
||||
"Environment variables for the Proton launcher on the Frame (e.g. DXVK_HUD=fps), one KEY=value per line.": "",
|
||||
"Every 'local' play space the game creates lines up with the one at the start. For games whose menus or screens jump to where you look on the Frame.": "",
|
||||
"Every game start on the Frame used to leak one kernel key, and after about 200 launches every game fails to start. FramePort switches that leak off, but keys already used only come back after a restart of the Frame.": "",
|
||||
"Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays black; this shows what the game itself draws. OpenGL ES games only.": "",
|
||||
"Every uploaded file is present with the right size.": "",
|
||||
"everything in ~ (not seen by games)": "",
|
||||
"Everything is up to date": "",
|
||||
@@ -328,6 +330,7 @@
|
||||
"Extra Lepton environment": "",
|
||||
"Extra settings for Proton (for testing).": "",
|
||||
"Extra settings for the Android container (for testing).": "",
|
||||
"Eye-image snapshots (diagnostics)": "",
|
||||
"failed": "",
|
||||
"Failed": "",
|
||||
"Failed: {error}": "",
|
||||
@@ -808,6 +811,7 @@
|
||||
"Removes Meta's old VR library when the game doesn't need it.": "",
|
||||
"Removes OVRPort's controller pose offset if controllers look misplaced.": "",
|
||||
"Removes requirements for Quest-only system parts.": "",
|
||||
"Removes the color scale/bias (XR_KHR_composition_layer_color_scale_bias) from the game's layers, so the runtime doesn't apply it. The Frame's runtime allocates GPU memory for it every frame without freeing it, e.g. Vader Immortal leaked ~20 MB/s on its loading screen. Fades done this way no longer show.": "",
|
||||
"Removes the game from the Frame. Saves are kept, so a reinstall picks up where you left off.": "",
|
||||
"Removes the previous version kept after each reinstall and any leftover uploads. Games and saves aren't touched.": "",
|
||||
"Removes {title} from this PC's Steam library (Steam restarts once). The game folder isn't touched.": "",
|
||||
|
||||
@@ -27,6 +27,46 @@ INPUT_CALLS = ("ovrp_GetConnectedControllers", "ovrp_GetControllerState4", "ovrp
|
||||
UNITY_INPUT_CALLS = ("ovrp_GetControllerState", "ovrp_GetControllerState2")
|
||||
|
||||
|
||||
ORR_W2_6 = 0x321F07E2 # orr w2, wzr, #6 (strncmp length 6)
|
||||
MOV_W2_0 = 0x52800002 # mov w2, #0 (strncmp length 0: always equal)
|
||||
|
||||
|
||||
def oculus_model_checks(data: bytes) -> tuple[bytes, int]:
|
||||
"""Unity's built-in Oculus input only reports controllers on a device whose model starts with "Oculus"
|
||||
(strncmp(SystemInfo.deviceModel, "Oculus", 6), next to its Oculus Go check "Oculus Pacific"). Lepton's Android is
|
||||
"Valve Lepton", so BattleSisters got no controller buttons. Every such compare (the literal loaded with adrp/add
|
||||
right before `orr w2, wzr, #6`) gets length 0, so any device but the Go counts as an Oculus one. The "Oculus"
|
||||
string itself stays: Unity also uses it as its VR device name."""
|
||||
import struct
|
||||
|
||||
literal = data.find(b"\0Oculus\0") + 1
|
||||
if literal <= 0:
|
||||
return data, 0
|
||||
buf, count = bytearray(data), 0
|
||||
end = len(data) - len(data) % 4
|
||||
for at in range(0, end, 4):
|
||||
if struct.unpack_from("<I", data, at)[0] != ORR_W2_6:
|
||||
continue
|
||||
# look back for `adrp xA, page` + `add x1, xA, #off` that computes the literal's address
|
||||
for back in range(4, 16, 4):
|
||||
add = struct.unpack_from("<I", data, at - back)[0] if at >= back else 0
|
||||
if add & 0xFFC00000 != 0x91000000 or add & 0x1F != 1: # add x1, xN, #imm (no shift)
|
||||
continue
|
||||
rn, imm12 = (add >> 5) & 0x1F, (add >> 10) & 0xFFF
|
||||
adrp_at = at - back - 4
|
||||
adrp = struct.unpack_from("<I", data, adrp_at)[0] if adrp_at >= 0 else 0
|
||||
if adrp & 0x9F000000 != 0x90000000 or adrp & 0x1F != rn:
|
||||
continue
|
||||
imm = ((adrp >> 5) & 0x7FFFF) << 2 | (adrp >> 29) & 3
|
||||
if imm & (1 << 20):
|
||||
imm -= 1 << 21
|
||||
if (adrp_at & ~0xFFF) + (imm << 12) + imm12 == literal:
|
||||
struct.pack_into("<I", buf, at, MOV_W2_0)
|
||||
count += 1
|
||||
break
|
||||
return bytes(buf), count
|
||||
|
||||
|
||||
class UnityOculusCheck(Patch):
|
||||
id = "frame.unity_oculus_check"
|
||||
title = "Unity: start VR without Meta's system apps"
|
||||
@@ -40,8 +80,9 @@ class UnityOculusCheck(Patch):
|
||||
"mouse click (Input.GetMouseButtonDown), which Go-era screens wait for (e.g. Accounting+'s motion "
|
||||
"warning) and which Lepton never delivers.")
|
||||
order = 45
|
||||
# 2: frame wait also for Unity 2019 without the Oculus XR Plugin; 3: controller presses as mouse clicks (ovrpshim)
|
||||
revision = 3
|
||||
# 2: frame wait also for Unity 2019 without the Oculus XR Plugin; 3: controller presses as mouse clicks
|
||||
# (ovrpshim); 4: Unity 2019's Oculus device-model checks (Touch controllers on Lepton)
|
||||
revision = 4
|
||||
|
||||
@staticmethod
|
||||
def _major(a) -> int:
|
||||
@@ -76,7 +117,10 @@ class UnityOculusCheck(Patch):
|
||||
return False
|
||||
data, loops = elf.replace_rodata_string(data, UPDATE, SHIM_UPDATE) if self.legacy_loop(ctx.analysis) \
|
||||
else (data, 0)
|
||||
if loops:
|
||||
if loops and self._major(ctx.analysis) >= 2019: # Accounting+ (2017) reads OVRInput: works without
|
||||
data, models = oculus_model_checks(data)
|
||||
if models:
|
||||
ctx.notes.append(f"libunity.so: {models} Oculus device-model checks accept Lepton's model")
|
||||
data, nodes = elf.replace_rodata_string(data, NODE, SHIM_NODE)
|
||||
if nodes:
|
||||
ctx.notes.append(f"libunity.so: {NODE} -> {SHIM_NODE}")
|
||||
|
||||
@@ -16,6 +16,14 @@ SETTINGS = [
|
||||
("scale", "float", 1.0, "Resolution scale",
|
||||
"Multiplies the recommended eye-buffer width and height (0.5–2.0). 1.5 ≈ 2.25× pixels."),
|
||||
("foveation_fix", "int", 1, "Hide Quest foveation", "Hides Quest foveation extensions the Frame runtime lacks."),
|
||||
("snapshot", "int", 0, "Eye-image snapshots (diagnostics)",
|
||||
"Every N seconds, saves the left-eye image the game submits (a quarter of its size) as fb_snap_0-7.ppm in the "
|
||||
"game's files folder on the Frame. Launch tests run without anyone wearing the headset, so the headset view stays "
|
||||
"black; this shows what the game itself draws. OpenGL ES games only."),
|
||||
("strip_color_bias", "int", 0, "Drop layer color fades",
|
||||
"Removes the color scale/bias (XR_KHR_composition_layer_color_scale_bias) from the game's layers, so the runtime "
|
||||
"doesn't apply it. The Frame's runtime allocates GPU memory for it every frame without freeing it, e.g. Vader "
|
||||
"Immortal leaked ~20 MB/s on its loading screen. Fades done this way no longer show."),
|
||||
("hide_space_warp", "int", 0, "Turn off space warp",
|
||||
"Hides XR_FB_space_warp, so the game renders every frame itself instead of half of them plus motion vectors "
|
||||
"(Application SpaceWarp). For games whose picture flickers or smears on the Frame (e.g. Unreal Engine 5 games "
|
||||
@@ -255,7 +263,7 @@ UI: dict[str, dict] = {
|
||||
control=("choice", [(0, "As the game sends it"), (2, "Flat")])),
|
||||
**{key: dict(group="troubleshooting", level="advanced", control=("switch",)) for key in (
|
||||
"foveation_fix", "hide_space_warp", "swapchain_fix", "layer_fix", "gl_hide_multiview", "mutable_fix",
|
||||
"flip_quads", "swap_eyes", "vk_validation", "rect_clamp", "gl_hide_msrtt",
|
||||
"flip_quads", "swap_eyes", "vk_validation", "rect_clamp", "gl_hide_msrtt", "strip_color_bias", "snapshot",
|
||||
"strip_depth", "respace_kick", "layer_debug", "eye_debug", "release_wait")},
|
||||
}
|
||||
|
||||
@@ -282,6 +290,10 @@ class AdapterSetting(Patch):
|
||||
if self.key == "controller_models" and uses_render_models(a):
|
||||
return Suggestion(True, "The game asks the headset for its controller models: show Steam Frame controllers "
|
||||
"instead of Quest Touch controllers.", {"value": 1})
|
||||
if self.key == "haptic_fix" and "libOVRPlugin.so" in a.libs:
|
||||
return Suggestion(True, "Meta's OVRPlugin vibrates controllers through OVRPort's loader, which turns some "
|
||||
"vibrations into gigabyte allocations that freeze the Frame (e.g. Lucky's Tale, "
|
||||
"BattleSisters): vibrations are converted before they reach it.", {"value": 1})
|
||||
if self.key == "equirect_emul" and uses_equirect_layers(a):
|
||||
return Suggestion(True, "The game draws 360° layers (e.g. a video player's theatre or 360° videos), which "
|
||||
"the Frame's runtime can't show: show them as panels around you.", {"value": 1})
|
||||
|
||||
@@ -1363,3 +1363,53 @@ def test_launch_registers_a_lost_devkit_entry_again(monkeypatch, tmp_path):
|
||||
out = a.cmd_launch({"package": "com.camouflaj.manta"})
|
||||
assert registered == ["com.camouflaj.manta"] and launches == [222, 333]
|
||||
assert out["via"] == "devkit" and out["steam"]["result"] == "started"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32" or not __import__("shutil").which("flock"), reason="bash + flock")
|
||||
def test_second_launch_while_starting_is_ignored(monkeypatch, tmp_path):
|
||||
"""Play pressed again while Lepton still boots made the second Lepton stop the first one's container (both died:
|
||||
Vader Immortal, BattleSisters). The launcher holds a lock for the game's lifetime; a second launch leaves it."""
|
||||
import time
|
||||
|
||||
a = load_agent(monkeypatch, tmp_path)
|
||||
base = tmp_path / "game"
|
||||
(base / "lepton-app").mkdir(parents=True)
|
||||
lepton = tmp_path / "lepton"
|
||||
lepton.write_text(f"#!/bin/bash\necho started >>{tmp_path}/starts\nsleep ${{FAKE_RUN:-30}}\n")
|
||||
lepton.chmod(0o755)
|
||||
text = a.LAUNCH_SH.format(title="T", pkg="com.x.y", base_q=str(base), appid=1, lepton_q=str(lepton), extra_env="",
|
||||
watchdog=a.WATCHDOG, dashboard="true", single=a.SINGLE_LINE, plays_start="true",
|
||||
plays_end="true")
|
||||
launcher = tmp_path / "launch.sh"
|
||||
launcher.write_text(text)
|
||||
launcher.chmod(0o755)
|
||||
first = subprocess.Popen([str(launcher)], start_new_session=True)
|
||||
try:
|
||||
for _ in range(100):
|
||||
if (tmp_path / "starts").exists():
|
||||
break
|
||||
time.sleep(0.05)
|
||||
second = subprocess.run([str(launcher)], timeout=10)
|
||||
assert second.returncode == 0 and first.poll() is None # ignored, the first keeps running
|
||||
assert (tmp_path / "starts").read_text().count("started") == 1
|
||||
assert "second launch ignored" in (base / "launch-dup.log").read_text()
|
||||
finally:
|
||||
os.killpg(first.pid, 15)
|
||||
first.wait(timeout=10)
|
||||
third = subprocess.run([str(launcher)], timeout=20, env={**os.environ, "FAKE_RUN": "0"})
|
||||
assert third.returncode == 0 and (tmp_path / "starts").read_text().count("started") == 2 # free again
|
||||
|
||||
|
||||
def test_upgrade_launchers_adds_the_single_launch_lock(monkeypatch, tmp_path):
|
||||
a = load_agent(monkeypatch, tmp_path)
|
||||
anchor = tmp_path / "Applications/quest-frame/com.x.y"
|
||||
anchor.mkdir(parents=True)
|
||||
old = ('#!/bin/bash\napp_dir=/x\n[[ -d "$app_dir/lepton-app" ]] || { exit 1; }\nfix_perms\nparent=$PPID\n'
|
||||
'( while sleep 2; do fix_perms; if true; then kill -TERM $$; fi; done ) & permfix=$!\nexport SteamAppId=1\n')
|
||||
(anchor / "launch.sh").write_text(old)
|
||||
a.upgrade_launchers()
|
||||
text = (anchor / "launch.sh").read_text()
|
||||
assert text.index(".launch.lock") < text.index("fix_perms") and "done ) 9>&- & permfix=$!" in text
|
||||
assert text.count(".launch.lock") == 1
|
||||
a.upgrade_launchers()
|
||||
assert (anchor / "launch.sh").read_text().count(".launch.lock") == 1
|
||||
@@ -510,3 +510,11 @@ def test_start_activity_after_overport_conversion(tmp_path):
|
||||
ctx = base.ApkContext(ws, a, {}, Reporter(), {"frame.start_activity": {}})
|
||||
assert patch.apply(ctx)
|
||||
assert patch.validate(ctx) == [("Only the VR activity is a launcher", True, "org.x.game.QuestActivity")]
|
||||
|
||||
|
||||
def test_haptic_fix_suggested_for_ovrplugin_games():
|
||||
base.load_all()
|
||||
p = base.REGISTRY["adapter.haptic_fix"]
|
||||
s = p.detect(_analysis(libs=["libunity.so", "libOVRPlugin.so"]))
|
||||
assert s and s.recommended and s.params == {"value": 1}
|
||||
assert p.detect(_analysis(libs=["libunity.so"])) is None
|
||||
@@ -273,3 +273,21 @@ def test_user_recipes_get_a_catalog_update_offer(tmp_path, monkeypatch):
|
||||
assert "catalog_update" not in g and g["recipe"]["patches"]["adapter.scale"] == {"value": 0.9}
|
||||
assert g["recipe"].get("source") != "user" and "adapter.vk_shader_fix" in g["recipe"]["patches"]
|
||||
|
||||
|
||||
|
||||
def test_oculus_model_checks_accept_any_device_but_the_go():
|
||||
import struct
|
||||
|
||||
from frameport.patches.frame import unity_oculus_check as C
|
||||
|
||||
# .text: adrp x1, page(0x1000); add x1, x1, #0x10; orr w2, wzr, #6; bl ... -> strncmp(model, "Oculus", 6)
|
||||
code = struct.pack("<4I", 0xB0000001, 0x91004021, C.ORR_W2_6, 0x94000000)
|
||||
other = struct.pack("<4I", 0xB0000001, 0x91008021, C.ORR_W2_6, 0x94000000) # a different string: untouched
|
||||
data = bytearray(0x2000)
|
||||
data[0:16], data[16:32] = code, other
|
||||
data[0x100F:0x1017] = b"\0Oculus\0" # the literal at 0x1010
|
||||
data[0x1020:0x1028] = b"OculusX\0"
|
||||
out, n = C.oculus_model_checks(bytes(data))
|
||||
assert n == 1
|
||||
assert struct.unpack_from("<I", out, 8)[0] == C.MOV_W2_0 and struct.unpack_from("<I", out, 24)[0] == C.ORR_W2_6
|
||||
assert out[0x1010:0x1016] == b"Oculus" # the string itself stays (Unity's VR device name)
|
||||
Reference in new issue
Block a user