mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-16 13:01:33 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e28aeaeda | ||
|
|
43476bd180 | ||
|
|
939845d379 | ||
|
|
35eb12df08 | ||
|
|
570a98b91a |
No files matched your search
@@ -459,9 +459,6 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
XR_KHR_composition_layer_equirect2/cylinder, XR_FB_composition_layer_image_layout (flip emulated by Vulkan blit).
|
||||
- `xrConvertTimespecTimeToTimeKHR`/`xrConvertTimeToTimespecTimeKHR` return FUNCTION_UNSUPPORTED → adapter emulates
|
||||
(offset = predictedDisplayTime − period − CLOCK_MONOTONIC, sampled in xrWaitFrame).
|
||||
- No XR_FB_display_refresh_rate (`xrGetDisplayRefreshRateFB` -7), yet OVRPort's dispatcher offers it and answered 0 Hz
|
||||
→ FrameBridge always answers with the measured rate (1/predictedDisplayPeriod, else 72). PreyVR (#306, idTech 4:
|
||||
`USERCMD_MSEC = 1000 / GetRefresh()`) had 0 ms tics: async thread spinning, `com_frameTime` stuck at 0, menu black.
|
||||
- Guardian STAGE bounds report 1×1 m (scene emulation uses ≥1.5 m).
|
||||
- GL goes through **Zink** (Mesa GL on Vulkan). Mesa GLSL is strict: `#pragma` before `#extension` fails, implicit
|
||||
int/float conversions fail (enable `GL_EXT_shader_implicit_conversions`), num_views=2 shaders on single-view FBOs
|
||||
@@ -1058,6 +1055,13 @@ GitHub identity + credentials are maintainer-local (`CLAUDE.local.md`): check `g
|
||||
GitHub write. **The owner approves before posting, labelling or closing anything.** Reply style: "I"/neutral, never
|
||||
we/our/us; never mention agent version numbers or internal tool versions; show the owner every draft in full with a
|
||||
short recap of the thread so far; read **all** comments on a thread first (a later one often has the zip or the answer).
|
||||
- **Every triage also sweeps for unanswered comments**, not just new threads: open AND recently closed issues, PRs
|
||||
(incl. merged/closed ones) and discussions (top-level comments and their replies) whose latest comment isn't the
|
||||
maintainer's, plus earlier user comments a later maintainer reply skipped. Users often ask follow-ups after a fix or
|
||||
after closing (#294: two questions after the reporter closed it; #249: a Q&A discussion with no reply for days;
|
||||
#161/#110: requests posted after the close). Quick check: `gh issue list --state all --search "updated:>=<date>"
|
||||
--json number,comments --jq '.[] | select(.comments[-1].author.login != "spoopyghosty0")'`; discussions via GraphQL
|
||||
(`discussions(orderBy:UPDATED_AT)` with `comments{replies}`).
|
||||
- **Issue templates & labels** (`.github/ISSUE_TEMPLATE/`): `[Game report]` → `game-compatibility` (a game that
|
||||
doesn't work right); `[Working recipe]` (`working-config.yml`) → `working-config` (a headset-verified config to add);
|
||||
`[App]` → `bug` (FramePort itself). Mislabel/relabel to match.
|
||||
|
||||
@@ -10,14 +10,14 @@ d6337034fb0bdcca7ef9b7063d85b7586b8508b63b435959e8fffaba0691091c ./arm64-v8a/li
|
||||
0167c2fde5dfde62897f9d85afc4fc8e97784e988b5b8df6cea8c407240b3424 ./arm64-v8a/libfp_tkeys.so
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
86261c4d7c4b78af5e182b81ae172f6d5fd5b12d8dc6194a876eee08b589d3ce ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1cc65695b223f7d56913275554c233f6b1d38a433068f26413fa97135f202a78 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
|
||||
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
|
||||
1a3b250835798f46049f527a6b32f9d0b33a2a976006d08f5997bfccc215ee89 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
53a7d6340707766a7c7465f20084c520d259c76581f02a7e516a52cfeb624d51 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
package: com.FlatHeadStudio.DeadlyDelivery
|
||||
title: Deadly Delivery
|
||||
status: unsupported
|
||||
notes: "Quits after Meta's entitlement check fails: it needs Meta's online services."
|
||||
tested_version: 1.0.30
|
||||
engine: Unity
|
||||
xr: OpenXR
|
||||
verified:
|
||||
date: '2026-10-15'
|
||||
issue: 322
|
||||
@@ -1,5 +1,5 @@
|
||||
package: com.asg.clockworkdev
|
||||
title: Clockwork
|
||||
title: The Last Clockwinder
|
||||
status: works
|
||||
tested_version: '1.29'
|
||||
engine: Unity
|
||||
|
||||
+12
-7
@@ -11,6 +11,16 @@ signatures:
|
||||
severity: error
|
||||
diagnosis: A DLC manager reports a Meta asset-file status error after VR starts (e.g. PixelRipped 1995). The asset-file check failed; the log does not identify the native call or asset ID. Try the experimental asset-file bypass (empty list, installed status with no file path); this does not supply DLC and has not been verified in the game.
|
||||
suggest: [frame.asset_files_stub]
|
||||
- id: meta-entitlement
|
||||
# Quest games (the pcvr oculus-entitlement signature covers Rift games). Only explicit failure results: a bare
|
||||
# ovr_PlatformInitialize error isn't matched. `error`, not fatal: offline games may log it and play on; an online
|
||||
# game that then quits (e.g. Deadly Delivery, GitHub #322) fails the test through its exit.
|
||||
pattern: '(?i)entitlement check failed|\bnot entitled to (use|play)|ovrPlatformInitialize_(NotEntitled|UnableToVerify|FileInvalid|SignatureInvalid)'
|
||||
severity: error
|
||||
diagnosis: The game's Meta licence check (entitlement) failed. It needs Meta's online login and licence check,
|
||||
which the Frame doesn't have; games that depend on it (often online games) quit or stay offline. FramePort
|
||||
doesn't get around such checks.
|
||||
suggest: []
|
||||
- id: lua-lightuserdata-pointer
|
||||
pattern: 'bad light userdata pointer'
|
||||
severity: fatal
|
||||
@@ -101,13 +111,8 @@ signatures:
|
||||
suggest: [frame.adapter]
|
||||
- id: cylinder-menu-layers-rejected
|
||||
pattern: 'xrEndFrame failed -16\b'
|
||||
severity: info
|
||||
# PreyVR (GitHub #306): one rejected frame at the start of every session (its space isn't locatable yet); the black
|
||||
# menu there had another cause (a 0 Hz refresh rate stopped the game clock), so this alone is no error
|
||||
diagnosis: The runtime rejected a frame's layers once (XR_ERROR_LAYER_INVALID). At the start of a session that is
|
||||
harmless. If it repeats every frame the picture stays black; FrameBridge shows a curved (cylinder) screen drawn
|
||||
to each eye separately (for example the Doom3Quest / PreyVR menu) as one set of flat strips for both eyes to stay
|
||||
within the runtime's layer limit, so rebuild and reinstall the game with the current FrameBridge.
|
||||
severity: error
|
||||
diagnosis: The game draws a curved (cylinder) screen to each eye separately (for example the Doom3Quest / PreyVR menu). The Frame's runtime has no cylinder layers, so FrameBridge shows each as flat quad strips; a per-eye pair makes too many quad layers for one frame and the runtime rejects the whole frame (XR_ERROR_LAYER_INVALID), leaving the menu black while the game runs. FrameBridge from this build shows the left eye's strips to both eyes to halve the count; rebuild and reinstall the game to try it.
|
||||
suggest: [frame.adapter]
|
||||
- id: missing-ovr-symbol
|
||||
pattern: '(UnsatisfiedLinkError|cannot locate symbol) .*"?(ovr_[A-Za-z0-9_]+|ovr[A-Z][A-Za-z0-9]*_ToString)'
|
||||
|
||||
+20
-3
@@ -14,10 +14,12 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Asgard's Wrath 2 | Quest | ✅ Works | |
|
||||
| Assassin's Creed Nexus | Quest | ✅ Works | |
|
||||
| Audica | Quest | ✅ Works | |
|
||||
| AugmentedEmpire | Quest | ✅ Works | |
|
||||
| BAM | Quest | ✅ Works | |
|
||||
| BARTENDER VR SIMULATOR | Quest | ✅ Works | |
|
||||
| Batman: Arkham Shadow | Quest | ✅ Works | |
|
||||
| BattleGlide | Quest | ✅ Works | |
|
||||
| Battlemarked | Quest | ✅ Works | |
|
||||
| BattleSisters | Quest | ✅ Works | |
|
||||
| Beat Saber | Quest | ✅ Works | |
|
||||
| Beat Saber | Quest | ✅ Works | |
|
||||
@@ -29,7 +31,6 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| BONELAB | Quest | ✅ Works | |
|
||||
| Carve Snowboarding | Quest | ✅ Works | |
|
||||
| Caves | Quest | ✅ Works | |
|
||||
| Clockwork | Quest | ✅ Works | |
|
||||
| COMPOUND | Quest | ✅ Works | |
|
||||
| Cook-Out | Quest | ✅ Works | |
|
||||
| Cosmodread | Quest | ✅ Works | |
|
||||
@@ -37,6 +38,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| cubism | Quest | ✅ Works | |
|
||||
| Dance Central | Quest | ✅ Works | |
|
||||
| Deep Cuts | Quest | ✅ Works | |
|
||||
| Demeo | Quest | ✅ Works | |
|
||||
| Demeter | Quest | ✅ Works | |
|
||||
| Dinosaur Island | Quest | ✅ Works | |
|
||||
| Doom3Quest | Quest | ✅ Works | |
|
||||
@@ -46,10 +48,12 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Eleven Table Tennis | Quest | ✅ Works | |
|
||||
| Eleven: Table Tennis VR | PC VR | ✅ Works | |
|
||||
| Espire 2 | Quest | ✅ Works | |
|
||||
| Eye of the Temple | Quest | ✅ Works | |
|
||||
| FrenzyVR | Quest | ✅ Works | |
|
||||
| Fruit Ninja | Quest | ✅ Works | |
|
||||
| Fruit Salon | Quest | ✅ Works | |
|
||||
| Genotype | Quest | ✅ Works | |
|
||||
| Ghost Town | Quest | ✅ Works | |
|
||||
| Ghostbusters: Rise of the Ghost Lord | Quest | ✅ Works | |
|
||||
| GORN2 | Quest | ✅ Works | |
|
||||
| Gravity Lab | Quest | ✅ Works | |
|
||||
@@ -61,6 +65,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| In Death: Unchained | Quest | ✅ Works | |
|
||||
| Into The Radius 2 | Quest | ✅ Works | |
|
||||
| Job Simulator | Quest | ✅ Works | |
|
||||
| Journey of the Gods | Quest | ✅ Works | |
|
||||
| Jurassic World Aftermath Collection | Quest | ✅ Works | |
|
||||
| Keep Talking and Nobody Explodes | Quest | ✅ Works | |
|
||||
| Kill It With Fire VR | Quest | ✅ Works | |
|
||||
@@ -68,18 +73,23 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Lambda1VR | Quest | ✅ Works | |
|
||||
| LEGO® Bricktales | Quest | ✅ Works | |
|
||||
| Lucky's Tale | Quest | ✅ Works | |
|
||||
| Maestro | Quest | ✅ Works | |
|
||||
| Marvel's Deadpool VR | Quest | ✅ Works | |
|
||||
| Marvel's Iron Man VR | Quest | ✅ Works | |
|
||||
| Max Mustard | Quest | ✅ Works | |
|
||||
| Medieval Dynasty New Settlement | Quest | ✅ Works | |
|
||||
| Metro Awakening | Quest | ✅ Works | |
|
||||
| Mini Motor Racing X | Quest | ✅ Works | |
|
||||
| Mobile Suit Gundam: Silver Phantom | Quest | ✅ Works | |
|
||||
| Moss | Quest | ✅ Works | |
|
||||
| Moss: Book II | Quest | ✅ Works | |
|
||||
| Munkr Funkr (The Legacy Days) | Quest | ✅ Works | |
|
||||
| Nano | Quest | ✅ Works | |
|
||||
| NEX Player | Quest | ✅ Works | |
|
||||
| NFL PRO ERA II | Quest | ✅ Works | |
|
||||
| NOPE CHALLENGE | Quest | ✅ Works | |
|
||||
| Ocean Rift | Quest | ✅ Works | |
|
||||
| Operation Wolf Returns: First Mission VR | Quest | ✅ Works | |
|
||||
| palazzo_santacruz | Quest | ✅ Works | |
|
||||
| Path of the Warrior | Quest | ✅ Works | |
|
||||
| Pistol Whip | Quest | ✅ Works | |
|
||||
@@ -97,9 +107,11 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Riven | Quest | ✅ Works | |
|
||||
| Robo Recall | Quest | ✅ Works | |
|
||||
| RUINSMAGUS | Quest | ✅ Works | |
|
||||
| Shadow Point | Quest | ✅ Works | |
|
||||
| Shoot The Zombirds VR | Quest | ✅ Works | |
|
||||
| Shores of Loci | Quest | ✅ Works | |
|
||||
| SilentSlayer | Quest | ✅ Works | |
|
||||
| Smash Drums | Quest | ✅ Works | |
|
||||
| Sniper Elite VR | Quest | ✅ Works | |
|
||||
| Sniper Elite VR: Winter Warrior | Quest | ✅ Works | |
|
||||
| Space Pirate Trainer Quest | Quest | ✅ Works | |
|
||||
@@ -112,9 +124,11 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| SUPERHOT VR | Quest | ✅ Works | |
|
||||
| SynthRiders | Quest | ✅ Works | |
|
||||
| Taskmaster | Quest | ✅ Works | |
|
||||
| Tea For God | Quest | ✅ Works | |
|
||||
| TetrisEffect | Quest | ✅ Works | |
|
||||
| The Boys VR | Quest | ✅ Works | |
|
||||
| The Climb 2 | Quest | ✅ Works | |
|
||||
| The Last Clockwinder | Quest | ✅ Works | |
|
||||
| The Room VR | Quest | ✅ Works | |
|
||||
| The Tale of Onogoro | Quest | ✅ Works | |
|
||||
| Time Crisis II VR (Test) | Quest | ✅ Works | |
|
||||
@@ -128,6 +142,7 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Vader Immortal: Episode II | Quest | ✅ Works | |
|
||||
| Vader Immortal: Episode III | Quest | ✅ Works | |
|
||||
| Vampire Survivors | Quest | ✅ Works | |
|
||||
| Vendetta Forever | Quest | ✅ Works | |
|
||||
| VR HOT Quest | Quest | ✅ Works | |
|
||||
| VR4 | Quest | ✅ Works | |
|
||||
| Walkabout Mini Golf | Quest | ✅ Works | |
|
||||
@@ -144,12 +159,15 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| ExploreVR | Quest | ⚠️ Works with issues | The 3D world warps and shifts with head movement (wrong perspective). |
|
||||
| Fisherman | Quest | ⚠️ Works with issues | Loading screens have corruption, but everything works otherwise. |
|
||||
| Freedom | Quest | ⚠️ Works with issues | Volume can't be changed in the game. |
|
||||
| Homeworld: Vast Reaches | Quest | ⚠️ Works with issues | The game starts but after selecting to start a new game it seems to crash. |
|
||||
| MacacoVR | Quest | ⚠️ Works with issues | 2d loading screens are glitchy, otherwise main gameplay is fine |
|
||||
| Majora's Mask VR | Quest | ⚠️ Works with issues | Audio on the file select screen sounds crunchy. |
|
||||
| Myst | Quest | ⚠️ Works with issues | Minor graphical glitches on some objects. |
|
||||
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
|
||||
| Pinball FX VR | Quest | ⚠️ Works with issues | Plays; mixed reality mode not working yet. |
|
||||
| Plezy | Quest | ⚠️ Works with issues | Some videos play with strong artifacts and wrong colours; the cause is unknown, possibly hardware video decoding. |
|
||||
| Quantum Void | Quest | ⚠️ Works with issues | no hands |
|
||||
| Ragnarock | Quest | ⚠️ Works with issues | |
|
||||
| Silhouette | Quest | ⚠️ Works with issues | Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky. |
|
||||
| Spunky Dungeon | Quest | ⚠️ Works with issues | Gear VR / Oculus Go game (32-bit only), runs through FramePort's 32-bit bridge (experimental). |
|
||||
| Swarm 2 | Quest | ⚠️ Works with issues | MSAA creates visible seams around edge geometry and also the skybox edge |
|
||||
@@ -158,9 +176,8 @@ Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
|
||||
| Time Stall | Quest | ⚠️ Works with issues | Both eyes distort during movement (unresolved). |
|
||||
| Vermillion | Quest | ⚠️ Works with issues | Runs. |
|
||||
| WiiCompiled VR | Quest | ⚠️ Works with issues | To add a game: in FramePort's Files tab, upload your .wcgame file to this game's storage, folder Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR… |
|
||||
| Deadly Delivery | Quest | ❌ Doesn't run | Quits after Meta's entitlement check fails: it needs Meta's online services. |
|
||||
| Espire 1: VR Operative (Quest Edition) | Quest | ❌ Doesn't run | Mesa GL driver crash during texture upload. |
|
||||
| GOLF+ | Quest | ❌ Doesn't run | Black screen. |
|
||||
| HITMAN 3 VR: Reloaded | Quest | ❌ Doesn't run | Vulkan driver crash (freedreno), even without Valve layers. |
|
||||
| Journey of the Gods | Quest | ❌ Doesn't run | 32-bit only; the Frame has no AArch32. |
|
||||
| Roblox | Quest | ❌ Doesn't run | Crashes on its first VR frame on the Frame. |
|
||||
| Shadow Point | Quest | ❌ Doesn't run | 32-bit only; the Frame has no AArch32. |
|
||||
+5
-5
@@ -511,7 +511,7 @@ FramePort's layer between the game and the Steam Frame: fills in what the Frame
|
||||
- **Suggested (on):** Required for every OpenXR/overport build on the Frame.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `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).
|
||||
- `cylinder-menu-layers-rejected` (error): The game draws a curved (cylinder) screen to each eye separately (for example the Doom3Quest / PreyVR menu).
|
||||
- `graphics-requirements-missing` (fatal): The game can't start VR on the Frame; rebuild it with the current FramePort.
|
||||
- `swapchain-rect-invalid` (fatal): The runtime rejects the game's frames, so it stops drawing (for example stuck at a "Waiting" box).
|
||||
- `time-conversion` (warning): The Frame's runtime lacks timespec time conversion; the current adapter build emulates it.
|
||||
@@ -904,7 +904,7 @@ Stops a gray or frozen screen in Unity games that turn on anti-aliasing while ru
|
||||
- `frame.unity_gl_shim`
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Unity GLES game that may switch to MSAA at runtime: hide multisampled render-to-texture, which crashes the Frame's GL driver (for example The Room VR).
|
||||
- **Catalog games (23):** Audica, BattleSisters, Beat Saber, Clockwork, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 15 more.
|
||||
- **Catalog games (23):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 15 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `unreal-msrtt-crash` (fatal): The game's graphics crashed a few seconds after start.
|
||||
- **Source:** [patches/frame/vrapi_bridge.py](../src/frameport/patches/frame/vrapi_bridge.py)
|
||||
@@ -973,7 +973,7 @@ Keeps a smoothing setting off that the game turns on itself and that can freeze
|
||||
- `frame.unity_runtime_msaa_off` · revision 2
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** GLES Unity game whose OVRManager turns 4x MSAA on at runtime: multisampled render-to-texture can hang the Frame's GPU.
|
||||
- **Catalog games (28):** Audica, BattleSisters, Beat Saber, Clockwork, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight and 20 more.
|
||||
- **Catalog games (28):** Audica, BattleSisters, Beat Saber, COMPOUND, Cosmodread, Down the Rabbit Hole, Drunkn Bar Fight, End Space and 20 more.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `unity-render-crash` (fatal): The game's graphics crashed, so the picture freezes or stays gray.
|
||||
- `unity-runtime-msaa` (info): The game turns MSAA on at runtime, which can hang the Frame's GPU.
|
||||
@@ -1621,7 +1621,7 @@ Logs details when flat panels stay black. No effect on the game.
|
||||
- Game settings → Troubleshooting → "Flat panel diagnostics (log)" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log). 2 (frameport settings) also checks every draw and logs the draw state, failed texture uploads and shader compiles and, for idTech 4 engines, the game clock.
|
||||
> Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log).
|
||||
|
||||
### <a id="adapter-haptic_fix"></a>Fix freezes when controllers vibrate
|
||||
|
||||
@@ -1828,7 +1828,7 @@ For apps that only know Quest 3/Pro controllers: without it they get no buttons.
|
||||
- Game settings → Controllers → "Treat newer Quest controllers as Touch" (advanced).
|
||||
- **Source:** [patches/settings.py](../src/frameport/patches/settings.py)
|
||||
|
||||
> Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead (components Touch lacks are dropped). Without it such apps get no controller input.
|
||||
> Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead (components Touch lacks are dropped), and the runtime's answer for each hand is reported to them as their own profile (Unity's OpenXR input only connects controllers of a profile it knows). Meta's detached-controller paths (XR_META_detached_controllers, which the Frame lacks) are left out of the app's actions and bindings: one such path made Unity attach no controller input at all. Without it such apps get no controller input.
|
||||
|
||||
### <a id="adapter-proximity_emul"></a>Animate fingers from touch
|
||||
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| A Unity OpenXR game closes right after the OpenXR instance is created: `F VrApiLoader: vrapi_SetPropertyInt was called before vrapi_Initialize()!`, SIGABRT on UnityMain (GitHub #62, Jurassic World Aftermath) | the APK still ships Meta's VrApi loader; OVRPlugin (1.89, on OpenXR) calls it without starting VrApi and Meta's loader aborts. The VrApi bridge can't replace it (39 of the loader's 114 functions: OVRPlugin wouldn't link) | `frame.vrapi_stub` (same exports, each returns 0); triage `vrapi-before-init` |
|
||||
| A VrApi game exits at once: `UnsatisfiedLinkError … dlopen failed: cannot locate symbol "vrapi_PollEvent"` (GitHub #57, BlazeRush) | the engine imports a VrApi function the VrApi bridge didn't implement | the bridge has `vrapi_PollEvent`, `RecenterPose`, `SetDisplayRefreshRate`, `GetSystemPropertyFloatArray` (rebuild the game); the static check "VrApi functions resolvable" names any other missing one; triage `vrapi-symbol-missing` |
|
||||
| A Team Beef port (Lambda1VR, RTCWQuest, …) exits at once: `UnsatisfiedLinkError: dlopen failed: library "libopenxr_loader_valve.so" not found` (GitHub #59) | TBXR loads `openxr_loader_<Build.MANUFACTURER>` (Lepton: valve) and takes the Meta path only when `strstr(OPENXR_HMD, "meta")` matches, else Pico's | `frame.tbxr_vendor` (empty `libopenxr_loader_valve.so` + the "meta" literal → "alve"). Game data (`xash/`, …) goes to /sdcard: upload it with the Files tab to the game's storage. Black eyes with `TBXR: Incomplete frame buffer object: GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE`: TBXR always uses multisampled render-to-texture (even `--msaa 1`); `frame.tbxr_vendor` links the GL shim, which hands it single-sampled versions (log `GL shim: multisampled render-to-texture … drawn single-sampled`) |
|
||||
| A Quest game renders, then logs `Entitlement check failed` / `ovrPlatformInitialize_NotEntitled` (often with retries) and quits, e.g. `SIG: 9` after `[Photon] Disconnected` (GitHub #322, Deadly Delivery) | The game's Meta licence check (entitlement) needs Meta's online login, which the Frame doesn't have; online-only games quit (triage `meta-entitlement`) | None: FramePort doesn't get around entitlement checks. Offline games that log it and play on are fine |
|
||||
| Black screen after VR starts; `Asset status error (code 100)` in `DLC_Manager:AssetStatusCallback` (GitHub #203, e.g. PixelRipped 1995) | Meta asset-file status fails after entitlement succeeds; the log does not identify the native call or asset ID | Try `frame.asset_files_stub` (opt-in, triage-only): native/langpack answers GetList with an empty successful list and StatusById with installed status, an empty path and the requested ID. No DLC files are supplied; download requests retain their existing behavior. Disable with `FRAMEPORT_ASSET_FILES_STUB=0`. Host-tested only; whether this fixes startup needs a game/device test |
|
||||
| Missing sound or extra content in a game whose OBB folder has separate `*.pak` files (GitHub #61, Star Wars: Tales from the Galaxy's Edge: seasons + Wwise banks) | the game asks Meta's platform for these asset files (`ovr_AssetFile_GetList` / `DownloadById`); OVRPort's loader doesn't list them | copy every content/sound-bank PAK and use `frame.asset_files` (native/langpack lists the data's `*.pak` as installed `default` assets, IAP `free`, and answers DownloadById at once with the path; off: env FRAMEPORT_ASSET_FILES=0). Tales also needs the splash and depth-size fixes below |
|
||||
| A SideQuest backup has `apk/` and a sibling `obb/` with content/sound-bank PAKs, but the Frame only has the main OBB (GitHub #61, Tales from the Galaxy's Edge) | older scans treated that layout as loose expansion files and selected only the named OBBs; the asset-file patch then found no PAKs | the scanner chooses the OBB folder by version first, then keeps the whole sibling data folder when all its expansion files are selected for this APK; app updates repair affected library entries and refresh asset/language-pack detection. Reinstall to copy the missing files, or scan the backup again first. Folders with other packages or unselected OBB versions still send only the selected OBBs |
|
||||
@@ -138,6 +139,8 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Unity game freezes on its loading screen ~10-15 s in (Yupitergrad, GitHub #235); launch.log has `GrantPermissionsActivity: Permission grant result … permission=android.permission.BLUETOOTH_CONNECT`, then OVRPlugin `SetupDisplayObjects(): Unable to find EGLConfig for configId 0` and no more frames; triage `bhaptics-permission-prompt` | bHaptics' vest SDK (`com.bhaptics.commons.PermissionUtils.requestBluetoothPermission`) asks for Android 12's BLUETOOTH_CONNECT; on Lepton's Android 11 it can't be granted at install, so Android's permission screen takes the foreground for a moment, the game pauses/resumes and OVRPlugin's GLES compositor stops drawing | `frame.bhaptics_permission` (suggested by analysis `bhaptics_permission`): the request returns at once (`Dex.return_early`) |
|
||||
| Unity game aborts at start: `Abort message: 'cannot create thread specific key for __cxa_get_globals()'` (SIGABRT in a library such as `libFirebaseCppApp-*.so`, for example Wander 0.3570, GitHub #198); triage `pthread-keys-exhausted` | Bionic allows 128 pthread keys per process; Unity 6 takes ~40 (most without a destructor), large native libraries (Gecko `libxul.so`, Firebase, avatar SDKs) and the Frame's own libraries (Mesa/Zink, FrameBridge, OVRPort, Valve's Vulkan layers) take the rest | `frame.unity_tls_key_pool` (opt-in; suggested for Unity 6 with Firebase C++ or Gecko): libunity.so's `pthread_key_*`/`pthread_get/setspecific` imports are renamed to `fp_tkey_*` (`elf.rename_imports`, versions set global) and `libfp_tkeys.so` (`native/tlskeys`) is added to its DT_NEEDED; destructor-less keys become slots behind one real key (logs `fp_tkeys: pooling Unity's thread keys`). Approach by Klownicle (#198) |
|
||||
| Controllers do nothing (for example stuck on a setup screen); log: `xrSuggestInteractionProfileBindings … XR_ERROR_PATH_UNSUPPORTED` | the game only suggests bindings for Meta's newer profiles (Touch Plus / Touch Pro); the Frame's Android runtime knows `oculus/touch_controller`, not those | adapter `profile_remap` (default on): the bindings are suggested again for Touch (log: `controller profile … -> oculus/touch_controller`) |
|
||||
| Controllers "don't sync": virtual hands/controllers lie on the floor below you, no buttons (Unity OpenXR games that only suggest Touch Plus/Pro, e.g. Hotel Infinity #327); log with `input_diag=1`: only `meta/touch_controller_plus` (or Pro) suggested, `controller profile … -> oculus/touch_controller` | the runtime then reports `oculus/touch_controller` for each hand, a profile the game never suggested: Unity's OpenXR input creates no device for it | adapter `profile_remap` (default on) reports the game's own profile for such hands (log: `profile_remap: /user/hand/left reported as meta/touch_controller_plus`) |
|
||||
| Controllers "don't sync": hands on the floor below you, no buttons, in a Unity OpenXR game with Meta's OpenXR features (e.g. Spatial Zombies #328); log: `xrCreateAction: XR_ERROR_PATH_UNSUPPORTED` (input_diag: `xrCreateAction -> XR_ERROR_PATH_UNSUPPORTED (thumbstick)`), then `xrGetCurrentInteractionProfile: XR_ERROR_ACTIONSET_NOT_ATTACHED` | Meta's "Detached … Controller" devices add `/user/detached_controller_meta/left|right` to Unity's merged actions; the Frame has no XR_META_detached_controllers, the action fails and Unity attaches no action set (OVRPlugin's input goes with it) | adapter `profile_remap` (default on) leaves those paths out of actions and bindings (log: `profile_remap: action thumbstick without 2 detached-controller path(s)`) |
|
||||
| Some buttons do nothing or the wrong thing (for example a Quest game's X/Y on the Frame's left controller) | the game binds only Quest Touch and SteamVR maps Touch onto the Frame controllers with its own remap (Touch left X/Y land on the d-pad, right X and Y repeat B), or the runtime rejected a profile or an input call | turn on adapter `input_diag` (diagnostics, off by default; `frameport settings <pkg> input_diag=1`) and look for `input_diag:` lines: `bindings: <profile> accepted` or `unsupported: interaction profile <profile> -> <result>` with the rejected paths, `bindings: /user/hand/<hand> uses <profile>`, `unsupported: function <name>`, `unsupported: xrCreateAction -> <result> (<action>)` |
|
||||
| Unity game hangs or the whole Frame restarts (for example in a menu); log: `The current MSAA level is 0, but the recommended MSAA level is 4. Switching to the recommended level.` (for example Lucky's Tale) | Meta's OVRManager (`useRecommendedMSAALevel`) turns 4x MSAA on at runtime, past QualitySettings; multisampled render-to-texture on GLES/Zink hangs the GPU | `frame.unity_runtime_msaa_off` (Cpp2IL: `OVRDisplay.get_recommendedMSAALevel` → 0) |
|
||||
| Unity game: one eye shows only effects or gray, the other is fine (for example I Am Cat, Oculus XR Plugin on GLES) | the game's single-pass multiview rendering goes wrong for array slice 1 (the adapter submits both slices correctly) | `frame.unity_multipass` (Cpp2IL: `OculusSettings.GetStereoRenderingMode` → MultiPass); `swap_eyes=1` tells whether the game or the adapter is at fault |
|
||||
@@ -168,7 +171,6 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| Black screen, audio works, log `Unsupported VrApi layer type N` | bridge drops frames containing that layer | extend the bridge (cylinder=3 is converted to a quad already) |
|
||||
| Only some draws visible (for example controllers) + `glGetError 0x502` | multiview shaders used on single-view FBOs | GL shim `gl_hide_multiview=1` (default) |
|
||||
| Own GLES engine (OpenXR, not VrApi): eye view fine, but HUD / menu / PDA panels black (for example Doom3Quest, GitHub #77; no error in the log) | every shader declares `layout(num_views=2) in;`, also the ones drawn into the engine's single-layer offscreen framebuffers (2D textures/renderbuffers); OVR_multiview makes such draws INVALID_OPERATION and Mesa drops them (`draw_validate.c`), Quest's driver doesn't | `frame.gl_multiview_fbo` (opt-in, experimental: `libfpglmv.so` interposes GLES and draws those cases with a single-view twin of the program; logs `GLMV: twin built: program N -> M`, per-5-s `single-view draws` counters; `gl_mv_debug=1` adds glGetError checks; triage `gl-multiview-twin-failed`). Alternative: the game's own fix (two-layer multiview pool, Xandrix's patch in #77) |
|
||||
| Team Beef idTech 4 port (PreyVR, GitHub #306): main menu black except the mouse cursor, loading screen fine, 72 fps, no GL error; one game thread at 100 % CPU | the game clock runs 1000 / refresh ms per tic (`USERCMD_MSEC`, from xrGetDisplayRefreshRateFB). The Frame's runtime lacks XR_FB_display_refresh_rate and OVRPort's dispatcher answered 0 Hz: 0 ms tics, the async thread spins (millions of tics/s) and `com_frameTime` stays 0, so the menu GUI stays at its first animation step (everything hidden) | FrameBridge (default, no setting) answers xrGetDisplayRefreshRateFB with the measured rate (1 / predictedDisplayPeriod, else 72 Hz); log `refresh rate: runtime reported 0.0 Hz (result -7), reporting 72.0 Hz`. Found with `gl_mv_debug=2` (`frameport settings <pkg> gl_mv_debug=2`, needs `frame.gl_multiview_fbo`): only 2 draws per frame (backdrop + cursor) and `trace clocks: com_frameTime=0` |
|
||||
| Upside-down image in a GL bridge game | GL images start at the bottom row | fixed in the bridge (swap angleUp/Down for GL chains) |
|
||||
| UI panels upside down (AC Nexus) | XrCompositionLayerImageLayoutFB VERTICAL_FLIP unsupported | adapter `flip_emul=1` (default); rotating quads makes them vanish |
|
||||
| Passthrough black (BAM) | XR_FB_passthrough missing | adapter `passthrough_emul=1` (default) + `patch_force_passthrough` for MR-only games |
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Included by session_fixes.c: Meta's detached controllers (XR_META_detached_controllers: a controller put down while
|
||||
// hand tracking runs) on a runtime without that extension. Uses profile_remap, runtime_has_detached_controllers,
|
||||
// active_instance, lookup and LOG.
|
||||
//
|
||||
// Meta's Unity OpenXR features add "Detached ... Controller" devices for /user/detached_controller_meta/left|right.
|
||||
// Unity merges same-named actions of every enabled device into one action, so the first action with such a subaction
|
||||
// path (e.g. "thumbstick") fails with XR_ERROR_PATH_UNSUPPORTED on the Frame's runtime, Unity attaches no action set
|
||||
// at all and neither Unity nor OVRPlugin get controller input: hands lying at the play-space origin on the floor, no
|
||||
// buttons (Spatial Zombies, GitHub #328). With profile_remap on and the runtime lacking the extension, these paths are
|
||||
// left out of the game's actions and suggested bindings, and xrGetCurrentInteractionProfile answers "no profile"
|
||||
// for them (a detached controller that never shows up) instead of XR_ERROR_PATH_UNSUPPORTED.
|
||||
|
||||
#define DETACHED_PREFIX "/user/detached_controller_meta/"
|
||||
|
||||
static int detached_filter_on(void) { return profile_remap && !runtime_has_detached_controllers && active_instance; }
|
||||
|
||||
// 1 if `path` is a detached controller's top-level path or one of its input/output paths
|
||||
static int detached_path(XrPath path) {
|
||||
PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
|
||||
char name[XR_MAX_PATH_LENGTH];
|
||||
uint32_t size = 0;
|
||||
return path && to_string && XR_SUCCEEDED(to_string(active_instance, path, sizeof(name), &size, name)) &&
|
||||
!strncmp(name, DETACHED_PREFIX, strlen(DETACHED_PREFIX));
|
||||
}
|
||||
|
||||
// xrCreateAction: `info` without the detached subaction paths, in *copy (paths in `kept`, room for all of them).
|
||||
// Returns `info` itself when there is nothing to leave out.
|
||||
static const XrActionCreateInfo *detached_strip_action(const XrActionCreateInfo *info, XrActionCreateInfo *copy,
|
||||
XrPath *kept) {
|
||||
if (!info || !info->countSubactionPaths || !info->subactionPaths || !kept || !detached_filter_on()) return info;
|
||||
uint32_t n = 0;
|
||||
for (uint32_t i = 0; i < info->countSubactionPaths; ++i)
|
||||
if (!detached_path(info->subactionPaths[i])) kept[n++] = info->subactionPaths[i];
|
||||
if (n == info->countSubactionPaths) return info;
|
||||
*copy = *info;
|
||||
copy->countSubactionPaths = n;
|
||||
copy->subactionPaths = kept;
|
||||
static int logged;
|
||||
if (logged++ < 3)
|
||||
LOG("profile_remap: action %s without %u detached-controller path(s) (the runtime has no "
|
||||
"XR_META_detached_controllers)", info->actionName, info->countSubactionPaths - n);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// xrSuggestInteractionProfileBindings: `suggested` without bindings of detached controllers, in *copy (bindings in
|
||||
// `kept`, room for all of them). Returns `suggested` itself when there is nothing to leave out.
|
||||
static const XrInteractionProfileSuggestedBinding *detached_strip_bindings(
|
||||
const XrInteractionProfileSuggestedBinding *suggested, XrInteractionProfileSuggestedBinding *copy,
|
||||
XrActionSuggestedBinding *kept) {
|
||||
if (!suggested || !suggested->countSuggestedBindings || !suggested->suggestedBindings || !kept ||
|
||||
!detached_filter_on())
|
||||
return suggested;
|
||||
uint32_t n = 0;
|
||||
for (uint32_t i = 0; i < suggested->countSuggestedBindings; ++i)
|
||||
if (!detached_path(suggested->suggestedBindings[i].binding)) kept[n++] = suggested->suggestedBindings[i];
|
||||
if (n == suggested->countSuggestedBindings) return suggested;
|
||||
*copy = *suggested;
|
||||
copy->countSuggestedBindings = n;
|
||||
copy->suggestedBindings = kept;
|
||||
static int logged;
|
||||
if (logged++ < 8) {
|
||||
PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
|
||||
char profile[XR_MAX_PATH_LENGTH] = "?";
|
||||
uint32_t size = 0;
|
||||
if (to_string) to_string(active_instance, suggested->interactionProfile, sizeof(profile), &size, profile);
|
||||
LOG("profile_remap: %s bindings without %u detached-controller path(s)", profile,
|
||||
suggested->countSuggestedBindings - n);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// xrGetCurrentInteractionProfile: a detached controller's path the runtime doesn't know has no profile.
|
||||
static XrResult detached_current_profile(XrPath user, XrInteractionProfileState *state, XrResult result) {
|
||||
if (result != XR_ERROR_PATH_UNSUPPORTED || !state || !detached_filter_on() || !detached_path(user)) return result;
|
||||
state->interactionProfile = XR_NULL_PATH;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
// * drops requested instance extensions the runtime lacks (e.g. XR_FB_passthrough);
|
||||
// * retries rejected swapchains with 1 sample / a supported format (swapchain_fix, overport #71);
|
||||
// * drops composition layers that reference swapchains that failed to create (layer_fix);
|
||||
// * answers xrGetDisplayRefreshRateFB with the measured display rate when the runtime lacks it or reports 0 Hz
|
||||
// (session_fixes.c);
|
||||
// * serves cube swapchains the runtime refuses as GL/Vulkan cube maps and drops their layers (cube_standin, cube_standin.c);
|
||||
// * optionally requests mutable-format swapchain images (mutable_fix, off by default);
|
||||
// * optionally swaps left/right images of stereo projection layers (swap_eyes, off by default);
|
||||
@@ -149,6 +147,7 @@ static int sync_guard; // xrSyncActions one at a time with xrPollEvent, pa
|
||||
static int profile_remap = 1; // Meta's newer controller profiles (rejected by the Frame) -> oculus/touch_controller
|
||||
static int proximity_emul; // finger proximity from capacitive touch: 1 thumb, 2 thumb + index (session_fixes.c)
|
||||
static int runtime_has_proximity; // the runtime has XR_FB_touch_controller_proximity itself
|
||||
static int runtime_has_detached_controllers; // the runtime has XR_META_detached_controllers (detached_controllers.c)
|
||||
static int layer_debug; // diagnostics: layers, swapchains, session states, spaces, aim/grip, refresh rates
|
||||
static int input_diag; // diagnostics: controller profiles, bindings, missing functions, failing input calls
|
||||
static int stable_local; // keep every LOCAL space the app creates on the session-start origin
|
||||
@@ -424,6 +423,8 @@ XRAPI_ATTR XrResult XRAPI_CALL xrCreateInstance(const XrInstanceCreateInfo *info
|
||||
for (uint32_t j = 0; available && j < available_count; ++j)
|
||||
if (!strcmp(available[j].extensionName, "XR_FB_composition_layer_image_layout")) runtime_has_image_layout = 1;
|
||||
else if (!strcmp(available[j].extensionName, "XR_FB_touch_controller_proximity")) runtime_has_proximity = 1;
|
||||
else if (!strcmp(available[j].extensionName, "XR_META_detached_controllers"))
|
||||
runtime_has_detached_controllers = 1;
|
||||
// Layer types are only valid if their extension ends up enabled; some apps submit them regardless.
|
||||
int has_equirect = 0, has_equirect2 = 0, has_cylinder = 0, has_cube = 0;
|
||||
for (uint32_t i = 0; i < kept; ++i) {
|
||||
@@ -2006,7 +2007,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetCurrentInteractionProfile(XrSession session,
|
||||
PFN_xrGetCurrentInteractionProfile fn =
|
||||
(PFN_xrGetCurrentInteractionProfile)lookup(active_instance, "xrGetCurrentInteractionProfile");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrResult result = fn(session, user, state);
|
||||
XrResult result = detached_current_profile(user, state, fn(session, user, state));
|
||||
if (input_diag && XR_SUCCEEDED(result) && state) input_diag_current_profile(user, state->interactionProfile);
|
||||
if (layer_debug && active_instance) { // which device each hand has, as the runtime reports it
|
||||
PFN_xrPathToString str = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
|
||||
@@ -2020,16 +2021,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetCurrentInteractionProfile(XrSession session,
|
||||
static int logged;
|
||||
if (logged++ < 40) LOG("layer_debug: interaction profile of %s: %s (result %d)", who, what, result);
|
||||
}
|
||||
if (!controller_fix || XR_FAILED(result) || !state || !state->interactionProfile || !active_instance)
|
||||
return result;
|
||||
PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
|
||||
PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(active_instance, "xrStringToPath");
|
||||
char name[XR_MAX_PATH_LENGTH];
|
||||
uint32_t size = 0;
|
||||
if (to_string && to_path &&
|
||||
XR_SUCCEEDED(to_string(active_instance, state->interactionProfile, sizeof(name), &size, name)) &&
|
||||
(strstr(name, "/valve/") || strstr(name, "/khr/generic_controller")))
|
||||
to_path(active_instance, "/interaction_profiles/oculus/touch_controller", &state->interactionProfile);
|
||||
if (XR_SUCCEEDED(result) && state) current_profile_fix(user, state);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2179,9 +2171,6 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
if (layer_debug || refresh_rate > 0) {
|
||||
HOOK_AS(xrRequestDisplayRefreshRateFB, hook_request_refresh_rate)
|
||||
}
|
||||
// The Frame's runtime has no XR_FB_display_refresh_rate, but OVRPort's dispatcher offers it to the game and answers
|
||||
// 0 Hz without it: the measured rate instead (the runtime's own answer when it has one).
|
||||
if (instance != XR_NULL_HANDLE) HOOK_AS(xrGetDisplayRefreshRateFB, hook_get_refresh_rate)
|
||||
if (surface_emul) HOOK_AS(xrCreateSwapchainAndroidSurfaceKHR, hook_xrCreateSwapchainAndroidSurfaceKHR)
|
||||
if (eye_debug || release_wait) HOOK_AS(xrReleaseSwapchainImage, eye_hook_xrReleaseSwapchainImage)
|
||||
if (layer_debug || cube_standin) { // counters + cube stand-ins; every other wait/release hook above takes precedence
|
||||
@@ -2204,7 +2193,7 @@ XRAPI_ATTR XrResult XRAPI_CALL xrGetInstanceProcAddr(XrInstance instance, const
|
||||
if (layer_debug) HOOK_AS(xrGetActionStateBoolean, hook_xrGetActionStateBoolean)
|
||||
if (layer_debug || aim_correction_on() || profile_remap || input_diag || proximity_emul)
|
||||
HOOK_AS(xrSuggestInteractionProfileBindings, hook_xrSuggestInteractionProfileBindings)
|
||||
if (proximity_emul) HOOK_AS(xrCreateAction, hook_xrCreateAction)
|
||||
if (proximity_emul || profile_remap) HOOK_AS(xrCreateAction, hook_xrCreateAction)
|
||||
if (layer_debug || aim_correction_on()) {
|
||||
HOOK_AS(xrCreateActionSpace, hook_xrCreateActionSpace)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Included by session_fixes.c (after TOUCH_PROFILE and app_suggested_touch): what xrGetCurrentInteractionProfile
|
||||
// tells the game about each hand. Uses controller_fix, profile_remap, active_instance, lookup and LOG.
|
||||
// * controller_fix: SteamVR's own profiles (valve/..., khr/generic_controller) are reported as oculus/touch_controller,
|
||||
// the profile Quest games have bindings for.
|
||||
// * profile_remap: when the game suggested bindings only for one of Meta's newer profiles (Touch Plus/Pro) and
|
||||
// hook_xrSuggestInteractionProfileBindings suggested them again for Touch, the runtime reports
|
||||
// oculus/touch_controller, a profile the game never suggested. Unity's OpenXR plugin creates its input devices by
|
||||
// the reported profile, finds none for Touch and leaves the controllers unconnected: hands lying at the play-space
|
||||
// origin on the floor, no buttons (e.g. Hotel Infinity, GitHub #327). Such a hand is reported with the game's own
|
||||
// profile instead (the last one remapped: the Touch bindings the runtime uses are that profile's).
|
||||
|
||||
static char remapped_profile[XR_MAX_PATH_LENGTH]; // set by hook_xrSuggestInteractionProfileBindings
|
||||
|
||||
static void current_profile_fix(XrPath user, XrInteractionProfileState *state) {
|
||||
if ((!controller_fix && !profile_remap) || !state->interactionProfile || !active_instance) return;
|
||||
PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(active_instance, "xrPathToString");
|
||||
PFN_xrStringToPath to_path = (PFN_xrStringToPath)lookup(active_instance, "xrStringToPath");
|
||||
char name[XR_MAX_PATH_LENGTH];
|
||||
uint32_t size = 0;
|
||||
if (!to_string || !to_path ||
|
||||
XR_FAILED(to_string(active_instance, state->interactionProfile, sizeof(name), &size, name)))
|
||||
return;
|
||||
int touch = !strcmp(name, TOUCH_PROFILE);
|
||||
if (controller_fix && (strstr(name, "/valve/") || strstr(name, "/khr/generic_controller")) &&
|
||||
XR_SUCCEEDED(to_path(active_instance, TOUCH_PROFILE, &state->interactionProfile)))
|
||||
touch = 1;
|
||||
if (!touch || !profile_remap || !*remapped_profile || app_suggested_touch ||
|
||||
XR_FAILED(to_path(active_instance, remapped_profile, &state->interactionProfile)))
|
||||
return;
|
||||
static int logged;
|
||||
if (logged++ < 4) {
|
||||
char who[XR_MAX_PATH_LENGTH] = "?";
|
||||
to_string(active_instance, user, sizeof(who), &size, who);
|
||||
LOG("profile_remap: %s reported as %s (the app's profile) instead of oculus/touch_controller", who,
|
||||
remapped_profile + strlen("/interaction_profiles/"));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Display refresh rate for xrGetDisplayRefreshRateFB when the runtime doesn't give one (session_fixes.c). Pure, so the
|
||||
// host test (tests/fixtures/src/refresh_rate_test.c) can check it.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// The rate the frame timing shows (1 / predictedDisplayPeriod of the last xrWaitFrame, whole Hz), else the
|
||||
// refresh_rate setting, else 72 Hz (the Frame's rate for Android apps).
|
||||
static inline float fb_refresh_rate(int64_t display_period_ns, float setting) {
|
||||
if (display_period_ns > 0) {
|
||||
double hz = 1e9 / (double)display_period_ns;
|
||||
if (hz >= 30.0 && hz <= 240.0) return (float)(int)(hz + 0.5);
|
||||
}
|
||||
return setting > 0 ? setting : 72.0f;
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
// * aim_pitch / aim_yaw / aim_forward: correct the aim pose of controllers (pointer rays) by a fixed offset;
|
||||
// * refresh_rate: display refresh rate requested at session start (and instead of the app's own requests).
|
||||
#include "layer_math.h"
|
||||
#include "refresh_rate.h"
|
||||
|
||||
static void emul_on_begin_session(const XrSessionBeginInfo *info); // layer_emul_gl.c
|
||||
|
||||
@@ -297,6 +296,8 @@ static const char *const TOUCH_LEFT[] = {"input/x/click", "input/x/touch", "inpu
|
||||
static const char *const TOUCH_RIGHT[] = {"input/a/click", "input/a/touch", "input/b/click", "input/b/touch",
|
||||
"input/system/click"};
|
||||
static int app_suggested_touch;
|
||||
#include "profile_report.c" // remapped_profile, current_profile_fix (xrGetCurrentInteractionProfile)
|
||||
#include "detached_controllers.c" // detached controllers' paths on a runtime without them
|
||||
|
||||
static int in_list(const char *s, const char *const *list, size_t n) {
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
@@ -333,8 +334,13 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrCreateAction(XrActionSet set, const
|
||||
XrAction *action) {
|
||||
PFN_xrCreateAction fn = (PFN_xrCreateAction)lookup(active_instance, "xrCreateAction");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrResult r = fn(set, info, action);
|
||||
if (XR_SUCCEEDED(r) && info && action) { // OVRPlugin's action names
|
||||
XrActionCreateInfo copy;
|
||||
XrPath *kept = info && info->countSubactionPaths ? calloc(info->countSubactionPaths, sizeof(*kept)) : NULL;
|
||||
XrResult r = fn(set, detached_strip_action(info, ©, kept), action);
|
||||
free(kept);
|
||||
if (XR_FAILED(r) && input_diag) // this hook replaces input_diag's wrapper of xrCreateAction
|
||||
diag_call_failed("xrCreateAction", r, diag_name_detail(info ? info->actionName : NULL));
|
||||
if (proximity_emul && XR_SUCCEEDED(r) && info && action) { // OVRPlugin's action names
|
||||
if (!strcmp(info->actionName, "hand_thumb_proximity")) thumb_proximity_action = *action;
|
||||
else if (!strcmp(info->actionName, "hand_trigger_proximity")) trigger_proximity_action = *action;
|
||||
else return r;
|
||||
@@ -430,11 +436,26 @@ static XrResult remap_to_touch(XrInstance instance, PFN_xrSuggestInteractionProf
|
||||
return r;
|
||||
}
|
||||
|
||||
static XrResult suggest_bindings(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn,
|
||||
const XrInteractionProfileSuggestedBinding *suggested);
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrSuggestInteractionProfileBindings(XrInstance instance,
|
||||
const XrInteractionProfileSuggestedBinding *suggested) {
|
||||
PFN_xrSuggestInteractionProfileBindings fn =
|
||||
(PFN_xrSuggestInteractionProfileBindings)lookup(instance, "xrSuggestInteractionProfileBindings");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
XrInteractionProfileSuggestedBinding copy;
|
||||
XrActionSuggestedBinding *kept = suggested && suggested->countSuggestedBindings
|
||||
? calloc(suggested->countSuggestedBindings, sizeof(*kept)) : NULL;
|
||||
const XrInteractionProfileSuggestedBinding *use = detached_strip_bindings(suggested, ©, kept);
|
||||
// a profile with only detached-controller bindings: nothing the runtime could bind
|
||||
XrResult r = use != suggested && !use->countSuggestedBindings ? XR_SUCCESS : suggest_bindings(instance, fn, use);
|
||||
free(kept);
|
||||
return r;
|
||||
}
|
||||
|
||||
static XrResult suggest_bindings(XrInstance instance, PFN_xrSuggestInteractionProfileBindings fn,
|
||||
const XrInteractionProfileSuggestedBinding *suggested) {
|
||||
PFN_xrPathToString to_string = (PFN_xrPathToString)lookup(instance, "xrPathToString");
|
||||
if (suggested && to_string && (layer_debug || aim_correction_on()))
|
||||
for (uint32_t i = 0; i < suggested->countSuggestedBindings; ++i) {
|
||||
@@ -457,6 +478,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_xrSuggestInteractionProfileBindings(X
|
||||
}
|
||||
if (r != XR_ERROR_PATH_UNSUPPORTED || app_suggested_touch || !IN(profile, NEWER_TOUCH)) return r;
|
||||
XrResult remapped = remap_to_touch(instance, fn, to_string, suggested, profile);
|
||||
if (XR_SUCCEEDED(remapped)) snprintf(remapped_profile, sizeof(remapped_profile), "%s", profile);
|
||||
return XR_SUCCEEDED(remapped) ? XR_SUCCESS : r;
|
||||
}
|
||||
|
||||
@@ -553,26 +575,6 @@ static XRAPI_ATTR XrResult XRAPI_CALL hook_request_refresh_rate(XrSession sessio
|
||||
return XR_FAILED(result) && refresh_rate > 0 ? XR_SUCCESS : result;
|
||||
}
|
||||
|
||||
// Always on: the Frame's runtime has no XR_FB_display_refresh_rate; OVRPort's dispatcher still offers the extension and
|
||||
// its xrGetDisplayRefreshRateFB then answers 0 Hz. idTech 4 VR ports (Team Beef's Doom3Quest engine: PreyVR, GitHub
|
||||
// #306) run their game clock at 1000 / refresh ms per tic: with 0 Hz the tic length is 0, the async thread spins and
|
||||
// the frame/GUI time never advances (main menu stuck at its first animation step: black, only the cursor). A rate the
|
||||
// runtime doesn't give is replaced by the one the frame timing shows (refresh_rate.h).
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_get_refresh_rate(XrSession session, float *rate) {
|
||||
if (!rate) return XR_ERROR_VALIDATION_FAILURE;
|
||||
PFN_xrGetDisplayRefreshRateFB fn = (PFN_xrGetDisplayRefreshRateFB)lookup(active_instance, "xrGetDisplayRefreshRateFB");
|
||||
XrResult result = fn ? fn(session, rate) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
if ((XR_SUCCEEDED(result) && *rate > 0.0f) || result == XR_ERROR_HANDLE_INVALID) return result;
|
||||
float runtime = XR_SUCCEEDED(result) ? *rate : 0.0f;
|
||||
*rate = fb_refresh_rate((int64_t)last_display_period, refresh_rate);
|
||||
static int logs;
|
||||
if (logs < 4) {
|
||||
++logs;
|
||||
LOG("refresh rate: runtime reported %.1f Hz (result %d), reporting %.1f Hz", runtime, result, *rate);
|
||||
}
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL hook_xrBeginSession(XrSession session, const XrSessionBeginInfo *info) {
|
||||
PFN_xrBeginSession fn = (PFN_xrBeginSession)lookup(active_instance, "xrBeginSession");
|
||||
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FramePort FrameBridge: eye-image snapshots for headless diagnostics (setting snapshot=<seconds>, GLES games only).
|
||||
// Every <seconds>, at xrEndFrame, the left-eye image the game just submitted (projection layer, view 0: its swapchain
|
||||
// image and array layer; without a projection layer the first quad/cylinder layer's) is read back on the game's own GL context and written at a quarter of its size as
|
||||
// image and array layer) is read back on the game's own GL context and written at a quarter of its size as
|
||||
// /sdcard/Android/data/<package>/files/fb_snap_<n>.ppm (n = 0-7, the oldest is overwritten). Nobody wears the headset
|
||||
// in a launch test, so the headset view stays black: this shows what the game itself draws (e.g. which screen it is
|
||||
// stuck on). The game's framebuffer, pack-buffer and pack-alignment bindings are restored afterwards.
|
||||
@@ -118,27 +118,18 @@ static void snap_end_frame(const XrFrameEndInfo *info) {
|
||||
int64_t t = (int64_t)now.tv_sec * 1000000000ll + now.tv_nsec;
|
||||
if (t < next_ns) return;
|
||||
next_ns = t + (int64_t)snapshot * 1000000000ll;
|
||||
// The projection layer's left eye; a frame without one (e.g. a menu shown only as a quad or cylinder layer) gives
|
||||
// its first quad/cylinder layer's image instead.
|
||||
const XrSwapchainSubImage *sub = NULL;
|
||||
int type = 0;
|
||||
for (uint32_t i = 0; i < info->layerCount && !sub; ++i)
|
||||
const XrCompositionLayerProjection *proj = NULL;
|
||||
for (uint32_t i = 0; i < info->layerCount && !proj; ++i)
|
||||
if (info->layers[i] && info->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION &&
|
||||
((const XrCompositionLayerProjection *)info->layers[i])->viewCount)
|
||||
sub = &((const XrCompositionLayerProjection *)info->layers[i])->views[0].subImage, type = info->layers[i]->type;
|
||||
for (uint32_t i = 0; i < info->layerCount && !sub; ++i) {
|
||||
const XrCompositionLayerBaseHeader *layer = info->layers[i];
|
||||
if (layer && layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD)
|
||||
sub = &((const XrCompositionLayerQuad *)layer)->subImage, type = layer->type;
|
||||
else if (layer && layer->type == XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR)
|
||||
sub = &((const XrCompositionLayerCylinderKHR *)layer)->subImage, type = layer->type;
|
||||
}
|
||||
if (!sub || !snap_load_gl()) return;
|
||||
proj = (const XrCompositionLayerProjection *)info->layers[i];
|
||||
if (!proj || !snap_load_gl()) return;
|
||||
if (!s_eglGetCurrentContext()) {
|
||||
static int logged;
|
||||
if (!logged++) LOG("snapshot: no GL context current in xrEndFrame, no snapshot");
|
||||
return;
|
||||
}
|
||||
const XrSwapchainSubImage *sub = &proj->views[0].subImage;
|
||||
pthread_mutex_lock(&snap_lock);
|
||||
int i = snap_find(sub->swapchain, 0);
|
||||
GLuint tex = 0;
|
||||
@@ -176,21 +167,6 @@ static void snap_end_frame(const XrFrameEndInfo *info) {
|
||||
s_glDeleteFramebuffers(1, &fb);
|
||||
while (s_glGetError() != GL_NO_ERROR) {} // our calls must not leave errors for the game to find
|
||||
if (rgba) {
|
||||
// brightness of the submitted area: tells a black image from a drawn one without fetching the file
|
||||
uint64_t sum = 0, n = 0;
|
||||
unsigned max = 0;
|
||||
const XrRect2Di *r = &sub->imageRect;
|
||||
for (int32_t y = r->offset.y; y < r->offset.y + r->extent.height; y += 4)
|
||||
for (int32_t x = r->offset.x; x < r->offset.x + r->extent.width; x += 4) {
|
||||
if (x < 0 || y < 0 || (uint32_t)x >= w || (uint32_t)y >= h) continue;
|
||||
const unsigned char *p = rgba + ((size_t)y * w + (size_t)x) * 4;
|
||||
unsigned v = p[0] > p[1] ? (p[0] > p[2] ? p[0] : p[2]) : (p[1] > p[2] ? p[1] : p[2]);
|
||||
sum += v, ++n;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
LOG("snapshot: layer type=%d image %u array=%u rect=%d,%d %dx%d of %ux%u: mean %.1f max %u",
|
||||
type, tex, sub->imageArrayIndex, r->offset.x, r->offset.y, r->extent.width, r->extent.height, w, h,
|
||||
n ? (double)sum / (double)n : 0.0, max);
|
||||
snap_write(rgba, w, h, taken++ % 8);
|
||||
free(rgba);
|
||||
}
|
||||
|
||||
+6
-277
@@ -16,8 +16,7 @@
|
||||
// block bindings and default-block uniform values (copied before every such draw), then the original is bound again.
|
||||
//
|
||||
// Logcat tag GLMV. Setting gl_mv_debug=1 (libframe_settings.so / framebridge.conf / FRAMEBRIDGE_CONFIG, as the GL
|
||||
// shim): glGetError after every twin draw, and per-5-s counters. gl_mv_debug=2 (trace) also checks every draw and
|
||||
// logs the state of the first draws of each 5 s window, failed texture uploads/copies and shader compiles.
|
||||
// shim): glGetError after every twin draw, and per-5-s counters.
|
||||
#define _GNU_SOURCE
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
@@ -112,27 +111,7 @@
|
||||
X(void, UniformMatrix2x4fv, (GLint, GLsizei, GLboolean, const GLfloat *)) \
|
||||
X(void, UniformMatrix4x2fv, (GLint, GLsizei, GLboolean, const GLfloat *)) \
|
||||
X(void, UniformMatrix3x4fv, (GLint, GLsizei, GLboolean, const GLfloat *)) \
|
||||
X(void, UniformMatrix4x3fv, (GLint, GLsizei, GLboolean, const GLfloat *)) \
|
||||
TRACE_LIST(X)
|
||||
|
||||
// gl_mv_debug=2 (trace): the draw state and texture/shader calls behind black output
|
||||
#define TRACE_LIST(X) \
|
||||
X(GLboolean, IsEnabled, (GLenum)) \
|
||||
X(void, GetBooleanv, (GLenum, GLboolean *)) \
|
||||
X(GLenum, CheckFramebufferStatus, (GLenum)) \
|
||||
X(void, GetTexLevelParameteriv, (GLenum, GLint, GLenum, GLint *)) \
|
||||
X(void, GetTexParameteriv, (GLenum, GLenum, GLint *)) \
|
||||
X(void, Clear, (GLbitfield)) \
|
||||
X(void, Viewport, (GLint, GLint, GLsizei, GLsizei)) \
|
||||
X(void, TexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void *)) \
|
||||
X(void, TexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void *)) \
|
||||
X(void, CompressedTexImage2D, (GLenum, GLint, GLenum, GLsizei, GLsizei, GLint, GLsizei, const void *)) \
|
||||
X(void, TexStorage2D, (GLenum, GLsizei, GLenum, GLsizei, GLsizei)) \
|
||||
X(void, CopyTexImage2D, (GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint)) \
|
||||
X(void, CopyTexSubImage2D, (GLenum, GLint, GLint, GLint, GLint, GLint, GLsizei, GLsizei)) \
|
||||
X(void, GenerateMipmap, (GLenum)) \
|
||||
X(void, BindTexture, (GLenum, GLuint)) \
|
||||
X(void, TexParameteri, (GLenum, GLenum, GLint))
|
||||
X(void, UniformMatrix4x3fv, (GLint, GLsizei, GLboolean, const GLfloat *))
|
||||
|
||||
#define DECL(ret, name, args) static ret(*r_##name) args;
|
||||
REAL_LIST(DECL)
|
||||
@@ -536,147 +515,6 @@ static void stats(void) {
|
||||
twin_draws = mv_draws = uniform_sets = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- trace (gl_mv_debug=2, diagnostics)
|
||||
// Every draw is checked with glGetError (this also consumes errors the game would read), counted per draw
|
||||
// framebuffer, and the first draws of every 5 s window are logged with the state that decides whether they write
|
||||
// anything: framebuffer status and views, program, viewport/scissor, colour/depth/stencil/blend state and the texture
|
||||
// on the active unit. Texture uploads, copies and shader compiles that fail are logged too.
|
||||
#define TRACE_DETAIL 12 // detailed draws per window
|
||||
#define TRACE_WINDOWS 40 // windows with detail
|
||||
#define NTFB 16
|
||||
struct tfb { GLint fb; unsigned long draws, errors, clears; };
|
||||
static int trace_detail, trace_windows;
|
||||
static unsigned long trace_draws, trace_errors, trace_clears;
|
||||
static GLenum trace_first_error;
|
||||
static struct tfb tfb[NTFB];
|
||||
static struct timespec trace_last;
|
||||
static pthread_mutex_t trace_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static struct tfb *tfb_slot(GLint fb) {
|
||||
for (int i = 0; i < NTFB; ++i)
|
||||
if (tfb[i].fb == fb && (tfb[i].draws || tfb[i].clears)) return &tfb[i];
|
||||
for (int i = 0; i < NTFB; ++i)
|
||||
if (!tfb[i].draws && !tfb[i].clears) { tfb[i].fb = fb; return &tfb[i]; }
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void trace_window(void) { // under trace_lock
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
if (!trace_last.tv_sec) trace_last = now;
|
||||
if (now.tv_sec - trace_last.tv_sec < 5) return;
|
||||
trace_last = now;
|
||||
char buf[700];
|
||||
int n = 0;
|
||||
for (int i = 0; i < NTFB && n < (int)sizeof(buf) - 60; ++i)
|
||||
if (tfb[i].draws || tfb[i].clears)
|
||||
n += snprintf(buf + n, sizeof(buf) - n, " fb%d:d%lu/e%lu/c%lu", tfb[i].fb, tfb[i].draws, tfb[i].errors,
|
||||
tfb[i].clears);
|
||||
LOG("trace 5s: %lu draws, %lu with a GL error (first 0x%x), %lu clears;%s", trace_draws, trace_errors,
|
||||
trace_first_error, trace_clears, n ? buf : " none");
|
||||
{ // idTech 4 engines (Doom3Quest, PreyVR) export their clocks; GUI animations run on com_frameTime, so one that
|
||||
// stands still leaves menus at their first step (PreyVR, GitHub #306: 0 Hz refresh rate -> 0 ms per tic)
|
||||
static int *frame_time, *tic_number, *frame_number, looked;
|
||||
if (!looked) {
|
||||
looked = 1;
|
||||
frame_time = dlsym(RTLD_DEFAULT, "com_frameTime");
|
||||
tic_number = dlsym(RTLD_DEFAULT, "com_ticNumber");
|
||||
frame_number = dlsym(RTLD_DEFAULT, "com_frameNumber");
|
||||
}
|
||||
if (frame_time && tic_number && frame_number)
|
||||
LOG("trace clocks: com_frameTime=%d com_ticNumber=%d com_frameNumber=%d", *frame_time, *tic_number,
|
||||
*frame_number);
|
||||
}
|
||||
memset(tfb, 0, sizeof(tfb));
|
||||
trace_draws = trace_errors = trace_clears = 0;
|
||||
trace_first_error = 0;
|
||||
if (trace_windows < TRACE_WINDOWS) {
|
||||
++trace_windows;
|
||||
trace_detail = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void trace_state(const char *what, GLenum mode, GLsizei count, GLenum err) {
|
||||
GLint fb = 0, prog = 0, vp[4] = {0}, sc[4] = {0}, unit = 0, tex = 0, dfunc = 0, bsrc = 0, bdst = 0, bsrca = 0,
|
||||
bdsta = 0, cullmode = 0;
|
||||
GLboolean cmask[4] = {0}, dmask = 0;
|
||||
r_GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb);
|
||||
r_GetIntegerv(GL_CURRENT_PROGRAM, &prog);
|
||||
r_GetIntegerv(GL_VIEWPORT, vp);
|
||||
r_GetIntegerv(GL_SCISSOR_BOX, sc);
|
||||
r_GetIntegerv(GL_ACTIVE_TEXTURE, &unit);
|
||||
r_GetIntegerv(GL_DEPTH_FUNC, &dfunc);
|
||||
r_GetIntegerv(GL_BLEND_SRC_RGB, &bsrc);
|
||||
r_GetIntegerv(GL_BLEND_DST_RGB, &bdst);
|
||||
r_GetIntegerv(GL_BLEND_SRC_ALPHA, &bsrca);
|
||||
r_GetIntegerv(GL_BLEND_DST_ALPHA, &bdsta);
|
||||
r_GetIntegerv(GL_CULL_FACE_MODE, &cullmode);
|
||||
r_GetBooleanv(GL_COLOR_WRITEMASK, cmask);
|
||||
r_GetBooleanv(GL_DEPTH_WRITEMASK, &dmask);
|
||||
GLenum status = r_CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
|
||||
GLint linked = 0;
|
||||
if (prog) r_GetProgramiv((GLuint)prog, GL_LINK_STATUS, &linked);
|
||||
struct prog *p = prog ? find_prog((GLuint)prog, 0) : NULL;
|
||||
r_GetIntegerv(GL_TEXTURE_BINDING_2D, &tex); // (on the active unit)
|
||||
GLint tw = 0, th = 0, tfmt = 0, minf = 0, maxl = 0, imm = 0;
|
||||
if (tex) {
|
||||
r_GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw);
|
||||
r_GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th);
|
||||
r_GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &tfmt);
|
||||
r_GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &minf);
|
||||
r_GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, &maxl);
|
||||
r_GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &imm);
|
||||
}
|
||||
r_GetError(); // (the queries above must not count as the game's)
|
||||
LOG("trace %s(mode=0x%x n=%d) err=0x%x fb=%d status=0x%x views=%d prog=%d linked=%d mv=%d vp=%d,%d %dx%d "
|
||||
"scissor=%d %d,%d %dx%d cmask=%d%d%d%d blend=%d 0x%x/0x%x,0x%x/0x%x depth=%d f=0x%x w=%d stencil=%d cull=%d "
|
||||
"0x%x unit=0x%x tex2d=%d %dx%d fmt=0x%x min=0x%x maxl=%d imm=%d",
|
||||
what, mode, count, err, fb, status, draw_fb_views(), prog, linked, p ? p->multiview : -1, vp[0], vp[1], vp[2],
|
||||
vp[3], r_IsEnabled(GL_SCISSOR_TEST), sc[0], sc[1], sc[2], sc[3], cmask[0], cmask[1], cmask[2], cmask[3],
|
||||
r_IsEnabled(GL_BLEND), bsrc, bdst, bsrca, bdsta, r_IsEnabled(GL_DEPTH_TEST), dfunc, dmask,
|
||||
r_IsEnabled(GL_STENCIL_TEST), r_IsEnabled(GL_CULL_FACE), cullmode, unit, tex, tw, th, tfmt, minf, maxl, imm);
|
||||
}
|
||||
|
||||
static void trace_before(void) {
|
||||
if (debug >= 2) r_GetError();
|
||||
}
|
||||
|
||||
static void trace_after(const char *what, GLenum mode, GLsizei count) {
|
||||
if (debug < 2) return;
|
||||
GLenum err = r_GetError();
|
||||
GLint fb = 0;
|
||||
r_GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb);
|
||||
int detail;
|
||||
pthread_mutex_lock(&trace_lock);
|
||||
++trace_draws;
|
||||
if (err) {
|
||||
++trace_errors;
|
||||
if (!trace_first_error) trace_first_error = err;
|
||||
}
|
||||
struct tfb *e = tfb_slot(fb);
|
||||
if (e) {
|
||||
++e->draws;
|
||||
if (err) ++e->errors;
|
||||
}
|
||||
detail = trace_windows < TRACE_WINDOWS && trace_detail < TRACE_DETAIL;
|
||||
if (detail) ++trace_detail;
|
||||
trace_window();
|
||||
pthread_mutex_unlock(&trace_lock);
|
||||
if (detail) trace_state(what, mode, count, err);
|
||||
}
|
||||
|
||||
static int trace_logs;
|
||||
#define TRACE_CALL(fmt, ...) \
|
||||
do { \
|
||||
if (debug >= 2) { \
|
||||
GLenum e_ = r_GetError(); \
|
||||
if ((e_ || trace_logs < 40) && trace_logs < 400) { \
|
||||
++trace_logs; \
|
||||
LOG("trace " fmt " err=0x%x", __VA_ARGS__, e_); \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Before a draw: bind the twin when the program is multiview and the framebuffer has no views. 1 = twin bound.
|
||||
static int begin_draw(void) {
|
||||
struct prog *p = cur_prog;
|
||||
@@ -720,14 +558,6 @@ EXPORT void glLinkProgram(GLuint program) {
|
||||
INIT();
|
||||
note_program(program);
|
||||
r_LinkProgram(program);
|
||||
if (debug >= 2) {
|
||||
GLint ok = 0;
|
||||
r_GetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
char log[700] = {0};
|
||||
r_GetProgramInfoLog(program, sizeof(log) - 1, NULL, log);
|
||||
struct prog *p = find_prog(program, 0);
|
||||
LOG("trace glLinkProgram(%u) ok=%d multiview=%d log: %s", program, ok, p ? p->multiview : -1, log);
|
||||
}
|
||||
}
|
||||
|
||||
EXPORT void glDeleteProgram(GLuint program) {
|
||||
@@ -796,143 +626,43 @@ static void w_ftmsmv(GLenum target, GLenum attachment, GLuint texture, GLint lev
|
||||
#define DRAW(name, call) \
|
||||
INIT(); \
|
||||
if (!r_##name) return; \
|
||||
trace_before(); \
|
||||
if (begin_draw()) { \
|
||||
r_##name call; \
|
||||
end_draw("gl" #name); \
|
||||
} else { \
|
||||
r_##name call; \
|
||||
} \
|
||||
trace_after("gl" #name, call_mode, call_count);
|
||||
}
|
||||
|
||||
EXPORT void glDrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawArrays, (mode, first, count))
|
||||
}
|
||||
EXPORT void glDrawArrays(GLenum mode, GLint first, GLsizei count) { DRAW(DrawArrays, (mode, first, count)) }
|
||||
EXPORT void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void *indices) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawElements, (mode, count, type, indices))
|
||||
}
|
||||
EXPORT void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instances) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawArraysInstanced, (mode, first, count, instances))
|
||||
}
|
||||
EXPORT void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instances) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawElementsInstanced, (mode, count, type, indices, instances))
|
||||
}
|
||||
EXPORT void glDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void *indices) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawRangeElements, (mode, start, end, count, type, indices))
|
||||
}
|
||||
EXPORT void glDrawArraysIndirect(GLenum mode, const void *indirect) {
|
||||
GLenum call_mode = mode; GLsizei call_count = -1;
|
||||
DRAW(DrawArraysIndirect, (mode, indirect))
|
||||
}
|
||||
EXPORT void glDrawArraysIndirect(GLenum mode, const void *indirect) { DRAW(DrawArraysIndirect, (mode, indirect)) }
|
||||
EXPORT void glDrawElementsIndirect(GLenum mode, GLenum type, const void *indirect) {
|
||||
GLenum call_mode = mode; GLsizei call_count = -1;
|
||||
DRAW(DrawElementsIndirect, (mode, type, indirect))
|
||||
}
|
||||
EXPORT void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void *indices, GLint base) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawElementsBaseVertex, (mode, count, type, indices, base))
|
||||
}
|
||||
EXPORT void glDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void *indices, GLint base) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawRangeElementsBaseVertex, (mode, start, end, count, type, indices, base))
|
||||
}
|
||||
EXPORT void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void *indices,
|
||||
GLsizei instances, GLint base) {
|
||||
GLenum call_mode = mode; GLsizei call_count = count;
|
||||
DRAW(DrawElementsInstancedBaseVertex, (mode, count, type, indices, instances, base))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- traced calls (pass-through unless gl_mv_debug=2)
|
||||
EXPORT void glClear(GLbitfield mask) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_Clear(mask);
|
||||
if (debug >= 2) {
|
||||
GLenum err = r_GetError();
|
||||
GLint fb = 0;
|
||||
r_GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb);
|
||||
pthread_mutex_lock(&trace_lock);
|
||||
++trace_clears;
|
||||
struct tfb *e = tfb_slot(fb);
|
||||
if (e) ++e->clears;
|
||||
pthread_mutex_unlock(&trace_lock);
|
||||
if (err) LOG("trace glClear(0x%x) fb=%d err=0x%x", mask, fb, err);
|
||||
}
|
||||
}
|
||||
EXPORT void glTexImage2D(GLenum t, GLint l, GLint ifmt, GLsizei w, GLsizei h, GLint b, GLenum f, GLenum ty,
|
||||
const void *px) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_TexImage2D(t, l, ifmt, w, h, b, f, ty, px);
|
||||
GLint tex = 0;
|
||||
if (debug >= 2) r_GetIntegerv(GL_TEXTURE_BINDING_2D, &tex);
|
||||
TRACE_CALL("glTexImage2D(target=0x%x tex=%d level=%d ifmt=0x%x %dx%d fmt=0x%x type=0x%x data=%d)", t, tex, l, ifmt,
|
||||
w, h, f, ty, px != NULL);
|
||||
}
|
||||
EXPORT void glTexSubImage2D(GLenum t, GLint l, GLint x, GLint y, GLsizei w, GLsizei h, GLenum f, GLenum ty,
|
||||
const void *px) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_TexSubImage2D(t, l, x, y, w, h, f, ty, px);
|
||||
TRACE_CALL("glTexSubImage2D(target=0x%x level=%d %d,%d %dx%d fmt=0x%x type=0x%x)", t, l, x, y, w, h, f, ty);
|
||||
}
|
||||
EXPORT void glCompressedTexImage2D(GLenum t, GLint l, GLenum ifmt, GLsizei w, GLsizei h, GLint b, GLsizei size,
|
||||
const void *data) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_CompressedTexImage2D(t, l, ifmt, w, h, b, size, data);
|
||||
TRACE_CALL("glCompressedTexImage2D(target=0x%x level=%d ifmt=0x%x %dx%d size=%d)", t, l, ifmt, w, h, size);
|
||||
}
|
||||
EXPORT void glTexStorage2D(GLenum t, GLsizei levels, GLenum ifmt, GLsizei w, GLsizei h) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_TexStorage2D(t, levels, ifmt, w, h);
|
||||
TRACE_CALL("glTexStorage2D(target=0x%x levels=%d ifmt=0x%x %dx%d)", t, levels, ifmt, w, h);
|
||||
}
|
||||
EXPORT void glCopyTexImage2D(GLenum t, GLint l, GLenum ifmt, GLint x, GLint y, GLsizei w, GLsizei h, GLint b) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_CopyTexImage2D(t, l, ifmt, x, y, w, h, b);
|
||||
GLint rfb = 0;
|
||||
if (debug >= 2) r_GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &rfb);
|
||||
TRACE_CALL("glCopyTexImage2D(target=0x%x level=%d ifmt=0x%x %d,%d %dx%d read fb=%d)", t, l, ifmt, x, y, w, h, rfb);
|
||||
}
|
||||
EXPORT void glCopyTexSubImage2D(GLenum t, GLint l, GLint xo, GLint yo, GLint x, GLint y, GLsizei w, GLsizei h) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_CopyTexSubImage2D(t, l, xo, yo, x, y, w, h);
|
||||
GLint rfb = 0;
|
||||
if (debug >= 2) r_GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &rfb);
|
||||
TRACE_CALL("glCopyTexSubImage2D(target=0x%x level=%d %d,%d from %d,%d %dx%d read fb=%d)", t, l, xo, yo, x, y, w, h,
|
||||
rfb);
|
||||
}
|
||||
EXPORT void glGenerateMipmap(GLenum t) {
|
||||
INIT();
|
||||
if (debug >= 2) r_GetError();
|
||||
r_GenerateMipmap(t);
|
||||
TRACE_CALL("glGenerateMipmap(target=0x%x)", t);
|
||||
}
|
||||
EXPORT void glCompileShader(GLuint sh) {
|
||||
INIT();
|
||||
r_CompileShader(sh);
|
||||
if (debug < 2) return;
|
||||
GLint ok = 0;
|
||||
r_GetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
||||
char log[700] = {0};
|
||||
r_GetShaderInfoLog(sh, sizeof(log) - 1, NULL, log);
|
||||
if (!ok || (log[0] && trace_logs < 400)) {
|
||||
++trace_logs;
|
||||
LOG("trace glCompileShader(%u) ok=%d log: %s", sh, ok, log);
|
||||
}
|
||||
}
|
||||
|
||||
// A context made current on this thread (engines with a render thread move theirs between threads, e.g. Doom3Quest's
|
||||
// GLimp_ActivateContext): what this thread knew about the bound program and framebuffer may be another context's.
|
||||
EXPORT EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
@@ -968,8 +698,7 @@ EXPORT __eglMustCastToProperFunctionPointerType eglGetProcAddress(const char *na
|
||||
OWN(glFramebufferTexture) OWN(glDrawBuffers) OWN(glDrawArrays) OWN(glDrawElements) OWN(glDrawArraysInstanced)
|
||||
OWN(glDrawElementsInstanced) OWN(glDrawRangeElements) OWN(glDrawArraysIndirect) OWN(glDrawElementsIndirect)
|
||||
OWN(glDrawElementsBaseVertex) OWN(glDrawRangeElementsBaseVertex) OWN(glDrawElementsInstancedBaseVertex)
|
||||
OWN(eglMakeCurrent) OWN(glClear) OWN(glTexImage2D) OWN(glTexSubImage2D) OWN(glCompressedTexImage2D)
|
||||
OWN(glTexStorage2D) OWN(glCopyTexImage2D) OWN(glCopyTexSubImage2D) OWN(glGenerateMipmap) OWN(glCompileShader)
|
||||
OWN(eglMakeCurrent)
|
||||
#undef OWN
|
||||
return fn;
|
||||
}
|
||||
@@ -173,7 +173,7 @@
|
||||
"Appearance": "",
|
||||
"AppImage": "",
|
||||
"Apply with Update on Frame.": "",
|
||||
"Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead (components Touch lacks are dropped). Without it such apps get no controller input.": "",
|
||||
"Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead (components Touch lacks are dropped), and the runtime's answer for each hand is reported to them as their own profile (Unity's OpenXR input only connects controllers of a profile it knows). Meta's detached-controller paths (XR_META_detached_controllers, which the Frame lacks) are left out of the app's actions and bindings: one such path made Unity attach no controller input at all. Without it such apps get no controller input.": "",
|
||||
"Artwork for {get}": "",
|
||||
"Artwork updated ({src}). The Frame's Steam library still shows the old art.": "",
|
||||
"Artwork updated for {get}": "",
|
||||
@@ -377,7 +377,7 @@
|
||||
"dev build": "",
|
||||
"Dev builds are early, less tested versions and may have bugs. You still get the next release as a normal update (you have {version}).": "",
|
||||
"Developer Mode lets FramePort find and connect to the Frame; the setup turns it on for you. With it on, you can also open Settings → Developer → Pair new host on the Frame and approve FramePort there.": "",
|
||||
"Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log). 2 (frameport settings) also checks every draw and logs the draw state, failed texture uploads and shader compiles and, for idTech 4 engines, the game clock.": "",
|
||||
"Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview shader and logs counters every 5 seconds (tag GLMV in launch.log).": "",
|
||||
"Diagnostics: removes layer color fades. Some fades no longer show.": "",
|
||||
"Diagnostics: the Vulkan shim (frame.vk_sanitize) adds Khronos' validation layer to the game's instance; its findings go to launch.log. The layer library (libVkLayer_khronos_validation.so) must be in the APK.": "",
|
||||
"Disable application space warp if used": "",
|
||||
|
||||
@@ -106,7 +106,10 @@ SETTINGS = [
|
||||
("profile_remap", "int", 1, "Newer Touch profiles as Touch",
|
||||
"Apps that suggest controller bindings only for Meta's newer profiles (Touch Plus, Touch Pro), which the Frame's "
|
||||
"runtime rejects (XR_ERROR_PATH_UNSUPPORTED), get the same bindings as oculus/touch_controller instead "
|
||||
"(components Touch lacks are dropped). Without it such apps get no controller input."),
|
||||
"(components Touch lacks are dropped), and the runtime's answer for each hand is reported to them as their own "
|
||||
"profile (Unity's OpenXR input only connects controllers of a profile it knows). Meta's detached-controller paths "
|
||||
"(XR_META_detached_controllers, which the Frame lacks) are left out of the app's actions and bindings: one such "
|
||||
"path made Unity attach no controller input at all. Without it such apps get no controller input."),
|
||||
("haptic_scale", "float", 1.0, "Vibration strength",
|
||||
"Scales every controller vibration the game asks for (0-1): plain vibrations, Meta's amplitude envelopes and "
|
||||
"PCM buffers. For games whose vibrations feel much stronger than on a Quest."),
|
||||
@@ -215,9 +218,7 @@ SETTINGS = [
|
||||
"findings go to launch.log. The layer library (libVkLayer_khronos_validation.so) must be in the APK."),
|
||||
("gl_mv_debug", "int", 0, "Multiview interposer: diagnostics",
|
||||
"Diagnostics for frame.gl_multiview_fbo: checks for GL errors after every single-view draw of a multiview "
|
||||
"shader and logs counters every 5 seconds (tag GLMV in launch.log). 2 (frameport settings) also checks every "
|
||||
"draw and logs the draw state, failed texture uploads and shader compiles and, for idTech 4 engines, the game "
|
||||
"clock."),
|
||||
"shader and logs counters every 5 seconds (tag GLMV in launch.log)."),
|
||||
("gl_hide_msrtt", "int", 1, "GL shim: hide multisampled render-to-texture",
|
||||
"GL shim only: hide GL_EXT_multisampled_render_to_texture(2) (Zink crashes rendering Unity's runtime MSAA eye "
|
||||
"buffer through it, for example The Room VR)."),
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// FrameBridge's xrGetDisplayRefreshRateFB fallback (native/adapter/refresh_rate.h, GitHub #306).
|
||||
#include <assert.h>
|
||||
#include "refresh_rate.h"
|
||||
|
||||
int main(void) {
|
||||
assert(fb_refresh_rate(13888888, 0) == 72.0f); // 72 Hz frame timing
|
||||
assert(fb_refresh_rate(11111111, 0) == 90.0f); // 90 Hz
|
||||
assert(fb_refresh_rate(8333333, 0) == 120.0f); // 120 Hz
|
||||
assert(fb_refresh_rate(6944444, 0) == 144.0f); // 144 Hz
|
||||
assert(fb_refresh_rate(0, 0) == 72.0f); // no xrWaitFrame yet: never 0 Hz (PreyVR: 1000 / 0 ms per tic)
|
||||
assert(fb_refresh_rate(0, 90.0f) == 90.0f); // ... else the refresh_rate setting
|
||||
assert(fb_refresh_rate(-5, 0) == 72.0f);
|
||||
assert(fb_refresh_rate(1000, 0) == 72.0f); // nonsense periods (1 MHz, 1 Hz) aren't believed
|
||||
assert(fb_refresh_rate(1000000000, 80.0f) == 80.0f);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
"""What FrameBridge's xrGetCurrentInteractionProfile reports (native/adapter/profile_report.c): SteamVR's profiles as
|
||||
Touch (controller_fix) and, after profile_remap moved a game's Touch Plus/Pro bindings to Touch, the game's own profile
|
||||
(GitHub #327: Unity's OpenXR input found no device for oculus/touch_controller, the hands lay on the floor). Compiled
|
||||
for this host with a small harness standing in for the adapter and the runtime.
|
||||
Opt-in (FRAMEPORT_NATIVE_TESTS=1, -m native): needs the OpenXR headers in native/.cache (from native/build.py)."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INC = next(iter((ROOT / "native/.cache").glob("openxr-*")), None)
|
||||
CC = shutil.which("cc") or shutil.which("clang")
|
||||
pytestmark = [pytest.mark.native, pytest.mark.skipif(
|
||||
not os.environ.get("FRAMEPORT_NATIVE_TESTS") or not INC or not CC,
|
||||
reason="set FRAMEPORT_NATIVE_TESTS=1 (needs the OpenXR headers from native/build.py and a host C compiler)")]
|
||||
|
||||
HARNESS = r"""
|
||||
#include <openxr/openxr.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define LOG(...) (fprintf(stderr, __VA_ARGS__), fputc('\n', stderr))
|
||||
static XrInstance active_instance = (XrInstance)(uintptr_t)1;
|
||||
static int controller_fix = 1, profile_remap = 1, app_suggested_touch;
|
||||
static const char *const TOUCH_PROFILE = "/interaction_profiles/oculus/touch_controller";
|
||||
static char paths[16][XR_MAX_PATH_LENGTH];
|
||||
static int path_count;
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fake_string_to_path(XrInstance i, const char *s, XrPath *out) {
|
||||
(void)i;
|
||||
for (int k = 0; k < path_count; ++k) if (!strcmp(paths[k], s)) { *out = (XrPath)(k + 1); return XR_SUCCESS; }
|
||||
snprintf(paths[path_count], XR_MAX_PATH_LENGTH, "%s", s);
|
||||
*out = (XrPath)++path_count;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fake_path_to_string(XrInstance i, XrPath p, uint32_t cap, uint32_t *n,
|
||||
char *out) {
|
||||
(void)i;
|
||||
if (!p || p > (XrPath)path_count) return XR_ERROR_PATH_INVALID;
|
||||
*n = (uint32_t)snprintf(out, cap, "%s", paths[p - 1]) + 1;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static PFN_xrVoidFunction lookup(XrInstance i, const char *name) {
|
||||
(void)i;
|
||||
if (!strcmp(name, "xrPathToString")) return (PFN_xrVoidFunction)fake_path_to_string;
|
||||
if (!strcmp(name, "xrStringToPath")) return (PFN_xrVoidFunction)fake_string_to_path;
|
||||
return NULL;
|
||||
}
|
||||
#include "profile_report.c"
|
||||
static XrPath P(const char *s) { XrPath p; fake_string_to_path(0, s, &p); return p; }
|
||||
static const char *answer(const char *hand, const char *runtime) {
|
||||
XrInteractionProfileState state = {XR_TYPE_INTERACTION_PROFILE_STATE, NULL, runtime ? P(runtime) : XR_NULL_PATH};
|
||||
current_profile_fix(P(hand), &state);
|
||||
return state.interactionProfile ? paths[state.interactionProfile - 1] : "(none)";
|
||||
}
|
||||
int main(void) {
|
||||
const char *touch = "/interaction_profiles/oculus/touch_controller";
|
||||
const char *frame = "/interaction_profiles/valve/frame_controller";
|
||||
const char *plus = "/interaction_profiles/meta/touch_controller_plus";
|
||||
// no remap: SteamVR's profile becomes Touch, Touch stays Touch, none stays none
|
||||
printf("%s\n", answer("/user/hand/left", frame));
|
||||
printf("%s\n", answer("/user/hand/left", touch));
|
||||
printf("%s\n", answer("/user/hand/left", NULL));
|
||||
// the game's Touch Plus bindings were moved to Touch: Touch (and SteamVR's profile) are reported as Touch Plus
|
||||
snprintf(remapped_profile, sizeof(remapped_profile), "%s", plus);
|
||||
printf("%s\n", answer("/user/hand/left", touch));
|
||||
printf("%s\n", answer("/user/hand/right", frame));
|
||||
printf("%s\n", answer("/user/hand/right", NULL));
|
||||
printf("%s\n", answer("/user/hand/right", "/interaction_profiles/htc/vive_controller"));
|
||||
// controller_fix off: SteamVR's own profile is passed on unchanged, Touch is still reported as the game's
|
||||
controller_fix = 0;
|
||||
printf("%s\n", answer("/user/hand/left", frame));
|
||||
printf("%s\n", answer("/user/hand/left", touch));
|
||||
// the game suggested Touch bindings itself: Touch is reported as Touch
|
||||
controller_fix = 1;
|
||||
app_suggested_touch = 1;
|
||||
printf("%s\n", answer("/user/hand/left", touch));
|
||||
// profile_remap off: Touch as Touch
|
||||
app_suggested_touch = 0;
|
||||
profile_remap = 0;
|
||||
printf("%s\n", answer("/user/hand/left", touch));
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
STDOUT = [
|
||||
"/interaction_profiles/oculus/touch_controller",
|
||||
"/interaction_profiles/oculus/touch_controller",
|
||||
"(none)",
|
||||
"/interaction_profiles/meta/touch_controller_plus",
|
||||
"/interaction_profiles/meta/touch_controller_plus",
|
||||
"(none)",
|
||||
"/interaction_profiles/htc/vive_controller",
|
||||
"/interaction_profiles/valve/frame_controller",
|
||||
"/interaction_profiles/meta/touch_controller_plus",
|
||||
"/interaction_profiles/oculus/touch_controller",
|
||||
"/interaction_profiles/oculus/touch_controller",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def harness(tmp_path_factory):
|
||||
d = tmp_path_factory.mktemp("profile_report")
|
||||
(d / "harness.c").write_text(HARNESS)
|
||||
subprocess.run([CC, "-std=c11", "-D_GNU_SOURCE", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function",
|
||||
"-Wno-missing-field-initializers", "-I", str(INC), "-I", str(ROOT / "native/adapter"),
|
||||
str(d / "harness.c"), "-o", str(d / "harness")], check=True)
|
||||
return d / "harness"
|
||||
|
||||
|
||||
def test_reports_the_games_profile_after_a_remap(harness):
|
||||
p = subprocess.run([str(harness)], capture_output=True, text=True)
|
||||
assert p.returncode == 0, p.stderr
|
||||
assert p.stdout.splitlines() == STDOUT
|
||||
logged = [line for line in p.stderr.splitlines() if line.startswith("profile_remap:")]
|
||||
assert logged[:2] == [
|
||||
"profile_remap: /user/hand/left reported as meta/touch_controller_plus (the app's profile) instead of "
|
||||
"oculus/touch_controller",
|
||||
"profile_remap: /user/hand/right reported as meta/touch_controller_plus (the app's profile) instead of "
|
||||
"oculus/touch_controller",
|
||||
]
|
||||
assert len(logged) == 3 # once per report (at most 4)
|
||||
|
||||
|
||||
DETACHED = r"""
|
||||
#include <openxr/openxr.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define LOG(...) (fprintf(stderr, __VA_ARGS__), fputc('\n', stderr))
|
||||
static XrInstance active_instance = (XrInstance)(uintptr_t)1;
|
||||
static int profile_remap = 1, runtime_has_detached_controllers;
|
||||
static char paths[32][XR_MAX_PATH_LENGTH];
|
||||
static int path_count;
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fake_string_to_path(XrInstance i, const char *s, XrPath *out) {
|
||||
(void)i;
|
||||
for (int k = 0; k < path_count; ++k) if (!strcmp(paths[k], s)) { *out = (XrPath)(k + 1); return XR_SUCCESS; }
|
||||
snprintf(paths[path_count], XR_MAX_PATH_LENGTH, "%s", s);
|
||||
*out = (XrPath)++path_count;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL fake_path_to_string(XrInstance i, XrPath p, uint32_t cap, uint32_t *n,
|
||||
char *out) {
|
||||
(void)i;
|
||||
if (!p || p > (XrPath)path_count) return XR_ERROR_PATH_INVALID;
|
||||
*n = (uint32_t)snprintf(out, cap, "%s", paths[p - 1]) + 1;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
static PFN_xrVoidFunction lookup(XrInstance i, const char *name) {
|
||||
(void)i;
|
||||
if (!strcmp(name, "xrPathToString")) return (PFN_xrVoidFunction)fake_path_to_string;
|
||||
return NULL;
|
||||
}
|
||||
#include "detached_controllers.c"
|
||||
static XrPath P(const char *s) { XrPath p; fake_string_to_path(0, s, &p); return p; }
|
||||
static void show_action(const XrActionCreateInfo *info) {
|
||||
printf("%s:", info->actionName);
|
||||
for (uint32_t i = 0; i < info->countSubactionPaths; ++i) printf(" %s", paths[info->subactionPaths[i] - 1]);
|
||||
printf("\n");
|
||||
}
|
||||
int main(void) {
|
||||
XrPath sub[4] = {P("/user/hand/left"), P("/user/hand/right"), P("/user/detached_controller_meta/left"),
|
||||
P("/user/detached_controller_meta/right")}, kept[4];
|
||||
XrActionCreateInfo info = {XR_TYPE_ACTION_CREATE_INFO, NULL, "thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT, 4, sub,
|
||||
"Thumbstick"}, copy;
|
||||
show_action(detached_strip_action(&info, ©, kept));
|
||||
info.countSubactionPaths = 2; // hands only: passed on as it is
|
||||
printf("%d\n", detached_strip_action(&info, ©, kept) == &info);
|
||||
|
||||
XrActionSuggestedBinding b[3] = {{(XrAction)(uintptr_t)7, P("/user/hand/left/input/thumbstick")},
|
||||
{(XrAction)(uintptr_t)7, P("/user/detached_controller_meta/left/input/thumbstick")},
|
||||
{(XrAction)(uintptr_t)8, P("/user/hand/right/input/a/click")}}, kb[3];
|
||||
XrInteractionProfileSuggestedBinding s = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING, NULL,
|
||||
P("/interaction_profiles/oculus/touch_controller"), 3, b}, scopy;
|
||||
const XrInteractionProfileSuggestedBinding *u = detached_strip_bindings(&s, &scopy, kb);
|
||||
for (uint32_t i = 0; i < u->countSuggestedBindings; ++i) printf("%s\n", paths[u->suggestedBindings[i].binding - 1]);
|
||||
|
||||
XrInteractionProfileState st = {XR_TYPE_INTERACTION_PROFILE_STATE, NULL, 5};
|
||||
XrResult r = detached_current_profile(sub[2], &st, XR_ERROR_PATH_UNSUPPORTED);
|
||||
printf("%d %llu\n", r, (unsigned long long)st.interactionProfile);
|
||||
printf("%d\n", detached_current_profile(sub[0], &st, XR_ERROR_PATH_UNSUPPORTED)); // hands: unchanged
|
||||
printf("%d\n", detached_current_profile(sub[2], &st, XR_ERROR_ACTIONSET_NOT_ATTACHED)); // other errors stay
|
||||
|
||||
runtime_has_detached_controllers = 1; // the runtime knows them: nothing changes
|
||||
info.countSubactionPaths = 4;
|
||||
printf("%d %d %d\n", detached_strip_action(&info, ©, kept) == &info,
|
||||
detached_strip_bindings(&s, &scopy, kb) == &s,
|
||||
detached_current_profile(sub[2], &st, XR_ERROR_PATH_UNSUPPORTED));
|
||||
runtime_has_detached_controllers = 0;
|
||||
profile_remap = 0; // off: nothing changes
|
||||
printf("%d %d\n", detached_strip_action(&info, ©, kept) == &info, detached_strip_bindings(&s, &scopy, kb) == &s);
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def test_detached_controller_paths_left_out(tmp_path):
|
||||
(tmp_path / "detached.c").write_text(DETACHED)
|
||||
subprocess.run([CC, "-std=c11", "-D_GNU_SOURCE", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function",
|
||||
"-Wno-missing-field-initializers", "-I", str(INC), "-I", str(ROOT / "native/adapter"),
|
||||
str(tmp_path / "detached.c"), "-o", str(tmp_path / "detached")], check=True)
|
||||
p = subprocess.run([str(tmp_path / "detached")], capture_output=True, text=True)
|
||||
assert p.returncode == 0, p.stderr
|
||||
assert p.stdout.splitlines() == [
|
||||
"thumbstick: /user/hand/left /user/hand/right",
|
||||
"1",
|
||||
"/user/hand/left/input/thumbstick",
|
||||
"/user/hand/right/input/a/click",
|
||||
"0 0",
|
||||
str(-22), # XR_ERROR_PATH_UNSUPPORTED
|
||||
str(-46), # XR_ERROR_ACTIONSET_NOT_ATTACHED
|
||||
"1 1 -22",
|
||||
"1 1",
|
||||
]
|
||||
assert p.stderr.splitlines() == [
|
||||
"profile_remap: action thumbstick without 2 detached-controller path(s) (the runtime has no "
|
||||
"XR_META_detached_controllers)",
|
||||
"profile_remap: /interaction_profiles/oculus/touch_controller bindings without 1 detached-controller path(s)",
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
"""FrameBridge answers xrGetDisplayRefreshRateFB with a real rate (GitHub #306, PreyVR's black main menu).
|
||||
|
||||
The Frame's runtime has no XR_FB_display_refresh_rate; OVRPort's dispatcher still offers it and answered 0 Hz. Team
|
||||
Beef's idTech 4 ports (PreyVR, the Doom3Quest engine) run 1000 / refresh ms per tic, so 0 Hz stopped their game and
|
||||
GUI clock: the main menu stayed at its first animation step (black, only the cursor)."""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from frameport.validate import triage
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_fallback_rate_is_never_zero(tmp_path):
|
||||
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||
if not cc or os.name == "nt":
|
||||
pytest.skip("needs a POSIX host C compiler")
|
||||
binary = tmp_path / "refresh-rate-test"
|
||||
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "native/adapter"),
|
||||
str(ROOT / "tests/fixtures/src/refresh_rate_test.c"), "-o", str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
|
||||
def test_the_hook_is_always_installed():
|
||||
src = (ROOT / "native/adapter/frame_adapter.c").read_text()
|
||||
# not behind a per-game setting: every game that asks gets a rate, also where the runtime lacks the function
|
||||
assert re.search(r"\n if \(instance != XR_NULL_HANDLE\) HOOK_AS\(xrGetDisplayRefreshRateFB, hook_get_refresh_rate\)",
|
||||
src)
|
||||
assert "fb_refresh_rate(" in (ROOT / "native/adapter/session_fixes.c").read_text()
|
||||
|
||||
|
||||
def test_a_single_rejected_frame_is_not_an_error():
|
||||
entries = {e["id"]: e for e in yaml.safe_load((ROOT / "catalog/triage.yaml").read_text())["signatures"]}
|
||||
assert entries["cylinder-menu-layers-rejected"]["severity"] == "info"
|
||||
log = "10-16 05:06:16.011 1188 1248 I FrameBridge: xrEndFrame failed -16: blend=1 layers=7\n"
|
||||
found = [f for f in triage.triage(log).findings if f.id == "cylinder-menu-layers-rejected"]
|
||||
assert found and found[0].severity == "info"
|
||||
@@ -510,3 +510,32 @@ def test_obb_and_cube_advice_describes_limits():
|
||||
advice = next(f for f in cube.findings if f.id == "cube-swapchain-refused")
|
||||
assert "GLES" in advice.diagnosis
|
||||
assert "For a Vulkan game, the current build cannot stand in" in advice.diagnosis
|
||||
|
||||
|
||||
# Deadly Delivery 1.0.30 (GitHub #322): renders, then its lobby's Meta entitlement check fails four times and it quits
|
||||
LOG_ENTITLEMENT = """
|
||||
10-15 12:45:23.964 212 240 I ActivityManager: Start proc 1214:com.FlatHeadStudio.DeadlyDelivery/u0a14 for \
|
||||
pre-top-activity {com.FlatHeadStudio.DeadlyDelivery/com.unity3d.player.UnityPlayerActivity}
|
||||
10-15 12:45:38.349 1214 1238 E Unity : [LobbyManager]: Entitlement check failed: .
|
||||
10-15 12:45:38.349 1214 1238 E Unity : FlatHeads.<>c__DisplayClass105_0:<CheckEntitlementAndConfigureAsync>b__0(Message)
|
||||
10-15 12:45:38.352 1214 1238 I Unity : [LobbyManager]: Retrying oculus authentication in 2 seconds.
|
||||
10-15 12:45:43.372 1214 1286 I FrameBridge: pacing: 71.6 fps, displayTime vs predicted: avg 13.97 ms, max 83.33 ms
|
||||
10-15 12:45:46.349 1214 1238 I Unity : stopping xr plugin
|
||||
10-15 12:45:47.424 1214 1238 I Unity : [Photon] Disconnected from Photon (None)
|
||||
10-15 12:45:48.857 1214 1214 I Process : Sending signal. PID: 1214 SIG: 9
|
||||
"""
|
||||
|
||||
|
||||
def test_meta_entitlement_quest():
|
||||
r = triage(LOG_ENTITLEMENT, "EXITED", "com.FlatHeadStudio.DeadlyDelivery")
|
||||
f = next(f for f in r.findings if f.id == "meta-entitlement")
|
||||
assert f.severity == "error" and f.suggest == []
|
||||
assert "Entitlement check failed" in f.evidence
|
||||
assert "oculus-entitlement" not in {x.id for x in r.findings} # the Rift signature stays PC VR only
|
||||
assert r.verdict == "fail"
|
||||
assert "meta-entitlement" in {x.id for x in triage("E Unity: ovrPlatformInitialize_NotEntitled", "EXITED").findings}
|
||||
# routine Platform SDK lines of games that work don't match; a game that logs it and keeps running still passes
|
||||
for ok in ("I Unity : CheckEntitlementAndConfigureAsync", "I Unity : Entitlement check passed",
|
||||
"I Unity : ovr_Entitlement_GetIsViewerEntitled", "I Unity : User is entitled"):
|
||||
assert "meta-entitlement" not in {x.id for x in triage(ok + "\n" + LOG_OK, "RUNNING").findings}
|
||||
assert triage("E Unity : Entitlement check failed\n" + LOG_OK, "RUNNING").verdict == "pass"
|
||||
Reference in new issue
Block a user