Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e185e24ed7 | ||
|
|
5e668f33d3 | ||
|
|
feb4658dde | ||
|
|
4ed7929b1c | ||
|
|
61a6b3e2b2 | ||
|
|
78fc926a48 | ||
|
|
503e64367c | ||
|
|
3deb279270 | ||
|
|
0df1212bfe |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 7.0 MiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 288 KiB |
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 +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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,¬e,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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||