mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-17 19:01:24 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a85e262c12 | ||
|
|
1f8c86b291 |
No files matched your search
@@ -11,13 +11,13 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
0f058921e9c1f5b9ef5847cc9e6b80a23e75028c76c1d3bb02d1610b535f81c9 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
8e34dc3b191c64bf0cc1aa2276fe6e2cc1a0c5ccbd3a8ad8bd9d041e2101915c ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
9e8a7ce599b62a7c607b0d3637e4ec1b1ea1974c3373a7af3f5920ad2adc5e00 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
f4d513c6359bed58dcd857f34bffec9212f26dd94318d1892824b4548e736c83 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
-7
@@ -71,13 +71,6 @@ signatures:
|
||||
severity: fatal
|
||||
diagnosis: This app is a website, not a game, and it can't open on the Frame. It is a Trusted Web Activity that opens Meta's browser (com.oculus.browser), which the Frame doesn't have; open the website (TWALauncherActivity's "Using URL" line) in a browser instead.
|
||||
suggest: []
|
||||
- id: gvr-not-supported
|
||||
# GitHub #335 (Blade Runner: Revelations, Daydream, Unity 2017.4): Unity's "Google VR Error [Daydream]", the GVR
|
||||
# library's Java side missing on the 32-bit bridge, or a bind to Daydream's VrCore service
|
||||
pattern: 'Google VR Error \[(Daydream|Cardboard)\]|Google VR had a fatal issue while loading|class com/google/vr/sdk/base/CardboardViewNativeImpl not found|act=com\.google\.vr\.vrcore\.\S+ pkg=com\.google\.vr\.vrcore'
|
||||
severity: fatal
|
||||
diagnosis: This is a Google Daydream / Cardboard VR build (Google VR SDK), and its VR can't reach the Frame's headset. Daydream needs Google's VR services (VrCore) and Cardboard draws for a phone in a viewer; FramePort has no translation for either, so the game runs as a flat window at best, usually without working controls. An Oculus build of the same game (Meta Quest, Oculus Go or Gear VR) goes through FramePort's normal path.
|
||||
suggest: []
|
||||
- id: unity-render-crash
|
||||
pattern: 'CRASH\s*:.*#\d+\s+pc [0-9a-f]+\s+\S*libgallium_dri\.so'
|
||||
severity: fatal
|
||||
@@ -250,6 +243,16 @@ signatures:
|
||||
severity: error
|
||||
diagnosis: The screen stays black because the game submits a layer the VrApi bridge can't show. Whole frames with that VrApi layer type are dropped.
|
||||
suggest: []
|
||||
- id: unreal-reserve-refused
|
||||
# GitHub #349: Unreal's out-of-memory handler dereferences its not yet created log while libUnreal.so's static
|
||||
# constructors run (System.loadLibrary), after the Binned3 allocator's up-front reservation failed
|
||||
pattern: '(?s)fault addr 0x10\b(?:(?!Fatal signal).){0,8000}?#00 pc [0-9a-f]+\s+\S*lib(?:Unreal|UE4)\.so(?:(?!Fatal signal).){0,8000}?soinfo17call_constructors'
|
||||
severity: fatal
|
||||
diagnosis: The game crashed while Unreal was still loading, before its first frame. Its memory allocator (Binned3)
|
||||
reserves tens of gigabytes of address space at once, which Quest allows and the Frame's kernel refuses in one
|
||||
piece (for example Transformers Beyond Reality). Rebuild with the reservation marked as not committed.
|
||||
suggest: [frame.unreal_noreserve]
|
||||
supersedes: [native-crash]
|
||||
- id: graphics-requirements-missing
|
||||
pattern: '(XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING|Failed to create XR session: -50\b|xrCreateSession[^\n]*-50\b)'
|
||||
severity: fatal
|
||||
@@ -326,6 +329,13 @@ signatures:
|
||||
diagnosis: 'The Frame''s Vulkan driver crashed on a call from Unreal. Unreal 5 passes things Quest''s driver ignores: a depth resolve in a subpass without a depth attachment (handled by the Vulkan shim), and image barriers and views for a missing image when it expects fragment density maps (foveation) from the headset (for example Metro Awakening).'
|
||||
suggest: [frame.vk_sanitize, adapter.vk_spec_fixes, adapter.vk_hide_fdm]
|
||||
supersedes: [native-crash]
|
||||
- id: quest-foveation-image-missing
|
||||
pattern: 'foveationImage = 0x0, width = 0, height = 0'
|
||||
unless: 'hide_fb_foveation: filtering dispatcher extensions'
|
||||
severity: info
|
||||
question: Did the Vulkan game stop on its first frames?
|
||||
diagnosis: OVRPlugin enabled Quest Vulkan foveation but received no density image. For a first-frame failure, compare with Quest foveation suppressed. This message alone does not prove the crash cause; a null Unreal graphics pipeline can also follow an earlier fatal error.
|
||||
suggest: [adapter.hide_fb_foveation]
|
||||
- id: unreal-fdm-missing
|
||||
pattern: 'vk shim: left out \d+ image barrier\(s\) without an image \(first: layout \d+ -> 1000218000'
|
||||
unless: 'vk shim: fragment density map extensions hidden'
|
||||
|
||||
+34
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 169 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
Every patch and game setting FramePort knows: 171 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
|
||||
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
|
||||
|
||||
@@ -11,8 +11,8 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
## Groups
|
||||
|
||||
- [Patches from OVRPort](#group-overport) (25)
|
||||
- [Steam Frame patches](#group-frame) (55)
|
||||
- [Game settings](#group-adapter) (68)
|
||||
- [Steam Frame patches](#group-frame) (56)
|
||||
- [Game settings](#group-adapter) (69)
|
||||
- [Files and settings on the Frame](#group-device) (8)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
|
||||
@@ -87,6 +87,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Unity: headset always counts as worn](#frame-unity_user_presence) | `frame.unity_user_presence` | Steam Frame patches | 1 |
|
||||
| [GL shim for Unreal (no multisampled render-to-texture)](#frame-unreal_gl_shim) | `frame.unreal_gl_shim` | Steam Frame patches | 1 |
|
||||
| [Unreal: match multiview depth to the eye size](#frame-unreal_multiview_depth) | `frame.unreal_multiview_depth` | Steam Frame patches | 1 |
|
||||
| [Unreal: reserve memory like on Quest](#frame-unreal_noreserve) | `frame.unreal_noreserve` | Steam Frame patches | |
|
||||
| [Unreal: stand-ins for OVRPlugin functions OVRPort lacks](#frame-unreal_ovrp_entrypoints) | `frame.unreal_ovrp_entrypoints` | Steam Frame patches | 2 |
|
||||
| [Unreal: Quest controller bindings (RPOC key map)](#frame-unreal_quest_keymap) | `frame.unreal_quest_keymap` | Steam Frame patches | 3 |
|
||||
| [Unreal: don't wait for the Quest shader precompile](#frame-unreal_quest_precompile) | `frame.unreal_quest_precompile` | Steam Frame patches | 3 |
|
||||
@@ -129,6 +130,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Flat panel diagnostics (log)](#adapter-gl_mv_debug) | `adapter.gl_mv_debug` | Game settings | |
|
||||
| [Fix freezes when controllers vibrate](#adapter-haptic_fix) | `adapter.haptic_fix` | Game settings | 88 |
|
||||
| [Vibration strength](#adapter-haptic_scale) | `adapter.haptic_scale` | Game settings | |
|
||||
| [Turn off Quest Vulkan foveation](#adapter-hide_fb_foveation) | `adapter.hide_fb_foveation` | Game settings | |
|
||||
| [Turn off space warp](#adapter-hide_space_warp) | `adapter.hide_space_warp` | Game settings | 5 |
|
||||
| [Controller input diagnostics (log)](#adapter-input_diag) | `adapter.input_diag` | Game settings | |
|
||||
| [Extra diagnostics (log)](#adapter-layer_debug) | `adapter.layer_debug` | Game settings | |
|
||||
@@ -1049,6 +1051,19 @@ Shows the full environment when its upper part is clipped but menu panels look c
|
||||
|
||||
> Some UE4 games keep the initial Android window size for their stereo depth buffer, clipping the upper part of the environment while menu panels look correct. Refreshes the cached depth dimensions from the current eye render target before allocation. Preserves multiview and multisampling. Verified in Tales from the Galaxy's Edge 1.1.9; changes one branch only when the function size and surrounding code match.
|
||||
|
||||
### <a id="frame-unreal_noreserve"></a>Unreal: reserve memory like on Quest
|
||||
|
||||
Lets the game load: it reserves more memory at start than the Frame grants in one piece and crashed before its first frame (for example Transformers Beyond Reality).
|
||||
|
||||
- `frame.unreal_noreserve` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unreal's Binned3 allocator reserves more memory at start than the Frame's kernel allows in one piece; without this the game crashes while it loads.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `unreal-reserve-refused` (fatal): The game crashed while Unreal was still loading, before its first frame.
|
||||
- **Source:** [patches/frame/unreal_noreserve.py](../src/frameport/patches/frame/unreal_noreserve.py)
|
||||
|
||||
> Unreal reserves its memory ranges on Android with ordinary read/write mappings. Games built with Unreal's Binned3 allocator reserve tens of gigabytes at once while the engine loads (76 GiB in Transformers Beyond Reality); Quest allows that, the Frame's kernel refuses mappings larger than its memory, and the game crashes before its first frame (SIGSEGV at fault addr 0x10 in libUnreal.so, inside System.loadLibrary). This marks those mappings MAP_NORESERVE, as Quest effectively treats them: memory is still only used when the game touches it.
|
||||
|
||||
### <a id="frame-unreal_ovrp_entrypoints"></a>Unreal: stand-ins for OVRPlugin functions OVRPort lacks
|
||||
|
||||
Lets older Unreal games start VR on the Frame (for example Star Wars Pinball VR).
|
||||
@@ -1652,6 +1667,22 @@ Turn it down if controller vibrations are too strong.
|
||||
|
||||
> Scales every controller vibration the game asks for (0-1): plain vibrations, Meta's amplitude envelopes and PCM buffers. For games whose vibrations feel much stronger than on a Quest.
|
||||
|
||||
### <a id="adapter-hide_fb_foveation"></a>Turn off Quest Vulkan foveation
|
||||
|
||||
Diagnostic for Vulkan games that fail on their first frames with missing Quest foveation images.
|
||||
|
||||
- `adapter.hide_fb_foveation`
|
||||
- **Changes:** Game setting: stored in the build and on the Frame (settings.conf); can be changed without a rebuild.
|
||||
- **Suggested:** not automatically; set on the game page (Customize), by a recipe or after a launch test.
|
||||
- **Options:**
|
||||
- Value: int, default `0`.
|
||||
- Game settings → Troubleshooting → "Turn off Quest Vulkan foveation" (advanced).
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `quest-foveation-image-missing` (info): OVRPlugin enabled Quest Vulkan foveation but received no density image.
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Hides Quest FB foveation extensions from the game and OVRPort, removes Vulkan foveation image/create chains, and refuses foveation profile updates. Diagnostic for first-frame failures with missing density images (for example Transformers Beyond Reality: Redux). Device verification pending; does not disable Valve foveation.
|
||||
|
||||
### <a id="adapter-hide_space_warp"></a>Turn off space warp
|
||||
|
||||
For games whose picture flickers or smears (for example Into The Radius 2).
|
||||
|
||||
+2
-1
@@ -87,7 +87,6 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| 32-bit Unity game on the bridge: player stands in the floor (Spunky Dungeon) | Unity's floor-level tracking origin: `vrapi_SetTrackingSpace(1)` (LOCAL_FLOOR) was ignored | The VrApi bridge puts the origin on the STAGE floor for LOCAL_FLOOR/STAGE |
|
||||
| Crash at start: `NoClassDefFoundError: … Landroid/window/OnBackInvokedCallback;` or `NoSuchMethodError: No static method storeStoreFence()V in class Ljava/lang/invoke/VarHandle;` (GitHub #71/#72, triage `android-too-new`) | The APK needs Android 13+ (minSdk 33 or more; those were 34) and calls its classes at start; Lepton's Android is 11 (API 30). OVRPort lowers minSdk to 29, so it installs anyway. minSdk ≤ 32 (Quest's Android 12L) is fine: 16 working catalog games declare 32 | None (analysis `extra.min_sdk` > 32 → a warning on the game page/CLI and in the recipe notes; never unsupported from the manifest alone, the launch test's triage decides). Waits for Valve to update Lepton's Android |
|
||||
| Nothing opens; `TWALauncherActivity: Using URL from Manifest (https://…)`, `Creating TwaLauncher for com.oculus.browser`, `NameNotFoundException: com.oculus.browser` (GitHub #86 Mahjong Table VR, triage `web-wrapper`) | The APK is a Trusted Web Activity (Bubblewrap / Meta's PWA packaging): a website that opens in Meta's browser, which Lepton doesn't have | None: open the URL in a browser. Analysis `extra.web_wrapper` (manifest meta-data `android.support.customtabs.trusted.DEFAULT_URL`, resolved through resources.arsc, or androidbrowserhelper's LauncherActivity) → a warning with the URL (not unsupported: the manifest alone never decides) |
|
||||
| A flat window, no VR and no way to interact; `Google VR Error [Daydream]: Google VR had a fatal issue while loading`, `class com/google/vr/sdk/base/CardboardViewNativeImpl not found` (32-bit bridge), `Unable to start service Intent { act=com.google.vr.vrcore.… }` (GitHub #335 Blade Runner: Revelations, triage `gvr-not-supported`) | A Google Daydream / Cardboard build: Google's VR SDK (GVR, `libgvr.so`) or the Cardboard SDK. Daydream needs Google's VrCore service, Cardboard draws a distorted side-by-side image for a phone screen; neither reaches OpenXR, the Frame's only way into the headset | None: get the game's Oculus build (Quest, Go or Gear VR), which goes through the normal path. Analysis `extra.google_vr` (`daydream`/`cardboard`: manifest categories `com.google.intent.category.DAYDREAM`/`CARDBOARD`, `enableVrMode="com.google.vr.vrcore/…"`, `libgvr.so`/Cardboard SDK libraries, GVR Java classes, Unity `vr-device-list`; not `libgvraudio.so`, which Quest games ship too) → a warning on the game page/CLI and in the recipe notes, only when the APK has no Oculus support; never unsupported from the manifest alone. A GVR → OpenXR shim (like the VrApi bridge) is possible in principle but not built |
|
||||
| `UnsatisfiedLinkError` / `cannot locate symbol "ovr_…"` | overport's platform loader lacks Meta platform functions | `frame.ovrstubs` (automatic, generated stubs) |
|
||||
| missing `ovrMessageType_ToString` | same, but the game needs a real string | `frame.ovrplatformcompat` (automatic) |
|
||||
| The game stays in its built-in language or waits at start although its OBB / files hold language packs (`<tag>.lang`, for example `de.lang`) | overport's platform loader answers `ovr_LanguagePack_GetCurrent/SetCurrent` with request id 0, so the game never gets the pack's path | `frame.langpacks` (opt-in, experimental, shown for games whose data has `*.lang`; `native/langpack`, build it with `python native/build.py --only langpack`). Default pack: the one the game applies, else env `FRAMEPORT_LANGPACK=<tag>`, else the only pack; search path override `FRAMEPORT_LANGPACK_DIRS`. Unreal games (Deadpool VR) gray out a pack whose asset `Metadata` differs from the game's version string: the patch writes the APK's versionName as Metadata (env `FRAMEPORT_LANGPACK_META` overrides) and reports the pack under the `/storage/emulated/0/Android/obb/<pkg>/` spelling of its path; with `/sdcard/…` the text switched but the dialogue stayed silent (verified in the headset with Deadpool VR) |
|
||||
@@ -188,6 +187,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Unity game crashes or hangs right as Vulkan starts, after `SLZ Graphics plugin loading!` and OVRPlugin's pre-init `xrDestroyInstance`: SIGSEGV with pc 0 (or pc == fault address in an unloaded library), x16/x23 in `libSLZQuestNative.so` (BONELAB 1.2974) | Stress Level Zero's graphics plugin hooks Unity's Vulkan start-up (IUnityGraphicsVulkanV2 interception: vkCreateInstance/vkCreateDevice) and vkCreateSampler; its vkCreateInstance wrapper calls an invalid pointer on the Frame | `frame.slz_vulkan_hooks` (its two registrations become no-ops; Unity starts Vulkan itself; ~35 s of shader prewarming without its pipeline cache) |
|
||||
| Unity game shows a picture but the player body is frozen: no head tracking, the controllers stay on the model, no buttons, although OpenXR input works (BONELAB 1.2068, Oculus XR Plugin) | OVRPlugin reports the headset as not worn (`ovrp_GetUserPresent2` → 0) a few seconds after start, and the game's rig only follows a present user (Marrow `XRHMD.IsUserPresent` = Unity's HMD `UserPresence`) | `frame.unity_user_presence` (the Oculus XR Plugin's lookup → `libfp_ovrp.so`, reports present; log `user presence: OVRPlugin 0 …`) |
|
||||
| Unreal 5 game crashes at its first render pass, backtrace `vulkan.freedreno.so` ← `libVkLayer_fossilize.so` ← `libfp_vk.so (create_render_pass2)` (Metro Awakening) | a subpass without a depth attachment names a depth/stencil resolve attachment; the Frame's Turnip reads the missing depth attachment (NULL) | the Vulkan shim drops that resolve (always, FramePort after 0.11.0; log `dropped the depth resolve of subpass N (no depth attachment)`); triage `unreal-vulkan-driver-crash` |
|
||||
| Unreal game crashes before its first frame: SIGSEGV `fault addr 0x10` on the main thread, backtrace all `libUnreal.so` above the linker's `soinfo::call_constructors` (inside `System.loadLibrary`), right after `Parsing commandline` (GitHub #349, Transformers Beyond Reality) | The game is built with Unreal's Binned3 allocator, which reserves its whole range while the library loads: one `mmap(NULL, 76 GiB, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS)`. Quest grants it; the Frame's kernel (heuristic overcommit, mode 0) refuses a writable private mapping larger than RAM + swap, and Unreal's out-of-memory handler ("Ran out of memory allocating ...") dereferences its not-yet-created log | `frame.unreal_noreserve` (suggested from analysis `unreal_binned3`): every such anonymous read/write mmap in the engine library gets `MAP_NORESERVE` (one instruction each: `mov w3, #0x22` → `#0x4022`); triage `unreal-reserve-refused`. Found by disassembling the crash pc (the OOM handler; its UTF-16 format string) and the caller's `mov x0, #0x1300000000` |
|
||||
| Unreal Vulkan game fails on its first frames and OVRPlugin reports `foveationImage = 0x0, width = 0, height = 0` (for example Transformers Beyond Reality: Redux, #349) | Quest FB foveation remains advertised by OVRPort's dispatcher despite `foveation_fix=1`. This is a hypothesis for the failure, not a confirmed cause: Redux's alternate no-ForceQuit build dereferences a null graphics-pipeline-state object during Vulkan pipeline binding, not a density image | Diagnostic `adapter.hide_fb_foveation=1` (off by default, Vulkan): filters the dispatcher and runtime extension lists/instance requests, removes foveation create/image chains and refuses profile creation/updates. Rebuild with the adapter **and xrshim** artifacts, then compare first-frame survival and Unreal's original fatal log. Does not disable Valve FDM injection. Device verification pending |
|
||||
| Unreal 5 game crashes in `vulkan.freedreno.so` called straight from `libUnreal.so` right after its swapchains, or a few seconds into the first frames (Metro Awakening) | Unreal turns on fragment-density-map foveation because Turnip offers VK_EXT_fragment_density_map, then records barriers (to `FRAGMENT_DENSITY_MAP_OPTIMAL`) and image views (2D array R8G8) for a density map the Frame never provides (VK_NULL_HANDLE); Turnip dereferences it | adapter `vk_hide_fdm=1` (the game doesn't see the extension) + `vk_spec_fixes=1` (leaves out null-image barriers, refuses null-image views); triage `unreal-fdm-missing`. Found by disassembling the driver at the crash pc: the struct offsets (0x58 = VkSubpassDescription2, 0x48/0x60 = image barriers, viewType/format in registers) name the call |
|
||||
| Own-engine game crashes in `je_free` from its own library on the first frames (Roblox) | the game frees memory it doesn't own; not the Vulkan shim or the format fallback | unsupported |
|
||||
|
||||
|
||||
@@ -34,3 +34,35 @@ static int fb_foveation_storage(XrSwapchainCreateInfo *info, FbStorageNode nodes
|
||||
}
|
||||
return p ? -1 : 0;
|
||||
}
|
||||
|
||||
// Strip FB nodes behind known Vulkan allocation nodes without changing const app memory.
|
||||
// An unknown prefix may be retained only when its entire tail is free of FB create info.
|
||||
static int fb_hide_swapchain_foveation(XrSwapchainCreateInfo *info, FbStorageNode nodes[16]) {
|
||||
const XrBaseInStructure *p = info->next;
|
||||
unsigned n = 0;
|
||||
int removed = 0;
|
||||
unsigned visited = 0;
|
||||
while (p) {
|
||||
if (++visited > 256) return -1;
|
||||
if (p->type == XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB) {
|
||||
removed = 1;
|
||||
p = p->next;
|
||||
continue;
|
||||
}
|
||||
if (p->type != XR_TYPE_VULKAN_SWAPCHAIN_CREATE_INFO_META || n == 16) {
|
||||
for (const XrBaseInStructure *tail = p; tail; tail = tail->next) {
|
||||
if (++visited > 256 || tail->type == XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB) return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
nodes[n].vk = *(const FbVulkanSwapchainCreateInfo *)p;
|
||||
// Foveation-only subsampled allocations are invalid without FB foveation.
|
||||
nodes[n].vk.additionalCreateFlags &= ~0x4000u;
|
||||
if (n) nodes[n - 1].base.next = &nodes[n].base;
|
||||
++n;
|
||||
p = p->next;
|
||||
}
|
||||
if (n) nodes[n - 1].base.next = p;
|
||||
info->next = n ? &nodes[0] : (const void *)p;
|
||||
return removed;
|
||||
}
|
||||
@@ -95,6 +95,9 @@ static int foveation_storage_fix; // opt-in ordinary shared storage for native V
|
||||
static int foveation_eye; // experimental profile upgrade; does not inject Vulkan gaze offsets
|
||||
static int foveation_eye_enabled, foveation_eye_supported;
|
||||
#include "foveation_storage.h"
|
||||
#include "hide_fb_foveation.h"
|
||||
static int hide_fb_foveation;
|
||||
static int hide_fb_foveation_vulkan;
|
||||
// hide XR_FB_space_warp: games then render every frame themselves (UE5 Application SpaceWarp flickered on the Frame,
|
||||
// e.g. Into The Radius 2; OVRPort's patch_disable_space_warp doesn't reach UE5's OpenXR plugin)
|
||||
static int hide_space_warp = 0;
|
||||
@@ -174,6 +177,7 @@ static void read_settings(const char *path) {
|
||||
if (sscanf(line, "foveation_fix=%f", &value) == 1) foveation_fix = value != 0;
|
||||
if (sscanf(line, "foveation_storage_fix=%f", &value) == 1) foveation_storage_fix = value != 0;
|
||||
if (sscanf(line, "foveation_eye=%f", &value) == 1) foveation_eye = value != 0;
|
||||
if (sscanf(line, "hide_fb_foveation=%f", &value) == 1) hide_fb_foveation = value != 0;
|
||||
if (sscanf(line, "hide_space_warp=%f", &value) == 1) hide_space_warp = value != 0;
|
||||
if (sscanf(line, "strip_color_bias=%f", &value) == 1) strip_color_bias = (int)value;
|
||||
if (sscanf(line, "fade_quad=%f", &value) == 1) fade_quad = value > 0 ? (int)value : 0;
|
||||
@@ -272,6 +276,7 @@ static void initialize(void) {
|
||||
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_fb_foveation) LOG("per-game: hide_fb_foveation=1 (Quest Vulkan foveation suppressed)");
|
||||
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 (fade_quad) LOG("per-game: fade_quad=%d (colour-scale fades drawn as a black quad%s)", fade_quad,
|
||||
@@ -307,6 +312,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
if (!fn) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
if (!info) return XR_ERROR_VALIDATION_FAILURE;
|
||||
|
||||
hide_fb_foveation_vulkan = hide_fb_foveation && fb_instance_uses_vulkan(info);
|
||||
int chained = 0, enabled = 0;
|
||||
for (const XrBaseInStructure *p = (const XrBaseInStructure *)info->next; p; p = p->next)
|
||||
if (p->type == XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR) {
|
||||
@@ -342,6 +348,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
int supported = !available; // if we could not enumerate, pass everything through
|
||||
for (uint32_t j = 0; available && j < available_count && !supported; ++j)
|
||||
supported = !strcmp(available[j].extensionName, ext);
|
||||
if (hide_fb_foveation_vulkan && fb_is_foveation_extension(ext)) {
|
||||
LOG("hide_fb_foveation: %s not enabled", ext);
|
||||
continue;
|
||||
}
|
||||
if (hide_space_warp && !strcmp(ext, "XR_FB_space_warp")) {
|
||||
LOG("hide_space_warp: XR_FB_space_warp not enabled");
|
||||
continue;
|
||||
@@ -382,7 +392,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
if (!requested && offered) { names[kept++] = "XR_FB_display_refresh_rate"; LOG("added XR_FB_display_refresh_rate (refresh_rate)"); }
|
||||
}
|
||||
foveation_eye_enabled = 0;
|
||||
if (foveation_eye && !foveation_fix) {
|
||||
if (foveation_eye && !foveation_fix && !hide_fb_foveation_vulkan) {
|
||||
int fb = 0, config = 0, offered = 0;
|
||||
for (uint32_t i = 0; i < kept; ++i) {
|
||||
fb |= !strcmp(names[i], "XR_FB_foveation");
|
||||
@@ -398,7 +408,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
}
|
||||
// Adapter-owned video outputs need an explicit full-rate profile even when
|
||||
// the shared injection layer enables foveation for application swapchains.
|
||||
if (surface_emul) {
|
||||
if (surface_emul && !hide_fb_foveation_vulkan) {
|
||||
const char *video_extensions[] = {"XR_FB_foveation", "XR_FB_foveation_configuration",
|
||||
"XR_FB_swapchain_update_state"};
|
||||
for (unsigned e = 0; e < 3; ++e) {
|
||||
@@ -584,6 +594,7 @@ static int known_swapchain(XrSwapchain handle) {
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrCreateFoveationProfileFB(XrSession session,
|
||||
const XrFoveationProfileCreateInfoFB *info, XrFoveationProfileFB *profile) {
|
||||
if (hide_fb_foveation_vulkan) return XR_ERROR_FEATURE_UNSUPPORTED;
|
||||
PFN_xrCreateFoveationProfileFB fn = (PFN_xrCreateFoveationProfileFB)lookup(active_instance, "xrCreateFoveationProfileFB");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if (!info) return XR_ERROR_VALIDATION_FAILURE;
|
||||
@@ -609,6 +620,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateFoveationProfileFB(XrSession session,
|
||||
}
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrGetFoveationEyeTrackedStateMETA(XrSession session, XrFoveationEyeTrackedStateMETA *state) {
|
||||
if (hide_fb_foveation_vulkan) return XR_ERROR_FEATURE_UNSUPPORTED;
|
||||
PFN_xrGetFoveationEyeTrackedStateMETA fn = (PFN_xrGetFoveationEyeTrackedStateMETA)lookup(active_instance, "xrGetFoveationEyeTrackedStateMETA");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrResult result = fn(session, state);
|
||||
@@ -626,11 +638,16 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchain(XrSession session, const XrSwap
|
||||
if (!info) return XR_ERROR_VALIDATION_FAILURE;
|
||||
XrSwapchainCreateInfo fixed = *info;
|
||||
FbStorageNode storage_nodes[16];
|
||||
if (!foveation_fix && foveation_storage_fix) {
|
||||
if (!foveation_fix && !hide_fb_foveation_vulkan && foveation_storage_fix) {
|
||||
int changed = fb_foveation_storage(&fixed, storage_nodes);
|
||||
LOG("foveation_storage_fix: shared allocation %s", changed > 0 ? "SUBSAMPLED cleared" :
|
||||
changed < 0 ? "unchanged (unsupported chain prefix)" : "already ordinary or no META node");
|
||||
}
|
||||
if (hide_fb_foveation_vulkan) {
|
||||
int stripped = fb_hide_swapchain_foveation(&fixed, storage_nodes);
|
||||
if (stripped < 0) return XR_ERROR_VALIDATION_FAILURE;
|
||||
if (stripped) LOG("hide_fb_foveation: removed swapchain foveation create info");
|
||||
}
|
||||
if (foveation_fix) {
|
||||
// Drop leading foveation structs; the chain is const so only a prefix can be skipped.
|
||||
const XrBaseInStructure *p = (const XrBaseInStructure *)fixed.next;
|
||||
@@ -1223,12 +1240,15 @@ XRAPI_ATTR XrResult XRAPI_CALL xrApplyHapticFeedback(XrSession session, const Xr
|
||||
return result;
|
||||
}
|
||||
|
||||
#include "hide_foveation_images.h"
|
||||
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain swapchain, uint32_t capacity, uint32_t *count,
|
||||
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)
|
||||
: hide_fb_foveation_vulkan ? fb_hide_foveation_images(fn, swapchain, capacity, count, images)
|
||||
: fn(swapchain, capacity, count, images);
|
||||
if (XR_SUCCEEDED(result) && images && capacity && count) {
|
||||
flip_on_enumerate_images(swapchain, *count, images);
|
||||
@@ -2037,7 +2057,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateInstanceExtensionProperties(const char
|
||||
XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
|
||||
if (!fn) return XR_ERROR_INITIALIZATION_FAILED;
|
||||
if (!count) return XR_ERROR_VALIDATION_FAILURE;
|
||||
if ((!foveation_fix && !hide_space_warp && !passthrough_emul && !scene_emul && !controller_models) || layer)
|
||||
if ((!foveation_fix && !hide_fb_foveation && !hide_space_warp && !passthrough_emul && !scene_emul && !controller_models) || layer)
|
||||
return fn(layer, capacity, count, properties);
|
||||
|
||||
uint32_t total = 0;
|
||||
@@ -2053,6 +2073,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateInstanceExtensionProperties(const char
|
||||
for (uint32_t i = 0; i < total; ++i) {
|
||||
if (!strcmp(all[i].extensionName, "XR_FB_passthrough")) runtime_has_fb_passthrough = 1;
|
||||
if (!strcmp(all[i].extensionName, XR_FB_RENDER_MODEL_EXTENSION_NAME)) runtime_has_render_model = 1;
|
||||
if (hide_fb_foveation && fb_is_foveation_extension(all[i].extensionName)) continue;
|
||||
if (foveation_fix && strstr(all[i].extensionName, "foveation")) continue;
|
||||
if (hide_space_warp && !strcmp(all[i].extensionName, "XR_FB_space_warp")) continue;
|
||||
if (properties && kept < capacity) properties[kept] = all[i];
|
||||
@@ -2092,9 +2113,29 @@ XRAPI_ATTR XrResult XRAPI_CALL xrEnumerateApiLayerProperties(uint32_t capacity,
|
||||
return fn ? fn(capacity, count, properties) : XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
|
||||
static XRAPI_ATTR int XRAPI_CALL framebridge_hide_fb_foveation(void) {
|
||||
pthread_once(&init_once, initialize);
|
||||
return hide_fb_foveation;
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fb_hide_update_swapchain(XrSwapchain swapchain, const XrSwapchainStateBaseHeaderFB *state) {
|
||||
if (state && state->type == XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB) {
|
||||
LOG("hide_fb_foveation: refused foveation profile update");
|
||||
return XR_ERROR_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
PFN_xrUpdateSwapchainFB fn = (PFN_xrUpdateSwapchainFB)lookup(active_instance, "xrUpdateSwapchainFB");
|
||||
return fn ? fn(swapchain, state) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
}
|
||||
|
||||
// For native/xrshim: emulated functions that overport's dispatcher doesn't know (it never asks us for them).
|
||||
__attribute__((visibility("default"))) PFN_xrVoidFunction framebridge_extension_proc(const char *name) {
|
||||
pthread_once(&init_once, initialize);
|
||||
if (name && !strcmp(name, "framebridge_hide_fb_foveation"))
|
||||
return (PFN_xrVoidFunction)framebridge_hide_fb_foveation;
|
||||
if (hide_fb_foveation_vulkan && name) {
|
||||
if (!strcmp(name, "xrCreateFoveationProfileFB")) return (PFN_xrVoidFunction)xrCreateFoveationProfileFB;
|
||||
if (!strcmp(name, "xrUpdateSwapchainFB")) return (PFN_xrVoidFunction)fb_hide_update_swapchain;
|
||||
}
|
||||
if (name && !foveation_fix && (foveation_storage_fix || foveation_eye)) {
|
||||
if (!strcmp(name, "xrCreateFoveationProfileFB")) return (PFN_xrVoidFunction)xrCreateFoveationProfileFB;
|
||||
if (!strcmp(name, "xrGetFoveationEyeTrackedStateMETA")) return (PFN_xrVoidFunction)xrGetFoveationEyeTrackedStateMETA;
|
||||
@@ -2135,6 +2176,10 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
if (!emulated) emulated = render_model_emulation(name);
|
||||
if (!emulated && emulate_scene && !strcmp(name, "xrLocateSpacesKHR")) emulated = (PFN_xrVoidFunction)emu_locate_spaces_khr;
|
||||
if (emulated) { *function = emulated; return XR_SUCCESS; }
|
||||
if (hide_fb_foveation_vulkan && !strcmp(name, "xrUpdateSwapchainFB")) {
|
||||
*function = (PFN_xrVoidFunction)fb_hide_update_swapchain;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
HOOK(xrGetSystemProperties)
|
||||
HOOK(xrApplyHapticFeedback)
|
||||
HOOK(xrCreateReferenceSpace)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Shared by the adapter and its shim above OVRPort's synthetic extension list.
|
||||
static int fb_is_foveation_extension(const char *name) {
|
||||
return !strcmp(name, "XR_FB_foveation_vulkan") || !strcmp(name, "XR_FB_foveation") ||
|
||||
!strcmp(name, "XR_FB_foveation_configuration") || !strcmp(name, "XR_META_foveation_eye_tracked");
|
||||
}
|
||||
|
||||
static int fb_instance_uses_vulkan(const XrInstanceCreateInfo *info) {
|
||||
for (uint32_t i = 0; info && i < info->enabledExtensionCount; ++i)
|
||||
if (!strcmp(info->enabledExtensionNames[i], "XR_KHR_vulkan_enable") ||
|
||||
!strcmp(info->enabledExtensionNames[i], "XR_KHR_vulkan_enable2")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline XrResult fb_hide_enumerate(PFN_xrEnumerateInstanceExtensionProperties fn, const char *layer,
|
||||
uint32_t capacity, uint32_t *count, XrExtensionProperties *properties) {
|
||||
if (!count || (capacity && !properties)) return XR_ERROR_VALIDATION_FAILURE;
|
||||
uint32_t total = 0;
|
||||
XrResult r = fn(layer, 0, &total, NULL);
|
||||
if (XR_FAILED(r)) return r;
|
||||
XrExtensionProperties *all = calloc(total ? total : 1, sizeof(*all));
|
||||
if (!all) return XR_ERROR_OUT_OF_MEMORY;
|
||||
for (uint32_t i = 0; i < total; ++i) all[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
||||
r = fn(layer, total, &total, all);
|
||||
if (XR_SUCCEEDED(r)) {
|
||||
uint32_t kept = 0;
|
||||
for (uint32_t i = 0; i < total; ++i) {
|
||||
if (fb_is_foveation_extension(all[i].extensionName)) continue;
|
||||
if (kept < capacity) properties[kept] = all[i];
|
||||
++kept;
|
||||
}
|
||||
*count = kept;
|
||||
if (capacity && capacity < kept) r = XR_ERROR_SIZE_INSUFFICIENT;
|
||||
}
|
||||
free(all);
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline XrResult fb_hide_create_instance(PFN_xrCreateInstance fn, const XrInstanceCreateInfo *info,
|
||||
XrInstance *instance) {
|
||||
if (!info) return XR_ERROR_VALIDATION_FAILURE;
|
||||
if (!fb_instance_uses_vulkan(info)) return fn(info, instance);
|
||||
const char **names = calloc(info->enabledExtensionCount ? info->enabledExtensionCount : 1, sizeof(*names));
|
||||
if (!names) return XR_ERROR_OUT_OF_MEMORY;
|
||||
XrInstanceCreateInfo fixed = *info;
|
||||
fixed.enabledExtensionCount = 0;
|
||||
fixed.enabledExtensionNames = names;
|
||||
for (uint32_t i = 0; i < info->enabledExtensionCount; ++i)
|
||||
if (!fb_is_foveation_extension(info->enabledExtensionNames[i]))
|
||||
names[fixed.enabledExtensionCount++] = info->enabledExtensionNames[i];
|
||||
XrResult r = fn(&fixed, instance);
|
||||
free(names);
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Output chains are writable. Unlink FB nodes during enumeration and restore every
|
||||
// app-owned link on both success and failure; preserve unrelated output structures.
|
||||
static XrResult fb_hide_foveation_images(PFN_xrEnumerateSwapchainImages fn, XrSwapchain swapchain,
|
||||
uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *images) {
|
||||
if (!images || !capacity || images->type != XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR)
|
||||
return fn(swapchain, capacity, count, images);
|
||||
XrSwapchainImageVulkanKHR *vk_images = (void *)images;
|
||||
size_t total = 0;
|
||||
for (uint32_t i = 0; i < capacity; ++i)
|
||||
for (XrBaseOutStructure *p = vk_images[i].next; p; p = p->next)
|
||||
total += p->type == XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB;
|
||||
struct FbImageLink { XrBaseOutStructure *parent, *node; };
|
||||
struct FbImageLink *links = calloc(total ? total : 1, sizeof(*links));
|
||||
if (!links) return XR_ERROR_OUT_OF_MEMORY;
|
||||
size_t n = 0;
|
||||
for (uint32_t i = 0; i < capacity; ++i) {
|
||||
XrBaseOutStructure *parent = (void *)&vk_images[i];
|
||||
while (parent->next) {
|
||||
XrBaseOutStructure *p = parent->next;
|
||||
if (p->type == XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB) {
|
||||
links[n++] = (struct FbImageLink){parent, p};
|
||||
parent->next = p->next;
|
||||
} else parent = p;
|
||||
}
|
||||
}
|
||||
XrResult result = fn(swapchain, capacity, count, images);
|
||||
while (n) { --n; links[n].parent->next = links[n].node; }
|
||||
free(links);
|
||||
if (total) LOG("hide_fb_foveation: removed %zu Vulkan foveation image node(s)", total);
|
||||
return result;
|
||||
}
|
||||
+45
-1
@@ -22,6 +22,8 @@
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "../adapter/hide_fb_foveation.h"
|
||||
|
||||
#define TAG "FrameBridge"
|
||||
#define EXPORT __attribute__((visibility("default")))
|
||||
@@ -45,14 +47,42 @@ static void resolve_overport(void) {
|
||||
static PFN_xrVoidFunction adapter_proc(const char *name) {
|
||||
static PFN_extension_proc fn;
|
||||
if (!fn) {
|
||||
// Loaded by overport's dispatcher once an instance exists; NOLOAD so the shim never loads it by itself.
|
||||
// Normally loaded by OVRPort; only the global settings query may load it early.
|
||||
void *h = dlopen("libopenxr_loader_generic.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
// Global enumeration can be answered entirely by the dispatcher. Load the
|
||||
// adapter for its settings query even before an OpenXR instance exists.
|
||||
if (!h && !strcmp(name, "framebridge_hide_fb_foveation"))
|
||||
h = dlopen("libopenxr_loader_generic.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (h) fn = (PFN_extension_proc)dlsym(h, "framebridge_extension_proc");
|
||||
if (!fn) return NULL;
|
||||
}
|
||||
return fn(name);
|
||||
}
|
||||
|
||||
// OVRPort advertises FB extensions itself, above the runtime adapter. Filter the
|
||||
// synthetic list too, including when the dispatcher never enumerates the runtime.
|
||||
static int hide_fb_enabled(void) {
|
||||
typedef int (*Query)(void);
|
||||
Query query = (Query)adapter_proc("framebridge_hide_fb_foveation");
|
||||
return query && query();
|
||||
}
|
||||
static PFN_xrEnumerateInstanceExtensionProperties dispatcher_enumerate;
|
||||
static PFN_xrCreateInstance dispatcher_create;
|
||||
static XrResult XRAPI_CALL enumerate_extensions(const char *layer, uint32_t capacity,
|
||||
uint32_t *count, XrExtensionProperties *properties) {
|
||||
uint32_t ignored = 0;
|
||||
XrResult r = dispatcher_enumerate(layer, 0, &ignored, NULL);
|
||||
if (XR_FAILED(r)) return r;
|
||||
if (!hide_fb_enabled()) return dispatcher_enumerate(layer, capacity, count, properties);
|
||||
__android_log_print(ANDROID_LOG_INFO, TAG, "hide_fb_foveation: filtering dispatcher extensions");
|
||||
return fb_hide_enumerate(dispatcher_enumerate, layer, capacity, count, properties);
|
||||
}
|
||||
static XrResult XRAPI_CALL create_instance(const XrInstanceCreateInfo *info, XrInstance *instance) {
|
||||
if (!hide_fb_enabled() || !fb_instance_uses_vulkan(info)) return dispatcher_create(info, instance);
|
||||
__android_log_print(ANDROID_LOG_INFO, TAG, "hide_fb_foveation: filtering dispatcher instance extensions");
|
||||
return fb_hide_create_instance(dispatcher_create, info, instance);
|
||||
}
|
||||
|
||||
static PFN_xrApplyHapticFeedback overport_haptics;
|
||||
static int haptics_logged;
|
||||
|
||||
@@ -96,6 +126,20 @@ static XRAPI_ATTR XrResult XRAPI_CALL apply_haptic_feedback(XrSession session, c
|
||||
EXPORT XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function) {
|
||||
pthread_once(&once, resolve_overport);
|
||||
if (name && function && overport_gipa &&
|
||||
(!strcmp(name, "xrEnumerateInstanceExtensionProperties") || !strcmp(name, "xrCreateInstance"))) {
|
||||
PFN_xrVoidFunction real = NULL;
|
||||
XrResult r = overport_gipa(instance, name, &real);
|
||||
if (XR_FAILED(r) || !real) return r;
|
||||
if (!strcmp(name, "xrCreateInstance")) {
|
||||
dispatcher_create = (PFN_xrCreateInstance)real;
|
||||
*function = (PFN_xrVoidFunction)create_instance;
|
||||
} else {
|
||||
dispatcher_enumerate = (PFN_xrEnumerateInstanceExtensionProperties)real;
|
||||
*function = (PFN_xrVoidFunction)enumerate_extensions;
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
if (name && function && instance != XR_NULL_HANDLE && overport_gipa && !strcmp(name, "xrApplyHapticFeedback")) {
|
||||
PFN_xrVoidFunction real = NULL;
|
||||
XrResult r = overport_gipa(instance, name, &real);
|
||||
|
||||
@@ -144,6 +144,11 @@
|
||||
"title": "Vibration strength",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.hide_fb_foveation": {
|
||||
"anchor": "adapter-hide_fb_foveation",
|
||||
"title": "Turn off Quest Vulkan foveation",
|
||||
"group": "Game settings"
|
||||
},
|
||||
"adapter.hide_space_warp": {
|
||||
"anchor": "adapter-hide_space_warp",
|
||||
"title": "Turn off space warp",
|
||||
@@ -589,6 +594,11 @@
|
||||
"title": "Unreal: match multiview depth to the eye size",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_noreserve": {
|
||||
"anchor": "frame-unreal_noreserve",
|
||||
"title": "Unreal: reserve memory like on Quest",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.unreal_ovrp_entrypoints": {
|
||||
"anchor": "frame-unreal_ovrp_entrypoints",
|
||||
"title": "Unreal: stand-ins for OVRPlugin functions OVRPort lacks",
|
||||
|
||||
@@ -43,9 +43,9 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
|
||||
# 17: headset (the Meta headset a VR app was made for: Gear VR / Go games run through frame.a32_bridge) (2026-10)
|
||||
# 18: headset "quest" also for 32-bit apps that require 6DoF head tracking (Santa Cruz builds, e.g. Sports
|
||||
# Scramble: Touch controllers on the a32 bridge) (2026-10)
|
||||
# 19: unreal_obb_delivery also for obfuscated DownloaderActivity predicates (GitHub #159) (2026-10)
|
||||
# 20: google_vr (Google Daydream / Cardboard builds: GVR or Cardboard SDK, no path to the Frame's headset,
|
||||
# GitHub #335) (2026-10)
|
||||
# 19: unreal_obb_delivery also recognises obfuscated DownloaderActivity predicates (GitHub #159, Medal of Honor)
|
||||
# 20: unreal_binned3 (Unreal built with FMallocBinned3 reserves tens of GiB at load: frame.unreal_noreserve,
|
||||
# GitHub #349) (2026-10)
|
||||
ANALYSIS_VERSION = 20
|
||||
|
||||
|
||||
@@ -90,6 +90,13 @@ def cryengine_project(data: bytes) -> bool:
|
||||
UE_VERIFY_OBB_KEY = ".GameActivity.bVerifyOBBOnStartUp"
|
||||
|
||||
|
||||
def unreal_binned3(engine: bytes) -> bool:
|
||||
"""Unreal built with FMallocBinned3 as its allocator (its own messages and source path are compiled in; other
|
||||
builds only name its console variables). It reserves its whole range while the library loads (GitHub #349)."""
|
||||
return (b"/HAL/MallocBinned3.cpp\0" in engine
|
||||
or "FMallocBinned3 Attempt to free".encode("utf-16-le") in engine)
|
||||
|
||||
|
||||
def unreal_verify_obb(meta: dict) -> bool:
|
||||
"""Unreal's DownloaderActivity checks every OBB entry's CRC at the first start (a full read; on a mismatch it waits
|
||||
on an error screen). Only a real boolean counts: GameActivity reads it with Bundle.getBoolean."""
|
||||
@@ -200,32 +207,6 @@ def vr_kind(libs: set[str], manifest_strings: list[str]) -> str:
|
||||
return "none"
|
||||
|
||||
|
||||
# Google VR (Daydream / Cardboard): the manifest categories of a Daydream / Cardboard app, Daydream's VR service
|
||||
# (android:enableVrMode="com.google.vr.vrcore/..."), the GVR SDK's library and the Cardboard SDK's
|
||||
# (github.com/googlevr/cardboard), and the GVR SDK's Java classes. Not libgvraudio.so / Unity's
|
||||
# libaudioplugingvrunity.so: Google's spatial audio, which Quest games ship too (e.g. Shadow Point).
|
||||
DAYDREAM_CATEGORY = "com.google.intent.category.DAYDREAM"
|
||||
CARDBOARD_CATEGORY = "com.google.intent.category.CARDBOARD"
|
||||
GOOGLE_VR_LIBS = {"libgvr.so", "libcardboard_api.so", "libGfxPluginCardboard.so"}
|
||||
GVR_DEX_MARKERS = (b"Lcom/google/vr/ndk/base/GvrLayout;", b"Lcom/google/vr/sdk/base/GvrView;",
|
||||
b"Lcom/google/vr/sdk/base/GvrActivity;", b"Lcom/google/cardboard/sdk/")
|
||||
|
||||
|
||||
def google_vr(libs: set[str], cats: set[str], manifest_strings: list[str], gvr_java: bool = False,
|
||||
boot: str = "") -> str:
|
||||
"""A Google VR app: "daydream" (Daydream: needs Google's VR services, VrCore), "cardboard" (Cardboard: draws
|
||||
both eyes side by side with lens distortion onto a phone screen) or "" (neither). Neither has a path to the
|
||||
Frame's headset: a Lepton app only reaches it through OpenXR (VrApi / OVRPlugin games are translated to it).
|
||||
`boot` = Unity's boot.config (vr-device-list=, where Unity builds list their VR devices)."""
|
||||
devices = next((ln.split("=", 1)[1].lower() for ln in boot.splitlines() if ln.startswith("vr-device-list=")), "")
|
||||
if (DAYDREAM_CATEGORY in cats or any(s.startswith("com.google.vr.vrcore") for s in manifest_strings)
|
||||
or "daydream" in devices):
|
||||
return "daydream"
|
||||
if CARDBOARD_CATEGORY in cats or "cardboard" in devices or libs & GOOGLE_VR_LIBS or gvr_java:
|
||||
return "cardboard"
|
||||
return ""
|
||||
|
||||
|
||||
# Kotlin's message for a failed non-null cast to ClipboardManager (Godot 4.2-4.4; 4.1 is Java, 4.5+ uses `as?`)
|
||||
GODOT_CLIPBOARD_CAST = b"null cannot be cast to non-null type android.content.ClipboardManager"
|
||||
|
||||
@@ -278,8 +259,6 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
for d in dexes)
|
||||
# video through Android's decoders (frame.hw_video_decode): Java MediaCodec, ExoPlayer/Media3, or libVLC
|
||||
media_codec = any(m in d for d in dexes for m in MEDIA_CODEC_MARKERS) or "libvlc.so" in libs
|
||||
# Google VR's Java side (GvrLayout / GvrView, the Cardboard SDK): a Daydream / Cardboard app (google_vr)
|
||||
gvr_java = any(m in d for d in dexes for m in GVR_DEX_MARKERS)
|
||||
from ..patches.frame.unreal_obb_check import has_delivery_check
|
||||
|
||||
# Includes obfuscated DownloaderActivity predicates; CRC verification is a separate manifest switch.
|
||||
@@ -403,8 +382,6 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
"xr_layer_exts": sorted(layer_exts), # composition-layer extensions the game's own libraries request
|
||||
"vr_kind": vr_kind(libset, manifest_strings),
|
||||
"headset": headset(meta, abis, vr_kind(libset, manifest_strings), requires_6dof(manifest)),
|
||||
# a Google Daydream / Cardboard build (GVR / Cardboard SDK): no path to the Frame's headset (GitHub #335)
|
||||
"google_vr": google_vr(libset, cats, manifest_strings, gvr_java, boot),
|
||||
# the base of a split APK set (Play "app bundle" installs): the code/libraries live in split APKs
|
||||
"split_apk": bool({"isSplitRequired", "requiredSplitTypes"} & set(manifest_strings)),
|
||||
# Unity (IL2CPP) text fields: they close at once on the Frame (frame.unity_text_input)
|
||||
@@ -443,6 +420,10 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
# Unreal CRC-checks the OBB before the game starts (frame.unreal_skip_obb_check, GitHub #159)
|
||||
"unreal_verify_obb": unreal_verify_obb(meta),
|
||||
"unreal_obb_delivery": unreal_obb_delivery,
|
||||
# Unreal's Binned3 allocator reserves more address space than the Frame's kernel grants in one writable
|
||||
# mapping (frame.unreal_noreserve, GitHub #349)
|
||||
"unreal_binned3": (unreal_binned3(lib_bytes.get(engine_lib.rsplit("/", 1)[1], b""))
|
||||
if engine_lib else False),
|
||||
# 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 []),
|
||||
|
||||
@@ -20,6 +20,23 @@ def _elf(data: bytes) -> ELFFile:
|
||||
return ELFFile(io.BytesIO(data))
|
||||
|
||||
|
||||
def dynsym_section(elf: ELFFile):
|
||||
"""The dynamic symbol table, found by section type: some shipped libraries have a scrambled section name table
|
||||
(GitHub #349: Transformers Beyond Reality's libOVRPlugin.so calls .dynsym "text"), types and links are intact."""
|
||||
sec = elf.get_section_by_name(".dynsym")
|
||||
if isinstance(sec, SymbolTableSection) and sec["sh_type"] == "SHT_DYNSYM":
|
||||
return sec
|
||||
return next((s for s in elf.iter_sections() if s["sh_type"] == "SHT_DYNSYM"), None)
|
||||
|
||||
|
||||
def dynstr_section(elf: ELFFile, dynsym=None):
|
||||
"""The string table of the dynamic symbols (dynsym's sh_link; by name as a fallback)."""
|
||||
dynsym = dynsym if dynsym is not None else dynsym_section(elf)
|
||||
if dynsym is not None and 0 < dynsym["sh_link"] < elf.num_sections():
|
||||
return elf.get_section(dynsym["sh_link"])
|
||||
return elf.get_section_by_name(".dynstr")
|
||||
|
||||
|
||||
def needed(data: bytes) -> list[str]:
|
||||
elf = _elf(data)
|
||||
for sec in elf.iter_sections():
|
||||
@@ -48,7 +65,7 @@ def dyn_symbols(data: bytes, defined: bool) -> set[str]:
|
||||
@lru_cache(maxsize=256)
|
||||
def _dyn_symbols_cached(data: bytes, defined: bool) -> frozenset[str]:
|
||||
elf = _elf(data)
|
||||
sec = elf.get_section_by_name(".dynsym")
|
||||
sec = dynsym_section(elf)
|
||||
out = set()
|
||||
if isinstance(sec, SymbolTableSection):
|
||||
for sym in sec.iter_symbols():
|
||||
@@ -69,7 +86,8 @@ def find_symbols(data: bytes, names) -> dict[str, tuple[int, int]]:
|
||||
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")
|
||||
dynsym = dynsym_section(elf)
|
||||
dynstr = dynstr_section(elf, dynsym)
|
||||
if dynsym is None or dynstr is None:
|
||||
return {}
|
||||
sym_off, sym_size = dynsym["sh_offset"], dynsym["sh_size"]
|
||||
@@ -147,7 +165,7 @@ def hide_exports(data: bytes, names) -> tuple[bytes, list[str]]:
|
||||
(native/langpack) still finds the original address. Returns (data, names actually hidden); ELF64 only."""
|
||||
if not is_elf(data) or not is_64bit(data) or data[5] != 1:
|
||||
return data, []
|
||||
sec = _elf(data).get_section_by_name(".dynsym")
|
||||
sec = dynsym_section(_elf(data))
|
||||
if not isinstance(sec, SymbolTableSection):
|
||||
return data, []
|
||||
wanted = set(names)
|
||||
@@ -171,7 +189,7 @@ def redirect_exports(data: bytes, names) -> tuple[bytes, list[str]]:
|
||||
"""
|
||||
if not is_elf(data) or not is_64bit(data) or data[5] != 1:
|
||||
return data, []
|
||||
sec = _elf(data).get_section_by_name(".dynsym")
|
||||
sec = dynsym_section(_elf(data))
|
||||
if not isinstance(sec, SymbolTableSection):
|
||||
return data, []
|
||||
wanted = set(names)
|
||||
@@ -201,10 +219,11 @@ def rename_imports(data: bytes, renames: dict[str, str]) -> tuple[bytes, list[st
|
||||
if len(new) > len(old):
|
||||
raise ValueError(f"{new} is longer than {old}")
|
||||
elf = _elf(data)
|
||||
dynsym, dynstr = elf.get_section_by_name(".dynsym"), elf.get_section_by_name(".dynstr")
|
||||
dynsym = dynsym_section(elf)
|
||||
dynstr = dynstr_section(elf, dynsym)
|
||||
if not isinstance(dynsym, SymbolTableSection) or dynstr is None:
|
||||
return data, []
|
||||
versym = elf.get_section_by_name(".gnu.version")
|
||||
versym = next((s for s in elf.iter_sections() if s["sh_type"] == "SHT_GNU_versym"), None)
|
||||
str_off = dynstr["sh_offset"]
|
||||
targets, refs = {}, {} # dynsym index -> st_name of an import to rename; st_name -> referencing dynsym indices
|
||||
for i, sym in enumerate(dynsym.iter_symbols()):
|
||||
@@ -376,24 +395,23 @@ def add_needed(data: bytes, library: str) -> bytes:
|
||||
write_ph(i, p)
|
||||
|
||||
# section headers: point .dynamic / .dynstr at the new copies (for tools; the loader uses program headers)
|
||||
# Found by type and link, not by name: some shipped libraries have a scrambled section name table (GitHub #349,
|
||||
# Transformers Beyond Reality's libOVRPlugin.so names .dynamic "dding"), whose types and links are still right.
|
||||
if shoff and shnum:
|
||||
shstrndx = eh[13]
|
||||
SHT_DYNAMIC = 6
|
||||
def sh(i):
|
||||
return list(struct.unpack_from(sh_fmt, buf, shoff + i * shentsize))
|
||||
names_hdr = sh(shstrndx)
|
||||
names_off = names_hdr[4]
|
||||
def sec_name(h):
|
||||
end = buf.index(b"\0", names_off + h[0])
|
||||
return bytes(buf[names_off + h[0]:end]).decode()
|
||||
for i in range(shnum):
|
||||
h = sh(i)
|
||||
n = sec_name(h)
|
||||
if n == ".dynstr":
|
||||
h[3], h[4], h[5] = va_start + str_off, file_end + str_off, len(new_str)
|
||||
elif n == ".dynamic":
|
||||
h[3], h[4], h[5] = va_start + dyn_off, file_end + dyn_off, len(dyn_bytes)
|
||||
else:
|
||||
continue
|
||||
headers = [sh(i) for i in range(shnum)] # sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link
|
||||
dyn_secs = [i for i, h in enumerate(headers) if h[1] == SHT_DYNAMIC]
|
||||
str_secs = {headers[i][6] for i in dyn_secs if 0 < headers[i][6] < shnum}
|
||||
str_secs |= {i for i, h in enumerate(headers) if h[3] == strtab_va and h[3]} # .dynstr by address
|
||||
for i in dyn_secs:
|
||||
h = headers[i]
|
||||
h[3], h[4], h[5] = va_start + dyn_off, file_end + dyn_off, len(dyn_bytes)
|
||||
struct.pack_into(sh_fmt, buf, shoff + i * shentsize, *h)
|
||||
for i in str_secs - set(dyn_secs):
|
||||
h = headers[i]
|
||||
h[3], h[4], h[5] = va_start + str_off, file_end + str_off, len(new_str)
|
||||
struct.pack_into(sh_fmt, buf, shoff + i * shentsize, *h)
|
||||
result = bytes(buf)
|
||||
got = needed(result)
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
"dev build": "",
|
||||
"Dev builds are early, less tested versions and may have bugs. You still get the next release as a normal update (you have {version}).": "",
|
||||
"Developer Mode lets FramePort find and connect to the Frame; the setup turns it on for you. With it on, you can also open Settings → Developer → Pair new host on the Frame and approve FramePort there.": "",
|
||||
"Diagnostic for Vulkan games that fail on their first frames with missing Quest foveation images.": "",
|
||||
"Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log).": "",
|
||||
"Diagnostics: removes layer color fades. Some fades no longer show.": "",
|
||||
"Diagnostics: the Vulkan shim (frame.vk_sanitize) adds Khronos' validation layer to the game's instance; its findings go to launch.log. The layer library (libVkLayer_khronos_validation.so) must be in the APK.": "",
|
||||
@@ -680,6 +681,7 @@
|
||||
"Hide Vulkan foveation": "",
|
||||
"Hides a Quest-only way of saving GPU power that the Frame lacks. Leave it on.": "",
|
||||
"Hides Android's back, home and recents buttons, which cover the app's own buttons.": "",
|
||||
"Hides Quest FB foveation extensions from the game and OVRPort, removes Vulkan foveation image/create chains, and refuses foveation profile updates. Diagnostic for first-frame failures with missing density images (for example Transformers Beyond Reality: Redux). Device verification pending; does not disable Valve foveation.": "",
|
||||
"Hides Quest foveation extensions the Frame runtime lacks.": "",
|
||||
"Hides the Frame's brief focus dips (up to focus_hold_ms) once the game has been focused for a second. On by default: the Frame's wear sensor flickers \"HMD off\" for 0.5-2 s while worn, and many games pause or recenter every time (for example Blade & Sorcery, Lucky's Tale).": "",
|
||||
"Hides XR_FB_space_warp, so the game renders every frame itself instead of half of them plus motion vectors (Application SpaceWarp). For games whose picture flickers or smears on the Frame (for example Unreal Engine 5 games such as Into The Radius 2).": "",
|
||||
@@ -840,6 +842,7 @@
|
||||
"Lets the game find extra content it keeps next to its data (for example Star Wars Tales' seasons).": "",
|
||||
"Lets the game finish loading when its separate loading splash stalls the render thread.": "",
|
||||
"Lets the game leave its loading screen: it waited for a Quest-only step that never starts on the Frame (for example Vader Immortal).": "",
|
||||
"Lets the game load: it reserves more memory at start than the Frame grants in one piece and crashed before its first frame (for example Transformers Beyond Reality).": "",
|
||||
"Lets the game use language files already in its data instead of waiting for a download that never comes.": "",
|
||||
"Lets the game use your controllers as a gamepad instead of a pointer, for 2D games with touch controls (for example SDL games).": "",
|
||||
"Lets typing reach this app from Steam's on-screen keyboard or your PC's keyboard. VR keeps working.": "",
|
||||
@@ -1609,7 +1612,6 @@
|
||||
"This Frame's SSH key changed since FramePort last connected (SteamOS was reset, or another device now has its address). If it's your Frame, choose Trust the new key on the Steam Frame page.": "",
|
||||
"This game can't run on the Steam Frame.": "",
|
||||
"This game is marked \"Can't run\" on the Steam Frame:": "",
|
||||
"This is a Google {platform} VR build. Its VR can't reach the Frame's headset, so it runs as a flat window at best, usually without working controls. An Oculus build of the game (Meta Quest, Oculus Go or Gear VR) works the normal way.": "",
|
||||
"This is a website in an app: it opens in Meta's browser, which the Frame doesn't have. If nothing opens, visit the website in a browser.": "",
|
||||
"This is the example button from frameport.app. Real buttons install real games.": "",
|
||||
"this PC": "",
|
||||
@@ -1641,6 +1643,7 @@
|
||||
"Try this setting": "",
|
||||
"Turn it down if controller vibrations are too strong.": "",
|
||||
"Turn off only for games you play with your hands instead of controllers.": "",
|
||||
"Turn off Quest Vulkan foveation": "",
|
||||
"Turn off space warp": "",
|
||||
"Turn on Developer Mode first": "",
|
||||
"Turn on if your copy is already patched: the APK is installed unchanged. PC VR games are never changed on this PC.": "",
|
||||
@@ -1693,12 +1696,14 @@
|
||||
"Unreal builds of Meta XR Audio abort ('ClassNotFoundException com.oculus.os.AnalyticsEvent') on non-Quest devices. Patches that one JNI FindClass call so the library continues (for example NOPE Challenge).": "",
|
||||
"Unreal games can check their whole data file (.obb) before starting (bVerifyOBBOnStartUp). On the Frame that check's progress and error screens are invisible (Lepton runs VR games without an Android window), so the game seems to hang on its DownloaderActivity, for example Contractors. This turns the check off in the manifest; Unreal still checks that the OBB is there with the right size, so a missing or wrong-version OBB still stops the game.": "",
|
||||
"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 reserves its memory ranges on Android with ordinary read/write mappings. Games built with Unreal's Binned3 allocator reserve tens of gigabytes at once while the engine loads (76 GiB in Transformers Beyond Reality); Quest allows that, the Frame's kernel refuses mappings larger than its memory, and the game crashes before its first frame (SIGSEGV at fault addr 0x10 in libUnreal.so, inside System.loadLibrary). This marks those mappings MAP_NORESERVE, as Quest effectively treats them: memory is still only used when the game touches it.": "",
|
||||
"Unreal's Java OBB delivery check can leave a game on an invisible DownloaderActivity screen even with the manifest CRC check off (e.g. Into The Radius). Makes known Unreal delivery predicates return true, including recognized obfuscated versions, bypassing the Java name and size gate. Experimental: the native engine still needs complete, matching, readable OBB data.": "",
|
||||
"Unreal's Oculus module looks up every OVRPlugin function it was built against and starts no VR when one is missing; OVRPort's OpenXR OVRPlugin lacks some old ones (for example ovrp_GetPTWNear, 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: bypass the stalled loading splash": "",
|
||||
"Unreal: don't wait for the Quest shader precompile": "",
|
||||
"Unreal: match multiview depth to the eye size": "",
|
||||
"Unreal: Quest controller bindings (RPOC key map)": "",
|
||||
"Unreal: reserve memory like on Quest": "",
|
||||
"Unreal: skip the OBB delivery check": "",
|
||||
"Unreal: stand-ins for OVRPlugin functions OVRPort lacks": "",
|
||||
"Unreal: start without the OBB check": "",
|
||||
|
||||
@@ -38,7 +38,7 @@ def _words(data: bytes | bytearray, off: int, n: int) -> list[int]:
|
||||
def _function_offset(data: bytes, size: int) -> int | None:
|
||||
"""File offset of FUNC in the loader (None: not an ELF, no such export, or shorter than `size` bytes)."""
|
||||
try:
|
||||
dynsym = elf._elf(data).get_section_by_name(".dynsym")
|
||||
dynsym = elf.dynsym_section(elf._elf(data))
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
found = dynsym.get_symbol_by_name(FUNC) if dynsym is not None else None
|
||||
|
||||
@@ -51,7 +51,7 @@ def _x0_address(data: bytes, segs, at: int, back: int = 8) -> int | None:
|
||||
|
||||
|
||||
def _function(data: bytes, name: str) -> tuple[int, int] | None:
|
||||
sec = elf._elf(data).get_section_by_name(".dynsym")
|
||||
sec = elf.dynsym_section(elf._elf(data))
|
||||
for sym in (sec.get_symbol_by_name(name) or []) if sec else []:
|
||||
if sym["st_value"] and sym["st_size"]:
|
||||
return sym["st_value"], sym["st_size"]
|
||||
|
||||
@@ -15,7 +15,7 @@ VIDEO_LIBS = ("libavcodec", "libffmpeg", "libvlc", "libmpv", "libijkffmpeg")
|
||||
def raise_swapchain_limit(data: bytes) -> tuple[bytes | None, int]:
|
||||
"""Rewrite the `cmp wN, #4096` size guards at the start of OVRPort's swapchain entry points."""
|
||||
try:
|
||||
dynsym = elf._elf(data).get_section_by_name(".dynsym")
|
||||
dynsym = elf.dynsym_section(elf._elf(data))
|
||||
except Exception: # noqa: BLE001
|
||||
return None, 0
|
||||
if dynsym is None:
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Unreal's Android memory reservations without commit-charge accounting (GitHub #349, Transformers Beyond Reality).
|
||||
|
||||
Unreal reserves virtual memory on Android with `mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1,
|
||||
0)` (FPlatformVirtualMemoryBlock::AllocateVirtual: Android has no separate commit step). Games built with
|
||||
FMallocBinned3 reserve the whole allocator range up front while libUnreal.so loads, 76 GiB in Transformers Beyond
|
||||
Reality. Quest's kernel lets that through; the Frame runs with the kernel's heuristic overcommit (mode 0), which
|
||||
refuses any single writable private mapping larger than RAM + swap (16 + 8 GiB), so the reservation fails, Unreal's
|
||||
out-of-memory handler runs before its log exists and dereferences a null pointer (SIGSEGV, fault addr 0x10, from a
|
||||
static constructor inside System.loadLibrary). Adding MAP_NORESERVE to those mmap calls gives them the Quest
|
||||
behaviour: pages are still only backed when touched, the mapping just isn't charged up front.
|
||||
|
||||
Each site is `mov w3, #0x22` (MAP_PRIVATE|MAP_ANONYMOUS) a few instructions before a `bl` to mmap's PLT stub, with
|
||||
`mov x0, xzr` and `mov w2, #3` in the same window; the one instruction becomes `mov w3, #0x4022`. Nothing else changes
|
||||
and nothing moves; only arm64 engine libraries."""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import struct
|
||||
|
||||
from ...analysis import elf
|
||||
from ..base import ApkContext, Patch, Suggestion, register
|
||||
|
||||
ENGINE_LIBS = ("libUE4.so", "libUnreal.so")
|
||||
MOV_W3_FLAGS = 0x52800443 # mov w3, #0x22 MAP_PRIVATE|MAP_ANONYMOUS
|
||||
MOV_W3_NORESERVE = 0x52880443 # mov w3, #0x4022 MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE
|
||||
MOV_W2_RW = 0x52800062 # mov w2, #3 PROT_READ|PROT_WRITE
|
||||
MOV_X0_NULL = 0xAA1F03E0 # mov x0, xzr
|
||||
WINDOW_BEFORE, WINDOW_AFTER = 8, 9
|
||||
|
||||
|
||||
def _mmap_slots(data: bytes) -> set[int]:
|
||||
"""GOT slots of mmap/mmap64 (R_AARCH64_JUMP_SLOT relocations)."""
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from elftools.elf.relocation import RelocationSection
|
||||
|
||||
e = ELFFile(io.BytesIO(data))
|
||||
dynsym = elf.dynsym_section(e)
|
||||
if dynsym is None:
|
||||
return set()
|
||||
slots = set()
|
||||
for sec in e.iter_sections():
|
||||
if not isinstance(sec, RelocationSection) or sec["sh_type"] != "SHT_RELA":
|
||||
continue
|
||||
for r in sec.iter_relocations():
|
||||
sym = r["r_info_sym"]
|
||||
if sym and dynsym.get_symbol(sym).name in ("mmap", "mmap64"):
|
||||
slots.add(r["r_offset"])
|
||||
return slots
|
||||
|
||||
|
||||
def _plt_slot(data: bytes, segs, va: int) -> int | None:
|
||||
"""The GOT slot a PLT stub at `va` jumps through: adrp x16, page; ldr x17, [x16, #off]."""
|
||||
off = elf.vaddr_to_offset(segs, va)
|
||||
if off is None or off + 8 > len(data):
|
||||
return None
|
||||
adrp, ldr = struct.unpack_from("<II", data, off)
|
||||
if adrp & 0x9F00001F != 0x90000010 or ldr & 0xFFC003FF != 0xF9400211:
|
||||
return None
|
||||
imm = ((adrp >> 29) & 3) | ((adrp >> 5) & 0x7FFFF) << 2
|
||||
if imm & 1 << 20:
|
||||
imm -= 1 << 21
|
||||
return (va & ~0xFFF) + (imm << 12) + ((ldr >> 10) & 0xFFF) * 8
|
||||
|
||||
|
||||
def mmap_sites(data: bytes) -> tuple[list[int], list[int]]:
|
||||
"""File offsets of the flag instructions of anonymous read/write mmap calls: (still MAP_PRIVATE|MAP_ANONYMOUS,
|
||||
already MAP_NORESERVE)."""
|
||||
if not elf.is_elf(data) or not elf.is_64bit(data) or data[5] != 1:
|
||||
return [], []
|
||||
slots = _mmap_slots(data)
|
||||
if not slots:
|
||||
return [], []
|
||||
segs = elf.load_segments(data)
|
||||
stubs: dict[int, bool] = {}
|
||||
todo, done = [], []
|
||||
for word, out in ((MOV_W3_FLAGS, todo), (MOV_W3_NORESERVE, done)):
|
||||
needle = struct.pack("<I", word)
|
||||
at = data.find(needle)
|
||||
while at >= 0:
|
||||
va = next((at - o + v for v, o, s in segs if o <= at < o + s), None) if at % 4 == 0 else None
|
||||
if va is not None:
|
||||
for k in range(1, WINDOW_AFTER + 1):
|
||||
if at + 4 * k + 4 > len(data):
|
||||
break
|
||||
w = struct.unpack_from("<I", data, at + 4 * k)[0]
|
||||
if w >> 26 != 0b100101: # bl
|
||||
continue
|
||||
imm = w & 0x3FFFFFF
|
||||
target = va + 4 * k + ((imm - (1 << 26)) if imm & 1 << 25 else imm) * 4
|
||||
if target not in stubs:
|
||||
stubs[target] = _plt_slot(data, segs, target) in slots
|
||||
if stubs[target]:
|
||||
window = {struct.unpack_from("<I", data, at + 4 * j)[0]
|
||||
for j in range(-WINDOW_BEFORE, k) if at + 4 * j >= 0}
|
||||
if MOV_W2_RW in window and MOV_X0_NULL in window:
|
||||
out.append(at)
|
||||
break
|
||||
at = data.find(needle, at + 1)
|
||||
return todo, done
|
||||
|
||||
|
||||
def patch_lib(data: bytes) -> tuple[bytes, int]:
|
||||
todo, _done = mmap_sites(data)
|
||||
if not todo:
|
||||
return data, 0
|
||||
buf = bytearray(data)
|
||||
for at in todo:
|
||||
struct.pack_into("<I", buf, at, MOV_W3_NORESERVE)
|
||||
return bytes(buf), len(todo)
|
||||
|
||||
|
||||
class UnrealNoReserve(Patch):
|
||||
id = "frame.unreal_noreserve"
|
||||
title = "Unreal: reserve memory like on Quest"
|
||||
description = ("Unreal reserves its memory ranges on Android with ordinary read/write mappings. Games built with "
|
||||
"Unreal's Binned3 allocator reserve tens of gigabytes at once while the engine loads (76 GiB in "
|
||||
"Transformers Beyond Reality); Quest allows that, the Frame's kernel refuses mappings larger "
|
||||
"than its memory, and the game crashes before its first frame (SIGSEGV at fault addr 0x10 in "
|
||||
"libUnreal.so, inside System.loadLibrary). This marks those mappings MAP_NORESERVE, as Quest "
|
||||
"effectively treats them: memory is still only used when the game touches it.")
|
||||
order = 49
|
||||
needs_vr = False
|
||||
|
||||
def applies(self, a):
|
||||
return a.engine == "Unreal" and "arm64-v8a" in a.abis
|
||||
|
||||
def detect(self, a):
|
||||
if self.applies(a) and (a.extra or {}).get("unreal_binned3"):
|
||||
return Suggestion(True, "Unreal's Binned3 allocator reserves more memory at start than the Frame's "
|
||||
"kernel allows in one piece; without this the game crashes while it loads.")
|
||||
return None
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
ws = ctx.ws
|
||||
if ws.abi != "arm64-v8a":
|
||||
return False
|
||||
lib = next((n for n in ENGINE_LIBS if ws.has(ws.lib(n))), None)
|
||||
if not lib:
|
||||
return False
|
||||
data, count = patch_lib(ws.read_lib(lib))
|
||||
if not count:
|
||||
if not mmap_sites(data)[1]:
|
||||
ctx.notes.append(f"{self.id}: no anonymous read/write mmap calls found in {lib}, left unchanged")
|
||||
return False
|
||||
ws.put(ws.lib(lib), data)
|
||||
ctx.notes.append(f"{count} mmap call(s) in {lib} reserve with MAP_NORESERVE")
|
||||
return True
|
||||
|
||||
def validate(self, ctx: ApkContext):
|
||||
ws = ctx.ws
|
||||
lib = next((n for n in ENGINE_LIBS if ws.has(ws.lib(n))), None)
|
||||
if not lib or ws.abi != "arm64-v8a":
|
||||
return []
|
||||
todo, done = mmap_sites(ws.read_lib(lib))
|
||||
return [("Unreal memory reservations use MAP_NORESERVE", bool(done) and not todo,
|
||||
f"{len(done)} rewritten, {len(todo)} left")]
|
||||
|
||||
|
||||
register(UnrealNoReserve)
|
||||
@@ -38,6 +38,10 @@ SETTINGS = [
|
||||
"scale_bias, for example Batman: Arkham Shadow's loading screens) can show the level loading instead of black on "
|
||||
"the Frame. FrameBridge then draws the fade itself: a black quad in front of the eyes whose opacity follows the "
|
||||
"game's dimming. 2 also removes the game's own dimming (if both show, the fade is doubled)."),
|
||||
("hide_fb_foveation", "int", 0, "Turn off Quest Vulkan foveation",
|
||||
"Hides Quest FB foveation extensions from the game and OVRPort, removes Vulkan foveation image/create chains, "
|
||||
"and refuses foveation profile updates. Diagnostic for first-frame failures with missing density images "
|
||||
"(for example Transformers Beyond Reality: Redux). Device verification pending; does not disable Valve foveation."),
|
||||
("hide_space_warp", "int", 0, "Turn off space warp",
|
||||
"Hides XR_FB_space_warp, so the game renders every frame itself instead of half of them plus motion vectors "
|
||||
"(Application SpaceWarp). For games whose picture flickers or smears on the Frame (for example Unreal Engine 5 "
|
||||
@@ -373,6 +377,9 @@ UI: dict[str, dict] = {
|
||||
"foveation_fix": dict(group="troubleshooting", level="advanced", label="Hide Quest foveation",
|
||||
help="Hides a Quest-only way of saving GPU power that the Frame lacks. Leave it on.",
|
||||
control=("switch",)),
|
||||
"hide_fb_foveation": dict(group="troubleshooting", level="advanced", label="Turn off Quest Vulkan foveation",
|
||||
help="Diagnostic for Vulkan games that fail on their first frames with missing Quest foveation images.",
|
||||
control=("switch",)),
|
||||
"hide_space_warp": dict(group="troubleshooting", level="advanced", label="Turn off space warp",
|
||||
help="For games whose picture flickers or smears (for example Into The Radius 2).",
|
||||
control=("switch",)),
|
||||
@@ -449,7 +456,7 @@ UI: dict[str, dict] = {
|
||||
# settings that need the xrshim (frame/adapter.py: OVRPlugin's own arm64 OpenXR lookups routed to the adapter), so
|
||||
# they don't work for 32-bit games on the bridge. surface_native: Batman's native video renderer hooks Vulkan
|
||||
# functions OVRPort's dispatcher doesn't know
|
||||
XRSHIM_SETTINGS = ("controller_models", "haptic_fix", "surface_native")
|
||||
XRSHIM_SETTINGS = ("hide_fb_foveation", "controller_models", "haptic_fix", "surface_native")
|
||||
|
||||
|
||||
class AdapterSetting(Patch):
|
||||
@@ -531,6 +538,7 @@ class AdapterSetting(Patch):
|
||||
"scene_depth": lambda a: ap.uses_scene(a) or a.extra.get("mr_only"),
|
||||
"passthrough_emul": lambda a: (a.extra.get("mr_only")
|
||||
or "com.oculus.feature.PASSTHROUGH" in (a.extra.get("features") or {})),
|
||||
"hide_fb_foveation": ap.is_vulkan,
|
||||
"flip_emul": ap.is_vulkan,
|
||||
"flip_quads": ap.is_vulkan,
|
||||
"mutable_fix": ap.is_vulkan,
|
||||
|
||||
@@ -118,6 +118,8 @@ SUMMARIES = {
|
||||
"empty Gear VR controls on the Frame, for example Vader Immortal).",
|
||||
"frame.unreal_splash_bypass": "Lets the game finish loading when its separate loading splash stalls the "
|
||||
"render thread.",
|
||||
"frame.unreal_noreserve": "Lets the game load: it reserves more memory at start than the Frame grants in one "
|
||||
"piece and crashed before its first frame (for example Transformers Beyond Reality).",
|
||||
"frame.unreal_multiview_depth": "Shows the full environment when its upper part is clipped but menu panels "
|
||||
"look correct.",
|
||||
"frame.unreal_thumb_touch": "Makes the hands' thumbs follow what your thumb touches (they always pointed up: the "
|
||||
|
||||
@@ -246,23 +246,10 @@ def web_wrapper_note(analysis: Analysis) -> str | None:
|
||||
f"Meta's browser, which the Frame doesn't have. If nothing opens, {where}.")
|
||||
|
||||
|
||||
def google_vr_note(analysis: Analysis) -> str | None:
|
||||
"""A Google Daydream / Cardboard build (analysis.detect.google_vr) that isn't also a Meta build: its VR can't
|
||||
reach the Frame's headset (GitHub #335)."""
|
||||
kind = (analysis.extra or {}).get("google_vr")
|
||||
if not kind or analysis.vr_kind == "quest": # a build with Oculus support too goes the normal way
|
||||
return None
|
||||
name = "Google Daydream" if kind == "daydream" else "Google Cardboard"
|
||||
return (f"This is a {name} VR build (Google VR SDK). Its VR can't reach the Steam Frame's headset: Daydream "
|
||||
"needs Google's VR services and Cardboard draws for a phone in a viewer, and FramePort has no "
|
||||
"translation for either, so it runs as a flat window at best, usually without working controls. An "
|
||||
"Oculus build of the same game (Meta Quest, Oculus Go or Gear VR) goes through FramePort's normal path.")
|
||||
|
||||
|
||||
def blocker_notes(analysis: Analysis) -> list[str]:
|
||||
"""Warnings read from the APK's manifest that it may not run on the Frame (whatever the recipe says). Only
|
||||
warnings: the manifest alone never marks a game unsupported (owner's rule); a launch test's triage decides."""
|
||||
return [n for n in (android_version_note(analysis), web_wrapper_note(analysis), google_vr_note(analysis)) if n]
|
||||
return [n for n in (android_version_note(analysis), web_wrapper_note(analysis)) if n]
|
||||
|
||||
|
||||
def _static_blockers(analysis: Analysis, recipe: Recipe) -> None:
|
||||
@@ -287,8 +274,7 @@ def _non_quest(analysis: Analysis, recipe: Recipe) -> None:
|
||||
recipe.reasons = {pid: v for pid, v in recipe.reasons.items() if pid in recipe.patches}
|
||||
recipe.alt_patches = []
|
||||
recipe.as_is = not any(base.get(pid).stage == "apk" for pid in recipe.patches) # the APK stays unchanged
|
||||
if not (analysis.extra or {}).get("google_vr"): # a Google VR build says so in its own note (blocker_notes)
|
||||
recipe.notes = _add(recipe.notes, "Android app without VR: installed without FramePort's VR translation.")
|
||||
recipe.notes = _add(recipe.notes, "Android app without VR: installed without FramePort's VR translation.")
|
||||
elif kind in ("pico_sdk", "wave"):
|
||||
recipe.status = "unsupported"
|
||||
recipe.notes = _add(recipe.notes, f"Uses {'Pico' if kind == 'pico_sdk' else 'HTC Vive Wave'}'s own VR SDK "
|
||||
|
||||
@@ -412,17 +412,8 @@ class GameView:
|
||||
"have. If nothing opens, visit the website in a browser."), T.TEXT)]
|
||||
+ ([C.meta(web["url"], T.TEXT_2, selectable=True)] if web.get("url") else []), spacing=T.px(4)),
|
||||
"warn", ft.Icons.LANGUAGE_ROUNDED))
|
||||
# a Google Daydream / Cardboard build (no Oculus support in it): its VR has no way to the headset (#335)
|
||||
gvr = (extra.get("google_vr") if not self.rift and not self.linux and extra.get("vr_kind") != "quest"
|
||||
else "")
|
||||
if gvr:
|
||||
out.append(C.callout(tr("This is a Google {platform} VR build. Its VR can't reach the Frame's headset, so "
|
||||
"it runs as a flat window at best, usually without working controls. An Oculus "
|
||||
"build of the game (Meta Quest, Oculus Go or Gear VR) works the normal way.")
|
||||
.format(platform="Daydream" if gvr == "daydream" else "Cardboard"),
|
||||
"warn", ft.Icons.VIEW_IN_AR_ROUNDED))
|
||||
static = " ".join(engine.blocker_notes(library.analysis_from_dict(g["analysis"]))) \
|
||||
if (too_new or web or gvr) else ""
|
||||
if (too_new or web) else ""
|
||||
if static and recipe.notes.strip() == static:
|
||||
pass # the recipe's note says the same (shown above)
|
||||
elif recipe.status == "unsupported":
|
||||
|
||||
+8
@@ -25,5 +25,13 @@ int main(void) {
|
||||
// An invalid cyclic chain is bounded and left untouched.
|
||||
fov.next = &fov; ci.next = &fov;
|
||||
assert(fb_foveation_storage(&ci, nodes) == -1 && ci.next == &fov);
|
||||
tail.next = NULL; fov.next = &tail; meta.next = &fov; ci.next = &meta;
|
||||
assert(fb_hide_swapchain_foveation(&ci, nodes) == 1);
|
||||
m = ci.next;
|
||||
assert(m != &meta && m->next == &tail && meta.next == &fov);
|
||||
assert(m->additionalCreateFlags == 0x8000 && m->additionalUsageFlags == 0x123);
|
||||
tail.next = (const XrBaseInStructure *)&fov; fov.next = NULL; ci.next = &tail;
|
||||
assert(fb_hide_swapchain_foveation(&ci, nodes) == -1 && ci.next == &tail);
|
||||
ci.next = NULL; assert(fb_hide_swapchain_foveation(&ci, nodes) == 0);
|
||||
return 0;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#include <assert.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <openxr/openxr.h>
|
||||
static int enabled, requested;
|
||||
static int query(void) { return enabled; }
|
||||
static PFN_xrVoidFunction extension(const char *name) {
|
||||
return !strcmp(name,"framebridge_hide_fb_foveation") ? (PFN_xrVoidFunction)query : NULL;
|
||||
}
|
||||
static XrResult XRAPI_CALL enumerate(const char *layer,uint32_t capacity,uint32_t *count,XrExtensionProperties *out) {
|
||||
(void)layer; *count=2;
|
||||
if (capacity && capacity<2) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
if (capacity) {
|
||||
strcpy(out[0].extensionName,"XR_FB_foveation");
|
||||
strcpy(out[1].extensionName,"XR_KHR_vulkan_enable");
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static XrResult XRAPI_CALL create(const XrInstanceCreateInfo *info,XrInstance *instance) {
|
||||
(void)instance; requested=info->enabledExtensionCount; return XR_SUCCESS;
|
||||
}
|
||||
static XrResult XRAPI_CALL gipa(XrInstance instance,const char *name,PFN_xrVoidFunction *fn) {
|
||||
(void)instance;
|
||||
if (!strcmp(name,"xrCreateInstance")) *fn=(PFN_xrVoidFunction)create;
|
||||
else if (!strcmp(name,"xrEnumerateInstanceExtensionProperties")) *fn=(PFN_xrVoidFunction)enumerate;
|
||||
else return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static void *fake_dlopen(const char *name,int flags) { (void)name;(void)flags;return (void *)(uintptr_t)1; }
|
||||
static void *fake_dlsym(void *handle,const char *name) {
|
||||
(void)handle;
|
||||
if (!strcmp(name,"xrGetInstanceProcAddr")) return (void *)gipa;
|
||||
if (!strcmp(name,"framebridge_extension_proc")) return (void *)extension;
|
||||
return NULL;
|
||||
}
|
||||
int __android_log_print(int priority,const char *tag,const char *format,...) {
|
||||
(void)priority;(void)tag;(void)format;return 0;
|
||||
}
|
||||
#define dlopen fake_dlopen
|
||||
#define dlsym fake_dlsym
|
||||
#include "xrshim.c"
|
||||
int main(void) {
|
||||
PFN_xrVoidFunction fn=NULL;
|
||||
assert(xrGetInstanceProcAddr(XR_NULL_HANDLE,"xrEnumerateInstanceExtensionProperties",&fn)==XR_SUCCESS);
|
||||
PFN_xrEnumerateInstanceExtensionProperties e=(PFN_xrEnumerateInstanceExtensionProperties)fn;
|
||||
const char *names[]={"XR_FB_foveation","XR_KHR_vulkan_enable"};
|
||||
XrInstanceCreateInfo ci={XR_TYPE_INSTANCE_CREATE_INFO};ci.enabledExtensionNames=names;ci.enabledExtensionCount=2;
|
||||
for (enabled=0;enabled<2;++enabled) {
|
||||
uint32_t count=0;
|
||||
assert(e(NULL,0,&count,NULL)==XR_SUCCESS && count==(enabled ? 1u : 2u));
|
||||
XrExtensionProperties out[2]={{XR_TYPE_EXTENSION_PROPERTIES},{XR_TYPE_EXTENSION_PROPERTIES}};
|
||||
assert(e(NULL,2,&count,out)==XR_SUCCESS && count==(enabled ? 1u : 2u));
|
||||
if (enabled) assert(!strcmp(out[0].extensionName,"XR_KHR_vulkan_enable"));
|
||||
assert(xrGetInstanceProcAddr(XR_NULL_HANDLE,"xrCreateInstance",&fn)==XR_SUCCESS);
|
||||
assert(((PFN_xrCreateInstance)fn)(&ci,NULL)==XR_SUCCESS);
|
||||
assert(requested==(enabled ? 1 : 2) && ci.enabledExtensionCount==2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <openxr/openxr.h>
|
||||
// Exact platform image layout; no Vulkan driver or host Vulkan SDK is needed.
|
||||
typedef struct { XrStructureType type; void *next; uint64_t image; } XrSwapchainImageVulkanKHR;
|
||||
#define LOG(...) ((void)0)
|
||||
#include "hide_fb_foveation.h"
|
||||
#include "hide_foveation_images.h"
|
||||
static int fail_images;
|
||||
static XrResult XRAPI_CALL enumerate(const char *layer, uint32_t capacity, uint32_t *count, XrExtensionProperties *out) {
|
||||
(void)layer;
|
||||
const char *names[] = {"XR_KHR_vulkan_enable2", "XR_FB_foveation", "XR_FB_foveation_vulkan",
|
||||
"XR_FB_foveation_configuration", "XR_META_foveation_eye_tracked", "XR_EXT_debug_utils"};
|
||||
*count = 6;
|
||||
if (capacity && capacity < 6) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||
if (capacity) for (unsigned i=0;i<6;++i) {
|
||||
assert(out[i].type == XR_TYPE_EXTENSION_PROPERTIES);
|
||||
strcpy(out[i].extensionName, names[i]);
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static XrResult XRAPI_CALL create(const XrInstanceCreateInfo *info, XrInstance *instance) {
|
||||
(void)instance;
|
||||
assert(info->enabledExtensionCount == 1);
|
||||
assert(!fb_is_foveation_extension(info->enabledExtensionNames[0]));
|
||||
return XR_ERROR_RUNTIME_FAILURE;
|
||||
}
|
||||
static XrResult XRAPI_CALL images(XrSwapchain swapchain, uint32_t capacity, uint32_t *count, XrSwapchainImageBaseHeader *base) {
|
||||
(void)swapchain;
|
||||
XrSwapchainImageVulkanKHR *out = (void *)base;
|
||||
for (uint32_t i=0;i<capacity;++i) {
|
||||
XrBaseOutStructure *p=out[i].next;
|
||||
assert(p && p->type == (XrStructureType)123 && !p->next);
|
||||
out[i].image=1234+i;
|
||||
}
|
||||
*count=capacity;
|
||||
return fail_images ? XR_ERROR_RUNTIME_FAILURE : XR_SUCCESS;
|
||||
}
|
||||
int main(void) {
|
||||
uint32_t count=0;
|
||||
assert(fb_hide_enumerate(enumerate,NULL,0,&count,NULL)==XR_SUCCESS && count==2);
|
||||
XrExtensionProperties out[2]={{XR_TYPE_EXTENSION_PROPERTIES},{XR_TYPE_EXTENSION_PROPERTIES}};
|
||||
assert(fb_hide_enumerate(enumerate,NULL,1,&count,out)==XR_ERROR_SIZE_INSUFFICIENT && count==2);
|
||||
assert(fb_hide_enumerate(enumerate,"layer",2,&count,out)==XR_SUCCESS);
|
||||
assert(!strcmp(out[1].extensionName,"XR_EXT_debug_utils"));
|
||||
assert(fb_hide_enumerate(enumerate,NULL,1,&count,NULL)==XR_ERROR_VALIDATION_FAILURE);
|
||||
const char *names[]={"XR_FB_foveation","XR_KHR_vulkan_enable2","XR_FB_foveation_vulkan"};
|
||||
XrInstanceCreateInfo ci={XR_TYPE_INSTANCE_CREATE_INFO}; ci.enabledExtensionNames=names; ci.enabledExtensionCount=3;
|
||||
assert(fb_hide_create_instance(create,&ci,NULL)==XR_ERROR_RUNTIME_FAILURE);
|
||||
assert(ci.enabledExtensionCount==3 && ci.enabledExtensionNames==names);
|
||||
const char *gles[]={"XR_KHR_opengl_es_enable"}; ci.enabledExtensionNames=gles; ci.enabledExtensionCount=1;
|
||||
assert(!fb_instance_uses_vulkan(&ci));
|
||||
assert(fb_hide_create_instance(create,&ci,NULL)==XR_ERROR_RUNTIME_FAILURE);
|
||||
XrBaseOutStructure fov2={XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB,NULL};
|
||||
XrBaseOutStructure unknown={(XrStructureType)123,&fov2};
|
||||
XrBaseOutStructure fov1={XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB,&unknown};
|
||||
XrSwapchainImageVulkanKHR image={XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR,&fov1,0};
|
||||
for (fail_images=0;fail_images<2;++fail_images) {
|
||||
XrResult r=fb_hide_foveation_images(images,XR_NULL_HANDLE,1,&count,(void *)&image);
|
||||
assert(r==(fail_images ? XR_ERROR_RUNTIME_FAILURE : XR_SUCCESS));
|
||||
assert(image.next==&fov1 && fov1.next==&unknown && unknown.next==&fov2);
|
||||
assert(image.image==1234);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -47,3 +47,26 @@ def test_add_needed(variant):
|
||||
assert all(off % 16384 == va % 16384 for va, off, _ in loads)
|
||||
# idempotent
|
||||
assert elf.add_needed(out, "libglshim.so") == out
|
||||
|
||||
|
||||
def _scramble_section_names(lib: bytes) -> bytes:
|
||||
"""Rotate every section's sh_name by one, like a shipped libOVRPlugin.so whose names are shifted (GitHub #349:
|
||||
.dynamic named "dding", .dynsym "text"); types and links stay right."""
|
||||
buf = bytearray(lib)
|
||||
shoff = struct.unpack_from("<Q", buf, 40)[0]
|
||||
shentsize, shnum = struct.unpack_from("<H", buf, 58)[0], struct.unpack_from("<H", buf, 60)[0]
|
||||
names = [struct.unpack_from("<I", buf, shoff + i * shentsize)[0] for i in range(1, shnum)]
|
||||
for i, name in enumerate(names[1:] + names[:1], start=1):
|
||||
struct.pack_into("<I", buf, shoff + i * shentsize, name)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def test_scrambled_section_names():
|
||||
base = elf.add_needed(build_stub_library(["ovr_X", "ovr_Y"], soname="libgame.so"), "liblog.so")
|
||||
lib = _scramble_section_names(base)
|
||||
assert elf.dynsym_section(elf._elf(lib)) is not None
|
||||
assert elf.dyn_symbols(lib, True) == {"ovr_X", "ovr_Y"}
|
||||
assert elf.find_symbols(lib, ["ovr_Y"]).keys() == {"ovr_Y"}
|
||||
out = elf.add_needed(lib, "libfp_x.so") # raised "produced NEEDED=[...]" when sections were found by name
|
||||
assert elf.needed(out) == ["libfp_x.so", "liblog.so"]
|
||||
assert elf.dyn_symbols(out, True) == {"ovr_X", "ovr_Y"}
|
||||
@@ -1,115 +0,0 @@
|
||||
"""Google Daydream / Cardboard (GVR SDK) apps: detected, warned about, never marked unsupported (GitHub #335)."""
|
||||
import zipfile
|
||||
|
||||
from conftest import build_axml
|
||||
|
||||
from frameport.analysis import detect
|
||||
from frameport.recommend import engine
|
||||
from frameport.validate.triage import triage
|
||||
|
||||
DAYDREAM_VR_MODE = "com.google.vr.vrcore/com.google.vr.vrcore.common.VrCoreListenerService"
|
||||
|
||||
|
||||
def _manifest(package="com.example.daydream", categories=(), enable_vr_mode=None):
|
||||
activity = [("name", "str", "com.unity3d.player.UnityPlayerActivity")]
|
||||
if enable_vr_mode:
|
||||
activity.append(("enableVrMode", "str", enable_vr_mode))
|
||||
cats = []
|
||||
for c in ("android.intent.category.LAUNCHER", *categories):
|
||||
cats += [("start", "category", [("name", "str", c)]), ("end", "category")]
|
||||
return build_axml([
|
||||
("start", "manifest", [("package", "str", package)]),
|
||||
("start", "uses-sdk", [("minSdkVersion", "int", 24), ("targetSdkVersion", "int", 26)]),
|
||||
("end", "uses-sdk"),
|
||||
("start", "application", []),
|
||||
("start", "activity", activity),
|
||||
("start", "intent-filter", []),
|
||||
("start", "action", [("name", "str", "android.intent.action.MAIN")]),
|
||||
("end", "action"),
|
||||
*cats,
|
||||
("end", "intent-filter"),
|
||||
("end", "activity"),
|
||||
("end", "application"),
|
||||
("end", "manifest"),
|
||||
])
|
||||
|
||||
|
||||
def _apk(tmp_path, manifest, libs=("libmain.so", "libunity.so"), abi="armeabi-v7a", dex=b"dex\n035\0"):
|
||||
p = tmp_path / "game.apk"
|
||||
with zipfile.ZipFile(p, "w") as z:
|
||||
z.writestr("AndroidManifest.xml", manifest)
|
||||
z.writestr("classes.dex", dex)
|
||||
for lib in libs:
|
||||
z.writestr(f"lib/{abi}/{lib}", b"\x7fELF-" + lib.encode())
|
||||
return p
|
||||
|
||||
|
||||
def test_google_vr_signals():
|
||||
assert detect.google_vr(set(), {detect.DAYDREAM_CATEGORY}, []) == "daydream"
|
||||
assert detect.google_vr(set(), set(), [DAYDREAM_VR_MODE]) == "daydream"
|
||||
assert detect.google_vr(set(), {detect.CARDBOARD_CATEGORY}, []) == "cardboard"
|
||||
assert detect.google_vr({"libgvr.so"}, set(), []) == "cardboard"
|
||||
assert detect.google_vr({"libGfxPluginCardboard.so"}, set(), []) == "cardboard"
|
||||
assert detect.google_vr(set(), set(), [], gvr_java=True) == "cardboard"
|
||||
assert detect.google_vr(set(), set(), [], boot="vr-enabled=1\nvr-device-list=Daydream,Cardboard\n") == "daydream"
|
||||
# both: Daydream is the more specific
|
||||
assert detect.google_vr({"libgvr.so"}, {detect.CARDBOARD_CATEGORY, detect.DAYDREAM_CATEGORY}, []) == "daydream"
|
||||
# Google's spatial audio alone isn't Google VR (Quest games ship it, e.g. Shadow Point)
|
||||
assert detect.google_vr({"libgvraudio.so", "libaudioplugingvrunity.so"}, set(), []) == ""
|
||||
assert detect.google_vr(set(), set(), [], boot="vr-enabled=1\nvr-device-list=Oculus\n") == ""
|
||||
|
||||
|
||||
def test_daydream_apk_is_detected(tmp_path):
|
||||
"""Blade Runner: Revelations' shape: Unity 2017, 32-bit, Daydream category + VrCore's VR mode, libgvr.so."""
|
||||
m = _manifest(categories=[detect.DAYDREAM_CATEGORY], enable_vr_mode=DAYDREAM_VR_MODE)
|
||||
a = detect.analyze(_apk(tmp_path, m, ("libmain.so", "libunity.so", "libgvr.so", "libaudioplugingvrunity.so")))
|
||||
assert a.extra["google_vr"] == "daydream"
|
||||
assert a.vr_kind == "none" and a.extra["analysis_version"] == detect.ANALYSIS_VERSION
|
||||
|
||||
|
||||
def test_cardboard_from_java_classes(tmp_path):
|
||||
dex = b"dex\n035\0...Lcom/google/vr/sdk/base/GvrView;..."
|
||||
a = detect.analyze(_apk(tmp_path, _manifest(), dex=dex))
|
||||
assert a.extra["google_vr"] == "cardboard"
|
||||
|
||||
|
||||
def test_quest_game_with_google_audio_is_not_google_vr(tmp_path):
|
||||
a = detect.analyze(_apk(tmp_path, _manifest(), ("libmain.so", "libunity.so", "libvrapi.so", "libgvraudio.so")))
|
||||
assert a.extra["google_vr"] == "" and engine.google_vr_note(a) is None
|
||||
|
||||
|
||||
def test_warning_but_never_unsupported(tmp_path):
|
||||
m = _manifest(categories=[detect.DAYDREAM_CATEGORY], enable_vr_mode=DAYDREAM_VR_MODE)
|
||||
a = detect.analyze(_apk(tmp_path, m, ("libmain.so", "libunity.so", "libgvr.so")))
|
||||
note = engine.google_vr_note(a)
|
||||
assert note and "Daydream" in note and "Oculus build" in note
|
||||
assert note in engine.blocker_notes(a)
|
||||
r = engine.suggest(a, use_catalog=False)
|
||||
assert r.status != "unsupported" # the manifest alone never decides; the launch test's triage does
|
||||
assert note in r.notes and "Android app without VR" not in r.notes
|
||||
|
||||
|
||||
def test_daydream_and_oculus_build_goes_the_normal_way(tmp_path):
|
||||
"""A Unity build with both Daydream and Oculus support: the Oculus path works, so no warning."""
|
||||
m = _manifest(categories=[detect.DAYDREAM_CATEGORY], enable_vr_mode=DAYDREAM_VR_MODE)
|
||||
a = detect.analyze(_apk(tmp_path, m, ("libmain.so", "libunity.so", "libgvr.so", "libvrapi.so")))
|
||||
assert a.extra["google_vr"] == "daydream" and a.vr_kind == "quest"
|
||||
assert engine.google_vr_note(a) is None
|
||||
|
||||
|
||||
def test_triage_gvr_not_supported():
|
||||
"""Lines from #335's launch log (Blade Runner: Revelations on the 32-bit bridge)."""
|
||||
lines = [
|
||||
"10-16 05:27:34.526 1355 1384 W a32bridge: libgvr.so: Java_com_google_vr_sdk_base_CardboardViewNativeImpl_"
|
||||
"nativeDestroy: class com/google/vr/sdk/base/CardboardViewNativeImpl not found",
|
||||
"10-16 05:27:34.560 1355 1384 E Unity : Google VR Error [Daydream]: Google VR had a fatal issue while "
|
||||
"loading. VR will not be available.",
|
||||
"10-16 05:27:43.854 225 1097 W ActivityManager: Unable to start service Intent { "
|
||||
"act=com.google.vr.vrcore.controller.BIND pkg=com.google.vr.vrcore } U=0: not found",
|
||||
]
|
||||
for line in lines:
|
||||
r = triage(line + "\n", "RUNNING")
|
||||
assert [f.id for f in r.findings] == ["gvr-not-supported"] and r.verdict == "fail", line
|
||||
# GVR's own start-up line alone isn't a failure (a Daydream build with Oculus support logs it too)
|
||||
assert not triage("10-16 05:27:34.555 1355 1355 I GVR : [vr/gvr/capi/src/gvr.cc:103] Initialized GVR "
|
||||
"version 1.101.0\n", "RUNNING").findings
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Native regression: dispatcher filtering and reversible Vulkan output-chain stripping."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INC = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
|
||||
CC = shutil.which("cc") or shutil.which("clang")
|
||||
pytestmark = [pytest.mark.native, pytest.mark.skipif(
|
||||
not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not INC or not CC,
|
||||
reason="needs FRAMEPORT_NATIVE_TESTS=1, cached OpenXR headers and host C compiler")]
|
||||
|
||||
def test_foveation_dispatcher_and_images(tmp_path):
|
||||
exe = tmp_path / "storage-test"
|
||||
subprocess.run([CC, "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-missing-field-initializers", "-I", str(INC),
|
||||
"-I", str(ROOT / "native/adapter"),
|
||||
str(ROOT / "tests/fixtures/src/hide_fb_foveation_test.c"),
|
||||
"-o", str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
|
||||
def test_shim_filters_synthetic_dispatcher_before_instance(tmp_path):
|
||||
android = tmp_path / "android"
|
||||
android.mkdir()
|
||||
(android / "log.h").write_text(
|
||||
"#define ANDROID_LOG_INFO 4\nint __android_log_print(int, const char *, const char *, ...);\n")
|
||||
exe = tmp_path / "dispatcher-test"
|
||||
subprocess.run([CC, "-std=c11", "-D_GNU_SOURCE", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-missing-field-initializers", "-I", str(INC), "-I", str(tmp_path),
|
||||
"-I", str(ROOT / "native/xrshim"),
|
||||
str(ROOT / "tests/fixtures/src/hide_fb_dispatcher_test.c"),
|
||||
"-pthread", "-lm", "-ldl", "-o", str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -1029,3 +1029,19 @@ def test_ovr_stable_user_suggested_for_platform_games():
|
||||
p = base.get("frame.ovr_stable_user")
|
||||
assert p.detect(_analysis(engine="Unity", libs=["libovrplatformloader.so", "libunity.so"])).recommended
|
||||
assert p.detect(_analysis(engine="Unity", libs=["libunity.so"])) is None
|
||||
|
||||
|
||||
def test_hide_fb_foveation_setting():
|
||||
from frameport.patches.settings import UI, XRSHIM_SETTINGS
|
||||
from frameport.patches.frame.adapter import needs_xrshim
|
||||
|
||||
patch = base.get("adapter.hide_fb_foveation")
|
||||
assert patch.applies(_analysis())
|
||||
assert not patch.applies(_analysis(graphics="OpenGL ES"))
|
||||
assert patch.detect(_analysis()) is None
|
||||
assert "hide_fb_foveation" not in adapter_settings({})
|
||||
recipe = {patch.id: {"value": 1}}
|
||||
assert adapter_settings(recipe)["hide_fb_foveation"] == 1
|
||||
assert "hide_fb_foveation" in XRSHIM_SETTINGS
|
||||
assert needs_xrshim(recipe)
|
||||
assert UI["hide_fb_foveation"]["level"] == "advanced"
|
||||
@@ -17,6 +17,27 @@ LOG_BAD = ("""
|
||||
""")
|
||||
|
||||
|
||||
def test_unreal_reservation_refused_while_loading():
|
||||
"""GitHub #349: Unreal's out-of-memory handler crashes inside libUnreal.so's static constructors."""
|
||||
lib = "/data/app/~~x==/com.Meta4.TFBeyondReality-y==/lib/arm64/libUnreal.so (BuildId: 57d064dc)"
|
||||
crash = (
|
||||
"10-16 14:41:01.814 1185 1185 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x10"
|
||||
" in tid 1185 (TFBeyondReality), pid 1185 (TFBeyondReality)\n"
|
||||
"10-16 14:41:01.858 1219 1219 F DEBUG : Cause: null pointer dereference\n"
|
||||
"10-16 14:41:01.955 1219 1219 F DEBUG : backtrace:\n"
|
||||
f"10-16 14:41:01.955 1219 1219 F DEBUG : #00 pc 00000000018bbe3c {lib}\n"
|
||||
f"10-16 14:41:01.955 1219 1219 F DEBUG : #01 pc 00000000017ceccc {lib}\n"
|
||||
"10-16 14:41:01.957 1219 1219 F DEBUG : #15 pc 000000000004b048 /apex/com.android.runtime/bin/"
|
||||
"linker64 (__dl__ZN6soinfo17call_constructorsEv+380)\n")
|
||||
r = triage("", "EXITED", crash=crash)
|
||||
ids = {f.id for f in r.findings}
|
||||
assert "unreal-reserve-refused" in ids and "native-crash" not in ids
|
||||
assert next(f for f in r.findings if f.id == "unreal-reserve-refused").suggest == ["frame.unreal_noreserve"]
|
||||
# the same fault address in a later render-thread crash isn't this
|
||||
later = crash.replace("__dl__ZN6soinfo17call_constructorsEv+380", "__pthread_start(void*)+64")
|
||||
assert "unreal-reserve-refused" not in {f.id for f in triage("", "EXITED", crash=later).findings}
|
||||
|
||||
|
||||
def test_lua_pointer_error_and_cleanup_stack_are_distinguished():
|
||||
pointer = triage("PANIC: bad light userdata pointer", "EXITED")
|
||||
finding = next(f for f in pointer.findings if f.id == "lua-lightuserdata-pointer")
|
||||
@@ -539,3 +560,14 @@ def test_meta_entitlement_quest():
|
||||
"I Unity : ovr_Entitlement_GetIsViewerEntitled", "I Unity : User is entitled"):
|
||||
assert "meta-entitlement" not in {x.id for x in triage(ok + "\n" + LOG_OK, "RUNNING").findings}
|
||||
assert triage("E Unity : Entitlement check failed\n" + LOG_OK, "RUNNING").verdict == "pass"
|
||||
|
||||
|
||||
def test_missing_quest_foveation_image_is_diagnostic():
|
||||
message = "OVRPlugin: foveationImage = 0x0, width = 0, height = 0"
|
||||
result = triage(message, "EXITED")
|
||||
finding = next(f for f in result.findings if f.id == "quest-foveation-image-missing")
|
||||
assert finding.severity == "info" and finding.question
|
||||
assert finding.suggest == ["adapter.hide_fb_foveation"]
|
||||
assert "adapter.hide_fb_foveation" not in result.suggestions()
|
||||
result = triage(message + "\nFrameBridge: hide_fb_foveation: filtering dispatcher extensions", "EXITED")
|
||||
assert "quest-foveation-image-missing" not in {f.id for f in result.findings}
|
||||
@@ -0,0 +1,91 @@
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
from test_unreal_splash import context, engine
|
||||
|
||||
from frameport.analysis import detect, elf
|
||||
from frameport.apk.workspace import ApkWorkspace
|
||||
from frameport.patches.frame import unreal_noreserve as nr
|
||||
|
||||
SLOT = 0x123458 # mmap's GOT slot (any address: _mmap_slots is replaced below)
|
||||
|
||||
|
||||
def adrp(rd: int, pc: int, target: int) -> int:
|
||||
pages = (target >> 12) - (pc >> 12)
|
||||
return 0x90000000 | (pages & 3) << 29 | ((pages >> 2) & 0x7FFFF) << 5 | rd
|
||||
|
||||
|
||||
def bl(pc: int, target: int) -> int:
|
||||
return 0x94000000 | ((target - pc) >> 2) & 0x3FFFFFF
|
||||
|
||||
|
||||
def mmap_engine(prot=nr.MOV_W2_RW, flags=nr.MOV_W3_FLAGS):
|
||||
"""An engine library whose code calls mmap(NULL, size, prot, flags, -1, 0) through a PLT-style stub."""
|
||||
data, off = engine(symbol="FAndroidVirtualReserve", size=0x80, prologue=b"")
|
||||
va = next(v for v, o, s in elf.load_segments(data) if o <= off < o + s) + off - next(
|
||||
o for v, o, s in elf.load_segments(data) if o <= off < o + s)
|
||||
stub = va + 0x40
|
||||
call = [nr.MOV_X0_NULL, prot, flags, 0x12800004, 0xAA1F03E5] # mov w4, #-1; mov x5, xzr
|
||||
code = call + [bl(va + 4 * len(call), stub)]
|
||||
words = code + [0xD503201F] * (16 - len(code)) # nop up to the stub
|
||||
words += [adrp(16, stub, SLOT), 0xF9400211 | ((SLOT & 0xFFF) // 8) << 10, 0x91000210, 0xD61F0220]
|
||||
buf = bytearray(data)
|
||||
struct.pack_into(f"<{len(words)}I", buf, off, *words)
|
||||
return bytes(buf), off + 8 # file offset of the flags instruction
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_got(monkeypatch):
|
||||
monkeypatch.setattr(nr, "_mmap_slots", lambda data: {SLOT})
|
||||
|
||||
|
||||
def test_reservation_gets_map_noreserve_and_survives_rebuild(tmp_path):
|
||||
data, flags_at = mmap_engine()
|
||||
assert nr.mmap_sites(data) == ([flags_at], [])
|
||||
patch = nr.UnrealNoReserve()
|
||||
ctx = context(tmp_path, data)
|
||||
with ctx.ws:
|
||||
assert patch.apply(ctx)
|
||||
fixed = ctx.ws.read_lib("libUE4.so")
|
||||
assert fixed == data[:flags_at] + struct.pack("<I", nr.MOV_W3_NORESERVE) + data[flags_at + 4:]
|
||||
assert patch.validate(ctx)[0][1]
|
||||
assert not patch.apply(ctx)
|
||||
ctx.ws.write(tmp_path / "out.apk")
|
||||
with ApkWorkspace(tmp_path / "out.apk") as ws:
|
||||
ctx.ws = ws
|
||||
assert not patch.apply(ctx)
|
||||
assert nr.mmap_sites(ws.read_lib("libUE4.so")) == ([], [flags_at])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kwargs", [{"prot": 0x52800022}, {"flags": 0x52800043}]) # PROT_READ only; MAP_SHARED
|
||||
def test_other_mmap_calls_are_left_alone(tmp_path, kwargs):
|
||||
data, _ = mmap_engine(**kwargs)
|
||||
assert nr.mmap_sites(data) == ([], [])
|
||||
ctx = context(tmp_path, data)
|
||||
with ctx.ws:
|
||||
assert not nr.UnrealNoReserve().apply(ctx)
|
||||
|
||||
|
||||
def test_call_to_another_function_is_left_alone(monkeypatch):
|
||||
data, _ = mmap_engine()
|
||||
monkeypatch.setattr(nr, "_mmap_slots", lambda data: {SLOT + 8})
|
||||
assert nr.mmap_sites(data) == ([], [])
|
||||
|
||||
|
||||
def test_binned3_detection():
|
||||
assert detect.unreal_binned3(b"x\0./Runtime/Core/Private/HAL/MallocBinned3.cpp\0y")
|
||||
assert detect.unreal_binned3("FMallocBinned3 Attempt to free an unrecognized pointer %p".encode("utf-16-le"))
|
||||
# other UE5 builds only name Binned3's console variables
|
||||
assert not detect.unreal_binned3("MallocBinned3.FlushThreadCacheMaxWaitTime".encode("utf-16-le"))
|
||||
|
||||
|
||||
def test_suggested_only_for_binned3_unreal():
|
||||
patch = nr.UnrealNoReserve()
|
||||
|
||||
class A:
|
||||
engine, abis, extra = "Unreal", ["arm64-v8a"], {"unreal_binned3": True}
|
||||
assert patch.detect(A()).recommended
|
||||
A.extra = {}
|
||||
assert patch.detect(A()) is None
|
||||
A.engine = "Unity"
|
||||
assert not patch.applies(A())
|
||||
Reference in new issue
Block a user