diff --git a/artifacts/SHA256SUMS b/artifacts/SHA256SUMS index c0fc130..ee6af82 100644 --- a/artifacts/SHA256SUMS +++ b/artifacts/SHA256SUMS @@ -3,10 +3,10 @@ a5b03bb7ae5d953d7ded2bf117b0a19c57fae604f16f54310847f8e89593dde6 ./arm64-v8a/li 6d2b9f8cea2f2cf33b8ff30d29a2f3d9c4d55549260a867168a24e44abada021 ./arm64-v8a/libfp_vk.so 8e00552d1ab90f82749db3caad4f773351fa8a28edcc5ea5cbd1b31c434ff32a ./arm64-v8a/libframe_xrshim.so 3dfcb0f2bec47857bcecc7261c58e076d872d4649718c40850f29e10c75477dd ./arm64-v8a/libglshim.so -507bb1c62168da9335a5883ec264aabed2ce09d04b6524e67be504a273473f8d ./arm64-v8a/libopenxr_loader_generic.so +6c836091a1a3bcbfd526de317c455d156d41875b69c8a6f05e7f761ca24b40f8 ./arm64-v8a/libopenxr_loader_generic.so 1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so 32525cd0a9ee3d9993d8871cd6905bdcf22cde4f4ba28f5db6434ce7d6554758 ./arm64-v8a/libvrapi.so -11b7019c6a8d3b2f0f89cf23011f84bbbcb8e907f0935978e1df675339abbb6e ./armeabi-v7a/libopenxr_loader_generic.so +309d6c5266b3a0fa4c7b1cb884b28653dc2e76957501c070378a10e8c5682cec ./armeabi-v7a/libopenxr_loader_generic.so 1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex 1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json 28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so diff --git a/artifacts/arm64-v8a/libopenxr_loader_generic.so b/artifacts/arm64-v8a/libopenxr_loader_generic.so index d55304f..be52cd8 100644 Binary files a/artifacts/arm64-v8a/libopenxr_loader_generic.so and b/artifacts/arm64-v8a/libopenxr_loader_generic.so differ diff --git a/artifacts/armeabi-v7a/libopenxr_loader_generic.so b/artifacts/armeabi-v7a/libopenxr_loader_generic.so index 73174bf..aab6429 100644 Binary files a/artifacts/armeabi-v7a/libopenxr_loader_generic.so and b/artifacts/armeabi-v7a/libopenxr_loader_generic.so differ diff --git a/docs/AUDIO_METADATA.md b/docs/AUDIO_METADATA.md new file mode 100644 index 0000000..bbed732 --- /dev/null +++ b/docs/AUDIO_METADATA.md @@ -0,0 +1,31 @@ +# Meta XR Audio metadata lifetime + +FrameBridge automatically handles metadata retirement in the verified AArch64 +Meta XR Audio Wwise build (`e1619e7fb839badf0ea5ecc22d9f028d0b17c34d`). It requires +the build ID and exact function prologues; other builds and 32-bit adapters are +left unchanged. No additional patch switch or recipe entry is required. + +The SDK queues terminated metadata for deletion, then drains those queues before +reading the current `AkAudioObjects` snapshot. In Batman: Arkham Shadow, that +snapshot can still reference queued metadata during smoke-bomb playback. The +audio thread subsequently clears its dirty flags at offset 8 through the stale +pointer. A hardware watch captured this store in +`OculusEndpointSink::ConsumeObjectExperimentalMetadata`, at library offset +`0x2ced90`. Reallocated input tree nodes occupy the same 48-byte size class; +the stale store corrupts their right pointer and the game crashes later. + +`native/adapter/audio_metadata.h` wraps the audio consumption boundary to make +the current snapshot available to metadata cleanup. `audio_metadata_queue.h` +retains queued objects still referenced by that snapshot, then reclaims them +normally when the references disappear. It preserves the SDK's queue mutex, +virtual deleting destructor, audio parameters and processing. There is no timed +quarantine, replacement allocator, audio suppression or diagnostic memory arena. + +Synthetic regression tests exercise mixed referenced and unreferenced queue +entries, both metadata types, subsequent retirement, and cleanup without a live +snapshot. On-headset smoke-bomb tests first showed the observed overwrite stopped, +then passed with ordinary allocations and all memory diagnostics removed. + +The existing `frame.adapter` install step includes the repair in its packaged +adapter and updates already wrapped APKs. Rebuild the adapters and checksums with +`python native/build.py --only adapter`. diff --git a/docs/PLAYBOOK.md b/docs/PLAYBOOK.md index 8d02147..e48d0e4 100644 --- a/docs/PLAYBOOK.md +++ b/docs/PLAYBOOK.md @@ -53,6 +53,7 @@ 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 (e.g. 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 | unfixed: hiding the dips (debounce) made AC Nexus stay black, so it 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) | | Game (Unity, GLES) freezes, `zink: DEVICE LOST` | multisampled render-to-texture hangs the GPU | `frame.unity_no_msaa`; if it persists: unfixable → PC version | | Only the Android home screen shows; the log has Unity's VR device as `None` (e.g. Accounting+, Unity 2017) | Unity 2017–2018 built-in Oculus support starts VR only when `com.oculus.systemactivities` is installed; Lepton has no Meta system apps, so Unity falls back to non-VR | `frame.unity_oculus_check`: that package name in libunity.so → `android`, plus `native/ovrpshim` (libfp_ovrp.so): Unity 2017's legacy frame loop (`ovrp_Update2`/`ovrp_BeginFrame`) never calls `ovrp_WaitToBeginFrame`, so without the shim no `xrWaitFrame` happens, `CompositorOpenXR::Update … outside of frame bounds` floods the log and the dashboard freezes. With it: log `ovrp frame loop shim: waited for frame N`, ~71 fps. Accounting+ then stops at "press any button" although its OVRInput gets clean input (`INPUT_PROBE` in the patch logs connected controllers/buttons/input focus per call) — unresolved | | Own-engine game crashes in `libVkLayer_fossilize.so` (Fossilize's recording thread, e.g. Roblox) | same as Deadpool VR: uninitialised pointers in what Fossilize records | `frame.vk_sanitize` now also covers own-engine libraries that load `libvulkan.so` by name | diff --git a/native/README.md b/native/README.md index 1f81fec..94fdae3 100644 --- a/native/README.md +++ b/native/README.md @@ -1,5 +1,9 @@ # Native components +FrameBridge includes automatic metadata retirement for the verified Meta XR Audio +Wwise build; see [AUDIO_METADATA.md](../docs/AUDIO_METADATA.md) for its lifetime +invariant and regression coverage. It uses the existing adapter installation. + Sources of the prebuilt binaries in `../artifacts/` (committed, with `SHA256SUMS`). End users never compile anything; developers rebuild with `python native/build.py` (downloads NDK r27c, OpenXR headers at pinned commits, d8 into `native/.cache/`, git-ignored). The adapter, GL shim and platform compat rebuild byte-identically; the VrApi bridge diff --git a/native/adapter/audio_metadata.h b/native/adapter/audio_metadata.h new file mode 100644 index 0000000..8674adb --- /dev/null +++ b/native/adapter/audio_metadata.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Meta XR Audio Wwise metadata retirement. Enabled automatically for the +// exact verified AArch64 SDK build; unrelated libraries remain untouched. +#pragma once +#if defined(__aarch64__) +#include +#include +#include +#include +#include +#include +#include +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_metadata_queue.h" +typedef struct {float previous,current;} MxAudioRamp; +static uintptr_t mx_audio_base; +static uint64_t (*mx_audio_consume_original)(void*,void*,void*,const void*,MxAudioRamp); +static void mx_audio_kill(unsigned experimental) { + void *mutex=(void*)(mx_audio_base+(experimental?0x5fe2e0:0x5fe380)); + int (*trylock)(void*)=(void*)(mx_audio_base+0x5b369c); + void (*unlock)(void*)=(void*)(mx_audio_base+0x5b36bc); + if(!trylock(mutex))return; + mx_audio_drain((void**)(mx_audio_base+(experimental?0x5fe2d8:0x5fe378)),experimental?40:24); + unlock(mutex); +} +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. + 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); + mx_audio_objects=previous;return result; +} +static int mx_audio_find(struct dl_phdr_info *info,size_t size,void *unused) { + (void)size;(void)unused; + if(!strstr(info->dlpi_name,"/libMetaXRAudioWwise.so"))return 0; + if(mx_audio_base)return 1; + const unsigned char id[20]={0xe1,0x61,0x9e,0x7f,0xb8,0x39,0xba,0xdf,0x0e,0xa5,0xec,0xc2,0x2d,0x9f,0x02,0x8d,0x0b,0x17,0xc3,0x4d}; + int match=0; + for(unsigned i=0;idlpi_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=(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) && note->n_descsz==20 && !memcmp(desc,id,20))match=1; + p=next; + } + } + if(!match)return 1; + const uint32_t consume[4]={0xd10743ff,0x6d1623e9,0xa9177bfd,0xa9186ffc}; + 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))return 1; + __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)) { + __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)) { + LOG("FrameBridge audio metadata: incomplete installation");return 1; + } + LOG("FrameBridge audio metadata: metadata reclamation follows current audio-frame references; Meta audio base=%p",base); + return 1; +} +static pthread_mutex_t mx_audio_install_mutex=PTHREAD_MUTEX_INITIALIZER; +static void mx_audio_initialize(void) { + if(__atomic_load_n(&mx_audio_base,__ATOMIC_ACQUIRE))return; + if(pthread_mutex_trylock(&mx_audio_install_mutex))return; + if(!mx_audio_page)mx_audio_page=(size_t)sysconf(_SC_PAGESIZE); + if(!mx_audio_base)dl_iterate_phdr(mx_audio_find,NULL); + pthread_mutex_unlock(&mx_audio_install_mutex); +} +#else +static void mx_audio_initialize(void) {} +#endif diff --git a/native/adapter/audio_metadata_queue.h b/native/adapter/audio_metadata_queue.h new file mode 100644 index 0000000..6cacafc --- /dev/null +++ b/native/adapter/audio_metadata_queue.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +#pragma once +#include +#include +#include +// Experimental lifetime repair for the pinned Meta XR Audio Wwise build. +// A queued metadata object must not be reclaimed while the current immutable +// AkAudioObjects snapshot still references it. Preserve the SDK's queue mutex, +// virtual destructor, list linkage, audio processing, and dirty-flag writes. +static _Thread_local const unsigned char *mx_audio_objects; +static unsigned mx_audio_held,mx_audio_reclaimed; +static int mx_audio_referenced(const void *pointer) { + if(!mx_audio_objects)return 0; + uint32_t count;memcpy(&count,mx_audio_objects,4); + const unsigned char *const *objects;memcpy(&objects,mx_audio_objects+16,8); + // Refuse to reclaim on an unrecognised snapshot rather than dereferencing + // unbounded arrays. These bounds exceed the game's ordinary voice counts. + if(count>4096 || (count && !objects))return 1; + for(unsigned i=0;i4096 || (metadata_count && !metadata))return 1; + for(unsigned j=0;j +#include +#include +#include +#define LOG(...) ((void)0) +#include "audio_metadata_queue.h" +static unsigned destroyed; +static void destroy(void *p) {destroyed++;free(p);} +static void (*vtable[2])(void*)={NULL,destroy}; +static void *parameter(size_t next_offset,void *next) { + unsigned char *p=calloc(1,48);assert(p); + memcpy(p,&(void*){vtable},8);memcpy(p+next_offset,&next,8);p[8]=255; + return p; +} +static void exercise(size_t next_offset) { + void *fourth=parameter(next_offset,NULL),*third=parameter(next_offset,fourth); + void *second=parameter(next_offset,third),*first=parameter(next_offset,second),*head=first; + unsigned char metadata[48]={0},object[120]={0},snapshot[24]={0}; + memcpy(metadata+8,&first,8);memcpy(metadata+32,&third,8); + void *metadata_pointer=metadata;memcpy(object+104,&metadata_pointer,8); + uint32_t two=2,one=1;memcpy(object+112,&two,4); + void *objects[1]={object},*objects_pointer=objects; + memcpy(snapshot,&one,4);memcpy(snapshot+16,&objects_pointer,8);mx_audio_objects=snapshot; + unsigned before=destroyed;mx_audio_drain(&head,next_offset); + assert(destroyed==before+2 && head==first); + void *next;memcpy(&next,(unsigned char*)first+next_offset,8);assert(next==third); + memcpy(&next,(unsigned char*)third+next_offset,8);assert(!next); + // Audio can still consume and modify the queued metadata in this frame. + ((unsigned char*)first)[8]=0;((unsigned char*)third)[8]=0; + mx_audio_drain(&head,next_offset);assert(destroyed==before+2); + // Once the audio snapshot drops the references, both objects are reclaimed. + memset(snapshot,0,4);mx_audio_drain(&head,next_offset); + assert(destroyed==before+4 && !head);mx_audio_objects=NULL; +} +int main(void) { + exercise(40);exercise(24); + assert(destroyed==8); + // No active frame preserves the original immediate reclamation behavior. + void *head=parameter(40,NULL);mx_audio_drain(&head,40);assert(!head && destroyed==9); + puts("Queued metadata survives live frame references; mixed queues unlink correctly; retired objects are reclaimed without leaks."); +} diff --git a/tests/test_audio_metadata.py b/tests/test_audio_metadata.py new file mode 100644 index 0000000..92d66fe --- /dev/null +++ b/tests/test_audio_metadata.py @@ -0,0 +1,19 @@ +"""Exercise metadata retirement with synthetic frames, without game files.""" +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + + +def test_audio_metadata_retirement(tmp_path): + cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang") + if not cc: + pytest.skip("Native metadata regression test needs a host C compiler") + root = Path(__file__).resolve().parents[1] + binary = tmp_path / ("audio-metadata-test.exe" if os.name == "nt" else "audio-metadata-test") + subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", + "-I", str(root / "native/adapter"), + str(root / "tests/fixtures/src/audio_metadata_test.c"), "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) diff --git a/tests/test_patches.py b/tests/test_patches.py index dc74cdf..79e6a41 100644 --- a/tests/test_patches.py +++ b/tests/test_patches.py @@ -54,6 +54,22 @@ def test_adapter_and_launcher(tmp_path, quest_manifest): assert names.count("lib/arm64-v8a/libopenxr_loader_generic.so") == 1 +def test_adapter_refreshes_existing_wrapper(tmp_path, quest_manifest): + """Reinstalling an already wrapped APK replaces its adapter with the shipped repair.""" + from frameport.patches.frame import artifact + + apk = _apk(tmp_path, quest_manifest) + with ApkWorkspace(apk) as ws: + ws.put(ws.lib("libopenxr_loader_original.so"), b"original-loader") + ws.put(ws.lib("libopenxr_loader_generic.so"), b"old-adapter") + patch = base.get("frame.adapter") + ctx = base.ApkContext(ws, _analysis(), {}, Reporter(), {"frame.adapter": {}}) + assert patch.apply(ctx) + assert ws.read_lib("libopenxr_loader_generic.so") == artifact(ws.abi, "libopenxr_loader_generic.so") + assert ws.read_lib("libopenxr_loader_original.so") == b"original-loader" + assert not patch.apply(ctx) + + def test_controller_models_adds_xrshim(tmp_path, quest_manifest): from pathlib import Path