Compare commits

..
Author SHA1 Message Date
saphidandClaude Opus 5.5 e288946b81 Merge main into vr-apks: VR installs report through install_hooks like every other install
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:58:55 +10:00
Alex Southwell 6fbc450eea Merge pull request #17 from saphid/analytics-and-updates
Analytics, self-update and Report a problem
2026-09-29 09:55:12 +10:00
Alex Southwell 4f6d40625a Merge pull request #36 from saphid/linux-vr-streaming
docs: Linux VR feasibility and first-party streaming options
2026-09-29 09:39:01 +10:00
saphid a6137351d9 docs: record Linux VR client blockers and streaming options 2026-09-28 22:23:19 +10:00
saphidandClaude Opus 5.5 038dcd48cd Treat bare activity names as package-relative when matching the alias target
Third review follow-up (PackageParser.buildClassName). Confirmed the
targetActivity resource id 0x01010202 in Open Saber Plus's manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:04:55 +10:00
saphidandClaude Opus 5.5 9e9e5950d0 Patch the activity the launcher alias targets, not the first MAIN filter
Second review follow-up: with several activities (e.g. a splash activity ahead
of the game), the alias fallback now prefers the real activity named by the
alias's android:targetActivity. Reads targetActivity by resource id, updates
the error text and docs/vr-apks.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:55:56 +10:00
saphidandClaude Opus 5.5 268afccf05 APK sources: shared interface for source modules
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:52:30 +10:00
saphidandClaude Opus 5.5 e1c0ac983c Repair alias-only launchers for flat apps too; never fail on a VR alias
Review follow-up. inspect() now returns 'repairable' and the filters it can
patch: the real activity's VR MAIN filter, or, when LAUNCHER/VR sits only on an
<activity-alias>, any real MAIN filter with a category to copy. install() and
patch() repair on 'repairable' instead of 'vr_activity', so a flat Godot 4
export is fixed and a VR category only on the alias no longer aborts install.
Tests cover both shapes, an already-launchable activity with an alias, and an
end-to-end patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:40:16 +10:00
saphidandClaude Opus 5.5 077eab2b79 Add LAUNCHER to the real VR activity when only an activity-alias has it
Lepton's apk-info-extractor ignores <activity-alias>, and Godot 4 exports put
LAUNCHER only on an alias (com.godot.game.GodotAppLauncher). Open Saber Plus
0.7.67 installed but Lepton exited with 'APP_ACTIVITY is empty'. Count only
real activities as launchable and patch the activity's VR intent filter.
Verified on the Frame: Open Saber Plus launches and renders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:19:38 +10:00
saphid 35ef3af54b Merge remote-tracking branch 'origin/main' into vr-apks
# Conflicts:
#	ui/frame_android.py
#	ui/index.html
2026-09-28 17:49:47 +10:00
saphidandClaude Opus 5.5 f10282294d Fix the cross-provider review's findings on VR APK support
patch() turns corrupt-manifest struct/index errors into FrameError; a missing
layer build says so; the signing key falls back to a rename where hard links
aren't supported and explains how to recover from a bad cached key; the layer
nulls an instance it can't destroy and logs xrLocateSpaces once.

Not changed: the 1.1 Meta profile names match xr.xml's promoted names
(meta/touch_pro_controller, meta/touch_plus_controller), and grip_surface is
palm_ext renamed, so the rewrite stays (now commented).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:47:35 +10:00
saphidandClaude Opus 5.5 50d5e14539 Inject the OpenXR compatibility layer into VR APKs on install
VR apps with an arm64 OpenXR loader get frame/openxr-compat's layer unless
--no-xr-compat; the app bundle ships the layer. Verified on the Frame: Wolvic's
Quest build gets through OpenXR start-up, Open Brush still reaches FOCUSED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:33:14 +10:00
saphid fd15f1de4c Merge branch 'openxr-compat' into vr-apks 2026-09-28 16:29:11 +10:00
saphidandClaude Opus 5.5 064cf95f6d VR APK notes: Lepton's missing clipboard also stops the Godot XR Tools demo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:29:11 +10:00
saphidandClaude Opus 5.5 8d75ede0ab OpenXR compat layer: keep the current refresh rate when SteamVR refuses a request
Quest apps ask for 72/90/120/144 Hz; SteamVR offers only the current rate and
Wolvic aborted on XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB. Verified on
the Frame: Wolvic's Quest build now reaches XR_SESSION_STATE_SYNCHRONIZED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:29:06 +10:00
saphid 0dbe18d824 Add APK-local OpenXR 1.1 compatibility layer for Frame 2026-09-28 16:24:27 +10:00
saphid a1bc6522c3 Detect VR APKs and repair launchers with pure Python v2 signing 2026-09-28 16:16:56 +10:00
saphidandClaude Opus 5.5 724b020e93 Document what it takes to run VR and Quest APKs in Lepton
Verified on the headset: hello_xr and Open Brush's Quest build run
immersively unmodified; Wolvic's Quest build needs a LAUNCHER category
and fails on SteamVR's Android runtime being OpenXR 1.0 only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:56:53 +10:00
35 changed files with 18576 additions and 14 deletions

No files matched your search

+8
View File
@@ -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",
+6
View File
@@ -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
+47
View File
@@ -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.
+195
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# Preserve upstream headers byte-for-byte, including their existing whitespace.
vendor/** -whitespace
+1
View File
@@ -0,0 +1 @@
/build-*/
+25
View File
@@ -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()
+254
View File
@@ -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"
}
]
}
}
+12
View File
@@ -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"
+88
View File
@@ -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
+9
View File
@@ -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
+110
View File
@@ -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
+289
View File
@@ -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, &copy);
} 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(&copy, &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;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
// Only the opaque jobject declaration is needed by openxr_platform.h.
typedef void* jobject;
+208
View File
@@ -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
View File
@@ -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
File diff suppressed because it is too large. Load diff
+819
View File
@@ -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
+297
View File
@@ -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()
+45
View File
@@ -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
View File
@@ -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
View File
@@ -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
+361
View File
@@ -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)
+128
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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: