diff --git a/.gitmodules b/.gitmodules
index 8a060de9b..1477e6474 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -61,3 +61,6 @@
[submodule "app/src/main/cpp/bzip2"]
path = app/src/main/cpp/bzip2
url = https://gitlab.com/bzip2/bzip2
+[submodule "app/src/main/cpp/OpenXR-SDK"]
+ path = app/src/main/cpp/OpenXR-SDK
+ url = https://github.com/KhronosGroup/OpenXR-SDK.git
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 60847d078..c820934c6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,10 @@
+
+
+
+
+
+
+
+
+
+
+
+
+#include "engine.h"
+#include "input.h"
+#include "math.h"
+#include "renderer.h"
+
+struct XrEngine xr_engine;
+struct XrInput xr_input;
+struct XrRenderer xr_renderer;
+bool xr_initialized = false;
+int xr_params[6] = {};
+
+#if defined(_DEBUG)
+#include
+void GLCheckErrors(const char* file, int line) {
+ for (int i = 0; i < 10; i++) {
+ const GLenum error = glGetError();
+ if (error == GL_NO_ERROR) {
+ break;
+ }
+ ALOGE("OpenGL error on line %s:%d %d", file, line, error);
+ }
+}
+
+void OXRCheckErrors(XrResult result, const char* file, int line) {
+ if (XR_FAILED(result)) {
+ char errorBuffer[XR_MAX_RESULT_STRING_SIZE];
+ xrResultToString(xr_engine->Instance, result, errorBuffer);
+ ALOGE("OpenXR error on line %s:%d %s", file, line, errorBuffer);
+ }
+}
+#endif
+
+JNIEXPORT void JNICALL Java_com_termux_x11_XrActivity_init(JNIEnv *env, jobject obj) {
+
+ // Do not allow second initialization
+ if (xr_initialized) {
+ return;
+ }
+
+ // Set platform flags
+ memset(&xr_engine, 0, sizeof(xr_engine));
+ xr_engine.PlatformFlag[PLATFORM_CONTROLLER_QUEST] = true;
+ xr_engine.PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH] = true;
+ xr_engine.PlatformFlag[PLATFORM_EXTENSION_PERFORMANCE] = true;
+
+ // Get Java VM
+ JavaVM* vm;
+ (*env)->GetJavaVM(env, &vm);
+
+ // Init XR
+ xrJava java;
+ java.vm = vm;
+ java.activity = (*env)->NewGlobalRef(env, obj);
+ XrEngineInit(&xr_engine, &java, "termux-x11", 1);
+ XrEngineEnter(&xr_engine);
+ XrInputInit(&xr_engine, &xr_input);
+ XrRendererInit(&xr_engine, &xr_renderer);
+ XrRendererGetResolution(&xr_engine, &xr_renderer, &xr_params[4], &xr_params[5]);
+ xr_initialized = true;
+ ALOGV("Init called");
+}
+
+JNIEXPORT void JNICALL Java_com_termux_x11_XrActivity_teardown(JNIEnv *env, jobject obj) {
+ if (!xr_initialized) {
+ return;
+ }
+
+ XrRendererDestroy(&xr_engine, &xr_renderer);
+ XrEngineLeave(&xr_engine);
+ XrEngineDestroy(&xr_engine);
+
+ memset(&xr_engine, 0, sizeof(xr_engine));
+ memset(&xr_input, 0, sizeof(xr_input));
+ memset(&xr_renderer, 0, sizeof(xr_renderer));
+ xr_initialized = false;
+}
+
+JNIEXPORT jboolean JNICALL Java_com_termux_x11_XrActivity_beginFrame(JNIEnv *env, jobject obj) {
+ if (XrRendererInitFrame(&xr_engine, &xr_renderer)) {
+
+ // Set renderer
+ int mode = xr_params[1] ? RENDER_MODE_MONO_6DOF : RENDER_MODE_MONO_SCREEN;
+ xr_renderer.ConfigFloat[CONFIG_CANVAS_DISTANCE] = xr_params[0];
+ xr_renderer.ConfigInt[CONFIG_PASSTHROUGH] = xr_params[2];
+ xr_renderer.ConfigInt[CONFIG_MODE] = mode;
+ xr_renderer.ConfigInt[CONFIG_SBS] = xr_params[3];
+
+ // Recenter if mode switched
+ static bool last_immersive = false;
+ if (last_immersive != xr_params[1]) {
+ XrRendererRecenter(&xr_engine, &xr_renderer);
+ last_immersive = xr_params[1];
+ }
+
+ // Update controllers state
+ XrInputUpdate(&xr_engine, &xr_input);
+
+ // Lock framebuffer
+ XrRendererBeginFrame(&xr_renderer, 0);
+
+ return true;
+ }
+ return false;
+}
+
+JNIEXPORT void JNICALL Java_com_termux_x11_XrActivity_finishFrame(JNIEnv *env, jobject obj) {
+ XrRendererEndFrame(&xr_renderer);
+ XrRendererFinishFrame(&xr_engine, &xr_renderer);
+}
+
+JNIEXPORT jfloatArray JNICALL Java_com_termux_x11_XrActivity_getAxes(JNIEnv *env, jobject obj) {
+ XrPosef lPose = XrInputGetPose(&xr_input, 0);
+ XrPosef rPose = XrInputGetPose(&xr_input, 1);
+ XrVector2f lThumbstick = XrInputGetJoystickState(&xr_input, 0);
+ XrVector2f rThumbstick = XrInputGetJoystickState(&xr_input, 1);
+ XrVector3f lPosition = xr_renderer.Projections[0].pose.position;
+ XrVector3f rPosition = xr_renderer.Projections[1].pose.position;
+ XrVector3f angles = xr_renderer.HmdOrientation;
+
+ int count = 0;
+ float data[32];
+ data[count++] = XrQuaternionfEulerAngles(lPose.orientation).x; //L_PITCH
+ data[count++] = XrQuaternionfEulerAngles(lPose.orientation).y; //L_YAW
+ data[count++] = XrQuaternionfEulerAngles(lPose.orientation).z; //L_ROLL
+ data[count++] = lThumbstick.x; //L_THUMBSTICK_X
+ data[count++] = lThumbstick.y; //L_THUMBSTICK_Y
+ data[count++] = lPose.position.x; //L_X
+ data[count++] = lPose.position.y; //L_Y
+ data[count++] = lPose.position.z; //L_Z
+ data[count++] = XrQuaternionfEulerAngles(rPose.orientation).x; //R_PITCH
+ data[count++] = XrQuaternionfEulerAngles(rPose.orientation).y; //R_YAW
+ data[count++] = XrQuaternionfEulerAngles(rPose.orientation).z; //R_ROLL
+ data[count++] = rThumbstick.x; //R_THUMBSTICK_X
+ data[count++] = rThumbstick.y; //R_THUMBSTICK_Y
+ data[count++] = rPose.position.x; //R_X
+ data[count++] = rPose.position.y; //R_Y
+ data[count++] = rPose.position.z; //R_Z
+ data[count++] = angles.x; //HMD_PITCH
+ data[count++] = angles.y; //HMD_YAW
+ data[count++] = angles.z; //HMD_ROLL
+ data[count++] = (lPosition.x + rPosition.x) * 0.5f; //HMD_X
+ data[count++] = (lPosition.y + rPosition.y) * 0.5f; //HMD_Y
+ data[count++] = (lPosition.z + rPosition.z) * 0.5f; //HMD_Z
+ data[count++] = XrVector3fDistance(lPosition, rPosition); //HMD_IPD
+
+ jfloat values[count];
+ memcpy(values, data, count * sizeof(float));
+ jfloatArray output = (*env)->NewFloatArray(env, count);
+ (*env)->SetFloatArrayRegion(env, output, (jsize)0, (jsize)count, values);
+ return output;
+}
+
+JNIEXPORT jbooleanArray JNICALL Java_com_termux_x11_XrActivity_getButtons(JNIEnv *env, jobject obj) {
+ uint32_t l = XrInputGetButtonState(&xr_input, 0);
+ uint32_t r = XrInputGetButtonState(&xr_input, 1);
+
+ int count = 0;
+ bool data[32];
+ data[count++] = l & (int)Grip; //L_GRIP
+ data[count++] = l & (int)Enter; //L_MENU
+ data[count++] = l & (int)LThumb; //L_THUMBSTICK_PRESS
+ data[count++] = l & (int)Left; //L_THUMBSTICK_LEFT
+ data[count++] = l & (int)Right; //L_THUMBSTICK_RIGHT
+ data[count++] = l & (int)Up; //L_THUMBSTICK_UP
+ data[count++] = l & (int)Down; //L_THUMBSTICK_DOWN
+ data[count++] = l & (int)Trigger; //L_TRIGGER
+ data[count++] = l & (int)X; //L_X
+ data[count++] = l & (int)Y; //L_Y
+ data[count++] = r & (int)A; //R_A
+ data[count++] = r & (int)B; //R_B
+ data[count++] = r & (int)Grip; //R_GRIP
+ data[count++] = r & (int)RThumb; //R_THUMBSTICK_PRESS
+ data[count++] = r & (int)Left; //R_THUMBSTICK_LEFT
+ data[count++] = r & (int)Right; //R_THUMBSTICK_RIGHT
+ data[count++] = r & (int)Up; //R_THUMBSTICK_UP
+ data[count++] = r & (int)Down; //R_THUMBSTICK_DOWN
+ data[count++] = r & (int)Trigger; //R_TRIGGER
+
+ jboolean values[count];
+ memcpy(values, data, count * sizeof(jboolean));
+ jbooleanArray output = (*env)->NewBooleanArray(env, count);
+ (*env)->SetBooleanArrayRegion(env, output, (jsize)0, (jsize)count, values);
+ return output;
+}
+
+
+JNIEXPORT jint JNICALL Java_com_termux_x11_XrActivity_getRenderParam(JNIEnv *env, jobject obj, jint param) {
+ return xr_params[param];
+}
+
+JNIEXPORT void JNICALL Java_com_termux_x11_XrActivity_setRenderParam(JNIEnv *env, jobject obj, jint param, jint value) {
+ xr_params[param] = value;
+}
diff --git a/app/src/main/cpp/xrio/engine.c b/app/src/main/cpp/xrio/engine.c
new file mode 100644
index 000000000..307cc367c
--- /dev/null
+++ b/app/src/main/cpp/xrio/engine.c
@@ -0,0 +1,190 @@
+#include "engine.h"
+
+#include
+#include
+#include
+
+void XrEngineInit(struct XrEngine* engine, void* system, const char* name, int version) {
+ if (engine->Initialized)
+ return;
+ memset(engine, 0, sizeof(engine));
+
+#ifdef ANDROID
+ PFN_xrInitializeLoaderKHR xrInitializeLoaderKHR;
+ xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
+ (PFN_xrVoidFunction*)&xrInitializeLoaderKHR);
+ if (xrInitializeLoaderKHR != NULL) {
+ xrJava* java = (xrJava*)system;
+ XrLoaderInitInfoAndroidKHR loader_info;
+ memset(&loader_info, 0, sizeof(loader_info));
+ loader_info.type = XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR;
+ loader_info.next = NULL;
+ loader_info.applicationVM = java->vm;
+ loader_info.applicationContext = java->activity;
+ xrInitializeLoaderKHR((XrLoaderInitInfoBaseHeaderKHR*)&loader_info);
+ }
+#endif
+
+ int count = 0;
+ const char* extensions[32];
+#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
+ extensions[count++] = XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME;
+#endif
+#ifdef ANDROID
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_INSTANCE]) {
+ extensions[count++] = XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME;
+ }
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH]) {
+ extensions[count++] = XR_FB_PASSTHROUGH_EXTENSION_NAME;
+ }
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_PERFORMANCE]) {
+ extensions[count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
+ extensions[count++] = XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME;
+ }
+#endif
+
+ // Create the OpenXR instance.
+ XrApplicationInfo app_info;
+ memset(&app_info, 0, sizeof(app_info));
+ strcpy(app_info.applicationName, name);
+ strcpy(app_info.engineName, name);
+ app_info.applicationVersion = version;
+ app_info.engineVersion = version;
+ app_info.apiVersion = XR_API_VERSION_1_0;
+
+ XrInstanceCreateInfo instance_info;
+ memset(&instance_info, 0, sizeof(instance_info));
+ instance_info.type = XR_TYPE_INSTANCE_CREATE_INFO;
+ instance_info.next = NULL;
+ instance_info.createFlags = 0;
+ instance_info.applicationInfo = app_info;
+ instance_info.enabledApiLayerCount = 0;
+ instance_info.enabledApiLayerNames = NULL;
+ instance_info.enabledExtensionCount = count;
+ instance_info.enabledExtensionNames = extensions;
+
+#ifdef ANDROID
+ XrInstanceCreateInfoAndroidKHR instance_info_android = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_INSTANCE]) {
+ xrJava* java = (xrJava*)system;
+ instance_info_android.applicationVM = java->vm;
+ instance_info_android.applicationActivity = java->activity;
+ instance_info.next = (XrBaseInStructure*)&instance_info_android;
+ }
+#endif
+
+ XrResult result;
+ OXR(result = xrCreateInstance(&instance_info, &engine->Instance));
+ if (result != XR_SUCCESS) {
+ ALOGE("Failed to create XR instance: %d", (int)result);
+ exit(1);
+ }
+
+ XrInstanceProperties instance_properties;
+ instance_properties.type = XR_TYPE_INSTANCE_PROPERTIES;
+ instance_properties.next = NULL;
+ OXR(xrGetInstanceProperties(engine->Instance, &instance_properties));
+ ALOGV("Runtime %s: Version : %d.%d.%d", instance_properties.runtimeName,
+ XR_VERSION_MAJOR(instance_properties.runtimeVersion),
+ XR_VERSION_MINOR(instance_properties.runtimeVersion),
+ XR_VERSION_PATCH(instance_properties.runtimeVersion));
+
+ XrSystemGetInfo system_info;
+ memset(&system_info, 0, sizeof(system_info));
+ system_info.type = XR_TYPE_SYSTEM_GET_INFO;
+ system_info.next = NULL;
+ system_info.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
+
+ OXR(result = xrGetSystem(engine->Instance, &system_info, &engine->SystemId));
+ if (result != XR_SUCCESS) {
+ ALOGE("Failed to get system");
+ exit(1);
+ }
+
+ // Get the graphics requirements.
+#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
+ PFN_xrGetOpenGLESGraphicsRequirementsKHR pfnGetOpenGLESGraphicsRequirementsKHR = NULL;
+ OXR(xrGetInstanceProcAddr(engine->Instance, "xrGetOpenGLESGraphicsRequirementsKHR",
+ (PFN_xrVoidFunction*)(&pfnGetOpenGLESGraphicsRequirementsKHR)));
+
+ XrGraphicsRequirementsOpenGLESKHR graphics_requirements = {};
+ graphics_requirements.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR;
+ OXR(pfnGetOpenGLESGraphicsRequirementsKHR(engine->Instance, engine->SystemId, &graphics_requirements));
+#endif
+
+#ifdef ANDROID
+ engine->MainThreadId = gettid();
+#endif
+ engine->Initialized = true;
+}
+
+void XrEngineDestroy(struct XrEngine* engine) {
+ if (engine->Initialized) {
+ xrDestroyInstance(engine->Instance);
+ engine->Initialized = false;
+ }
+}
+
+void XrEngineEnter(struct XrEngine* engine) {
+ if (engine->Session) {
+ ALOGE("EnterXR called with existing session");
+ return;
+ }
+
+ // Create the OpenXR Session.
+ XrSessionCreateInfo session_info;
+ memset(&session_info, 0, sizeof(session_info));
+#ifdef XR_USE_GRAPHICS_API_OPENGL_ES
+ XrGraphicsBindingOpenGLESAndroidKHR graphics_binding_gl = {};
+ graphics_binding_gl.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR;
+ graphics_binding_gl.next = NULL;
+ graphics_binding_gl.display = eglGetCurrentDisplay();
+ graphics_binding_gl.config = NULL;
+ graphics_binding_gl.context = eglGetCurrentContext();
+ session_info.next = &graphics_binding_gl;
+#endif
+ session_info.type = XR_TYPE_SESSION_CREATE_INFO;
+ session_info.createFlags = 0;
+ session_info.systemId = engine->SystemId;
+
+ XrResult result;
+ OXR(result = xrCreateSession(engine->Instance, &session_info, &engine->Session));
+ if (result != XR_SUCCESS) {
+ ALOGE("Failed to create XR session: %d", (int)result);
+ exit(1);
+ }
+
+ // Create a space to the first path
+ XrReferenceSpaceCreateInfo space_info = {};
+ space_info.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO;
+ space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
+ space_info.poseInReferenceSpace.orientation.w = 1.0f;
+ OXR(xrCreateReferenceSpace(engine->Session, &space_info, &engine->HeadSpace));
+}
+
+void XrEngineLeave(struct XrEngine* engine) {
+ if (engine->Session) {
+ OXR(xrDestroySpace(engine->HeadSpace));
+ // StageSpace is optional.
+ if (engine->StageSpace != XR_NULL_HANDLE) {
+ OXR(xrDestroySpace(engine->StageSpace));
+ }
+ OXR(xrDestroySpace(engine->FakeSpace));
+ engine->CurrentSpace = XR_NULL_HANDLE;
+ OXR(xrDestroySession(engine->Session));
+ engine->Session = XR_NULL_HANDLE;
+ }
+}
+
+void XrEngineWaitForFrame(struct XrEngine* engine) {
+ XrFrameWaitInfo wait_frame_info = {};
+ wait_frame_info.type = XR_TYPE_FRAME_WAIT_INFO;
+ wait_frame_info.next = NULL;
+
+ XrFrameState frame_state = {};
+ frame_state.type = XR_TYPE_FRAME_STATE;
+ frame_state.next = NULL;
+
+ OXR(xrWaitFrame(engine->Session, &wait_frame_info, &frame_state));
+ engine->PredictedDisplayTime = frame_state.predictedDisplayTime;
+}
diff --git a/app/src/main/cpp/xrio/engine.h b/app/src/main/cpp/xrio/engine.h
new file mode 100644
index 000000000..7c15a0c4b
--- /dev/null
+++ b/app/src/main/cpp/xrio/engine.h
@@ -0,0 +1,90 @@
+#pragma once
+
+#include
+
+//#define _DEBUG
+
+#if defined(_DEBUG)
+void GLCheckErrors(const char* file, int line);
+void OXRCheckErrors(XrResult result, const char* file, int line);
+
+#define GL(func) func; GLCheckErrors(__FILE__ , __LINE__);
+#define OXR(func) OXRCheckErrors(func, __FILE__ , __LINE__);
+#else
+#define GL(func) func;
+#define OXR(func) func;
+#endif
+
+#ifdef ANDROID
+#include
+#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, "OpenXR", __VA_ARGS__);
+#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, "OpenXR", __VA_ARGS__);
+
+#include
+#include
+#include
+#define XR_USE_PLATFORM_ANDROID 1
+#define XR_USE_GRAPHICS_API_OPENGL_ES 1
+#else
+#include
+#define ALOGE(...) printf(__VA_ARGS__)
+#define ALOGV(...) printf(__VA_ARGS__)
+#endif
+
+#include
+#include
+
+enum {
+ XrMaxLayerCount = 2
+};
+enum {
+ XrMaxNumEyes = 2
+};
+
+enum XrPlatformFlag {
+ PLATFORM_CONTROLLER_PICO,
+ PLATFORM_CONTROLLER_QUEST,
+ PLATFORM_EXTENSION_INSTANCE,
+ PLATFORM_EXTENSION_PASSTHROUGH,
+ PLATFORM_EXTENSION_PERFORMANCE,
+ PLATFORM_TRACKING_FLOOR,
+ PLATFORM_MAX
+};
+
+typedef union {
+ XrCompositionLayerProjection projection;
+ XrCompositionLayerQuad quad;
+} XrCompositorLayer;
+
+#ifdef ANDROID
+typedef struct {
+ jobject activity;
+ JNIEnv* env;
+ JavaVM* vm;
+} xrJava;
+#endif
+
+struct XrEngine {
+ XrInstance Instance;
+ XrSession Session;
+ XrSystemId SystemId;
+
+ XrSpace CurrentSpace;
+ XrSpace FakeSpace;
+ XrSpace HeadSpace;
+ XrSpace StageSpace;
+
+ XrTime PredictedDisplayTime;
+
+ int MainThreadId;
+ int RenderThreadId;
+
+ bool PlatformFlag[PLATFORM_MAX];
+ bool Initialized;
+};
+
+void XrEngineInit(struct XrEngine* engine, void* system, const char* name, int version);
+void XrEngineDestroy(struct XrEngine* engine);
+void XrEngineEnter(struct XrEngine* engine);
+void XrEngineLeave(struct XrEngine* engine);
+void XrEngineWaitForFrame(struct XrEngine* engine);
diff --git a/app/src/main/cpp/xrio/framebuffer.c b/app/src/main/cpp/xrio/framebuffer.c
new file mode 100644
index 000000000..ac27bda69
--- /dev/null
+++ b/app/src/main/cpp/xrio/framebuffer.c
@@ -0,0 +1,127 @@
+#include "framebuffer.h"
+
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+#include
+#include
+#endif
+
+#include
+#include
+
+bool XrFramebufferCreate(struct XrFramebuffer *framebuffer, XrSession session, int width, int height) {
+ memset(framebuffer, 0, sizeof(framebuffer));
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+ return XrFramebufferCreateGL(framebuffer, session, width, height);
+#else
+ return false;
+#endif
+}
+
+void XrFramebufferDestroy(struct XrFramebuffer *framebuffer) {
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+ GL(glDeleteRenderbuffers(framebuffer->SwapchainLength, framebuffer->GLDepthBuffers));
+ GL(glDeleteFramebuffers(framebuffer->SwapchainLength, framebuffer->GLFrameBuffers));
+ free(framebuffer->GLDepthBuffers);
+ free(framebuffer->GLFrameBuffers);
+#endif
+ OXR(xrDestroySwapchain(framebuffer->Handle));
+ free(framebuffer->SwapchainImage);
+}
+
+void XrFramebufferAcquire(struct XrFramebuffer *framebuffer) {
+ XrSwapchainImageAcquireInfo acquire_info = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO, NULL};
+ OXR(xrAcquireSwapchainImage(framebuffer->Handle, &acquire_info, &framebuffer->SwapchainIndex));
+
+ XrSwapchainImageWaitInfo wait_info;
+ wait_info.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO;
+ wait_info.next = NULL;
+ wait_info.timeout = 1000000; /* timeout in nanoseconds */
+ XrResult res = xrWaitSwapchainImage(framebuffer->Handle, &wait_info);
+ int i = 0;
+ while ((res != XR_SUCCESS) && (i < 10)) {
+ res = xrWaitSwapchainImage(framebuffer->Handle, &wait_info);
+ i++;
+ ALOGV("Retry xrWaitSwapchainImage %d times due XR_TIMEOUT_EXPIRED (duration %lf ms",
+ i, wait_info.timeout * (1E-9));
+ }
+
+ framebuffer->Acquired = res == XR_SUCCESS;
+ XrFramebufferSetCurrent(framebuffer);
+}
+
+void XrFramebufferRelease(struct XrFramebuffer *framebuffer) {
+ if (framebuffer->Acquired) {
+ XrSwapchainImageReleaseInfo release_info = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO, NULL};
+ OXR(xrReleaseSwapchainImage(framebuffer->Handle, &release_info));
+ framebuffer->Acquired = false;
+ }
+}
+
+void XrFramebufferSetCurrent(struct XrFramebuffer *framebuffer) {
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+ GL(glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->GLFrameBuffers[framebuffer->SwapchainIndex]));
+#endif
+}
+
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+bool XrFramebufferCreateGL(struct XrFramebuffer *framebuffer, XrSession session, int width, int height) {
+ XrSwapchainCreateInfo swapchain_info;
+ memset(&swapchain_info, 0, sizeof(swapchain_info));
+ swapchain_info.type = XR_TYPE_SWAPCHAIN_CREATE_INFO;
+ swapchain_info.sampleCount = 1;
+ swapchain_info.width = width;
+ swapchain_info.height = height;
+ swapchain_info.faceCount = 1;
+ swapchain_info.mipCount = 1;
+ swapchain_info.arraySize = 1;
+
+ framebuffer->Width = swapchain_info.width;
+ framebuffer->Height = swapchain_info.height;
+
+ // Create the color swapchain.
+ swapchain_info.format = GL_SRGB8_ALPHA8_EXT;
+ swapchain_info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
+ OXR(xrCreateSwapchain(session, &swapchain_info, &framebuffer->Handle));
+ OXR(xrEnumerateSwapchainImages(framebuffer->Handle, 0, &framebuffer->SwapchainLength, NULL));
+ framebuffer->SwapchainImage = malloc(framebuffer->SwapchainLength * sizeof(XrSwapchainImageOpenGLESKHR));
+
+ // Populate the swapchain image array.
+ for (uint32_t i = 0; i < framebuffer->SwapchainLength; i++) {
+ XrSwapchainImageOpenGLESKHR* swapchain = (XrSwapchainImageOpenGLESKHR*)framebuffer->SwapchainImage;
+ swapchain[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
+ swapchain[i].next = NULL;
+ }
+ OXR(xrEnumerateSwapchainImages(framebuffer->Handle, framebuffer->SwapchainLength,
+ &framebuffer->SwapchainLength,
+ (XrSwapchainImageBaseHeader*)framebuffer->SwapchainImage));
+
+ framebuffer->GLDepthBuffers = (GLuint*)malloc(framebuffer->SwapchainLength * sizeof(GLuint));
+ framebuffer->GLFrameBuffers = (GLuint*)malloc(framebuffer->SwapchainLength * sizeof(GLuint));
+ for (uint32_t i = 0; i < framebuffer->SwapchainLength; i++) {
+ // Create color and depth buffers.
+ GLuint color_texture = ((XrSwapchainImageOpenGLESKHR*)framebuffer->SwapchainImage)[i].image;
+ GL(glGenRenderbuffers(1, &framebuffer->GLDepthBuffers[i]));
+ GL(glBindRenderbuffer(GL_RENDERBUFFER, framebuffer->GLDepthBuffers[i]));
+ GL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES, width, height));
+ GL(glBindRenderbuffer(GL_RENDERBUFFER, 0));
+
+ // Create the frame buffer.
+ GL(glGenFramebuffers(1, &framebuffer->GLFrameBuffers[i]));
+ GL(glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->GLFrameBuffers[i]));
+ GL(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
+ framebuffer->GLDepthBuffers[i]));
+ GL(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
+ framebuffer->GLDepthBuffers[i]));
+ GL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ color_texture, 0));
+ GL(GLenum renderFramebufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER));
+ GL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
+ if (renderFramebufferStatus != GL_FRAMEBUFFER_COMPLETE) {
+ ALOGE("Incomplete frame buffer object: %d", renderFramebufferStatus);
+ return false;
+ }
+ }
+
+ return true;
+}
+#endif
diff --git a/app/src/main/cpp/xrio/framebuffer.h b/app/src/main/cpp/xrio/framebuffer.h
new file mode 100644
index 000000000..5983b165b
--- /dev/null
+++ b/app/src/main/cpp/xrio/framebuffer.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "engine.h"
+
+struct XrFramebuffer {
+ int Width;
+ int Height;
+ bool Acquired;
+ XrSwapchain Handle;
+
+ uint32_t SwapchainIndex;
+ uint32_t SwapchainLength;
+ void* SwapchainImage;
+
+ unsigned int* GLDepthBuffers;
+ unsigned int* GLFrameBuffers;
+};
+
+bool XrFramebufferCreate(struct XrFramebuffer *framebuffer, XrSession session, int width, int height);
+void XrFramebufferDestroy(struct XrFramebuffer *framebuffer);
+
+void XrFramebufferAcquire(struct XrFramebuffer *framebuffer);
+void XrFramebufferRelease(struct XrFramebuffer *framebuffer);
+void XrFramebufferSetCurrent(struct XrFramebuffer *framebuffer);
+
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+bool XrFramebufferCreateGL(struct XrFramebuffer *framebuffer, XrSession session, int width, int height);
+#endif
diff --git a/app/src/main/cpp/xrio/input.c b/app/src/main/cpp/xrio/input.c
new file mode 100644
index 000000000..cec79f8c5
--- /dev/null
+++ b/app/src/main/cpp/xrio/input.c
@@ -0,0 +1,404 @@
+#include "input.h"
+#include "math.h"
+
+#include
+#include
+
+void XrInputInit(struct XrEngine* engine, struct XrInput* input) {
+ if (input->Initialized)
+ return;
+ memset(input, 0, sizeof(input));
+
+ // Actions
+ input->ActionSet = XrInputCreateActionSet(engine->Instance, "running_action_set", "Actionset");
+ input->IndexLeft = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "index_left", "Index left", 0, NULL);
+ input->IndexRight = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "index_right","Index right", 0, NULL);
+ input->ButtonMenu = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "menu_action", "ButtonMenu", 0, NULL);
+ input->ButtonA = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_a", "XrButton A", 0, NULL);
+ input->ButtonB = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_b", "XrButton B", 0, NULL);
+ input->ButtonX = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_x", "XrButton X", 0, NULL);
+ input->ButtonY = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_y", "XrButton Y", 0, NULL);
+ input->GripLeft = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_FLOAT_INPUT, "grip_left", "Grip left", 0, NULL);
+ input->GripRight = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_FLOAT_INPUT, "grip_right", "Grip right", 0, NULL);
+ input->JoystickLeft = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_VECTOR2F_INPUT, "move_on_left_joy","Move on left Joy", 0, NULL);
+ input->JoystickRight = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_VECTOR2F_INPUT, "move_on_right_joy","Move on right Joy", 0, NULL);
+ input->ThumbLeft = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "thumbstick_left","Thumbstick left", 0, NULL);
+ input->ThumbRight = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "thumbstick_right","Thumbstick right", 0, NULL);
+ input->VibrateLeftFeedback = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_VIBRATION_OUTPUT, "vibrate_left_feedback","Vibrate Left Controller", 0, NULL);
+ input->VibrateRightFeedback = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_VIBRATION_OUTPUT, "vibrate_right_feedback","Vibrate Right Controller", 0, NULL);
+
+ OXR(xrStringToPath(engine->Instance, "/user/hand/left", &input->LeftHandPath));
+ OXR(xrStringToPath(engine->Instance, "/user/hand/right", &input->RightHandPath));
+ input->HandPoseLeft = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_POSE_INPUT, "hand_pose_left", NULL,1, &input->LeftHandPath);
+ input->HandPoseRight = XrInputCreateAction(input->ActionSet, XR_ACTION_TYPE_POSE_INPUT, "hand_pose_right", NULL,1, &input->RightHandPath);
+
+ XrPath interactionProfilePath = XR_NULL_PATH;
+ if (engine->PlatformFlag[PLATFORM_CONTROLLER_QUEST]) {
+ OXR(xrStringToPath(engine->Instance, "/interaction_profiles/oculus/touch_controller",&interactionProfilePath));
+ } else if (engine->PlatformFlag[PLATFORM_CONTROLLER_PICO]) {
+ OXR(xrStringToPath(engine->Instance, "/interaction_profiles/pico/neo3_controller",&interactionProfilePath));
+ }
+
+ // Map bindings
+ XrInstance instance = engine->Instance;
+ XrActionSuggestedBinding bindings[32]; // large enough for all profiles
+ int curr = 0;
+
+ if (engine->PlatformFlag[PLATFORM_CONTROLLER_QUEST]) {
+ bindings[curr++] = XrInputGetBinding(instance, input->IndexLeft, "/user/hand/left/input/trigger");
+ bindings[curr++] = XrInputGetBinding(instance, input->IndexRight, "/user/hand/right/input/trigger");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonMenu, "/user/hand/left/input/menu/click");
+ } else if (engine->PlatformFlag[PLATFORM_CONTROLLER_PICO]) {
+ bindings[curr++] = XrInputGetBinding(instance, input->IndexLeft, "/user/hand/left/input/trigger/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->IndexRight, "/user/hand/right/input/trigger/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonMenu, "/user/hand/left/input/back/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonMenu, "/user/hand/right/input/back/click");
+ }
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonX, "/user/hand/left/input/x/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonY, "/user/hand/left/input/y/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonA, "/user/hand/right/input/a/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ButtonB, "/user/hand/right/input/b/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->GripLeft, "/user/hand/left/input/squeeze/value");
+ bindings[curr++] = XrInputGetBinding(instance, input->GripRight, "/user/hand/right/input/squeeze/value");
+ bindings[curr++] = XrInputGetBinding(instance, input->JoystickLeft, "/user/hand/left/input/thumbstick");
+ bindings[curr++] = XrInputGetBinding(instance, input->JoystickRight, "/user/hand/right/input/thumbstick");
+ bindings[curr++] = XrInputGetBinding(instance, input->ThumbLeft, "/user/hand/left/input/thumbstick/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->ThumbRight, "/user/hand/right/input/thumbstick/click");
+ bindings[curr++] = XrInputGetBinding(instance, input->VibrateLeftFeedback, "/user/hand/left/output/haptic");
+ bindings[curr++] = XrInputGetBinding(instance, input->VibrateRightFeedback, "/user/hand/right/output/haptic");
+ bindings[curr++] = XrInputGetBinding(instance, input->HandPoseLeft, "/user/hand/left/input/aim/pose");
+ bindings[curr++] = XrInputGetBinding(instance, input->HandPoseRight, "/user/hand/right/input/aim/pose");
+
+ XrInteractionProfileSuggestedBinding suggested_bindings = {};
+ suggested_bindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;
+ suggested_bindings.next = NULL;
+ suggested_bindings.interactionProfile = interactionProfilePath;
+ suggested_bindings.suggestedBindings = bindings;
+ suggested_bindings.countSuggestedBindings = curr;
+ OXR(xrSuggestInteractionProfileBindings(engine->Instance, &suggested_bindings));
+
+ // Attach actions
+ XrSessionActionSetsAttachInfo attach_info = {};
+ attach_info.type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO;
+ attach_info.next = NULL;
+ attach_info.countActionSets = 1;
+ attach_info.actionSets = &input->ActionSet;
+ OXR(xrAttachSessionActionSets(engine->Session, &attach_info));
+
+ // Enumerate actions
+ char string_buffer[256];
+ XrPath action_paths_buffer[32];
+ XrAction actions_to_enumerate[] = {input->IndexLeft,
+ input->IndexRight,
+ input->ButtonMenu,
+ input->ButtonA,
+ input->ButtonB,
+ input->ButtonX,
+ input->ButtonY,
+ input->GripLeft,
+ input->GripRight,
+ input->JoystickLeft,
+ input->JoystickRight,
+ input->ThumbLeft,
+ input->ThumbRight,
+ input->VibrateLeftFeedback,
+ input->VibrateRightFeedback,
+ input->HandPoseLeft,
+ input->HandPoseRight};
+ for (int i = 0; i < sizeof(actions_to_enumerate) / sizeof(XrAction); i++) {
+ XrBoundSourcesForActionEnumerateInfo e = {};
+ e.type = XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO;
+ e.next = NULL;
+ e.action = actions_to_enumerate[i];
+
+ // Get Count
+ uint32_t count_output = 0;
+ OXR(xrEnumerateBoundSourcesForAction(engine->Session, &e, 0, &count_output, NULL));
+
+ if (count_output < 32) {
+ OXR(xrEnumerateBoundSourcesForAction(engine->Session, &e, 32, &count_output,
+ action_paths_buffer));
+ for (uint32_t a = 0; a < count_output; ++a) {
+ XrInputSourceLocalizedNameGetInfo name_info = {};
+ name_info.type = XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO;
+ name_info.next = NULL;
+ name_info.sourcePath = action_paths_buffer[a];
+ name_info.whichComponents = XR_INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT |
+ XR_INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT |
+ XR_INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT;
+
+ uint32_t str_count = 0u;
+ OXR(xrGetInputSourceLocalizedName(engine->Session, &name_info, 0, &str_count, NULL));
+ if (str_count < 256) {
+ OXR(xrGetInputSourceLocalizedName(engine->Session, &name_info, 256, &str_count,
+ string_buffer));
+ char path_str[256];
+ uint32_t str_len = 0;
+ OXR(xrPathToString(engine->Instance, action_paths_buffer[a],
+ (uint32_t)sizeof(path_str), &str_len, path_str));
+ ALOGV("mapped %s -> %s", path_str, string_buffer);
+ }
+ }
+ }
+ }
+ input->Initialized = true;
+}
+
+uint32_t XrInputGetButtonState(struct XrInput* input, int controller) {
+ switch (controller) {
+ case 0:
+ return input->ButtonsLeft;
+ case 1:
+ return input->ButtonsRight;
+ default:
+ return 0;
+ }
+}
+
+XrVector2f XrInputGetJoystickState(struct XrInput* input, int controller) {
+ return input->JoystickState[controller].currentState;
+}
+
+XrPosef XrInputGetPose(struct XrInput* input, int controller) {
+ return input->ControllerPose[controller].pose;
+}
+
+void XrInputUpdate(struct XrEngine* engine, struct XrInput* input) {
+ // sync action data
+ XrActiveActionSet activeActionSet = {};
+ activeActionSet.actionSet = input->ActionSet;
+ activeActionSet.subactionPath = XR_NULL_PATH;
+
+ XrActionsSyncInfo sync_info = {};
+ sync_info.type = XR_TYPE_ACTIONS_SYNC_INFO;
+ sync_info.next = NULL;
+ sync_info.countActiveActionSets = 1;
+ sync_info.activeActionSets = &activeActionSet;
+ OXR(xrSyncActions(engine->Session, &sync_info));
+
+ // query input action states
+ XrActionStateGetInfo get_info = {};
+ get_info.type = XR_TYPE_ACTION_STATE_GET_INFO;
+ get_info.next = NULL;
+ get_info.subactionPath = XR_NULL_PATH;
+
+ XrSession session = engine->Session;
+ XrInputProcessHaptics(input, session);
+
+ if (input->LeftControllerSpace == XR_NULL_HANDLE) {
+ input->LeftControllerSpace = XrInputCreateActionSpace(session, input->HandPoseLeft, input->LeftHandPath);
+ }
+ if (input->RightControllerSpace == XR_NULL_HANDLE) {
+ input->RightControllerSpace = XrInputCreateActionSpace(session, input->HandPoseRight, input->RightHandPath);
+ }
+
+ // button mapping
+ input->ButtonsLeft = 0;
+ if (XrInputGetActionStateBoolean(session, input->ButtonMenu).currentState)
+ input->ButtonsLeft |= (int)Enter;
+ if (XrInputGetActionStateBoolean(session, input->ButtonX).currentState)
+ input->ButtonsLeft |= (int)X;
+ if (XrInputGetActionStateBoolean(session, input->ButtonY).currentState)
+ input->ButtonsLeft |= (int)Y;
+ if (XrInputGetActionStateBoolean(session, input->IndexLeft).currentState)
+ input->ButtonsLeft |= (int)Trigger;
+ if (XrInputGetActionStateFloat(session, input->GripLeft).currentState > 0.5f)
+ input->ButtonsLeft |= (int)Grip;
+ if (XrInputGetActionStateBoolean(session, input->ThumbLeft).currentState)
+ input->ButtonsLeft |= (int)LThumb;
+ input->ButtonsRight = 0;
+ if (XrInputGetActionStateBoolean(session, input->ButtonA).currentState)
+ input->ButtonsRight |= (int)A;
+ if (XrInputGetActionStateBoolean(session, input->ButtonB).currentState)
+ input->ButtonsRight |= (int)B;
+ if (XrInputGetActionStateBoolean(session, input->IndexRight).currentState)
+ input->ButtonsRight |= (int)Trigger;
+ if (XrInputGetActionStateFloat(session, input->GripRight).currentState > 0.5f)
+ input->ButtonsRight |= (int)Grip;
+ if (XrInputGetActionStateBoolean(session, input->ThumbRight).currentState)
+ input->ButtonsRight |= (int)RThumb;
+
+ // thumbstick
+ input->JoystickState[0] = XrInputGetActionStateVector2(session, input->JoystickLeft);
+ input->JoystickState[1] = XrInputGetActionStateVector2(session, input->JoystickRight);
+ if (input->JoystickState[0].currentState.x > 0.5)
+ input->ButtonsLeft |= (int)Right;
+ if (input->JoystickState[0].currentState.x < -0.5)
+ input->ButtonsLeft |= (int)Left;
+ if (input->JoystickState[0].currentState.y > 0.5)
+ input->ButtonsLeft |= (int)Up;
+ if (input->JoystickState[0].currentState.y < -0.5)
+ input->ButtonsLeft |= (int)Down;
+ if (input->JoystickState[1].currentState.x > 0.5)
+ input->ButtonsRight |= (int)Right;
+ if (input->JoystickState[1].currentState.x < -0.5)
+ input->ButtonsRight |= (int)Left;
+ if (input->JoystickState[1].currentState.y > 0.5)
+ input->ButtonsRight |= (int)Up;
+ if (input->JoystickState[1].currentState.y < -0.5)
+ input->ButtonsRight |= (int)Down;
+
+ // pose
+ for (int i = 0; i < 2; i++) {
+ memset(&input->ControllerPose[i], 0, sizeof(input->ControllerPose[i]));
+ input->ControllerPose[i].type = XR_TYPE_SPACE_LOCATION;
+ XrSpace aim_space[] = {input->LeftControllerSpace, input->RightControllerSpace};
+ xrLocateSpace(aim_space[i], engine->CurrentSpace,
+ (XrTime)(engine->PredictedDisplayTime), &input->ControllerPose[i]);
+ }
+}
+
+void XrInputVibrate(struct XrInput* input, int duration, int chan, float intensity) {
+ for (int i = 0; i < 2; ++i) {
+ int channel = i & chan;
+ if (channel) {
+ if (input->VibrationChannelDuration[channel] > 0.0f)
+ return;
+
+ if (input->VibrationChannelDuration[channel] == -1.0f && duration != 0.0f)
+ return;
+
+ input->VibrationChannelDuration[channel] = (float)duration;
+ input->VibrationChannelIntensity[channel] = intensity;
+ }
+ }
+}
+
+XrAction XrInputCreateAction(XrActionSet output_set, XrActionType type, const char* name,
+ const char* desc, int count_subaction_path, XrPath* subaction_path) {
+ XrActionCreateInfo action_info = {};
+ action_info.type = XR_TYPE_ACTION_CREATE_INFO;
+ action_info.next = NULL;
+ action_info.actionType = type;
+ if (count_subaction_path > 0) {
+ action_info.countSubactionPaths = count_subaction_path;
+ action_info.subactionPaths = subaction_path;
+ }
+ strcpy(action_info.actionName, name);
+ strcpy(action_info.localizedActionName, desc ? desc : name);
+ XrAction output = XR_NULL_HANDLE;
+ OXR(xrCreateAction(output_set, &action_info, &output));
+ return output;
+}
+
+XrActionSet XrInputCreateActionSet(XrInstance instance, const char* name, const char* desc) {
+ XrActionSetCreateInfo action_set_info = {};
+ action_set_info.type = XR_TYPE_ACTION_SET_CREATE_INFO;
+ action_set_info.next = NULL;
+ action_set_info.priority = 1;
+ strcpy(action_set_info.actionSetName, name);
+ strcpy(action_set_info.localizedActionSetName, desc);
+ XrActionSet output = XR_NULL_HANDLE;
+ OXR(xrCreateActionSet(instance, &action_set_info, &output));
+ return output;
+}
+
+XrSpace XrInputCreateActionSpace(XrSession session, XrAction action, XrPath subaction_path) {
+ XrActionSpaceCreateInfo action_space_info = {};
+ action_space_info.type = XR_TYPE_ACTION_SPACE_CREATE_INFO;
+ action_space_info.action = action;
+ action_space_info.poseInActionSpace.orientation.w = 1.0f;
+ action_space_info.subactionPath = subaction_path;
+ XrSpace output = XR_NULL_HANDLE;
+ OXR(xrCreateActionSpace(session, &action_space_info, &output));
+ return output;
+}
+
+XrActionStateBoolean XrInputGetActionStateBoolean(XrSession session, XrAction action) {
+ XrActionStateGetInfo get_info = {};
+ get_info.type = XR_TYPE_ACTION_STATE_GET_INFO;
+ get_info.action = action;
+
+ XrActionStateBoolean state = {};
+ state.type = XR_TYPE_ACTION_STATE_BOOLEAN;
+
+ OXR(xrGetActionStateBoolean(session, &get_info, &state));
+ return state;
+}
+
+XrActionStateFloat XrInputGetActionStateFloat(XrSession session, XrAction action) {
+ XrActionStateGetInfo get_info = {};
+ get_info.type = XR_TYPE_ACTION_STATE_GET_INFO;
+ get_info.action = action;
+
+ XrActionStateFloat state = {};
+ state.type = XR_TYPE_ACTION_STATE_FLOAT;
+
+ OXR(xrGetActionStateFloat(session, &get_info, &state));
+ return state;
+}
+
+XrActionStateVector2f XrInputGetActionStateVector2(XrSession session, XrAction action) {
+ XrActionStateGetInfo get_info = {};
+ get_info.type = XR_TYPE_ACTION_STATE_GET_INFO;
+ get_info.action = action;
+
+ XrActionStateVector2f state = {};
+ state.type = XR_TYPE_ACTION_STATE_VECTOR2F;
+
+ OXR(xrGetActionStateVector2f(session, &get_info, &state));
+ return state;
+}
+
+XrActionSuggestedBinding XrInputGetBinding(XrInstance instance, XrAction action, const char* name) {
+ XrPath bindingPath;
+ OXR(xrStringToPath(instance, name, &bindingPath));
+
+ XrActionSuggestedBinding output;
+ output.action = action;
+ output.binding = bindingPath;
+ return output;
+}
+
+int XrInputGetMilliseconds(struct XrInput* input) {
+ struct timeval tp;
+
+ gettimeofday(&tp, NULL);
+
+ if (!input->SysTimeBase) {
+ input->SysTimeBase = tp.tv_sec;
+ return tp.tv_usec / 1000;
+ }
+
+ return (tp.tv_sec - input->SysTimeBase) * 1000 + tp.tv_usec / 1000;
+}
+
+void XrInputProcessHaptics(struct XrInput* input, XrSession session) {
+ static float last_frame_timestamp = 0.0f;
+ float timestamp = (float)(XrInputGetMilliseconds(input));
+ float frametime = timestamp - last_frame_timestamp;
+ last_frame_timestamp = timestamp;
+
+ for (int i = 0; i < 2; ++i) {
+ if (input->VibrationChannelDuration[i] > 0.0f || input->VibrationChannelDuration[i] == -1.0f) {
+ // fire haptics using output action
+ XrHapticVibration vibration = {};
+ vibration.type = XR_TYPE_HAPTIC_VIBRATION;
+ vibration.next = NULL;
+ vibration.amplitude = input->VibrationChannelIntensity[i];
+ vibration.duration = ToXrTime(input->VibrationChannelDuration[i]);
+ vibration.frequency = 3000;
+ XrHapticActionInfo haptic_info = {};
+ haptic_info.type = XR_TYPE_HAPTIC_ACTION_INFO;
+ haptic_info.next = NULL;
+ haptic_info.action = i == 0 ? input->VibrateLeftFeedback : input->VibrateRightFeedback;
+ OXR(xrApplyHapticFeedback(session, &haptic_info, (const XrHapticBaseHeader*)&vibration));
+
+ if (input->VibrationChannelDuration[i] != -1.0f) {
+ input->VibrationChannelDuration[i] -= frametime;
+
+ if (input->VibrationChannelDuration[i] < 0.0f) {
+ input->VibrationChannelDuration[i] = 0.0f;
+ input->VibrationChannelIntensity[i] = 0.0f;
+ }
+ }
+ } else {
+ // Stop haptics
+ XrHapticActionInfo haptic_info = {};
+ haptic_info.type = XR_TYPE_HAPTIC_ACTION_INFO;
+ haptic_info.next = NULL;
+ haptic_info.action = i == 0 ? input->VibrateLeftFeedback : input->VibrateRightFeedback;
+ OXR(xrStopHapticFeedback(session, &haptic_info));
+ }
+ }
+}
diff --git a/app/src/main/cpp/xrio/input.h b/app/src/main/cpp/xrio/input.h
new file mode 100644
index 000000000..090a14dfa
--- /dev/null
+++ b/app/src/main/cpp/xrio/input.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "engine.h"
+
+enum XrButton {
+ A = 0x00000001, // Set for trigger pulled on the Gear VR and Go Controllers
+ B = 0x00000002,
+ RThumb = 0x00000004,
+
+ X = 0x00000100,
+ Y = 0x00000200,
+ LThumb = 0x00000400,
+
+ Up = 0x00010000,
+ Down = 0x00020000,
+ Left = 0x00040000,
+ Right = 0x00080000,
+ Enter = 0x00100000, //< Set for touchpad click on the Go Controller, menu
+ // button on Left Quest Controller
+ Back = 0x00200000, //< Back button on the Go Controller (only set when
+ // a short press comes up)
+ Grip = 0x04000000, //< grip trigger engaged
+ Trigger = 0x20000000 //< Index Trigger engaged
+};
+
+struct XrInput {
+
+ bool Initialized;
+
+ // OpenXR controller mapping
+ XrActionSet ActionSet;
+ XrPath LeftHandPath;
+ XrPath RightHandPath;
+ XrAction HandPoseLeft;
+ XrAction HandPoseRight;
+ XrAction IndexLeft;
+ XrAction IndexRight;
+ XrAction ButtonMenu;
+ XrAction ButtonA;
+ XrAction ButtonB;
+ XrAction ButtonX;
+ XrAction ButtonY;
+ XrAction GripLeft;
+ XrAction GripRight;
+ XrAction JoystickLeft;
+ XrAction JoystickRight;
+ XrAction ThumbLeft;
+ XrAction ThumbRight;
+ XrAction VibrateLeftFeedback;
+ XrAction VibrateRightFeedback;
+ XrSpace LeftControllerSpace;
+ XrSpace RightControllerSpace;
+
+ // Controller state
+ uint32_t ButtonsLeft;
+ uint32_t ButtonsRight;
+ XrSpaceLocation ControllerPose[2];
+ XrActionStateVector2f JoystickState[2];
+ float VibrationChannelDuration[2];
+ float VibrationChannelIntensity[2];
+
+ // Timer
+ unsigned long SysTimeBase;
+};
+
+void XrInputInit(struct XrEngine* engine, struct XrInput* input);
+uint32_t XrInputGetButtonState(struct XrInput* input, int controller);
+XrVector2f XrInputGetJoystickState(struct XrInput* input, int controller);
+XrPosef XrInputGetPose(struct XrInput* input, int controller);
+void XrInputUpdate(struct XrEngine* engine, struct XrInput* input);
+void XrInputVibrate(struct XrInput* input, int duration, int chan, float intensity);
+
+XrAction XrInputCreateAction(XrActionSet output_set, XrActionType type, const char* name,
+ const char* desc, int count_subaction_path, XrPath* subaction_path);
+XrActionSet XrInputCreateActionSet(XrInstance instance, const char* name, const char* desc);
+XrSpace XrInputCreateActionSpace(XrSession session, XrAction action, XrPath subaction_path);
+XrActionStateBoolean XrInputGetActionStateBoolean(XrSession session, XrAction action);
+XrActionStateFloat XrInputGetActionStateFloat(XrSession session, XrAction action);
+XrActionStateVector2f XrInputGetActionStateVector2(XrSession session, XrAction action);
+XrActionSuggestedBinding XrInputGetBinding(XrInstance instance, XrAction action, const char* name);
+int XrInputGetMilliseconds(struct XrInput* input);
+void XrInputProcessHaptics(struct XrInput* input, XrSession session);
diff --git a/app/src/main/cpp/xrio/math.c b/app/src/main/cpp/xrio/math.c
new file mode 100644
index 000000000..b179cc33a
--- /dev/null
+++ b/app/src/main/cpp/xrio/math.c
@@ -0,0 +1,194 @@
+#include "math.h"
+
+#include
+#include
+
+double FromXrTime(const XrTime time) {
+ return (time * 1e-9);
+}
+
+XrTime ToXrTime(const double time_in_seconds) {
+ return (XrTime)(time_in_seconds * 1e9);
+}
+
+float ToDegrees(float rad) {
+ return (float)(rad / M_PI * 180.0f);
+}
+
+float ToRadians(float deg) {
+ return (float)(deg * M_PI / 180.0f);
+}
+
+/*
+================================================================================
+
+XrQuaternionf
+
+================================================================================
+*/
+
+XrQuaternionf XrQuaternionfCreateFromVectorAngle(const XrVector3f axis, const float angle) {
+ XrQuaternionf r;
+ if (XrVector3fLengthSquared(axis) == 0.0f) {
+ r.x = 0;
+ r.y = 0;
+ r.z = 0;
+ r.w = 1;
+ return r;
+ }
+
+ XrVector3f unitAxis = XrVector3fNormalized(axis);
+ float sinHalfAngle = sinf(angle * 0.5f);
+
+ r.w = cosf(angle * 0.5f);
+ r.x = unitAxis.x * sinHalfAngle;
+ r.y = unitAxis.y * sinHalfAngle;
+ r.z = unitAxis.z * sinHalfAngle;
+ return r;
+}
+
+XrQuaternionf XrQuaternionfMultiply(const XrQuaternionf a, const XrQuaternionf b) {
+ XrQuaternionf c;
+ c.x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y;
+ c.y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x;
+ c.z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w;
+ c.w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z;
+ return c;
+}
+
+XrVector3f XrQuaternionfEulerAngles(const XrQuaternionf q) {
+ float M[16];
+ XrQuaternionfToMatrix4f(&q, M);
+
+ XrVector4f v1 = {0, 0, -1, 0};
+ XrVector4f v2 = {1, 0, 0, 0};
+ XrVector4f v3 = {0, 1, 0, 0};
+
+ XrVector4f forwardInVRSpace = XrVector4fMultiplyMatrix4f(M, &v1);
+ XrVector4f rightInVRSpace = XrVector4fMultiplyMatrix4f(M, &v2);
+ XrVector4f upInVRSpace = XrVector4fMultiplyMatrix4f(M, &v3);
+
+ XrVector3f forward = {-forwardInVRSpace.z, -forwardInVRSpace.x, forwardInVRSpace.y};
+ XrVector3f right = {-rightInVRSpace.z, -rightInVRSpace.x, rightInVRSpace.y};
+ XrVector3f up = {-upInVRSpace.z, -upInVRSpace.x, upInVRSpace.y};
+
+ XrVector3f forwardNormal = XrVector3fNormalized(forward);
+ XrVector3f rightNormal = XrVector3fNormalized(right);
+ XrVector3f upNormal = XrVector3fNormalized(up);
+
+ return XrVector3fGetAnglesFromVectors(forwardNormal, rightNormal, upNormal);
+}
+
+void XrQuaternionfToMatrix4f(const XrQuaternionf* q, float* m) {
+ const float ww = q->w * q->w;
+ const float xx = q->x * q->x;
+ const float yy = q->y * q->y;
+ const float zz = q->z * q->z;
+
+ float M[4][4];
+ M[0][0] = ww + xx - yy - zz;
+ M[0][1] = 2 * (q->x * q->y - q->w * q->z);
+ M[0][2] = 2 * (q->x * q->z + q->w * q->y);
+ M[0][3] = 0;
+
+ M[1][0] = 2 * (q->x * q->y + q->w * q->z);
+ M[1][1] = ww - xx + yy - zz;
+ M[1][2] = 2 * (q->y * q->z - q->w * q->x);
+ M[1][3] = 0;
+
+ M[2][0] = 2 * (q->x * q->z - q->w * q->y);
+ M[2][1] = 2 * (q->y * q->z + q->w * q->x);
+ M[2][2] = ww - xx - yy + zz;
+ M[2][3] = 0;
+
+ M[3][0] = 0;
+ M[3][1] = 0;
+ M[3][2] = 0;
+ M[3][3] = 1;
+
+ memcpy(m, &M, sizeof(float) * 16);
+}
+
+/*
+================================================================================
+
+XrVector3f, XrVector4f
+
+================================================================================
+*/
+
+
+float XrVector3fDistance(const XrVector3f a, const XrVector3f b) {
+ XrVector3f diff;
+ diff.x = a.x - b.x;
+ diff.y = a.y - b.y;
+ diff.z = a.z - b.z;
+ return sqrt(XrVector3fLengthSquared(diff));
+}
+
+float XrVector3fLengthSquared(const XrVector3f v) {
+ return v.x * v.x + v.y * v.y + v.z * v.z;
+}
+
+XrVector3f XrVector3fGetAnglesFromVectors(XrVector3f forward, XrVector3f right, XrVector3f up) {
+ float sp = -forward.z;
+
+ float cp_x_cy = forward.x;
+ float cp_x_sy = forward.y;
+ float cp_x_sr = -right.z;
+ float cp_x_cr = up.z;
+
+ float yaw = atan2(cp_x_sy, cp_x_cy);
+ float roll = atan2(cp_x_sr, cp_x_cr);
+
+ float cy = cos(yaw);
+ float sy = sin(yaw);
+ float cr = cos(roll);
+ float sr = sin(roll);
+
+ float cp;
+ if (fabs(cy) > EPSILON) {
+ cp = cp_x_cy / cy;
+ } else if (fabs(sy) > EPSILON) {
+ cp = cp_x_sy / sy;
+ } else if (fabs(sr) > EPSILON) {
+ cp = cp_x_sr / sr;
+ } else if (fabs(cr) > EPSILON) {
+ cp = cp_x_cr / cr;
+ } else {
+ cp = cos(asin(sp));
+ }
+
+ float pitch = atan2(sp, cp);
+
+ XrVector3f angles;
+ angles.x = ToDegrees(pitch);
+ angles.y = ToDegrees(yaw);
+ angles.z = ToDegrees(roll);
+ return angles;
+}
+
+XrVector3f XrVector3fNormalized(const XrVector3f v) {
+ float rcpLen = 1.0f / sqrtf(XrVector3fLengthSquared(v));
+ return XrVector3fScalarMultiply(v, rcpLen);
+}
+
+XrVector3f XrVector3fScalarMultiply(const XrVector3f v, float scale) {
+ XrVector3f u;
+ u.x = v.x * scale;
+ u.y = v.y * scale;
+ u.z = v.z * scale;
+ return u;
+}
+
+XrVector4f XrVector4fMultiplyMatrix4f(const float* m, const XrVector4f* v) {
+ float M[4][4];
+ memcpy(&M, m, sizeof(float) * 16);
+
+ XrVector4f out;
+ out.x = M[0][0] * v->x + M[0][1] * v->y + M[0][2] * v->z + M[0][3] * v->w;
+ out.y = M[1][0] * v->x + M[1][1] * v->y + M[1][2] * v->z + M[1][3] * v->w;
+ out.z = M[2][0] * v->x + M[2][1] * v->y + M[2][2] * v->z + M[2][3] * v->w;
+ out.w = M[3][0] * v->x + M[3][1] * v->y + M[3][2] * v->z + M[3][3] * v->w;
+ return out;
+}
diff --git a/app/src/main/cpp/xrio/math.h b/app/src/main/cpp/xrio/math.h
new file mode 100644
index 000000000..dd50cf1de
--- /dev/null
+++ b/app/src/main/cpp/xrio/math.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+
+#ifndef EPSILON
+#define EPSILON 0.001f
+#endif
+
+double FromXrTime(const XrTime time);
+XrTime ToXrTime(const double time_in_seconds);
+float ToDegrees(float rad);
+float ToRadians(float deg);
+
+// XrQuaternionf
+XrQuaternionf XrQuaternionfCreateFromVectorAngle(const XrVector3f axis, const float angle);
+XrQuaternionf XrQuaternionfMultiply(const XrQuaternionf a, const XrQuaternionf b);
+XrVector3f XrQuaternionfEulerAngles(const XrQuaternionf q);
+void XrQuaternionfToMatrix4f(const XrQuaternionf* q, float* m);
+
+// XrVector3f, XrVector4f
+float XrVector3fDistance(const XrVector3f a, const XrVector3f b);
+float XrVector3fLengthSquared(const XrVector3f v);
+XrVector3f XrVector3fGetAnglesFromVectors(XrVector3f forward, XrVector3f right, XrVector3f up);
+XrVector3f XrVector3fNormalized(const XrVector3f v);
+XrVector3f XrVector3fScalarMultiply(const XrVector3f v, float scale);
+XrVector4f XrVector4fMultiplyMatrix4f(const float* m, const XrVector4f* v);
diff --git a/app/src/main/cpp/xrio/renderer.c b/app/src/main/cpp/xrio/renderer.c
new file mode 100644
index 000000000..4aae2ad1d
--- /dev/null
+++ b/app/src/main/cpp/xrio/renderer.c
@@ -0,0 +1,565 @@
+#include
+#include
+#include
+#include
+#include "engine.h"
+#include "math.h"
+#include "renderer.h"
+
+#define DECL_PFN(pfn) PFN_##pfn pfn = NULL
+#define INIT_PFN(pfn) OXR(xrGetInstanceProcAddr(engine->Instance, #pfn, (PFN_xrVoidFunction*)(&pfn)))
+
+DECL_PFN(xrCreatePassthroughFB);
+DECL_PFN(xrDestroyPassthroughFB);
+DECL_PFN(xrPassthroughStartFB);
+DECL_PFN(xrPassthroughPauseFB);
+DECL_PFN(xrCreatePassthroughLayerFB);
+DECL_PFN(xrDestroyPassthroughLayerFB);
+DECL_PFN(xrPassthroughLayerPauseFB);
+DECL_PFN(xrPassthroughLayerResumeFB);
+
+void XrRendererInit(struct XrEngine* engine, struct XrRenderer* renderer) {
+ if (renderer->Initialized) {
+ XrRendererDestroy(engine, renderer);
+ }
+ memset(renderer, 0, sizeof(renderer));
+
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH]) {
+ INIT_PFN(xrCreatePassthroughFB);
+ INIT_PFN(xrDestroyPassthroughFB);
+ INIT_PFN(xrPassthroughStartFB);
+ INIT_PFN(xrPassthroughPauseFB);
+ INIT_PFN(xrCreatePassthroughLayerFB);
+ INIT_PFN(xrDestroyPassthroughLayerFB);
+ INIT_PFN(xrPassthroughLayerPauseFB);
+ INIT_PFN(xrPassthroughLayerResumeFB);
+ }
+
+ int eyeW, eyeH;
+ XrRendererGetResolution(engine, renderer, &eyeW, &eyeH);
+ renderer->ConfigInt[CONFIG_VIEWPORT_WIDTH] = eyeW;
+ renderer->ConfigInt[CONFIG_VIEWPORT_HEIGHT] = eyeH;
+
+ // Get the viewport configuration info for the chosen viewport configuration type.
+ renderer->ViewportConfig.type = XR_TYPE_VIEW_CONFIGURATION_PROPERTIES;
+ OXR(xrGetViewConfigurationProperties(engine->Instance, engine->SystemId,
+ XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
+ &renderer->ViewportConfig));
+
+ uint32_t num_spaces = 0;
+ OXR(xrEnumerateReferenceSpaces(engine->Session, 0, &num_spaces, NULL));
+ XrReferenceSpaceType* spaces = (XrReferenceSpaceType*)malloc(num_spaces * sizeof(XrReferenceSpaceType));
+ OXR(xrEnumerateReferenceSpaces(engine->Session, num_spaces, &num_spaces, spaces));
+
+ for (uint32_t i = 0; i < num_spaces; i++) {
+ if (spaces[i] == XR_REFERENCE_SPACE_TYPE_STAGE) {
+ renderer->StageSupported = true;
+ break;
+ }
+ }
+
+ free(spaces);
+
+ if (engine->CurrentSpace == XR_NULL_HANDLE) {
+ XrRendererRecenter(engine, renderer);
+ }
+
+ renderer->Projections = (XrView*)(malloc(XrMaxNumEyes * sizeof(XrView)));
+ for (int eye = 0; eye < XrMaxNumEyes; eye++) {
+ memset(&renderer->Projections[eye], 0, sizeof(XrView));
+ renderer->Projections[eye].type = XR_TYPE_VIEW;
+ }
+
+ // Create framebuffers.
+ int width = renderer->ViewConfig[0].recommendedImageRectWidth;
+ int height = renderer->ViewConfig[0].recommendedImageRectHeight;
+ for (int i = 0; i < XrMaxNumEyes; i++) {
+ XrFramebufferCreate(&renderer->Framebuffer[i], engine->Session, width, height);
+ }
+
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH]) {
+ XrPassthroughCreateInfoFB ptci = {XR_TYPE_PASSTHROUGH_CREATE_INFO_FB};
+ XrResult result;
+ OXR(result = xrCreatePassthroughFB(engine->Session, &ptci, &renderer->Passthrough));
+
+ if (XR_SUCCEEDED(result)) {
+ XrPassthroughLayerCreateInfoFB plci = {XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB};
+ plci.passthrough = renderer->Passthrough;
+ plci.purpose = XR_PASSTHROUGH_LAYER_PURPOSE_RECONSTRUCTION_FB;
+ OXR(xrCreatePassthroughLayerFB(engine->Session, &plci, &renderer->PassthroughLayer));
+ }
+
+ OXR(xrPassthroughStartFB(renderer->Passthrough));
+ OXR(xrPassthroughLayerResumeFB(renderer->PassthroughLayer));
+ }
+ renderer->Initialized = true;
+}
+
+void XrRendererDestroy(struct XrEngine* engine, struct XrRenderer* renderer) {
+ if (!renderer->Initialized) {
+ return;
+ }
+
+ if (engine->PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH]) {
+ if (renderer->PassthroughRunning) {
+ OXR(xrPassthroughLayerPauseFB(renderer->PassthroughLayer));
+ }
+ OXR(xrPassthroughPauseFB(renderer->Passthrough));
+ OXR(xrDestroyPassthroughFB(renderer->Passthrough));
+ renderer->Passthrough = XR_NULL_HANDLE;
+ }
+
+ for (int i = 0; i < XrMaxNumEyes; i++) {
+ XrFramebufferDestroy(&renderer->Framebuffer[i]);
+ }
+ free(renderer->Projections);
+ renderer->Initialized = false;
+}
+
+
+void XrRendererGetResolution(struct XrEngine* engine, struct XrRenderer* renderer, int* pWidth, int* pHeight) {
+ static int width = 0;
+ static int height = 0;
+
+ if (engine) {
+ // Enumerate the viewport configurations.
+ uint32_t viewport_config_count = 0;
+ OXR(xrEnumerateViewConfigurations(engine->Instance, engine->SystemId, 0,
+ &viewport_config_count, NULL));
+
+ XrViewConfigurationType* viewport_configs =
+ (XrViewConfigurationType*)malloc(viewport_config_count * sizeof(XrViewConfigurationType));
+
+ OXR(xrEnumerateViewConfigurations(engine->Instance, engine->SystemId,
+ viewport_config_count, &viewport_config_count,
+ viewport_configs));
+
+ for (uint32_t i = 0; i < viewport_config_count; i++) {
+ const XrViewConfigurationType viewport_config_type = viewport_configs[i];
+
+ ALOGV("Viewport configuration type %d", (int)viewport_config_type);
+
+ XrViewConfigurationProperties viewport_config;
+ viewport_config.type = XR_TYPE_VIEW_CONFIGURATION_PROPERTIES;
+ OXR(xrGetViewConfigurationProperties(engine->Instance, engine->SystemId,
+ viewport_config_type, &viewport_config));
+
+ uint32_t view_count;
+ OXR(xrEnumerateViewConfigurationViews(engine->Instance, engine->SystemId,
+ viewport_config_type, 0, &view_count, NULL));
+
+ if (view_count > 0) {
+ XrViewConfigurationView* elements =
+ (XrViewConfigurationView*)malloc(view_count * sizeof(XrViewConfigurationView));
+
+ for (uint32_t e = 0; e < view_count; e++) {
+ elements[e].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
+ elements[e].next = NULL;
+ }
+
+ OXR(xrEnumerateViewConfigurationViews(engine->Instance, engine->SystemId,
+ viewport_config_type, view_count, &view_count,
+ elements));
+
+ // Cache the view config properties for the selected config type.
+ if (viewport_config_type == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) {
+ assert(view_count == XrMaxNumEyes);
+ for (uint32_t e = 0; e < view_count; e++) {
+ renderer->ViewConfig[e] = elements[e];
+ }
+ }
+
+ free(elements);
+ } else {
+ ALOGE("Empty viewport configuration");
+ }
+ }
+
+ free(viewport_configs);
+
+ *pWidth = width = renderer->ViewConfig[0].recommendedImageRectWidth;
+ *pHeight = height = renderer->ViewConfig[0].recommendedImageRectHeight;
+ } else {
+ // use cached values
+ *pWidth = width;
+ *pHeight = height;
+ }
+}
+
+bool XrRendererInitFrame(struct XrEngine* engine, struct XrRenderer* renderer) {
+ if (!renderer->Initialized) {
+ return false;
+ }
+ XrRendererHandleXrEvents(engine, renderer);
+ if (!renderer->SessionActive) {
+ return false;
+ }
+
+ XrRendererUpdateStageBounds(engine);
+
+ // Update passthrough
+ if (renderer->PassthroughRunning != renderer->ConfigInt[CONFIG_PASSTHROUGH]) {
+ if (renderer->ConfigInt[CONFIG_PASSTHROUGH]) {
+ OXR(xrPassthroughLayerResumeFB(renderer->PassthroughLayer));
+ } else {
+ OXR(xrPassthroughLayerPauseFB(renderer->PassthroughLayer));
+ }
+ renderer->PassthroughRunning = renderer->ConfigInt[CONFIG_PASSTHROUGH];
+ }
+
+ XrEngineWaitForFrame(engine);
+
+ XrViewLocateInfo projection_info = {};
+ projection_info.type = XR_TYPE_VIEW_LOCATE_INFO;
+ projection_info.next = NULL;
+ projection_info.viewConfigurationType = renderer->ViewportConfig.viewConfigurationType;
+ projection_info.displayTime = engine->PredictedDisplayTime;
+ projection_info.space = engine->CurrentSpace;
+
+ XrViewState view_state = {XR_TYPE_VIEW_STATE, NULL};
+
+ uint32_t projection_capacity = XrMaxNumEyes;
+ uint32_t projection_count = projection_capacity;
+
+ OXR(xrLocateViews(engine->Session, &projection_info, &view_state, projection_capacity,
+ &projection_count, renderer->Projections));
+
+ // Get the HMD pose, predicted for the middle of the time period during which
+ // the new eye images will be displayed. The number of frames predicted ahead
+ // depends on the pipeline depth of the engine and the synthesis rate.
+ // The better the prediction, the less black will be pulled in at the edges.
+ XrFrameBeginInfo begin_frame_info = {};
+ begin_frame_info.type = XR_TYPE_FRAME_BEGIN_INFO;
+ begin_frame_info.next = NULL;
+ OXR(xrBeginFrame(engine->Session, &begin_frame_info));
+
+ renderer->Fov.angleLeft = 0;
+ renderer->Fov.angleRight = 0;
+ renderer->Fov.angleUp = 0;
+ renderer->Fov.angleDown = 0;
+ for (int eye = 0; eye < XrMaxNumEyes; eye++) {
+ renderer->Fov.angleLeft += renderer->Projections[eye].fov.angleLeft / 2.0f;
+ renderer->Fov.angleRight += renderer->Projections[eye].fov.angleRight / 2.0f;
+ renderer->Fov.angleUp += renderer->Projections[eye].fov.angleUp / 2.0f;
+ renderer->Fov.angleDown += renderer->Projections[eye].fov.angleDown / 2.0f;
+ renderer->InvertedViewPose[eye] = renderer->Projections[eye].pose;
+ }
+
+ renderer->HmdOrientation = XrQuaternionfEulerAngles(renderer->InvertedViewPose[0].orientation);
+ renderer->LayerCount = 0;
+ memset(renderer->Layers, 0, sizeof(XrCompositorLayer) * XrMaxLayerCount);
+ return true;
+}
+
+void XrRendererBeginFrame(struct XrRenderer* renderer, int fbo_index) {
+ renderer->ConfigInt[CONFIG_CURRENT_FBO] = fbo_index;
+ XrFramebufferAcquire(&renderer->Framebuffer[fbo_index]);
+}
+
+void XrRendererEndFrame(struct XrRenderer* renderer) {
+ int fbo_index = renderer->ConfigInt[CONFIG_CURRENT_FBO];
+ XrFramebufferRelease(&renderer->Framebuffer[fbo_index]);
+}
+
+void XrRendererFinishFrame(struct XrEngine* engine, struct XrRenderer* renderer) {
+ int x = 0;
+ int y = 0;
+ int w = renderer->Framebuffer[0].Width;
+ int h = renderer->Framebuffer[0].Height;
+ if (renderer->ConfigInt[CONFIG_SBS]) {
+ w /= 2;
+ }
+
+ int mode = renderer->ConfigInt[CONFIG_MODE];
+ XrCompositionLayerProjectionView projection_layer_elements[2] = {};
+ if ((mode == RENDER_MODE_MONO_6DOF) || (mode == RENDER_MODE_STEREO_6DOF)) {
+ renderer->ConfigFloat[CONFIG_MENU_YAW] = renderer->HmdOrientation.y;
+
+ for (int eye = 0; eye < XrMaxNumEyes; eye++) {
+ struct XrFramebuffer* framebuffer = &renderer->Framebuffer[0];
+ XrPosef pose = renderer->InvertedViewPose[0];
+ if (renderer->ConfigInt[CONFIG_SBS] && (eye == 1)) {
+ x += w;
+ } else if (mode != RENDER_MODE_MONO_6DOF) {
+ framebuffer = &renderer->Framebuffer[eye];
+ pose = renderer->InvertedViewPose[eye];
+ }
+
+ XrVector3f pitch_axis = {1, 0, 0};
+ XrVector3f yaw_axis = {0, 1, 0};
+ XrVector3f rotation = XrQuaternionfEulerAngles(pose.orientation);
+ XrQuaternionf pitch = XrQuaternionfCreateFromVectorAngle(pitch_axis, -ToRadians(rotation.x));
+ XrQuaternionf yaw = XrQuaternionfCreateFromVectorAngle(yaw_axis, ToRadians(rotation.y));
+ pose.orientation = XrQuaternionfMultiply(pitch, yaw);
+
+ memset(&projection_layer_elements[eye], 0, sizeof(XrCompositionLayerProjectionView));
+ projection_layer_elements[eye].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
+ projection_layer_elements[eye].pose = pose;
+ projection_layer_elements[eye].fov = renderer->Fov;
+
+ memset(&projection_layer_elements[eye].subImage, 0, sizeof(XrSwapchainSubImage));
+ projection_layer_elements[eye].subImage.swapchain = framebuffer->Handle;
+ projection_layer_elements[eye].subImage.imageRect.offset.x = x;
+ projection_layer_elements[eye].subImage.imageRect.offset.y = y;
+ projection_layer_elements[eye].subImage.imageRect.extent.width = w;
+ projection_layer_elements[eye].subImage.imageRect.extent.height = h;
+ projection_layer_elements[eye].subImage.imageArrayIndex = 0;
+ }
+
+ XrCompositionLayerProjection projection_layer = {};
+ projection_layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;
+ projection_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
+ projection_layer.layerFlags |= XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT;
+ projection_layer.space = engine->CurrentSpace;
+ projection_layer.viewCount = XrMaxNumEyes;
+ projection_layer.views = projection_layer_elements;
+
+ renderer->Layers[renderer->LayerCount++].projection = projection_layer;
+ } else if ((mode == RENDER_MODE_MONO_SCREEN) || (mode == RENDER_MODE_STEREO_SCREEN)) {
+ // Flat screen pose
+ float distance = renderer->ConfigFloat[CONFIG_CANVAS_DISTANCE];
+ float menu_pitch = ToRadians(renderer->ConfigFloat[CONFIG_MENU_PITCH]);
+ float menu_yaw = ToRadians(renderer->ConfigFloat[CONFIG_MENU_YAW]);
+ XrVector3f pos = {renderer->InvertedViewPose[0].position.x - sinf(menu_yaw) * cosf(menu_pitch) * distance,
+ renderer->InvertedViewPose[0].position.y - sinf(menu_pitch) * distance,
+ renderer->InvertedViewPose[0].position.z - cosf(menu_yaw) * cosf(menu_pitch) * distance};
+ XrVector3f pitch_axis = {1, 0, 0};
+ XrVector3f yaw_axis = {0, 1, 0};
+ XrQuaternionf pitch = XrQuaternionfCreateFromVectorAngle(pitch_axis, -menu_pitch);
+ XrQuaternionf yaw = XrQuaternionfCreateFromVectorAngle(yaw_axis, menu_yaw);
+
+ // Setup quad layer
+ struct XrFramebuffer* framebuffer = &renderer->Framebuffer[0];
+ XrCompositionLayerQuad quad_layer = {};
+ quad_layer.type = XR_TYPE_COMPOSITION_LAYER_QUAD;
+ quad_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
+ quad_layer.space = engine->CurrentSpace;
+ memset(&quad_layer.subImage, 0, sizeof(XrSwapchainSubImage));
+ quad_layer.subImage.imageRect.offset.x = x;
+ quad_layer.subImage.imageRect.offset.y = y;
+ quad_layer.subImage.imageRect.extent.width = w;
+ quad_layer.subImage.imageRect.extent.height = h;
+ quad_layer.subImage.swapchain = framebuffer->Handle;
+ quad_layer.subImage.imageArrayIndex = 0;
+ quad_layer.pose.orientation = XrQuaternionfMultiply(pitch, yaw);
+ quad_layer.pose.position = pos;
+ quad_layer.size.width = 4;
+ quad_layer.size.height = 4;
+
+ // Build the cylinder layer
+ if (renderer->ConfigInt[CONFIG_SBS]) {
+ quad_layer.eyeVisibility = XR_EYE_VISIBILITY_LEFT;
+ renderer->Layers[renderer->LayerCount++].quad = quad_layer;
+ quad_layer.eyeVisibility = XR_EYE_VISIBILITY_RIGHT;
+ quad_layer.subImage.imageRect.offset.x = w;
+ renderer->Layers[renderer->LayerCount++].quad = quad_layer;
+ } else if (mode == RENDER_MODE_MONO_SCREEN) {
+ quad_layer.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
+ renderer->Layers[renderer->LayerCount++].quad = quad_layer;
+ } else {
+ quad_layer.eyeVisibility = XR_EYE_VISIBILITY_LEFT;
+ renderer->Layers[renderer->LayerCount++].quad = quad_layer;
+ quad_layer.eyeVisibility = XR_EYE_VISIBILITY_RIGHT;
+ quad_layer.subImage.swapchain = renderer->Framebuffer[1].Handle;
+ renderer->Layers[renderer->LayerCount++].quad = quad_layer;
+ }
+ } else {
+ assert(false);
+ }
+
+ // Compose the layers for this frame.
+ const XrCompositionLayerBaseHeader* layers[XrMaxLayerCount] = {};
+ for (int i = 0; i < renderer->LayerCount; i++) {
+ layers[i] = (const XrCompositionLayerBaseHeader*)&renderer->Layers[i];
+ }
+
+ XrFrameEndInfo end_frame_info = {};
+ end_frame_info.type = XR_TYPE_FRAME_END_INFO;
+ end_frame_info.displayTime = engine->PredictedDisplayTime;
+ end_frame_info.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
+ end_frame_info.layerCount = renderer->LayerCount;
+ end_frame_info.layers = layers;
+ OXR(xrEndFrame(engine->Session, &end_frame_info));
+}
+
+void XrRendererBindFramebuffer(struct XrRenderer* renderer) {
+ if (!renderer->Initialized)
+ return;
+ int fbo_index = renderer->ConfigInt[CONFIG_CURRENT_FBO];
+ XrFramebufferSetCurrent(&renderer->Framebuffer[fbo_index]);
+}
+
+
+void XrRendererRecenter(struct XrEngine* engine, struct XrRenderer* renderer) {
+ // Calculate recenter reference
+ XrReferenceSpaceCreateInfo space_info = {};
+ space_info.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO;
+ space_info.poseInReferenceSpace.orientation.w = 1.0f;
+ if (engine->CurrentSpace != XR_NULL_HANDLE) {
+ XrSpaceLocation loc = {};
+ loc.type = XR_TYPE_SPACE_LOCATION;
+ OXR(xrLocateSpace(engine->HeadSpace, engine->CurrentSpace,
+ engine->PredictedDisplayTime, &loc));
+ renderer->HmdOrientation = XrQuaternionfEulerAngles(loc.pose.orientation);
+
+ renderer->ConfigFloat[CONFIG_RECENTER_YAW] += renderer->HmdOrientation.y;
+ float renceter_yaw = ToRadians(renderer->ConfigFloat[CONFIG_RECENTER_YAW]);
+ space_info.poseInReferenceSpace.orientation.x = 0;
+ space_info.poseInReferenceSpace.orientation.y = sinf(renceter_yaw / 2);
+ space_info.poseInReferenceSpace.orientation.z = 0;
+ space_info.poseInReferenceSpace.orientation.w = cosf(renceter_yaw / 2);
+ }
+
+ // Delete previous space instances
+ if (engine->StageSpace != XR_NULL_HANDLE) {
+ OXR(xrDestroySpace(engine->StageSpace));
+ }
+ if (engine->FakeSpace != XR_NULL_HANDLE) {
+ OXR(xrDestroySpace(engine->FakeSpace));
+ }
+
+ // Create a default stage space to use if SPACE_TYPE_STAGE is not
+ // supported, or calls to xrGetReferenceSpaceBoundsRect fail.
+ space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
+ if (engine->PlatformFlag[PLATFORM_TRACKING_FLOOR]) {
+ space_info.poseInReferenceSpace.position.y = -1.6750f;
+ }
+ OXR(xrCreateReferenceSpace(engine->Session, &space_info, &engine->FakeSpace));
+ ALOGV("Created fake stage space from local space with offset");
+ engine->CurrentSpace = engine->FakeSpace;
+
+ if (renderer->StageSupported) {
+ space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;
+ space_info.poseInReferenceSpace.position.y = 0.0;
+ OXR(xrCreateReferenceSpace(engine->Session, &space_info, &engine->StageSpace));
+ ALOGV("Created stage space");
+ if (engine->PlatformFlag[PLATFORM_TRACKING_FLOOR]) {
+ engine->CurrentSpace = engine->StageSpace;
+ }
+ }
+
+ // Update menu orientation
+ renderer->ConfigFloat[CONFIG_MENU_PITCH] = renderer->HmdOrientation.x;
+ renderer->ConfigFloat[CONFIG_MENU_YAW] = 0.0f;
+}
+
+void XrRendererHandleSessionStateChanges(struct XrEngine* engine, struct XrRenderer* renderer, XrSessionState state) {
+ if (state == XR_SESSION_STATE_READY) {
+ assert(renderer->SessionActive == false);
+
+ XrSessionBeginInfo session_begin_info;
+ memset(&session_begin_info, 0, sizeof(session_begin_info));
+ session_begin_info.type = XR_TYPE_SESSION_BEGIN_INFO;
+ session_begin_info.next = NULL;
+ session_begin_info.primaryViewConfigurationType = renderer->ViewportConfig.viewConfigurationType;
+
+ XrResult result;
+ OXR(result = xrBeginSession(engine->Session, &session_begin_info));
+ renderer->SessionActive = (result == XR_SUCCESS);
+ ALOGV("Session active = %d", renderer->SessionActive);
+
+#ifdef ANDROID
+ if (renderer->SessionActive && engine->PlatformFlag[PLATFORM_EXTENSION_PERFORMANCE]) {
+ PFN_xrPerfSettingsSetPerformanceLevelEXT pfnPerfSettingsSetPerformanceLevelEXT = NULL;
+ OXR(xrGetInstanceProcAddr(engine->Instance, "xrPerfSettingsSetPerformanceLevelEXT",
+ (PFN_xrVoidFunction*)(&pfnPerfSettingsSetPerformanceLevelEXT)));
+
+ OXR(pfnPerfSettingsSetPerformanceLevelEXT(
+ engine->Session, XR_PERF_SETTINGS_DOMAIN_CPU_EXT, XR_PERF_SETTINGS_LEVEL_BOOST_EXT));
+ OXR(pfnPerfSettingsSetPerformanceLevelEXT(
+ engine->Session, XR_PERF_SETTINGS_DOMAIN_GPU_EXT, XR_PERF_SETTINGS_LEVEL_BOOST_EXT));
+
+ PFN_xrSetAndroidApplicationThreadKHR pfnSetAndroidApplicationThreadKHR = NULL;
+ OXR(xrGetInstanceProcAddr(engine->Instance, "xrSetAndroidApplicationThreadKHR",
+ (PFN_xrVoidFunction*)(&pfnSetAndroidApplicationThreadKHR)));
+
+ OXR(pfnSetAndroidApplicationThreadKHR(engine->Session,
+ XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR,
+ engine->MainThreadId));
+ OXR(pfnSetAndroidApplicationThreadKHR(engine->Session,
+ XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR,
+ engine->RenderThreadId));
+ }
+#endif
+ } else if (state == XR_SESSION_STATE_STOPPING) {
+ assert(renderer->SessionActive);
+
+ OXR(xrEndSession(engine->Session));
+ renderer->SessionActive = false;
+ }
+}
+
+void XrRendererHandleXrEvents(struct XrEngine* engine, struct XrRenderer* renderer) {
+ XrEventDataBuffer event_data_bufer = {};
+
+ // Poll for events
+ for (;;) {
+ XrEventDataBaseHeader* base_event_handler = (XrEventDataBaseHeader*)(&event_data_bufer);
+ base_event_handler->type = XR_TYPE_EVENT_DATA_BUFFER;
+ base_event_handler->next = NULL;
+ XrResult r;
+ OXR(r = xrPollEvent(engine->Instance, &event_data_bufer));
+ if (r != XR_SUCCESS) {
+ break;
+ }
+
+ switch (base_event_handler->type) {
+ case XR_TYPE_EVENT_DATA_EVENTS_LOST:
+ ALOGV("xrPollEvent: received XR_TYPE_EVENT_DATA_EVENTS_LOST");
+ break;
+ case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
+ {
+ const XrEventDataInstanceLossPending* instance_loss_pending_event =
+ (XrEventDataInstanceLossPending*)(base_event_handler);
+ ALOGV("xrPollEvent: received XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: time %lf",
+ FromXrTime(instance_loss_pending_event->lossTime));
+ }
+ break;
+ case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED:
+ ALOGV("xrPollEvent: received XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED");
+ break;
+ case XR_TYPE_EVENT_DATA_PERF_SETTINGS_EXT:
+ break;
+ case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:
+ XrRendererRecenter(engine, renderer);
+ break;
+ case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:
+ {
+ const XrEventDataSessionStateChanged* session_state_changed_event =
+ (XrEventDataSessionStateChanged*)(base_event_handler);
+ switch (session_state_changed_event->state) {
+ case XR_SESSION_STATE_FOCUSED:
+ renderer->SessionFocused = true;
+ break;
+ case XR_SESSION_STATE_VISIBLE:
+ renderer->SessionFocused = false;
+ break;
+ case XR_SESSION_STATE_READY:
+ case XR_SESSION_STATE_STOPPING:
+ XrRendererHandleSessionStateChanges(engine, renderer, session_state_changed_event->state);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ ALOGV("xrPollEvent: Unknown event");
+ break;
+ }
+ }
+}
+
+void XrRendererUpdateStageBounds(struct XrEngine* engine) {
+ XrExtent2Df stage_bounds = {};
+
+ XrResult result;
+ OXR(result = xrGetReferenceSpaceBoundsRect(engine->Session, XR_REFERENCE_SPACE_TYPE_STAGE,
+ &stage_bounds));
+ if (result != XR_SUCCESS) {
+ stage_bounds.width = 1.0f;
+ stage_bounds.height = 1.0f;
+
+ engine->CurrentSpace = engine->FakeSpace;
+ }
+}
diff --git a/app/src/main/cpp/xrio/renderer.h b/app/src/main/cpp/xrio/renderer.h
new file mode 100644
index 000000000..ffda39ea0
--- /dev/null
+++ b/app/src/main/cpp/xrio/renderer.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "engine.h"
+#include "framebuffer.h"
+
+enum XrConfigFloat {
+ // 2D canvas positioning
+ CONFIG_CANVAS_DISTANCE,
+ CONFIG_MENU_PITCH,
+ CONFIG_MENU_YAW,
+ CONFIG_RECENTER_YAW,
+
+ CONFIG_FLOAT_MAX
+};
+
+enum XrConfigInt {
+ // switching between modes
+ CONFIG_MODE,
+ CONFIG_PASSTHROUGH,
+ CONFIG_SBS,
+ // viewport setup
+ CONFIG_VIEWPORT_WIDTH,
+ CONFIG_VIEWPORT_HEIGHT,
+ // render status
+ CONFIG_CURRENT_FBO,
+
+ // end
+ CONFIG_INT_MAX
+};
+
+enum XrRenderMode {
+ RENDER_MODE_MONO_SCREEN,
+ RENDER_MODE_STEREO_SCREEN,
+ RENDER_MODE_MONO_6DOF,
+ RENDER_MODE_STEREO_6DOF
+};
+
+struct XrRenderer {
+ bool SessionActive;
+ bool SessionFocused;
+ bool Initialized;
+ bool StageSupported;
+ float ConfigFloat[CONFIG_FLOAT_MAX];
+ int ConfigInt[CONFIG_INT_MAX];
+
+ struct XrFramebuffer Framebuffer[XrMaxNumEyes];
+
+ int LayerCount;
+ XrCompositorLayer Layers[XrMaxLayerCount];
+ XrPassthroughFB Passthrough;
+ XrPassthroughLayerFB PassthroughLayer;
+ bool PassthroughRunning;
+ XrViewConfigurationProperties ViewportConfig;
+ XrViewConfigurationView ViewConfig[XrMaxNumEyes];
+
+ XrFovf Fov;
+ XrView* Projections;
+ XrPosef InvertedViewPose[2];
+ XrVector3f HmdOrientation;
+};
+
+void XrRendererInit(struct XrEngine* engine, struct XrRenderer* renderer);
+void XrRendererDestroy(struct XrEngine* engine, struct XrRenderer* renderer);
+void XrRendererGetResolution(struct XrEngine* engine, struct XrRenderer* renderer, int* pWidth, int* pHeight);
+
+bool XrRendererInitFrame(struct XrEngine* engine, struct XrRenderer* renderer);
+void XrRendererBeginFrame(struct XrRenderer* renderer, int fbo_index);
+void XrRendererEndFrame(struct XrRenderer* renderer);
+void XrRendererFinishFrame(struct XrEngine* engine, struct XrRenderer* renderer);
+
+void XrRendererBindFramebuffer(struct XrRenderer* renderer);
+void XrRendererRecenter(struct XrEngine* engine, struct XrRenderer* renderer);
+
+void XrRendererHandleSessionStateChanges(struct XrEngine* engine, struct XrRenderer* renderer, XrSessionState state);
+void XrRendererHandleXrEvents(struct XrEngine* engine, struct XrRenderer* renderer);
+void XrRendererUpdateStageBounds(struct XrEngine* engine);
diff --git a/app/src/main/java/com/termux/x11/MainActivity.java b/app/src/main/java/com/termux/x11/MainActivity.java
index 84434a82c..a3791a913 100644
--- a/app/src/main/java/com/termux/x11/MainActivity.java
+++ b/app/src/main/java/com/termux/x11/MainActivity.java
@@ -84,7 +84,7 @@ public class MainActivity extends AppCompatActivity implements View.OnApplyWindo
public static Handler handler = new Handler();
FrameLayout frm;
private TouchInputHandler mInputHandler;
- private ICmdEntryInterface service = null;
+ protected ICmdEntryInterface service = null;
public TermuxX11ExtraKeys mExtraKeys;
private Notification mNotification;
private final int mNotificationId = 7892;
@@ -846,6 +846,12 @@ public static void toggleKeyboardVisibility(Context context) {
@SuppressWarnings("SameParameterValue")
void clientConnectedStateChanged(boolean connected) {
+ if (connected && XrActivity.isEnabled() && !(this instanceof XrActivity)) {
+ XrActivity.openIntent(this);
+ mClientConnected = connected;
+ return;
+ }
+
runOnUiThread(()-> {
SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this);
mClientConnected = connected;
diff --git a/app/src/main/java/com/termux/x11/XrActivity.java b/app/src/main/java/com/termux/x11/XrActivity.java
new file mode 100644
index 000000000..6f9056734
--- /dev/null
+++ b/app/src/main/java/com/termux/x11/XrActivity.java
@@ -0,0 +1,363 @@
+package com.termux.x11;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.display.DisplayManager;
+import android.opengl.GLES11Ext;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.inputmethod.InputMethodManager;
+
+import com.termux.x11.input.InputStub;
+import com.termux.x11.utils.GLUtility;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class XrActivity extends MainActivity implements GLSurfaceView.Renderer {
+ // Order of the enum has to be the same as in xrio/android.c
+ public enum ControllerAxis {
+ L_PITCH, L_YAW, L_ROLL, L_THUMBSTICK_X, L_THUMBSTICK_Y, L_X, L_Y, L_Z,
+ R_PITCH, R_YAW, R_ROLL, R_THUMBSTICK_X, R_THUMBSTICK_Y, R_X, R_Y, R_Z,
+ HMD_PITCH, HMD_YAW, HMD_ROLL, HMD_X, HMD_Y, HMD_Z, HMD_IPD
+ }
+
+ // Order of the enum has to be the same as in xrio/android.c
+ public enum ControllerButton {
+ L_GRIP, L_MENU, L_THUMBSTICK_PRESS, L_THUMBSTICK_LEFT, L_THUMBSTICK_RIGHT, L_THUMBSTICK_UP, L_THUMBSTICK_DOWN, L_TRIGGER, L_X, L_Y,
+ R_A, R_B, R_GRIP, R_THUMBSTICK_PRESS, R_THUMBSTICK_LEFT, R_THUMBSTICK_RIGHT, R_THUMBSTICK_UP, R_THUMBSTICK_DOWN, R_TRIGGER,
+ }
+
+ // Order of the enum has to be the same as in xrio/android.c
+ public enum RenderParam {
+ CANVAS_DISTANCE, IMMERSIVE, PASSTHROUGH, SBS, VIEWPORT_WIDTH, VIEWPORT_HEIGHT,
+ }
+
+ private static boolean isDeviceDetectionFinished = false;
+ private static boolean isDeviceSupported = false;
+
+ private boolean isImmersive = false;
+ private boolean isSBS = false;
+ private long lastEnter;
+ private final float[] lastAxes = new float[ControllerAxis.values().length];
+ private final boolean[] lastButtons = new boolean[ControllerButton.values().length];
+ private final float[] mouse = new float[2];
+
+ private int program;
+ private int texture;
+ private SurfaceTexture surface;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ GLSurfaceView view = new GLSurfaceView(this);
+ view.setEGLContextClientVersion(2);
+ view.setRenderer(this);
+ frm.addView(view);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ teardown();
+
+ // Going back to the Android 2D rendering isn't supported.
+ // Kill the app to ensure there is no unexpected behaviour.
+ System.exit(0);
+ }
+
+ public static boolean isEnabled() {
+ return isSupported();
+ }
+
+ public static boolean isSupported() {
+ if (!isDeviceDetectionFinished) {
+ if (Build.MANUFACTURER.compareToIgnoreCase("META") == 0) {
+ isDeviceSupported = true;
+ }
+ if (Build.MANUFACTURER.compareToIgnoreCase("OCULUS") == 0) {
+ isDeviceSupported = true;
+ }
+ isDeviceDetectionFinished = true;
+ }
+ return isDeviceSupported;
+ }
+
+ public static void openIntent(Activity context) {
+ // 0. Create the launch intent
+ Intent intent = new Intent(context, XrActivity.class);
+
+ // 1. Locate the main display ID and add that to the intent
+ final int mainDisplayId = getMainDisplay(context);
+ ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId);
+
+ // 2. Set the flags: start in a new task
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ // 3. Launch the activity.
+ // Don't use the container's ContextWrapper, which is adding arguments
+ context.getBaseContext().startActivity(intent, options.toBundle());
+
+ // 4. Finish the previous activity: this avoids an audio bug
+ context.finish();
+ }
+
+ private static int getMainDisplay(Context context) {
+ final DisplayManager displayManager =
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ for (Display display : displayManager.getDisplays()) {
+ if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ return display.getDisplayId();
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ void clientConnectedStateChanged(boolean connected) {
+ if (!connected && (surface != null)) {
+ teardown();
+
+ // Going back to the Android 2D rendering isn't supported.
+ // Kill the app to ensure there is no unexpected behaviour.
+ System.exit(0);
+ } else {
+ super.clientConnectedStateChanged(connected);
+ }
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
+ System.loadLibrary("XRio");
+ if (isSupported()) {
+ init();
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl10, int w, int h) {
+ texture = GLUtility.createTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
+ program = GLUtility.createProgram(true);
+ surface = new SurfaceTexture(texture);
+
+ runOnUiThread(() -> getLorieView().setCallback((sfc, surfaceWidth, surfaceHeight, screenWidth, screenHeight) -> {
+ LorieView.sendWindowChange(screenWidth, screenHeight, 60);
+ surface.setDefaultBufferSize(screenWidth, screenHeight);
+
+ if (service != null) {
+ try {
+ service.windowChanged(new Surface(surface), "OpenXR");
+ } catch (RemoteException e) {
+ Log.e("XrActivity", "failed to send windowChanged request", e);
+ }
+ }
+ }));
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl10) {
+ if (isSupported()) {
+ setRenderParam(RenderParam.CANVAS_DISTANCE.ordinal(), 5);
+ setRenderParam(RenderParam.IMMERSIVE.ordinal(), isImmersive ? 1 : 0);
+ setRenderParam(RenderParam.PASSTHROUGH.ordinal(), isImmersive ? 0 : 1);
+ setRenderParam(RenderParam.SBS.ordinal(), isSBS ? 1 : 0);
+
+ if (beginFrame()) {
+ renderFrame(gl10);
+ finishFrame();
+ processInput();
+ }
+ } else {
+ renderFrame(gl10);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Meta HorizonOS sends up event only (as for in v66)
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+
+ // The OS sends 4x enter event, filter it
+ if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ if (event.getDownTime() - lastEnter < 250) {
+ return true;
+ }
+ lastEnter = event.getDownTime();
+ }
+
+ // Send key press, give system chance to notice it and send key release
+ getLorieView().sendKeyEvent(0, event.getKeyCode(), true);
+ try {
+ Thread.sleep(50);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ getLorieView().sendKeyEvent(0, event.getKeyCode(), false);
+ }
+ return true;
+ }
+
+ private void processInput() {
+ float[] axes = getAxes();
+ boolean[] buttons = getButtons();
+ LorieView view = getLorieView();
+ Rect frame = view.getHolder().getSurfaceFrame();
+
+ // Mouse control with hand
+ float meter2px = frame.width() * 10.0f;
+ float dx = (axes[ControllerAxis.R_X.ordinal()] - lastAxes[ControllerAxis.R_X.ordinal()]) * meter2px;
+ float dy = (axes[ControllerAxis.R_Y.ordinal()] - lastAxes[ControllerAxis.R_Y.ordinal()]) * meter2px;
+
+ // Mouse control with head
+ if (isImmersive) {
+ float angle2px = frame.width() * 0.05f;
+ dx = getAngleDiff(lastAxes[ControllerAxis.HMD_YAW.ordinal()], axes[ControllerAxis.HMD_YAW.ordinal()]) * angle2px;
+ dy = getAngleDiff(lastAxes[ControllerAxis.HMD_PITCH.ordinal()], axes[ControllerAxis.HMD_PITCH.ordinal()]) * angle2px;
+ if (Float.isNaN(dy)) {
+ dy = 0;
+ }
+ }
+
+ // Mouse speed adjust
+ float mouseSpeed = isImmersive ? 1 : 0.25f;
+ dx *= mouseSpeed;
+ dy *= mouseSpeed;
+
+ // Mouse "snap turn"
+ int snapturn = 125;
+ if (getButtonClicked(buttons, ControllerButton.R_THUMBSTICK_LEFT)) {
+ dx = -snapturn;
+ }
+ if (getButtonClicked(buttons, ControllerButton.R_THUMBSTICK_RIGHT)) {
+ dx = snapturn;
+ }
+
+ // Mouse smoothing
+ if (isImmersive) {
+ mouse[0] = dx;
+ mouse[1] = dy;
+ } else {
+ float smoothFactor = 0.75f;
+ mouse[0] = mouse[0] * smoothFactor + dx * (1 - smoothFactor);
+ mouse[1] = mouse[1] * smoothFactor + dy * (1 - smoothFactor);
+ }
+
+ // Set mouse status
+ int scrollStep = 150;
+ view.sendMouseEvent(mouse[0], -mouse[1], 0, false, true);
+ mapMouse(view, buttons, ControllerButton.R_TRIGGER, InputStub.BUTTON_LEFT);
+ mapMouse(view, buttons, ControllerButton.R_GRIP, InputStub.BUTTON_RIGHT);
+ mapScroll(view, buttons, ControllerButton.R_THUMBSTICK_UP, 0, -scrollStep);
+ mapScroll(view, buttons, ControllerButton.R_THUMBSTICK_DOWN, 0, scrollStep);
+
+ // Switch immersive/SBS mode
+ if (getButtonClicked(buttons, ControllerButton.L_THUMBSTICK_PRESS)) {
+ if (buttons[ControllerButton.R_GRIP.ordinal()]) {
+ isSBS = !isSBS;
+ }
+ else {
+ isImmersive = !isImmersive;
+ }
+ }
+
+ // Show system keyboard
+ if (getButtonClicked(buttons, ControllerButton.R_THUMBSTICK_PRESS)) {
+ getInstance().runOnUiThread(() -> {
+ isSBS = false;
+ isImmersive = false;
+ getWindow().getDecorView().postDelayed(() -> {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }, 500L);
+ });
+ }
+
+ // Update keyboard
+ mapKey(view, buttons, ControllerButton.R_A, KeyEvent.KEYCODE_A);
+ mapKey(view, buttons, ControllerButton.R_B, KeyEvent.KEYCODE_B);
+ mapKey(view, buttons, ControllerButton.L_X, KeyEvent.KEYCODE_X);
+ mapKey(view, buttons, ControllerButton.L_Y, KeyEvent.KEYCODE_Y);
+ mapKey(view, buttons, ControllerButton.L_GRIP, KeyEvent.KEYCODE_SPACE);
+ mapKey(view, buttons, ControllerButton.L_MENU, KeyEvent.KEYCODE_BACK);
+ mapKey(view, buttons, ControllerButton.L_TRIGGER, KeyEvent.KEYCODE_ENTER);
+ mapKey(view, buttons, ControllerButton.L_THUMBSTICK_LEFT, KeyEvent.KEYCODE_DPAD_LEFT);
+ mapKey(view, buttons, ControllerButton.L_THUMBSTICK_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT);
+ mapKey(view, buttons, ControllerButton.L_THUMBSTICK_UP, KeyEvent.KEYCODE_DPAD_UP);
+ mapKey(view, buttons, ControllerButton.L_THUMBSTICK_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
+
+ // Store the OpenXR data
+ System.arraycopy(axes, 0, lastAxes, 0, axes.length);
+ System.arraycopy(buttons, 0, lastButtons, 0, buttons.length);
+ }
+
+ private float getAngleDiff(float oldAngle, float newAngle) {
+ float diff = oldAngle - newAngle;
+ while (diff > 180) {
+ diff -= 360;
+ }
+ while (diff < -180) {
+ diff += 360;
+ }
+ return diff;
+ }
+
+ private boolean getButtonClicked(boolean[] buttons, ControllerButton button) {
+ return buttons[button.ordinal()] && !lastButtons[button.ordinal()];
+ }
+
+ private void mapKey(LorieView v, boolean[] buttons, ControllerButton b, int keycode) {
+ if (buttons[b.ordinal()] != lastButtons[b.ordinal()]) {
+ v.sendKeyEvent(0, keycode, buttons[b.ordinal()]);
+ }
+ }
+
+ private void mapMouse(LorieView v, boolean[] buttons, ControllerButton b, int button) {
+ if (buttons[b.ordinal()] != lastButtons[b.ordinal()]) {
+ v.sendMouseEvent(0, 0, button, buttons[b.ordinal()], true);
+ }
+ }
+
+ private void mapScroll(LorieView v, boolean[] buttons, ControllerButton b, float x, float y) {
+ if (getButtonClicked(buttons, b)) {
+ v.sendMouseWheelEvent(x, y);
+ }
+ }
+
+ private void renderFrame(GL10 gl10) {
+ if (isSupported()) {
+ int w = getRenderParam(RenderParam.VIEWPORT_WIDTH.ordinal());
+ int h = getRenderParam(RenderParam.VIEWPORT_HEIGHT.ordinal());
+ gl10.glViewport(0, 0, w, h);
+ }
+
+ surface.updateTexImage();
+ float aspect = 1;
+ if (isSupported()) {
+ LorieView view = getLorieView();
+ int w = view.getWidth();
+ int h = view.getHeight();
+ aspect = Math.min(w, h) / (float)Math.max(w, h);
+ }
+ GLUtility.drawTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, program, texture, -aspect);
+ }
+
+ private native void init();
+ private native void teardown();
+ private native boolean beginFrame();
+ private native void finishFrame();
+ public native float[] getAxes();
+ public native boolean[] getButtons();
+ public native int getRenderParam(int param);
+ public native void setRenderParam(int param, int value);
+}
diff --git a/app/src/main/java/com/termux/x11/utils/GLUtility.java b/app/src/main/java/com/termux/x11/utils/GLUtility.java
new file mode 100644
index 000000000..512d0c253
--- /dev/null
+++ b/app/src/main/java/com/termux/x11/utils/GLUtility.java
@@ -0,0 +1,130 @@
+package com.termux.x11.utils;
+
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class GLUtility {
+
+ private static final String SIMPLE_FRAGMENT_SHADER =
+ "precision highp float;\n" +
+ "uniform sampler2D u_Texture;\n" +
+ "varying vec2 v_TexCoord;\n" +
+ "\n" +
+ "void main(void){\n" +
+ " gl_FragColor = texture2D(u_Texture, v_TexCoord);\n" +
+ "}";
+
+ private static final String SIMPLE_FRAGMENT_EXT_SHADER =
+ "#extension GL_OES_EGL_image_external : require\n" +
+ "precision highp float;\n" +
+ "uniform samplerExternalOES u_Texture;\n" +
+ "varying vec2 v_TexCoord;\n" +
+ "\n" +
+ "void main(void){\n" +
+ " gl_FragColor = texture2D(u_Texture, v_TexCoord);\n" +
+ "}";
+
+ private static final String SIMPLE_VERTEX_SHADER =
+ "precision highp float;\n" +
+ "attribute vec3 a_Position;\n" +
+ "attribute vec2 a_TexCoord;\n" +
+ "varying vec2 v_TexCoord;\n" +
+ "\n" +
+ "void main(void)\n" +
+ "{\n" +
+ " gl_Position = vec4(a_Position, 1.0);\n" +
+ " v_TexCoord = a_TexCoord;\n" +
+ "}";
+
+ private static final String TAG = "GLUtility";
+
+ private static float lastAspect;
+
+ public static int createProgram(boolean externalTexture) {
+ if (externalTexture) {
+ return createProgram(SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_EXT_SHADER);
+ } else {
+ return createProgram(SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_SHADER);
+ }
+ }
+
+ public static int createProgram(String vertexShader, String fragmentShader) {
+ int program = GLES20.glCreateProgram();
+ GLES20.glAttachShader(program, compileShader(GLES20.GL_VERTEX_SHADER, vertexShader));
+ GLES20.glAttachShader(program, compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader));
+ GLES20.glLinkProgram(program);
+ GLES20.glUseProgram(program);
+ return program;
+ }
+
+ public static int createTexture(int type) {
+ int[] texture = new int[1];
+ GLES20.glGenTextures(texture.length, texture, 0);
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(type, texture[0]);
+ GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ return texture[0];
+ }
+
+ public static void drawTexture(int type, int program, int texture, float aspect) {
+ if (Math.abs(lastAspect - aspect) > 0.00001f) {
+
+ float[] vertices = new float[] {
+ -1, aspect, -1, -aspect, 1, -aspect,
+ -1, aspect, 1, -aspect, 1, aspect,
+ };
+ float[] uvs = new float[] {
+ 0, 1, 0, 0, 1, 0,
+ 0, 1, 1, 0, 1, 1
+ };
+
+ attribPointer(program, "a_Position", vertices, 2);
+ attribPointer(program, "a_TexCoord", uvs, 2);
+
+ lastAspect = aspect;
+ }
+
+ GLES20.glUseProgram(program);
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(type, texture);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
+ }
+
+ private static void attribPointer(int program, String location, float[] data, int stride) {
+ int handle = GLES20.glGetAttribLocation(program, location);
+ GLES20.glEnableVertexAttribArray(handle);
+ GLES20.glVertexAttribPointer(
+ handle,
+ stride,
+ GLES20.GL_FLOAT,
+ false,
+ 0,
+ ByteBuffer.allocateDirect(data.length * 4)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer()
+ .put(data)
+ .position(0)
+ );
+ }
+
+ private static int compileShader(int shaderType, String shaderSource) {
+ int shader = GLES20.glCreateShader(shaderType);
+ if (shader != 0) {
+ GLES20.glShaderSource(shader, shaderSource);
+ GLES20.glCompileShader(shader);
+
+ final int[] compileStatus = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
+ if (compileStatus[0] == GLES20.GL_FALSE) {
+ Log.e(TAG, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader));
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ }
+ }
+ return shader;
+ }
+}