diff --git a/app/.gitmodules b/app/.gitmodules
new file mode 100644
index 0000000..e1b3a21
--- /dev/null
+++ b/app/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src/main/cpp/OpenXR-SDK"]
+ path = 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 64e4e5f..1ef03ec 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,15 @@
+
+
+
@@ -37,6 +46,21 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation"
android:screenOrientation="sensorLandscape" />
+
+
+
+
+
+
+
+#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_CURRENT_API_VERSION;
+
+ 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/xr/engine.h b/app/src/main/cpp/xr/engine.h
new file mode 100644
index 0000000..d0c35ea
--- /dev/null
+++ b/app/src/main/cpp/xr/engine.h
@@ -0,0 +1,99 @@
+#pragma once
+
+#include
+
+//#define _DEBUG
+
+#ifndef ANDROID
+#define ANDROID 1
+#endif
+
+#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/xr/framebuffer.c b/app/src/main/cpp/xr/framebuffer.c
new file mode 100644
index 0000000..e79b54f
--- /dev/null
+++ b/app/src/main/cpp/xr/framebuffer.c
@@ -0,0 +1,148 @@
+#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);
+
+#if XR_USE_GRAPHICS_API_OPENGL_ES
+ GL(glEnable(GL_SCISSOR_TEST));
+ GL(glViewport(0, 0, framebuffer->Width, framebuffer->Height));
+ GL(glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
+ GL(glScissor(0, 0, framebuffer->Width, framebuffer->Height));
+ GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
+ GL(glScissor(0, 0, 0, 0));
+ GL(glDisable(GL_SCISSOR_TEST));
+#endif
+}
+
+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/xr/framebuffer.h b/app/src/main/cpp/xr/framebuffer.h
new file mode 100644
index 0000000..5983b16
--- /dev/null
+++ b/app/src/main/cpp/xr/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/xr/input.c b/app/src/main/cpp/xr/input.c
new file mode 100644
index 0000000..73ddf8f
--- /dev/null
+++ b/app/src/main/cpp/xr/input.c
@@ -0,0 +1,443 @@
+#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/xr/input.h b/app/src/main/cpp/xr/input.h
new file mode 100644
index 0000000..204b23b
--- /dev/null
+++ b/app/src/main/cpp/xr/input.h
@@ -0,0 +1,83 @@
+#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/xr/main.c b/app/src/main/cpp/xr/main.c
new file mode 100644
index 0000000..e10747a
--- /dev/null
+++ b/app/src/main/cpp/xr/main.c
@@ -0,0 +1,189 @@
+#include "engine.h"
+#include "input.h"
+#include "math.h"
+#include "renderer.h"
+
+#include
+
+struct XrEngine xr_module_engine;
+struct XrInput xr_module_input;
+struct XrRenderer xr_module_renderer;
+bool xr_initialized = false;
+
+#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_module_engine->Instance, result, errorBuffer);
+ ALOGE("OpenXR error on line %s:%d %s", file, line, errorBuffer);
+ }
+}
+#endif
+
+JNIEXPORT void JNICALL Java_com_winlator_XrActivity_init(JNIEnv *env, jobject obj) {
+
+ // Do not allow second initialization
+ if (xr_initialized) {
+ return;
+ }
+
+ // Set platform flags
+ memset(&xr_module_engine, 0, sizeof(xr_module_engine));
+ xr_module_engine.PlatformFlag[PLATFORM_CONTROLLER_QUEST] = true;
+ xr_module_engine.PlatformFlag[PLATFORM_EXTENSION_PASSTHROUGH] = true;
+ xr_module_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_module_engine, &java, "Winlator", 1);
+
+ // Enter XR
+ XrEngineEnter(&xr_module_engine);
+ XrInputInit(&xr_module_engine, &xr_module_input);
+ XrRendererInit(&xr_module_engine, &xr_module_renderer);
+ xr_initialized = true;
+ ALOGV("Init called");
+}
+
+JNIEXPORT void JNICALL Java_com_winlator_XrActivity_bindFramebuffer(JNIEnv *env, jobject obj) {
+ if (xr_initialized) {
+ XrRendererBindFramebuffer(&xr_module_renderer);
+ }
+}
+
+JNIEXPORT jint JNICALL Java_com_winlator_XrActivity_getWidth(JNIEnv *env, jobject obj) {
+ int w, h;
+ XrRendererGetResolution(&xr_module_engine, &xr_module_renderer, &w, &h);
+ return w;
+}
+JNIEXPORT jint JNICALL Java_com_winlator_XrActivity_getHeight(JNIEnv *env, jobject obj) {
+ int w, h;
+ XrRendererGetResolution(&xr_module_engine, &xr_module_renderer, &w, &h);
+ return h;
+}
+
+JNIEXPORT jboolean JNICALL Java_com_winlator_XrActivity_beginFrame(JNIEnv *env, jobject obj, jboolean immersive, jboolean sbs) {
+ if (XrRendererInitFrame(&xr_module_engine, &xr_module_renderer)) {
+
+ // Set render canvas
+ int mode = immersive ? RENDER_MODE_MONO_6DOF : RENDER_MODE_MONO_SCREEN;
+ xr_module_renderer.ConfigFloat[CONFIG_CANVAS_DISTANCE] = 5.0f;
+ xr_module_renderer.ConfigInt[CONFIG_PASSTHROUGH] = !immersive;
+ xr_module_renderer.ConfigInt[CONFIG_MODE] = mode;
+ xr_module_renderer.ConfigInt[CONFIG_SBS] = sbs;
+
+ // Recenter if mode switched
+ static bool last_immersive = false;
+ if (last_immersive != immersive) {
+ XrRendererRecenter(&xr_module_engine, &xr_module_renderer);
+ last_immersive = immersive;
+ }
+
+ // Update controllers state
+ XrInputUpdate(&xr_module_engine, &xr_module_input);
+
+ // Lock framebuffer
+ XrRendererBeginFrame(&xr_module_renderer, 0);
+
+ return true;
+ }
+ return false;
+}
+
+JNIEXPORT void JNICALL Java_com_winlator_XrActivity_endFrame(JNIEnv *env, jobject obj) {
+ XrRendererEndFrame(&xr_module_renderer);
+ XrRendererFinishFrame(&xr_module_engine, &xr_module_renderer);
+}
+
+JNIEXPORT jfloatArray JNICALL Java_com_winlator_XrActivity_getAxes(JNIEnv *env, jobject obj) {
+ XrPosef lPose = XrInputGetPose(&xr_module_input, 0);
+ XrPosef rPose = XrInputGetPose(&xr_module_input, 1);
+ XrVector2f lThumbstick = XrInputGetJoystickState(&xr_module_input, 0);
+ XrVector2f rThumbstick = XrInputGetJoystickState(&xr_module_input, 1);
+ XrVector3f lPosition = xr_module_renderer.Projections[0].pose.position;
+ XrVector3f rPosition = xr_module_renderer.Projections[1].pose.position;
+ XrVector3f angles = xr_module_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_winlator_XrActivity_getButtons(JNIEnv *env, jobject obj) {
+ uint32_t l = XrInputGetButtonState(&xr_module_input, 0);
+ uint32_t r = XrInputGetButtonState(&xr_module_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;
+}
diff --git a/app/src/main/cpp/xr/math.c b/app/src/main/cpp/xr/math.c
new file mode 100644
index 0000000..f5e334f
--- /dev/null
+++ b/app/src/main/cpp/xr/math.c
@@ -0,0 +1,218 @@
+#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/xr/math.h b/app/src/main/cpp/xr/math.h
new file mode 100644
index 0000000..dd50cf1
--- /dev/null
+++ b/app/src/main/cpp/xr/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/xr/renderer.c b/app/src/main/cpp/xr/renderer.c
new file mode 100644
index 0000000..9aa2783
--- /dev/null
+++ b/app/src/main/cpp/xr/renderer.c
@@ -0,0 +1,625 @@
+#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 (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];
+ }
+
+ 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/xr/renderer.h b/app/src/main/cpp/xr/renderer.h
new file mode 100644
index 0000000..62decca
--- /dev/null
+++ b/app/src/main/cpp/xr/renderer.h
@@ -0,0 +1,79 @@
+#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/winlator/ContainersFragment.java b/app/src/main/java/com/winlator/ContainersFragment.java
index dd892c3..079fb44 100644
--- a/app/src/main/java/com/winlator/ContainersFragment.java
+++ b/app/src/main/java/com/winlator/ContainersFragment.java
@@ -145,9 +145,12 @@ public class ContainersFragment extends Fragment {
listItemMenu.setOnMenuItemClickListener((menuItem) -> {
switch (menuItem.getItemId()) {
case R.id.container_run:
- Intent intent = new Intent(context, XServerDisplayActivity.class);
- intent.putExtra("container_id", container.id);
- context.startActivity(intent);
+ if (!XrActivity.isSupported()) {
+ Intent intent = new Intent(context, XServerDisplayActivity.class);
+ intent.putExtra("container_id", container.id);
+ context.startActivity(intent);
+ }
+ else XrActivity.openIntent(getActivity(), container.id, null);
break;
case R.id.container_edit:
FragmentManager fragmentManager = getParentFragmentManager();
diff --git a/app/src/main/java/com/winlator/ShortcutsFragment.java b/app/src/main/java/com/winlator/ShortcutsFragment.java
index e7f8dbd..c4095dc 100644
--- a/app/src/main/java/com/winlator/ShortcutsFragment.java
+++ b/app/src/main/java/com/winlator/ShortcutsFragment.java
@@ -1,5 +1,6 @@
package com.winlator;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
@@ -133,11 +134,15 @@ public class ShortcutsFragment extends Fragment {
}
private void runFromShortcut(Shortcut shortcut) {
- Context context = getContext();
- Intent intent = new Intent(context, XServerDisplayActivity.class);
- intent.putExtra("container_id", shortcut.container.id);
- intent.putExtra("shortcut_path", shortcut.file.getPath());
- context.startActivity(intent);
+ Activity activity = getActivity();
+
+ if (!XrActivity.isSupported()) {
+ Intent intent = new Intent(activity, XServerDisplayActivity.class);
+ intent.putExtra("container_id", shortcut.container.id);
+ intent.putExtra("shortcut_path", shortcut.file.getPath());
+ activity.startActivity(intent);
+ }
+ else XrActivity.openIntent(activity, shortcut.container.id, shortcut.file.getPath());
}
}
}
diff --git a/app/src/main/java/com/winlator/XrActivity.java b/app/src/main/java/com/winlator/XrActivity.java
new file mode 100644
index 0000000..b6a8c89
--- /dev/null
+++ b/app/src/main/java/com/winlator/XrActivity.java
@@ -0,0 +1,353 @@
+package com.winlator;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Display;
+import android.view.View;
+import android.widget.EditText;
+
+import androidx.preference.PreferenceManager;
+
+import com.winlator.core.AppUtils;
+import com.winlator.xserver.Keyboard;
+import com.winlator.xserver.Pointer;
+import com.winlator.xserver.XKeycode;
+import com.winlator.xserver.XLock;
+import com.winlator.xserver.XServer;
+
+/*
+ WinlatorXR implementation by lvonasek (https://github.com/lvonasek)
+ */
+
+public class XrActivity extends XServerDisplayActivity implements TextWatcher {
+ // Order of the enum has to be the as in xr/main.cpp
+ 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 as in xr/main.cpp
+ 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,
+ }
+
+ private static boolean isDeviceDetectionFinished = false;
+ private static boolean isDeviceSupported = false;
+ private static boolean isImmersive = false;
+ private static boolean isSBS = false;
+ private static final float[] lastAxes = new float[ControllerAxis.values().length];
+ private static final boolean[] lastButtons = new boolean[ControllerButton.values().length];
+ private static String lastText = "";
+ private static float mouseSpeed = 1;
+ private static final float[] smoothedMouse = new float[2];
+ private static XrActivity instance;
+
+ @Override
+ public synchronized void onPause() {
+ EditText text = findViewById(R.id.XRTextInput);
+ text.removeTextChangedListener(this);
+ super.onPause();
+ }
+
+ @Override
+ public synchronized void onResume() {
+ super.onResume();
+ instance = this;
+ mouseSpeed = PreferenceManager.getDefaultSharedPreferences(this).getFloat("cursor_speed", 1.0f);
+
+ EditText text = findViewById(R.id.XRTextInput);
+ text.setVisibility(View.VISIBLE);
+ text.getEditableText().clear();
+ text.addTextChangedListener(this);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public synchronized void afterTextChanged(Editable e) {
+ EditText text = findViewById(R.id.XRTextInput);
+ String s = text.getEditableText().toString();
+ if (s.length() > lastText.length()) {
+ lastText = s;
+ char c = s.charAt(s.length() - 1);
+ boolean uppercase = c >= 'A' && c <= 'Z';
+ if (uppercase) {
+ c = (char)((int)c - (int)'A' + (int)'a');
+ }
+
+ switch (c) {
+ case '0': setKeyPressed(XKeycode.KEY_0, uppercase); break;
+ case '1': setKeyPressed(XKeycode.KEY_1, uppercase); break;
+ case '2': setKeyPressed(XKeycode.KEY_2, uppercase); break;
+ case '3': setKeyPressed(XKeycode.KEY_3, uppercase); break;
+ case '4': setKeyPressed(XKeycode.KEY_4, uppercase); break;
+ case '5': setKeyPressed(XKeycode.KEY_5, uppercase); break;
+ case '6': setKeyPressed(XKeycode.KEY_6, uppercase); break;
+ case '7': setKeyPressed(XKeycode.KEY_7, uppercase); break;
+ case '8': setKeyPressed(XKeycode.KEY_8, uppercase); break;
+ case '9': setKeyPressed(XKeycode.KEY_9, uppercase); break;
+
+ case 'a': setKeyPressed(XKeycode.KEY_A, uppercase); break;
+ case 'b': setKeyPressed(XKeycode.KEY_B, uppercase); break;
+ case 'c': setKeyPressed(XKeycode.KEY_C, uppercase); break;
+ case 'd': setKeyPressed(XKeycode.KEY_D, uppercase); break;
+ case 'e': setKeyPressed(XKeycode.KEY_E, uppercase); break;
+ case 'f': setKeyPressed(XKeycode.KEY_F, uppercase); break;
+ case 'g': setKeyPressed(XKeycode.KEY_G, uppercase); break;
+ case 'h': setKeyPressed(XKeycode.KEY_H, uppercase); break;
+ case 'i': setKeyPressed(XKeycode.KEY_I, uppercase); break;
+ case 'j': setKeyPressed(XKeycode.KEY_J, uppercase); break;
+ case 'k': setKeyPressed(XKeycode.KEY_K, uppercase); break;
+ case 'l': setKeyPressed(XKeycode.KEY_L, uppercase); break;
+ case 'm': setKeyPressed(XKeycode.KEY_M, uppercase); break;
+ case 'n': setKeyPressed(XKeycode.KEY_N, uppercase); break;
+ case 'o': setKeyPressed(XKeycode.KEY_O, uppercase); break;
+ case 'p': setKeyPressed(XKeycode.KEY_P, uppercase); break;
+ case 'q': setKeyPressed(XKeycode.KEY_Q, uppercase); break;
+ case 'r': setKeyPressed(XKeycode.KEY_R, uppercase); break;
+ case 's': setKeyPressed(XKeycode.KEY_S, uppercase); break;
+ case 't': setKeyPressed(XKeycode.KEY_T, uppercase); break;
+ case 'u': setKeyPressed(XKeycode.KEY_U, uppercase); break;
+ case 'v': setKeyPressed(XKeycode.KEY_V, uppercase); break;
+ case 'w': setKeyPressed(XKeycode.KEY_W, uppercase); break;
+ case 'x': setKeyPressed(XKeycode.KEY_X, uppercase); break;
+ case 'y': setKeyPressed(XKeycode.KEY_Y, uppercase); break;
+ case 'z': setKeyPressed(XKeycode.KEY_Z, uppercase); break;
+
+ case ' ': setKeyPressed(XKeycode.KEY_SPACE, uppercase); break;
+ case '.': setKeyPressed(XKeycode.KEY_PERIOD, uppercase); break;
+ case ',': setKeyPressed(XKeycode.KEY_COMMA, uppercase); break;
+ }
+ }
+ else {
+ lastText = s;
+ setKeyPressed(XKeycode.KEY_BKSP, false);
+ }
+ resetText();
+ }
+
+ private synchronized void resetText() {
+ EditText text = findViewById(R.id.XRTextInput);
+ text.removeTextChangedListener(this);
+ text.getEditableText().clear();
+ text.getEditableText().append(" ");
+ text.addTextChangedListener(this);
+ }
+
+ public static XrActivity getInstance() {
+ return instance;
+ }
+
+ public static boolean getImmersive() {
+ return isImmersive;
+ }
+
+ public static boolean getSBS() {
+ return isSBS;
+ }
+
+ 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, int containerId, String path) {
+ // 0. Create the launch intent
+ Intent intent = new Intent(context, XrActivity.class);
+ intent.putExtra("container_id", containerId);
+ if (path != null) {
+ intent.putExtra("shortcut_path", path);
+ }
+
+ // 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 and replace any existing tasks in the app stack
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ // 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();
+ }
+
+ public static void updateControllers() {
+ // Get OpenXR data
+ float[] axes = instance.getAxes();
+ boolean[] buttons = instance.getButtons();
+
+
+ try (XLock lock = instance.getXServer().lock(XServer.Lockable.WINDOW_MANAGER, XServer.Lockable.INPUT_DEVICE)) {
+ // Mouse control with hand
+ float f = 0.75f;
+ float meter2px = instance.getXServer().screenInfo.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
+ Pointer mouse = instance.getXServer().pointer;
+ if (isImmersive) {
+ float angle2px = instance.getXServer().screenInfo.width * 0.05f / f;
+ 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;
+ }
+ smoothedMouse[0] = mouse.getClampedX() + 0.5f;
+ smoothedMouse[1] = mouse.getClampedY() + 0.5f;
+ }
+
+ // Mouse smoothing
+ dx *= mouseSpeed;
+ dy *= mouseSpeed;
+ smoothedMouse[0] = smoothedMouse[0] * f + (mouse.getClampedX() + 0.5f + dx) * (1 - f);
+ smoothedMouse[1] = smoothedMouse[1] * f + (mouse.getClampedY() + 0.5f - dy) * (1 - f);
+
+ // Mouse "snap turn"
+ int snapturn = isImmersive ? 125 : 25;
+ if (getButtonClicked(buttons, ControllerButton.R_THUMBSTICK_LEFT)) {
+ smoothedMouse[0] = mouse.getClampedX() - snapturn;
+ }
+ if (getButtonClicked(buttons, ControllerButton.R_THUMBSTICK_RIGHT)) {
+ smoothedMouse[0] = mouse.getClampedX() + snapturn;
+ }
+
+ // Set mouse status
+ mouse.moveTo((int) smoothedMouse[0], (int) smoothedMouse[1]);
+ mouse.setButton(Pointer.Button.BUTTON_LEFT, buttons[ControllerButton.R_TRIGGER.ordinal()]);
+ mouse.setButton(Pointer.Button.BUTTON_RIGHT, buttons[ControllerButton.R_GRIP.ordinal()]);
+ mouse.setButton(Pointer.Button.BUTTON_SCROLL_UP, buttons[ControllerButton.R_THUMBSTICK_UP.ordinal()]);
+ mouse.setButton(Pointer.Button.BUTTON_SCROLL_DOWN, buttons[ControllerButton.R_THUMBSTICK_DOWN.ordinal()]);
+
+ // 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)) {
+ instance.runOnUiThread(() -> {
+ isSBS = false;
+ isImmersive = false;
+ instance.resetText();
+ AppUtils.showKeyboard(instance);
+ instance.findViewById(R.id.XRTextInput).requestFocus();
+ });
+ }
+
+ // Store the OpenXR data
+ System.arraycopy(axes, 0, lastAxes, 0, axes.length);
+ System.arraycopy(buttons, 0, lastButtons, 0, buttons.length);
+
+ // Update keyboard
+ mapKey(ControllerButton.R_A, XKeycode.KEY_A.id);
+ mapKey(ControllerButton.R_B, XKeycode.KEY_B.id);
+ mapKey(ControllerButton.L_X, XKeycode.KEY_X.id);
+ mapKey(ControllerButton.L_Y, XKeycode.KEY_Y.id);
+ mapKey(ControllerButton.L_GRIP, XKeycode.KEY_SPACE.id);
+ mapKey(ControllerButton.L_MENU, XKeycode.KEY_ESC.id);
+ mapKey(ControllerButton.L_TRIGGER, XKeycode.KEY_ENTER.id);
+ mapKey(ControllerButton.L_THUMBSTICK_LEFT, XKeycode.KEY_LEFT.id);
+ mapKey(ControllerButton.L_THUMBSTICK_RIGHT, XKeycode.KEY_RIGHT.id);
+ mapKey(ControllerButton.L_THUMBSTICK_UP, XKeycode.KEY_UP.id);
+ mapKey(ControllerButton.L_THUMBSTICK_DOWN, XKeycode.KEY_DOWN.id);
+ }
+ }
+
+ private static float getAngleDiff(float oldAngle, float newAngle) {
+ float diff = oldAngle - newAngle;
+ while (diff > 180) {
+ diff -= 360;
+ }
+ while (diff < -180) {
+ diff += 360;
+ }
+ return diff;
+ }
+
+ private static boolean getButtonClicked(boolean[] buttons, ControllerButton button) {
+ return buttons[button.ordinal()] && !lastButtons[button.ordinal()];
+ }
+
+ 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;
+ }
+
+ private static void mapKey(ControllerButton xrButton, byte xKeycode) {
+ Keyboard keyboard = instance.getXServer().keyboard;
+ if (lastButtons[xrButton.ordinal()]) {
+ keyboard.setKeyPress(xKeycode, 0);
+ } else {
+ keyboard.setKeyRelease(xKeycode);
+ }
+ }
+
+ private static void setKeyPressed(XKeycode xKeycode, boolean uppercase) {
+ XServer server = instance.getXServer();
+ if (uppercase) {
+ server.injectKeyPress(XKeycode.KEY_SHIFT_L);
+ }
+ server.injectKeyPress(xKeycode);
+
+ // Give the system bit of time to notice the key press
+ try {
+ Thread.sleep(50);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ server.injectKeyRelease(xKeycode);
+ if (uppercase) {
+ server.injectKeyRelease(XKeycode.KEY_SHIFT_L);
+ }
+ }
+
+ // Rendering
+ public native void init();
+ public native void bindFramebuffer();
+ public native int getWidth();
+ public native int getHeight();
+ public native boolean beginFrame(boolean immersive, boolean sbs);
+ public native void endFrame();
+
+ // Input
+ public native float[] getAxes();
+ public native boolean[] getButtons();
+}
diff --git a/app/src/main/java/com/winlator/renderer/GLRenderer.java b/app/src/main/java/com/winlator/renderer/GLRenderer.java
index 378688d..03213ec 100644
--- a/app/src/main/java/com/winlator/renderer/GLRenderer.java
+++ b/app/src/main/java/com/winlator/renderer/GLRenderer.java
@@ -7,6 +7,7 @@ import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import com.winlator.R;
+import com.winlator.XrActivity;
import com.winlator.math.Mathf;
import com.winlator.math.XForm;
import com.winlator.renderer.material.CursorMaterial;
@@ -83,6 +84,13 @@ public class GLRenderer implements GLSurfaceView.Renderer, WindowManager.OnWindo
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
+ if (XrActivity.isSupported()) {
+ XrActivity activity = XrActivity.getInstance();
+ activity.init();
+ width = activity.getWidth();
+ height = activity.getHeight();
+ }
+
surfaceWidth = width;
surfaceHeight = height;
viewport.update(width, height, xServer.screenInfo.width, xServer.screenInfo.height);
@@ -100,6 +108,11 @@ public class GLRenderer implements GLSurfaceView.Renderer, WindowManager.OnWindo
}
private void drawFrame() {
+ boolean xrFrame = false;
+ if (XrActivity.isSupported()) {
+ xrFrame = XrActivity.getInstance().beginFrame(XrActivity.getImmersive(), XrActivity.getSBS());
+ }
+
if (viewportNeedsUpdate) {
if (fullscreen) {
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
@@ -128,6 +141,12 @@ public class GLRenderer implements GLSurfaceView.Renderer, WindowManager.OnWindo
renderWindows();
if (cursorVisible) renderCursor();
+
+ if (xrFrame) {
+ XrActivity.getInstance().endFrame();
+ XrActivity.updateControllers();
+ xServerView.requestRender();
+ }
}
@Override
diff --git a/app/src/main/java/com/winlator/renderer/Texture.java b/app/src/main/java/com/winlator/renderer/Texture.java
index 3d0da67..22e7942 100644
--- a/app/src/main/java/com/winlator/renderer/Texture.java
+++ b/app/src/main/java/com/winlator/renderer/Texture.java
@@ -3,6 +3,7 @@ package com.winlator.renderer;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
+import com.winlator.XrActivity;
import com.winlator.xserver.Drawable;
import java.nio.ByteBuffer;
@@ -35,6 +36,7 @@ public class Texture {
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, minFilter);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ if (XrActivity.isSupported()) XrActivity.getInstance().bindFramebuffer();
}
public int getWrapS() {
diff --git a/app/src/main/res/layout/xserver_display_activity.xml b/app/src/main/res/layout/xserver_display_activity.xml
index 304bcf4..15aea7c 100644
--- a/app/src/main/res/layout/xserver_display_activity.xml
+++ b/app/src/main/res/layout/xserver_display_activity.xml
@@ -2,6 +2,7 @@
@@ -20,4 +21,14 @@
app:headerLayout="@layout/main_menu_header"
app:itemIconTint="@color/colorAccent"
app:menu="@menu/xserver_menu"/>
+
+
\ No newline at end of file