From 3af161cdef0b5791b3385c735b2b74550e717037 Mon Sep 17 00:00:00 2001 From: brunodev85 Date: Thu, 28 Mar 2024 11:29:54 -0300 Subject: [PATCH] Add WinlatorXR by lvonasek (https://github.com/lvonasek) --- app/.gitmodules | 3 + app/src/main/AndroidManifest.xml | 24 + app/src/main/cpp/CMakeLists.txt | 10 + app/src/main/cpp/OpenXR-SDK | 1 + app/src/main/cpp/xr/engine.c | 207 ++++++ app/src/main/cpp/xr/engine.h | 99 +++ app/src/main/cpp/xr/framebuffer.c | 148 +++++ app/src/main/cpp/xr/framebuffer.h | 28 + app/src/main/cpp/xr/input.c | 443 +++++++++++++ app/src/main/cpp/xr/input.h | 83 +++ app/src/main/cpp/xr/main.c | 189 ++++++ app/src/main/cpp/xr/math.c | 218 ++++++ app/src/main/cpp/xr/math.h | 26 + app/src/main/cpp/xr/renderer.c | 625 ++++++++++++++++++ app/src/main/cpp/xr/renderer.h | 79 +++ .../java/com/winlator/ContainersFragment.java | 9 +- .../java/com/winlator/ShortcutsFragment.java | 15 +- .../main/java/com/winlator/XrActivity.java | 353 ++++++++++ .../com/winlator/renderer/GLRenderer.java | 19 + .../java/com/winlator/renderer/Texture.java | 2 + .../res/layout/xserver_display_activity.xml | 11 + 21 files changed, 2584 insertions(+), 8 deletions(-) create mode 100644 app/.gitmodules create mode 160000 app/src/main/cpp/OpenXR-SDK create mode 100644 app/src/main/cpp/xr/engine.c create mode 100644 app/src/main/cpp/xr/engine.h create mode 100644 app/src/main/cpp/xr/framebuffer.c create mode 100644 app/src/main/cpp/xr/framebuffer.h create mode 100644 app/src/main/cpp/xr/input.c create mode 100644 app/src/main/cpp/xr/input.h create mode 100644 app/src/main/cpp/xr/main.c create mode 100644 app/src/main/cpp/xr/math.c create mode 100644 app/src/main/cpp/xr/math.h create mode 100644 app/src/main/cpp/xr/renderer.c create mode 100644 app/src/main/cpp/xr/renderer.h create mode 100644 app/src/main/java/com/winlator/XrActivity.java 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