mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-09 17:00:49 +02:00
Compare commits
17
Commits
ui-refresh
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa69ffefd2 | ||
|
|
f78bc82cc4 | ||
|
|
dfacf6a206 | ||
|
|
b2f14c1e2e | ||
|
|
03d7a311bf | ||
|
|
cd5119278b | ||
|
|
6ab3608a44 | ||
|
|
c0539af368 | ||
|
|
c6363140f4 | ||
|
|
187105b729 | ||
|
|
b983c0f11f | ||
|
|
c7cee33889 | ||
|
|
d9d73b8aec | ||
|
|
48455b9b72 | ||
|
|
af798c0bf3 | ||
|
|
ed714c9d65 | ||
|
|
832b401630 |
No files matched your search
@@ -641,6 +641,17 @@ head-pose deadline ends VR mode without a worn headset (not a game bug).
|
||||
|
||||
**Language packs (merged from PR #20, 2026-10-04):** overport's `libovrplatformloader.so` is a dispatcher that `dlopen`s Meta's own loader (`libovrplatformloader_meta.so` / `_meta_q1.so`, also `libpxrplatformloader.so`) and keeps its own message queue; `ovr_LanguagePack_GetCurrent/SetCurrent` are 8-byte `return 0` stubs in it, `ovr_AssetFile_GetList` forwards to Meta's loader. `frame.langpacks` (opt-in, `native/langpack`) serves `<tag>.lang` files from the game's data; `elf.hide_exports` marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's `is_symbol_global_and_defined` is from memory). `libfp_langpack.so` is built here (`python native/build.py --only langpack`) and committed. Owner-verified 2026-10-04: Deadpool VR with only `en.lang` and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get `lang_packs` filled after an app update (`library._refresh_data_fields`). The library logs to logcat under the tag `fp_langpack` (dirs looked in, packs found, every language-pack call), so it shows up in the game's `launch.log`; an `ovr_AssetFile_GetList` the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its `Metadata` equals the game's own version string (`ULanguagePacksSubsystem` compares it with `%s.%s.%s.%s.%s` built from the build info, e.g. `1.0.40.356975.Quest` = versionName; found by disassembling `libUE4.so`): the patch writes the APK's versionName into the library (`@FPMETA@` slot, `with_metadata`), `FRAMEPORT_LANGPACK_META` overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled `/sdcard/Android/obb/<pkg>/x.lang`; with the `/storage/emulated/0/Android/obb/<pkg>/x.lang` spelling (Unreal's own, now listed first in `scan_dirs`) German dialogue plays. `FRAMEPORT_LANGPACK_SKIP=<tags>` or a file `fp_langpack_skip` in the obb folder leaves packs out of the list (experiments).
|
||||
|
||||
**Cube swapchains (GitHub #107, 2026-10-09):** the Frame's runtime refuses `faceCount=6` swapchains (-2,
|
||||
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
|
||||
ovrp_EndFrame4 (memset). FrameBridge `cube_standin` (default on, `native/adapter/cube_standin.c`) serves a refused cube
|
||||
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
|
||||
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
|
||||
**Vivox API 31 (GitHub #101, 2026-10-09):** newer Vivox builds (Green Hell VR) call Android 12 AudioManager
|
||||
communication-device methods from `com.vivox.sdk.AudioChangeListener` with no SDK check → NoSuchMethodError on Lepton's
|
||||
Android 11. `frame.vivox_audio_route` (analysis `vivox_api31`, ANALYSIS_VERSION 4) makes every such method return at
|
||||
once (`Dex.return_early`: return-void / `const/4 v0,0; return v0`; nopping the invoke would leave a move-result the
|
||||
verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified
|
||||
headless: Vivox initialises, 150 s at ~65-70 fps.
|
||||
**Unresolved (as of 2026-09-28):** Arcsmith (right-eye distortion) and Time Stall (both eyes) — swap, tracking, Valve
|
||||
layers, depth, pacing ruled out. Sniper Elite VR (DEVICE LOST), Espire 1 (Mesa GL upload crash), HITMAN 3 (freedreno
|
||||
crash): use PC versions.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
|
||||
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
@@ -5,10 +6,10 @@ e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/li
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
fc559dbe0b87eecc3641256837293260cefb7c2ce7b21d808d10edd2a2dba9fc ./arm64-v8a/libglshim.so
|
||||
cc5b493cadefe76ad13fd096d3f8494a140ba593e0e1692db12d284bc548c1cf ./arm64-v8a/libopenxr_loader_generic.so
|
||||
763fc2c25c4f62aebc03aa90080483ad82fedb42c3e65415374ae9d22a07583b ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
aa4dc0020c77e12d41ef6ce80b23ad90c9cb7e882338eddf2261efe8645cf38d ./arm64-v8a/libvrapi.so
|
||||
c10412dd76a65a1d587b7ed226367d1a6f2bec238c733c97ee67eb8870c868e8 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
c7a97bad05b839665f73b353571bed62568700096db82e58ac8d2543482ff370 ./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.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
package: com.CyberneticWalrus.DoesitStackMeta
|
||||
title: Does it Stack?
|
||||
status: issues
|
||||
notes: Mixed reality mode does not work (works on Demeo), everything else seems to be OK
|
||||
tested_version: 1.02 (3683)
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.oculusos
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
refresh_rate: 90.0
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
known_good_sha256: e9fce6ac812f1bb317a7a6959be89c5ff54e87f56af9ad4c42dff5fecad4efe8
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261006.6173745'
|
||||
agent: 59
|
||||
issue: 112
|
||||
source_hint: Does It Stack
|
||||
@@ -0,0 +1,20 @@
|
||||
package: com.ForwardXP.nuke
|
||||
title: Please Don't Touch Anything
|
||||
status: works
|
||||
notes: Seems functional
|
||||
tested_version: '2.2'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
verified:
|
||||
date: '2026-10-08'
|
||||
known_good_sha256: 17460c8691b9ff82f6c7ca5597afd0aacf1a94aa30ebb6a6108802991b60f51b
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261006.6173745'
|
||||
agent: 59
|
||||
issue: 106
|
||||
source_hint: apks
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.FunktronicLabs.TheLightBrigade
|
||||
title: The Light Brigade
|
||||
status: issues
|
||||
notes: Judder in weapons
|
||||
tested_version: '746'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 114
|
||||
source_hint: The Light Brigade
|
||||
@@ -1,20 +1,38 @@
|
||||
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.
|
||||
status: unknown
|
||||
notes: To test in the headset - five fixes from a reporter's playthrough of chapter 1 (GitHub issue 49) - the loading
|
||||
card now ends, grip/trigger bindings, the lightspeed jump's GPU hang, hands lagging behind the controllers and thumbs
|
||||
that never moved. Needs FramePort 0.12.1 or newer.
|
||||
details: Unreal Engine 4 (GLES). Found and verified in gameplay on a Frame by Klownicle (GitHub issue 49); FramePort
|
||||
reimplements them as patches that match the game's code exactly and change nothing when it differs. The engine's
|
||||
Quest checks (IsRunningOnSantaCruz) answer "not a Quest" on the Frame - the menu waited for a Quest-only shader
|
||||
precompile that never starts (frame.unreal_quest_precompile makes the non-Quest branch report 100 %), and the game's
|
||||
key map picked the empty Gear VR controls (frame.unreal_quest_keymap keeps the Quest set). Two lightspeed shaders,
|
||||
as the Frame's GL driver (Zink) compiles them, read loop counters and accumulators before setting them and hang the
|
||||
GPU; a Vulkan layer under Zink inserts the stores that zero them (frame.zink_shader_fix, zink_shader_fix; the
|
||||
captured modules are from Lepton 2.8.14 and stop matching if a Frame update changes the driver's output - the layer
|
||||
logs that). OVRPlugin asks for poses at its own clock (pose_time_fix) and reports thumb proximity from an extension
|
||||
the Frame lacks (proximity_emul binds it to the capacitive touch inputs). The Frame's SteamVR runtime leaks about 20
|
||||
MB a second while the game runs (reported to Valve).
|
||||
tested_version: 1.1.1+667256.cl.387770
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.unreal_quest_precompile
|
||||
- frame.unreal_quest_keymap
|
||||
- frame.zink_shader_fix
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
proximity_emul: 1
|
||||
# the two lightspeed fragment shaders as Zink compiles them (Lepton 2.8.14): twelve OpStores each that zero three
|
||||
# loop counters and nine accumulators read before they are set (Klownicle's capture, GitHub issue 49)
|
||||
zink_shader_fix: 12016:b2919629761ad268e0b21c72ac8520f81b76e72c3adc26897f97eae9a3e70a66:3092:0x0003003e,166,82,0x0003003e,177,82,0x0003003e,183,82,0x0003003e,189,82,0x0003003e,256,82,0x0003003e,266,82,0x0003003e,272,82,0x0003003e,278,82,0x0003003e,318,82,0x0003003e,328,82,0x0003003e,334,82,0x0003003e,340,82;11436:6f18be49f2aaa2452f962eb16f7af744c5c22a7b25f37ce681e0d6399e35a869:2932:0x0003003e,164,80,0x0003003e,175,80,0x0003003e,181,80,0x0003003e,187,80,0x0003003e,254,80,0x0003003e,264,80,0x0003003e,270,80,0x0003003e,276,80,0x0003003e,316,80,0x0003003e,326,80,0x0003003e,332,80,0x0003003e,338,80
|
||||
verified:
|
||||
date: '2026-10-06'
|
||||
issue: 49
|
||||
updated: '2026-10-06'
|
||||
updated: '2026-10-09'
|
||||
min_app: 0.12.1
|
||||
source_hint: Vader Immortal- Episode I
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.MightyCoconut.WalkaboutMiniGolf
|
||||
title: Walkabout Mini Golf
|
||||
status: works
|
||||
tested_version: '6.7'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.oculusos
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
controller_models: 1
|
||||
haptic_fix: 1
|
||||
scale: 1.5
|
||||
verified:
|
||||
date: '2026-10-08'
|
||||
known_good_sha256: cf3b5cbe131af8a2baaa3f5c665b1c7001c8b299473abfcf98d90e3c02b08a2f
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 111
|
||||
source_hint: Walkabout Mini Golf
|
||||
@@ -1,12 +1,14 @@
|
||||
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).
|
||||
notes: Plays; controller buttons and vibration work, hands follow the controllers (pose_time_fix).
|
||||
details: Unity 2019.4 with Unity's built-in Oculus support. frame.unity_oculus_check starts VR (Meta's system-app
|
||||
check), adds the frame wait its legacy loop never makes (without it the GPU hung, black screen) and lets Unity's
|
||||
Oculus input accept Lepton's device model (it only reported controllers on a device named "Oculus ...", so buttons
|
||||
were dead). haptic_fix stops the first controller vibration from freezing the Frame (OVRPort's loader read its
|
||||
duration in nanoseconds as seconds and the game ran out of memory). With pose_time_fix the hands follow the controllers
|
||||
- OVRPlugin asked for hand poses at Android's monotonic "now", which on SteamOS 0.4.5 is 2.56 s behind the runtime's
|
||||
XrTime, so the hands lagged far behind.
|
||||
tested_version: 1.2.4
|
||||
engine: Unity
|
||||
xr: VrApi
|
||||
@@ -18,9 +20,10 @@ frame:
|
||||
- frame.unity_gl_shim
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
pose_time_fix: 1
|
||||
verified:
|
||||
date: '2026-10-06'
|
||||
date: '2026-10-08'
|
||||
issue: 48
|
||||
updated: '2026-10-06'
|
||||
min_app: 0.11.1
|
||||
updated: '2026-10-08'
|
||||
min_app: 0.12.1
|
||||
source_hint: Battle Sister
|
||||
@@ -0,0 +1,21 @@
|
||||
package: com.ToastVR.Matilda
|
||||
title: Matilda
|
||||
status: works
|
||||
tested_version: 1.1.0
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-08'
|
||||
known_good_sha256: 9a67459fa36c3b9ac0d035e6c43b1809b39772699333e8cfd7d2a4d7f17926a7
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 110
|
||||
source_hint: com.ToastVR.Matilda
|
||||
@@ -1,16 +1,21 @@
|
||||
package: com.camouflaj.manta
|
||||
title: 'Batman: Arkham Shadow'
|
||||
tested_version: 1.4.1-350961
|
||||
source_hint: 'Batman- Arkham Shadow'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
status: works
|
||||
notes: Large data set (~27 GiB) incl. all language packs.
|
||||
details: Uses Meta XR Audio (Wwise), handled by patch_meta_xr_audio. Includes all language packs (~28 GB).
|
||||
details: 'Uses Meta XR Audio (Wwise), handled by patch_meta_xr_audio. Includes all language packs (~28 GB). sync_guard
|
||||
(to test, GitHub #102) - a reporter''s game quit to Steam''s waiting screen after the Frame runtime crashed in
|
||||
xrSyncActions (CVRInputLatest::UpdateActionState), the input race Myst had.'
|
||||
tested_version: 1.4.1-350961
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
overport_extra:
|
||||
- patch_disable_space_warp
|
||||
adapter:
|
||||
sync_guard: 1
|
||||
verified:
|
||||
date: '2026-09-28'
|
||||
overport_cli: 1.2.3
|
||||
overport_runtime: 3.4.3-23204ea
|
||||
known_good_sha256: 796d1cab08f614d72c50d9702974c719c0908f5de91183cfe02d1fd5eb09551a
|
||||
updated: '2026-10-09'
|
||||
source_hint: Batman- Arkham Shadow
|
||||
@@ -0,0 +1,27 @@
|
||||
package: com.ilmxlab.tales
|
||||
title: 'Star Wars: Tales from the Galaxy''s Edge'
|
||||
status: unknown
|
||||
notes: Not working yet - after the intro logos and a loading animation the picture goes black (GitHub issue 61). Gets
|
||||
the controller fixes found for Vader Immortal (same studio) to test.
|
||||
details: Unreal Engine 4 (GLES) by ILMxLAB, like Vader Immortal, but a different engine build - it has none of Vader's
|
||||
Quest-only branches (no GetQuestShaderPrecompilePercent or RPOC key map; it precompiles shaders through Unreal's own
|
||||
pipeline cache), so frame.unreal_quest_precompile/_keymap don't apply. Its seasons and Wwise banks are Meta platform
|
||||
asset files (frame.asset_files). Vader's OVRPlugin findings apply to it as well - poses asked for at OVRPlugin's own
|
||||
clock (pose_time_fix) and thumb proximity from an extension the Frame lacks (proximity_emul). Ruled out for the black
|
||||
picture so far (see the diagnostics in issue 61) - asset-file paks, Valve's foveation, GL errors; the game's own eye
|
||||
image reads back black.
|
||||
tested_version: 1.1.9+734762.cl.439699
|
||||
engine: Unreal
|
||||
xr: VrApi
|
||||
alt_overport:
|
||||
- patch_remove_unreal_force_quit
|
||||
frame:
|
||||
- frame.asset_files
|
||||
adapter:
|
||||
pose_time_fix: 1
|
||||
proximity_emul: 1
|
||||
verified:
|
||||
issue: 61
|
||||
updated: '2026-10-09'
|
||||
min_app: 0.12.1
|
||||
source_hint: Star Wars- Tales from the Galaxys Edge
|
||||
@@ -0,0 +1,24 @@
|
||||
package: com.markschramm.gravitylab
|
||||
title: Gravity Lab
|
||||
status: works
|
||||
notes: Works great, along with passthrough mode
|
||||
tested_version: '1.221'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
- frame.unity_runtime_msaa_off
|
||||
- frame.unity_gl_shim
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-09'
|
||||
known_good_sha256: 82c95ee7487a130b6e37b967dd97c8362bf4d7e873fcff823f2c7877a62ff99f
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261007.6125817'
|
||||
agent: 59
|
||||
issue: 113
|
||||
source_hint: Gravity Lab
|
||||
@@ -0,0 +1,21 @@
|
||||
package: jp.co.amata.nvr
|
||||
title: The Tale of Onogoro
|
||||
status: works
|
||||
tested_version: 1.1.0248
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
frame:
|
||||
- frame.unity_text_input
|
||||
device:
|
||||
- device.text_input_window
|
||||
adapter:
|
||||
haptic_fix: 1
|
||||
verified:
|
||||
date: '2026-10-08'
|
||||
known_good_sha256: d6258a22a0a6e4588bbec36170f126bffc44ed64784f99e37cf4b03ad212fb79
|
||||
app: 0.12.0
|
||||
overport_cli: 1.2.5
|
||||
frame_build: '20261006.6173745'
|
||||
agent: 59
|
||||
issue: 108
|
||||
source_hint: The Tale of Onogoro
|
||||
@@ -1,11 +1,13 @@
|
||||
package: rift.superhot_vr
|
||||
title: SUPERHOT VR
|
||||
status: works
|
||||
notes: 'Supports SteamVR/OpenXR itself: runs directly, without Revive.'
|
||||
notes: 'Supports SteamVR/OpenXR itself: runs directly, without Revive. Older builds with both Oculus and SteamVR support
|
||||
(SUPERHOTVR.exe) start with -vrmode OpenVR.'
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
kind: rift
|
||||
pcvr:
|
||||
- pcvr.launch_args
|
||||
- pcvr.xr_timefix
|
||||
- pcvr.steamvr_tuning
|
||||
pcvr_remove:
|
||||
@@ -17,4 +19,5 @@ verified:
|
||||
app: 0.6.3
|
||||
overport_cli: 1.2.5
|
||||
issue: 28
|
||||
updated: '2026-10-09'
|
||||
source_hint: SUPERHOT VR
|
||||
+31
-1
@@ -43,6 +43,13 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: The game crashed on a return-address check (autiasp) its code never signed. The Quest's CPU ignores these checks, the Steam Frame's enforces them; some OpenSSL assembly in game engines has such unpaired checks (e.g. Star Wars Pinball VR on its first network connection, thread HttpManager).
|
||||
suggest: [frame.pac_hints]
|
||||
- id: cube-swapchain-refused
|
||||
pattern: 'xrCreateSwapchain \d+x\d+ .*faces=6 .*result=-\d+'
|
||||
unless: 'cube_standin: runtime refused'
|
||||
severity: fatal
|
||||
diagnosis: The game asked for a cube-map swapchain (e.g. an OVROverlay skybox or loading cube), which the Frame's runtime doesn't have. OVRPlugin goes on without images and crashes in ovrp_EndFrame4 (Unity's render thread SIGSEGV in memset, e.g. Budget Cuts Ultimate). FrameBridge from FramePort 0.12 serves such swapchains itself and drops their cube layers (cube_standin, GLES games) - rebuild and reinstall the game.
|
||||
suggest: [frame.adapter]
|
||||
supersedes: [unity-render-crash, native-crash]
|
||||
- id: swapchain-rect-invalid
|
||||
pattern: 'xrEndFrame failed -25\b'
|
||||
severity: fatal
|
||||
@@ -75,10 +82,26 @@ signatures:
|
||||
severity: error
|
||||
diagnosis: The Frame rejected the swapchain format (GLES games must use sRGB formats, no MSAA).
|
||||
suggest: [adapter.swapchain_fix]
|
||||
- id: zink-shader-fix-mismatch
|
||||
pattern: 'shader fix layer: a \d+-byte module differs from fix'
|
||||
severity: warning
|
||||
diagnosis: The game's OpenGL ES shader fix (zink_shader_fix) no longer matches the shader the Frame's GL driver (Zink)
|
||||
builds - a Frame or Lepton update changed the driver's output - so the shader it fixes (e.g. Vader Immortal's
|
||||
lightspeed jump) may hang the GPU again. Capture the new modules with zink_shader_dump=1 (files/fp_spirv/ in the
|
||||
game's storage) and update the fix.
|
||||
suggest: [adapter.zink_shader_dump]
|
||||
- id: zink-shader-layer-inactive
|
||||
pattern: 'shader fix layer: NOT active'
|
||||
severity: warning
|
||||
diagnosis: FramePort's shader-fix Vulkan layer couldn't add itself to the game's Vulkan layers (Android's
|
||||
GraphicsEnv functions it uses were not found or behaved differently, e.g. after a Lepton update), so the game's
|
||||
OpenGL ES shader fixes (zink_shader_fix) don't apply.
|
||||
suggest: []
|
||||
- id: zink-device-lost
|
||||
pattern: 'zink.*DEVICE[_ ]LOST|VK_ERROR_DEVICE_LOST'
|
||||
severity: fatal
|
||||
diagnosis: The GPU hung (Zink/Mesa). For GLES Unity games MSAA render-to-texture is the usual cause.
|
||||
diagnosis: The GPU hung (Zink/Mesa). For GLES Unity games MSAA render-to-texture is the usual cause; at one effect
|
||||
only, a shader reading undefined loop counters (e.g. Vader Immortal's lightspeed jump - frame.zink_shader_fix).
|
||||
suggest: [frame.unity_no_msaa, frame.unity_runtime_msaa_off]
|
||||
- id: unity-runtime-msaa
|
||||
pattern: 'recommended MSAA level is [248]\. Switching to the recommended level'
|
||||
@@ -251,6 +274,12 @@ signatures:
|
||||
diagnosis: Stress Level Zero's graphics plugin (libSLZQuestNative.so) hooks Unity's Vulkan start-up and calls an invalid function there on the Frame (e.g. BONELAB 1.2974); the game crashes or hangs before its first frame.
|
||||
suggest: [frame.slz_vulkan_hooks]
|
||||
supersedes: [java-crash, native-crash]
|
||||
- id: vivox-api31
|
||||
pattern: 'NoSuchMethodError: No virtual method \w*CommunicationDevice\w*\(.*Landroid/media/AudioManager'
|
||||
severity: fatal
|
||||
diagnosis: The game's Vivox voice-chat SDK calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes (e.g. Green Hell VR). The patch makes Vivox's audio-route check return early (voice chat keeps the default route).
|
||||
suggest: [frame.vivox_audio_route]
|
||||
supersedes: [java-crash]
|
||||
- id: native-crash
|
||||
pattern: 'Fatal signal (\d+) \(SIG[A-Z]+\)'
|
||||
severity: fatal
|
||||
@@ -275,6 +304,7 @@ signatures:
|
||||
suggest: []
|
||||
- id: container-not-started
|
||||
pattern: "is not a running context|OCI runtime error"
|
||||
unless: 'Boot complete!' # Lepton 3.x prints the error while its container is still starting, then boots fine
|
||||
severity: fatal
|
||||
diagnosis: The Lepton container failed to start (see the error above it in launch.log).
|
||||
suggest: []
|
||||
|
||||
@@ -76,6 +76,13 @@
|
||||
`device.foveation`: `fixed` = `FDM_DEBUG=disable_offsets`, `off` = `VK_INSTANCE_LAYERS=""`. Lepton passes `FDM`,
|
||||
`FDM_DEBUG`, `FOVE_LEVEL` and `FDM_SWAPCHAIN_SIZE` through to the container (liblepton/mounting.sh PASSTHROUGH_VARS);
|
||||
the layers are chosen on the host, so setting them inside the game does nothing.
|
||||
- How Lepton loads them: `liblepton/vulkan_layers.sh` mounts the chosen layers (only those in the OS image's
|
||||
`/usr/share/guestos/android/vendor/vulkan_layers`) into the app's lib dir and writes their names to Android's
|
||||
`settings global gpu_debug_layers` for `gpu_debug_app` = the game; Android's loader reads that list from GraphicsEnv
|
||||
at each vkCreateInstance and searches the app's lib dir (`/data/app/…/lib/arm64`). A layer bundled in the APK is
|
||||
found there too; FramePort's shader-fix layer (`frame.zink_shader_fix`) adds its own name to GraphicsEnv's list from
|
||||
inside the process (`android::GraphicsEnv::setDebugLayers`, exported by the guest's libgraphicsenv.so, Lepton 3.0.5).
|
||||
That is the only way to reach the Vulkan side of OpenGL ES games (Zink creates the instance inside Mesa).
|
||||
- GL ES: Zink (Mesa GL on Vulkan). Strict GLSL (see PLAYBOOK) and occasional `DEVICE LOST` with MSAA render-to-texture.
|
||||
|
||||
## Proton / Windows games (surveyed 2026-09-29; running a Rift game under it not yet verified)
|
||||
|
||||
+7
-1
@@ -30,6 +30,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Espire 2 | Quest | ✅ Works | |
|
||||
| Genotype | Quest | ✅ Works | |
|
||||
| GORN2 | Quest | ✅ Works | |
|
||||
| Gravity Lab | Quest | ✅ Works | |
|
||||
| H.U.N.T | Quest | ✅ Works | |
|
||||
| I Am Cat | Quest | ✅ Works | |
|
||||
| I Am Monkey | Quest | ✅ Works | |
|
||||
@@ -43,6 +44,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Lucky's Tale | Quest | ✅ Works | |
|
||||
| Marvel's Deadpool VR | Quest | ✅ Works | |
|
||||
| Marvel's Iron Man VR | Quest | ✅ Works | |
|
||||
| Matilda | Quest | ✅ Works | |
|
||||
| Medieval Dynasty New Settlement | Quest | ✅ Works | |
|
||||
| Metro Awakening | Quest | ✅ Works | |
|
||||
| Mobile Suit Gundam: Silver Phantom | Quest | ✅ Works | |
|
||||
@@ -52,6 +54,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| palazzo_santacruz | Quest | ✅ Works | |
|
||||
| Path of the Warrior | Quest | ✅ Works | |
|
||||
| Pistol Whip | Quest | ✅ Works | |
|
||||
| Please Don't Touch Anything | Quest | ✅ Works | |
|
||||
| PowerWash Simulator VR | Quest | ✅ Works | |
|
||||
| QuestCraft | Quest | ✅ Works | |
|
||||
| Retronika | Quest | ✅ Works | |
|
||||
@@ -71,23 +74,26 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| The Boys VR | Quest | ✅ Works | |
|
||||
| The Climb 2 | Quest | ✅ Works | |
|
||||
| The Room VR | Quest | ✅ Works | |
|
||||
| The Tale of Onogoro | Quest | ✅ Works | |
|
||||
| Time Crisis VR (Experimental) | Quest | ✅ Works | |
|
||||
| Toy Master | Quest | ✅ Works | |
|
||||
| Under Cover | Quest | ✅ Works | |
|
||||
| VR HOT Quest | Quest | ✅ Works | |
|
||||
| VR4 | Quest | ✅ Works | |
|
||||
| Walkabout Mini Golf | Quest | ✅ Works | |
|
||||
| Wallace & Gromit in The Grand Getaway | Quest | ✅ Works | |
|
||||
| Waltz of the Wizard: Extended Edition | Quest | ✅ Works | |
|
||||
| Wander | Quest | ✅ Works | |
|
||||
| Arcsmith | Quest | ⚠️ Works with issues | Right eye distorts during movement (unresolved; swap, tracking, Valve layers, depth and pacing ruled out). |
|
||||
| Assassin's Creed Nexus | Quest | ⚠️ Works with issues | Some launch warning text is still upside down; the rest of the UI is fixed by flip emulation. |
|
||||
| Does it Stack? | Quest | ⚠️ Works with issues | Mixed reality mode does not work (works on Demeo), everything else seems to be OK |
|
||||
| Doom3Quest | Quest | ⚠️ Works with issues | PDA shows black screen. |
|
||||
| Myst | Quest | ⚠️ Works with issues | Minor graphical glitches on some objects. |
|
||||
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
|
||||
| Pinball FX VR | Quest | ⚠️ Works with issues | Plays; mixed reality mode not working yet. |
|
||||
| Silhouette | Quest | ⚠️ Works with issues | Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky. |
|
||||
| The Light Brigade | Quest | ⚠️ Works with issues | Judder in weapons |
|
||||
| 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… |
|
||||
| BlazeRush | Quest | ❌ Doesn't run | Starts and reaches the menu room, but the room shows no controllers and ignores all input (it all reaches the game); no fix yet. |
|
||||
| Espire 1: VR Operative (Quest Edition) | Quest | ❌ Doesn't run | Mesa GL driver crash during texture upload. |
|
||||
|
||||
@@ -78,6 +78,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Video player / app finds no local videos; MediaProvider "Requested path /home/steamos/... doesn't appear under ..." | Lepton's /sdcard is a symlink to a host path, so Android's media index rejects every file | upload in the Files tab (Videos = /sdcard/Movies) and browse folders in the app |
|
||||
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only; see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
|
||||
| Game stays on a loading or "Waiting" box although it reached FOCUSED; log `FrameBridge: xrEndFrame failed -25` then no more `pacing:` lines (e.g. PowerWash Simulator on some Frames, GitHub #39) | The eye image rect ends a few pixels past its swapchain (Unity rounds the swapchain width, the runtime's recommended size differs per Frame); SteamVR rejects every frame with XR_ERROR_SWAPCHAIN_RECT_INVALID | FrameBridge clamps every projection/quad image rect to its swapchain (`rect_clamp`, default on since 0.11.0; log `rect_clamp: view N image rect clamped`): rebuild + reinstall the game |
|
||||
| Unity game (OVRPlugin, GLES) crashes ~10 s in: `FrameBridge: xrCreateSwapchain … faces=6 … result=-2`, OVRPlugin `CreateSwapchain for eye 0: 0x0, 0 stages`, then render-thread SIGSEGV in memset ← libOVRPlugin ← `ovrp_EndFrame4` (libgallium/ANR follow; e.g. Budget Cuts Ultimate, GitHub #107); triage `cube-swapchain-refused` | The game shows a cube-map layer (OVROverlay cubemap); the Frame's runtime has no cube layers and refuses cube swapchains (XR_ERROR_RUNTIME_FAILURE); OVRPlugin doesn't check and writes into the image list it never got | FrameBridge `cube_standin` (default on): a refused cube swapchain is served as a GL cube map in the game's context and its layers are dropped (log `cube_standin: runtime refused …`): rebuild + reinstall. GLES only; a Vulkan game keeps the error |
|
||||
| Game (Unity, GLES) freezes, `zink: DEVICE LOST` | multisampled render-to-texture hangs the GPU | `frame.unity_no_msaa`; if it persists: unfixable → PC version |
|
||||
| Unreal GLES game crashes ~3 s after start: `Fatal signal 11 … fault addr 0x10000 in tid … (RHIThread)`, `#01 … libgallium_dri.so` (GitHub #83 Star Wars Pinball VR, triage `unreal-msrtt-crash`) | Unreal's mobile MSAA renders through multisampled render-to-texture; Zink's `find_rp_state` then indexes `rendering_state_cache[6]` one past its end (sample count ≥ 32) and calls a junk hash function (0x10000). Mesa bug (no bounds check) | `frame.unreal_gl_shim` (GL shim with `gl_hide_msrtt`, multiview kept for Unreal: Unreal 4.25 only enables multiview with GL_OVR_multiview, GL_OVR_multiview2 **and** GL_OVR_multiview_multisampled_render_to_texture, so the shim keeps the last one visible for Unreal and maps glFramebufferTextureMultisampleMultiviewOVR to the single-sampled glFramebufferTextureMultiviewOVR; log `multiview multisampled render-to-texture (N samples) drawn single-sampled`) |
|
||||
| Crash with `SIGILL (ILL_ILLOPN)` and `*pc=0xd50323bf` (autiasp), e.g. on Unreal's HttpManager thread (GitHub #83 Star Wars Pinball VR, triage `pac-unpaired`) | The engine's OpenSSL ARMv8 assembly has autiasp without a matching paciasp (libUE4.so: 38 paciasp, 40 autiasp; Poly1305 NEON). Quest CPUs treat PAC hints as NOPs, the Frame's CPU checks them | `frame.pac_hints`: in a library with unpaired counts every paciasp/autiasp becomes a NOP (in place) |
|
||||
@@ -86,21 +87,27 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Black flat window, then a GPU hang: kernel `hangcheck detected gpu lockup` (offending task the game), `zink: DEVICE LOST`, Unity render-thread crash; `outside of frame bounds` thousands of times a second (e.g. BattleSisters, Unity 2019.4) | Unity 2019 on its built-in VR (no `libOculusXRPlugin.so`) uses the same legacy frame loop as Unity 2017: no `xrWaitFrame`, the render thread floods the GPU | `frame.unity_oculus_check` adds the ovrpshim frame wait for these too (revision 2) |
|
||||
| Unity built-in VR game (with `frame.unity_oculus_check`) freezes at a scene switch: the log stops after `Boot: Activating Scene: …`, the process stays alive, every thread sleeps (e.g. Sniper Elite VR, Unity 2019.4) | The shim's `xrWaitFrame` blocks until the previously waited frame is begun; Unity skipped beginning it at the scene switch and its render thread then waited for the main thread (gdb: UnityMain in vrclient `CSxrSession::StartNextFrame` ← `ovrp_WaitToBeginFrame` ← `fpov_Update2`). OVRPlugin also only begins the frame index it waited for | `frame.unity_oculus_check` revision 5: libunity.so's `ovrp_BeginFrame`/`ovrp_EndFrame` go through the shim, which waits ≤ 50 ms for the last waited frame to begin, else skips the wait (log `the last waited frame wasn't begun`), and gives a begin/end of another index the waited one |
|
||||
| Unity built-in VR game: the hands/weapons trail the controllers by centimetres when moving (headset fine, 72 fps), e.g. Sniper Elite VR | Unity's physics-step `ovrp_Update2` (prediction 0) makes OVRPlugin locate the nodes at the monotonic clock's "now", the XrTime base on a Quest but 0.05–0.9 s behind the Frame's XrTime; the runtime extrapolates backwards and the render-step reads after it get those poses too. A larger prediction is capped by OVRPlugin (~0.07 s) | `frame.unity_oculus_check` revision 5 holds the physics-step update back: both steps read the render update's display-time poses |
|
||||
| OVRPlugin game: the hands lag behind the controllers (also with `ovrp_hold_physics` on or off; rendering fine), e.g. BattleSisters | OVRPlugin passes its monotonic-clock "now" on as the OpenXR time (OVRPort's dispatcher converts XR_KHR_convert_timespec_time 1:1: the Frame's runtime lacks it, and FrameBridge's emulation is never asked), but the Frame's XrTime runs ahead of CLOCK_MONOTONIC (2.56 s on SteamOS 0.4.5): with `pose_debug=1` the hand spaces show `time - predicted display time -2564 ms`; BattleSisters also asks for its head at XrTime 0.1 s every frame | `adapter.pose_time_fix` (FrameBridge, suggested for Unity built-in OVRPlugin games): a located time nearer the monotonic clock than XrTime's "now" is moved by the clock offset measured at xrWaitFrame, one more than 0.5 s before the display time goes to "now" (log `pose_time_fix: …`; `pose_debug=1` counts both per 5 s). Once a build has this FrameBridge it is toggled in settings.conf (no rebuild) The offset is the largest of the last ~2 s of xrWaitFrame samples (hitches made single samples dip by up to 2.5 s and pushed fixed times into the future); far-past requests get the frame's display time. Headless survey of 54 games (2026-10-09): no head/view request on the monotonic clock in any other game; UE4 OVRPlugin 1.89 games (Robo Recall, Vader, Phantom, Time Stall) and The Room VR ask for head/views at XrTime ≈ 0 every frame, which works today; hands only show in the headset. Suggested for Unity built-in Oculus games only |
|
||||
| Unreal game stuck on a loading image after the intro, frames keep coming (72 fps), no crash (e.g. Vader Immortal) | suspect: a Meta Platform SDK request no message ever answers | diagnostics: `frame.ovr_trace` (opt-in) logs every `ovr_*` call, the answers `ovr_PopMessage` returns and every 10 s the unanswered requests (`fp_ovrtrace` in launch.log). Never fake an entitlement answer |
|
||||
| Own-engine game crashes in `libVkLayer_fossilize.so` (Fossilize's recording thread, e.g. Roblox) | same as Deadpool VR: uninitialised pointers in what Fossilize records | `frame.vk_sanitize` now also covers own-engine libraries that load `libvulkan.so` by name |
|
||||
| Crash right after the game gets focus, in `vrclient.so … UpdateActionStateInternal` / `sxr_xrSyncActions` (e.g. Myst) | a race in the Frame runtime's input code on the first sync after focus (not every launch) | adapter `sync_guard` (input syncs one at a time with event polling, paused 250 ms after FOCUSED) |
|
||||
| View jumps sideways / game pauses or freezes for a moment every few seconds (e.g. Blade & Sorcery, BattleGlide) | the Frame drops focus for ~0.5 s (FOCUSED→VISIBLE→SYNCHRONIZED); games pause or re-align the player (`OnVRPresence … Teleport`) | adapter `focus_hold` |
|
||||
| SDL / LÖVE app crashes at start: `NullPointerException … ClipboardManager.addPrimaryClipChangedListener` in `SDLClipboardHandler.<init>` (e.g. Dramatic Shape) | Lepton's Android has no clipboard service (`service check clipboard`: not found) | `frame.sdl_clipboard` (that call → nops in classes.dex, in place, `apk/dex.py`); reproduced and verified with LÖVE for Android 11.5 |
|
||||
| Game crashes at start: `java.lang.NoSuchMethodError: No virtual method getAvailableCommunicationDevices()… in class Landroid/media/AudioManager` at `com.vivox.sdk.AudioChangeListener.checkAudioRouteAndApplyChanges` (e.g. Green Hell VR, GitHub #101); triage `vivox-api31` | Newer Vivox (voice chat) builds call Android 12 audio-routing methods without a version check; Lepton is Android 11. Older Vivox builds (Eleven Table Tennis, BattleSisters) don't have that code | `frame.vivox_audio_route` (suggested by analysis `vivox_api31`): every AudioChangeListener method that calls one of them returns at once (`Dex.return_early`: return-void / `const/4 v0, 0; return v0`, in place); voice chat keeps the default audio route |
|
||||
| Controllers do nothing (e.g. stuck on a setup screen); log: `xrSuggestInteractionProfileBindings … XR_ERROR_PATH_UNSUPPORTED` | the game only suggests bindings for Meta's newer profiles (Touch Plus / Touch Pro); the Frame's Android runtime knows `oculus/touch_controller`, not those | adapter `profile_remap` (default on): the bindings are suggested again for Touch (log: `controller profile … -> oculus/touch_controller`) |
|
||||
| Some buttons do nothing or the wrong thing (e.g. a Quest game's X/Y on the Frame's left controller) | the game binds only Quest Touch and SteamVR maps Touch onto the Frame controllers with its own remap (Touch left X/Y land on the d-pad, right X and Y repeat B), or the runtime rejected a profile or an input call | turn on adapter `input_diag` (diagnostics, off by default; `frameport settings <pkg> input_diag=1`) and look for `input_diag:` lines: `bindings: <profile> accepted` or `unsupported: interaction profile <profile> -> <result>` with the rejected paths, `bindings: /user/hand/<hand> uses <profile>`, `unsupported: function <name>`, `unsupported: xrCreateAction -> <result> (<action>)` |
|
||||
| Unity game hangs or the whole Frame restarts (e.g. in a menu); log: `The current MSAA level is 0, but the recommended MSAA level is 4. Switching to the recommended level.` (e.g. Lucky's Tale) | Meta's OVRManager (`useRecommendedMSAALevel`) turns 4x MSAA on at runtime, past QualitySettings; multisampled render-to-texture on GLES/Zink hangs the GPU | `frame.unity_runtime_msaa_off` (Cpp2IL: `OVRDisplay.get_recommendedMSAALevel` → 0) |
|
||||
| Unity game: one eye shows only effects or grey, the other is fine (e.g. I Am Cat, Oculus XR Plugin on GLES) | the game's single-pass multiview rendering goes wrong for array slice 1 (the adapter submits both slices correctly) | `frame.unity_multipass` (Cpp2IL: `OculusSettings.GetStereoRenderingMode` → MultiPass); `swap_eyes=1` tells whether the game or the adapter is at fault |
|
||||
| Setup/intro loops every launch (Espire 2) | save folders created without write permission | launcher repairs permissions every 2 s (built in) |
|
||||
| VrApi bridge: never enters VR | Frame reaches FOCUSED later than Quest; tracking only when worn | bridge waits 30 s; test in the headset |
|
||||
| Unreal game (ILMxLAB, e.g. Vader Immortal) plays its intro, then stays on its loading card (portrait + progress bar) at 72 fps and ignores input (GitHub #49) | the menu waits for `UVRUtils::GetQuestShaderPrecompilePercent()` to reach 100 %; the precompile only starts when `IsRunningOnSantaCruz()` (a Quest), the other branch returns 0.0 | `frame.unreal_quest_precompile` (non-Quest branch returns 1.0; found by Klownicle); suggested when the engine exports that function (analysis `unreal_quest_gates`) |
|
||||
|
||||
## Picture problems (headset on)
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Unreal game (ILMxLAB, e.g. Vader Immortal): controllers track but grip/trigger do nothing, grabbing never works (Unreal's own input values move, the game's stay 0) | `URPOCKeyMapManagerComponent` picks the Quest or the Gear VR key set by `IsRunningOnSantaCruz()`; on the Frame it takes Gear VR, empty in a Quest build | `frame.unreal_quest_keymap` (the Oculus case keeps the Quest set, for action and axis mappings; found by Klownicle, GitHub #49) |
|
||||
| The hands' thumbs never move when you touch the thumbstick or buttons (Unreal `ThumbUp` stays 1, e.g. Vader Immortal) | OVRPlugin reports thumb proximity from XR_FB_touch_controller_proximity, which the Frame's runtime lacks; OVRPort's loader offers it anyway and the proximity actions never get a binding | adapter `proximity_emul=1` (thumb proximity from the touch inputs: thumbstick, face buttons, thumb rest; 2 = also the index finger from trigger touch); log `proximity_emul: finger proximity bound to touch` |
|
||||
| OpenGL ES game: the GPU hangs at one effect (freeze/exit, compositor watchdog, e.g. Vader Immortal's lightspeed jump) | a shader (as Zink compiles it) reads loop counters/accumulators before setting them | capture the SPIR-V (adapter `zink_shader_dump=1` writes every module to `files/fp_spirv/`, or a GPU capture), then a `zink_shader_fix` entry (same format as `vk_shader_fix`) + `frame.zink_shader_fix` (Vulkan layer under Zink, activated through GraphicsEnv's debug layer list); log `shader fix layer: fixed shader module N`; triage `zink-shader-fix-mismatch` when a Frame update changed the driver's output |
|
||||
| 2D Android app: Steam says it's running but nothing shows | Lepton runs apps headless unless the app folder has `lepton-show-flatscreen` | FramePort adds it for apps without VR (agent v28+); reinstall apps installed before |
|
||||
| A phone/tablet app shows nothing in the headset, or a VR app opens as a flat window | FramePort's VR/2D guess (`vr_kind`) is wrong for this app | patch `device.display_mode` → **Flat window** or **VR** (Customize → "Show as VR or as a flat window"), then Update on Frame |
|
||||
| "Install with FrameDrop" button opens FrameDrop (or nothing) instead of FramePort | another program owns `framedrop://`, or the switch is off, or macOS (Flet can't receive links there) | Settings → Install links → **Use FramePort for these links**; else Add games → Install from a link… (paste the button's address) |
|
||||
|
||||
+2
-1
@@ -11,11 +11,12 @@ embeds debug paths (`-g`) and the dex depends on the d8 version, so those differ
|
||||
|
||||
| Dir | Artifact | What / why |
|
||||
|---|---|---|
|
||||
| `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`); `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. |
|
||||
| `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, `vrapi_PollEvent`/`RecenterPose`/`SetDisplayRefreshRate`/`GetSystemPropertyFloatArray` (BlazeRush imports them, GitHub #57), 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. |
|
||||
| `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). |
|
||||
| `xrshim/` | `arm64-v8a/libframe_xrshim.so` | FrameBridge extension shim, only in builds with `controller_models=1`. overport's dispatcher (`libopenxr_loader.so`) answers `xrGetInstanceProcAddr` from a fixed table and refuses everything else (`overportOXR: Unknown proc addr: xrLoadRenderModelFB`), so the adapter never sees XR_FB_render_model lookups. Meta's OVRPlugin gets the function with `dlopen("libopenxr_loader.so")` + `dlsym`, so FramePort rewrites that `.rodata` string in `libOVRPlugin.so` (same length, in place) to `libframe_xrshim.so`. The shim returns the adapter's emulated functions (`framebridge_extension_proc`) and forwards every other name to overport. A DT_NEEDED interposer does not work here (dlsym on a handle searches that library first); tried and dropped. |
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Included by frame_adapter.c (after layer_emul_gl.c, whose GL entry points it shares). cube_standin (default on):
|
||||
// the Frame's runtime has no cube layers (XR_KHR_composition_layer_cube) and refuses cube swapchains (faceCount 6)
|
||||
// with XR_ERROR_RUNTIME_FAILURE. OVRPlugin doesn't check: it goes on with "CreateSwapchain for eye 0: 0x0, 0 stages"
|
||||
// and writes past its empty image list in the next ovrp_EndFrame4 (Unity's render thread SIGSEGV in memset, e.g.
|
||||
// Budget Cuts Ultimate's cube-map overlay, GitHub #107). Only when the runtime refused one, a cube swapchain is served
|
||||
// by the adapter instead: one GL cube-map texture (the format, size and mip levels asked for) in the app's current
|
||||
// GLES context, acquire/wait/release succeed at once. The app renders or copies into it as usual; layers that use it
|
||||
// are dropped in xrEndFrame (the runtime couldn't show them anyway). Vulkan sessions (no current EGL context) keep
|
||||
// the runtime's error.
|
||||
#define STANDIN_MAX 8
|
||||
static struct { XrSwapchain handle; GLuint tex; } standins[STANDIN_MAX];
|
||||
static pthread_mutex_t standin_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int standin_find(XrSwapchain handle) {
|
||||
if (!handle) return -1;
|
||||
int slot = -1;
|
||||
pthread_mutex_lock(&standin_lock);
|
||||
for (int i = 0; i < STANDIN_MAX && slot < 0; ++i)
|
||||
if (standins[i].handle == handle) slot = i;
|
||||
pthread_mutex_unlock(&standin_lock);
|
||||
return slot;
|
||||
}
|
||||
|
||||
static int is_standin(XrSwapchain handle) { return cube_standin && standin_find(handle) >= 0; }
|
||||
|
||||
// After the runtime refused a swapchain: serve a cube stand-in if it was a cube swapchain in a GLES context.
|
||||
static XrResult standin_after_failure(const XrSwapchainCreateInfo *info, XrSwapchain *out, XrResult result) {
|
||||
if (XR_SUCCEEDED(result) || !cube_standin || !info || !out || info->faceCount != 6) return result;
|
||||
if (info->width != info->height || !info->width || info->arraySize > 1) return result;
|
||||
if (!p_eglCreateContext && !emul_load_gl()) { LOG("cube_standin: no GLES, the runtime's error stays"); return result; }
|
||||
if (p_eglGetCurrentContext() == EGL_NO_CONTEXT) {
|
||||
LOG("cube_standin: no current GLES context (Vulkan?), the runtime's error stays");
|
||||
return result;
|
||||
}
|
||||
int slot = -1;
|
||||
pthread_mutex_lock(&standin_lock);
|
||||
for (int i = 0; i < STANDIN_MAX && slot < 0; ++i)
|
||||
if (!standins[i].handle) slot = i;
|
||||
if (slot >= 0) standins[slot].handle = (XrSwapchain)1; // reserved
|
||||
pthread_mutex_unlock(&standin_lock);
|
||||
if (slot < 0) { LOG("cube_standin: too many cube swapchains, the runtime's error stays"); return result; }
|
||||
GLsizei levels = info->mipCount > 1 ? (GLsizei)info->mipCount : 1;
|
||||
for (int i = 0; i < 8 && p_glGetError() != GL_NO_ERROR; ++i) {}
|
||||
GLint previous = 0;
|
||||
p_glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &previous);
|
||||
GLuint tex = 0;
|
||||
p_glGenTextures(1, &tex);
|
||||
p_glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
|
||||
p_glTexStorage2D(GL_TEXTURE_CUBE_MAP, levels, (GLenum)info->format, (GLsizei)info->width, (GLsizei)info->height);
|
||||
GLenum err = p_glGetError();
|
||||
p_glBindTexture(GL_TEXTURE_CUBE_MAP, (GLuint)previous); // the app's binding, untouched
|
||||
if (err != GL_NO_ERROR) {
|
||||
p_glDeleteTextures(1, &tex);
|
||||
pthread_mutex_lock(&standin_lock);
|
||||
standins[slot].handle = XR_NULL_HANDLE;
|
||||
pthread_mutex_unlock(&standin_lock);
|
||||
LOG("cube_standin: couldn't make a %ux%u cube map (GL 0x%x), the runtime's error stays", info->width,
|
||||
info->height, err);
|
||||
return result;
|
||||
}
|
||||
XrSwapchain handle = FAKE_HANDLE(XrSwapchain);
|
||||
pthread_mutex_lock(&standin_lock);
|
||||
standins[slot].handle = handle;
|
||||
standins[slot].tex = tex;
|
||||
pthread_mutex_unlock(&standin_lock);
|
||||
*out = handle;
|
||||
LOG("cube_standin: runtime refused a %ux%u cube swapchain (result=%d): served by FrameBridge (GL cube map %u, "
|
||||
"%d levels); its cube layers are dropped", info->width, info->height, result, tex, (int)levels);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
static XrResult standin_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images) {
|
||||
if (!count) return XR_ERROR_VALIDATION_FAILURE;
|
||||
*count = 1;
|
||||
if (!capacity) return XR_SUCCESS;
|
||||
if (!images) return XR_ERROR_VALIDATION_FAILURE;
|
||||
if (images->type != XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR) return XR_ERROR_VALIDATION_FAILURE;
|
||||
int slot = standin_find(handle);
|
||||
((emul_gles_image *)images)[0].image = slot >= 0 ? standins[slot].tex : 0;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
static void standin_destroy(XrSwapchain handle) {
|
||||
pthread_mutex_lock(&standin_lock);
|
||||
for (int i = 0; i < STANDIN_MAX; ++i)
|
||||
if (standins[i].handle == handle) {
|
||||
if (p_eglGetCurrentContext && p_eglGetCurrentContext() != EGL_NO_CONTEXT) p_glDeleteTextures(1, &standins[i].tex);
|
||||
standins[i].handle = XR_NULL_HANDLE;
|
||||
standins[i].tex = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&standin_lock);
|
||||
}
|
||||
|
||||
// A layer that shows a stand-in: never passed to the runtime (whatever layer_fix says).
|
||||
static int layer_uses_standin(const XrCompositionLayerBaseHeader *layer) {
|
||||
if (!cube_standin || !layer) return 0;
|
||||
switch (layer->type) {
|
||||
case XR_TYPE_COMPOSITION_LAYER_CUBE_KHR:
|
||||
return is_standin(((const XrCompositionLayerCubeKHR *)layer)->swapchain);
|
||||
case XR_TYPE_COMPOSITION_LAYER_QUAD:
|
||||
return is_standin(((const XrCompositionLayerQuad *)layer)->subImage.swapchain);
|
||||
case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR:
|
||||
return is_standin(((const XrCompositionLayerCylinderKHR *)layer)->subImage.swapchain);
|
||||
case XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR:
|
||||
return is_standin(((const XrCompositionLayerEquirectKHR *)layer)->subImage.swapchain);
|
||||
case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR:
|
||||
return is_standin(((const XrCompositionLayerEquirect2KHR *)layer)->subImage.swapchain);
|
||||
case XR_TYPE_COMPOSITION_LAYER_PROJECTION: {
|
||||
const XrCompositionLayerProjection *p = (const XrCompositionLayerProjection *)layer;
|
||||
for (uint32_t v = 0; v < p->viewCount; ++v)
|
||||
if (is_standin(p->views[v].subImage.swapchain)) return 1;
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// XR_FB_swapchain_update_state on a stand-in: nothing to update (only hooked when the runtime has the function).
|
||||
static PFN_xrUpdateSwapchainFB real_update_swapchain;
|
||||
static PFN_xrGetSwapchainStateFB real_get_swapchain_state;
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL standin_update_swapchain(XrSwapchain swapchain, const XrSwapchainStateBaseHeaderFB *state) {
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
return real_update_swapchain ? real_update_swapchain(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
}
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL standin_get_swapchain_state(XrSwapchain swapchain, XrSwapchainStateBaseHeaderFB *state) {
|
||||
if (is_standin(swapchain)) return XR_ERROR_VALIDATION_FAILURE; // no state to report (apps treat it as "unsupported")
|
||||
return real_get_swapchain_state ? real_get_swapchain_state(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL eye_hook_xrReleaseSwapchainImage(XrSwapcha
|
||||
const XrSwapchainImageReleaseInfo *info) {
|
||||
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
if (release_wait_active()) eye_wait_for_gpu();
|
||||
XrResult result = fn(swapchain, info);
|
||||
if (eye_debug) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// * drops requested instance extensions the runtime lacks (e.g. XR_FB_passthrough);
|
||||
// * retries rejected swapchains with 1 sample / a supported format (swapchain_fix, overport #71);
|
||||
// * drops composition layers that reference swapchains that failed to create (layer_fix);
|
||||
// * serves cube swapchains the runtime refuses as GL cube maps and drops their layers (cube_standin, cube_standin.c);
|
||||
// * optionally requests mutable-format swapchain images (mutable_fix, off by default);
|
||||
// * optionally swaps left/right images of stereo projection layers (swap_eyes, off by default);
|
||||
// * emulates XR_FB_passthrough with XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND when the runtime lacks it
|
||||
@@ -103,8 +104,11 @@ 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 cube_standin = 1; // cube swapchains the runtime refuses: served by the adapter (cube_standin.c)
|
||||
static int rect_clamp = 1;
|
||||
static int pose_consistency = 0; // repeat xrLocateViews queries for one display time get the same poses (GitHub #8) // clamp submitted image rects to their swapchain (SteamVR rejects a 1 px overrun)
|
||||
static int pose_time_fix; // poses asked for at CLOCK_MONOTONIC "now" or far in the past: at XrTime "now" (pose_time.c)
|
||||
static int pose_debug; // diagnostics: requested pose times vs the predicted display time (pose_time.c)
|
||||
static int layer_fix = 1;
|
||||
static int mutable_fix = 0;
|
||||
static int swap_eyes = 0;
|
||||
@@ -127,6 +131,8 @@ static int controller_models; // serve Frame controller models via XR_FB_render
|
||||
// Per-game settings, all off by default (session_fixes.c, layer_emul_gl.c).
|
||||
static int sync_guard; // xrSyncActions one at a time with xrPollEvent, paused briefly after focus returns
|
||||
static int profile_remap = 1; // Meta's newer controller profiles (rejected by the Frame) -> oculus/touch_controller
|
||||
static int proximity_emul; // finger proximity from capacitive touch: 1 thumb, 2 thumb + index (session_fixes.c)
|
||||
static int runtime_has_proximity; // the runtime has XR_FB_touch_controller_proximity itself
|
||||
static int layer_debug; // diagnostics: layers, swapchains, session states, spaces, aim/grip, refresh rates
|
||||
static int input_diag; // diagnostics: controller profiles, bindings, missing functions, failing input calls
|
||||
static int stable_local; // keep every LOCAL space the app creates on the session-start origin
|
||||
@@ -158,8 +164,11 @@ static void read_settings(const char *path) {
|
||||
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, "cube_standin=%f", &value) == 1) cube_standin = value != 0;
|
||||
if (sscanf(line, "rect_clamp=%f", &value) == 1) rect_clamp = value != 0;
|
||||
if (sscanf(line, "pose_consistency=%f", &value) == 1) pose_consistency = value != 0;
|
||||
if (sscanf(line, "pose_debug=%f", &value) == 1) pose_debug = value != 0;
|
||||
if (sscanf(line, "pose_time_fix=%f", &value) == 1) pose_time_fix = value != 0;
|
||||
if (sscanf(line, "layer_fix=%f", &value) == 1) layer_fix = value != 0;
|
||||
if (sscanf(line, "mutable_fix=%f", &value) == 1) mutable_fix = value != 0;
|
||||
if (sscanf(line, "swap_eyes=%f", &value) == 1) swap_eyes = value != 0;
|
||||
@@ -180,6 +189,7 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "eye_debug=%f", &value) == 1) eye_debug = value != 0;
|
||||
if (sscanf(line, "release_wait=%f", &value) == 1) release_wait = (int)value;
|
||||
if (sscanf(line, "profile_remap=%f", &value) == 1) profile_remap = value != 0;
|
||||
if (sscanf(line, "proximity_emul=%f", &value) == 1 && value >= 0 && value <= 2) proximity_emul = (int)value;
|
||||
if (sscanf(line, "sync_guard=%f", &value) == 1) sync_guard = value != 0;
|
||||
if (sscanf(line, "stable_local=%f", &value) == 1) stable_local = value != 0;
|
||||
if (sscanf(line, "focus_hold=%f", &value) == 1) focus_hold = value != 0;
|
||||
@@ -240,6 +250,8 @@ static void initialize(void) {
|
||||
log_file = fopen(path, "a");
|
||||
}
|
||||
if (input_diag) LOG("per-game: input_diag=1 (controller-input diagnostics)");
|
||||
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 (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)");
|
||||
@@ -359,6 +371,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
}
|
||||
for (uint32_t j = 0; available && j < available_count; ++j)
|
||||
if (!strcmp(available[j].extensionName, "XR_FB_composition_layer_image_layout")) runtime_has_image_layout = 1;
|
||||
else if (!strcmp(available[j].extensionName, "XR_FB_touch_controller_proximity")) runtime_has_proximity = 1;
|
||||
// Layer types are only valid if their extension ends up enabled; some apps submit them regardless.
|
||||
int has_equirect = 0, has_equirect2 = 0, has_cylinder = 0, has_cube = 0;
|
||||
for (uint32_t i = 0; i < kept; ++i) {
|
||||
@@ -499,6 +512,10 @@ static int emul_is_virtual(XrSwapchain handle);
|
||||
static void emul_virtual_destroy(XrSwapchain handle);
|
||||
static XrResult emul_virtual_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
|
||||
static void emul_on_acquire(XrSwapchain handle, uint32_t index);
|
||||
static XrResult standin_after_failure(const XrSwapchainCreateInfo *info, XrSwapchain *out, XrResult result);
|
||||
static int is_standin(XrSwapchain handle);
|
||||
static void standin_destroy(XrSwapchain handle);
|
||||
static XrResult standin_enumerate(XrSwapchain handle, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images);
|
||||
|
||||
static int known_swapchain(XrSwapchain handle) {
|
||||
int found = 0;
|
||||
@@ -539,7 +556,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
snap_on_create(*swapchain, &fixed);
|
||||
return result;
|
||||
}
|
||||
if (!swapchain_fix) return result;
|
||||
if (!swapchain_fix) return standin_after_failure(&fixed, swapchain, result);
|
||||
|
||||
// Frame's runtime rejects some GLES formats (GL_RGBA8) and MSAA swapchains (overport #71).
|
||||
if (fixed.sampleCount > 1) {
|
||||
@@ -562,7 +579,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
emul_on_create_swapchain(*swapchain, &fixed);
|
||||
snap_on_create(*swapchain, &fixed);
|
||||
}
|
||||
return result;
|
||||
return standin_after_failure(&fixed, swapchain, result);
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
|
||||
@@ -572,6 +589,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain swapchain) {
|
||||
static int logged;
|
||||
if (logged++ < 200) LOG("layer_debug: xrDestroySwapchain %p", (void *)swapchain);
|
||||
}
|
||||
if (is_standin(swapchain)) { standin_destroy(swapchain); return XR_SUCCESS; }
|
||||
forget_swapchain(swapchain);
|
||||
surf_on_destroy(swapchain);
|
||||
flip_on_destroy(swapchain);
|
||||
@@ -753,6 +771,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetSystemProperties(XrInstance instance, XrSyst
|
||||
}
|
||||
|
||||
#include "flip_vk.c"
|
||||
#include "pose_time.c"
|
||||
|
||||
// XR_KHR_convert_timespec_time for runtimes that lack it (Frame): XrTime is derived from the monotonic clock
|
||||
// using the offset measured at the last xrWaitFrame. The runtime's own implementation is preferred.
|
||||
@@ -762,10 +781,14 @@ static PFN_xrConvertTimeToTimespecTimeKHR runtime_time_to_timespec;
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL emu_timespec_to_time(XrInstance instance, const struct timespec *ts, XrTime *time) {
|
||||
if (runtime_timespec_to_time) {
|
||||
XrResult r = runtime_timespec_to_time(instance, ts, time);
|
||||
if (r != XR_ERROR_FUNCTION_UNSUPPORTED) return r;
|
||||
if (r != XR_ERROR_FUNCTION_UNSUPPORTED) {
|
||||
if (XR_SUCCEEDED(r) && time) pose_time_note("xrConvertTimespecTimeToTimeKHR (runtime)", 0, 0, *time);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
if (!ts || !time) return XR_ERROR_VALIDATION_FAILURE;
|
||||
*time = (XrTime)((int64_t)ts->tv_sec * 1000000000ll + ts->tv_nsec + (xr_time_calibrated ? xr_time_offset : 0));
|
||||
pose_time_note("xrConvertTimespecTimeToTimeKHR", 0, 0, *time);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL emu_time_to_timespec(XrInstance instance, XrTime time, struct timespec *ts) {
|
||||
@@ -781,6 +804,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL emu_time_to_timespec(XrInstance instance,
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpace(XrSpace space, XrSpace baseSpace, XrTime time, XrSpaceLocation *location) {
|
||||
time = pose_time_fixed(time);
|
||||
pose_time_note("xrLocateSpace", (uint64_t)(uintptr_t)space, (uint64_t)(uintptr_t)baseSpace, time);
|
||||
if (emulate_scene && location) {
|
||||
XrPosef pose; XrSpaceLocationFlags flags;
|
||||
if (locate_fake(space, baseSpace, time, &pose, &flags)) {
|
||||
@@ -796,6 +821,14 @@ XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpace(XrSpace space, XrSpace baseSpace, X
|
||||
static XrResult locate_spaces_common(const char *name, XrSession session, const XrSpacesLocateInfo *info,
|
||||
XrSpaceLocations *locations) {
|
||||
int any_fake = 0;
|
||||
XrSpacesLocateInfo moved;
|
||||
if (info && pose_time_fix) {
|
||||
moved = *info;
|
||||
moved.time = pose_time_fixed(info->time);
|
||||
info = &moved;
|
||||
}
|
||||
if (info) pose_time_note(name, info->spaceCount ? (uint64_t)(uintptr_t)info->spaces[0] : 0,
|
||||
(uint64_t)(uintptr_t)info->baseSpace, info->time);
|
||||
if (emulate_scene && info && locations) {
|
||||
any_fake = fake_space_index(info->baseSpace) >= 0;
|
||||
for (uint32_t i = 0; i < info->spaceCount && !any_fake; ++i) any_fake = fake_space_index(info->spaces[i]) >= 0;
|
||||
@@ -829,6 +862,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrDestroySpace(XrSpace space) {
|
||||
#include "input_diag.c"
|
||||
#include "session_fixes.c"
|
||||
#include "layer_emul_gl.c"
|
||||
#include "cube_standin.c"
|
||||
#include "snapshot_gl.c"
|
||||
#include "surface_swapchain.c"
|
||||
#include "eye_debug.c"
|
||||
@@ -938,6 +972,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
|
||||
XrSwapchainImageBaseHeader *images) {
|
||||
PFN_xrEnumerateSwapchainImages fn = (PFN_xrEnumerateSwapchainImages)lookup(active_instance, "xrEnumerateSwapchainImages");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (is_standin(swapchain)) return standin_enumerate(swapchain, capacity, count, images);
|
||||
XrResult result = equirect_emul && emul_is_virtual(swapchain) ? emul_virtual_enumerate(swapchain, capacity, count, images)
|
||||
: fn(swapchain, capacity, count, images);
|
||||
if (XR_SUCCEEDED(result) && images && capacity && count) {
|
||||
@@ -952,6 +987,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain,
|
||||
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;
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
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);
|
||||
@@ -961,6 +997,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain swapchain, const
|
||||
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;
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
__atomic_add_fetch(&sc_releases, 1, __ATOMIC_RELAXED);
|
||||
return fn(swapchain, info);
|
||||
}
|
||||
@@ -969,6 +1006,11 @@ XRAPI_ATTR XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain swapchain, co
|
||||
uint32_t *index) {
|
||||
PFN_xrAcquireSwapchainImage fn = (PFN_xrAcquireSwapchainImage)lookup(active_instance, "xrAcquireSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (is_standin(swapchain)) { // adapter-served cube stand-in: one image
|
||||
if (!index) return XR_ERROR_VALIDATION_FAILURE;
|
||||
*index = 0;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
if (equirect_emul && emul_is_virtual(swapchain)) { // adapter-served write-once picture: one image
|
||||
if (!index) return XR_ERROR_VALIDATION_FAILURE;
|
||||
*index = 0;
|
||||
@@ -1079,6 +1121,9 @@ XRAPI_ATTR XrResult XRAPI_CALL xrWaitFrame(XrSession session, const XrFrameWaitI
|
||||
// predictedDisplayTime is about one display period ahead of "now".
|
||||
xr_time_offset = (int64_t)(state->predictedDisplayTime - state->predictedDisplayPeriod) - mono;
|
||||
xr_time_calibrated = 1;
|
||||
last_display_period = state->predictedDisplayPeriod;
|
||||
pose_time_sample(xr_time_offset);
|
||||
pose_time_note_offset((long long)(state->predictedDisplayTime - mono));
|
||||
if (layer_debug) debug_aim_vs_grip(state->predictedDisplayTime);
|
||||
}
|
||||
return result;
|
||||
@@ -1143,6 +1188,13 @@ XRAPI_ATTR XrResult XRAPI_CALL xrLocateViews(XrSession session, const XrViewLoca
|
||||
uint32_t capacity, uint32_t *count, XrView *views) {
|
||||
PFN_xrLocateViews fn = (PFN_xrLocateViews)lookup(active_instance, "xrLocateViews");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrViewLocateInfo moved;
|
||||
if (info && pose_time_fix) {
|
||||
moved = *info;
|
||||
moved.displayTime = pose_time_fixed(info->displayTime);
|
||||
info = &moved;
|
||||
}
|
||||
if (info) pose_time_note("xrLocateViews", (uint64_t)(uintptr_t)info->space, 0, info->displayTime);
|
||||
int cacheable = pose_consistency && info && state && count && views && capacity > 0;
|
||||
if (cacheable && pose_cache_lookup(session, info, state, capacity, count, views)) return XR_SUCCESS;
|
||||
XrResult result = fn(session, info, state, capacity, count, views);
|
||||
@@ -1257,6 +1309,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
__atomic_exchange_n(&input_sync_ok, 0, __ATOMIC_RELAXED), input_last_sync_result,
|
||||
__atomic_exchange_n(&input_bool_reads, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&input_bool_true, 0, __ATOMIC_RELAXED));
|
||||
pose_time_report();
|
||||
frames = 0; drift_sum = drift_max = 0;
|
||||
}
|
||||
}
|
||||
@@ -1348,6 +1401,12 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInf
|
||||
++dropped; // an emulated video surface without a frame yet
|
||||
continue;
|
||||
}
|
||||
if (layer_uses_standin(layer)) { // a cube layer whose swapchain the runtime refused (cube_standin.c)
|
||||
static int logged;
|
||||
if (logged++ < 3) LOG("cube_standin: dropped a layer (type=%d) that shows a stand-in swapchain", layer->type);
|
||||
++dropped;
|
||||
continue;
|
||||
}
|
||||
if (!layer || (layer_fix && !layer_usable(layer))) { ++dropped; continue; }
|
||||
if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
|
||||
const XrCompositionLayerQuad *flipped = flip_quad(session, (const XrCompositionLayerQuad *)layer, &quads[count]);
|
||||
@@ -1738,14 +1797,27 @@ 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
|
||||
if (layer_debug || cube_standin) { // counters + cube stand-ins; every other wait/release hook above takes precedence
|
||||
HOOK(xrWaitSwapchainImage)
|
||||
HOOK(xrReleaseSwapchainImage)
|
||||
}
|
||||
if (cube_standin && (!strcmp(name, "xrUpdateSwapchainFB") || !strcmp(name, "xrGetSwapchainStateFB"))) {
|
||||
XrResult result = next_gipa(instance, name, function); // only wrapped where the runtime has it
|
||||
if (XR_FAILED(result) || !*function) return result;
|
||||
if (!strcmp(name, "xrUpdateSwapchainFB")) {
|
||||
real_update_swapchain = (PFN_xrUpdateSwapchainFB)*function;
|
||||
*function = (PFN_xrVoidFunction)standin_update_swapchain;
|
||||
} else {
|
||||
real_get_swapchain_state = (PFN_xrGetSwapchainStateFB)*function;
|
||||
*function = (PFN_xrVoidFunction)standin_get_swapchain_state;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (sync_guard || layer_debug || input_diag) HOOK_AS(xrSyncActions, hook_xrSyncActions)
|
||||
if (layer_debug) HOOK_AS(xrGetActionStateBoolean, hook_xrGetActionStateBoolean)
|
||||
if (layer_debug || aim_correction_on() || profile_remap || input_diag)
|
||||
if (layer_debug || aim_correction_on() || profile_remap || input_diag || proximity_emul)
|
||||
HOOK_AS(xrSuggestInteractionProfileBindings, hook_xrSuggestInteractionProfileBindings)
|
||||
if (proximity_emul) HOOK_AS(xrCreateAction, hook_xrCreateAction)
|
||||
if (layer_debug || aim_correction_on()) {
|
||||
HOOK_AS(xrCreateActionSpace, hook_xrCreateActionSpace)
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ static void emul_on_destroy_swapchain(XrSwapchain handle) {
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *info) {
|
||||
PFN_xrReleaseSwapchainImage fn = (PFN_xrReleaseSwapchainImage)lookup(active_instance, "xrReleaseSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
pthread_mutex_lock(&emul_lock);
|
||||
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
|
||||
int index = s && s->acquired_count ? (int)s->acquired[0] : -1;
|
||||
@@ -335,6 +336,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrReleaseSwapchainImage(XrSwapchain s
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageWaitInfo *info) {
|
||||
PFN_xrWaitSwapchainImage fn = (PFN_xrWaitSwapchainImage)lookup(active_instance, "xrWaitSwapchainImage");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (is_standin(swapchain)) return XR_SUCCESS;
|
||||
XrResult result = emul_is_virtual(swapchain) ? XR_SUCCESS : fn(swapchain, info);
|
||||
pthread_mutex_lock(&emul_lock);
|
||||
emul_source *s = emul_active() ? emul_find_source(swapchain, 0) : NULL;
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// Included by frame_adapter.c: the times games locate poses at (pose_time_fix, pose_debug).
|
||||
//
|
||||
// pose_debug (diagnostics): every ~5 s, per located space (xrLocateSpace/xrLocateSpaces) and for xrLocateViews, the
|
||||
// number of calls and the min/max of (requested time - the last xrWaitFrame's predictedDisplayTime); for
|
||||
// xrConvertTimespecTimeToTimeKHR the same for its answers, plus the spread of the XrTime-vs-CLOCK_MONOTONIC offset
|
||||
// measured at xrWaitFrame.
|
||||
|
||||
static XrDuration last_display_period;
|
||||
|
||||
#define PT_SLOTS 12
|
||||
typedef struct {
|
||||
const char *kind;
|
||||
uint64_t space, base;
|
||||
int count;
|
||||
long long min, max; // ns relative to the last predicted display time
|
||||
} pt_stat;
|
||||
static pt_stat pt_stats[PT_SLOTS];
|
||||
static int pt_overflow;
|
||||
static long long pt_offset_min, pt_offset_max;
|
||||
static int pt_offset_count;
|
||||
static pthread_mutex_t pt_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void pose_time_note(const char *kind, uint64_t space, uint64_t base, XrTime time) {
|
||||
if (!pose_debug || !last_predicted_time) return;
|
||||
long long d = (long long)(time - last_predicted_time);
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
int i = 0;
|
||||
while (i < PT_SLOTS && pt_stats[i].count &&
|
||||
!(pt_stats[i].kind == kind && pt_stats[i].space == space && pt_stats[i].base == base))
|
||||
++i;
|
||||
if (i == PT_SLOTS) {
|
||||
++pt_overflow;
|
||||
} else {
|
||||
pt_stat *s = &pt_stats[i];
|
||||
if (!s->count) s->kind = kind, s->space = space, s->base = base, s->min = s->max = d;
|
||||
if (d < s->min) s->min = d;
|
||||
if (d > s->max) s->max = d;
|
||||
++s->count;
|
||||
}
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
}
|
||||
|
||||
// The offset sampled at xrWaitFrame dips (by up to 2.5 s) whenever xrWaitFrame returns late after a hitch, which put
|
||||
// corrected times up to 2 s in the future (headless survey, 2026-10-09). The steady value is the largest recent sample:
|
||||
// pose_time_fix uses the maximum of the last PT_RING samples (~2 s at 72 Hz; the offset drifts only ~0.14 s per hour).
|
||||
#define PT_RING 144
|
||||
static int64_t pt_ring[PT_RING];
|
||||
static int pt_ring_n, pt_ring_i;
|
||||
|
||||
static void pose_time_sample(int64_t offset) { // XrTime "now" minus CLOCK_MONOTONIC, from xrWaitFrame
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
pt_ring[pt_ring_i] = offset;
|
||||
pt_ring_i = (pt_ring_i + 1) % PT_RING;
|
||||
if (pt_ring_n < PT_RING) ++pt_ring_n;
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
}
|
||||
|
||||
static int64_t pose_time_offset(void) {
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
int64_t best = pt_ring_n ? pt_ring[0] : 0;
|
||||
for (int i = 1; i < pt_ring_n; i++)
|
||||
if (pt_ring[i] > best) best = pt_ring[i];
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
return best;
|
||||
}
|
||||
|
||||
static void pose_time_note_offset(long long offset) {
|
||||
if (!pose_debug) return;
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
if (!pt_offset_count || offset < pt_offset_min) pt_offset_min = offset;
|
||||
if (!pt_offset_count || offset > pt_offset_max) pt_offset_max = offset;
|
||||
++pt_offset_count;
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
}
|
||||
|
||||
// pose_time_fix: Meta's OVRPlugin takes "now" from CLOCK_MONOTONIC and passes it on as an XrTime unchanged (no
|
||||
// conversion reaches FrameBridge: OVRPort's dispatcher answers XR_KHR_convert_timespec_time itself, 1:1, since the
|
||||
// Frame's runtime lacks it). On a Quest XrTime is the monotonic clock; on the Frame it runs ahead (2.56 s measured on
|
||||
// SteamOS 0.4.5, 0.05-0.9 s seen on older builds), so every "now" pose (Unity's physics step, OVRInput's controller
|
||||
// poses) was located that far in the past and the hands trailed the controllers (BattleSisters, Sniper Elite VR).
|
||||
// A located time nearer the monotonic clock than XrTime's "now" is taken as a monotonic timestamp and moved to the
|
||||
// same moment in XrTime (offset measured at xrWaitFrame); a time more than PT_PAST_LIMIT before the predicted display
|
||||
// time that isn't one (BattleSisters asks for its head at XrTime 0.1 s; Robo Recall, Phantom, Vader, Time Stall and
|
||||
// The Room VR at ~0 every frame) is located at the predicted display time, the time the game renders with.
|
||||
#define PT_PAST_LIMIT 500000000ll // 0.5 s: no runtime keeps a longer pose history; real past queries are far shorter
|
||||
#define PT_MIN_OFFSET 5000000ll // XrTime and the monotonic clock this close: nothing to tell apart or to fix
|
||||
static int pt_fixed_mono, pt_fixed_past;
|
||||
static long long pt_fixed_min;
|
||||
|
||||
static XrTime pose_time_fixed(XrTime time) {
|
||||
if (!pose_time_fix || !last_predicted_time || pt_ring_n < 8) return time; // a few frames of offset samples first
|
||||
int64_t offset = pose_time_offset(); // XrTime "now" minus the monotonic clock (robust, see above)
|
||||
if (offset < PT_MIN_OFFSET && offset > -PT_MIN_OFFSET) return time;
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
long long mono = (long long)ts.tv_sec * 1000000000ll + ts.tv_nsec;
|
||||
long long d_mono = (long long)time - mono, d_xr = (long long)time - (mono + offset);
|
||||
long long abs_mono = d_mono < 0 ? -d_mono : d_mono, abs_xr = d_xr < 0 ? -d_xr : d_xr;
|
||||
XrTime fixed;
|
||||
int kind;
|
||||
if (abs_mono < PT_PAST_LIMIT && abs_mono < abs_xr) {
|
||||
fixed = (XrTime)(time + offset), kind = 0; // a monotonic timestamp
|
||||
} else if ((long long)(time - last_predicted_time) < -PT_PAST_LIMIT) {
|
||||
fixed = last_predicted_time, kind = 1; // far in the past: the frame's display time
|
||||
} else {
|
||||
return time;
|
||||
}
|
||||
if (pose_debug) {
|
||||
long long d = (long long)(time - last_predicted_time);
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
if (!(pt_fixed_mono + pt_fixed_past) || d < pt_fixed_min) pt_fixed_min = d;
|
||||
++*(kind ? &pt_fixed_past : &pt_fixed_mono);
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
}
|
||||
static int logged[2];
|
||||
if (logged[kind]++ < 3)
|
||||
LOG("pose_time_fix: pose asked for at %.1f ms from the predicted display time (%s) located at %.1f ms",
|
||||
(long long)(time - last_predicted_time) / 1e6, kind ? "far in the past" : "a monotonic-clock time",
|
||||
(long long)(fixed - last_predicted_time) / 1e6);
|
||||
return fixed;
|
||||
}
|
||||
|
||||
// called from the pacing statistics in xrEndFrame
|
||||
static void pose_time_report(void) {
|
||||
if (!pose_debug) return;
|
||||
pthread_mutex_lock(&pt_lock);
|
||||
LOG("pose_debug: period %.2f ms; XrTime - monotonic at xrWaitFrame (%d): %.2f..%.2f ms", last_display_period / 1e6,
|
||||
pt_offset_count, pt_offset_min / 1e6, pt_offset_max / 1e6);
|
||||
for (int i = 0; i < PT_SLOTS && pt_stats[i].count; ++i)
|
||||
LOG("pose_debug: %s space=0x%llx base=0x%llx: %d calls, time - predicted display time %.2f..%.2f ms",
|
||||
pt_stats[i].kind, (unsigned long long)pt_stats[i].space, (unsigned long long)pt_stats[i].base,
|
||||
pt_stats[i].count, pt_stats[i].min / 1e6, pt_stats[i].max / 1e6);
|
||||
if (pt_overflow) LOG("pose_debug: %d calls not counted (more than %d spaces)", pt_overflow, PT_SLOTS);
|
||||
if (pt_fixed_mono + pt_fixed_past)
|
||||
LOG("pose_debug: pose_time_fix moved %d monotonic-clock times and %d far-past times (earliest asked %.2f ms)",
|
||||
pt_fixed_mono, pt_fixed_past, pt_fixed_min / 1e6);
|
||||
memset(pt_stats, 0, sizeof(pt_stats));
|
||||
pt_overflow = pt_offset_count = pt_fixed_mono = pt_fixed_past = 0;
|
||||
pthread_mutex_unlock(&pt_lock);
|
||||
}
|
||||
@@ -285,6 +285,90 @@ static int touch_has(const char *path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- finger proximity from touch (proximity_emul)
|
||||
// Meta's OVRPlugin reports a thumb or index finger resting near the controller through XR_FB_touch_controller_proximity
|
||||
// (.../thumb_fb/proximity_fb, .../trigger/proximity_fb). The Frame's runtime doesn't have it; OVRPort's loader offers
|
||||
// it to the game anyway, and the proximity actions never get a working binding: "near" stays false, so games that
|
||||
// animate the hands from it show a thumb that never moves (Unreal's ThumbUp axis is "no thumb proximity", e.g. Vader
|
||||
// Immortal; found by Klownicle, GitHub #49). With proximity_emul those actions are bound to the capacitive touch inputs
|
||||
// instead (OpenXR combines several bindings of one boolean action with OR): thumb = thumbstick, face buttons or thumb
|
||||
// rest touched; index (proximity_emul=2) = trigger touched. Only when the runtime lacks the extension itself.
|
||||
static XrAction thumb_proximity_action, trigger_proximity_action;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrCreateAction(XrActionSet set, const XrActionCreateInfo *info,
|
||||
XrAction *action) {
|
||||
PFN_xrCreateAction fn = (PFN_xrCreateAction)lookup(active_instance, "xrCreateAction");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrResult r = fn(set, info, action);
|
||||
if (XR_SUCCEEDED(r) && info && action) { // OVRPlugin's action names
|
||||
if (!strcmp(info->actionName, "hand_thumb_proximity")) thumb_proximity_action = *action;
|
||||
else if (!strcmp(info->actionName, "hand_trigger_proximity")) trigger_proximity_action = *action;
|
||||
else return r;
|
||||
LOG("proximity_emul: game action %s found", info->actionName);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static const char *const THUMB_TOUCH_LEFT[] = {"x/touch", "y/touch", "thumbstick/touch", "thumbrest/touch"};
|
||||
static const char *const THUMB_TOUCH_RIGHT[] = {"a/touch", "b/touch", "thumbstick/touch", "thumbrest/touch"};
|
||||
|
||||
static int add_binding(XrInstance instance, PFN_xrStringToPath to_path, XrActionSuggestedBinding *b, uint32_t *n,
|
||||
XrAction action, const char *hand, const char *input) {
|
||||
char path[XR_MAX_PATH_LENGTH];
|
||||
snprintf(path, sizeof(path), "/user/hand/%s/input/%s", hand, input);
|
||||
XrPath p;
|
||||
if (XR_FAILED(to_path(instance, path, &p))) return 0;
|
||||
for (uint32_t i = 0; i < *n; ++i)
|
||||
if (b[i].action == action && b[i].binding == p) return 0;
|
||||
b[*n].action = action;
|
||||
b[(*n)++].binding = p;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Suggests Touch bindings with finger proximity bound to the touch inputs (proximity_emul), falling back to the
|
||||
// bindings as given when the runtime refuses them.
|
||||
static XrResult suggest_touch(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn,
|
||||
PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested) {
|
||||
PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(instance, "xrStringToPath");
|
||||
if (!proximity_emul || runtime_has_proximity || !to_path || !to_string) return fn(instance, suggested);
|
||||
XrAction thumb = thumb_proximity_action, index = proximity_emul > 1 ? trigger_proximity_action : XR_NULL_HANDLE;
|
||||
uint32_t n = suggested->countSuggestedBindings, kept = 0, dropped = 0, added = 0;
|
||||
XrActionSuggestedBinding *b = calloc(n + 2 * 5, sizeof(*b));
|
||||
if (!b) return fn(instance, suggested);
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
char path[XR_MAX_PATH_LENGTH];
|
||||
uint32_t size = 0;
|
||||
const XrActionSuggestedBinding *s = &suggested->suggestedBindings[i];
|
||||
if (XR_SUCCEEDED(to_string(instance, s->binding, sizeof(path), &size, path)) && strstr(path, "/proximity_")) {
|
||||
// the game's own proximity binding, which the runtime doesn't know: its action is bound to touch below
|
||||
if (strstr(path, "/thumb")) { if (!thumb) thumb = s->action; }
|
||||
else if (proximity_emul > 1 && !index) index = s->action;
|
||||
++dropped;
|
||||
continue;
|
||||
}
|
||||
b[kept++] = *s;
|
||||
}
|
||||
for (int hand = 0; hand < 2; ++hand) {
|
||||
const char *name = hand ? "right" : "left";
|
||||
const char *const *thumb_inputs = hand ? THUMB_TOUCH_RIGHT : THUMB_TOUCH_LEFT;
|
||||
for (int c = 0; thumb && c < 4; ++c) added += add_binding(instance, to_path, b, &kept, thumb, name, thumb_inputs[c]);
|
||||
if (index) added += add_binding(instance, to_path, b, &kept, index, name, "trigger/touch");
|
||||
}
|
||||
XrResult r;
|
||||
if (!added && !dropped) r = fn(instance, suggested);
|
||||
else {
|
||||
XrInteractionProfileSuggestedBinding copy = *suggested;
|
||||
copy.countSuggestedBindings = kept;
|
||||
copy.suggestedBindings = b;
|
||||
r = fn(instance, ©);
|
||||
LOG("proximity_emul: finger proximity bound to touch (%u binding(s) added, %u proximity path(s) replaced): %d",
|
||||
added, dropped, r);
|
||||
if (XR_FAILED(r)) r = fn(instance, suggested);
|
||||
}
|
||||
free(b);
|
||||
return r;
|
||||
}
|
||||
|
||||
static XrResult remap_to_touch(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn,
|
||||
PFN_xrPathToString to_string, const XrInteractionProfileSuggestedBinding *suggested,
|
||||
const char *profile) {
|
||||
@@ -305,7 +389,7 @@ static XrResult remap_to_touch(XrInstance instance, PFN_xrSuggestInteractionProf
|
||||
copy.interactionProfile = touch;
|
||||
copy.countSuggestedBindings = kept;
|
||||
copy.suggestedBindings = b;
|
||||
XrResult r = kept ? fn(instance, ©) : XR_ERROR_PATH_UNSUPPORTED;
|
||||
XrResult r = kept ? suggest_touch(instance, fn, to_string, ©) : XR_ERROR_PATH_UNSUPPORTED;
|
||||
free(b);
|
||||
LOG("controller profile %s -> oculus/touch_controller (%u bindings, %u dropped): %d", profile + 22, kept,
|
||||
n - kept, r);
|
||||
@@ -325,12 +409,14 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrSuggestInteractionProfileBindings(X
|
||||
if (XR_SUCCEEDED(to_string(instance, suggested->suggestedBindings[i].binding, sizeof(path), &size, path)))
|
||||
note_pose_binding(suggested->suggestedBindings[i].action, path);
|
||||
}
|
||||
XrResult r = fn(instance, suggested);
|
||||
input_diag_suggested(instance, suggested, r, to_string); // the runtime's answer to the game's own suggestion
|
||||
if (!profile_remap || !suggested || !to_string) return r;
|
||||
char profile[XR_MAX_PATH_LENGTH] = {0};
|
||||
uint32_t size = 0;
|
||||
if (XR_FAILED(to_string(instance, suggested->interactionProfile, sizeof(profile), &size, profile))) return r;
|
||||
int known = suggested && to_string &&
|
||||
XR_SUCCEEDED(to_string(instance, suggested->interactionProfile, sizeof(profile), &size, profile));
|
||||
XrResult r = known && !strcmp(profile, TOUCH_PROFILE) ? suggest_touch(instance, fn, to_string, suggested)
|
||||
: fn(instance, suggested);
|
||||
input_diag_suggested(instance, suggested, r, to_string); // the runtime's answer to the game's own suggestion
|
||||
if (!profile_remap || !known) return r;
|
||||
if (!strcmp(profile, TOUCH_PROFILE)) {
|
||||
if (XR_SUCCEEDED(r)) app_suggested_touch = 1; // the app's own Touch bindings win over a remap
|
||||
return r;
|
||||
|
||||
+12
-3
@@ -10,7 +10,7 @@ timefix OpenXR layer for Proton games (linux-arm64, glibc; the NDK's clang build
|
||||
OculusHMDConnected helper for Rift games under Proton (win-x64 PE; the NDK's clang + lld-link, no Windows SDK), the
|
||||
live view's hardware H.264 encoder fp_venc (linux-arm64-bin, static freestanding executable), and rewrites artifacts/SHA256SUMS. Run `frameport parity` afterwards to see which games change.
|
||||
|
||||
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,ovrpshim,vrsettings,venc] [--ndk PATH]
|
||||
python native/build.py [--only adapter,bridge,compat,langpack,glshim,eglfmt,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,ovrpshim,vrsettings,venc] [--ndk PATH]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -300,6 +300,14 @@ def build_vkshim(tc: Path):
|
||||
cwd=src)
|
||||
|
||||
|
||||
def build_zinkfix(tc: Path):
|
||||
"""Shader-fix Vulkan layer for OpenGL ES games (see zinkfix/zinkfix.c): between Zink and the driver."""
|
||||
src = HERE / "zinkfix"
|
||||
run([exe(tc, "aarch64-linux-android29-clang"), "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
"-fvisibility=hidden", "-Wl,-soname,libVkLayer_fp_shaderfix.so", "-Wl,-z,max-page-size=16384", "zinkfix.c",
|
||||
"-ldl", "-llog", "-o", ART / "arm64-v8a/libVkLayer_fp_shaderfix.so"], cwd=src)
|
||||
|
||||
|
||||
def build_ovrpshim(tc: Path):
|
||||
"""OVRPlugin frame-loop shim (see ovrpshim/ovrpshim.c) for Unity 2017-2018 built-in Oculus support."""
|
||||
src = HERE / "ovrpshim"
|
||||
@@ -352,7 +360,7 @@ def write_sums():
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,ovrpshim,vrsettings,venc")
|
||||
ap.add_argument("--only", default="adapter,bridge,compat,langpack,glshim,eglfmt,ovrtrace,dex,xrlayer,oculushmd,xrshim,vkshim,zinkfix,ovrpshim,vrsettings,venc")
|
||||
ap.add_argument("--ndk")
|
||||
args = ap.parse_args()
|
||||
parts = set(args.only.split(","))
|
||||
@@ -363,9 +371,10 @@ def main():
|
||||
"langpack": lambda: build_langpack(tc),
|
||||
"glshim": lambda: build_glshim(tc), "eglfmt": lambda: build_eglfmt(tc), "ovrtrace": lambda: build_ovrtrace(tc), "xrshim": lambda: build_xrshim(tc), "dex": build_dex, "xrlayer": lambda: build_xrlayer(tc),
|
||||
"oculushmd": lambda: build_oculushmd(tc), "vkshim": lambda: build_vkshim(tc),
|
||||
"zinkfix": lambda: build_zinkfix(tc),
|
||||
"ovrpshim": lambda: build_ovrpshim(tc),
|
||||
"vrsettings": lambda: build_vrsettings(tc), "venc": lambda: build_venc(tc)}
|
||||
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "ovrtrace", "xrshim", "vkshim", "ovrpshim", "dex", "xrlayer", "oculushmd", "vrsettings", "venc"):
|
||||
for name in ("adapter", "bridge", "compat", "langpack", "glshim", "eglfmt", "ovrtrace", "xrshim", "vkshim", "zinkfix", "ovrpshim", "dex", "xrlayer", "oculushmd", "vrsettings", "venc"):
|
||||
if name in parts:
|
||||
log(f"build {name}")
|
||||
steps[name]()
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// FramePort shader-fix Vulkan layer for OpenGL ES games (libVkLayer_fp_shaderfix.so, layer VK_LAYER_FP_shader_fix).
|
||||
//
|
||||
// On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan): the game's GLSL becomes SPIR-V inside the driver, which
|
||||
// the Vulkan shim (vkshim, only in front of an engine's own Vulkan) never sees. Some of those modules read locals
|
||||
// before writing them (e.g. two lightspeed shaders of Vader Immortal: loop counters and accumulators undefined, the
|
||||
// GPU hangs; captured and fixed by Klownicle, GitHub #49). This layer sits between Zink and the driver and inserts
|
||||
// words into modules whose size and SHA-256 match a fix, exactly like vkshim's vk_shader_fix; every other module
|
||||
// passes through unchanged.
|
||||
// Settings (same sources as FrameBridge: libframe_settings.so next to this library, the game's framebridge.conf,
|
||||
// FRAMEBRIDGE_CONFIG):
|
||||
// zink_shader_fix=<size>:<sha256 hex>:<byte offset>:<word>,<word>,...[;<next fix>]
|
||||
// zink_shader_dump=1 writes every distinct module to Android/data/<pkg>/files/fp_spirv/<size>_<sha>.spv (capture
|
||||
// new variants after a driver update; diagnostics)
|
||||
//
|
||||
// Activation: Android's Vulkan loader only loads the layers GraphicsEnv names (Lepton fills that list from its own
|
||||
// layer set for the running app), from the app's native library directory among others. The engine library loads this
|
||||
// library (DT_NEEDED) before it starts EGL; its constructor puts this layer in front of GraphicsEnv's debug layer list
|
||||
// for this process only, through GraphicsEnv's (private, Android 11) functions found via libvulkan.so's dependencies.
|
||||
// Missing functions are logged and nothing changes.
|
||||
#define _GNU_SOURCE
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vulkan/vk_layer.h>
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define TAG "FrameBridge"
|
||||
#define EXPORT __attribute__((visibility("default")))
|
||||
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LAYER_NAME "VK_LAYER_FP_shader_fix"
|
||||
|
||||
// ---------------------------------------------------------------- settings
|
||||
|
||||
#define MAX_FIXES 16
|
||||
#define MAX_WORDS 64
|
||||
static struct {
|
||||
size_t size, offset;
|
||||
uint8_t sha[32];
|
||||
uint32_t words[MAX_WORDS];
|
||||
int nwords, applied;
|
||||
} fixes[MAX_FIXES];
|
||||
static int nfixes, dump;
|
||||
static char process[256];
|
||||
|
||||
static int hexval(char c) {
|
||||
return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1;
|
||||
}
|
||||
|
||||
static void parse_fixes(char *v) {
|
||||
for (char *save = NULL, *item = strtok_r(v, ";", &save); item && nfixes < MAX_FIXES; item = strtok_r(NULL, ";", &save)) {
|
||||
char *f[4] = {0}, *save2 = NULL;
|
||||
int n = 0;
|
||||
for (char *t = strtok_r(item, ":", &save2); t && n < 4; t = strtok_r(NULL, ":", &save2)) f[n++] = t;
|
||||
if (n != 4 || strlen(f[1]) != 64) continue;
|
||||
int ok = 1;
|
||||
for (int i = 0; i < 32 && ok; i++) {
|
||||
int hi = hexval(f[1][2 * i]), lo = hexval(f[1][2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) ok = 0; else fixes[nfixes].sha[i] = (uint8_t)(hi << 4 | lo);
|
||||
}
|
||||
fixes[nfixes].size = strtoul(f[0], NULL, 0);
|
||||
fixes[nfixes].offset = strtoul(f[2], NULL, 0);
|
||||
int nw = 0;
|
||||
for (char *save3 = NULL, *w = strtok_r(f[3], ",", &save3); w && nw < MAX_WORDS; w = strtok_r(NULL, ",", &save3))
|
||||
fixes[nfixes].words[nw++] = (uint32_t)strtoul(w, NULL, 0);
|
||||
fixes[nfixes].nwords = nw;
|
||||
fixes[nfixes].applied = 0;
|
||||
if (ok && nw && fixes[nfixes].size % 4 == 0 && fixes[nfixes].offset % 4 == 0 &&
|
||||
fixes[nfixes].offset >= 20 && fixes[nfixes].offset <= fixes[nfixes].size)
|
||||
nfixes++;
|
||||
}
|
||||
}
|
||||
|
||||
static void read_settings(const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return;
|
||||
char line[8192];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
if (!strncmp(line, "zink_shader_dump=", 17)) dump = atoi(line + 17) != 0;
|
||||
if (!strncmp(line, "zink_shader_fix=", 16)) {
|
||||
nfixes = 0; // later sources override earlier ones (as for the adapter's settings)
|
||||
parse_fixes(line + 16);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void read_all_settings(void) {
|
||||
char path[600];
|
||||
Dl_info info;
|
||||
if (dladdr((void *)read_all_settings, &info) && info.dli_fname) {
|
||||
const char *slash = strrchr(info.dli_fname, '/');
|
||||
if (slash && slash - info.dli_fname < 500) {
|
||||
snprintf(path, sizeof path, "%.*s/libframe_settings.so", (int)(slash - info.dli_fname), info.dli_fname);
|
||||
read_settings(path);
|
||||
}
|
||||
}
|
||||
FILE *f = fopen("/proc/self/cmdline", "r");
|
||||
if (f) {
|
||||
size_t n = fread(process, 1, sizeof process - 1, f);
|
||||
process[n] = 0;
|
||||
fclose(f);
|
||||
}
|
||||
char *colon = strchr(process, ':');
|
||||
if (colon) *colon = 0;
|
||||
if (*process && !strchr(process, '/')) {
|
||||
snprintf(path, sizeof path, "/sdcard/Android/data/%s/files/framebridge.conf", process);
|
||||
read_settings(path);
|
||||
}
|
||||
const char *env = getenv("FRAMEBRIDGE_CONFIG");
|
||||
if (env && *env) read_settings(env);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- SHA-256 (FIPS 180-4)
|
||||
|
||||
static const uint32_t K[64] = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01,
|
||||
0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
||||
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08,
|
||||
0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||||
#define ROR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||
|
||||
static void sha256_block(uint32_t h[8], const uint8_t *p) {
|
||||
uint32_t w[64];
|
||||
for (int i = 0; i < 16; i++) w[i] = (uint32_t)p[4 * i] << 24 | (uint32_t)p[4 * i + 1] << 16 | (uint32_t)p[4 * i + 2] << 8 | p[4 * i + 3];
|
||||
for (int i = 16; i < 64; i++) {
|
||||
uint32_t s0 = ROR(w[i - 15], 7) ^ ROR(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||||
uint32_t s1 = ROR(w[i - 2], 17) ^ ROR(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||
}
|
||||
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
|
||||
for (int i = 0; i < 64; i++) {
|
||||
uint32_t t1 = hh + (ROR(e, 6) ^ ROR(e, 11) ^ ROR(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
|
||||
uint32_t t2 = (ROR(a, 2) ^ ROR(a, 13) ^ ROR(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
|
||||
hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
|
||||
}
|
||||
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
|
||||
}
|
||||
|
||||
static void sha256(const uint8_t *data, size_t len, uint8_t out[32]) {
|
||||
uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||||
size_t i = 0;
|
||||
for (; i + 64 <= len; i += 64) sha256_block(h, data + i);
|
||||
uint8_t tail[128] = {0};
|
||||
size_t rest = len - i;
|
||||
memcpy(tail, data + i, rest);
|
||||
tail[rest] = 0x80;
|
||||
size_t blocks = rest + 9 > 64 ? 2 : 1;
|
||||
uint64_t bits = (uint64_t)len * 8;
|
||||
for (int b = 0; b < 8; b++) tail[blocks * 64 - 1 - b] = (uint8_t)(bits >> (8 * b));
|
||||
for (size_t b = 0; b < blocks; b++) sha256_block(h, tail + 64 * b);
|
||||
for (int b = 0; b < 8; b++) {
|
||||
out[4 * b] = h[b] >> 24; out[4 * b + 1] = h[b] >> 16; out[4 * b + 2] = h[b] >> 8; out[4 * b + 3] = h[b];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the fix and the dump
|
||||
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define MAX_DUMPED 4096
|
||||
static uint8_t dumped[MAX_DUMPED][8];
|
||||
static int ndumped, dump_dir_ok = -1;
|
||||
|
||||
static void dump_module(const uint32_t *code, size_t size, const uint8_t digest[32]) {
|
||||
pthread_mutex_lock(&lock);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < ndumped && !seen; i++) seen = !memcmp(dumped[i], digest, 8);
|
||||
if (!seen && ndumped < MAX_DUMPED) memcpy(dumped[ndumped++], digest, 8);
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (seen || !*process || strchr(process, '/')) return;
|
||||
char path[512];
|
||||
snprintf(path, sizeof path, "/sdcard/Android/data/%s/files/fp_spirv", process);
|
||||
if (dump_dir_ok < 0) {
|
||||
dump_dir_ok = mkdir(path, 0775) == 0 || errno == EEXIST;
|
||||
LOG("shader fix layer: dumping SPIR-V modules to %s (%s)", path, dump_dir_ok ? "ok" : strerror(errno));
|
||||
}
|
||||
if (!dump_dir_ok) return;
|
||||
char hex[65];
|
||||
for (int i = 0; i < 32; i++) snprintf(hex + 2 * i, 3, "%02x", digest[i]);
|
||||
snprintf(path + strlen(path), sizeof path - strlen(path), "/%zu_%s.spv", size, hex);
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
fwrite(code, 1, size, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// The fixed copy of `code` (caller frees) and its size, or NULL when no fix matches.
|
||||
static uint32_t *fixed_shader(const uint32_t *code, size_t size, size_t *out_size) {
|
||||
uint8_t digest[32];
|
||||
int hashed = 0;
|
||||
if (dump) {
|
||||
sha256((const uint8_t *)code, size, digest);
|
||||
hashed = 1;
|
||||
dump_module(code, size, digest);
|
||||
}
|
||||
for (int i = 0; i < nfixes; i++) {
|
||||
if (fixes[i].size != size) continue;
|
||||
if (!hashed) sha256((const uint8_t *)code, size, digest), hashed = 1;
|
||||
if (memcmp(digest, fixes[i].sha, 32)) {
|
||||
static int near_logged;
|
||||
if (near_logged++ < 8) LOG("shader fix layer: a %zu-byte module differs from fix %d (another build?)", size, i + 1);
|
||||
continue;
|
||||
}
|
||||
size_t extra = (size_t)fixes[i].nwords * 4;
|
||||
uint32_t *copy = malloc(size + extra);
|
||||
if (!copy) return NULL;
|
||||
memcpy(copy, code, fixes[i].offset);
|
||||
memcpy((uint8_t *)copy + fixes[i].offset, fixes[i].words, extra);
|
||||
memcpy((uint8_t *)copy + fixes[i].offset + extra, (const uint8_t *)code + fixes[i].offset, size - fixes[i].offset);
|
||||
*out_size = size + extra;
|
||||
if (fixes[i].applied++ < 3)
|
||||
LOG("shader fix layer: fixed shader module %d (%zu -> %zu bytes)", i + 1, size, size + extra);
|
||||
return copy;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- dispatch (per instance / device, by dispatch key)
|
||||
|
||||
#define MAX_HANDLES 16
|
||||
static struct { void *key; VkInstance instance; PFN_vkGetInstanceProcAddr gipa; } instances[MAX_HANDLES];
|
||||
static struct { void *key; PFN_vkGetDeviceProcAddr gdpa; PFN_vkCreateShaderModule create_shader; } devices[MAX_HANDLES];
|
||||
|
||||
static void *key_of(const void *handle) { return handle ? *(void *const *)handle : NULL; }
|
||||
|
||||
static PFN_vkGetInstanceProcAddr instance_gipa(const void *handle, VkInstance *instance) {
|
||||
void *key = key_of(handle);
|
||||
PFN_vkGetInstanceProcAddr found = NULL, last = NULL;
|
||||
pthread_mutex_lock(&lock);
|
||||
for (int i = 0; i < MAX_HANDLES; i++) {
|
||||
if (!instances[i].gipa) continue;
|
||||
last = instances[i].gipa;
|
||||
if (instance) *instance = instances[i].instance;
|
||||
if (instances[i].key == key) { found = instances[i].gipa; break; }
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
return found ? found : last; // a physical device: its instance (or the only one there is)
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *a,
|
||||
VkInstance *instance) {
|
||||
VkLayerInstanceCreateInfo *link = (VkLayerInstanceCreateInfo *)ci->pNext;
|
||||
while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO))
|
||||
link = (VkLayerInstanceCreateInfo *)link->pNext;
|
||||
if (!link || !link->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED;
|
||||
PFN_vkGetInstanceProcAddr next = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
|
||||
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
|
||||
PFN_vkCreateInstance create = (PFN_vkCreateInstance)next(VK_NULL_HANDLE, "vkCreateInstance");
|
||||
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
|
||||
VkResult r = create(ci, a, instance);
|
||||
if (r != VK_SUCCESS) return r;
|
||||
pthread_mutex_lock(&lock);
|
||||
int slot = 0;
|
||||
for (int i = 0; i < MAX_HANDLES; i++)
|
||||
if (!instances[i].gipa || instances[i].key == key_of(*instance)) { slot = i; break; }
|
||||
instances[slot].key = key_of(*instance);
|
||||
instances[slot].instance = *instance;
|
||||
instances[slot].gipa = next;
|
||||
pthread_mutex_unlock(&lock);
|
||||
static int logged;
|
||||
if (logged++ < 4) LOG("shader fix layer: active on instance %p (%d fix(es))", (void *)*instance, nfixes);
|
||||
return r;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL layer_CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci,
|
||||
const VkAllocationCallbacks *a, VkDevice *device) {
|
||||
VkLayerDeviceCreateInfo *link = (VkLayerDeviceCreateInfo *)ci->pNext;
|
||||
while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO))
|
||||
link = (VkLayerDeviceCreateInfo *)link->pNext;
|
||||
if (!link || !link->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED;
|
||||
PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
|
||||
PFN_vkGetDeviceProcAddr next_gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
|
||||
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
instance_gipa(pd, &instance);
|
||||
PFN_vkCreateDevice create = (PFN_vkCreateDevice)next_gipa(instance, "vkCreateDevice");
|
||||
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
|
||||
VkResult r = create(pd, ci, a, device);
|
||||
if (r != VK_SUCCESS) return r;
|
||||
pthread_mutex_lock(&lock);
|
||||
int slot = 0;
|
||||
for (int i = 0; i < MAX_HANDLES; i++)
|
||||
if (!devices[i].gdpa || devices[i].key == key_of(*device)) { slot = i; break; }
|
||||
devices[slot].key = key_of(*device);
|
||||
devices[slot].gdpa = next_gdpa;
|
||||
devices[slot].create_shader = (PFN_vkCreateShaderModule)next_gdpa(*device, "vkCreateShaderModule");
|
||||
pthread_mutex_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void *device_entry(VkDevice device, int shader) {
|
||||
void *key = key_of(device), *fn = NULL;
|
||||
pthread_mutex_lock(&lock);
|
||||
for (int i = 0; i < MAX_HANDLES && !fn; i++)
|
||||
if (devices[i].gdpa && devices[i].key == key) fn = shader ? (void *)devices[i].create_shader : (void *)devices[i].gdpa;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return fn;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL layer_CreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo *ci,
|
||||
const VkAllocationCallbacks *a, VkShaderModule *module) {
|
||||
PFN_vkCreateShaderModule next = (PFN_vkCreateShaderModule)device_entry(device, 1);
|
||||
if (!next) return VK_ERROR_INITIALIZATION_FAILED;
|
||||
size_t size = 0;
|
||||
uint32_t *copy = ci && ci->pCode && ci->codeSize >= 20 ? fixed_shader(ci->pCode, ci->codeSize, &size) : NULL;
|
||||
if (!copy) return next(device, ci, a, module);
|
||||
VkShaderModuleCreateInfo fixed = *ci;
|
||||
fixed.codeSize = size;
|
||||
fixed.pCode = copy;
|
||||
VkResult r = next(device, &fixed, a, module);
|
||||
free(copy);
|
||||
return r;
|
||||
}
|
||||
|
||||
static PFN_vkVoidFunction own(const char *name);
|
||||
|
||||
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_GetDeviceProcAddr(VkDevice device, const char *name) {
|
||||
PFN_vkVoidFunction fn = name && (!strcmp(name, "vkCreateShaderModule") || !strcmp(name, "vkGetDeviceProcAddr"))
|
||||
? own(name) : NULL;
|
||||
if (fn) return fn;
|
||||
PFN_vkGetDeviceProcAddr next = (PFN_vkGetDeviceProcAddr)device_entry(device, 0);
|
||||
return next ? next(device, name) : NULL;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_GetInstanceProcAddr(VkInstance instance, const char *name) {
|
||||
PFN_vkVoidFunction fn = own(name);
|
||||
if (fn) return fn;
|
||||
PFN_vkGetInstanceProcAddr next = instance ? instance_gipa(instance, NULL) : NULL;
|
||||
return next ? next(instance, name) : NULL;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- layer enumeration (what the loader asks for)
|
||||
|
||||
static const VkLayerProperties properties = {LAYER_NAME, VK_MAKE_VERSION(1, 1, 0), 1, "FramePort shader fixes"};
|
||||
|
||||
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceLayerProperties(uint32_t *count, VkLayerProperties *props) {
|
||||
if (!props) { *count = 1; return VK_SUCCESS; }
|
||||
if (*count < 1) return VK_INCOMPLETE;
|
||||
props[0] = properties;
|
||||
*count = 1;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(const char *layer, uint32_t *count,
|
||||
VkExtensionProperties *props) {
|
||||
(void)props;
|
||||
if (!layer || strcmp(layer, LAYER_NAME)) return VK_ERROR_LAYER_NOT_PRESENT;
|
||||
*count = 0;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceLayerProperties(VkPhysicalDevice pd, uint32_t *count,
|
||||
VkLayerProperties *props) {
|
||||
(void)pd;
|
||||
return vkEnumerateInstanceLayerProperties(count, props);
|
||||
}
|
||||
|
||||
EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceExtensionProperties(VkPhysicalDevice pd, const char *layer,
|
||||
uint32_t *count, VkExtensionProperties *props) {
|
||||
(void)pd;
|
||||
return vkEnumerateInstanceExtensionProperties(layer, count, props);
|
||||
}
|
||||
|
||||
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL VK_LAYER_FP_shader_fixGetInstanceProcAddr(VkInstance i, const char *n) {
|
||||
return layer_GetInstanceProcAddr(i, n);
|
||||
}
|
||||
|
||||
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL VK_LAYER_FP_shader_fixGetDeviceProcAddr(VkDevice d, const char *n) {
|
||||
return layer_GetDeviceProcAddr(d, n);
|
||||
}
|
||||
|
||||
static PFN_vkVoidFunction own(const char *name) {
|
||||
if (!name) return NULL;
|
||||
#define OWN(fn, impl) if (!strcmp(name, fn)) return (PFN_vkVoidFunction)impl;
|
||||
OWN("vkGetInstanceProcAddr", layer_GetInstanceProcAddr)
|
||||
OWN("vkGetDeviceProcAddr", layer_GetDeviceProcAddr)
|
||||
OWN("vkCreateInstance", layer_CreateInstance)
|
||||
OWN("vkCreateDevice", layer_CreateDevice)
|
||||
OWN("vkCreateShaderModule", layer_CreateShaderModule)
|
||||
OWN("vkEnumerateInstanceLayerProperties", vkEnumerateInstanceLayerProperties)
|
||||
OWN("vkEnumerateInstanceExtensionProperties", vkEnumerateInstanceExtensionProperties)
|
||||
OWN("vkEnumerateDeviceLayerProperties", vkEnumerateDeviceLayerProperties)
|
||||
#undef OWN
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- activation (GraphicsEnv debug layers)
|
||||
|
||||
// libc++ std::string as Android 11's platform libc++ lays it out (little endian, short-string flag in bit 0)
|
||||
struct cxx_string { size_t cap; size_t size; char *data; };
|
||||
|
||||
static const char *cxx_chars(const void *s, size_t *len) {
|
||||
const unsigned char *b = s;
|
||||
if (b[0] & 1) {
|
||||
const struct cxx_string *l = s;
|
||||
*len = l->size;
|
||||
return l->data;
|
||||
}
|
||||
*len = b[0] >> 1;
|
||||
return (const char *)b + 1;
|
||||
}
|
||||
|
||||
#define GET_INSTANCE "_ZN7android11GraphicsEnv11getInstanceEv"
|
||||
#define GET_LAYERS "_ZN7android11GraphicsEnv14getDebugLayersEv"
|
||||
#define SET_LAYERS "_ZN7android11GraphicsEnv14setDebugLayersENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"
|
||||
|
||||
__attribute__((constructor)) static void activate(void) {
|
||||
read_all_settings();
|
||||
if (!nfixes && !dump) return;
|
||||
void *vk = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
|
||||
void *(*get_instance)(void) = vk ? (void *(*)(void))dlsym(vk, GET_INSTANCE) : NULL;
|
||||
const void *(*get_layers)(void *) = vk ? (const void *(*)(void *))dlsym(vk, GET_LAYERS) : NULL;
|
||||
void (*set_layers)(void *, struct cxx_string *) = vk ? (void (*)(void *, struct cxx_string *))dlsym(vk, SET_LAYERS) : NULL;
|
||||
if (!get_instance || !get_layers || !set_layers) {
|
||||
LOG("shader fix layer: NOT active, Android's GraphicsEnv functions not found (%s%s%s)", get_instance ? "" : "getInstance ",
|
||||
get_layers ? "" : "getDebugLayers ", set_layers ? "" : "setDebugLayers");
|
||||
return;
|
||||
}
|
||||
void *env = get_instance();
|
||||
size_t len = 0;
|
||||
const char *current = env ? cxx_chars(get_layers(env), &len) : NULL;
|
||||
if (!env || (len && !current) || len > 4096) {
|
||||
LOG("shader fix layer: NOT active, unexpected GraphicsEnv state");
|
||||
return;
|
||||
}
|
||||
char old[4097];
|
||||
memcpy(old, current ? current : "", len);
|
||||
old[len] = 0;
|
||||
if (strstr(old, LAYER_NAME)) return; // loaded twice (the loader's own dlopen): already in the list
|
||||
size_t n = strlen(LAYER_NAME) + (len ? 1 + len : 0);
|
||||
size_t cap = (n + 16) & ~(size_t)15;
|
||||
char *data = malloc(cap);
|
||||
if (!data) return;
|
||||
snprintf(data, cap, "%s%s%s", LAYER_NAME, len ? ":" : "", old);
|
||||
// passed by value: the callee gets a pointer to our temporary and copies it; freed below unless it was moved
|
||||
struct cxx_string tmp = {cap | 1, n, data};
|
||||
set_layers(env, &tmp);
|
||||
if ((tmp.cap & 1) && tmp.data == data) free(data);
|
||||
const char *now = cxx_chars(get_layers(env), &len);
|
||||
LOG("shader fix layer: debug layers now \"%.*s\" (%d fix(es)%s)", (int)len, now ? now : "", nfixes,
|
||||
dump ? ", dumping modules" : "");
|
||||
}
|
||||
@@ -23,7 +23,9 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
|
||||
# 1: sdl_java, min_sdk, web_wrapper, expects_obb, vr_activity, unity_version (2026-10)
|
||||
# 2: unity_split (a Unity split-binary build expects an OBB too: expects_obb) (2026-10)
|
||||
# 3: unreal_ovrp_lookups (the OVRPlugin functions Unreal's Oculus module looks up: frame.unreal_ovrp_entrypoints)
|
||||
ANALYSIS_VERSION = 3
|
||||
# 4: vivox_api31 (Vivox's audio routing calls Android 12 AudioManager methods: frame.vivox_audio_route) (2026-10)
|
||||
# 5: unreal_quest_gates (Quest-only branches in ILMxLAB's Unreal: frame.unreal_quest_precompile/_keymap) (2026-10)
|
||||
ANALYSIS_VERSION = 5
|
||||
|
||||
|
||||
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
|
||||
@@ -159,8 +161,11 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
prefix = f"lib/{abi}/" if abi else None
|
||||
libs = sorted(n[len(prefix):] for n in names if prefix and n.startswith(prefix) and n.endswith(".so"))
|
||||
# SDL's Java side (SDL2 / LÖVE apps): crashes in Lepton without a clipboard service (frame.sdl_clipboard)
|
||||
sdl_java = any(b"Lorg/libsdl/app/SDLClipboardHandler;" in z.read(n) for n in names
|
||||
if n.startswith("classes") and n.endswith(".dex"))
|
||||
dexes = [z.read(n) for n in names if n.startswith("classes") and n.endswith(".dex")]
|
||||
sdl_java = any(b"Lorg/libsdl/app/SDLClipboardHandler;" in d for d in dexes)
|
||||
# Vivox voice chat calling Android 12 audio-routing methods: crashes in Lepton (Android 11)
|
||||
vivox_api31 = any(b"Lcom/vivox/sdk/AudioChangeListener;" in d and b"CommunicationDevice" in d for d in dexes)
|
||||
del dexes
|
||||
manifest = z.read("AndroidManifest.xml")
|
||||
lib_bytes = {}
|
||||
if deep and prefix:
|
||||
@@ -279,6 +284,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
# Meta's OVRManager raises MSAA at runtime (frame.unity_runtime_msaa_off); Oculus XR Plugin (multiview)
|
||||
"ovr_runtime_msaa": bool(il2cpp_meta) and b"\0useRecommendedMSAALevel\0" in il2cpp_meta,
|
||||
"sdl_java": sdl_java,
|
||||
"vivox_api31": vivox_api31,
|
||||
"oculus_xr_plugin": bool(il2cpp_meta) and b"\0m_StereoRenderingModeAndroid\0" in il2cpp_meta,
|
||||
# Unity's built-in Oculus support checks for Meta's system apps before VR (frame.unity_oculus_check)
|
||||
"unity_oculus_check": b"\0com.oculus.systemactivities\0" in lib_bytes.get("libunity.so", b""),
|
||||
@@ -296,10 +302,26 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
# Unreal's Oculus module needs every OVRPlugin function it looks up (frame.unreal_ovrp_entrypoints)
|
||||
"unreal_ovrp_lookups": (ovrp_lookups(lib_bytes.get(engine_lib.rsplit("/", 1)[1], b""))
|
||||
if engine_lib and "libOVRPlugin.so" in libset else []),
|
||||
# Quest-only branches that leave ILMxLAB's Unreal games stuck on the Frame (frame.unreal_quest_*)
|
||||
"unreal_quest_gates": (unreal_quest_gates(lib_bytes.get(engine_lib.rsplit("/", 1)[1], b""))
|
||||
if engine_lib else []),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Quest-only branches in ILMxLAB's Unreal (IsRunningOnSantaCruz) that leave the game stuck on the Frame: the exported
|
||||
# function each frame.unreal_quest_* patch rewrites
|
||||
UNREAL_QUEST_GATES = {
|
||||
"quest_precompile": "_ZN8UVRUtils31GetQuestShaderPrecompilePercentEv",
|
||||
"rpoc_keymap": "_ZN27URPOCKeyMapManagerComponent14AddAxisMappingERK15FRPOCKeyMappingR16FRPOCInputMapSet",
|
||||
}
|
||||
|
||||
|
||||
def unreal_quest_gates(data: bytes) -> list[str]:
|
||||
"""The gate functions an Unreal engine library exports (a search of the symbol names, no ELF parsing)."""
|
||||
return sorted(k for k, sym in UNREAL_QUEST_GATES.items() if b"\0" + sym.encode() + b"\0" in data)
|
||||
|
||||
|
||||
def ovrp_lookups(data: bytes) -> list[str]:
|
||||
"""The OVRPlugin function names (`ovrp_*` strings) an engine library holds: Unreal's Oculus module looks each one
|
||||
up with dlsym in libOVRPlugin.so and gives up on VR when any is missing (FOculusHMDModule::
|
||||
|
||||
@@ -59,6 +59,38 @@ def _dyn_symbols_cached(data: bytes, defined: bool) -> frozenset[str]:
|
||||
return frozenset(out)
|
||||
|
||||
|
||||
def find_symbols(data: bytes, names) -> dict[str, tuple[int, int]]:
|
||||
"""(address, size) of the defined dynamic symbols `names` (ELF64 little-endian). Looks the names up in .dynstr and
|
||||
their entries in .dynsym by byte search instead of reading every symbol: engine libraries (libUE4.so) have
|
||||
hundreds of thousands."""
|
||||
import struct
|
||||
|
||||
if not is_elf(data) or data[4] != 2 or data[5] != 1:
|
||||
return {}
|
||||
elf = _elf(data)
|
||||
dynsym, dynstr = elf.get_section_by_name(".dynsym"), elf.get_section_by_name(".dynstr")
|
||||
if dynsym is None or dynstr is None:
|
||||
return {}
|
||||
sym_off, sym_size = dynsym["sh_offset"], dynsym["sh_size"]
|
||||
str_off, str_end = dynstr["sh_offset"], dynstr["sh_offset"] + dynstr["sh_size"]
|
||||
out = {}
|
||||
for name in names:
|
||||
raw = name.encode() + b"\0"
|
||||
at = data.find(raw, str_off, str_end)
|
||||
while at >= 0 and name not in out:
|
||||
needle = struct.pack("<I", at - str_off)
|
||||
hit = data.find(needle, sym_off, sym_off + sym_size)
|
||||
while hit >= 0:
|
||||
if (hit - sym_off) % 24 == 0:
|
||||
_, _, _, shndx, value, size = struct.unpack_from("<IBBHQQ", data, hit)
|
||||
if shndx and value:
|
||||
out[name] = (value, size)
|
||||
break
|
||||
hit = data.find(needle, hit + 1, sym_off + sym_size)
|
||||
at = data.find(raw, at + 1, str_end)
|
||||
return out
|
||||
|
||||
|
||||
def is_64bit(data: bytes) -> bool:
|
||||
return is_elf(data) and data[4] == 2
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Minimal in-place dex edits: find one method's bytecode and turn a call in it into no-ops (same length), then fix the
|
||||
header's SHA-1 signature and Adler-32 checksum. Nothing moves, so the rest of classes.dex stays byte-identical (no
|
||||
smali round trip)."""
|
||||
"""Minimal in-place dex edits: find one method's bytecode and turn a call in it into no-ops, or make the method return
|
||||
at once (same length), then fix the header's SHA-1 signature and Adler-32 checksum. Nothing moves, so the rest of
|
||||
classes.dex stays byte-identical (no smali round trip)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
@@ -8,6 +8,29 @@ import struct
|
||||
import zlib
|
||||
|
||||
INVOKE_VIRTUAL, INVOKE_VIRTUAL_RANGE = 0x6E, 0x74 # 35c / 3rc: 3 code units, method index in the second
|
||||
INVOKES = frozenset(range(0x6E, 0x73)) | frozenset(range(0x74, 0x79)) # invoke-virtual..interface(/range)
|
||||
RETURN_VOID = 0x0E
|
||||
CONST4_V0_0, RETURN_V0 = 0x0012, 0x000F # const/4 v0, #0 ; return v0
|
||||
|
||||
|
||||
def _widths() -> list[int]:
|
||||
"""Code units of each Dalvik opcode (0 = unused/unknown)."""
|
||||
w = [0] * 256
|
||||
spans = [((0x00, 0x00), 1), ((0x01, 0x01), 1), ((0x02, 0x02), 2), ((0x03, 0x03), 3), ((0x04, 0x04), 1),
|
||||
((0x05, 0x05), 2), ((0x06, 0x06), 3), ((0x07, 0x07), 1), ((0x08, 0x08), 2), ((0x09, 0x09), 3),
|
||||
((0x0A, 0x12), 1), ((0x13, 0x13), 2), ((0x14, 0x14), 3), ((0x15, 0x16), 2), ((0x17, 0x17), 3),
|
||||
((0x18, 0x18), 5), ((0x19, 0x1A), 2), ((0x1B, 0x1B), 3), ((0x1C, 0x1C), 2), ((0x1D, 0x1E), 1),
|
||||
((0x1F, 0x20), 2), ((0x21, 0x21), 1), ((0x22, 0x23), 2), ((0x24, 0x26), 3), ((0x27, 0x28), 1),
|
||||
((0x29, 0x29), 2), ((0x2A, 0x2C), 3), ((0x2D, 0x3D), 2), ((0x44, 0x6D), 2), ((0x6E, 0x72), 3),
|
||||
((0x74, 0x78), 3), ((0x7B, 0x8F), 1), ((0x90, 0xAF), 2), ((0xB0, 0xCF), 1), ((0xD0, 0xE2), 2),
|
||||
((0xFA, 0xFB), 4), ((0xFC, 0xFD), 3), ((0xFE, 0xFF), 2)]
|
||||
for (lo, hi), n in spans:
|
||||
for op in range(lo, hi + 1):
|
||||
w[op] = n
|
||||
return w
|
||||
|
||||
|
||||
WIDTHS = _widths()
|
||||
|
||||
|
||||
def _uleb(data: bytes, pos: int) -> tuple[int, int]:
|
||||
@@ -43,11 +66,20 @@ class Dex:
|
||||
cls, _proto, name = struct.unpack_from("<HHI", self.data, self.methods_off + 8 * idx)
|
||||
return self.type_name(cls), self.string(name)
|
||||
|
||||
def return_type(self, idx: int) -> str:
|
||||
proto = struct.unpack_from("<HHI", self.data, self.methods_off + 8 * idx)[1]
|
||||
protos_off = struct.unpack_from("<I", self.data, 76)[0]
|
||||
return self.type_name(struct.unpack_from("<I", self.data, protos_off + 12 * proto + 4)[0])
|
||||
|
||||
def method_index(self, cls: str, name: str) -> list[int]:
|
||||
return [i for i in range(self.methods_n) if self.method(i) == (cls, name)]
|
||||
|
||||
def code_items(self, cls: str, name: str) -> list[int]:
|
||||
"""Offsets of the code items of the methods `name` in class `cls` (direct and virtual)."""
|
||||
return [code for _idx, code in self.class_methods(cls) if self.method(_idx)[1] == name]
|
||||
|
||||
def class_methods(self, cls: str) -> list[tuple[int, int]]:
|
||||
"""(method index, code item offset) of every method of class `cls` that has code."""
|
||||
out = []
|
||||
for c in range(self.classes_n):
|
||||
base = self.classes_off + 32 * c
|
||||
@@ -71,8 +103,8 @@ class Dex:
|
||||
_, pos = _uleb(self.data, pos)
|
||||
code, pos = _uleb(self.data, pos)
|
||||
idx += diff
|
||||
if code and self.method(idx)[1] == name:
|
||||
out.append(code)
|
||||
if code:
|
||||
out.append((idx, code))
|
||||
return out
|
||||
|
||||
def nop_calls(self, code_off: int, targets: set[int]) -> int:
|
||||
@@ -87,6 +119,60 @@ class Dex:
|
||||
done += 1
|
||||
return done
|
||||
|
||||
def invoked(self, code_off: int) -> set[int]:
|
||||
"""Method indices invoked (virtual/super/direct/static/interface, also /range) in one code item."""
|
||||
out: set[int] = set()
|
||||
for _pos, unit in self._insns(code_off):
|
||||
if unit[0] & 0xFF in INVOKES:
|
||||
out.add(unit[1])
|
||||
return out
|
||||
|
||||
def _insns(self, code_off: int):
|
||||
"""(position, code units) of each instruction, decoded in order; stops at payloads or unknown opcodes."""
|
||||
insns_n = struct.unpack_from("<I", self.data, code_off + 12)[0]
|
||||
units = struct.unpack_from(f"<{insns_n}H", self.data, code_off + 16)
|
||||
i = 0
|
||||
while i < insns_n:
|
||||
op = units[i] & 0xFF
|
||||
if op == 0 and units[i] >> 8: # packed-switch / sparse-switch / fill-array-data payload
|
||||
return
|
||||
n = WIDTHS[op]
|
||||
if not n or i + n > insns_n:
|
||||
return
|
||||
yield i, units[i:i + n]
|
||||
i += n
|
||||
|
||||
def return_void(self, code_off: int) -> bool:
|
||||
"""Make a void method return at once (see return_early)."""
|
||||
return self.return_early(code_off, "V")
|
||||
|
||||
def return_early(self, code_off: int, ret: str) -> bool:
|
||||
"""Make a method return at once: its first instruction(s) become `return-void` (ret "V") or `const/4 v0, 0;
|
||||
return v0` (ret Z/B/S/C/I: false / 0), padded with nops to the next instruction boundary. The rest of the code
|
||||
stays as it was (unreachable). False if the method already starts that way, the return type isn't supported
|
||||
(wide, objects) or its first instructions can't be decoded."""
|
||||
if ret == "V":
|
||||
new = [RETURN_VOID]
|
||||
elif ret in ("Z", "B", "S", "C", "I"):
|
||||
if not struct.unpack_from("<H", self.data, code_off)[0]: # registers_size: v0 must exist
|
||||
return False
|
||||
new = [CONST4_V0_0, RETURN_V0]
|
||||
else:
|
||||
return False
|
||||
start = code_off + 16
|
||||
insns_n = struct.unpack_from("<I", self.data, code_off + 12)[0]
|
||||
if list(struct.unpack_from(f"<{min(len(new), insns_n)}H", self.data, start)) == new:
|
||||
return False
|
||||
width = 0
|
||||
for pos, units in self._insns(code_off):
|
||||
width = pos + len(units)
|
||||
if width >= len(new):
|
||||
break
|
||||
if width < len(new):
|
||||
return False
|
||||
struct.pack_into(f"<{width}H", self.data, start, *new, *([0] * (width - len(new))))
|
||||
return True
|
||||
|
||||
def finish(self) -> bytes:
|
||||
self.data[12:32] = hashlib.sha1(bytes(self.data[32:])).digest()
|
||||
struct.pack_into("<I", self.data, 8, zlib.adler32(bytes(self.data[12:])) & 0xFFFFFFFF)
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"Android app (APK)": "",
|
||||
"Android app without VR: installed unchanged and shown as a flat window": "",
|
||||
"Android VR": "",
|
||||
"Animate fingers from touch": "",
|
||||
"Another app": "",
|
||||
"Answer the open install question first": "",
|
||||
"Any": "",
|
||||
@@ -359,6 +360,7 @@
|
||||
"Environment variables exported by the launcher (e.g. VK_INSTANCE_LAYERS=\"\" to test without Valve's layers).": "",
|
||||
"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 5 s, logs per located space how far the requested times are from the frame's predicted display time, and the runtime clock's offset from the monotonic clock. No effect on the game.": "",
|
||||
"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.": "",
|
||||
@@ -395,6 +397,7 @@
|
||||
"Fix crash when the game prepares the microphone": "",
|
||||
"Fix depth flicker (Unreal Vulkan)": "",
|
||||
"Fix freezes when controllers vibrate": "",
|
||||
"Fix hands lagging behind controllers": "",
|
||||
"Fix launcher icon entry": "",
|
||||
"Fix minimal Android SDK": "",
|
||||
"Fix UE4 crash with Unity stub": "",
|
||||
@@ -416,6 +419,8 @@
|
||||
"For games that judder or shimmer while you hold your head still (e.g. I Am Cat).": "",
|
||||
"For games that pause or recenter when the headset briefly loses focus.": "",
|
||||
"For games that show the headset's own controllers. Takes effect after a reinstall.": "",
|
||||
"For games whose hands trail the controllers when you move them (e.g. BattleSisters).": "",
|
||||
"For games whose hands' thumbs never move when you touch the buttons or thumbstick (e.g. Vader Immortal).": "",
|
||||
"For games whose menus or screens jump to wherever you look.": "",
|
||||
"For heavy games that use application space warp: it causes artifacts or hangs on non-Quest runtimes (e.g. Asgard's Wrath 2, Batman: Arkham Shadow).": "",
|
||||
"For mixed-reality-only games. On the Frame, passthrough is emulated by the FrameBridge adapter (greyscale cameras).": "",
|
||||
@@ -544,6 +549,8 @@
|
||||
"If Windows asked whether FramePort may use the network, allow it (Private networks).": "",
|
||||
"If you know the Frame's address.": "",
|
||||
"Ignore focus dips up to": "",
|
||||
"ILMxLAB's Unreal games (e.g. Vader Immortal) bind their controls through URPOCKeyMapManagerComponent, which for an Oculus headset picks the Quest (\"SantaCruz\") or the Gear VR key set by IsRunningOnSantaCruz. On the Frame it picks Gear VR, which is empty in a Quest build: grip, trigger and touch reach Unreal but never the game's hands (grabbing does nothing). Removes the branch to the Gear VR set in the Oculus case of the key selector both action and axis mappings use. Found by Klownicle (GitHub #49).": "",
|
||||
"ILMxLAB's Unreal games (e.g. Vader Immortal) wait on their loading card until UVRUtils::GetQuestShaderPrecompilePercent reaches 100 %. The precompile only starts on a Quest (IsRunningOnSantaCruz); on any other device the function returns 0.0, so the loading card (a portrait with a progress bar) never ends. Makes that non-Quest branch return 1.0. The Quest branch and the game's normal shader compiling stay as they are. Found by Klownicle (GitHub #49).": "",
|
||||
"Image for the {label}": "",
|
||||
"In the menu and on the desktop": "",
|
||||
"In your Steam library": "",
|
||||
@@ -635,6 +642,7 @@
|
||||
"Lepton's Android has no clipboard service, and SDL's Java code (SDL2 and LÖVE games, e.g. Dramatic Shape) registers a clipboard listener at start without checking, so the app crashes with a NullPointerException in SDLClipboardHandler. That call is skipped (an in-place edit of classes.dex).": "",
|
||||
"Lets games made for Oculus PCs run on SteamVR.": "",
|
||||
"Lets games start that would stop on Meta's avatar system (e.g. BlazeRush); no Meta avatars.": "",
|
||||
"Lets games with Vivox voice chat start on the Frame (they'd crash on an Android 12 audio call).": "",
|
||||
"Lets games written for newer OpenXR versions run on the Frame.": "",
|
||||
"Lets mixed-reality games start even though they insist on a camera view of your room.": "",
|
||||
"Lets older Unity games start VR without Meta's system apps (they'd stay on a 2D screen).": "",
|
||||
@@ -643,6 +651,7 @@
|
||||
"Lets Team Beef ports (e.g. Lambda1VR) start on the Frame and use their Meta Quest setup.": "",
|
||||
"Lets the game find content it keeps in separate files next to its data (e.g. Star Wars Tales' seasons).": "",
|
||||
"Lets the game find language files (like de.lang) that are already in its data, instead of waiting for a download that never comes.": "",
|
||||
"Lets the game leave its loading screen: it waited for a Quest-only step that never starts on the Frame (e.g. Vader Immortal).": "",
|
||||
"Lets typing reach this app: Steam's on-screen keyboard or your computer's keyboard (VR is unaffected).": "",
|
||||
"Lets Unity games accept a headset that isn't a Quest.": "",
|
||||
"Lets Unity start the graphics itself instead of Stress Level Zero's plugin, which crashes on the Frame (e.g. BONELAB 1.2974).": "",
|
||||
@@ -662,6 +671,7 @@
|
||||
"Live view": "",
|
||||
"Loading…": "",
|
||||
"Loads FramePort's OpenXR layer under Proton on the Frame. The Frame's SteamVR runtime only accepts OpenXR 1.0 apps, but Proton's VR helper asks for 1.1, so without the layer VR never starts (the game shows as a flat window, or Revive fails with 'Unable to load LibOVRRT DLL'); the layer retries as 1.0. It also emulates xrConvertTimespecTimeToTimeKHR if a runtime refuses it. Ignored on this PC.": "",
|
||||
"Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every other module is untouched), e.g. stores that set loop counters and accumulators a shader reads before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub #49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer adds itself to Android's Vulkan debug layer list for the game's process only.": "",
|
||||
"Loads libglshim.so ahead of libEGL in every engine library that fetches GL through eglGetProcAddress. Fixes Quest-only leniency that Mesa/Zink rejects: #pragma before #extension, implicit int/float conversions (enables GL_EXT_shader_implicit_conversions), and multiview shaders used on single-view framebuffers (hides GL_OVR_multiview; gl_hide_multiview=0 turns that off). Logs every failed shader with its source lines (logcat tag GLShim). Symptom: black screen with working audio in a GLES game.": "",
|
||||
"Loads the GL shim into a Unity GLES game and hides GL_EXT_multisampled_render_to_texture (and the multiview variant). Some Unity games switch their eye buffer to 4x MSAA at runtime; the Frame only gives single-sampled swapchains, Unity then renders through multisampled render-to-texture and Mesa/Zink crashes (SIGSEGV in libgallium_dri.so on the render thread: grey or frozen screen, e.g. The Room VR). Hidden, Unity uses an ordinary MSAA buffer and resolves it. Unity's multiview stays on (gl_hide_multiview defaults to 0 here).": "",
|
||||
"Loads the GL shim into an Unreal GLES game and hides GL_EXT_multisampled_render_to_texture. Unreal's mobile MSAA renders through multisampled render-to-texture, and the Frame's GL driver (Mesa/Zink) can crash on it: SIGSEGV with fault address 0x10000 in libgallium_dri.so on the RHIThread a few seconds after start (Zink looks up a render-pass cache slot past its end, e.g. Star Wars Pinball VR). Hidden, Unreal renders without that path. Unreal's multiview stays on: Unreal only uses it when GL_OVR_multiview_multisampled_render_to_texture is listed too, so for Unreal the shim keeps that one visible and draws its multiview MSAA attachments single-sampled (glFramebufferTextureMultiviewOVR).": "",
|
||||
@@ -678,6 +688,7 @@
|
||||
"Low": "",
|
||||
"macOS' firewall is on: allow incoming connections when macOS asks about FramePort (or System Settings → Network → Firewall → Options).": "",
|
||||
"Make Unity text fields work without a system keyboard": "",
|
||||
"Makes grip, trigger and touch controls reach the game's hands (it picked the empty Gear VR controls on the Frame, e.g. Vader Immortal).": "",
|
||||
"Makes Meta's 3D audio library work outside Quest headsets.": "",
|
||||
"Makes sure the game has an entry that can be started.": "",
|
||||
"Makes the game start from the Frame's Steam library.": "",
|
||||
@@ -697,6 +708,7 @@
|
||||
"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\", e.g. BlazeRush). Replaces it with a library that does nothing, so the game runs without Meta avatars.": "",
|
||||
"Meta's OVRManager switches Unity to the headset's recommended MSAA level (4x) while the game runs, whatever its quality settings say (log: \"Switching to the recommended level\"). Multisampled render-to-texture on GLES can hang the Frame's GPU, up to a restart of the whole headset (e.g. Lucky's Tale in a menu) or a crash of its GL driver (e.g. The Room VR, whose own code sets 4x). Rewrites OVRDisplay.recommendedMSAALevel -> 0 and makes QualitySettings.antiAliasing's setter do nothing in libil2cpp.so (found with Cpp2IL), so MSAA stays off.": "",
|
||||
"Meta's OVROverlay copies a texture into its own compositor layer every frame. For some games (e.g. The Room VR's screen fade, a 4x4 overlay) that copy crashes the Frame's GL driver: SIGSEGV in libgallium_dri.so on Unity's render thread right after a tiny xrCreateSwapchain, grey or frozen screen. Rewrites OVROverlay.PopulateLayer -> false in libil2cpp.so (found with Cpp2IL): overlays are skipped (fades become instant cuts, overlay splash screens don't show). Only for games with this crash: menus drawn as overlays would disappear too.": "",
|
||||
"Meta's OVRPlugin reports a thumb (or index finger) resting near the controller through XR_FB_touch_controller_proximity, which the Frame's runtime lacks: \"near\" stays false and games that animate the hands from it show a thumb that never moves (e.g. Vader Immortal; found by Klownicle, GitHub #49). Binds the game's proximity actions to the capacitive touch inputs instead: 1 = thumb (thumbstick, face buttons or thumb rest touched), 2 = also the index finger (trigger touched). Only when the runtime lacks the extension.": "",
|
||||
"MiB": "",
|
||||
"Minecraft asks for desktop OpenGL's GL_DEPTH_COMPONENT32 depth textures. Some builds of LTW, the OpenGL-to-OpenGL ES wrapper in Minecraft launchers such as QuestCraft, pass that on unchanged; Quest's driver accepts it, the Frame's Mesa rejects it (GL_INVALID_OPERATION in glTexImage2D), so the depth buffer never exists and the picture stays black. LTW then loads OpenGL ES through a small shim that asks for GL_DEPTH_COMPONENT32F (or 24-bit) instead.": "",
|
||||
"Mirrored": "",
|
||||
@@ -784,6 +796,7 @@
|
||||
"Oculus game — needs Revive, which doesn't run on the Frame. Play it on this PC (SteamVR).": "",
|
||||
"Oculus OS telemetry class stubs": "",
|
||||
"Oculus XR Plugin games render both eyes in one pass (multiview) on Android. If one eye shows only effects or grey (e.g. I Am Cat), rendering each eye in its own pass can fix it, at some GPU cost. Rewrites OculusSettings.GetStereoRenderingMode and its inlined read in OculusLoader.Initialize -> MultiPass in libil2cpp.so (found with Cpp2IL). Try it when one eye is wrong.": "",
|
||||
"Off": "",
|
||||
"Off: these links open whatever app is set up for them, if any.": "",
|
||||
"off: {names}": "",
|
||||
"Offline": "",
|
||||
@@ -810,6 +823,9 @@
|
||||
"Open viewer": "",
|
||||
"Open {scheme}:// links with FramePort?": "",
|
||||
"OpenGL ES games run on the Frame through Zink (OpenGL on top of Vulkan), which is stricter than Quest drivers. Vulkan games run natively.": "",
|
||||
"OpenGL ES shader dump (diagnostics)": "",
|
||||
"OpenGL ES shader fixes": "",
|
||||
"OpenGL ES: shader fixes (Vulkan layer under Zink)": "",
|
||||
"Opens a prefilled GitHub issue with this game's recipe, so it can join the built-in catalog (no account token needed; you review and submit it on GitHub). No game files or personal data are sent.": "",
|
||||
"Opens the app straight in VR, skipping its flat launcher screen (turn off to use the launcher, e.g. to import new content).": "",
|
||||
"Opens this player's storage on the Frame (Files tab): upload videos into the folder it lists": "",
|
||||
@@ -874,6 +890,8 @@
|
||||
"Pointer start": "",
|
||||
"Pointer turn": "",
|
||||
"Points Oculus games at Revive on the Frame.": "",
|
||||
"Pose time diagnostics (log)": "",
|
||||
"Poses a game asks for at a CLOCK_MONOTONIC time, or more than 0.5 s before the frame's display time, are located at the same moment in the runtime's time (or now). Meta's OVRPlugin passes the monotonic clock on as the OpenXR time, which on the Frame runs seconds behind the runtime's clock, so its \"now\" poses (Unity's physics step, controller poses) came from the past: hands trailing the controllers (e.g. BattleSisters, Sniper Elite VR).": "",
|
||||
"Power": "",
|
||||
"Power ICs": "",
|
||||
"Prepare {title}": "",
|
||||
@@ -958,6 +976,7 @@
|
||||
"Rename": "",
|
||||
"Rename {name}": "",
|
||||
"Rename…": "",
|
||||
"Repairs shaders that hang the graphics chip at certain effects, from the game's recipe (e.g. Vader Immortal's lightspeed jump).": "",
|
||||
"Repeat xrLocateViews queries for the same display time get the first, fully tracked answer again. Games that ask several times per frame got slightly different poses and rendered parts of the frame with different heads: judder (e.g. I Am Cat; proposed by Klownicle, GitHub #8).": "",
|
||||
"Replace application label and icon": "",
|
||||
"Replaces libvrapi.so with a VrApi→OpenXR bridge (Android-XR-Bridge/OVRPort fork, GPL-3.0, patched for the Frame: GLES sessions, cylinder→quad layers, sRGB format fallback, 30 s VR-mode deadline, emulated time conversion). Needed when the engine calls VrApi itself instead of through OVRPlugin (e.g. The Climb 2, Path of the Warrior); OVRPort cannot translate those.": "",
|
||||
@@ -1037,6 +1056,8 @@
|
||||
"Sets Unity QualitySettings antiAliasing to 0. Unity's multisampled render-to-texture path can hang the Frame's GL driver ('zink: DEVICE LOST', e.g. Sniper Elite VR). Try it for GLES Unity games that freeze or crash the GPU.": "",
|
||||
"Settings": "",
|
||||
"shader cache: makes later starts load faster; rebuilt if deleted": "",
|
||||
"Shader-fix Vulkan layer (frame.zink_shader_fix) for OpenGL ES games, whose GLSL the Frame's GL driver (Zink) turns into SPIR-V: modules matching size + SHA-256 get words inserted at a byte offset, e.g. stores that set loop counters and accumulators a shader reads before writing (Vader Immortal's lightspeed jump hung the GPU; found by Klownicle, GitHub #49). Same format as vk_shader_fix. Comes from a game's recipe; the driver's output can change with a Frame update, then the fix no longer matches (logged).": "",
|
||||
"Shader-fix Vulkan layer (frame.zink_shader_fix): writes every distinct SPIR-V module the GL driver creates to Android/data/<package>/files/fp_spirv/<size>_<sha256>.spv, to capture a shader for a new zink_shader_fix. No effect on the game.": "",
|
||||
"Share working config · {title}": "",
|
||||
"Share working config…": "",
|
||||
"Sharpness": "",
|
||||
@@ -1092,6 +1113,7 @@
|
||||
"Sort": "",
|
||||
"Source": "",
|
||||
"square, 256 × 256": "",
|
||||
"Stand-in cube swapchains": "",
|
||||
"Standard for every game": "",
|
||||
"Start live view": "",
|
||||
"Start straight in VR (skip the 2D launcher)": "",
|
||||
@@ -1190,6 +1212,7 @@
|
||||
"The Frame's battery is at {pct} %: installs paused so it doesn't switch off mid-upload. Plug it in; the queue continues by itself.": "",
|
||||
"The Frame's CPU is arm64. Programs built for x86_64 PCs run through FEX, Valve's x86 translator, with the x86 libraries SteamOS ships for it (glibc, Mesa) - the way Steam on the Frame runs x86 Linux games. That works for many programs but is slower: use an arm64 build when there is one.": "",
|
||||
"The Frame's desktop password, only needed the first time so FramePort can add its own key. After that it connects with the key.": "",
|
||||
"The Frame's runtime has no cube-map layers and refuses their swapchains; OVRPlugin then crashes when it submits the next frame (e.g. Budget Cuts Ultimate). FrameBridge serves such a swapchain itself (a GL cube map, GLES games) and drops its layers, so the game runs on without that layer.": "",
|
||||
"The Frame's Steam couldn't start {title} (Steam error {code}). Please send a problem report so we can see why.": "",
|
||||
"The Frame's Steam didn't accept {title}'s library entry, so FramePort added it as \"Devkit Game: …\" instead. Starting it now — put the headset on.": "",
|
||||
"The Frame's Steam library still shows the old artwork.": "",
|
||||
@@ -1237,6 +1260,8 @@
|
||||
"This removes:": "",
|
||||
"This source checkout has uncommitted changes: commit or stash them, then update": "",
|
||||
"This system can't send web links to FramePort: use Add games → Install from a link…": "",
|
||||
"Thumbs": "",
|
||||
"Thumbs and index fingers": "",
|
||||
"Tilts the controllers' pointing ray up (+) or down (−), for games whose pointer doesn't hit what you aim at.": "",
|
||||
"To finish, close FramePort and delete its program folder.": "",
|
||||
"Today": "",
|
||||
@@ -1300,6 +1325,8 @@
|
||||
"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 (e.g. NOPE Challenge).": "",
|
||||
"Unreal games start CrashReportClient when they crash, which leaves a crash dialog instead of simply closing. This passes -nocrashreports to the game and, on the Frame, renames the game's copy of CrashReportClient.exe so it can't start (your game files on this PC aren't changed).": "",
|
||||
"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 (e.g. ovrp_GetPTWNear, which Unreal 4.25 builds like Star Wars Pinball VR look up). The game then never starts an OpenXR session and crashes a few seconds in. Adds a generated library to libOVRPlugin.so with a stand-in for each missing function that reports failure (ovrpFailure), so Unreal's Oculus support starts. The functions OVRPort has are untouched.": "",
|
||||
"Unreal: don't wait for the Quest shader precompile": "",
|
||||
"Unreal: Quest controller bindings (RPOC key map)": "",
|
||||
"Unreal: stand-ins for OVRPlugin functions OVRPort lacks": "",
|
||||
"Untested": "",
|
||||
"Update all": "",
|
||||
@@ -1358,6 +1385,8 @@
|
||||
"View": "",
|
||||
"View screenshot": "",
|
||||
"Viewer address on this PC: {url}": "",
|
||||
"Vivox voice chat: skip Android 12 audio routing": "",
|
||||
"Vivox's voice-chat SDK (e.g. 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.": "",
|
||||
"VR (OpenXR)": "",
|
||||
"VrApi → OpenXR adapter (OVRPort)": "",
|
||||
"VrApi → OpenXR bridge": "",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Unreal games that pick Quest code paths with IsRunningOnSantaCruz() (ILMxLAB's Unreal 4 fork, e.g. Vader Immortal).
|
||||
|
||||
The engine answers "not a Quest" on the Frame (Lepton's Android isn't a Quest), so the game takes its Gear VR / PC
|
||||
paths. Two of them leave a Quest build stuck; each fix rewrites one instruction in the engine library at the branch that
|
||||
matters and changes nothing else (the device stays "not a Quest" everywhere else):
|
||||
|
||||
* GetQuestShaderPrecompilePercent: the Quest branch reports the progress of the shader precompile the game starts on a
|
||||
Quest; the other branch returns 0.0 although no precompile was ever started there. The menu waits for 100 %: the
|
||||
loading card (Vader's portrait with a progress bar) never ends. The other branch returns 1.0 instead.
|
||||
* URPOCKeyMapManagerComponent's key selector (shared by AddAxisMapping and AddActionMapping): for an OculusHMD it
|
||||
chooses the Quest ("SantaCruz") key set or the Gear VR one by IsRunningOnSantaCruz(). A Quest build's Gear VR set is
|
||||
empty, so grip/trigger/touch bindings never reach the game's hands (input arrives in Unreal, the game's axes stay 0).
|
||||
The branch to the Gear VR set is removed for the OculusHMD case only.
|
||||
|
||||
Both are located by their exported symbol and checked instruction by instruction (found by Klownicle in Vader Immortal:
|
||||
Episode I 1.1.1, GitHub #49); a library whose code doesn't match exactly is left alone. Already patched code counts as
|
||||
done."""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from ...analysis import elf
|
||||
from ...analysis.detect import UNREAL_QUEST_GATES
|
||||
from ..base import ApkContext, Patch, Suggestion, register
|
||||
|
||||
ENGINE_LIBS = ("libUE4.so", "libUnreal.so")
|
||||
SANTACRUZ = ("_ZN15FSantaCruzUtils20IsRunningOnSantaCruzEv", "_ZN23UNTNinjaFunctionLibrary20IsRunningOnSantaCruzEv")
|
||||
PRECOMPILE = UNREAL_QUEST_GATES["quest_precompile"]
|
||||
ADD_AXIS = UNREAL_QUEST_GATES["rpoc_keymap"]
|
||||
ADD_ACTION = "_ZN27URPOCKeyMapManagerComponent16AddActionMappingERK15FRPOCKeyMappingR16FRPOCInputMapSet"
|
||||
|
||||
FMOV_S0_ZERO = 0x1E2703E0 # fmov s0, wzr
|
||||
FMOV_S0_ONE = 0x1E2E1000 # fmov s0, #1.0
|
||||
LDP_FP_LR = 0xA8C17BFD # ldp x29, x30, [sp], #0x10
|
||||
RET = 0xD65F03C0
|
||||
NOP = 0xD503201F
|
||||
SELECTOR_SCAN = 0x400 # bytes of the key selector searched for the branch
|
||||
|
||||
|
||||
def _words(data: bytes, segs, va: int, size: int) -> list[int] | None:
|
||||
off = elf.vaddr_to_offset(segs, va)
|
||||
if off is None or size <= 0 or off + size > len(data):
|
||||
return None
|
||||
return list(struct.unpack_from(f"<{size // 4}I", data, off))
|
||||
|
||||
|
||||
def _bl_target(va: int, w: int) -> int | None:
|
||||
if w & 0xFC000000 != 0x94000000:
|
||||
return None
|
||||
imm = w & 0x03FFFFFF
|
||||
return va + ((imm - (1 << 26) if imm & (1 << 25) else imm) << 2)
|
||||
|
||||
|
||||
def _tbz_w0_0_target(va: int, w: int) -> int | None: # tbz w0, #0, <target>
|
||||
if w & 0xFFF8001F != 0x36000000:
|
||||
return None
|
||||
imm = (w >> 5) & 0x3FFF
|
||||
return va + ((imm - (1 << 14) if imm & (1 << 13) else imm) << 2)
|
||||
|
||||
|
||||
def _is_add_x0_x19(w: int) -> bool: # add x0, x19, #imm
|
||||
return w & 0xFFC003FF == 0x91000260
|
||||
|
||||
|
||||
def _is_b(w: int) -> bool:
|
||||
return w & 0xFC000000 == 0x14000000
|
||||
|
||||
|
||||
def precompile_site(data: bytes) -> tuple[int, int] | None:
|
||||
"""(file offset, current word) of the non-Quest return value in GetQuestShaderPrecompilePercent: the target of
|
||||
`bl IsRunningOnSantaCruz; tbz w0, #0, L` where L is `fmov s0, wzr; ldp x29, x30, [sp], #16; ret` (or already
|
||||
`fmov s0, #1.0`)."""
|
||||
syms = elf.find_symbols(data, (PRECOMPILE, *SANTACRUZ))
|
||||
if PRECOMPILE not in syms:
|
||||
return None
|
||||
targets = {syms[s][0] for s in SANTACRUZ if s in syms}
|
||||
va, size = syms[PRECOMPILE]
|
||||
segs = elf.load_segments(data)
|
||||
words = _words(data, segs, va, min(size, 0x100))
|
||||
if not words or not targets:
|
||||
return None
|
||||
sites = []
|
||||
for i in range(len(words) - 1):
|
||||
if _bl_target(va + 4 * i, words[i]) not in targets:
|
||||
continue
|
||||
to = _tbz_w0_0_target(va + 4 * (i + 1), words[i + 1])
|
||||
j = (to - va) // 4 if to is not None else -1
|
||||
if 0 <= j < len(words) - 2 and words[j] in (FMOV_S0_ZERO, FMOV_S0_ONE) and words[j + 1] == LDP_FP_LR \
|
||||
and words[j + 2] == RET:
|
||||
sites.append((elf.vaddr_to_offset(segs, va + 4 * j), words[j]))
|
||||
return sites[0] if len(sites) == 1 else None
|
||||
|
||||
|
||||
def keymap_site(data: bytes) -> tuple[int, int] | None:
|
||||
"""(file offset, current word) of the `tbz w0, #0, <Gear VR keys>` after `bl IsRunningOnSantaCruz` in the key
|
||||
selector both URPOCKeyMapManagerComponent::AddAxisMapping and ::AddActionMapping call. The Quest side must be
|
||||
`add x0, x19, #field; b <return>` and the Gear VR side the same with another field; a nop there = already done."""
|
||||
syms = elf.find_symbols(data, (ADD_AXIS, ADD_ACTION, *SANTACRUZ))
|
||||
if ADD_AXIS not in syms or ADD_ACTION not in syms:
|
||||
return None
|
||||
targets = {syms[s][0] for s in SANTACRUZ if s in syms}
|
||||
segs = elf.load_segments(data)
|
||||
calls = []
|
||||
for name in (ADD_AXIS, ADD_ACTION):
|
||||
va, size = syms[name]
|
||||
words = _words(data, segs, va, min(size, 0x1000)) or []
|
||||
calls.append({_bl_target(va + 4 * i, w) for i, w in enumerate(words)} - {None})
|
||||
sites = []
|
||||
for sel in sorted(calls[0] & calls[1]):
|
||||
words = _words(data, segs, sel, SELECTOR_SCAN)
|
||||
if not words:
|
||||
continue
|
||||
for i in range(len(words) - 3):
|
||||
if _bl_target(sel + 4 * i, words[i]) not in targets:
|
||||
continue
|
||||
branch, quest, back = words[i + 1], words[i + 2], words[i + 3]
|
||||
if not (_is_add_x0_x19(quest) and _is_b(back)):
|
||||
continue
|
||||
if branch == NOP:
|
||||
sites.append((elf.vaddr_to_offset(segs, sel + 4 * (i + 1)), NOP))
|
||||
continue
|
||||
to = _tbz_w0_0_target(sel + 4 * (i + 1), branch)
|
||||
j = (to - sel) // 4 if to is not None else -1
|
||||
if 0 <= j < len(words) - 1 and _is_add_x0_x19(words[j]) and words[j] != quest and _is_b(words[j + 1]):
|
||||
sites.append((elf.vaddr_to_offset(segs, sel + 4 * (i + 1)), branch))
|
||||
return sites[0] if len(sites) == 1 else None
|
||||
|
||||
|
||||
def _engine(ws) -> str | None:
|
||||
return next((n for n in ENGINE_LIBS if ws.has(ws.lib(n))), None)
|
||||
|
||||
|
||||
class _SantaCruzGate(Patch):
|
||||
gate = ""
|
||||
done_word = 0
|
||||
order = 47
|
||||
|
||||
def site(self, data: bytes) -> tuple[int, int] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def applies(self, a):
|
||||
if a.engine != "Unreal" or "arm64-v8a" not in a.abis:
|
||||
return False
|
||||
gates = (a.extra or {}).get("unreal_quest_gates")
|
||||
return gates is None or self.gate in gates # analysed by an older FramePort: maybe
|
||||
|
||||
def detect(self, a):
|
||||
if self.gate in ((a.extra or {}).get("unreal_quest_gates") or []) and "arm64-v8a" in a.abis:
|
||||
return Suggestion(True, self.reason)
|
||||
return None
|
||||
|
||||
reason = ""
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
ws = ctx.ws
|
||||
lib = _engine(ws) if ws.abi == "arm64-v8a" else None
|
||||
if not lib:
|
||||
return False
|
||||
data = ws.read_lib(lib)
|
||||
found = self.site(data)
|
||||
if not found:
|
||||
ctx.notes.append(f"{self.id}: {lib} doesn't match the expected code, left unchanged")
|
||||
return False
|
||||
off, word = found
|
||||
if word == self.done_word:
|
||||
return False
|
||||
buf = bytearray(data)
|
||||
struct.pack_into("<I", buf, off, self.done_word)
|
||||
ws.put(ws.lib(lib), bytes(buf))
|
||||
ctx.notes.append(f"{self.note} ({lib} file offset {off:#x})")
|
||||
return True
|
||||
|
||||
def validate(self, ctx: ApkContext):
|
||||
ws = ctx.ws
|
||||
lib = _engine(ws)
|
||||
if not lib or ws.abi != "arm64-v8a":
|
||||
return []
|
||||
found = self.site(ws.read_lib(lib))
|
||||
return [(self.check, bool(found) and found[1] == self.done_word,
|
||||
"" if found else "code not found (different build)")]
|
||||
|
||||
|
||||
class QuestPrecompileDone(_SantaCruzGate):
|
||||
id = "frame.unreal_quest_precompile"
|
||||
title = "Unreal: don't wait for the Quest shader precompile"
|
||||
description = ("ILMxLAB's Unreal games (e.g. Vader Immortal) wait on their loading card until "
|
||||
"UVRUtils::GetQuestShaderPrecompilePercent reaches 100 %. The precompile only starts on a Quest "
|
||||
"(IsRunningOnSantaCruz); on any other device the function returns 0.0, so the loading card (a "
|
||||
"portrait with a progress bar) never ends. Makes that non-Quest branch return 1.0. The Quest "
|
||||
"branch and the game's normal shader compiling stay as they are. Found by Klownicle (GitHub #49).")
|
||||
gate = "quest_precompile"
|
||||
done_word = FMOV_S0_ONE
|
||||
note = "GetQuestShaderPrecompilePercent: non-Quest branch returns 1.0"
|
||||
check = "Quest shader precompile reported finished"
|
||||
reason = ("The menu waits for a Quest-only shader precompile that never starts on the Frame (stuck loading card, "
|
||||
"e.g. Vader Immortal).")
|
||||
|
||||
def site(self, data):
|
||||
return precompile_site(data)
|
||||
|
||||
|
||||
class QuestKeyMap(_SantaCruzGate):
|
||||
id = "frame.unreal_quest_keymap"
|
||||
title = "Unreal: Quest controller bindings (RPOC key map)"
|
||||
description = ("ILMxLAB's Unreal games (e.g. Vader Immortal) bind their controls through URPOCKeyMapManager"
|
||||
"Component, which for an Oculus headset picks the Quest (\"SantaCruz\") or the Gear VR key set by "
|
||||
"IsRunningOnSantaCruz. On the Frame it picks Gear VR, which is empty in a Quest build: grip, "
|
||||
"trigger and touch reach Unreal but never the game's hands (grabbing does nothing). Removes the "
|
||||
"branch to the Gear VR set in the Oculus case of the key selector both action and axis mappings "
|
||||
"use. Found by Klownicle (GitHub #49).")
|
||||
gate = "rpoc_keymap"
|
||||
done_word = NOP
|
||||
note = "RPOC key map: Oculus headsets get the Quest key set"
|
||||
check = "Quest key set selected for Oculus headsets"
|
||||
reason = ("The game's key map picks the empty Gear VR controls when the headset isn't a Quest (grip/trigger do "
|
||||
"nothing, e.g. Vader Immortal).")
|
||||
|
||||
def site(self, data):
|
||||
return keymap_site(data)
|
||||
|
||||
|
||||
register(QuestPrecompileDone)
|
||||
register(QuestKeyMap)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Vivox voice chat (Unity's Vivox package, e.g. Green Hell VR, GitHub #101) crashes at start in Lepton: its Java
|
||||
audio-route listener (com.vivox.sdk.AudioChangeListener) calls Android 12 AudioManager methods
|
||||
(getAvailableCommunicationDevices, setCommunicationDevice, ...) without checking the Android version, and Lepton runs
|
||||
Android 11 (this Vivox build has no version checks at all) → java.lang.NoSuchMethodError. Every method of that class
|
||||
that calls one of them is made to return at once (in-place classes.dex edit: void methods return, boolean/int ones
|
||||
return false/0; methods returning objects are left alone). Voice chat then keeps Android's default audio route; nothing
|
||||
else changes. The calls themselves can't simply be skipped (the following move-result would fail Android's bytecode
|
||||
verifier), which is why whole methods return instead."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...apk.dex import Dex
|
||||
from ..base import ApkContext, Patch, Suggestion, register
|
||||
|
||||
LISTENER = "Lcom/vivox/sdk/AudioChangeListener;"
|
||||
AUDIO_MANAGER = "Landroid/media/AudioManager;"
|
||||
API31 = ("getAvailableCommunicationDevices", "setCommunicationDevice", "clearCommunicationDevice",
|
||||
"getCommunicationDevice", "addOnCommunicationDeviceChangedListener",
|
||||
"removeOnCommunicationDeviceChangedListener")
|
||||
|
||||
|
||||
def patch_dex(dex: Dex) -> list[str]:
|
||||
"""Methods of Vivox's AudioChangeListener that now return at once; [] if nothing applies."""
|
||||
targets = {i for name in API31 for i in dex.method_index(AUDIO_MANAGER, name)}
|
||||
if not targets:
|
||||
return []
|
||||
done = []
|
||||
for idx, code in dex.class_methods(LISTENER):
|
||||
if not dex.invoked(code) & targets:
|
||||
continue
|
||||
if dex.return_early(code, dex.return_type(idx)):
|
||||
done.append(dex.method(idx)[1])
|
||||
return done
|
||||
|
||||
|
||||
class VivoxAudioRoute(Patch):
|
||||
id = "frame.vivox_audio_route"
|
||||
title = "Vivox voice chat: skip Android 12 audio routing"
|
||||
description = ("Vivox's voice-chat SDK (e.g. 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.")
|
||||
order = 44
|
||||
needs_vr = False
|
||||
|
||||
def applies(self, a):
|
||||
return bool((a.extra or {}).get("vivox_api31"))
|
||||
|
||||
def detect(self, a):
|
||||
if self.applies(a):
|
||||
return Suggestion(True, "Vivox voice chat: it calls Android 12 audio methods, which Lepton (Android 11) "
|
||||
"lacks, and crashes.")
|
||||
return None
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
changed = False
|
||||
for name in sorted(n for n in ctx.ws.names() if n.startswith("classes") and n.endswith(".dex")):
|
||||
data = ctx.ws.read(name)
|
||||
if LISTENER.encode() not in data:
|
||||
continue
|
||||
dex = Dex(data)
|
||||
done = patch_dex(dex)
|
||||
if done:
|
||||
ctx.ws.put(name, dex.finish())
|
||||
ctx.notes.append(f"{name}: Vivox AudioChangeListener returns at once in {', '.join(sorted(done))}")
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
register(VivoxAudioRoute)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Shader fixes for OpenGL ES games: a Vulkan layer between Zink and the Frame's driver (native/zinkfix).
|
||||
|
||||
On the Frame, OpenGL ES runs on Zink (Mesa's GL on Vulkan), which turns the game's GLSL into SPIR-V inside the driver.
|
||||
The Vulkan shim's vk_shader_fix only reaches engines that load Vulkan themselves, so GLES games get this layer instead:
|
||||
it inserts words into the SPIR-V modules whose size and SHA-256 match the adapter setting zink_shader_fix (same format
|
||||
as vk_shader_fix) and passes every other module through. The engine library loads the layer library first
|
||||
(DT_NEEDED); its constructor adds the layer to the process's Vulkan debug layer list (Android's GraphicsEnv) before
|
||||
the game starts EGL. Found for Vader Immortal's lightspeed shaders (loop counters and accumulators read before they
|
||||
are set hang the GPU) by Klownicle, GitHub #49."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...analysis import elf
|
||||
from ..base import ApkContext, Patch, register
|
||||
from . import artifact
|
||||
|
||||
LAYER = "libVkLayer_fp_shaderfix.so"
|
||||
ENGINE_LIBS = ("libUE4.so", "libUnreal.so", "libunity.so")
|
||||
SETTING = "adapter.zink_shader_fix"
|
||||
# not the engine: XR/Meta/FramePort libraries that may start EGL too
|
||||
NOT_ENGINE = ("libopenxr", "libOVR", "libovr", "libvrapi", "libfp_", "libframe", "libVkLayer", "libglshim", "libc++")
|
||||
|
||||
|
||||
def engine_lib(ws) -> str | None:
|
||||
"""The library that starts EGL: the engine's, else the first of the game's libraries that imports eglInitialize."""
|
||||
lib = next((n for n in ENGINE_LIBS if ws.has(ws.lib(n))), None)
|
||||
if lib:
|
||||
return lib
|
||||
for name in ws.libs():
|
||||
if name.startswith(NOT_ENGINE):
|
||||
continue
|
||||
data = ws.read_lib(name)
|
||||
if elf.is_elf(data) and "eglInitialize" in elf.dyn_symbols(data, False):
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def configured(recipe_patches: dict) -> bool:
|
||||
params = recipe_patches.get(SETTING) or {}
|
||||
dump = (recipe_patches.get("adapter.zink_shader_dump") or {}).get("value")
|
||||
return bool(str(params.get("value") or "").strip()) or bool(dump)
|
||||
|
||||
|
||||
class ZinkShaderFix(Patch):
|
||||
id = "frame.zink_shader_fix"
|
||||
title = "OpenGL ES: shader fixes (Vulkan layer under Zink)"
|
||||
description = ("Loads FramePort's shader-fix Vulkan layer into an OpenGL ES game. On the Frame, OpenGL ES runs on "
|
||||
"Zink (Mesa's GL on Vulkan); the layer sits between Zink and the driver and inserts the words of "
|
||||
"the adapter setting zink_shader_fix into the SPIR-V modules whose size and SHA-256 match (every "
|
||||
"other module is untouched), e.g. stores that set loop counters and accumulators a shader reads "
|
||||
"before writing, which hang the GPU (Vader Immortal's lightspeed jump; found by Klownicle, GitHub "
|
||||
"#49). Does nothing without zink_shader_fix (or zink_shader_dump) in the recipe. The layer adds "
|
||||
"itself to Android's Vulkan debug layer list for the game's process only.")
|
||||
order = 73
|
||||
|
||||
def applies(self, a):
|
||||
return "GLES" in a.graphics and "arm64-v8a" in a.abis
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
ws = ctx.ws
|
||||
if ws.abi != "arm64-v8a" or not configured(ctx.recipe_patches):
|
||||
return False
|
||||
lib = engine_lib(ws)
|
||||
if not lib:
|
||||
ctx.notes.append(f"{self.id}: no library that starts EGL found, layer not added")
|
||||
return False
|
||||
layer = artifact(ws.abi, LAYER)
|
||||
changed = False
|
||||
data = ws.read_lib(lib)
|
||||
if LAYER not in elf.needed(data):
|
||||
ws.put(ws.lib(lib), elf.add_needed(data, LAYER))
|
||||
changed = True
|
||||
if not ws.has(ws.lib(LAYER)) or ws.read_lib(LAYER) != layer:
|
||||
ws.put(ws.lib(LAYER), layer)
|
||||
changed = True
|
||||
if changed:
|
||||
ctx.notes.append(f"shader-fix Vulkan layer loaded by {lib}")
|
||||
return changed
|
||||
|
||||
def validate(self, ctx: ApkContext):
|
||||
ws = ctx.ws
|
||||
if ws.abi != "arm64-v8a" or not configured(ctx.recipe_patches):
|
||||
return []
|
||||
lib = engine_lib(ws)
|
||||
loaded = bool(lib) and LAYER in elf.needed(ws.read_lib(lib))
|
||||
return [("Shader-fix layer present and loaded", loaded and ws.has(ws.lib(LAYER)), lib or "no EGL library")]
|
||||
|
||||
|
||||
register(ZinkShaderFix)
|
||||
@@ -33,6 +33,10 @@ SETTINGS = [
|
||||
"tracking (e.g. Silhouette)."),
|
||||
("swapchain_fix", "int", 1, "Swapchain format fallback",
|
||||
"Retry rejected GLES formats/MSAA with sRGB, samples=1."),
|
||||
("cube_standin", "int", 1, "Stand-in cube swapchains",
|
||||
"The Frame's runtime has no cube-map layers and refuses their swapchains; OVRPlugin then crashes when it submits "
|
||||
"the next frame (e.g. Budget Cuts Ultimate). FrameBridge serves such a swapchain itself (a GL cube map, GLES "
|
||||
"games) and drops its layers, so the game runs on without that layer."),
|
||||
("layer_fix", "int", 1, "Drop invalid layers",
|
||||
"Drop layers whose swapchain failed or whose extension isn't enabled."),
|
||||
("passthrough_emul", "int", 1, "Emulate passthrough",
|
||||
@@ -129,6 +133,21 @@ SETTINGS = [
|
||||
"Repeat xrLocateViews queries for the same display time get the first, fully tracked answer again. Games that "
|
||||
"ask several times per frame got slightly different poses and rendered parts of the frame with different heads: "
|
||||
"judder (e.g. I Am Cat; proposed by Klownicle, GitHub #8)."),
|
||||
("pose_time_fix", "int", 0, "Locate poses at the right time",
|
||||
"Poses a game asks for at a CLOCK_MONOTONIC time, or more than 0.5 s before the frame's display time, are located "
|
||||
"at the same moment in the runtime's time (or now). Meta's OVRPlugin passes the monotonic clock on as the "
|
||||
"OpenXR time, which on the Frame runs seconds behind the runtime's clock, so its \"now\" poses (Unity's physics "
|
||||
"step, controller poses) came from the past: hands trailing the controllers (e.g. BattleSisters, Sniper Elite "
|
||||
"VR)."),
|
||||
("proximity_emul", "int", 0, "Finger proximity from touch",
|
||||
"Meta's OVRPlugin reports a thumb (or index finger) resting near the controller through "
|
||||
"XR_FB_touch_controller_proximity, which the Frame's runtime lacks: \"near\" stays false and games that animate "
|
||||
"the hands from it show a thumb that never moves (e.g. Vader Immortal; found by Klownicle, GitHub #49). Binds the "
|
||||
"game's proximity actions to the capacitive touch inputs instead: 1 = thumb (thumbstick, face buttons or thumb "
|
||||
"rest touched), 2 = also the index finger (trigger touched). Only when the runtime lacks the extension."),
|
||||
("pose_debug", "int", 0, "Pose time diagnostics (log)",
|
||||
"Every 5 s, logs per located space how far the requested times are from the frame's predicted display time, and "
|
||||
"the runtime clock's offset from the monotonic clock. No effect on the game."),
|
||||
("haptic_fix", "int", 0, "Fix controller vibration freezes",
|
||||
"Routes OVRPlugin through FramePort's extension shim, which turns Meta's amplitude-envelope vibrations into "
|
||||
"plain ones: OVRPort's loader reads their nanosecond duration as seconds and allocates gigabytes, freezing the "
|
||||
@@ -148,6 +167,16 @@ SETTINGS = [
|
||||
"e.g. stores that initialize locals a shader reads before writing (an undefined loop counter hung the GPU in "
|
||||
"VR4's campaign). Format: <size>:<sha256>:<byte offset>:<word>,<word>,...; several separated by ';'. Comes from "
|
||||
"a game's recipe."),
|
||||
("zink_shader_fix", "str", "", "OpenGL ES shader fixes",
|
||||
"Shader-fix Vulkan layer (frame.zink_shader_fix) for OpenGL ES games, whose GLSL the Frame's GL driver (Zink) "
|
||||
"turns into SPIR-V: modules matching size + SHA-256 get words inserted at a byte offset, e.g. stores that set "
|
||||
"loop counters and accumulators a shader reads before writing (Vader Immortal's lightspeed jump hung the GPU; "
|
||||
"found by Klownicle, GitHub #49). Same format as vk_shader_fix. Comes from a game's recipe; the driver's "
|
||||
"output can change with a Frame update, then the fix no longer matches (logged)."),
|
||||
("zink_shader_dump", "int", 0, "OpenGL ES shader dump (diagnostics)",
|
||||
"Shader-fix Vulkan layer (frame.zink_shader_fix): writes every distinct SPIR-V module the GL driver creates to "
|
||||
"Android/data/<package>/files/fp_spirv/<size>_<sha256>.spv, to capture a shader for a new zink_shader_fix. No "
|
||||
"effect on the game."),
|
||||
("vk_query_slots", "int", 0, "Vulkan shim: slots per occlusion query",
|
||||
"Vulkan shim (frame.vk_sanitize): on gives every occlusion query room for both eyes. In multiview passes the "
|
||||
"Frame's driver writes a zero result for the second eye into the next query, so engines that count on one slot "
|
||||
@@ -210,6 +239,13 @@ UI: dict[str, dict] = {
|
||||
"pose_consistency": dict(group="picture", level="advanced", label="Steadier head tracking",
|
||||
help="For games that judder or shimmer while you hold your head still (e.g. I Am Cat).",
|
||||
control=("switch",)),
|
||||
"pose_time_fix": dict(group="controllers", level="advanced", label="Fix hands lagging behind controllers",
|
||||
help="For games whose hands trail the controllers when you move them (e.g. BattleSisters).",
|
||||
control=("switch",)),
|
||||
"proximity_emul": dict(group="controllers", level="advanced", label="Animate fingers from touch",
|
||||
help="For games whose hands' thumbs never move when you touch the buttons or thumbstick "
|
||||
"(e.g. Vader Immortal).",
|
||||
control=("choice", [(0, "Off"), (1, "Thumbs"), (2, "Thumbs and index fingers")])),
|
||||
"haptic_fix": dict(group="controllers", level="advanced", label="Fix freezes when controllers vibrate",
|
||||
help="For games that freeze the Frame when a controller vibrates (e.g. Lucky's Tale). Needs a "
|
||||
"rebuild.",
|
||||
@@ -288,10 +324,11 @@ UI: dict[str, dict] = {
|
||||
help="Shows 3D 360° pictures flat if they look doubled.",
|
||||
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", "strip_color_bias", "snapshot",
|
||||
"strip_depth", "respace_kick", "layer_debug", "eye_debug", "input_diag", "release_wait", "vk_hide_fdm",
|
||||
"ovrp_begin_gate", "ovrp_hold_physics")},
|
||||
"foveation_fix", "hide_space_warp", "swapchain_fix", "cube_standin", "layer_fix", "gl_hide_multiview",
|
||||
"mutable_fix", "flip_quads", "swap_eyes", "vk_validation", "rect_clamp", "gl_hide_msrtt", "strip_color_bias",
|
||||
"snapshot", "strip_depth", "respace_kick", "layer_debug", "eye_debug", "input_diag", "release_wait",
|
||||
"vk_hide_fdm", "ovrp_begin_gate", "ovrp_hold_physics", "pose_debug",
|
||||
"zink_shader_dump")},
|
||||
}
|
||||
|
||||
|
||||
@@ -321,6 +358,11 @@ class AdapterSetting(Patch):
|
||||
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 == "pose_time_fix" and a.engine == "Unity" and "libOVRPlugin.so" in a.libs
|
||||
and not a.extra.get("oculus_xr_plugin") and "libOculusXRPlugin.so" not in a.libs):
|
||||
return Suggestion(True, "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 (e.g. BattleSisters, Sniper Elite VR).", {"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})
|
||||
@@ -356,7 +398,11 @@ class AdapterSetting(Patch):
|
||||
"vk_hide_fdm": lambda a: a.engine == "Unreal", # Unreal's Vulkan isn't always detected (Metro Awakening)
|
||||
"vk_query_slots": lambda a: a.engine == "Unreal" and ap.is_vulkan(a),
|
||||
"vk_validation": lambda a: a.engine == "Unreal",
|
||||
"zink_shader_fix": ap.is_gles,
|
||||
"zink_shader_dump": ap.is_gles,
|
||||
"haptic_fix": lambda a: "libOVRPlugin.so" in a.libs,
|
||||
"pose_time_fix": lambda a: "libOVRPlugin.so" in a.libs,
|
||||
"proximity_emul": lambda a: "libOVRPlugin.so" in a.libs,
|
||||
"vk_spec_fixes": lambda a: a.engine == "Unreal" and ap.is_vulkan(a),
|
||||
"controller_models": ap.may_use_render_models,
|
||||
**{k: ap.is_gles for k in ("equirect_emul", "equirect_face", "equirect_res", "equirect_flip",
|
||||
|
||||
@@ -68,6 +68,8 @@ SUMMARIES = {
|
||||
"frame.unity_runtime_msaa_off": "Keeps a smoothing setting off that the game turns on itself and that can freeze "
|
||||
"the Frame.",
|
||||
"frame.sdl_clipboard": "Lets SDL and LÖVE games start on the Frame (they'd crash looking for a clipboard).",
|
||||
"frame.vivox_audio_route": "Lets games with Vivox voice chat start on the Frame (they'd crash on an Android 12 "
|
||||
"audio call).",
|
||||
"frame.unity_oculus_check": "Lets older Unity games start VR without Meta's system apps (they'd stay on a 2D "
|
||||
"screen).",
|
||||
"frame.unity_multipass": "Draws each eye separately; try it when one eye shows a grey or broken picture.",
|
||||
@@ -85,6 +87,12 @@ SUMMARIES = {
|
||||
"frame.oculusos": "Provides stand-ins for Quest system reporting some games call at start.",
|
||||
"frame.unity_text_input": "Lets you type into this app's text fields on the Frame (they'd close at once).",
|
||||
"frame.vk_sanitize": "Cleans up graphics data that crashes some Unreal games on the Frame.",
|
||||
"frame.unreal_quest_precompile": "Lets the game leave its loading screen: it waited for a Quest-only step that "
|
||||
"never starts on the Frame (e.g. Vader Immortal).",
|
||||
"frame.unreal_quest_keymap": "Makes grip, trigger and touch controls reach the game's hands (it picked the "
|
||||
"empty Gear VR controls on the Frame, e.g. Vader Immortal).",
|
||||
"frame.zink_shader_fix": "Repairs shaders that hang the graphics chip at certain effects, from the game's "
|
||||
"recipe (e.g. Vader Immortal's lightspeed jump).",
|
||||
# Files and environment on the Frame
|
||||
"device.files": "Puts settings files next to the game on the Frame (e.g. to turn off an unsupported effect).",
|
||||
"device.hide_navbar": "Hides Android's back/home/recents buttons, which cover the app's own buttons.",
|
||||
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// pose_time_fix (native/adapter/pose_time.c) with synthetic clocks: which located times are moved, and where to.
|
||||
#define _GNU_SOURCE
|
||||
#include <assert.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#define LOG(...) ((void)0)
|
||||
typedef int64_t XrTime;
|
||||
typedef int64_t XrDuration;
|
||||
static XrTime last_predicted_time;
|
||||
static int64_t xr_time_offset;
|
||||
static int pose_time_fix = 1, pose_debug = 1;
|
||||
#include "pose_time.c"
|
||||
|
||||
#define MS 1000000ll
|
||||
|
||||
static long long mono_now(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000000000ll + ts.tv_nsec;
|
||||
}
|
||||
|
||||
// XrTime runs `offset` ahead of the monotonic clock; the frame being prepared shows two periods from now
|
||||
static void frame(long long offset) {
|
||||
xr_time_offset = offset;
|
||||
pt_ring_n = pt_ring_i = 0; // a new clock: fill the offset window with steady samples
|
||||
for (int i = 0; i < 10; i++) pose_time_sample(offset);
|
||||
last_predicted_time = mono_now() + offset + 21 * MS;
|
||||
}
|
||||
|
||||
static void near(XrTime got, long long want) {
|
||||
long long d = (long long)got - want;
|
||||
if (d < -5 * MS || d > 5 * MS) {
|
||||
fprintf(stderr, "got %lld, want %lld (%+lld ms)\n", (long long)got, want, d / MS);
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const long long offsets[] = {2564 * MS, 50 * MS, 900 * MS}; // SteamOS 0.4.5; the range seen on older builds
|
||||
for (unsigned i = 0; i < sizeof(offsets) / sizeof(*offsets); ++i) {
|
||||
long long offset = offsets[i];
|
||||
frame(offset);
|
||||
XrTime display = last_predicted_time;
|
||||
assert(pose_time_fixed(display) == display); // the frame's display time
|
||||
assert(pose_time_fixed(display + 30 * MS) == display + 30 * MS); // predicted further ahead
|
||||
long long mono = mono_now();
|
||||
near(pose_time_fixed(mono), mono + offset); // OVRPlugin's monotonic "now"
|
||||
near(pose_time_fixed(mono - 8 * MS), mono - 8 * MS + offset); // a monotonic time just before
|
||||
assert(pose_time_fixed(100 * MS) == display); // nonsense far in the past: the display time
|
||||
pose_time_sample(offset - 1500 * MS); // a hitch: xrWaitFrame returned late
|
||||
near(pose_time_fixed(mono), mono + offset); // the steady offset still wins
|
||||
}
|
||||
frame(2564 * MS);
|
||||
assert(pose_time_fixed(last_predicted_time - 100 * MS) == last_predicted_time - 100 * MS); // recent past: kept
|
||||
assert(pt_fixed_mono == 9 && pt_fixed_past == 3);
|
||||
pt_ring_n = 3; // too few samples yet: nothing is moved
|
||||
assert(pose_time_fixed(100 * MS) == 100 * MS);
|
||||
pt_ring_n = PT_RING < 10 ? PT_RING : 10;
|
||||
pt_fixed_mono = 9;
|
||||
last_display_period = 10 * MS; // the diagnostics count per space and start over after each report
|
||||
pose_time_note_offset(2564 * MS);
|
||||
pose_time_note("xrLocateSpace", 1, 2, last_predicted_time - 2564 * MS);
|
||||
pose_time_note("xrLocateSpace", 1, 2, last_predicted_time);
|
||||
assert(pt_stats[0].count == 2 && pt_stats[0].min == -2564 * MS && pt_stats[0].max == 0);
|
||||
pose_time_report();
|
||||
assert(!pt_stats[0].count && !pt_fixed_mono && !pt_offset_count);
|
||||
frame(2 * MS); // XrTime is the monotonic clock (a Quest): nothing to tell apart, nothing moved
|
||||
assert(pose_time_fixed(100 * MS) == 100 * MS);
|
||||
long long mono = mono_now();
|
||||
assert(pose_time_fixed(mono) == mono);
|
||||
frame(2564 * MS);
|
||||
pose_time_fix = 0;
|
||||
assert(pose_time_fixed(mono) == mono && pose_time_fixed(100 * MS) == 100 * MS);
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"""pose_time_fix (FrameBridge): located times from the monotonic clock or far in the past are moved, others kept."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from frameport.patches import base
|
||||
from frameport.patches.settings import adapter_settings
|
||||
|
||||
|
||||
def test_pose_time_fix_moves_monotonic_and_far_past_times(tmp_path):
|
||||
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||
if not cc or os.name == "nt":
|
||||
pytest.skip("needs a POSIX host C compiler")
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
binary = tmp_path / "pose-time-test"
|
||||
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread", "-I", str(root / "native/adapter"),
|
||||
str(root / "tests/fixtures/src/pose_time_test.c"), "-o", str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
|
||||
def _analysis(**kw):
|
||||
from frameport.core.models import Analysis
|
||||
|
||||
d = dict(package="com.x.game", version="1", label="X", abis=["arm64-v8a"], engine="Unity", xr="VrApi",
|
||||
graphics="GLES", direct_vrapi=False, libs=["libunity.so", "libOVRPlugin.so"], launcher_activity=None,
|
||||
has_info_category=True, meta_permissions=[], uses_glad_gl=False, unity_msaa_levels=0,
|
||||
oculus_os_classes=False, is_overport_output=False, debuggable=True, extra={})
|
||||
d.update(kw)
|
||||
return Analysis(**d)
|
||||
|
||||
|
||||
def test_pose_time_fix_suggested_for_unity_builtin_oculus_games():
|
||||
base.load_all()
|
||||
p = base.REGISTRY["adapter.pose_time_fix"]
|
||||
s = p.detect(_analysis(extra={"oculus_xr_plugin": False}))
|
||||
assert s and s.recommended and s.params == {"value": 1}
|
||||
assert p.detect(_analysis(extra={"oculus_xr_plugin": True})) is None # Oculus XR Plugin: not measured
|
||||
assert p.detect(_analysis(libs=["libunity.so", "libOVRPlugin.so", "libOculusXRPlugin.so"], extra={})) is None
|
||||
assert p.detect(_analysis(engine="Unreal", libs=["libUE4.so", "libOVRPlugin.so"], extra={})) is None
|
||||
assert not p.applies(_analysis(libs=["libunity.so"], extra={}))
|
||||
assert adapter_settings({"adapter.pose_time_fix": {"value": 1}})["pose_time_fix"] == 1
|
||||
assert base.REGISTRY["adapter.pose_debug"].default == 0
|
||||
@@ -32,6 +32,17 @@ def test_failures_map_to_patches():
|
||||
assert r.verdict == "fail"
|
||||
|
||||
|
||||
def test_shader_fix_layer_findings():
|
||||
log = LOG_OK + ("09-28 17:39:06.000 1147 1174 I FrameBridge: shader fix layer: a 12016-byte module differs"
|
||||
" from fix 1 (another build?)\n"
|
||||
"09-28 17:39:06.000 1147 1147 I FrameBridge: shader fix layer: NOT active, Android's GraphicsEnv"
|
||||
" functions not found (setDebugLayers)\n")
|
||||
r = triage(log, "RUNNING", "com.example.game")
|
||||
ids = {f.id for f in r.findings}
|
||||
assert {"zink-shader-fix-mismatch", "zink-shader-layer-inactive"} <= ids
|
||||
assert "adapter.zink_shader_dump" in r.suggestions()
|
||||
|
||||
|
||||
def test_launcher_signature():
|
||||
r = triage("lepton: APP_ACTIVITY is empty\n", "NEVER_STARTED")
|
||||
assert r.suggestions() == ["frame.launcher"]
|
||||
@@ -212,3 +223,39 @@ def test_slz_vulkan_hook_crash_is_recognised():
|
||||
assert f.suggest == ["frame.slz_vulkan_hooks"] and "java-crash" not in [x.id for x in r.findings]
|
||||
other = triage(log.replace("SLZ Graphics", "Other"), "EXITED", None)
|
||||
assert "slz-vulkan-hook-crash" not in [x.id for x in other.findings]
|
||||
|
||||
|
||||
def test_lepton3_transient_not_running_context_is_not_a_failure():
|
||||
"""Lepton 3.0.5 prints "is not a running context" while the container is still starting, then boots (VR4 ran
|
||||
at 72 fps); only a container that never boots is a failure."""
|
||||
from frameport.validate import triage
|
||||
|
||||
err = "ERROR: 'steamlaunch-2272591617' is not a running context, use 'lepton ps' to list them, like this:\n"
|
||||
booted = triage.triage("Waiting for boot...\n" + err + "Boot complete!\n", "RUNNING")
|
||||
assert "container-not-started" not in [f.id for f in booted.findings]
|
||||
failed = triage.triage("Waiting for boot...\n" + err, "EXITED")
|
||||
assert "container-not-started" in [f.id for f in failed.findings]
|
||||
|
||||
|
||||
def test_refused_cube_swapchain_explains_the_render_crash():
|
||||
"""GitHub #107 (Budget Cuts Ultimate): the runtime refuses a cube swapchain, OVRPlugin crashes in EndFrame."""
|
||||
from frameport.validate.triage import triage as run
|
||||
|
||||
log = ("10-09 02:34:43.131 1149 1219 I FrameBridge: xrCreateSwapchain 2048x2048 format=35907 samples=1 array=1 "
|
||||
"faces=6 usage=0x21 flags=0x0 result=-2\n"
|
||||
"10-09 02:34:43.131 1149 1219 D OVRPlugin: CompositorOpenXR_GLES::Layer::Initialize(): CreateSwapchain "
|
||||
"for eye 0: 0x0, 0 stages\n"
|
||||
"10-09 02:34:43.900 1149 1219 F libc : Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR)\n"
|
||||
"10-09 02:34:44.134 1149 1223 E CRASH : #00 pc 0000000000a85a80 /vendor/lib64/dri/"
|
||||
"libgallium_dri.so (BuildId: 95)\n")
|
||||
r = run(log, "EXITED", "com.NeatCorporation.BudgetCutsUltimate")
|
||||
assert [f.id for f in r.findings] == ["cube-swapchain-refused"]
|
||||
assert r.findings[0].suggest == ["frame.adapter"]
|
||||
served = log + "I FrameBridge: cube_standin: runtime refused a 2048x2048 cube swapchain (result=-2): served\n"
|
||||
assert "cube-swapchain-refused" not in [f.id for f in run(served, "RUNNING", "x").findings]
|
||||
|
||||
|
||||
def test_cube_standin_is_a_default_on_adapter_setting():
|
||||
from frameport.patches.settings import SETTINGS
|
||||
|
||||
assert [s[2] for s in SETTINGS if s[0] == "cube_standin"] == [1]
|
||||
@@ -0,0 +1,236 @@
|
||||
"""ILMxLAB's Unreal Quest gates (GitHub #49, found by Klownicle in Vader Immortal: Episode I 1.1.1):
|
||||
frame.unreal_quest_precompile (GetQuestShaderPrecompilePercent's non-Quest branch returns 1.0) and
|
||||
frame.unreal_quest_keymap (the RPOC key selector keeps the Quest key set), plus frame.zink_shader_fix (the shader-fix
|
||||
Vulkan layer for GLES games) and the adapter's proximity_emul setting."""
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
from frameport.analysis import detect, elf
|
||||
from frameport.analysis.stubgen import build_stub_library
|
||||
from frameport.apk.workspace import ApkWorkspace
|
||||
from frameport.core.events import Reporter
|
||||
from frameport.core.models import Analysis
|
||||
from frameport.patches import base
|
||||
from frameport.patches.frame import unreal_santacruz as sc
|
||||
from frameport.patches.frame import zink_shader_fix
|
||||
from frameport.patches.settings import adapter_settings
|
||||
|
||||
FSC = "_ZN15FSantaCruzUtils20IsRunningOnSantaCruzEv"
|
||||
NINJA = "_ZN23UNTNinjaFunctionLibrary20IsRunningOnSantaCruzEv"
|
||||
SELECTOR = "_ZN27URPOCKeyMapManagerComponent11SelectKeysEv" # local in the game; named here for the test
|
||||
STP, MOV_FP, LDRB, RET, LDP = 0xA9BF7BFD, 0x910003FD, 0x39400100, 0xD65F03C0, 0xA8C17BFD
|
||||
|
||||
|
||||
def bl(pc, target):
|
||||
return 0x94000000 | ((target - pc) >> 2) & 0x3FFFFFF
|
||||
|
||||
|
||||
def b(pc, target):
|
||||
return 0x14000000 | ((target - pc) >> 2) & 0x3FFFFFF
|
||||
|
||||
|
||||
def tbz(pc, target):
|
||||
return 0x36000000 | (((target - pc) >> 2) & 0x3FFF) << 5
|
||||
|
||||
|
||||
def add_x0_x19(imm):
|
||||
return 0x91000260 | imm << 10
|
||||
|
||||
|
||||
TEXT = 0x1000
|
||||
|
||||
|
||||
def layout():
|
||||
"""Function addresses: each function gets 0x80 bytes from TEXT on."""
|
||||
names = [FSC, NINJA, sc.PRECOMPILE, SELECTOR, sc.ADD_AXIS, sc.ADD_ACTION]
|
||||
return {n: TEXT + 0x80 * i for i, n in enumerate(names)}
|
||||
|
||||
|
||||
def bodies(at, fmov=sc.FMOV_S0_ZERO, keymap_branch=None):
|
||||
p, s = at[sc.PRECOMPILE], at[SELECTOR]
|
||||
return {
|
||||
FSC: [LDRB, RET],
|
||||
NINJA: [b(at[NINJA], at[FSC])],
|
||||
# stp; mov; bl IsRunningOnSantaCruz; tbz w0, #0, L; ldp; b <precompile progress>; L: fmov s0, wzr; ldp; ret
|
||||
sc.PRECOMPILE: [STP, MOV_FP, bl(p + 8, at[FSC]), tbz(p + 12, p + 24), LDP, b(p + 20, at[FSC]), fmov, LDP, RET],
|
||||
# ...; bl IsRunningOnSantaCruz; tbz w0, #0, GearVR; add x0, x19, #0x38; b E; GearVR: add x0, x19, #0x68; b E;
|
||||
# E: ldp; ret
|
||||
SELECTOR: [STP, MOV_FP, bl(s + 8, at[NINJA]),
|
||||
keymap_branch if keymap_branch is not None else tbz(s + 12, s + 24),
|
||||
add_x0_x19(0x38), b(s + 20, s + 32), add_x0_x19(0x68), b(s + 28, s + 32), LDP, RET],
|
||||
sc.ADD_AXIS: [STP, bl(at[sc.ADD_AXIS] + 4, s), RET],
|
||||
sc.ADD_ACTION: [STP, bl(at[sc.ADD_ACTION] + 4, s), RET],
|
||||
}
|
||||
|
||||
|
||||
def make_engine(**kw) -> bytes:
|
||||
"""A minimal ELF64 (one PT_LOAD at vaddr 0 = file offset, .dynsym/.dynstr/.text sections) with these functions."""
|
||||
at = layout()
|
||||
text = bytearray(0x80 * len(at))
|
||||
for name, words in bodies(at, **kw).items():
|
||||
struct.pack_into(f"<{len(words)}I", text, at[name] - TEXT, *words)
|
||||
dynstr = bytearray(b"\0")
|
||||
syms = [bytes(24)]
|
||||
for name, addr in at.items():
|
||||
off = len(dynstr)
|
||||
dynstr += name.encode() + b"\0"
|
||||
syms.append(struct.pack("<IBBHQQ", off, 0x12, 0, 3, addr, 0x80)) # GLOBAL FUNC in section 3 (.text)
|
||||
dynsym = b"".join(syms)
|
||||
shstr = b"\0.dynsym\0.dynstr\0.text\0.shstrtab\0"
|
||||
out = bytearray(TEXT)
|
||||
out += text
|
||||
dynstr_off = len(out)
|
||||
out += dynstr
|
||||
while len(out) % 8:
|
||||
out += b"\0"
|
||||
dynsym_off = len(out)
|
||||
out += dynsym
|
||||
shstr_off = len(out)
|
||||
out += shstr
|
||||
while len(out) % 8:
|
||||
out += b"\0"
|
||||
sh_off = len(out)
|
||||
|
||||
def sh(name, typ, off, size, link=0, entsize=0, flags=0, addr=0):
|
||||
return struct.pack("<IIQQQQIIQQ", name, typ, flags, addr, off, size, link, 1 if typ == 11 else 0, 8, entsize)
|
||||
|
||||
out += bytes(64)
|
||||
out += sh(1, 11, dynsym_off, len(dynsym), link=2, entsize=24, flags=2, addr=dynsym_off)
|
||||
out += sh(9, 3, dynstr_off, len(dynstr), flags=2, addr=dynstr_off)
|
||||
out += sh(17, 1, TEXT, len(text), flags=6, addr=TEXT)
|
||||
out += sh(23, 3, shstr_off, len(shstr))
|
||||
ident = b"\x7fELF\x02\x01\x01" + bytes(9)
|
||||
struct.pack_into("<16sHHIQQQIHHHHHH", out, 0, ident, 3, 183, 1, 0, 64, sh_off, 0, 64, 56, 1, 64, 5, 4)
|
||||
struct.pack_into("<IIQQQQQQ", out, 64, 1, 5, 0, 0, 0, sh_off + 5 * 64, sh_off + 5 * 64, 0x4000)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _analysis(**extra):
|
||||
return Analysis(package="com.example.ilm", version="1.0", label="ILM Game", abis=["arm64-v8a"],
|
||||
engine="Unreal", xr="VrApi", graphics="GLES or unknown (no Vulkan declaration)",
|
||||
direct_vrapi=False, libs=["libOVRPlugin.so", "libUE4.so"], launcher_activity=None,
|
||||
has_info_category=False, meta_permissions=[], uses_glad_gl=False, unity_msaa_levels=0,
|
||||
oculus_os_classes=False, is_overport_output=True, debuggable=False, extra={"size": 1, **extra})
|
||||
|
||||
|
||||
def _apk(tmp_path, manifest, engine):
|
||||
p = tmp_path / "in.apk"
|
||||
with zipfile.ZipFile(p, "w") as z:
|
||||
z.writestr("AndroidManifest.xml", manifest)
|
||||
z.writestr("classes.dex", b"dex\n035\0")
|
||||
z.writestr("lib/arm64-v8a/libUE4.so", engine)
|
||||
return p
|
||||
|
||||
|
||||
def word_at(data, va):
|
||||
return struct.unpack_from("<I", data, elf.vaddr_to_offset(elf.load_segments(data), va))[0]
|
||||
|
||||
|
||||
def test_find_symbols_reads_dynsym_by_name():
|
||||
data, at = make_engine(), layout()
|
||||
found = elf.find_symbols(data, [sc.PRECOMPILE, FSC, "missing"])
|
||||
assert found == {sc.PRECOMPILE: (at[sc.PRECOMPILE], 0x80), FSC: (at[FSC], 0x80)}
|
||||
|
||||
|
||||
def test_sites_are_found_at_the_checked_instructions():
|
||||
data, at = make_engine(), layout()
|
||||
assert sc.precompile_site(data) == (at[sc.PRECOMPILE] + 24, sc.FMOV_S0_ZERO)
|
||||
assert sc.keymap_site(data) == (at[SELECTOR] + 12, tbz(at[SELECTOR] + 12, at[SELECTOR] + 24))
|
||||
assert detect.unreal_quest_gates(data) == ["quest_precompile", "rpoc_keymap"]
|
||||
assert detect.unreal_quest_gates(build_stub_library(["x"], soname="libUE4.so")) == []
|
||||
|
||||
|
||||
def test_other_code_is_left_alone():
|
||||
assert sc.precompile_site(make_engine(fmov=0x1E270000)) is None # not fmov s0, wzr: another build
|
||||
assert sc.keymap_site(make_engine(keymap_branch=0x37000000)) is None # tbnz: not the expected branch
|
||||
assert sc.precompile_site(build_stub_library([sc.PRECOMPILE], soname="libUE4.so")) is None # mov x0, #0; ret
|
||||
|
||||
|
||||
def test_patches_rewrite_one_instruction_each_and_only_once(tmp_path, quest_manifest):
|
||||
data, at = make_engine(), layout()
|
||||
a = _analysis(unreal_quest_gates=["quest_precompile", "rpoc_keymap"])
|
||||
for pid in ("frame.unreal_quest_precompile", "frame.unreal_quest_keymap"):
|
||||
patch = base.get(pid)
|
||||
assert patch.applies(a) and patch.detect(a).recommended
|
||||
assert not patch.applies(_analysis(unreal_quest_gates=[]))
|
||||
assert patch.applies(_analysis()) # analysed by an older FramePort: maybe
|
||||
with ApkWorkspace(_apk(tmp_path, quest_manifest, data)) as ws:
|
||||
ctx = base.ApkContext(ws, a, {}, Reporter(), {})
|
||||
pre, key = base.get("frame.unreal_quest_precompile"), base.get("frame.unreal_quest_keymap")
|
||||
assert pre.apply(ctx) and key.apply(ctx)
|
||||
out = ws.read_lib("libUE4.so")
|
||||
assert word_at(out, at[sc.PRECOMPILE] + 24) == sc.FMOV_S0_ONE
|
||||
assert word_at(out, at[SELECTOR] + 12) == sc.NOP
|
||||
diff = [i for i in range(0, len(data), 4) if data[i:i + 4] != out[i:i + 4]]
|
||||
assert len(diff) == 2 # nothing else changed
|
||||
assert not pre.apply(ctx) and not key.apply(ctx) # already patched: no-op
|
||||
assert pre.validate(ctx)[0][1] and key.validate(ctx)[0][1]
|
||||
assert any("non-Quest branch returns 1.0" in n for n in ctx.notes)
|
||||
|
||||
|
||||
def test_unexpected_code_is_reported_not_patched(tmp_path, quest_manifest):
|
||||
data = make_engine(fmov=0x1E270000)
|
||||
with ApkWorkspace(_apk(tmp_path, quest_manifest, data)) as ws:
|
||||
ctx = base.ApkContext(ws, _analysis(), {}, Reporter(), {})
|
||||
assert not base.get("frame.unreal_quest_precompile").apply(ctx)
|
||||
assert ws.read_lib("libUE4.so") == data
|
||||
assert any("doesn't match" in n for n in ctx.notes)
|
||||
|
||||
|
||||
def test_analysis_records_the_gates(tmp_path, quest_manifest):
|
||||
a = detect.analyze(_apk(tmp_path, quest_manifest, make_engine()))
|
||||
assert a.extra["unreal_quest_gates"] == ["quest_precompile", "rpoc_keymap"]
|
||||
assert a.extra["analysis_version"] == detect.ANALYSIS_VERSION >= 4
|
||||
|
||||
|
||||
def test_shader_fix_layer_is_loaded_only_when_configured(tmp_path, quest_manifest):
|
||||
engine = build_stub_library(["eglInitialize_user"], soname="libUE4.so")
|
||||
patch = base.get("frame.zink_shader_fix")
|
||||
a = _analysis()
|
||||
assert patch.applies(a) and not patch.detect(a)
|
||||
with ApkWorkspace(_apk(tmp_path, quest_manifest, engine)) as ws:
|
||||
assert not patch.apply(base.ApkContext(ws, a, {}, Reporter(), {patch.id: {}})) # no fix configured
|
||||
assert not ws.has(ws.lib(zink_shader_fix.LAYER))
|
||||
recipe = {patch.id: {}, "adapter.zink_shader_fix": {"value": "8:" + "0" * 64 + ":20:1"}}
|
||||
ctx = base.ApkContext(ws, a, {}, Reporter(), recipe)
|
||||
assert patch.apply(ctx)
|
||||
assert elf.needed(ws.read_lib("libUE4.so"))[0] == zink_shader_fix.LAYER
|
||||
assert ws.has(ws.lib(zink_shader_fix.LAYER))
|
||||
assert patch.validate(ctx)[0][1]
|
||||
assert not patch.apply(ctx)
|
||||
assert adapter_settings(recipe)["zink_shader_fix"].startswith("8:")
|
||||
|
||||
|
||||
def test_shader_fix_layer_exports_what_androids_loader_looks_up():
|
||||
from frameport.patches.frame import artifact
|
||||
|
||||
layer = artifact("arm64-v8a", zink_shader_fix.LAYER)
|
||||
exports = elf.dyn_symbols(layer, True)
|
||||
assert {"vkEnumerateInstanceLayerProperties", "vkEnumerateInstanceExtensionProperties",
|
||||
"VK_LAYER_FP_shader_fixGetInstanceProcAddr", "VK_LAYER_FP_shader_fixGetDeviceProcAddr"} <= exports
|
||||
# never the plain entry points: the engine library loads the layer first and would resolve Vulkan to it
|
||||
assert not {"vkGetInstanceProcAddr", "vkCreateInstance", "vkCreateShaderModule"} & exports
|
||||
assert b"VK_LAYER_FP_shader_fix\0" in layer and b"zink_shader_fix=" in layer
|
||||
|
||||
|
||||
def test_proximity_emul_setting():
|
||||
patch = base.get("adapter.proximity_emul")
|
||||
assert patch.applies(_analysis()) and not patch.detect(_analysis())
|
||||
assert adapter_settings({"adapter.proximity_emul": {"value": 1}})["proximity_emul"] == 1
|
||||
from frameport.patches.frame import artifact
|
||||
|
||||
adapter = artifact("arm64-v8a", "libopenxr_loader_generic.so")
|
||||
assert b"proximity_emul=%f" in adapter and b"hand_thumb_proximity" in adapter
|
||||
|
||||
|
||||
def test_vader_recipe_from_the_catalog():
|
||||
from frameport.recommend import catalog
|
||||
|
||||
entry = catalog.load()["com.ILMxLAB.VaderImmortal.ep1"]
|
||||
assert {"frame.unreal_quest_precompile", "frame.unreal_quest_keymap", "frame.zink_shader_fix"} <= set(entry.frame)
|
||||
assert entry.adapter["pose_time_fix"] == 1 and entry.adapter["proximity_emul"] == 1
|
||||
fixes = entry.adapter["zink_shader_fix"].split(";")
|
||||
assert [f.split(":")[0] for f in fixes] == ["12016", "11436"]
|
||||
for f in fixes: # twelve OpStores (3 words each) after all OpVariables
|
||||
words = f.split(":")[3].split(",")
|
||||
assert len(words) == 36 and words[::3] == ["0x0003003e"] * 12
|
||||
@@ -0,0 +1,158 @@
|
||||
"""frame.vivox_audio_route (GitHub #101, Green Hell VR): Vivox's AudioChangeListener calls Android 12 AudioManager
|
||||
methods; the methods that call them return at once (in-place dex edit)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from frameport.apk.dex import Dex
|
||||
from frameport.patches.frame.vivox_audio_route import LISTENER, VivoxAudioRoute, patch_dex
|
||||
from frameport.validate.triage import triage
|
||||
|
||||
|
||||
def _uleb(n: int) -> bytes:
|
||||
out = bytearray()
|
||||
while True:
|
||||
b = n & 0x7F
|
||||
n >>= 7
|
||||
out.append(b | (0x80 if n else 0))
|
||||
if not n:
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def build_dex(methods: list[tuple[str, str, str, list[int] | None]]) -> bytes:
|
||||
"""A minimal dex: methods = (class, name, return type, code units or None for a referenced-only method). Every
|
||||
method with code belongs to one class_def per class (virtual methods)."""
|
||||
strings: list[str] = []
|
||||
|
||||
def s(x: str) -> int:
|
||||
if x not in strings:
|
||||
strings.append(x)
|
||||
return strings.index(x)
|
||||
|
||||
types: list[int] = []
|
||||
|
||||
def t(x: str) -> int:
|
||||
i = s(x)
|
||||
if i not in types:
|
||||
types.append(i)
|
||||
return types.index(i)
|
||||
|
||||
protos: list[tuple[int, int]] = []
|
||||
|
||||
def p(ret: str) -> int:
|
||||
key = (s(ret if len(ret) == 1 else "L"), t(ret))
|
||||
if key not in protos:
|
||||
protos.append(key)
|
||||
return protos.index(key)
|
||||
|
||||
mids = [(t(c), p(r), s(n)) for c, n, r, _ in methods]
|
||||
header = 0x70
|
||||
off = header
|
||||
sid_off = off
|
||||
off += 4 * len(strings)
|
||||
tid_off = off
|
||||
off += 4 * len(types)
|
||||
pid_off = off
|
||||
off += 12 * len(protos)
|
||||
mid_off = off
|
||||
off += 8 * len(mids)
|
||||
classes = sorted({c for c, _n, _r, code in methods if code is not None})
|
||||
cdef_off = off
|
||||
off += 32 * len(classes)
|
||||
data = bytearray()
|
||||
|
||||
def place(blob: bytes, align: int = 1) -> int:
|
||||
while (off + len(data)) % align:
|
||||
data.append(0)
|
||||
pos = off + len(data)
|
||||
data.extend(blob)
|
||||
return pos
|
||||
|
||||
str_offs = [place(_uleb(len(x)) + x.encode() + b"\0") for x in strings]
|
||||
code_offs = {}
|
||||
for i, (_c, _n, _r, code) in enumerate(methods):
|
||||
if code is not None:
|
||||
item = struct.pack("<4HII", 4, 1, 1, 0, 0, len(code)) + struct.pack(f"<{len(code)}H", *code)
|
||||
code_offs[i] = place(item, 4)
|
||||
class_data = {}
|
||||
for c in classes:
|
||||
idxs = [i for i, m in enumerate(methods) if m[0] == c and m[3] is not None]
|
||||
blob = _uleb(0) + _uleb(0) + _uleb(0) + _uleb(len(idxs))
|
||||
prev = 0
|
||||
for i in idxs:
|
||||
blob += _uleb(i - prev) + _uleb(1) + _uleb(code_offs[i])
|
||||
prev = i
|
||||
class_data[c] = place(blob)
|
||||
out = bytearray(header)
|
||||
out[:8] = b"dex\n035\0"
|
||||
struct.pack_into("<10I", out, 56, len(strings), sid_off, len(types), tid_off, len(protos), pid_off, 0, 0,
|
||||
len(mids), mid_off)
|
||||
struct.pack_into("<2I", out, 96, len(classes), cdef_off)
|
||||
out += b"".join(struct.pack("<I", o) for o in str_offs)
|
||||
out += b"".join(struct.pack("<I", i) for i in types)
|
||||
out += b"".join(struct.pack("<3I", a, b, 0) for a, b in protos)
|
||||
out += b"".join(struct.pack("<HHI", *m) for m in mids)
|
||||
out += b"".join(struct.pack("<8I", t(c), 1, 0, 0, 0, 0, class_data[c], 0) for c in classes)
|
||||
out += data
|
||||
return bytes(out)
|
||||
|
||||
|
||||
AM = "Landroid/media/AudioManager;"
|
||||
# method indices: 0 getAvailableCommunicationDevices, 1 getCommunicationDevice (both referenced only)
|
||||
CHECK = [0x011D, 0x1054, 0x0000, 0x106E, 0, 0x0000, 0x000C, 0x000E] # monitor-enter; iget-object; invoke; move-result
|
||||
SET = [0x1054, 0x0000, 0x106E, 0, 0x0000, 0x000A, 0x000F] # iget-object; invoke; move-result v0; return v0
|
||||
GET = [0x106E, 1, 0x0000, 0x000C, 0x0011] # invoke; move-result-object; return-object
|
||||
OTHER = [0x000E]
|
||||
|
||||
|
||||
def _dex() -> bytes:
|
||||
return build_dex([(AM, "getAvailableCommunicationDevices", "Ljava/util/List;", None),
|
||||
(AM, "getCommunicationDevice", "Landroid/media/AudioDeviceInfo;", None),
|
||||
(LISTENER, "checkAudioRouteAndApplyChanges", "V", CHECK),
|
||||
(LISTENER, "setCommunicationDevice", "Z", SET),
|
||||
(LISTENER, "getCommunicationDevice", "Lcom/vivox/sdk/jni/VxaRenderRoute;", GET),
|
||||
(LISTENER, "setContext", "V", OTHER)])
|
||||
|
||||
|
||||
def _code(dex: Dex, name: str) -> list[int]:
|
||||
off = dex.code_items(LISTENER, name)[0]
|
||||
n = struct.unpack_from("<I", dex.data, off + 12)[0]
|
||||
return list(struct.unpack_from(f"<{n}H", dex.data, off + 16))
|
||||
|
||||
|
||||
def test_vivox_methods_return_early_in_place():
|
||||
raw = _dex()
|
||||
dex = Dex(raw)
|
||||
assert dex.return_type(2) == "V" and dex.return_type(3) == "Z"
|
||||
assert sorted(patch_dex(dex)) == ["checkAudioRouteAndApplyChanges", "setCommunicationDevice"]
|
||||
assert _code(dex, "checkAudioRouteAndApplyChanges") == [0x000E] + CHECK[1:] # monitor-enter -> return-void
|
||||
assert _code(dex, "setCommunicationDevice") == [0x0012, 0x000F] + SET[2:] # const/4 v0, 0; return v0
|
||||
assert _code(dex, "getCommunicationDevice") == GET # object return: left alone
|
||||
assert _code(dex, "setContext") == OTHER
|
||||
out = dex.finish()
|
||||
assert len(out) == len(raw) and patch_dex(Dex(out)) == [] # same size; a second run changes nothing
|
||||
|
||||
|
||||
def test_return_early_pads_to_an_instruction_boundary():
|
||||
dex = Dex(build_dex([(LISTENER, "f", "I", [0x0013, 0x0005, 0x000F])])) # const/16 v0, 5; return v0
|
||||
assert dex.return_early(dex.code_items(LISTENER, "f")[0], "I") is True
|
||||
assert _code(dex, "f") == [0x0012, 0x000F, 0x000F]
|
||||
dex = Dex(build_dex([(LISTENER, "g", "V", [0x0018, 1, 2, 3, 4, 0x000E])])) # const-wide (5 units)
|
||||
assert dex.return_early(dex.code_items(LISTENER, "g")[0], "V") is True
|
||||
assert _code(dex, "g") == [0x000E, 0, 0, 0, 0, 0x000E]
|
||||
assert dex.return_early(dex.code_items(LISTENER, "g")[0], "J") is False
|
||||
|
||||
|
||||
def test_detect_and_triage():
|
||||
class A:
|
||||
extra = {"vivox_api31": True}
|
||||
assert VivoxAudioRoute().detect(A()).recommended
|
||||
A.extra = {}
|
||||
assert VivoxAudioRoute().detect(A()) is None
|
||||
log = ("10-08 21:00:01.000 900 950 E AndroidRuntime: FATAL EXCEPTION: main\n"
|
||||
"10-08 21:00:01.000 900 950 E AndroidRuntime: java.lang.NoSuchMethodError: No virtual method "
|
||||
"getAvailableCommunicationDevices()Ljava/util/List; in class Landroid/media/AudioManager; or its super "
|
||||
"classes (declaration of 'android.media.AudioManager' appears in /system/framework/framework.jar)\n")
|
||||
r = triage(log, "EXITED", "com.Incuvo.GreenHellVR")
|
||||
assert [f.id for f in r.findings] == ["vivox-api31"]
|
||||
assert r.findings[0].suggest == ["frame.vivox_audio_route"]
|
||||
Reference in new issue
Block a user