Hello OpenXR [Quest編]

Quest pro で Native 開発をするべく OpenXR しよう。
既存のプロジェクトを改造ビルドして参考にさせてもらうことで、プロジェクト構成などを理解する。

gradle と cmake の使いこなしをある程度マスターしないと、
ビルドではまる。

NativeActivity + openxr のプロジェクトをひたすらビルドしていく。

# 開発環境

- Windows11(64bit)
- `AndroidStudio-2021.3.1`
- `OpenXRMobileSDK-0.46` (Quest pro 発売時の最新)
- VR実機: 初代Quest, QuestPro で実験

# OpenXR とは

OpenGL のデスクトップアプリの glfw の VR 版のような感じです。
HMD Tracking や Controller Tracking やボタン操作を入力し、HMD に対して描画するための swapchain を提供するのがコア機能です。

ライブラリとしては `openxr_loader` です。
https://github.com/KhronosGroup/OpenXR-SDK-Source
で提供されています。

仕様

https://microsoft.github.io/OpenXR-MixedReality/openxr_preview/specs/openxr.html

[12. List of Current Extensions](https://microsoft.github.io/OpenXR-MixedReality/openxr_preview/specs/openxr.html#extension-appendices-list)

に拡張が列挙されています。

* XR_EXT_hand_tracking
* XR_FB_passthrough
* XR_FB_spatial_entity
* XR_FB_scene

等は既に記載があります。
一方で、`QuestPro` で新規に導入された、

* XR_FB_body_tracking
* XR_FB_face_tracking
* XR_FB_eye_tracking_sotial

などは、まだ書いていない。

# Android Studio を中心とした開発環境
C++の開発に VSCode などの他のエディタを活用する場合でも、関連するツールのインストール、バージョン合わせに有用なので AndroidStudio をインストールするとよい。

https://developer.android.com/studio/command-line/variables?hl=ja

## Java
AndroidStudio のインストールフォルダに付属するのでこれを使う。

`JAVA_HOME=ANDROID_STUDIO_INSTALL\jre`

## AndroidSdk
APPDATA下にインストールされる。

`ANDROID_HOME=%USERPROFILE%\AppData\Local\Android\Sdk`
`ANDROID_HOME/platform-tools` に `adb.exe` が入っている。

## CMake
ANDROID_HOME下の `cmake\3.18.1` などにインストールされる。
`cmake.exe` と同じパスに `ninja.exe` があるのでパスを通すと `vscode` などで便利。
紆余曲折を経て、`ANDROID_HOME/cmake/3.22.1` を使ってます。

:::note warn 
path の通った cmake に注意

システムに他の cmake が入っている場合はなるべくアンインストールする。
最低でも PATH が通っていない状態にする。
意図しない違うバージョンの cmake が使われたり、 cmake が見つからないエラーが起きる場合がある。
例えば `pip install cmake` で `C:/Python310/Scripts/cmake.exe` にPATHを通して使っていたのだが、これは gradle 運用で cmake が見つからないというトラブルが起きた。
:::

:::note warn 
gradle のデフォルトは cmake-3.10

https://developer.android.com/studio/projects/install-ndk?hl=ja#default-version

```groovy
android {
    defaultConfig {
        cmake {
            // ここではない
        }
    }

    externalNativeBuild {
        cmake {
            version "3.22.1" // 👈 こっち
            path "CMakeLists.txt"
        }
    }
}
```

cmake のバグ？らしく CMake で複数の so を生成すると、
最初の一つ以外が消えてしまう挙動に遭遇。`CMake-3.22.1` にしたらなおった。

どうしても version 指定がうまくいかない場合は、
local.properties などに下のように書く方法がある。
```
cmake.dir = "PATH_TO_CMAKE_3_22_1"
```
:::

## NDK
`ANDROID_HOME/ndk/21.4.7075529` などにインストールされる。

# 追加インストール

## Python
OpenXR-SDK の CMake が使っている。
`gradle-7.4` デフォルトの `CMake-3.10` から find できる一番新しいバージョンである `Python-3.7` をインストールする。

:::note warn 
OpenXR-SDK の cmake で使われている
`find_package(PythonInterp 3)` が失敗する。

cmake のバージョンによって発見できる Python のバージョンに限界があります。

|cmake|python|
|-|-|
|3.10|3.7|
|3.18|3.9|
|3.22|3.10|

`CMAKE_VERSION\share\cmake-VERSION\Modules\FindPython\Support.cmake` などを見ると
対応している python がわかります。
:::

## Gradle
コマンドラインで使う場合は追加でダウンロードしてパスを通す。
gradlew を使うことが多いので必要ないことが多い
ANDROID_STUDIO と同じ `gradle-7.4` を選ぶ。

`ANDROID_STUDIO_INSTALL\plugins\gradle` にライブラリは入っているがコマンドラインが無い。

::: note info
gradle-7.4 で AGP(AndroidGradlePlugin)-7.3.1 を使う

gradlew で gradlew を更新できます。

```
> cd src/tests/hello_xr
src/tests/hello_xr> .\gradlew --version
7.0.2
src/tests/hello_xr> .\gradlew wrapper --gradle-version 7.4
src/tests/hello_xr> .\gradlew --version
7.4
```

AGP のバージョン指定は rootProject の最初の方です。

```gradle
buildscript {
    repositories {
       google()
       mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.3.1" // 👈 AGP のバージョン指定
    }
}
```

:::

# adb によるデバイス接続確認

`ANDROID_HOME/platform-tools/adb.exe` です。

* スマホのQuestアプリでQuestを開発者モードにする
* Windows 側で QUEST の usb ドライバをインストールする。 https://developer.oculus.com/downloads/package/oculus-adb-drivers
* Quest 実機を PCに USB 接続する
* Quest 側で USB 接続を許可する

```
# 接続されたデバイスを一覧する
$ adb devices -l
```

# NDK練習
最初に、[ndk-samples](https://github.com/android/ndk-samples) から native_activity をやります。

native_activy は java 無しで CMake を使う小さいサンプルで、300行くらいの c++ で OpenGLES で `glClear` するだけという手頃なプロジェクトです。
gradle と CMake の構成もシンプルでわかりやすいです。

## AndroidStudio で open

native_activity を開く。
ビルドしてデプロイする。

## app/build.gradle の構成

抜粋。

```gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    ndkVersion '22.1.7171670'

    defaultConfig {
        applicationId = 'com.example.native_activity'
        minSdkVersion 14
        targetSdkVersion 28
        externalNativeBuild { // 👈 defaultConfig.externalNativeBuild。artuments などが指定できる。cmake 発動しない
            cmake {
                arguments '-DANDROID_STL=c++_static'
            }
        }
    }

    externalNativeBuild { // 👈 defaultConfig.externalNativeBuild と別物。cmake で指定できる内容が違う
        cmake {
            version '3.18.1'
            path 'src/main/cpp/CMakeLists.txt' // 👈 必須ぽい？これが書いてあると cmake が発動する。
        }
    }
}
```

## CMake の構成
main.cpp 単体で so ファイルを作成するシンプルなプロジェクトです。

```cmake:native-activity/app/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

# build native_app_glue as a static lib
set(${CMAKE_C_FLAGS}, "${CMAKE_C_FLAGS}")
add_library(native_app_glue STATIC
    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c) # 👈 NDK で必要

# now build app's shared lib
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall -Werror")

# Export ANativeActivity_onCreate(),
# Refer to: https://github.com/android-ndk/ndk/issues/381.
set(CMAKE_SHARED_LINKER_FLAGS
    "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate") # 👈 NDK で必要

add_library(native-activity SHARED main.cpp) # 👈 apk の lib に libnative-activity.so を含める

target_include_directories(native-activity PRIVATE
    ${ANDROID_NDK}/sources/android/native_app_glue)

# add lib dependencies
target_link_libraries(native-activity
    android # 👈 システムの so にリンク。apk に含まない
    native_app_glue # android_native_app_glue.c
    EGL # 👈 システムの so にリンク。apk に含まない
    GLESv1_CM # 👈 システムの so にリンク。apk に含まない
    log # 👈 システムの so にリンク。apk に含まない
)
```

## gradle からの cmake 呼び出し

エラーメッセージから得た。
下記のような感じで呼び出しています。

```
CMAKE_EXE
-HPROJECT_DIR
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DCMAKE_SYSTEM_VERSION=24
-DANDROID_PLATFORM=android-24
-DANDROID_ABI=arm64-v8a
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DANDROID_NDK=ANDROID_HOME\\ndk\\21.4.7075529
-DCMAKE_ANDROID_NDK=ANDROID_HOME\\ndk\\21.4.7075529
-DCMAKE_TOOLCHAIN_FILE=NDK_DIR\\build\\cmake\\android.toolchain.cmake
-DCMAKE_MAKE_PROGRAM=ANDROID_HOME\\cmake\\3.18.1\\bin\\ninja.exe
-DCMAKE_CXX_FLAGS=GRDLE_EXTERNAL_NATIVE_BUILD_CMAKE_CPPFLAGS
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=PROJECT_DIR\\build\\intermediates\\cxx\\Debug\\__TMP__\\obj\\arm64-v8a
-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=PROJECT_DIR\\build\\intermediates\\cxx\\Debug\\__TMP__\\obj\\arm64-v8a
-DCMAKE_BUILD_TYPE=Debug
-BPROJECT_DIR\\.cxx\\Debug\\__TMP__\\arm64-v8a
-GNinja	
```

## ビルド結果を apk に送り込む

gradle から cmake を呼び出したときに `CMAKE_LIBRARY_OUTPUT_DIRECTORY` が指定されてそこに `so` が出力されます。

```
native-activity\app\build\intermediates\cxx\Debug\RANDOM\obj

x86_64\libnative-activity.so
arm64-v8a\libnative-activity.so
x86\libnative-activity.so
armeabi-v7a\libnative-activity.so
```

abi 毎に４回ビルドされる。

## apk の構成

`app\build\outputs\apk` に出力されます。
zip アーカイブなので中身の lib フォルダを見ると下記のようになっています。
AndroidStudio の ApkAnalyzer で見ることができます。

```
native-activity\app\build\outputs\apk\debug\app-debug.apk

lib/arm64-v8a/libnative-activity.so
lib/x86_64/libnative-activity.so
lib/armeabi-v7a/libnative-activity.so
lib/x86/libnative-activity.so
```

## 要するに

```cmake:CMakeLists.txt
add_library(LIB_NAME SHARED some.cpp)
```

`libLIB_NAME.so` が apk に含まれます。

# OpenXR-SDK
[OpenXR-SDK-Source](https://github.com/KhronosGroup/OpenXR-SDK-Source) から hello_xr をやります。

デスクトップは cmake でビルドするだけなので簡単です。

https://qiita.com/ousttrue/items/af1b1e62dd5348b4ce59

## OpenXR-SDK と OpenXR-SDK-Source

[OpenXR-SDK-Source](https://github.com/KhronosGroup/OpenXR-SDK-Source) には `openxr.h` が含まれておらず、ビルドすることで生成することができます。
生成物をコミットしたのが [OpenXR-SDK](https://github.com/KhronosGroup/OpenXR-SDK) です。
 `openxr_loader.dll` などをビルドしたのが `OpenXR.Loader.1.0.25.nupkg` です。
Windows 環境では openxr_loader が複数の RunTime のどれかをロードするという挙動をするので、
openxr_loader 自体は自前ビルドでよいようです。

Android の openxr_loader はそのデバイスのランタイムをロードする専用の `libopenxr_loader.so` を、
デバイスのベンダーが配布するという方式のようです。

:::note info
libopenxr_loader.so は OpenXRMobileSDK に含まれるものを使う

[OVR_MobileSdk](https://developer.oculus.com/documentation/native/android/mobile-intro/) の中に入っています。

```
OpenXRMobileSDK\OpenXR\Libs\Android\armeabi-v7a\Debug\libopenxr_loader.so
OpenXRMobileSDK\OpenXR\Libs\Android\armeabi-v7a\Release\libopenxr_loader.so
OpenXRMobileSDK\OpenXR\Libs\Android\arm64-v8a\Debug\libopenxr_loader.so
OpenXRMobileSDK\OpenXR\Libs\Android\arm64-v8a\Release\libopenxr_loader.so
```
:::

:::note warn
Android の libopenxr_loader.so の自前ビルドは (たぶん) 動かない

OpenXR-SDK もしくは OpenXR-SDK-Source からビルドすることができます。
試しに hello_xr で使ってみたところ
真っ黒になりました。
Quest固有の機能を提供できないので何もしないローダーになっていそう。
:::

## OpenXR-SDK-Source の src/tests/hello_xr を Quest 向けにビルドする

native_activiy に、openxr_loader が追加された VR のプロジェクトです。
OpenXR の拡張機能を使わない、HMD Tracking, コントローラ Tracking と入力、VR レンダリング の入ったプロジェクトです。
d3d, opengl, vulkan, windows, android 全対応のため CMake の構成が複雑化しています。

## AndroidStudio で hello_xr を開く
AndroidStudio で OpenXR-SDK-Source の `src/tests/hello_xr` フォルダを開きます。

## 修正してビルドする
Quest 向けに hello_xr を改造する手順が下記の URL に書いてあります。
https://developer.oculus.com/documentation/native/android/mobile-build-run-hello-xr-app/

* [cmake]openxr_loader をビルドせずに OpenXRMobileSDK の物を使うようにする
* [gladle]cmake 引数を変更
* [AndroidManifest.xml]intent を quest 用に変更

:::note info
ビルド済みのライブラリを apk に含める

```cmake
add_library(openxr_loader SHARED IMPORTED) # 👈 SHARED IMPORTED でできます
get_filename_component(
  MOBILE_SDK_DIR
  ${CMAKE_CURRENT_LIST_DIR}/../../OpenXRMobileSDK ABSOLUTE) # 👈 パスを文字列で使うと失敗するときがあったので、絶対パスで回避
set_property(
 TARGET
 openxr_loader
 PROPERTY
 IMPORTED_LOCATION # 👈 IMPORTED_LOCATION でパスを指定します
 ${MOBILE_SDK_DIR}/OpenXR/Libs/Android/${ANDROID_ABI}/release/libopenxr_loader.so
)
```
:::

## APK の構成
hello_xr の apk には以下の so が含まれます。

```
armeabi-v7a\libXrApiLayer_core_validation.so
armeabi-v7a\libXrApiLayer_api_dump.so
armeabi-v7a\libopenxr_loader.so
armeabi-v7a\libhello_xr.so
armeabi-v7a\libGLESv3.so 👈 たぶん不要
armeabi-v7a\libEGL.so 👈 たぶん不要
arm64-v8a\libXrApiLayer_core_validation.so
arm64-v8a\libXrApiLayer_api_dump.so
arm64-v8a\libopenxr_loader.so
arm64-v8a\libhello_xr.so
arm64-v8a\libGLESv3.so 👈 たぶん不要
arm64-v8a\libEGL.so 👈 たぶん不要
```

# OpenXRMobileSDK のサンプル

`v0.46`

[OVR_MobileSdk](https://developer.oculus.com/documentation/native/android/mobile-intro/) から XrHandsFB をやります。
native_activiy に、openxr_loader と XR_EXT_hand_tracking が追加された VR のプロジェクトです。

* 中規模の C++ フレームワークが ndk-build(Makefile) で記述されている
* gradle の構成が変則的で、python スクリプト `OpenXRMobileSDK/XrSamples/XrHandsFB/Projects/Android/build.py` から gradle を実行するためわかりにくい
  * `OpenXRMobileSDK/build.gradle` + `OpenXRMobileSDK/XrSamples/XrHandsFB/Projects/Android/settings.gradle` の組み合わせで動くトリックになっている様子
  * そのため AndroidStudio で開けない

## XrSamples/XrHandsFB

XrSamples は ndk-build を使う方式記述されているので、以下の `Makefile` に C++ の構成が記述されている。

* `XrSamples\XrHandsFB\Projects\Android\jni\Android.mk`
* OpenXR\Projects\AndroidPrebuilt\jni\Android.mk
* SampleXrFramework\Projects\Android\jni\Android.mk
* SampleCommon\Projects\Android\jni\Android.mk
* 3rdParty\khronos\ktx\makefiles\androidprebuilt\jni\Android.mk
* 3rdParty\khronos\ktx\makefiles\android\jni\Android.mk

`OpenXRMobileSDK/XrSamples/XrHandsFB/Projects/Android/build.py` を実行する。
=> `OpenXRMobileSDK\XrSamples\XrHandsFB\Projects\Android\build\outputs\apk\release\xrhandsfb-release.apk` 

`OpenXRMobileSDK/bin/scripts/ovrbuild.py` を呼び出していて、
`ovrbuild` は、環境変数 `ANDROID_HOME` と `ANDROID_NDK_HOME` を必要としている。
`build.py` 実行前に追加の環境変数 `ANDROID_NDK_HOME = $ANDROID_HOME/ndk/25.1.8937393` のように設定します。

## build.gradle と CMake を書いてみた

https://github.com/ousttrue/openxr_samples/tree/master/XrHandsFB

AndroidStudio で開けるようになった。
首尾よく Quest で動きました。

OpenXR のプロジェクト最小構成はだいたい以下の通り。

* libopenxr_loader.so を含む(OpenXRMobileSDK に含まれるものを使う)
  * `openxr/openxr.h` は OpenXR-SDK を使う
  * `openxr/openxr.h` に含まれていない新しい拡張のヘッダは OpenXRMobileSDK に入っている。
* NativeActivity で Java 抜き
  * NativeActivity を実装する so を含む(自作する)
* OpenGLES で描画する
* Quest 専用の intent `com.oculus.intent.category.VR` が必要

# VSCode で C++ の開発をする

gradle からの CMake の呼び出しが

```
CMAKE_EXE
-HPROJECT_DIR
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DCMAKE_SYSTEM_VERSION=24
-DANDROID_PLATFORM=android-24
-DANDROID_ABI=arm64-v8a
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DANDROID_NDK=ANDROID_HOME\\ndk\\21.4.7075529
-DCMAKE_ANDROID_NDK=ANDROID_HOME\\ndk\\21.4.7075529
-DCMAKE_TOOLCHAIN_FILE=NDK_DIR\\build\\cmake\\android.toolchain.cmake
-DCMAKE_MAKE_PROGRAM=ANDROID_HOME\\cmake\\3.18.1\\bin\\ninja.exe
-DCMAKE_CXX_FLAGS=GRDLE_EXTERNAL_NATIVE_BUILD_CMAKE_CPPFLAGS
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=PROJECT_DIR\\build\\intermediates\\cxx\\Debug\\__TMP__\\obj\\arm64-v8a
-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=PROJECT_DIR\\build\\intermediates\\cxx\\Debug\\__TMP__\\obj\\arm64-v8a
-DCMAKE_BUILD_TYPE=Debug
-BPROJECT_DIR\\.cxx\\Debug\\__TMP__\\arm64-v8a
-GNinja	
```

という感じなので以下のような設定で、vscode の F7 で cmake ビルドし、
lsp として clangd  を稼働させることができます。
build 結果は ${workspaceFolder}/build に出ます。
必要に応じて、gradle の `android.defaultConfig.externalNativeBuild.cmake.arguments` の内容を追記します。

```json:.vscode/settings.json
{
    "cmake.sourceDirectory": "${workspaceFolder}/DIR_TO_CMAKELISTS_TXT",
    "cmake.configureSettings": {
        "CMAKE_TOOLCHAIN_FILE": "${env:ANDROID_HOME}/ndk/21.4.7075529/build/cmake/android.toolchain.cmake",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
        "ANDROID": "ON",
        "ANDROID_PLATFORM": "android-28",
        "ANDROID_ABI": "arm64-v8a",
        "CMAKE_ANDROID_ARCH_ABI": "arm64-v8a",
    },
    "clangd.arguments": [
        "--compile-commands-dir=${workspaceFolder}/build" // 👈 GRADLE_PROJECT\app\.cxx\Debug\RANDOM\arm64-v8a\compile_commands.json
    ],
}
```

gradle の出力する `compile_commands.json` を参照させることもできるけど、
パスに乱数が含まれていて不便。
おそらく、`${workspaceFolder}/build` を指定する方が便利。

# LÖVR

https://github.com/bjornbytes/lovr

lua で VR するフレームワーク。
まだ、ビルドして動作するところまでしか見てない。

apk 作成も含めて全部 CMake でビルドするようになっていて gradle が無い。
AndroidStudio からデバッガをアタッチしたいので gradle プロジェクトを作る。

TODO:

# 関連

https://qiita.com/ousttrue/items/8f0cc2727c55fcfd02e1

https://qiita.com/ousttrue/items/af1b1e62dd5348b4ce59