├── CloudXRLaunchOptions.txt
├── settings.gradle
├── app
├── src
│ └── main
│ │ ├── jni
│ │ ├── Application.mk
│ │ ├── log.h
│ │ ├── Android.mk
│ │ ├── jni.cpp
│ │ ├── WaveCloudXRApp.h
│ │ └── WaveCloudXRApp.cpp
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── jni_icon.png
│ │ │ ├── purenative_icon.png
│ │ │ └── singlebuffer_icon.png
│ │ ├── mipmap-mdpi
│ │ │ ├── jni_icon.png
│ │ │ ├── purenative_icon.png
│ │ │ └── singlebuffer_icon.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── jni_icon.png
│ │ │ ├── purenative_icon.png
│ │ │ └── singlebuffer_icon.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── jni_icon.png
│ │ │ ├── purenative_icon.png
│ │ │ └── singlebuffer_icon.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── jni_icon.png
│ │ │ ├── purenative_icon.png
│ │ │ └── singlebuffer_icon.png
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ ├── values
│ │ │ ├── styles.xml
│ │ │ ├── dimens.xml
│ │ │ └── strings.xml
│ │ └── values-w820dp
│ │ │ └── dimens.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── htc
│ │ └── vr
│ │ └── samples
│ │ └── wavecloudxr
│ │ └── MainActivity.java
├── libs
│ └── NVIDIA CloudXR SDK 1.0 License.pdf
├── build_base.gradle
├── build.gradle
└── build_sdk.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── README.md
├── gradlew.bat
├── LICENSE
└── gradlew
/CloudXRLaunchOptions.txt:
--------------------------------------------------------------------------------
1 | -s 192.168.1.1
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/jni/Application.mk:
--------------------------------------------------------------------------------
1 | APP_STL := c++_shared
2 | APP_CPPFLAGS := -std=c++11
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea/
3 | app/.cxx/
4 | app/build/
5 | repo/
6 | app/libs/CloudXR.aar
7 | local.properties
8 | app/libs/oboe-1.5.0.aar
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/jni_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/jni_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/jni_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/jni_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/jni_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/jni_icon.png
--------------------------------------------------------------------------------
/app/libs/NVIDIA CloudXR SDK 1.0 License.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/libs/NVIDIA CloudXR SDK 1.0 License.pdf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/jni_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/jni_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/jni_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/jni_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/purenative_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/purenative_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/singlebuffer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/singlebuffer_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/purenative_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/purenative_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/singlebuffer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/singlebuffer_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/purenative_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/purenative_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/purenative_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/purenative_icon.png
--------------------------------------------------------------------------------
/app/build_base.gradle:
--------------------------------------------------------------------------------
1 | android {
2 | compileSdkVersion 29
3 |
4 | defaultConfig {
5 | minSdkVersion 25
6 | targetSdkVersion 29
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/singlebuffer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/singlebuffer_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/singlebuffer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/singlebuffer_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/purenative_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/purenative_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/singlebuffer_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/singlebuffer_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Wave CloudXR Sample Client
3 | Wave CloudXR Sample Client
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Aug 30 19:16:42 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/jni/log.h:
--------------------------------------------------------------------------------
1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 |
3 | #ifndef _LOG
4 | #define _LOG
5 |
6 | #ifndef LOG_TAG
7 | #define LOG_TAG "WaveCloudXRJNI"
8 | #endif
9 |
10 | #include
11 |
12 | #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
13 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
14 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
15 | #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
16 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
17 | #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)
18 |
19 | #define LogD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__)
20 | #define LogE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__)
21 |
22 |
23 | #endif // __LOG
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 | org.gradle.daemon=true
10 |
11 | # Specifies the JVM arguments used for the daemon process.
12 | # The setting is particularly useful for tweaking memory settings.
13 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
14 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
15 | org.gradle.jvmargs=-Xmx1563M
16 |
17 | # When configured, Gradle will run in incubating parallel mode.
18 | # This option should only be used with decoupled projects. More details, visit
19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
20 | # org.gradle.parallel=true
21 |
22 | org.gradle.configureondemand=true
23 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply from: "$projectDir/build_base.gradle"
3 | apply from: "$projectDir/build_sdk.gradle"
4 |
5 | android {
6 | buildToolsVersion '28.0.3'
7 | ndkVersion '21.4.7075529'
8 | defaultConfig {
9 | applicationId "com.htc.vr.samples.wavecloudxr"
10 | versionCode 4
11 | versionName "1.5"
12 | }
13 |
14 | signingConfigs {
15 | debug {}
16 | }
17 |
18 | buildTypes {
19 | debug {
20 | signingConfig signingConfigs.debug
21 | }
22 | release {
23 | signingConfig signingConfigs.debug
24 | }
25 | }
26 |
27 | flavorDimensions "version"
28 | productFlavors {
29 |
30 | bit64 {
31 | dimension "version"
32 | applicationId "com.htc.vr.samples.wavecloudxr"
33 | ndk {
34 | abiFilter 'arm64-v8a'
35 | }
36 | }
37 | }
38 | }
39 |
40 | repositories {
41 | flatDir{
42 | dirs 'libs'
43 | }
44 | }
45 |
46 | dependencies {
47 | api 'com.htc.vr:wvr_client:+'
48 | implementation "com.nvidia.CloudXRClient:CloudXR@aar"
49 | implementation 'com.android.support:support-compat:28.0.0'
50 | implementation 'com.android.support:support-v4:28.0.0'
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/jni/Android.mk:
--------------------------------------------------------------------------------
1 | # ========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 | LOCAL_PATH := $(call my-dir)
3 |
4 | VR_SDK_LIB := $(VR_SDK_ROOT)/jni/$(TARGET_ARCH_ABI)
5 | OBOE_SDK_LIB := $(OBOE_SDK_ROOT)/prefab/modules/oboe/libs/android.$(TARGET_ARCH_ABI)
6 | CLOUDXR_SDK_LIB := $(CLOUDXR_SDK_ROOT)/jni/$(TARGET_ARCH_ABI)
7 |
8 | include $(CLEAR_VARS)
9 | LOCAL_MODULE := wvr_api
10 | LOCAL_SRC_FILES := $(VR_SDK_LIB)/libwvr_api.so
11 | include $(PREBUILT_SHARED_LIBRARY)
12 |
13 | include $(CLEAR_VARS)
14 | LOCAL_MODULE := Oboe
15 | LOCAL_SRC_FILES := $(OBOE_SDK_LIB)/liboboe.so
16 | include $(PREBUILT_SHARED_LIBRARY)
17 |
18 | include $(CLEAR_VARS)
19 | LOCAL_MODULE := CloudXRClient
20 | LOCAL_SRC_FILES := $(CLOUDXR_SDK_LIB)/libCloudXRClient.so
21 | include $(PREBUILT_SHARED_LIBRARY)
22 |
23 | COMMON_INCLUDES := \
24 | $(LOCAL_PATH)/include \
25 | $(VR_SDK_ROOT)/include \
26 | $(CLOUDXR_SDK_ROOT)/include \
27 | $(OBOE_SDK_ROOT)/prefab/modules/oboe/include \
28 | $(LOCAL_PATH)
29 |
30 | COMMON_FILES := \
31 | WaveCloudXRApp.cpp \
32 | jni.cpp
33 |
34 | #USE_CONTROLLER use device controller.
35 | #USE_CUSTOM_CONTROLLER use device emitter.
36 |
37 | include $(CLEAR_VARS)
38 | LOCAL_MODULE := WaveCloudXRJNI
39 | LOCAL_C_INCLUDES := $(COMMON_INCLUDES)
40 | LOCAL_SRC_FILES := $(COMMON_FILES)
41 | LOCAL_CPPFLAGS += -fexceptions
42 | LOCAL_LDLIBS := -llog -ljnigraphics -landroid -lEGL -lGLESv3
43 | LOCAL_SHARED_LIBRARIES := wvr_api Oboe CloudXRClient
44 | include $(BUILD_SHARED_LIBRARY)
45 |
--------------------------------------------------------------------------------
/app/build_sdk.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$projectDir/build_base.gradle"
2 | def jniDir = "src/main/jni"
3 | def aarFilename = "wvr_client"
4 |
5 | // unzip CloudXR SDK into build directory
6 | def cxrFile = file("${projectDir}/libs/CloudXR.aar")
7 | def CLOUDXR_SDK_ROOT = file("${buildDir}/CloudXR")
8 | if (cxrFile != null) {
9 | copy {
10 | println 'unzipping ' + cxrFile + ' into ' + CLOUDXR_SDK_ROOT
11 | from zipTree(cxrFile)
12 | into CLOUDXR_SDK_ROOT
13 | }
14 | }
15 |
16 | // unzip the Oboe sdk into build directory
17 | def oboeVersion = "1.5.0"
18 | def oboeFile = file("${projectDir}/libs/oboe-${oboeVersion}.aar")
19 | def OBOE_SDK_ROOT = file("${buildDir}/Oboe")
20 | if (oboeFile != null) {
21 | copy {
22 | println 'unzipping ' + oboeFile + ' into ' + OBOE_SDK_ROOT
23 | from zipTree(oboeFile)
24 | into OBOE_SDK_ROOT
25 | }
26 | }
27 |
28 | android {
29 | defaultConfig {
30 | externalNativeBuild { ndkBuild {
31 | def VR_SDK_ROOT = "${buildDir}/${aarFilename}"
32 | arguments "VR_SDK_ROOT=$VR_SDK_ROOT"
33 | arguments "OBOE_SDK_ROOT=$OBOE_SDK_ROOT"
34 | arguments "CLOUDXR_SDK_ROOT=$CLOUDXR_SDK_ROOT"
35 | }}
36 | ndk {
37 | abiFilters 'arm64-v8a'
38 | }
39 | }
40 |
41 | externalNativeBuild {
42 | ndkBuild {
43 | path "${jniDir}/Android.mk"
44 | }
45 | }
46 |
47 | buildTypes {
48 | release {
49 | externalNativeBuild { ndkBuild {
50 | arguments "NDK_DEBUG=0"
51 | }}
52 | }
53 | debug {
54 | externalNativeBuild { ndkBuild {
55 | arguments "NDK_DEBUG=1"
56 | }}
57 | }
58 | }
59 | }
60 |
61 | dependencies {
62 | compile 'com.htc.vr:wvr_client:+'
63 | }
64 |
65 | // unzip aar
66 | afterEvaluate {
67 | def unzipDir = file("${buildDir}/${aarFilename}")
68 | def artifacts = configurations.compile.resolvedConfiguration.resolvedArtifacts
69 | artifacts.find { it.name == aarFilename }?.with { aar ->
70 | copy {
71 | println 'unzip: ' + aar
72 | from zipTree(aar.file)
73 | into unzipDir
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/jni/jni.cpp:
--------------------------------------------------------------------------------
1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | bool gPaused = true;
10 |
11 | int main(int argc, char *argv[]) {
12 |
13 | WaveCloudXRApp *app = new WaveCloudXRApp();
14 | if (!app->initVR()) {
15 | app->shutdownVR();
16 | delete app;
17 | return 1;
18 | }
19 |
20 | if (!app->initGL()) {
21 | app->shutdownGL();
22 | app->shutdownVR();
23 | delete app;
24 | return 1;
25 | }
26 |
27 | if (!app->initCloudXR()) {
28 | app->shutdownGL();
29 | app->shutdownVR();
30 | app->shutdownCloudXR();
31 | delete app;
32 | return 1;
33 | }
34 |
35 | app->beginPoseStream();
36 | while (1) {
37 | if (!app->HandleCloudXRLifecycle(gPaused))
38 | break;
39 |
40 | if (!app->handleInput())
41 | break;
42 |
43 | if (!app->renderFrame())
44 | break;
45 |
46 | // app->updatePose();
47 | }
48 | app->stopPoseStream();
49 |
50 | LOGE("Stop streaming.");
51 | LOGE("Shutting down components.");
52 | app->shutdownGL();
53 | app->shutdownVR();
54 | app->shutdownCloudXR();
55 |
56 | delete app;
57 | return 0;
58 | }
59 |
60 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeInit(JNIEnv * env, jobject activityInstance, jobject assetManagerInstance) {
61 | LOGI("Register WVR main");
62 | WVR_RegisterMain(main);
63 | }
64 |
65 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeOnPause(JNIEnv * env, jobject activityInstance) {
66 | gPaused = true;
67 | }
68 |
69 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeOnResume(JNIEnv * env, jobject activityInstance) {
70 | gPaused = false;
71 | }
72 |
73 |
74 | jint JNI_OnLoad(JavaVM* vm, void* reserved) {
75 |
76 | return JNI_VERSION_1_6;
77 | }
78 |
79 | jint JNI_OnUnLoad(JavaVM* vm, void* reserved) {
80 |
81 | return 0;
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Wave CloudXR Sample Client
3 |
4 | Demonstrate how to program with NVIDIA CloudXR SDK for VIVE Focus 3 and VIVE XR Elite headset. You can start to develop your own CloudXR application based on this sample client.
5 |
6 | Below are the instructions to build from source. Alternatively you can find a pre-built APK in the [Releases](https://github.com/ViveSoftware/Wave-CloudXR-Sample/releases) section.
7 |
8 | ## Requirements
9 | - HTC VIVE Focus 3 or VIVE XR Elite
10 | - [Wave Native SDK 4.3.0](https://developer.vive.com/resources/vive-wave/download/latest/) or later
11 | - [CloudXR SDK 4.0](https://developer.nvidia.com/nvidia-cloudxr-sdk)
12 | - [Google OBOE SDK 1.5.0](https://github.com/google/oboe/releases/tag/1.5.0)
13 | - Android development environment
14 | - Android Studio 4.0 or later
15 | - Android SDK 7.1.1 ‘Nougat’ (API level 25) or higher
16 | - Android build tools 28.0.3
17 | - Android NDK 21.4.7075529
18 | - OpenJDK 1.8n
19 |
20 | ## Build Instructions
21 | 1. Download [CloudXR SDK](https://developer.nvidia.com/nvidia-cloudxr-sdk) and [Google OBOE SDK 1.5.0](https://github.com/google/oboe/releases/tag/1.5.0).
22 | 2. Put ***CloudXR.aar*** and ***oboe-1.5.0.aar*** in ***[ProjectRoot]/app/libs***
23 | 3. Download Wave SDK, extract the zip file and copy the ***repo*** folder to ***[ProjectRoot]***, alongside with ***app*** and ***gradle*** folders (paths can be modified in ***build_sdk.gradle***)
24 | 4. You are ready to build.
25 |
26 | ## Installation & Usage
27 | 1. Install CloudXR server on your PC.
28 | 2. Build Wave CloudXR Sample Client and install the apk to your headset
29 | 3. Modify the IP address in ***CloudXRLaunchOptions.txt*** and push it into ***/sdcard*** of your headset.
30 | - Please read [CloudXR Command-Line Options](https://docs.nvidia.com/cloudxr-sdk/usr_guide/cmd_line_options.html#command-line-options) for the format of ***CloudXRLaunchOptions.txt***)
31 | 5. Launch the apk to start streaming
32 |
33 | ## Notes
34 | * The application requires WRITE_EXTERNAL_STORAGE permission to proceed, for loading a config file from sdcard and writing CloudXR logs.
35 | * If RECORD_AUDIO permission is denied, microphone feature will be disabled.
36 | >The above permission requests will be prompted in-headset on first launch. To install it with permissions granted, use the *-g* flag with *adb install*.
37 | > `adb install -g client.apk`
38 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Wave CloudXR Sample client
3 |
4 | © 2021 HTC Corporation. All Rights Reserved.
5 |
6 | This document describes a contract between you and HTC Corporation (“HTC”) for the Works which refer to this software provided by HTC under the terms of this license. Please read it carefully before downloading or using this Work. If you do not agree to the terms of this license, please do not download or use this Work.
7 |
8 | This Work contains source code of CloudXR SDK provided by NVIDIA (https://developer.nvidia.com/nvidia-cloudxr-sdk) licensed under License Agreement for NVIDIA Software Development Kits. You must read and abide by restriction and other terms set for the in the NVIDIA license agreement. To review NVIDIA license agreement, look for file titled [NVIDIA CloudXR SDK 1.0 License.pdf] in NVIDIA CloudXR SDK distribution.
9 |
10 | Unless otherwise provided herein, the information contained in the Work is the exclusive property of HTC.
11 |
12 | HTC grants the users the rights to use, distribute and modify (if provided in a source code form) the Work within the scope of the legitimate development of software. The usage of the Work, with or without modification, is permitted provided that the following conditions are met:
13 |
14 | * The Work is distributed in a source code form must retain the above copyright notice, this list of conditions and the following disclaimer.
15 | * The Work is distributed in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distributions.
16 | * Neither HTC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
17 | * If you voluntarily provide HTC with any suggestions, enhancements, modifications, feature request or other feedback to the Work, you hereby grant HTC and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. HTC will use your feedback at its discretion.
18 |
19 | THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS IN THE WORK.
20 |
21 | IN NO EVENT SHALL HTC OR ITS SUPPLIERS BE LIABLE FOR ANY SPECIAL INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF INABILITY TO USE THE WORK, EVEN IF HTC HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/htc/vr/samples/wavecloudxr/MainActivity.java:
--------------------------------------------------------------------------------
1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 |
3 | package com.htc.vr.samples.wavecloudxr;
4 |
5 | import com.htc.vr.sdk.VRActivity;
6 |
7 | import android.Manifest;
8 | import android.support.v4.app.ActivityCompat;
9 | import android.support.v4.content.ContextCompat;
10 | import android.content.pm.PackageInfo;
11 | import android.content.pm.PackageManager;
12 | import android.content.res.AssetManager;
13 | import android.os.Bundle;
14 | import android.util.Log;
15 |
16 | public class MainActivity extends VRActivity {
17 | private static final String TAG = "WaveCloudXRJAVA";
18 | private final int PERMISSION_REQUEST_CODE = 1;
19 | private boolean mPermissionGranted = false;
20 |
21 | static {
22 | System.loadLibrary("WaveCloudXRJNI");
23 | System.loadLibrary("CloudXRClient");
24 | }
25 | public MainActivity() {
26 | }
27 |
28 | // Permission prompt is triggered by WVROverlayService. In order to use the service, WVR_Init must be called first, at native layer.
29 | // So the order is: JAVA onCreate() > Native WVR_Init() > JAVA requestPermission() > JAVA onRequestPermissionsResult > Native CloudXR Init
30 | @Override
31 | protected void onCreate(Bundle icicle) {
32 | super.onCreate(icicle);
33 | nativeInit(getResources().getAssets());
34 |
35 | try {
36 | PackageManager pm = getPackageManager();
37 | PackageInfo info = pm.getPackageInfo(getApplicationInfo().packageName, 0);
38 | Log.i(TAG, "Application Version: " + info.versionName + " code: " + info.versionCode);
39 | } catch (PackageManager.NameNotFoundException e) {
40 | e.printStackTrace();
41 | }
42 |
43 | // Request storage permission for loading CloudXRLaunchOptions (required) & writing CloudXR log to sdcard
44 | // Request audio permission for microphone (optional)
45 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
46 | ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
47 | ActivityCompat.requestPermissions(this, new String[] {
48 | Manifest.permission.WRITE_EXTERNAL_STORAGE,
49 | Manifest.permission.RECORD_AUDIO
50 | }, PERMISSION_REQUEST_CODE);
51 | Log.w(TAG, "Permission request sent.");
52 | } else {
53 | mPermissionGranted = true;
54 | OnPermission();
55 | }
56 | }
57 |
58 | protected void OnPermission() {
59 | if (mPermissionGranted) {
60 | Log.e(TAG, "Permission granted.");
61 | } else {
62 | Log.e(TAG, "Failed to grant necessary permission. Aborting the program.");
63 | finish();
64 | }
65 | }
66 |
67 | @Override
68 | protected void onResume() {
69 | super.onResume();
70 | if (mPermissionGranted) nativeOnResume();
71 |
72 | }
73 |
74 | @Override
75 | protected void onPause() {
76 | super.onPause();
77 | if (mPermissionGranted) nativeOnPause();
78 | }
79 |
80 | @Override
81 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
82 | if (requestCode == PERMISSION_REQUEST_CODE && grantResults != null && grantResults.length > 0) {
83 | // Write
84 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
85 | Log.e(TAG, "Storage permission denied. Perm: "+ permissions[0] +" Result: " + grantResults[0]);
86 | mPermissionGranted = false;
87 | } else {
88 | mPermissionGranted = true;
89 | }
90 |
91 | // Audio
92 | if (grantResults[2] != PackageManager.PERMISSION_GRANTED) {
93 | Log.e(TAG, "Audio permission denied. Unable to use microphone during this streaming session. Perm: "+ permissions[1] +" Result: " + grantResults[1]);
94 | }
95 | }
96 |
97 | OnPermission();
98 | }
99 |
100 | // JNI
101 | static native void nativeInit(AssetManager am);
102 | static native void nativeOnPause();
103 | static native void nativeOnResume();
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/jni/WaveCloudXRApp.h:
--------------------------------------------------------------------------------
1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 |
3 | #pragma once
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | #include "oboe/Oboe.h"
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include
23 | #include
24 |
25 | class WaveCloudXRApp : public oboe::AudioStreamDataCallback
26 | {
27 | public:
28 | WaveCloudXRApp();
29 |
30 | bool initVR();
31 | bool initGL();
32 | bool initCloudXR();
33 | void shutdownVR();
34 | void shutdownGL();
35 | void shutdownCloudXR();
36 |
37 | bool handleInput();
38 | bool HandleCloudXRLifecycle(const bool pause);
39 | void beginPoseStream();
40 | void stopPoseStream();
41 | void updatePose();
42 | bool renderFrame();
43 |
44 | // Audio interface
45 | oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream,
46 | void *audioData, int32_t numFrames) override;
47 |
48 | // CloudXR callback
49 | void GetTrackingState(cxrVRTrackingState* trackingState);
50 | void TriggerHaptic(const cxrHapticFeedback* haptic);
51 | cxrBool RenderAudio(const cxrAudioFrame*);
52 | // void HandleClientState(cxrClientState state, cxrStateReason reason);
53 | void HandleClientState(void* context, cxrClientState state, cxrError error);
54 |
55 | /* CloudXR interfaces */
56 | /*
57 | * Send connection request to specified server IP
58 | * */
59 | bool Connect(const bool async = true);
60 | protected:
61 | void Pause();
62 | void Resume();
63 |
64 | protected:
65 | uint16_t GetTouchInputIndex(const bool touched, const WVR_InputId wvrInputId);
66 | uint16_t GetPressInputIndex(const uint8_t hand, const bool pressed, const WVR_InputId wvrInputId);
67 | uint16_t GetAnalogInputIndex(const bool pressed, const WVR_InputId wvrInputId);
68 | void updateTime();
69 | void processVREvent(const WVR_Event_t & event);
70 |
71 | void ReleaseFramebuffers();
72 | void RecreateFramebuffer(const uint32_t width, const uint32_t height);
73 |
74 | GLuint CreateGLFramebuffer(const GLuint texId);
75 |
76 | // CloudXR function
77 | protected:
78 | bool LoadConfig();
79 | bool InitAudio();
80 | bool InitCallbacks();
81 | bool InitDeviceDesc();
82 | bool InitReceiver();
83 |
84 | /*
85 | * Fetch video frame from CloudXR server
86 | */
87 | bool UpdateFrame();
88 |
89 | /*
90 | * Get device poses/inputs from WaveVR and update to CloudXR Server
91 | * */
92 | bool UpdateHMDPose(const WVR_PoseState_t hmdPose);
93 | bool UpdateDevicePose(const WVR_DeviceType type, const WVR_PoseState_t ctrlPose);
94 | bool UpdateInput(const WVR_Event_t& event);
95 | bool UpdateAnalog();
96 |
97 | /*
98 | * Render the video frame to the currently bound target surface
99 | */
100 | bool Render(const uint32_t eye, WVR_TextureParams_t eyeTexture, const bool frameValid);
101 |
102 | void CheckStreamQuality();
103 | private:
104 |
105 | // CloudXR
106 | cxrFramesLatched mFramesLatched{};
107 | cxrReceiverHandle mReceiver= nullptr;
108 | cxrDeviceDesc mDeviceDesc{};
109 | cxrClientCallbacks mClientCallbacks;
110 | cxrGraphicsContext mContext;
111 | CloudXR::ClientOptions mOptions;
112 | cxrConnectionDesc mConnectionDesc = {};
113 |
114 | bool mStateDirty = false;
115 | cxrClientState mClientState = cxrClientState_ReadyToConnect;
116 | // cxrStateReason mClientStateReason = cxrStateReason_NoError;
117 |
118 | // Audio
119 | oboe::AudioStream* mPlaybackStream= nullptr;
120 | oboe::AudioStream* mRecordStream= nullptr;
121 |
122 | // Pose
123 | std::mutex mPoseMutex;
124 | std::thread *mPoseStream = nullptr;
125 | bool mExitPoseStream = false;
126 | cxrVRTrackingState mCXRPoseState;
127 | WVR_PoseState_t mHmdPose;
128 | WVR_PoseState_t mCtrlPoses[2];
129 |
130 | // Input
131 | const uint8_t HAND_LEFT = 0;
132 | const uint8_t HAND_RIGHT = 1;
133 | const uint8_t IDX_TRIGGER = 0;
134 | const uint8_t IDX_GRIP = 1;
135 | const uint8_t IDX_THUMBSTICK = 2;
136 | cxrControllerHandle mControllers[2] = {};
137 | bool mUpdateAnalogs[2][3] = {false};// [L|R][TRIGGER|GRIP|THUMBSTICK]
138 | // CXR Input event container
139 | cxrControllerEvent mCTLEvents[2][64] = {};
140 | uint32_t mCTLEventCount[2] = {0};
141 |
142 | uint32_t mLastButtons = 0;
143 | uint32_t mLastTouches = 0;
144 | WVR_AnalogState_t mLastAnalogs[3];
145 |
146 | bool mIs6DoFHMD = false;
147 | bool mIs6DoFController[2] = {false, false};
148 |
149 | bool mConnected;
150 | bool mPaused;
151 | bool mInited;
152 |
153 | // Render
154 | void* mLeftEyeQ;
155 | void* mRightEyeQ;
156 |
157 | std::vector mLeftEyeFBO;
158 | std::vector mRightEyeFBO;
159 |
160 | uint32_t mRenderWidth;
161 | uint32_t mRenderHeight;
162 |
163 | float mFrameInvalidTime = 0.0f;
164 |
165 | // Statistics
166 | float mTimeDiff;
167 | uint32_t mTimeAccumulator2S; // add in micro second.
168 | struct timeval mRtcTime;
169 | int mFrameCount = 0;
170 | float mFPS = 0;
171 | uint32_t mClockCount = 0;
172 | uint8_t mRetryConnCount = 0;
173 | const uint8_t mMaxRetryConnCount = 5;
174 |
175 | int mFramesUntilStats = 60;
176 | };
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save ( ) {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/jni/WaveCloudXRApp.cpp:
--------------------------------------------------------------------------------
1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. ===========
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 |
21 | #include "CloudXRMatrixHelpers.h"
22 | #include "WaveCloudXRApp.h"
23 |
24 | // Return micro second. Should always positive because now is bigger.
25 | #define timeval_subtract(now, last) \
26 | ((now.tv_sec - last.tv_sec) * 1000000LL + now.tv_usec - last.tv_usec)
27 |
28 | #define VR_MAX_CLOCKS 200
29 | #define LATCHFRAME_TIMEOUT_MS 100 // timeout to fetch a frame from CloudXR server
30 | #define FRAME_TIMEOUT_SECOND 10.0f // retry connection if no valid frame or connection timeout
31 |
32 | #define VERSION_CODE "v1.7"
33 |
34 | #define CASE(x) \
35 | case x: \
36 | return #x
37 |
38 | const char* ClientStateEnumToString(cxrClientState state)
39 | {
40 | switch (state)
41 | {
42 | CASE(cxrClientState_ReadyToConnect);
43 | CASE(cxrClientState_ConnectionAttemptInProgress);
44 | CASE(cxrClientState_ConnectionAttemptFailed);
45 | CASE(cxrClientState_StreamingSessionInProgress);
46 | CASE(cxrClientState_Disconnected);
47 | CASE(cxrClientState_Exiting);
48 | default:
49 | return "";
50 | }
51 | }
52 | #undef CASE
53 | static constexpr int inputTouchLegacyCount = 21;
54 |
55 | static const char* inputsTouchLegacy[inputTouchLegacyCount] =
56 | {
57 | "/input/system/click",
58 | "/input/application_menu/click",
59 | "/input/trigger/click", // 2
60 | "/input/trigger/touch", // 3
61 | "/input/trigger/value", // 4
62 | "/input/grip/click", // 5
63 | "/input/grip/touch", // 6
64 | "/input/grip/value", // 7
65 | "/input/joystick/click", // 8
66 | "/input/joystick/touch", // 9
67 | "/input/joystick/x", // 10
68 | "/input/joystick/y", // 11
69 | "/input/a/click", // 12
70 | "/input/b/click", // 13
71 | "/input/x/click", // 14
72 | "/input/y/click", // 15
73 | "/input/a/touch", // 16
74 | "/input/b/touch", // 17
75 | "/input/x/touch", // 18
76 | "/input/y/touch", // 19
77 | "/input/thumb_rest/touch",
78 | };
79 |
80 | cxrInputValueType inputValuesTouchLegacy[inputTouchLegacyCount] =
81 | {
82 | cxrInputValueType_boolean, //input/system/click
83 | cxrInputValueType_boolean, //input/application_menu/click
84 | cxrInputValueType_boolean, //input/trigger/click
85 | cxrInputValueType_boolean, //input/trigger/touch
86 | cxrInputValueType_float32, //input/trigger/value
87 | cxrInputValueType_boolean, //input/grip/click
88 | cxrInputValueType_boolean, //input/grip/touch
89 | cxrInputValueType_float32, //input/grip/value
90 | cxrInputValueType_boolean, //input/joystick/click
91 | cxrInputValueType_boolean, //input/joystick/touch
92 | cxrInputValueType_float32, //input/joystick/x
93 | cxrInputValueType_float32, //input/joystick/y
94 | cxrInputValueType_boolean, //input/a/click
95 | cxrInputValueType_boolean, //input/b/click
96 | cxrInputValueType_boolean, //input/x/click
97 | cxrInputValueType_boolean, //input/y/click
98 | cxrInputValueType_boolean, //input/a/touch
99 | cxrInputValueType_boolean, //input/b/touch
100 | cxrInputValueType_boolean, //input/x/touch
101 | cxrInputValueType_boolean, //input/y/touch
102 | cxrInputValueType_boolean, //input/thumb_rest/touch
103 | };
104 | WaveCloudXRApp::WaveCloudXRApp()
105 | : mTimeDiff(0.0f)
106 | , mLeftEyeQ(nullptr)
107 | , mRightEyeQ(nullptr)
108 | , mReceiver(nullptr)
109 | , mPlaybackStream(nullptr)
110 | , mRecordStream(nullptr)
111 | , mClientState(cxrClientState_ReadyToConnect)
112 | , mRenderWidth(1720)
113 | , mRenderHeight(1720)
114 | , mConnected(false)
115 | , mInited(false)
116 | , mPaused(true)
117 | , mStateDirty(true)
118 | {}
119 |
120 | bool WaveCloudXRApp::initVR() {
121 | LOGI("Wave CloudXR Sample %s", VERSION_CODE);
122 |
123 | // Init WVR Runtime
124 | WVR_InitError eError = WVR_Init(WVR_AppType_VRContent);
125 | if (eError != WVR_InitError_None) {
126 | LOGE("Unable to init VR runtime: %s", WVR_GetInitErrorString(eError));
127 | return false;
128 | }
129 |
130 | /*
131 | Controller mapping
132 | */
133 | mIs6DoFHMD = false;
134 | if (WVR_NumDoF_6DoF==WVR_GetDegreeOfFreedom(WVR_DeviceType_HMD))
135 | mIs6DoFHMD = true;
136 |
137 | mIs6DoFController[0] = (WVR_GetDegreeOfFreedom(WVR_DeviceType_Controller_Left)==WVR_NumDoF_6DoF);
138 | mIs6DoFController[1] = (WVR_GetDegreeOfFreedom(WVR_DeviceType_Controller_Right)==WVR_NumDoF_6DoF);
139 | LOGV("6DOF HMD = %d, Con1 = %d, Con2 = %d.", mIs6DoFHMD?1:0, mIs6DoFController[0]?1:0, mIs6DoFController[1]?1:0);
140 |
141 | WVR_SetPosePredictEnabled(WVR_DeviceType_HMD, true, true);
142 | WVR_SetPosePredictEnabled(WVR_DeviceType_Controller_Left, true, true);
143 | WVR_SetPosePredictEnabled(WVR_DeviceType_Controller_Right, true, true);
144 |
145 | WVR_SetArmModel(WVR_SimulationType_Auto);
146 | WVR_SetArmSticky(false);
147 |
148 | WVR_InputAttribute inputIdAndTypes[] =
149 | {
150 | {WVR_InputId_Alias1_System, WVR_InputType_Button, WVR_AnalogType_None},
151 | {WVR_InputId_Alias1_Menu, WVR_InputType_Button, WVR_AnalogType_None},
152 | {WVR_InputId_Alias1_Grip, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_1D},
153 | {WVR_InputId_Alias1_DPad_Left, WVR_InputType_Button, WVR_AnalogType_None},
154 | {WVR_InputId_Alias1_DPad_Up, WVR_InputType_Button, WVR_AnalogType_None},
155 | {WVR_InputId_Alias1_DPad_Right, WVR_InputType_Button, WVR_AnalogType_None},
156 | {WVR_InputId_Alias1_DPad_Down, WVR_InputType_Button, WVR_AnalogType_None},
157 | {WVR_InputId_Alias1_Volume_Up, WVR_InputType_Button, WVR_AnalogType_None},
158 | {WVR_InputId_Alias1_Volume_Down, WVR_InputType_Button, WVR_AnalogType_None},
159 | {WVR_InputId_Alias1_Bumper, WVR_InputType_Button , WVR_AnalogType_None},
160 | {WVR_InputId_Alias1_A, WVR_InputType_Button, WVR_AnalogType_None},
161 | {WVR_InputId_Alias1_B, WVR_InputType_Button, WVR_AnalogType_None},
162 | {WVR_InputId_Alias1_Back, WVR_InputType_Button, WVR_AnalogType_None},
163 | {WVR_InputId_Alias1_Enter, WVR_InputType_Button, WVR_AnalogType_None},
164 | {WVR_InputId_Alias1_Touchpad, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_2D},
165 | {WVR_InputId_Alias1_Thumbstick, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_2D},
166 | {WVR_InputId_Alias1_Trigger, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_1D}
167 | };
168 |
169 | const size_t numAttribs = sizeof(inputIdAndTypes)/sizeof(*inputIdAndTypes);
170 |
171 | WVR_SetInputRequest(WVR_DeviceType_HMD, inputIdAndTypes, numAttribs);
172 | WVR_SetInputRequest(WVR_DeviceType_Controller_Left, inputIdAndTypes, numAttribs);
173 | WVR_SetInputRequest(WVR_DeviceType_Controller_Right, inputIdAndTypes, numAttribs);
174 |
175 | // Init Wave Render
176 | WVR_RenderInitParams_t param;
177 | param = { WVR_GraphicsApiType_OpenGL, WVR_RenderConfig_Default };
178 |
179 | WVR_RenderError pError = WVR_RenderInit(¶m);
180 | if (pError != WVR_RenderError_None) {
181 | LOGE("Present init failed - Error[%d]", pError);
182 | }
183 |
184 | LOGI("initVR done");
185 | return true;
186 | }
187 |
188 | bool WaveCloudXRApp::initGL() {
189 |
190 | gettimeofday(&mRtcTime, NULL);
191 |
192 | // Setup stereo render targets
193 | // WVR_GetRenderTargetSize(&mRenderWidth, &mRenderHeight);
194 |
195 | // Focus 3 panel resolution
196 | mRenderWidth = 2448;
197 | mRenderHeight = 2448;
198 | LOGD("Recommended size is %ux%u", mRenderWidth, mRenderHeight);
199 | if (mRenderWidth == 0 || mRenderHeight == 0) {
200 | LOGE("Please check server configure");
201 | return false;
202 | }
203 |
204 | mLeftEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, mRenderWidth, mRenderHeight, 0);
205 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) {
206 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mLeftEyeQ, i).id);
207 | mLeftEyeFBO.push_back(fbo);
208 | }
209 |
210 | mRightEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, mRenderWidth, mRenderHeight, 0);
211 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) {
212 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mRightEyeQ, i).id);
213 | mRightEyeFBO.push_back(fbo);
214 | }
215 |
216 | return true;
217 | }
218 |
219 | GLuint WaveCloudXRApp::CreateGLFramebuffer(const GLuint texId)
220 | {
221 | GLuint fbo;
222 | glGenFramebuffers(1, &fbo);
223 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
224 | glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
225 | GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0);
226 |
227 | GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
228 |
229 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
230 |
231 | if (status != GL_FRAMEBUFFER_COMPLETE)
232 | {
233 | LOGE("Incomplete frame buffer object (%d). Requested dimensions: %d x %d.", status, mRenderWidth, mRenderHeight);
234 | }
235 | return fbo;
236 | }
237 |
238 | void WaveCloudXRApp::shutdownGL() {
239 |
240 | ReleaseFramebuffers();
241 | }
242 |
243 | void WaveCloudXRApp::shutdownVR() {
244 | WVR_Quit();
245 | }
246 |
247 | void WaveCloudXRApp::shutdownCloudXR() {
248 |
249 | if (mPlaybackStream)
250 | {
251 | mPlaybackStream->close();
252 | mPlaybackStream = nullptr;
253 | }
254 |
255 | if (mRecordStream)
256 | {
257 | mRecordStream->close();
258 | mRecordStream = nullptr;
259 | }
260 |
261 | if (mReceiver) {
262 | cxrDestroyReceiver(mReceiver);
263 | mReceiver = nullptr;
264 | }
265 |
266 | mInited = false;
267 | LOGE("ShutdownCloudXR done");
268 | }
269 |
270 | bool WaveCloudXRApp::HandleCloudXRLifecycle(const bool pause)
271 | {
272 | if (mPaused != pause) {
273 | if (pause) {
274 | Pause();
275 | } else {
276 | Resume();
277 | }
278 | }
279 |
280 | if (mStateDirty) {
281 | switch (mClientState) {
282 | case cxrClientState_ReadyToConnect:
283 | LOGI("Client is ready to connect.");
284 | Connect();
285 | break;
286 | case cxrClientState_Disconnected:
287 | if (mRetryConnCount < mMaxRetryConnCount) {
288 | LOGE("Disconnected, reconnecting ... %d", mRetryConnCount);
289 | shutdownCloudXR();
290 | if (initCloudXR()) {
291 | mRetryConnCount++;
292 | Connect();
293 | } else {
294 | LOGE("Reinitialization failed, exiting app.");
295 | return false;
296 | }
297 | } else {
298 | LOGE("Unrecoverable disconnection, exiting app. ");
299 | return false;
300 | }
301 | break;
302 | case cxrClientState_Exiting:
303 | return false;
304 |
305 | default:
306 | break;
307 | }
308 | mStateDirty = false;
309 | }
310 |
311 | return true;
312 | }
313 |
314 |
315 | //-----------------------------------------------------------------------------
316 | // Purpose: Poll events. Quit application if return true.
317 | //-----------------------------------------------------------------------------
318 | bool WaveCloudXRApp::handleInput() {
319 | // Process WVR events
320 | WVR_Event_t event;
321 | while(WVR_PollEventQueue(&event)) {
322 | if(event.common.type == WVR_EventType_Quit) {
323 | shutdownCloudXR();
324 | return false;
325 | }
326 |
327 | processVREvent(event);
328 | UpdateInput(event);
329 | }
330 | UpdateAnalog();
331 |
332 | return true;
333 | }
334 |
335 | void WaveCloudXRApp::ReleaseFramebuffers()
336 | {
337 | if (mLeftEyeQ != 0) {
338 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) {
339 | glDeleteFramebuffers(1, &mLeftEyeFBO.at(i));
340 | }
341 | WVR_ReleaseTextureQueue(mLeftEyeQ);
342 | }
343 |
344 | if (mRightEyeQ != 0) {
345 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) {
346 | glDeleteFramebuffers(1, &mRightEyeFBO.at(i));
347 | }
348 | WVR_ReleaseTextureQueue(mRightEyeQ);
349 | }
350 | }
351 |
352 | void WaveCloudXRApp::RecreateFramebuffer(const uint32_t width, const uint32_t height) {
353 |
354 | if (mRenderWidth == width && mRenderHeight == height)
355 | return;
356 |
357 | ReleaseFramebuffers();
358 |
359 | mLeftEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, width, height, 0);
360 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) {
361 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mLeftEyeQ, i).id);
362 | mLeftEyeFBO.push_back(fbo);
363 | }
364 |
365 | mRightEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, width, height, 0);
366 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) {
367 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mRightEyeQ, i).id);
368 | mRightEyeFBO.push_back(fbo);
369 | }
370 |
371 | mRenderWidth = width;
372 | mRenderHeight = height;
373 | LOGD("Recreated buffer %dx%d", mRenderWidth, mRenderHeight);
374 | }
375 |
376 | //-----------------------------------------------------------------------------
377 | // Purpose: Processes a single VR event
378 | //-----------------------------------------------------------------------------
379 | void WaveCloudXRApp::processVREvent(const WVR_Event_t & event) {
380 | switch(event.common.type) {
381 | case WVR_EventType_IpdChanged:
382 | {
383 | WVR_RenderProps_t props;
384 | bool ret = WVR_GetRenderProps(&props);
385 | float ipd = 0;
386 | if (ret) {
387 | mDeviceDesc.ipd = ipd; // used when re-init
388 | mCXRPoseState.hmd.ipd = ipd; // updated along with pose
389 | }
390 | LOGI("Receive WVR_EventType_IpdChanged = %d", ipd);
391 | }
392 | break;
393 | case WVR_EventType_RenderingToBePaused:
394 | case WVR_EventType_DeviceSuspend:
395 | {
396 | LOGE("Device %d suspended.", event.device.deviceType);
397 | mPaused = true;
398 | break;
399 | }
400 |
401 | case WVR_EventType_RenderingToBeResumed:
402 | case WVR_EventType_DeviceResume:
403 | {
404 | LOGE("WVR Device %d resumed.", event.device.deviceType);
405 | mPaused = false;
406 | break;
407 | }
408 |
409 | default:
410 | break;
411 | }
412 | }
413 | bool WaveCloudXRApp::renderFrame() {
414 | updateTime();
415 |
416 | bool frameValid = UpdateFrame();
417 | if (!frameValid) {
418 | // Exit program when no valid frame for too long
419 | mFrameInvalidTime += mTimeDiff;
420 | if (mFrameInvalidTime > FRAME_TIMEOUT_SECOND) {
421 | LOGD("No valid frame for %f seconds", mFrameInvalidTime);
422 | return false;
423 | }
424 | } else {
425 | mFrameInvalidTime = 0.0f;
426 |
427 | CheckStreamQuality();
428 | }
429 |
430 | /*
431 | * Render & Submit
432 | * Left Eye
433 | * */
434 | int32_t leftIdx = WVR_GetAvailableTextureIndex(mLeftEyeQ);
435 | GLuint fbo = mLeftEyeFBO.at(leftIdx);
436 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
437 |
438 | WVR_TextureParams_t leftEyeTexture = WVR_GetTexture(mLeftEyeQ, leftIdx);
439 |
440 | glViewport(0, 0, mRenderWidth, mRenderHeight);
441 | glScissor(0, 0, mRenderWidth, mRenderHeight);
442 | WVR_PreRenderEye(WVR_Eye_Left, &leftEyeTexture);
443 | WVR_RenderMask(WVR_Eye_Left);
444 |
445 | glClear(GL_COLOR_BUFFER_BIT);
446 | Render(WVR_Eye_Left, leftEyeTexture, frameValid);
447 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
448 |
449 | /*
450 | * Render & Submit
451 | * Right Eye
452 | * */
453 | int32_t rightIdx = WVR_GetAvailableTextureIndex(mRightEyeQ);
454 | fbo = mRightEyeFBO.at(rightIdx);
455 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
456 | WVR_TextureParams_t rightEyeTexture = WVR_GetTexture(mRightEyeQ, rightIdx);
457 |
458 | glViewport(0, 0, mRenderWidth, mRenderHeight);
459 | glScissor(0, 0, mRenderWidth, mRenderHeight);
460 | WVR_PreRenderEye(WVR_Eye_Right, &rightEyeTexture);
461 | WVR_RenderMask(WVR_Eye_Right);
462 |
463 | glClear(GL_COLOR_BUFFER_BIT);
464 | Render(WVR_Eye_Right, rightEyeTexture, frameValid);
465 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
466 |
467 | // Clear
468 | {
469 | // We want to make sure the glFinish waits for the entire present to complete, not just the submission
470 | // of the command. So, we do a clear here right here so the glFinish will wait fully for the swap.
471 | glClearColor(0, 0, 0, 1);
472 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
473 | }
474 |
475 | usleep(1); // ?
476 | return true;
477 | }
478 |
479 | static uint updatePoseCount = 0;
480 | static uint getPoseCount = 0;
481 | void WaveCloudXRApp::updateTime() {
482 | // Process time variable.
483 | struct timeval now;
484 | gettimeofday(&now, NULL);
485 |
486 | mClockCount++;
487 | if (mRtcTime.tv_usec > now.tv_usec)
488 | mClockCount = 0;
489 | if (mClockCount >= VR_MAX_CLOCKS)
490 | mClockCount--;
491 |
492 | uint32_t timeDiff = timeval_subtract(now, mRtcTime);
493 | mTimeDiff = timeDiff / 1000000.0f;
494 | mTimeAccumulator2S += timeDiff;
495 | mRtcTime = now;
496 | mFrameCount++;
497 | if (mTimeAccumulator2S > 1000000) {
498 | mFPS = mFrameCount / (mTimeAccumulator2S / 1000000.0f);
499 | LOGI("FPS %2.0f, UpdatePose: %d, GetPose: %d", mFPS, updatePoseCount, getPoseCount);
500 |
501 | updatePoseCount = 0;
502 | getPoseCount = 0;
503 | mFrameCount = 0;
504 | mTimeAccumulator2S = 0;
505 | }
506 | }
507 |
508 | void WaveCloudXRApp::beginPoseStream() {
509 | if (mPoseStream == nullptr) {
510 | mPoseStream = new std::thread(&WaveCloudXRApp::updatePose, this);
511 | }
512 | }
513 |
514 | void WaveCloudXRApp::stopPoseStream() {
515 | if(mPoseStream!=nullptr) {
516 | mExitPoseStream = true;
517 | if(mPoseStream->joinable()) {
518 | mPoseStream->join();
519 | delete mPoseStream;
520 | mPoseStream = nullptr;
521 | }
522 | }
523 | }
524 | // 1 sec = 1,000ms = 1,000,000,000ns
525 | void WaveCloudXRApp::updatePose() {
526 | // Update pose 250 per second by default
527 | int deno = (mDeviceDesc.posePollFreq == 0) ? 250 : mDeviceDesc.posePollFreq;
528 | long long int sleepNs = 1000000000 / deno;
529 | LOGI("PoseStream Update per %lldns", sleepNs);
530 |
531 | while (!mExitPoseStream) {
532 |
533 | while (mInited && mConnected)
534 | {
535 | std::lock_guard lock(mPoseMutex);
536 | {
537 | WVR_PoseOriginModel pom = WVR_PoseOriginModel_OriginOnGround;
538 | // Returns immediately with latest pose
539 | WVR_GetPoseState(WVR_DeviceType_HMD, pom, 0, &mHmdPose);
540 | UpdateHMDPose(mHmdPose);
541 |
542 | pom = mIs6DoFHMD ? WVR_PoseOriginModel_OriginOnGround
543 | : WVR_PoseOriginModel_OriginOnHead_3DoF;
544 |
545 | WVR_GetPoseState(WVR_DeviceType_Controller_Left, pom, 0, &mCtrlPoses[0]);
546 | UpdateDevicePose(WVR_DeviceType_Controller_Left, mCtrlPoses[0]);
547 |
548 | WVR_GetPoseState(WVR_DeviceType_Controller_Right, pom, 0, &mCtrlPoses[1]);
549 | UpdateDevicePose(WVR_DeviceType_Controller_Right, mCtrlPoses[1]);
550 | updatePoseCount++;
551 | }
552 | std::this_thread::sleep_for(std::chrono::nanoseconds(sleepNs));
553 | }
554 | }
555 |
556 | LOGI("PoseStream end");
557 | }
558 |
559 | /* CloudXR */
560 |
561 | WVR_Matrix4f_t Convert(const cxrMatrix34& mtx)
562 | {
563 | WVR_Matrix4f_t out{};
564 | memcpy(&out, &mtx, sizeof(mtx));
565 | out.m[3][3] = 1.0f;
566 |
567 | return out;
568 | }
569 |
570 | cxrMatrix34 Convert(const WVR_Matrix4f_t& mtx)
571 | {
572 | cxrMatrix34 out{};
573 | memcpy(&out, &mtx, sizeof(out));
574 |
575 | return out;
576 | }
577 |
578 | cxrVector3 Convert(const WVR_Vector3f& vec)
579 | {
580 | return cxrVector3{vec.v[0], vec.v[1], vec.v[2]};
581 | }
582 |
583 | WVR_Vector3f Convert(const cxrVector3& vec)
584 | {
585 | return WVR_Vector3f{vec.v[0], vec.v[1], vec.v[2]};
586 | }
587 |
588 | // Make sure storage permission is granted before LoadConfig() and InitReceiver()
589 | // Make sure WaveVR is initialized before InitDeviceDesc()
590 | bool WaveCloudXRApp::initCloudXR() {
591 |
592 | if (mInited) {
593 | return true;
594 | }
595 |
596 | if (!LoadConfig()) return false;
597 | if (!InitCallbacks()) return false;
598 | if (!InitDeviceDesc()) return false;
599 | if (!InitReceiver()) return false;
600 | if (!InitAudio()) return false;
601 |
602 | mInited = true;
603 | LOGW("CloudXR initialization success");
604 | return mInited;
605 | }
606 |
607 | bool WaveCloudXRApp::LoadConfig() {
608 |
609 | ParseStatus result = mOptions.ParseFile("/sdcard/CloudXRLaunchOptions.txt");
610 |
611 | bool ret = false;
612 | switch(result)
613 | {
614 | case ParseStatus_Success:
615 | LOGI("Loaded server IP from config: %s", mOptions.mServerIP.c_str());
616 | ret = true;
617 | break;
618 | case ParseStatus_FileNotFound:
619 | LOGE("Config file not found.");
620 | case ParseStatus_Fail:
621 | case ParseStatus_ExitRequested:
622 | case ParseStatus_BadVal:
623 | LOGE("Config file loading failed.");
624 | ret = false;
625 | break;
626 | default:
627 | break;
628 | }
629 | return ret;
630 | }
631 |
632 | bool WaveCloudXRApp::InitCallbacks() {
633 |
634 | mClientCallbacks.GetTrackingState = [](void* context, cxrVRTrackingState* trackingState)
635 | {
636 | return reinterpret_cast(context)->GetTrackingState(trackingState);
637 | };
638 | mClientCallbacks.TriggerHaptic = [](void* context, const cxrHapticFeedback* haptic)
639 | {
640 | return reinterpret_cast(context)->TriggerHaptic(haptic);
641 | };
642 | mClientCallbacks.RenderAudio = [](void* context, const cxrAudioFrame *audioFrame)
643 | {
644 | return reinterpret_cast(context)->RenderAudio(audioFrame);
645 | };
646 | //mClientCallbacks.ReceiveUserData = [](void* context, const void* data, uint32_t size)
647 | //{
648 | // return reinterpret_cast(context)->ReceiveUserData(data, size);
649 | //};
650 |
651 | mClientCallbacks.UpdateClientState = [](void* context, cxrClientState state, cxrError error)
652 | {
653 | return reinterpret_cast(context)->HandleClientState(context, state, error);
654 | };
655 |
656 | mClientCallbacks.LogMessage = [](void* context, cxrLogLevel level, cxrMessageCategory category, void* extra, const char* tag, const char* const messageText)
657 | {
658 | switch(level) {
659 | case cxrLogLevel::cxrLL_Verbose:
660 | //LOGV("[%s] %s", tag, messageText); // this opens up verbose decoder logging
661 | break;
662 | case cxrLogLevel::cxrLL_Info:
663 | LOGI("[%s] %s", tag, messageText);
664 | break;
665 | case cxrLogLevel::cxrLL_Debug:
666 | LOGD("[%s] %s", tag, messageText);
667 | break;
668 | case cxrLogLevel::cxrLL_Warning:
669 | LOGW("[%s] %s", tag, messageText);
670 | break;
671 | case cxrLogLevel::cxrLL_Critical:
672 | case cxrLogLevel::cxrLL_Error:
673 | LOGE("[%s] %s", tag, messageText);
674 | break;
675 | default:
676 | //LOGI("[%s] %s", tag, messageText);
677 | break;
678 | }
679 | };
680 |
681 | mClientCallbacks.clientContext = this;
682 |
683 | return true;
684 | }
685 |
686 | bool WaveCloudXRApp::InitAudio() {
687 |
688 | if (mDeviceDesc.receiveAudio)
689 | {
690 | // Initialize audio playback
691 | oboe::AudioStreamBuilder playbackStreamBuilder;
692 | playbackStreamBuilder.setDirection(oboe::Direction::Output);
693 | playbackStreamBuilder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
694 | playbackStreamBuilder.setSharingMode(oboe::SharingMode::Exclusive);
695 | playbackStreamBuilder.setFormat(oboe::AudioFormat::I16);
696 | playbackStreamBuilder.setChannelCount(oboe::ChannelCount::Stereo);
697 | playbackStreamBuilder.setSampleRate(CXR_AUDIO_SAMPLING_RATE);
698 |
699 | // TODO: proceed without audio?
700 | oboe::Result r = playbackStreamBuilder.openStream(&mPlaybackStream);
701 | if (r != oboe::Result::OK) {
702 | LOGE("Failed to open playback stream. Error: %s", oboe::convertToText(r));
703 | return false;
704 | }
705 |
706 | int bufferSizeFrames = mPlaybackStream->getFramesPerBurst() * 2;
707 | r = mPlaybackStream->setBufferSizeInFrames(bufferSizeFrames);
708 | if (r != oboe::Result::OK) {
709 | LOGE("Failed to set playback stream buffer size to: %d. Error: %s",
710 | bufferSizeFrames, oboe::convertToText(r));
711 | return false;
712 | }
713 |
714 | r = mPlaybackStream->start();
715 | if (r != oboe::Result::OK) {
716 | LOGE("Failed to start playback stream. Error: %s", oboe::convertToText(r));
717 | return false;
718 | }
719 | }
720 |
721 | if (mDeviceDesc.sendAudio)
722 | {
723 | // Initialize audio recording
724 | oboe::AudioStreamBuilder recordingStreamBuilder;
725 | recordingStreamBuilder.setDirection(oboe::Direction::Input);
726 | recordingStreamBuilder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
727 | recordingStreamBuilder.setSharingMode(oboe::SharingMode::Exclusive);
728 | recordingStreamBuilder.setFormat(oboe::AudioFormat::I16);
729 | recordingStreamBuilder.setChannelCount(oboe::ChannelCount::Stereo);
730 | recordingStreamBuilder.setSampleRate(CXR_AUDIO_SAMPLING_RATE);
731 | recordingStreamBuilder.setInputPreset(oboe::InputPreset::VoiceCommunication);
732 | recordingStreamBuilder.setDataCallback(this);
733 |
734 | oboe::Result r = recordingStreamBuilder.openStream(&mRecordStream);
735 | if (r != oboe::Result::OK) {
736 | LOGE("Failed to open recording stream. Error: %s", oboe::convertToText(r));
737 | LOGE("Continuing to run, without recording ability.");
738 | mDeviceDesc.sendAudio = false;
739 | } else {
740 | r = mRecordStream->start();
741 | if (r != oboe::Result::OK)
742 | {
743 | LOGE("Failed to start recording stream. Error: %s", oboe::convertToText(r));
744 | LOGE("Continuing to run, without recording ability.");
745 | mRecordStream->close();
746 | mDeviceDesc.sendAudio = false;
747 | }
748 | }
749 | }
750 |
751 | return true;
752 | }
753 |
754 | bool WaveCloudXRApp::InitDeviceDesc() {
755 | WVR_RenderProps_t props;
756 | WVR_GetRenderProps(&props);
757 |
758 | mDeviceDesc.numVideoStreamDescs = CXR_NUM_VIDEO_STREAMS_XR;
759 | for (uint32_t i = 0; i < mDeviceDesc.numVideoStreamDescs; i++) {
760 | mDeviceDesc.videoStreamDescs[i].format = cxrClientSurfaceFormat_RGB;
761 | mDeviceDesc.videoStreamDescs[i].width = mRenderWidth;
762 | mDeviceDesc.videoStreamDescs[i].height = mRenderHeight;
763 | mDeviceDesc.videoStreamDescs[i].fps = props.refreshRate;
764 | mDeviceDesc.videoStreamDescs[i].maxBitrate = mOptions.mMaxVideoBitrate;
765 | }
766 |
767 | mDeviceDesc.stereoDisplay = true;
768 | mDeviceDesc.maxResFactor = mOptions.mMaxResFactor;
769 |
770 | mDeviceDesc.ipd = props.ipdMeter;
771 | mDeviceDesc.receiveAudio = mOptions.mReceiveAudio;
772 | mDeviceDesc.sendAudio = mOptions.mSendAudio;
773 | mDeviceDesc.posePollFreq = 0;
774 |
775 | mDeviceDesc.disablePosePrediction = false;
776 | mDeviceDesc.angularVelocityInDeviceSpace = true;
777 | mDeviceDesc.foveatedScaleFactor = (mOptions.mFoveation < 100) ? mOptions.mFoveation : 0;
778 | mDeviceDesc.disableVVSync = false;
779 |
780 | // Frustum
781 | float l,r,t,b;
782 | for (int i=0; i<2; ++i) {
783 | WVR_GetClippingPlaneBoundary((WVR_Eye)i, &l, &r, &t, &b);
784 | if(l < 0) l *= -1;
785 | if(r < 0) r *= -1;
786 | if(t < 0) t *= -1;
787 | if(b < 0) b *= -1;
788 | mDeviceDesc.proj[i][0] = -l;
789 | mDeviceDesc.proj[i][1] = r;
790 | mDeviceDesc.proj[i][2] = -b;
791 | mDeviceDesc.proj[i][3] = t;
792 | }
793 |
794 | mDeviceDesc.predOffset = 0.01f;
795 |
796 | // Set up server chaperone play area
797 | mDeviceDesc.chaperone.universe = cxrUniverseOrigin_Standing;
798 | mDeviceDesc.chaperone.origin.m[0][0] = mDeviceDesc.chaperone.origin.m[1][1] = mDeviceDesc.chaperone.origin.m[2][2] = 1;
799 | mDeviceDesc.chaperone.origin.m[0][1] = mDeviceDesc.chaperone.origin.m[0][2] = mDeviceDesc.chaperone.origin.m[0][3] = 0;
800 | mDeviceDesc.chaperone.origin.m[1][0] = mDeviceDesc.chaperone.origin.m[1][2] = mDeviceDesc.chaperone.origin.m[1][3] = 0;
801 | mDeviceDesc.chaperone.origin.m[2][0] = mDeviceDesc.chaperone.origin.m[2][1] = mDeviceDesc.chaperone.origin.m[2][3] = 0;
802 |
803 | WVR_Arena_t arena = WVR_GetArena();
804 | if (arena.shape == WVR_ArenaShape_Round)
805 | {
806 | mDeviceDesc.chaperone.playArea.v[0] = arena.area.round.diameter;
807 | mDeviceDesc.chaperone.playArea.v[1] = arena.area.round.diameter;
808 | } else if (arena.shape == WVR_ArenaShape_Rectangle ) {
809 | mDeviceDesc.chaperone.playArea.v[0] = arena.area.rectangle.width;
810 | mDeviceDesc.chaperone.playArea.v[1] = arena.area.rectangle.length;
811 | } else {
812 | mDeviceDesc.chaperone.playArea.v[0] = 1.0f;
813 | mDeviceDesc.chaperone.playArea.v[1] = 1.0f;
814 | }
815 |
816 | LOGI("Device property stream#0: IPD: %f, FPS: %f, display %dx%d, play area %.2fx%.2f\n",
817 | mDeviceDesc.ipd, mDeviceDesc.videoStreamDescs[0].fps,
818 | mDeviceDesc.videoStreamDescs[0].width, mDeviceDesc.videoStreamDescs[0].height,
819 | mDeviceDesc.chaperone.playArea.v[0], mDeviceDesc.chaperone.playArea.v[1]);
820 |
821 | LOGI("Device property stream#1: IPD: %f, FPS: %f, display %dx%d, play area %.2fx%.2f\n",
822 | mDeviceDesc.ipd, mDeviceDesc.videoStreamDescs[1].fps,
823 | mDeviceDesc.videoStreamDescs[1].width, mDeviceDesc.videoStreamDescs[1].height,
824 | mDeviceDesc.chaperone.playArea.v[0], mDeviceDesc.chaperone.playArea.v[1]);
825 | return true;
826 | }
827 |
828 | bool WaveCloudXRApp::InitReceiver() {
829 |
830 | if (mReceiver) {
831 | LOGV("Receiver already created");
832 | return true;
833 | }
834 |
835 | mContext.type = cxrGraphicsContext_GLES;
836 | mContext.egl.display = eglGetCurrentDisplay();
837 | mContext.egl.context = eglGetCurrentContext();
838 | if (mContext.egl.context == nullptr) {
839 | LOGV("eglContext invalid");
840 | return false;
841 | }
842 |
843 | cxrReceiverDesc desc = { 0 };
844 | desc.requestedVersion = CLOUDXR_VERSION_DWORD;
845 | desc.deviceDesc = mDeviceDesc;
846 | desc.clientCallbacks = mClientCallbacks;
847 | desc.shareContext = &mContext;
848 | desc.debugFlags = mOptions.mDebugFlags | cxrDebugFlags_OutputLinearRGBColor | cxrDebugFlags_EnableAImageReaderDecoder;
849 | desc.logMaxSizeKB = CLOUDXR_LOG_MAX_DEFAULT;
850 | desc.logMaxAgeDays = CLOUDXR_LOG_MAX_DEFAULT;
851 | strncpy(desc.appOutputPath, "sdcard/CloudXR/logs/", CXR_MAX_PATH - 1); // log file path
852 | desc.appOutputPath[CXR_MAX_PATH-1] = 0; // ensure null terminated if string was too long.
853 | cxrError err = cxrCreateReceiver(&desc, &mReceiver);
854 | if (err != cxrError_Success)
855 | {
856 | LOGE("Failed to create CloudXR receiver. Error %d, %s.", err, cxrErrorString(err));
857 | return false;
858 | }
859 |
860 | LOGV("Receiver created!");
861 | return true;
862 | }
863 |
864 | bool WaveCloudXRApp::Connect(const bool async) {
865 |
866 | if (mConnected) {
867 | LOGE("Already connected");
868 | return true;
869 | }
870 |
871 | if (!mInited) {
872 | LOGE("CloudXR is not initialized.");
873 | return false;
874 | }
875 |
876 | if (mOptions.mServerIP.empty()) {
877 | LOGE("Server IP is not specified.");
878 | return false;
879 | }
880 |
881 | mConnectionDesc.async = async;
882 | mConnectionDesc.useL4S = mOptions.mUseL4S; // Low Latency, Low Loss, and Scalable Throughput
883 | mConnectionDesc.clientNetwork = mOptions.mClientNetwork;
884 | mConnectionDesc.topology = mOptions.mTopology;
885 | cxrError err = cxrConnect(mReceiver, mOptions.mServerIP.c_str(), &mConnectionDesc);
886 | std::string constr = async ? "Connection request sent" : "Connection" ;
887 | if (err != cxrError_Success) {
888 | LOGE("%s failed, %s. Error %d, %s.",
889 | constr.c_str(),
890 | mOptions.mServerIP.c_str(), (int) err, cxrErrorString(err));
891 | shutdownCloudXR();
892 | return false;
893 | }
894 |
895 | LOGV("%s success. %s", constr.c_str(), mOptions.mServerIP.c_str());
896 | return true;
897 | }
898 |
899 | bool WaveCloudXRApp::UpdateFrame() {
900 |
901 | if (mPaused || !mInited) {
902 | return false;
903 | }
904 |
905 | // Fetch a video frame if available.
906 | bool frameValid = false;
907 | if (mReceiver)
908 | {
909 | if (mConnected)
910 | {
911 | cxrError frameErr = cxrLatchFrame(mReceiver, &mFramesLatched,
912 | cxrFrameMask_All, LATCHFRAME_TIMEOUT_MS);
913 | frameValid = (frameErr == cxrError_Success);
914 | if (!frameValid)
915 | {
916 | if (frameErr == cxrError_Frame_Not_Ready)
917 | LOGW("LatchFrame failed, frame not ready for %d ms", LATCHFRAME_TIMEOUT_MS);
918 | else if (frameErr == cxrError_Not_Connected)
919 | LOGW("LatchFrame failed, receiver no longer connected.");
920 | else
921 | LOGE("Error in LatchFrame [%0d] = %s", frameErr, cxrErrorString(frameErr));
922 | } else {
923 |
924 | // CloudXR SDK 3.1.1:
925 | // If network condition is bad, e.g. bitrate usage down below ~5Mbps
926 | // Frames received will vary frequently in size (~5 times in 1 second)
927 | // and crash here due to no read/write protection
928 |
929 | // You can add mutex to protect buffer recreation process and potentially increase latency
930 | // or comment this function call to avoid crash caused by frequent size change
931 | // Not recreating buffer to match incoming frame size might result in blurry display.
932 |
933 | // CloudXR SDK 3.2:
934 | // Recreating frame buffer causes reconnection process failed after HMD idle/wakeup.
935 | // Reason: cxrStateReason_DeviceDescriptorMismatch.
936 |
937 | if ( mFramesLatched.frames[0].widthFinal != mRenderWidth ||
938 | mFramesLatched.frames[0].heightFinal != mRenderHeight ) {
939 | /*LOGE("Receive frame %dx%d, buffer size %dx%d",
940 | mFramesLatched.frames[0].widthFinal, mFramesLatched.frames[0].heightFinal,
941 | mRenderWidth, mRenderHeight);*/
942 | //RecreateFramebuffer(mFramesLatched.frames[0].widthFinal, mFramesLatched.frames[0].heightFinal);
943 | }
944 | }
945 | }
946 | }
947 | return frameValid;
948 | }
949 |
950 | bool WaveCloudXRApp::UpdateHMDPose(const WVR_PoseState_t hmdPose) {
951 |
952 | if (mPaused || !mInited) {
953 | return false;
954 | }
955 |
956 | {
957 | if (mDeviceDesc.ipd != 0.0f)
958 | {
959 | // is this flag supposed to be cleared after CXR gets it?
960 | mCXRPoseState.hmd.flags = cxrHmdTrackingFlags_HasIPD;
961 | mCXRPoseState.hmd.ipd = mDeviceDesc.ipd;
962 | }
963 |
964 | mCXRPoseState.hmd.pose.poseIsValid = hmdPose.isValidPose; //cxrTrue;
965 | mCXRPoseState.hmd.pose.deviceIsConnected = cxrTrue;
966 | mCXRPoseState.hmd.pose.trackingResult = cxrTrackingResult_Running_OK;
967 |
968 | cxrMatrix34 mat = Convert(mHmdPose.poseMatrix);
969 | cxrMatrixToVecQuat(&mat, &mCXRPoseState.hmd.pose.position, &mCXRPoseState.hmd.pose.rotation);
970 | mCXRPoseState.hmd.pose.velocity = Convert(hmdPose.velocity);
971 | mCXRPoseState.hmd.pose.angularVelocity = Convert(hmdPose.angularVelocity);
972 |
973 | mHmdPose = hmdPose;
974 | }
975 | return true;
976 | }
977 |
978 | bool WaveCloudXRApp::UpdateDevicePose(const WVR_DeviceType type, const WVR_PoseState_t ctrlPose) {
979 |
980 | if (mPaused || !mInited) {
981 | return false;
982 | }
983 |
984 | {
985 | size_t idx = type == WVR_DeviceType_Controller_Left ? 0 : 1;
986 | if (!WVR_IsDeviceConnected(type))
987 | {
988 | mCXRPoseState.controller[idx].pose.poseIsValid = cxrFalse;
989 | mCXRPoseState.controller[idx].pose.deviceIsConnected = cxrFalse;
990 | } else {
991 | mCXRPoseState.controller[idx].pose.poseIsValid = ctrlPose.isValidPose; //cxrTrue;
992 | mCXRPoseState.controller[idx].pose.deviceIsConnected = cxrTrue;
993 | mCXRPoseState.controller[idx].pose.trackingResult = cxrTrackingResult_Running_OK;
994 |
995 | cxrMatrix34 mat = Convert(ctrlPose.poseMatrix);
996 | cxrMatrixToVecQuat(&mat, &mCXRPoseState.controller[idx].pose.position, &mCXRPoseState.controller[idx].pose.rotation);
997 | mCXRPoseState.controller[idx].pose.velocity = Convert(ctrlPose.velocity);
998 | mCXRPoseState.controller[idx].pose.angularVelocity = Convert(ctrlPose.angularVelocity);
999 | }
1000 | }
1001 | return true;
1002 | }
1003 |
1004 | bool WaveCloudXRApp::UpdateAnalog()
1005 | {
1006 | if (mPaused || !mInited) {
1007 | return false;
1008 | }
1009 |
1010 | if (!mConnected) {
1011 | return false;
1012 | }
1013 |
1014 | for(size_t hand = HAND_LEFT; hand <= HAND_RIGHT; ++hand) {
1015 |
1016 | WVR_DeviceType ctl = (hand == HAND_LEFT) ?
1017 | WVR_DeviceType_Controller_Left : WVR_DeviceType_Controller_Right;
1018 |
1019 | if(!WVR_IsDeviceConnected(ctl)) {
1020 | continue;
1021 | }
1022 |
1023 | uint32_t inputType = WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog;
1024 | uint32_t buttons = 0;
1025 | uint32_t touches = 0;
1026 | WVR_AnalogState_t analogState[3];
1027 | uint32_t analogCount = (uint32_t)WVR_GetInputTypeCount(ctl, WVR_InputType_Analog);
1028 | if (!WVR_GetInputDeviceState(ctl, inputType, &buttons, &touches, analogState, analogCount)) {
1029 | continue;
1030 | }
1031 | int stateCount = sizeof(analogState) / sizeof(WVR_AnalogState_t);
1032 |
1033 | for(size_t buttonType = IDX_TRIGGER; buttonType <= IDX_THUMBSTICK; ++buttonType) {
1034 | if (mUpdateAnalogs[hand][buttonType]) {
1035 | for(int i = 0; i < stateCount; i++) {
1036 | if(analogState[i].id == WVR_InputId_Alias1_Trigger ||
1037 | analogState[i].id == WVR_InputId_Alias1_Grip) {
1038 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]];
1039 | e.inputValue.valueType = cxrInputValueType_float32;
1040 | // e.clientTimeNS = event.device.common.timestamp;
1041 | e.clientInputIndex = GetAnalogInputIndex(true, analogState[i].id);
1042 | e.inputValue.vF32 = analogState[i].axis.x;
1043 | mCTLEventCount[hand]++;
1044 |
1045 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, Analog %f, Timestamp %lu",
1046 | hand == HAND_LEFT ? "LEFT" : "RIGHT", analogState[i].id,
1047 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vF32, e.clientTimeNS);*/
1048 | } else if (analogState[i].id == WVR_InputId_Alias1_Thumbstick) {
1049 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]];
1050 | e.inputValue.valueType = cxrInputValueType_float32;
1051 | // e.clientTimeNS = event.device.common.timestamp;
1052 | e.clientInputIndex = 10; // "/input/joystick/x"
1053 | e.inputValue.vF32 = analogState[i].axis.x;
1054 | mCTLEventCount[hand]++;
1055 |
1056 | cxrControllerEvent &e2 = mCTLEvents[hand][mCTLEventCount[hand]];
1057 | e2.inputValue.valueType = cxrInputValueType_float32;
1058 | // e.clientTimeNS = event.device.common.timestamp;
1059 | e2.clientInputIndex = 11; // "/input/joystick/y"
1060 | e2.inputValue.vF32 = analogState[i].axis.y;
1061 | mCTLEventCount[hand]++;
1062 |
1063 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, Analog (%f, %f), Timestamp %lu",
1064 | hand == HAND_LEFT ? "LEFT" : "RIGHT", analogState[i].id,
1065 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vF32, e2.inputValue.vF32 , e.clientTimeNS);*/
1066 | }
1067 | }
1068 |
1069 | }
1070 | }
1071 |
1072 | if (mCTLEventCount[hand] > 0) {
1073 | cxrError err = cxrFireControllerEvents(mReceiver, mControllers[hand], mCTLEvents[hand], mCTLEventCount[hand]);
1074 | if (err != cxrError_Success)
1075 | {
1076 | LOGE("[UpdateInput] cxrFireControllerEvents failed: %s", cxrErrorString(err));
1077 |
1078 | }
1079 | }
1080 | mCTLEventCount[hand] = 0;
1081 | }
1082 |
1083 |
1084 | return true;
1085 | }
1086 |
1087 | bool WaveCloudXRApp::Render(const uint32_t eye, WVR_TextureParams_t eyeTexture, const bool frameValid) {
1088 |
1089 | if (frameValid) {
1090 | // Submit frame with pose that render this frame
1091 | auto& framePose = mFramesLatched.poseMatrix;
1092 | WVR_Matrix4f_t headMatrix = Convert(framePose);
1093 | WVR_ConvertMatrixQuaternion(&headMatrix, &mHmdPose.rawPose.rotation, true);
1094 |
1095 | cxrBool result = cxrBlitFrame(mReceiver, &mFramesLatched, 1 << eye);
1096 |
1097 | WVR_SubmitExtend ext = WVR_SubmitExtend_Default;
1098 | WVR_SubmitFrame((WVR_Eye)eye, &eyeTexture, &mHmdPose, ext);
1099 |
1100 | if (eye == (uint32_t)WVR_Eye_Right && mReceiver && mConnected) {
1101 | cxrReleaseFrame(mReceiver, &mFramesLatched);
1102 | }
1103 |
1104 | }
1105 | // Render grey color gradient when frame invalid for whatever reason
1106 | else {
1107 | static const float ping = 0.0f;
1108 | static const float pong = 0.4f;
1109 | static const float step = 0.0025f;
1110 | static float direction = 1.0f;
1111 | static float color = ping;
1112 |
1113 | color += direction * step;
1114 | if (color > pong || color < ping) direction *= -1.0f;
1115 |
1116 | // LOGD("loading %f", color);
1117 | glClearColor(color, color, color, 1.0f);
1118 | glClear(GL_COLOR_BUFFER_BIT);
1119 |
1120 | WVR_SubmitExtend ext = WVR_SubmitExtend_Default;
1121 | WVR_SubmitFrame((WVR_Eye)eye, &eyeTexture, &mHmdPose, ext);
1122 | }
1123 |
1124 | return true;
1125 | }
1126 |
1127 | // Fire 1 input event once at a time
1128 | // checkme: update all button states at fixed freq and fire all button event at once
1129 | bool WaveCloudXRApp::UpdateInput(const WVR_Event_t& event)
1130 | {
1131 | if (mPaused || !mInited) {
1132 | return false;
1133 | }
1134 |
1135 | if (!mConnected) {
1136 | return false;
1137 | }
1138 |
1139 | // filter out non-controller mCTLEvents
1140 | /*if (event.common.type < WVR_EventType_ButtonPressed || event.common.type > WVR_EventType_UpToDownSwipe) {
1141 | return false;
1142 | }*/
1143 |
1144 | WVR_DeviceType ctl = event.device.deviceType;
1145 | if (ctl != WVR_DeviceType_Controller_Left && ctl != WVR_DeviceType_Controller_Right) {
1146 | return false;
1147 | }
1148 |
1149 | uint8_t hand = (ctl == WVR_DeviceType_Controller_Left) ? HAND_LEFT : HAND_RIGHT;
1150 | // Create CXR controller handle
1151 | if (mControllers[hand] == nullptr) {
1152 | if (!WVR_IsDeviceConnected(ctl)) {
1153 | // device disconnected but have input event incoming?
1154 | // continue;
1155 | return false;
1156 | }
1157 |
1158 | cxrControllerDesc desc = {};
1159 | desc.id = ctl;
1160 | desc.role = (hand == HAND_LEFT) ?
1161 | "cxr://input/hand/left" : "cxr://input/hand/right";
1162 | desc.controllerName = "Oculus Touch";
1163 | //desc.controllerName = "vive_focus3_controller"; // CXR server does not recognize this name
1164 | // desc.controllerName = "VIVE FOCUS 3 Controller";
1165 | desc.inputCount = inputTouchLegacyCount;
1166 | desc.inputPaths = inputsTouchLegacy;
1167 | desc.inputValueTypes = inputValuesTouchLegacy;
1168 | cxrError e = cxrAddController(mReceiver, &desc, &mControllers[hand]);
1169 | if (e!=cxrError_Success)
1170 | {
1171 | LOGE("[UpdateInput] Error adding controller: %s", cxrErrorString(e));
1172 | //continue;
1173 | return false;
1174 | }
1175 | LOGE("[UpdateInput] Added controller %s, %s", desc.controllerName, desc.role);
1176 | } else {
1177 | // Controller handle exist but device is actually disconnected
1178 | if (!WVR_IsDeviceConnected(ctl)) {
1179 | LOGE("[UpdateInput] Device %d is disconnected.", ctl);
1180 | // destroy controller handle
1181 | }
1182 | }
1183 |
1184 | // button
1185 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]];
1186 | bool updateAnalog = true;
1187 | switch (event.common.type) {
1188 | case WVR_EventType_TouchTapped:{
1189 | e.inputValue.vBool = cxrTrue;
1190 | e.clientInputIndex = GetTouchInputIndex(true, event.input.inputId);
1191 | break;
1192 | }
1193 | case WVR_EventType_TouchUntapped:{
1194 | e.inputValue.vBool = cxrFalse;
1195 | e.clientInputIndex = GetTouchInputIndex(false, event.input.inputId);
1196 | break;
1197 | }
1198 | case WVR_EventType_ButtonPressed:{
1199 | e.inputValue.vBool = cxrTrue;
1200 | e.clientInputIndex = GetPressInputIndex(hand, true, event.input.inputId);
1201 | break;
1202 | }
1203 | case WVR_EventType_ButtonUnpressed:{
1204 | e.inputValue.vBool = cxrFalse;
1205 | e.clientInputIndex = GetPressInputIndex(hand, false, event.input.inputId);
1206 | break;
1207 | }
1208 | default:
1209 | updateAnalog = false;
1210 | break;
1211 | }
1212 |
1213 | if (e.clientInputIndex >= inputTouchLegacyCount) {
1214 | // skip unbinded input
1215 | LOGE("[UpdateInput] skip unbinded input %s, WVRInputId %d, CXRInputIndex %d",
1216 | hand == HAND_LEFT ? "LEFT" : "RIGHT", event.input.inputId,
1217 | e.clientInputIndex);
1218 | return false;
1219 | }
1220 |
1221 | e.clientTimeNS = event.device.common.timestamp;
1222 | e.inputValue.valueType = cxrInputValueType_boolean;
1223 | mCTLEventCount[hand]++;
1224 |
1225 | // analog flag
1226 | switch(event.input.inputId) {
1227 | case WVR_InputId_Alias1_Trigger:
1228 | mUpdateAnalogs[hand][IDX_TRIGGER] = updateAnalog;
1229 | break;
1230 | case WVR_InputId_Alias1_Grip:
1231 | mUpdateAnalogs[hand][IDX_GRIP] = updateAnalog;
1232 | break;
1233 | case WVR_InputId_Alias1_Thumbstick:
1234 | mUpdateAnalogs[hand][IDX_THUMBSTICK] = updateAnalog;
1235 | break;
1236 | default:
1237 | break;
1238 | }
1239 |
1240 | if (mCTLEventCount[hand] > 0) {
1241 | cxrError err = cxrFireControllerEvents(mReceiver, mControllers[hand], mCTLEvents[hand], mCTLEventCount[hand]);
1242 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, %d, Timestamp %lu",
1243 | hand == HAND_LEFT ? "LEFT" : "RIGHT", event.input.inputId,
1244 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vBool, e.clientTimeNS);*/
1245 | if (err != cxrError_Success)
1246 | {
1247 | LOGE("[UpdateInput] cxrFireControllerEvents failed: %s", cxrErrorString(err));
1248 | // TODO: how to handle UNUSUAL API errors? might just return up.
1249 | }
1250 | }
1251 | mCTLEventCount[hand] = 0;
1252 |
1253 | return true;
1254 | }
1255 |
1256 | /*
1257 | * CloudXR callbacks
1258 | *
1259 | * */
1260 | oboe::DataCallbackResult WaveCloudXRApp::onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {
1261 | cxrAudioFrame recordedFrame{};
1262 | recordedFrame.streamBuffer = (int16_t*)audioData;
1263 | recordedFrame.streamSizeBytes = numFrames * CXR_AUDIO_CHANNEL_COUNT * CXR_AUDIO_SAMPLE_SIZE;
1264 | cxrSendAudio(mReceiver, &recordedFrame);
1265 |
1266 | return oboe::DataCallbackResult::Continue;
1267 | }
1268 |
1269 | void WaveCloudXRApp::GetTrackingState(cxrVRTrackingState *trackingState) {
1270 |
1271 | if (mPaused || !mConnected || nullptr == trackingState)
1272 | return;
1273 |
1274 | // std::lock_guard lock(mPoseMutex);
1275 | *trackingState = mCXRPoseState;
1276 |
1277 | getPoseCount++;
1278 | }
1279 |
1280 | cxrBool WaveCloudXRApp::RenderAudio(const cxrAudioFrame *audioFrame) {
1281 | if (!mPlaybackStream || !mInited || !mConnected)
1282 | {
1283 | return cxrFalse;
1284 | }
1285 |
1286 | const uint32_t timeout = audioFrame->streamSizeBytes / CXR_AUDIO_BYTES_PER_MS;
1287 | const uint32_t numFrames = timeout * CXR_AUDIO_SAMPLING_RATE / 1000;
1288 | uint32_t timeoutMS = 4*timeout; // WAR for oboe timing issue on Focus+.
1289 | mPlaybackStream->write(audioFrame->streamBuffer, numFrames, timeoutMS * oboe::kNanosPerMillisecond);
1290 |
1291 | return cxrTrue;
1292 | }
1293 |
1294 | void WaveCloudXRApp::TriggerHaptic(const cxrHapticFeedback *haptic) {
1295 |
1296 | if (!mConnected || !mInited)
1297 | return;
1298 |
1299 | // Apply haptic feedback
1300 | if (haptic->seconds <= 0)
1301 | return;
1302 |
1303 | // deviceID is not necessary VR controller, can be gamepad or anything
1304 | // need to map this ID to WVR Device
1305 | WVR_TriggerVibration(haptic->deviceID == 0 ?
1306 | WVR_DeviceType_Controller_Left : WVR_DeviceType_Controller_Right,
1307 | WVR_InputId_Max, static_cast(haptic->seconds*1000000), 1,
1308 | WVR_Intensity_Normal);
1309 | }
1310 |
1311 | void WaveCloudXRApp::Pause() {
1312 | if (!mPaused) {
1313 | LOGW("Receive pause");
1314 | mPaused = true;
1315 | shutdownCloudXR();
1316 | } else {
1317 | // already paused, skip
1318 | }
1319 | }
1320 |
1321 | void WaveCloudXRApp::Resume() {
1322 | if (mPaused) {
1323 | LOGW("Receive resume");
1324 | mPaused = false;
1325 | } else {
1326 | // already resumed
1327 | }
1328 | }
1329 |
1330 | // This is called from CloudXR thread
1331 | void WaveCloudXRApp::HandleClientState(void* context, cxrClientState state, cxrError error) {
1332 | switch (state)
1333 | {
1334 | case cxrClientState_ConnectionAttemptInProgress:
1335 | LOGW("Connection attempt in progress.");
1336 | mConnected = false;
1337 | break;
1338 | case cxrClientState_StreamingSessionInProgress:
1339 | LOGW("Connection attempt succeeded.");
1340 | mConnected = true;
1341 | break;
1342 | case cxrClientState_ConnectionAttemptFailed:
1343 | LOGE("Connection attempt failed with error: %s", cxrErrorString(error));
1344 | state = cxrClientState_Disconnected; // retry connection
1345 | mConnected = false;
1346 | break;
1347 | case cxrClientState_Disconnected:
1348 | LOGE("Server disconnected with error: [%s]", cxrErrorString(error));
1349 | mConnected = false;
1350 | break;
1351 | default:
1352 | LOGW("Client state updated: %s to %s, reason: %s", ClientStateEnumToString(mClientState), ClientStateEnumToString(state), cxrErrorString(error));
1353 | break;
1354 | }
1355 |
1356 | if (mClientState != state) {
1357 | mClientState = state;
1358 |
1359 | mStateDirty = true;
1360 | }
1361 | }
1362 |
1363 | uint16_t WaveCloudXRApp::GetTouchInputIndex(const bool touched, const WVR_InputId wvrInputId) {
1364 |
1365 | uint16_t ret = 999;
1366 | switch(wvrInputId) {
1367 | case WVR_InputId_Alias1_Thumbstick:
1368 | ret = 9; // "/input/joystick/touch"
1369 | break;
1370 | case WVR_InputId_Alias1_Trigger:
1371 | ret = 3; // "/input/trigger/touch"
1372 | break;
1373 | case WVR_InputId_Alias1_Grip:
1374 | ret = 6; // "/input/grip/touch"
1375 | break;
1376 | case WVR_InputId_Alias1_A:
1377 | ret = 16; // "/input/a/touch"
1378 | break;
1379 | case WVR_InputId_Alias1_B:
1380 | ret = 17; // "/input/b/touch"
1381 | break;
1382 | case WVR_InputId_Alias1_X:
1383 | ret = 18; // "/input/x/touch"
1384 | break;
1385 | case WVR_InputId_Alias1_Y:
1386 | ret = 19; // "/input/y/touch"
1387 | break;
1388 | //case WVR_InputId_Alias1_Menu:
1389 | //case WVR_InputId_Alias1_System:
1390 | default:
1391 | break;
1392 | }
1393 |
1394 | return ret;
1395 | }
1396 |
1397 | uint16_t WaveCloudXRApp::GetPressInputIndex(const uint8_t hand, const bool pressed, const WVR_InputId wvrInputId) {
1398 |
1399 | uint16_t ret = 999;
1400 | switch(wvrInputId) {
1401 | case WVR_InputId_Alias1_Thumbstick:
1402 | ret = 8; // "/input/joystick/click"
1403 | break;
1404 | case WVR_InputId_Alias1_Trigger:
1405 | ret = 2; // "/input/trigger/click"
1406 | break;
1407 | case WVR_InputId_Alias1_Grip:
1408 | ret = 5; // "/input/grip/click"
1409 | break;
1410 | // WVR only sends A/B
1411 | case WVR_InputId_Alias1_A:
1412 | if (hand == HAND_RIGHT) ret = 12; // "/input/a/click"
1413 | if (hand == HAND_LEFT) ret = 14; // "/input/x/click"
1414 | break;
1415 | case WVR_InputId_Alias1_B:
1416 | if (hand == HAND_RIGHT) ret = 13; // "/input/b/click"
1417 | if (hand == HAND_LEFT) ret = 15; // "/input/y/click"
1418 | break;
1419 | case WVR_InputId_Alias1_X:
1420 | ret = 14; // "/input/x/click"
1421 | break;
1422 | case WVR_InputId_Alias1_Y:
1423 | ret = 15; // "/input/y/click"
1424 | break;
1425 | case WVR_InputId_Alias1_Menu:
1426 | // ret = 1; // "/input/application_menu/click"
1427 | ret = 0; // "/input/system/click"
1428 | break;
1429 | case WVR_InputId_Alias1_System:
1430 | ret = 0; // "/input/system/click"
1431 | break;
1432 | default:
1433 | break;
1434 | }
1435 | return ret;
1436 | }
1437 |
1438 | uint16_t WaveCloudXRApp::GetAnalogInputIndex(const bool pressed, const WVR_InputId wvrInputId) {
1439 |
1440 | uint16_t ret = 999;
1441 | switch(wvrInputId) {
1442 | /*case WVR_InputId_Alias1_Thumbstick:
1443 | ret = 10; // "/input/joystick/x"
1444 | ret = 11; // "/input/joystick/y"
1445 | break;*/
1446 | case WVR_InputId_Alias1_Trigger:
1447 | ret = 4; // "/input/trigger/click"
1448 | break;
1449 | case WVR_InputId_Alias1_Grip:
1450 | ret = 7; // "/input/grip/click"
1451 | break;
1452 | default:
1453 | break;
1454 | }
1455 | return ret;
1456 | }
1457 |
1458 | void WaveCloudXRApp::CheckStreamQuality() {
1459 |
1460 | // Log connection stats every 3 seconds
1461 | const int STATS_INTERVAL_SEC = 3;
1462 | mFramesUntilStats--;
1463 | cxrConnectionStats mStats = {};
1464 | if (mFramesUntilStats <= 0 &&
1465 | cxrGetConnectionStats(mReceiver, &mStats) == cxrError_Success)
1466 | {
1467 | // Capture the key connection statistics
1468 | char statsString[64] = { 0 };
1469 | snprintf(statsString, 64, "FPS: %6.1f Bitrate (kbps): %5d Latency (ms): %3d", mStats.framesPerSecond, mStats.bandwidthUtilizationKbps, mStats.roundTripDelayMs);
1470 |
1471 | // Turn the connection quality into a visual representation along the lines of a signal strength bar
1472 | char qualityString[64] = { 0 };
1473 | snprintf(qualityString, 64, "Connection quality: [%s]",
1474 | mStats.quality == cxrConnectionQuality_Bad ? "Bad" :
1475 | mStats.quality == cxrConnectionQuality_Poor ? "Poor" :
1476 | mStats.quality == cxrConnectionQuality_Fair ? "Fair" :
1477 | mStats.quality == cxrConnectionQuality_Good ? "Good" :
1478 | mStats.quality == cxrConnectionQuality_Excellent ? "Excellent" : "Invalid");
1479 |
1480 | // There could be multiple reasons for low quality however we show only the most impactful to the end user here
1481 | char reasonString[64] = { 0 };
1482 | if (mStats.quality <= cxrConnectionQuality_Fair)
1483 | {
1484 | if (mStats.qualityReasons == cxrConnectionQualityReason_EstimatingQuality)
1485 | {
1486 | snprintf(reasonString, 64, "Reason: Estimating quality");
1487 | }
1488 | else if (mStats.qualityReasons & cxrConnectionQualityReason_HighLatency)
1489 | {
1490 | snprintf(reasonString, 64, "Reason: High Latency (ms): %3d", mStats.roundTripDelayMs);
1491 | }
1492 | else if (mStats.qualityReasons & cxrConnectionQualityReason_LowBandwidth)
1493 | {
1494 | snprintf(reasonString, 64, "Reason: Low Bandwidth (kbps): %5d", mStats.bandwidthAvailableKbps);
1495 | }
1496 | else if (mStats.qualityReasons & cxrConnectionQualityReason_HighPacketLoss)
1497 | {
1498 | if (mStats.totalPacketsLost == 0)
1499 | {
1500 | snprintf(reasonString, 64, "Reason: High Packet Loss (Recoverable)");
1501 | }
1502 | else
1503 | {
1504 | snprintf(reasonString, 64, "Reason: High Packet Loss (%%): %3.1f", 100.0f * mStats.totalPacketsLost / mStats.totalPacketsReceived);
1505 | }
1506 | }
1507 | }
1508 |
1509 | LOGI("%s %s %s", statsString, qualityString, reasonString);
1510 | mFramesUntilStats = (int)mStats.framesPerSecond * STATS_INTERVAL_SEC;
1511 | }
1512 | }
1513 |
1514 |
--------------------------------------------------------------------------------