Compare commits

..
Author SHA1 Message Date
spoopyghosty0 e185e24ed7 Docs: update screenshots and videos 2026-10-17 21:03:01 +00:00
spoopyghosty0 5e668f33d3 Batman audio hooks: place stubs in verified free gaps, say which step failed
On the Frame the atomic-branch detours never installed ("installation failed"): an mmap hint isn't honoured,
so the stub landed out of B range. Stubs now go into free gaps from /proc/self/maps within +-128 MiB of the
entry, mapped with MAP_FIXED_NOREPLACE (a relocated result is rejected); no gap = no hook. Every failure logs
the target and the failing step.
2026-10-17 16:47:06 -04:00
spoopyghosty0 feb4658dde Batman audio hooks: publish detours with one atomic branch (#389, #34)
The detour helper from PR #34 (used by the metadata hook and PR #389's two geometry hooks) wrote a 16-byte
branch + literal in two steps while game threads may run the function (installation is retried from
xrPollEvent). Each hook now publishes a single aligned release-atomic AArch64 B to a stub within +-128 MiB;
the trampoline replays the one verified instruction and returns to entry+4. Every mprotect is checked;
failures before publication free the stub and trampoline and leave the function untouched; no stub in range =
no hook. Adapter artifacts rebuilt by us (the PR's binary isn't shipped).
2026-10-17 16:47:06 -04:00
Lucas-Mathieu 4ed7929b1c Include package revisions in showcase build fingerprints 2026-10-17 16:47:06 -04:00
Lucas-Mathieu 61a6b3e2b2 Keep the ELF installer regression Linux-specific 2026-10-17 16:47:06 -04:00
Lucas-Mathieu 78fc926a48 Fix Batman audio geometry input lifetime across background loading 2026-10-17 16:47:05 -04:00
spoopyghosty0 503e64367c Showcase: monitor_settled waits for the newest fake stream
The Frame card starts a stream before the Monitor tab, whose faster interval starts a new one: the hook
returned on the first stream's flag while the new one was still replaying (CI rendered 71 vs 72 fps).
Each frozen session now has its own settled flag; the hook waits for the newest one.
2026-10-17 16:42:54 -04:00
spoopyghosty0 3deb279270 Headset model: an unknown OVRPort dispatcher keeps Quest 2 instead of failing the build (#373)
Quest 3 is the default for every Quest game, but the dispatcher patch only knows OVRPort 3.4.3-aa54c3f: an
APK converted with another runtime (found with an already converted Batman copy) failed to build. It now
keeps OVRPort's Quest 2 answers and says so in the build checks.
2026-10-17 16:35:35 -04:00
spoopyghosty0 0df1212bfe Showcase: the monitor shot waits for the frozen stream's replay
The Monitor tab asks the frozen fake stream to replay its history; on CI's slower runner the shot came
halfway through, so its numbers differed between renders ("three renders, no two alike"). New hook
monitor_settled waits until the replay is done.
2026-10-17 16:34:23 -04:00
34 changed files with 800 additions and 36 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
2d1726aaa8ee0ce788df1b8727bd55ab72efae749e7652f38f67e5e317b74b2f ./arm64-v8a/libopenxr_loader_generic.so
db23c2f84255bc00fca0c6aaf22188372f7d243619eded3c3654bc7f16dfcdcc ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
Binary file not shown.
+6
View File
@@ -523,6 +523,12 @@ signatures:
diagnosis: The game crashed while leaving VR, inside Valve's foveation layer when it destroys a swapchain (for example Lambda1VR). This can happen after manually quitting; it is not a startup crash and does not explain distorted geometry seen before quitting.
suggest: []
supersedes: [native-crash]
- id: batman-audio-geometry-allocation
pattern: '(?s)\A(?=.*com\.camouflaj\.manta)(?=.*Trying to allocate: [1-9][0-9]{10,}B)(?=.*MetaXRAudioUnityMemoryAllocator\.cpp)'
severity: fatal
diagnosis: "Batman's Meta XR Audio geometry loader requested tens of gigabytes while loading a scene. Its background task can outlive the UnityWebRequest that owns the input buffer. Update Batman on Frame with the current FramePort: the adapter retains a private copy until parsing finishes, for the verified Batman build. This is a game allocation crash, not evidence of a headset thermal shutdown."
suggest: [frame.adapter]
supersedes: [native-crash]
- id: native-crash
pattern: 'Fatal signal (\d+) \(SIG[A-Z]+\)'
severity: fatal
+105
View File
@@ -0,0 +1,105 @@
# Batman audio geometry input lifetime
Batman: Arkham Shadow 1.4.1 could abort while loading a save or streaming the area
around a real-time cutscene. Unity reported requests for 26,388,238,103 or
147,520,448,640 bytes through `MetaXRAudioUnityMemoryAllocator.cpp`, followed by
`SIGTRAP` on an IL2CPP worker. The first captured stack reached
`GeometricAudioMeshPreprocessor::readMeshInfo` through
`ovrAudio_AudioGeometryReadMeshMemory`.
The serialized asset on disk was intact. In one failed run its vertex count was
read as 1 instead of 17,251. The managed-code trace showed a lifetime risk:
`LoadGeometryAsync` obtains a `NativeArray<byte>.ReadOnly` from a
`UnityWebRequest` download handler, calls async-void `LoadGeometryFromMemory`,
then returns. The background task retains the pointer/length pair, but not the
request owning those bytes. Its `GC.KeepAlive` boxes the read-only view rather
than retaining the request. The trace and device test support premature source
reuse as the cause; no hardware watch established the exact freeing instruction.
## Repair and isolation
`audio_geometry_owned.h` copies the input synchronously at
`LoadGeometryFromMemory`, before `Task.Run` can outlive the request. A mutex
protects the list of copies. The original worker still parses and processes the
audio geometry; its completion wrapper removes and frees the corresponding copy.
This also handles a worker finishing before the queuing call returns. NativeArray
length/padding bits and IL2CPP's hidden method-info arguments are preserved.
Null/invalid lengths and allocation failure retain the original call. A task
that never runs retains its copy until process exit; there is no timed expiry
that could free data beneath a late worker.
`batman_audio_geometry.h` enables the repair only in `com.camouflaj.manta`, on
AArch64, after the existing audio metadata initializer verifies Meta XR Audio
Wwise build `e1619e7fb839badf0ea5ecc22d9f028d0b17c34d`. It additionally requires
IL2CPP build `1d48a41d7e9011bef5fae0c23c404c62a4e7a73c` and exact 16-byte entry
sequences at both managed hooks. Unknown builds are left untouched. The live
IL2CPP addresses are `0x2e61bb4` (queue) and `0x2e63a28` (worker); text file
offsets in this build are 0x1000 lower. These offsets must not be reused for
another game or build.
Both geometry hooks and the metadata hooks use `audio_detour.h`. After the
16-byte entry guard, only the first, naturally aligned instruction is replaced:
a release atomic store publishes an AArch64 `B` to a nearby stub. The stub's
absolute jump and the trampoline (one verified non-PC-relative instruction,
then a direct branch to entry+4) are complete, instruction-cache flushed and RX
before publication. The original-call pointer is published first. Remaining
entry instructions are unchanged, so workers already past the entry can finish
normally. `/proc/self/maps` supplies free gaps, clipped to the ranges of both
branches. Page-aligned candidates are tried outward from the target (lower
address first on equal distances) with `MAP_FIXED_NOREPLACE`; EEXIST/EINVAL
advance to the next candidate. Existing mappings are never replaced, and a
kernel that ignores the flag and relocates a mapping has that result unmapped.
If no candidate works, that hook is skipped without changing its entry. No ELF
page-slack fallback is used: zero bytes alone cannot establish that padding is
outside all section content, and the loaded program headers do not prove this.
Every mprotect result is checked. Failures before entry publication free the
mapping. If restoring RX after publication fails, the entry word is restored,
its instruction cache is flushed, and restoring RX is attempted again. The
mapping and original-call pointer must then remain valid until process exit:
a worker may already have followed the published branch. Freeing them without
quiescing those threads would introduce a use-after-unmap. A persistent kernel
mprotect failure can leave the entry page RWX. Each hook failure logs its target
and failing operation once; metadata installation is attempted once per verified
library. Allocation failure reports the closest eligible free page, last
attempted address, actual mmap result, errno and attempt count. Null closest and
zero attempts mean no eligible free gap. Protection failures identify stub RX,
entry RWX, or entry RX, including the rollback RX errno (zero means it succeeded).
This exceptional rollback cannot provide immediate reclamation or guarantee RX
when the kernel refuses it. The previous hint-based publication build failed installation on the Steam
Frame; this gap-search replacement has host validation only.
This is separate from the existing [audio metadata retirement repair](AUDIO_METADATA.md).
It does not change the decoder, acoustic processing, tracking, foveation,
resolution, refresh rate, or other games' recipes. Temporary input dumps, native
parser bypasses and redundant late copies used during investigation are not
included. Production logs are limited to installation and the first four
copy/completion pairs. No game assets are included in the repository.
The `frame.adapter` package revision increases only for Batman. Existing Batman
installs therefore offer **Update on Frame**; other games are not marked outdated
by this repair. Updating/rebuilding Batman installs the new adapter through the
normal path; no launcher script or additional option is needed.
## Validation
- Native regressions cover deferred and inline completion, source destruction
before worker execution, concurrent loads, cleanup, allocation failure,
argument preservation, and rejection of unknown build IDs/function entries.
Host C tests also cover branch-range endpoints, stub/trampoline encoding,
publication order, synthetic maps with in-range/out-of-range/absent gaps and
adjacent mappings, outward placement order, EEXIST/EINVAL fallback, rejected
relocated mmap results and mprotect failure cleanup/rollback.
- On the headset, the affected save loaded successfully with the repair. Logs
showed private copies of 2,599,300, 159,020 and 628,088 bytes made before dispatch;
each original parser completed and each copy was released. Another 180,120-byte
geometry input also completed normally.
- The game continued for over seven minutes after that load with zero allocation
failures, rejected meshes or fatal signals. Acoustic geometry was loaded, not
disabled. That device test used the ownership repair with diagnostic guards;
none of the guards rejected a mesh. The production repair omits those guards.
- The rebuilt latest adapter passes the repository checks recorded in the PR;
the device result above is for the experimental build tested before integration.
This does not establish long-term stability or resolve the separately observed
whole-headset power-offs. Those require their own power/reset investigation.
+1 -1
View File
@@ -288,7 +288,7 @@ Steam placeholder set (no Steam art at all) → fixed in artwork/steam.py.
## PR #34: Meta XR Audio metadata (Batman)
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). Verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
**PR #34 (merged 2026-10-04, Lucas-Mathieu):** `native/adapter/audio_metadata.h` patches Meta XR Audio Wwise (only build ID e1619e7f…, Batman: Arkham Shadow 1.4.1) so queued audio metadata isn't freed while the current audio frame references it (smoke-bomb crash). The shared detour now publishes one aligned AArch64 branch to a range-checked nearby stub, with a one-instruction trampoline; protection failures roll back (published mappings must remain alive for in-flight threads). See [AUDIO_GEOMETRY.md](AUDIO_GEOMETRY.md) for failure limits and host validation; this publication change has not been device-tested. Original repair verified headless: installs ("metadata reclamation follows …"), 72 fps; we added a once-per-second scan limit. Outside PRs: review source, rebuild `artifacts/` ourselves, merge locally (contributor's commit kept), push main.
## Heuristics eval (2026-10-04)
+1
View File
@@ -524,6 +524,7 @@ FramePort's layer between the game and the Steam Frame: fills in what the Frame
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
- **Suggested (on):** Required for every OpenXR/overport build on the Frame.
- **Suggested by launch-test signatures:**
- `batman-audio-geometry-allocation` (fatal): Batman's Meta XR Audio geometry loader requested tens of gigabytes while loading a scene.
- `cube-swapchain-refused` (fatal): The game asked for a cube-map swapchain (for example an OVROverlay skybox or loading cube), which the Frame's runtime doesn't have.
- `cylinder-menu-layers-rejected` (info): The runtime rejected a frame's layers once (XR_ERROR_LAYER_INVALID).
- `graphics-requirements-missing` (fatal): The game can't start VR on the Frame; rebuild it with the current FramePort.
+2 -1
View File
@@ -122,7 +122,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
| PC VR game judders/stutters although the GPU keeps up; vrcompositor.txt "Timed out. N total" high or frames dropped | the game misses the headset's refresh (for example 96 Hz = 10.4 ms; Stormland's slow frames take 10.7 ms) | `pcvr.steamvr_tuning` (default on): next Play sets SteamVR per-app `preferredRefreshRate` + `motionSmoothingOverride` via `fp_vrsettings.exe` |
| Quest app keeps recentering / snapping the view when you turn your head; log shows session state 5→4→3→4→5 within a second | the Frame briefly takes focus; the app recenters on focus changes | adapter `focus_hold` (on by default; an earlier global debounce made AC Nexus stay black and was removed) |
| Video player / app finds no local videos; MediaProvider "Requested path /home/steamos/... doesn't appear under ..." | Lepton's /sdcard is a symlink to a host path, so Android's media index rejects every file | upload in the Files tab (Videos = /sdcard/Movies) and browse folders in the app |
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only; see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
| Batman: Arkham Shadow closes during smoke-bomb effects; later input-tree corruption | Meta XR Audio Wwise deletes queued metadata still referenced by the current audio frame, then writes through the stale pointer | FrameBridge automatically retires metadata after current-frame references disappear, for the verified AArch64 SDK build only, using a single-word entry branch and nearby stub (hook skipped if no stub fits); see [AUDIO_METADATA.md](AUDIO_METADATA.md) |
| Batman: Arkham Shadow aborts loading a save/scene, with a 26–147 GB allocation through `MetaXRAudioUnityMemoryAllocator.cpp` | The asynchronous audio geometry task retains a native view without its owning UnityWebRequest; source bytes can be reused before parsing | Update Batman on Frame: the adapter copies geometry before background dispatch and releases it after the original worker completes, only for the verified Batman IL2CPP/Meta SDK builds; see [AUDIO_GEOMETRY.md](AUDIO_GEOMETRY.md) |
| Game stays on a loading or "Waiting" box although it reached FOCUSED; log `FrameBridge: xrEndFrame failed -25` then no more `pacing:` lines (for example PowerWash Simulator on some Frames, GitHub #39) | The eye image rect ends a few pixels past its swapchain (Unity rounds the swapchain width, the runtime's recommended size differs per Frame); SteamVR rejects every frame with XR_ERROR_SWAPCHAIN_RECT_INVALID | FrameBridge clamps every projection/quad image rect to its swapchain (`rect_clamp`, default on since 0.11.0; log `rect_clamp: view N image rect clamped`): rebuild + reinstall the game |
| Black from the start, `projection view 0: ... rect=0,0 0x0` then `xrEndFrame failed -25` twice and OVRPlugin "Unable to submit Oculus frame" (BattleTalent, GitHub #293; right after Unity's "Switching to the recommended level" MSAA line) | The game submits its first projection views with an empty image rect; SteamVR rejects them and OVRPlugin gives up submitting | `rect_clamp` replaces an empty rect with the whole swapchain image (log `rect_clamp: view N empty image rect -> full swapchain WxH`): rebuild + reinstall. Check the build output for a WARN from the Unity IL2CPP fixes (`frame.unity_runtime_msaa_off` etc. "code not found in this game"): turned on, they may still change nothing |
| Black flash, then the SteamVR home environment while the game keeps running (sound plays); vrserver.txt `[OpenXR] SetXrInputConfig failed. Failed to load manifest provided by SetXrInputConfig`, then the app "has exited" and reconnects as `VRApplication_OpenXRInstance`; headless launch tests pace at 10 fps (for example Scared By Squares, GitHub #323) | Two controller actions in one action set share a name (Unreal: an action mapping and an axis mapping both called "Turn"). The Frame's runtime accepts the duplicate but can't load the input manifest it builds and drops the app as a scene app | FrameBridge gives the later duplicate a free name (`action_dedup`, default on; log `action_dedup: action "turn" ("Turn") already exists in its set: created as "turn_2"`): rebuild + reinstall the game. `input_diag=1` logs every action set and action the game creates |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.
+1
View File
@@ -87,6 +87,7 @@ shots:
steps:
- go: monitor
- wait_for: "Processes"
- hook: monitor_settled # the history replay finished (a slow runner shot it halfway: numbers differed)
# newer screens (docs/SHOWCASE.md's gallery; the README picks what it needs)
- name: live-view # idle: a running stream shows its relay's random port and the data rate (no stable picture)
+151
View File
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#ifndef MAP_FIXED_NOREPLACE
#define MAP_FIXED_NOREPLACE 0x100000
#endif
#include <string.h>
#include <sys/mman.h>
static size_t mx_audio_page;
static int mx_audio_branch(uintptr_t from,uintptr_t to,uint32_t *word) {
if((from|to)&3)return 0;
if(to>=from) {if(to-from>=((uintptr_t)1<<27))return 0;}
else if(from-to>((uintptr_t)1<<27))return 0;
*word=0x14000000u|((uint32_t)((to-from)>>2)&0x03ffffffu);
return 1;
}
static void mx_audio_jump(unsigned char *out,uintptr_t destination) {
const uint32_t jump[2]={0x58000050u,0xd61f0200u};
memcpy(out,jump,8);memcpy(out+8,&destination,8);
}
struct mx_audio_candidate { uintptr_t address,distance; };
static int mx_audio_candidate_order(const void *a,const void *b) {
const struct mx_audio_candidate *x=a,*y=b;
if(x->distance!=y->distance)return x->distance<y->distance?-1:1;
return x->address<y->address?-1:x->address>y->address;
}
// Maps are sorted by address. Clip every free gap to the intersection of both
// branch ranges; enumerate pages, then sort outward (lower address breaks ties).
static int mx_audio_candidates(FILE *maps,uintptr_t target,
struct mx_audio_candidate **out,size_t *count,const char **failure) {
uintptr_t radius=(uintptr_t)1<<27;
uintptr_t low=target>radius?target-radius:0;
uintptr_t high=target>UINTPTR_MAX-radius?UINTPTR_MAX:target+radius;
uintptr_t cursor=low;
size_t capacity=0,line_capacity=0;char *line=NULL;
*out=NULL;*count=0;
for(;;) {
unsigned long long start,end;
*failure="read /proc/self/maps";
int eof=getline(&line,&line_capacity,maps)<0;
if(eof && (ferror(maps) || !feof(maps)))goto fail;
if(!eof && (sscanf(line,"%llx-%llx",&start,&end)!=2 || end<=start)) {*failure="parse /proc/self/maps";errno=EINVAL;goto fail;}
uintptr_t stop=eof?high:(uintptr_t)start;
if(stop>high)stop=high;
uintptr_t page=cursor;
if(page%mx_audio_page)page+=mx_audio_page-page%mx_audio_page;
for(;page<stop && stop-page>=mx_audio_page;page+=mx_audio_page) {
uint32_t word;
if(!page || !mx_audio_branch(target,page,&word) ||
!mx_audio_branch(page+20,target+4,&word))continue;
if(*count==capacity) {
size_t next=capacity?capacity*2:64;
void *p=realloc(*out,next*sizeof(**out));
if(!p){*failure="allocate stub candidates";goto fail;}
*out=p;capacity=next;
}
(*out)[(*count)++]=(struct mx_audio_candidate){page,page>target?page-target:target-page};
}
if(eof || stop==high)break;
if((uintptr_t)end>cursor)cursor=(uintptr_t)end;
if(cursor>=high)break;
}
free(line);
if(*count)qsort(*out,*count,sizeof(**out),mx_audio_candidate_order);
return 1;
fail:
{int saved=errno;free(line);free(*out);*out=NULL;*count=0;errno=saved;return 0;}
}
// Callers serialize installation. Only the verified SUB SP / STP prologues
// below may be replayed: no PC-relative instruction or branch relocation.
static void *mx_audio_detour(unsigned char *address,void *replacement,const uint32_t expected[4],void *original_slot) {
uint32_t branch;
uintptr_t target=(uintptr_t)address;
const char *failure=NULL;int failure_errno=0,rollback_errno=0;
if(target&3)failure="unaligned entry";
else if(memcmp(address,expected,16))failure="16-byte guard mismatch";
else if(expected[0]!=0xd10743ffu && expected[0]!=0xa9bd7bfdu &&
expected[0]!=0xd10243ffu && expected[0]!=0xd10183ffu)
failure="unsupported replay instruction";
if(failure) {
LOG("FrameBridge audio detour: target=%p failed: %s; entry unchanged",address,failure);
return NULL;
}
if(!mx_audio_page || (mx_audio_page&(mx_audio_page-1))) {
LOG("FrameBridge audio detour: target=%p failed: invalid page size=%zu; entry unchanged",address,mx_audio_page);
return NULL;
}
struct mx_audio_candidate *candidates=NULL;size_t count=0;
FILE *maps=fopen("/proc/self/maps","r");
if(!maps) {
LOG("FrameBridge audio detour: target=%p failed: open /proc/self/maps errno=%d; entry unchanged",address,errno);
return NULL;
}
int parsed=mx_audio_candidates(maps,target,&candidates,&count,&failure);
int parse_errno=errno;fclose(maps);
if(!parsed) {
LOG("FrameBridge audio detour: target=%p failed: %s errno=%d; entry unchanged",address,failure,parse_errno);
return NULL;
}
unsigned char *code=MAP_FAILED;void *result=MAP_FAILED,*attempt=NULL;
void *closest=count?(void*)candidates[0].address:NULL;
int map_errno=0;size_t attempts=0;
for(size_t i=0;i<count;i++) {
attempt=(void*)candidates[i].address;attempts++;
result=mmap(attempt,mx_audio_page,PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0);
if(result==MAP_FAILED) {map_errno=errno;continue;}
// Old kernels may ignore NOREPLACE. Never accept a relocated mapping.
if(result!=attempt) {munmap(result,mx_audio_page);map_errno=0;continue;}
code=result;break;
}
free(candidates);
if(code==MAP_FAILED) {
LOG("FrameBridge audio detour: target=%p failed: stub allocation closest=%p attempted=%p mmap=%p errno=%d attempts=%zu; no safe ELF slack fallback; entry unchanged",
address,closest,attempt,result,map_errno,attempts);
return NULL;
}
uint32_t back;
if(!mx_audio_branch(target,(uintptr_t)code,&branch) ||
!mx_audio_branch((uintptr_t)code+20,target+4,&back)) {
failure="branch range validation";goto fail;
}
mx_audio_jump(code,(uintptr_t)replacement);
memcpy(code+16,expected,4);memcpy(code+20,&back,4);
__builtin___clear_cache((char*)code,(char*)code+24);
if(mprotect(code,mx_audio_page,PROT_READ|PROT_EXEC)) {failure="mprotect stub RX";failure_errno=errno;goto fail;}
unsigned char *page=(void*)(target&~(mx_audio_page-1));
if(mprotect(page,mx_audio_page,PROT_READ|PROT_WRITE|PROT_EXEC)) {failure="mprotect entry RWX";failure_errno=errno;goto fail;}
// Publish the callable original before any worker can enter the hook.
if(original_slot)__atomic_store_n((void**)original_slot,code+16,__ATOMIC_RELEASE);
__atomic_store_n((uint32_t*)address,branch,__ATOMIC_RELEASE);
__builtin___clear_cache((char*)address,(char*)address+4);
if(mprotect(page,mx_audio_page,PROT_READ|PROT_EXEC)) {
failure_errno=errno;
__atomic_store_n((uint32_t*)address,expected[0],__ATOMIC_RELEASE);
__builtin___clear_cache((char*)address,(char*)address+4);
if(mprotect(page,mx_audio_page,PROT_READ|PROT_EXEC))
rollback_errno=errno; // Cannot force the kernel to restore RX.
// A worker may already be in the stub/hook/trampoline. No grace period
// exists here, so neither the mapping nor original_slot can be retired.
LOG("FrameBridge audio detour: target=%p failed: mprotect entry RX errno=%d; rollback mprotect entry RX errno=%d; entry restored; published code retained",address,failure_errno,rollback_errno);return NULL;
}
return code+16;
fail:
munmap(code,mx_audio_page);
LOG("FrameBridge audio detour: target=%p failed: %s errno=%d; entry unchanged",address,failure,failure_errno);return NULL;
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// The source NativeArray belongs to a UnityWebRequest. Copy while its caller
// still owns that request, before async-void queues the background parser.
typedef struct MxGeometryOwned {
struct MxGeometryOwned *next;
size_t size;
unsigned char bytes[];
} MxGeometryOwned;
static pthread_mutex_t mx_geometry_owned_mutex=PTHREAD_MUTEX_INITIALIZER;
static MxGeometryOwned *mx_geometry_owned_head;
static unsigned mx_geometry_queued,mx_geometry_finished;
static void (*mx_geometry_queue_original)(void*,const void*,uint64_t,void*);
static void (*mx_geometry_worker_original)(void*,void*);
static void mx_geometry_queue(void *object,const void *data,uint64_t packed_length,void *method) {
void (*original)(void*,const void*,uint64_t,void*);
while(!(original=__atomic_load_n(&mx_geometry_queue_original,__ATOMIC_ACQUIRE)))sched_yield();
int32_t length=(int32_t)(uint32_t)packed_length;
size_t size=length>0?(size_t)length:0;
if(!data || !size || size>SIZE_MAX-sizeof(MxGeometryOwned)){original(object,data,packed_length,method);return;}
MxGeometryOwned *owned=malloc(sizeof(*owned)+size);
if(!owned){original(object,data,packed_length,method);return;}
owned->size=size;
memcpy(owned->bytes,data,size);
pthread_mutex_lock(&mx_geometry_owned_mutex);
owned->next=mx_geometry_owned_head;mx_geometry_owned_head=owned;
pthread_mutex_unlock(&mx_geometry_owned_mutex);
unsigned queued=__atomic_fetch_add(&mx_geometry_queued,1,__ATOMIC_RELAXED);
if(queued<4)LOG("FrameBridge audio geometry: owned %zu bytes before background task queued",size);
original(object,owned->bytes,packed_length,method);
}
static void mx_geometry_worker(void *closure,void *method) {
const void *data=NULL;
if(closure)memcpy(&data,(const unsigned char*)closure+16,8);
void (*original)(void*,void*);
while(!(original=__atomic_load_n(&mx_geometry_worker_original,__ATOMIC_ACQUIRE)))sched_yield();
original(closure,method);
MxGeometryOwned *finished=NULL;
pthread_mutex_lock(&mx_geometry_owned_mutex);
for(MxGeometryOwned **p=&mx_geometry_owned_head;*p;p=&(*p)->next)if((*p)->bytes==data) {
finished=*p;*p=finished->next;break;
}
pthread_mutex_unlock(&mx_geometry_owned_mutex);
if(finished) {
unsigned count=__atomic_fetch_add(&mx_geometry_finished,1,__ATOMIC_RELAXED);
if(count<4)LOG("FrameBridge audio geometry: background parse finished; releasing %zu owned bytes",finished->size);
free(finished);
}
}
+11 -26
View File
@@ -11,26 +11,7 @@
#include <time.h>
#include <link.h>
#include <string.h>
static size_t mx_audio_page;
static void *mx_audio_detour(unsigned char *address,void *replacement,const uint32_t expected[4]) {
if(memcmp(address,expected,16)) return NULL;
unsigned char *trampoline=mmap(NULL,mx_audio_page,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if(trampoline==MAP_FAILED) return NULL;
uint32_t branch[2]={0x58000050u,0xd61f0200u}; // ldr x16, +8; br x16
memcpy(trampoline,address,16);
memcpy(trampoline+16,branch,8);
uintptr_t continuation=(uintptr_t)address+16;
memcpy(trampoline+24,&continuation,8);
__builtin___clear_cache((char*)trampoline,(char*)trampoline+32);
if(mprotect(trampoline,mx_audio_page,PROT_READ|PROT_EXEC)) return NULL;
unsigned char *page=(unsigned char*)((uintptr_t)address&~(mx_audio_page-1));
if(mprotect(page,mx_audio_page,PROT_READ|PROT_WRITE|PROT_EXEC)) return NULL;
memcpy(address,branch,8);
memcpy(address+8,&replacement,8);
__builtin___clear_cache((char*)address,(char*)address+16);
mprotect(page,mx_audio_page,PROT_READ|PROT_EXEC);
return trampoline;
}
#include "audio_detour.h"
#include "audio_metadata_queue.h"
typedef struct {float previous,current;} MxAudioRamp;
static uintptr_t mx_audio_base;
@@ -49,8 +30,8 @@ static void mx_audio_kill_experimental(void) {mx_audio_kill(1);}
static void mx_audio_kill_regular(void) {mx_audio_kill(0);}
static uint64_t mx_audio_consume(void *sink,void *main_mix,void *passthrough,const void *objects,MxAudioRamp ramp) {
const unsigned char *previous=mx_audio_objects;mx_audio_objects=objects;
// An audio thread can enter just after its prologue was patched, before
// the installing thread publishes the completed trampoline.
// The installer publishes the completed trampoline before the entry branch.
// Keep the acquire load paired with that publication.
uint64_t (*original)(void*,void*,void*,const void*,MxAudioRamp);
while(!(original=__atomic_load_n(&mx_audio_consume_original,__ATOMIC_ACQUIRE)))sched_yield();
uint64_t result=original(sink,main_mix,passthrough,objects,ramp);
@@ -79,15 +60,19 @@ static int mx_audio_find(struct dl_phdr_info *info,size_t size,void *unused) {
const uint32_t kill[4]={0xa9bd7bfd,0xf9000bf5,0xa9024ff4,0x910003fd};
unsigned char *base=(void*)info->dlpi_addr;
if(memcmp(base+0x2c8fac,consume,16) || memcmp(base+0x2be2c0,kill,16) || memcmp(base+0x2be7b0,kill,16)) {
const uintptr_t offsets[3]={0x2c8fac,0x2be2c0,0x2be7b0};
for(unsigned i=0;i<3;i++)if(memcmp(base+offsets[i],i?kill:consume,16))
LOG("FrameBridge audio detour: target=%p failed: 16-byte guard mismatch; entry unchanged",base+offsets[i]);
__atomic_store_n(&mx_audio_checked,1,__ATOMIC_RELEASE);return 1;
}
// One installation attempt per verified library, including failure paths.
__atomic_store_n(&mx_audio_checked,1,__ATOMIC_RELEASE);
__atomic_store_n(&mx_audio_base,info->dlpi_addr,__ATOMIC_RELEASE);
void *original=mx_audio_detour(base+0x2c8fac,(void*)mx_audio_consume,consume);
__atomic_store_n(&mx_audio_consume_original,original,__ATOMIC_RELEASE);
if(!__atomic_load_n(&mx_audio_consume_original,__ATOMIC_ACQUIRE)) {
void *original=mx_audio_detour(base+0x2c8fac,(void*)mx_audio_consume,consume,&mx_audio_consume_original);
if(!original) {
__atomic_store_n(&mx_audio_base,0,__ATOMIC_RELEASE);return 1;
}
if(!mx_audio_detour(base+0x2be2c0,(void*)mx_audio_kill_experimental,kill) || !mx_audio_detour(base+0x2be7b0,(void*)mx_audio_kill_regular,kill)) {
if(!mx_audio_detour(base+0x2be2c0,(void*)mx_audio_kill_experimental,kill,NULL) || !mx_audio_detour(base+0x2be7b0,(void*)mx_audio_kill_regular,kill,NULL)) {
LOG("FrameBridge audio metadata: incomplete installation");return 1;
}
LOG("FrameBridge audio metadata: metadata reclamation follows current audio-frame references; Meta audio base=%p",base);
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
// Batman 1.4.1: keep the web request's native audio data alive across Task.Run.
#pragma once
#if defined(__aarch64__)
#include "audio_geometry_owned.h"
#include "batman_audio_geometry_target.h"
static int mx_geometry_checked;
static int mx_geometry_find(struct dl_phdr_info *info,size_t ignored,void *unused) {
(void)ignored;(void)unused;
if(!strstr(info->dlpi_name,"/libil2cpp.so"))return 0;
int match=0;
for(unsigned i=0;i<info->dlpi_phnum;i++)if(info->dlpi_phdr[i].p_type==PT_NOTE) {
const unsigned char *p=(void*)(info->dlpi_addr+info->dlpi_phdr[i].p_vaddr);
const unsigned char *end=p+info->dlpi_phdr[i].p_memsz;
while(p+sizeof(Elf64_Nhdr)<=end) {
const Elf64_Nhdr *note=(const void*)p;
const unsigned char *name=p+sizeof(*note),*desc=name+((note->n_namesz+3)&~3u);
const unsigned char *next=desc+((note->n_descsz+3)&~3u);
if(next>end || next<=p)break;
if(note->n_type==NT_GNU_BUILD_ID && note->n_namesz==4 && !memcmp(name,"GNU",4) &&
mx_geometry_matches_id(desc,note->n_descsz))match=1;
p=next;
}
}
__atomic_store_n(&mx_geometry_checked,1,__ATOMIC_RELEASE);
if(!match)return 1;
unsigned char *base=(void*)info->dlpi_addr;
// Live VAs, not the file offsets printed by the managed-code dump.
unsigned char *queue=base+0x2e61bb4,*worker=base+0x2e63a28;
if(!mx_geometry_matches_entries(queue,worker)) {
if(memcmp(queue,mx_geometry_queue_entry,16))
LOG("FrameBridge audio detour: target=%p failed: 16-byte guard mismatch; entry unchanged",queue);
if(memcmp(worker,mx_geometry_worker_entry,16))
LOG("FrameBridge audio detour: target=%p failed: 16-byte guard mismatch; entry unchanged",worker);
return 1;
}
// Hook the completion boundary first so every queued copy has a consumer.
void *original=mx_audio_detour(worker,(void*)mx_geometry_worker,mx_geometry_worker_entry,&mx_geometry_worker_original);
if(!original)return 1;
original=mx_audio_detour(queue,(void*)mx_geometry_queue,mx_geometry_queue_entry,&mx_geometry_queue_original);
LOG("FrameBridge audio geometry: Batman async input lifetime %s",original?"installed":"not installed");
return 1;
}
static pthread_mutex_t mx_geometry_install_mutex=PTHREAD_MUTEX_INITIALIZER;
static void mx_batman_geometry_initialize(void) {
// The metadata initializer already verified the Meta SDK build/prologues.
// Keep this independent of its early-out: IL2CPP may be loaded afterwards.
if(__atomic_load_n(&mx_geometry_checked,__ATOMIC_ACQUIRE) ||
!__atomic_load_n(&mx_audio_base,__ATOMIC_ACQUIRE))return;
if(pthread_mutex_trylock(&mx_geometry_install_mutex))return;
if(!mx_geometry_checked) {
static time_t last_scan;
struct timespec now;clock_gettime(CLOCK_MONOTONIC,&now);
if(now.tv_sec!=last_scan) {
last_scan=now.tv_sec;
char package[64]={0};FILE *cmd=fopen("/proc/self/cmdline","rb");
if(cmd){(void)fread(package,1,sizeof(package)-1,cmd);fclose(cmd);}
if(strcmp(package,"com.camouflaj.manta"))__atomic_store_n(&mx_geometry_checked,1,__ATOMIC_RELEASE);
else dl_iterate_phdr(mx_geometry_find,NULL);
}
}
pthread_mutex_unlock(&mx_geometry_install_mutex);
}
#else
static void mx_batman_geometry_initialize(void) {}
#endif
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
static const unsigned char mx_geometry_il2cpp_id[20]={
0x1d,0x48,0xa4,0x1d,0x7e,0x90,0x11,0xbe,0xf5,0xfa,0xe0,0xc2,0x3c,0x40,0x4c,0x62,0xa4,0xe7,0xa7,0x3c
};
static const uint32_t mx_geometry_queue_entry[4]={0xd10243ff,0xa9065ffe,0xa90757f6,0xa9084ff4};
static const uint32_t mx_geometry_worker_entry[4]={0xd10183ff,0xa90267fe,0xa9035ff8,0xa90457f6};
static int mx_geometry_matches_id(const void *id,size_t size) {
return size==sizeof(mx_geometry_il2cpp_id) && !memcmp(id,mx_geometry_il2cpp_id,size);
}
static int mx_geometry_matches_entries(const void *queue,const void *worker) {
return !memcmp(queue,mx_geometry_queue_entry,sizeof(mx_geometry_queue_entry)) &&
!memcmp(worker,mx_geometry_worker_entry,sizeof(mx_geometry_worker_entry));
}
+4 -3
View File
@@ -77,6 +77,7 @@ static void fb_log(const char *fmt, ...) {
}
#define LOG(...) fb_log(__VA_ARGS__)
#include "audio_metadata.h"
#include "batman_audio_geometry.h"
#include "surface_views.h"
static void *loader;
@@ -469,7 +470,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
}
no_equirect = !has_equirect; no_equirect2 = !has_equirect2; no_cylinder = !has_cylinder; no_cube = !has_cube;
free(available);
mx_audio_initialize();
mx_audio_initialize(); mx_batman_geometry_initialize();
XrResult result = fn(&fixed, instance);
free(names);
LOG("xrCreateInstance result=%d", result);
@@ -481,7 +482,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
LOG("color_space: emulating XR_FB_color_space; REC709 SDR compatibility, no colour conversion");
}
}
if (XR_SUCCEEDED(result)) { active_instance = *instance; mx_audio_initialize(); }
if (XR_SUCCEEDED(result)) { active_instance = *instance; mx_audio_initialize(); mx_batman_geometry_initialize(); }
return result;
}
@@ -1394,7 +1395,7 @@ static XrSession focused_session = XR_NULL_HANDLE;
static int kicks_pending = -1; // -1: not yet armed
XRAPI_ATTR XrResult XRAPI_CALL xrPollEvent(XrInstance instance, XrEventDataBuffer *event) {
mx_audio_initialize();
mx_audio_initialize(); mx_batman_geometry_initialize();
PFN_xrPollEvent fn = (PFN_xrPollEvent)lookup(instance, "xrPollEvent");
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
if (emulate_scene && event) {
+1 -1
View File
@@ -92,7 +92,7 @@ def _entry(spec: dict, tech: dict, now: float) -> dict:
rdict = library.recipe_to_dict(recipe)
fields = dict(title=title, name=title, data_bytes=tech.get("data_bytes") or 0, analysis=a.to_dict(),
recipe=rdict, suggested=rdict, status=recipe.status, added=now - DAY * spec.get("added", 30),
build={"sha256": fake_sha(pkg), "ok": True, "recipe_fp": recipe_fingerprint(rdict),
build={"sha256": fake_sha(pkg), "ok": True, "recipe_fp": recipe_fingerprint(rdict, pkg),
"applied": sorted(p for p in rdict.get("patches") or {} if "." in p)})
if rift:
folder = f"D:/Games/PC VR/{title}"
+16
View File
@@ -634,6 +634,16 @@ class FrozenMonitorSession(FakeMonitorSession):
"""The fake stream for still pictures: the two minutes of history, then nothing new (every render of a page shows
the same numbers, whenever the screenshot is taken)."""
# the newest session (the Monitor tab's faster interval starts a new one after the Frame card's) and whether it
# has replayed its history; the monitor_settled hook waits for that, else a slow machine shot the page halfway
# through the replay (set_modules("all") asks for the replay again)
LATEST: FrozenMonitorSession | None = None
def __init__(self, *args, **kwargs):
self.settled = threading.Event()
FrozenMonitorSession.LATEST = self
super().__init__(*args, **kwargs)
def _run(self) -> None:
self.t = 0
while not self.closed:
@@ -641,8 +651,14 @@ class FrozenMonitorSession(FakeMonitorSession):
self.on_sample(self._sample())
self.t += 1
else:
self.settled.set()
time.sleep(0.2)
def set_modules(self, m):
if m == "all":
self.settled.clear()
return super().set_modules(m)
class LiveMonitorSession(FakeMonitorSession):
"""The fake stream, livelier for videos: a sample every 0.6 s and a steady frame rate with a short, shallow dip
+16 -1
View File
@@ -240,6 +240,20 @@ def _monitor_details(session) -> None:
fakes.open_monitor_details(session.app)
def _monitor_settled(session, timeout: float = 30) -> None:
"""Still pictures of the Monitor: wait until the frozen stream has replayed its history (then one more second
for the page to draw the last sample)."""
end = time.monotonic() + timeout
while time.monotonic() < end: # the newest session, settled, and still the newest a moment later
session_ = fakes.FrozenMonitorSession.LATEST
if session_ is None:
time.sleep(0.2)
elif session_.settled.wait(max(0.0, end - time.monotonic())):
time.sleep(1)
if fakes.FrozenMonitorSession.LATEST is session_ and session_.settled.is_set():
return
def _type_tab(session) -> None:
fakes.open_type_tab(session.app)
@@ -399,4 +413,5 @@ HOOKS = {"fake_install": _fake_install, "wait_jobs": _wait_jobs, "held_install":
"settings_section": _settings_section, "first_run": _first_run, "pairing_done": _pairing_done,
"frame_asks": _frame_asks,
"easter_eggs": _easter_eggs, "fps_dip": _fps_dip, "perfect_pacing": _perfect_pacing,
"battery": _battery, "long_upload": _long_upload, "press_keys": _press_keys}
"battery": _battery, "long_upload": _long_upload, "press_keys": _press_keys,
"monitor_settled": _monitor_settled}
+1
View File
@@ -24,6 +24,7 @@ def settings_text(recipe_patches: dict) -> bytes:
class FrameBridgeAdapter(Patch):
id = "frame.adapter"
package_revisions = {"com.camouflaj.manta": 3} # above revision 2: Batman builds before #389 are outdated
a32 = True # also for 32-bit games on the bridge
title = "FrameBridge OpenXR adapter"
description = (
+8 -1
View File
@@ -91,7 +91,14 @@ class HeadsetModel(Patch):
if not ws.has(ws.lib(loader)):
ctx.checks.append((self.id, None, "No OVRPort dispatcher; only FrameBridge's systemName override applies."))
return False
fixed = patch_dispatcher(ws.read_lib(loader), key)
try:
fixed = patch_dispatcher(ws.read_lib(loader), key)
except ValueError:
# Quest 3 is everyone's default: an APK converted with another OVRPort runtime (e.g. an already converted
# copy) keeps OVRPort's Quest 2 answers instead of failing its build
ctx.checks.append((self.id, None, "This OVRPort dispatcher isn't a known build; the game keeps seeing a "
"Quest 2 (only FrameBridge's systemName override applies)."))
return False
ws.put(ws.lib(loader), fixed)
ctx.notes.append(f"{loader}: systemName={SYSTEM_NAMES[key].decode()}, HeadsetId={UUIDS[key].hex()}")
return True
+135
View File
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-3.0-only
#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
static _Alignas(4096) unsigned char entry[4096],arena[4096];
static unsigned maps,unmaps,protects,logs,fail_at;
static int outside,missing,restore_fails;
static uintptr_t hints[3];
static char last_log[1024];
static void *slot;
static const uint32_t expected[4]={0xd10243ff,0xa9065ffe,0xa90757f6,0xa9084ff4};
static void *fake_mmap(void *hint,size_t size,int prot,int flags,int fd,off_t offset) {
(void)size;(void)prot;(void)fd;(void)offset;
assert(flags & 0x100000);
if(!missing)assert(hint==arena);
assert(maps<3);hints[maps]=(uintptr_t)hint;
maps++;
if(missing){errno=maps==1?EEXIST:maps==2?EINVAL:ENOMEM;return MAP_FAILED;}
return outside?(void*)((uintptr_t)entry+0x10000000):arena;
}
static int fake_munmap(void *p,size_t size) {
(void)p;assert(size==4096);unmaps++;return 0;
}
static int fake_mprotect(void *p,size_t size,int prot) {
assert(size==4096);protects++;
if(protects==1) {
assert(p==arena && prot==(PROT_READ|PROT_EXEC));
assert(!memcmp(entry,expected,16));
}
if(protects==3) {
assert(p==entry && prot==(PROT_READ|PROT_EXEC));
assert(slot==arena+16); // original is available when entry goes live
assert(*(uint32_t*)entry!=expected[0]);
assert(!memcmp(entry+4,expected+1,12));
}
if(protects==4)assert(!memcmp(entry,expected,16));
if(protects==fail_at || (restore_fails && protects>=3)){errno=EACCES;return -1;}
return 0;
}
static FILE *fake_fopen(const char *path,const char *mode) {
(void)path;(void)mode;
FILE *f=tmpfile();assert(f);
fprintf(f,"0-%llx r-xp\n%llx-ffffffffffffffff r-xp\n",
(unsigned long long)(uintptr_t)arena,
(unsigned long long)((uintptr_t)arena+(missing?12288:4096)));
rewind(f);return f;
}
static void test_log(const char *format,...) {
char message[1024];va_list ap;va_start(ap,format);
vsnprintf(message,sizeof(message),format,ap);va_end(ap);
assert(strstr(message,"target="));strcpy(last_log,message);logs++;
}
#define fopen fake_fopen
#define mmap fake_mmap
#define munmap fake_munmap
#define mprotect fake_mprotect
#define LOG(...) test_log(__VA_ARGS__)
#include "audio_detour.h"
static void reset(void) {
maps=unmaps=protects=0;fail_at=0;outside=missing=restore_fails=0;slot=NULL;
memcpy(entry,expected,16);memset(arena,0,sizeof(arena));mx_audio_page=4096;
}
static void gap_test(const char *text,uintptr_t target,size_t expected_count,uintptr_t first) {
FILE *f=tmpfile();assert(f);fputs(text,f);rewind(f);
struct mx_audio_candidate *c=NULL;size_t n=0;
const char *failure=NULL;
assert(mx_audio_candidates(f,target,&c,&n,&failure));fclose(f);
assert(n==expected_count);if(n)assert(c[0].address==first);
for(size_t i=0;i<n;i++){uint32_t w;assert(mx_audio_branch(target,c[i].address,&w));assert(mx_audio_branch(c[i].address+20,target+4,&w));}
free(c);
}
int main(void) {
mx_audio_page=4096;
gap_test("0-10000000 r-xp\n10003000-ffffffffffffffff r-xp\n",0x10001000,3,0x10001000);
gap_test("0-10000000 r-xp\n10001000-10002000 r-xp\n10002000-ffffffffffffffff r-xp\n",0x10002000,1,0x10000000);
gap_test("0-20000000 r-xp\n20001000-ffffffffffffffff r-xp\n",0x10000000,0,0);
gap_test("0-10000000 r-xp\n10000000-ffffffffffffffff r-xp\n",0x10000000,0,0);
gap_test("0-18000000 r-xp\n18001000-ffffffffffffffff r-xp\n",0x10000004,0,0);
uint32_t b;
assert(mx_audio_branch(0x10000000,0x10000000,&b) && b==0x14000000);
assert(mx_audio_branch(0x10000000,0x17fffffc,&b) && b==0x15ffffff);
assert(mx_audio_branch(0x10000000,0x08000000,&b) && b==0x16000000);
assert(mx_audio_branch(0x10000004,0x10000000,&b) && b==0x17ffffff);
assert(!mx_audio_branch(0x10000000,0x18000000,&b));
assert(!mx_audio_branch(0x10000004,0x08000000,&b));
assert(!mx_audio_branch(1,4,&b) && !mx_audio_branch(4,1,&b));
reset();
assert(mx_audio_detour(entry,(void*)0x123456789abcdef0ULL,expected,&slot)==arena+16);
uint32_t jump[2]={0x58000050,0xd61f0200};uintptr_t destination;
assert(!memcmp(arena,jump,8));memcpy(&destination,arena+8,8);
assert(destination==0x123456789abcdef0ULL);
assert(*(uint32_t*)(arena+16)==expected[0]);
assert(mx_audio_branch((uintptr_t)arena+20,(uintptr_t)entry+4,&b));
assert(*(uint32_t*)(arena+20)==b && maps==1 && !unmaps && protects==3);
for(unsigned failure=1;failure<=3;failure++) {
reset();fail_at=failure;
assert(!mx_audio_detour(entry,(void*)16,expected,&slot));
assert(strstr(last_log,failure==1?"mprotect stub RX":failure==2?"mprotect entry RWX":"mprotect entry RX"));
assert(strstr(last_log,"errno=13"));
assert(!memcmp(entry,expected,16));
assert(unmaps==(failure<3?1u:0u));
assert(slot==(failure<3?NULL:arena+16));
assert(protects==(failure==3?4:failure));
}
reset();restore_fails=1;
assert(!mx_audio_detour(entry,(void*)16,expected,&slot));
assert(!memcmp(entry,expected,16) && !unmaps && protects==4 && slot==arena+16);
reset();outside=1;
assert(!mx_audio_detour(entry,(void*)16,expected,&slot));
assert(maps==1 && unmaps==maps && !protects && !memcmp(entry,expected,16));
reset();missing=1;
assert(!mx_audio_detour(entry,(void*)16,expected,&slot));
assert(maps==3 && !unmaps && !protects && !memcmp(entry,expected,16));
for(unsigned i=1;i<3;i++) {
uintptr_t t=(uintptr_t)entry;
uintptr_t a=hints[i-1]>t?hints[i-1]-t:t-hints[i-1];
uintptr_t b=hints[i]>t?hints[i]-t:t-hints[i];
assert(a<=b && hints[i]!=hints[i-1]);
}
assert(strstr(last_log,"errno=12 attempts=3"));
reset();entry[4]^=1;
assert(!mx_audio_detour(entry,(void*)16,expected,&slot) && !maps);
assert(strstr(last_log,"16-byte guard mismatch"));
reset();uint32_t bad[4]={0x10000000,0,0,0};memcpy(entry,bad,16);
assert(!mx_audio_detour(entry,(void*)16,bad,&slot) && !maps);
assert(strstr(last_log,"unsupported replay instruction"));
assert(logs==8);
puts("PASS: maps gaps, outward NOREPLACE retries, branch limits, stub/trampoline, publication order, failure diagnostics/cleanup/rollback, guards");
}
+47
View File
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-only
#define _GNU_SOURCE
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <link.h>
#include <string.h>
#define LOG(...) ((void)0)
static uintptr_t mx_audio_base;
static unsigned detours,scans;
static void *mx_audio_detour(unsigned char *address,void *replacement,const uint32_t expected[4],void *slot) {
(void)slot;assert(replacement && !memcmp(address,expected,16));detours++;return (void*)1;
}
static int fake_iterate(int (*callback)(struct dl_phdr_info*,size_t,void*),void *data) {
(void)callback;(void)data;scans++;return 0;
}
#define dl_iterate_phdr fake_iterate
// Exercise the AArch64-only installer as C without executing ARM instructions.
#ifndef __aarch64__
#define __aarch64__ 1
#endif
#include "batman_audio_geometry.h"
int main(void) {
mx_batman_geometry_initialize();assert(!scans && !detours); // unverified Meta SDK
mx_audio_base=1;
mx_batman_geometry_initialize();assert(!scans && !detours && mx_geometry_checked); // another package
unsigned char *base=calloc(1,0x2e63a28+32);assert(base);
Elf64_Phdr phdr={.p_type=PT_NOTE,.p_vaddr=256,.p_memsz=36};
struct dl_phdr_info info={.dlpi_addr=(uintptr_t)base,.dlpi_name="/lib/libil2cpp.so",
.dlpi_phdr=&phdr,.dlpi_phnum=1};
Elf64_Nhdr note={.n_namesz=4,.n_descsz=20,.n_type=NT_GNU_BUILD_ID};
memcpy(base+256,&note,sizeof(note));memcpy(base+268,"GNU",4);
memcpy(base+272,mx_geometry_il2cpp_id,20);
memcpy(base+0x2e61bb4,mx_geometry_queue_entry,16);
memcpy(base+0x2e63a28,mx_geometry_worker_entry,16);
base[272]^=1;mx_geometry_find(&info,sizeof(info),NULL);assert(!detours); // other build
base[272]^=1;base[0x2e63a28]^=1;
mx_geometry_find(&info,sizeof(info),NULL);assert(!detours); // changed worker
base[0x2e63a28]^=1;base[0x2e61bb4]^=1;
mx_geometry_find(&info,sizeof(info),NULL);assert(!detours); // changed queue
base[0x2e61bb4]^=1;
mx_geometry_find(&info,sizeof(info),NULL);assert(detours==2); // only the exact pair is hooked
free(base);
puts("PASS: other package/SDK/build/entries skipped; matching binary hooks both boundaries");
}
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-only
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
#define LOG(...) ((void)0)
static int fail_allocation;
static _Atomic unsigned allocations,releases;
static void *test_alloc(size_t size) {
if(fail_allocation)return NULL;
allocations++;return malloc(size);
}
static void test_free(void *p) {releases++;free(p);}
#define malloc test_alloc
#define free test_free
#include "audio_geometry_owned.h"
#undef malloc
#undef free
#include "batman_audio_geometry_target.h"
typedef struct {
unsigned char header[16];
const void *data;
uint64_t length;
int immediate;
const void *source;
} Closure;
static void original_worker(void *context,void *method) {
Closure *closure=context;
assert(method==(void*)42 && closure->length==UINT64_C(0x123400000040));
assert(closure->data!=closure->source);
for(unsigned i=0;i<64;i++)assert(((const unsigned char*)closure->data)[i]==(unsigned char)i);
}
static void original_queue(void *object,const void *data,uint64_t length,void *method) {
Closure *closure=object;
assert(method==(void*)42);
closure->data=data;closure->length=length;
if(closure->immediate)mx_geometry_worker(closure,method);
}
static void *concurrent_load(void *unused) {
(void)unused;
for(unsigned n=0;n<100;n++) {
unsigned char source[64];for(unsigned i=0;i<64;i++)source[i]=(unsigned char)i;
Closure closure={.immediate=1,.source=source};
mx_geometry_queue(&closure,source,UINT64_C(0x123400000040),(void*)42);
}
return NULL;
}
int main(void) {
mx_geometry_queue_original=original_queue;mx_geometry_worker_original=original_worker;
unsigned char source[64];for(unsigned i=0;i<64;i++)source[i]=(unsigned char)i;
Closure closure={.source=source};
mx_geometry_queue(&closure,source,UINT64_C(0x123400000040),(void*)42);
assert(mx_geometry_owned_head && closure.data!=source && allocations==1 && releases==0);
memset(source,0xff,sizeof(source)); // download handler's source goes away before the task runs
mx_geometry_worker(&closure,(void*)42);
assert(!mx_geometry_owned_head && allocations==releases);
for(unsigned i=0;i<64;i++)source[i]=(unsigned char)i;
closure.immediate=1;
mx_geometry_queue(&closure,source,UINT64_C(0x123400000040),(void*)42);
assert(!mx_geometry_owned_head && allocations==releases); // task finishes before queue() returns
fail_allocation=1;closure.immediate=0;
mx_geometry_queue(&closure,source,64,(void*)42);
assert(closure.data==source && !mx_geometry_owned_head); // preserve original call on allocation failure
fail_allocation=0;
mx_geometry_queue(&closure,NULL,0,(void*)42);
assert(!closure.data && !mx_geometry_owned_head);
mx_geometry_queue(&closure,source,UINT32_MAX,(void*)42); // negative NativeArray length: unchanged
assert(closure.data==source && !mx_geometry_owned_head);
pthread_t threads[4];
for(unsigned i=0;i<4;i++)assert(!pthread_create(&threads[i],NULL,concurrent_load,NULL));
for(unsigned i=0;i<4;i++)assert(!pthread_join(threads[i],NULL));
assert(!mx_geometry_owned_head && mx_geometry_queued==mx_geometry_finished);
assert(mx_geometry_matches_id(mx_geometry_il2cpp_id,20));
unsigned char wrong_id[20];memcpy(wrong_id,mx_geometry_il2cpp_id,20);wrong_id[0]^=1;
assert(!mx_geometry_matches_id(wrong_id,20) && !mx_geometry_matches_id(wrong_id,19));
assert(mx_geometry_matches_entries(mx_geometry_queue_entry,mx_geometry_worker_entry));
uint32_t wrong_entry[4];memcpy(wrong_entry,mx_geometry_queue_entry,16);wrong_entry[3]^=1;
assert(!mx_geometry_matches_entries(wrong_entry,mx_geometry_worker_entry));
assert(!mx_geometry_matches_entries(mx_geometry_queue_entry,wrong_entry));
puts("PASS: deferred/inline/concurrent ownership, cleanup, ABI arguments, fallback and target guards");
}
+55
View File
@@ -0,0 +1,55 @@
"""Batman audio ownership and the scope of the adapter update."""
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
@pytest.mark.parametrize("fixture", ["audio_geometry_test.c", "audio_geometry_target_test.c", "audio_detour_test.c"])
def test_audio_geometry_native(tmp_path, fixture):
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
if not cc or os.name == "nt":
pytest.skip("Native ownership regression requires a POSIX C compiler with pthreads")
if fixture == "audio_geometry_target_test.c" and sys.platform != "linux":
pytest.skip("Installer regression models Linux ELF loading")
root = Path(__file__).resolve().parents[1]
binary = tmp_path / "audio-geometry-test"
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread",
"-I", str(root / "native/adapter"),
str(root / "tests/fixtures/src" / fixture), "-o", str(binary)], check=True)
subprocess.run([str(binary)], check=True)
def test_audio_geometry_update_outdates_batman_only(monkeypatch):
from frameport.patches.base import get, recipe_fingerprint
patch = get("frame.adapter")
recipe = {"patches": {"frame.adapter": {}}}
packages = ("com.camouflaj.manta", "com.Armature.VR4", "com.crytek.climb2", "com.example.other")
current = {p: recipe_fingerprint(recipe, p) for p in packages}
assert patch.revision_for(packages[0]) == 3
assert all(patch.revision_for(p) == 2 for p in packages[1:])
monkeypatch.setattr(patch, "package_revisions", {})
assert current[packages[0]] != recipe_fingerprint(recipe, packages[0])
assert all(current[p] == recipe_fingerprint(recipe, p) for p in packages[1:])
def test_audio_geometry_allocation_triage_is_specific_to_batman():
from frameport.validate.triage import triage
log = ("package com.camouflaj.manta\n"
"E Unity : Could not allocate memory: System out of memory!\n"
"E Unity : Trying to allocate: 26388238103B with 16 alignment. MemoryLabel: NativePlugin\n"
"E Unity : Allocation happened at: MetaXRAudioUnityMemoryAllocator.cpp\n"
"F libc : Fatal signal 5 (SIGTRAP)\n")
result = triage(log, "EXITED")
finding = next(f for f in result.findings if f.id == "batman-audio-geometry-allocation")
assert finding.severity == "fatal" and finding.suggest == ["frame.adapter"]
assert "native-crash" not in {f.id for f in result.findings}
for ordinary in (log.replace("com.camouflaj.manta", "com.example.other"),
log.replace("26388238103B", "4096B"),
log.replace("MetaXRAudioUnityMemoryAllocator.cpp", "OtherAllocator.cpp")):
assert "batman-audio-geometry-allocation" not in {f.id for f in triage(ordinary, "EXITED").findings}
+16
View File
@@ -189,6 +189,22 @@ def test_dispatcher_rejects_unknown_and_partial_matches():
assert patch_dispatcher(b'unknown runtime', 'quest2') == b'unknown runtime'
def test_unknown_dispatcher_keeps_quest2_instead_of_failing_the_build():
"""Quest 3 is the default for every Quest game: an APK converted with another OVRPort runtime (found with an
already converted Batman copy) must still build, keeping OVRPort's Quest 2 answers."""
from types import SimpleNamespace
from frameport.patches import base
base.load_all()
put = []
ws = SimpleNamespace(lib=lambda n: n, has=lambda p: True, read_lib=lambda n: b"another runtime",
put=lambda p, d: put.append(p))
ctx = SimpleNamespace(analysis=SimpleNamespace(vr_kind="quest", extra={}), params={}, ws=ws, checks=[], notes=[])
assert base.get("device.headset_model").apply(ctx) is False
assert not put and ctx.checks and "Quest 2" in ctx.checks[0][2]
def test_real_dispatcher_when_available():
import os
+1 -1
View File
@@ -13,7 +13,7 @@ def test_video_patches_outdate_only_the_recipes_that_gain_them():
from frameport.ui.components import install_state
# revision 2 (#365) adds the dormant foveation diagnostic to every build; this test is about the package check
assert get("frame.adapter").revision == 2 and not get("frame.adapter").package_revisions
assert get("frame.adapter").revision_for("com.example.other") == 2
before = {"patches": {"frame.adapter": {}}}
after = {"patches": {"frame.adapter": {}, "frame.hw_video_decode": {}, "adapter.surface_native": {"value": 1}}}
for package, recipe, expected in (("com.camouflaj.manta", after, "outdated"),