mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e288946b81 | ||
|
|
6fbc450eea | ||
|
|
4f6d40625a | ||
|
|
a6137351d9 | ||
|
|
038dcd48cd | ||
|
|
9e9e5950d0 | ||
|
|
268afccf05 | ||
|
|
e1c0ac983c | ||
|
|
077eab2b79 | ||
|
|
35ef3af54b | ||
|
|
f10282294d | ||
|
|
50d5e14539 | ||
|
|
fd15f1de4c | ||
|
|
064cf95f6d | ||
|
|
8d75ede0ab | ||
|
|
0dbe18d824 | ||
|
|
a1bc6522c3 | ||
|
|
724b020e93 |
No files matched your search
@@ -66,6 +66,14 @@
|
|||||||
"*.py"
|
"*.py"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from": "../frame/openxr-compat",
|
||||||
|
"to": "frame/openxr-compat",
|
||||||
|
"filter": [
|
||||||
|
"XrApiLayer_FRAME_compat.json",
|
||||||
|
"prebuilt/**/*.so"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "../frame/devkit-utils",
|
"from": "../frame/devkit-utils",
|
||||||
"to": "frame/devkit-utils",
|
"to": "frame/devkit-utils",
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ in **Lepton**, Valve's Waydroid-based container. Lepton is built for games,
|
|||||||
not general Android use
|
not general Android use
|
||||||
([GamingOnLinux](https://www.gamingonlinux.com/2026/09/lepton-from-valve-to-run-android-games-on-linux-is-now-open-source/)).
|
([GamingOnLinux](https://www.gamingonlinux.com/2026/09/lepton-from-valve-to-run-android-games-on-linux-is-now-open-source/)).
|
||||||
|
|
||||||
|
**VR streaming clients:** WiVRn 26.9 and ALVR 20.14.1 install, but both fail
|
||||||
|
OpenXR instance creation on the checked Frame because its Android runtime lacks
|
||||||
|
`XR_KHR_convert_timespec_time` (**verified** 2026-09-28, SteamOS 0.4.1,
|
||||||
|
BUILD_ID 20260925.6191901). They are not Frame Control dependencies. See
|
||||||
|
[the feasibility results and options](linux-vr-streaming.md).
|
||||||
|
|
||||||
## Install from the Mac: one app, one Lepton instance (verified 2026-09-25)
|
## Install from the Mac: one app, one Lepton instance (verified 2026-09-25)
|
||||||
|
|
||||||
Use Frame Control's **Android apps** section (search, Install, Test, Report), drop
|
Use Frame Control's **Android apps** section (search, Install, Test, Report), drop
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
Frame client feasibility, 2026-09-28
|
||||||
|
SteamOS VERSION_ID=0.4.1 BUILD_ID=20260925.6191901; uname -m=aarch64
|
||||||
|
SteamVR: vrserver log reports 2.18.1; process 2256 remained alive across checks.
|
||||||
|
Base: dcf9689f6459e576d35fc507eb702ca6b2bf4dad (main).
|
||||||
|
Unmodified upstream release APKs; installed with ui/frame_android.py install APK --vr.
|
||||||
|
No Linux host attached. No pairing, streamed video, input, audio or worn-headset checks.
|
||||||
|
Selected logcat lines only; timestamps in Android logs are UTC.
|
||||||
|
|
||||||
|
WiVRn-release.apk
|
||||||
|
https://github.com/WiVRn/WiVRn/releases/tag/v26.9
|
||||||
|
sha256=1df6649ec77224fcc821af0ab4897222bdf3d7eb6ce6ad636461336724111331
|
||||||
|
E/OpenXR-Loader( 1140): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
|
||||||
|
I/WiVRn ( 1140): [2026-09-28 12:07:58.987] [WiVRn] [info] Failed to create OpenXR instance version 1.1.58: XR_ERROR_EXTENSION_NOT_PRESENT
|
||||||
|
E/OpenXR-Loader( 1140): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
|
||||||
|
I/WiVRn ( 1140): [2026-09-28 12:07:59.034] [WiVRn] [info] Failed to create OpenXR instance version 1.0.58: XR_ERROR_EXTENSION_NOT_PRESENT
|
||||||
|
E/WiVRn ( 1140): [2026-09-28 12:07:59.035] [WiVRn] [error] Error during initialization: Failed to create OpenXR instance: XR_ERROR_EXTENSION_NOT_PRESENT
|
||||||
|
|
||||||
|
alvr_client_android.apk
|
||||||
|
https://github.com/alvr-org/ALVR/releases/tag/v20.14.1
|
||||||
|
sha256=be68feeb02665e3d69f1cdbcabf38ea4d15c42868a7ec6b5e698dbefee4e4e36
|
||||||
|
E/OpenXR-Loader( 1139): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
|
||||||
|
I/RustStdoutStderr( 1139): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
|
||||||
|
E/[ALVR NATIVE-RUST]( 1139): ALVR panicked: What happened:
|
||||||
|
E/[ALVR NATIVE-RUST]( 1139): panicked at alvr/client_openxr/src/lib.rs:220:10:
|
||||||
|
E/[ALVR NATIVE-RUST]( 1139): called `Result::unwrap()` on an `Err` value: ERROR_EXTENSION_NOT_PRESENT
|
||||||
|
|
||||||
|
Cleanup verified: both app directories, compatdata directories and Steam shortcuts absent.
|
||||||
|
Both test containers stopped and removed. No Steam/SteamVR restart or global setting changes.
|
||||||
|
|
||||||
|
Valve release notes fetched from ISteamNews/GetNewsForApp/v2 (appid=250820).
|
||||||
|
|
||||||
|
SteamVR Beta Updated - 2.18.1
|
||||||
|
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1844751498219787
|
||||||
|
Added tethered Quest support over USB (must be used with Steam Link Beta)
|
||||||
|
|
||||||
|
Introducing SteamVR 2.17
|
||||||
|
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1843481262693486
|
||||||
|
Adds initial support for USB streaming. Note: Requires new Steam Client Beta.
|
||||||
|
Fix crash using Steam Link on Linux when games submit invalid textures.
|
||||||
|
Improve streaming recovery when using Steam Link on Linux.
|
||||||
|
|
||||||
|
SteamVR Beta Updated - 2.17.8
|
||||||
|
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1842212951314598
|
||||||
|
Fix crash using Steam Link on Linux when games submit invalid textures.
|
||||||
|
Improve streaming recovery when using Steam Link on Linux.
|
||||||
|
Adds initial support for USB streaming.
|
||||||
|
USB streaming can be used without WiFi by opting into the Steam Client Beta.
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# PC VR streaming from Linux
|
||||||
|
|
||||||
|
**Recommendation, 2026-09-28:** test Valve's current SteamVR/Steam Link path
|
||||||
|
on a Linux gaming PC before building another streamer. Valve now documents
|
||||||
|
Linux streaming fixes and USB support. We have no Linux host attached, so
|
||||||
|
Linux-to-Frame VR streaming remains **unverified here**.
|
||||||
|
|
||||||
|
This is the feasibility and options report for
|
||||||
|
[#24](https://github.com/saphid/frame-control/issues/24), not a shipped streaming
|
||||||
|
feature. Frame Control's features must use our own implementation or standard
|
||||||
|
platform components. WiVRn and ALVR are research comparisons, not dependencies.
|
||||||
|
An optional install shortcut is the most we would offer for a third-party app.
|
||||||
|
Our own streamer requires Alex's choice before implementation.
|
||||||
|
|
||||||
|
## What was checked on the Frame
|
||||||
|
|
||||||
|
**Verified** on 2026-09-28: aarch64, SteamOS **0.4.1**, BUILD_ID
|
||||||
|
`20260925.6191901`, SteamVR **2.18.1**. Version and build are recorded separately;
|
||||||
|
earlier docs associate this build with other SteamOS version labels.
|
||||||
|
|
||||||
|
| Client | Installation | Runtime result |
|
||||||
|
|---|---|---|
|
||||||
|
| WiVRn **26.9**, upstream `WiVRn-release.apk` | API 29, arm64-v8a; installed in its own immersive Lepton instance | OpenXR instance creation fails: missing `XR_KHR_convert_timespec_time`. Both 1.1.58 and 1.0.58 attempts return `XR_ERROR_EXTENSION_NOT_PRESENT` |
|
||||||
|
| ALVR **20.14.1**, upstream `alvr_client_android.apk` | API 26, arm64-v8a; installed in its own immersive Lepton instance | Same missing extension. Client panics at `client_openxr/src/lib.rs:220` with `ERROR_EXTENSION_NOT_PRESENT` |
|
||||||
|
|
||||||
|
The [evidence excerpt](evidence/linux-vr/2026-09-28.txt) includes APK SHA-256s,
|
||||||
|
upstream release links, loader errors and cleanup results. These are failures
|
||||||
|
before an OpenXR session, not successful VR clients. WiVRn was launched twice;
|
||||||
|
ALVR's container remained up despite its client panic. Container liveness alone
|
||||||
|
does not establish VR compatibility.
|
||||||
|
|
||||||
|
Both APKs already declare `MAIN` and `LAUNCHER`. They were installed unmodified
|
||||||
|
using this branch's existing `python3 ui/frame_android.py install APK --vr`,
|
||||||
|
then launched through their Steam shortcuts. Logs came from the instance's
|
||||||
|
`podman exec … /system/bin/logcat`; the user journal also retained WiVRn's errors
|
||||||
|
after its container exited. No headset was worn and no host was connected.
|
||||||
|
|
||||||
|
All test app files, compatdata, shortcuts and containers were removed afterwards.
|
||||||
|
SteamVR's original process remained running. No global settings changed.
|
||||||
|
|
||||||
|
### Relation to the VR APK branch
|
||||||
|
|
||||||
|
**Documented from source:** [PR #20](https://github.com/saphid/frame-control/pull/20)
|
||||||
|
was read, not edited (branch inspected at
|
||||||
|
[`038dcd4`](https://github.com/saphid/frame-control/commit/038dcd48cd75336f6a86c63c7878bfc9c52deec9)).
|
||||||
|
Its compatibility layer handles OpenXR version negotiation, some controller
|
||||||
|
profiles and refresh-rate requests. It does **not** implement
|
||||||
|
`XR_KHR_convert_timespec_time`. Its launcher fix is unnecessary for these APKs.
|
||||||
|
This report has **no unmerged code dependency** on that PR, and neither APK was
|
||||||
|
tested with its layer injected.
|
||||||
|
|
||||||
|
**Documented from upstream source:** WiVRn requests the extension in
|
||||||
|
[`application.cpp`](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/client/application.cpp#L1286)
|
||||||
|
and uses it to convert `CLOCK_MONOTONIC` into `XrTime` in
|
||||||
|
[`instance::now()`](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/client/xr/instance.cpp#L335).
|
||||||
|
ALVR also [requests it unconditionally](https://github.com/alvr-org/ALVR/blob/a9f6542fa507a841f40ab4f3fcb531427cd02550/alvr/client_openxr/src/lib.rs#L188).
|
||||||
|
Simply deleting the extension request or returning made-up timestamps would
|
||||||
|
not prove correct tracking or timing. A real fix needs a valid clock mapping
|
||||||
|
and further runtime tests. No such patch was made.
|
||||||
|
|
||||||
|
### Native SteamOS aarch64 clients
|
||||||
|
|
||||||
|
**Verified:** the Frame has a native OpenXR runtime manifest at
|
||||||
|
`~/.config/openxr/1/active_runtime.json`, pointing to SteamVR's
|
||||||
|
`bin/linuxarm64/vrclient.so`.
|
||||||
|
|
||||||
|
**Documented:** WiVRn's [26.9 README](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/README.md)
|
||||||
|
describes its Linux client as debugging-only, without audio or hardware decode.
|
||||||
|
ALVR 20.14.1's [non-Android decoder](https://github.com/alvr-org/ALVR/blob/a9f6542fa507a841f40ab4f3fcb531427cd02550/alvr/client_core/src/video_decoder/mod.rs)
|
||||||
|
returns no decoded frames. The inspected releases ship Android clients, not a
|
||||||
|
ready-to-run native Frame client.
|
||||||
|
|
||||||
|
**Inferred:** a native port is possible research, but neither release offers a
|
||||||
|
demonstrated native alternative to the blocked APKs. Native builds, native
|
||||||
|
extension enumeration, hardware decoding and audio were **not tested**. The
|
||||||
|
Android extension failure does not establish that the native runtime lacks it.
|
||||||
|
|
||||||
|
## (a) Valve's own path — recommended first
|
||||||
|
|
||||||
|
**Documented**, from Valve's release notes rather than launch-window reports:
|
||||||
|
|
||||||
|
- [SteamVR 2.17.8 beta](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1842212951314598)
|
||||||
|
says “Fix crash using Steam Link on Linux when games submit invalid textures”
|
||||||
|
and “Improve streaming recovery when using Steam Link on Linux.” It also
|
||||||
|
adds initial USB streaming, with Steam Client Beta required to use USB
|
||||||
|
without Wi-Fi.
|
||||||
|
- [SteamVR 2.17 release](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1843481262693486)
|
||||||
|
repeats Linux streaming fixes and initial USB support. USB is no longer
|
||||||
|
solely a claim about an old beta, but version/channel requirements still
|
||||||
|
need checking on the actual host.
|
||||||
|
- [SteamVR 2.18.1 beta](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1844751498219787)
|
||||||
|
adds USB-tethered **Quest** support with Steam Link Beta. That entry is not
|
||||||
|
proof of a Frame/Linux combination.
|
||||||
|
- The [Steam Link page](https://store.steampowered.com/app/353380/Steam_Link/)
|
||||||
|
lists Linux desktop clients, while its Quest VR requirements still say
|
||||||
|
Windows 10 or newer. Desktop Steam Link support is not equivalent to VR host
|
||||||
|
support, and the Quest requirements are not a Frame support matrix.
|
||||||
|
|
||||||
|
**Inferred:** Valve has a Linux VR streaming path worth testing. The old blanket
|
||||||
|
claim “Linux cannot stream VR” is no longer justified by the evidence. These
|
||||||
|
release notes do not establish which Linux GPU/driver/Frame combinations work.
|
||||||
|
USB changes the transport; it does not by itself prove host encoder support.
|
||||||
|
|
||||||
|
**Not verified:** Linux host discovery, pairing, wireless or USB streaming,
|
||||||
|
stereo rendering, controllers, haptics, audio, latency, or a game. Frame-only
|
||||||
|
inspection cannot establish any of these. Flat Remote Play and a desktop shown
|
||||||
|
on a panel are not substitutes for this test.
|
||||||
|
|
||||||
|
Next test, once a Linux gaming PC is available: record distro, GPU/driver,
|
||||||
|
Steam client channel/version and SteamVR version; use the Frame's built-in
|
||||||
|
Steam connection flow, first wirelessly and then over a data-capable USB cable.
|
||||||
|
Launch a free OpenXR sample or developer-consented VR game. Verify stereo,
|
||||||
|
head/controller tracking, haptics and audio while worn; retain both ends' logs
|
||||||
|
and measure latency and recovery after a link interruption. Restore any test
|
||||||
|
channel changes. Do not change the shared headset's channel just for this report.
|
||||||
|
|
||||||
|
If that works, Frame Control can provide our own host checks, setup guidance
|
||||||
|
and session controls around Valve's existing platform. First establish which
|
||||||
|
controls have a usable interface; no stable automated pairing API has been
|
||||||
|
verified. **Estimate (inferred):** 2–5 engineer-days for the hardware feasibility
|
||||||
|
pass; another 1–2 weeks for a small integration if those interfaces exist.
|
||||||
|
|
||||||
|
## (b) Our own streaming — proposal only
|
||||||
|
|
||||||
|
This is a new VR transport and device integration, not a desktop capture feature.
|
||||||
|
A plausible first target is **one Linux GPU family, one host, one Frame**, using
|
||||||
|
SteamVR on both ends. Our host driver would expose a remote HMD/controllers,
|
||||||
|
receive poses and inputs, and obtain stereo textures for hardware encoding.
|
||||||
|
Our Frame OpenXR app would decode, submit the correct eye views and render poses
|
||||||
|
at predicted display times, and return tracking/input. SteamVR/OpenXR, bundled
|
||||||
|
codec/transport libraries and platform GPU APIs fit the ownership rule; a
|
||||||
|
WiVRn/ALVR/Monado server dependency would not.
|
||||||
|
|
||||||
|
**Inferred design risks:** Linux SteamVR texture-sharing/driver interfaces and
|
||||||
|
Frame decode-to-GPU interoperability need a spike before committing to this
|
||||||
|
architecture. Sending an already-composited desktop mirror loses the stereo,
|
||||||
|
pose and timing information we need. Late reprojection, clock conversion,
|
||||||
|
backpressure, controller bindings, audio sync and reconnects are substantial
|
||||||
|
work. A runtime shim must not assume `XrTime` equals monotonic nanoseconds.
|
||||||
|
|
||||||
|
### Reuse from `mac-in-headset`
|
||||||
|
|
||||||
|
**Documented from our code**, read-only at
|
||||||
|
[`1b90c64`](https://github.com/saphid/frame-control/commit/1b90c64b73bace54c63a3aae154c5d29a9448d72):
|
||||||
|
|
||||||
|
- Reuse the ideas for low-latency encoding without B-frames, dropping work
|
||||||
|
before encoding, bounded queues, keyframe recovery, adaptive bitrate,
|
||||||
|
per-frame timing and authenticated session setup.
|
||||||
|
- Its VideoToolbox encoder and ScreenCaptureKit capture are macOS-specific.
|
||||||
|
Linux needs a new GPU encoder path (for example VA-API or NVENC via bundled
|
||||||
|
libraries) and VR texture capture, not a port of window capture.
|
||||||
|
- Its WebSocket over SSH is useful for a first controlled transport experiment
|
||||||
|
and control messages. Reliable TCP can stall behind lost packets; a VR media
|
||||||
|
path needs measured deadline behaviour, likely datagrams with loss recovery
|
||||||
|
using an ordinary bundled transport library. Do not invent cryptography.
|
||||||
|
- Its Chromium/WebCodecs panel viewer is not a VR client. That branch reports
|
||||||
|
software H.264 decoding and occasional long Wi-Fi stalls on the Frame.
|
||||||
|
Its desktop latency measurements are not motion-to-photon measurements or
|
||||||
|
evidence that a 90/120 Hz stereo stream will work.
|
||||||
|
|
||||||
|
**Size/effort estimate (inferred, one experienced full-time engineer, hardware
|
||||||
|
available):**
|
||||||
|
|
||||||
|
| Phase | Deliverable / stop condition | Effort |
|
||||||
|
|---|---|---|
|
||||||
|
| Feasibility | Linux driver texture access, Frame hardware decode into OpenXR, pose/clock loop; stop if any cannot meet frame deadlines | 2–4 weeks |
|
||||||
|
| First end-to-end prototype | One GPU/codec, stereo sample over a controlled LAN, head/controllers, logs and teardown | 4–8 additional weeks |
|
||||||
|
| Usable limited beta | Audio/haptics, pairing, recovery, bitrate/loss handling, installer, worn testing and latency work | 6–12 additional weeks |
|
||||||
|
| Wider support | Multiple GPU vendors/distros, USB and Wi-Fi variation, long-session stability | 2–4 additional months |
|
||||||
|
|
||||||
|
Planning range: **12–24 engineer-weeks for a limited beta**, roughly
|
||||||
|
**10–25k lines of our code plus tests/tooling**, excluding bundled libraries.
|
||||||
|
This is a low-confidence scope estimate, not a delivery promise; an unsupported
|
||||||
|
driver or decode interface could block it entirely. Foveated streaming,
|
||||||
|
eye tracking and parity with Valve are excluded. A Linux gaming PC and repeatable
|
||||||
|
worn-headset testing are prerequisites. **Do not build this until Alex chooses.**
|
||||||
|
|
||||||
|
## (c) Optional “install WiVRn” shortcut only
|
||||||
|
|
||||||
|
Allowed as a clearly optional convenience, never a prerequisite for a Frame
|
||||||
|
Control feature. **Documented:** WiVRn's server Flatpak ID is
|
||||||
|
`io.github.wivrn.wivrn`; its client/server versions must match, and its Flatpak
|
||||||
|
includes xrizer/OpenComposite. Those are properties of an independently
|
||||||
|
installed third-party stack, not components of our implementation.
|
||||||
|
|
||||||
|
**Recommendation:** defer the shortcut while the current client fails before
|
||||||
|
session creation. If offered later, label that compatibility result and let
|
||||||
|
the user choose the install; do not present “install” as “streaming works.”
|
||||||
|
**Estimate (inferred):** 1–2 engineer-days for an optional host-side shortcut
|
||||||
|
with package/version detection and honest status, excluding third-party fixes.
|
||||||
|
No shortcut, host install, pairing automation or streaming UI was built here.
|
||||||
|
|
||||||
|
Choose **(a)** for the next hardware test. Keep **(b)** as a separately approved
|
||||||
|
project if Valve's path fails or lacks a required capability. **(c)** does not
|
||||||
|
solve the verified client blocker and should not be the product's foundation.
|
||||||
+7
-3
@@ -5,6 +5,8 @@ This covers three directions, plus input:
|
|||||||
- **A. Frame → Mac**: see and control the headset from the Mac.
|
- **A. Frame → Mac**: see and control the headset from the Mac.
|
||||||
- **B. Mac → Frame**: use the Mac's desktop inside the headset.
|
- **B. Mac → Frame**: use the Mac's desktop inside the headset.
|
||||||
- **C. iPhone → Frame**: mirror the phone inside the headset.
|
- **C. iPhone → Frame**: mirror the phone inside the headset.
|
||||||
|
- **PC VR from Linux**: [feasibility and options](linux-vr-streaming.md),
|
||||||
|
including Valve's streaming and USB support. No Linux host tested yet.
|
||||||
- **Input**: type and point in the Frame from the Mac or iPhone.
|
- **Input**: type and point in the Frame from the Mac or iPhone.
|
||||||
|
|
||||||
The confidence labels are the same as in [ssh.md](ssh.md).
|
The confidence labels are the same as in [ssh.md](ssh.md).
|
||||||
@@ -24,11 +26,13 @@ Mac with keyboard, mouse, and clipboard.
|
|||||||
|
|
||||||
## B. Show the Mac's desktop inside the Frame
|
## B. Show the Mac's desktop inside the Frame
|
||||||
|
|
||||||
The Frame's streaming features are built around a **Windows PC running
|
The Frame's VR streaming uses **SteamVR** on the host. Linux hosts had
|
||||||
SteamVR** plus the USB Wi-Fi 6E dongle. Even Linux hosts had VR-streaming
|
VR-streaming problems at launch
|
||||||
problems at launch
|
|
||||||
([Steam discussion](https://steamcommunity.com/app/4165890/discussions/0/528765047224280796/),
|
([Steam discussion](https://steamcommunity.com/app/4165890/discussions/0/528765047224280796/),
|
||||||
[gbl08ma](https://gbl08ma.com/posts/steam-frame-a-linux-machine-doesnt-support-linux/)).
|
[gbl08ma](https://gbl08ma.com/posts/steam-frame-a-linux-machine-doesnt-support-linux/)).
|
||||||
|
Valve's later 2.17.8 notes explicitly describe Steam Link fixes on Linux and
|
||||||
|
initial USB streaming support (**documented**, not tested from a Linux host
|
||||||
|
here). See [the current comparison](linux-vr-streaming.md#a-valves-own-path--recommended-first).
|
||||||
**macOS isn't a supported SteamVR host**, so for the Mac we're only looking at
|
**macOS isn't a supported SteamVR host**, so for the Mac we're only looking at
|
||||||
flat 2D desktop streaming into a window on the Frame's Linux desktop.
|
flat 2D desktop streaming into a window on the Frame's Linux desktop.
|
||||||
|
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
# VR APKs and Quest games in Lepton
|
||||||
|
|
||||||
|
What it takes to run an immersive (OpenXR) Android app, including Meta Quest
|
||||||
|
builds, on the Frame. Checked on SteamOS BUILD_ID 20260925.6191901, Lepton
|
||||||
|
v2.8.14 (rootfs v2.8.11), SteamVR 2.18.1, on 2026-09-28, unless marked
|
||||||
|
**inferred**.
|
||||||
|
|
||||||
|
## How a VR APK reaches SteamVR (verified)
|
||||||
|
|
||||||
|
- Lepton ships a standard Khronos system runtime manifest,
|
||||||
|
`/vendor/etc/openxr/1/active_runtime.json`, pointing at SteamVR's Android
|
||||||
|
client, `/data/steamvr/runtime/bin/androidarm64/vrclient.so`. That is the
|
||||||
|
host's `/opt/steamvr/bin/androidarm64/`, bind-mounted in.
|
||||||
|
- An APK's own Khronos-style `libopenxr_loader.so` tries the runtime brokers
|
||||||
|
(`org.khronos.openxr.runtime_broker`, `…system_runtime_broker`), finds
|
||||||
|
neither, then falls back to that manifest. Nothing in the APK has to change
|
||||||
|
for discovery.
|
||||||
|
- Install the APK without the flatscreen marker
|
||||||
|
(`python3 ui/frame_android.py install app.apk --vr`). The marker only
|
||||||
|
controls Lepton's 2D Android surface; the app itself has to start an
|
||||||
|
OpenXR session.
|
||||||
|
- Lepton also loads Valve's `XR_APILAYER_VALVE_fdm_injection` layer from
|
||||||
|
`/vendor/etc/openxr/1/api_layers/implicit.d/`. Only layers in Valve's own
|
||||||
|
directories are picked up (`liblepton/vulkan_layers.sh`), so a third-party
|
||||||
|
layer has to ship inside the APK.
|
||||||
|
|
||||||
|
**Open Brush 2.32.29, the Quest APK from its GitHub release (Unity OpenXR,
|
||||||
|
Vulkan), works unmodified.** Its manifest already has `LAUNCHER` next to
|
||||||
|
`com.oculus.intent.category.VR`. Unity asked for OpenXR 1.1, got
|
||||||
|
`XR_ERROR_API_VERSION_UNSUPPORTED`, retried with 1.0 and succeeded. SteamVR
|
||||||
|
took it as the scene app, created Touch, simple-controller and Frame-controller
|
||||||
|
bindings, and the session reached `XR_SESSION_STATE_FOCUSED`. A headset capture
|
||||||
|
(`ui/frame_vrshot.py`, after waking the compositor and closing the dashboard)
|
||||||
|
showed a dark sky over a mountain horizon; nobody wore the headset to confirm
|
||||||
|
it was Open Brush's scene or to try drawing.
|
||||||
|
|
||||||
|
**Khronos `hello_xr` (Vulkan, 1.1.63 release APK) works unmodified:**
|
||||||
|
`Instance RuntimeName=SteamVR/OpenXR RuntimeVersion=2.18.1`, 1728×1728
|
||||||
|
swapchains per eye, session `IDLE → READY → SYNCHRONIZED` (the headset was
|
||||||
|
not being worn, so it did not reach `FOCUSED`).
|
||||||
|
|
||||||
|
## What SteamVR's Android runtime supports (verified, from `vrclient.so`)
|
||||||
|
|
||||||
|
- **OpenXR 1.0 only.** An app requesting `XR_API_VERSION_1_0` works; one
|
||||||
|
requesting 1.1 (`XR_CURRENT_API_VERSION` in a 1.1 SDK) gets
|
||||||
|
`XR_ERROR_API_VERSION_UNSUPPORTED` from the runtime.
|
||||||
|
- Extensions include `XR_KHR_opengl_es_enable`, `XR_KHR_vulkan_enable{,2}`,
|
||||||
|
`XR_KHR_composition_layer_depth`, `XR_KHR_locate_spaces`,
|
||||||
|
`XR_EXT_local_floor`, `XR_EXT_uuid`, `XR_EXT_palm_pose`,
|
||||||
|
`XR_EXT_hand_tracking`, `XR_EXT_eye_gaze_interaction`, and these Meta ones:
|
||||||
|
`XR_FB_display_refresh_rate`, `XR_FB_foveation{,_configuration,_vulkan}`,
|
||||||
|
`XR_FB_space_warp`, `XR_FB_swapchain_update_state`,
|
||||||
|
`XR_META_foveation_eye_tracked`, `XR_META_recommended_layer_resolution`,
|
||||||
|
`XR_META_vulkan_swapchain_create_info`, `XR_META_performance_metrics`.
|
||||||
|
- Not present: `XR_FB_passthrough`, `XR_FB_hand_tracking_*`,
|
||||||
|
`XR_FB_spatial_entity*`, `XR_FB_color_space`,
|
||||||
|
`XR_KHR_android_thread_settings`, `XR_OCULUS_*`.
|
||||||
|
- Interaction profiles include `oculus/touch_controller`, `khr/simple_controller`,
|
||||||
|
`valve/frame_controller` and the usual PC controllers. Valve documents Touch
|
||||||
|
bindings as a working fallback on the Frame controllers.
|
||||||
|
|
||||||
|
## What stops a Quest APK (verified with Wolvic 1.9, `oculusvr` build)
|
||||||
|
|
||||||
|
1. **Lepton won't start it.** Lepton's `apk-info-extractor` only accepts an
|
||||||
|
activity whose intent filter has `android.intent.action.MAIN` and
|
||||||
|
`android.intent.category.LAUNCHER`. Quest apps use
|
||||||
|
`com.oculus.intent.category.VR` instead, so Lepton logs `APP_ACTIVITY is
|
||||||
|
empty` and exits. There is no override. **Fix:** add the `LAUNCHER`
|
||||||
|
category to that intent filter and re-sign. After that, Wolvic started.
|
||||||
|
2. **OpenXR 1.1.** Wolvic's Quest build then requested OpenXR 1.1 and aborted
|
||||||
|
on `XR_ERROR_API_VERSION_UNSUPPORTED`. Unity's OpenXR plugin retries with
|
||||||
|
1.0 (Open Brush, above), so this mostly bites native and non-Unity apps. **Fix (inferred):** an API layer
|
||||||
|
inside the APK that asks the runtime for 1.0 and maps the 1.1 core
|
||||||
|
functions to the extensions the runtime does have (`XR_KHR_locate_spaces`,
|
||||||
|
`XR_EXT_local_floor`, `XR_EXT_uuid`, `XR_EXT_palm_pose`).
|
||||||
|
3. **Lepton's missing clipboard service** still applies to VR apps. The Godot
|
||||||
|
XR Tools demo's Quest build (itch.io) dies in `Godot.<init>` casting the
|
||||||
|
null clipboard service to `ClipboardManager`, before any OpenXR call. See
|
||||||
|
the clipboard table in [apks.md](apks.md).
|
||||||
|
4. **Not yet reached:** required Meta-only extensions (each app differs),
|
||||||
|
swapchain formats (the Lynx Wolvic build needed `GL_SRGB8_ALPHA8`), and
|
||||||
|
Meta platform services.
|
||||||
|
|
||||||
|
The loader was never the problem: Wolvic's Quest `libopenxr_loader.so` is a
|
||||||
|
Khronos-style loader and found SteamVR through `/vendor`.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- **Meta entitlement.** Apps that call the Oculus Platform SDK
|
||||||
|
(`libovrplatformloader.so`) to check the Quest store licence need Meta's
|
||||||
|
services. Frame Control won't work around that.
|
||||||
|
- **VrApi-era apps** (`libvrapi.so`, before OpenXR) need an API translator,
|
||||||
|
not a patch.
|
||||||
|
|
||||||
|
## Frame Control does this for you
|
||||||
|
|
||||||
|
APK uploads and `python3 ui/frame_android.py install app.apk` detect VR
|
||||||
|
manifest categories, Samsung's `vr_only` flag and the arm64 OpenXR loader.
|
||||||
|
VR apps default to immersive mode without the flatscreen marker. The upload
|
||||||
|
selector or CLI `--flat` / `--vr` overrides that choice. Compatibility notes
|
||||||
|
identify legacy VrApi, Meta platform SDK and OpenXR libraries.
|
||||||
|
|
||||||
|
Lepton only starts an `<activity>` whose MAIN intent filter has LAUNCHER; it
|
||||||
|
ignores `<activity-alias>`, which is where Godot 4 exports put LAUNCHER. When no
|
||||||
|
real activity qualifies, Frame Control adds LAUNCHER to the VR activity's MAIN
|
||||||
|
filter, or to the activity the launcher alias targets, then repacks and v2-signs
|
||||||
|
the APK locally before copying it; `meta.json` records
|
||||||
|
`"patched": ["launcher"]`. Unchanged ZIP members retain their compressed
|
||||||
|
bytes; stored libraries are aligned to 16 KiB. The RSA signing identity lives
|
||||||
|
in Frame Control's per-user app-data directory as `apk-signing-key.json`
|
||||||
|
(mode 0600). Keep this key to preserve the signer on subsequent patched
|
||||||
|
updates. A re-signed APK cannot update an installation signed by its original
|
||||||
|
publisher; Android also treats it as a different signer for signature checks.
|
||||||
|
|
||||||
|
VR apps with an arm64 OpenXR loader also get the OpenXR compatibility layer
|
||||||
|
([frame/openxr-compat](../frame/openxr-compat/README.md)): an implicit API
|
||||||
|
layer in the APK's `assets/openxr/1/api_layers/implicit.d/`, which the app's
|
||||||
|
own loader picks up next to Valve's layer. It asks SteamVR for OpenXR 1.0 when
|
||||||
|
the app wants 1.1 and enables the extensions that became 1.1 core; maps
|
||||||
|
`xrLocateSpaces` to `xrLocateSpacesKHR` and `grip_surface` to `palm_ext`; drops
|
||||||
|
1.1 controller profiles SteamVR doesn't know; stubs
|
||||||
|
`XR_KHR_android_thread_settings` and `XR_OCULUS_android_session_state_enable`;
|
||||||
|
and keeps the current refresh rate when SteamVR refuses a requested one.
|
||||||
|
`meta.json` records `"patched": ["openxr-compat"]`. Skip it with
|
||||||
|
`install … --no-xr-compat`. Its decisions go to logcat under `FrameXrCompat`.
|
||||||
|
|
||||||
|
Verified on the headset (2026-09-28):
|
||||||
|
|
||||||
|
- **Wolvic 1.9, Quest build**, installed as downloaded: Frame Control added
|
||||||
|
`LAUNCHER` and the layer. The layer turned OpenXR 1.1.48 into 1.0.63, the
|
||||||
|
instance and session were created, and a 144 Hz refresh request that SteamVR
|
||||||
|
refused was kept at the current rate. The session reached `SYNCHRONIZED`;
|
||||||
|
then Wolvic's Gecko engine crashed (null SIGSEGV on its Gecko thread, the
|
||||||
|
same crash its Lynx build has), which is Wolvic's, not OpenXR's.
|
||||||
|
- **Open Brush, Quest build**, with the layer: 1.1.54 → 1.0.63, the thread
|
||||||
|
settings stub in use, `bytedance/pico4_controller` bindings dropped, and the
|
||||||
|
session reached `FOCUSED`, the same as without the layer.
|
||||||
|
|
||||||
|
Inspect or prepare an APK without contacting the headset:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 ui/frame_android.py info app.apk
|
||||||
|
python3 ui/frame_android.py patch app.apk patched.apk
|
||||||
|
python3 ui/frame_android.py patch app.apk patched.apk --add assets/openxr/1/api_layers/implicit.d/X.json=X.json --add lib/arm64-v8a/libX.so=libX.so
|
||||||
|
```
|
||||||
|
|
||||||
|
The patch fixes Lepton's launch-category requirement. It does not supply an
|
||||||
|
OpenXR 1.1 translation layer, Meta services or a VrApi implementation.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Preserve upstream headers byte-for-byte, including their existing whitespace.
|
||||||
|
vendor/** -whitespace
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/build-*/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
project(FrameXrCompat LANGUAGES CXX)
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
add_library(compat_logic INTERFACE)
|
||||||
|
target_include_directories(compat_logic INTERFACE . vendor)
|
||||||
|
if(ANDROID)
|
||||||
|
add_library(XrApiLayer_FRAME_compat SHARED layer.cpp)
|
||||||
|
target_link_libraries(XrApiLayer_FRAME_compat PRIVATE compat_logic log)
|
||||||
|
target_compile_definitions(XrApiLayer_FRAME_compat PRIVATE XR_USE_PLATFORM_ANDROID XR_NO_PROTOTYPES)
|
||||||
|
target_compile_options(XrApiLayer_FRAME_compat PRIVATE -O2 -fvisibility=hidden -Wall -Wextra -Werror)
|
||||||
|
target_link_options(XrApiLayer_FRAME_compat PRIVATE -Wl,-z,max-page-size=16384 -Wl,--no-undefined -Wl,--exclude-libs,ALL -Wl,-s)
|
||||||
|
else()
|
||||||
|
enable_testing()
|
||||||
|
add_executable(compat_tests tests/compat_tests.cpp)
|
||||||
|
target_link_libraries(compat_tests PRIVATE compat_logic)
|
||||||
|
target_compile_options(compat_tests PRIVATE -Wall -Wextra -Werror)
|
||||||
|
add_test(NAME compat_logic COMMAND compat_tests)
|
||||||
|
add_executable(dispatch_tests tests/dispatch_tests.cpp)
|
||||||
|
target_link_libraries(dispatch_tests PRIVATE compat_logic)
|
||||||
|
target_include_directories(dispatch_tests PRIVATE tests/shims)
|
||||||
|
target_compile_definitions(dispatch_tests PRIVATE XR_USE_PLATFORM_ANDROID XR_NO_PROTOTYPES)
|
||||||
|
target_compile_options(dispatch_tests PRIVATE -Wall -Wextra -Werror)
|
||||||
|
add_test(NAME layer_dispatch COMMAND dispatch_tests)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# Frame OpenXR compatibility API layer
|
||||||
|
|
||||||
|
`XR_APILAYER_FRAME_compat` is an APK-local implicit API layer for Steam Frame's
|
||||||
|
OpenXR 1.0 Android runtime. It translates selected 1.1 functionality; it is not
|
||||||
|
a conformant replacement for a complete 1.1 runtime. Device facts are in
|
||||||
|
[../../docs/vr-apks.md](../../docs/vr-apks.md). No headset was contacted during
|
||||||
|
implementation. Loading, Valve-layer coexistence, controller usability, and
|
||||||
|
Wolvic startup/rendering on Lepton remain **unverified on device**.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- The negotiated `createApiLayerInstance` hook handles `xrCreateInstance`.
|
||||||
|
API requests >= 1.1 become `XR_API_VERSION_1_0` (1.0.63 with these headers),
|
||||||
|
rather than carrying an arbitrary application's patch into the 1.0 request.
|
||||||
|
Requests below 1.1 are unchanged. The loader may reject future API versions
|
||||||
|
before any layer runs.
|
||||||
|
- Enumerate downstream extensions through the **next layer's** GIPA before
|
||||||
|
creation. Only advertised promoted extensions are added, with duplicates
|
||||||
|
removed. All other application extensions remain, except the two stubs below.
|
||||||
|
Log added, stubbed and missing extensions with tag `FrameXrCompat`.
|
||||||
|
- Missing `XR_KHR_android_thread_settings` is advertised and accepted;
|
||||||
|
`xrSetAndroidApplicationThreadKHR` logs and returns success without changing
|
||||||
|
thread scheduling. Missing `XR_OCULUS_android_session_state_enable` is
|
||||||
|
advertised and accepted, with no commands or additional session behavior.
|
||||||
|
If downstream supports either extension, preserve it and its real commands.
|
||||||
|
Stubbing these capabilities is intentionally limited to accepting the app's
|
||||||
|
request; it does not provide Meta services.
|
||||||
|
- The manifest's `instance_extensions` makes both stubs visible during the
|
||||||
|
loader's pre-instance enumeration and validation. The layer also exposes
|
||||||
|
an enumeration implementation with count/capacity handling. Before a next
|
||||||
|
dispatch is available it can enumerate only its own extensions; normal app
|
||||||
|
global enumeration is performed by the loader, merging runtime and manifest
|
||||||
|
entries. See [loader_core.cpp L106–145](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/loader_core.cpp#L106)
|
||||||
|
and [manifest parsing L549–558](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/manifest_file.cpp#L549).
|
||||||
|
- Unknown missing extensions are never silently removed. Khronos may reject
|
||||||
|
them **before** entering the layer and name them in `OpenXR-Loader` logs
|
||||||
|
([loader_instance.cpp L149–176](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/loader_instance.cpp#L149)).
|
||||||
|
If they reach this layer, it logs their names and preserves them for the
|
||||||
|
downstream failure.
|
||||||
|
- `xrLocateSpaces` dispatches to `xrLocateSpacesKHR` per session/instance.
|
||||||
|
This is the only command added in 1.1. The three relevant structure types
|
||||||
|
(`SPACES_LOCATE_INFO`, `SPACE_LOCATIONS`, `SPACE_VELOCITIES`) and their KHR
|
||||||
|
versions are **numeric aliases**, and the structs are typedef aliases in
|
||||||
|
these headers. Explicit type conversion of local input/output copies is
|
||||||
|
therefore an identity conversion; the velocity chain passes through without
|
||||||
|
mutating its types or linkage. Output arrays remain shared and receive the
|
||||||
|
runtime's results, including on error; caller struct headers remain intact.
|
||||||
|
- With palm pose enabled, rewrite both hand paths ending in
|
||||||
|
`/input/grip_surface/pose` to `/input/palm_ext/pose` in `xrStringToPath` and
|
||||||
|
in binding suggestions (including paths obtained before this layer rewrote
|
||||||
|
them). Unknown paths are left alone.
|
||||||
|
- `xrGetInstanceProperties` and unrelated commands pass through. The real
|
||||||
|
runtime's identity is not spoofed. Dispatch tables and session ownership are
|
||||||
|
locked, and removed after successful destruction. Downstream calls occur
|
||||||
|
outside the lock.
|
||||||
|
|
||||||
|
## Spec basis and interaction profiles
|
||||||
|
|
||||||
|
The pinned [1.1 promotions appendix, versions.adoc L16–156](https://github.com/KhronosGroup/OpenXR-Docs/blob/release-1.1.63/specification/sources/chapters/versions.adoc#L16)
|
||||||
|
includes a generated promotion list. Its source is the
|
||||||
|
[1.1.63 XML registry](https://github.com/KhronosGroup/OpenXR-Docs/blob/release-1.1.63/specification/registry/xr.xml),
|
||||||
|
selecting `extension[@promotedto='XR_VERSION_1_1']`. The full list implemented:
|
||||||
|
|
||||||
|
| Promoted extension | Handling |
|
||||||
|
| --- | --- |
|
||||||
|
| `XR_KHR_locate_spaces` | Enable if advertised; core command alias |
|
||||||
|
| `XR_EXT_local_floor` | Enable if advertised; reference-space enum is an alias |
|
||||||
|
| `XR_EXT_uuid` | Enable if advertised; type alias, no commands |
|
||||||
|
| `XR_EXT_palm_pose` | Enable if advertised; rewrite grip-surface paths |
|
||||||
|
| `XR_VARJO_quad_views` | Enable if advertised; view-configuration enum alias, support remains optional |
|
||||||
|
| `XR_EXT_samsung_odyssey_controller` | Enable if advertised |
|
||||||
|
| `XR_EXT_hp_mixed_reality_controller` | Enable if advertised |
|
||||||
|
| `XR_HTC_vive_cosmos_controller_interaction` | Enable if advertised |
|
||||||
|
| `XR_HTC_vive_focus3_controller_interaction` | Enable if advertised |
|
||||||
|
| `XR_ML_ml2_controller_interaction` | Enable if advertised |
|
||||||
|
| `XR_FB_touch_controller_pro` | Enable if advertised; old profile still usable |
|
||||||
|
| `XR_META_touch_controller_plus` | Enable if advertised; old profile still usable |
|
||||||
|
| `XR_BD_controller_interaction` | Enable if advertised |
|
||||||
|
| `XR_KHR_maintenance1` | Enable if advertised (included in the pinned registry's promotion list) |
|
||||||
|
|
||||||
|
`XR_EXT_hand_interaction` is **not** promoted to 1.1. Preserve it if requested;
|
||||||
|
it is not auto-enabled merely because the runtime advertises it. The parent
|
||||||
|
provided its availability; the device notes list extensions non-exhaustively.
|
||||||
|
|
||||||
|
The registry's `XR_VERSION_1_1` feature introduces 13 profile paths. For
|
||||||
|
Samsung Odyssey, HP mixed reality, HTC Cosmos/Focus3, ML2 and the three
|
||||||
|
ByteDance Pico profiles, drop a whole suggestion call with success when the
|
||||||
|
corresponding extension is absent. Keep it when the extension is enabled.
|
||||||
|
Drop the five new Meta paths: `touch_pro_controller`, `touch_plus_controller`,
|
||||||
|
`touch_controller_rift_cv1`, `touch_controller_quest_1_rift_s`, and
|
||||||
|
`touch_controller_quest_2`. The verified runtime notes do not list these;
|
||||||
|
Pro/Plus promotion also renames several components, so the old extension's
|
||||||
|
presence alone does not prove support for the new profile. This layer does
|
||||||
|
not implement those component conversions. Existing old Pro/Plus profile
|
||||||
|
paths are passed through if the app uses them.
|
||||||
|
|
||||||
|
Preserve the documented `oculus/touch_controller`, `khr/simple_controller`,
|
||||||
|
`valve/frame_controller` and original PC profile paths. The notes' "usual PC
|
||||||
|
controllers" is not a complete enumerated list: availability of promoted PC
|
||||||
|
profiles is inferred from the runtime's extension advertisement, not invented
|
||||||
|
from that phrase. OpenXR has no general profile enumeration API. Dropping
|
||||||
|
suggestions prevents an unsupported 1.1 profile from aborting initialization;
|
||||||
|
it does not create bindings, so the app must also suggest a supported fallback.
|
||||||
|
|
||||||
|
## Headers and licensing
|
||||||
|
|
||||||
|
The required public headers (`openxr.h`, `openxr_platform.h`,
|
||||||
|
`openxr_platform_defines.h`) are unmodified from
|
||||||
|
[OpenXR-SDK release-1.1.63](https://github.com/KhronosGroup/OpenXR-SDK/tree/release-1.1.63/include/openxr),
|
||||||
|
commit [f2448a8](https://github.com/KhronosGroup/OpenXR-SDK/commit/f2448a8797c85814aa892efc1ab8707900fbcc78).
|
||||||
|
The requested filename `loader_interfaces.h` no longer exists in 1.1 SDK
|
||||||
|
releases: negotiation was ratified and moved to `openxr_loader_negotiation.h`
|
||||||
|
in 1.0.33 ([spec history L166–186](https://github.com/KhronosGroup/OpenXR-Docs/blob/release-1.1.63/specification/sources/chapters/versions.adoc#L166)).
|
||||||
|
To retain that requested interface filename, the layer uses the unmodified,
|
||||||
|
ABI-compatible [last legacy header from SDK-Source release-1.0.32](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.32/src/common/loader_interfaces.h).
|
||||||
|
Only these four headers are vendored. They offer Apache-2.0 OR MIT; this
|
||||||
|
vendoring uses Apache-2.0, reproduced in [vendor/LICENSE](vendor/LICENSE).
|
||||||
|
|
||||||
|
## Android discovery and coexistence
|
||||||
|
|
||||||
|
**Minimum asset-discovery loader release: 1.0.25 (2022-09-02).** Its
|
||||||
|
[release commit, 15c3d8e](https://github.com/KhronosGroup/OpenXR-SDK-Source/commit/15c3d8eb99994e5365d6b6f96eefaf4c51a65a9d)
|
||||||
|
explicitly announces APK-packaged API layers;
|
||||||
|
[manifest_file.cpp L665–723](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.25/src/loader/manifest_file.cpp#L665)
|
||||||
|
implements discovery, and [L934–940](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.0.25/src/loader/manifest_file.cpp#L934)
|
||||||
|
adds asset manifests after filesystem manifests. This is not a 1.1-only feature.
|
||||||
|
|
||||||
|
**For an app requesting API 1.1, use a 1.1 loader (first release 1.1.36) or
|
||||||
|
newer.** A 1.0 loader rejects the request before the layer can downgrade it;
|
||||||
|
see [loader_core.cpp L217–230](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/loader_core.cpp#L217).
|
||||||
|
The runtime can remain 1.0.
|
||||||
|
|
||||||
|
Source inspection at release-1.1.63 confirms:
|
||||||
|
|
||||||
|
- [manifest_file.cpp L720–783](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/manifest_file.cpp#L720)
|
||||||
|
uses the initialized Android asset manager and scans
|
||||||
|
`openxr/1/api_layers/implicit.d/` for JSON. The application must initialize
|
||||||
|
the loader with its Android context (`xrInitializeLoaderKHR`). In the APK,
|
||||||
|
the entry is `assets/openxr/1/api_layers/implicit.d/XrApiLayer_FRAME_compat.json`.
|
||||||
|
- A bare `library_path`, as used here, is **not explicitly concatenated** with
|
||||||
|
`nativeLibraryDir`. [L878–900](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/manifest_file.cpp#L878)
|
||||||
|
leaves a bare name for normal dynamic-linker search in the application's
|
||||||
|
namespace (including its native library directory). Relative paths with a
|
||||||
|
slash use [LocateLibraryInAssets L943–952](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/manifest_file.cpp#L943),
|
||||||
|
which resolves against `GetAndroidNativeLibraryDir()`.
|
||||||
|
[loader_init_data.cpp L87–96](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/loader_init_data.cpp#L87)
|
||||||
|
obtains the asset manager and `ApplicationInfo.nativeLibraryDir` via JNI.
|
||||||
|
Wolvic's supplied manifest has `android:extractNativeLibs="true"`.
|
||||||
|
- [android_utilities.cpp L267–322](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/android_utilities.cpp#L267)
|
||||||
|
handles runtime broker discovery, not APK layer discovery. Its
|
||||||
|
`native_lib_dir + so_filename` and `dlopen` refer to the runtime package.
|
||||||
|
They must not be confused with the app's layer library resolution.
|
||||||
|
- [manifest_file.cpp L1008–1023](https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/release-1.1.63/src/loader/manifest_file.cpp#L1008)
|
||||||
|
retains filesystem manifests and then appends asset manifests. This leaves
|
||||||
|
`/vendor`'s `XR_APILAYER_VALVE_fdm_injection` discoverable. This layer advances
|
||||||
|
`nextInfo` exactly once, uses next-layer GIPA, preserves the rest of the
|
||||||
|
create-info chain and never opens `vrclient.so` directly. No environment
|
||||||
|
layer-list override or assumed ordering is needed.
|
||||||
|
|
||||||
|
`DISABLE_FRAME_XR_COMPAT` is the manifest's disable environment variable.
|
||||||
|
Leave it unset to activate the implicit layer. No `enable_environment` is
|
||||||
|
required.
|
||||||
|
|
||||||
|
## Rebuild and verify
|
||||||
|
|
||||||
|
NDK **r30 / 30.0.16248370**, installed at
|
||||||
|
`~/Library/Android/ndk/30.0.16248370`, was the latest stable/LTS package shown
|
||||||
|
by the [Android download page](https://developer.android.com/ndk/downloads)
|
||||||
|
on 2026-09-28. Downloaded
|
||||||
|
[android-ndk-r30-darwin.dmg](https://dl.google.com/android/repository/android-ndk-r30-darwin.dmg)
|
||||||
|
(1,072,970,961 bytes). The page publishes SHA-1, not SHA-256:
|
||||||
|
|
||||||
|
```
|
||||||
|
expected: 48591224b6657f46eebbc9d95d4af09bbed8d107
|
||||||
|
actual: 48591224b6657f46eebbc9d95d4af09bbed8d107 (PASS)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mounted read-only with `hdiutil`; copied
|
||||||
|
`AndroidNDK16248370.app/Contents/NDK` into that version directory. Despite the
|
||||||
|
`darwin-x86_64` toolchain directory name, clang is universal arm64/x86_64 and
|
||||||
|
ran on this Apple Silicon Mac.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
frame/openxr-compat/build.sh
|
||||||
|
# Or set ANDROID_NDK_HOME to an installed NDK.
|
||||||
|
cmake -S frame/openxr-compat -B frame/openxr-compat/build-host -G Ninja
|
||||||
|
cmake --build frame/openxr-compat/build-host
|
||||||
|
ctest --test-dir frame/openxr-compat/build-host --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
The script produces arm64-v8a / android-24, C++17, `-O2`, static libc++, hidden
|
||||||
|
internal symbols, stripped output, and `-Wl,-z,max-page-size=16384`.
|
||||||
|
The committed prebuilt is `prebuilt/arm64-v8a/libXrApiLayer_FRAME_compat.so`.
|
||||||
|
Its SHA-256 is recorded below with the APK checks.
|
||||||
|
|
||||||
|
[Host test output](evidence/ctest.txt): two tests passed, covering pure logic
|
||||||
|
and the actual layer with a fake next layer and host-only log/JNI shims.
|
||||||
|
[Full llvm-readelf -d -s -l output](evidence/readelf.txt) records three LOAD
|
||||||
|
segments aligned `0x4000`, only `xrNegotiateLoaderApiLayerInterface` exported,
|
||||||
|
and only `libc.so`, `libm.so`, `libdl.so`, `liblog.so` as NEEDED libraries.
|
||||||
|
All 46 undefined imports are versioned `@LIBC` except `__android_log_print`.
|
||||||
|
There is no `libc++_shared.so` dependency and no unstripped `.symtab`.
|
||||||
|
|
||||||
|
No independent model review was run: the task explicitly prohibits delegation.
|
||||||
|
No `CODING_STANDARDS.md` exists in this worktree or the primary worktree.
|
||||||
|
|
||||||
|
## Wolvic injection artifact (Mac only)
|
||||||
|
|
||||||
|
Copied `/tmp/vrapk/wq-dec` to `/tmp/vrapk/wq-layer-dec`; retained its existing
|
||||||
|
LAUNCHER fix and native-library extraction setting. Added only:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/openxr/1/api_layers/implicit.d/XrApiLayer_FRAME_compat.json
|
||||||
|
lib/arm64-v8a/libXrApiLayer_FRAME_compat.so
|
||||||
|
```
|
||||||
|
|
||||||
|
Built using `/tmp/vrapk/jdk/Contents/Home/bin/java` and
|
||||||
|
`/tmp/vrapk/apktool_3.0.3.jar`, then signed in place using
|
||||||
|
`/tmp/vrapk/uber-apk-signer-1.3.0.jar --overwrite` (embedded debug certificate):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/tmp/vrapk/jdk/Contents/Home/bin/java -jar /tmp/vrapk/apktool_3.0.3.jar \
|
||||||
|
b /tmp/vrapk/wq-layer-dec -o /tmp/vrapk/wolvic-quest-compat.apk
|
||||||
|
/tmp/vrapk/jdk/Contents/Home/bin/java -jar /tmp/vrapk/uber-apk-signer-1.3.0.jar \
|
||||||
|
-a /tmp/vrapk/wolvic-quest-compat.apk --overwrite
|
||||||
|
/tmp/vrapk/jdk/Contents/Home/bin/java -jar /tmp/vrapk/uber-apk-signer-1.3.0.jar \
|
||||||
|
-a /tmp/vrapk/wolvic-quest-compat.apk --onlyVerify
|
||||||
|
```
|
||||||
|
|
||||||
|
Final APK: `/tmp/vrapk/wolvic-quest-compat.apk` (not committed).
|
||||||
|
The bundled `lib/arm64-v8a/libopenxr_loader.so` is byte-for-byte unchanged.
|
||||||
|
Its strings include both `openxr/1/api_layers/implicit.d/` and
|
||||||
|
`openxr/1/api_layers/explicit.d/`, `AddManifestFilesAndroid` error messages,
|
||||||
|
and `xrLocateSpaces`. Thus it contains APK asset discovery and evidence of
|
||||||
|
1.1 command support. Its exact upstream release number was not established
|
||||||
|
from the binary; runtime execution of those paths remains unverified.
|
||||||
|
|
||||||
|
Prebuilt library SHA-256:
|
||||||
|
|
||||||
|
```
|
||||||
|
479d31c374f137906e03f73209b581d65d7e6a41b8476e6425fa55a6aa40bd68
|
||||||
|
```
|
||||||
|
|
||||||
|
APK SHA-256:
|
||||||
|
|
||||||
|
```
|
||||||
|
355a681625e38b71563dcffecb031799eff27894d6ab910a659bace057e82c1f
|
||||||
|
```
|
||||||
|
|
||||||
|
Final `--onlyVerify` exited 0: zip alignment verified, v2/v3 signatures
|
||||||
|
verified, one APK processed and zero errors. ZIP readback confirmed that the
|
||||||
|
injected library and manifest match the committed inputs, the loader is
|
||||||
|
unchanged, and the binary Android manifest retains the LAUNCHER category.
|
||||||
|
See [APK verification evidence](evidence/apk-verification.txt).
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"file_format_version": "1.0.0",
|
||||||
|
"api_layer": {
|
||||||
|
"name": "XR_APILAYER_FRAME_compat",
|
||||||
|
"library_path": "libXrApiLayer_FRAME_compat.so",
|
||||||
|
"api_version": "1.1",
|
||||||
|
"implementation_version": "1",
|
||||||
|
"description": "Steam Frame OpenXR 1.1 to 1.0 compatibility",
|
||||||
|
"disable_environment": "DISABLE_FRAME_XR_COMPAT",
|
||||||
|
"instance_extensions": [
|
||||||
|
{
|
||||||
|
"name": "XR_KHR_android_thread_settings",
|
||||||
|
"extension_version": "6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XR_OCULUS_android_session_state_enable",
|
||||||
|
"extension_version": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
ndk=${ANDROID_NDK_HOME:-"$HOME/Library/Android/ndk/30.0.16248370"}
|
||||||
|
cmake -S "$here" -B "$here/build-android" -G Ninja \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$ndk/build/cmake/android.toolchain.cmake" \
|
||||||
|
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 \
|
||||||
|
-DANDROID_STL=c++_static -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build "$here/build-android"
|
||||||
|
mkdir -p "$here/prebuilt/arm64-v8a"
|
||||||
|
cp "$here/build-android/libXrApiLayer_FRAME_compat.so" "$here/prebuilt/arm64-v8a/"
|
||||||
|
shasum -a 256 "$here/prebuilt/arm64-v8a/libXrApiLayer_FRAME_compat.so"
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <openxr/openxr.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace frame {
|
||||||
|
using Names = std::vector<std::string>;
|
||||||
|
inline constexpr const char* layerName = "XR_APILAYER_FRAME_compat";
|
||||||
|
inline constexpr const char* stubs[] = {"XR_KHR_android_thread_settings", "XR_OCULUS_android_session_state_enable"};
|
||||||
|
// OpenXR-Docs release-1.1.63 registry: promotedto="XR_VERSION_1_1".
|
||||||
|
inline constexpr const char* promotions[] = {
|
||||||
|
"XR_KHR_locate_spaces", "XR_EXT_local_floor", "XR_EXT_uuid", "XR_EXT_palm_pose",
|
||||||
|
"XR_VARJO_quad_views", "XR_EXT_samsung_odyssey_controller", "XR_EXT_hp_mixed_reality_controller",
|
||||||
|
"XR_HTC_vive_cosmos_controller_interaction", "XR_HTC_vive_focus3_controller_interaction",
|
||||||
|
"XR_ML_ml2_controller_interaction", "XR_FB_touch_controller_pro", "XR_META_touch_controller_plus",
|
||||||
|
"XR_BD_controller_interaction", "XR_KHR_maintenance1"};
|
||||||
|
inline bool has(const Names& names, const std::string& n) {
|
||||||
|
return std::find(names.begin(), names.end(), n) != names.end();
|
||||||
|
}
|
||||||
|
inline bool downgrade(XrVersion v) { return v >= XR_MAKE_VERSION(1, 1, 0); }
|
||||||
|
inline XrVersion runtimeVersion(XrVersion v) { return downgrade(v) ? XR_API_VERSION_1_0 : v; }
|
||||||
|
struct ExtensionPlan { Names downstream, stubbed, missing, added; };
|
||||||
|
inline ExtensionPlan extensions(const Names& requested, const Names& available, bool promote) {
|
||||||
|
ExtensionPlan p;
|
||||||
|
for (const auto& n : requested) {
|
||||||
|
if (!has(available, n) && (n == stubs[0] || n == stubs[1])) p.stubbed.push_back(n);
|
||||||
|
else {
|
||||||
|
if (!has(p.downstream, n)) p.downstream.push_back(n);
|
||||||
|
if (!has(available, n)) p.missing.push_back(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (promote) for (auto n : promotions) if (has(available, n) && !has(p.downstream, n)) {
|
||||||
|
p.downstream.push_back(n); p.added.push_back(n);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
// 1.1's grip_surface is XR_EXT_palm_pose's palm_ext renamed (same pose), not the grip pose.
|
||||||
|
inline std::string rewritePath(std::string p, bool palm) {
|
||||||
|
const std::string suffix = "/input/grip_surface/pose";
|
||||||
|
if (palm && (p == "/user/hand/left" + suffix || p == "/user/hand/right" + suffix))
|
||||||
|
p.replace(p.size() - suffix.size(), suffix.size(), "/input/palm_ext/pose");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
// Profiles added to 1.1. The three legacy Meta splits have no 1.0 extension.
|
||||||
|
// The renamed Pro/Plus profiles have different component semantics; do not
|
||||||
|
// claim the old extension implements the new profile merely because it exists.
|
||||||
|
inline bool dropProfile(const std::string& p, const Names& enabled) {
|
||||||
|
for (auto name : {"touch_pro_controller", "touch_plus_controller", "touch_controller_rift_cv1",
|
||||||
|
"touch_controller_quest_1_rift_s", "touch_controller_quest_2"})
|
||||||
|
if (p == std::string("/interaction_profiles/meta/") + name) return true;
|
||||||
|
const char* profiles[][2] = {
|
||||||
|
{"samsung/odyssey_controller", "XR_EXT_samsung_odyssey_controller"},
|
||||||
|
{"hp/mixed_reality_controller", "XR_EXT_hp_mixed_reality_controller"},
|
||||||
|
{"htc/vive_cosmos_controller", "XR_HTC_vive_cosmos_controller_interaction"},
|
||||||
|
{"htc/vive_focus3_controller", "XR_HTC_vive_focus3_controller_interaction"},
|
||||||
|
{"ml/ml2_controller", "XR_ML_ml2_controller_interaction"},
|
||||||
|
{"bytedance/pico_neo3_controller", "XR_BD_controller_interaction"},
|
||||||
|
{"bytedance/pico4_controller", "XR_BD_controller_interaction"},
|
||||||
|
{"bytedance/pico_g3_controller", "XR_BD_controller_interaction"}};
|
||||||
|
for (auto& entry : profiles) if (p == std::string("/interaction_profiles/") + entry[0])
|
||||||
|
return !has(enabled, entry[1]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inline XrStructureType extensionType(XrStructureType t) {
|
||||||
|
switch (t) {
|
||||||
|
case XR_TYPE_SPACES_LOCATE_INFO: return XR_TYPE_SPACES_LOCATE_INFO_KHR;
|
||||||
|
case XR_TYPE_SPACE_LOCATIONS: return XR_TYPE_SPACE_LOCATIONS_KHR;
|
||||||
|
case XR_TYPE_SPACE_VELOCITIES: return XR_TYPE_SPACE_VELOCITIES_KHR;
|
||||||
|
default: return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline XrResult locate(PFN_xrLocateSpacesKHR next, XrSession session,
|
||||||
|
const XrSpacesLocateInfo* info, XrSpaceLocations* locations) {
|
||||||
|
if (!info || !locations) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
static_assert(XR_TYPE_SPACE_VELOCITIES == XR_TYPE_SPACE_VELOCITIES_KHR);
|
||||||
|
static_assert(XR_TYPE_SPACE_LOCATIONS == XR_TYPE_SPACE_LOCATIONS_KHR);
|
||||||
|
static_assert(XR_TYPE_SPACES_LOCATE_INFO == XR_TYPE_SPACES_LOCATE_INFO_KHR);
|
||||||
|
auto in = *info;
|
||||||
|
auto out = *locations;
|
||||||
|
in.type = extensionType(in.type); out.type = extensionType(out.type);
|
||||||
|
// The enums and typedefs are exact aliases. All chained velocities,
|
||||||
|
// including unknown next structures, can be forwarded without mutation.
|
||||||
|
const auto result = next(session, &in, &out);
|
||||||
|
locations->locationCount = out.locationCount;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
2026-09-28, Mac only; no device connection.
|
||||||
|
APK: /tmp/vrapk/wolvic-quest-compat.apk
|
||||||
|
SHA-256: 355a681625e38b71563dcffecb031799eff27894d6ab910a659bace057e82c1f
|
||||||
|
|
||||||
|
Command:
|
||||||
|
/tmp/vrapk/jdk/Contents/Home/bin/java -jar /tmp/vrapk/uber-apk-signer-1.3.0.jar -a /tmp/vrapk/wolvic-quest-compat.apk --onlyVerify
|
||||||
|
Exit status: 0
|
||||||
|
Relevant output:
|
||||||
|
- zipalign verified
|
||||||
|
- signature verified [v2, v3]
|
||||||
|
Successfully processed 1 APKs and 0 errors in 0.63 seconds.
|
||||||
|
|
||||||
|
ZIP readback checks: PASS
|
||||||
|
- Injected JSON identical to frame/openxr-compat/XrApiLayer_FRAME_compat.json.
|
||||||
|
- Injected SO identical to prebuilt/arm64-v8a/libXrApiLayer_FRAME_compat.so.
|
||||||
|
- Existing libopenxr_loader.so identical to /tmp/vrapk/wq-dec version.
|
||||||
|
- Binary AndroidManifest.xml contains android.intent.category.LAUNCHER.
|
||||||
|
|
||||||
|
Loader strings present:
|
||||||
|
openxr/1/api_layers/implicit.d/
|
||||||
|
openxr/1/api_layers/explicit.d/
|
||||||
|
ApiLayerManifestFile::AddManifestFilesAndroid unable to open asset
|
||||||
|
xrLocateSpaces
|
||||||
|
|
||||||
|
NDK: 30.0.16248370 (r30)
|
||||||
|
DMG size: 1072970961 bytes
|
||||||
|
Download-page SHA-1: 48591224b6657f46eebbc9d95d4af09bbed8d107
|
||||||
|
Measured SHA-1: 48591224b6657f46eebbc9d95d4af09bbed8d107
|
||||||
|
SHA check: PASS
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Test project /Users/saphid/projects/steam-frame-xrlayer/frame/openxr-compat/build-host
|
||||||
|
Start 1: compat_logic
|
||||||
|
1/2 Test #1: compat_logic ..................... Passed 0.01 sec
|
||||||
|
Start 2: layer_dispatch
|
||||||
|
2/2 Test #2: layer_dispatch ................... Passed 0.42 sec
|
||||||
|
|
||||||
|
100% tests passed out of 2
|
||||||
|
|
||||||
|
Total Test time (real) = 0.43 sec
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
Elf file type is DYN (Shared object file)
|
||||||
|
Entry point 0x0
|
||||||
|
There are 9 program headers, starting at offset 64
|
||||||
|
|
||||||
|
Program Headers:
|
||||||
|
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
|
||||||
|
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0001f8 0x0001f8 R 0x8
|
||||||
|
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x0169e0 0x0169e0 R E 0x4000
|
||||||
|
LOAD 0x0169e0 0x000000000001a9e0 0x000000000001a9e0 0x000a90 0x001620 RW 0x4000
|
||||||
|
LOAD 0x017470 0x000000000001f470 0x000000000001f470 0x000068 0x000bc0 RW 0x4000
|
||||||
|
DYNAMIC 0x017048 0x000000000001b048 0x000000000001b048 0x0001c0 0x0001c0 RW 0x8
|
||||||
|
GNU_RELRO 0x0169e0 0x000000000001a9e0 0x000000000001a9e0 0x000a90 0x001620 R 0x1
|
||||||
|
GNU_EH_FRAME 0x004fb4 0x0000000000004fb4 0x0000000000004fb4 0x00070c 0x00070c R 0x4
|
||||||
|
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0
|
||||||
|
NOTE 0x000238 0x0000000000000238 0x0000000000000238 0x0000bc 0x0000bc R 0x4
|
||||||
|
|
||||||
|
Section to Segment mapping:
|
||||||
|
Segment Sections...
|
||||||
|
00
|
||||||
|
01 .note.android.ident .note.gnu.build-id .dynsym .gnu.version .gnu.version_r .gnu.hash .dynstr .rela.dyn .rela.plt .gcc_except_table .rodata .eh_frame_hdr .eh_frame .text __lcxx_override .plt
|
||||||
|
02 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt .relro_padding
|
||||||
|
03 .data .bss
|
||||||
|
04 .dynamic
|
||||||
|
05 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt .relro_padding
|
||||||
|
06 .eh_frame_hdr
|
||||||
|
07
|
||||||
|
08 .note.android.ident .note.gnu.build-id
|
||||||
|
None .comment .shstrtab
|
||||||
|
Dynamic section at offset 0x17048 contains 28 entries:
|
||||||
|
Tag Type Name/Value
|
||||||
|
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
|
||||||
|
0x0000000000000001 (NEEDED) Shared library: [libm.so]
|
||||||
|
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
|
||||||
|
0x0000000000000001 (NEEDED) Shared library: [libc.so]
|
||||||
|
0x000000000000000e (SONAME) Library soname: [libXrApiLayer_FRAME_compat.so]
|
||||||
|
0x000000000000001e (FLAGS) BIND_NOW
|
||||||
|
0x000000006ffffffb (FLAGS_1) NOW
|
||||||
|
0x0000000000000007 (RELA) 0xae8
|
||||||
|
0x0000000000000008 (RELASZ) 5424 (bytes)
|
||||||
|
0x0000000000000009 (RELAENT) 24 (bytes)
|
||||||
|
0x000000006ffffff9 (RELACOUNT) 225
|
||||||
|
0x0000000000000017 (JMPREL) 0x2018
|
||||||
|
0x0000000000000002 (PLTRELSZ) 1080 (bytes)
|
||||||
|
0x0000000000000003 (PLTGOT) 0x1b2f0
|
||||||
|
0x0000000000000014 (PLTREL) RELA
|
||||||
|
0x0000000000000006 (SYMTAB) 0x2f8
|
||||||
|
0x000000000000000b (SYMENT) 24 (bytes)
|
||||||
|
0x0000000000000005 (STRTAB) 0x838
|
||||||
|
0x000000000000000a (STRSZ) 681 (bytes)
|
||||||
|
0x000000006ffffef5 (GNU_HASH) 0x818
|
||||||
|
0x0000000000000019 (INIT_ARRAY) 0x1b030
|
||||||
|
0x000000000000001b (INIT_ARRAYSZ) 24 (bytes)
|
||||||
|
0x000000000000001a (FINI_ARRAY) 0x1b020
|
||||||
|
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
|
||||||
|
0x000000006ffffff0 (VERSYM) 0x778
|
||||||
|
0x000000006ffffffe (VERNEED) 0x7d8
|
||||||
|
0x000000006fffffff (VERNEEDNUM) 2
|
||||||
|
0x0000000000000000 (NULL) 0x0
|
||||||
|
|
||||||
|
Symbol table '.dynsym' contains 48 entries:
|
||||||
|
Num: Value Size Type Bind Vis Ndx Name
|
||||||
|
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
|
||||||
|
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC
|
||||||
|
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC
|
||||||
|
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcmp@LIBC
|
||||||
|
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __android_log_print
|
||||||
|
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@LIBC
|
||||||
|
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memmove@LIBC
|
||||||
|
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@LIBC
|
||||||
|
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@LIBC
|
||||||
|
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __strcpy_chk@LIBC
|
||||||
|
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memset@LIBC
|
||||||
|
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcmp@LIBC
|
||||||
|
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_mutex_lock@LIBC
|
||||||
|
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_mutex_unlock@LIBC
|
||||||
|
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno@LIBC
|
||||||
|
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND snprintf@LIBC
|
||||||
|
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_mutex_destroy@LIBC
|
||||||
|
17: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strerror_r@LIBC
|
||||||
|
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND abort@LIBC
|
||||||
|
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_getspecific@LIBC
|
||||||
|
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_setspecific@LIBC
|
||||||
|
21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_key_create@LIBC
|
||||||
|
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND syscall@LIBC
|
||||||
|
23: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __sF@LIBC
|
||||||
|
24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@LIBC
|
||||||
|
25: 0000000000000000 0 FUNC GLOBAL DEFAULT UND vfprintf@LIBC
|
||||||
|
26: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fputc@LIBC
|
||||||
|
27: 0000000000000000 0 FUNC GLOBAL DEFAULT UND vasprintf@LIBC
|
||||||
|
28: 0000000000000000 0 FUNC GLOBAL DEFAULT UND android_set_abort_message@LIBC
|
||||||
|
29: 0000000000000000 0 FUNC GLOBAL DEFAULT UND openlog@LIBC
|
||||||
|
30: 0000000000000000 0 FUNC GLOBAL DEFAULT UND syslog@LIBC
|
||||||
|
31: 0000000000000000 0 FUNC GLOBAL DEFAULT UND closelog@LIBC
|
||||||
|
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@LIBC
|
||||||
|
33: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@LIBC
|
||||||
|
34: 0000000000000000 0 FUNC GLOBAL DEFAULT UND posix_memalign@LIBC
|
||||||
|
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND realloc@LIBC
|
||||||
|
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_once@LIBC
|
||||||
|
37: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_key_delete@LIBC
|
||||||
|
38: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getauxval@LIBC
|
||||||
|
39: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __system_property_get@LIBC
|
||||||
|
40: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strncmp@LIBC
|
||||||
|
41: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fprintf@LIBC
|
||||||
|
42: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fflush@LIBC
|
||||||
|
43: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_rwlock_wrlock@LIBC
|
||||||
|
44: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_rwlock_unlock@LIBC
|
||||||
|
45: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dl_iterate_phdr@LIBC
|
||||||
|
46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread_rwlock_rdlock@LIBC
|
||||||
|
47: 00000000000081a0 284 FUNC GLOBAL DEFAULT 14 xrNegotiateLoaderApiLayerInterface
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
#include "compat_logic.h"
|
||||||
|
#include <loader_interfaces.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <openxr/openxr_platform.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "FrameXrCompat", __VA_ARGS__)
|
||||||
|
#define GUARD_END catch (const std::bad_alloc&) { return XR_ERROR_OUT_OF_MEMORY; } catch (...) { return XR_ERROR_RUNTIME_FAILURE; }
|
||||||
|
namespace {
|
||||||
|
struct Dispatch {
|
||||||
|
PFN_xrGetInstanceProcAddr gipa{};
|
||||||
|
PFN_xrDestroyInstance destroy{};
|
||||||
|
PFN_xrCreateSession createSession{};
|
||||||
|
PFN_xrDestroySession destroySession{};
|
||||||
|
PFN_xrLocateSpacesKHR locate{};
|
||||||
|
PFN_xrStringToPath stringToPath{};
|
||||||
|
PFN_xrPathToString pathToString{};
|
||||||
|
PFN_xrSuggestInteractionProfileBindings suggest{};
|
||||||
|
PFN_xrSetAndroidApplicationThreadKHR thread{};
|
||||||
|
PFN_xrRequestDisplayRefreshRateFB refresh{};
|
||||||
|
frame::Names enabled, stubbed;
|
||||||
|
XrInstance instance{};
|
||||||
|
bool promote{}, palm{};
|
||||||
|
};
|
||||||
|
std::mutex mutex;
|
||||||
|
std::unordered_map<XrInstance, std::shared_ptr<Dispatch>> instances;
|
||||||
|
std::unordered_map<XrSession, std::shared_ptr<Dispatch>> sessions;
|
||||||
|
// No runtime library is opened here: every call goes through the next layer.
|
||||||
|
PFN_xrEnumerateInstanceExtensionProperties enumerateNext{};
|
||||||
|
template<class T> T proc(PFN_xrGetInstanceProcAddr gipa, XrInstance i, const char* name) {
|
||||||
|
PFN_xrVoidFunction f{};
|
||||||
|
if (XR_FAILED(gipa(i, name, &f))) return nullptr;
|
||||||
|
return reinterpret_cast<T>(f);
|
||||||
|
}
|
||||||
|
std::shared_ptr<Dispatch> get(XrInstance i) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
auto it = instances.find(i); return it == instances.end() ? nullptr : it->second;
|
||||||
|
}
|
||||||
|
std::shared_ptr<Dispatch> get(XrSession s) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
auto it = sessions.find(s); return it == sessions.end() ? nullptr : it->second;
|
||||||
|
}
|
||||||
|
XrResult properties(PFN_xrEnumerateInstanceExtensionProperties fn,
|
||||||
|
std::vector<XrExtensionProperties>& out) {
|
||||||
|
if (!fn) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||||
|
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||||
|
uint32_t n{};
|
||||||
|
auto r = fn(nullptr, 0, &n, nullptr);
|
||||||
|
if (XR_FAILED(r)) return r;
|
||||||
|
out.assign(n, {XR_TYPE_EXTENSION_PROPERTIES, nullptr, {}, 0});
|
||||||
|
if (!n) return XR_SUCCESS;
|
||||||
|
r = fn(nullptr, n, &n, out.data());
|
||||||
|
if (r == XR_ERROR_SIZE_INSUFFICIENT) continue;
|
||||||
|
if (XR_FAILED(r)) return r;
|
||||||
|
out.resize(n); return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
return XR_ERROR_RUNTIME_FAILURE;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL enumerate(const char* layer, uint32_t capacity, uint32_t* count,
|
||||||
|
XrExtensionProperties* output) try {
|
||||||
|
if (!count || (capacity && !output)) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
const bool own = layer && std::strcmp(layer, frame::layerName) == 0;
|
||||||
|
PFN_xrEnumerateInstanceExtensionProperties next;
|
||||||
|
{ std::lock_guard<std::mutex> lock(mutex); next = enumerateNext; }
|
||||||
|
if (layer && *layer && !own) return next ? next(layer, capacity, count, output) : XR_ERROR_API_LAYER_NOT_PRESENT;
|
||||||
|
std::vector<XrExtensionProperties> all;
|
||||||
|
if (!own && next) {
|
||||||
|
auto r = properties(next, all); if (XR_FAILED(r)) return r;
|
||||||
|
}
|
||||||
|
for (unsigned i = 0; i < 2; ++i) {
|
||||||
|
bool found = false;
|
||||||
|
for (auto& e : all) if (!std::strcmp(e.extensionName, frame::stubs[i])) found = true;
|
||||||
|
if (!found) {
|
||||||
|
XrExtensionProperties p{XR_TYPE_EXTENSION_PROPERTIES, nullptr, {}, i == 0 ? XR_KHR_android_thread_settings_SPEC_VERSION : 1u};
|
||||||
|
std::strcpy(p.extensionName, frame::stubs[i]); all.push_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*count = static_cast<uint32_t>(all.size());
|
||||||
|
LOG("enumerate extensions: %u (includes stubs)", *count);
|
||||||
|
if (!capacity) return XR_SUCCESS;
|
||||||
|
if (capacity < all.size()) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||||
|
for (size_t i = 0; i < all.size(); ++i) {
|
||||||
|
if (output[i].type != XR_TYPE_EXTENSION_PROPERTIES) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
std::strcpy(output[i].extensionName, all[i].extensionName);
|
||||||
|
output[i].extensionVersion = all[i].extensionVersion;
|
||||||
|
}
|
||||||
|
return XR_SUCCESS;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL destroyInstance(XrInstance i) try {
|
||||||
|
auto d = get(i); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
auto r = d->destroy(i);
|
||||||
|
if (XR_SUCCEEDED(r)) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
for (auto it = sessions.begin(); it != sessions.end();) {
|
||||||
|
if (it->second == d) it = sessions.erase(it); else ++it;
|
||||||
|
}
|
||||||
|
instances.erase(i);
|
||||||
|
if (instances.empty()) enumerateNext = nullptr;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL createSession(XrInstance i, const XrSessionCreateInfo* info, XrSession* s) try {
|
||||||
|
auto d = get(i); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
auto r = d->createSession(i, info, s);
|
||||||
|
if (XR_SUCCEEDED(r)) {
|
||||||
|
try { std::lock_guard<std::mutex> lock(mutex); sessions.emplace(*s, d); }
|
||||||
|
catch (...) { d->destroySession(*s); *s = XR_NULL_HANDLE; throw; }
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL destroySession(XrSession s) try {
|
||||||
|
auto d = get(s); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
auto r = d->destroySession(s);
|
||||||
|
if (XR_SUCCEEDED(r)) { std::lock_guard<std::mutex> lock(mutex); sessions.erase(s); }
|
||||||
|
return r;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL locateSpaces(XrSession s, const XrSpacesLocateInfo* in, XrSpaceLocations* out) try {
|
||||||
|
auto d = get(s); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
if (!d->locate) return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||||
|
static std::once_flag logged; // called every frame
|
||||||
|
std::call_once(logged, [] { LOG("xrLocateSpaces -> xrLocateSpacesKHR (core/KHR type aliases)"); });
|
||||||
|
return frame::locate(d->locate, s, in, out);
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL threadSettings(XrSession s, XrAndroidThreadTypeKHR type, uint32_t tid) try {
|
||||||
|
auto d = get(s); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
if (frame::has(d->stubbed, frame::stubs[0])) {
|
||||||
|
LOG("stub xrSetAndroidApplicationThreadKHR type=%d tid=%u -> XR_SUCCESS (no scheduling change)", type, tid);
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
return d->thread ? d->thread(s, type, tid) : XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||||
|
} GUARD_END
|
||||||
|
// SteamVR offers only the current refresh rate; Quest apps ask for 72/90/120 Hz
|
||||||
|
// and abort on the error, so keep the current rate and report success.
|
||||||
|
XrResult XRAPI_CALL requestRefreshRate(XrSession s, float hz) try {
|
||||||
|
auto d = get(s); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
auto r = d->refresh(s, hz);
|
||||||
|
if (r == XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB) {
|
||||||
|
LOG("xrRequestDisplayRefreshRateFB %.1f Hz unsupported; keeping the current rate", hz);
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL stringToPath(XrInstance i, const char* path, XrPath* out) try {
|
||||||
|
auto d = get(i); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
if (!path) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
auto rewritten = frame::rewritePath(path, d->palm);
|
||||||
|
if (rewritten != path) LOG("path %s -> %s", path, rewritten.c_str());
|
||||||
|
return d->stringToPath(i, rewritten.c_str(), out);
|
||||||
|
} GUARD_END
|
||||||
|
XrResult pathString(const Dispatch& d, XrPath path, std::string& out) {
|
||||||
|
uint32_t n{};
|
||||||
|
auto r = d.pathToString(d.instance, path, 0, &n, nullptr);
|
||||||
|
if (XR_FAILED(r)) return r;
|
||||||
|
std::vector<char> text(n);
|
||||||
|
r = d.pathToString(d.instance, path, n, &n, text.data());
|
||||||
|
if (XR_SUCCEEDED(r)) out.assign(text.data());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL suggest(XrInstance i, const XrInteractionProfileSuggestedBinding* info) try {
|
||||||
|
auto d = get(i); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
if (!info || (info->countSuggestedBindings && !info->suggestedBindings)) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
std::string profile;
|
||||||
|
auto r = pathString(*d, info->interactionProfile, profile);
|
||||||
|
if (XR_FAILED(r)) return r;
|
||||||
|
if (d->promote && frame::dropProfile(profile, d->enabled)) {
|
||||||
|
LOG("drop unsupported 1.1 interaction profile %s (%u suggestions)", profile.c_str(), info->countSuggestedBindings);
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
auto copy = *info;
|
||||||
|
std::vector<XrActionSuggestedBinding> bindings;
|
||||||
|
for (uint32_t index = 0; index < info->countSuggestedBindings; ++index) {
|
||||||
|
auto b = info->suggestedBindings[index];
|
||||||
|
std::string path;
|
||||||
|
r = pathString(*d, b.binding, path); if (XR_FAILED(r)) return r;
|
||||||
|
auto rewritten = frame::rewritePath(path, d->palm);
|
||||||
|
if (rewritten != path) {
|
||||||
|
LOG("binding %s -> %s", path.c_str(), rewritten.c_str());
|
||||||
|
r = d->stringToPath(i, rewritten.c_str(), &b.binding); if (XR_FAILED(r)) return r;
|
||||||
|
}
|
||||||
|
bindings.push_back(b);
|
||||||
|
}
|
||||||
|
copy.suggestedBindings = bindings.data();
|
||||||
|
return d->suggest(i, ©);
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL gipa(XrInstance, const char*, PFN_xrVoidFunction*);
|
||||||
|
XrResult XRAPI_CALL createLayer(const XrInstanceCreateInfo* info, const XrApiLayerCreateInfo* layer,
|
||||||
|
XrInstance* instance) try {
|
||||||
|
if (!info || !instance || !layer || layer->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_CREATE_INFO ||
|
||||||
|
layer->structVersion != XR_API_LAYER_CREATE_INFO_STRUCT_VERSION || layer->structSize != sizeof(*layer) ||
|
||||||
|
!layer->nextInfo || layer->nextInfo->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_NEXT_INFO ||
|
||||||
|
layer->nextInfo->structVersion != XR_API_LAYER_NEXT_INFO_STRUCT_VERSION ||
|
||||||
|
layer->nextInfo->structSize != sizeof(XrApiLayerNextInfo) ||
|
||||||
|
!layer->nextInfo->nextGetInstanceProcAddr || !layer->nextInfo->nextCreateApiLayerInstance ||
|
||||||
|
(info->enabledExtensionCount && !info->enabledExtensionNames)) return XR_ERROR_INITIALIZATION_FAILED;
|
||||||
|
*instance = XR_NULL_HANDLE;
|
||||||
|
auto next = layer->nextInfo->nextGetInstanceProcAddr;
|
||||||
|
auto enumFn = proc<PFN_xrEnumerateInstanceExtensionProperties>(next, XR_NULL_HANDLE, "xrEnumerateInstanceExtensionProperties");
|
||||||
|
std::vector<XrExtensionProperties> available;
|
||||||
|
auto r = properties(enumFn, available); if (XR_FAILED(r)) { LOG("runtime extension enumeration failed: %d", r); return r; }
|
||||||
|
frame::Names requested, supported;
|
||||||
|
for (auto& p : available) supported.emplace_back(p.extensionName);
|
||||||
|
for (uint32_t n = 0; n < info->enabledExtensionCount; ++n) {
|
||||||
|
if (!info->enabledExtensionNames[n]) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
requested.emplace_back(info->enabledExtensionNames[n]);
|
||||||
|
}
|
||||||
|
auto d = std::make_shared<Dispatch>();
|
||||||
|
d->promote = frame::downgrade(info->applicationInfo.apiVersion);
|
||||||
|
auto plan = frame::extensions(requested, supported, d->promote);
|
||||||
|
for (auto& n : plan.added) LOG("enable promoted extension %s", n.c_str());
|
||||||
|
for (auto& n : plan.stubbed) LOG("emulate missing extension %s; remove downstream", n.c_str());
|
||||||
|
for (auto& n : plan.missing) LOG("unsupported extension %s retained; runtime must reject", n.c_str());
|
||||||
|
auto copy = *info;
|
||||||
|
copy.applicationInfo.apiVersion = frame::runtimeVersion(info->applicationInfo.apiVersion);
|
||||||
|
if (d->promote) LOG("API %u.%u.%u -> 1.0.%u", unsigned(XR_VERSION_MAJOR(info->applicationInfo.apiVersion)),
|
||||||
|
unsigned(XR_VERSION_MINOR(info->applicationInfo.apiVersion)), unsigned(XR_VERSION_PATCH(info->applicationInfo.apiVersion)),
|
||||||
|
unsigned(XR_VERSION_PATCH(copy.applicationInfo.apiVersion)));
|
||||||
|
std::vector<const char*> names;
|
||||||
|
for (auto& n : plan.downstream) names.push_back(n.c_str());
|
||||||
|
copy.enabledExtensionCount = static_cast<uint32_t>(names.size()); copy.enabledExtensionNames = names.data();
|
||||||
|
auto chain = *layer; chain.nextInfo = layer->nextInfo->next;
|
||||||
|
r = layer->nextInfo->nextCreateApiLayerInstance(©, &chain, instance);
|
||||||
|
if (XR_FAILED(r)) { LOG("downstream xrCreateInstance -> %d", r); return r; }
|
||||||
|
d->gipa = next; d->instance = *instance;
|
||||||
|
d->destroy = proc<PFN_xrDestroyInstance>(next, *instance, "xrDestroyInstance");
|
||||||
|
// A successful runtime instance must expose xrDestroyInstance.
|
||||||
|
if (!d->destroy) { // nothing could ever destroy it, so don't hand it out
|
||||||
|
LOG("invalid downstream: missing xrDestroyInstance"); *instance = XR_NULL_HANDLE; return XR_ERROR_INITIALIZATION_FAILED;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
d->enabled = std::move(plan.downstream); d->stubbed = std::move(plan.stubbed);
|
||||||
|
d->palm = frame::has(d->enabled, "XR_EXT_palm_pose");
|
||||||
|
#define LOAD(field, name) d->field = proc<PFN_##name>(next, *instance, #name)
|
||||||
|
LOAD(createSession, xrCreateSession); LOAD(destroySession, xrDestroySession);
|
||||||
|
LOAD(stringToPath, xrStringToPath); LOAD(pathToString, xrPathToString);
|
||||||
|
LOAD(suggest, xrSuggestInteractionProfileBindings);
|
||||||
|
if (frame::has(d->enabled, "XR_KHR_locate_spaces")) LOAD(locate, xrLocateSpacesKHR);
|
||||||
|
if (frame::has(d->enabled, frame::stubs[0])) LOAD(thread, xrSetAndroidApplicationThreadKHR);
|
||||||
|
if (frame::has(d->enabled, "XR_FB_display_refresh_rate")) LOAD(refresh, xrRequestDisplayRefreshRateFB);
|
||||||
|
#undef LOAD
|
||||||
|
if (!d->createSession || !d->destroySession || !d->stringToPath || !d->pathToString || !d->suggest) {
|
||||||
|
d->destroy(*instance); *instance = XR_NULL_HANDLE; return XR_ERROR_INITIALIZATION_FAILED;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
instances.emplace(*instance, d); enumerateNext = enumFn;
|
||||||
|
} catch (...) { d->destroy(*instance); *instance = XR_NULL_HANDLE; throw; }
|
||||||
|
LOG("created instance; next-layer dispatch retained (Valve layer coexists)");
|
||||||
|
return r;
|
||||||
|
} GUARD_END
|
||||||
|
XrResult XRAPI_CALL gipa(XrInstance i, const char* name, PFN_xrVoidFunction* out) try {
|
||||||
|
if (!name || !out) return XR_ERROR_VALIDATION_FAILURE;
|
||||||
|
*out = nullptr;
|
||||||
|
#define RETURN_PROC(n, fn) if (!std::strcmp(name, n)) { *out = reinterpret_cast<PFN_xrVoidFunction>(fn); return XR_SUCCESS; }
|
||||||
|
RETURN_PROC("xrGetInstanceProcAddr", gipa)
|
||||||
|
RETURN_PROC("xrCreateApiLayerInstance", createLayer)
|
||||||
|
RETURN_PROC("xrEnumerateInstanceExtensionProperties", enumerate)
|
||||||
|
auto d = get(i); if (!d) return XR_ERROR_HANDLE_INVALID;
|
||||||
|
RETURN_PROC("xrDestroyInstance", destroyInstance)
|
||||||
|
RETURN_PROC("xrCreateSession", createSession)
|
||||||
|
RETURN_PROC("xrDestroySession", destroySession)
|
||||||
|
RETURN_PROC("xrStringToPath", stringToPath)
|
||||||
|
RETURN_PROC("xrSuggestInteractionProfileBindings", suggest)
|
||||||
|
if (d->locate && d->promote) { RETURN_PROC("xrLocateSpaces", locateSpaces) }
|
||||||
|
if (frame::has(d->stubbed, frame::stubs[0])) { RETURN_PROC("xrSetAndroidApplicationThreadKHR", threadSettings) }
|
||||||
|
if (d->refresh) { RETURN_PROC("xrRequestDisplayRefreshRateFB", requestRefreshRate) }
|
||||||
|
#undef RETURN_PROC
|
||||||
|
// Includes xrGetInstanceProperties: report the actual runtime's identity.
|
||||||
|
return d->gipa(i, name, out);
|
||||||
|
} GUARD_END
|
||||||
|
} // namespace
|
||||||
|
extern "C" __attribute__((visibility("default"))) XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(
|
||||||
|
const XrNegotiateLoaderInfo* loader, const char* name, XrNegotiateApiLayerRequest* request) {
|
||||||
|
if (!loader || !name || !request || std::strcmp(name, frame::layerName) ||
|
||||||
|
loader->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||||
|
loader->structVersion != XR_LOADER_INFO_STRUCT_VERSION || loader->structSize != sizeof(*loader) ||
|
||||||
|
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||||
|
request->structVersion != XR_API_LAYER_INFO_STRUCT_VERSION || request->structSize != sizeof(*request) ||
|
||||||
|
loader->minInterfaceVersion > 1 || loader->maxInterfaceVersion < 1 ||
|
||||||
|
loader->minApiVersion > XR_CURRENT_API_VERSION || loader->maxApiVersion < XR_MAKE_VERSION(1, 1, 0))
|
||||||
|
return XR_ERROR_INITIALIZATION_FAILED;
|
||||||
|
request->layerInterfaceVersion = 1;
|
||||||
|
request->layerApiVersion = std::min<XrVersion>(XR_CURRENT_API_VERSION, loader->maxApiVersion);
|
||||||
|
request->getInstanceProcAddr = gipa; request->createApiLayerInstance = createLayer;
|
||||||
|
LOG("negotiated interface 1, API 1.1");
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
|||||||
|
#include "compat_logic.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <type_traits>
|
||||||
|
#define CHECK(expr) do { if (!(expr)) { std::cerr << __LINE__ << ": " #expr "\n"; std::exit(1); } } while (false)
|
||||||
|
namespace {
|
||||||
|
void* sentinel = reinterpret_cast<void*>(0x1234);
|
||||||
|
XrResult XRAPI_CALL fakeLocate(XrSession, const XrSpacesLocateInfo* in, XrSpaceLocations* out) {
|
||||||
|
CHECK(in->type == XR_TYPE_SPACES_LOCATE_INFO_KHR);
|
||||||
|
CHECK(out->type == XR_TYPE_SPACE_LOCATIONS_KHR);
|
||||||
|
CHECK(in->next == sentinel);
|
||||||
|
auto* velocities = static_cast<XrSpaceVelocitiesKHR*>(out->next);
|
||||||
|
CHECK(velocities->type == XR_TYPE_SPACE_VELOCITIES_KHR);
|
||||||
|
CHECK(velocities->next == sentinel);
|
||||||
|
CHECK(in->spaceCount == 1 && in->time == 123);
|
||||||
|
out->locations[0].pose.position.x = 42;
|
||||||
|
velocities->velocities[0].linearVelocity.y = 7;
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int main() {
|
||||||
|
using namespace frame;
|
||||||
|
CHECK(!downgrade(XR_MAKE_VERSION(1, 0, 99)));
|
||||||
|
CHECK(runtimeVersion(XR_MAKE_VERSION(1, 0, 42)) == XR_MAKE_VERSION(1, 0, 42));
|
||||||
|
CHECK(runtimeVersion(XR_MAKE_VERSION(1, 1, 0)) == XR_API_VERSION_1_0);
|
||||||
|
CHECK(runtimeVersion(XR_MAKE_VERSION(1, 1, 999)) == XR_API_VERSION_1_0);
|
||||||
|
CHECK(runtimeVersion(XR_MAKE_VERSION(2, 0, 0)) == XR_API_VERSION_1_0);
|
||||||
|
Names available{"XR_KHR_locate_spaces", "XR_EXT_palm_pose", "XR_EXT_hand_interaction", "XR_VARJO_quad_views"};
|
||||||
|
Names requested{stubs[0], stubs[1], "XR_MISSING_test", "XR_EXT_palm_pose"};
|
||||||
|
auto p = extensions(requested, available, true);
|
||||||
|
CHECK(p.stubbed.size() == 2 && p.missing == Names{"XR_MISSING_test"});
|
||||||
|
CHECK(has(p.downstream, "XR_MISSING_test"));
|
||||||
|
CHECK(has(p.downstream, "XR_KHR_locate_spaces") && has(p.downstream, "XR_VARJO_quad_views"));
|
||||||
|
CHECK(!has(p.downstream, "XR_EXT_hand_interaction"));
|
||||||
|
CHECK(!has(p.downstream, "XR_EXT_local_floor"));
|
||||||
|
CHECK(std::count(p.downstream.begin(), p.downstream.end(), "XR_EXT_palm_pose") == 1);
|
||||||
|
available.push_back(stubs[0]);
|
||||||
|
CHECK(extensions(requested, available, true).stubbed == Names{stubs[1]});
|
||||||
|
CHECK(extensions({}, available, false).downstream.empty());
|
||||||
|
CHECK(rewritePath("/user/hand/left/input/grip_surface/pose", true) == "/user/hand/left/input/palm_ext/pose");
|
||||||
|
CHECK(rewritePath("/user/hand/right/input/grip_surface/pose", true) == "/user/hand/right/input/palm_ext/pose");
|
||||||
|
CHECK(rewritePath("/user/hand/left/input/grip_surface/pose", false) == "/user/hand/left/input/grip_surface/pose");
|
||||||
|
CHECK(rewritePath("/user/hand/left/input/grip_surface/pose/extra", true) == "/user/hand/left/input/grip_surface/pose/extra");
|
||||||
|
CHECK(rewritePath("/user/hand/left/input/grip/pose", true) == "/user/hand/left/input/grip/pose");
|
||||||
|
CHECK(dropProfile("/interaction_profiles/meta/touch_pro_controller", {}));
|
||||||
|
CHECK(dropProfile("/interaction_profiles/meta/touch_controller_quest_2", {}));
|
||||||
|
CHECK(dropProfile("/interaction_profiles/hp/mixed_reality_controller", {}));
|
||||||
|
CHECK(!dropProfile("/interaction_profiles/hp/mixed_reality_controller", {"XR_EXT_hp_mixed_reality_controller"}));
|
||||||
|
CHECK(!dropProfile("/interaction_profiles/oculus/touch_controller", {}));
|
||||||
|
CHECK(!dropProfile("/interaction_profiles/khr/simple_controller", {}));
|
||||||
|
CHECK(!dropProfile("/interaction_profiles/valve/frame_controller", {}));
|
||||||
|
CHECK(!dropProfile("/interaction_profiles/ext/hand_interaction_ext", {}));
|
||||||
|
CHECK(extensionType(XR_TYPE_SPACES_LOCATE_INFO) == XR_TYPE_SPACES_LOCATE_INFO_KHR);
|
||||||
|
CHECK(extensionType(XR_TYPE_SPACE_LOCATIONS) == XR_TYPE_SPACE_LOCATIONS_KHR);
|
||||||
|
CHECK(extensionType(XR_TYPE_SPACE_VELOCITIES) == XR_TYPE_SPACE_VELOCITIES_KHR);
|
||||||
|
CHECK(extensionType(XR_TYPE_INSTANCE_CREATE_INFO) == XR_TYPE_INSTANCE_CREATE_INFO);
|
||||||
|
static_assert(std::is_same_v<XrSpaceVelocities, XrSpaceVelocitiesKHR>);
|
||||||
|
XrSpace space{};
|
||||||
|
XrSpaceLocationData data{};
|
||||||
|
XrSpaceVelocityData velocity{};
|
||||||
|
XrSpaceVelocities velocities{XR_TYPE_SPACE_VELOCITIES, sentinel, 1, &velocity};
|
||||||
|
XrSpaceLocations locations{XR_TYPE_SPACE_LOCATIONS, &velocities, 1, &data};
|
||||||
|
XrSpacesLocateInfo info{XR_TYPE_SPACES_LOCATE_INFO, sentinel, XR_NULL_HANDLE, 123, 1, &space};
|
||||||
|
CHECK(locate(fakeLocate, XR_NULL_HANDLE, &info, &locations) == XR_SUCCESS);
|
||||||
|
CHECK(data.pose.position.x == 42 && velocity.linearVelocity.y == 7);
|
||||||
|
CHECK(info.next == sentinel && locations.next == &velocities && velocities.next == sentinel);
|
||||||
|
CHECK(info.type == XR_TYPE_SPACES_LOCATE_INFO && locations.type == XR_TYPE_SPACE_LOCATIONS && velocities.type == XR_TYPE_SPACE_VELOCITIES);
|
||||||
|
CHECK(locate(fakeLocate, XR_NULL_HANDLE, nullptr, &locations) == XR_ERROR_VALIDATION_FAILURE);
|
||||||
|
std::cout << "All version, extension, path/profile and locate-chain checks passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// Compile the actual layer with host-only JNI/logging shims, and a fake next
|
||||||
|
// layer. This exercises ABI routing without a runtime or a headset.
|
||||||
|
#include "../layer.cpp"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#define CHECK(expr) do { if (!(expr)) { std::cerr << __LINE__ << ": " #expr "\n"; std::exit(1); } } while (false)
|
||||||
|
namespace {
|
||||||
|
uintptr_t serial = 100;
|
||||||
|
frame::Names seenExtensions;
|
||||||
|
XrVersion seenVersion{};
|
||||||
|
XrApiLayerNextInfo* seenNext{};
|
||||||
|
int suggestionCalls{}, locateCalls{}, realThreadCalls{};
|
||||||
|
bool nativeThread{};
|
||||||
|
std::unordered_map<XrPath, std::string> paths;
|
||||||
|
XrResult XRAPI_CALL fakeEnumerate(const char*, uint32_t capacity, uint32_t* count, XrExtensionProperties* out) {
|
||||||
|
const char* extensions[] = {"XR_KHR_locate_spaces", "XR_EXT_palm_pose", frame::stubs[0]};
|
||||||
|
*count = nativeThread ? 3 : 2;
|
||||||
|
if (!capacity) return XR_SUCCESS;
|
||||||
|
if (capacity < *count) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||||
|
for (uint32_t j = 0; j < *count; ++j) { std::strcpy(out[j].extensionName, extensions[j]); out[j].extensionVersion = 1; }
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakeCreate(const XrInstanceCreateInfo* in, const XrApiLayerCreateInfo* layer, XrInstance* i) {
|
||||||
|
seenVersion = in->applicationInfo.apiVersion; seenNext = layer->nextInfo;
|
||||||
|
seenExtensions.clear();
|
||||||
|
for (uint32_t j = 0; j < in->enabledExtensionCount; ++j) seenExtensions.emplace_back(in->enabledExtensionNames[j]);
|
||||||
|
if (frame::has(seenExtensions, "XR_MISSING_test")) return XR_ERROR_EXTENSION_NOT_PRESENT;
|
||||||
|
*i = reinterpret_cast<XrInstance>(++serial); return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakeDestroy(XrInstance) { return XR_SUCCESS; }
|
||||||
|
XrResult XRAPI_CALL fakeCreateSession(XrInstance, const XrSessionCreateInfo*, XrSession* s) {
|
||||||
|
*s = reinterpret_cast<XrSession>(++serial); return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakeDestroySession(XrSession) { return XR_SUCCESS; }
|
||||||
|
XrResult XRAPI_CALL fakeString(XrInstance, const char* text, XrPath* p) {
|
||||||
|
*p = ++serial; paths[*p] = text; return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakePath(XrInstance, XrPath p, uint32_t cap, uint32_t* count, char* out) {
|
||||||
|
if (!paths.count(p)) return XR_ERROR_PATH_INVALID;
|
||||||
|
*count = static_cast<uint32_t>(paths[p].size() + 1);
|
||||||
|
if (!cap) return XR_SUCCESS;
|
||||||
|
if (cap < *count) return XR_ERROR_SIZE_INSUFFICIENT;
|
||||||
|
std::strcpy(out, paths[p].c_str()); return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakeSuggest(XrInstance, const XrInteractionProfileSuggestedBinding* info) {
|
||||||
|
++suggestionCalls;
|
||||||
|
if (info->countSuggestedBindings) CHECK(paths[info->suggestedBindings[0].binding] == "/user/hand/left/input/palm_ext/pose");
|
||||||
|
return XR_SUCCESS;
|
||||||
|
}
|
||||||
|
XrResult XRAPI_CALL fakeLocate(XrSession, const XrSpacesLocateInfo*, XrSpaceLocations*) { ++locateCalls; return XR_SUCCESS; }
|
||||||
|
XrResult XRAPI_CALL fakeThread(XrSession, XrAndroidThreadTypeKHR, uint32_t) { ++realThreadCalls; return XR_TIMEOUT_EXPIRED; }
|
||||||
|
XrResult XRAPI_CALL fakeProperties(XrInstance, XrInstanceProperties*) { return XR_SUCCESS; }
|
||||||
|
XrResult XRAPI_CALL fakeGipa(XrInstance, const char* n, PFN_xrVoidFunction* out) {
|
||||||
|
#define MAP(name, f) if (!std::strcmp(n, name)) { *out = reinterpret_cast<PFN_xrVoidFunction>(f); return XR_SUCCESS; }
|
||||||
|
MAP("xrEnumerateInstanceExtensionProperties", fakeEnumerate)
|
||||||
|
MAP("xrDestroyInstance", fakeDestroy)
|
||||||
|
MAP("xrCreateSession", fakeCreateSession) MAP("xrDestroySession", fakeDestroySession)
|
||||||
|
MAP("xrStringToPath", fakeString) MAP("xrPathToString", fakePath)
|
||||||
|
MAP("xrSuggestInteractionProfileBindings", fakeSuggest) MAP("xrLocateSpacesKHR", fakeLocate)
|
||||||
|
MAP("xrSetAndroidApplicationThreadKHR", fakeThread) MAP("xrGetInstanceProperties", fakeProperties)
|
||||||
|
#undef MAP
|
||||||
|
*out = nullptr; return XR_ERROR_FUNCTION_UNSUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int main() {
|
||||||
|
XrNegotiateLoaderInfo loader{XR_LOADER_INTERFACE_STRUCT_LOADER_INFO, 1, sizeof(XrNegotiateLoaderInfo), 1, 1,
|
||||||
|
XR_MAKE_VERSION(1,0,0), XR_MAKE_VERSION(1,1023,4095)};
|
||||||
|
XrNegotiateApiLayerRequest request{};
|
||||||
|
request.structType = XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST; request.structVersion = 1; request.structSize = sizeof(request);
|
||||||
|
CHECK(xrNegotiateLoaderApiLayerInterface(&loader, frame::layerName, &request) == XR_SUCCESS);
|
||||||
|
CHECK(request.getInstanceProcAddr && request.createApiLayerInstance);
|
||||||
|
loader.maxApiVersion = XR_MAKE_VERSION(1,1,0);
|
||||||
|
CHECK(xrNegotiateLoaderApiLayerInterface(&loader, frame::layerName, &request) == XR_SUCCESS);
|
||||||
|
CHECK(request.layerApiVersion == loader.maxApiVersion);
|
||||||
|
loader.maxApiVersion = XR_MAKE_VERSION(1,0,99);
|
||||||
|
CHECK(xrNegotiateLoaderApiLayerInterface(&loader, frame::layerName, &request) == XR_ERROR_INITIALIZATION_FAILED);
|
||||||
|
loader.maxApiVersion = XR_MAKE_VERSION(1,1023,4095);
|
||||||
|
CHECK(xrNegotiateLoaderApiLayerInterface(&loader, "wrong", &request) == XR_ERROR_INITIALIZATION_FAILED);
|
||||||
|
XrApiLayerNextInfo tail{};
|
||||||
|
XrApiLayerNextInfo next{XR_LOADER_INTERFACE_STRUCT_API_LAYER_NEXT_INFO, 1, sizeof(XrApiLayerNextInfo), {}, fakeGipa, fakeCreate, &tail};
|
||||||
|
XrApiLayerCreateInfo layer{XR_LOADER_INTERFACE_STRUCT_API_LAYER_CREATE_INFO, 1, sizeof(XrApiLayerCreateInfo), nullptr, {}, &next};
|
||||||
|
XrInstanceCreateInfo info{XR_TYPE_INSTANCE_CREATE_INFO, nullptr, 0, {}, 0, nullptr, 2, frame::stubs};
|
||||||
|
info.applicationInfo.apiVersion = XR_MAKE_VERSION(1,1,999);
|
||||||
|
XrInstance a{}, b{};
|
||||||
|
CHECK(createLayer(&info, &layer, &a) == XR_SUCCESS);
|
||||||
|
CHECK(seenVersion == XR_API_VERSION_1_0 && info.applicationInfo.apiVersion == XR_MAKE_VERSION(1,1,999));
|
||||||
|
CHECK(seenNext == &tail && layer.nextInfo == &next);
|
||||||
|
CHECK(!frame::has(seenExtensions, frame::stubs[0]) && frame::has(seenExtensions, "XR_KHR_locate_spaces"));
|
||||||
|
XrSession sa{}, sb{};
|
||||||
|
CHECK(createSession(a, nullptr, &sa) == XR_SUCCESS);
|
||||||
|
CHECK(threadSettings(sa, XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR, 1) == XR_SUCCESS);
|
||||||
|
nativeThread = true;
|
||||||
|
CHECK(createLayer(&info, &layer, &b) == XR_SUCCESS);
|
||||||
|
CHECK(frame::has(seenExtensions, frame::stubs[0]));
|
||||||
|
CHECK(createSession(b, nullptr, &sb) == XR_SUCCESS);
|
||||||
|
PFN_xrVoidFunction fn{};
|
||||||
|
CHECK(gipa(b, "xrSetAndroidApplicationThreadKHR", &fn) == XR_SUCCESS);
|
||||||
|
CHECK(reinterpret_cast<PFN_xrSetAndroidApplicationThreadKHR>(fn)(sb, XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR, 1) == XR_TIMEOUT_EXPIRED);
|
||||||
|
CHECK(realThreadCalls == 1);
|
||||||
|
CHECK(threadSettings(sa, XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR, 1) == XR_SUCCESS);
|
||||||
|
CHECK(gipa(a, "xrGetInstanceProperties", &fn) == XR_SUCCESS && fn == reinterpret_cast<PFN_xrVoidFunction>(fakeProperties));
|
||||||
|
CHECK(gipa(a, "xrLocateSpaces", &fn) == XR_SUCCESS);
|
||||||
|
XrSpacesLocateInfo locateInfo{XR_TYPE_SPACES_LOCATE_INFO, nullptr, XR_NULL_HANDLE, 1, 0, nullptr};
|
||||||
|
XrSpaceLocations locations{XR_TYPE_SPACE_LOCATIONS, nullptr, 0, nullptr};
|
||||||
|
CHECK(reinterpret_cast<PFN_xrLocateSpaces>(fn)(sa, &locateInfo, &locations) == XR_SUCCESS && locateCalls == 1);
|
||||||
|
XrPath binding{}, profile{};
|
||||||
|
CHECK(stringToPath(a, "/user/hand/left/input/grip_surface/pose", &binding) == XR_SUCCESS);
|
||||||
|
CHECK(paths[binding] == "/user/hand/left/input/palm_ext/pose");
|
||||||
|
fakeString(a, "/user/hand/left/input/grip_surface/pose", &binding); // preexisting path: rewrite in suggest too
|
||||||
|
fakeString(a, "/interaction_profiles/oculus/touch_controller", &profile);
|
||||||
|
XrActionSuggestedBinding action{XR_NULL_HANDLE, binding};
|
||||||
|
XrInteractionProfileSuggestedBinding suggested{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING, nullptr, profile, 1, &action};
|
||||||
|
CHECK(suggest(a, &suggested) == XR_SUCCESS && suggestionCalls == 1);
|
||||||
|
CHECK(paths[action.binding] == "/user/hand/left/input/grip_surface/pose");
|
||||||
|
fakeString(a, "/interaction_profiles/meta/touch_pro_controller", &suggested.interactionProfile);
|
||||||
|
CHECK(suggest(a, &suggested) == XR_SUCCESS && suggestionCalls == 1);
|
||||||
|
uint32_t count{};
|
||||||
|
CHECK(enumerate(frame::layerName, 0, &count, nullptr) == XR_SUCCESS && count == 2);
|
||||||
|
XrExtensionProperties props[2]{{XR_TYPE_EXTENSION_PROPERTIES, nullptr, {}, 0}, {XR_TYPE_EXTENSION_PROPERTIES, nullptr, {}, 0}};
|
||||||
|
CHECK(enumerate(frame::layerName, 1, &count, props) == XR_ERROR_SIZE_INSUFFICIENT);
|
||||||
|
CHECK(enumerate(frame::layerName, 2, &count, props) == XR_SUCCESS && count == 2);
|
||||||
|
CHECK(enumerate(nullptr, 0, &count, nullptr) == XR_SUCCESS && count == 4);
|
||||||
|
const char* missing = "XR_MISSING_test";
|
||||||
|
info.enabledExtensionCount = 1; info.enabledExtensionNames = &missing;
|
||||||
|
XrInstance failed{};
|
||||||
|
CHECK(createLayer(&info, &layer, &failed) == XR_ERROR_EXTENSION_NOT_PRESENT && failed == XR_NULL_HANDLE);
|
||||||
|
CHECK(destroySession(sa) == XR_SUCCESS);
|
||||||
|
CHECK(threadSettings(sa, XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR, 1) == XR_ERROR_HANDLE_INVALID);
|
||||||
|
CHECK(destroyInstance(a) == XR_SUCCESS);
|
||||||
|
CHECK(get(sb) && !get(a));
|
||||||
|
CHECK(destroyInstance(b) == XR_SUCCESS && !get(sb));
|
||||||
|
CHECK(instances.empty() && sessions.empty() && enumerateNext == nullptr);
|
||||||
|
std::cout << "Negotiation, next-layer chaining, multi-instance dispatch, stubs, forwarding and cleanup passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
// Host-only logcat replacement. Android builds use the NDK header and liblog.
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#define ANDROID_LOG_INFO 4
|
||||||
|
inline int __android_log_print(int, const char*, const char* fmt, ...) {
|
||||||
|
va_list args; va_start(args, fmt); auto r = std::vfprintf(stderr, fmt, args);
|
||||||
|
va_end(args); std::fputc('\n', stderr); return r;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
// Only the opaque jobject declaration is needed by openxr_platform.h.
|
||||||
|
typedef void* jobject;
|
||||||
Vendored
+208
@@ -0,0 +1,208 @@
|
|||||||
|
Apache License
|
||||||
|
|
||||||
|
Version 2.0, January 2004
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
|
||||||
|
AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and distribution
|
||||||
|
as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||||
|
owner that is granting the License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||||
|
that control, are controlled by, or are under common control with that entity.
|
||||||
|
For the purposes of this definition, "control" means (i) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity, whether
|
||||||
|
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
|
||||||
|
of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
|
||||||
|
granted by this License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including
|
||||||
|
but not limited to software source code, documentation source, and configuration
|
||||||
|
files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation
|
||||||
|
or translation of a Source form, including but not limited to compiled object
|
||||||
|
code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form,
|
||||||
|
made available under the License, as indicated by a copyright notice that
|
||||||
|
is included in or attached to the work (an example is provided in the Appendix
|
||||||
|
below).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form,
|
||||||
|
that is based on (or derived from) the Work and for which the editorial revisions,
|
||||||
|
annotations, elaborations, or other modifications represent, as a whole, an
|
||||||
|
original work of authorship. For the purposes of this License, Derivative
|
||||||
|
Works shall not include works that remain separable from, or merely link (or
|
||||||
|
bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version
|
||||||
|
of the Work and any modifications or additions to that Work or Derivative
|
||||||
|
Works thereof, that is intentionally submitted to Licensor for inclusion in
|
||||||
|
the Work by the copyright owner or by an individual or Legal Entity authorized
|
||||||
|
to submit on behalf of the copyright owner. For the purposes of this definition,
|
||||||
|
"submitted" means any form of electronic, verbal, or written communication
|
||||||
|
sent to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems, and
|
||||||
|
issue tracking systems that are managed by, or on behalf of, the Licensor
|
||||||
|
for the purpose of discussing and improving the Work, but excluding communication
|
||||||
|
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||||
|
owner as "Not a Contribution."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||||
|
of whom a Contribution has been received by Licensor and subsequently incorporated
|
||||||
|
within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this
|
||||||
|
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
|
||||||
|
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
|
||||||
|
the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License,
|
||||||
|
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section) patent
|
||||||
|
license to make, have made, use, offer to sell, sell, import, and otherwise
|
||||||
|
transfer the Work, where such license applies only to those patent claims
|
||||||
|
licensable by such Contributor that are necessarily infringed by their Contribution(s)
|
||||||
|
alone or by combination of their Contribution(s) with the Work to which such
|
||||||
|
Contribution(s) was submitted. If You institute patent litigation against
|
||||||
|
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that the Work or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses granted to You
|
||||||
|
under this License for that Work shall terminate as of the date such litigation
|
||||||
|
is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or
|
||||||
|
Derivative Works thereof in any medium, with or without modifications, and
|
||||||
|
in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or Derivative Works a copy
|
||||||
|
of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices stating that
|
||||||
|
You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works that You distribute,
|
||||||
|
all copyright, patent, trademark, and attribution notices from the Source
|
||||||
|
form of the Work, excluding those notices that do not pertain to any part
|
||||||
|
of the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
||||||
|
then any Derivative Works that You distribute must include a readable copy
|
||||||
|
of the attribution notices contained within such NOTICE file, excluding those
|
||||||
|
notices that do not pertain to any part of the Derivative Works, in at least
|
||||||
|
one of the following places: within a NOTICE text file distributed as part
|
||||||
|
of the Derivative Works; within the Source form or documentation, if provided
|
||||||
|
along with the Derivative Works; or, within a display generated by the Derivative
|
||||||
|
Works, if and wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and do not modify the
|
||||||
|
License. You may add Your own attribution notices within Derivative Works
|
||||||
|
that You distribute, alongside or as an addendum to the NOTICE text from the
|
||||||
|
Work, provided that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide
|
||||||
|
additional or different license terms and conditions for use, reproduction,
|
||||||
|
or distribution of Your modifications, or for any such Derivative Works as
|
||||||
|
a whole, provided Your use, reproduction, and distribution of the Work otherwise
|
||||||
|
complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
||||||
|
Contribution intentionally submitted for inclusion in the Work by You to the
|
||||||
|
Licensor shall be under the terms and conditions of this License, without
|
||||||
|
any additional terms or conditions. Notwithstanding the above, nothing herein
|
||||||
|
shall supersede or modify the terms of any separate license agreement you
|
||||||
|
may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names,
|
||||||
|
trademarks, service marks, or product names of the Licensor, except as required
|
||||||
|
for reasonable and customary use in describing the origin of the Work and
|
||||||
|
reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
|
||||||
|
in writing, Licensor provides the Work (and each Contributor provides its
|
||||||
|
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied, including, without limitation, any warranties
|
||||||
|
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
|
||||||
|
of using or redistributing the Work and assume any risks associated with Your
|
||||||
|
exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether
|
||||||
|
in tort (including negligence), contract, or otherwise, unless required by
|
||||||
|
applicable law (such as deliberate and grossly negligent acts) or agreed to
|
||||||
|
in writing, shall any Contributor be liable to You for damages, including
|
||||||
|
any direct, indirect, special, incidental, or consequential damages of any
|
||||||
|
character arising as a result of this License or out of the use or inability
|
||||||
|
to use the Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all other commercial
|
||||||
|
damages or losses), even if such Contributor has been advised of the possibility
|
||||||
|
of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work
|
||||||
|
or Derivative Works thereof, You may choose to offer, and charge a fee for,
|
||||||
|
acceptance of support, warranty, indemnity, or other liability obligations
|
||||||
|
and/or rights consistent with this License. However, in accepting such obligations,
|
||||||
|
You may act only on Your own behalf and on Your sole responsibility, not on
|
||||||
|
behalf of any other Contributor, and only if You agree to indemnify, defend,
|
||||||
|
and hold each Contributor harmless for any liability incurred by, or claims
|
||||||
|
asserted against, such Contributor by reason of your accepting any such warranty
|
||||||
|
or additional liability. END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate
|
||||||
|
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
|
||||||
|
information. (Don't include the brackets!) The text should be enclosed in
|
||||||
|
the appropriate comment syntax for the file format. We also recommend that
|
||||||
|
a file or class name and description of purpose be included on the same "printed
|
||||||
|
page" as the copyright notice for easier identification within third-party
|
||||||
|
archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
|
||||||
|
limitations under the License.
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (c) 2017-2023, The Khronos Group Inc.
|
||||||
|
// Copyright (c) 2017 Valve Corporation
|
||||||
|
// Copyright (c) 2017 LunarG, Inc.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
|
//
|
||||||
|
// Initial Author: Mark Young <marky@lunarg.com>
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <openxr/openxr.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Forward declare.
|
||||||
|
typedef struct XrApiLayerCreateInfo XrApiLayerCreateInfo;
|
||||||
|
|
||||||
|
// Function pointer prototype for the xrCreateApiLayerInstance function used in place of xrCreateInstance.
|
||||||
|
// This function allows us to pass special API layer information to each layer during the process of creating an Instance.
|
||||||
|
typedef XrResult(XRAPI_PTR *PFN_xrCreateApiLayerInstance)(const XrInstanceCreateInfo *info,
|
||||||
|
const XrApiLayerCreateInfo *apiLayerInfo, XrInstance *instance);
|
||||||
|
|
||||||
|
// Loader/API Layer Interface versions
|
||||||
|
// 1 - First version, introduces negotiation structure and functions
|
||||||
|
#define XR_CURRENT_LOADER_API_LAYER_VERSION 1
|
||||||
|
|
||||||
|
// Loader/Runtime Interface versions
|
||||||
|
// 1 - First version, introduces negotiation structure and functions
|
||||||
|
#define XR_CURRENT_LOADER_RUNTIME_VERSION 1
|
||||||
|
|
||||||
|
// Version negotiation values
|
||||||
|
typedef enum XrLoaderInterfaceStructs {
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_UNINTIALIZED = 0,
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_LOADER_INFO,
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST,
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST,
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_API_LAYER_CREATE_INFO,
|
||||||
|
XR_LOADER_INTERFACE_STRUCT_API_LAYER_NEXT_INFO,
|
||||||
|
} XrLoaderInterfaceStructs;
|
||||||
|
|
||||||
|
#define XR_LOADER_INFO_STRUCT_VERSION 1
|
||||||
|
typedef struct XrNegotiateLoaderInfo {
|
||||||
|
XrLoaderInterfaceStructs structType; // XR_LOADER_INTERFACE_STRUCT_LOADER_INFO
|
||||||
|
uint32_t structVersion; // XR_LOADER_INFO_STRUCT_VERSION
|
||||||
|
size_t structSize; // sizeof(XrNegotiateLoaderInfo)
|
||||||
|
uint32_t minInterfaceVersion;
|
||||||
|
uint32_t maxInterfaceVersion;
|
||||||
|
XrVersion minApiVersion;
|
||||||
|
XrVersion maxApiVersion;
|
||||||
|
} XrNegotiateLoaderInfo;
|
||||||
|
|
||||||
|
#define XR_API_LAYER_INFO_STRUCT_VERSION 1
|
||||||
|
typedef struct XrNegotiateApiLayerRequest {
|
||||||
|
XrLoaderInterfaceStructs structType; // XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST
|
||||||
|
uint32_t structVersion; // XR_API_LAYER_INFO_STRUCT_VERSION
|
||||||
|
size_t structSize; // sizeof(XrNegotiateApiLayerRequest)
|
||||||
|
uint32_t layerInterfaceVersion; // CURRENT_LOADER_API_LAYER_VERSION
|
||||||
|
XrVersion layerApiVersion;
|
||||||
|
PFN_xrGetInstanceProcAddr getInstanceProcAddr;
|
||||||
|
PFN_xrCreateApiLayerInstance createApiLayerInstance;
|
||||||
|
} XrNegotiateApiLayerRequest;
|
||||||
|
|
||||||
|
#define XR_RUNTIME_INFO_STRUCT_VERSION 1
|
||||||
|
typedef struct XrNegotiateRuntimeRequest {
|
||||||
|
XrLoaderInterfaceStructs structType; // XR_LOADER_INTERFACE_STRUCT_RUNTIME_REQUEST
|
||||||
|
uint32_t structVersion; // XR_RUNTIME_INFO_STRUCT_VERSION
|
||||||
|
size_t structSize; // sizeof(XrNegotiateRuntimeRequest)
|
||||||
|
uint32_t runtimeInterfaceVersion; // CURRENT_LOADER_RUNTIME_VERSION
|
||||||
|
XrVersion runtimeApiVersion;
|
||||||
|
PFN_xrGetInstanceProcAddr getInstanceProcAddr;
|
||||||
|
} XrNegotiateRuntimeRequest;
|
||||||
|
|
||||||
|
// Function used to negotiate an interface betewen the loader and an API layer. Each library exposing one or
|
||||||
|
// more API layers needs to expose at least this function.
|
||||||
|
typedef XrResult(XRAPI_PTR *PFN_xrNegotiateLoaderApiLayerInterface)(const XrNegotiateLoaderInfo *loaderInfo,
|
||||||
|
const char *apiLayerName,
|
||||||
|
XrNegotiateApiLayerRequest *apiLayerRequest);
|
||||||
|
|
||||||
|
// Function used to negotiate an interface betewen the loader and a runtime. Each runtime should expose
|
||||||
|
// at least this function.
|
||||||
|
typedef XrResult(XRAPI_PTR *PFN_xrNegotiateLoaderRuntimeInterface)(const XrNegotiateLoaderInfo *loaderInfo,
|
||||||
|
XrNegotiateRuntimeRequest *runtimeRequest);
|
||||||
|
|
||||||
|
// Forward declare.
|
||||||
|
typedef struct XrApiLayerNextInfo XrApiLayerNextInfo;
|
||||||
|
|
||||||
|
#define XR_API_LAYER_NEXT_INFO_STRUCT_VERSION 1
|
||||||
|
struct XrApiLayerNextInfo {
|
||||||
|
XrLoaderInterfaceStructs structType; // XR_LOADER_INTERFACE_STRUCT_API_LAYER_NEXT_INFO
|
||||||
|
uint32_t structVersion; // XR_API_LAYER_NEXT_INFO_STRUCT_VERSION
|
||||||
|
size_t structSize; // sizeof(XrApiLayerNextInfo)
|
||||||
|
char layerName[XR_MAX_API_LAYER_NAME_SIZE]; // Name of API layer which should receive this info
|
||||||
|
PFN_xrGetInstanceProcAddr nextGetInstanceProcAddr; // Pointer to next API layer's xrGetInstanceProcAddr
|
||||||
|
PFN_xrCreateApiLayerInstance nextCreateApiLayerInstance; // Pointer to next API layer's xrCreateApiLayerInstance
|
||||||
|
XrApiLayerNextInfo *next; // Pointer to the next API layer info in the sequence
|
||||||
|
};
|
||||||
|
|
||||||
|
#define XR_API_LAYER_MAX_SETTINGS_PATH_SIZE 512
|
||||||
|
#define XR_API_LAYER_CREATE_INFO_STRUCT_VERSION 1
|
||||||
|
typedef struct XrApiLayerCreateInfo {
|
||||||
|
XrLoaderInterfaceStructs structType; // XR_LOADER_INTERFACE_STRUCT_API_LAYER_CREATE_INFO
|
||||||
|
uint32_t structVersion; // XR_API_LAYER_CREATE_INFO_STRUCT_VERSION
|
||||||
|
size_t structSize; // sizeof(XrApiLayerCreateInfo)
|
||||||
|
void *loaderInstance; // Pointer to the LoaderInstance class
|
||||||
|
char settings_file_location[XR_API_LAYER_MAX_SETTINGS_PATH_SIZE]; // Location to the found settings file (or empty '\0')
|
||||||
|
XrApiLayerNextInfo *nextInfo; // Pointer to the next API layer's Info
|
||||||
|
} XrApiLayerCreateInfo;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
||||||
+14911
File diff suppressed because it is too large.
Load diff
+819
@@ -0,0 +1,819 @@
|
|||||||
|
#ifndef OPENXR_PLATFORM_H_
|
||||||
|
#define OPENXR_PLATFORM_H_ 1
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Copyright 2017-2026 The Khronos Group Inc.
|
||||||
|
**
|
||||||
|
** SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
** This header is generated from the Khronos OpenXR XML API Registry.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "openxr.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_KHR_android_thread_settings is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_android_thread_settings 1
|
||||||
|
#define XR_KHR_android_thread_settings_SPEC_VERSION 6
|
||||||
|
#define XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME "XR_KHR_android_thread_settings"
|
||||||
|
|
||||||
|
typedef enum XrAndroidThreadTypeKHR {
|
||||||
|
XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR = 1,
|
||||||
|
XR_ANDROID_THREAD_TYPE_APPLICATION_WORKER_KHR = 2,
|
||||||
|
XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR = 3,
|
||||||
|
XR_ANDROID_THREAD_TYPE_RENDERER_WORKER_KHR = 4,
|
||||||
|
XR_ANDROID_THREAD_TYPE_MAX_ENUM_KHR = 0x7FFFFFFF
|
||||||
|
} XrAndroidThreadTypeKHR;
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrSetAndroidApplicationThreadKHR)(XrSession session, XrAndroidThreadTypeKHR threadType, uint32_t threadId);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrSetAndroidApplicationThreadKHR(
|
||||||
|
XrSession session,
|
||||||
|
XrAndroidThreadTypeKHR threadType,
|
||||||
|
uint32_t threadId);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_KHR_android_surface_swapchain is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_android_surface_swapchain 1
|
||||||
|
#define XR_KHR_android_surface_swapchain_SPEC_VERSION 4
|
||||||
|
#define XR_KHR_ANDROID_SURFACE_SWAPCHAIN_EXTENSION_NAME "XR_KHR_android_surface_swapchain"
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrCreateSwapchainAndroidSurfaceKHR)(XrSession session, const XrSwapchainCreateInfo* info, XrSwapchain* swapchain, jobject* surface);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSwapchainAndroidSurfaceKHR(
|
||||||
|
XrSession session,
|
||||||
|
const XrSwapchainCreateInfo* info,
|
||||||
|
XrSwapchain* swapchain,
|
||||||
|
jobject* surface);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_KHR_android_create_instance is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_android_create_instance 1
|
||||||
|
#define XR_KHR_android_create_instance_SPEC_VERSION 3
|
||||||
|
#define XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME "XR_KHR_android_create_instance"
|
||||||
|
// XrInstanceCreateInfoAndroidKHR extends XrInstanceCreateInfo
|
||||||
|
typedef struct XrInstanceCreateInfoAndroidKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
void* XR_MAY_ALIAS applicationVM;
|
||||||
|
void* XR_MAY_ALIAS applicationActivity;
|
||||||
|
} XrInstanceCreateInfoAndroidKHR;
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_KHR_vulkan_swapchain_format_list is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_vulkan_swapchain_format_list 1
|
||||||
|
#define XR_KHR_vulkan_swapchain_format_list_SPEC_VERSION 5
|
||||||
|
#define XR_KHR_VULKAN_SWAPCHAIN_FORMAT_LIST_EXTENSION_NAME "XR_KHR_vulkan_swapchain_format_list"
|
||||||
|
// XrVulkanSwapchainFormatListCreateInfoKHR extends XrSwapchainCreateInfo
|
||||||
|
typedef struct XrVulkanSwapchainFormatListCreateInfoKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
uint32_t viewFormatCount;
|
||||||
|
const VkFormat* viewFormats;
|
||||||
|
} XrVulkanSwapchainFormatListCreateInfoKHR;
|
||||||
|
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_OPENGL
|
||||||
|
|
||||||
|
// XR_KHR_opengl_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_opengl_enable 1
|
||||||
|
#define XR_KHR_opengl_enable_SPEC_VERSION 12
|
||||||
|
#define XR_KHR_OPENGL_ENABLE_EXTENSION_NAME "XR_KHR_opengl_enable"
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
// XrGraphicsBindingOpenGLWin32KHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingOpenGLWin32KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
HDC hDC;
|
||||||
|
HGLRC hGLRC;
|
||||||
|
} XrGraphicsBindingOpenGLWin32KHR;
|
||||||
|
#endif // XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_XLIB
|
||||||
|
// XrGraphicsBindingOpenGLXlibKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingOpenGLXlibKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
Display* xDisplay;
|
||||||
|
uint32_t visualid;
|
||||||
|
GLXFBConfig glxFBConfig;
|
||||||
|
GLXDrawable glxDrawable;
|
||||||
|
GLXContext glxContext;
|
||||||
|
} XrGraphicsBindingOpenGLXlibKHR;
|
||||||
|
#endif // XR_USE_PLATFORM_XLIB
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_XCB
|
||||||
|
// XrGraphicsBindingOpenGLXcbKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingOpenGLXcbKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
xcb_connection_t* connection;
|
||||||
|
uint32_t screenNumber;
|
||||||
|
xcb_glx_fbconfig_t fbconfigid;
|
||||||
|
xcb_visualid_t visualid;
|
||||||
|
xcb_glx_drawable_t glxDrawable;
|
||||||
|
xcb_glx_context_t glxContext;
|
||||||
|
} XrGraphicsBindingOpenGLXcbKHR;
|
||||||
|
#endif // XR_USE_PLATFORM_XCB
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_WAYLAND
|
||||||
|
// XrGraphicsBindingOpenGLWaylandKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingOpenGLWaylandKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
struct wl_display* display;
|
||||||
|
} XrGraphicsBindingOpenGLWaylandKHR;
|
||||||
|
#endif // XR_USE_PLATFORM_WAYLAND
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageOpenGLKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
uint32_t image;
|
||||||
|
} XrSwapchainImageOpenGLKHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsOpenGLKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
XrVersion minApiVersionSupported;
|
||||||
|
XrVersion maxApiVersionSupported;
|
||||||
|
} XrGraphicsRequirementsOpenGLKHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetOpenGLGraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsOpenGLKHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetOpenGLGraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsOpenGLKHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_OPENGL */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
|
||||||
|
|
||||||
|
// XR_KHR_opengl_es_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_opengl_es_enable 1
|
||||||
|
#define XR_KHR_opengl_es_enable_SPEC_VERSION 10
|
||||||
|
#define XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME "XR_KHR_opengl_es_enable"
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
// XrGraphicsBindingOpenGLESAndroidKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingOpenGLESAndroidKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
EGLDisplay display;
|
||||||
|
EGLConfig config;
|
||||||
|
EGLContext context;
|
||||||
|
} XrGraphicsBindingOpenGLESAndroidKHR;
|
||||||
|
#endif // XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageOpenGLESKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
uint32_t image;
|
||||||
|
} XrSwapchainImageOpenGLESKHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsOpenGLESKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
XrVersion minApiVersionSupported;
|
||||||
|
XrVersion maxApiVersionSupported;
|
||||||
|
} XrGraphicsRequirementsOpenGLESKHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetOpenGLESGraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsOpenGLESKHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetOpenGLESGraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsOpenGLESKHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_OPENGL_ES */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_KHR_vulkan_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_vulkan_enable 1
|
||||||
|
#define XR_KHR_vulkan_enable_SPEC_VERSION 10
|
||||||
|
#define XR_KHR_VULKAN_ENABLE_EXTENSION_NAME "XR_KHR_vulkan_enable"
|
||||||
|
// XrGraphicsBindingVulkanKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingVulkanKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
VkInstance instance;
|
||||||
|
VkPhysicalDevice physicalDevice;
|
||||||
|
VkDevice device;
|
||||||
|
uint32_t queueFamilyIndex;
|
||||||
|
uint32_t queueIndex;
|
||||||
|
} XrGraphicsBindingVulkanKHR;
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageVulkanKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
VkImage image;
|
||||||
|
} XrSwapchainImageVulkanKHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsVulkanKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
XrVersion minApiVersionSupported;
|
||||||
|
XrVersion maxApiVersionSupported;
|
||||||
|
} XrGraphicsRequirementsVulkanKHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanInstanceExtensionsKHR)(XrInstance instance, XrSystemId systemId, uint32_t bufferCapacityInput, uint32_t* bufferCountOutput, char* buffer);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanDeviceExtensionsKHR)(XrInstance instance, XrSystemId systemId, uint32_t bufferCapacityInput, uint32_t* bufferCountOutput, char* buffer);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanGraphicsDeviceKHR)(XrInstance instance, XrSystemId systemId, VkInstance vkInstance, VkPhysicalDevice* vkPhysicalDevice);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanGraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsVulkanKHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanInstanceExtensionsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
uint32_t bufferCapacityInput,
|
||||||
|
uint32_t* bufferCountOutput,
|
||||||
|
char* buffer);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanDeviceExtensionsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
uint32_t bufferCapacityInput,
|
||||||
|
uint32_t* bufferCountOutput,
|
||||||
|
char* buffer);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanGraphicsDeviceKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
VkInstance vkInstance,
|
||||||
|
VkPhysicalDevice* vkPhysicalDevice);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanGraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsVulkanKHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_D3D11
|
||||||
|
|
||||||
|
// XR_KHR_D3D11_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_D3D11_enable 1
|
||||||
|
#define XR_KHR_D3D11_enable_SPEC_VERSION 11
|
||||||
|
#define XR_KHR_D3D11_ENABLE_EXTENSION_NAME "XR_KHR_D3D11_enable"
|
||||||
|
// XrGraphicsBindingD3D11KHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingD3D11KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
ID3D11Device* device;
|
||||||
|
} XrGraphicsBindingD3D11KHR;
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageD3D11KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
ID3D11Texture2D* texture;
|
||||||
|
} XrSwapchainImageD3D11KHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsD3D11KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
LUID adapterLuid;
|
||||||
|
D3D_FEATURE_LEVEL minFeatureLevel;
|
||||||
|
} XrGraphicsRequirementsD3D11KHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetD3D11GraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsD3D11KHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetD3D11GraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsD3D11KHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_D3D11 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_D3D12
|
||||||
|
|
||||||
|
// XR_KHR_D3D12_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_D3D12_enable 1
|
||||||
|
#define XR_KHR_D3D12_enable_SPEC_VERSION 11
|
||||||
|
#define XR_KHR_D3D12_ENABLE_EXTENSION_NAME "XR_KHR_D3D12_enable"
|
||||||
|
// XrGraphicsBindingD3D12KHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingD3D12KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
ID3D12Device* device;
|
||||||
|
ID3D12CommandQueue* queue;
|
||||||
|
} XrGraphicsBindingD3D12KHR;
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageD3D12KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
ID3D12Resource* texture;
|
||||||
|
} XrSwapchainImageD3D12KHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsD3D12KHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
LUID adapterLuid;
|
||||||
|
D3D_FEATURE_LEVEL minFeatureLevel;
|
||||||
|
} XrGraphicsRequirementsD3D12KHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetD3D12GraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsD3D12KHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetD3D12GraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsD3D12KHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_D3D12 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_METAL
|
||||||
|
|
||||||
|
// XR_KHR_metal_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_metal_enable 1
|
||||||
|
#define XR_KHR_metal_enable_SPEC_VERSION 3
|
||||||
|
#define XR_KHR_METAL_ENABLE_EXTENSION_NAME "XR_KHR_metal_enable"
|
||||||
|
// XrGraphicsBindingMetalKHR extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingMetalKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
void* XR_MAY_ALIAS commandQueue;
|
||||||
|
} XrGraphicsBindingMetalKHR;
|
||||||
|
|
||||||
|
typedef struct XrSwapchainImageMetalKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
void* XR_MAY_ALIAS texture;
|
||||||
|
} XrSwapchainImageMetalKHR;
|
||||||
|
|
||||||
|
typedef struct XrGraphicsRequirementsMetalKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
void* XR_MAY_ALIAS metalDevice;
|
||||||
|
} XrGraphicsRequirementsMetalKHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetMetalGraphicsRequirementsKHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsMetalKHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetMetalGraphicsRequirementsKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsMetalKHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_METAL */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
// XR_KHR_win32_convert_performance_counter_time is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_win32_convert_performance_counter_time 1
|
||||||
|
#define XR_KHR_win32_convert_performance_counter_time_SPEC_VERSION 1
|
||||||
|
#define XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME "XR_KHR_win32_convert_performance_counter_time"
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrConvertWin32PerformanceCounterToTimeKHR)(XrInstance instance, const LARGE_INTEGER* performanceCounter, XrTime* time);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrConvertTimeToWin32PerformanceCounterKHR)(XrInstance instance, XrTime time, LARGE_INTEGER* performanceCounter);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrConvertWin32PerformanceCounterToTimeKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
const LARGE_INTEGER* performanceCounter,
|
||||||
|
XrTime* time);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrConvertTimeToWin32PerformanceCounterKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrTime time,
|
||||||
|
LARGE_INTEGER* performanceCounter);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_WIN32 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_TIMESPEC
|
||||||
|
|
||||||
|
// XR_KHR_convert_timespec_time is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_convert_timespec_time 1
|
||||||
|
#define XR_KHR_convert_timespec_time_SPEC_VERSION 1
|
||||||
|
#define XR_KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME "XR_KHR_convert_timespec_time"
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrConvertTimespecTimeToTimeKHR)(XrInstance instance, const struct timespec* timespecTime, XrTime* time);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrConvertTimeToTimespecTimeKHR)(XrInstance instance, XrTime time, struct timespec* timespecTime);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrConvertTimespecTimeToTimeKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
const struct timespec* timespecTime,
|
||||||
|
XrTime* time);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrConvertTimeToTimespecTimeKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrTime time,
|
||||||
|
struct timespec* timespecTime);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_TIMESPEC */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_KHR_loader_init_android is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_loader_init_android 1
|
||||||
|
#define XR_KHR_loader_init_android_SPEC_VERSION 1
|
||||||
|
#define XR_KHR_LOADER_INIT_ANDROID_EXTENSION_NAME "XR_KHR_loader_init_android"
|
||||||
|
typedef struct XrLoaderInitInfoAndroidKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
void* XR_MAY_ALIAS applicationVM;
|
||||||
|
void* XR_MAY_ALIAS applicationContext;
|
||||||
|
} XrLoaderInitInfoAndroidKHR;
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_KHR_vulkan_enable2 is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_KHR_vulkan_enable2 1
|
||||||
|
#define XR_KHR_vulkan_enable2_SPEC_VERSION 4
|
||||||
|
#define XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME "XR_KHR_vulkan_enable2"
|
||||||
|
typedef XrFlags64 XrVulkanInstanceCreateFlagsKHR;
|
||||||
|
|
||||||
|
// Flag bits for XrVulkanInstanceCreateFlagsKHR
|
||||||
|
|
||||||
|
typedef XrFlags64 XrVulkanDeviceCreateFlagsKHR;
|
||||||
|
|
||||||
|
// Flag bits for XrVulkanDeviceCreateFlagsKHR
|
||||||
|
|
||||||
|
typedef struct XrVulkanInstanceCreateInfoKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
XrSystemId systemId;
|
||||||
|
XrVulkanInstanceCreateFlagsKHR createFlags;
|
||||||
|
PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr;
|
||||||
|
const VkInstanceCreateInfo* vulkanCreateInfo;
|
||||||
|
const VkAllocationCallbacks* vulkanAllocator;
|
||||||
|
} XrVulkanInstanceCreateInfoKHR;
|
||||||
|
|
||||||
|
typedef struct XrVulkanDeviceCreateInfoKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
XrSystemId systemId;
|
||||||
|
XrVulkanDeviceCreateFlagsKHR createFlags;
|
||||||
|
PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr;
|
||||||
|
VkPhysicalDevice vulkanPhysicalDevice;
|
||||||
|
const VkDeviceCreateInfo* vulkanCreateInfo;
|
||||||
|
const VkAllocationCallbacks* vulkanAllocator;
|
||||||
|
} XrVulkanDeviceCreateInfoKHR;
|
||||||
|
|
||||||
|
typedef XrGraphicsBindingVulkanKHR XrGraphicsBindingVulkan2KHR;
|
||||||
|
|
||||||
|
typedef struct XrVulkanGraphicsDeviceGetInfoKHR {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
XrSystemId systemId;
|
||||||
|
VkInstance vulkanInstance;
|
||||||
|
} XrVulkanGraphicsDeviceGetInfoKHR;
|
||||||
|
|
||||||
|
typedef XrSwapchainImageVulkanKHR XrSwapchainImageVulkan2KHR;
|
||||||
|
|
||||||
|
typedef XrGraphicsRequirementsVulkanKHR XrGraphicsRequirementsVulkan2KHR;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrCreateVulkanInstanceKHR)(XrInstance instance, const XrVulkanInstanceCreateInfoKHR* createInfo, VkInstance* vulkanInstance, VkResult* vulkanResult);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrCreateVulkanDeviceKHR)(XrInstance instance, const XrVulkanDeviceCreateInfoKHR* createInfo, VkDevice* vulkanDevice, VkResult* vulkanResult);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanGraphicsDevice2KHR)(XrInstance instance, const XrVulkanGraphicsDeviceGetInfoKHR* getInfo, VkPhysicalDevice* vulkanPhysicalDevice);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetVulkanGraphicsRequirements2KHR)(XrInstance instance, XrSystemId systemId, XrGraphicsRequirementsVulkanKHR* graphicsRequirements);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateVulkanInstanceKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
const XrVulkanInstanceCreateInfoKHR* createInfo,
|
||||||
|
VkInstance* vulkanInstance,
|
||||||
|
VkResult* vulkanResult);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateVulkanDeviceKHR(
|
||||||
|
XrInstance instance,
|
||||||
|
const XrVulkanDeviceCreateInfoKHR* createInfo,
|
||||||
|
VkDevice* vulkanDevice,
|
||||||
|
VkResult* vulkanResult);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanGraphicsDevice2KHR(
|
||||||
|
XrInstance instance,
|
||||||
|
const XrVulkanGraphicsDeviceGetInfoKHR* getInfo,
|
||||||
|
VkPhysicalDevice* vulkanPhysicalDevice);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetVulkanGraphicsRequirements2KHR(
|
||||||
|
XrInstance instance,
|
||||||
|
XrSystemId systemId,
|
||||||
|
XrGraphicsRequirementsVulkanKHR* graphicsRequirements);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_EGL
|
||||||
|
|
||||||
|
// XR_MNDX_egl_enable is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_MNDX_egl_enable 1
|
||||||
|
#define XR_MNDX_egl_enable_SPEC_VERSION 2
|
||||||
|
#define XR_MNDX_EGL_ENABLE_EXTENSION_NAME "XR_MNDX_egl_enable"
|
||||||
|
typedef PFN_xrVoidFunction (*PFN_xrEglGetProcAddressMNDX)(const char *name);
|
||||||
|
// XrGraphicsBindingEGLMNDX extends XrSessionCreateInfo
|
||||||
|
typedef struct XrGraphicsBindingEGLMNDX {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
PFN_xrEglGetProcAddressMNDX getProcAddress;
|
||||||
|
EGLDisplay display;
|
||||||
|
EGLConfig config;
|
||||||
|
EGLContext context;
|
||||||
|
} XrGraphicsBindingEGLMNDX;
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_EGL */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
// XR_MSFT_perception_anchor_interop is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_MSFT_perception_anchor_interop 1
|
||||||
|
#define XR_MSFT_perception_anchor_interop_SPEC_VERSION 1
|
||||||
|
#define XR_MSFT_PERCEPTION_ANCHOR_INTEROP_EXTENSION_NAME "XR_MSFT_perception_anchor_interop"
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrCreateSpatialAnchorFromPerceptionAnchorMSFT)(XrSession session, IUnknown* perceptionAnchor, XrSpatialAnchorMSFT* anchor);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrTryGetPerceptionAnchorFromSpatialAnchorMSFT)(XrSession session, XrSpatialAnchorMSFT anchor, IUnknown** perceptionAnchor);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSpatialAnchorFromPerceptionAnchorMSFT(
|
||||||
|
XrSession session,
|
||||||
|
IUnknown* perceptionAnchor,
|
||||||
|
XrSpatialAnchorMSFT* anchor);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrTryGetPerceptionAnchorFromSpatialAnchorMSFT(
|
||||||
|
XrSession session,
|
||||||
|
XrSpatialAnchorMSFT anchor,
|
||||||
|
IUnknown** perceptionAnchor);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_WIN32 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
// XR_MSFT_holographic_window_attachment is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_MSFT_holographic_window_attachment 1
|
||||||
|
#define XR_MSFT_holographic_window_attachment_SPEC_VERSION 1
|
||||||
|
#define XR_MSFT_HOLOGRAPHIC_WINDOW_ATTACHMENT_EXTENSION_NAME "XR_MSFT_holographic_window_attachment"
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
// XrHolographicWindowAttachmentMSFT extends XrSessionCreateInfo
|
||||||
|
typedef struct XrHolographicWindowAttachmentMSFT {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
IUnknown* holographicSpace;
|
||||||
|
IUnknown* coreWindow;
|
||||||
|
} XrHolographicWindowAttachmentMSFT;
|
||||||
|
#endif // XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_WIN32 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_FB_android_surface_swapchain_create is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_FB_android_surface_swapchain_create 1
|
||||||
|
#define XR_FB_android_surface_swapchain_create_SPEC_VERSION 1
|
||||||
|
#define XR_FB_ANDROID_SURFACE_SWAPCHAIN_CREATE_EXTENSION_NAME "XR_FB_android_surface_swapchain_create"
|
||||||
|
typedef XrFlags64 XrAndroidSurfaceSwapchainFlagsFB;
|
||||||
|
|
||||||
|
// Flag bits for XrAndroidSurfaceSwapchainFlagsFB
|
||||||
|
static const XrAndroidSurfaceSwapchainFlagsFB XR_ANDROID_SURFACE_SWAPCHAIN_SYNCHRONOUS_BIT_FB = 0x00000001;
|
||||||
|
static const XrAndroidSurfaceSwapchainFlagsFB XR_ANDROID_SURFACE_SWAPCHAIN_USE_TIMESTAMPS_BIT_FB = 0x00000002;
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
// XrAndroidSurfaceSwapchainCreateInfoFB extends XrSwapchainCreateInfo
|
||||||
|
typedef struct XrAndroidSurfaceSwapchainCreateInfoFB {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
XrAndroidSurfaceSwapchainFlagsFB createFlags;
|
||||||
|
} XrAndroidSurfaceSwapchainCreateInfoFB;
|
||||||
|
#endif // XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ML
|
||||||
|
|
||||||
|
// XR_ML_compat is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_ML_compat 1
|
||||||
|
#define XR_ML_compat_SPEC_VERSION 1
|
||||||
|
#define XR_ML_COMPAT_EXTENSION_NAME "XR_ML_compat"
|
||||||
|
typedef struct XrCoordinateSpaceCreateInfoML {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
MLCoordinateFrameUID cfuid;
|
||||||
|
XrPosef poseInCoordinateSpace;
|
||||||
|
} XrCoordinateSpaceCreateInfoML;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrCreateSpaceFromCoordinateFrameUIDML)(XrSession session, const XrCoordinateSpaceCreateInfoML *createInfo, XrSpace* space);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrCreateSpaceFromCoordinateFrameUIDML(
|
||||||
|
XrSession session,
|
||||||
|
const XrCoordinateSpaceCreateInfoML * createInfo,
|
||||||
|
XrSpace* space);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_ML */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_WIN32
|
||||||
|
|
||||||
|
// XR_OCULUS_audio_device_guid is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_OCULUS_audio_device_guid 1
|
||||||
|
#define XR_OCULUS_audio_device_guid_SPEC_VERSION 1
|
||||||
|
#define XR_OCULUS_AUDIO_DEVICE_GUID_EXTENSION_NAME "XR_OCULUS_audio_device_guid"
|
||||||
|
#define XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS 128
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetAudioOutputDeviceGuidOculus)(XrInstance instance, wchar_t buffer[XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS]);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrGetAudioInputDeviceGuidOculus)(XrInstance instance, wchar_t buffer[XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS]);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetAudioOutputDeviceGuidOculus(
|
||||||
|
XrInstance instance,
|
||||||
|
wchar_t buffer[XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS]);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrGetAudioInputDeviceGuidOculus(
|
||||||
|
XrInstance instance,
|
||||||
|
wchar_t buffer[XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS]);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_WIN32 */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_FB_foveation_vulkan is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_FB_foveation_vulkan 1
|
||||||
|
#define XR_FB_foveation_vulkan_SPEC_VERSION 1
|
||||||
|
#define XR_FB_FOVEATION_VULKAN_EXTENSION_NAME "XR_FB_foveation_vulkan"
|
||||||
|
// XrSwapchainImageFoveationVulkanFB extends XrSwapchainImageVulkanKHR
|
||||||
|
typedef struct XrSwapchainImageFoveationVulkanFB {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
VkImage image;
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
} XrSwapchainImageFoveationVulkanFB;
|
||||||
|
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_FB_swapchain_update_state_android_surface is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_FB_swapchain_update_state_android_surface 1
|
||||||
|
#define XR_FB_swapchain_update_state_android_surface_SPEC_VERSION 1
|
||||||
|
#define XR_FB_SWAPCHAIN_UPDATE_STATE_ANDROID_SURFACE_EXTENSION_NAME "XR_FB_swapchain_update_state_android_surface"
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
typedef struct XrSwapchainStateAndroidSurfaceDimensionsFB {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
} XrSwapchainStateAndroidSurfaceDimensionsFB;
|
||||||
|
#endif // XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
|
||||||
|
|
||||||
|
// XR_FB_swapchain_update_state_opengl_es is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_FB_swapchain_update_state_opengl_es 1
|
||||||
|
#define XR_FB_swapchain_update_state_opengl_es_SPEC_VERSION 1
|
||||||
|
#define XR_FB_SWAPCHAIN_UPDATE_STATE_OPENGL_ES_EXTENSION_NAME "XR_FB_swapchain_update_state_opengl_es"
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
|
||||||
|
typedef struct XrSwapchainStateSamplerOpenGLESFB {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
EGLenum minFilter;
|
||||||
|
EGLenum magFilter;
|
||||||
|
EGLenum wrapModeS;
|
||||||
|
EGLenum wrapModeT;
|
||||||
|
EGLenum swizzleRed;
|
||||||
|
EGLenum swizzleGreen;
|
||||||
|
EGLenum swizzleBlue;
|
||||||
|
EGLenum swizzleAlpha;
|
||||||
|
float maxAnisotropy;
|
||||||
|
XrColor4f borderColor;
|
||||||
|
} XrSwapchainStateSamplerOpenGLESFB;
|
||||||
|
#endif // XR_USE_GRAPHICS_API_OPENGL_ES
|
||||||
|
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_OPENGL_ES */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_FB_swapchain_update_state_vulkan is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_FB_swapchain_update_state_vulkan 1
|
||||||
|
#define XR_FB_swapchain_update_state_vulkan_SPEC_VERSION 1
|
||||||
|
#define XR_FB_SWAPCHAIN_UPDATE_STATE_VULKAN_EXTENSION_NAME "XR_FB_swapchain_update_state_vulkan"
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
typedef struct XrSwapchainStateSamplerVulkanFB {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
VkFilter minFilter;
|
||||||
|
VkFilter magFilter;
|
||||||
|
VkSamplerMipmapMode mipmapMode;
|
||||||
|
VkSamplerAddressMode wrapModeS;
|
||||||
|
VkSamplerAddressMode wrapModeT;
|
||||||
|
VkComponentSwizzle swizzleRed;
|
||||||
|
VkComponentSwizzle swizzleGreen;
|
||||||
|
VkComponentSwizzle swizzleBlue;
|
||||||
|
VkComponentSwizzle swizzleAlpha;
|
||||||
|
float maxAnisotropy;
|
||||||
|
XrColor4f borderColor;
|
||||||
|
} XrSwapchainStateSamplerVulkanFB;
|
||||||
|
#endif // XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_GRAPHICS_API_VULKAN
|
||||||
|
|
||||||
|
// XR_META_vulkan_swapchain_create_info is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_META_vulkan_swapchain_create_info 1
|
||||||
|
#define XR_META_vulkan_swapchain_create_info_SPEC_VERSION 1
|
||||||
|
#define XR_META_VULKAN_SWAPCHAIN_CREATE_INFO_EXTENSION_NAME "XR_META_vulkan_swapchain_create_info"
|
||||||
|
// XrVulkanSwapchainCreateInfoMETA extends XrSwapchainCreateInfo
|
||||||
|
typedef struct XrVulkanSwapchainCreateInfoMETA {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
VkImageCreateFlags additionalCreateFlags;
|
||||||
|
VkImageUsageFlags additionalUsageFlags;
|
||||||
|
} XrVulkanSwapchainCreateInfoMETA;
|
||||||
|
|
||||||
|
#endif /* XR_USE_GRAPHICS_API_VULKAN */
|
||||||
|
|
||||||
|
#ifdef XR_USE_PLATFORM_ANDROID
|
||||||
|
|
||||||
|
// XR_ANDROID_anchor_sharing_export is a preprocessor guard. Do not pass it to API calls.
|
||||||
|
#define XR_ANDROID_anchor_sharing_export 1
|
||||||
|
#define XR_ANDROID_anchor_sharing_export_SPEC_VERSION 1
|
||||||
|
#define XR_ANDROID_ANCHOR_SHARING_EXPORT_EXTENSION_NAME "XR_ANDROID_anchor_sharing_export"
|
||||||
|
typedef struct XrAnchorSharingInfoANDROID {
|
||||||
|
XrStructureType type;
|
||||||
|
const void* XR_MAY_ALIAS next;
|
||||||
|
XrSpace anchor;
|
||||||
|
} XrAnchorSharingInfoANDROID;
|
||||||
|
|
||||||
|
typedef struct XrAnchorSharingTokenANDROID {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
struct AIBinder* token;
|
||||||
|
} XrAnchorSharingTokenANDROID;
|
||||||
|
|
||||||
|
// XrSystemAnchorSharingExportPropertiesANDROID extends XrSystemProperties
|
||||||
|
typedef struct XrSystemAnchorSharingExportPropertiesANDROID {
|
||||||
|
XrStructureType type;
|
||||||
|
void* XR_MAY_ALIAS next;
|
||||||
|
XrBool32 supportsAnchorSharingExport;
|
||||||
|
} XrSystemAnchorSharingExportPropertiesANDROID;
|
||||||
|
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrShareAnchorANDROID)(XrSession session, const XrAnchorSharingInfoANDROID* sharingInfo, XrAnchorSharingTokenANDROID* anchorToken);
|
||||||
|
typedef XrResult (XRAPI_PTR *PFN_xrUnshareAnchorANDROID)(XrSession session, XrSpace anchor);
|
||||||
|
|
||||||
|
#ifndef XR_NO_PROTOTYPES
|
||||||
|
#ifdef XR_EXTENSION_PROTOTYPES
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrShareAnchorANDROID(
|
||||||
|
XrSession session,
|
||||||
|
const XrAnchorSharingInfoANDROID* sharingInfo,
|
||||||
|
XrAnchorSharingTokenANDROID* anchorToken);
|
||||||
|
|
||||||
|
XRAPI_ATTR XrResult XRAPI_CALL xrUnshareAnchorANDROID(
|
||||||
|
XrSession session,
|
||||||
|
XrSpace anchor);
|
||||||
|
#endif /* XR_EXTENSION_PROTOTYPES */
|
||||||
|
#endif /* !XR_NO_PROTOTYPES */
|
||||||
|
#endif /* XR_USE_PLATFORM_ANDROID */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
** Copyright (c) 2017-2026 The Khronos Group Inc.
|
||||||
|
**
|
||||||
|
** SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OPENXR_PLATFORM_DEFINES_H_
|
||||||
|
#define OPENXR_PLATFORM_DEFINES_H_ 1
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Platform-specific calling convention macros.
|
||||||
|
*
|
||||||
|
* Platforms should define these so that OpenXR clients call OpenXR functions
|
||||||
|
* with the same calling conventions that the OpenXR implementation expects.
|
||||||
|
*
|
||||||
|
* XRAPI_ATTR - Placed before the return type in function declarations.
|
||||||
|
* Useful for C++11 and GCC/Clang-style function attribute syntax.
|
||||||
|
* XRAPI_CALL - Placed after the return type in function declarations.
|
||||||
|
* Useful for MSVC-style calling convention syntax.
|
||||||
|
* XRAPI_PTR - Placed between the '(' and '*' in function pointer types.
|
||||||
|
*
|
||||||
|
* Function declaration: XRAPI_ATTR void XRAPI_CALL xrFunction(void);
|
||||||
|
* Function pointer type: typedef void (XRAPI_PTR *PFN_xrFunction)(void);
|
||||||
|
*/
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define XRAPI_ATTR
|
||||||
|
// On Windows, functions use the stdcall convention
|
||||||
|
#define XRAPI_CALL __stdcall
|
||||||
|
#define XRAPI_PTR XRAPI_CALL
|
||||||
|
#elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH < 7
|
||||||
|
#error "API not supported for the 'armeabi' NDK ABI"
|
||||||
|
#elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH >= 7 && defined(__ARM_32BIT_STATE)
|
||||||
|
// On Android 32-bit ARM targets, functions use the "hardfloat"
|
||||||
|
// calling convention, i.e. float parameters are passed in registers. This
|
||||||
|
// is true even if the rest of the application passes floats on the stack,
|
||||||
|
// as it does by default when compiling for the armeabi-v7a NDK ABI.
|
||||||
|
#define XRAPI_ATTR __attribute__((pcs("aapcs-vfp")))
|
||||||
|
#define XRAPI_CALL
|
||||||
|
#define XRAPI_PTR XRAPI_ATTR
|
||||||
|
#else
|
||||||
|
// On other platforms, use the default calling convention
|
||||||
|
#define XRAPI_ATTR
|
||||||
|
#define XRAPI_CALL
|
||||||
|
#define XRAPI_PTR
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#if !defined(XR_NO_STDINT_H)
|
||||||
|
#if defined(_MSC_VER) && (_MSC_VER < 1600)
|
||||||
|
typedef signed __int8 int8_t;
|
||||||
|
typedef unsigned __int8 uint8_t;
|
||||||
|
typedef signed __int16 int16_t;
|
||||||
|
typedef unsigned __int16 uint16_t;
|
||||||
|
typedef signed __int32 int32_t;
|
||||||
|
typedef unsigned __int32 uint32_t;
|
||||||
|
typedef signed __int64 int64_t;
|
||||||
|
typedef unsigned __int64 uint64_t;
|
||||||
|
#else
|
||||||
|
#include <stdint.h>
|
||||||
|
#endif
|
||||||
|
#endif // !defined( XR_NO_STDINT_H )
|
||||||
|
|
||||||
|
// XR_PTR_SIZE (in bytes)
|
||||||
|
#if (defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(__powerpc64__))
|
||||||
|
#define XR_PTR_SIZE 8
|
||||||
|
#else
|
||||||
|
#define XR_PTR_SIZE 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Needed so we can use clang __has_feature portably.
|
||||||
|
#if !defined(XR_COMPILER_HAS_FEATURE)
|
||||||
|
#if defined(__clang__)
|
||||||
|
#define XR_COMPILER_HAS_FEATURE(x) __has_feature(x)
|
||||||
|
#else
|
||||||
|
#define XR_COMPILER_HAS_FEATURE(x) 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Identifies if the current compiler has C++11 support enabled.
|
||||||
|
// Does not by itself identify if any given C++11 feature is present.
|
||||||
|
#if !defined(XR_CPP11_ENABLED) && defined(__cplusplus)
|
||||||
|
#if defined(__GNUC__) && defined(__GXX_EXPERIMENTAL_CXX0X__)
|
||||||
|
#define XR_CPP11_ENABLED 1
|
||||||
|
#elif defined(_MSC_VER) && (_MSC_VER >= 1600)
|
||||||
|
#define XR_CPP11_ENABLED 1
|
||||||
|
#elif (__cplusplus >= 201103L) // 201103 is the first C++11 version.
|
||||||
|
#define XR_CPP11_ENABLED 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Identifies if the current compiler supports C++11 nullptr.
|
||||||
|
#if !defined(XR_CPP_NULLPTR_SUPPORTED)
|
||||||
|
#if defined(XR_CPP11_ENABLED) && \
|
||||||
|
((defined(__clang__) && XR_COMPILER_HAS_FEATURE(cxx_nullptr)) || \
|
||||||
|
(defined(__GNUC__) && (((__GNUC__ * 1000) + __GNUC_MINOR__) >= 4006)) || \
|
||||||
|
(defined(_MSC_VER) && (_MSC_VER >= 1600)) || \
|
||||||
|
(defined(__EDG_VERSION__) && (__EDG_VERSION__ >= 403)))
|
||||||
|
#define XR_CPP_NULLPTR_SUPPORTED 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(XR_CPP_NULLPTR_SUPPORTED)
|
||||||
|
#define XR_CPP_NULLPTR_SUPPORTED 0
|
||||||
|
#endif // !defined(XR_CPP_NULLPTR_SUPPORTED)
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
"""Local-only VR manifest, raw ZIP and independent signature checks."""
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'ui'))
|
||||||
|
import frame_apk
|
||||||
|
import frame_apk_vr as vr
|
||||||
|
import frame_apk_sign as signing
|
||||||
|
import frame_android
|
||||||
|
from test_frame_apk import pool
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT = 'android.intent.category.DEFAULT'
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(utf8=False, launcher=False, category=None, samsung=False, split=False, alias=False, splash=False):
|
||||||
|
strings = ['manifest', 'package', 'org.test.vr', 'application', 'activity', 'intent-filter',
|
||||||
|
'action', 'category', 'name', vr.MAIN, category or next(iter(sorted(vr.VR))),
|
||||||
|
vr.LAUNCHER, 'http://schemas.android.com/apk/res/android', 'meta-data', 'value',
|
||||||
|
'com.samsung.android.vr.application.mode', 'vr_only', 'activity-alias', DEFAULT, next(iter(sorted(vr.VR))), 'targetActivity', '.Splash', '.Game']
|
||||||
|
def start(tag, attrs=()):
|
||||||
|
body = struct.pack('<IIHHHHHH', 0xffffffff, strings.index(tag), 20, 20, len(attrs), 0, 0, 0)
|
||||||
|
for name, value in attrs:
|
||||||
|
ns = 0xffffffff if name == 'package' else 12
|
||||||
|
body += struct.pack('<IIIHBBI', ns, strings.index(name), strings.index(value), 8, 0, 3, strings.index(value))
|
||||||
|
return struct.pack('<HHIII', 0x102, 16, 16 + len(body), 1, 0xffffffff) + body
|
||||||
|
def end(tag):
|
||||||
|
return struct.pack('<HHIIIII', 0x103, 16, 24, 1, 0xffffffff, 0xffffffff, strings.index(tag))
|
||||||
|
def leaf(tag, attrs):
|
||||||
|
return start(tag, attrs) + end(tag)
|
||||||
|
b = pool(strings, utf8) + start('manifest', [('package', 'org.test.vr')]) + start('application')
|
||||||
|
if samsung:
|
||||||
|
b += leaf('meta-data', [('name', strings[15]), ('value', 'vr_only')])
|
||||||
|
if splash: # a helper activity with its own MAIN filter, ahead of the game's
|
||||||
|
b += start('activity', [('name', '.Splash')]) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||||
|
b += leaf('category', [('name', DEFAULT)]) + end('intent-filter') + end('activity')
|
||||||
|
b += start('activity', [('name', '.Game')] if splash else []) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||||
|
b += leaf('category', [('name', strings[10])])
|
||||||
|
if split:
|
||||||
|
b += end('intent-filter') + start('intent-filter')
|
||||||
|
if launcher:
|
||||||
|
b += leaf('category', [('name', vr.LAUNCHER)])
|
||||||
|
b += end('intent-filter') + end('activity')
|
||||||
|
if alias: # Godot 4: LAUNCHER only on an alias of the activity ('vr' or 'flat')
|
||||||
|
b += start('activity-alias', [('targetActivity', '.Game')] if splash else []) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||||
|
if alias == 'vr':
|
||||||
|
b += leaf('category', [('name', next(iter(sorted(vr.VR))))])
|
||||||
|
b += leaf('category', [('name', vr.LAUNCHER)])
|
||||||
|
b += end('intent-filter') + end('activity-alias')
|
||||||
|
b += end('application') + end('manifest')
|
||||||
|
return struct.pack('<HHI', 3, 8, len(b) + 8) + b
|
||||||
|
|
||||||
|
|
||||||
|
class VRTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.tmp = tempfile.TemporaryDirectory()
|
||||||
|
cls.keypath = Path(cls.tmp.name) / 'key.json'
|
||||||
|
cls.key = signing.signing_key(cls.keypath)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.tmp.cleanup()
|
||||||
|
|
||||||
|
def test_manifest_patch(self):
|
||||||
|
for utf8 in (False, True):
|
||||||
|
for category in vr.VR:
|
||||||
|
original = manifest(utf8, category=category)
|
||||||
|
result = vr.add_launcher_category(original)
|
||||||
|
info, filters = vr.inspect(result)
|
||||||
|
self.assertTrue(info['launchable'])
|
||||||
|
self.assertTrue(info['vr'])
|
||||||
|
self.assertIn(vr.LAUNCHER, filters[0]['categories'])
|
||||||
|
self.assertEqual(struct.unpack_from('<I', result, 4)[0], len(result))
|
||||||
|
self.assertEqual(vr.add_launcher_category(result), result)
|
||||||
|
self.assertEqual(vr.add_launcher_category(manifest(utf8, True)), manifest(utf8, True))
|
||||||
|
|
||||||
|
def test_filter_boundaries(self):
|
||||||
|
self.assertFalse(vr.inspect(manifest(launcher=True, split=True))[0]['launchable'])
|
||||||
|
self.assertTrue(vr.inspect(vr.add_launcher_category(manifest(launcher=True, split=True)))[0]['launchable'])
|
||||||
|
|
||||||
|
def test_alias_launcher_is_not_enough(self):
|
||||||
|
# (manifest, still VR after patching)
|
||||||
|
cases = [(manifest(alias='vr'), True), # Godot 4 VR export
|
||||||
|
(manifest(alias='vr', category=DEFAULT), True), # VR category only on the alias
|
||||||
|
(manifest(alias='flat', category=DEFAULT), False)] # Godot 4 flat export
|
||||||
|
for original, is_vr in cases:
|
||||||
|
info, filters = vr.inspect(original)
|
||||||
|
self.assertFalse(info['launchable'])
|
||||||
|
self.assertTrue(info['repairable'])
|
||||||
|
self.assertEqual(len(filters), 1)
|
||||||
|
self.assertFalse(filters[0]['alias'])
|
||||||
|
result = vr.add_launcher_category(original)
|
||||||
|
info, _ = vr.inspect(result)
|
||||||
|
self.assertTrue(info['launchable'])
|
||||||
|
self.assertFalse(info['repairable'])
|
||||||
|
self.assertEqual(info['vr'], is_vr)
|
||||||
|
|
||||||
|
def test_alias_target_activity_is_patched(self):
|
||||||
|
original = manifest(alias='flat', category=DEFAULT, splash=True)
|
||||||
|
info, filters = vr.inspect(original)
|
||||||
|
self.assertTrue(info['repairable'])
|
||||||
|
self.assertEqual(filters[0]['activity'], 'org.test.vr.Game')
|
||||||
|
owner, launchers = None, []
|
||||||
|
for tag, attrs in frame_apk.manifest_elements(vr.add_launcher_category(original)):
|
||||||
|
if tag in ('activity', 'activity-alias'):
|
||||||
|
owner = (tag, attrs.get('name', (0, 0, None))[2])
|
||||||
|
if tag == 'category' and attrs['name'][2] == vr.LAUNCHER:
|
||||||
|
launchers.append(owner)
|
||||||
|
self.assertEqual(launchers, [('activity', '.Game'), ('activity-alias', None)])
|
||||||
|
|
||||||
|
def test_alias_with_launchable_activity_is_left_alone(self):
|
||||||
|
original = manifest(launcher=True, alias='vr')
|
||||||
|
info, _ = vr.inspect(original)
|
||||||
|
self.assertTrue(info['launchable'])
|
||||||
|
self.assertFalse(info['repairable'])
|
||||||
|
self.assertEqual(vr.add_launcher_category(original), original)
|
||||||
|
|
||||||
|
def test_patch_alias_apk(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
src, dst = Path(d) / 'in.apk', Path(d) / 'out.apk'
|
||||||
|
with zipfile.ZipFile(src, 'w') as z:
|
||||||
|
z.writestr('AndroidManifest.xml', manifest(alias='flat', category=DEFAULT))
|
||||||
|
with patch.object(signing, 'signing_key', return_value=self.key):
|
||||||
|
result = frame_android.patch(src, dst)
|
||||||
|
self.assertEqual(result['patched'], ['launcher'])
|
||||||
|
self.assertTrue(frame_apk.apk_info(dst)['launchable'])
|
||||||
|
self.assertTrue(signing.verify(dst))
|
||||||
|
|
||||||
|
def test_styled_pool(self):
|
||||||
|
for utf8 in (False, True):
|
||||||
|
p = bytearray(pool(['styled'], utf8))
|
||||||
|
old_start = struct.unpack_from('<I', p, 20)[0]
|
||||||
|
p[old_start:old_start] = struct.pack('<I', 0)
|
||||||
|
style_start = len(p)
|
||||||
|
p += struct.pack('<III', 0, 0, 2) + b'\xff' * 12
|
||||||
|
struct.pack_into('<I', p, 4, len(p))
|
||||||
|
struct.pack_into('<I', p, 16, (0x100 if utf8 else 0) | 1)
|
||||||
|
struct.pack_into('<I', p, 12, 1)
|
||||||
|
struct.pack_into('<II', p, 20, old_start + 4, style_start)
|
||||||
|
result, idx = vr._append_string(bytes(p), vr.LAUNCHER)
|
||||||
|
self.assertEqual(frame_apk._string_pool(result, 0), ['styled', vr.LAUNCHER])
|
||||||
|
new_style = struct.unpack_from('<I', result, 24)[0]
|
||||||
|
self.assertEqual(result[new_style:], p[style_start:])
|
||||||
|
self.assertEqual(len(result) % 4, 0)
|
||||||
|
|
||||||
|
def test_detection(self):
|
||||||
|
names = {'lib/arm64-v8a/' + n for n in ('libvrapi.so', 'libopenxr_loader.so', 'libovrplatformloader.so')}
|
||||||
|
info = vr.detection(manifest(category='android.intent.category.LAUNCHER'), names)
|
||||||
|
self.assertTrue(info['vr'])
|
||||||
|
self.assertTrue(info['launchable'])
|
||||||
|
self.assertEqual(len(info['vr_issues']), 3)
|
||||||
|
self.assertFalse(vr.detection(manifest(category=vr.LAUNCHER), set())['vr'])
|
||||||
|
self.assertTrue(vr.detection(manifest(category=vr.LAUNCHER, samsung=True), set())['vr'])
|
||||||
|
|
||||||
|
def test_key_cache(self):
|
||||||
|
with patch.object(signing, '_prime', side_effect=AssertionError('regenerated')):
|
||||||
|
self.assertEqual(signing.signing_key(self.keypath), self.key)
|
||||||
|
if os.name != 'nt':
|
||||||
|
self.assertEqual(self.keypath.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_repack_sign_tamper(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
src, dst = Path(d) / 'in.apk', Path(d) / 'out.apk'
|
||||||
|
with zipfile.ZipFile(src, 'w') as z:
|
||||||
|
z.writestr('AndroidManifest.xml', manifest(), compress_type=8)
|
||||||
|
z.writestr('classes.dex', b'compress me' * 10000, compress_type=8)
|
||||||
|
z.writestr('resources.arsc', b'1234')
|
||||||
|
z.writestr('lib/arm64-v8a/libx.so', b'ELF' * 500000)
|
||||||
|
z.writestr('META-INF/OLD.RSA', b'old')
|
||||||
|
z.writestr('META-INF/MANIFEST.MF', b'old')
|
||||||
|
with patch.object(signing, 'signing_key', return_value=self.key):
|
||||||
|
result = frame_android.patch(src, dst, {'assets/layer.json': b'{}', 'lib/arm64-v8a/liblayer.so': b'layer'})
|
||||||
|
self.assertEqual(result['patched'], ['launcher'])
|
||||||
|
self.assertTrue(frame_apk.apk_info(dst)['launchable'])
|
||||||
|
self.assertTrue(signing.verify(dst))
|
||||||
|
def compressed(path, info):
|
||||||
|
data = path.read_bytes()
|
||||||
|
nl, el = struct.unpack_from('<HH', data, info.header_offset + 26)
|
||||||
|
start = info.header_offset + 30 + nl + el
|
||||||
|
return data[start:start + info.compress_size], start
|
||||||
|
with zipfile.ZipFile(src) as a, zipfile.ZipFile(dst) as b:
|
||||||
|
self.assertNotIn('META-INF/OLD.RSA', b.namelist())
|
||||||
|
self.assertNotIn('META-INF/MANIFEST.MF', b.namelist())
|
||||||
|
for i in b.infolist():
|
||||||
|
raw, start = compressed(dst, i)
|
||||||
|
if i.compress_type == 0:
|
||||||
|
self.assertEqual(start % (16384 if i.filename.endswith('.so') else 4), 0)
|
||||||
|
if i.filename in ('classes.dex', 'resources.arsc', 'lib/arm64-v8a/libx.so'):
|
||||||
|
self.assertEqual(raw, compressed(src, a.getinfo(i.filename))[0])
|
||||||
|
_, off = compressed(dst, b.getinfo('resources.arsc'))
|
||||||
|
original = dst.read_bytes()
|
||||||
|
data = bytearray(original)
|
||||||
|
eo, cd = signing._eocd(data)
|
||||||
|
size = struct.unpack_from('<Q', data, cd - 24)[0]
|
||||||
|
block_start = cd - size - 8
|
||||||
|
value = data[block_start + 20:cd - 24]
|
||||||
|
signer = signing._parts(signing._parts(value)[0])[0]
|
||||||
|
signed, signatures, pub = signing._parts(signer)
|
||||||
|
signature = signing._parts(signing._parts(signatures)[0][4:])[0]
|
||||||
|
sig_offset = data.index(signature, block_start)
|
||||||
|
data[sig_offset] ^= 1
|
||||||
|
dst.write_bytes(data)
|
||||||
|
with self.assertRaisesRegex(ValueError, 'RSA signature'):
|
||||||
|
signing.verify(dst)
|
||||||
|
data = bytearray(original)
|
||||||
|
data[off] ^= 1
|
||||||
|
dst.write_bytes(data)
|
||||||
|
with self.assertRaisesRegex(ValueError, 'digest'):
|
||||||
|
signing.verify(dst)
|
||||||
|
|
||||||
|
@unittest.skipUnless(Path('/usr/bin/openssl').exists(), 'openssl absent')
|
||||||
|
def test_openssl(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
d = Path(d)
|
||||||
|
(d / 'cert.der').write_bytes(signing.certificate(self.key))
|
||||||
|
(d / 'message').write_bytes(b'independent signature check')
|
||||||
|
(d / 'signature').write_bytes(signing.rsa_sign(b'independent signature check', self.key))
|
||||||
|
def run(*args):
|
||||||
|
return subprocess.run(['/usr/bin/openssl', *args], check=True, capture_output=True).stdout
|
||||||
|
run('x509', '-inform', 'DER', '-in', str(d / 'cert.der'), '-out', str(d / 'cert.pem'))
|
||||||
|
run('verify', '-check_ss_sig', '-CAfile', str(d / 'cert.pem'), str(d / 'cert.pem'))
|
||||||
|
(d / 'pub.pem').write_bytes(run('x509', '-in', str(d / 'cert.pem'), '-pubkey', '-noout'))
|
||||||
|
output = run('dgst', '-sha256', '-verify', str(d / 'pub.pem'), '-signature', str(d / 'signature'), str(d / 'message'))
|
||||||
|
self.assertIn(b'Verified OK', output)
|
||||||
|
|
||||||
|
def test_install_auto_and_override(self):
|
||||||
|
base = {'package': 'org.test.vr', 'label': 'VR', 'abis': [], 'min_sdk': None,
|
||||||
|
'vr': True, 'vr_activity': True, 'launchable': False, 'repairable': True}
|
||||||
|
with patch.object(frame_android, 'apk_info', return_value=base), \
|
||||||
|
patch.object(frame_android, 'xr_compat_files', return_value={}), \
|
||||||
|
patch.object(frame_android, 'patch', return_value={'patched': ['launcher']}) as repair, \
|
||||||
|
patch.object(frame_android, '_install', return_value={}) as install:
|
||||||
|
frame_android.install('original.apk')
|
||||||
|
repair.assert_called_once()
|
||||||
|
self.assertFalse(install.call_args.args[3])
|
||||||
|
self.assertEqual(install.call_args.args[1]['patched'], ['launcher'])
|
||||||
|
frame_android.install('original.apk', flatscreen=True)
|
||||||
|
self.assertTrue(install.call_args.args[3])
|
||||||
|
|
||||||
|
def test_xr_compat_layer(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
apk = Path(d) / 'a.apk'
|
||||||
|
with zipfile.ZipFile(apk, 'w') as z:
|
||||||
|
z.writestr('lib/arm64-v8a/libopenxr_loader.so', b'')
|
||||||
|
add = frame_android.xr_compat_files(str(apk))
|
||||||
|
self.assertEqual(set(add), set(frame_android.XR_COMPAT_FILES))
|
||||||
|
self.assertIn(b'XR_APILAYER_FRAME_compat', add['assets/openxr/1/api_layers/implicit.d/XrApiLayer_FRAME_compat.json'])
|
||||||
|
self.assertTrue(add['lib/arm64-v8a/libXrApiLayer_FRAME_compat.so'].startswith(b'\x7fELF'))
|
||||||
|
with zipfile.ZipFile(apk, 'a') as z: # already injected: nothing more to add
|
||||||
|
z.writestr('lib/arm64-v8a/libXrApiLayer_FRAME_compat.so', b'')
|
||||||
|
self.assertEqual(frame_android.xr_compat_files(str(apk)), {})
|
||||||
|
flat = Path(d) / 'flat.apk'
|
||||||
|
with zipfile.ZipFile(flat, 'w') as z:
|
||||||
|
z.writestr('classes.dex', b'')
|
||||||
|
self.assertEqual(frame_android.xr_compat_files(str(flat)), {})
|
||||||
|
|
||||||
|
def test_xr_compat_layer_missing(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
apk = Path(d) / 'a.apk'
|
||||||
|
with zipfile.ZipFile(apk, 'w') as z:
|
||||||
|
z.writestr('lib/arm64-v8a/libopenxr_loader.so', b'')
|
||||||
|
with patch.object(frame_android, 'XR_COMPAT', d):
|
||||||
|
with self.assertRaisesRegex(frame_android.FrameError, 'build.sh'):
|
||||||
|
frame_android.xr_compat_files(str(apk))
|
||||||
|
|
||||||
|
def test_patch_rejects_corrupt_manifest_cleanly(self):
|
||||||
|
with patch.object(frame_android, 'apk_info', side_effect=struct.error('bad')):
|
||||||
|
with self.assertRaises(frame_android.FrameError):
|
||||||
|
frame_android.patch('x.apk', 'y.apk')
|
||||||
|
|
||||||
|
def test_install_adds_layer_to_vr_apps(self):
|
||||||
|
base = {'package': 'org.test.vr', 'label': 'VR', 'abis': [], 'min_sdk': None,
|
||||||
|
'vr': True, 'vr_activity': True, 'launchable': True, 'repairable': False}
|
||||||
|
layer = {'x': b''}
|
||||||
|
with patch.object(frame_android, 'apk_info', return_value=dict(base)), \
|
||||||
|
patch.object(frame_android, 'xr_compat_files', return_value=layer), \
|
||||||
|
patch.object(frame_android, 'patch', return_value={'patched': ['openxr-compat']}) as repair, \
|
||||||
|
patch.object(frame_android, '_install', return_value={}) as install:
|
||||||
|
frame_android.install('game.apk')
|
||||||
|
self.assertIs(repair.call_args.args[2], layer)
|
||||||
|
self.assertEqual(install.call_args.args[1]['patched'], ['openxr-compat'])
|
||||||
|
repair.reset_mock()
|
||||||
|
frame_android.install('game.apk', xr_compat=False) # launchable, no layer: install as is
|
||||||
|
repair.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""APK sources: every place Frame Control can find and download APKs.
|
||||||
|
|
||||||
|
One module per source kind in this package. Each module exposes the same small
|
||||||
|
interface so ``search.py`` can query them all and show where every result came
|
||||||
|
from. Python stdlib only; must run on Python 3.9.
|
||||||
|
|
||||||
|
Module interface
|
||||||
|
----------------
|
||||||
|
KIND = 'sidequest' # stable id of the source kind
|
||||||
|
def sources() -> list[dict] # configured sources of this kind (a kind can have
|
||||||
|
# several, e.g. one per F-Droid-format repo)
|
||||||
|
def search(source, query, limit=50) -> list[dict] # Entry dicts, best first
|
||||||
|
def details(source, entry_id) -> dict # Entry with 'versions'
|
||||||
|
def download(source, entry_id, version_code=None) -> dict
|
||||||
|
# {'apk': local path, 'obb': [paths], 'sha256': hex or None, 'verified': bool}
|
||||||
|
# Raise SourceError with a user-readable message on failure.
|
||||||
|
|
||||||
|
Source dict
|
||||||
|
-----------
|
||||||
|
{'id': 'sidequest', 'kind': KIND, 'name': 'SideQuest', 'url': 'https://...',
|
||||||
|
'builtin': True, 'enabled': True, 'trust': 'official' | 'community' | 'user'}
|
||||||
|
|
||||||
|
Entry dict (missing facts are None, never guessed)
|
||||||
|
----------
|
||||||
|
{'source': source id, 'id': source-local id, 'package': 'org.example.app' or None,
|
||||||
|
'name': str, 'summary': str, 'icon': url or None, 'page': url or None,
|
||||||
|
'version': '1.2', 'version_code': 12, 'min_sdk': 24, 'abis': ['arm64-v8a'],
|
||||||
|
'vr': True/False/None, 'size': bytes, 'free': True, 'license': 'GPL-3.0' or None,
|
||||||
|
'updated': 'YYYY-MM-DD', 'downloadable': bool, # False = open page only
|
||||||
|
'versions': [ {version, version_code, min_sdk, size, updated}, ... ]} # details() only
|
||||||
|
|
||||||
|
Rules
|
||||||
|
-----
|
||||||
|
- Only sources that distribute APKs with the developer's consent: free listings,
|
||||||
|
never paid apps re-hosted, no licence or entitlement workarounds.
|
||||||
|
- Honour each site's terms and robots rules; if automated download isn't allowed,
|
||||||
|
return entries with 'downloadable': False and a 'page' link instead.
|
||||||
|
- Cache indexes under frame_host.cache_dir('apk-sources'); send a clear
|
||||||
|
User-Agent ('FrameControl/<version>'); keep requests modest.
|
||||||
|
- Tests use recorded fixtures, never the network.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SourceError(Exception):
|
||||||
|
"""User-readable failure from a source (network, format, verification)."""
|
||||||
+90
-7
@@ -6,12 +6,18 @@ apps, the lepton-show-flatscreen marker; plus a non-Steam shortcut, so it shows
|
|||||||
in the Steam library and gets its own SteamVR panel. Nothing goes through
|
in the Steam library and gets its own SteamVR panel. Nothing goes through
|
||||||
Lepton Development, which wipes its apps on exit. See docs/apks.md.
|
Lepton Development, which wipes its apps on exit. See docs/apks.md.
|
||||||
|
|
||||||
Python stdlib only. CLI: python3 ui/frame_android.py {info APK|versions APK-or-PKG|install APK|list|launch PKG|stop PKG|remove PKG|probe PKG}
|
Python stdlib only. CLI: python3 ui/frame_android.py
|
||||||
|
install APK [--vr|--flat] [--no-xr-compat] | info APK | versions APK-or-PKG
|
||||||
|
patch SRC DST [--add NAME=PATH ...] | list | launch PKG | stop PKG | remove PKG | probe PKG
|
||||||
"""
|
"""
|
||||||
import json, os, re, shlex, shutil, subprocess, sys, threading, time, zlib
|
import json, os, re, shlex, shutil, struct, subprocess, sys, threading, time, zlib
|
||||||
|
|
||||||
import frame_apk
|
import frame_apk
|
||||||
import frame_host
|
import frame_host
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from frame_apk_vr import add_launcher_category
|
||||||
|
from frame_apk_sign import repack
|
||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
FRAME = os.environ.get('FRAME_ALIAS', 'frame')
|
FRAME = os.environ.get('FRAME_ALIAS', 'frame')
|
||||||
@@ -20,6 +26,13 @@ COMPAT = '.local/share/Steam/steamapps/compatdata'
|
|||||||
SHADERS = '.local/share/Steam/steamapps/shadercache'
|
SHADERS = '.local/share/Steam/steamapps/shadercache'
|
||||||
LAUNCHER = os.path.join(ROOT, 'frame', 'android', 'lepton-app.sh')
|
LAUNCHER = os.path.join(ROOT, 'frame', 'android', 'lepton-app.sh')
|
||||||
SHORTCUTS = os.path.join(ROOT, 'frame', 'android', 'steam_shortcuts.py')
|
SHORTCUTS = os.path.join(ROOT, 'frame', 'android', 'steam_shortcuts.py')
|
||||||
|
# OpenXR API layer that lets OpenXR 1.1 apps run on SteamVR's 1.0-only Android
|
||||||
|
# runtime (frame/openxr-compat, docs/vr-apks.md). Injected into VR APKs.
|
||||||
|
XR_COMPAT = os.path.join(ROOT, 'frame', 'openxr-compat')
|
||||||
|
XR_COMPAT_FILES = {
|
||||||
|
'assets/openxr/1/api_layers/implicit.d/XrApiLayer_FRAME_compat.json': 'XrApiLayer_FRAME_compat.json',
|
||||||
|
'lib/arm64-v8a/libXrApiLayer_FRAME_compat.so': 'prebuilt/arm64-v8a/libXrApiLayer_FRAME_compat.so',
|
||||||
|
}
|
||||||
PKG_RE = re.compile(r'^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$')
|
PKG_RE = re.compile(r'^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$')
|
||||||
SSH_OPTS = ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8']
|
SSH_OPTS = ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8']
|
||||||
|
|
||||||
@@ -65,6 +78,22 @@ def apk_info(path):
|
|||||||
raise FrameError(f'{os.path.basename(path)}: {e}')
|
raise FrameError(f'{os.path.basename(path)}: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def xr_compat_files(apk_path):
|
||||||
|
"""The layer's files to add, or {} if the APK has no OpenXR loader or already has the layer."""
|
||||||
|
with zipfile.ZipFile(apk_path) as z:
|
||||||
|
names = set(z.namelist())
|
||||||
|
if 'lib/arm64-v8a/libopenxr_loader.so' not in names or names & set(XR_COMPAT_FILES):
|
||||||
|
return {}
|
||||||
|
add = {}
|
||||||
|
for entry, rel in XR_COMPAT_FILES.items():
|
||||||
|
try:
|
||||||
|
with open(os.path.join(XR_COMPAT, rel), 'rb') as f:
|
||||||
|
add[entry] = f.read()
|
||||||
|
except OSError:
|
||||||
|
raise FrameError("the OpenXR compatibility layer isn't built; run frame/openxr-compat/build.sh")
|
||||||
|
return add
|
||||||
|
|
||||||
|
|
||||||
def check_installable(info):
|
def check_installable(info):
|
||||||
if info['min_sdk'] and info['min_sdk'] > 30:
|
if info['min_sdk'] and info['min_sdk'] > 30:
|
||||||
raise FrameError(f"{info['label']} needs Android API {info['min_sdk']}; Lepton is Android 11 (API 30)")
|
raise FrameError(f"{info['label']} needs Android API {info['min_sdk']}; Lepton is Android 11 (API 30)")
|
||||||
@@ -111,7 +140,7 @@ def _write_meta(d, meta):
|
|||||||
install_hooks = []
|
install_hooks = []
|
||||||
|
|
||||||
|
|
||||||
def install(apk_path, flatscreen=True, name=None, source=None, icon_png=None):
|
def install(apk_path, flatscreen=None, name=None, source=None, icon_png=None, xr_compat=None):
|
||||||
start, info = time.time(), None
|
start, info = time.time(), None
|
||||||
try:
|
try:
|
||||||
info = apk_info(apk_path)
|
info = apk_info(apk_path)
|
||||||
@@ -121,8 +150,20 @@ def install(apk_path, flatscreen=True, name=None, source=None, icon_png=None):
|
|||||||
pkg = info['package']
|
pkg = info['package']
|
||||||
if not PKG_RE.match(pkg):
|
if not PKG_RE.match(pkg):
|
||||||
raise FrameError(f'unexpected package name {pkg!r}')
|
raise FrameError(f'unexpected package name {pkg!r}')
|
||||||
|
if flatscreen is None:
|
||||||
|
flatscreen = not info['vr']
|
||||||
|
# VR apps get the OpenXR compatibility layer unless told otherwise; it only
|
||||||
|
# changes calls SteamVR would otherwise reject.
|
||||||
|
add = xr_compat_files(apk_path) if (info['vr'] if xr_compat is None else xr_compat) else {}
|
||||||
with _install_lock:
|
with _install_lock:
|
||||||
meta = _install(apk_path, info, pkg, flatscreen, name, source)
|
if add or info['repairable']:
|
||||||
|
with tempfile.TemporaryDirectory(prefix='frame-vr-') as tmp:
|
||||||
|
patched = os.path.join(tmp, 'app.apk')
|
||||||
|
info['patched'] = patch(apk_path, patched, add)['patched']
|
||||||
|
info['launchable'] = True
|
||||||
|
meta = _install(patched, info, pkg, flatscreen, name, source or os.path.basename(apk_path))
|
||||||
|
else:
|
||||||
|
meta = _install(apk_path, info, pkg, flatscreen, name, source)
|
||||||
except FrameError as e:
|
except FrameError as e:
|
||||||
_after_install(info, None, e, start)
|
_after_install(info, None, e, start)
|
||||||
raise
|
raise
|
||||||
@@ -163,6 +204,8 @@ def _install(apk_path, info, pkg, flatscreen, name, source):
|
|||||||
raise FrameError(f'Steam did not return a shortcut id (got {reply[:80]!r})')
|
raise FrameError(f'Steam did not return a shortcut id (got {reply[:80]!r})')
|
||||||
meta = {'package': pkg, 'label': name or info['label'], 'version': info['version'],
|
meta = {'package': pkg, 'label': name or info['label'], 'version': info['version'],
|
||||||
'instance': iid, 'shortcut': shortcut, 'game_id': game_id(shortcut),
|
'instance': iid, 'shortcut': shortcut, 'game_id': game_id(shortcut),
|
||||||
|
'vr': info.get('vr', False), 'vr_issues': info.get('vr_issues', []),
|
||||||
|
'launchable': info.get('launchable', False), 'patched': info.get('patched', []),
|
||||||
'flatscreen': flatscreen, 'installed': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
'flatscreen': flatscreen, 'installed': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
'source': source or os.path.basename(apk_path)}
|
'source': source or os.path.basename(apk_path)}
|
||||||
_write_meta(d, meta)
|
_write_meta(d, meta)
|
||||||
@@ -293,19 +336,59 @@ def probe(pkg, wait=20):
|
|||||||
'container_up': ctr in running_instances()}
|
'container_up': ctr in running_instances()}
|
||||||
|
|
||||||
|
|
||||||
|
def patch(src, dst, add=None):
|
||||||
|
try:
|
||||||
|
info = apk_info(src)
|
||||||
|
with zipfile.ZipFile(src) as z:
|
||||||
|
original = frame_apk._read(z, 'AndroidManifest.xml', frame_apk.MAX_MANIFEST)
|
||||||
|
manifest = add_launcher_category(original) if info['repairable'] else original
|
||||||
|
if not info['launchable'] and not info['repairable']:
|
||||||
|
raise FrameError('APK has no MAIN/LAUNCHER activity that Frame Control can patch')
|
||||||
|
repack(src, dst, replace={'AndroidManifest.xml': manifest}, add=add)
|
||||||
|
result = apk_info(dst)
|
||||||
|
result.pop('icon_png', None)
|
||||||
|
result['patched'] = (['launcher'] if manifest != original else []) + \
|
||||||
|
(['openxr-compat'] if add and set(XR_COMPAT_FILES) <= set(add) else [])
|
||||||
|
return result
|
||||||
|
except (OSError, ValueError, IndexError, struct.error, zipfile.BadZipFile, frame_apk.ApkError) as e:
|
||||||
|
raise FrameError(str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cmd, *args = sys.argv[1:] or ['help']
|
cmd, *args = sys.argv[1:] or ['help']
|
||||||
try:
|
try:
|
||||||
if cmd in ('info', 'versions'):
|
if cmd in ('info', 'versions'):
|
||||||
import frame_apk_versions
|
import frame_apk_versions
|
||||||
if cmd == 'info':
|
if cmd == 'info':
|
||||||
print(frame_apk_versions.describe(apk_info(args[0])))
|
info = apk_info(args[0])
|
||||||
|
print(frame_apk_versions.describe(info))
|
||||||
|
fix = '; Frame Control adds the LAUNCHER entry Lepton needs' if info.get('repairable') else ''
|
||||||
|
if info.get('vr') or fix:
|
||||||
|
print(('VR app' if info.get('vr') else 'Android app') + fix)
|
||||||
|
for note in info.get('vr_issues', []):
|
||||||
|
print(note)
|
||||||
return
|
return
|
||||||
info = apk_info(args[0]) if os.path.isfile(args[0]) or args[0].lower().endswith('.apk') else None
|
info = apk_info(args[0]) if os.path.isfile(args[0]) or args[0].lower().endswith('.apk') else None
|
||||||
r = frame_apk_versions.alternatives(
|
r = frame_apk_versions.alternatives(
|
||||||
info['package'] if info else args[0], info.get('version_code') if info else None)
|
info['package'] if info else args[0], info.get('version_code') if info else None)
|
||||||
elif cmd == 'install':
|
elif cmd == 'install':
|
||||||
r = install(args[0], flatscreen='--vr' not in args)
|
r = install(args[0], flatscreen=False if '--vr' in args else True if '--flat' in args else None,
|
||||||
|
xr_compat=False if '--no-xr-compat' in args else None)
|
||||||
|
elif cmd == 'patch':
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Patch and v2-sign an APK locally')
|
||||||
|
parser.add_argument('src')
|
||||||
|
parser.add_argument('dst')
|
||||||
|
parser.add_argument('--add', action='append', default=[], metavar='NAME=PATH')
|
||||||
|
opts = parser.parse_args(args)
|
||||||
|
additions = {}
|
||||||
|
for item in opts.add:
|
||||||
|
if '=' not in item:
|
||||||
|
raise FrameError('--add requires NAME=PATH')
|
||||||
|
entry, path = item.split('=', 1)
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
additions[entry] = f.read()
|
||||||
|
r = patch(opts.src, opts.dst, additions)
|
||||||
elif cmd == 'list':
|
elif cmd == 'list':
|
||||||
r = list_apps()
|
r = list_apps()
|
||||||
elif cmd in ('launch', 'stop', 'probe'):
|
elif cmd in ('launch', 'stop', 'probe'):
|
||||||
@@ -314,7 +397,7 @@ def main():
|
|||||||
r = remove(args[0], keep_data='--keep-data' in args)
|
r = remove(args[0], keep_data='--keep-data' in args)
|
||||||
else:
|
else:
|
||||||
sys.exit(__doc__)
|
sys.exit(__doc__)
|
||||||
except FrameError as e:
|
except (FrameError, OSError) as e:
|
||||||
sys.exit(f'error: {e}')
|
sys.exit(f'error: {e}')
|
||||||
print(json.dumps(r, indent=1))
|
print(json.dumps(r, indent=1))
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -10,7 +10,8 @@ import zipfile
|
|||||||
|
|
||||||
# android: attribute resource ids; names can be stripped by shrinkers, ids can't.
|
# android: attribute resource ids; names can be stripped by shrinkers, ids can't.
|
||||||
ATTR = {0x01010001: 'label', 0x01010002: 'icon', 0x01010003: 'name',
|
ATTR = {0x01010001: 'label', 0x01010002: 'icon', 0x01010003: 'name',
|
||||||
0x0101021b: 'versionCode', 0x0101021c: 'versionName', 0x0101020c: 'minSdkVersion'}
|
0x01010024: 'value', 0x0101021b: 'versionCode', 0x0101021c: 'versionName', 0x0101020c: 'minSdkVersion',
|
||||||
|
0x01010202: 'targetActivity'}
|
||||||
T_REF, T_STRING, T_INT_DEC, T_INT_HEX = 0x01, 0x03, 0x10, 0x11
|
T_REF, T_STRING, T_INT_DEC, T_INT_HEX = 0x01, 0x03, 0x10, 0x11
|
||||||
# APKs can come from websites (install links), so nothing read from one may be
|
# APKs can come from websites (install links), so nothing read from one may be
|
||||||
# unbounded. zipfile stops at a member's declared size, so checking it is enough.
|
# unbounded. zipfile stops at a member's declared size, so checking it is enough.
|
||||||
@@ -215,8 +216,11 @@ def apk_info(path):
|
|||||||
if 'AndroidManifest.xml' not in names:
|
if 'AndroidManifest.xml' not in names:
|
||||||
raise ApkError('not an APK: no AndroidManifest.xml')
|
raise ApkError('not an APK: no AndroidManifest.xml')
|
||||||
try:
|
try:
|
||||||
elements = manifest_elements(_read(z, 'AndroidManifest.xml', MAX_MANIFEST))
|
manifest_data = _read(z, 'AndroidManifest.xml', MAX_MANIFEST)
|
||||||
|
elements = manifest_elements(manifest_data)
|
||||||
res = Resources(_read(z, 'resources.arsc', MAX_ARSC) if 'resources.arsc' in names else b'')
|
res = Resources(_read(z, 'resources.arsc', MAX_ARSC) if 'resources.arsc' in names else b'')
|
||||||
|
from frame_apk_vr import detection
|
||||||
|
vr_info = detection(manifest_data, names)
|
||||||
except (struct.error, IndexError, zipfile.BadZipFile) as e:
|
except (struct.error, IndexError, zipfile.BadZipFile) as e:
|
||||||
raise ApkError(f'could not read the APK manifest: {e}')
|
raise ApkError(f'could not read the APK manifest: {e}')
|
||||||
tags = {}
|
tags = {}
|
||||||
@@ -236,6 +240,7 @@ def apk_info(path):
|
|||||||
'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None,
|
'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None,
|
||||||
'icon_png': None,
|
'icon_png': None,
|
||||||
}
|
}
|
||||||
|
info.update(vr_info)
|
||||||
try:
|
try:
|
||||||
info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res))
|
info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res))
|
||||||
except Exception: # noqa: BLE001 - any unreadable icon just means no icon
|
except Exception: # noqa: BLE001 - any unreadable icon just means no icon
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
"""Lossless ZIP repacking and APK v2 RSA/SHA-256 signing, Python 3.9 stdlib.
|
||||||
|
|
||||||
|
Spec: https://source.android.com/docs/security/features/apksigning/v2
|
||||||
|
Sections: APK Signing Block; APK Signature Scheme v2 Block; Integrity-protected
|
||||||
|
contents; Verification. No verity algorithm is used, so no verity padding.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
import struct
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
import frame_host
|
||||||
|
|
||||||
|
MAGIC = b'APK Sig Block 42'
|
||||||
|
V2 = 0x7109871a
|
||||||
|
ALG = 0x0103
|
||||||
|
SHA256_DER = bytes.fromhex('3031300d060960864801650304020105000420')
|
||||||
|
|
||||||
|
|
||||||
|
def u32(n):
|
||||||
|
return struct.pack('<I', n)
|
||||||
|
|
||||||
|
|
||||||
|
def lp(b):
|
||||||
|
return u32(len(b)) + b
|
||||||
|
|
||||||
|
|
||||||
|
def der(tag, b):
|
||||||
|
n = len(b)
|
||||||
|
length = bytes([n]) if n < 128 else bytes([128 + (n.bit_length() + 7) // 8]) + n.to_bytes((n.bit_length() + 7) // 8, 'big')
|
||||||
|
return bytes([tag]) + length + b
|
||||||
|
|
||||||
|
|
||||||
|
def integer(n):
|
||||||
|
b = n.to_bytes((n.bit_length() + 7) // 8 or 1, 'big')
|
||||||
|
return der(2, (b'\0' if b[0] & 128 else b'') + b)
|
||||||
|
|
||||||
|
|
||||||
|
def sequence(*items):
|
||||||
|
return der(0x30, b''.join(items))
|
||||||
|
|
||||||
|
|
||||||
|
RSA_ALG = bytes.fromhex('300d06092a864886f70d0101010500')
|
||||||
|
CERT_ALG = bytes.fromhex('300d06092a864886f70d01010b0500')
|
||||||
|
|
||||||
|
|
||||||
|
def public_key(key):
|
||||||
|
return sequence(RSA_ALG, der(3, b'\0' + sequence(integer(key['n']), integer(key['e']))))
|
||||||
|
|
||||||
|
|
||||||
|
def encoded_hash(data, size):
|
||||||
|
digest = SHA256_DER + hashlib.sha256(data).digest()
|
||||||
|
return b'\0\1' + b'\xff' * (size - len(digest) - 3) + b'\0' + digest
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_sign(data, key):
|
||||||
|
size = (key['n'].bit_length() + 7) // 8
|
||||||
|
return pow(int.from_bytes(encoded_hash(data, size), 'big'), key['d'], key['n']).to_bytes(size, 'big')
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_verify(data, sig, n, e):
|
||||||
|
size = (n.bit_length() + 7) // 8
|
||||||
|
if len(sig) != size or int.from_bytes(sig, 'big') >= n:
|
||||||
|
raise ValueError('invalid RSA signature size/value')
|
||||||
|
actual = pow(int.from_bytes(sig, 'big'), e, n).to_bytes(size, 'big')
|
||||||
|
if actual != encoded_hash(data, size):
|
||||||
|
raise ValueError('RSA signature mismatch')
|
||||||
|
|
||||||
|
|
||||||
|
def certificate(key):
|
||||||
|
name = sequence(der(0x31, sequence(bytes.fromhex('0603550403'), der(12, b'Frame Control APK signer'))))
|
||||||
|
validity = sequence(der(0x17, b'200101000000Z'), der(0x18, b'21200101000000Z'))
|
||||||
|
tbs = sequence(der(0xa0, integer(2)), integer(1), CERT_ALG, name, validity, name, public_key(key))
|
||||||
|
return sequence(tbs, CERT_ALG, der(3, b'\0' + rsa_sign(tbs, key)))
|
||||||
|
|
||||||
|
|
||||||
|
def _prime(bits):
|
||||||
|
small = (3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47)
|
||||||
|
while True:
|
||||||
|
n = secrets.randbits(bits) | (3 << (bits - 2)) | 1
|
||||||
|
if any(n % p == 0 for p in small) or (n - 1) % 65537 == 0:
|
||||||
|
continue
|
||||||
|
d, s = n - 1, 0
|
||||||
|
while d % 2 == 0:
|
||||||
|
d //= 2
|
||||||
|
s += 1
|
||||||
|
for _ in range(40): # Miller-Rabin error bound <= 2^-80
|
||||||
|
x = pow(secrets.randbelow(n - 3) + 2, d, n)
|
||||||
|
if x in (1, n - 1):
|
||||||
|
continue
|
||||||
|
for _ in range(s - 1):
|
||||||
|
x = pow(x, 2, n)
|
||||||
|
if x == n - 1:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def signing_key(path=None):
|
||||||
|
"""Persistent identity in app data, never an evictable cache. Atomic publication.
|
||||||
|
|
||||||
|
Hard-linking a fully written private temp file prevents concurrent first-use
|
||||||
|
callers from selecting different identities or reading a partial key.
|
||||||
|
"""
|
||||||
|
path = Path(path) if path is not None else frame_host.data_dir('apk-signing-key.json')
|
||||||
|
if not path.exists():
|
||||||
|
p, q = _prime(1024), _prime(1024)
|
||||||
|
while q == p:
|
||||||
|
q = _prime(1024)
|
||||||
|
key = {'n': p * q, 'e': 65537, 'd': pow(65537, -1, math.lcm(p - 1, q - 1))}
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd, tmp = tempfile.mkstemp(prefix='.apk-key-', dir=str(path.parent))
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, 'w') as f:
|
||||||
|
json.dump(key, f)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
try:
|
||||||
|
os.link(tmp, path) # never replaces a key another process wrote first
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
except OSError: # no hard links (FAT/exFAT): plain rename
|
||||||
|
if not path.exists():
|
||||||
|
os.replace(tmp, path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.unlink(tmp)
|
||||||
|
os.chmod(path, 0o600) # Windows ignores this; the per-user app-data folder is the protection there
|
||||||
|
key = json.loads(path.read_text())
|
||||||
|
if key['n'].bit_length() != 2048 or key['e'] != 65537:
|
||||||
|
raise ValueError(f'invalid cached APK signing key; delete {path} to make a new one '
|
||||||
|
'(re-signed apps then need reinstalling)')
|
||||||
|
rsa_verify(b'key check', rsa_sign(b'key check', key), key['n'], key['e'])
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def _eocd(data):
|
||||||
|
# ZIP comments can contain the EOCD signature; accept only an exact EOF fit.
|
||||||
|
for at in range(len(data) - 22, max(-1, len(data) - 65558), -1):
|
||||||
|
if data[at:at + 4] == b'PK\5\6' and at + 22 + struct.unpack_from('<H', data, at + 20)[0] == len(data):
|
||||||
|
disk, cd_disk, count_disk, count, size, cd = struct.unpack_from('<HHHHII', data, at + 4)
|
||||||
|
if disk or cd_disk or count_disk != count or count == 65535 or cd + size != at:
|
||||||
|
raise ValueError('multi-disk/ZIP64 or invalid APK directory')
|
||||||
|
return at, cd
|
||||||
|
raise ValueError('missing ZIP end record')
|
||||||
|
|
||||||
|
|
||||||
|
def content_digest(sections):
|
||||||
|
chunks = []
|
||||||
|
for section in sections:
|
||||||
|
for off in range(0, len(section), 1024 * 1024):
|
||||||
|
part = section[off:off + 1024 * 1024]
|
||||||
|
chunks.append(hashlib.sha256(b'\xa5' + u32(len(part)) + part).digest())
|
||||||
|
return hashlib.sha256(b'\x5a' + u32(len(chunks)) + b''.join(chunks)).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def sign_apk(data, key):
|
||||||
|
eo, cd = _eocd(data)
|
||||||
|
digest = content_digest((memoryview(data)[:cd], memoryview(data)[cd:eo], data[eo:]))
|
||||||
|
signed = lp(lp(u32(ALG) + lp(digest))) + lp(lp(certificate(key))) + lp(b'')
|
||||||
|
signer = lp(signed) + lp(lp(u32(ALG) + lp(rsa_sign(signed, key)))) + lp(public_key(key))
|
||||||
|
value = lp(lp(signer))
|
||||||
|
pair = struct.pack('<Q', 4 + len(value)) + u32(V2) + value
|
||||||
|
size = len(pair) + 24
|
||||||
|
block = struct.pack('<Q', size) + pair + struct.pack('<Q', size) + MAGIC
|
||||||
|
end = bytearray(data[eo:])
|
||||||
|
struct.pack_into('<I', end, 16, cd + len(block))
|
||||||
|
return data[:cd] + block + data[cd:eo] + end
|
||||||
|
|
||||||
|
|
||||||
|
def _parts(data):
|
||||||
|
result, off = [], 0
|
||||||
|
while off < len(data):
|
||||||
|
if off + 4 > len(data):
|
||||||
|
raise ValueError('truncated length prefix')
|
||||||
|
size = struct.unpack_from('<I', data, off)[0]
|
||||||
|
off += 4
|
||||||
|
if off + size > len(data):
|
||||||
|
raise ValueError('length prefix outside block')
|
||||||
|
result.append(data[off:off + size])
|
||||||
|
off += size
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _der_parts(data):
|
||||||
|
result, off = [], 0
|
||||||
|
while off < len(data):
|
||||||
|
start = off
|
||||||
|
tag, size = data[off:off + 2]
|
||||||
|
off += 2
|
||||||
|
if size & 128:
|
||||||
|
count = size & 127
|
||||||
|
if not count or count > 4:
|
||||||
|
raise ValueError('invalid DER length')
|
||||||
|
size = int.from_bytes(data[off:off + count], 'big')
|
||||||
|
off += count
|
||||||
|
if off + size > len(data):
|
||||||
|
raise ValueError('truncated DER')
|
||||||
|
result.append((tag, data[off:off + size], data[start:off + size]))
|
||||||
|
off += size
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _cert_key(cert):
|
||||||
|
outer = _der_parts(cert)
|
||||||
|
if len(outer) != 1 or outer[0][0] != 0x30:
|
||||||
|
raise ValueError('invalid certificate')
|
||||||
|
fields = _der_parts(outer[0][1])
|
||||||
|
tbs = _der_parts(fields[0][1])
|
||||||
|
spki = tbs[6 if tbs[0][0] == 0xa0 else 5][2]
|
||||||
|
pub = _der_parts(_der_parts(spki)[0][1])
|
||||||
|
if pub[0][2] != RSA_ALG or pub[1][1][:1] != b'\0':
|
||||||
|
raise ValueError('certificate is not RSA')
|
||||||
|
numbers = _der_parts(_der_parts(pub[1][1][1:])[0][1])
|
||||||
|
n, e = [int.from_bytes(item[1], 'big') for item in numbers]
|
||||||
|
return n, e, spki
|
||||||
|
|
||||||
|
|
||||||
|
def verify(path):
|
||||||
|
"""Verify this v2-only format; return True or raise ValueError on corruption.
|
||||||
|
|
||||||
|
A valid signature establishes integrity, not trust in the APK publisher.
|
||||||
|
"""
|
||||||
|
data = Path(path).read_bytes()
|
||||||
|
try:
|
||||||
|
eo, cd = _eocd(data)
|
||||||
|
if data[cd - 16:cd] != MAGIC:
|
||||||
|
raise ValueError('no APK signing block')
|
||||||
|
size = struct.unpack_from('<Q', data, cd - 24)[0]
|
||||||
|
start = cd - size - 8
|
||||||
|
if start < 0 or struct.unpack_from('<Q', data, start)[0] != size:
|
||||||
|
raise ValueError('invalid signing block size')
|
||||||
|
off, values = start + 8, []
|
||||||
|
while off < cd - 24:
|
||||||
|
length = struct.unpack_from('<Q', data, off)[0]
|
||||||
|
if length < 4 or off + 8 + length > cd - 24:
|
||||||
|
raise ValueError('invalid signing pair')
|
||||||
|
ident = struct.unpack_from('<I', data, off + 8)[0]
|
||||||
|
if ident == V2:
|
||||||
|
values.append(data[off + 12:off + 8 + length])
|
||||||
|
off += 8 + length
|
||||||
|
if off != cd - 24 or len(values) != 1:
|
||||||
|
raise ValueError('missing or duplicate v2 signer block')
|
||||||
|
end = bytearray(data[eo:])
|
||||||
|
struct.pack_into('<I', end, 16, start)
|
||||||
|
digest = content_digest((memoryview(data)[:start], memoryview(data)[cd:eo], end))
|
||||||
|
wrappers = _parts(values[0])
|
||||||
|
if len(wrappers) != 1:
|
||||||
|
raise ValueError('invalid signers sequence')
|
||||||
|
signers = _parts(wrappers[0])
|
||||||
|
if not signers:
|
||||||
|
raise ValueError('no signers')
|
||||||
|
for signer in signers:
|
||||||
|
signed, signatures, pub = _parts(signer)
|
||||||
|
digests, certs, attrs = _parts(signed)
|
||||||
|
cert = _parts(certs)[0]
|
||||||
|
n, e, cert_pub = _cert_key(cert)
|
||||||
|
if cert_pub != pub:
|
||||||
|
raise ValueError('public key differs from certificate')
|
||||||
|
sigs, digs = _parts(signatures), _parts(digests)
|
||||||
|
if len(sigs) != 1 or len(digs) != 1 or sigs[0][:4] != u32(ALG) or digs[0][:4] != u32(ALG):
|
||||||
|
raise ValueError('unsupported signature algorithm')
|
||||||
|
if _parts(digs[0][4:]) != [digest]:
|
||||||
|
raise ValueError('APK content digest mismatch')
|
||||||
|
sig = _parts(sigs[0][4:])
|
||||||
|
if len(sig) != 1:
|
||||||
|
raise ValueError('invalid signature sequence')
|
||||||
|
rsa_verify(signed, sig[0], n, e)
|
||||||
|
return True
|
||||||
|
except (IndexError, struct.error, OverflowError) as exc:
|
||||||
|
raise ValueError('malformed APK signature: ' + str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def repack(src, dst, replace=None, add=None, sign=True):
|
||||||
|
"""Copy raw compressed members; reconstruct ZIP headers without descriptors.
|
||||||
|
|
||||||
|
Reject ZIP64/encrypted archives. Output is atomically replaced after signing.
|
||||||
|
"""
|
||||||
|
replace, add = dict(replace or {}), dict(add or {})
|
||||||
|
central, output = [], io.BytesIO()
|
||||||
|
with open(src, 'rb') as raw, zipfile.ZipFile(raw) as archive:
|
||||||
|
names = archive.namelist()
|
||||||
|
if len(set(names)) != len(names):
|
||||||
|
raise ValueError('duplicate ZIP member names')
|
||||||
|
if set(replace) - set(names) or set(add) & set(names) or set(add) & set(replace):
|
||||||
|
raise ValueError('replace must exist and add must be new')
|
||||||
|
items = archive.infolist() + [zipfile.ZipInfo(name) for name in add]
|
||||||
|
for info in items:
|
||||||
|
name = info.filename
|
||||||
|
if re.match(r'^META-INF/(?:[^/]+\.(?:SF|RSA|EC|DSA)|MANIFEST\.MF)$', name, re.I):
|
||||||
|
continue
|
||||||
|
if info.flag_bits & 1 or info.compress_type not in (0, 8):
|
||||||
|
raise ValueError('unsupported ZIP encryption/compression')
|
||||||
|
method = info.compress_type
|
||||||
|
content = add.get(name) if name in add else replace.get(name)
|
||||||
|
if content is None:
|
||||||
|
raw.seek(info.header_offset)
|
||||||
|
header = raw.read(30)
|
||||||
|
if header[:4] != b'PK\3\4':
|
||||||
|
raise ValueError('invalid local ZIP header')
|
||||||
|
nl, el = struct.unpack_from('<HH', header, 26)
|
||||||
|
raw.seek(nl + el, 1)
|
||||||
|
compressed = raw.read(info.compress_size)
|
||||||
|
crc, usize = info.CRC, info.file_size
|
||||||
|
if len(compressed) != info.compress_size:
|
||||||
|
raise ValueError('truncated ZIP member')
|
||||||
|
else:
|
||||||
|
crc, usize = zlib.crc32(content), len(content)
|
||||||
|
if method == 8:
|
||||||
|
compressor = zlib.compressobj(6, zlib.DEFLATED, -15)
|
||||||
|
compressed = compressor.compress(content) + compressor.flush()
|
||||||
|
else:
|
||||||
|
compressed = content
|
||||||
|
encoded = name.encode('utf-8')
|
||||||
|
offset = output.tell()
|
||||||
|
align = 16384 if name.endswith('.so') else 4
|
||||||
|
needs_alignment = method == 0 or name.endswith('.so')
|
||||||
|
padding = (-(offset + 30 + len(encoded) + 6) % align) if needs_alignment else 0
|
||||||
|
# Android zipalign extra: alignment (uint16), then padding bytes.
|
||||||
|
extra = struct.pack('<HHH', 0xd935, padding + 2, align) + bytes(padding) if needs_alignment else b''
|
||||||
|
dt = info.date_time
|
||||||
|
dos_time = (dt[3] << 11) | (dt[4] << 5) | (dt[5] // 2)
|
||||||
|
dos_date = ((dt[0] - 1980) << 9) | (dt[1] << 5) | dt[2]
|
||||||
|
csize = len(compressed)
|
||||||
|
if max(offset, csize, usize) >= 0xffffffff:
|
||||||
|
raise ValueError('ZIP64 APKs are unsupported')
|
||||||
|
output.write(struct.pack('<IHHHHHIIIHH', 0x04034b50, 20, 0x800, method, dos_time, dos_date,
|
||||||
|
crc, csize, usize, len(encoded), len(extra)) + encoded + extra + compressed)
|
||||||
|
central.append(struct.pack('<IHHHHHHIIIHHHHHII', 0x02014b50, 0x314, 20, 0x800, method,
|
||||||
|
dos_time, dos_date, crc, csize, usize, len(encoded), 0, len(info.comment),
|
||||||
|
0, info.internal_attr, info.external_attr, offset) + encoded + info.comment)
|
||||||
|
cd = output.tell()
|
||||||
|
directory = b''.join(central)
|
||||||
|
if len(central) >= 65535 or cd + len(directory) >= 0xffffffff:
|
||||||
|
raise ValueError('ZIP64 APKs are unsupported')
|
||||||
|
output.write(directory)
|
||||||
|
output.write(struct.pack('<IHHHHIIH', 0x06054b50, 0, 0, len(central), len(central), len(directory), cd, len(archive.comment)) + archive.comment)
|
||||||
|
data = output.getvalue()
|
||||||
|
if sign:
|
||||||
|
data = sign_apk(data, signing_key())
|
||||||
|
dst = Path(dst)
|
||||||
|
fd, tmp = tempfile.mkstemp(prefix='.apk-', dir=str(dst.parent))
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
if sign:
|
||||||
|
verify(tmp)
|
||||||
|
os.replace(tmp, dst)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.unlink(tmp)
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"""Binary-manifest VR inspection and minimal launcher repair (stdlib only)."""
|
||||||
|
import struct
|
||||||
|
import frame_apk
|
||||||
|
|
||||||
|
MAIN = 'android.intent.action.MAIN'
|
||||||
|
LAUNCHER = 'android.intent.category.LAUNCHER'
|
||||||
|
VR = {'com.oculus.intent.category.VR', 'org.khronos.openxr.intent.category.IMMERSIVE_HMD'}
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(data):
|
||||||
|
elements = iter(frame_apk.manifest_elements(data))
|
||||||
|
stack, filters, samsung = [], [], False
|
||||||
|
current, owner, package = None, None, ''
|
||||||
|
for kind, hs, off, size in frame_apk._chunks(data, 8, len(data)):
|
||||||
|
if kind == 0x0102:
|
||||||
|
tag, attrs = next(elements)
|
||||||
|
value = attrs.get('name', (None, None, None))[2]
|
||||||
|
if tag == 'manifest':
|
||||||
|
package = attrs.get('package', (None, None, None))[2] or ''
|
||||||
|
if tag in ('activity', 'activity-alias'):
|
||||||
|
owner = attrs.get('targetActivity', (None, None, None))[2] if tag == 'activity-alias' else value
|
||||||
|
if owner and '.' not in owner: # PackageParser.buildClassName: bare names are relative too
|
||||||
|
owner = '.' + owner
|
||||||
|
owner = package + owner if owner and owner.startswith('.') else owner
|
||||||
|
if tag == 'intent-filter' and stack and stack[-1] in ('activity', 'activity-alias'):
|
||||||
|
current = {'actions': set(), 'categories': set(), 'templates': [], 'alias': stack[-1] == 'activity-alias', 'activity': owner}
|
||||||
|
if current is not None and stack and stack[-1] == 'intent-filter':
|
||||||
|
if tag == 'action':
|
||||||
|
current['actions'].add(value)
|
||||||
|
if tag == 'category':
|
||||||
|
current['categories'].add(value)
|
||||||
|
current['templates'].append((off, size, hs))
|
||||||
|
if tag == 'meta-data' and stack and stack[-1] == 'application':
|
||||||
|
samsung |= value == 'com.samsung.android.vr.application.mode' and attrs.get('value', (0, 0, None))[2] == 'vr_only'
|
||||||
|
stack.append(tag)
|
||||||
|
elif kind == 0x0103 and stack:
|
||||||
|
tag = stack.pop()
|
||||||
|
if tag == 'intent-filter' and current is not None:
|
||||||
|
current['end'] = off
|
||||||
|
filters.append(current)
|
||||||
|
current = None
|
||||||
|
mains = [f for f in filters if MAIN in f['actions']]
|
||||||
|
vr_filters = [f for f in mains if VR & f['categories']]
|
||||||
|
# Lepton's apk-info-extractor ignores <activity-alias>; Godot 4 puts LAUNCHER only there.
|
||||||
|
real = [f for f in mains if not f['alias']]
|
||||||
|
targets = [f for f in real if VR & f['categories']]
|
||||||
|
if not targets and any(LAUNCHER in f['categories'] or VR & f['categories'] for f in mains if f['alias']):
|
||||||
|
aimed = {f['activity'] for f in mains if f['alias'] and (LAUNCHER in f['categories'] or VR & f['categories'])}
|
||||||
|
targets = [f for f in real if f['templates']] # the patch copies an existing <category>
|
||||||
|
targets = [f for f in targets if f['activity'] in aimed] or targets
|
||||||
|
launchable = any(LAUNCHER in f['categories'] for f in real)
|
||||||
|
return {'launchable': launchable, 'repairable': not launchable and bool(targets),
|
||||||
|
'vr_activity': bool(vr_filters), 'vr': bool(vr_filters) or samsung}, targets
|
||||||
|
|
||||||
|
|
||||||
|
def _append_string(chunk, text):
|
||||||
|
_, hs, size, count, styles, flags, start, style_start = struct.unpack_from('<HHIIIIII', chunk)
|
||||||
|
strings_end = style_start or size
|
||||||
|
encoded = text.encode('utf-8' if flags & 0x100 else 'utf-16-le')
|
||||||
|
# LAUNCHER is short enough for both single-unit length encodings.
|
||||||
|
new = (bytes([len(text), len(encoded)]) + encoded + b'\0' if flags & 0x100
|
||||||
|
else struct.pack('<H', len(text)) + encoded + b'\0\0')
|
||||||
|
string_data = chunk[start:strings_end] + new
|
||||||
|
string_data += bytes(-len(string_data) % 4)
|
||||||
|
header = bytearray(chunk[:hs])
|
||||||
|
new_start = start + 4
|
||||||
|
new_styles = new_start + len(string_data) if style_start else 0
|
||||||
|
body = (chunk[hs:hs + count * 4] + struct.pack('<I', strings_end - start)
|
||||||
|
+ chunk[hs + count * 4:start] + string_data + (chunk[style_start:] if style_start else b''))
|
||||||
|
struct.pack_into('<IIIII', header, 8, count + 1, styles, flags & ~1, new_start, new_styles)
|
||||||
|
struct.pack_into('<I', header, 4, len(header) + len(body))
|
||||||
|
return bytes(header) + body, count
|
||||||
|
|
||||||
|
|
||||||
|
def add_launcher_category(axml_bytes):
|
||||||
|
info, filters = inspect(axml_bytes)
|
||||||
|
if info['launchable']:
|
||||||
|
return axml_bytes
|
||||||
|
if not filters:
|
||||||
|
raise frame_apk.ApkError('no activity with a MAIN intent filter that Frame Control can patch')
|
||||||
|
target = filters[0]
|
||||||
|
chunks = list(frame_apk._chunks(axml_bytes, 8, len(axml_bytes)))
|
||||||
|
pool = next(c for c in chunks if c[0] == 1)
|
||||||
|
_, _, po, ps = pool
|
||||||
|
new_pool, index = _append_string(axml_bytes[po:po + ps], LAUNCHER)
|
||||||
|
off, size, hs = target['templates'][0]
|
||||||
|
start = bytearray(axml_bytes[off:off + size])
|
||||||
|
attr_start, attr_size, count = struct.unpack_from('<HHH', start, hs + 8)
|
||||||
|
strings = frame_apk._string_pool(axml_bytes, po)
|
||||||
|
resmap = []
|
||||||
|
for kind, header_size, offset, chunk_size in chunks:
|
||||||
|
if kind == 0x180:
|
||||||
|
resmap = struct.unpack_from('<%dI' % ((chunk_size - header_size) // 4),
|
||||||
|
axml_bytes, offset + header_size)
|
||||||
|
for i in range(count):
|
||||||
|
a = hs + attr_start + i * attr_size
|
||||||
|
name = struct.unpack_from('<I', start, a + 4)[0]
|
||||||
|
if (name < len(resmap) and resmap[name] == 0x01010003) or strings[name] == 'name':
|
||||||
|
struct.pack_into('<I', start, a + 8, index)
|
||||||
|
struct.pack_into('<HBBI', start, a + 12, 8, 0, 3, index)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise frame_apk.ApkError('category has no name attribute')
|
||||||
|
end = struct.pack('<HHI', 0x0103, hs, hs + 8) + start[8:hs] + start[hs:hs + 8]
|
||||||
|
result = bytearray(axml_bytes[:8])
|
||||||
|
for _, _, off, size in chunks:
|
||||||
|
if off == target['end']:
|
||||||
|
result += start + end
|
||||||
|
result += new_pool if off == po else axml_bytes[off:off + size]
|
||||||
|
struct.pack_into('<I', result, 4, len(result))
|
||||||
|
result = bytes(result)
|
||||||
|
if not inspect(result)[0]['launchable']:
|
||||||
|
raise frame_apk.ApkError('launcher repair failed verification')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def detection(data, names):
|
||||||
|
info, _ = inspect(data)
|
||||||
|
issues = []
|
||||||
|
if 'lib/arm64-v8a/libvrapi.so' in names:
|
||||||
|
issues.append("Uses Meta's legacy VrApi, which the Frame doesn't have; it won't run.")
|
||||||
|
if any(n.endswith('/libovrplatformloader.so') for n in names):
|
||||||
|
issues.append("Uses Meta's platform SDK; if it checks your Quest store licence it will quit.")
|
||||||
|
if 'lib/arm64-v8a/libopenxr_loader.so' in names:
|
||||||
|
info['vr'] = True
|
||||||
|
issues.append('Uses OpenXR (good).')
|
||||||
|
info['vr_issues'] = issues
|
||||||
|
return info
|
||||||
@@ -640,6 +640,12 @@
|
|||||||
<span class="desk-only">You can also drop files anywhere in this window.</span>
|
<span class="desk-only">You can also drop files anywhere in this window.</span>
|
||||||
<input type="file" id="fileInput" multiple hidden>
|
<input type="file" id="fileInput" multiple hidden>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="sub" for="apkDisplay">Android display</label>
|
||||||
|
<select id="apkDisplay" aria-label="Android app display mode">
|
||||||
|
<option value="auto">Automatic (detect VR)</option>
|
||||||
|
<option value="flat">Flat screen</option>
|
||||||
|
<option value="vr">VR app</option>
|
||||||
|
</select>
|
||||||
<div class="progress" id="prog"><i></i></div>
|
<div class="progress" id="prog"><i></i></div>
|
||||||
<div class="shelf-head" style="margin-top:18px"><h2>Clipboard</h2></div>
|
<div class="shelf-head" style="margin-top:18px"><h2>Clipboard</h2></div>
|
||||||
<textarea id="clipText" placeholder="Text to put on the Frame's clipboard…"></textarea>
|
<textarea id="clipText" placeholder="Text to put on the Frame's clipboard…"></textarea>
|
||||||
@@ -1494,6 +1500,7 @@ function upload(file, mode) {
|
|||||||
xhr.setRequestHeader("X-Frame-UI", UI_KEY);
|
xhr.setRequestHeader("X-Frame-UI", UI_KEY);
|
||||||
xhr.setRequestHeader("X-Filename", encodeURIComponent(file.name));
|
xhr.setRequestHeader("X-Filename", encodeURIComponent(file.name));
|
||||||
xhr.setRequestHeader("X-Mode", mode);
|
xhr.setRequestHeader("X-Mode", mode);
|
||||||
|
if (mode === "apk") xhr.setRequestHeader("X-APK-Display", $("apkDisplay").value);
|
||||||
const bar = $("prog").firstElementChild;
|
const bar = $("prog").firstElementChild;
|
||||||
$("prog").style.display = "block"; bar.style.width = "0";
|
$("prog").style.display = "block"; bar.style.width = "0";
|
||||||
xhr.upload.onprogress = e => { if (e.lengthComputable) bar.style.width = (100 * e.loaded / e.total) + "%"; };
|
xhr.upload.onprogress = e => { if (e.lengthComputable) bar.style.width = (100 * e.loaded / e.total) + "%"; };
|
||||||
|
|||||||
+8
-2
@@ -1636,10 +1636,16 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
raise Failure(str(e), 400, {"package": info["package"], "version_code": info.get("version_code"), "blocker": str(e)})
|
raise Failure(str(e), 400, {"package": info["package"], "version_code": info.get("version_code"), "blocker": str(e)})
|
||||||
ensure_master()
|
ensure_master()
|
||||||
try:
|
try:
|
||||||
m = frame_android.install(str(dest), source=name)
|
display = self.headers.get("X-APK-Display", "auto")
|
||||||
|
if display not in ("auto", "flat", "vr"):
|
||||||
|
raise frame_android.FrameError("invalid APK display mode")
|
||||||
|
m = frame_android.install(str(dest), source=name,
|
||||||
|
flatscreen=None if display == "auto" else display == "flat")
|
||||||
except frame_android.FrameError as e:
|
except frame_android.FrameError as e:
|
||||||
raise Failure(str(e), 400)
|
raise Failure(str(e), 400)
|
||||||
return {"message": f"Installed {m['label']} as its own app in the Steam library", "app": m}
|
kind = "VR app" if not m['flatscreen'] else "app"
|
||||||
|
notes = " ".join(m.get("vr_issues", []))
|
||||||
|
return {"message": f"Installed {m['label']} as its own {kind} in the Steam library. {notes}".strip(), "app": m}
|
||||||
return {"message": push_file(dest)}
|
return {"message": push_file(dest)}
|
||||||
finally:
|
finally:
|
||||||
if not keep:
|
if not keep:
|
||||||
|
|||||||
Reference in new issue
Block a user