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