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